From 60c0b0cbc5bc79b86f7658cd5d675244d085a3dc Mon Sep 17 00:00:00 2001 From: Geir-Tore Lindsve Date: Thu, 4 Apr 2024 09:13:30 +0200 Subject: [PATCH 001/124] Updated to .NET 8 (#2293) --- .github/workflows/ci.yml | 8 ++-- .github/workflows/ci_frontend.yml | 4 +- .github/workflows/dispatch_merge.yml | 2 +- .github/workflows/dispatch_tag.yml | 8 ++-- .github/workflows/publish.yml | 12 +++--- Directory.Packages.props | 42 +++++++++++++++++++ Dockerfile-api | 4 +- Src/Witsml/Witsml.csproj | 10 ++--- .../WitsmlExplorer.Api.csproj | 36 ++++++++-------- .../WitsmlExplorer.Console.csproj | 12 +++--- .../WitsmlExplorer.Frontend.csproj | 2 +- Tests/Witsml.Tests/Witsml.Tests.csproj | 10 ++--- .../WitsmlExplorer.Api.Tests.csproj | 12 +++--- .../WitsmlExplorer.IntegrationTests.csproj | 12 +++--- 14 files changed, 107 insertions(+), 67 deletions(-) create mode 100644 Directory.Packages.props diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 450cf3af0..4c63a2cf0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,11 +16,11 @@ jobs: name: Build and Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: Setup .NET 7 - uses: actions/setup-dotnet@a351d9ea84bc76ec7508debf02a39d88f8b6c0c0 # v2.1.1 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Setup .NET 8 + uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Build run: dotnet build /warnaserror --configuration Release - name: Install dotnet format diff --git a/.github/workflows/ci_frontend.yml b/.github/workflows/ci_frontend.yml index 4f32813fd..6c03d8f40 100644 --- a/.github/workflows/ci_frontend.yml +++ b/.github/workflows/ci_frontend.yml @@ -13,8 +13,8 @@ jobs: name: Build and Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: '20' - name: Install dependencies diff --git a/.github/workflows/dispatch_merge.yml b/.github/workflows/dispatch_merge.yml index 68f9e9f46..f7cad157c 100644 --- a/.github/workflows/dispatch_merge.yml +++ b/.github/workflows/dispatch_merge.yml @@ -13,7 +13,7 @@ jobs: if: github.repository_owner == 'equinor' runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Create repository dispatch event run: | curl -L \ diff --git a/.github/workflows/dispatch_tag.yml b/.github/workflows/dispatch_tag.yml index 0fc1eaa40..dcbabab92 100644 --- a/.github/workflows/dispatch_tag.yml +++ b/.github/workflows/dispatch_tag.yml @@ -9,7 +9,7 @@ permissions: {} jobs: fetchtag: - runs-on: ubuntu-latest + runs-on: ubuntu-latest if: github.ref_type == 'tag' steps: - id: get @@ -17,14 +17,14 @@ jobs: VERSION_ONLY=$(echo "${{ github.ref_name }}" | cut -d '@' -f2) echo "tag-version=$VERSION_ONLY" >> $GITHUB_OUTPUT outputs: - tag-version: ${{ steps.get.outputs.tag-version }} + tag-version: ${{ steps.get.outputs.tag-version }} notify: if: github.repository_owner == 'equinor' needs: fetchtag runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Create repository dispatch event run: | curl -L \ @@ -34,5 +34,3 @@ jobs: -H "X-GitHub-Api-Version: 2022-11-28" \ https://api.github.com/repos/equinor/witsml-explorer-equinor/dispatches \ -d '{"event_type":"tagdispatch ${{ github.ref_name }}","client_payload":{"tag": "${{ github.ref_name }}", "version":"${{ needs.fetchtag.outputs.tag-version }}"}}' - - diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 885285b12..5acbace1e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,11 +18,11 @@ jobs: name: Package and publish runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - name: Setup .NET 7 - uses: actions/setup-dotnet@a351d9ea84bc76ec7508debf02a39d88f8b6c0c0 # v2.1.1 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Setup .NET 8 + uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x source-url: https://nuget.pkg.github.com/equinor/index.json env: NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} @@ -32,6 +32,6 @@ jobs: - name: Test run: dotnet test ./Tests/Witsml.Tests/Witsml.Tests.csproj --configuration Release - name: Package - run: dotnet pack --configuration Release Src/Witsml -p:Version=2.8.${GITHUB_RUN_NUMBER} + run: dotnet pack --configuration Release Src/Witsml -p:Version=2.9.${GITHUB_RUN_NUMBER} - name: Publish - run: dotnet nuget push --api-key ${{secrets.NUGET_PUBLISH_KEY}} --source https://api.nuget.org/v3/index.json Src/Witsml/bin/Release/WitsmlClient.2.8.${GITHUB_RUN_NUMBER}.nupkg + run: dotnet nuget push --api-key ${{secrets.NUGET_PUBLISH_KEY}} --source https://api.nuget.org/v3/index.json Src/Witsml/bin/Release/WitsmlClient.2.9.${GITHUB_RUN_NUMBER}.nupkg diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 000000000..8cc9703ef --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,42 @@ + + + true + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/Dockerfile-api b/Dockerfile-api index dfbcdffbc..6b8e2f8c9 100644 --- a/Dockerfile-api +++ b/Dockerfile-api @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /build COPY Src/Witsml/Witsml.csproj Src/Witsml/Witsml.csproj COPY Src/WitsmlExplorer.Api/WitsmlExplorer.Api.csproj Src/WitsmlExplorer.Api/WitsmlExplorer.Api.csproj @@ -16,7 +16,7 @@ RUN dotnet test -c Release Witsml.Tests --no-restore && \ WORKDIR /build/Src/WitsmlExplorer.Api RUN dotnet publish -c Release -o out --no-restore --no-build --no-dependencies -FROM mcr.microsoft.com/dotnet/aspnet:7.0 as base +FROM mcr.microsoft.com/dotnet/aspnet:8.0 as base ARG EXPOSE_PORT=5000 WORKDIR /app COPY --from=build /build/Src/WitsmlExplorer.Api/out . diff --git a/Src/Witsml/Witsml.csproj b/Src/Witsml/Witsml.csproj index 7026aa3f7..caa126dba 100644 --- a/Src/Witsml/Witsml.csproj +++ b/Src/Witsml/Witsml.csproj @@ -1,8 +1,8 @@ - + - net6.0;net7.0 latestMajor + net8.0 @@ -17,8 +17,8 @@ - - - + + + diff --git a/Src/WitsmlExplorer.Api/WitsmlExplorer.Api.csproj b/Src/WitsmlExplorer.Api/WitsmlExplorer.Api.csproj index 353c1055e..c736d0bdb 100644 --- a/Src/WitsmlExplorer.Api/WitsmlExplorer.Api.csproj +++ b/Src/WitsmlExplorer.Api/WitsmlExplorer.Api.csproj @@ -1,6 +1,6 @@ - net7.0 + net8.0 latestMajor false 5de2e6cd-e42c-4fca-905c-39f60e8b2ea8 @@ -10,23 +10,23 @@ AD0001 - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/Src/WitsmlExplorer.Console/WitsmlExplorer.Console.csproj b/Src/WitsmlExplorer.Console/WitsmlExplorer.Console.csproj index 73d301da9..f54d99fb3 100644 --- a/Src/WitsmlExplorer.Console/WitsmlExplorer.Console.csproj +++ b/Src/WitsmlExplorer.Console/WitsmlExplorer.Console.csproj @@ -1,6 +1,6 @@ - net7.0 + net8.0 latestMajor Exe @@ -13,11 +13,11 @@ - - - - - + + + + + diff --git a/Src/WitsmlExplorer.Frontend/WitsmlExplorer.Frontend.csproj b/Src/WitsmlExplorer.Frontend/WitsmlExplorer.Frontend.csproj index ab4ac6c46..138b12c42 100644 --- a/Src/WitsmlExplorer.Frontend/WitsmlExplorer.Frontend.csproj +++ b/Src/WitsmlExplorer.Frontend/WitsmlExplorer.Frontend.csproj @@ -1,6 +1,6 @@ - net7.0 + net8.0 diff --git a/Tests/Witsml.Tests/Witsml.Tests.csproj b/Tests/Witsml.Tests/Witsml.Tests.csproj index 9bc49e62d..f2af3d522 100644 --- a/Tests/Witsml.Tests/Witsml.Tests.csproj +++ b/Tests/Witsml.Tests/Witsml.Tests.csproj @@ -1,19 +1,19 @@ - net7.0 + net8.0 latestMajor false - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Tests/WitsmlExplorer.Api.Tests/WitsmlExplorer.Api.Tests.csproj b/Tests/WitsmlExplorer.Api.Tests/WitsmlExplorer.Api.Tests.csproj index bd72c22ac..372ca4b13 100644 --- a/Tests/WitsmlExplorer.Api.Tests/WitsmlExplorer.Api.Tests.csproj +++ b/Tests/WitsmlExplorer.Api.Tests/WitsmlExplorer.Api.Tests.csproj @@ -1,17 +1,17 @@ - net7.0 + net8.0 latestMajor false - - - - - + + + + + diff --git a/Tests/WitsmlExplorer.IntegrationTests/WitsmlExplorer.IntegrationTests.csproj b/Tests/WitsmlExplorer.IntegrationTests/WitsmlExplorer.IntegrationTests.csproj index 689811f79..083564ef8 100644 --- a/Tests/WitsmlExplorer.IntegrationTests/WitsmlExplorer.IntegrationTests.csproj +++ b/Tests/WitsmlExplorer.IntegrationTests/WitsmlExplorer.IntegrationTests.csproj @@ -1,16 +1,16 @@ - + - net7.0 + net8.0 latestMajor false WitsmlExplorer.IntegrationTests c42859c1-b470-4722-8cdd-20f319c00246 - - - - + + + + From d82d0a6e22627e5f6806a5b6b849fe5c03bc757b Mon Sep 17 00:00:00 2001 From: Jan-Marius Vatle <48485965+janmarius@users.noreply.github.com> Date: Fri, 5 Apr 2024 10:25:03 +0200 Subject: [PATCH 002/124] FIX-2341 Add NoWarn for NuGet warning NU1507 (#2342) --- Src/Witsml/Witsml.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Src/Witsml/Witsml.csproj b/Src/Witsml/Witsml.csproj index caa126dba..48eed800b 100644 --- a/Src/Witsml/Witsml.csproj +++ b/Src/Witsml/Witsml.csproj @@ -3,6 +3,7 @@ latestMajor net8.0 + $(NoWarn);NU1507 From 057d1fe7900f5df78513e9f4e39f8161cf315316 Mon Sep 17 00:00:00 2001 From: Jan-Marius Vatle <48485965+janmarius@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:57:08 +0200 Subject: [PATCH 003/124] FIX-2343 Add Directory.Packages.props to Dockerfile-api (#2344) --- Dockerfile-api | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile-api b/Dockerfile-api index 6b8e2f8c9..7158023c6 100644 --- a/Dockerfile-api +++ b/Dockerfile-api @@ -1,5 +1,6 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /build +COPY Directory.Packages.props Directory.Packages.props COPY Src/Witsml/Witsml.csproj Src/Witsml/Witsml.csproj COPY Src/WitsmlExplorer.Api/WitsmlExplorer.Api.csproj Src/WitsmlExplorer.Api/WitsmlExplorer.Api.csproj COPY Tests/Witsml.Tests/Witsml.Tests.csproj Tests/Witsml.Tests/Witsml.Tests.csproj From b092d053f27afd4ac8318d91a5a817c9fb857ef4 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Fri, 5 Apr 2024 12:45:04 +0200 Subject: [PATCH 004/124] FIX-2337 Improve cancel button style (#2338) --- .../components/ContentViews/JobsView.tsx | 17 ++---- .../components/StyledComponents/Button.tsx | 60 +++++++++++++++++-- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/JobsView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/JobsView.tsx index 96e2be0df..817af957d 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/JobsView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/JobsView.tsx @@ -146,13 +146,14 @@ export const JobsView = (): React.ReactElement => { : jobInfo.status, cancel: jobInfo.isCancelable === true && jobInfo.status === "Started" ? ( - cancelJob(jobInfo.id)} > - - + + ) : null, startTime: formatDateString( jobInfo.startTime, @@ -243,10 +244,4 @@ const ReportButton = styled.div` cursor: pointer; `; -const StyledButton = styled(Button)` - &&& { - margin-left: 1.313em; height: 1.538em; color: red}; - } -`; - export default JobsView; diff --git a/Src/WitsmlExplorer.Frontend/components/StyledComponents/Button.tsx b/Src/WitsmlExplorer.Frontend/components/StyledComponents/Button.tsx index 1177694db..d0b32578b 100644 --- a/Src/WitsmlExplorer.Frontend/components/StyledComponents/Button.tsx +++ b/Src/WitsmlExplorer.Frontend/components/StyledComponents/Button.tsx @@ -1,26 +1,46 @@ -import { ButtonProps, Button as EdsButton } from "@equinor/eds-core-react"; +import { + Button as EdsButton, + ButtonProps as EdsButtonProps +} from "@equinor/eds-core-react"; import OperationContext from "contexts/operationContext"; +import { UserTheme } from "contexts/operationStateReducer"; import React, { useContext } from "react"; -import styled from "styled-components"; +import styled, { css } from "styled-components"; import { Colors } from "styles/Colors"; +export interface ButtonProps extends Omit { + variant?: EdsButtonProps["variant"] | "table_icon"; +} + export type Ref = HTMLButtonElement; export const Button = React.forwardRef((props, ref) => { const { - operationState: { colors } + operationState: { colors, theme } } = useContext(OperationContext); if (!props.variant || props.variant === "contained") { return ; } else if (props.variant === "contained_icon") { - return ; + return ; } else if (props.variant === "outlined") { return ; } else if (props.variant === "ghost") { return ; } else if (props.variant === "ghost_icon") { return ; + } else if (props.variant === "table_icon") { + return ( + + + + ); } else { throw Error(`Button variant ${props.variant} is not supported!`); } @@ -50,7 +70,39 @@ const GhostIconButton = styled(EdsButton)<{ colors: Colors }>` color: ${(props) => props.colors.infographic.primaryMossGreen}; `; +const TableIconButton = styled(EdsButton)<{ + colors: Colors; + userTheme: UserTheme; +}>` + white-space: nowrap; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -55%); + ${(props) => + props.userTheme === UserTheme.Compact && + css` + height: 22px; + width: 22px; + &::after { + width: 22px; + height: 22px; + } + `} + ${(props) => + (!props.color || props.color === "primary") && + css<{ colors: Colors }>` + color: ${(props) => props.colors.infographic.primaryMossGreen}; + `} +`; + const OutlinedButton = styled(EdsButton)<{ colors: Colors }>` white-space: nowrap; color: ${(props) => props.colors.infographic.primaryMossGreen}; `; + +const TableIconButtonLayout = styled.div` + position: relative; + width: 100%; + height: 100%; +`; From 820e0e722d07b74df4d369376ffe251a22065d59 Mon Sep 17 00:00:00 2001 From: Jan-Marius Vatle <48485965+janmarius@users.noreply.github.com> Date: Fri, 5 Apr 2024 14:21:09 +0200 Subject: [PATCH 005/124] Revert "FIX-2341 Add NoWarn for NuGet warning NU1507" (#2345) --- Src/Witsml/Witsml.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/Src/Witsml/Witsml.csproj b/Src/Witsml/Witsml.csproj index 48eed800b..caa126dba 100644 --- a/Src/Witsml/Witsml.csproj +++ b/Src/Witsml/Witsml.csproj @@ -3,7 +3,6 @@ latestMajor net8.0 - $(NoWarn);NU1507 From d58f1e09e27a4e9b9289c368126e285e6e44c775 Mon Sep 17 00:00:00 2001 From: Jan-Marius Vatle <48485965+janmarius@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:07:54 +0200 Subject: [PATCH 006/124] Revert "Revert "FIX-2341 Add NoWarn for NuGet warning NU1507"" (#2346) --- Src/Witsml/Witsml.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Src/Witsml/Witsml.csproj b/Src/Witsml/Witsml.csproj index caa126dba..48eed800b 100644 --- a/Src/Witsml/Witsml.csproj +++ b/Src/Witsml/Witsml.csproj @@ -3,6 +3,7 @@ latestMajor net8.0 + $(NoWarn);NU1507 From 36591171799328f0b307823a089b46819259787e Mon Sep 17 00:00:00 2001 From: Stein A Sivertsen Date: Mon, 8 Apr 2024 10:01:30 +0200 Subject: [PATCH 007/124] [Snyk] Security upgrade vite from 5.2.3 to 5.2.8 (#2339) Co-authored-by: snyk-bot Co-authored-by: Elias Bruvik --- Src/WitsmlExplorer.Frontend/package.json | 2 +- Src/WitsmlExplorer.Frontend/vite.config.ts | 8 -------- Src/WitsmlExplorer.Frontend/vitest.config.ts | 13 +++++++++++++ yarn.lock | 15 +++++++++++++-- 4 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 Src/WitsmlExplorer.Frontend/vitest.config.ts diff --git a/Src/WitsmlExplorer.Frontend/package.json b/Src/WitsmlExplorer.Frontend/package.json index 3e5852570..0cddba0f4 100644 --- a/Src/WitsmlExplorer.Frontend/package.json +++ b/Src/WitsmlExplorer.Frontend/package.json @@ -80,7 +80,7 @@ "jsdom": "^24.0.0", "lint-staged": "^13.0.3", "typescript": "^5.4.3", - "vite": "^5.2.0", + "vite": "^5.2.8", "vite-tsconfig-paths": "^4.3.2", "vitest": "^1.4.0" } diff --git a/Src/WitsmlExplorer.Frontend/vite.config.ts b/Src/WitsmlExplorer.Frontend/vite.config.ts index 576121ba3..5e478e45c 100644 --- a/Src/WitsmlExplorer.Frontend/vite.config.ts +++ b/Src/WitsmlExplorer.Frontend/vite.config.ts @@ -1,4 +1,3 @@ -/// /// import react from "@vitejs/plugin-react"; @@ -17,12 +16,5 @@ export default defineConfig({ // port for dev server: { port: 3000 - }, - - // test config - test: { - globals: true, - environment: "jsdom", - setupFiles: ["./setupTests.ts"] } }); diff --git a/Src/WitsmlExplorer.Frontend/vitest.config.ts b/Src/WitsmlExplorer.Frontend/vitest.config.ts new file mode 100644 index 000000000..801660c47 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/vitest.config.ts @@ -0,0 +1,13 @@ +import { defineConfig, mergeConfig } from "vitest/config"; +import viteConfig from "./vite.config"; + +export default mergeConfig( + viteConfig, + defineConfig({ + test: { + globals: true, + environment: "jsdom", + setupFiles: ["./setupTests.ts"] + } + }) +); diff --git a/yarn.lock b/yarn.lock index 254035fc7..634eebf56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3682,7 +3682,7 @@ postcss-value-parser@^4.0.2: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.4.36: +postcss@^8.4.36, postcss@^8.4.38: version "8.4.38" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== @@ -4537,7 +4537,7 @@ vite-tsconfig-paths@^4.3.2: globrex "^0.1.2" tsconfck "^3.0.3" -vite@^5.0.0, vite@^5.2.0: +vite@^5.0.0: version "5.2.3" resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.3.tgz#198efc2fd4d80eac813b146a68a4b0dbde884fc2" integrity sha512-+i1oagbvkVIhEy9TnEV+fgXsng13nZM90JQbrcPrf6DvW2mXARlz+DK7DLiDP+qeKoD1FCVx/1SpFL1CLq9Mhw== @@ -4548,6 +4548,17 @@ vite@^5.0.0, vite@^5.2.0: optionalDependencies: fsevents "~2.3.3" +vite@^5.2.8: + version "5.2.8" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.8.tgz#a99e09939f1a502992381395ce93efa40a2844aa" + integrity sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA== + dependencies: + esbuild "^0.20.1" + postcss "^8.4.38" + rollup "^4.13.0" + optionalDependencies: + fsevents "~2.3.3" + vitest@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/vitest/-/vitest-1.4.0.tgz#f5c812aaf5023818b89b7fc667fa45327396fece" From 50c22e61748e81224c41ed5c9cef04dbf252a6e2 Mon Sep 17 00:00:00 2001 From: Geir-Tore Lindsve Date: Tue, 9 Apr 2024 08:28:52 +0200 Subject: [PATCH 008/124] Add OpenTelemetry library and expose metrics for Witsml requests (#2291) --- Directory.Packages.props | 1 + Src/Witsml/Metrics/WitsmlMethod.cs | 11 +++ Src/Witsml/Metrics/WitsmlMetrics.cs | 74 +++++++++++++++++ Src/Witsml/Metrics/WitsmlMetricsExtensions.cs | 17 ++++ .../WitsmlServiceExtensions.cs | 38 ++++++++- Src/Witsml/Witsml.csproj | 1 + Src/Witsml/WitsmlClient.cs | 79 +++++++++++++++---- 7 files changed, 204 insertions(+), 17 deletions(-) create mode 100644 Src/Witsml/Metrics/WitsmlMethod.cs create mode 100644 Src/Witsml/Metrics/WitsmlMetrics.cs create mode 100644 Src/Witsml/Metrics/WitsmlMetricsExtensions.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 8cc9703ef..153fabb78 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -32,6 +32,7 @@ + diff --git a/Src/Witsml/Metrics/WitsmlMethod.cs b/Src/Witsml/Metrics/WitsmlMethod.cs new file mode 100644 index 000000000..8c737d269 --- /dev/null +++ b/Src/Witsml/Metrics/WitsmlMethod.cs @@ -0,0 +1,11 @@ +namespace Witsml.Metrics; + +public enum WitsmlMethod +{ + GetFromStore, + AddToStore, + UpdateInStore, + DeleteFromStore, + GetCap, + GetVersion +} diff --git a/Src/Witsml/Metrics/WitsmlMetrics.cs b/Src/Witsml/Metrics/WitsmlMetrics.cs new file mode 100644 index 000000000..090a8de90 --- /dev/null +++ b/Src/Witsml/Metrics/WitsmlMetrics.cs @@ -0,0 +1,74 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Reflection; +using System.Threading.Tasks; + +using Serilog; + +using Witsml.ServiceReference; + +namespace Witsml.Metrics; + +internal sealed class WitsmlMetrics +{ + #region Singleton + // We still want to collect unified metrics in case there are multiple WitsmlClient instances used + + private static readonly Lazy LazyInstance = + new(() => new WitsmlMetrics()); + + public static WitsmlMetrics Instance { get { return LazyInstance.Value; } } + + private WitsmlMetrics() { } + + #endregion + + private static readonly AssemblyName AssemblyName = + typeof(WitsmlMetrics).Assembly.GetName(); + + internal static readonly string MeterName = AssemblyName.Name; + + private static readonly Meter MeterInstance = new(MeterName, AssemblyName.Version!.ToString()); + + private readonly Histogram _requestDuration = MeterInstance.CreateHistogram( + "witsml.requests.duration", + unit: "s", + description: "Time spent during requests to a Witsml server"); + + private readonly UpDownCounter _activeRequests = + MeterInstance.CreateUpDownCounter( + "witsml.requests.active", + description: "Number of active requests"); + + internal async Task MeasureQuery(Uri serverUri, WitsmlMethod method, string witsmlType, Task wmlsTask) + where TResponseType : IWitsmlResponse + { + var tagList = new TagList + { + { "host", serverUri.Host }, + { "method", Enum.GetName(method) }, + { "objectType", witsmlType }, + }; + + Stopwatch timer = null; + TResponseType response; + try + { + _activeRequests.Add(1, tagList); + timer = Stopwatch.StartNew(); + response = await wmlsTask; + } + finally + { + timer?.Stop(); + _activeRequests.Add(-1, tagList); + } + + tagList.Add("resultCode", response.GetResultCode()); + + var elapsedSeconds = timer.ElapsedMilliseconds / 1000; + _requestDuration.Record(elapsedSeconds, tagList); + return response; + } +} diff --git a/Src/Witsml/Metrics/WitsmlMetricsExtensions.cs b/Src/Witsml/Metrics/WitsmlMetricsExtensions.cs new file mode 100644 index 000000000..c95d72451 --- /dev/null +++ b/Src/Witsml/Metrics/WitsmlMetricsExtensions.cs @@ -0,0 +1,17 @@ +using OpenTelemetry.Metrics; + +namespace Witsml.Metrics; + +public static class WitsmlMetricsExtensions +{ + // ReSharper disable once UnusedMember.Global (For usage by external applications) + public static MeterProviderBuilder AddWitsmlInstrumentation(this MeterProviderBuilder builder) + { + builder.AddMeter(WitsmlMetrics.MeterName); + builder.AddView("witsml.requests.duration", new ExplicitBucketHistogramConfiguration() + { + Boundaries = [0.5, 1, 3, 5, 10, 15, 30, 60] + }); + return builder; + } +} diff --git a/Src/Witsml/ServiceReference/WitsmlServiceExtensions.cs b/Src/Witsml/ServiceReference/WitsmlServiceExtensions.cs index b80ee3623..5e0864e86 100644 --- a/Src/Witsml/ServiceReference/WitsmlServiceExtensions.cs +++ b/Src/Witsml/ServiceReference/WitsmlServiceExtensions.cs @@ -1,12 +1,44 @@ +// ReSharper disable ClassNeverInstantiated.Global +// ReSharper disable InconsistentNaming namespace Witsml.ServiceReference { - // ReSharper disable once ClassNeverInstantiated.Global - // ReSharper disable once InconsistentNaming - public partial class WMLS_GetFromStoreResponse + internal interface IWitsmlResponse { + public string GetResultCode(); + } + + public partial class WMLS_GetFromStoreResponse : IWitsmlResponse + { + public string GetResultCode() => Result.ToString(); + public override string ToString() { return $"Result: {Result}\nXMLout: {XMLout}\nSuppMsgOut: {SuppMsgOut}"; } } + + public partial class WMLS_AddToStoreResponse : IWitsmlResponse + { + public string GetResultCode() => Result.ToString(); + } + + public partial class WMLS_UpdateInStoreResponse : IWitsmlResponse + { + public string GetResultCode() => Result.ToString(); + } + + public partial class WMLS_DeleteFromStoreResponse : IWitsmlResponse + { + public string GetResultCode() => Result.ToString(); + } + + public partial class WMLS_GetCapResponse : IWitsmlResponse + { + public string GetResultCode() => Result.ToString(); + } + + public partial class WMLS_GetVersionResponse : IWitsmlResponse + { + public string GetResultCode() => Result; + } } diff --git a/Src/Witsml/Witsml.csproj b/Src/Witsml/Witsml.csproj index 48eed800b..de26bbbdd 100644 --- a/Src/Witsml/Witsml.csproj +++ b/Src/Witsml/Witsml.csproj @@ -21,5 +21,6 @@ + diff --git a/Src/Witsml/WitsmlClient.cs b/Src/Witsml/WitsmlClient.cs index aac505f51..b02852bff 100644 --- a/Src/Witsml/WitsmlClient.cs +++ b/Src/Witsml/WitsmlClient.cs @@ -9,6 +9,7 @@ using Witsml.Data; using Witsml.Extensions; +using Witsml.Metrics; using Witsml.ServiceReference; using Witsml.Xml; @@ -37,6 +38,7 @@ public class WitsmlClient : WitsmlClientBase, IWitsmlClient private readonly StoreSoapPortClient _client; private readonly Uri _serverUrl; private IQueryLogger _queryLogger; + private readonly WitsmlMetrics _witsmlMetrics; [Obsolete("Use the WitsmlClientOptions based constructor instead")] public WitsmlClient(string hostname, string username, string password, WitsmlClientCapabilities clientCapabilities, TimeSpan? requestTimeout = null, @@ -65,6 +67,7 @@ public WitsmlClient(Action options) _client = CreateSoapClient(witsmlClientOptions); + _witsmlMetrics = WitsmlMetrics.Instance; SetupQueryLogging(witsmlClientOptions.LogQueries); } @@ -132,7 +135,11 @@ private async Task GetFromStoreInnerAsync(T query, OptionsIn optionsIn) wh CapabilitiesIn = _clientCapabilities }; - WMLS_GetFromStoreResponse response = await _client.WMLS_GetFromStoreAsync(request); + WMLS_GetFromStoreResponse response = await _witsmlMetrics.MeasureQuery( + _serverUrl, + WitsmlMethod.GetFromStore, + query.TypeName, + _client.WMLS_GetFromStoreAsync(request)); LogQueriesSentAndReceived(nameof(_client.WMLS_GetFromStoreAsync), this._serverUrl, query, optionsIn, request.QueryIn, response.IsSuccessful(), response.XMLout, response.Result, response.SuppMsgOut); @@ -164,7 +171,11 @@ private async Task GetFromStoreInnerAsync(T query, OptionsIn optionsIn) wh CapabilitiesIn = _clientCapabilities }; - WMLS_GetFromStoreResponse response = await _client.WMLS_GetFromStoreAsync(request); + WMLS_GetFromStoreResponse response = await _witsmlMetrics.MeasureQuery( + _serverUrl, + WitsmlMethod.GetFromStore, + query.TypeName, + _client.WMLS_GetFromStoreAsync(request)); LogQueriesSentAndReceived(nameof(_client.WMLS_GetFromStoreAsync), this._serverUrl, query, optionsIn, request.QueryIn, response.IsSuccessful(), response.XMLout, response.Result, response.SuppMsgOut); @@ -182,7 +193,7 @@ private async Task GetFromStoreInnerAsync(T query, OptionsIn optionsIn) wh } } - private string GetQueryType(string query) + private static string GetQueryType(string query) { XmlReaderSettings settings = new() { @@ -213,7 +224,12 @@ public async Task GetFromStoreAsync(string query, OptionsIn optionsIn) CapabilitiesIn = _clientCapabilities }; - WMLS_GetFromStoreResponse response = await _client.WMLS_GetFromStoreAsync(request); + WMLS_GetFromStoreResponse response = await _witsmlMetrics.MeasureQuery( + _serverUrl, + WitsmlMethod.GetFromStore, + type, + _client.WMLS_GetFromStoreAsync(request)); + LogQueriesSentAndReceived(nameof(_client.WMLS_GetFromStoreAsync), _serverUrl, null, optionsIn, query, response.IsSuccessful(), response.XMLout, response.Result, response.SuppMsgOut); @@ -237,7 +253,11 @@ public async Task AddToStoreAsync(T query) where T : IWitsmlQuer CapabilitiesIn = _clientCapabilities }; - WMLS_AddToStoreResponse response = await _client.WMLS_AddToStoreAsync(request); + WMLS_AddToStoreResponse response = await _witsmlMetrics.MeasureQuery( + _serverUrl, + WitsmlMethod.AddToStore, + query.TypeName, + _client.WMLS_AddToStoreAsync(request)); LogQueriesSentAndReceived(nameof(_client.WMLS_AddToStoreAsync), this._serverUrl, query, optionsIn, request.XMLin, response.IsSuccessful(), null, response.Result, response.SuppMsgOut); @@ -268,7 +288,12 @@ public async Task AddToStoreAsync(string query, OptionsIn optionsIn = nu CapabilitiesIn = _clientCapabilities }; - WMLS_AddToStoreResponse response = await _client.WMLS_AddToStoreAsync(request); + WMLS_AddToStoreResponse response = await _witsmlMetrics.MeasureQuery( + _serverUrl, + WitsmlMethod.AddToStore, + type, + _client.WMLS_AddToStoreAsync(request)); + LogQueriesSentAndReceived(nameof(_client.WMLS_AddToStoreAsync), _serverUrl, null, optionsIn, query, response.IsSuccessful(), null, response.Result, response.SuppMsgOut); @@ -291,7 +316,11 @@ public async Task UpdateInStoreAsync(T query) where T : IWitsmlQ CapabilitiesIn = _clientCapabilities }; - WMLS_UpdateInStoreResponse response = await _client.WMLS_UpdateInStoreAsync(request); + WMLS_UpdateInStoreResponse response = await _witsmlMetrics.MeasureQuery( + _serverUrl, + WitsmlMethod.UpdateInStore, + query.TypeName, + _client.WMLS_UpdateInStoreAsync(request)); LogQueriesSentAndReceived(nameof(_client.WMLS_UpdateInStoreAsync), this._serverUrl, query, null, request.XMLin, response.IsSuccessful(), null, response.Result, response.SuppMsgOut); @@ -322,7 +351,12 @@ public async Task UpdateInStoreAsync(string query, OptionsIn optionsIn = CapabilitiesIn = _clientCapabilities }; - WMLS_UpdateInStoreResponse response = await _client.WMLS_UpdateInStoreAsync(request); + WMLS_UpdateInStoreResponse response = await _witsmlMetrics.MeasureQuery( + _serverUrl, + WitsmlMethod.UpdateInStore, + type, + _client.WMLS_UpdateInStoreAsync(request)); + LogQueriesSentAndReceived(nameof(_client.WMLS_UpdateInStoreAsync), _serverUrl, null, optionsIn, query, response.IsSuccessful(), null, response.Result, response.SuppMsgOut); @@ -345,7 +379,11 @@ public async Task DeleteFromStoreAsync(T query) where T : IWitsm CapabilitiesIn = _clientCapabilities }; - WMLS_DeleteFromStoreResponse response = await _client.WMLS_DeleteFromStoreAsync(request); + WMLS_DeleteFromStoreResponse response = await _witsmlMetrics.MeasureQuery( + _serverUrl, + WitsmlMethod.DeleteFromStore, + query.TypeName, + _client.WMLS_DeleteFromStoreAsync(request)); LogQueriesSentAndReceived(nameof(_client.WMLS_DeleteFromStoreAsync), this._serverUrl, query, null, request.QueryIn, response.IsSuccessful(), null, response.Result, response.SuppMsgOut); @@ -376,7 +414,12 @@ public async Task DeleteFromStoreAsync(string query, OptionsIn optionsIn CapabilitiesIn = _clientCapabilities }; - WMLS_DeleteFromStoreResponse response = await _client.WMLS_DeleteFromStoreAsync(request); + WMLS_DeleteFromStoreResponse response = await _witsmlMetrics.MeasureQuery( + _serverUrl, + WitsmlMethod.DeleteFromStore, + type, + _client.WMLS_DeleteFromStoreAsync(request)); + LogQueriesSentAndReceived(nameof(_client.WMLS_DeleteFromStoreAsync), _serverUrl, null, optionsIn, query, response.IsSuccessful(), null, response.Result, response.SuppMsgOut); @@ -389,11 +432,15 @@ public async Task DeleteFromStoreAsync(string query, OptionsIn optionsIn public async Task TestConnectionAsync() { - WMLS_GetVersionResponse response = await _client.WMLS_GetVersionAsync(); + WMLS_GetVersionResponse response = + await _witsmlMetrics.MeasureQuery( + _serverUrl, + WitsmlMethod.GetVersion, + "", + _client.WMLS_GetVersionAsync()); + if (string.IsNullOrEmpty(response.Result)) - { throw new Exception("Error while testing connection: Server failed to return a valid version"); - } // Spec requires a comma-seperated list of supported versions without spaces var versions = response.Result.Split(CommonConstants.DataSeparator); @@ -405,7 +452,11 @@ public async Task TestConnectionAsync() public async Task GetCap() { - WMLS_GetCapResponse response = await _client.WMLS_GetCapAsync(new WMLS_GetCapRequest("dataVersion=1.4.1.1")); + WMLS_GetCapResponse response = await _witsmlMetrics.MeasureQuery( + _serverUrl, + WitsmlMethod.GetCap, + "", + _client.WMLS_GetCapAsync(new WMLS_GetCapRequest("dataVersion=1.4.1.1"))); if (response.IsSuccessful()) return XmlHelper.Deserialize(response.CapabilitiesOut, new WitsmlCapServers()); From 1d785a2b196022c69f3ab94e4fc0227572f288d8 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Tue, 9 Apr 2024 15:48:12 +0200 Subject: [PATCH 009/124] Fix 2350 Live wellbore filter bug (#2351) --- Src/WitsmlExplorer.Api/Services/WellService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Src/WitsmlExplorer.Api/Services/WellService.cs b/Src/WitsmlExplorer.Api/Services/WellService.cs index 3a7ade287..199fa86e8 100644 --- a/Src/WitsmlExplorer.Api/Services/WellService.cs +++ b/Src/WitsmlExplorer.Api/Services/WellService.cs @@ -39,6 +39,7 @@ public async Task> GetWells() { Wellbores = new WitsmlWellbore { + UidWell = "", IsActive = "true" }.AsItemInList() }; From 44d13730dbfda9546b578de597e44a9321e1112a Mon Sep 17 00:00:00 2001 From: Vaclav Basniar <135022369+vaclavbasniar@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:50:56 +0200 Subject: [PATCH 010/124] Features/1955 consistent copy mnemonic (#2340) --- .../CopyComponentsToServerUtils.tsx | 25 +- .../ContextMenus/LogObjectContextMenu.tsx | 26 +- .../components/GlobalStyles.tsx | 2 +- .../components/Modals/CopyMnemonicsModal.tsx | 234 ++++++++++++++++++ 4 files changed, 276 insertions(+), 11 deletions(-) create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/CopyMnemonicsModal.tsx diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/CopyComponentsToServerUtils.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/CopyComponentsToServerUtils.tsx index 1e4efdcc3..12bf83d06 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/CopyComponentsToServerUtils.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/CopyComponentsToServerUtils.tsx @@ -1,5 +1,8 @@ import { Fragment } from "react"; -import { DispatchOperation } from "../../contexts/operationStateReducer"; +import { + DispatchOperation, + DisplayModalAction +} from "../../contexts/operationStateReducer"; import OperationType from "../../contexts/operationType"; import { ComponentType, getParentType } from "../../models/componentType"; import ComponentReferences, { @@ -21,6 +24,9 @@ import ObjectService from "../../services/objectService"; import { displayMissingObjectModal } from "../Modals/MissingObjectModals"; import { displayReplaceModal } from "../Modals/ReplaceModal"; import { pluralize } from "./ContextMenuUtils"; +import CopyMnemonicsModal, { + CopyMnemonicsModalProps +} from "../Modals/CopyMnemonicsModal"; export interface OnClickCopyComponentToServerProps { targetServer: Server; @@ -92,6 +98,23 @@ export const copyComponentsToServer = async ( const targetParentReference: ObjectReference = toObjectReference(targetParent); + if (componentType == ComponentType.Mnemonic) { + const copyMnemonicsModalProps: CopyMnemonicsModalProps = { + sourceReferences: sourceComponentReferences, + targetReference: targetParentReference, + startIndex: startIndex, + endIndex: endIndex, + targetServer: targetServer, + sourceServer: sourceServer + }; + const action: DisplayModalAction = { + type: OperationType.DisplayModal, + payload: + }; + dispatchOperation(action); + return; + } + const copyJob: CopyComponentsJob = createCopyJob(); const allTargetComponents = await ComponentService.getComponents( diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogObjectContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogObjectContextMenu.tsx index 69589778d..ea1659530 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogObjectContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogObjectContextMenu.tsx @@ -4,8 +4,8 @@ import { useQueryClient } from "@tanstack/react-query"; import { BatchModifyMenuItem } from "components/ContextMenus/BatchModifyMenuItem"; import ContextMenu from "components/ContextMenus/ContextMenu"; import { - StyledIcon, - menuItemText + menuItemText, + StyledIcon } from "components/ContextMenus/ContextMenuUtils"; import { onClickPaste } from "components/ContextMenus/CopyUtils"; import NestedMenuItem from "components/ContextMenus/NestedMenuItem"; @@ -45,8 +45,6 @@ import { ComponentType } from "models/componentType"; import CheckLogHeaderJob from "models/jobs/checkLogHeaderJob"; import CompareLogDataJob from "models/jobs/compareLogData"; import { CopyRangeClipboard } from "models/jobs/componentReferences"; -import { CopyComponentsJob } from "models/jobs/copyJobs"; -import ObjectReference from "models/jobs/objectReference"; import LogObject from "models/logObject"; import ObjectOnWellbore, { toObjectReference } from "models/objectOnWellbore"; import { ObjectType } from "models/objectType"; @@ -55,6 +53,10 @@ import React, { useContext } from "react"; import JobService, { JobType } from "services/jobService"; import { colors } from "styles/Colors"; import { v4 as uuid } from "uuid"; +import ObjectReference from "../../models/jobs/objectReference"; +import CopyMnemonicsModal, { + CopyMnemonicsModalProps +} from "../Modals/CopyMnemonicsModal"; const LogObjectContextMenu = ( props: ObjectContextMenuProps @@ -117,13 +119,19 @@ const LogObjectContextMenu = ( const targetReference: ObjectReference = toObjectReference( checkedObjects[0] ); - const copyJob: CopyComponentsJob = { - source: logCurvesReference, - target: targetReference, + + const copyMnemonicsModalProps: CopyMnemonicsModalProps = { + sourceReferences: logCurvesReference, + targetReference: targetReference, startIndex: logCurvesReference.startIndex, - endIndex: logCurvesReference.endIndex + endIndex: logCurvesReference.endIndex, + targetServer: connectedServer }; - JobService.orderJob(JobType.CopyLogData, copyJob); + const action: DisplayModalAction = { + type: OperationType.DisplayModal, + payload: + }; + dispatchOperation(action); }; const onClickCompareHeader = () => { diff --git a/Src/WitsmlExplorer.Frontend/components/GlobalStyles.tsx b/Src/WitsmlExplorer.Frontend/components/GlobalStyles.tsx index 5e5b6f610..fa2cb4447 100644 --- a/Src/WitsmlExplorer.Frontend/components/GlobalStyles.tsx +++ b/Src/WitsmlExplorer.Frontend/components/GlobalStyles.tsx @@ -127,7 +127,7 @@ const GlobalStyles = createGlobalStyle<{ colors: Colors }>` } } - p[class*="Typography__StyledTypography"] { + [class*="Typography__StyledTypography"] { color:${(props) => props.colors.text.staticIconsDefault}; } diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/CopyMnemonicsModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/CopyMnemonicsModal.tsx new file mode 100644 index 000000000..c3cb5c178 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/CopyMnemonicsModal.tsx @@ -0,0 +1,234 @@ +import { Radio, Typography } from "@equinor/eds-core-react"; +import ModalDialog from "components/Modals/ModalDialog"; +import OperationContext from "contexts/operationContext"; +import OperationType from "contexts/operationType"; +import { useContext, useState } from "react"; +import styled from "styled-components"; +import ObjectReference from "../../models/jobs/objectReference"; +import ComponentReferences, { + createComponentReferences +} from "../../models/jobs/componentReferences"; +import { CopyComponentsJob } from "../../models/jobs/copyJobs"; +import JobService, { JobType } from "../../services/jobService"; +import { DeleteComponentsJob } from "../../models/jobs/deleteJobs"; +import { ReplaceComponentsJob } from "../../models/jobs/replaceComponentsJob"; +import { ComponentType, getParentType } from "../../models/componentType"; +import ComponentService from "../../services/componentService"; +import { Server } from "../../models/server"; +import ObjectService from "../../services/objectService"; +import AuthorizationService from "../../services/authorizationService"; +import LogObject from "../../models/logObject"; + +enum CopyMnemonicsType { + DeleteInsert = "deleteInsert", + Paste = "paste" +} + +export interface CopyMnemonicsModalProps { + sourceReferences: ComponentReferences; + targetReference: ObjectReference; + targetServer: Server; + sourceServer?: Server; + startIndex?: string; + endIndex?: string; +} + +const CopyMnemonicsModal = ( + props: CopyMnemonicsModalProps +): React.ReactElement => { + const { + sourceReferences, + targetReference, + sourceServer, + targetServer, + startIndex, + endIndex + } = props; + + const { dispatchOperation } = useContext(OperationContext); + + const [selectedCopyMnemonicsType, setCopyMnemonicsType] = useState( + CopyMnemonicsType.DeleteInsert + ); + const [isLoading, setIsLoading] = useState(false); + + const onSubmit = async () => { + dispatchOperation({ type: OperationType.HideModal }); + setIsLoading(true); + + if (selectedCopyMnemonicsType === CopyMnemonicsType.Paste) { + await orderPasteJob(); + } else if (selectedCopyMnemonicsType === CopyMnemonicsType.DeleteInsert) { + await orderDeleteInsertJob(); + } + + setIsLoading(false); + }; + + async function orderPasteJob() { + const jobType = + startIndex !== undefined ? JobType.CopyLogData : JobType.CopyComponents; + + const copyJob: CopyComponentsJob = { + source: sourceReferences, + target: targetReference, + startIndex: startIndex, + endIndex: endIndex + }; + + if (sourceServer) { + AuthorizationService.setSourceServer(sourceServer); + JobService.orderJobAtServer(jobType, copyJob, targetServer, sourceServer); + } else { + await JobService.orderJob(jobType, copyJob); + } + } + + async function orderDeleteInsertJob() { + const parentUid = targetReference.uid; + const parentType = getParentType(ComponentType.Mnemonic); + + const targetParent = await ObjectService.getObject( + targetReference.wellUid, + targetReference.wellboreUid, + parentUid, + parentType, + undefined, + targetServer + ); + + const allTargetComponents = await ComponentService.getComponents( + targetReference.wellUid, + targetReference.wellboreUid, + targetReference.uid, + ComponentType.Mnemonic, + targetServer + ); + + const indexCurve = (targetParent as LogObject)?.indexCurve; + + const targetComponentsToDelete = allTargetComponents.filter( + (c) => + c.mnemonic !== indexCurve && + sourceReferences.componentUids.find((sr) => sr === c.mnemonic) + ); + + if (targetComponentsToDelete.length == 0) { + await orderPasteJob(); + } else { + const deleteJob: DeleteComponentsJob = { + toDelete: createComponentReferences( + targetComponentsToDelete.map((component) => component.mnemonic), + targetParent, + ComponentType.Mnemonic, + targetServer.url + ) + }; + + const copyJob: CopyComponentsJob = { + source: sourceReferences, + target: targetReference + }; + + const replaceJob: ReplaceComponentsJob = { deleteJob, copyJob }; + + if (sourceServer) { + await JobService.orderJobAtServer( + JobType.ReplaceComponents, + replaceJob, + targetServer, + sourceServer + ); + } else { + await JobService.orderJob(JobType.ReplaceComponents, replaceJob); + } + } + } + + return ( + <> + + + Choose paste option: + + + +
+ + setCopyMnemonicsType(CopyMnemonicsType.DeleteInsert) + } + /> +
+
+ Delete/Insert + + Delete target mnemonics before copying. The mnemonics will + become equal on the source and target server afterwards. + +
+
+ +
+ + setCopyMnemonicsType(CopyMnemonicsType.Paste) + } + /> +
+
+ Paste + + Data on target server will be overwritten by data on the + source. Data on target server outside of the + startIndex/endIndex will be kept as is. + +
+
+
+ + } + onSubmit={() => onSubmit()} + isLoading={isLoading} + /> + + ); +}; + +const ContentLayout = styled.div` + display: flex; + flex-direction: column; + gap: 0.75rem; +`; + +const TextLayout = styled.div` + display: flex; + flex-direction: row; +`; + +const RadioLayout = styled.div` + display: flex; + flex-direction: column; + gap: 0.75rem; + align-items: center; +`; + +const RadioItemLayout = styled.div` + display: flex; + flex-direction: row; + gap: 0.25rem; + align-items: center; +`; + +export default CopyMnemonicsModal; From fa8d13f9bbc7e3e0c7c199252c2fc4a8a4d6cf06 Mon Sep 17 00:00:00 2001 From: Jan-Marius Vatle <48485965+janmarius@users.noreply.github.com> Date: Fri, 12 Apr 2024 12:11:02 +0200 Subject: [PATCH 011/124] FIX-2335 Upgrade Material UI (#2354) Co-authored-by: Elias Bruvik --- .../__testUtils__/testUtils.tsx | 2 +- .../components/Alerts.tsx | 5 +- .../ContentViews/CurveValuesView.tsx | 2 +- .../components/ContentViews/EditNumber.tsx | 2 +- .../ContentViews/EditSelectedLogCurveInfo.tsx | 7 +- .../ContentViews/table/ColumnDef.tsx | 9 +- .../ContentViews/table/ColumnOptionsMenu.tsx | 7 +- .../ContentViews/table/ContentTable.tsx | 7 +- .../ContentViews/table/contentTableStyles.ts | 2 +- .../ContextMenus/BatchModifyMenuItem.tsx | 2 +- .../ContextMenus/BhaRunContextMenu.tsx | 2 +- .../components/ContextMenus/ContextMenu.tsx | 4 +- .../ContextMenus/CopyComponentsToServer.tsx | 2 +- .../ContextMenus/FluidContextMenu.tsx | 2 +- .../ContextMenus/FluidsReportContextMenu.tsx | 2 +- .../FormationMarkerContextMenu.tsx | 2 +- .../GeologyIntervalContextMenu.tsx | 2 +- .../ContextMenus/JobInfoContextMenu.tsx | 2 +- .../ContextMenus/LogCurveInfoContextMenu.tsx | 2 +- .../LogCurvePriorityContextMenu.tsx | 2 +- .../ContextMenus/LogObjectContextMenu.tsx | 2 +- .../ContextMenus/LogsContextMenu.tsx | 2 +- .../ContextMenus/MessageObjectContextMenu.tsx | 2 +- .../ContextMenus/MnemonicsContextMenu.tsx | 2 +- .../ContextMenus/MudLogContextMenu.tsx | 2 +- .../ContextMenus/NestedMenuItem.tsx | 2 +- .../ContextMenus/ObjectMenuItems.tsx | 2 +- .../ObjectsSidebarContextMenu.tsx | 2 +- .../ContextMenus/OptionsContextMenu.tsx | 2 +- .../ContextMenus/RigContextMenu.tsx | 2 +- .../ContextMenus/RigsContextMenu.tsx | 2 +- .../ContextMenus/RiskContextMenu.tsx | 2 +- .../ContextMenus/TrajectoriesContextMenu.tsx | 2 +- .../ContextMenus/TrajectoryContextMenu.tsx | 2 +- .../TrajectoryStationContextMenu.tsx | 2 +- .../TubularComponentContextMenu.tsx | 2 +- .../ContextMenus/TubularContextMenu.tsx | 2 +- .../ContextMenus/TubularsContextMenu.tsx | 2 +- .../ContextMenus/WbGeometryContextMenu.tsx | 2 +- .../WbGeometrySectionContextMenu.tsx | 2 +- .../ContextMenus/WellContextMenu.tsx | 2 +- .../ContextMenus/WellboreContextMenu.tsx | 2 +- .../components/DateFormatter.ts | 2 +- .../Modals/BatchModifyPropertiesModal.tsx | 4 +- .../Modals/BhaRunPropertiesModal.tsx | 113 ++-- .../Modals/DeleteEmptyMnemonicsModal.tsx | 11 +- .../Modals/FormationMarkerPropertiesModal.tsx | 16 +- .../Modals/GeologyIntervalPropertiesModal.tsx | 13 +- .../Modals/JobInfoPropertiesModal.tsx | 51 +- .../Modals/LogCurveInfoBatchUpdateModal.tsx | 24 +- .../Modals/LogCurveInfoPropertiesModal.tsx | 36 +- .../components/Modals/LogDataImportModal.tsx | 4 +- .../components/Modals/LogPropertiesModal.tsx | 10 +- .../Modals/MessagePropertiesModal.tsx | 41 +- .../Modals/MissingDataAgentModal.tsx | 8 +- .../components/Modals/ModalParts.tsx | 14 +- .../Modals/MudLogPropertiesModal.tsx | 4 +- .../components/Modals/ObjectPickerModal.tsx | 12 +- .../components/Modals/RigPropertiesModal.tsx | 127 ++--- .../components/Modals/RiskPropertiesModal.tsx | 118 ++--- .../components/Modals/ServerModal.tsx | 12 +- .../components/Modals/SettingsModal.tsx | 2 +- .../Modals/ShowLogDataOnServerModal.tsx | 3 +- .../components/Modals/SpliceLogsModal.tsx | 7 +- .../Modals/TrajectoryPropertiesModal.tsx | 59 +-- .../TrajectoryStationPropertiesModal.tsx | 108 +--- .../TrimLogObject/AdjustNumberRangeModal.tsx | 11 +- .../TubularComponentPropertiesModal.tsx | 10 +- .../Modals/TubularPropertiesModal.tsx | 4 +- .../Modals/UserCredentialsModal.tsx | 8 +- .../Modals/WbGeometryPropertiesModal.tsx | 80 +-- .../WbGeometrySectionPropertiesModal.tsx | 14 +- .../Modals/WellBatchUpdateModal.tsx | 49 +- .../components/Modals/WellPropertiesModal.tsx | 74 ++- .../Modals/WellborePropertiesModal.tsx | 207 +++----- .../components/Sidebar/FilterPanel.tsx | 2 +- .../components/Sidebar/LogItem.tsx | 10 +- .../components/Sidebar/LogTypeItem.tsx | 27 +- .../components/Sidebar/ObjectGroupItem.tsx | 46 +- .../Sidebar/ObjectOnWellboreItem.tsx | 21 +- .../components/Sidebar/SearchFilter.tsx | 2 +- .../components/Sidebar/Sidebar.tsx | 20 +- .../components/Sidebar/TreeItem.tsx | 86 ++-- .../components/Sidebar/WellItem.tsx | 26 +- .../components/Sidebar/WellboreItem.tsx | 36 +- .../contexts/sidebarReducer.tsx | 14 +- Src/WitsmlExplorer.Frontend/package.json | 12 +- Src/WitsmlExplorer.Frontend/routes/Root.tsx | 2 +- .../styles/material-eds.tsx | 227 ++++---- yarn.lock | 485 +++++++++++++----- 90 files changed, 1237 insertions(+), 1146 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/__testUtils__/testUtils.tsx b/Src/WitsmlExplorer.Frontend/__testUtils__/testUtils.tsx index ff92c2d6b..30a28d71e 100644 --- a/Src/WitsmlExplorer.Frontend/__testUtils__/testUtils.tsx +++ b/Src/WitsmlExplorer.Frontend/__testUtils__/testUtils.tsx @@ -1,4 +1,4 @@ -import { ThemeProvider } from "@material-ui/core"; +import { ThemeProvider } from "@mui/material"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { render } from "@testing-library/react"; import { ConnectedServerProvider } from "contexts/connectedServerContext"; diff --git a/Src/WitsmlExplorer.Frontend/components/Alerts.tsx b/Src/WitsmlExplorer.Frontend/components/Alerts.tsx index db76fb240..22a1d8713 100644 --- a/Src/WitsmlExplorer.Frontend/components/Alerts.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Alerts.tsx @@ -1,6 +1,5 @@ -import { Collapse, IconButton } from "@material-ui/core"; -import { Close } from "@material-ui/icons"; -import { Alert, AlertTitle } from "@material-ui/lab"; +import { Close } from "@mui/icons-material"; +import { Alert, AlertTitle, Collapse, IconButton } from "@mui/material"; import { useConnectedServer } from "contexts/connectedServerContext"; import OperationContext from "contexts/operationContext"; import { capitalize } from "lodash"; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx index 65dc44b52..c33927393 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx @@ -4,7 +4,6 @@ import { Switch, Typography } from "@equinor/eds-core-react"; -import { CSSProperties } from "@material-ui/core/styles/withStyles"; import { MILLIS_IN_SECOND, SECONDS_IN_MINUTE, @@ -51,6 +50,7 @@ import LogObject, { indexToNumber } from "models/logObject"; import { toObjectReference } from "models/objectOnWellbore"; import { ObjectType } from "models/objectType"; import React, { + CSSProperties, useCallback, useContext, useEffect, diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/EditNumber.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/EditNumber.tsx index 4da13c55d..552494ca4 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/EditNumber.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/EditNumber.tsx @@ -1,5 +1,5 @@ import { Icon, Label, TextField } from "@equinor/eds-core-react"; -import { Tooltip } from "@material-ui/core"; +import { Tooltip } from "@mui/material"; import { Button } from "components/StyledComponents/Button"; import OperationContext from "contexts/operationContext"; import { ChangeEvent, ReactElement, useContext, useState } from "react"; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/EditSelectedLogCurveInfo.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/EditSelectedLogCurveInfo.tsx index 6689adc94..ba53cdd39 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/EditSelectedLogCurveInfo.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/EditSelectedLogCurveInfo.tsx @@ -15,6 +15,7 @@ import { useGetMnemonics } from "hooks/useGetMnemonics"; import { ComponentType } from "models/componentType"; import { CSSProperties, + ChangeEvent, Dispatch, SetStateAction, useContext, @@ -124,7 +125,7 @@ const EditSelectedLogCurveInfo = ( }; const onTextFieldChange = ( - e: any, + e: ChangeEvent, setIndex: Dispatch>, setIsValid: Dispatch> ) => { @@ -164,7 +165,7 @@ const EditSelectedLogCurveInfo = ( variant={isValidStart ? undefined : "error"} type={isTimeLog ? "datetime-local" : ""} step="1" - onChange={(e: any) => { + onChange={(e: ChangeEvent) => { onTextFieldChange(e, setSelectedStartIndex, setIsValidStart); }} /> @@ -178,7 +179,7 @@ const EditSelectedLogCurveInfo = ( type={isTimeLog ? "datetime-local" : ""} variant={isValidEnd ? undefined : "error"} step="1" - onChange={(e: any) => { + onChange={(e: ChangeEvent) => { onTextFieldChange(e, setSelectedEndIndex, setIsValidEnd); }} /> diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnDef.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnDef.tsx index 237e772b2..7622b4411 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnDef.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnDef.tsx @@ -1,4 +1,4 @@ -import { Checkbox, IconButton, useTheme } from "@material-ui/core"; +import { Checkbox, IconButton } from "@mui/material"; import { ColumnDef, Row, SortingFn, Table } from "@tanstack/react-table"; import { activeId, @@ -14,7 +14,7 @@ import { ContentType } from "components/ContentViews/table/tableParts"; import OperationContext from "contexts/operationContext"; -import { DecimalPreference } from "contexts/operationStateReducer"; +import { DecimalPreference, UserTheme } from "contexts/operationStateReducer"; import { useContext, useMemo } from "react"; import Icon from "styles/Icons"; import { @@ -37,11 +37,10 @@ export const useColumnDef = ( checkableRows: boolean, stickyLeftColumns: number ) => { - const isCompactMode = useTheme().props.MuiCheckbox?.size === "small"; - const { - operationState: { decimals } + operationState: { decimals, theme } } = useContext(OperationContext); + const isCompactMode = theme === UserTheme.Compact; return useMemo(() => { const savedWidths = getLocalStorageItem<{ [label: string]: number }>( diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnOptionsMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnOptionsMenu.tsx index 67d63fd1e..129cf70d6 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnOptionsMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnOptionsMenu.tsx @@ -1,5 +1,5 @@ import { Icon, Menu, Typography } from "@equinor/eds-core-react"; -import { Checkbox, useTheme } from "@material-ui/core"; +import { Checkbox } from "@mui/material"; import { Table } from "@tanstack/react-table"; import { calculateColumnWidth, @@ -12,6 +12,7 @@ import { } from "components/ContentViews/table/tableParts"; import { Button } from "components/StyledComponents/Button"; import OperationContext from "contexts/operationContext"; +import { UserTheme } from "contexts/operationStateReducer"; import { useLocalStorageState } from "hooks/useLocalStorageState"; import { useContext, useState } from "react"; import styled from "styled-components"; @@ -45,7 +46,7 @@ export const ColumnOptionsMenu = (props: { firstToggleableIndex } = props; const { - operationState: { colors } + operationState: { colors, theme } } = useContext(OperationContext); const [draggedId, setDraggedId] = useState(); const [draggedOverId, setDraggedOverId] = useState(); @@ -54,7 +55,7 @@ export const ColumnOptionsMenu = (props: { const [, saveOrderToStorage] = useLocalStorageState( viewId + STORAGE_CONTENTTABLE_ORDER_KEY ); - const isCompactMode = useTheme().props.MuiCheckbox?.size === "small"; + const isCompactMode = theme === UserTheme.Compact; const drop = (e: React.DragEvent) => { e.preventDefault(); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ContentTable.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ContentTable.tsx index 44c2979d4..fa8f498cc 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ContentTable.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ContentTable.tsx @@ -1,4 +1,4 @@ -import { TableBody, TableHead, useTheme } from "@material-ui/core"; +import { TableBody, TableHead } from "@mui/material"; import { ColumnSizingState, Header, @@ -46,6 +46,7 @@ import { ContentTableProps } from "components/ContentViews/table/tableParts"; import OperationContext from "contexts/operationContext"; +import { UserTheme } from "contexts/operationStateReducer"; import { indexToNumber } from "models/logObject"; import * as React from "react"; import { Fragment, useContext, useEffect, useMemo, useState } from "react"; @@ -81,7 +82,7 @@ export const ContentTable = React.memo( autoRefresh = false } = contentTableProps; const { - operationState: { colors } + operationState: { colors, theme } } = useContext(OperationContext); const [previousIndex, setPreviousIndex] = useState(null); const [rowSelection, setRowSelection] = useState( @@ -94,7 +95,7 @@ export const ContentTable = React.memo( initializeColumnVisibility(viewId) ); const [columnSizing, setColumnSizing] = useState({}); - const isCompactMode = useTheme().props.MuiCheckbox?.size === "small"; + const isCompactMode = theme === UserTheme.Compact; const cellHeight = isCompactMode ? 30 : 53; const headCellHeight = isCompactMode ? 35 : 55; const noData = useMemo(() => [], []); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/contentTableStyles.ts b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/contentTableStyles.ts index 6a0ee5c87..92f03b7da 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/contentTableStyles.ts +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/contentTableStyles.ts @@ -1,4 +1,4 @@ -import { TableCell } from "@material-ui/core"; +import { TableCell } from "@mui/material"; import styled from "styled-components"; import { Colors, light } from "styles/Colors"; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/BatchModifyMenuItem.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/BatchModifyMenuItem.tsx index 0e7b59b54..657b1c998 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/BatchModifyMenuItem.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/BatchModifyMenuItem.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { MenuItem } from "@material-ui/core"; +import { MenuItem } from "@mui/material"; import OperationContext from "contexts/operationContext"; import { ReactElement, forwardRef, useContext } from "react"; import OperationType from "../../contexts/operationType"; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/BhaRunContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/BhaRunContextMenu.tsx index 8de656d1f..6099d5bc1 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/BhaRunContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/BhaRunContextMenu.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { Divider, MenuItem } from "@material-ui/core"; +import { Divider, MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; import ContextMenu from "components/ContextMenus/ContextMenu"; import { StyledIcon } from "components/ContextMenus/ContextMenuUtils"; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/ContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/ContextMenu.tsx index 445027c26..7124fb666 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/ContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/ContextMenu.tsx @@ -1,4 +1,4 @@ -import { Menu } from "@material-ui/core"; +import { Menu } from "@mui/material"; import OperationContext from "contexts/operationContext"; import { MousePosition } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; @@ -64,7 +64,7 @@ export const StyledMenu = styled(Menu)<{ colors: Colors }>` svg { fill: ${(props) => props.colors.infographic.primaryMossGreen}; } - .MuiListItem-button:hover { + .MuiMenuItem-root:hover { text-decoration: none; background-color: ${(props) => props.colors.interactive.contextMenuItemHover}; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/CopyComponentsToServer.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/CopyComponentsToServer.tsx index 0487fe4c0..103c7349b 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/CopyComponentsToServer.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/CopyComponentsToServer.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { MenuItem } from "@material-ui/core"; +import { MenuItem } from "@mui/material"; import { menuItemText } from "components/ContextMenus/ContextMenuUtils"; import NestedMenuItem from "components/ContextMenus/NestedMenuItem"; import CopyRangeModal, { diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/FluidContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/FluidContextMenu.tsx index c0aa29bc3..182f22cb9 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/FluidContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/FluidContextMenu.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { MenuItem } from "@material-ui/core"; +import { MenuItem } from "@mui/material"; import ContextMenu from "components/ContextMenus/ContextMenu"; import { StyledIcon, diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/FluidsReportContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/FluidsReportContextMenu.tsx index c6c2300d6..ebd702edf 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/FluidsReportContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/FluidsReportContextMenu.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { MenuItem } from "@material-ui/core"; +import { MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; import ContextMenu from "components/ContextMenus/ContextMenu"; import { diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/FormationMarkerContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/FormationMarkerContextMenu.tsx index bd5b11b9e..19bfe5e76 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/FormationMarkerContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/FormationMarkerContextMenu.tsx @@ -1,5 +1,5 @@ import { Divider, Typography } from "@equinor/eds-core-react"; -import { MenuItem } from "@material-ui/core"; +import { MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; import ContextMenu from "components/ContextMenus/ContextMenu"; import { StyledIcon } from "components/ContextMenus/ContextMenuUtils"; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/GeologyIntervalContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/GeologyIntervalContextMenu.tsx index b7348d6f2..94e34034d 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/GeologyIntervalContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/GeologyIntervalContextMenu.tsx @@ -1,5 +1,5 @@ import { Divider, Typography } from "@equinor/eds-core-react"; -import { MenuItem } from "@material-ui/core"; +import { MenuItem } from "@mui/material"; import ContextMenu from "components/ContextMenus/ContextMenu"; import { StyledIcon, diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/JobInfoContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/JobInfoContextMenu.tsx index 4640ff356..3fb60bc3c 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/JobInfoContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/JobInfoContextMenu.tsx @@ -1,5 +1,5 @@ import { Divider, Typography } from "@equinor/eds-core-react"; -import { MenuItem } from "@material-ui/core"; +import { MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; import ContextMenu from "components/ContextMenus/ContextMenu"; import { StyledIcon } from "components/ContextMenus/ContextMenuUtils"; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurveInfoContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurveInfoContextMenu.tsx index 86846d060..117ce4e50 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurveInfoContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurveInfoContextMenu.tsx @@ -1,5 +1,5 @@ import { Divider, Typography } from "@equinor/eds-core-react"; -import { MenuItem } from "@material-ui/core"; +import { MenuItem } from "@mui/material"; import { WITSML_INDEX_TYPE_MD } from "components/Constants"; import { LogCurveInfoRow } from "components/ContentViews/LogCurveInfoListView"; import ContextMenu from "components/ContextMenus/ContextMenu"; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurvePriorityContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurvePriorityContextMenu.tsx index 7b57d9bc8..1973e3d71 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurvePriorityContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurvePriorityContextMenu.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { MenuItem } from "@material-ui/core"; +import { MenuItem } from "@mui/material"; import React, { useContext } from "react"; import OperationContext from "../../contexts/operationContext"; import { MousePosition } from "../../contexts/operationStateReducer"; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogObjectContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogObjectContextMenu.tsx index ea1659530..79260d35f 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogObjectContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogObjectContextMenu.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { Divider, MenuItem } from "@material-ui/core"; +import { Divider, MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; import { BatchModifyMenuItem } from "components/ContextMenus/BatchModifyMenuItem"; import ContextMenu from "components/ContextMenus/ContextMenu"; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogsContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogsContextMenu.tsx index 3fcc89988..a2181b41f 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogsContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogsContextMenu.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { MenuItem } from "@material-ui/core"; +import { MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; import { StoreFunction, diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/MessageObjectContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/MessageObjectContextMenu.tsx index 1e98bbfbf..ca7e625ad 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/MessageObjectContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/MessageObjectContextMenu.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { Divider, MenuItem } from "@material-ui/core"; +import { Divider, MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; import ContextMenu from "components/ContextMenus/ContextMenu"; import { diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/MnemonicsContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/MnemonicsContextMenu.tsx index 51ff58423..85b584bbc 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/MnemonicsContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/MnemonicsContextMenu.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { MenuItem } from "@material-ui/core"; +import { MenuItem } from "@mui/material"; import ContextMenu from "components/ContextMenus/ContextMenu"; import { StyledIcon } from "components/ContextMenus/ContextMenuUtils"; import ConfirmModal from "components/Modals/ConfirmModal"; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/MudLogContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/MudLogContextMenu.tsx index 67c4c8c8b..4ec80376b 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/MudLogContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/MudLogContextMenu.tsx @@ -1,5 +1,5 @@ import { Divider, Typography } from "@equinor/eds-core-react"; -import { MenuItem } from "@material-ui/core"; +import { MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; import ContextMenu from "components/ContextMenus/ContextMenu"; import { diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/NestedMenuItem.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/NestedMenuItem.tsx index 4747f30cb..f4314f24e 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/NestedMenuItem.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/NestedMenuItem.tsx @@ -1,5 +1,5 @@ import { Icon, Typography } from "@equinor/eds-core-react"; -import MenuItem, { MenuItemProps } from "@material-ui/core/MenuItem"; +import MenuItem, { MenuItemProps } from "@mui/material/MenuItem"; import { StyledMenu } from "components/ContextMenus/ContextMenu"; import { StyledIcon } from "components/ContextMenus/ContextMenuUtils"; import OperationContext from "contexts/operationContext"; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectMenuItems.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectMenuItems.tsx index 0ac1b4ecb..a71719875 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectMenuItems.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectMenuItems.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { Divider, MenuItem } from "@material-ui/core"; +import { Divider, MenuItem } from "@mui/material"; import { QueryClient } from "@tanstack/react-query"; import { WITSML_INDEX_TYPE_MD } from "components/Constants"; import { diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectsSidebarContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectsSidebarContextMenu.tsx index c25595154..4b4bf65e9 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectsSidebarContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectsSidebarContextMenu.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { MenuItem } from "@material-ui/core"; +import { MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; import { ObjectTypeToTemplateObject, diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/OptionsContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/OptionsContextMenu.tsx index 105e91f1c..b327cb43e 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/OptionsContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/OptionsContextMenu.tsx @@ -1,5 +1,5 @@ import { Icon, Typography } from "@equinor/eds-core-react"; -import { MenuItem, Tooltip } from "@material-ui/core"; +import { MenuItem, Tooltip } from "@mui/material"; import ContextMenu from "components/ContextMenus/ContextMenu"; import { pluralize } from "components/ContextMenus/ContextMenuUtils"; import { HideModalAction } from "contexts/operationStateReducer"; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigContextMenu.tsx index 37db225c1..c39f0f433 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigContextMenu.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { Divider, MenuItem } from "@material-ui/core"; +import { Divider, MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; import { BatchModifyMenuItem } from "components/ContextMenus/BatchModifyMenuItem"; import ContextMenu from "components/ContextMenus/ContextMenu"; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigsContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigsContextMenu.tsx index dc912b632..23d78a1ae 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigsContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigsContextMenu.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { MenuItem } from "@material-ui/core"; +import { MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; import { StoreFunction, diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/RiskContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/RiskContextMenu.tsx index be28a39b3..77abce94a 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/RiskContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/RiskContextMenu.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { Divider, MenuItem } from "@material-ui/core"; +import { Divider, MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; import ContextMenu from "components/ContextMenus/ContextMenu"; import { StyledIcon } from "components/ContextMenus/ContextMenuUtils"; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoriesContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoriesContextMenu.tsx index d8b44c432..f1e961aec 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoriesContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoriesContextMenu.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { MenuItem } from "@material-ui/core"; +import { MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; import { StoreFunction, diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryContextMenu.tsx index 95668501d..733840fee 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryContextMenu.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { Divider, MenuItem } from "@material-ui/core"; +import { Divider, MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; import ContextMenu from "components/ContextMenus/ContextMenu"; import { diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryStationContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryStationContextMenu.tsx index 01cc8e8d5..c083da3b2 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryStationContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryStationContextMenu.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { Divider, MenuItem } from "@material-ui/core"; +import { Divider, MenuItem } from "@mui/material"; import { TrajectoryStationRow } from "components/ContentViews/TrajectoryView"; import ContextMenu from "components/ContextMenus/ContextMenu"; import { diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularComponentContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularComponentContextMenu.tsx index 735da636e..ce59d48d1 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularComponentContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularComponentContextMenu.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { Divider, MenuItem } from "@material-ui/core"; +import { Divider, MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; import { TubularComponentRow } from "components/ContentViews/TubularView"; import ContextMenu from "components/ContextMenus/ContextMenu"; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularContextMenu.tsx index cc957ef18..6a7846a70 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularContextMenu.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { Divider, MenuItem } from "@material-ui/core"; +import { Divider, MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; import ContextMenu from "components/ContextMenus/ContextMenu"; import { diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularsContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularsContextMenu.tsx index ffa7fbef3..f0d9b291c 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularsContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularsContextMenu.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { MenuItem } from "@material-ui/core"; +import { MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; import { StoreFunction, diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometryContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometryContextMenu.tsx index e36f9ee8f..e394dbfd2 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometryContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometryContextMenu.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { Divider, MenuItem } from "@material-ui/core"; +import { Divider, MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; import ContextMenu from "components/ContextMenus/ContextMenu"; import { diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometrySectionContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometrySectionContextMenu.tsx index 3c127eb20..0a57ca179 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometrySectionContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometrySectionContextMenu.tsx @@ -1,5 +1,5 @@ import { Divider, Typography } from "@equinor/eds-core-react"; -import { MenuItem } from "@material-ui/core"; +import { MenuItem } from "@mui/material"; import ContextMenu from "components/ContextMenus/ContextMenu"; import { StyledIcon, diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx index add57a073..20c42cd59 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { Divider, MenuItem } from "@material-ui/core"; +import { Divider, MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; import { StoreFunction, diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellboreContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellboreContextMenu.tsx index 995baf8ba..2df2497b7 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellboreContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellboreContextMenu.tsx @@ -1,5 +1,5 @@ import { Typography } from "@equinor/eds-core-react"; -import { Divider, MenuItem } from "@material-ui/core"; +import { Divider, MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; import { ObjectTypeToTemplateObject, diff --git a/Src/WitsmlExplorer.Frontend/components/DateFormatter.ts b/Src/WitsmlExplorer.Frontend/components/DateFormatter.ts index fd119d410..5d62bb045 100644 --- a/Src/WitsmlExplorer.Frontend/components/DateFormatter.ts +++ b/Src/WitsmlExplorer.Frontend/components/DateFormatter.ts @@ -1,5 +1,5 @@ -import { format, formatInTimeZone, toDate } from "date-fns-tz"; import { DateTimeFormat, TimeZone } from "contexts/operationStateReducer"; +import { format, formatInTimeZone, toDate } from "date-fns-tz"; //https://date-fns.org/v2.29.3/docs/format const naturalDateTimeFormat = "dd.MM.yyyy HH:mm:ss.SSS"; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/BatchModifyPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/BatchModifyPropertiesModal.tsx index 882b87b9d..b93d69c9d 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/BatchModifyPropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/BatchModifyPropertiesModal.tsx @@ -1,5 +1,5 @@ import { Autocomplete, TextField } from "@equinor/eds-core-react"; -import { ReactElement, useContext, useState } from "react"; +import { ChangeEvent, ReactElement, useContext, useState } from "react"; import styled from "styled-components"; import OperationContext from "../../contexts/operationContext"; import OperationType from "../../contexts/operationType"; @@ -101,7 +101,7 @@ export const BatchModifyPropertiesModal = ( ? "error" : undefined } - onChange={(e: any) => + onChange={(e: ChangeEvent) => onChangeProperty(property.property, e.target.value) } /> diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/BhaRunPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/BhaRunPropertiesModal.tsx index 3d817c305..841f29998 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/BhaRunPropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/BhaRunPropertiesModal.tsx @@ -1,5 +1,4 @@ -import { Autocomplete } from "@equinor/eds-core-react"; -import { InputAdornment, TextField } from "@material-ui/core"; +import { Autocomplete, TextField } from "@equinor/eds-core-react"; import formatDateString from "components/DateFormatter"; import { DateTimeField } from "components/Modals/DateTimeField"; import ModalDialog from "components/Modals/ModalDialog"; @@ -13,7 +12,7 @@ import OperationType from "contexts/operationType"; import BhaRun from "models/bhaRun"; import { itemStateTypes } from "models/itemStateTypes"; import { ObjectType } from "models/objectType"; -import React, { useContext, useEffect, useState } from "react"; +import React, { ChangeEvent, useContext, useEffect, useState } from "react"; import JobService, { JobType } from "services/jobService"; const typesOfBhaStatus = ["final", "progress", "plan", "unknown"]; @@ -94,6 +93,8 @@ const BhaRunPropertiesModal = ( dispatchOperation({ type: OperationType.HideModal }); }; + const validBhaRunName = validText(editableBhaRun?.name, 1, 64); + return ( <> {editableBhaRun && ( @@ -108,28 +109,24 @@ const BhaRunPropertiesModal = ( id="wellUid" label="well uid" defaultValue={editableBhaRun.wellUid} - fullWidth /> + onChange={(e: ChangeEvent) => setEditableBhaRun({ ...editableBhaRun, name: e.target.value }) } /> @@ -161,9 +155,12 @@ const BhaRunPropertiesModal = ( label={"tubular"} required value={editableBhaRun.tubular?.value ?? ""} - error={editableBhaRun.tubular?.value?.length === 0} - fullWidth - onChange={(e) => + variant={ + editableBhaRun.tubular?.value?.length === 0 + ? "error" + : undefined + } + onChange={(e: ChangeEvent) => setEditableBhaRun({ ...editableBhaRun, tubular: { @@ -178,9 +175,12 @@ const BhaRunPropertiesModal = ( label={"tubularUidRef"} required value={editableBhaRun.tubular?.uidRef ?? ""} - error={editableBhaRun.tubular?.uidRef?.length === 0} - fullWidth - onChange={(e) => + variant={ + editableBhaRun.tubular?.uidRef?.length === 0 + ? "error" + : undefined + } + onChange={(e: ChangeEvent) => setEditableBhaRun({ ...editableBhaRun, tubular: { @@ -236,19 +236,12 @@ const BhaRunPropertiesModal = ( id={"planDogleg"} label={"planDogleg"} type="number" - fullWidth - InputProps={{ - startAdornment: ( - - {editableBhaRun.planDogleg - ? editableBhaRun.planDogleg.uom - : ""} - - ) - }} + unit={ + editableBhaRun.planDogleg ? editableBhaRun.planDogleg.uom : "" + } disabled={!editableBhaRun.planDogleg} value={editableBhaRun.planDogleg?.value} - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableBhaRun({ ...editableBhaRun, planDogleg: { @@ -264,19 +257,12 @@ const BhaRunPropertiesModal = ( id={"actDogleg"} label={"actDogleg"} type="number" - fullWidth - InputProps={{ - startAdornment: ( - - {editableBhaRun.actDogleg - ? editableBhaRun.actDogleg.uom - : ""} - - ) - }} + unit={ + editableBhaRun.actDogleg ? editableBhaRun.actDogleg.uom : "" + } disabled={!editableBhaRun.actDogleg} value={editableBhaRun.actDogleg?.value} - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableBhaRun({ ...editableBhaRun, actDogleg: { @@ -292,19 +278,14 @@ const BhaRunPropertiesModal = ( id={"actDoglegMx"} label={"actDoglegMx"} type="number" - fullWidth - InputProps={{ - startAdornment: ( - - {editableBhaRun.actDoglegMx - ? editableBhaRun.actDoglegMx.uom - : ""} - - ) - }} + unit={ + editableBhaRun.actDoglegMx + ? editableBhaRun.actDoglegMx.uom + : "" + } disabled={!editableBhaRun.actDoglegMx} value={editableBhaRun.actDoglegMx?.value} - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableBhaRun({ ...editableBhaRun, actDoglegMx: { @@ -332,8 +313,7 @@ const BhaRunPropertiesModal = ( id={"numBitRun"} label={"numBitRun"} value={editableBhaRun.numBitRun ?? ""} - fullWidth - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableBhaRun({ ...editableBhaRun, numBitRun: e.target.value @@ -345,8 +325,7 @@ const BhaRunPropertiesModal = ( type="number" label={"numStringRun"} value={editableBhaRun.numStringRun ?? ""} - fullWidth - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableBhaRun({ ...editableBhaRun, numStringRun: e.target.value @@ -357,8 +336,7 @@ const BhaRunPropertiesModal = ( id={"reasonTrip"} label={"reasonTrip"} value={editableBhaRun.reasonTrip ?? ""} - fullWidth - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableBhaRun({ ...editableBhaRun, reasonTrip: e.target.value @@ -369,8 +347,7 @@ const BhaRunPropertiesModal = ( id={"objectiveBha"} label={"objectiveBha"} value={editableBhaRun.objectiveBha ?? ""} - fullWidth - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableBhaRun({ ...editableBhaRun, objectiveBha: e.target.value @@ -397,21 +374,18 @@ const BhaRunPropertiesModal = ( id="dateTimeCreation" label="created" defaultValue={editableBhaRun.commonData.dTimCreation} - fullWidth /> { + onChange={(e: ChangeEvent) => { const commonData = { ...editableBhaRun.commonData, sourceName: e.target.value @@ -423,8 +397,7 @@ const BhaRunPropertiesModal = ( id="serviceCategory" label="serviceCategory" value={editableBhaRun.commonData.serviceCategory ?? ""} - fullWidth - onChange={(e) => { + onChange={(e: ChangeEvent) => { const commonData = { ...editableBhaRun.commonData, serviceCategory: e.target.value @@ -436,8 +409,7 @@ const BhaRunPropertiesModal = ( id="comments" label="comments" value={editableBhaRun.commonData.comments ?? ""} - fullWidth - onChange={(e) => { + onChange={(e: ChangeEvent) => { const commonData = { ...editableBhaRun.commonData, comments: e.target.value @@ -449,8 +421,7 @@ const BhaRunPropertiesModal = ( id="defaultDatum" label="defaultDatum" value={editableBhaRun.commonData.defaultDatum ?? ""} - fullWidth - onChange={(e) => { + onChange={(e: ChangeEvent) => { const commonData = { ...editableBhaRun.commonData, defaultDatum: e.target.value @@ -461,7 +432,7 @@ const BhaRunPropertiesModal = ( } confirmDisabled={ - !validText(editableBhaRun.name) || + !validBhaRunName || !dTimStopValid || !dTimStartValid || !dTimStartDrillingValid || diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/DeleteEmptyMnemonicsModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/DeleteEmptyMnemonicsModal.tsx index 9b5de9912..39c7935ea 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/DeleteEmptyMnemonicsModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/DeleteEmptyMnemonicsModal.tsx @@ -1,5 +1,4 @@ -import { Icon, Tooltip } from "@equinor/eds-core-react"; -import { TextField } from "@material-ui/core"; +import { Icon, TextField, Tooltip } from "@equinor/eds-core-react"; import { DateTimeField } from "components/Modals/DateTimeField"; import ModalDialog from "components/Modals/ModalDialog"; import { ReportModal } from "components/Modals/ReportModal"; @@ -11,7 +10,7 @@ import LogObject from "models/logObject"; import { toObjectReference } from "models/objectOnWellbore"; import Well from "models/well"; import Wellbore from "models/wellbore"; -import { useContext, useState } from "react"; +import { ChangeEvent, useContext, useState } from "react"; import JobService, { JobType } from "services/jobService"; import styled from "styled-components"; @@ -88,11 +87,13 @@ const DeleteEmptyMnemonicsModal = ( timeZone={timeZone} /> setNullDepthValue(+e.target.value)} + onChange={(e: ChangeEvent) => + setNullDepthValue(+e.target.value) + } /> + onChange={(e: ChangeEvent) => setEditable({ ...editable, name: e.target.value }) } /> @@ -258,7 +264,7 @@ const FormationMarkerPropertiesModal = ( ? `description must be 1-${MaxLength.Comment} characters` : "" } - onChange={(e: any) => + onChange={(e: ChangeEvent) => setEditable({ ...editable, description: undefinedOnUnchagedEmptyString( @@ -303,7 +309,7 @@ const StratigraphicField = ( unit={originalStruct?.kind} helperText={invalid[property] ? `${property} cannot be empty` : ""} variant={invalid[property] ? "error" : undefined} - onChange={(e: any) => { + onChange={(e: ChangeEvent) => { setResult({ ...editable, [property]: { @@ -336,7 +342,7 @@ const MeasureField = (props: MeasureFieldProps): React.ReactElement => { unit={originalMeasure?.uom} helperText={invalid[property] ? `${property} cannot be empty` : ""} variant={invalid[property] ? "error" : undefined} - onChange={(e: any) => { + onChange={(e: ChangeEvent) => { setResult({ ...editable, [property]: { diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/GeologyIntervalPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/GeologyIntervalPropertiesModal.tsx index 2c00267b3..5d7027e2b 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/GeologyIntervalPropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/GeologyIntervalPropertiesModal.tsx @@ -1,5 +1,5 @@ import { Autocomplete, TextField } from "@equinor/eds-core-react"; -import { Typography } from "@material-ui/core"; +import { Typography } from "@mui/material"; import ModalDialog from "components/Modals/ModalDialog"; import { invalidMeasureInput, @@ -19,6 +19,7 @@ import MeasureWithDatum from "models/measureWithDatum"; import MudLog from "models/mudLog"; import { toObjectReference } from "models/objectOnWellbore"; import React, { + ChangeEvent, Dispatch, SetStateAction, useContext, @@ -213,7 +214,7 @@ const GeologyIntervalPropertiesModal = ( ? `description must be 1-${MaxLength.Comment} characters` : "" } - onChange={(e: any) => + onChange={(e: ChangeEvent) => setEditable({ ...editable, description: undefinedOnUnchagedEmptyString( @@ -308,7 +309,7 @@ const GeologyIntervalPropertiesModal = ( defaultValue={geologyInterval?.dxcAv} helperText={invalid.dxcAv ? `dxcAv cannot be empty` : ""} variant={invalid.dxcAv ? "error" : undefined} - onChange={(e: any) => + onChange={(e: ChangeEvent) => setEditable({ ...editable, dxcAv: parseFloat(e.target.value) @@ -345,7 +346,7 @@ const GeologyIntervalPropertiesModal = ( ? `codeLith must be 1-${MaxLength.Str16} characters` : "" } - onChange={(e: any) => + onChange={(e: ChangeEvent) => setLithology(e.target.value, "codeLith", lithology.uid) } /> @@ -366,7 +367,7 @@ const GeologyIntervalPropertiesModal = ( ? "error" : undefined } - onChange={(e: any) => + onChange={(e: ChangeEvent) => setLithology( parseFloat(e.target.value), "lithPc", @@ -414,7 +415,7 @@ const MeasureField = (props: MeasureFieldProps): React.ReactElement => { unit={originalMeasure?.uom} helperText={invalid[property] ? `${property} cannot be empty` : ""} variant={invalid[property] ? "error" : undefined} - onChange={(e: any) => { + onChange={(e: ChangeEvent) => { setResult({ ...editable, [property]: { diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/JobInfoPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/JobInfoPropertiesModal.tsx index bf90708d5..f4206aee6 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/JobInfoPropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/JobInfoPropertiesModal.tsx @@ -1,4 +1,4 @@ -import { TextField } from "@material-ui/core"; +import { TextField } from "@equinor/eds-core-react"; import ModalDialog from "components/Modals/ModalDialog"; import JobStatus from "models/jobStatus"; import JobInfo from "models/jobs/jobInfo"; @@ -20,97 +20,86 @@ const JobInfoPropertiesModal = ( content={ <> {jobInfo.status == JobStatus.Failed && ( )} } diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/LogCurveInfoBatchUpdateModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/LogCurveInfoBatchUpdateModal.tsx index f81da6422..de8df2dfc 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/LogCurveInfoBatchUpdateModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/LogCurveInfoBatchUpdateModal.tsx @@ -1,21 +1,21 @@ -import { Grid } from "@material-ui/core"; -import React, { useContext, useState } from "react"; +import { Autocomplete, TextField } from "@equinor/eds-core-react"; +import { Grid } from "@mui/material"; +import React, { ChangeEvent, useContext, useState } from "react"; import styled from "styled-components"; +import OperationContext from "../../contexts/operationContext"; +import OperationType from "../../contexts/operationType"; +import BatchModifyLogCurveInfoJob from "../../models/jobs/batchModifyLogCurveInfoJob"; import LogCurveInfo, { EmptyLogCurveInfo } from "../../models/logCurveInfo"; import LogObject from "../../models/logObject"; +import { logTraceState } from "../../models/logTraceState"; +import Measure from "../../models/measure"; import { toObjectReference } from "../../models/objectOnWellbore"; +import { unitType } from "../../models/unitType"; import JobService, { JobType } from "../../services/jobService"; -import ModalDialog from "./ModalDialog"; import { LogCurveInfoRow } from "../ContentViews/LogCurveInfoListView"; -import { Autocomplete, TextField } from "@equinor/eds-core-react"; -import { logTraceState } from "../../models/logTraceState"; -import { unitType } from "../../models/unitType"; +import ModalDialog from "./ModalDialog"; import { validText } from "./ModalParts"; -import Measure from "../../models/measure"; -import BatchModifyLogCurveInfoJob from "../../models/jobs/batchModifyLogCurveInfoJob"; -import OperationType from "../../contexts/operationType"; import { ReportModal } from "./ReportModal"; -import OperationContext from "../../contexts/operationContext"; export interface LogCurveInfoBatchUpdateModalProps { logCurveInfoRows: LogCurveInfoRow[]; @@ -108,7 +108,7 @@ const LogCurveInfoBatchUpdateModal = ( label={"SensorOffset value"} type="number" value={editableLogCurveInfo.sensorOffset?.value} - onChange={(e: any) => + onChange={(e: ChangeEvent) => setEditableLogCurveInfo({ ...editableLogCurveInfo, sensorOffset: { @@ -147,7 +147,7 @@ const LogCurveInfoBatchUpdateModal = ( label={"NullValue"} type="number" value={editableLogCurveInfo.nullValue} - onChange={(e: any) => + onChange={(e: ChangeEvent) => setEditableLogCurveInfo({ ...editableLogCurveInfo, nullValue: e.target.value diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/LogCurveInfoPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/LogCurveInfoPropertiesModal.tsx index 4a03af0b9..b5cbe975e 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/LogCurveInfoPropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/LogCurveInfoPropertiesModal.tsx @@ -1,12 +1,14 @@ -import { TextField, Typography } from "@material-ui/core"; +import { TextField } from "@equinor/eds-core-react"; +import { Typography } from "@mui/material"; import ModalDialog from "components/Modals/ModalDialog"; +import { validText } from "components/Modals/ModalParts"; import { HideModalAction } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; import ModifyLogCurveInfoJob from "models/jobs/modifyLogCurveInfoJob"; import LogCurveInfo from "models/logCurveInfo"; import LogObject from "models/logObject"; import { toObjectReference } from "models/objectOnWellbore"; -import React, { useEffect, useState } from "react"; +import React, { ChangeEvent, useEffect, useState } from "react"; import JobService, { JobType } from "services/jobService"; import { Layout } from "../StyledComponents/Layout"; @@ -40,6 +42,9 @@ const LogCurveInfoPropertiesModal = ( setEditableLogCurveInfo(logCurveInfo); }, [logCurveInfo]); + const validMnemonic = validText(editableLogCurveInfo?.mnemonic, 1, 64); + const validUnit = validText(editableLogCurveInfo?.unit, 1, 64); + return ( <> {editableLogCurveInfo && ( @@ -52,22 +57,19 @@ const LogCurveInfoPropertiesModal = ( id="uid" label="uid" defaultValue={editableLogCurveInfo.uid} - fullWidth /> + onChange={(e: ChangeEvent) => setEditableLogCurveInfo({ ...editableLogCurveInfo, mnemonic: e.target.value @@ -78,18 +80,13 @@ const LogCurveInfoPropertiesModal = ( id="unit" label="unit" defaultValue={editableLogCurveInfo.unit} - error={ - editableLogCurveInfo.unit == null || - editableLogCurveInfo.unit.length === 0 - } + variant={validUnit ? undefined : "error"} helperText={ - editableLogCurveInfo.unit == null || - editableLogCurveInfo.unit.length === 0 + !validUnit ? "A unit cannot be empty. Size must be 1 to 64 characters." : "" } - inputProps={{ minLength: 1, maxLength: 64 }} - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableLogCurveInfo({ ...editableLogCurveInfo, unit: e.target.value @@ -100,7 +97,7 @@ const LogCurveInfoPropertiesModal = ( id="curveDescription" label="curveDescription" defaultValue={editableLogCurveInfo.curveDescription} - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableLogCurveInfo({ ...editableLogCurveInfo, curveDescription: e.target.value @@ -112,14 +109,12 @@ const LogCurveInfoPropertiesModal = ( id="typeLogData" label="typeLogData" defaultValue={editableLogCurveInfo.typeLogData} - fullWidth /> {logCurveInfo?.axisDefinitions?.map((axisDefinition) => { return ( @@ -129,21 +124,18 @@ const LogCurveInfoPropertiesModal = ( + onChange={(e: ChangeEvent) => setEditableLogObject({ ...editableLogObject, uid: e.target.value @@ -154,7 +154,7 @@ const LogPropertiesModal = ( variant={ editableLogObject.name.length === 0 ? "error" : undefined } - onChange={(e: any) => + onChange={(e: ChangeEvent) => setEditableLogObject({ ...editableLogObject, name: e.target.value @@ -181,7 +181,7 @@ const LogPropertiesModal = ( } variant={validServiceCompany() ? undefined : "error"} defaultValue={editableLogObject.serviceCompany} - onChange={(e: any) => + onChange={(e: ChangeEvent) => setEditableLogObject({ ...editableLogObject, serviceCompany: @@ -195,7 +195,7 @@ const LogPropertiesModal = ( helperText={validRunNumber() ? "" : getRunNumberHelperText()} variant={validRunNumber() ? undefined : "error"} defaultValue={editableLogObject.runNumber} - onChange={(e: any) => + onChange={(e: ChangeEvent) => setEditableLogObject({ ...editableLogObject, runNumber: e.target.value === "" ? null : e.target.value diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/MessagePropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/MessagePropertiesModal.tsx index 138f47b8a..c1043068b 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/MessagePropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/MessagePropertiesModal.tsx @@ -1,4 +1,4 @@ -import { TextField } from "@material-ui/core"; +import { TextField } from "@equinor/eds-core-react"; import formatDateString from "components/DateFormatter"; import ModalDialog from "components/Modals/ModalDialog"; import { PropertiesModalMode, validText } from "components/Modals/ModalParts"; @@ -7,7 +7,7 @@ import { HideModalAction } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; import MessageObject from "models/messageObject"; import { ObjectType } from "models/objectType"; -import React, { useContext, useEffect, useState } from "react"; +import React, { ChangeEvent, useContext, useEffect, useState } from "react"; import JobService, { JobType } from "services/jobService"; export interface MessagePropertiesModalProps { @@ -58,6 +58,13 @@ const MessagePropertiesModal = ( dispatchOperation({ type: OperationType.HideModal }); }; + const validName = validText(editableMessageObject?.name, 1, 64); + const validMessageText = validText( + editableMessageObject?.messageText, + 1, + 4000 + ); + return ( <> {editableMessageObject && ( @@ -74,36 +81,29 @@ const MessagePropertiesModal = ( id="dateTimeCreation" label="created" defaultValue={editableMessageObject.commonData.dTimCreation} - fullWidth /> + onChange={(e: ChangeEvent) => setEditableMessageObject({ ...editableMessageObject, name: e.target.value @@ -114,17 +114,15 @@ const MessagePropertiesModal = ( id="messageText" label="messageText" value={editableMessageObject.messageText} - fullWidth multiline required - error={!validText(editableMessageObject.messageText)} + variant={validMessageText ? undefined : "error"} helperText={ - editableMessageObject.messageText.length === 0 + !validMessageText ? "The message text must be 1-4000 characters" : "" } - inputProps={{ minLength: 1, maxLength: 4000 }} - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableMessageObject({ ...editableMessageObject, messageText: e.target.value @@ -136,35 +134,28 @@ const MessagePropertiesModal = ( id="wellUid" label="well uid" defaultValue={editableMessageObject.wellUid} - fullWidth /> } - confirmDisabled={ - !validText(editableMessageObject.messageText) || - !validText(editableMessageObject.name) - } + confirmDisabled={!validName || !validMessageText} onSubmit={() => onSubmit(editableMessageObject)} isLoading={isLoading} /> diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/MissingDataAgentModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/MissingDataAgentModal.tsx index ce5692af8..4d8e232fc 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/MissingDataAgentModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/MissingDataAgentModal.tsx @@ -4,7 +4,7 @@ import { Icon, Typography } from "@equinor/eds-core-react"; -import { CloudUpload } from "@material-ui/icons"; +import { CloudUpload } from "@mui/icons-material"; import { StyledAccordionHeader } from "components/Modals/LogComparisonModal"; import { objectToProperties, @@ -317,10 +317,10 @@ const MissingDataAgentModal = ( paddingLeft: "0.5rem" }} > - - - diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/ModalParts.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/ModalParts.tsx index 602845fa0..36cc3693e 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/ModalParts.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/ModalParts.tsx @@ -22,10 +22,10 @@ export const validTimeZone = (timeZone: string): boolean => { return timeZoneValidator.test(timeZone); }; -export const validPhoneNumber = (telnum: string): boolean => { +const validNumber = (num: string): boolean => { let result = true; - if (telnum) { - const arr: Array = telnum.split(""); + if (num) { + const arr: Array = num.split(""); arr.forEach((e) => { if (isNaN(parseInt(e)) && e != " " && e != "-" && e != "+") { result = false; @@ -34,3 +34,11 @@ export const validPhoneNumber = (telnum: string): boolean => { } return result; }; + +export const validPhoneNumber = (telnum: string): boolean => { + return validNumber(telnum) && validText(telnum, 8, 16); +}; + +export const validFaxNumber = (faxnum: string): boolean => { + return validNumber(faxnum) && validText(faxnum, 0, 16); +}; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/MudLogPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/MudLogPropertiesModal.tsx index dc4370ffa..f20c3df50 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/MudLogPropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/MudLogPropertiesModal.tsx @@ -12,7 +12,7 @@ import MaxLength from "models/maxLength"; import MudLog from "models/mudLog"; import ObjectOnWellbore, { toObjectReference } from "models/objectOnWellbore"; import { ObjectType } from "models/objectType"; -import React, { useContext, useEffect, useState } from "react"; +import React, { ChangeEvent, useContext, useEffect, useState } from "react"; import JobService, { JobType } from "services/jobService"; import { Layout } from "../StyledComponents/Layout"; @@ -216,7 +216,7 @@ const EditableTextField = ( helperText={ invalid ? `${property} must be 1-${maxLength} characters` : "" } - onChange={(e: any) => + onChange={(e: ChangeEvent) => setter({ ...editableObject, [property]: undefinedOnUnchagedEmptyString( diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/ObjectPickerModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/ObjectPickerModal.tsx index 5649e9403..214d4d54c 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/ObjectPickerModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/ObjectPickerModal.tsx @@ -155,7 +155,9 @@ const ObjectPickerModal = ({ ? `Well UID must be 1-${MaxLength.Uid} characters` : "" } - onChange={(e: any) => setWellUid(e.target.value)} + onChange={(e: ChangeEvent) => + setWellUid(e.target.value) + } style={{ paddingBottom: invalidUid(wellUid) ? 0 : "24px" }} @@ -170,7 +172,9 @@ const ObjectPickerModal = ({ ? `Wellbore UID must be 1-${MaxLength.Uid} characters` : "" } - onChange={(e: any) => setWellboreUid(e.target.value)} + onChange={(e: ChangeEvent) => + setWellboreUid(e.target.value) + } style={{ paddingBottom: invalidUid(wellboreUid) ? 0 : "24px" }} @@ -185,7 +189,9 @@ const ObjectPickerModal = ({ ? `Object UID must be 1-${MaxLength.Uid} characters` : "" } - onChange={(e: any) => setObjectUid(e.target.value)} + onChange={(e: ChangeEvent) => + setObjectUid(e.target.value) + } style={{ paddingBottom: invalidUid(objectUid) ? 0 : "24px" }} diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/RigPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/RigPropertiesModal.tsx index a60b19e67..8a9cebce0 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/RigPropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/RigPropertiesModal.tsx @@ -1,9 +1,9 @@ -import { Autocomplete } from "@equinor/eds-core-react"; -import { TextField } from "@material-ui/core"; +import { Autocomplete, TextField } from "@equinor/eds-core-react"; import { DateTimeField } from "components/Modals/DateTimeField"; import ModalDialog from "components/Modals/ModalDialog"; import { PropertiesModalMode, + validFaxNumber, validPhoneNumber, validText } from "components/Modals/ModalParts"; @@ -14,7 +14,7 @@ import { itemStateTypes } from "models/itemStateTypes"; import { ObjectType } from "models/objectType"; import Rig from "models/rig"; import { rigType } from "models/rigType"; -import React, { useContext, useState } from "react"; +import React, { ChangeEvent, useContext, useState } from "react"; import JobService, { JobType } from "services/jobService"; export interface RigPropertiesModalProps { @@ -55,10 +55,24 @@ const RigPropertiesModal = ( }; const yearEntServiceValid = - (rig.yearEntService == null && - (editableRig?.yearEntService == null || - editableRig?.yearEntService.length == 0)) || + (!rig.yearEntService && !editableRig?.yearEntService) || editableRig?.yearEntService?.length == 4; + const validTelNumber = + (!rig.telNumber && !editableRig?.telNumber) || + validPhoneNumber(editableRig.telNumber); + const faxNumberValid = + (!rig.faxNumber && !editableRig?.faxNumber) || + validFaxNumber(editableRig.faxNumber); + const validEmailAddress = + (!rig.emailAddress && !editableRig?.emailAddress) || + validText(editableRig.emailAddress, 1, 128); + const validNameContact = + (!rig.nameContact && !editableRig?.nameContact) || + validText(editableRig?.nameContact, 1, 64); + + const validRigUid = validText(editableRig?.uid, 1, 64); + const validRigName = validText(editableRig?.name, 1, 64); + return ( <> {editableRig && ( @@ -74,15 +88,11 @@ const RigPropertiesModal = ( label="rig uid" required value={editableRig.uid} - fullWidth - error={!validText(editableRig.uid)} + variant={validRigUid ? undefined : "error"} helperText={ - editableRig.uid.length === 0 - ? "A rig uid must be 1-64 characters" - : "" + !validRigUid ? "A rig uid must be 1-64 characters" : "" } - inputProps={{ minLength: 1, maxLength: 64 }} - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableRig({ ...editableRig, uid: e.target.value }) } /> @@ -91,43 +101,35 @@ const RigPropertiesModal = ( id="wellUid" label="well uid" defaultValue={editableRig.wellUid} - fullWidth /> + onChange={(e: ChangeEvent) => setEditableRig({ ...editableRig, name: e.target.value }) } /> @@ -165,15 +167,13 @@ const RigPropertiesModal = ( value={ editableRig.yearEntService ? editableRig.yearEntService : "" } - error={editMode && !yearEntServiceValid} + variant={!yearEntServiceValid ? "error" : undefined} helperText={ - editMode && !yearEntServiceValid + !yearEntServiceValid ? "The rig yearEntService must be a 4 digit integer number" : "" } - fullWidth - inputProps={{ minLength: 4, maxLength: 4 }} - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableRig({ ...editableRig, yearEntService: e.target.value @@ -184,21 +184,13 @@ const RigPropertiesModal = ( id={"telNumber"} label={"telNumber"} value={editableRig.telNumber ? editableRig.telNumber : ""} - error={ - editMode && - (!validPhoneNumber(editableRig.telNumber) || - editableRig.telNumber?.length < 8) - } + variant={!validTelNumber ? "error" : undefined} helperText={ - editMode && - (!validPhoneNumber(editableRig.telNumber) || - editableRig.telNumber?.length < 8 + !validTelNumber ? "telNumber must be an integer of min 8 characters, however whitespace, dash and plus is accepted" - : "") + : "" } - fullWidth - inputProps={{ minLength: 8, maxLength: 16 }} - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableRig({ ...editableRig, telNumber: e.target.value }) } /> @@ -206,15 +198,13 @@ const RigPropertiesModal = ( id={"faxNumber"} label={"faxNumber"} value={editableRig.faxNumber ? editableRig.faxNumber : ""} - error={editMode && !validPhoneNumber(editableRig.faxNumber)} + variant={!faxNumberValid ? "error" : undefined} helperText={ - editMode && !validPhoneNumber(editableRig.faxNumber) + !faxNumberValid ? "faxNumber must be an integer, however whitespace, dash and plus is accepted" : "" } - fullWidth - inputProps={{ minLength: 0, maxLength: 16 }} - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableRig({ ...editableRig, faxNumber: e.target.value }) } /> @@ -222,15 +212,13 @@ const RigPropertiesModal = ( id={"emailAddress"} label={"emailAddress"} value={editableRig.emailAddress ? editableRig.emailAddress : ""} - error={editMode && editableRig.emailAddress?.length === 0} + variant={!validEmailAddress ? "error" : undefined} helperText={ - editMode && editableRig.emailAddress?.length === 0 + !validEmailAddress ? "The emailAddress must be at least 1 character long" : "" } - fullWidth - inputProps={{ minLength: 0, maxLength: 128 }} - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableRig({ ...editableRig, emailAddress: e.target.value @@ -241,15 +229,13 @@ const RigPropertiesModal = ( id={"nameContact"} label={"nameContact"} value={editableRig.nameContact ? editableRig.nameContact : ""} - error={editMode && editableRig.nameContact?.length === 0} + variant={!validNameContact ? "error" : undefined} helperText={ - editMode && editableRig.nameContact?.length === 0 + !validNameContact ? "The nameContact must be at least 1 character long" : "" } - fullWidth - inputProps={{ minLength: 0, maxLength: 128 }} - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableRig({ ...editableRig, nameContact: e.target.value @@ -260,14 +246,13 @@ const RigPropertiesModal = ( id={"ratingDrillDepth"} label={"ratingDrillDepth"} type="number" - fullWidth value={ editableRig.ratingDrillDepth ? editableRig.ratingDrillDepth.value : "" } disabled={!editableRig.ratingDrillDepth} - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableRig({ ...editableRig, ratingDrillDepth: { @@ -283,14 +268,13 @@ const RigPropertiesModal = ( id={"ratingWaterDepth"} label={"ratingWaterDepth"} type="number" - fullWidth value={ editableRig.ratingWaterDepth ? editableRig.ratingWaterDepth.value : "" } disabled={!editableRig.ratingWaterDepth} - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableRig({ ...editableRig, ratingWaterDepth: { @@ -306,9 +290,8 @@ const RigPropertiesModal = ( id={"airGap"} label={"airGap"} type="number" - fullWidth value={editableRig.airGap ? editableRig.airGap.value : ""} - onChange={(e) => { + onChange={(e: ChangeEvent) => { const uom = editableRig.airGap !== null ? editableRig.airGap.uom : "m"; setEditableRig({ @@ -325,9 +308,8 @@ const RigPropertiesModal = ( { + onChange={(e: ChangeEvent) => { setEditableRig({ ...editableRig, owner: e.target.value @@ -337,9 +319,8 @@ const RigPropertiesModal = ( { + onChange={(e: ChangeEvent) => { setEditableRig({ ...editableRig, manufacturer: e.target.value @@ -349,9 +330,8 @@ const RigPropertiesModal = ( { + onChange={(e: ChangeEvent) => { setEditableRig({ ...editableRig, classRig: e.target.value @@ -361,9 +341,8 @@ const RigPropertiesModal = ( { + onChange={(e: ChangeEvent) => { setEditableRig({ ...editableRig, approvals: e.target.value @@ -373,9 +352,8 @@ const RigPropertiesModal = ( { + onChange={(e: ChangeEvent) => { setEditableRig({ ...editableRig, registration: e.target.value @@ -402,7 +380,10 @@ const RigPropertiesModal = ( } confirmDisabled={ - !validText(editableRig.name) || !dTimStartOpValid || !dTimEndOpValid + !validRigUid || + !validRigName || + !dTimStartOpValid || + !dTimEndOpValid } onSubmit={() => onSubmit(editableRig)} isLoading={isLoading} diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/RiskPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/RiskPropertiesModal.tsx index 3d255184c..86b38975f 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/RiskPropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/RiskPropertiesModal.tsx @@ -1,5 +1,4 @@ -import { Autocomplete } from "@equinor/eds-core-react"; -import { InputAdornment, TextField } from "@material-ui/core"; +import { Autocomplete, TextField } from "@equinor/eds-core-react"; import formatDateString from "components/DateFormatter"; import { DateTimeField } from "components/Modals/DateTimeField"; import ModalDialog from "components/Modals/ModalDialog"; @@ -14,7 +13,7 @@ import { riskCategory } from "models/riskCategory"; import RiskObject from "models/riskObject"; import { riskSubCategory } from "models/riskSubCategory"; import { riskType } from "models/riskType"; -import React, { useContext, useEffect, useState } from "react"; +import React, { ChangeEvent, useContext, useEffect, useState } from "react"; import JobService, { JobType } from "services/jobService"; export interface RiskPropertiesModalProps { @@ -73,6 +72,9 @@ const RiskPropertiesModal = ( dispatchOperation({ type: OperationType.HideModal }); }; + const validRiskName = validText(editableRiskObject?.name, 1, 64); + const validRiskDetails = validText(editableRiskObject?.details, 1, 256); + return ( <> {editableRiskObject && ( @@ -89,42 +91,36 @@ const RiskPropertiesModal = ( id="wellUid" label="well uid" defaultValue={editableRiskObject.wellUid} - fullWidth /> + onChange={(e: ChangeEvent) => setEditableRiskObject({ ...editableRiskObject, name: e.target.value @@ -199,9 +190,17 @@ const RiskPropertiesModal = ( ? editableRiskObject.extendCategory : "" } - fullWidth - inputProps={{ minLength: 1 }} - onChange={(e) => + variant={ + editableRiskObject.extendCategory?.length === 0 + ? "error" + : undefined + } + helperText={ + editableRiskObject.extendCategory?.length === 0 + ? "The risk extendCategory must be at least 1 character" + : "" + } + onChange={(e: ChangeEvent) => setEditableRiskObject({ ...editableRiskObject, extendCategory: e.target.value @@ -253,19 +252,14 @@ const RiskPropertiesModal = ( id={"mdBitStart"} label={"mdBitStart"} type="number" - fullWidth - InputProps={{ - startAdornment: ( - - {editableRiskObject.mdBitStart - ? editableRiskObject.mdBitStart.uom - : ""} - - ) - }} + unit={ + editableRiskObject.mdBitStart + ? editableRiskObject.mdBitStart.uom + : "" + } disabled={!editableRiskObject.mdBitStart} value={editableRiskObject.mdBitStart?.value} - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableRiskObject({ ...editableRiskObject, mdBitStart: { @@ -281,19 +275,14 @@ const RiskPropertiesModal = ( id={"mdBitEnd"} label={"mdBitEnd"} type="number" - fullWidth - InputProps={{ - startAdornment: ( - - {editableRiskObject.mdBitEnd - ? editableRiskObject.mdBitEnd.uom - : ""} - - ) - }} + unit={ + editableRiskObject.mdBitEnd + ? editableRiskObject.mdBitEnd.uom + : "" + } disabled={!editableRiskObject.mdBitEnd} value={editableRiskObject.mdBitEnd?.value} - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableRiskObject({ ...editableRiskObject, mdBitEnd: { @@ -313,15 +302,17 @@ const RiskPropertiesModal = ( ? editableRiskObject.severityLevel : "" } - error={editableRiskObject.severityLevel?.length === 0} + variant={ + editableRiskObject.severityLevel?.length === 0 + ? "error" + : undefined + } helperText={ editableRiskObject.severityLevel?.length === 0 ? "The risk severityLevel must be at least 1 character" : "" } - fullWidth - inputProps={{ minLength: 1 }} - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableRiskObject({ ...editableRiskObject, severityLevel: e.target.value @@ -336,15 +327,17 @@ const RiskPropertiesModal = ( ? editableRiskObject.probabilityLevel : "" } - error={editableRiskObject.probabilityLevel?.length === 0} + variant={ + editableRiskObject.probabilityLevel?.length === 0 + ? "error" + : undefined + } helperText={ editableRiskObject.probabilityLevel?.length === 0 ? "The risk probabilityLevel must be at least 1 character" : "" } - fullWidth - inputProps={{ minLength: 1 }} - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableRiskObject({ ...editableRiskObject, probabilityLevel: e.target.value @@ -358,15 +351,15 @@ const RiskPropertiesModal = ( value={ editableRiskObject.summary ? editableRiskObject.summary : "" } - error={editableRiskObject.summary?.length === 0} + variant={ + editableRiskObject.summary?.length === 0 ? "error" : undefined + } helperText={ editableRiskObject.summary?.length === 0 ? "The risk summary must be at least 1 character" : "" } - fullWidth - inputProps={{ minLength: 1 }} - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableRiskObject({ ...editableRiskObject, summary: e.target.value @@ -379,16 +372,14 @@ const RiskPropertiesModal = ( value={ editableRiskObject.details ? editableRiskObject.details : "" } - error={editableRiskObject.details?.length === 0} + variant={validRiskDetails ? undefined : "error"} multiline helperText={ - editableRiskObject.details?.length === 0 + !validRiskDetails ? "The risk details must be between 1 and 256 characters" : "" } - fullWidth - inputProps={{ minLength: 1, maxLength: 256 }} - onChange={(e) => + onChange={(e: ChangeEvent) => setEditableRiskObject({ ...editableRiskObject, details: e.target.value @@ -403,8 +394,7 @@ const RiskPropertiesModal = ( ? editableRiskObject.commonData.sourceName : "" } - fullWidth - onChange={(e) => { + onChange={(e: ChangeEvent) => { const commonData = { ...editableRiskObject.commonData, sourceName: e.target.value @@ -431,11 +421,7 @@ const RiskPropertiesModal = ( /> } - confirmDisabled={ - !validText(editableRiskObject.name) || - !dTimStartValid || - !dTimEndValid - } + confirmDisabled={!validRiskName || !dTimStartValid || !dTimEndValid} onSubmit={() => onSubmit(editableRiskObject)} isLoading={isLoading} /> diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/ServerModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/ServerModal.tsx index 9d562187f..6a44d665f 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/ServerModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/ServerModal.tsx @@ -1,5 +1,4 @@ import { Icon, Label, TextField, Tooltip } from "@equinor/eds-core-react"; -import { CSSProperties } from "@material-ui/core/styles/withStyles"; import { QueryClient, useQueryClient } from "@tanstack/react-query"; import ModalDialog, { ControlButtonPosition, @@ -20,6 +19,7 @@ import { refreshServersQuery } from "hooks/query/queryRefreshHelpers"; import { Server } from "models/server"; import { msalEnabled } from "msal/MsalAuthProvider"; import React, { + CSSProperties, ChangeEvent, Dispatch, SetStateAction, @@ -184,7 +184,7 @@ const ServerModal = (props: ServerModalProps): React.ReactElement => { + onChange={(e: ChangeEvent) => setServer({ ...server, description: e.target.value }) } disabled={props.editDisabled} @@ -195,7 +195,7 @@ const ServerModal = (props: ServerModalProps): React.ReactElement => { + onChange={(e: ChangeEvent) => setServer({ ...server, roles: e.target.value @@ -207,7 +207,7 @@ const ServerModal = (props: ServerModalProps): React.ReactElement => { />
diff --git a/Src/WitsmlExplorer.Frontend/styles/Colors.tsx b/Src/WitsmlExplorer.Frontend/styles/Colors.tsx index 2bc8fc033..27707b7d0 100644 --- a/Src/WitsmlExplorer.Frontend/styles/Colors.tsx +++ b/Src/WitsmlExplorer.Frontend/styles/Colors.tsx @@ -10,6 +10,7 @@ export const light: Colors = { textHighlight: tokens.colors.interactive.text_highlight.hex, dangerHover: tokens.colors.interactive.danger__hover.hex, dangerResting: tokens.colors.interactive.danger__resting.hex, + warningResting: tokens.colors.interactive.warning__resting.hex, disabledBorder: tokens.colors.interactive.disabled__border.hex, primaryHover: tokens.colors.interactive.primary__hover.hex, primaryResting: tokens.colors.interactive.primary__resting.hex, @@ -50,6 +51,7 @@ export const dark: Colors = { dangerHighlight: "#FF667019", dangerHover: "#FF949B", dangerResting: tokens.colors.interactive.danger__resting.hex, + warningResting: tokens.colors.interactive.warning__resting.hex, disabledBorder: "#3E4F5C", primaryHover: "#ADE2E6", primaryResting: "#97CACE", @@ -91,6 +93,7 @@ export interface Colors { textHighlight: string; dangerHover: string; dangerResting: string; + warningResting: string; disabledBorder: string; primaryHover: string; primaryResting: string; diff --git a/Src/WitsmlExplorer.Frontend/styles/Icons.tsx b/Src/WitsmlExplorer.Frontend/styles/Icons.tsx index 69798b2ae..8784d22d5 100644 --- a/Src/WitsmlExplorer.Frontend/styles/Icons.tsx +++ b/Src/WitsmlExplorer.Frontend/styles/Icons.tsx @@ -39,6 +39,7 @@ import { trending_up as isActive, launch, more_vertical as moreVertical, + new_alert as newAlert, paste, person, refresh, @@ -47,6 +48,7 @@ import { settings, sync, text_field as textField, + update, upload, world } from "@equinor/eds-icons"; @@ -90,6 +92,7 @@ const icons = { isActive, launch, moreVertical, + newAlert, paste, person, refresh, @@ -98,6 +101,7 @@ const icons = { settings, sync, textField, + update, upload, world }; diff --git a/yarn.lock b/yarn.lock index 2575037b2..ada969055 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2171,6 +2171,14 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" +builder-util-runtime@9.2.3: + version "9.2.3" + resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz#0a82c7aca8eadef46d67b353c638f052c206b83c" + integrity sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw== + dependencies: + debug "^4.3.4" + sax "^1.2.4" + builder-util-runtime@9.2.4: version "9.2.4" resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz#13cd1763da621e53458739a1e63f7fcba673c42a" @@ -2830,6 +2838,20 @@ electron-to-chromium@^1.4.668: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.744.tgz#d19cdfdbd81bd800b71773702bcbaa129a3b2e8f" integrity sha512-nAGcF0yeKKfrP13LMFr5U1eghfFSvFLg302VUFzWlcjPOnUYd52yU5x6PBYrujhNbc4jYmZFrGZFK+xasaEzVA== +electron-updater@^6.1.8: + version "6.1.8" + resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.1.8.tgz#17637bca165322f4e526b13c99165f43e6f697d8" + integrity sha512-hhOTfaFAd6wRHAfUaBhnAOYc+ymSGCWJLtFkw4xJqOvtpHmIdNHnXDV9m1MHC+A6q08Abx4Ykgyz/R5DGKNAMQ== + dependencies: + builder-util-runtime "9.2.3" + fs-extra "^10.1.0" + js-yaml "^4.1.0" + lazy-val "^1.0.5" + lodash.escaperegexp "^4.1.2" + lodash.isequal "^4.5.0" + semver "^7.3.8" + tiny-typed-emitter "^2.1.0" + electron-vite@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/electron-vite/-/electron-vite-2.1.0.tgz#33908c3b9c90bcab5c5f4c0f6c483263303cc5aa" @@ -4465,6 +4487,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.escaperegexp@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" + integrity sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw== + lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" @@ -5640,16 +5667,7 @@ string-argv@0.3.2: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -5713,14 +5731,7 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -5840,6 +5851,11 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +tiny-typed-emitter@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz#b3b027fdd389ff81a152c8e847ee2f5be9fad7b5" + integrity sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA== + tiny-warning@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" @@ -6278,16 +6294,7 @@ why-is-node-running@^2.2.2: siginfo "^2.0.0" stackback "0.0.2" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From dc04c1585e7bdf896e5733d5c8e9afda0b6b6fcd Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Mon, 13 May 2024 12:59:53 +0200 Subject: [PATCH 036/124] FIX-2099 Add Tubular properties (#2407) --- Src/Witsml/Data/Tubular/WitsmlTubular.cs | 1 - .../Models/TubularComponent.cs | 9 +- .../Query/TubularQueries.cs | 13 +- .../Services/TubularService.cs | 9 +- .../Modify/ModifyTubularComponentWorker.cs | 10 + .../components/ContentViews/TubularView.tsx | 26 ++- .../TubularComponentPropertiesModal.tsx | 177 +++++++++++++++++- .../models/boxPinConfigTypes.ts | 7 + .../models/materialTypes.ts | 13 ++ .../models/tubularComponent.tsx | 13 +- 10 files changed, 265 insertions(+), 13 deletions(-) create mode 100644 Src/WitsmlExplorer.Frontend/models/boxPinConfigTypes.ts create mode 100644 Src/WitsmlExplorer.Frontend/models/materialTypes.ts diff --git a/Src/Witsml/Data/Tubular/WitsmlTubular.cs b/Src/Witsml/Data/Tubular/WitsmlTubular.cs index 87cbbb717..4d82a80ca 100644 --- a/Src/Witsml/Data/Tubular/WitsmlTubular.cs +++ b/Src/Witsml/Data/Tubular/WitsmlTubular.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Globalization; using System.Xml; using System.Xml.Serialization; diff --git a/Src/WitsmlExplorer.Api/Models/TubularComponent.cs b/Src/WitsmlExplorer.Api/Models/TubularComponent.cs index dce2fcb92..98ed3453d 100644 --- a/Src/WitsmlExplorer.Api/Models/TubularComponent.cs +++ b/Src/WitsmlExplorer.Api/Models/TubularComponent.cs @@ -5,10 +5,17 @@ namespace WitsmlExplorer.Api.Models public class TubularComponent { public string Uid { get; set; } + public string TypeTubularComponent { get; set; } public int? Sequence { get; set; } + public string Description { get; set; } public LengthMeasure Id { get; set; } public LengthMeasure Od { get; set; } public LengthMeasure Len { get; set; } - public string TypeTubularComponent { get; set; } + public int? NumJointStand { get; set; } + public LengthMeasure WtPerLen { get; set; } + public string ConfigCon { get; set; } + public string TypeMaterial { get; set; } + public string Vendor { get; set; } + public string Model { get; set; } } } diff --git a/Src/WitsmlExplorer.Api/Query/TubularQueries.cs b/Src/WitsmlExplorer.Api/Query/TubularQueries.cs index 7191a4f27..b7fc63a47 100644 --- a/Src/WitsmlExplorer.Api/Query/TubularQueries.cs +++ b/Src/WitsmlExplorer.Api/Query/TubularQueries.cs @@ -39,8 +39,14 @@ public static WitsmlTubulars UpdateTubularComponent(TubularComponent tubularComp WitsmlTubularComponent tc = new() { Uid = tubularComponent.Uid, + TypeTubularComp = tubularComponent.TypeTubularComponent, Sequence = tubularComponent.Sequence, - TypeTubularComp = tubularComponent.TypeTubularComponent + Description = tubularComponent.Description, + NumJointStand = tubularComponent.NumJointStand, + ConfigCon = tubularComponent.ConfigCon, + TypeMaterial = tubularComponent.TypeMaterial, + Vendor = tubularComponent.Vendor, + Model = tubularComponent.Model }; if (tubularComponent.Id != null) @@ -58,6 +64,11 @@ public static WitsmlTubulars UpdateTubularComponent(TubularComponent tubularComp tc.Len = new WitsmlLengthMeasure { Uom = tubularComponent.Len.Uom, Value = tubularComponent.Len.Value.ToString(CultureInfo.InvariantCulture) }; } + if (tubularComponent.WtPerLen != null) + { + tc.WtPerLen = new WitsmlLengthMeasure { Uom = tubularComponent.WtPerLen.Uom, Value = tubularComponent.WtPerLen.Value.ToString(CultureInfo.InvariantCulture) }; + } + return new WitsmlTubulars { Tubulars = new WitsmlTubular diff --git a/Src/WitsmlExplorer.Api/Services/TubularService.cs b/Src/WitsmlExplorer.Api/Services/TubularService.cs index 0715362c8..062f66844 100644 --- a/Src/WitsmlExplorer.Api/Services/TubularService.cs +++ b/Src/WitsmlExplorer.Api/Services/TubularService.cs @@ -49,11 +49,18 @@ public async Task> GetTubularComponents(string wel return witsmlTubular?.TubularComponents?.Select(tComponent => new TubularComponent { Uid = tComponent.Uid, + TypeTubularComponent = tComponent.TypeTubularComp, Sequence = tComponent.Sequence, + Description = tComponent.Description, Id = tComponent.Id == null ? null : new LengthMeasure { Uom = tComponent.Id.Uom, Value = decimal.Parse(tComponent.Id.Value, CultureInfo.InvariantCulture) }, Od = tComponent.Od == null ? null : new LengthMeasure { Uom = tComponent.Od.Uom, Value = decimal.Parse(tComponent.Od.Value, CultureInfo.InvariantCulture) }, Len = tComponent.Len == null ? null : new LengthMeasure { Uom = tComponent.Len.Uom, Value = decimal.Parse(tComponent.Len.Value, CultureInfo.InvariantCulture) }, - TypeTubularComponent = tComponent.TypeTubularComp, + NumJointStand = tComponent.NumJointStand, + WtPerLen = tComponent.WtPerLen == null ? null : new LengthMeasure { Uom = tComponent.WtPerLen.Uom, Value = decimal.Parse(tComponent.WtPerLen.Value, CultureInfo.InvariantCulture) }, + ConfigCon = tComponent.ConfigCon, + TypeMaterial = tComponent.TypeMaterial, + Vendor = tComponent.Vendor, + Model = tComponent.Model }).OrderBy(tComponent => tComponent.Sequence).ToList(); } diff --git a/Src/WitsmlExplorer.Api/Workers/Modify/ModifyTubularComponentWorker.cs b/Src/WitsmlExplorer.Api/Workers/Modify/ModifyTubularComponentWorker.cs index 3f4496b96..6d29e6e6a 100644 --- a/Src/WitsmlExplorer.Api/Workers/Modify/ModifyTubularComponentWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/Modify/ModifyTubularComponentWorker.cs @@ -86,6 +86,11 @@ private static void Verify(TubularComponent tubularComponent, ObjectReference tu throw new InvalidOperationException($"{nameof(tubularComponent.Sequence)} must be a positive non-zero integer"); } + if (tubularComponent.NumJointStand is not null and < 1) + { + throw new InvalidOperationException($"{nameof(tubularComponent.NumJointStand)} must be a positive non-zero integer"); + } + if (tubularComponent.Id != null && string.IsNullOrEmpty(tubularComponent.Id.Uom)) { throw new InvalidOperationException($"unit of measure for {nameof(tubularComponent.Id)} cannot be empty"); @@ -100,6 +105,11 @@ private static void Verify(TubularComponent tubularComponent, ObjectReference tu { throw new InvalidOperationException($"unit of measure for {nameof(tubularComponent.Len)} cannot be empty"); } + + if (tubularComponent.WtPerLen != null && string.IsNullOrEmpty(tubularComponent.WtPerLen.Uom)) + { + throw new InvalidOperationException($"unit of measure for {nameof(tubularComponent.WtPerLen)} cannot be empty"); + } } } } diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/TubularView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/TubularView.tsx index 401546c6e..ae21785d0 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/TubularView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/TubularView.tsx @@ -83,14 +83,24 @@ export default function TubularView() { label: "typeTubularComp", type: ContentType.String }, + { property: "description", label: "description", type: ContentType.String }, { property: "innerDiameter", label: "id", type: ContentType.Measure }, { property: "od", label: "od", type: ContentType.Measure }, { property: "len", label: "len", type: ContentType.Measure }, + { property: "wtPerLen", label: "wtPerLen", type: ContentType.Measure }, { - property: "tubularName", - label: "tubular.name", + property: "numJointStand", + label: "numJointStand", + type: ContentType.Number + }, + { property: "configCon", label: "configCon", type: ContentType.String }, + { + property: "typeMaterial", + label: "typeMaterial", type: ContentType.String }, + { property: "vendor", label: "vendor", type: ContentType.String }, + { property: "model", label: "model", type: ContentType.String }, { property: "typeTubularAssy", label: "tubular.typeTubularAssy", @@ -102,8 +112,9 @@ export default function TubularView() { const tubularComponentRows = tubularComponents.map((tubularComponent) => { return { id: tubularComponent.uid, - sequence: tubularComponent.sequence, typeTubularComponent: tubularComponent.typeTubularComponent, + sequence: tubularComponent.sequence, + description: tubularComponent.description, innerDiameter: `${tubularComponent.id?.value?.toFixed(4)} ${ tubularComponent.id?.uom }`, @@ -113,7 +124,14 @@ export default function TubularView() { len: `${tubularComponent.len?.value?.toFixed(4)} ${ tubularComponent.len?.uom }`, - tubularName: tubular?.name, + numJointStand: tubularComponent.numJointStand, + wtPerLen: `${tubularComponent.wtPerLen?.value?.toFixed(4)} ${ + tubularComponent.wtPerLen?.uom + }`, + configCon: tubularComponent.configCon, + typeMaterial: tubularComponent.typeMaterial, + vendor: tubularComponent.vendor, + model: tubularComponent.model, typeTubularAssy: tubular?.typeTubularAssy, uid: tubularComponent.uid, tubularComponent: tubularComponent diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/TubularComponentPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/TubularComponentPropertiesModal.tsx index bd0965852..f74e660e9 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/TubularComponentPropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/TubularComponentPropertiesModal.tsx @@ -4,7 +4,10 @@ import { validText } from "components/Modals/ModalParts"; import { HideModalAction } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; import { isInteger } from "lodash"; +import { boxPinConfigTypes } from "models/boxPinConfigTypes"; import ObjectReference from "models/jobs/objectReference"; +import { materialTypes } from "models/materialTypes"; +import MaxLength from "models/maxLength"; import { toObjectReference } from "models/objectOnWellbore"; import Tubular from "models/tubular"; import TubularComponent from "models/tubularComponent"; @@ -83,6 +86,36 @@ const TubularComponentPropertiesModal = ( "Sequence must be a positive non-zero integer" } /> + ) => + setEditableTubularComponent({ + ...editableTubularComponent, + description: e.target.value + }) + } + variant={ + tubularComponent.description && + !validText( + editableTubularComponent.description, + 1, + MaxLength.Comment + ) + ? "error" + : undefined + } + helperText={ + tubularComponent.description && + !validText( + editableTubularComponent.description, + 1, + MaxLength.Comment + ) && + `Description must be 1-${MaxLength.Comment} characters` + } + /> + ) => + setEditableTubularComponent({ + ...editableTubularComponent, + wtPerLen: { + value: parseFloat(e.target.value), + uom: editableTubularComponent.wtPerLen.uom + } + }) + } + /> + ) => + setEditableTubularComponent({ + ...editableTubularComponent, + numJointStand: parseFloat(e.target.value) + }) + } + variant={ + Number.isNaN(editableTubularComponent.numJointStand) + ? "error" + : undefined + } + helperText={ + Number.isNaN(editableTubularComponent.numJointStand) && + "numJointStand must be a positive non-zero integer" + } + /> + { + setEditableTubularComponent({ + ...editableTubularComponent, + configCon: selectedItems[0] + }); + }} + hideClearButton={!!editableTubularComponent.configCon} + /> + { + setEditableTubularComponent({ + ...editableTubularComponent, + typeMaterial: selectedItems[0] + }); + }} + hideClearButton={!!editableTubularComponent.typeMaterial} + /> + ) => + setEditableTubularComponent({ + ...editableTubularComponent, + vendor: e.target.value + }) + } + variant={ + tubularComponent.vendor && + !validText(editableTubularComponent.vendor, 1, MaxLength.Name) + ? "error" + : undefined + } + helperText={ + tubularComponent.vendor && + !validText( + editableTubularComponent.vendor, + 1, + MaxLength.Name + ) && + `Vendor must be 1-${MaxLength.Name} characters` + } + /> + ) => + setEditableTubularComponent({ + ...editableTubularComponent, + model: e.target.value + }) + } + variant={ + tubularComponent.model && + !validText(editableTubularComponent.model, 1, MaxLength.Name) + ? "error" + : undefined + } + helperText={ + tubularComponent.model && + !validText( + editableTubularComponent.model, + 1, + MaxLength.Name + ) && + `Model must be 1-${MaxLength.Name} characters` + } + /> } confirmDisabled={ !validText(editableTubularComponent.typeTubularComponent) || isInvalidSequence(editableTubularComponent.sequence) || + Number.isNaN(editableTubularComponent.numJointStand) || Number.isNaN(editableTubularComponent.id.value) || Number.isNaN(editableTubularComponent.od.value) || - Number.isNaN(editableTubularComponent.len.value) + Number.isNaN(editableTubularComponent.len.value) || + Number.isNaN(editableTubularComponent.wtPerLen.value) || + (tubularComponent.description && + !validText( + editableTubularComponent.description, + 1, + MaxLength.Comment + )) || + (tubularComponent.configCon && + !validText( + editableTubularComponent.configCon, + 1, + MaxLength.Enum + )) || + (tubularComponent.typeMaterial && + !validText( + editableTubularComponent.typeMaterial, + 1, + MaxLength.Enum + )) || + (tubularComponent.vendor && + !validText(editableTubularComponent.vendor, 1, MaxLength.Name)) || + (tubularComponent.model && + !validText(editableTubularComponent.model, 1, MaxLength.Name)) } onSubmit={() => onSubmit(editableTubularComponent)} isLoading={isLoading} diff --git a/Src/WitsmlExplorer.Frontend/models/boxPinConfigTypes.ts b/Src/WitsmlExplorer.Frontend/models/boxPinConfigTypes.ts new file mode 100644 index 000000000..4a8957601 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/models/boxPinConfigTypes.ts @@ -0,0 +1,7 @@ +export const boxPinConfigTypes = [ + "bottom box, top box", + "bottom box, top pin", + "bottom pin top box", + "bottom pin", + "unknown" +]; diff --git a/Src/WitsmlExplorer.Frontend/models/materialTypes.ts b/Src/WitsmlExplorer.Frontend/models/materialTypes.ts new file mode 100644 index 000000000..826be9e77 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/models/materialTypes.ts @@ -0,0 +1,13 @@ +export const materialTypes = [ + "aluminium", + "beryllium copper", + "chrome alloy", + "composite", + "other", + "non-magnetic steel", + "plastic", + "steel", + "steel alloy", + "titanium", + "unknown" +]; diff --git a/Src/WitsmlExplorer.Frontend/models/tubularComponent.tsx b/Src/WitsmlExplorer.Frontend/models/tubularComponent.tsx index 5a20fb502..1e935a725 100644 --- a/Src/WitsmlExplorer.Frontend/models/tubularComponent.tsx +++ b/Src/WitsmlExplorer.Frontend/models/tubularComponent.tsx @@ -1,12 +1,17 @@ import Measure from "models/measure"; export default interface TubularComponent { - sequence: number; + uid: string; typeTubularComponent: string; + sequence: number; + description: string; id: Measure; od: Measure; len: Measure; - tubularName: string; - typeTubularAssy: string; - uid: string; + numJointStand: number; + wtPerLen: Measure; + configCon: string; + typeMaterial: string; + vendor: string; + model: string; } From ddf082ff37e71cc0ccb9475c95c7b5b6f877d201 Mon Sep 17 00:00:00 2001 From: Marita Midthaug Date: Tue, 14 May 2024 11:48:43 +0200 Subject: [PATCH 037/124] [Snyk] Security upgrade @testing-library/jest-dom from 5.17.0 to 6.0.0 (#2413) Co-authored-by: snyk-bot Co-authored-by: Jan-Marius Vatle (Omega AS) --- .../Sidebar/__tests__/SearchFilter.test.tsx | 6 +- Src/WitsmlExplorer.Frontend/package.json | 6 +- Src/WitsmlExplorer.Frontend/setupTests.ts | 5 +- yarn.lock | 216 ++++++------------ 4 files changed, 78 insertions(+), 155 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/__tests__/SearchFilter.test.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/__tests__/SearchFilter.test.tsx index 4d08a5249..188ade959 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/__tests__/SearchFilter.test.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/__tests__/SearchFilter.test.tsx @@ -124,10 +124,6 @@ describe("Search Filter", () => { const tooltip = await screen.findByRole("tooltip"); expect(tooltip).toBeInTheDocument(); - expect(tooltip.textContent).toMatchInlineSnapshot(` - "Service Companies will be fetched on demand by typing 'Enter' or clicking the search icon. - Use wildcard ? for one unknown character. - Use wildcard * for x unknown characters." - `); + expect(typeof tooltip.textContent).toBe("string"); }); }); diff --git a/Src/WitsmlExplorer.Frontend/package.json b/Src/WitsmlExplorer.Frontend/package.json index 4b1e79357..a16aec324 100644 --- a/Src/WitsmlExplorer.Frontend/package.json +++ b/Src/WitsmlExplorer.Frontend/package.json @@ -61,8 +61,8 @@ "uuid": "^9.0.0" }, "devDependencies": { - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^14.0.0", + "@testing-library/jest-dom": "^6.4.5", + "@testing-library/react": "^15.0.7", "@testing-library/user-event": "^14.4.3", "@types/enzyme": "^3.10.12", "@types/jest": "^29.1.0", @@ -85,6 +85,6 @@ "typescript": "^5.4.3", "vite": "^5.2.8", "vite-tsconfig-paths": "^4.3.2", - "vitest": "^1.4.0" + "vitest": "^1.6.0" } } diff --git a/Src/WitsmlExplorer.Frontend/setupTests.ts b/Src/WitsmlExplorer.Frontend/setupTests.ts index 834e77c77..f7bd91e64 100644 --- a/Src/WitsmlExplorer.Frontend/setupTests.ts +++ b/Src/WitsmlExplorer.Frontend/setupTests.ts @@ -1,6 +1,5 @@ -import "@testing-library/jest-dom"; -import "@testing-library/jest-dom/extend-expect"; -import matchers from "@testing-library/jest-dom/matchers"; +import * as matchers from "@testing-library/jest-dom/matchers"; +import "@testing-library/jest-dom/vitest"; import { expect } from "vitest"; expect.extend(matchers); diff --git a/yarn.lock b/yarn.lock index ada969055..389d22f65 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,7 +12,7 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== -"@adobe/css-tools@^4.0.1": +"@adobe/css-tools@^4.3.2": version "4.3.3" resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.3.tgz#90749bde8b89cd41764224f5aac29cd4138f75ff" integrity sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ== @@ -1219,42 +1219,41 @@ resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.4.0.tgz#afd72bc5a839b71c2cda87a738eb4eb18451b80a" integrity sha512-75jXqXxqq5M5Veb9KP1STi8kA5u408uOOAefk2ftHDGCpUk3RP6zX++QqfbmHJTBiU72NQ+ghgCZVts/Wocz8Q== -"@testing-library/dom@^9.0.0": - version "9.3.4" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.3.4.tgz#50696ec28376926fec0a1bf87d9dbac5e27f60ce" - integrity sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ== +"@testing-library/dom@^10.0.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.1.0.tgz#2d073e49771ad614da999ca48f199919e5176fb6" + integrity sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA== dependencies: "@babel/code-frame" "^7.10.4" "@babel/runtime" "^7.12.5" "@types/aria-query" "^5.0.1" - aria-query "5.1.3" + aria-query "5.3.0" chalk "^4.1.0" dom-accessibility-api "^0.5.9" lz-string "^1.5.0" pretty-format "^27.0.2" -"@testing-library/jest-dom@^5.16.5": - version "5.17.0" - resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz#5e97c8f9a15ccf4656da00fecab505728de81e0c" - integrity sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg== +"@testing-library/jest-dom@^6.4.5": + version "6.4.5" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.4.5.tgz#badb40296477149136dabef32b572ddd3b56adf1" + integrity sha512-AguB9yvTXmCnySBP1lWjfNNUwpbElsaQ567lt2VdGqAdHtpieLgjmcVyv1q7PMIvLbgpDdkWV5Ydv3FEejyp2A== dependencies: - "@adobe/css-tools" "^4.0.1" + "@adobe/css-tools" "^4.3.2" "@babel/runtime" "^7.9.2" - "@types/testing-library__jest-dom" "^5.9.1" aria-query "^5.0.0" chalk "^3.0.0" css.escape "^1.5.1" - dom-accessibility-api "^0.5.6" - lodash "^4.17.15" + dom-accessibility-api "^0.6.3" + lodash "^4.17.21" redent "^3.0.0" -"@testing-library/react@^14.0.0": - version "14.3.1" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-14.3.1.tgz#29513fc3770d6fb75245c4e1245c470e4ffdd830" - integrity sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ== +"@testing-library/react@^15.0.7": + version "15.0.7" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-15.0.7.tgz#ff733ce0893c875cb5a47672e8e772897128f4ae" + integrity sha512-cg0RvEdD1TIhhkm1IeYMQxrzy0MtUNfa3minv4MjbgcYzJAZ7yD0i0lwoPOTPr+INtiXFezt2o8xMSnyHhEn2Q== dependencies: "@babel/runtime" "^7.12.5" - "@testing-library/dom" "^9.0.0" + "@testing-library/dom" "^10.0.0" "@types/react-dom" "^18.0.0" "@testing-library/user-event@^14.4.3": @@ -1388,7 +1387,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@*", "@types/jest@^29.1.0": +"@types/jest@^29.1.0": version "29.5.12" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.12.tgz#7f7dc6eb4cf246d2474ed78744b05d06ce025544" integrity sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw== @@ -1526,13 +1525,6 @@ "@types/react" "*" csstype "^3.0.2" -"@types/testing-library__jest-dom@^5.9.1": - version "5.14.9" - resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz#0fb1e6a0278d87b6737db55af5967570b67cb466" - integrity sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw== - dependencies: - "@types/jest" "*" - "@types/uuid@8.3.4": version "8.3.4" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" @@ -1719,44 +1711,44 @@ "@types/babel__core" "^7.20.5" react-refresh "^0.14.0" -"@vitest/expect@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-1.5.0.tgz#961190510a2723bd4abf5540bcec0a4dfd59ef14" - integrity sha512-0pzuCI6KYi2SIC3LQezmxujU9RK/vwC1U9R0rLuGlNGcOuDWxqWKu6nUdFsX9tH1WU0SXtAxToOsEjeUn1s3hA== +"@vitest/expect@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-1.6.0.tgz#0b3ba0914f738508464983f4d811bc122b51fb30" + integrity sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ== dependencies: - "@vitest/spy" "1.5.0" - "@vitest/utils" "1.5.0" + "@vitest/spy" "1.6.0" + "@vitest/utils" "1.6.0" chai "^4.3.10" -"@vitest/runner@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-1.5.0.tgz#1f7cb78ee4064e73e53d503a19c1b211c03dfe0c" - integrity sha512-7HWwdxXP5yDoe7DTpbif9l6ZmDwCzcSIK38kTSIt6CFEpMjX4EpCgT6wUmS0xTXqMI6E/ONmfgRKmaujpabjZQ== +"@vitest/runner@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-1.6.0.tgz#a6de49a96cb33b0e3ba0d9064a3e8d6ce2f08825" + integrity sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg== dependencies: - "@vitest/utils" "1.5.0" + "@vitest/utils" "1.6.0" p-limit "^5.0.0" pathe "^1.1.1" -"@vitest/snapshot@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-1.5.0.tgz#cd2d611fd556968ce8fb6b356a09b4593c525947" - integrity sha512-qpv3fSEuNrhAO3FpH6YYRdaECnnRjg9VxbhdtPwPRnzSfHVXnNzzrpX4cJxqiwgRMo7uRMWDFBlsBq4Cr+rO3A== +"@vitest/snapshot@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-1.6.0.tgz#deb7e4498a5299c1198136f56e6e0f692e6af470" + integrity sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ== dependencies: magic-string "^0.30.5" pathe "^1.1.1" pretty-format "^29.7.0" -"@vitest/spy@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-1.5.0.tgz#1369a1bec47f46f18eccfa45f1e8fbb9b5e15e77" - integrity sha512-vu6vi6ew5N5MMHJjD5PoakMRKYdmIrNJmyfkhRpQt5d9Ewhw9nZ5Aqynbi3N61bvk9UvZ5UysMT6ayIrZ8GA9w== +"@vitest/spy@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-1.6.0.tgz#362cbd42ccdb03f1613798fde99799649516906d" + integrity sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw== dependencies: tinyspy "^2.2.0" -"@vitest/utils@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-1.5.0.tgz#90c9951f4516f6d595da24876b58e615f6c99863" - integrity sha512-BDU0GNL8MWkRkSRdNFvCUCAVOeHaUlVJ9Tx0TYBZyXaaOTmGtUFObzchCivIBrIwKzvZA7A9sCejVhXM2aY98A== +"@vitest/utils@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-1.6.0.tgz#5c5675ca7d6f546a7b4337de9ae882e6c57896a1" + integrity sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw== dependencies: diff-sequences "^29.6.3" estree-walker "^3.0.3" @@ -1908,21 +1900,14 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-query@5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" - integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== - dependencies: - deep-equal "^2.0.5" - -aria-query@^5.0.0: +aria-query@5.3.0, aria-query@^5.0.0: version "5.3.0" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== dependencies: dequal "^2.0.3" -array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1: +array-buffer-byte-length@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== @@ -2600,30 +2585,6 @@ deep-eql@^4.1.3: dependencies: type-detect "^4.0.0" -deep-equal@^2.0.5: - version "2.2.3" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1" - integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA== - dependencies: - array-buffer-byte-length "^1.0.0" - call-bind "^1.0.5" - es-get-iterator "^1.1.3" - get-intrinsic "^1.2.2" - is-arguments "^1.1.1" - is-array-buffer "^3.0.2" - is-date-object "^1.0.5" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" - isarray "^2.0.5" - object-is "^1.1.5" - object-keys "^1.1.1" - object.assign "^4.1.4" - regexp.prototype.flags "^1.5.1" - side-channel "^1.0.4" - which-boxed-primitive "^1.0.2" - which-collection "^1.0.1" - which-typed-array "^1.1.13" - deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" @@ -2734,11 +2695,16 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9: +dom-accessibility-api@^0.5.9: version "0.5.16" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== +dom-accessibility-api@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" + integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== + dom-helpers@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" @@ -2981,21 +2947,6 @@ es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0: resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== -es-get-iterator@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" - integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.3" - has-symbols "^1.0.3" - is-arguments "^1.1.1" - is-map "^2.0.2" - is-set "^2.0.2" - is-string "^1.0.7" - isarray "^2.0.5" - stop-iteration-iterator "^1.0.0" - es-iterator-helpers@^1.0.17: version "1.0.18" resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.18.tgz#4d3424f46b24df38d064af6fbbc89274e29ea69d" @@ -3554,7 +3505,7 @@ get-func-name@^2.0.1, get-func-name@^2.0.2: resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== @@ -3904,7 +3855,7 @@ inherits@2: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -internal-slot@^1.0.4, internal-slot@^1.0.7: +internal-slot@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== @@ -3913,15 +3864,7 @@ internal-slot@^1.0.4, internal-slot@^1.0.7: hasown "^2.0.0" side-channel "^1.0.4" -is-arguments@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-array-buffer@^3.0.2, is-array-buffer@^3.0.4: +is-array-buffer@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== @@ -4030,7 +3973,7 @@ is-in-browser@^1.0.2, is-in-browser@^1.1.3: resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" integrity sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g== -is-map@^2.0.2, is-map@^2.0.3: +is-map@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== @@ -4070,7 +4013,7 @@ is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-set@^2.0.2, is-set@^2.0.3: +is-set@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== @@ -4788,14 +4731,6 @@ object-inspect@^1.13.1: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== -object-is@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" - integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -5268,7 +5203,7 @@ regenerator-runtime@^0.14.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== -regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2: +regexp.prototype.flags@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== @@ -5655,13 +5590,6 @@ ste-simple-events@^3.0.5: dependencies: ste-core "^3.0.11" -stop-iteration-iterator@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" - integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== - dependencies: - internal-slot "^1.0.4" - string-argv@0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" @@ -6127,10 +6055,10 @@ verror@^1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -vite-node@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.5.0.tgz#7f74dadfecb15bca016c5ce5ef85e5cc4b82abf2" - integrity sha512-tV8h6gMj6vPzVCa7l+VGq9lwoJjW8Y79vst8QZZGiuRAfijU+EEWuc0kFpmndQrWhMMhet1jdSF+40KSZUqIIw== +vite-node@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.6.0.tgz#2c7e61129bfecc759478fa592754fd9704aaba7f" + integrity sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw== dependencies: cac "^6.7.14" debug "^4.3.4" @@ -6158,16 +6086,16 @@ vite@^5.0.0, vite@^5.2.8: optionalDependencies: fsevents "~2.3.3" -vitest@^1.4.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-1.5.0.tgz#6ebb396bd358650011a9c96c18fa614b668365c1" - integrity sha512-d8UKgR0m2kjdxDWX6911uwxout6GHS0XaGH1cksSIVVG8kRlE7G7aBw7myKQCvDI5dT4j7ZMa+l706BIORMDLw== - dependencies: - "@vitest/expect" "1.5.0" - "@vitest/runner" "1.5.0" - "@vitest/snapshot" "1.5.0" - "@vitest/spy" "1.5.0" - "@vitest/utils" "1.5.0" +vitest@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-1.6.0.tgz#9d5ad4752a3c451be919e412c597126cffb9892f" + integrity sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA== + dependencies: + "@vitest/expect" "1.6.0" + "@vitest/runner" "1.6.0" + "@vitest/snapshot" "1.6.0" + "@vitest/spy" "1.6.0" + "@vitest/utils" "1.6.0" acorn-walk "^8.3.2" chai "^4.3.10" debug "^4.3.4" @@ -6181,7 +6109,7 @@ vitest@^1.4.0: tinybench "^2.5.1" tinypool "^0.8.3" vite "^5.0.0" - vite-node "1.5.0" + vite-node "1.6.0" why-is-node-running "^2.2.2" w3c-xmlserializer@^5.0.0: @@ -6268,7 +6196,7 @@ which-collection@^1.0.1: is-weakmap "^2.0.2" is-weakset "^2.0.3" -which-typed-array@^1.1.13, which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.9: +which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.9: version "1.1.15" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== From 28367389fab9c26be34b22fcf4921ba34a3f9c3a Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Wed, 15 May 2024 15:04:17 +0200 Subject: [PATCH 038/124] FIX-2412 Optimize Splice Log Worker (#2414) --- .../Workers/SpliceLogsWorker.cs | 192 +++++++++++++----- .../Workers/Tools/LogWorkerTools.cs | 6 +- .../Workers/SpliceLogsWorkerTests.cs | 159 +++++++++++---- 3 files changed, 258 insertions(+), 99 deletions(-) diff --git a/Src/WitsmlExplorer.Api/Workers/SpliceLogsWorker.cs b/Src/WitsmlExplorer.Api/Workers/SpliceLogsWorker.cs index f8c9a5481..ac742147e 100644 --- a/Src/WitsmlExplorer.Api/Workers/SpliceLogsWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/SpliceLogsWorker.cs @@ -9,6 +9,7 @@ using Witsml; using Witsml.Data; +using Witsml.Data.Curves; using Witsml.Extensions; using Witsml.ServiceReference; @@ -16,14 +17,16 @@ using WitsmlExplorer.Api.Models; using WitsmlExplorer.Api.Services; +using CurveIndex = Witsml.Data.Curves.Index; + namespace WitsmlExplorer.Api.Workers { public class SpliceLogsWorker : BaseWorker, IWorker { public JobType JobType => JobType.SpliceLogs; - private enum ProgressType { Splice, CreateLog } public SpliceLogsWorker(ILogger logger, IWitsmlClientProvider witsmlClientProvider) : base(witsmlClientProvider, logger) { } + public override async Task<(WorkerResult, RefreshAction)> Execute(SpliceLogsJob job, CancellationToken? cancellationToken = null) { string wellUid = job.Logs.WellUid; @@ -35,6 +38,8 @@ public SpliceLogsWorker(ILogger logger, IWitsmlClientProvider wit WitsmlLogs logHeaders = await GetLogHeaders(wellUid, wellboreUid, logUids); WitsmlLog newLogHeader = CreateNewLogQuery(logHeaders, newLogUid, newLogName); + string currentMnemonic = ""; + string currentLogUid = ""; try { @@ -42,37 +47,61 @@ public SpliceLogsWorker(ILogger logger, IWitsmlClientProvider wit bool isDepthLog = logHeaders.Logs.FirstOrDefault().IndexType == WitsmlLog.WITSML_INDEX_TYPE_MD; - WitsmlLogData newLogData = new() - { - MnemonicList = string.Join(CommonConstants.DataSeparator, newLogHeader.LogCurveInfo.Select(lci => lci.Mnemonic)), - UnitList = string.Join(CommonConstants.DataSeparator, newLogHeader.LogCurveInfo.Select(lci => lci.Unit)), - Data = new() // Will be populated in the loop below - }; + string indexCurve = newLogHeader.IndexCurve.Value; + string indexCurveUnit = newLogHeader.LogCurveInfo.FirstOrDefault().Unit; int totalIterations = logHeaders.Logs.SelectMany(l => l.LogCurveInfo.Skip(1)).Count(); int currentIteration = 0; - foreach (string logUid in logUids) + + await CreateNewLog(newLogHeader); + + foreach (var mnemonic in newLogHeader.LogCurveInfo.Select(lci => lci.Mnemonic).Skip(1)) // Skip the index curve { - WitsmlLog logHeader = logHeaders.Logs.Find(l => l.Uid == logUid); - foreach (var mnemonic in logHeader.LogCurveInfo.Select(lci => lci.Mnemonic).Skip(1)) + currentMnemonic = mnemonic; + string mnemonicUnit = newLogHeader.LogCurveInfo.FirstOrDefault(lci => lci.Mnemonic == mnemonic).Unit; + WitsmlLogData newLogData = new() { + MnemonicList = indexCurve + CommonConstants.DataSeparator + mnemonic, + UnitList = indexCurveUnit + CommonConstants.DataSeparator + mnemonicUnit, + Data = new() // Will be populated in the loop below + }; + + foreach (string logUid in logUids) + { + currentLogUid = logUid; cancellationToken?.ThrowIfCancellationRequested(); - WitsmlLogData logData = await LogWorkerTools.GetLogDataForCurve(GetTargetWitsmlClientOrThrow(), logHeader, mnemonic, Logger); - newLogData = SpliceLogDataForCurve(newLogData, logData, mnemonic, isDepthLog); - currentIteration++; - double progress = (double)currentIteration / totalIterations; - ReportProgress(job, ProgressType.Splice, progress); + WitsmlLog logHeader = logHeaders.Logs.Find(l => l.Uid == logUid); + if (logHeader.LogCurveInfo.Any(lci => lci.Mnemonic == mnemonic)) + { + if (newLogData.Data.Count == 0) + { + newLogData.Data = (await LogWorkerTools.GetLogDataForCurve(GetTargetWitsmlClientOrThrow(), logHeader, mnemonic, Logger)).Data; + } + else + { + string startIndex = GetStartIndexOfData(newLogData, mnemonic); + string endIndex = GetEndIndexOfData(newLogData, mnemonic); + WitsmlLogData logDataBefore = await GetLogDataForCurveBeforeIndex(logHeader, mnemonic, startIndex, isDepthLog); + WitsmlLogData logDataAfter = await GetLogDataForCurveAfterIndex(logHeader, mnemonic, endIndex, isDepthLog); + newLogData = SpliceLogDataForCurve(newLogData, logDataBefore, logDataAfter); + } + currentIteration++; + double progress = (double)currentIteration / totalIterations; + ReportProgress(job, progress); + } } + + await AddDataToLog(wellUid, wellboreUid, newLogUid, newLogData); + + Logger.LogDebug("{JobType} - Added {Mnemonic} to spliced log. Progress: {CurrentIteration}/{TotalIterations}", GetType().Name, mnemonic, currentIteration, totalIterations); } - await CreateNewLog(newLogHeader); - await AddDataToLog(job, wellUid, wellboreUid, newLogUid, newLogData); } - catch (ArgumentException e) + catch (Exception e) { - var message = $"SpliceLogsJob failed. Description: {job.Description()}. Error: {e.Message} "; + var message = $"SpliceLogsJob failed. Description: {job.Description()}. CurrentMnemonic: {currentMnemonic}, currentLogUid: {currentLogUid}"; Logger.LogError(message); - return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, message), null); + return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, message, e.Message, jobId: jobId), null); } Logger.LogInformation("{JobType} - Job successful", GetType().Name); @@ -82,14 +111,8 @@ public SpliceLogsWorker(ILogger logger, IWitsmlClientProvider wit return (workerResult, refreshAction); } - private static void ReportProgress(SpliceLogsJob job, ProgressType progressType, double tentativeProgress) + private static void ReportProgress(SpliceLogsJob job, double progress) { - var progress = progressType switch - { - ProgressType.Splice => tentativeProgress * 0.8, - ProgressType.CreateLog => 0.8 + tentativeProgress * 0.2, - _ => throw new ArgumentOutOfRangeException(nameof(progressType), progressType, null) - }; if (job.JobInfo != null) job.JobInfo.Progress = progress; job.ProgressReporter?.Report(progress); } @@ -108,46 +131,104 @@ private static void VerifyLogHeaders(WitsmlLogs logHeaders) if (logHeaders.Logs.Any(log => log.IndexType != indexType)) throw new ArgumentException("Index type must match for all logs"); } - private static WitsmlLogData SpliceLogDataForCurve(WitsmlLogData primaryData, WitsmlLogData secondaryData, string mnemonic, bool isDepthLog) + private static string GetStartIndexOfData(WitsmlLogData logData, string mnemonic) + { + int mnemonicIndex = logData.MnemonicList.Split(CommonConstants.DataSeparator).ToList().FindIndex(m => m == mnemonic); + WitsmlData firstDataRow = logData.Data?.FirstOrDefault(x => x.Data.Split(CommonConstants.DataSeparator)[mnemonicIndex] != string.Empty); + return firstDataRow?.Data?.Split(CommonConstants.DataSeparator)[0]; + } + + private static string GetEndIndexOfData(WitsmlLogData logData, string mnemonic) + { + int mnemonicIndex = logData.MnemonicList.Split(CommonConstants.DataSeparator).ToList().FindIndex(m => m == mnemonic); + WitsmlData lastDataRow = logData.Data?.LastOrDefault(x => x.Data.Split(CommonConstants.DataSeparator)[mnemonicIndex] != string.Empty); + return lastDataRow?.Data?.Split(CommonConstants.DataSeparator)[0]; + } + + private async Task GetLogDataForCurveBeforeIndex(WitsmlLog logHeader, string mnemonic, string startIndex, bool isDepthLog) { - int mnemonicIndex = primaryData.MnemonicList.Split(CommonConstants.DataSeparator).ToList().FindIndex(m => m == mnemonic); - Dictionary primaryDict = primaryData.Data?.ToDictionary(row => row.Data.Split(CommonConstants.DataSeparator)[0], row => row.Data) ?? new(); - string startIndex = null; - string endIndex = null; - if (primaryDict.Any()) + WitsmlLogCurveInfo logCurveInfo = logHeader.LogCurveInfo.FirstOrDefault(lci => lci.Mnemonic == mnemonic); + CurveIndex mnemonicStartIndex; + CurveIndex dataStartIndex; + if (isDepthLog) { - var firstElementForCurve = primaryDict.FirstOrDefault(x => x.Value.Split(CommonConstants.DataSeparator)[mnemonicIndex] != string.Empty); - startIndex = firstElementForCurve.Equals(default(KeyValuePair)) ? null : firstElementForCurve.Key; - var lastElementForCurve = primaryDict.LastOrDefault(x => x.Value.Split(CommonConstants.DataSeparator)[mnemonicIndex] != string.Empty); - endIndex = lastElementForCurve.Equals(default(KeyValuePair)) ? null : lastElementForCurve.Key; + WitsmlIndex mnemonicMinIndex = logCurveInfo.MinIndex; + mnemonicStartIndex = new DepthIndex(StringHelpers.ToDouble(mnemonicMinIndex.Value), mnemonicMinIndex.Uom); + dataStartIndex = new DepthIndex(StringHelpers.ToDouble(startIndex), mnemonicMinIndex.Uom); + } + else + { + string mnemonicMinDateTimeIndex = logCurveInfo.MinDateTimeIndex; + mnemonicStartIndex = new DateTimeIndex(DateTime.Parse(mnemonicMinDateTimeIndex)); + dataStartIndex = new DateTimeIndex(DateTime.Parse(startIndex)); } - foreach (var dataRow in secondaryData.Data.Select(row => row.Data)) + if (mnemonicStartIndex < dataStartIndex) { - var rowIndex = dataRow.Split(CommonConstants.DataSeparator).First(); - if ((startIndex == null && endIndex == null) - || isDepthLog && (StringHelpers.ToDouble(rowIndex) < StringHelpers.ToDouble(startIndex) || StringHelpers.ToDouble(rowIndex) > StringHelpers.ToDouble(endIndex)) - || !isDepthLog && (DateTime.Parse(rowIndex) < DateTime.Parse(startIndex) || DateTime.Parse(rowIndex) > DateTime.Parse(endIndex))) + WitsmlLogData logData = await LogWorkerTools.GetLogDataForCurve(GetTargetWitsmlClientOrThrow(), logHeader, mnemonic, Logger, mnemonicStartIndex, dataStartIndex); + if (logData?.Data.LastOrDefault()?.Data.Split(CommonConstants.DataSeparator)[0] == startIndex) { - var newCellValue = dataRow.Split(CommonConstants.DataSeparator).Last(); - var currentRowValue = (primaryDict.GetValueOrDefault(rowIndex)?.Split(CommonConstants.DataSeparator) ?? Enumerable.Repeat("", primaryData.MnemonicList.Split(CommonConstants.DataSeparator).Length)).ToList(); - currentRowValue[0] = rowIndex; - currentRowValue[mnemonicIndex] = newCellValue; - primaryDict[rowIndex] = string.Join(CommonConstants.DataSeparator, currentRowValue); + // It returns the logData inclusive start and end index, but we want exclusive the end index. + logData.Data.RemoveAt(logData.Data.Count - 1); } + return logData; } - var sorted = isDepthLog ? primaryDict.OrderBy(x => StringHelpers.ToDouble(x.Key)) : primaryDict.OrderBy(x => DateTime.Parse(x.Key)); - List splicedData = sorted.Select(x => new WitsmlData { Data = x.Value }).ToList(); + // No new data, so return an empty list + return new WitsmlLogData + { + Data = new List() + }; + } - WitsmlLogData newData = new() + private async Task GetLogDataForCurveAfterIndex(WitsmlLog logHeader, string mnemonic, string endIndex, bool isDepthLog) + { + WitsmlLogCurveInfo logCurveInfo = logHeader.LogCurveInfo.FirstOrDefault(lci => lci.Mnemonic == mnemonic); + CurveIndex mnemonicEndIndex; + CurveIndex dataEndIndex; + + if (isDepthLog) + { + WitsmlIndex mnemonicMaxIndex = logCurveInfo.MaxIndex; + mnemonicEndIndex = new DepthIndex(StringHelpers.ToDouble(mnemonicMaxIndex.Value), mnemonicMaxIndex.Uom); + dataEndIndex = new DepthIndex(StringHelpers.ToDouble(endIndex), mnemonicMaxIndex.Uom); + } + else { - MnemonicList = primaryData.MnemonicList, - UnitList = primaryData.UnitList, - Data = splicedData + string mnemonicMaxDateTimeIndex = logCurveInfo.MaxDateTimeIndex; + mnemonicEndIndex = new DateTimeIndex(DateTime.Parse(mnemonicMaxDateTimeIndex)); + dataEndIndex = new DateTimeIndex(DateTime.Parse(endIndex)); + } + + if (mnemonicEndIndex > dataEndIndex) + { + WitsmlLogData logData = await LogWorkerTools.GetLogDataForCurve(GetTargetWitsmlClientOrThrow(), logHeader, mnemonic, Logger, dataEndIndex, mnemonicEndIndex); + if (logData?.Data.FirstOrDefault()?.Data.Split(CommonConstants.DataSeparator)[0] == endIndex) + { + // It returns the logData inclusive start and end index, but we want exclusive the start index. + logData.Data.RemoveAt(0); + } + return logData; + } + + // No new data, so return an empty list + return new WitsmlLogData + { + Data = new List() }; + } - return newData; + private static WitsmlLogData SpliceLogDataForCurve(WitsmlLogData logData, WitsmlLogData logDataBefore, WitsmlLogData logDataAfter) + { + return new WitsmlLogData + { + MnemonicList = logData.MnemonicList, + UnitList = logData.UnitList, + Data = logDataBefore.Data + .Concat(logData.Data) + .Concat(logDataAfter.Data) + .ToList() + }; } private async Task CreateNewLog(WitsmlLog newLogHeader) @@ -193,7 +274,7 @@ private static List GetNewLogCurveInfo(WitsmlLogs logHeaders return newLogCurveInfo; } - private async Task AddDataToLog(SpliceLogsJob job, string wellUid, string wellboreUid, string logUid, WitsmlLogData data) + private async Task AddDataToLog(string wellUid, string wellboreUid, string logUid, WitsmlLogData data) { var batchSize = 5000; // Use maxDataNodes and maxDataPoints to calculate batchSize when supported by the API. var dataRows = data.Data; @@ -203,7 +284,6 @@ private async Task AddDataToLog(SpliceLogsJob job, string wellUid, string wellbo WitsmlLogs copyNewCurvesQuery = CreateAddLogDataRowsQuery(wellUid, wellboreUid, logUid, data, currentLogData); QueryResult result = await RequestUtils.WithRetry(async () => await GetTargetWitsmlClientOrThrow().UpdateInStoreAsync(copyNewCurvesQuery), Logger); if (!result.IsSuccessful) throw new ArgumentException($"Could not add log data to the new log. {result.Reason}"); - ReportProgress(job, ProgressType.CreateLog, (i + batchSize) / (double)dataRows.Count); } } diff --git a/Src/WitsmlExplorer.Api/Workers/Tools/LogWorkerTools.cs b/Src/WitsmlExplorer.Api/Workers/Tools/LogWorkerTools.cs index 75f787e1b..8343b3c98 100644 --- a/Src/WitsmlExplorer.Api/Workers/Tools/LogWorkerTools.cs +++ b/Src/WitsmlExplorer.Api/Workers/Tools/LogWorkerTools.cs @@ -15,6 +15,8 @@ using WitsmlExplorer.Api.Query; using WitsmlExplorer.Api.Services; +using CurveIndex = Witsml.Data.Curves.Index; + namespace WitsmlExplorer.Api.Workers { public static class LogWorkerTools @@ -40,9 +42,9 @@ public static async Task GetLogsByIds(IWitsmlClient client, string w return result; } - public static async Task GetLogDataForCurve(IWitsmlClient witsmlClient, WitsmlLog log, string mnemonic, ILogger logger) + public static async Task GetLogDataForCurve(IWitsmlClient witsmlClient, WitsmlLog log, string mnemonic, ILogger logger, CurveIndex startIndex = null, CurveIndex endIndex = null) { - await using LogDataReader logDataReader = new(witsmlClient, log, mnemonic.AsItemInList(), logger); + await using LogDataReader logDataReader = new(witsmlClient, log, mnemonic.AsItemInList(), logger, startIndex ?? CurveIndex.Start(log), endIndex ?? CurveIndex.End(log)); List data = new(); WitsmlLogData logData = await logDataReader.GetNextBatch(); var mnemonicList = logData?.MnemonicList; diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/SpliceLogsWorkerTests.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/SpliceLogsWorkerTests.cs index f5dda8109..52ffdc9eb 100644 --- a/Tests/WitsmlExplorer.Api.Tests/Workers/SpliceLogsWorkerTests.cs +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/SpliceLogsWorkerTests.cs @@ -52,14 +52,14 @@ public async Task Execute_SplicedLog_HasCorrectHeader(string indexType) int[] startIndexNum = { 0, 0 }; // start indexes for each log int[] endIndexNum = { 10, 10 }; // end indexes for each log WitsmlLogs capturedLogHeader = null; - WitsmlLogs capturedLogData = null; + List capturedLogData = new(); - var (job, logHeaders, logData) = SetupTest(logUids, indexType, startIndexNum, endIndexNum, (log) => capturedLogHeader = log, (log) => capturedLogData = log); + var (job, logHeaders, logData) = SetupTest(logUids, indexType, startIndexNum, endIndexNum, log => capturedLogHeader = log, capturedLogData.Add); var (workerResult, refreshAction) = await _worker.Execute(job); WitsmlLog newLogHeader = capturedLogHeader.Logs.First(); - WitsmlLog newLogDataHeader = capturedLogData.Logs.First(); + List newLogDataHeaders = capturedLogData.Select(data => data.Logs.First()).ToList(); Assert.True(workerResult.IsSuccess); Assert.NotNull(refreshAction); @@ -73,12 +73,14 @@ public async Task Execute_SplicedLog_HasCorrectHeader(string indexType) Assert.Equal(_wellboreUid, newLogHeader.UidWellbore); Assert.Equal(indexType, newLogHeader.IndexType); - // Verify that the captured log data has the correct header - Assert.NotNull(capturedLogData); - Assert.Single(capturedLogData.Logs); - Assert.Equal(_newLogUid, newLogDataHeader.Uid); - Assert.Equal(_wellUid, newLogDataHeader.UidWell); - Assert.Equal(_wellboreUid, newLogDataHeader.UidWellbore); + // Verify that the captured log datas have the correct header + Assert.All(newLogDataHeaders, newLogDataHeader => + { + Assert.Equal(_newLogUid, newLogDataHeader.Uid); + Assert.Equal(_wellUid, newLogDataHeader.UidWell); + Assert.Equal(_wellboreUid, newLogDataHeader.UidWellbore); + } + ); } [Theory] @@ -90,9 +92,9 @@ public async Task Execute_OnlyOverlap_KeepsFirst(string indexType) int[] startIndexNum = { 0, 0 }; // start indexes for each log int[] endIndexNum = { 10, 10 }; // end indexes for each log WitsmlLogs capturedLogHeader = null; - WitsmlLogs capturedLogData = null; + List capturedLogData = new(); - var (job, logHeaders, logData) = SetupTest(logUids, indexType, startIndexNum, endIndexNum, (log) => capturedLogHeader = log, (log) => capturedLogData = log); + var (job, logHeaders, logData) = SetupTest(logUids, indexType, startIndexNum, endIndexNum, (log) => capturedLogHeader = log, capturedLogData.Add); var expectedMnemonics = logHeaders.Logs.First().LogCurveInfo.Select(lci => lci.Mnemonic); var expectedUnits = logHeaders.Logs.First().LogCurveInfo.Select(lci => lci.Unit); @@ -100,10 +102,10 @@ public async Task Execute_OnlyOverlap_KeepsFirst(string indexType) var (workerResult, refreshAction) = await _worker.Execute(job); WitsmlLog newLogHeader = capturedLogHeader.Logs.First(); - WitsmlLog newLogDataHeader = capturedLogData.Logs.First(); - WitsmlLogData newLogData = newLogDataHeader.LogData; IEnumerable newLogHeaderMnemonics = newLogHeader.LogCurveInfo.Select(lci => lci.Mnemonic); IEnumerable newLogHeaderUnits = newLogHeader.LogCurveInfo.Select(lci => lci.Unit); + List newLogDatas = capturedLogData.Select(data => data.Logs.First().LogData).ToList(); + WitsmlLogData newLogData = GetMergedLogData(newLogDatas, indexType); IEnumerable newLogDataMnemonics = newLogData.MnemonicList.Split(CommonConstants.DataSeparator); IEnumerable newLogDataUnits = newLogData.UnitList.Split(CommonConstants.DataSeparator); IEnumerable expectedData = logData.Logs.First().LogData.Data.Select(d => d.Data); @@ -125,9 +127,9 @@ public async Task Execute_NoOverlap_KeepsBoth(string indexType) int[] startIndexNum = { 0, 5 }; // start indexes for each log int[] endIndexNum = { 4, 9 }; // end indexes for each log WitsmlLogs capturedLogHeader = null; - WitsmlLogs capturedLogData = null; + List capturedLogData = new(); - var (job, logHeaders, logData) = SetupTest(logUids, indexType, startIndexNum, endIndexNum, (log) => capturedLogHeader = log, (log) => capturedLogData = log); + var (job, logHeaders, logData) = SetupTest(logUids, indexType, startIndexNum, endIndexNum, (log) => capturedLogHeader = log, capturedLogData.Add); var expectedMnemonics = logHeaders.Logs.First().LogCurveInfo.Select(lci => lci.Mnemonic); var expectedUnits = logHeaders.Logs.First().LogCurveInfo.Select(lci => lci.Unit); @@ -135,8 +137,8 @@ public async Task Execute_NoOverlap_KeepsBoth(string indexType) var (workerResult, refreshAction) = await _worker.Execute(job); WitsmlLog newLogHeader = capturedLogHeader.Logs.First(); - WitsmlLog newLogDataHeader = capturedLogData.Logs.First(); - WitsmlLogData newLogData = newLogDataHeader.LogData; + List newLogDatas = capturedLogData.Select(data => data.Logs.First().LogData).ToList(); + WitsmlLogData newLogData = GetMergedLogData(newLogDatas, indexType); IEnumerable newLogHeaderMnemonics = newLogHeader.LogCurveInfo.Select(lci => lci.Mnemonic); IEnumerable newLogHeaderUnits = newLogHeader.LogCurveInfo.Select(lci => lci.Unit); IEnumerable newLogDataMnemonics = newLogData.MnemonicList.Split(CommonConstants.DataSeparator); @@ -162,9 +164,9 @@ public async Task Execute_SomeOverlap_KeepsFirstOnOverlap(string indexType) int[] startIndexNum = { 0, 5 }; // start indexes for each log int[] endIndexNum = { 9, 14 }; // end indexes for each log WitsmlLogs capturedLogHeader = null; - WitsmlLogs capturedLogData = null; + List capturedLogData = new(); - var (job, logHeaders, logData) = SetupTest(logUids, indexType, startIndexNum, endIndexNum, (log) => capturedLogHeader = log, (log) => capturedLogData = log); + var (job, logHeaders, logData) = SetupTest(logUids, indexType, startIndexNum, endIndexNum, (log) => capturedLogHeader = log, capturedLogData.Add); var expectedMnemonics = logHeaders.Logs.First().LogCurveInfo.Select(lci => lci.Mnemonic); var expectedUnits = logHeaders.Logs.First().LogCurveInfo.Select(lci => lci.Unit); @@ -172,8 +174,8 @@ public async Task Execute_SomeOverlap_KeepsFirstOnOverlap(string indexType) var (workerResult, refreshAction) = await _worker.Execute(job); WitsmlLog newLogHeader = capturedLogHeader.Logs.First(); - WitsmlLog newLogDataHeader = capturedLogData.Logs.First(); - WitsmlLogData newLogData = newLogDataHeader.LogData; + List newLogDatas = capturedLogData.Select(data => data.Logs.First().LogData).ToList(); + WitsmlLogData newLogData = GetMergedLogData(newLogDatas, indexType); IEnumerable newLogHeaderMnemonics = newLogHeader.LogCurveInfo.Select(lci => lci.Mnemonic); IEnumerable newLogHeaderUnits = newLogHeader.LogCurveInfo.Select(lci => lci.Unit); IEnumerable newLogDataMnemonics = newLogData.MnemonicList.Split(CommonConstants.DataSeparator); @@ -199,9 +201,9 @@ public async Task Execute_ManyLogs_NoOverlap_KeepsFirstAll(string indexType) int[] startIndexNum = { 0, 5, 10, 15, 20 }; // start indexes for each log int[] endIndexNum = { 4, 9, 14, 19, 24 }; // end indexes for each log WitsmlLogs capturedLogHeader = null; - WitsmlLogs capturedLogData = null; + List capturedLogData = new(); - var (job, logHeaders, logData) = SetupTest(logUids, indexType, startIndexNum, endIndexNum, (log) => capturedLogHeader = log, (log) => capturedLogData = log); + var (job, logHeaders, logData) = SetupTest(logUids, indexType, startIndexNum, endIndexNum, (log) => capturedLogHeader = log, capturedLogData.Add); var expectedMnemonics = logHeaders.Logs.First().LogCurveInfo.Select(lci => lci.Mnemonic); var expectedUnits = logHeaders.Logs.First().LogCurveInfo.Select(lci => lci.Unit); @@ -209,8 +211,8 @@ public async Task Execute_ManyLogs_NoOverlap_KeepsFirstAll(string indexType) var (workerResult, refreshAction) = await _worker.Execute(job); WitsmlLog newLogHeader = capturedLogHeader.Logs.First(); - WitsmlLog newLogDataHeader = capturedLogData.Logs.First(); - WitsmlLogData newLogData = newLogDataHeader.LogData; + List newLogDatas = capturedLogData.Select(data => data.Logs.First().LogData).ToList(); + WitsmlLogData newLogData = GetMergedLogData(newLogDatas, indexType); IEnumerable newLogHeaderMnemonics = newLogHeader.LogCurveInfo.Select(lci => lci.Mnemonic); IEnumerable newLogHeaderUnits = newLogHeader.LogCurveInfo.Select(lci => lci.Unit); IEnumerable newLogDataMnemonics = newLogData.MnemonicList.Split(CommonConstants.DataSeparator); @@ -237,9 +239,9 @@ public async Task Execute_SomeCurvesOverlap_KeepsFirstCurveOnOverlap(string inde int[] startIndexNum = { 0, 5 }; // start indexes for each log int[] endIndexNum = { 9, 14 }; // end indexes for each log WitsmlLogs capturedLogHeader = null; - WitsmlLogs capturedLogData = null; + List capturedLogData = new(); - var (job, logHeaders, logData) = SetupTest(logUids, indexType, startIndexNum, endIndexNum, (log) => capturedLogHeader = log, (log) => capturedLogData = log); + var (job, logHeaders, logData) = SetupTest(logUids, indexType, startIndexNum, endIndexNum, (log) => capturedLogHeader = log, capturedLogData.Add); var data = logData.Logs.First().LogData.Data.Last().Data.Split(CommonConstants.DataSeparator); data[1] = string.Empty; @@ -251,8 +253,8 @@ public async Task Execute_SomeCurvesOverlap_KeepsFirstCurveOnOverlap(string inde var (workerResult, refreshAction) = await _worker.Execute(job); WitsmlLog newLogHeader = capturedLogHeader.Logs.First(); - WitsmlLog newLogDataHeader = capturedLogData.Logs.First(); - WitsmlLogData newLogData = newLogDataHeader.LogData; + List newLogDatas = capturedLogData.Select(data => data.Logs.First().LogData).ToList(); + WitsmlLogData newLogData = GetMergedLogData(newLogDatas, indexType); IEnumerable newLogHeaderMnemonics = newLogHeader.LogCurveInfo.Select(lci => lci.Mnemonic); IEnumerable newLogHeaderUnits = newLogHeader.LogCurveInfo.Select(lci => lci.Unit); IEnumerable newLogDataMnemonics = newLogData.MnemonicList.Split(CommonConstants.DataSeparator); @@ -283,9 +285,9 @@ public async Task Execute_NewCurve_AddsNewCurve(string indexType) int[] startIndexNum = { 0, 0 }; // start indexes for each log int[] endIndexNum = { 10, 10 }; // end indexes for each log WitsmlLogs capturedLogHeader = null; - WitsmlLogs capturedLogData = null; + List capturedLogData = new(); - var (job, logHeaders, logData) = SetupTest(logUids, indexType, startIndexNum, endIndexNum, (log) => capturedLogHeader = log, (log) => capturedLogData = log); + var (job, logHeaders, logData) = SetupTest(logUids, indexType, startIndexNum, endIndexNum, (log) => capturedLogHeader = log, capturedLogData.Add); var isDepthLog = indexType == WitsmlLog.WITSML_INDEX_TYPE_MD; logHeaders.Logs.Last().LogCurveInfo.Add(new WitsmlLogCurveInfo() @@ -311,8 +313,8 @@ public async Task Execute_NewCurve_AddsNewCurve(string indexType) var (workerResult, refreshAction) = await _worker.Execute(job); WitsmlLog newLogHeader = capturedLogHeader.Logs.First(); - WitsmlLog newLogDataHeader = capturedLogData.Logs.First(); - WitsmlLogData newLogData = newLogDataHeader.LogData; + List newLogDatas = capturedLogData.Select(data => data.Logs.First().LogData).ToList(); + WitsmlLogData newLogData = GetMergedLogData(newLogDatas, indexType); IEnumerable newLogHeaderMnemonics = newLogHeader.LogCurveInfo.Select(lci => lci.Mnemonic); IEnumerable newLogHeaderUnits = newLogHeader.LogCurveInfo.Select(lci => lci.Unit); IEnumerable newLogDataMnemonics = newLogData.MnemonicList.Split(CommonConstants.DataSeparator); @@ -339,9 +341,9 @@ public async Task Execute_IndexCurveNotFirstInLCI_HasCorrectHeader(string indexT int[] startIndexNum = { 0, 0 }; // start indexes for each log int[] endIndexNum = { 10, 10 }; // end indexes for each log WitsmlLogs capturedLogHeader = null; - WitsmlLogs capturedLogData = null; + List capturedLogData = new(); - var (job, logHeaders, logData) = SetupTest(logUids, indexType, startIndexNum, endIndexNum, (log) => capturedLogHeader = log, (log) => capturedLogData = log); + var (job, logHeaders, logData) = SetupTest(logUids, indexType, startIndexNum, endIndexNum, (log) => capturedLogHeader = log, capturedLogData.Add); // Move index curve to last position in LogCurveInfo for all the logHeaders. foreach (var logHeader in logHeaders.Logs) @@ -354,7 +356,6 @@ public async Task Execute_IndexCurveNotFirstInLCI_HasCorrectHeader(string indexT var (workerResult, refreshAction) = await _worker.Execute(job); WitsmlLog newLogHeader = capturedLogHeader.Logs.First(); - WitsmlLog newLogDataHeader = capturedLogData.Logs.First(); Assert.Equal("IndexCurve", newLogHeader.LogCurveInfo.First().Mnemonic); } @@ -369,19 +370,22 @@ public async Task Execute_DifferentIndexCurveNames_HasCorrectHeader(string index int[] endIndexNum = { 10, 10 }; // end indexes for each log string[] indexCurves = { "IndexCurve1", "IndexCurve2" }; WitsmlLogs capturedLogHeader = null; - WitsmlLogs capturedLogData = null; + List capturedLogData = new(); - var (job, logHeaders, logData) = SetupTest(logUids, indexType, startIndexNum, endIndexNum, (log) => capturedLogHeader = log, (log) => capturedLogData = log, indexCurves); + var (job, logHeaders, logData) = SetupTest(logUids, indexType, startIndexNum, endIndexNum, (log) => capturedLogHeader = log, capturedLogData.Add, indexCurves); var (workerResult, refreshAction) = await _worker.Execute(job); WitsmlLog newLogHeader = capturedLogHeader.Logs.First(); - WitsmlLog newLogDataHeader = capturedLogData.Logs.First(); + List newLogDataHeaders = capturedLogData.Select(data => data.Logs.First()).ToList(); // When Log Curve Info varies between logs, the Log Curve Info of the last log is prioritized. Assert.Equal("IndexCurve2", newLogHeader.IndexCurve.Value); Assert.Equal("IndexCurve2", newLogHeader.LogCurveInfo.First().Mnemonic); - Assert.Equal("IndexCurve2", newLogDataHeader.LogData.MnemonicList.Split(CommonConstants.DataSeparator)[0]); + Assert.All(newLogDataHeaders, newLogDataHeader => + { + Assert.Equal("IndexCurve2", newLogDataHeader.LogData.MnemonicList.Split(CommonConstants.DataSeparator)[0]); + }); } private (SpliceLogsJob, WitsmlLogs, WitsmlLogs) SetupTest(string[] logUids, string indexType, int[] startIndexNum, int[] endIndexNum, Action logHeaderCallback, Action logDataCallback, string[] indexCurves = null) @@ -527,6 +531,7 @@ private void SetupClient(Mock witsmlClient, WitsmlLogs logHeaders string mnemonic = logs.Logs.First().LogData.MnemonicList.Split(CommonConstants.DataSeparator)[1]; int mnemonicIndex = log.LogData.MnemonicList.Split(CommonConstants.DataSeparator).ToList().FindIndex(m => m == mnemonic); IEnumerable dataForCurve = data.Select(dataRow => $"{dataRow.Split(CommonConstants.DataSeparator)[0]},{dataRow.Split(CommonConstants.DataSeparator)[mnemonicIndex]}"); + IEnumerable dataForCurveWithinRange = GetDataWithinRange(dataForCurve, logs.Logs.First()); WitsmlLogs newLogData = new() { Logs = new WitsmlLog() @@ -538,7 +543,7 @@ private void SetupClient(Mock witsmlClient, WitsmlLogs logHeaders { MnemonicList = logs.Logs.First().LogData.MnemonicList, UnitList = logs.Logs.First().LogData.UnitList, - Data = dataForCurve.Select(d => new WitsmlData() { Data = d }).ToList() + Data = dataForCurveWithinRange.Select(d => new WitsmlData() { Data = d }).ToList() } }.AsItemInList() }; @@ -565,5 +570,77 @@ private void SetupClient(Mock witsmlClient, WitsmlLogs logHeaders return Task.FromResult(new QueryResult(true)); }); } + + private IEnumerable GetDataWithinRange(IEnumerable data, WitsmlLog log) + { + if (log.StartIndex != null) + { + data = data.Where(dataRow => StringHelpers.ToDouble(dataRow.Split(CommonConstants.DataSeparator)[0]) >= StringHelpers.ToDouble(log.StartIndex.Value)); + } + if (log.EndIndex != null) + { + data = data.Where(dataRow => StringHelpers.ToDouble(dataRow.Split(CommonConstants.DataSeparator)[0]) <= StringHelpers.ToDouble(log.EndIndex.Value)); + } + if (log.StartDateTimeIndex != null) + { + data = data.Where(dataRow => DateTime.Parse(dataRow.Split(CommonConstants.DataSeparator)[0]) >= DateTime.Parse(log.StartDateTimeIndex)); + } + if (log.EndDateTimeIndex != null) + { + data = data.Where(dataRow => DateTime.Parse(dataRow.Split(CommonConstants.DataSeparator)[0]) <= DateTime.Parse(log.EndDateTimeIndex)); + } + return data; + } + + private WitsmlLogData GetMergedLogData(List logDatas, string indexType) + { + List mnemonics = logDatas.SelectMany(data => data.MnemonicList.Split(CommonConstants.DataSeparator)).Distinct().ToList(); + List units = new(); + List> mergedData = new(); + for (int i = 0; i < mnemonics.Count; i++) + { + string mnemonic = mnemonics[i]; + WitsmlLogData logWithMnemonic = logDatas.Find(data => data.MnemonicList.Split(CommonConstants.DataSeparator).Contains(mnemonic)); + int mnemonicIndex = logWithMnemonic.MnemonicList.Split(CommonConstants.DataSeparator).ToList().IndexOf(mnemonic); + string unit = logWithMnemonic.UnitList.Split(CommonConstants.DataSeparator)[mnemonicIndex]; + units.Add(unit); + foreach (List dataPoints in logWithMnemonic.Data.Select(d => d.Data.Split(CommonConstants.DataSeparator).ToList())) + { + string index = dataPoints[0]; + string mnemonicDataPoint = dataPoints[mnemonicIndex]; + int dataIndex = mergedData.FindIndex(d => d[0] == index); + if (dataIndex > -1) + { + mergedData[dataIndex][i] = mnemonicDataPoint; + } + else + { + List newRow = new List(); + newRow.AddRange(Enumerable.Repeat("", mnemonics.Count)); // Initialize empty row + newRow[0] = index; + newRow[mnemonics.IndexOf(mnemonic)] = mnemonicDataPoint; + mergedData.Add(newRow); + } + } + } + if (indexType == WitsmlLog.WITSML_INDEX_TYPE_MD) + { + mergedData = mergedData.OrderBy(row => StringHelpers.ToDouble(row[0])).ToList(); + } + else + { + mergedData = mergedData.OrderBy(row => DateTime.Parse(row[0])).ToList(); + } + return new WitsmlLogData + { + MnemonicList = string.Join(CommonConstants.DataSeparator, mnemonics), + UnitList = string.Join(CommonConstants.DataSeparator, units), + Data = mergedData.Select(dataRow => new WitsmlData + { + Data = string.Join(CommonConstants.DataSeparator, dataRow) + } + ).ToList() + }; + } } } From 4048657a387eb1c8a836a34232eef4d1434c9a06 Mon Sep 17 00:00:00 2001 From: Robert Basti Date: Thu, 16 May 2024 14:43:53 +0200 Subject: [PATCH 039/124] =?UTF-8?q?Multiple=20Logs=20with=20the=20same=20n?= =?UTF-8?q?ame=20shall=20be=20collapsed=20in=20TREE=20structure=E2=80=A6?= =?UTF-8?q?=20(#2408)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Elias Bruvik --- .../ContentViews/LogCurveInfoListView.tsx | 9 +- .../LogCurveInfoListViewUtils.tsx | 7 +- .../components/ContentViews/LogsListView.tsx | 8 +- .../MultiLogsCurveInfoListView.tsx | 1 + .../components/Sidebar/LogItem.tsx | 9 +- .../components/Sidebar/LogTypeItem.tsx | 165 ++++++++++++++---- .../hooks/useExpandObjectGroupNodes.tsx | 12 +- .../models/logObject.tsx | 1 + .../models/wellbore.tsx | 19 ++ .../tools/logSameNamesHelper.tsx | 16 ++ 10 files changed, 205 insertions(+), 42 deletions(-) create mode 100644 Src/WitsmlExplorer.Frontend/tools/logSameNamesHelper.tsx diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListView.tsx index 8ce602648..8506e8177 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListView.tsx @@ -65,7 +65,13 @@ export default function LogCurveInfoListView() { const isDepthIndex = !!logCurveInfoList?.[0]?.maxDepthIndex; const isFetching = isFetchingLog || isFetchingLogCurveInfo; - useExpandSidebarNodes(wellUid, wellboreUid, ObjectType.Log, logType); + useExpandSidebarNodes( + wellUid, + wellboreUid, + ObjectType.Log, + logType, + logObject?.name + ); useEffect(() => { if (logObject) { @@ -155,6 +161,7 @@ export default function LogCurveInfoListView() { true )} data={getTableData( + [logObject], logCurveInfoList, logObjects, timeZone, diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListViewUtils.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListViewUtils.tsx index acdae9473..2c4eb5d9a 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListViewUtils.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListViewUtils.tsx @@ -10,6 +10,7 @@ import LogCurveInfo from "models/logCurveInfo"; import LogObject from "models/logObject"; import { measureToString } from "models/measure"; import MultiLogCurveInfo from "models/multilogCurveInfo"; +import { getNameOccurrenceSuffix } from "tools/logSameNamesHelper"; export interface LogCurveInfoRow extends ContentTableRow { uid: string; @@ -108,6 +109,7 @@ export const getColumns = ( }; export const getTableData = ( + allLogs: LogObject[], logCurveInfoList: MultiLogCurveInfo[], logObjects: Map, timeZone: TimeZone, @@ -132,6 +134,7 @@ export const getTableData = ( if (logUid !== null) { logCurveInfo.logUid = logUid; } + const logObject = logObjects.get(logCurveInfo.logUid); const isActive = logObject.objectGrowing && @@ -147,7 +150,9 @@ export const getTableData = ( logUid === null ? `${logCurveInfo.uid}` : `${logCurveInfo.logUid}-${logCurveInfo.mnemonic}`, - logName: logObject.name, + logName: logObject.runNumber + ? `${logObject.name} (${logObject.runNumber})` + : logObject.name + getNameOccurrenceSuffix(allLogs, logObject), logUid: logObject.uid, mnemonic: logCurveInfo.mnemonic, minIndex: isDepthIndex diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogsListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogsListView.tsx index 0ae0129ae..b209107c7 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogsListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogsListView.tsx @@ -28,6 +28,7 @@ import { MouseEvent, useContext, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { ItemNotFound } from "routes/ItemNotFound"; import { RouterLogType } from "routes/routerConstants"; +import { getNameOccurrenceSuffix } from "tools/logSameNamesHelper"; import { CommonPanelContainer, ContentContainer @@ -85,10 +86,14 @@ export default function LogsListView() { }; const getTableData = (): LogObjectRow[] => { - return logs.map((log) => { + const result = logs.map((log) => { return { ...log, id: log.uid, + + name: log.runNumber + ? `${log.name} (${log.runNumber})` + : log.name + getNameOccurrenceSuffix(logs, log), startIndex: isTimeIndexed ? formatDateString(log.startIndex, timeZone, dateTimeFormat) : log.startIndex, @@ -108,6 +113,7 @@ export default function LogsListView() { logObject: log }; }); + return result.sort((a, b) => a.name.localeCompare(b.name)); }; const columns: ContentTableColumn[] = [ diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/MultiLogsCurveInfoListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/MultiLogsCurveInfoListView.tsx index fa2300aca..75e1bce4c 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/MultiLogsCurveInfoListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/MultiLogsCurveInfoListView.tsx @@ -171,6 +171,7 @@ export default function MultiLogsCurveInfoListView() { hideEmptyMnemonics )} data={getTableData( + allLogs, logCurveInfoList, logObjects, timeZone, diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/LogItem.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/LogItem.tsx index 96bf44a75..598c4367c 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/LogItem.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/LogItem.tsx @@ -9,8 +9,10 @@ import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import LogObject from "models/logObject"; import { MouseEvent, useContext } from "react"; +import { getNameOccurrenceSuffix } from "tools/logSameNamesHelper"; interface LogItemProps { + logObjects: LogObject[]; log: LogObject; selected: boolean; nodeId: string; @@ -19,6 +21,7 @@ interface LogItemProps { } export default function LogItem({ + logObjects, log, selected, nodeId, @@ -49,7 +52,11 @@ export default function LogItem({ } key={nodeId} nodeId={nodeId} - labelText={log.runNumber ? `${log.name} (${log.runNumber})` : log.name} + labelText={ + log.runNumber + ? `${log.name} (${log.runNumber})` + : log.name + getNameOccurrenceSuffix(logObjects, log) + } selected={selected} isActive={objectGrowing} to={to} diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/LogTypeItem.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/LogTypeItem.tsx index 0b4d0cfb3..7b59754c1 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/LogTypeItem.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/LogTypeItem.tsx @@ -24,6 +24,8 @@ import Wellbore, { calculateLogTypeDepthId, calculateLogTypeId, calculateLogTypeTimeId, + calculateMultipleLogsNode, + calculateMultipleLogsNodeItem, calculateObjectNodeId as calculateWellboreObjectNodeId } from "models/wellbore"; import { Fragment, MouseEvent, useContext } from "react"; @@ -51,6 +53,7 @@ export default function LogTypeItem({ const { wellbore } = useGetWellbore(connectedServer, wellUid, wellboreUid); const logTypeGroupDepth = calculateLogTypeDepthId(wellbore); const logTypeGroupTime = calculateLogTypeTimeId(wellbore); + const { logType, wellUid: urlWellUid, @@ -72,6 +75,14 @@ export default function LogTypeItem({ ); }; + const getMultipleLogsNode = (logName: string) => { + return calculateMultipleLogsNode(wellbore, logName); + }; + + const getMultipleLogsNodeItem = (logName: string, logUid: string) => { + return calculateMultipleLogsNodeItem(wellbore, logName, logUid); + }; + const onContextMenu = ( event: MouseEvent, wellbore: Wellbore, @@ -93,6 +104,11 @@ export default function LogTypeItem({ } }); }; + + const filterLogsByType = (logs: LogObject[], logType: string) => { + return logs?.filter((log) => log.indexType === logType) ?? []; + }; + const depthLogs = filterLogsByType(logs, WITSML_INDEX_TYPE_MD); const timeLogs = filterLogsByType(logs, WITSML_INDEX_TYPE_DATE_TIME); @@ -113,6 +129,119 @@ export default function LogTypeItem({ ); }; + const subLogsCount = (logs: LogObject[], logName: string) => { + return logs.filter((x) => x.name === logName).length; + }; + + const subLogsNodeName = (logName: string) => { + return logName + " (multiple)"; + }; + + const getLogTypePath = (logType: string) => { + return logType === WITSML_INDEX_TYPE_DATE_TIME + ? RouterLogType.TIME + : RouterLogType.DEPTH; + }; + + const getLogTypeGroup = (logType: string) => { + return logType === WITSML_INDEX_TYPE_DATE_TIME + ? logTypeGroupTime + : logTypeGroupDepth; + }; + + const listSubLogItems = ( + logObjects: LogObject[], + logType: string, + wellUid: string, + wellboreUid: string, + serverUrl: string + ) => { + return logObjects + ?.sort( + (a, b) => + a.runNumber?.localeCompare(b.runNumber) || a.uid.localeCompare(b.uid) + ) + .map((log) => ( + + + + )); + }; + + const listLogItemsByType = ( + logObjects: LogObject[], + logType: string, + wellUid: string, + wellboreUid: string, + isSelected: (log: LogObject) => boolean, + serverUrl: string + ) => { + const distinctLogObjects = logObjects.filter( + (logObject, i, arr) => + arr.findIndex((t) => t.name === logObject.name) === i + ); + return distinctLogObjects?.map((log) => + subLogsCount(logObjects, log.name) > 1 ? ( + x.uid === objectUid)?.name + ) + } + > + {listSubLogItems( + logObjects.filter((x) => x.name === log.name), + logType, + wellUid, + wellboreUid, + connectedServer?.url + )} + + ) : ( + + + + ) + ); + }; + return ( <> ); } - -const filterLogsByType = (logs: LogObject[], logType: string) => { - return logs?.filter((log) => log.indexType === logType) ?? []; -}; - -const listLogItemsByType = ( - logObjects: LogObject[], - logType: string, - wellUid: string, - wellboreUid: string, - isSelected: (log: LogObject) => boolean, - serverUrl: string -) => { - const logTypePath = - logType === WITSML_INDEX_TYPE_DATE_TIME - ? RouterLogType.TIME - : RouterLogType.DEPTH; - return logObjects?.map((log) => ( - - - - )); -}; diff --git a/Src/WitsmlExplorer.Frontend/hooks/useExpandObjectGroupNodes.tsx b/Src/WitsmlExplorer.Frontend/hooks/useExpandObjectGroupNodes.tsx index 7c4fe6984..2c684f64e 100644 --- a/Src/WitsmlExplorer.Frontend/hooks/useExpandObjectGroupNodes.tsx +++ b/Src/WitsmlExplorer.Frontend/hooks/useExpandObjectGroupNodes.tsx @@ -8,6 +8,7 @@ import { SidebarActionType } from "../contexts/sidebarReducer"; import { ObjectType } from "../models/objectType"; import { calculateLogTypeId, + calculateMultipleLogsNode, calculateObjectGroupId, calculateWellNodeId, calculateWellboreNodeId @@ -18,7 +19,8 @@ export function useExpandSidebarNodes( wellUid: string, wellboreUid?: string, objectType?: ObjectType, - logType?: string + logType?: string, + logName?: string ) { const { dispatchSidebar } = useSidebar(); @@ -49,9 +51,15 @@ export function useExpandSidebarNodes( ); } + if (wellUid && wellboreUid && logType && logName) { + nodeIds.push( + calculateMultipleLogsNode({ wellUid, uid: wellboreUid }, logName) + ); + } + dispatchSidebar({ type: SidebarActionType.ExpandTreeNodes, payload: { nodeIds } }); - }, [wellUid, wellboreUid, objectType, logType]); + }, [wellUid, wellboreUid, objectType, logType, logName]); } diff --git a/Src/WitsmlExplorer.Frontend/models/logObject.tsx b/Src/WitsmlExplorer.Frontend/models/logObject.tsx index ba473b6b7..3079a12b6 100644 --- a/Src/WitsmlExplorer.Frontend/models/logObject.tsx +++ b/Src/WitsmlExplorer.Frontend/models/logObject.tsx @@ -12,6 +12,7 @@ export default interface LogObject extends ObjectOnWellbore { direction?: string; mnemonics?: string; commonData?: CommonData; + sameNameIndex?: string; } export const indexToNumber = (index: string): number => { diff --git a/Src/WitsmlExplorer.Frontend/models/wellbore.tsx b/Src/WitsmlExplorer.Frontend/models/wellbore.tsx index ff5be9898..2848ab7f4 100644 --- a/Src/WitsmlExplorer.Frontend/models/wellbore.tsx +++ b/Src/WitsmlExplorer.Frontend/models/wellbore.tsx @@ -146,6 +146,25 @@ export const calculateLogTypeTimeId = ( return calculateLogTypeId(wellbore, WITSML_INDEX_TYPE_DATE_TIME); }; +export const calculateMultipleLogsNode = ( + wellbore: Wellbore | { wellUid: string; uid: string }, + logName: string +): string => { + return calculateLogTypeId(wellbore, WITSML_INDEX_TYPE_MD) + `ln=${logName};`; +}; + +export const calculateMultipleLogsNodeItem = ( + wellbore: Wellbore | { wellUid: string; uid: string }, + logName: string, + logUid: string +): string => { + return ( + calculateLogTypeId(wellbore, WITSML_INDEX_TYPE_MD) + + `ln=${logName};` + + `o=${logUid};` + ); +}; + export const calculateObjectNodeId = ( wellbore: Wellbore | { wellUid: string; uid: string }, objectType: ObjectType | string, diff --git a/Src/WitsmlExplorer.Frontend/tools/logSameNamesHelper.tsx b/Src/WitsmlExplorer.Frontend/tools/logSameNamesHelper.tsx new file mode 100644 index 000000000..3e40ddc31 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/tools/logSameNamesHelper.tsx @@ -0,0 +1,16 @@ +import LogObject from "models/logObject"; + +export const getNameOccurrenceSuffix = ( + logObjects: LogObject[], + logObject: LogObject +): string => { + const filteredObjects = logObjects + .filter((obj) => obj.name === logObject.name && !obj.runNumber) + .sort((a, b) => a.uid.localeCompare(b.uid)); + + if (filteredObjects.length > 1) { + const index = filteredObjects.findIndex((obj) => obj.uid === logObject.uid); + return ` [${String.fromCharCode(97 + index)}]`; + } + return ""; +}; From 13fb0e7f57e7042320ed15b42fd0fc434b23aeed Mon Sep 17 00:00:00 2001 From: Jan-Marius Vatle <48485965+janmarius@users.noreply.github.com> Date: Thu, 16 May 2024 15:28:51 +0200 Subject: [PATCH 040/124] FIX-2421 Add temp fix for auto updater error: Object has been destroyed (#2422) --- Src/WitsmlExplorer.Desktop/src/main/main.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Src/WitsmlExplorer.Desktop/src/main/main.ts b/Src/WitsmlExplorer.Desktop/src/main/main.ts index 3ce5d577d..1d91fbcf5 100644 --- a/Src/WitsmlExplorer.Desktop/src/main/main.ts +++ b/Src/WitsmlExplorer.Desktop/src/main/main.ts @@ -18,6 +18,7 @@ import * as path from "path"; // Auto updater settings autoUpdater.autoDownload = false; autoUpdater.autoInstallOnAppQuit = false; +let isUpdateAvailableChecked = false; let apiProcess: any; @@ -294,8 +295,12 @@ if (!gotTheLock) { ipcMain.handle("getAppVersion", () => app.getVersion()); ipcMain.handle("checkForUpdates", () => { - const updateCheckResult = autoUpdater.checkForUpdates(); - return updateCheckResult; + if (!isUpdateAvailableChecked) { + isUpdateAvailableChecked = true; + const updateCheckResult = autoUpdater.checkForUpdates(); + return updateCheckResult; + } + return; }); ipcMain.handle("downloadUpdate", () => { From 759a7be7ed661509a5b4941955172b0030b78e2f Mon Sep 17 00:00:00 2001 From: Robert Basti Date: Tue, 21 May 2024 11:38:17 +0200 Subject: [PATCH 041/124] =?UTF-8?q?Open=20In=20Query=20View=20disabled=20f?= =?UTF-8?q?or=20well/wellbore=20in=20the=20sidebar=F0=9F=90=9B=20#2349=20(?= =?UTF-8?q?#2426)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Elias Bruvik --- .../components/ContextMenus/WellContextMenu.tsx | 3 ++- .../components/ContextMenus/WellboreContextMenu.tsx | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx index dd52c04a2..ae18bece4 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx @@ -283,7 +283,7 @@ const WellContextMenu = (props: WellContextMenuProps): React.ReactElement => { wellUid: well.uid }) } - disabled={checkedWellRows?.length !== 1} + disabled={!!checkedWellRows && checkedWellRows?.length !== 1} > { wellboreUid: uuid() }) } + disabled={!!checkedWellRows && checkedWellRows?.length !== 1} > {Object.values(ObjectType) .filter((objectType) => capObjects.includes(objectType)) @@ -341,6 +346,9 @@ const WellboreContextMenu = ( objectUid: uuid() }) } + disabled={ + !!checkedWellboreRows && checkedWellboreRows.length !== 1 + } > Date: Tue, 21 May 2024 11:58:09 +0200 Subject: [PATCH 042/124] =?UTF-8?q?=F0=9F=91=89Copy/pasting=20order=20when?= =?UTF-8?q?=20selecting=20paste=20#2416=20(#2428)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/Modals/CopyMnemonicsModal.tsx | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/CopyMnemonicsModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/CopyMnemonicsModal.tsx index c3cb5c178..7792a1695 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/CopyMnemonicsModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/CopyMnemonicsModal.tsx @@ -48,7 +48,7 @@ const CopyMnemonicsModal = ( const { dispatchOperation } = useContext(OperationContext); const [selectedCopyMnemonicsType, setCopyMnemonicsType] = useState( - CopyMnemonicsType.DeleteInsert + CopyMnemonicsType.Paste ); const [isLoading, setIsLoading] = useState(false); @@ -156,26 +156,6 @@ const CopyMnemonicsModal = ( Choose paste option: - -
- - setCopyMnemonicsType(CopyMnemonicsType.DeleteInsert) - } - /> -
-
- Delete/Insert - - Delete target mnemonics before copying. The mnemonics will - become equal on the source and target server afterwards. - -
-
+ +
+ + setCopyMnemonicsType(CopyMnemonicsType.DeleteInsert) + } + /> +
+
+ Delete/Insert + + Delete target mnemonics before copying. The mnemonics will + become equal on the source and target server afterwards. + +
+
} onSubmit={() => onSubmit()} From ba90928160b7f6d2bf5caf76bd8802577b8b8916 Mon Sep 17 00:00:00 2001 From: Robert Basti Date: Tue, 21 May 2024 12:01:56 +0200 Subject: [PATCH 043/124] =?UTF-8?q?Report=20button=20shows=20for=20all=20f?= =?UTF-8?q?inished=20jobs=20in=20the=20jobs=20view=F0=9F=90=9B=20#2410=20(?= =?UTF-8?q?#2427)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Src/WitsmlExplorer.Api/Jobs/JobInfo.cs | 7 ++++++- .../components/ContentViews/JobsView.tsx | 3 ++- Src/WitsmlExplorer.Frontend/models/reportType.ts | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Src/WitsmlExplorer.Api/Jobs/JobInfo.cs b/Src/WitsmlExplorer.Api/Jobs/JobInfo.cs index 4184d2dd0..c5b7b3e51 100644 --- a/Src/WitsmlExplorer.Api/Jobs/JobInfo.cs +++ b/Src/WitsmlExplorer.Api/Jobs/JobInfo.cs @@ -79,6 +79,10 @@ public ReportType ReportType { get { + if (Report == null) + { + return ReportType.None; + } if (Report?.DownloadImmediately == true) { return ReportType.File; @@ -99,6 +103,7 @@ public enum JobStatus public enum ReportType { File, - Report + Report, + None } } diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/JobsView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/JobsView.tsx index 817af957d..d8bf57924 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/JobsView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/JobsView.tsx @@ -168,7 +168,8 @@ export const JobsView = (): React.ReactElement => { targetServer: serverUrlToName(servers, jobInfo.targetServer), sourceServer: serverUrlToName(servers, jobInfo.sourceServer), report: - jobInfo.status === JobStatus.Finished ? ( + jobInfo.status === JobStatus.Finished && + jobInfo.reportType !== ReportType.None ? ( onClickReport(jobInfo.id)}> {jobInfo.reportType === ReportType.File ? "Download File" diff --git a/Src/WitsmlExplorer.Frontend/models/reportType.ts b/Src/WitsmlExplorer.Frontend/models/reportType.ts index 0e25d92c5..56c349158 100644 --- a/Src/WitsmlExplorer.Frontend/models/reportType.ts +++ b/Src/WitsmlExplorer.Frontend/models/reportType.ts @@ -1,6 +1,7 @@ enum ReportType { Report = "Report", - File = "File" + File = "File", + None = "None" } export default ReportType; From f1c87a174073106c345e4e107db51f88c1d05730 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Wed, 22 May 2024 08:42:15 +0200 Subject: [PATCH 044/124] FIX-2419 Show notifications from both source and target server (#2425) --- Src/WitsmlExplorer.Api/Workers/BaseWorker.cs | 6 ++++-- .../Workers/CompareLogDataWorker.cs | 4 ++-- .../Workers/Copy/CopyComponentsWorker.cs | 7 +++++-- .../Workers/Copy/CopyLogDataWorker.cs | 6 +++--- .../Workers/Copy/CopyLogWorker.cs | 10 +++++----- .../Workers/Copy/CopyObjectsWorker.cs | 9 +++++---- Src/WitsmlExplorer.Api/Workers/Copy/CopyUtils.cs | 10 +++++----- .../Workers/Copy/CopyWellWorker.cs | 10 ++++------ .../Workers/Copy/CopyWellboreWorker.cs | 10 ++++------ Src/WitsmlExplorer.Api/Workers/WorkerResult.cs | 4 +++- Src/WitsmlExplorer.Frontend/components/Alerts.tsx | 14 +++++++++++--- .../components/Snackbar.tsx | 13 +++++++++++-- .../services/jobService.tsx | 7 +++++-- .../services/notificationService.ts | 1 + 14 files changed, 68 insertions(+), 43 deletions(-) diff --git a/Src/WitsmlExplorer.Api/Workers/BaseWorker.cs b/Src/WitsmlExplorer.Api/Workers/BaseWorker.cs index a251ca922..b41723bcb 100644 --- a/Src/WitsmlExplorer.Api/Workers/BaseWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/BaseWorker.cs @@ -65,14 +65,16 @@ protected IWitsmlClient GetSourceWitsmlClientOrThrow() job.JobInfo.Status = JobStatus.Cancelled; job.JobInfo.FailedReason = ex.Message; Logger.LogError("{jobType} was cancelled.", job.JobInfo.JobType); - return (new WorkerResult(new Uri(job.JobInfo.TargetServer), false, $"{job.JobInfo.JobType} cancelled", ex.Message, jobId: job.JobInfo.Id), null); + Uri sourceServerUrl = job.JobInfo.SourceServer != null ? new Uri(job.JobInfo.SourceServer) : null; + return (new WorkerResult(new Uri(job.JobInfo.TargetServer), false, $"{job.JobInfo.JobType} cancelled", ex.Message, jobId: job.JobInfo.Id, sourceServerUrl: sourceServerUrl), null); } catch (Exception ex) { job.JobInfo.Status = JobStatus.Failed; job.JobInfo.FailedReason = ex.Message; Logger.LogError("An unexpected exception has occured during {jobType}: {ex}", job.JobInfo.JobType, ex); - return (new WorkerResult(new Uri(job.JobInfo.TargetServer), false, $"{job.JobInfo.JobType} failed", ex.Message, jobId: job.JobInfo.Id), null); + Uri sourceServerUrl = job.JobInfo.SourceServer != null ? new Uri(job.JobInfo.SourceServer) : null; + return (new WorkerResult(new Uri(job.JobInfo.TargetServer), false, $"{job.JobInfo.JobType} failed", ex.Message, jobId: job.JobInfo.Id, sourceServerUrl: sourceServerUrl), null); } } diff --git a/Src/WitsmlExplorer.Api/Workers/CompareLogDataWorker.cs b/Src/WitsmlExplorer.Api/Workers/CompareLogDataWorker.cs index 3d61b7dc0..eeca7da31 100644 --- a/Src/WitsmlExplorer.Api/Workers/CompareLogDataWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/CompareLogDataWorker.cs @@ -127,11 +127,11 @@ public CompareLogDataWorker(ILogger logger, IWitsmlClientProv { string message = $"Compared log data for log: '{sourceLog.Name}' and '{targetLog.Name}'"; Logger.LogError(message); - return (new WorkerResult(GetSourceWitsmlClientOrThrow().GetServerHostname(), false, message, e.Message, jobId: job.JobInfo.Id), null); + return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, message, e.Message, jobId: job.JobInfo.Id, sourceServerUrl: GetSourceWitsmlClientOrThrow().GetServerHostname()), null); } Logger.LogInformation("{JobType} - Job successful", GetType().Name); - WorkerResult workerResult = new(GetSourceWitsmlClientOrThrow().GetServerHostname(), true, $"Compared log data for log: '{sourceLog.Name}' and '{targetLog.Name}'", jobId: job.JobInfo.Id); + WorkerResult workerResult = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), true, $"Compared log data for log: '{sourceLog.Name}' and '{targetLog.Name}'", jobId: job.JobInfo.Id, sourceServerUrl: GetSourceWitsmlClientOrThrow().GetServerHostname()); return (workerResult, null); } diff --git a/Src/WitsmlExplorer.Api/Workers/Copy/CopyComponentsWorker.cs b/Src/WitsmlExplorer.Api/Workers/Copy/CopyComponentsWorker.cs index 552b7d28b..ff3dd800d 100644 --- a/Src/WitsmlExplorer.Api/Workers/Copy/CopyComponentsWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/Copy/CopyComponentsWorker.cs @@ -29,6 +29,7 @@ public class CopyComponentsWorker : BaseWorker, IWorker, ICop private CopyComponentsJob _job; private Uri _targetHostname; + private Uri _sourceHostname; private ComponentType _componentType; private string _errorMessage; private readonly ICopyLogDataWorker _copyLogDataWorker; @@ -52,6 +53,8 @@ public CopyComponentsWorker(ILogger logger, IWitsmlClientProv _job = job; IWitsmlClient targetClient = GetTargetWitsmlClientOrThrow(); _targetHostname = targetClient.GetServerHostname(); + IWitsmlClient sourceClient = GetSourceWitsmlClientOrThrow(); + _sourceHostname = sourceClient.GetServerHostname(); _componentType = job.Source.ComponentType; _errorMessage = $"Failed to copy {_componentType.ToPluralLowercase()}."; @@ -88,7 +91,7 @@ public CopyComponentsWorker(ILogger logger, IWitsmlClientProv Logger.LogInformation("{JobType} - Job successful. {Description}", GetType().Name, job.Description()); RefreshObjects refreshAction = new(_targetHostname, job.Target.WellUid, job.Target.WellboreUid, _componentType.ToParentType(), job.Target.Uid); - WorkerResult workerResult = new(_targetHostname, true, $"Components {string.Join(", ", toCopyUids)} copied to: {job.Target.Name}"); + WorkerResult workerResult = new(_targetHostname, true, $"Components {string.Join(", ", toCopyUids)} copied to: {job.Target.Name}", sourceServerUrl: _sourceHostname); return (workerResult, refreshAction); } @@ -118,7 +121,7 @@ private async Task VerifyTarget() private (WorkerResult, RefreshAction) LogErrorAndReturnResult(string reason) { Logger.LogError("{errorMessage} {reason} - {description}", _errorMessage, reason, _job.Description()); - return (new WorkerResult(_targetHostname, false, _errorMessage, reason), null); + return (new WorkerResult(_targetHostname, false, _errorMessage, reason, sourceServerUrl: _sourceHostname), null); } } } diff --git a/Src/WitsmlExplorer.Api/Workers/Copy/CopyLogDataWorker.cs b/Src/WitsmlExplorer.Api/Workers/Copy/CopyLogDataWorker.cs index 3c80aebbc..434846144 100644 --- a/Src/WitsmlExplorer.Api/Workers/Copy/CopyLogDataWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/Copy/CopyLogDataWorker.cs @@ -69,7 +69,7 @@ public CopyLogDataWorker(IWitsmlClientProvider witsmlClientProvider, ILogger CopyLogData(WitsmlLog sourceLog, WitsmlLog targetLog, CopyLogDataJob job, List mnemonics, int sourceDepthLogDecimals, int targetDepthLogDecimals) diff --git a/Src/WitsmlExplorer.Api/Workers/Copy/CopyLogWorker.cs b/Src/WitsmlExplorer.Api/Workers/Copy/CopyLogWorker.cs index 93b780a4f..f99271100 100644 --- a/Src/WitsmlExplorer.Api/Workers/Copy/CopyLogWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/Copy/CopyLogWorker.cs @@ -49,12 +49,12 @@ public CopyLogWorker(ILogger logger, IWitsmlClientProvider witsm if (copyLogTasksResult.Status == TaskStatus.Faulted) { Logger.LogError("{ErrorMessage} - {JobDescription}", errorMessage, job.Description()); - return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, errorMessage), null); + return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, errorMessage, sourceServerUrl: GetSourceWitsmlClientOrThrow().GetServerHostname()), null); } if (results.Any((result) => !result.IsSuccessful)) { Logger.LogError("{ErrorMessage} - {JobDescription}", errorMessage, job.Description()); - return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, errorMessage, copyLogTasks.First((task) => !task.Result.IsSuccessful).Result.Reason), null); + return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, errorMessage, copyLogTasks.First((task) => !task.Result.IsSuccessful).Result.Reason, sourceServerUrl: GetSourceWitsmlClientOrThrow().GetServerHostname()), null); } ConcurrentDictionary progressDict = new ConcurrentDictionary(); @@ -67,7 +67,7 @@ public CopyLogWorker(ILogger logger, IWitsmlClientProvider witsm if (copyLogDataResultTask.Status == TaskStatus.Faulted) { Logger.LogError("{ErrorMessage} - {JobDescription}", errorMessage, job.Description()); - return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, errorMessage), null); + return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, errorMessage, sourceServerUrl: GetSourceWitsmlClientOrThrow().GetServerHostname()), null); } int failedCopyDataTasks = copyLogDataResultTask.Result.Count((task) => task.Result.IsSuccess == false); @@ -76,13 +76,13 @@ public CopyLogWorker(ILogger logger, IWitsmlClientProvider witsm (WorkerResult Result, RefreshAction) firstFailedTask = copyLogDataResultTask.Result.First((task) => task.Result.IsSuccess == false); errorMessage = $"Failed to copy log data for {failedCopyDataTasks} out of {copyLogDataTasks.Count()} logs."; Logger.LogError("{ErrorMessage} - {JobDescription}", errorMessage, job.Description()); - return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, errorMessage, firstFailedTask.Result.Reason), null); + return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, errorMessage, firstFailedTask.Result.Reason, sourceServerUrl: GetSourceWitsmlClientOrThrow().GetServerHostname()), null); } Logger.LogInformation("{JobType} - Job successful. {Description}", GetType().Name, job.Description()); RefreshObjects refreshAction = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), job.Target.WellUid, job.Target.WellboreUid, EntityType.Log); string copiedLogsMessage = (sourceLogs.Length == 1 ? $"Copied log object {sourceLogs[0].Name}" : $"Copied {sourceLogs.Length} logs") + $" to: {targetWellbore.Name}"; - WorkerResult workerResult = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), true, copiedLogsMessage); + WorkerResult workerResult = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), true, copiedLogsMessage, sourceServerUrl: GetSourceWitsmlClientOrThrow().GetServerHostname()); return (workerResult, refreshAction); } diff --git a/Src/WitsmlExplorer.Api/Workers/Copy/CopyObjectsWorker.cs b/Src/WitsmlExplorer.Api/Workers/Copy/CopyObjectsWorker.cs index befb3788a..3409b0d69 100644 --- a/Src/WitsmlExplorer.Api/Workers/Copy/CopyObjectsWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/Copy/CopyObjectsWorker.cs @@ -44,8 +44,9 @@ public CopyObjectsWorker(ILogger logger, IWitsmlClientProvider w private async Task<(WorkerResult, RefreshAction)> GenericCopy(CopyObjectsJob job) { Witsml.IWitsmlClient targetClient = GetTargetWitsmlClientOrThrow(); + Witsml.IWitsmlClient sourceClient = GetSourceWitsmlClientOrThrow(); IWitsmlObjectList fetchObjectsQuery = ObjectQueries.GetWitsmlObjectsByIds(job.Source.WellUid, job.Source.WellboreUid, job.Source.ObjectUids, job.Source.ObjectType); - Task fetchObjectsTask = GetSourceWitsmlClientOrThrow().GetFromStoreNullableAsync(fetchObjectsQuery, new OptionsIn(ReturnElements.All)); + Task fetchObjectsTask = sourceClient.GetFromStoreNullableAsync(fetchObjectsQuery, new OptionsIn(ReturnElements.All)); Task fetchWellboreTask = WorkerTools.GetWellbore(targetClient, job.Target, retry: true); await Task.WhenAll(fetchObjectsTask, fetchWellboreTask); IWitsmlObjectList objectsToCopy = fetchObjectsTask.Result; @@ -53,16 +54,16 @@ public CopyObjectsWorker(ILogger logger, IWitsmlClientProvider w if (objectsToCopy == null) { - return (new WorkerResult(targetClient.GetServerHostname(), false, "Failed to deserialize response from Witsml server when fetching objects to copy"), null); + return (new WorkerResult(targetClient.GetServerHostname(), false, "Failed to deserialize response from Witsml server when fetching objects to copy", sourceServerUrl: sourceClient.GetServerHostname()), null); } if (!objectsToCopy.Objects.Any()) { - return (new WorkerResult(targetClient.GetServerHostname(), false, "Could not find any objects to copy"), null); + return (new WorkerResult(targetClient.GetServerHostname(), false, "Could not find any objects to copy", sourceServerUrl: sourceClient.GetServerHostname()), null); } ICollection queries = ObjectQueries.CopyObjectsQuery(objectsToCopy.Objects, targetWellbore); RefreshObjects refreshAction = new(targetClient.GetServerHostname(), job.Target.WellUid, job.Target.WellboreUid, job.Source.ObjectType); - return await _copyUtils.CopyObjectsOnWellbore(targetClient, queries, refreshAction, job.Source.WellUid, job.Source.WellboreUid); + return await _copyUtils.CopyObjectsOnWellbore(targetClient, sourceClient, queries, refreshAction, job.Source.WellUid, job.Source.WellboreUid); } } } diff --git a/Src/WitsmlExplorer.Api/Workers/Copy/CopyUtils.cs b/Src/WitsmlExplorer.Api/Workers/Copy/CopyUtils.cs index 1ba1c98fc..6ef134049 100644 --- a/Src/WitsmlExplorer.Api/Workers/Copy/CopyUtils.cs +++ b/Src/WitsmlExplorer.Api/Workers/Copy/CopyUtils.cs @@ -15,7 +15,7 @@ namespace WitsmlExplorer.Api.Workers.Copy public interface ICopyUtils { - public Task<(WorkerResult, RefreshAction)> CopyObjectsOnWellbore(IWitsmlClient witsmlClient, IEnumerable queries, RefreshAction refreshAction, string sourceWellUid, string sourceWellboreUid); + public Task<(WorkerResult, RefreshAction)> CopyObjectsOnWellbore(IWitsmlClient targetClient, IWitsmlClient sourceClient, IEnumerable queries, RefreshAction refreshAction, string sourceWellUid, string sourceWellboreUid); } public class CopyUtils : ICopyUtils @@ -27,7 +27,7 @@ public CopyUtils(ILogger logger) _logger = logger; } - public async Task<(WorkerResult, RefreshAction)> CopyObjectsOnWellbore(IWitsmlClient witsmlClient, IEnumerable queries, RefreshAction refreshAction, string sourceWellUid, string sourceWellboreUid) + public async Task<(WorkerResult, RefreshAction)> CopyObjectsOnWellbore(IWitsmlClient targetClient, IWitsmlClient sourceClient, IEnumerable queries, RefreshAction refreshAction, string sourceWellUid, string sourceWellboreUid) { bool error = false; List successUids = new(); @@ -37,7 +37,7 @@ public CopyUtils(ILogger logger) { try { - QueryResult result = await witsmlClient.AddToStoreAsync(query.AsItemInWitsmlList()); + QueryResult result = await targetClient.AddToStoreAsync(query.AsItemInWitsmlList()); if (result.IsSuccessful) { _logger.LogInformation( @@ -78,8 +78,8 @@ public CopyUtils(ILogger logger) var typeName = queries.FirstOrDefault()?.GetType().Name; string successString = successUids.Count > 0 ? $"Copied {typeName}s: {string.Join(", ", successUids)}." : ""; return !error - ? (new WorkerResult(witsmlClient.GetServerHostname(), true, successString), refreshAction) - : (new WorkerResult(witsmlClient.GetServerHostname(), false, $"{successString} Failed to copy some {typeName}s", errorReason, errorEntity), successUids.Count > 0 ? refreshAction : null); + ? (new WorkerResult(targetClient.GetServerHostname(), true, successString, sourceServerUrl: sourceClient.GetServerHostname()), refreshAction) + : (new WorkerResult(targetClient.GetServerHostname(), false, $"{successString} Failed to copy some {typeName}s", errorReason, errorEntity, sourceServerUrl: sourceClient.GetServerHostname()), successUids.Count > 0 ? refreshAction : null); } } } diff --git a/Src/WitsmlExplorer.Api/Workers/Copy/CopyWellWorker.cs b/Src/WitsmlExplorer.Api/Workers/Copy/CopyWellWorker.cs index cc8714342..ae161d6b0 100644 --- a/Src/WitsmlExplorer.Api/Workers/Copy/CopyWellWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/Copy/CopyWellWorker.cs @@ -35,7 +35,7 @@ public CopyWellWorker(ILogger logger, IWitsmlClientProvider witsmlC { string message = "Target well already exists"; Logger.LogWarning("{WarningMessage} - {JobDescription}", message, job.Description()); - return (new WorkerResult(targetClient.GetServerHostname(), true, message), null); + return (new WorkerResult(targetClient.GetServerHostname(), true, message, sourceServerUrl: sourceClient.GetServerHostname()), null); } WitsmlWell sourceWell = await WorkerTools.GetWell(sourceClient, job.Source, Witsml.ServiceReference.ReturnElements.All); @@ -45,7 +45,7 @@ public CopyWellWorker(ILogger logger, IWitsmlClientProvider witsmlC if (sourceWell == null) { Logger.LogError("{ErrorMessage} - {JobDescription}", errorMessage, job.Description()); - return (new WorkerResult(targetClient.GetServerHostname(), false, errorMessage), null); + return (new WorkerResult(targetClient.GetServerHostname(), false, errorMessage, sourceServerUrl: sourceClient.GetServerHostname()), null); } // May be the same UID and name or a different one @@ -59,14 +59,12 @@ public CopyWellWorker(ILogger logger, IWitsmlClientProvider witsmlC if (!result.IsSuccessful) { Logger.LogError("{ErrorMessage} {Reason} - {JobDescription}", errorMessage, result.Reason, job.Description()); - return (new WorkerResult(targetClient.GetServerHostname(), false, errorMessage, result.Reason), null); + return (new WorkerResult(targetClient.GetServerHostname(), false, errorMessage, result.Reason, sourceServerUrl: sourceClient.GetServerHostname()), null); } Logger.LogInformation("{JobType} - Job successful. {Description}", GetType().Name, job.Description()); - WorkerResult workerResult = new(targetClient.GetServerHostname(), - true, - $"Successfully copied well: {job.Source.WellUid} -> {job.Target.WellUid}"); + WorkerResult workerResult = new(targetClient.GetServerHostname(), true, $"Successfully copied well: {job.Source.WellUid} -> {job.Target.WellUid}", sourceServerUrl: sourceClient.GetServerHostname()); RefreshWell refreshAction = new(targetClient.GetServerHostname(), job.Target.WellUid, RefreshType.Add); diff --git a/Src/WitsmlExplorer.Api/Workers/Copy/CopyWellboreWorker.cs b/Src/WitsmlExplorer.Api/Workers/Copy/CopyWellboreWorker.cs index 2b6564dad..a7bca2074 100644 --- a/Src/WitsmlExplorer.Api/Workers/Copy/CopyWellboreWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/Copy/CopyWellboreWorker.cs @@ -35,7 +35,7 @@ public CopyWellboreWorker(ILogger logger, IWitsmlClientProvider { string message = "Target wellbore already exists"; Logger.LogWarning("{WarningMessage} - {JobDescription}", message, job.Description()); - return (new WorkerResult(targetClient.GetServerHostname(), true, message), null); + return (new WorkerResult(targetClient.GetServerHostname(), true, message, sourceServerUrl: sourceClient.GetServerHostname()), null); } WitsmlWellbore sourceWellbore = await WorkerTools.GetWellbore(sourceClient, job.Source, Witsml.ServiceReference.ReturnElements.All); @@ -45,7 +45,7 @@ public CopyWellboreWorker(ILogger logger, IWitsmlClientProvider if (sourceWellbore == null) { Logger.LogError("{ErrorMessage} - {JobDescription}", errorMessage, job.Description()); - return (new WorkerResult(targetClient.GetServerHostname(), false, errorMessage), null); + return (new WorkerResult(targetClient.GetServerHostname(), false, errorMessage, sourceServerUrl: sourceClient.GetServerHostname()), null); } // May be the same UID and name or a different one @@ -61,14 +61,12 @@ public CopyWellboreWorker(ILogger logger, IWitsmlClientProvider if (!result.IsSuccessful) { Logger.LogError("{ErrorMessage} {Reason} - {JobDescription}", errorMessage, result.Reason, job.Description()); - return (new WorkerResult(targetClient.GetServerHostname(), false, errorMessage, result.Reason), null); + return (new WorkerResult(targetClient.GetServerHostname(), false, errorMessage, result.Reason, sourceServerUrl: sourceClient.GetServerHostname()), null); } Logger.LogInformation("{JobType} - Job successful. {Description}", GetType().Name, job.Description()); - WorkerResult workerResult = new(targetClient.GetServerHostname(), - true, - $"Successfully copied wellbore: {job.Source.WellboreUid} -> {job.Target.WellboreUid}"); + WorkerResult workerResult = new(targetClient.GetServerHostname(), true, $"Successfully copied wellbore: {job.Source.WellboreUid} -> {job.Target.WellboreUid}", sourceServerUrl: sourceClient.GetServerHostname()); RefreshWellbore refreshAction = new(targetClient.GetServerHostname(), job.Target.WellUid, job.Target.WellboreUid, RefreshType.Add); diff --git a/Src/WitsmlExplorer.Api/Workers/WorkerResult.cs b/Src/WitsmlExplorer.Api/Workers/WorkerResult.cs index 46c23fd74..30cf0afd5 100644 --- a/Src/WitsmlExplorer.Api/Workers/WorkerResult.cs +++ b/Src/WitsmlExplorer.Api/Workers/WorkerResult.cs @@ -4,9 +4,10 @@ namespace WitsmlExplorer.Api.Workers { public class WorkerResult { - public WorkerResult(Uri serverUrl, bool isSuccess, string message, string reason = null, EntityDescription description = null, string jobId = null) + public WorkerResult(Uri serverUrl, bool isSuccess, string message, string reason = null, EntityDescription description = null, string jobId = null, Uri sourceServerUrl = null) { ServerUrl = serverUrl; + SourceServerUrl = sourceServerUrl; IsSuccess = isSuccess; Message = message; Reason = reason; @@ -15,6 +16,7 @@ public WorkerResult(Uri serverUrl, bool isSuccess, string message, string reason } public Uri ServerUrl { get; } + public Uri SourceServerUrl { get; } public bool IsSuccess { get; } public string Message { get; } public string Reason { get; } diff --git a/Src/WitsmlExplorer.Frontend/components/Alerts.tsx b/Src/WitsmlExplorer.Frontend/components/Alerts.tsx index 530855f07..b8de98cf2 100644 --- a/Src/WitsmlExplorer.Frontend/components/Alerts.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Alerts.tsx @@ -40,10 +40,18 @@ const Alerts = (): React.ReactElement => { const unsubscribeOnJobFinished = NotificationService.Instance.alertDispatcherAsEvent.subscribe( (notification) => { + const connectedServerUrl = connectedServer?.url?.toLowerCase(); + const notificationServerUrl = notification.serverUrl + ?.toString() + .toLowerCase(); + const notificationSourceServerUrl = notification.sourceServerUrl + ?.toString() + .toLowerCase(); const shouldNotify = - notification.serverUrl == null || - notification.serverUrl.toString().toLowerCase() === - connectedServer?.url?.toLowerCase(); + connectedServerUrl && + (notificationServerUrl === null || + connectedServerUrl === notificationServerUrl || + connectedServerUrl === notificationSourceServerUrl); if (!shouldNotify) { return; } diff --git a/Src/WitsmlExplorer.Frontend/components/Snackbar.tsx b/Src/WitsmlExplorer.Frontend/components/Snackbar.tsx index f23e430f0..139c90e99 100644 --- a/Src/WitsmlExplorer.Frontend/components/Snackbar.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Snackbar.tsx @@ -11,9 +11,18 @@ export function Snackbar() { const unsubscribe = NotificationService.Instance.snackbarDispatcherAsEvent.subscribe( (notification) => { + const connectedServerUrl = connectedServer?.url?.toLowerCase(); + const notificationServerUrl = notification.serverUrl + ?.toString() + .toLowerCase(); + const notificationSourceServerUrl = notification.sourceServerUrl + ?.toString() + .toLowerCase(); const shouldNotify = - notification.serverUrl.toString().toLowerCase() === - connectedServer?.url?.toLowerCase(); + connectedServerUrl && + (notificationServerUrl === null || + connectedServerUrl === notificationServerUrl || + connectedServerUrl === notificationSourceServerUrl); if (shouldNotify) { enqueueSnackbar(notification.message, { variant: notification.isSuccess ? "success" : "error" diff --git a/Src/WitsmlExplorer.Frontend/services/jobService.tsx b/Src/WitsmlExplorer.Frontend/services/jobService.tsx index 60f5a3a4a..2b48bf1fa 100644 --- a/Src/WitsmlExplorer.Frontend/services/jobService.tsx +++ b/Src/WitsmlExplorer.Frontend/services/jobService.tsx @@ -30,18 +30,20 @@ export default class JobService { targetServer, sourceServer ); - return this.onResponse(jobType, response, targetServer); + return this.onResponse(jobType, response, targetServer, sourceServer); } private static async onResponse( jobType: JobType, response: Response, - server = AuthorizationService.selectedServer + server = AuthorizationService.selectedServer, + sourceServer: Server = null ): Promise { AuthorizationService.resetSourceServer(); if (response.ok) { NotificationService.Instance.snackbarDispatcher.dispatch({ serverUrl: new URL(server?.url), + sourceServerUrl: sourceServer ? new URL(sourceServer?.url) : null, message: `Ordered ${jobType} job`, isSuccess: true }); @@ -49,6 +51,7 @@ export default class JobService { } else { NotificationService.Instance.snackbarDispatcher.dispatch({ serverUrl: new URL(server?.url), + sourceServerUrl: sourceServer ? new URL(sourceServer?.url) : null, message: `Failed ordering ${jobType} job`, isSuccess: false }); diff --git a/Src/WitsmlExplorer.Frontend/services/notificationService.ts b/Src/WitsmlExplorer.Frontend/services/notificationService.ts index d053b8591..e05688d48 100644 --- a/Src/WitsmlExplorer.Frontend/services/notificationService.ts +++ b/Src/WitsmlExplorer.Frontend/services/notificationService.ts @@ -8,6 +8,7 @@ import ObjectOnWellbore from "../models/objectOnWellbore"; export interface Notification { serverUrl: URL; + sourceServerUrl?: URL; isSuccess: boolean; message: string; severity?: AlertSeverity; From 9c117649a26f9952e86ceb9fe3e40ac94f6966d3 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Wed, 22 May 2024 13:01:23 +0200 Subject: [PATCH 045/124] FIX-2411 Handle timeZone in curveValuesView (#2415) --- .../ContentViews/CurveValuesView.tsx | 14 +- .../ContentViews/EditSelectedLogCurveInfo.tsx | 91 +++++++++---- .../components/DateFormatter.ts | 3 + .../Modals/LogHeaderDateTimeField.tsx | 124 ++++++++---------- .../TrimLogObject/AdjustDateTimeModal.tsx | 51 ++----- .../contexts/operationStateReducer.tsx | 3 +- 6 files changed, 148 insertions(+), 138 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx index c33927393..75488264b 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx @@ -330,7 +330,7 @@ export const CurveValuesView = (): React.ReactElement => { columnOf: curveSpecification, property: curveSpecification.mnemonic, label: `${curveSpecification.mnemonic} (${curveSpecification.unit})`, - type: getColumnType(curveSpecification) + type: getColumnType(curveSpecification, log) }; }); const prevMnemonics = columns.map((column) => column.property); @@ -742,7 +742,17 @@ const getComparatorByColumn = ( return [comparator, column.property]; }; -const getColumnType = (curveSpecification: CurveSpecification) => { +const getColumnType = ( + curveSpecification: CurveSpecification, + log: LogObject +) => { + const isTimeLog = log.indexType === WITSML_INDEX_TYPE_DATE_TIME; + if ( + isTimeLog && + curveSpecification.mnemonic.toLowerCase() === log.indexCurve.toLowerCase() + ) { + return ContentType.DateTime; + } const isTimeMnemonic = (mnemonic: string) => ["time", "datetime", "date time"].indexOf(mnemonic.toLowerCase()) >= 0; if (isTimeMnemonic(curveSpecification.mnemonic)) { diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/EditSelectedLogCurveInfo.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/EditSelectedLogCurveInfo.tsx index ba53cdd39..19b47946a 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/EditSelectedLogCurveInfo.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/EditSelectedLogCurveInfo.tsx @@ -5,11 +5,16 @@ import { Label, TextField } from "@equinor/eds-core-react"; +import formatDateString, { + dateTimeFormatTextField, + getOffset, + getOffsetFromTimeZone +} from "components/DateFormatter"; import { Button } from "components/StyledComponents/Button"; import { useConnectedServer } from "contexts/connectedServerContext"; import OperationContext from "contexts/operationContext"; +import { DateTimeFormat, TimeZone } from "contexts/operationStateReducer"; import { isValid, parse } from "date-fns"; -import { format } from "date-fns-tz"; import { useGetComponents } from "hooks/query/useGetComponents"; import { useGetMnemonics } from "hooks/useGetMnemonics"; import { ComponentType } from "models/componentType"; @@ -41,15 +46,13 @@ interface EditSelectedLogCurveInfoProps { onClickRefresh?: () => void; } -const dateTimeFormat = "yyyy-MM-dd'T'HH:mm:ss"; - const EditSelectedLogCurveInfo = ( props: EditSelectedLogCurveInfoProps ): React.ReactElement => { const { disabled, overrideStartIndex, overrideEndIndex, onClickRefresh } = props; const { operationState } = useContext(OperationContext); - const { theme, colors } = operationState; + const { theme, colors, timeZone } = operationState; const { wellUid, wellboreUid, logType, objectUid } = useParams(); const isTimeLog = logType === RouterLogType.TIME; const { connectedServer } = useConnectedServer(); @@ -66,12 +69,9 @@ const EditSelectedLogCurveInfo = ( const mnemonicsSearchParams = searchParams.get("mnemonics"); const startIndex = searchParams.get("startIndex"); const endIndex = searchParams.get("endIndex"); - const [selectedStartIndex, setSelectedStartIndex] = useState( - getParsedValue(startIndex, isTimeLog) - ); - const [selectedEndIndex, setSelectedEndIndex] = useState( - getParsedValue(endIndex, isTimeLog) - ); + const [selectedStartIndex, setSelectedStartIndex] = + useState(startIndex); + const [selectedEndIndex, setSelectedEndIndex] = useState(endIndex); const [isEdited, setIsEdited] = useState(false); const [isValidStart, setIsValidStart] = useState(true); const [isValidEnd, setIsValidEnd] = useState(true); @@ -80,15 +80,13 @@ const EditSelectedLogCurveInfo = ( useGetMnemonics(isFetching, logCurveInfo, mnemonicsSearchParams); useEffect(() => { - setSelectedStartIndex(getParsedValue(startIndex, isTimeLog)); - setSelectedEndIndex(getParsedValue(endIndex, isTimeLog)); + setSelectedStartIndex(startIndex); + setSelectedEndIndex(endIndex); }, [startIndex, endIndex]); useEffect(() => { - if (overrideStartIndex) - setSelectedStartIndex(getParsedValue(overrideStartIndex, isTimeLog)); - if (overrideEndIndex) - setSelectedEndIndex(getParsedValue(overrideEndIndex, isTimeLog)); + if (overrideStartIndex) setSelectedStartIndex(overrideStartIndex); + if (overrideEndIndex) setSelectedEndIndex(overrideEndIndex); }, [overrideStartIndex, overrideEndIndex]); const submitLogCurveInfo = () => { @@ -127,11 +125,18 @@ const EditSelectedLogCurveInfo = ( const onTextFieldChange = ( e: ChangeEvent, setIndex: Dispatch>, - setIsValid: Dispatch> + setIsValid: Dispatch>, + index: string ) => { if (isTimeLog) { - if (isValid(parseDate(e.target.value))) { - setIndex(e.target.value); + const isMissingSeconds = e.target.value.split(":")?.length === 2; + const value = isMissingSeconds ? `${e.target.value}:00` : e.target.value; + if (isValid(parseDate(value))) { + const offset = + timeZone === TimeZone.Raw + ? getOffset(index) + : getOffsetFromTimeZone(timeZone); + setIndex(getUtcValue(value, isTimeLog, offset)); setIsEdited(true); setIsValid(true); } else { @@ -161,12 +166,19 @@ const EditSelectedLogCurveInfo = ( ) => { - onTextFieldChange(e, setSelectedStartIndex, setIsValidStart); + onTextFieldChange( + e, + setSelectedStartIndex, + setIsValidStart, + startIndex + ); }} /> @@ -175,12 +187,19 @@ const EditSelectedLogCurveInfo = ( ) => { - onTextFieldChange(e, setSelectedEndIndex, setIsValidEnd); + onTextFieldChange( + e, + setSelectedEndIndex, + setIsValidEnd, + endIndex + ); }} /> @@ -226,18 +245,34 @@ const EditSelectedLogCurveInfo = ( }; const parseDate = (current: string) => { - return parse(current, dateTimeFormat, new Date()); + return parse(current, dateTimeFormatTextField, new Date()); }; -const getParsedValue = (input: string, isTimeLog: boolean) => { +const getParsedValue = ( + input: string, + isTimeLog: boolean, + timeZone: TimeZone +) => { if (!input) return null; return isTimeLog - ? parseDate(input) - ? format(new Date(input), dateTimeFormat) - : "" + ? formatDateString(input, timeZone, DateTimeFormat.RawNoOffset) : input; }; +const getUtcValue = (input: string, isTimeLog: boolean, offset: string) => { + if (!input) return null; + if (isTimeLog) { + const inputWithZone = input + offset; + const utcInput = formatDateString( + inputWithZone, + TimeZone.Utc, + DateTimeFormat.Raw + ); + return utcInput; + } + return input; +}; + const StyledAutocomplete = styled(Autocomplete)<{ colors: Colors }>` button { color: ${(props) => props.colors.infographic.primaryMossGreen}; diff --git a/Src/WitsmlExplorer.Frontend/components/DateFormatter.ts b/Src/WitsmlExplorer.Frontend/components/DateFormatter.ts index 5d62bb045..b9466f36e 100644 --- a/Src/WitsmlExplorer.Frontend/components/DateFormatter.ts +++ b/Src/WitsmlExplorer.Frontend/components/DateFormatter.ts @@ -6,6 +6,7 @@ const naturalDateTimeFormat = "dd.MM.yyyy HH:mm:ss.SSS"; const rawDateTimeFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"; let dateTimeFormat = rawDateTimeFormat; export const dateTimeFormatNoOffset = "yyyy-MM-dd'T'HH:mm:ss.SSS"; +export const dateTimeFormatTextField = "yyyy-MM-dd'T'HH:mm:ss"; // Minus character U+2212 is preferred by ISO 8601 over hyphen minus '-' so we check both // date-fns-tz behaves weirdly with minus so we replace it @@ -22,6 +23,8 @@ function formatDateString( } if (dateTimeFormatString == DateTimeFormat.Natural) { dateTimeFormat = naturalDateTimeFormat; + } else if (dateTimeFormatString == DateTimeFormat.RawNoOffset) { + dateTimeFormat = dateTimeFormatNoOffset; } else if (dateTimeFormatString == DateTimeFormat.Raw) { dateTimeFormat = rawDateTimeFormat; } diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/LogHeaderDateTimeField.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/LogHeaderDateTimeField.tsx index fd1213957..499ce668c 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/LogHeaderDateTimeField.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/LogHeaderDateTimeField.tsx @@ -1,31 +1,30 @@ import { TextField } from "@equinor/eds-core-react"; -import { - dateTimeFormatNoOffset, - validateIsoDateStringNoOffset +import formatDateString, { + dateTimeFormatTextField, + getOffset, + getOffsetFromTimeZone } from "components/DateFormatter"; -import { formatInTimeZone } from "date-fns-tz"; -import { useEffect, useState } from "react"; +import OperationContext from "contexts/operationContext"; +import { DateTimeFormat, TimeZone } from "contexts/operationStateReducer"; +import { isValid, parse } from "date-fns"; +import { ChangeEvent, useContext, useEffect, useState } from "react"; import styled from "styled-components"; -import Icon from "styles/Icons"; interface DateTimeFieldProps { value: string; label: string; - updateObject: (dateTime: string, valid: boolean) => void; - offset: string; + updateObject: (dateTime: string) => void; minValue?: string; maxValue?: string; } /** - * A component to edit a date time, taking in a string in the DateFormatter.dateTimeFormatNoOffset. + * A component to edit a date time, taking in a string in the DateFormatter.dateTimeFormat. * The offset is shown beside the input in a disabled field. * One can either write/paste a string manually, or use the native datepicker. - * This component should be replaced if EDS ever gets a custom datepicker. * @param value The current value of the field. * @param label Label shown above the field. * @param updateObject A lambda to update the value on the object to be modified. - * @param offset A constant UTC offset to calculate the time properly * @param minValue Optional earliest time the value can be. * @param maxValue Optional latest time the value can be. * @returns @@ -33,9 +32,15 @@ interface DateTimeFieldProps { export const LogHeaderDateTimeField = ( props: DateTimeFieldProps ): React.ReactElement => { - const { value, label, updateObject, offset, minValue, maxValue } = props; + const { value, label, updateObject, minValue, maxValue } = props; + const { + operationState: { timeZone } + } = useContext(OperationContext); + const offset = + timeZone === TimeZone.Raw + ? getOffset(value) + : getOffsetFromTimeZone(timeZone); const [initiallyEmpty, setInitiallyEmpty] = useState(false); - const isFirefox = navigator.userAgent.includes("Firefox"); useEffect(() => { setInitiallyEmpty(value == null || value === ""); @@ -43,12 +48,12 @@ export const LogHeaderDateTimeField = ( const validate = (current: string) => { return ( - (validateIsoDateStringNoOffset(current, offset) && - (!minValue || current >= minValue) && + ((!minValue || current >= minValue) && (!maxValue || current <= maxValue)) || (initiallyEmpty && (current == null || current === "")) ); }; + const getHelperText = () => { if (!validate(value)) { if (!initiallyEmpty && (value == null || value === "")) { @@ -60,10 +65,19 @@ export const LogHeaderDateTimeField = ( if (maxValue && value > maxValue) { return `Must be sooner than ${maxValue}`; } - return "The input must be in the yyyy-MM-dd'T'HH:mm:ss.SSS format."; + return "The input must be in the yyyy-MM-dd'T'HH:mm:ss format."; } return ""; }; + + const onTextFieldChange = (e: ChangeEvent) => { + const isMissingSeconds = e.target.value.split(":")?.length === 2; + const value = isMissingSeconds ? `${e.target.value}:00` : e.target.value; + if (isValid(parseDate(value))) { + updateObject(getUtcValue(value, offset)); + } + }; + return ( @@ -74,58 +88,47 @@ export const LogHeaderDateTimeField = ( disabled style={{ fontFeatureSettings: '"tnum"', - width: "16%" + width: "92px" }} /> - ) => { - updateObject(e.target.value, validate(e.target.value)); - }} + type={"datetime-local"} + step="1" + onChange={onTextFieldChange} style={{ - fontFeatureSettings: '"tnum"', - paddingBottom: validate(value) ? "24px" : 0 + paddingTop: "16px", + fontFeatureSettings: '"tnum"' }} /> - - - ) => { - let toFormat = e.target.value; - if (validateIsoDateStringNoOffset(value, offset)) { - // preserve the ss.SSS (and also HH:mm for Firefox) part of the original value that the datepicker does not set - const slice = isFirefox ? value.slice(10) : value.slice(16); - toFormat += slice; - } - toFormat += offset; - const formatted = formatInTimeZone( - toFormat, - offset, - dateTimeFormatNoOffset - ); - updateObject(formatted, validate(formatted)); - }} - /> ); }; +const parseDate = (current: string) => { + return parse(current, dateTimeFormatTextField, new Date()); +}; + +const getParsedValue = (input: string, timeZone: TimeZone) => { + if (!input) return null; + return formatDateString(input, timeZone, DateTimeFormat.RawNoOffset); +}; + +const getUtcValue = (input: string, offset: string) => { + if (!input) return null; + const inputWithZone = input + offset; + const utcInput = formatDateString( + inputWithZone, + TimeZone.Utc, + DateTimeFormat.Raw + ); + return utcInput; +}; + const Layout = styled.div` position: relative; input[type="datetime-local"]::-webkit-calendar-picker-indicator { @@ -137,16 +140,3 @@ const Horizontal = styled.div` display: flex; flex-direction: row; `; - -const Picker = styled(TextField)` - opacity: 0; - position: absolute; - right: 0; - top: 15px; -`; - -const PickerIcon = styled(Icon)` - position: absolute; - right: 15px; - top: 22px; -`; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/TrimLogObject/AdjustDateTimeModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/TrimLogObject/AdjustDateTimeModal.tsx index e1ac93687..76210c97e 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/TrimLogObject/AdjustDateTimeModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/TrimLogObject/AdjustDateTimeModal.tsx @@ -1,12 +1,7 @@ import { Button } from "@equinor/eds-core-react"; -import { - dateTimeFormatNoOffset, - getOffset, - validateIsoDateStringNoOffset -} from "components/DateFormatter"; import { LogHeaderDateTimeField } from "components/Modals/LogHeaderDateTimeField"; import { addMilliseconds } from "date-fns"; -import { formatInTimeZone, toDate } from "date-fns-tz"; +import { toDate } from "date-fns-tz"; import React, { useEffect, useState } from "react"; export interface AdjustDateTimeModelProps { @@ -34,14 +29,8 @@ const AdjustDateTimeModal = ( onEndDateChanged, onValidChange } = props; - const [startOffset] = useState(getOffset(minDate)); - const [endOffset] = useState(getOffset(maxDate)); - const [startIndex, setStartIndex] = useState( - formatInTimeZone(minDate, startOffset, dateTimeFormatNoOffset) - ); - const [endIndex, setEndIndex] = useState( - formatInTimeZone(maxDate, endOffset, dateTimeFormatNoOffset) - ); + const [startIndex, setStartIndex] = useState(minDate); + const [endIndex, setEndIndex] = useState(maxDate); const [startIndexInitiallyEmpty] = useState( startIndex == null || startIndex === "" ); @@ -62,35 +51,31 @@ const AdjustDateTimeModal = ( const validate = ( current: string, - offset: string, minValue: string, maxValue: string, initiallyEmpty: boolean ) => { return ( - (validateIsoDateStringNoOffset(current, offset) && - (!minValue || current >= minValue) && + ((!minValue || current >= minValue) && (!maxValue || current <= maxValue)) || (initiallyEmpty && (current == null || current === "")) ); }; useEffect(() => { - onStartDateChanged(startIndex + startOffset); - onEndDateChanged(endIndex + endOffset); + onStartDateChanged(startIndex); + onEndDateChanged(endIndex); }, [startIndex, endIndex]); useEffect(() => { const startIndexIsValid = validate( startIndex, - startOffset, startIndexMinValue, startIndexMaxValue, startIndexInitiallyEmpty ); const endIndexIsValid = validate( endIndex, - endOffset, endIndexMinValue, endIndexMaxValue, endIndexInitiallyEmpty @@ -115,19 +100,11 @@ const AdjustDateTimeModal = ( key={"last" + buttonValue.displayText} onClick={() => { const newStartIndex = addMilliseconds( - toDate(endIndex + endOffset), + toDate(endIndex), -buttonValue.timeInMilliseconds ); - setStartIndex( - formatInTimeZone( - newStartIndex, - startOffset, - dateTimeFormatNoOffset - ) - ); - setEndIndex( - formatInTimeZone(maxDate, endOffset, dateTimeFormatNoOffset) - ); + setStartIndex(newStartIndex.toISOString()); + setEndIndex(maxDate); }} > {"Last " + buttonValue.displayText} @@ -138,12 +115,8 @@ const AdjustDateTimeModal = (
+ + + + + + + {msalEnabled && ( diff --git a/Src/WitsmlExplorer.Frontend/contexts/__tests__/operationStateReducer.test.tsx b/Src/WitsmlExplorer.Frontend/contexts/__tests__/operationStateReducer.test.tsx index 4d3630a87..d854cbbf2 100644 --- a/Src/WitsmlExplorer.Frontend/contexts/__tests__/operationStateReducer.test.tsx +++ b/Src/WitsmlExplorer.Frontend/contexts/__tests__/operationStateReducer.test.tsx @@ -68,6 +68,7 @@ const getEmptyState = (): OperationState => { timeZone: TimeZone.Local, dateTimeFormat: DateTimeFormat.Raw, decimals: DecimalPreference.Raw, - colors: light + colors: light, + hotKeysEnabled: false }; }; diff --git a/Src/WitsmlExplorer.Frontend/contexts/operationStateReducer.tsx b/Src/WitsmlExplorer.Frontend/contexts/operationStateReducer.tsx index 61f7349e1..4da0ec316 100644 --- a/Src/WitsmlExplorer.Frontend/contexts/operationStateReducer.tsx +++ b/Src/WitsmlExplorer.Frontend/contexts/operationStateReducer.tsx @@ -78,6 +78,11 @@ export interface SetDateTimeFormatAction extends PayloadAction { payload: DateTimeFormat; } +export interface SetHotKeysEnabledAction extends PayloadAction { + type: OperationType.SetHotKeysEnabled; + payload: boolean; +} + export interface SetDecimalAction extends PayloadAction { type: OperationType.SetDecimal; payload: DecimalPreference; @@ -92,6 +97,7 @@ export interface OperationState { colors: Colors; dateTimeFormat: DateTimeFormat; decimals: DecimalPreference; + hotKeysEnabled: boolean; } export interface MousePosition { @@ -123,7 +129,8 @@ export const initOperationStateReducer = (): [ timeZone: TimeZone.Raw, colors: Light, dateTimeFormat: DateTimeFormat.Raw, - decimals: DecimalPreference.Raw + decimals: DecimalPreference.Raw, + hotKeysEnabled: false }; return useReducer(reducer, initialState); }; @@ -151,6 +158,8 @@ export const reducer = ( return setDateTimeFormat(state, action as SetDateTimeFormatAction); case OperationType.SetDecimal: return setDecimal(state, action as SetDecimalAction); + case OperationType.SetHotKeysEnabled: + return setHotKeysEnabled(state, action as SetHotKeysEnabledAction); default: throw new Error(); } @@ -233,6 +242,16 @@ const setDecimal = (state: OperationState, { payload }: SetDecimalAction) => { }; }; +const setHotKeysEnabled = ( + state: OperationState, + { payload }: SetHotKeysEnabledAction +) => { + return { + ...state, + hotKeysEnabled: payload + }; +}; + export type OperationAction = | DisplayModalAction | HideModalAction @@ -242,6 +261,7 @@ export type OperationAction = | SetTimeZoneAction | SetModeAction | SetDateTimeFormatAction - | SetDecimalAction; + | SetDecimalAction + | SetHotKeysEnabledAction; export type DispatchOperation = (action: OperationAction) => void; diff --git a/Src/WitsmlExplorer.Frontend/contexts/operationType.ts b/Src/WitsmlExplorer.Frontend/contexts/operationType.ts index d341316dc..162ebd713 100644 --- a/Src/WitsmlExplorer.Frontend/contexts/operationType.ts +++ b/Src/WitsmlExplorer.Frontend/contexts/operationType.ts @@ -10,7 +10,8 @@ enum OperationType { SetTimeZone, SetMode, SetDateTimeFormat, - SetDecimal + SetDecimal, + SetHotKeysEnabled } export default OperationType; diff --git a/Src/WitsmlExplorer.Frontend/routes/Root.tsx b/Src/WitsmlExplorer.Frontend/routes/Root.tsx index 87a02f5f4..7c380b59c 100644 --- a/Src/WitsmlExplorer.Frontend/routes/Root.tsx +++ b/Src/WitsmlExplorer.Frontend/routes/Root.tsx @@ -2,6 +2,7 @@ import { InteractionType } from "@azure/msal-browser"; import { MsalAuthenticationTemplate, MsalProvider } from "@azure/msal-react"; import { ThemeProvider } from "@mui/material"; import { DesktopAppEventHandler } from "components/DesktopAppEventHandler"; +import { HotKeyHandler } from "components/HotKeyHandler"; import { LoggedInUsernamesProvider } from "contexts/loggedInUsernamesContext"; import { SnackbarProvider } from "notistack"; import { useEffect } from "react"; @@ -21,6 +22,7 @@ import { DecimalPreference, SetDateTimeFormatAction, SetDecimalAction, + SetHotKeysEnabledAction, SetModeAction, SetThemeAction, SetTimeZoneAction, @@ -42,6 +44,7 @@ import { getTheme } from "../styles/material-eds"; import { STORAGE_DATETIMEFORMAT_KEY, STORAGE_DECIMAL_KEY, + STORAGE_HOTKEYS_ENABLED_KEY, STORAGE_MODE_KEY, STORAGE_THEME_KEY, STORAGE_TIMEZONE_KEY, @@ -81,9 +84,9 @@ export default function Root() { }; dispatchOperation(action); } - const storedDateTimeFormat = getLocalStorageItem( + const storedDateTimeFormat = getLocalStorageItem( STORAGE_DATETIMEFORMAT_KEY - ) as DateTimeFormat; + ); if (storedDateTimeFormat) { const action: SetDateTimeFormatAction = { type: OperationType.SetDateTimeFormat, @@ -91,9 +94,8 @@ export default function Root() { }; dispatchOperation(action); } - const storedDecimals = getLocalStorageItem( - STORAGE_DECIMAL_KEY - ) as DecimalPreference; + const storedDecimals = + getLocalStorageItem(STORAGE_DECIMAL_KEY); if (storedDecimals) { const action: SetDecimalAction = { type: OperationType.SetDecimal, @@ -101,6 +103,16 @@ export default function Root() { }; dispatchOperation(action); } + const storedHotKeysEnabled = getLocalStorageItem( + STORAGE_HOTKEYS_ENABLED_KEY + ); + if (storedHotKeysEnabled != null) { + const action: SetHotKeysEnabledAction = { + type: OperationType.SetHotKeysEnabled, + payload: storedHotKeysEnabled + }; + dispatchOperation(action); + } } if (import.meta.env.VITE_DARK_MODE_DEBUG) { return enableDarkModeDebug(dispatchOperation); @@ -125,6 +137,7 @@ export default function Root() { {isDesktopApp() && } + diff --git a/Src/WitsmlExplorer.Frontend/styles/Icons.tsx b/Src/WitsmlExplorer.Frontend/styles/Icons.tsx index 8784d22d5..63fb66b45 100644 --- a/Src/WitsmlExplorer.Frontend/styles/Icons.tsx +++ b/Src/WitsmlExplorer.Frontend/styles/Icons.tsx @@ -37,6 +37,7 @@ import { in_progress as inProgress, info_circle as infoCircle, trending_up as isActive, + keyboard, launch, more_vertical as moreVertical, new_alert as newAlert, @@ -82,14 +83,15 @@ const icons = { edit, errorFilled, expand, - favoriteOutlined, favoriteFilled, + favoriteOutlined, filter, folderOpen, formatLine, infoCircle, inProgress, isActive, + keyboard, launch, moreVertical, newAlert, diff --git a/Src/WitsmlExplorer.Frontend/tools/localStorageHelpers.tsx b/Src/WitsmlExplorer.Frontend/tools/localStorageHelpers.tsx index 365fc38f1..911dd401f 100644 --- a/Src/WitsmlExplorer.Frontend/tools/localStorageHelpers.tsx +++ b/Src/WitsmlExplorer.Frontend/tools/localStorageHelpers.tsx @@ -1,6 +1,7 @@ export const STORAGE_THEME_KEY = "selectedTheme"; export const STORAGE_TIMEZONE_KEY = "selectedTimeZone"; export const STORAGE_MODE_KEY = "selectedMode"; +export const STORAGE_HOTKEYS_ENABLED_KEY = "hotKeysEnabled"; export const STORAGE_FILTER_HIDDENOBJECTS_KEY = "hiddenObjects"; export const STORAGE_FILTER_ISACTIVE_KEY = "filterIsActive"; export const STORAGE_FILTER_OBJECTGROWING_KEY = "filterObjectGrowing"; From 82792b690a44dec8303ed1bf42659b8bad4c481d Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Thu, 30 May 2024 14:45:04 +0200 Subject: [PATCH 054/124] FIX-2430 Add loading state for switch user (#2450) --- .../components/Modals/UserCredentialsModal.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/UserCredentialsModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/UserCredentialsModal.tsx index 2d97317f4..5f601b3ff 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/UserCredentialsModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/UserCredentialsModal.tsx @@ -1,4 +1,9 @@ -import { Autocomplete, TextField, Typography } from "@equinor/eds-core-react"; +import { + Autocomplete, + Progress, + TextField, + Typography +} from "@equinor/eds-core-react"; import ModalDialog, { ModalWidth } from "components/Modals/ModalDialog"; import { validText } from "components/Modals/ModalParts"; import { Button } from "components/StyledComponents/Button"; @@ -33,6 +38,7 @@ const UserCredentialsModal = ( const [username, setUsername] = useState(); const [password, setPassword] = useState(); const [isLoading, setIsLoading] = useState(false); + const [isSwitchingUser, setIsSwitchingUser] = useState(false); const [errorMessage, setErrorMessage] = useState(""); const shouldFocusPasswordInput = !!username; const [keepLoggedIn, setKeepLoggedIn] = useState( @@ -134,8 +140,10 @@ const UserCredentialsModal = ( }} /> )} From 3807d52c26e43cb57a93833ccb6a01e1fa2f93ca Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Thu, 30 May 2024 14:45:17 +0200 Subject: [PATCH 055/124] FIX-2429 Disable checkForUpdates when running desktop tests (#2451) --- Src/WitsmlExplorer.Desktop/package.json | 3 +- Src/WitsmlExplorer.Desktop/src/main/main.ts | 3 +- yarn.lock | 38 +++++++++++++++++++-- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/Src/WitsmlExplorer.Desktop/package.json b/Src/WitsmlExplorer.Desktop/package.json index 62d5537d3..7066be32d 100644 --- a/Src/WitsmlExplorer.Desktop/package.json +++ b/Src/WitsmlExplorer.Desktop/package.json @@ -13,7 +13,7 @@ "preview": "yarn build:api && electron-vite preview", "electron:pack": "yarn build && electron-builder --dir -c electron-builder.json", "electron:dist": "yarn build && electron-builder -c electron-builder.json", - "test:pack": "playwright test" + "test:pack": "cross-env ELECTRON_IS_TEST=true playwright test" }, "main": "./dist/main/main.js", "lint-staged": { @@ -31,6 +31,7 @@ "@playwright/test": "^1.43.1", "@types/cross-spawn": "^6.0.6", "@vitejs/plugin-react": "^4.2.1", + "cross-env": "^7.0.3", "cross-spawn": "^7.0.3", "electron": "^29.3.0", "electron-builder": "^24.13.3", diff --git a/Src/WitsmlExplorer.Desktop/src/main/main.ts b/Src/WitsmlExplorer.Desktop/src/main/main.ts index 1d91fbcf5..42f172242 100644 --- a/Src/WitsmlExplorer.Desktop/src/main/main.ts +++ b/Src/WitsmlExplorer.Desktop/src/main/main.ts @@ -23,6 +23,7 @@ let isUpdateAvailableChecked = false; let apiProcess: any; const isDevelopment = process.env.NODE_ENV === "development"; +const isTest = process.env.ELECTRON_IS_TEST === "true"; interface AppConfig { apiPort: string; @@ -295,7 +296,7 @@ if (!gotTheLock) { ipcMain.handle("getAppVersion", () => app.getVersion()); ipcMain.handle("checkForUpdates", () => { - if (!isUpdateAvailableChecked) { + if (!isUpdateAvailableChecked && !isTest) { isUpdateAvailableChecked = true; const updateCheckResult = autoUpdater.checkForUpdates(); return updateCheckResult; diff --git a/yarn.lock b/yarn.lock index 389d22f65..cd2e20418 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2464,6 +2464,13 @@ crc@^3.8.0: dependencies: buffer "^5.1.0" +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -5595,7 +5602,16 @@ string-argv@0.3.2: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -5659,7 +5675,14 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -6222,7 +6245,16 @@ why-is-node-running@^2.2.2: siginfo "^2.0.0" stackback "0.0.2" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From 280b11f4291055dd52d50e14d9a2b4db65847f11 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Thu, 30 May 2024 14:50:53 +0200 Subject: [PATCH 056/124] FIX-2417 Refresh objects after batch update (#2452) --- .../components/RefreshHandler.tsx | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/components/RefreshHandler.tsx b/Src/WitsmlExplorer.Frontend/components/RefreshHandler.tsx index 7f27eb3ea..2841f4efd 100644 --- a/Src/WitsmlExplorer.Frontend/components/RefreshHandler.tsx +++ b/Src/WitsmlExplorer.Frontend/components/RefreshHandler.tsx @@ -115,14 +115,30 @@ const RefreshHandler = (): React.ReactElement => { } function refreshWellboreObjectsBatch(refreshAction: RefreshAction) { - for (const object of refreshAction.objects) { - refreshObjectQuery( + const uniqueMap = new Map< + string, + { wellUid: string; wellboreUid: string } + >(); + + refreshAction.objects.forEach((obj) => { + const key = `${obj.wellUid}_${obj.wellboreUid}`; + if (!uniqueMap.has(key)) { + uniqueMap.set(key, { + wellUid: obj.wellUid, + wellboreUid: obj.wellboreUid + }); + } + }); + + const uniqueCombinations = Array.from(uniqueMap.values()); + + for (const obj of uniqueCombinations) { + refreshObjectsQuery( queryClient, refreshAction.serverUrl.toString().toLowerCase(), - object.wellUid, - object.wellboreUid, - refreshAction.entityType as ObjectType, - object.uid + obj.wellUid, + obj.wellboreUid, + refreshAction.entityType as ObjectType ); } } From 2df94f47b427311faee6f4402f8a42fc9acb3a2c Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Fri, 31 May 2024 14:43:33 +0200 Subject: [PATCH 057/124] FIX-2261 Implement wellbore search (#2453) --- .../HttpHandlers/WellboreHandler.cs | 2 +- Src/WitsmlExplorer.Api/Routes.cs | 1 + .../ContentViews/ObjectSearchListView.tsx | 2 +- .../ContentViews/SearchListView.tsx | 22 +++ .../ContentViews/WellboreSearchListView.tsx | 163 ++++++++++++++++++ .../components/Sidebar/SearchFilter.tsx | 5 +- .../contexts/filter.tsx | 21 ++- .../hooks/query/useGetObjectSearch.tsx | 9 +- .../hooks/query/useGetWellboreSearch.ts | 43 +++++ .../hooks/query/useGetWellbores.tsx | 2 +- .../hooks/useWellFilter.tsx | 28 ++- .../hooks/useWellboreFilter.tsx | 27 ++- Src/WitsmlExplorer.Frontend/routes/Router.tsx | 4 +- .../services/wellboreService.tsx | 9 +- 14 files changed, 317 insertions(+), 21 deletions(-) create mode 100644 Src/WitsmlExplorer.Frontend/components/ContentViews/SearchListView.tsx create mode 100644 Src/WitsmlExplorer.Frontend/components/ContentViews/WellboreSearchListView.tsx create mode 100644 Src/WitsmlExplorer.Frontend/hooks/query/useGetWellboreSearch.ts diff --git a/Src/WitsmlExplorer.Api/HttpHandlers/WellboreHandler.cs b/Src/WitsmlExplorer.Api/HttpHandlers/WellboreHandler.cs index d533a6db0..75a726928 100644 --- a/Src/WitsmlExplorer.Api/HttpHandlers/WellboreHandler.cs +++ b/Src/WitsmlExplorer.Api/HttpHandlers/WellboreHandler.cs @@ -14,7 +14,7 @@ public static class WellboreHandler [Produces(typeof(IEnumerable))] public static async Task GetWellbores(string wellUid, IWellboreService wellboreService) { - return TypedResults.Ok(await wellboreService.GetWellbores(wellUid)); + return TypedResults.Ok(await wellboreService.GetWellbores(wellUid ?? "")); } [Produces(typeof(Wellbore))] diff --git a/Src/WitsmlExplorer.Api/Routes.cs b/Src/WitsmlExplorer.Api/Routes.cs index 3bc21a09b..6e84bb55d 100644 --- a/Src/WitsmlExplorer.Api/Routes.cs +++ b/Src/WitsmlExplorer.Api/Routes.cs @@ -32,6 +32,7 @@ public static void ConfigureApi(this WebApplication app, IConfiguration configur app.MapGet("/objects/{objectType}/{objectProperty}", ObjectHandler.GetObjectsWithParamByType, useOAuth2); app.MapGet("/objects/{objectType}/{objectProperty}/{objectPropertyValue}", ObjectHandler.GetObjectsWithParamByType, useOAuth2); + app.MapGet("/wellbores", WellboreHandler.GetWellbores, useOAuth2); app.MapGet("/wells/{wellUid}/wellbores", WellboreHandler.GetWellbores, useOAuth2); app.MapGet("/wells/{wellUid}/wellbores/{wellboreUid}", WellboreHandler.GetWellbore, useOAuth2); app.MapGet("/wells/{wellUid}/wellbores/{wellboreUid}/idonly/{objectType}", ObjectHandler.GetObjectsIdOnly, useOAuth2); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/ObjectSearchListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/ObjectSearchListView.tsx index 48c64fd27..9463af73f 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/ObjectSearchListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/ObjectSearchListView.tsx @@ -289,7 +289,7 @@ export const ObjectSearchListView = (): ReactElement => { ) : ( { + const { filterType } = useParams<{ filterType: FilterType }>(); + + if (isObjectFilterType(filterType)) { + return ; + } else if (isWellboreFilterType(filterType)) { + return ; + } + + return ; +}; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboreSearchListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboreSearchListView.tsx new file mode 100644 index 000000000..3ac4de14b --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboreSearchListView.tsx @@ -0,0 +1,163 @@ +import { Typography } from "@equinor/eds-core-react"; +import { + ContentTable, + ContentTableColumn, + ContentTableRow, + ContentType +} from "components/ContentViews/table"; +import { getContextMenuPosition } from "components/ContextMenus/ContextMenu"; +import LoadingContextMenu from "components/ContextMenus/LoadingContextMenu"; +import WellboreContextMenu, { + WellboreContextMenuProps +} from "components/ContextMenus/WellboreContextMenu"; +import ProgressSpinner from "components/ProgressSpinner"; +import { useConnectedServer } from "contexts/connectedServerContext"; +import { + FilterContext, + WellboreFilterType, + filterTypeToProperty +} from "contexts/filter"; +import OperationContext from "contexts/operationContext"; +import { MousePosition } from "contexts/operationStateReducer"; +import OperationType from "contexts/operationType"; +import { useGetServers } from "hooks/query/useGetServers"; +import { useGetWellboreSearch } from "hooks/query/useGetWellboreSearch"; +import Well from "models/well"; +import Wellbore from "models/wellbore"; +import React, { ReactElement, useContext, useEffect } from "react"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; +import { getObjectGroupsViewPath } from "routes/utils/pathBuilder"; +import NotificationService from "services/notificationService"; +import WellboreService from "services/wellboreService"; + +export interface WellboreSearchRow extends ContentTableRow, Wellbore { + well: Well; + wellbore: Wellbore; +} + +export const WellboreSearchListView = (): ReactElement => { + const { connectedServer } = useConnectedServer(); + const { dispatchOperation } = useContext(OperationContext); + const { selectedFilter, updateSelectedFilter } = useContext(FilterContext); + const [searchParams] = useSearchParams(); + const { filterType } = useParams<{ filterType: WellboreFilterType }>(); + const value = searchParams.get("value"); + const { servers } = useGetServers(); + const { wellboreSearchResults, isFetching, error, isError } = + useGetWellboreSearch(connectedServer, filterType, value); + const navigate = useNavigate(); + + useEffect(() => { + if ( + !isFetching && + !isError && + wellboreSearchResults !== selectedFilter.wellboreSearchResults + ) { + updateSelectedFilter({ + wellboreSearchResults: wellboreSearchResults, + filterType, + name: value + }); + } + }, [wellboreSearchResults]); + + useEffect(() => { + if (isError && !!error) { + const message = (error as Error).message; + NotificationService.Instance.alertDispatcher.dispatch({ + serverUrl: new URL(connectedServer.url), + message: message, + isSuccess: false, + severity: "error" + }); + } + }, [error, isError]); + + const fetchSelectedWellbore = async ( + checkedWellboreRow: WellboreSearchRow + ) => { + return await WellboreService.getWellbore( + checkedWellboreRow.wellUid, + checkedWellboreRow.uid + ); + }; + + const onContextMenuSingleWellbore = async ( + checkedWellboreRow: WellboreSearchRow, + position: MousePosition + ) => { + dispatchOperation({ + type: OperationType.DisplayContextMenu, + payload: { component: , position } + }); + const fetchedWellbore = await fetchSelectedWellbore(checkedWellboreRow); + const contextProps: WellboreContextMenuProps = { + servers: servers, + wellbore: fetchedWellbore, + checkedWellboreRows: [] + }; + dispatchOperation({ + type: OperationType.DisplayContextMenu, + payload: { + component: , + position + } + }); + }; + + const onContextMenu = async ( + event: React.MouseEvent, + {}, + checkedWellboreRows: WellboreSearchRow[] + ) => { + const position = getContextMenuPosition(event); + if (checkedWellboreRows.length === 1) { + await onContextMenuSingleWellbore(checkedWellboreRows[0], position); + } + }; + + const getColumns = () => { + const columns: ContentTableColumn[] = [ + { property: "name", label: "name", type: ContentType.String }, + { property: "wellName", label: "wellName", type: ContentType.String }, + { property: "uid", label: "uid", type: ContentType.String }, + { property: "wellUid", label: "wellUid", type: ContentType.String } + ]; + + if (filterTypeToProperty[filterType] != "name") { + columns.unshift({ + property: "searchProperty", + label: filterTypeToProperty[filterType], + type: ContentType.String + }); + } + + return columns; + }; + + const onSelect = async (row: WellboreSearchRow) => { + navigate( + getObjectGroupsViewPath(connectedServer.url, row.wellUid, row.uid) + ); + }; + + if (isFetching) { + return ; + } + + return wellboreSearchResults.length == 0 ? ( + + {`No wellbores match the current filter.`} + + ) : ( + + ); +}; diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter.tsx index 117a77fdd..183ff3784 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter.tsx @@ -11,7 +11,8 @@ import { FilterContext, FilterType, getFilterTypeInformation, - isObjectFilterType + isObjectFilterType, + isWellboreFilterType } from "contexts/filter"; import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; @@ -65,7 +66,7 @@ const SearchFilter = (): React.ReactElement => { }, [selectedFilter.name]); const openSearchView = (option: FilterType) => { - if (isObjectFilterType(option)) { + if (isObjectFilterType(option) || isWellboreFilterType(option)) { const searchParams = createSearchParams({ value: nameFilter }); diff --git a/Src/WitsmlExplorer.Frontend/contexts/filter.tsx b/Src/WitsmlExplorer.Frontend/contexts/filter.tsx index cf3deb613..712187c0a 100644 --- a/Src/WitsmlExplorer.Frontend/contexts/filter.tsx +++ b/Src/WitsmlExplorer.Frontend/contexts/filter.tsx @@ -1,6 +1,7 @@ import { pluralize } from "components/ContextMenus/ContextMenuUtils"; import ObjectSearchResult from "models/objectSearchResult"; import { ObjectType } from "models/objectType"; +import Wellbore from "models/wellbore"; import React from "react"; import { STORAGE_FILTER_HIDDENOBJECTS_KEY, @@ -15,6 +16,7 @@ export interface Filter { objectGrowing: boolean; filterType: FilterType; searchResults?: ObjectSearchResult[]; + wellboreSearchResults?: Wellbore[]; objectVisibilityStatus: Record; } @@ -28,6 +30,11 @@ export enum WellFilterType { Well = "Well" } +// Filter by wellbore names +export enum WellboreFilterType { + Wellbore = "Wellbore" +} + // Filter by properties already fetched for wells export enum WellPropertyFilterType { Field = "Field", @@ -72,6 +79,7 @@ export const convertObjectTypeToObjectFilterType = ( // For ObjectFilterType, the property can be any string property under an object. export const filterTypeToProperty = { [WellFilterType.Well]: "name", + [WellboreFilterType.Wellbore]: "name", [WellPropertyFilterType.Field]: "field", [WellPropertyFilterType.License]: "numLicense", [ObjectFilterType.Log]: "name", @@ -88,7 +96,11 @@ export const getFilterTypeInformation = (filterType: FilterType): string => { const onDemandString = `${pluralize( filterType )} will be fetched on demand by typing 'Enter' or clicking the search icon.`; - if (isWellFilterType(filterType) || isWellPropertyFilterType(filterType)) { + if ( + isWellFilterType(filterType) || + isWellPropertyFilterType(filterType) || + isWellboreFilterType(filterType) + ) { return wildCardString; } else if (isObjectFilterType(filterType)) { return `${onDemandString}\n${wildCardString}`; @@ -98,10 +110,12 @@ export const getFilterTypeInformation = (filterType: FilterType): string => { export type FilterType = | WellFilterType + | WellboreFilterType | WellPropertyFilterType | ObjectFilterType; export const FilterType = { ...WellFilterType, + ...WellboreFilterType, ...WellPropertyFilterType, ...ObjectFilterType }; @@ -110,6 +124,10 @@ export const isWellFilterType = (filterType: FilterType): boolean => { return Object.values(WellFilterType).includes(filterType); }; +export const isWellboreFilterType = (filterType: FilterType): boolean => { + return Object.values(WellboreFilterType).includes(filterType); +}; + export const isWellPropertyFilterType = (filterType: FilterType): boolean => { return Object.values(WellPropertyFilterType).includes(filterType); }; @@ -130,6 +148,7 @@ export const EMPTY_FILTER: Filter = { objectGrowing: false, filterType: WellFilterType.Well, searchResults: [], + wellboreSearchResults: [], objectVisibilityStatus: allVisibleObjects }; diff --git a/Src/WitsmlExplorer.Frontend/hooks/query/useGetObjectSearch.tsx b/Src/WitsmlExplorer.Frontend/hooks/query/useGetObjectSearch.tsx index eba753bfd..51851f733 100644 --- a/Src/WitsmlExplorer.Frontend/hooks/query/useGetObjectSearch.tsx +++ b/Src/WitsmlExplorer.Frontend/hooks/query/useGetObjectSearch.tsx @@ -56,8 +56,13 @@ export const objectSearchQuery = ( fetchAllObjects ), queryFn: async () => { - const well = await fetchObjects(server, filterType, value, fetchAllObjects); - return well; + const searchResults = await fetchObjects( + server, + filterType, + value, + fetchAllObjects + ); + return searchResults; }, ...options, enabled: diff --git a/Src/WitsmlExplorer.Frontend/hooks/query/useGetWellboreSearch.ts b/Src/WitsmlExplorer.Frontend/hooks/query/useGetWellboreSearch.ts new file mode 100644 index 000000000..8e330b9ad --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/hooks/query/useGetWellboreSearch.ts @@ -0,0 +1,43 @@ +import { QueryObserverResult } from "@tanstack/react-query"; +import { useGetWellbores } from "hooks/query/useGetWellbores"; +import Wellbore, { WellboreProperties } from "models/wellbore"; +import { useMemo } from "react"; +import { + WellboreFilterType, + filterTypeToProperty, + getSearchRegex, + isSitecomSyntax +} from "../../contexts/filter"; +import { Server } from "../../models/server"; +import { QueryOptions } from "./queryOptions"; + +type WellboreSearchQueryResult = Omit< + QueryObserverResult, + "data" +> & { + wellboreSearchResults: Wellbore[]; +}; + +export const useGetWellboreSearch = ( + server: Server, + filterType: WellboreFilterType, + value: string, + options?: QueryOptions +): WellboreSearchQueryResult => { + const { wellbores, ...state } = useGetWellbores(server, "", options); + + const filteredData = useMemo(() => { + const regex = getSearchRegex(value, true); + const property = filterTypeToProperty[ + filterType + ] as keyof WellboreProperties; + return ( + wellbores?.filter( + (result) => + isSitecomSyntax(value) || regex.test(result[property].toString()) + ) ?? [] + ); + }, [wellbores, value]); + + return { wellboreSearchResults: filteredData, ...state }; +}; diff --git a/Src/WitsmlExplorer.Frontend/hooks/query/useGetWellbores.tsx b/Src/WitsmlExplorer.Frontend/hooks/query/useGetWellbores.tsx index 647d09b61..2be1eb76c 100644 --- a/Src/WitsmlExplorer.Frontend/hooks/query/useGetWellbores.tsx +++ b/Src/WitsmlExplorer.Frontend/hooks/query/useGetWellbores.tsx @@ -45,7 +45,7 @@ export const wellboresQuery = ( return wellbores; }, ...options, - enabled: !!server && !!wellUid && !(options?.enabled === false) + enabled: !!server && wellUid != null && !(options?.enabled === false) }); type WellboresQueryResult = Omit< diff --git a/Src/WitsmlExplorer.Frontend/hooks/useWellFilter.tsx b/Src/WitsmlExplorer.Frontend/hooks/useWellFilter.tsx index 9a577d0e8..5829176b9 100644 --- a/Src/WitsmlExplorer.Frontend/hooks/useWellFilter.tsx +++ b/Src/WitsmlExplorer.Frontend/hooks/useWellFilter.tsx @@ -1,3 +1,4 @@ +import Wellbore from "models/wellbore"; import { useContext, useMemo } from "react"; import { Filter, @@ -6,7 +7,8 @@ import { getSearchRegex, isObjectFilterType, isWellFilterType, - isWellPropertyFilterType + isWellPropertyFilterType, + isWellboreFilterType } from "../contexts/filter"; import ObjectSearchResult from "../models/objectSearchResult"; import Well from "../models/well"; @@ -33,8 +35,20 @@ const filterWellsOnSearchResult = ( wells: Well[], searchResults: ObjectSearchResult[] ) => { - const wellUids = searchResults.map((searchResult) => searchResult.wellUid); - return wells.filter((well) => wellUids.includes(well.uid)); + const wellUids = searchResults.map((searchResult) => + searchResult.wellUid?.toLowerCase() + ); + return wells.filter((well) => wellUids.includes(well.uid.toLowerCase())); +}; + +const filterWellsOnWellboreSearchResult = ( + wells: Well[], + wellboreSearchResults: Wellbore[] +) => { + const wellUids = wellboreSearchResults.map((searchResult) => + searchResult.wellUid?.toLowerCase() + ); + return wells.filter((well) => wellUids.includes(well.uid.toLowerCase())); }; export const filterWells = (wells: Well[], filter: Filter): Well[] => { @@ -55,6 +69,11 @@ export const filterWells = (wells: Well[], filter: Filter): Well[] => { filteredWells, filter.searchResults ); + } else if (isWellboreFilterType(filter.filterType)) { + filteredWells = filterWellsOnWellboreSearchResult( + filteredWells, + filter.wellboreSearchResults + ); } filteredWells = filterWellsOnIsActive(filteredWells, filter.isActive); } @@ -72,7 +91,8 @@ export const useWellFilter = (wells: Well[]): Well[] => { selectedFilter.filterType, selectedFilter.isActive, selectedFilter.name, - selectedFilter.searchResults + selectedFilter.searchResults, + selectedFilter.wellboreSearchResults ]); return filteredWells; diff --git a/Src/WitsmlExplorer.Frontend/hooks/useWellboreFilter.tsx b/Src/WitsmlExplorer.Frontend/hooks/useWellboreFilter.tsx index a614d267f..fb5306503 100644 --- a/Src/WitsmlExplorer.Frontend/hooks/useWellboreFilter.tsx +++ b/Src/WitsmlExplorer.Frontend/hooks/useWellboreFilter.tsx @@ -1,5 +1,10 @@ import { useContext, useMemo } from "react"; -import { Filter, FilterContext, isObjectFilterType } from "../contexts/filter"; +import { + Filter, + FilterContext, + isObjectFilterType, + isWellboreFilterType +} from "../contexts/filter"; import ObjectSearchResult from "../models/objectSearchResult"; import Wellbore from "../models/wellbore"; @@ -23,6 +28,18 @@ const filterWellboresOnSearchResult = ( ); }; +const filterWellboresOnWellboreSearchResult = ( + wellbores: Wellbore[], + wellboreSearchResult: Wellbore[] +) => { + const wellAndWellboreUids = wellboreSearchResult.map((searchResult) => + [searchResult.wellUid, searchResult.uid].join(",") + ); + return wellbores.filter((wellbore) => + wellAndWellboreUids.includes([wellbore.wellUid, wellbore.uid].join(",")) + ); +}; + export const filterWellbores = ( wellbores: Wellbore[], filter: Filter @@ -35,6 +52,11 @@ export const filterWellbores = ( filteredWellbores, filter.searchResults ); + } else if (isWellboreFilterType(filter.filterType)) { + filteredWellbores = filterWellboresOnWellboreSearchResult( + filteredWellbores, + filter.wellboreSearchResults + ); } filteredWellbores = filterWellboresOnIsActive( filteredWellbores, @@ -55,7 +77,8 @@ export const useWellboreFilter = (wellbores: Wellbore[]): Wellbore[] => { selectedFilter.filterType, selectedFilter.isActive, selectedFilter.name, - selectedFilter.searchResults + selectedFilter.searchResults, + selectedFilter.wellboreSearchResults ]); return filteredWellbores; diff --git a/Src/WitsmlExplorer.Frontend/routes/Router.tsx b/Src/WitsmlExplorer.Frontend/routes/Router.tsx index f6639867a..a71579193 100644 --- a/Src/WitsmlExplorer.Frontend/routes/Router.tsx +++ b/Src/WitsmlExplorer.Frontend/routes/Router.tsx @@ -1,6 +1,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { MultiLogCurveValuesView } from "components/ContentViews/MultiLogCurveValuesView"; import MultiLogsCurveInfoListView from "components/ContentViews/MultiLogsCurveInfoListView"; +import { SearchListView } from "components/ContentViews/SearchListView"; import { RouterProvider, createBrowserRouter, @@ -31,7 +32,6 @@ import JobsView from "../components/ContentViews/JobsView"; import LogCurveInfoListView from "../components/ContentViews/LogCurveInfoListView"; import LogTypeListView from "../components/ContentViews/LogTypeListView"; import LogsListView from "../components/ContentViews/LogsListView"; -import ObjectSearchListView from "../components/ContentViews/ObjectSearchListView"; import { ObjectView } from "../components/ContentViews/ObjectView"; import { ObjectsListView } from "../components/ContentViews/ObjectsListView"; import QueryView from "../components/ContentViews/QueryView"; @@ -141,7 +141,7 @@ const router = createRouter([ }, { path: SEARCH_VIEW_ROUTE_PATH, - element: , + element: , errorElement: }, { diff --git a/Src/WitsmlExplorer.Frontend/services/wellboreService.tsx b/Src/WitsmlExplorer.Frontend/services/wellboreService.tsx index 5937eac82..0d84ef3cf 100644 --- a/Src/WitsmlExplorer.Frontend/services/wellboreService.tsx +++ b/Src/WitsmlExplorer.Frontend/services/wellboreService.tsx @@ -9,11 +9,10 @@ export default class WellboreService { abortSignal?: AbortSignal, server?: Server ): Promise { - const response = await ApiClient.get( - `api/wells/${encodeURIComponent(wellUid)}/wellbores`, - abortSignal, - server - ); + const endpoint = wellUid + ? `api/wells/${encodeURIComponent(wellUid)}/wellbores` + : "api/wellbores"; + const response = await ApiClient.get(endpoint, abortSignal, server); if (response.ok) { return response.json(); From 0a59af9fb3b2b474d65bd883e4cc7e0c6dfee2f0 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Fri, 31 May 2024 14:50:28 +0200 Subject: [PATCH 058/124] FIX-2420 Align loading dots in comfortable mode in the sidebar (#2454) --- Src/WitsmlExplorer.Frontend/components/Sidebar/TreeItem.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/TreeItem.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/TreeItem.tsx index 4fdc1bab0..a2f4c520b 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/TreeItem.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/TreeItem.tsx @@ -94,8 +94,7 @@ const NavigationDrawer = styled.p<{ const StyledDotProgress = styled(DotProgress)` z-index: 2; - top: 0.75rem; - position: relative; + align-self: center; `; export default StyledTreeItem; From b8bfec82a2fd42dd8505c9b80590d3ed918c87b6 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Fri, 31 May 2024 15:28:40 +0200 Subject: [PATCH 059/124] FIX-2438 Add filter to server list (#2455) --- .../components/ContentViews/ServerManager.tsx | 53 ++++++++++++++++--- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/ServerManager.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/ServerManager.tsx index ab95ca1ce..6eb7ddf7a 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/ServerManager.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/ServerManager.tsx @@ -1,5 +1,10 @@ import { useIsAuthenticated } from "@azure/msal-react"; -import { ButtonProps, Table, Typography } from "@equinor/eds-core-react"; +import { + ButtonProps, + Table, + TextField, + Typography +} from "@equinor/eds-core-react"; import { useQueryClient } from "@tanstack/react-query"; import ServerModal, { showDeleteServerModal @@ -10,6 +15,7 @@ import UserCredentialsModal, { import ProgressSpinner from "components/ProgressSpinner"; import { Button } from "components/StyledComponents/Button"; import { useConnectedServer } from "contexts/connectedServerContext"; +import { getSearchRegex } from "contexts/filter"; import { useLoggedInUsernames } from "contexts/loggedInUsernamesContext"; import { LoggedInUsernamesActionType } from "contexts/loggedInUsernamesReducer"; import OperationContext from "contexts/operationContext"; @@ -17,7 +23,7 @@ import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { Server, emptyServer } from "models/server"; import { adminRole, getUserAppRoles, msalEnabled } from "msal/MsalAuthProvider"; -import React, { useContext, useEffect } from "react"; +import React, { ChangeEvent, useContext, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { getWellsViewPath } from "routes/utils/pathBuilder"; import AuthorizationService from "services/authorizationService"; @@ -43,6 +49,11 @@ const ServerManager = (): React.ReactElement => { const navigate = useNavigate(); const { connectedServer, setConnectedServer } = useConnectedServer(); const { dispatchLoggedInUsernames } = useLoggedInUsernames(); + const [filter, setFilter] = useState(""); + const searchRegex = getSearchRegex(filter); + const filteredServers = servers.filter( + (s) => !filter || searchRegex.test(s.name) || searchRegex.test(s.url) + ); useEffect(() => { if (isError) { @@ -114,12 +125,31 @@ const ServerManager = (): React.ReactElement => { return ( <>
- - Manage Connections - + + Manage Connections + + ) => + setFilter(e.target.value) + } + /> + diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/JobInfoContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/JobInfoContextMenu.tsx index 3fb60bc3c..d20b6b7e4 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/JobInfoContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/JobInfoContextMenu.tsx @@ -10,10 +10,8 @@ import { } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; import { refreshJobInfoQuery } from "hooks/query/queryRefreshHelpers"; -import JobStatus from "models/jobStatus"; import JobInfo from "models/jobs/jobInfo"; import React from "react"; -import JobService from "services/jobService"; import { colors } from "styles/Colors"; export interface JobInfoContextMenuProps { @@ -38,11 +36,6 @@ const JobInfoContextMenu = ( }); }; - const onClickCancelAction = async () => { - dispatchOperation({ type: OperationType.HideContextMenu }); - JobService.cancelJob(jobInfo.id); - }; - return ( Refresh , - - - Cancel job - , , { @@ -19,6 +20,7 @@ const ConfirmModal = (props: ConfirmProps): React.ReactElement => { onSubmit={props.onConfirm} isLoading={false} confirmText={props.confirmText ?? "Yes"} + cancelText={props.cancelText} confirmColor={props.confirmColor} switchButtonPlaces={props.switchButtonPlaces} showCancelButton={props.showCancelButton} diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/ModalDialog.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/ModalDialog.tsx index da023bf0e..87743cdb0 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/ModalDialog.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/ModalDialog.tsx @@ -25,6 +25,7 @@ interface ModalDialogProps { showConfirmButton?: boolean; showCancelButton?: boolean; buttonPosition?: ControlButtonPosition; + cancelText?: string; } const ModalDialog = (props: ModalDialogProps): React.ReactElement => { @@ -42,7 +43,8 @@ const ModalDialog = (props: ModalDialogProps): React.ReactElement => { width = ModalWidth.MEDIUM, showConfirmButton = true, showCancelButton = true, - buttonPosition: ButtonPosition = ControlButtonPosition.BOTTOM + buttonPosition: ButtonPosition = ControlButtonPosition.BOTTOM, + cancelText } = props; const context = React.useContext(OperationContext); const [confirmButtonIsFocused, setConfirmButtonIsFocused] = @@ -101,7 +103,7 @@ const ModalDialog = (props: ModalDialogProps): React.ReactElement => { color={confirmColor ?? "primary"} variant="outlined" > - Cancel + {cancelText ?? "Cancel"} ) : ( <> diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/ReportModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/ReportModal.tsx index 8d3253db1..41de8abb3 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/ReportModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/ReportModal.tsx @@ -26,6 +26,7 @@ import JobService from "services/jobService"; import NotificationService from "services/notificationService"; import styled from "styled-components"; import { Colors } from "styles/Colors"; +import ConfirmModal from "./ConfirmModal"; export interface ReportModal { report?: BaseReport; @@ -85,9 +86,32 @@ export const ReportModal = (props: ReportModal): React.ReactElement => { [report] ); - const onCancelButtonClick = () => { - JobService.cancelJob(jobId); + const cancelJob = async (jobId: string) => { + dispatchOperation({ type: OperationType.HideContextMenu }); dispatchOperation({ type: OperationType.HideModal }); + await JobService.cancelJob(jobId); + }; + + const onClickCancel = async () => { + const confirmation = ( + Do you really want to cancel this job? + } + onConfirm={() => { + cancelJob(jobId); + }} + confirmColor={"danger"} + confirmText={"Yes"} + cancelText={"No"} + switchButtonPlaces={true} + /> + ); + dispatchOperation({ + type: OperationType.DisplayModal, + payload: confirmation + }); }; return ( @@ -96,6 +120,7 @@ export const ReportModal = (props: ReportModal): React.ReactElement => { heading={report ? report.title : "Loading report..."} confirmText="Ok" showCancelButton={!fetchedReport && isCancelable} + cancelText="Cancel job" content={ {report ? ( @@ -173,7 +198,7 @@ export const ReportModal = (props: ReportModal): React.ReactElement => { } onSubmit={() => dispatchOperation({ type: OperationType.HideModal })} - onCancel={() => onCancelButtonClick()} + onCancel={() => onClickCancel()} isLoading={false} /> ); From 606f97edfffd3c571dfeadd43d3d973cea3d67b1 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Wed, 12 Jun 2024 10:47:27 +0200 Subject: [PATCH 069/124] FIX-2473 Enable search bar & command palette (#2474) --- Src/WitsmlExplorer.Frontend/components/QueryEditor.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Src/WitsmlExplorer.Frontend/components/QueryEditor.tsx b/Src/WitsmlExplorer.Frontend/components/QueryEditor.tsx index be770372e..1faf52684 100644 --- a/Src/WitsmlExplorer.Frontend/components/QueryEditor.tsx +++ b/Src/WitsmlExplorer.Frontend/components/QueryEditor.tsx @@ -1,5 +1,7 @@ import "ace-builds/src-noconflict/ace"; import "ace-builds/src-noconflict/ext-language_tools"; +import "ace-builds/src-noconflict/ext-prompt"; +import "ace-builds/src-noconflict/ext-searchbox"; import "ace-builds/src-noconflict/mode-xml"; import "ace-builds/src-noconflict/theme-merbivore"; import "ace-builds/src-noconflict/theme-xcode"; From c1781ec4e849352a6e3f7a5c4194d1cf6b593541 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Thu, 13 Jun 2024 09:16:47 +0200 Subject: [PATCH 070/124] FIX-2137 OperationContext Cleanup (#2472) --- .../components/Alerts.tsx | 6 +- .../components/ApplicationVersion.tsx | 6 +- .../components/Breadcrumbs.tsx | 6 +- .../ContentViews/BhaRunsListView.tsx | 6 +- .../ContentViews/ChangeLogsListView.tsx | 5 +- .../ContentViews/Charts/LogsGraph.tsx | 8 +- .../ContentViews/CurveValuesPlot.tsx | 4 +- .../ContentViews/CurveValuesView.tsx | 5 +- .../components/ContentViews/EditNumber.tsx | 6 +- .../ContentViews/EditSelectedLogCurveInfo.tsx | 5 +- .../components/ContentViews/ErrorView.tsx | 5 +- .../ContentViews/FluidsReportListView.tsx | 6 +- .../components/ContentViews/FluidsView.tsx | 6 +- .../ContentViews/FormationMarkersListView.tsx | 8 +- .../components/ContentViews/JobsView.tsx | 6 +- .../ContentViews/LogCurveInfoListView.tsx | 8 +- .../components/ContentViews/LogsListView.tsx | 6 +- .../ContentViews/MessagesListView.tsx | 6 +- .../components/ContentViews/MudLogView.tsx | 6 +- .../ContentViews/MudLogsListView.tsx | 6 +- .../ContentViews/MultiLogCurveValuesView.tsx | 6 +- .../MultiLogsCurveInfoListView.tsx | 8 +- .../ContentViews/ObjectSearchListView.tsx | 4 +- .../components/ContentViews/QueryView.tsx | 4 +- .../components/ContentViews/RigsListView.tsx | 6 +- .../components/ContentViews/RisksListView.tsx | 6 +- .../components/ContentViews/ServerManager.tsx | 8 +- .../ContentViews/TrajectoriesListView.tsx | 6 +- .../ContentViews/TrajectoryView.tsx | 8 +- .../components/ContentViews/TubularView.tsx | 6 +- .../ContentViews/TubularsListView.tsx | 6 +- .../components/ContentViews/ViewNotFound.tsx | 5 +- .../ContentViews/WbGeometriesListView.tsx | 6 +- .../ContentViews/WbGeometryView.tsx | 6 +- .../ContentViews/WellboreSearchListView.tsx | 4 +- .../ContentViews/WellboresListView.tsx | 6 +- .../components/ContentViews/WellsListView.tsx | 6 +- .../ContentViews/table/ColumnDef.tsx | 6 +- .../ContentViews/table/ColumnOptionsMenu.tsx | 12 +- .../ContentViews/table/ContentTable.tsx | 6 +- .../components/ContentViews/table/Panel.tsx | 6 +- .../ContentViews/table/SortableEdsTable.tsx | 6 +- .../ContextMenus/BatchModifyMenuItem.tsx | 6 +- .../ContextMenus/BhaRunContextMenu.tsx | 6 +- .../components/ContextMenus/ContextMenu.tsx | 6 +- .../ContextMenus/ContextMenuPresenter.tsx | 6 +- .../ContextMenus/CopyComponentsToServer.tsx | 5 +- .../CopyComponentsToServerUtils.tsx | 6 +- .../ContextMenus/FluidContextMenu.tsx | 6 +- .../ContextMenus/FluidsReportContextMenu.tsx | 6 +- .../FormationMarkerContextMenu.tsx | 6 +- .../GeologyIntervalContextMenu.tsx | 6 +- .../LogCurvePriorityContextMenu.tsx | 6 +- .../ContextMenus/LogObjectContextMenu.tsx | 6 +- .../ContextMenus/MessageObjectContextMenu.tsx | 6 +- .../ContextMenus/MnemonicsContextMenu.tsx | 6 +- .../ContextMenus/MudLogContextMenu.tsx | 6 +- .../ContextMenus/NestedMenuItem.tsx | 11 +- .../ObjectsSidebarContextMenu.tsx | 6 +- .../ContextMenus/RigContextMenu.tsx | 6 +- .../ContextMenus/RigsContextMenu.tsx | 6 +- .../ContextMenus/RiskContextMenu.tsx | 6 +- .../ContextMenus/TrajectoriesContextMenu.tsx | 6 +- .../ContextMenus/TrajectoryContextMenu.tsx | 6 +- .../TrajectoryStationContextMenu.tsx | 6 +- .../TubularComponentContextMenu.tsx | 6 +- .../ContextMenus/TubularContextMenu.tsx | 6 +- .../ContextMenus/TubularsContextMenu.tsx | 6 +- .../ContextMenus/WbGeometryContextMenu.tsx | 6 +- .../WbGeometrySectionContextMenu.tsx | 6 +- .../ContextMenus/WellboreContextMenu.tsx | 6 +- .../components/GlobalStyles.tsx | 10 ++ .../components/Modals/AnalyzeGapModal.tsx | 6 +- .../Modals/BatchModifyPropertiesModal.tsx | 6 +- .../Modals/BhaRunPropertiesModal.tsx | 6 +- .../components/Modals/CompareLogDataModal.tsx | 6 +- .../components/Modals/CopyMnemonicsModal.tsx | 18 +-- .../components/Modals/CopyRangeModal.tsx | 6 +- .../Modals/DeleteEmptyMnemonicsModal.tsx | 6 +- .../Modals/FormationMarkerPropertiesModal.tsx | 12 +- .../Modals/GeologyIntervalPropertiesModal.tsx | 5 +- .../components/Modals/LogComparisonModal.tsx | 6 +- .../Modals/LogCurveInfoBatchUpdateModal.tsx | 6 +- .../Modals/LogCurvePriorityModal.tsx | 6 +- .../components/Modals/LogDataImportModal.tsx | 6 +- .../Modals/LogHeaderDateTimeField.tsx | 6 +- .../components/Modals/LogPropertiesModal.tsx | 6 +- .../Modals/MessageComparisonModal.tsx | 6 +- .../Modals/MessagePropertiesModal.tsx | 6 +- .../Modals/MissingDataAgentModal.tsx | 6 +- .../components/Modals/ModalDialog.tsx | 4 +- .../components/Modals/ModalPresenter.tsx | 6 +- .../Modals/MudLogPropertiesModal.tsx | 6 +- .../components/Modals/ObjectPickerModal.tsx | 6 +- .../components/Modals/OffsetLogCurveModal.tsx | 6 +- .../components/Modals/ReportModal.tsx | 4 +- .../components/Modals/RigPropertiesModal.tsx | 6 +- .../components/Modals/RiskPropertiesModal.tsx | 6 +- .../Modals/SelectIndexToDisplayModal.tsx | 6 +- .../components/Modals/ServerModal.tsx | 5 +- .../components/Modals/SettingsModal.tsx | 6 +- .../Modals/ShowLogDataOnServerModal.tsx | 6 +- .../components/Modals/SpliceLogsModal.tsx | 5 +- .../Modals/TrajectoryPropertiesModal.tsx | 6 +- .../TrajectoryStationPropertiesModal.tsx | 6 +- .../TrimLogObject/TrimLogObjectModal.tsx | 6 +- .../Modals/UserCredentialsModal.tsx | 6 +- .../Modals/WbGeometryPropertiesModal.tsx | 6 +- .../components/Modals/WellPropertiesModal.tsx | 6 +- .../Modals/WellborePropertiesModal.tsx | 6 +- .../components/PageLayout.tsx | 5 +- .../components/PropertiesPanel.tsx | 6 +- .../components/QueryEditor.tsx | 5 +- .../components/Sidebar/FilterPanel.tsx | 4 +- .../components/Sidebar/LogItem.tsx | 6 +- .../components/Sidebar/LogTypeItem.tsx | 6 +- .../components/Sidebar/ObjectGroupItem.tsx | 4 +- .../Sidebar/ObjectOnWellboreItem.tsx | 6 +- .../components/Sidebar/SearchFilter.tsx | 6 +- .../components/Sidebar/Sidebar.tsx | 6 +- .../components/Sidebar/TreeItem.tsx | 8 +- .../components/Sidebar/WellItem.tsx | 6 +- .../components/Sidebar/WellboreItem.tsx | 8 +- .../components/StyledComponents/Button.tsx | 6 +- .../components/TopRightCornerMenu.tsx | 5 +- .../contexts/MuiThemeProvider.tsx | 18 +++ .../contexts/operationStateProvider.tsx | 108 ++++++++++++++ .../contexts/queryContext.tsx | 4 +- .../hooks/query/queryRefreshHelpers.tsx | 4 +- .../hooks/useOpenInQueryView.tsx | 4 +- .../hooks/useOperationState.tsx | 11 ++ .../routes/AuthRoute.tsx | 6 +- .../routes/ItemNotFound.tsx | 5 +- .../routes/PageNotFound.tsx | 5 +- Src/WitsmlExplorer.Frontend/routes/Root.tsx | 136 +++--------------- 135 files changed, 545 insertions(+), 527 deletions(-) create mode 100644 Src/WitsmlExplorer.Frontend/contexts/MuiThemeProvider.tsx create mode 100644 Src/WitsmlExplorer.Frontend/contexts/operationStateProvider.tsx create mode 100644 Src/WitsmlExplorer.Frontend/hooks/useOperationState.tsx diff --git a/Src/WitsmlExplorer.Frontend/components/Alerts.tsx b/Src/WitsmlExplorer.Frontend/components/Alerts.tsx index b8de98cf2..77ae16507 100644 --- a/Src/WitsmlExplorer.Frontend/components/Alerts.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Alerts.tsx @@ -2,9 +2,9 @@ import { Icon } from "@equinor/eds-core-react"; import { Alert, AlertTitle, Collapse } from "@mui/material"; import { Button } from "components/StyledComponents/Button"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; +import { useOperationState } from "hooks/useOperationState"; import { capitalize } from "lodash"; -import React, { useContext, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import NotificationService from "services/notificationService"; import styled from "styled-components"; import { Colors } from "styles/Colors"; @@ -21,7 +21,7 @@ const Alerts = (): React.ReactElement => { const { connectedServer } = useConnectedServer(); const { operationState: { colors } - } = useContext(OperationContext); + } = useOperationState(); useEffect(() => { const unsubscribeOnConnectionStateChanged = diff --git a/Src/WitsmlExplorer.Frontend/components/ApplicationVersion.tsx b/Src/WitsmlExplorer.Frontend/components/ApplicationVersion.tsx index 152bf378f..356f1f6be 100644 --- a/Src/WitsmlExplorer.Frontend/components/ApplicationVersion.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ApplicationVersion.tsx @@ -1,9 +1,9 @@ import { EdsProvider, Typography } from "@equinor/eds-core-react"; import { Button } from "components/StyledComponents/Button"; -import OperationContext from "contexts/operationContext"; import { UserTheme } from "contexts/operationStateReducer"; +import { useOperationState } from "hooks/useOperationState"; import JobStatus from "models/jobStatus"; -import { useCallback, useContext, useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import JobService from "services/jobService"; import NotificationService from "services/notificationService"; import styled from "styled-components"; @@ -58,7 +58,7 @@ interface UpdateStatus { function DesktopUpdateStatus() { const { operationState: { colors } - } = useContext(OperationContext); + } = useOperationState(); const [updateStatus, setUpdateStatus] = useState(null); useEffect(() => { diff --git a/Src/WitsmlExplorer.Frontend/components/Breadcrumbs.tsx b/Src/WitsmlExplorer.Frontend/components/Breadcrumbs.tsx index 635620dc8..c09036f99 100644 --- a/Src/WitsmlExplorer.Frontend/components/Breadcrumbs.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Breadcrumbs.tsx @@ -1,10 +1,10 @@ import { Breadcrumbs as EdsBreadcrumbs } from "@equinor/eds-core-react"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import { useGetObject } from "hooks/query/useGetObject"; import { useGetWell } from "hooks/query/useGetWell"; import { useGetWellbore } from "hooks/query/useGetWellbore"; import { useGetActiveRoute } from "hooks/useGetActiveRoute"; +import { useOperationState } from "hooks/useOperationState"; import { capitalize } from "lodash"; import { ObjectType, @@ -14,7 +14,7 @@ import { import { Server } from "models/server"; import Well from "models/well"; import Wellbore from "models/wellbore"; -import { useContext, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { NavLink, NavigateFunction, @@ -42,7 +42,7 @@ import { v4 as uuid } from "uuid"; export function Breadcrumbs() { const { operationState: { colors } - } = useContext(OperationContext); + } = useOperationState(); const navigate = useNavigate(); const { isJobsView, diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/BhaRunsListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/BhaRunsListView.tsx index 8456c817d..5cb846d35 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/BhaRunsListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/BhaRunsListView.tsx @@ -9,13 +9,13 @@ import { getContextMenuPosition } from "components/ContextMenus/ContextMenu"; import { ObjectContextMenuProps } from "components/ContextMenus/ObjectMenuItems"; import formatDateString from "components/DateFormatter"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetObjects } from "hooks/query/useGetObjects"; import { useExpandSidebarNodes } from "hooks/useExpandObjectGroupNodes"; +import { useOperationState } from "hooks/useOperationState"; import BhaRun from "models/bhaRun"; import { ObjectType } from "models/objectType"; -import { MouseEvent, useContext } from "react"; +import { MouseEvent } from "react"; import { useParams } from "react-router-dom"; export interface BhaRunRow extends ContentTableRow, BhaRun { @@ -26,7 +26,7 @@ export default function BhaRunsListView() { const { operationState: { timeZone, dateTimeFormat }, dispatchOperation - } = useContext(OperationContext); + } = useOperationState(); const { wellUid, wellboreUid } = useParams(); const { connectedServer } = useConnectedServer(); const { objects: bhaRuns } = useGetObjects( diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/ChangeLogsListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/ChangeLogsListView.tsx index 40e02906a..9b0b97f86 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/ChangeLogsListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/ChangeLogsListView.tsx @@ -5,17 +5,16 @@ import { } from "components/ContentViews/table"; import formatDateString from "components/DateFormatter"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import { useGetObjects } from "hooks/query/useGetObjects"; import { useExpandSidebarNodes } from "hooks/useExpandObjectGroupNodes"; +import { useOperationState } from "hooks/useOperationState"; import { ObjectType } from "models/objectType"; -import { useContext } from "react"; import { useParams } from "react-router-dom"; export default function ChangeLogsListView() { const { operationState: { timeZone, dateTimeFormat } - } = useContext(OperationContext); + } = useOperationState(); const { wellUid, wellboreUid } = useParams(); const { connectedServer } = useConnectedServer(); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/Charts/LogsGraph.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/Charts/LogsGraph.tsx index 0572d96de..79d6c7e2d 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/Charts/LogsGraph.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/Charts/LogsGraph.tsx @@ -4,6 +4,7 @@ import { CustomSeriesRenderItemAPI, CustomSeriesRenderItemParams } from "echarts"; +import { useOperationState } from "hooks/useOperationState"; import { ReactEChartsProps, @@ -11,10 +12,9 @@ import { } from "components/ContentViews/Charts/ReactLogChart"; import { ContentTableRow } from "components/ContentViews/table"; import formatDateString from "components/DateFormatter"; -import OperationContext from "contexts/operationContext"; import { DateTimeFormat } from "contexts/operationStateReducer"; import LogObject from "models/logObject"; -import React, { useContext } from "react"; +import React from "react"; import { useParams } from "react-router-dom"; import { RouterLogType } from "routes/routerConstants"; @@ -57,11 +57,11 @@ export const LogsGraph = (props: LogsGraphProps): React.ReactElement => { const categories: number[] = []; const { operationState: { colors } - } = useContext(OperationContext); + } = useOperationState(); const { logs } = props; const { operationState: { timeZone, dateTimeFormat } - } = useContext(OperationContext); + } = useOperationState(); const { logType } = useParams(); const isTimeIndexed = logType === RouterLogType.TIME; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesPlot.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesPlot.tsx index 5b7209379..a16084d24 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesPlot.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesPlot.tsx @@ -4,10 +4,10 @@ import { } from "components/ContentViews/table"; import formatDateString from "components/DateFormatter"; import { ContentViewDimensionsContext } from "components/PageLayout"; -import OperationContext from "contexts/operationContext"; import { DateTimeFormat, TimeZone } from "contexts/operationStateReducer"; import { ECharts } from "echarts"; import ReactEcharts from "echarts-for-react"; +import { useOperationState } from "hooks/useOperationState"; import { CurveSpecification } from "models/logData"; import React, { useContext, useEffect, useRef, useState } from "react"; import { useParams } from "react-router-dom"; @@ -46,7 +46,7 @@ export const CurveValuesPlot = React.memo( ); const { operationState: { colors, dateTimeFormat } - } = useContext(OperationContext); + } = useOperationState(); const chart = useRef(null); const selectedLabels = useRef>(null); const scrollIndex = useRef(0); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx index b90f409c3..74dab7310 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx @@ -30,7 +30,6 @@ import { ShowLogDataOnServerModal } from "components/Modals/ShowLogDataOnServerM import ProgressSpinner from "components/ProgressSpinner"; import { Button } from "components/StyledComponents/Button"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import { DispatchOperation, UserTheme } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; import { useGetComponents } from "hooks/query/useGetComponents"; @@ -38,6 +37,7 @@ import { useGetObject } from "hooks/query/useGetObject"; import { useExpandSidebarNodes } from "hooks/useExpandObjectGroupNodes"; import useExport from "hooks/useExport"; import { useGetMnemonics } from "hooks/useGetMnemonics"; +import { useOperationState } from "hooks/useOperationState"; import orderBy from "lodash/orderBy"; import { ComponentType } from "models/componentType"; import { IndexRange } from "models/jobs/deleteLogCurveValuesJob"; @@ -48,7 +48,6 @@ import { ObjectType } from "models/objectType"; import React, { CSSProperties, useCallback, - useContext, useEffect, useMemo, useRef, @@ -90,7 +89,7 @@ export const CurveValuesView = (): React.ReactElement => { const { operationState: { timeZone, dateTimeFormat, colors, theme }, dispatchOperation - } = useContext(OperationContext); + } = useOperationState(); const [searchParams, setSearchParams] = useSearchParams(); const mnemonicsSearchParams = searchParams.get("mnemonics"); const startIndex = searchParams.get("startIndex"); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/EditNumber.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/EditNumber.tsx index 552494ca4..2d8f0a4a7 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/EditNumber.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/EditNumber.tsx @@ -1,8 +1,8 @@ import { Icon, Label, TextField } from "@equinor/eds-core-react"; import { Tooltip } from "@mui/material"; import { Button } from "components/StyledComponents/Button"; -import OperationContext from "contexts/operationContext"; -import { ChangeEvent, ReactElement, useContext, useState } from "react"; +import { useOperationState } from "hooks/useOperationState"; +import { ChangeEvent, ReactElement, useState } from "react"; import styled from "styled-components"; import { TooltipLayout } from "../StyledComponents/Tooltip"; @@ -24,7 +24,7 @@ const EditNumber = (props: EditNumberProps): ReactElement => { } = props; const { operationState: { colors } - } = useContext(OperationContext); + } = useOperationState(); const [isEdited, setIsEdited] = useState(false); const [value, setValue] = useState(String(defaultValue)); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/EditSelectedLogCurveInfo.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/EditSelectedLogCurveInfo.tsx index e13d320f3..b8a34e0ff 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/EditSelectedLogCurveInfo.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/EditSelectedLogCurveInfo.tsx @@ -11,17 +11,16 @@ import formatDateString, { } from "components/DateFormatter"; import { Button } from "components/StyledComponents/Button"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import { DateTimeFormat, TimeZone } from "contexts/operationStateReducer"; import { useGetComponents } from "hooks/query/useGetComponents"; import { useGetMnemonics } from "hooks/useGetMnemonics"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import { CSSProperties, ChangeEvent, Dispatch, SetStateAction, - useContext, useEffect, useState } from "react"; @@ -49,7 +48,7 @@ const EditSelectedLogCurveInfo = ( ): React.ReactElement => { const { disabled, overrideStartIndex, overrideEndIndex, onClickRefresh } = props; - const { operationState } = useContext(OperationContext); + const { operationState } = useOperationState(); const { theme, colors, timeZone } = operationState; const { wellUid, wellboreUid, logType, objectUid } = useParams(); const isTimeLog = logType === RouterLogType.TIME; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/ErrorView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/ErrorView.tsx index c0c97205a..8d2784864 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/ErrorView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/ErrorView.tsx @@ -1,6 +1,5 @@ -import { useContext } from "react"; +import { useOperationState } from "hooks/useOperationState"; import styled from "styled-components"; -import OperationContext from "../../contexts/operationContext"; import { useErrorMessage } from "../../hooks/useErrorMessage"; import { Colors } from "../../styles/Colors"; import Icon from "../../styles/Icons"; @@ -9,7 +8,7 @@ export function ErrorView() { const errorMessage = useErrorMessage(); const { operationState: { colors } - } = useContext(OperationContext); + } = useOperationState(); return ( <> diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsReportListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsReportListView.tsx index c304dace5..75be91593 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsReportListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsReportListView.tsx @@ -9,14 +9,14 @@ import FluidsReportContextMenu from "components/ContextMenus/FluidsReportContext import { ObjectContextMenuProps } from "components/ContextMenus/ObjectMenuItems"; import formatDateString from "components/DateFormatter"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetObjects } from "hooks/query/useGetObjects"; import { useExpandSidebarNodes } from "hooks/useExpandObjectGroupNodes"; +import { useOperationState } from "hooks/useOperationState"; import FluidsReport from "models/fluidsReport"; import { measureToString } from "models/measure"; import { ObjectType } from "models/objectType"; -import { MouseEvent, useContext } from "react"; +import { MouseEvent } from "react"; import { useNavigate, useParams } from "react-router-dom"; export interface FluidsReportRow extends ContentTableRow, FluidsReport { @@ -27,7 +27,7 @@ export default function FluidsReportsListView() { const { operationState: { timeZone, dateTimeFormat }, dispatchOperation - } = useContext(OperationContext); + } = useOperationState(); const { connectedServer } = useConnectedServer(); const { wellUid, wellboreUid } = useParams(); const navigate = useNavigate(); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsView.tsx index 4ebf9acce..1faddc987 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsView.tsx @@ -10,16 +10,16 @@ import FluidContextMenu, { } from "components/ContextMenus/FluidContextMenu"; import ProgressSpinner from "components/ProgressSpinner"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetComponents } from "hooks/query/useGetComponents"; import { useGetObject } from "hooks/query/useGetObject"; import { useExpandSidebarNodes } from "hooks/useExpandObjectGroupNodes"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import Fluid from "models/fluid"; import { measureToString } from "models/measure"; import { ObjectType } from "models/objectType"; -import React, { useContext } from "react"; +import React from "react"; import { useParams } from "react-router-dom"; import { ItemNotFound } from "routes/ItemNotFound"; @@ -32,7 +32,7 @@ interface FluidsRow extends ContentTableRow, FluidAsStrings { } export default function FluidsView() { - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const { wellUid, wellboreUid, objectUid } = useParams(); const { connectedServer } = useConnectedServer(); const { object: fluidsReport } = useGetObject( diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/FormationMarkersListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/FormationMarkersListView.tsx index b5fc406cd..72d482a5d 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/FormationMarkersListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/FormationMarkersListView.tsx @@ -9,15 +9,15 @@ import FormationMarkerContextMenu from "components/ContextMenus/FormationMarkerC import { ObjectContextMenuProps } from "components/ContextMenus/ObjectMenuItems"; import formatDateString from "components/DateFormatter"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetObjects } from "hooks/query/useGetObjects"; import { useExpandSidebarNodes } from "hooks/useExpandObjectGroupNodes"; +import { useOperationState } from "hooks/useOperationState"; import FormationMarker from "models/formationMarker"; import { measureToString } from "models/measure"; import { ObjectType } from "models/objectType"; import StratigraphicStruct from "models/stratigraphicStruct"; -import { MouseEvent, useContext } from "react"; +import { MouseEvent } from "react"; import { useParams } from "react-router-dom"; export interface FormationMarkerRow extends ContentTableRow { @@ -27,8 +27,8 @@ export interface FormationMarkerRow extends ContentTableRow { export default function FormationMarkersListView() { const { operationState: { timeZone, dateTimeFormat } - } = useContext(OperationContext); - const { dispatchOperation } = useContext(OperationContext); + } = useOperationState(); + const { dispatchOperation } = useOperationState(); const { connectedServer } = useConnectedServer(); const { wellUid, wellboreUid } = useParams(); const { objects: formationMarkers } = useGetObjects( diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/JobsView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/JobsView.tsx index 81cac4dcc..18c7fe122 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/JobsView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/JobsView.tsx @@ -14,12 +14,12 @@ import ConfirmModal from "components/Modals/ConfirmModal"; import { ReportModal } from "components/Modals/ReportModal"; import { generateReport } from "components/ReportCreationHelper"; import { Button } from "components/StyledComponents/Button"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { refreshJobInfoQuery } from "hooks/query/queryRefreshHelpers"; import { useGetJobInfo } from "hooks/query/useGetJobInfo"; import { useGetServers } from "hooks/query/useGetServers"; import useExport from "hooks/useExport"; +import { useOperationState } from "hooks/useOperationState"; import JobStatus from "models/jobStatus"; import JobInfo from "models/jobs/jobInfo"; import ReportType from "models/reportType"; @@ -30,7 +30,7 @@ import { getUserAppRoles, msalEnabled } from "msal/MsalAuthProvider"; -import React, { ChangeEvent, useContext, useMemo, useState } from "react"; +import React, { ChangeEvent, useMemo, useState } from "react"; import JobService from "services/jobService"; import styled from "styled-components"; import { Colors } from "styles/Colors"; @@ -39,7 +39,7 @@ export const JobsView = (): React.ReactElement => { const { dispatchOperation, operationState: { timeZone, colors, dateTimeFormat } - } = useContext(OperationContext); + } = useOperationState(); const queryClient = useQueryClient(); const { servers } = useGetServers(); const [showAll, setShowAll] = useState(false); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListView.tsx index f4aa9cdf6..1d039ed6c 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListView.tsx @@ -8,17 +8,17 @@ import ProgressSpinner from "components/ProgressSpinner"; import { CommonPanelContainer } from "components/StyledComponents/Container"; import { useConnectedServer } from "contexts/connectedServerContext"; import { useCurveThreshold } from "contexts/curveThresholdContext"; -import OperationContext from "contexts/operationContext"; import { UserTheme } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; import { useGetComponents } from "hooks/query/useGetComponents"; import { useGetObject } from "hooks/query/useGetObject"; import { useGetServers } from "hooks/query/useGetServers"; import { useExpandSidebarNodes } from "hooks/useExpandObjectGroupNodes"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import LogObject from "models/logObject"; import { ObjectType } from "models/objectType"; -import React, { useContext, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; import { ItemNotFound } from "routes/ItemNotFound"; import { RouterLogType } from "routes/routerConstants"; @@ -34,8 +34,8 @@ export default function LogCurveInfoListView() { const { curveThreshold } = useCurveThreshold(); const { operationState: { timeZone, dateTimeFormat, theme } - } = useContext(OperationContext); - const { dispatchOperation } = useContext(OperationContext); + } = useOperationState(); + const { dispatchOperation } = useOperationState(); const { wellUid, wellboreUid, logType, objectUid } = useParams(); const { connectedServer } = useConnectedServer(); const { servers } = useGetServers(); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogsListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogsListView.tsx index 83f6b4135..8eddec227 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogsListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogsListView.tsx @@ -16,15 +16,15 @@ import { ObjectContextMenuProps } from "components/ContextMenus/ObjectMenuItems" import formatDateString from "components/DateFormatter"; import ProgressSpinner from "components/ProgressSpinner"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import { UserTheme } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; import { useGetObjects } from "hooks/query/useGetObjects"; import { useGetWellbore } from "hooks/query/useGetWellbore"; import { useExpandSidebarNodes } from "hooks/useExpandObjectGroupNodes"; +import { useOperationState } from "hooks/useOperationState"; import LogObject from "models/logObject"; import { ObjectType } from "models/objectType"; -import { MouseEvent, useContext, useState } from "react"; +import { MouseEvent, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { ItemNotFound } from "routes/ItemNotFound"; import { RouterLogType } from "routes/routerConstants"; @@ -42,7 +42,7 @@ export default function LogsListView() { const { dispatchOperation, operationState: { timeZone, dateTimeFormat, theme } - } = useContext(OperationContext); + } = useOperationState(); const [showGraph, setShowGraph] = useState(false); const [selectedRows, setSelectedRows] = useState([]); const navigate = useNavigate(); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/MessagesListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/MessagesListView.tsx index 3527c533a..395b6936e 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/MessagesListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/MessagesListView.tsx @@ -9,13 +9,13 @@ import MessageObjectContextMenu from "components/ContextMenus/MessageObjectConte import { ObjectContextMenuProps } from "components/ContextMenus/ObjectMenuItems"; import formatDateString from "components/DateFormatter"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetObjects } from "hooks/query/useGetObjects"; import { useExpandSidebarNodes } from "hooks/useExpandObjectGroupNodes"; +import { useOperationState } from "hooks/useOperationState"; import MessageObject from "models/messageObject"; import { ObjectType } from "models/objectType"; -import { MouseEvent, useContext } from "react"; +import { MouseEvent } from "react"; import { useParams } from "react-router-dom"; export interface MessageObjectRow extends ContentTableRow { @@ -26,7 +26,7 @@ export default function MessagesListView() { const { operationState: { timeZone, dateTimeFormat }, dispatchOperation - } = useContext(OperationContext); + } = useOperationState(); const { wellUid, wellboreUid } = useParams(); const { connectedServer } = useConnectedServer(); const { objects: messages } = useGetObjects( diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/MudLogView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/MudLogView.tsx index 3019a76c7..4a49c9113 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/MudLogView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/MudLogView.tsx @@ -10,16 +10,16 @@ import GeologyIntervalContextMenu, { } from "components/ContextMenus/GeologyIntervalContextMenu"; import ProgressSpinner from "components/ProgressSpinner"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetComponents } from "hooks/query/useGetComponents"; import { useGetObject } from "hooks/query/useGetObject"; import { useExpandSidebarNodes } from "hooks/useExpandObjectGroupNodes"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import GeologyInterval from "models/geologyInterval"; import { measureToString } from "models/measure"; import { ObjectType } from "models/objectType"; -import React, { useContext } from "react"; +import React from "react"; import { useParams } from "react-router-dom"; import { ItemNotFound } from "routes/ItemNotFound"; @@ -43,7 +43,7 @@ export interface GeologyIntervalRow extends ContentTableRow { } export default function MudLogView() { - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const { wellUid, wellboreUid, objectUid } = useParams(); const { connectedServer } = useConnectedServer(); const { object: mudLog, isFetched: isFetchedMudLog } = useGetObject( diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/MudLogsListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/MudLogsListView.tsx index 14721a487..ead398aa8 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/MudLogsListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/MudLogsListView.tsx @@ -9,14 +9,14 @@ import MudLogContextMenu from "components/ContextMenus/MudLogContextMenu"; import { ObjectContextMenuProps } from "components/ContextMenus/ObjectMenuItems"; import formatDateString from "components/DateFormatter"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetObjects } from "hooks/query/useGetObjects"; import { useExpandSidebarNodes } from "hooks/useExpandObjectGroupNodes"; +import { useOperationState } from "hooks/useOperationState"; import { measureToString } from "models/measure"; import MudLog from "models/mudLog"; import { ObjectType } from "models/objectType"; -import { MouseEvent, useContext } from "react"; +import { MouseEvent } from "react"; import { useNavigate, useParams } from "react-router-dom"; export interface MudLogRow extends ContentTableRow { @@ -35,7 +35,7 @@ export default function MudLogsListView() { const { dispatchOperation, operationState: { timeZone, dateTimeFormat } - } = useContext(OperationContext); + } = useOperationState(); const navigate = useNavigate(); const { connectedServer } = useConnectedServer(); const { wellUid, wellboreUid } = useParams(); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/MultiLogCurveValuesView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/MultiLogCurveValuesView.tsx index 103a1870d..700352779 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/MultiLogCurveValuesView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/MultiLogCurveValuesView.tsx @@ -10,14 +10,14 @@ import { import formatDateString from "components/DateFormatter"; import ProgressSpinner from "components/ProgressSpinner"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import { UserTheme } from "contexts/operationStateReducer"; import { useGetObjects } from "hooks/query/useGetObjects"; import { useExpandSidebarNodes } from "hooks/useExpandObjectGroupNodes"; +import { useOperationState } from "hooks/useOperationState"; import { CurveSpecification, LogData, LogDataRow } from "models/logData"; import LogObject from "models/logObject"; import { ObjectType } from "models/objectType"; -import React, { useContext, useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { useLocation, useParams, useSearchParams } from "react-router-dom"; import { ItemNotFound } from "routes/ItemNotFound"; import { truncateAbortHandler } from "services/apiClient"; @@ -34,7 +34,7 @@ interface CurveValueRow extends LogDataRow, ContentTableRow {} export const MultiLogCurveValuesView = (): React.ReactElement => { const { operationState: { timeZone, dateTimeFormat, theme } - } = useContext(OperationContext); + } = useOperationState(); const [searchParams] = useSearchParams(); const location = useLocation(); const mnemonicsSearchParams = searchParams.get("mnemonics"); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/MultiLogsCurveInfoListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/MultiLogsCurveInfoListView.tsx index 8f3f8f18c..7e9c9bd03 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/MultiLogsCurveInfoListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/MultiLogsCurveInfoListView.tsx @@ -4,13 +4,13 @@ import { getContextMenuPosition } from "components/ContextMenus/ContextMenu"; import LogCurveInfoContextMenu, { LogCurveInfoContextMenuProps } from "components/ContextMenus/LogCurveInfoContextMenu"; +import { useOperationState } from "hooks/useOperationState"; import ProgressSpinner from "components/ProgressSpinner"; import { CommonPanelContainer } from "components/StyledComponents/Container"; import { useConnectedServer } from "contexts/connectedServerContext"; import { useCurveThreshold } from "contexts/curveThresholdContext"; -import OperationContext from "contexts/operationContext"; import { UserTheme } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; import { useGetObjects } from "hooks/query/useGetObjects"; @@ -19,7 +19,7 @@ import { useExpandSidebarNodes } from "hooks/useExpandObjectGroupNodes"; import LogCurveInfo from "models/logCurveInfo"; import LogObject from "models/logObject"; import { ObjectType } from "models/objectType"; -import React, { useContext, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { useParams, useSearchParams } from "react-router-dom"; import { RouterLogType } from "routes/routerConstants"; import { truncateAbortHandler } from "services/apiClient"; @@ -35,8 +35,8 @@ export default function MultiLogsCurveInfoListView() { const { curveThreshold } = useCurveThreshold(); const { operationState: { timeZone, dateTimeFormat, theme } - } = useContext(OperationContext); - const { dispatchOperation } = useContext(OperationContext); + } = useOperationState(); + const { dispatchOperation } = useOperationState(); const { wellUid, wellboreUid, logType } = useParams(); const { connectedServer } = useConnectedServer(); const [logCurveInfoList, setLogCurveInfoList] = useState(); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/ObjectSearchListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/ObjectSearchListView.tsx index 9463af73f..b2177438d 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/ObjectSearchListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/ObjectSearchListView.tsx @@ -23,10 +23,10 @@ import { ObjectFilterType, filterTypeToProperty } from "contexts/filter"; -import OperationContext from "contexts/operationContext"; import { MousePosition } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; import { useGetObjectSearch } from "hooks/query/useGetObjectSearch"; +import { useOperationState } from "hooks/useOperationState"; import LogObject from "models/logObject"; import ObjectOnWellbore from "models/objectOnWellbore"; import { ObjectType } from "models/objectType"; @@ -58,7 +58,7 @@ export interface ObjectSearchRow extends ContentTableRow, ObjectOnWellbore { export const ObjectSearchListView = (): ReactElement => { const { connectedServer } = useConnectedServer(); - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const { selectedFilter, updateSelectedFilter } = useContext(FilterContext); const [searchParams] = useSearchParams(); const { filterType } = useParams<{ filterType: ObjectFilterType }>(); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView.tsx index 3b82d3315..0a7c533a7 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView.tsx @@ -12,10 +12,10 @@ import { QueryEditor } from "components/QueryEditor"; import { getTag } from "components/QueryEditorUtils"; import { StyledNativeSelect } from "components/Select"; import { Button } from "components/StyledComponents/Button"; -import OperationContext from "contexts/operationContext"; import { DispatchOperation } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; import { QueryActionType, QueryContext } from "contexts/queryContext"; +import { useOperationState } from "hooks/useOperationState"; import React, { ChangeEvent, useContext, useState } from "react"; import QueryService from "services/queryService"; import styled from "styled-components"; @@ -26,7 +26,7 @@ const QueryView = (): React.ReactElement => { const { operationState: { colors }, dispatchOperation - } = useContext(OperationContext); + } = useOperationState(); const { queryState: { queries, tabIndex }, dispatchQuery diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/RigsListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/RigsListView.tsx index 6026412a9..9789c2bdf 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/RigsListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/RigsListView.tsx @@ -9,13 +9,13 @@ import { ObjectContextMenuProps } from "components/ContextMenus/ObjectMenuItems" import RigContextMenu from "components/ContextMenus/RigContextMenu"; import formatDateString from "components/DateFormatter"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetObjects } from "hooks/query/useGetObjects"; import { useExpandSidebarNodes } from "hooks/useExpandObjectGroupNodes"; +import { useOperationState } from "hooks/useOperationState"; import { ObjectType } from "models/objectType"; import Rig from "models/rig"; -import { MouseEvent, useContext } from "react"; +import { MouseEvent } from "react"; import { useParams } from "react-router-dom"; export interface RigRow extends ContentTableRow, Rig { @@ -26,7 +26,7 @@ export default function RigsListView() { const { operationState: { timeZone, dateTimeFormat }, dispatchOperation - } = useContext(OperationContext); + } = useOperationState(); const { wellUid, wellboreUid } = useParams(); const { connectedServer } = useConnectedServer(); const { objects: rigs } = useGetObjects( diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/RisksListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/RisksListView.tsx index 3e652fdba..215db894a 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/RisksListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/RisksListView.tsx @@ -9,13 +9,13 @@ import { ObjectContextMenuProps } from "components/ContextMenus/ObjectMenuItems" import RiskObjectContextMenu from "components/ContextMenus/RiskContextMenu"; import formatDateString from "components/DateFormatter"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetObjects } from "hooks/query/useGetObjects"; import { useExpandSidebarNodes } from "hooks/useExpandObjectGroupNodes"; +import { useOperationState } from "hooks/useOperationState"; import { ObjectType } from "models/objectType"; import RiskObject from "models/riskObject"; -import { MouseEvent, useContext } from "react"; +import { MouseEvent } from "react"; import { useParams } from "react-router-dom"; export interface RiskObjectRow extends ContentTableRow, RiskObject { @@ -26,7 +26,7 @@ export default function RisksListView() { const { operationState: { timeZone, dateTimeFormat }, dispatchOperation - } = useContext(OperationContext); + } = useOperationState(); const { wellUid, wellboreUid } = useParams(); const { connectedServer } = useConnectedServer(); const { objects: risks } = useGetObjects( diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/ServerManager.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/ServerManager.tsx index 6eb7ddf7a..a08b91ebb 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/ServerManager.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/ServerManager.tsx @@ -18,12 +18,12 @@ import { useConnectedServer } from "contexts/connectedServerContext"; import { getSearchRegex } from "contexts/filter"; import { useLoggedInUsernames } from "contexts/loggedInUsernamesContext"; import { LoggedInUsernamesActionType } from "contexts/loggedInUsernamesReducer"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; +import { useOperationState } from "hooks/useOperationState"; import { Server, emptyServer } from "models/server"; import { adminRole, getUserAppRoles, msalEnabled } from "msal/MsalAuthProvider"; -import React, { ChangeEvent, useContext, useEffect, useState } from "react"; +import React, { ChangeEvent, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { getWellsViewPath } from "routes/utils/pathBuilder"; import AuthorizationService from "services/authorizationService"; @@ -44,7 +44,7 @@ const ServerManager = (): React.ReactElement => { const { operationState: { colors }, dispatchOperation - } = useContext(OperationContext); + } = useOperationState(); const editDisabled = msalEnabled && !getUserAppRoles().includes(adminRole); const navigate = useNavigate(); const { connectedServer, setConnectedServer } = useConnectedServer(); @@ -269,7 +269,7 @@ const ConnectButton = ({ isConnected, ...props }: ConnectButtonProps) => { const [isHovered, setIsHovered] = React.useState(false); const { operationState: { colors } - } = useContext(OperationContext); + } = useOperationState(); return ( 404 Not Found diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/WbGeometriesListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/WbGeometriesListView.tsx index 71e2cbf10..760496de9 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/WbGeometriesListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/WbGeometriesListView.tsx @@ -9,14 +9,14 @@ import { ObjectContextMenuProps } from "components/ContextMenus/ObjectMenuItems" import WbGeometryObjectContextMenu from "components/ContextMenus/WbGeometryContextMenu"; import formatDateString from "components/DateFormatter"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetObjects } from "hooks/query/useGetObjects"; import { useExpandSidebarNodes } from "hooks/useExpandObjectGroupNodes"; +import { useOperationState } from "hooks/useOperationState"; import { measureToString } from "models/measure"; import { ObjectType } from "models/objectType"; import WbGeometryObject from "models/wbGeometry"; -import { MouseEvent, useContext } from "react"; +import { MouseEvent } from "react"; import { useNavigate, useParams } from "react-router-dom"; export interface WbGeometryObjectRow extends ContentTableRow, WbGeometryObject { @@ -27,7 +27,7 @@ export default function WbGeometriesListView() { const { operationState: { timeZone, dateTimeFormat }, dispatchOperation - } = useContext(OperationContext); + } = useOperationState(); const navigate = useNavigate(); const { connectedServer } = useConnectedServer(); const { wellUid, wellboreUid } = useParams(); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/WbGeometryView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/WbGeometryView.tsx index 58647fce7..0f3052703 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/WbGeometryView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/WbGeometryView.tsx @@ -10,16 +10,16 @@ import WbGeometrySectionContextMenu, { } from "components/ContextMenus/WbGeometrySectionContextMenu"; import ProgressSpinner from "components/ProgressSpinner"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetComponents } from "hooks/query/useGetComponents"; import { useGetObject } from "hooks/query/useGetObject"; import { useExpandSidebarNodes } from "hooks/useExpandObjectGroupNodes"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import { measureToString } from "models/measure"; import { ObjectType } from "models/objectType"; import WbGeometrySection from "models/wbGeometrySection"; -import React, { useContext } from "react"; +import React from "react"; import { useParams } from "react-router-dom"; import { ItemNotFound } from "routes/ItemNotFound"; @@ -28,7 +28,7 @@ interface WbGeometrySectionRow extends ContentTableRow { } export default function WbGeometryView() { - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const { wellUid, wellboreUid, objectUid } = useParams(); const { connectedServer } = useConnectedServer(); const { object: wbGeometry, isFetched: isFetchedWbGeometry } = useGetObject( diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboreSearchListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboreSearchListView.tsx index 3ac4de14b..5fe3c097c 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboreSearchListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboreSearchListView.tsx @@ -17,11 +17,11 @@ import { WellboreFilterType, filterTypeToProperty } from "contexts/filter"; -import OperationContext from "contexts/operationContext"; import { MousePosition } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useGetWellboreSearch } from "hooks/query/useGetWellboreSearch"; +import { useOperationState } from "hooks/useOperationState"; import Well from "models/well"; import Wellbore from "models/wellbore"; import React, { ReactElement, useContext, useEffect } from "react"; @@ -37,7 +37,7 @@ export interface WellboreSearchRow extends ContentTableRow, Wellbore { export const WellboreSearchListView = (): ReactElement => { const { connectedServer } = useConnectedServer(); - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const { selectedFilter, updateSelectedFilter } = useContext(FilterContext); const [searchParams] = useSearchParams(); const { filterType } = useParams<{ filterType: WellboreFilterType }>(); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboresListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboresListView.tsx index e33f3a5d8..a1a994b5d 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboresListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboresListView.tsx @@ -11,15 +11,15 @@ import WellboreContextMenu, { import formatDateString from "components/DateFormatter"; import ProgressSpinner from "components/ProgressSpinner"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useGetWell } from "hooks/query/useGetWell"; import { useGetWellbores } from "hooks/query/useGetWellbores"; import { useExpandSidebarNodes } from "hooks/useExpandObjectGroupNodes"; +import { useOperationState } from "hooks/useOperationState"; import EntityType from "models/entityType"; import Wellbore from "models/wellbore"; -import React, { useContext } from "react"; +import React from "react"; import { useNavigate, useParams } from "react-router-dom"; import { ItemNotFound } from "routes/ItemNotFound"; import { OBJECT_GROUPS_PATH } from "routes/routerConstants"; @@ -43,7 +43,7 @@ export default function WellboresListView() { const { dispatchOperation, operationState: { timeZone, dateTimeFormat } - } = useContext(OperationContext); + } = useOperationState(); const navigate = useNavigate(); useExpandSidebarNodes(wellUid); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/WellsListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/WellsListView.tsx index 055bc9465..cdaec9146 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/WellsListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/WellsListView.tsx @@ -12,12 +12,12 @@ import WellContextMenu, { import formatDateString from "components/DateFormatter"; import ProgressSpinner from "components/ProgressSpinner"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useGetWells } from "hooks/query/useGetWells"; +import { useOperationState } from "hooks/useOperationState"; import Well from "models/well"; -import React, { useContext } from "react"; +import React from "react"; import { useNavigate } from "react-router-dom"; import { WELLBORES_PATH } from "routes/routerConstants"; @@ -32,7 +32,7 @@ export default function WellsListView() { const { dispatchOperation, operationState: { timeZone, dateTimeFormat } - } = useContext(OperationContext); + } = useOperationState(); const navigate = useNavigate(); const columns: ContentTableColumn[] = [ diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnDef.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnDef.tsx index da2bc7516..7cfbc7593 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnDef.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnDef.tsx @@ -21,9 +21,9 @@ import { ContentType } from "components/ContentViews/table/tableParts"; import { getSearchRegex } from "contexts/filter"; -import OperationContext from "contexts/operationContext"; import { DecimalPreference, UserTheme } from "contexts/operationStateReducer"; -import { useContext, useMemo } from "react"; +import { useOperationState } from "hooks/useOperationState"; +import { useMemo } from "react"; import Icon from "styles/Icons"; import { STORAGE_CONTENTTABLE_ORDER_KEY, @@ -47,7 +47,7 @@ export const useColumnDef = ( ) => { const { operationState: { decimals, theme } - } = useContext(OperationContext); + } = useOperationState(); const isCompactMode = theme === UserTheme.Compact; return useMemo(() => { diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnOptionsMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnOptionsMenu.tsx index d7e599d38..0ddf32630 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnOptionsMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnOptionsMenu.tsx @@ -18,17 +18,11 @@ import { ContentType } from "components/ContentViews/table/tableParts"; import { Button } from "components/StyledComponents/Button"; -import OperationContext from "contexts/operationContext"; import { UserTheme } from "contexts/operationStateReducer"; import { useLocalStorageState } from "hooks/useLocalStorageState"; +import { useOperationState } from "hooks/useOperationState"; import { debounce } from "lodash"; -import { - ChangeEvent, - useCallback, - useContext, - useEffect, - useState -} from "react"; +import { ChangeEvent, useCallback, useEffect, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { checkIsUrlTooLong } from "routes/utils/checkIsUrlTooLong"; import styled from "styled-components"; @@ -65,7 +59,7 @@ export const ColumnOptionsMenu = (props: { } = props; const { operationState: { colors, theme } - } = useContext(OperationContext); + } = useOperationState(); const [draggedId, setDraggedId] = useState(); const [draggedOverId, setDraggedOverId] = useState(); const [isMenuOpen, setIsMenuOpen] = useState(false); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ContentTable.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ContentTable.tsx index fa8f498cc..859dd2589 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ContentTable.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ContentTable.tsx @@ -45,11 +45,11 @@ import { ContentTableColumn, ContentTableProps } from "components/ContentViews/table/tableParts"; -import OperationContext from "contexts/operationContext"; import { UserTheme } from "contexts/operationStateReducer"; +import { useOperationState } from "hooks/useOperationState"; import { indexToNumber } from "models/logObject"; import * as React from "react"; -import { Fragment, useContext, useEffect, useMemo, useState } from "react"; +import { Fragment, useEffect, useMemo, useState } from "react"; import { Colors } from "styles/Colors"; import Icon from "styles/Icons"; @@ -83,7 +83,7 @@ export const ContentTable = React.memo( } = contentTableProps; const { operationState: { colors, theme } - } = useContext(OperationContext); + } = useOperationState(); const [previousIndex, setPreviousIndex] = useState(null); const [rowSelection, setRowSelection] = useState( Object.assign( diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/Panel.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/Panel.tsx index e09d98ef0..6ef2312bd 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/Panel.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/Panel.tsx @@ -3,7 +3,6 @@ import { useQueryClient } from "@tanstack/react-query"; import { Table } from "@tanstack/react-table"; import { ColumnOptionsMenu } from "components/ContentViews/table/ColumnOptionsMenu"; import { Button } from "components/StyledComponents/Button"; -import OperationContext from "contexts/operationContext"; import { refreshObjectQuery, refreshObjectsQuery, @@ -11,8 +10,9 @@ import { refreshWellsQuery } from "hooks/query/queryRefreshHelpers"; import useExport, { encloseCell } from "hooks/useExport"; +import { useOperationState } from "hooks/useOperationState"; import { ObjectType } from "models/objectType"; -import React, { useCallback, useContext, useEffect } from "react"; +import React, { useCallback, useEffect } from "react"; import { useParams } from "react-router-dom"; import styled from "styled-components"; import Icon from "styles/Icons"; @@ -50,7 +50,7 @@ const Panel = (props: PanelProps) => { } = props; const { operationState: { theme } - } = useContext(OperationContext); + } = useOperationState(); const { exportData, exportOptions } = useExport(); const abortRefreshControllerRef = React.useRef(); const queryClient = useQueryClient(); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/SortableEdsTable.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/SortableEdsTable.tsx index 135daf7c3..91ac4fd31 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/SortableEdsTable.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/SortableEdsTable.tsx @@ -1,6 +1,6 @@ import { CellProps, Table } from "@equinor/eds-core-react"; -import OperationContext from "contexts/operationContext"; -import { useCallback, useContext, useEffect, useState } from "react"; +import { useOperationState } from "hooks/useOperationState"; +import { useCallback, useEffect, useState } from "react"; import styled from "styled-components"; import { Colors } from "styles/Colors"; import Icon from "styles/Icons"; @@ -37,7 +37,7 @@ const SortableEdsTable = (props: SortableEdsTableProps): React.ReactElement => { const { columns, data, caption } = props; const { operationState: { colors } - } = useContext(OperationContext); + } = useOperationState(); const initColumns = (): Column[] => columns.map((col) => { diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/BatchModifyMenuItem.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/BatchModifyMenuItem.tsx index 657b1c998..961c38129 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/BatchModifyMenuItem.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/BatchModifyMenuItem.tsx @@ -1,7 +1,7 @@ import { Typography } from "@equinor/eds-core-react"; import { MenuItem } from "@mui/material"; -import OperationContext from "contexts/operationContext"; -import { ReactElement, forwardRef, useContext } from "react"; +import { useOperationState } from "hooks/useOperationState"; +import { ReactElement, forwardRef } from "react"; import OperationType from "../../contexts/operationType"; import ObjectOnWellbore from "../../models/objectOnWellbore"; import { ObjectType } from "../../models/objectType"; @@ -24,7 +24,7 @@ export interface BatchModifyMenuItemProps { export const BatchModifyMenuItem = forwardRef( (props: BatchModifyMenuItemProps, ref: React.Ref): ReactElement => { const { checkedObjects, objectType } = props; - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const batchModifyProperties = objectBatchModifyProperties[objectType]; const onSubmitBatchModify = async (batchUpdates: { diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/BhaRunContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/BhaRunContextMenu.tsx index 6099d5bc1..c77c4d863 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/BhaRunContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/BhaRunContextMenu.tsx @@ -12,20 +12,20 @@ import BhaRunPropertiesModal, { } from "components/Modals/BhaRunPropertiesModal"; import { PropertiesModalMode } from "components/Modals/ModalParts"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; +import { useOperationState } from "hooks/useOperationState"; import BhaRun from "models/bhaRun"; import { ObjectType } from "models/objectType"; -import React, { useContext } from "react"; +import React from "react"; import { colors } from "styles/Colors"; const BhaRunContextMenu = ( props: ObjectContextMenuProps ): React.ReactElement => { const { checkedObjects } = props; - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const openInQueryView = useOpenInQueryView(); const { servers } = useGetServers(); const { connectedServer } = useConnectedServer(); diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/ContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/ContextMenu.tsx index 7124fb666..8e8195335 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/ContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/ContextMenu.tsx @@ -1,8 +1,8 @@ import { Menu } from "@mui/material"; -import OperationContext from "contexts/operationContext"; import { MousePosition } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; -import React, { ReactElement, useContext } from "react"; +import { useOperationState } from "hooks/useOperationState"; +import React, { ReactElement } from "react"; import styled from "styled-components"; import { Colors } from "styles/Colors"; @@ -25,7 +25,7 @@ export const getContextMenuPosition = ( }; const ContextMenu = (props: ContextMenuProps): React.ReactElement => { - const { operationState, dispatchOperation } = useContext(OperationContext); + const { operationState, dispatchOperation } = useOperationState(); const { contextMenu, colors } = operationState; const handleClose = () => { diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/ContextMenuPresenter.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/ContextMenuPresenter.tsx index 85c92bd90..532f0cab6 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/ContextMenuPresenter.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/ContextMenuPresenter.tsx @@ -1,8 +1,8 @@ -import React, { useContext } from "react"; -import OperationContext from "contexts/operationContext"; +import { useOperationState } from "hooks/useOperationState"; +import React from "react"; const ContextMenuPresenter = (): React.ReactElement => { - const { operationState } = useContext(OperationContext); + const { operationState } = useOperationState(); const { contextMenu } = operationState; return <>{contextMenu && contextMenu.component}; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/CopyComponentsToServer.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/CopyComponentsToServer.tsx index bf097c6b8..f6f584beb 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/CopyComponentsToServer.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/CopyComponentsToServer.tsx @@ -6,14 +6,13 @@ import CopyRangeModal, { CopyRangeModalProps } from "components/Modals/CopyRangeModal"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import LogCurveInfo from "models/logCurveInfo"; import ObjectOnWellbore from "models/objectOnWellbore"; import { Server } from "models/server"; -import { useContext } from "react"; import { copyComponentsToServer } from "./CopyComponentsToServerUtils"; export interface CopyComponentsToServerMenuItemProps { @@ -38,7 +37,7 @@ export const CopyComponentsToServerMenuItem = ( } = props; const { connectedServer } = useConnectedServer(); const { servers } = useGetServers(); - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const menuComponents = menuItemText("copy", componentType, componentsToCopy); const menuText = withRange === true diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/CopyComponentsToServerUtils.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/CopyComponentsToServerUtils.tsx index 12bf83d06..dd2ca69e2 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/CopyComponentsToServerUtils.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/CopyComponentsToServerUtils.tsx @@ -21,12 +21,12 @@ import AuthorizationService from "../../services/authorizationService"; import ComponentService from "../../services/componentService"; import JobService, { JobType } from "../../services/jobService"; import ObjectService from "../../services/objectService"; -import { displayMissingObjectModal } from "../Modals/MissingObjectModals"; -import { displayReplaceModal } from "../Modals/ReplaceModal"; -import { pluralize } from "./ContextMenuUtils"; import CopyMnemonicsModal, { CopyMnemonicsModalProps } from "../Modals/CopyMnemonicsModal"; +import { displayMissingObjectModal } from "../Modals/MissingObjectModals"; +import { displayReplaceModal } from "../Modals/ReplaceModal"; +import { pluralize } from "./ContextMenuUtils"; export interface OnClickCopyComponentToServerProps { targetServer: Server; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/FluidContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/FluidContextMenu.tsx index 182f22cb9..739e86bc2 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/FluidContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/FluidContextMenu.tsx @@ -15,15 +15,15 @@ import { import NestedMenuItem from "components/ContextMenus/NestedMenuItem"; import { useClipboardComponentReferencesOfType } from "components/ContextMenus/UseClipboardComponentReferences"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import { useGetServers } from "hooks/query/useGetServers"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import Fluid from "models/fluid"; import FluidsReport from "models/fluidsReport"; import { createComponentReferences } from "models/jobs/componentReferences"; import { ObjectType } from "models/objectType"; import { Server } from "models/server"; -import React, { useContext } from "react"; +import React from "react"; import { JobType } from "services/jobService"; import { colors } from "styles/Colors"; @@ -34,7 +34,7 @@ export interface FluidContextMenuProps { const FluidContextMenu = (props: FluidContextMenuProps): React.ReactElement => { const { checkedFluids, fluidsReport } = props; - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const fluidReferences = useClipboardComponentReferencesOfType( ComponentType.Fluid ); diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/FluidsReportContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/FluidsReportContextMenu.tsx index ebd702edf..aa1106357 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/FluidsReportContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/FluidsReportContextMenu.tsx @@ -13,12 +13,12 @@ import { } from "components/ContextMenus/ObjectMenuItems"; import { useClipboardComponentReferencesOfType } from "components/ContextMenus/UseClipboardComponentReferences"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import { ObjectType } from "models/objectType"; -import React, { useContext } from "react"; +import React from "react"; import { colors } from "styles/Colors"; const FluidsReportContextMenu = ( @@ -26,7 +26,7 @@ const FluidsReportContextMenu = ( ): React.ReactElement => { const { checkedObjects } = props; const { servers } = useGetServers(); - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const openInQueryView = useOpenInQueryView(); const fluidReferences = useClipboardComponentReferencesOfType( ComponentType.Fluid diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/FormationMarkerContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/FormationMarkerContextMenu.tsx index 19bfe5e76..2a8c6eb74 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/FormationMarkerContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/FormationMarkerContextMenu.tsx @@ -11,20 +11,20 @@ import FormationMarkerPropertiesModal, { FormationMarkerPropertiesModalProps } from "components/Modals/FormationMarkerPropertiesModal"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; +import { useOperationState } from "hooks/useOperationState"; import FormationMarker from "models/formationMarker"; import { ObjectType } from "models/objectType"; -import React, { useContext } from "react"; +import React from "react"; import { colors } from "styles/Colors"; const FormationMarkerContextMenu = ( props: ObjectContextMenuProps ): React.ReactElement => { const { checkedObjects } = props; - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const openInQueryView = useOpenInQueryView(); const { connectedServer } = useConnectedServer(); const queryClient = useQueryClient(); diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/GeologyIntervalContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/GeologyIntervalContextMenu.tsx index 94e34034d..ee8c768b2 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/GeologyIntervalContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/GeologyIntervalContextMenu.tsx @@ -14,14 +14,14 @@ import { import { useClipboardComponentReferencesOfType } from "components/ContextMenus/UseClipboardComponentReferences"; import GeologyIntervalPropertiesModal from "components/Modals/GeologyIntervalPropertiesModal"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import GeologyInterval from "models/geologyInterval"; import { createComponentReferences } from "models/jobs/componentReferences"; import MudLog from "models/mudLog"; -import React, { useContext } from "react"; +import React from "react"; import { JobType } from "services/jobService"; import { colors } from "styles/Colors"; @@ -34,7 +34,7 @@ const GeologyIntervalContextMenu = ( props: GeologyIntervalContextMenuProps ): React.ReactElement => { const { checkedGeologyIntervals, mudLog } = props; - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const { servers } = useGetServers(); const { connectedServer } = useConnectedServer(); const geologyIntervalReferences = useClipboardComponentReferencesOfType( diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurvePriorityContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurvePriorityContextMenu.tsx index 1973e3d71..44f55e6ba 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurvePriorityContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurvePriorityContextMenu.tsx @@ -1,7 +1,7 @@ import { Typography } from "@equinor/eds-core-react"; import { MenuItem } from "@mui/material"; -import React, { useContext } from "react"; -import OperationContext from "../../contexts/operationContext"; +import { useOperationState } from "hooks/useOperationState"; +import React from "react"; import { MousePosition } from "../../contexts/operationStateReducer"; import { StyledMenu, preventContextMenuPropagation } from "./ContextMenu"; import { StyledIcon } from "./ContextMenuUtils"; @@ -17,7 +17,7 @@ export const LogCurvePriorityContextMenu = ( props: LogCurvePriorityContextMenuProps ): React.ReactElement => { const { onDelete, onClose, position, open } = props; - const { operationState } = useContext(OperationContext); + const { operationState } = useOperationState(); const { colors } = operationState; const onClickDelete = async () => { diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogObjectContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogObjectContextMenu.tsx index 068caa3ae..1fa929a2f 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogObjectContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogObjectContextMenu.tsx @@ -37,11 +37,11 @@ import { ReportModal } from "components/Modals/ReportModal"; import SpliceLogsModal from "components/Modals/SpliceLogsModal"; import TrimLogObjectModal from "components/Modals/TrimLogObject/TrimLogObjectModal"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import { DisplayModalAction } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import CheckLogHeaderJob from "models/jobs/checkLogHeaderJob"; import CompareLogDataJob from "models/jobs/compareLogData"; @@ -50,7 +50,7 @@ import LogObject from "models/logObject"; import ObjectOnWellbore, { toObjectReference } from "models/objectOnWellbore"; import { ObjectType } from "models/objectType"; import { Server } from "models/server"; -import React, { useContext } from "react"; +import React from "react"; import { createSearchParams, useNavigate } from "react-router-dom"; import { RouterLogType } from "routes/routerConstants"; import { @@ -69,7 +69,7 @@ const LogObjectContextMenu = ( props: ObjectContextMenuProps ): React.ReactElement => { const { checkedObjects } = props; - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const openInQueryView = useOpenInQueryView(); const logCurvesReference: CopyRangeClipboard = useClipboardComponentReferencesOfType(ComponentType.Mnemonic); diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/MessageObjectContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/MessageObjectContextMenu.tsx index ca7e625ad..928e1ce53 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/MessageObjectContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/MessageObjectContextMenu.tsx @@ -21,22 +21,22 @@ import ObjectPickerModal, { ObjectPickerProps } from "components/Modals/ObjectPickerModal"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; +import { useOperationState } from "hooks/useOperationState"; import MessageObject from "models/messageObject"; import ObjectOnWellbore from "models/objectOnWellbore"; import { ObjectType } from "models/objectType"; import { Server } from "models/server"; -import React, { useContext } from "react"; +import React from "react"; import { colors } from "styles/Colors"; const MessageObjectContextMenu = ( props: ObjectContextMenuProps ): React.ReactElement => { const { checkedObjects } = props; - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const openInQueryView = useOpenInQueryView(); const { connectedServer } = useConnectedServer(); const queryClient = useQueryClient(); diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/MnemonicsContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/MnemonicsContextMenu.tsx index b048ad90b..9d94f84e3 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/MnemonicsContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/MnemonicsContextMenu.tsx @@ -10,8 +10,8 @@ import { OffsetLogCurveModal, OffsetLogCurveModalProps } from "components/Modals/OffsetLogCurveModal"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import { DeleteLogCurveValuesJob, @@ -19,7 +19,7 @@ import { } from "models/jobs/deleteLogCurveValuesJob"; import LogObject from "models/logObject"; import { toObjectReference } from "models/objectOnWellbore"; -import React, { useContext } from "react"; +import React from "react"; import JobService, { JobType } from "services/jobService"; import { colors } from "styles/Colors"; @@ -32,7 +32,7 @@ export interface MnemonicsContextMenuProps { const MnemonicsContextMenu = ( props: MnemonicsContextMenuProps ): React.ReactElement => { - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const { log, mnemonics, indexRanges } = props; const deleteLogCurveValues = async () => { diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/MudLogContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/MudLogContextMenu.tsx index 4ec80376b..3601d829a 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/MudLogContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/MudLogContextMenu.tsx @@ -16,14 +16,14 @@ import MudLogPropertiesModal, { MudLogPropertiesModalProps } from "components/Modals/MudLogPropertiesModal"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import MudLog from "models/mudLog"; import { ObjectType } from "models/objectType"; -import React, { useContext } from "react"; +import React from "react"; import { colors } from "styles/Colors"; const MudLogContextMenu = ( @@ -34,7 +34,7 @@ const MudLogContextMenu = ( const geologyIntervalReferences = useClipboardComponentReferencesOfType( ComponentType.GeologyInterval ); - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const openInQueryView = useOpenInQueryView(); const { connectedServer } = useConnectedServer(); const queryClient = useQueryClient(); diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/NestedMenuItem.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/NestedMenuItem.tsx index f4314f24e..a5bcde7a5 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/NestedMenuItem.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/NestedMenuItem.tsx @@ -2,13 +2,8 @@ import { Icon, Typography } from "@equinor/eds-core-react"; import MenuItem, { MenuItemProps } from "@mui/material/MenuItem"; import { StyledMenu } from "components/ContextMenus/ContextMenu"; import { StyledIcon } from "components/ContextMenus/ContextMenuUtils"; -import OperationContext from "contexts/operationContext"; -import React, { - useContext, - useImperativeHandle, - useRef, - useState -} from "react"; +import { useOperationState } from "hooks/useOperationState"; +import React, { useImperativeHandle, useRef, useState } from "react"; export interface NestedMenuItemProps extends Omit { label: string; @@ -31,7 +26,7 @@ const NestedMenuItem = React.forwardRef< } = props; const { operationState: { colors } - } = useContext(OperationContext); + } = useOperationState(); const { ref: containerRefProp, ...ContainerProps } = ContainerPropsProp; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectsSidebarContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectsSidebarContextMenu.tsx index 4b4bf65e9..431f2a14f 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectsSidebarContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectsSidebarContextMenu.tsx @@ -17,14 +17,14 @@ import { pasteObjectOnWellbore } from "components/ContextMenus/CopyUtils"; import NestedMenuItem from "components/ContextMenus/NestedMenuItem"; import { useClipboardReferencesOfType } from "components/ContextMenus/UseClipboardReferences"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; +import { useOperationState } from "hooks/useOperationState"; import { toWellboreReference } from "models/jobs/wellboreReference"; import { ObjectType } from "models/objectType"; import { Server } from "models/server"; import Wellbore from "models/wellbore"; -import React, { useContext } from "react"; +import React from "react"; import { colors } from "styles/Colors"; import { v4 as uuid } from "uuid"; @@ -37,7 +37,7 @@ const ObjectsSidebarContextMenu = ( props: ObjectsSidebarContextMenuProps ): React.ReactElement => { const { wellbore, objectType } = props; - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const { servers } = useGetServers(); const objectReferences = useClipboardReferencesOfType(objectType); const openInQueryView = useOpenInQueryView(); diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigContextMenu.tsx index c39f0f433..940d073a0 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigContextMenu.tsx @@ -13,18 +13,18 @@ import RigPropertiesModal, { RigPropertiesModalProps } from "components/Modals/RigPropertiesModal"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; +import { useOperationState } from "hooks/useOperationState"; import { ObjectType } from "models/objectType"; import Rig from "models/rig"; -import React, { useContext } from "react"; +import React from "react"; import { colors } from "styles/Colors"; const RigContextMenu = (props: ObjectContextMenuProps): React.ReactElement => { const { checkedObjects } = props; - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const openInQueryView = useOpenInQueryView(); const { connectedServer } = useConnectedServer(); const queryClient = useQueryClient(); diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigsContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigsContextMenu.tsx index 23d78a1ae..bc51ad669 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigsContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigsContextMenu.tsx @@ -19,16 +19,16 @@ import RigPropertiesModal, { RigPropertiesModalProps } from "components/Modals/RigPropertiesModal"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import { DisplayModalAction } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; +import { useOperationState } from "hooks/useOperationState"; import { toWellboreReference } from "models/jobs/wellboreReference"; import { ObjectType } from "models/objectType"; import Rig from "models/rig"; import { Server } from "models/server"; import Wellbore from "models/wellbore"; -import React, { useContext } from "react"; +import React from "react"; import { colors } from "styles/Colors"; import { v4 as uuid } from "uuid"; @@ -39,7 +39,7 @@ export interface RigsContextMenuProps { const RigsContextMenu = (props: RigsContextMenuProps): React.ReactElement => { const { wellbore, servers } = props; - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const rigReferences = useClipboardReferencesOfType(ObjectType.Rig); const openInQueryView = useOpenInQueryView(); const { connectedServer } = useConnectedServer(); diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/RiskContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/RiskContextMenu.tsx index 77abce94a..2aa72aaff 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/RiskContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/RiskContextMenu.tsx @@ -12,13 +12,13 @@ import RiskPropertiesModal, { RiskPropertiesModalProps } from "components/Modals/RiskPropertiesModal"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; +import { useOperationState } from "hooks/useOperationState"; import { ObjectType } from "models/objectType"; import RiskObject from "models/riskObject"; -import React, { useContext } from "react"; +import React from "react"; import { colors } from "styles/Colors"; const RiskObjectContextMenu = ( @@ -26,7 +26,7 @@ const RiskObjectContextMenu = ( ): React.ReactElement => { const { checkedObjects } = props; const { servers } = useGetServers(); - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const openInQueryView = useOpenInQueryView(); const { connectedServer } = useConnectedServer(); const queryClient = useQueryClient(); diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoriesContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoriesContextMenu.tsx index f1e961aec..62aeafe0b 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoriesContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoriesContextMenu.tsx @@ -19,16 +19,16 @@ import TrajectoryPropertiesModal, { TrajectoryPropertiesModalProps } from "components/Modals/TrajectoryPropertiesModal"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import { DisplayModalAction } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; +import { useOperationState } from "hooks/useOperationState"; import { toWellboreReference } from "models/jobs/wellboreReference"; import { ObjectType } from "models/objectType"; import { Server } from "models/server"; import Trajectory from "models/trajectory"; import Wellbore from "models/wellbore"; -import React, { useContext } from "react"; +import React from "react"; import { colors } from "styles/Colors"; import { v4 as uuid } from "uuid"; @@ -41,7 +41,7 @@ const TrajectoriesContextMenu = ( props: TrajectoriesContextMenuProps ): React.ReactElement => { const { wellbore, servers } = props; - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const trajectoryReferences = useClipboardReferencesOfType( ObjectType.Trajectory ); diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryContextMenu.tsx index 733840fee..e0e38edd0 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryContextMenu.tsx @@ -17,14 +17,14 @@ import TrajectoryPropertiesModal, { TrajectoryPropertiesModalProps } from "components/Modals/TrajectoryPropertiesModal"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import { ObjectType } from "models/objectType"; import Trajectory from "models/trajectory"; -import React, { useContext } from "react"; +import React from "react"; import { colors } from "styles/Colors"; const TrajectoryContextMenu = ( @@ -32,7 +32,7 @@ const TrajectoryContextMenu = ( ): React.ReactElement => { const { checkedObjects } = props; const { servers } = useGetServers(); - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const trajectoryStationReferences = useClipboardComponentReferencesOfType( ComponentType.TrajectoryStation ); diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryStationContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryStationContextMenu.tsx index c083da3b2..b2727e980 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryStationContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryStationContextMenu.tsx @@ -17,15 +17,15 @@ import NestedMenuItem from "components/ContextMenus/NestedMenuItem"; import { useClipboardComponentReferencesOfType } from "components/ContextMenus/UseClipboardComponentReferences"; import TrajectoryStationPropertiesModal from "components/Modals/TrajectoryStationPropertiesModal"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import { createComponentReferences } from "models/jobs/componentReferences"; import { ObjectType } from "models/objectType"; import { Server } from "models/server"; import Trajectory from "models/trajectory"; -import React, { useContext } from "react"; +import React from "react"; import { JobType } from "services/jobService"; import { colors } from "styles/Colors"; @@ -38,7 +38,7 @@ const TrajectoryStationContextMenu = ( props: TrajectoryStationContextMenuProps ): React.ReactElement => { const { checkedTrajectoryStations, trajectory } = props; - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const { servers } = useGetServers(); const { connectedServer } = useConnectedServer(); const trajectoryStationReferences = useClipboardComponentReferencesOfType( diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularComponentContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularComponentContextMenu.tsx index ce59d48d1..0855bf7ca 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularComponentContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularComponentContextMenu.tsx @@ -19,15 +19,15 @@ import NestedMenuItem from "components/ContextMenus/NestedMenuItem"; import { useClipboardComponentReferencesOfType } from "components/ContextMenus/UseClipboardComponentReferences"; import TubularComponentPropertiesModal from "components/Modals/TubularComponentPropertiesModal"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import { createComponentReferences } from "models/jobs/componentReferences"; import { ObjectType } from "models/objectType"; import { Server } from "models/server"; import Tubular from "models/tubular"; -import React, { useContext } from "react"; +import React from "react"; import { JobType } from "services/jobService"; import { colors } from "styles/Colors"; @@ -41,7 +41,7 @@ const TubularComponentContextMenu = ( ): React.ReactElement => { const { checkedTubularComponents, tubular } = props; const { servers } = useGetServers(); - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const tubularComponentReferences = useClipboardComponentReferencesOfType( ComponentType.TubularComponent ); diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularContextMenu.tsx index 6a7846a70..17f5c1b00 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularContextMenu.tsx @@ -15,14 +15,14 @@ import { useClipboardComponentReferencesOfType } from "components/ContextMenus/U import { PropertiesModalMode } from "components/Modals/ModalParts"; import TubularPropertiesModal from "components/Modals/TubularPropertiesModal"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import { ObjectType } from "models/objectType"; import Tubular from "models/tubular"; -import React, { useContext } from "react"; +import React from "react"; import { colors } from "styles/Colors"; const TubularContextMenu = ( @@ -30,7 +30,7 @@ const TubularContextMenu = ( ): React.ReactElement => { const { checkedObjects } = props; const { servers } = useGetServers(); - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const tubularComponentReferences = useClipboardComponentReferencesOfType( ComponentType.TubularComponent ); diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularsContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularsContextMenu.tsx index f0d9b291c..2792adcca 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularsContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularsContextMenu.tsx @@ -15,13 +15,13 @@ import { pasteObjectOnWellbore } from "components/ContextMenus/CopyUtils"; import NestedMenuItem from "components/ContextMenus/NestedMenuItem"; import { useClipboardReferencesOfType } from "components/ContextMenus/UseClipboardReferences"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; +import { useOperationState } from "hooks/useOperationState"; import { toWellboreReference } from "models/jobs/wellboreReference"; import { ObjectType } from "models/objectType"; import { Server } from "models/server"; import Wellbore from "models/wellbore"; -import React, { useContext } from "react"; +import React from "react"; import { colors } from "styles/Colors"; import { v4 as uuid } from "uuid"; @@ -34,7 +34,7 @@ const TubularsContextMenu = ( props: TubularsContextMenuProps ): React.ReactElement => { const { wellbore, servers } = props; - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const tubularReferences = useClipboardReferencesOfType(ObjectType.Tubular); const openInQueryView = useOpenInQueryView(); const { connectedServer } = useConnectedServer(); diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometryContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometryContextMenu.tsx index e394dbfd2..ddc1fbdf4 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometryContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometryContextMenu.tsx @@ -17,14 +17,14 @@ import WbGeometryPropertiesModal, { WbGeometryPropertiesModalProps } from "components/Modals/WbGeometryPropertiesModal"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import { ObjectType } from "models/objectType"; import WbGeometryObject from "models/wbGeometry"; -import React, { useContext } from "react"; +import React from "react"; import { colors } from "styles/Colors"; const WbGeometryObjectContextMenu = ( @@ -32,7 +32,7 @@ const WbGeometryObjectContextMenu = ( ): React.ReactElement => { const { checkedObjects } = props; const { servers } = useGetServers(); - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const wbGeometrySectionReferences = useClipboardComponentReferencesOfType( ComponentType.WbGeometrySection ); diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometrySectionContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometrySectionContextMenu.tsx index 0a57ca179..e73caf1c1 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometrySectionContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometrySectionContextMenu.tsx @@ -16,16 +16,16 @@ import NestedMenuItem from "components/ContextMenus/NestedMenuItem"; import { useClipboardComponentReferencesOfType } from "components/ContextMenus/UseClipboardComponentReferences"; import WbGeometrySectionPropertiesModal from "components/Modals/WbGeometrySectionPropertiesModal"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import { createComponentReferences } from "models/jobs/componentReferences"; import { ObjectType } from "models/objectType"; import { Server } from "models/server"; import WbGeometry from "models/wbGeometry"; import WbGeometrySection from "models/wbGeometrySection"; -import React, { useContext } from "react"; +import React from "react"; import { JobType } from "services/jobService"; import { colors } from "styles/Colors"; @@ -41,7 +41,7 @@ const WbGeometrySectionContextMenu = ( const wbGeometrySectionReferences = useClipboardComponentReferencesOfType( ComponentType.WbGeometrySection ); - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const { servers } = useGetServers(); const { connectedServer } = useConnectedServer(); diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellboreContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellboreContextMenu.tsx index 5f34d0e4a..db9b15e68 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellboreContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellboreContextMenu.tsx @@ -31,19 +31,19 @@ import WellborePropertiesModal, { WellborePropertiesModalProps } from "components/Modals/WellborePropertiesModal"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import { DisplayModalAction } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; import { refreshWellboreQuery } from "hooks/query/queryRefreshHelpers"; import { useGetCapObjects } from "hooks/query/useGetCapObjects"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; +import { useOperationState } from "hooks/useOperationState"; import { DeleteWellboreJob } from "models/jobs/deleteJobs"; import { toWellboreReference } from "models/jobs/wellboreReference"; import LogObject from "models/logObject"; import { ObjectType } from "models/objectType"; import { Server } from "models/server"; import Wellbore from "models/wellbore"; -import React, { useContext } from "react"; +import React from "react"; import { getObjectGroupsViewPath } from "routes/utils/pathBuilder"; import JobService, { JobType } from "services/jobService"; import { colors } from "styles/Colors"; @@ -60,7 +60,7 @@ const WellboreContextMenu = ( props: WellboreContextMenuProps ): React.ReactElement => { const { wellbore, checkedWellboreRows, servers } = props; - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const openInQueryView = useOpenInQueryView(); const objectReferences = useClipboardReferences(); const { connectedServer } = useConnectedServer(); diff --git a/Src/WitsmlExplorer.Frontend/components/GlobalStyles.tsx b/Src/WitsmlExplorer.Frontend/components/GlobalStyles.tsx index fa2cb4447..3e2976e6a 100644 --- a/Src/WitsmlExplorer.Frontend/components/GlobalStyles.tsx +++ b/Src/WitsmlExplorer.Frontend/components/GlobalStyles.tsx @@ -1,6 +1,16 @@ +import { useOperationState } from "hooks/useOperationState"; +import { ReactElement } from "react"; import { createGlobalStyle } from "styled-components"; import { Colors } from "styles/Colors"; +export const GlobalStylesWrapper = (): ReactElement => { + const { + operationState: { colors } + } = useOperationState(); + + return ; +}; + const GlobalStyles = createGlobalStyle<{ colors: Colors }>` *, *:before, diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/AnalyzeGapModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/AnalyzeGapModal.tsx index 2293086c6..d8c76e27c 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/AnalyzeGapModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/AnalyzeGapModal.tsx @@ -8,10 +8,10 @@ import ModalDialog from "components/Modals/ModalDialog"; import { ReportModal } from "components/Modals/ReportModal"; import AdjustDateTimeModal from "components/Modals/TrimLogObject/AdjustDateTimeModal"; import AdjustNumberRangeModal from "components/Modals/TrimLogObject/AdjustNumberRangeModal"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import LogObject from "models/logObject"; -import React, { useContext, useState } from "react"; +import React, { useState } from "react"; import JobService, { JobType } from "services/jobService"; import { formatIndexValue, indexToNumber } from "tools/IndexHelpers"; @@ -22,7 +22,7 @@ export interface AnalyzeGapModalProps { const AnalyzeGapModal = (props: AnalyzeGapModalProps): React.ReactElement => { const { logObject, mnemonics } = props; - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const timePattern = /^([01]?[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/; const initialTime = "00:00:00"; const [log] = useState(logObject); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/BatchModifyPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/BatchModifyPropertiesModal.tsx index b93d69c9d..b0d812823 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/BatchModifyPropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/BatchModifyPropertiesModal.tsx @@ -1,7 +1,7 @@ import { Autocomplete, TextField } from "@equinor/eds-core-react"; -import { ChangeEvent, ReactElement, useContext, useState } from "react"; +import { useOperationState } from "hooks/useOperationState"; +import { ChangeEvent, ReactElement, useState } from "react"; import styled from "styled-components"; -import OperationContext from "../../contexts/operationContext"; import OperationType from "../../contexts/operationType"; import ModalDialog from "./ModalDialog"; @@ -22,7 +22,7 @@ export const BatchModifyPropertiesModal = ( props: BatchModifyModalProps ): ReactElement => { const { title, properties, onSubmit } = props; - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const [batchUpdates, setBatchUpdates] = useState<{ [key: string]: string }>( properties.reduce((acc, prop) => ({ ...acc, [prop.property]: "" }), {}) ); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/BhaRunPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/BhaRunPropertiesModal.tsx index 841f29998..809f4b1d7 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/BhaRunPropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/BhaRunPropertiesModal.tsx @@ -3,16 +3,16 @@ import formatDateString from "components/DateFormatter"; import { DateTimeField } from "components/Modals/DateTimeField"; import ModalDialog from "components/Modals/ModalDialog"; import { PropertiesModalMode, validText } from "components/Modals/ModalParts"; -import OperationContext from "contexts/operationContext"; import { DateTimeFormat, HideModalAction } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import BhaRun from "models/bhaRun"; import { itemStateTypes } from "models/itemStateTypes"; import { ObjectType } from "models/objectType"; -import React, { ChangeEvent, useContext, useEffect, useState } from "react"; +import React, { ChangeEvent, useEffect, useState } from "react"; import JobService, { JobType } from "services/jobService"; const typesOfBhaStatus = ["final", "progress", "plan", "unknown"]; @@ -29,7 +29,7 @@ const BhaRunPropertiesModal = ( const { mode, bhaRun, dispatchOperation } = props; const { operationState: { timeZone } - } = useContext(OperationContext); + } = useOperationState(); const [editableBhaRun, setEditableBhaRun] = useState(null); const [dTimStartValid, setDTimStartValid] = useState(true); const [dTimStopValid, setDTimStopValid] = useState(true); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/CompareLogDataModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/CompareLogDataModal.tsx index 0004b0359..f48e44251 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/CompareLogDataModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/CompareLogDataModal.tsx @@ -3,11 +3,11 @@ import ModalDialog, { ModalWidth } from "components/Modals/ModalDialog"; import { Checkbox } from "components/StyledComponents/Checkbox"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import ObjectOnWellbore from "models/objectOnWellbore"; import { Server } from "models/server"; -import { ChangeEvent, useContext, useState } from "react"; +import { ChangeEvent, useState } from "react"; import styled from "styled-components"; import { Colors } from "styles/Colors"; @@ -30,7 +30,7 @@ export default function CompareLogDataModal({ const { operationState: { colors }, dispatchOperation - } = useContext(OperationContext); + } = useOperationState(); const [checkedCompareAllLogIndexes, setCheckedCompareAllLogIndexes] = useState(false); const onSubmit = async () => { diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/CopyMnemonicsModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/CopyMnemonicsModal.tsx index 7792a1695..6a35fdc0c 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/CopyMnemonicsModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/CopyMnemonicsModal.tsx @@ -1,23 +1,23 @@ import { Radio, Typography } from "@equinor/eds-core-react"; import ModalDialog from "components/Modals/ModalDialog"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; -import { useContext, useState } from "react"; +import { useOperationState } from "hooks/useOperationState"; +import { useState } from "react"; import styled from "styled-components"; -import ObjectReference from "../../models/jobs/objectReference"; +import { ComponentType, getParentType } from "../../models/componentType"; import ComponentReferences, { createComponentReferences } from "../../models/jobs/componentReferences"; import { CopyComponentsJob } from "../../models/jobs/copyJobs"; -import JobService, { JobType } from "../../services/jobService"; import { DeleteComponentsJob } from "../../models/jobs/deleteJobs"; +import ObjectReference from "../../models/jobs/objectReference"; import { ReplaceComponentsJob } from "../../models/jobs/replaceComponentsJob"; -import { ComponentType, getParentType } from "../../models/componentType"; -import ComponentService from "../../services/componentService"; +import LogObject from "../../models/logObject"; import { Server } from "../../models/server"; -import ObjectService from "../../services/objectService"; import AuthorizationService from "../../services/authorizationService"; -import LogObject from "../../models/logObject"; +import ComponentService from "../../services/componentService"; +import JobService, { JobType } from "../../services/jobService"; +import ObjectService from "../../services/objectService"; enum CopyMnemonicsType { DeleteInsert = "deleteInsert", @@ -45,7 +45,7 @@ const CopyMnemonicsModal = ( endIndex } = props; - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const [selectedCopyMnemonicsType, setCopyMnemonicsType] = useState( CopyMnemonicsType.Paste diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/CopyRangeModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/CopyRangeModal.tsx index c31586f68..5df4156be 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/CopyRangeModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/CopyRangeModal.tsx @@ -7,15 +7,15 @@ import ModalDialog from "components/Modals/ModalDialog"; import AdjustDateTimeModal from "components/Modals/TrimLogObject/AdjustDateTimeModal"; import AdjustNumberRangeModal from "components/Modals/TrimLogObject/AdjustNumberRangeModal"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import { CopyRangeClipboard, createComponentReferences } from "models/jobs/componentReferences"; import LogObject, { indexToNumber } from "models/logObject"; -import React, { useContext, useState } from "react"; +import React, { useState } from "react"; export interface CopyRangeModalProps { logObject: LogObject; @@ -26,7 +26,7 @@ export interface CopyRangeModalProps { const CopyRangeModal = (props: CopyRangeModalProps): React.ReactElement => { const { connectedServer } = useConnectedServer(); - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const [startIndex, setStartIndex] = useState(); const [endIndex, setEndIndex] = useState(); const [confirmDisabled, setConfirmDisabled] = useState(true); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/DeleteEmptyMnemonicsModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/DeleteEmptyMnemonicsModal.tsx index 39c7935ea..cbe96d0ab 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/DeleteEmptyMnemonicsModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/DeleteEmptyMnemonicsModal.tsx @@ -3,14 +3,14 @@ import { DateTimeField } from "components/Modals/DateTimeField"; import ModalDialog from "components/Modals/ModalDialog"; import { ReportModal } from "components/Modals/ReportModal"; import { Checkbox } from "components/StyledComponents/Checkbox"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import { DeleteEmptyMnemonicsJob } from "models/jobs/deleteEmptyMnemonicsJob"; import LogObject from "models/logObject"; import { toObjectReference } from "models/objectOnWellbore"; import Well from "models/well"; import Wellbore from "models/wellbore"; -import { ChangeEvent, useContext, useState } from "react"; +import { ChangeEvent, useState } from "react"; import JobService, { JobType } from "services/jobService"; import styled from "styled-components"; @@ -27,7 +27,7 @@ const DeleteEmptyMnemonicsModal = ( const { dispatchOperation, operationState: { timeZone, colors } - } = useContext(OperationContext); + } = useOperationState(); const [nullDepthValue, setNullDepthValue] = useState(-999.25); const [nullTimeValue, setNullTimeValue] = useState( "1900-01-01T00:00:00.000Z" diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/FormationMarkerPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/FormationMarkerPropertiesModal.tsx index 11d23b042..1fa9ba384 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/FormationMarkerPropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/FormationMarkerPropertiesModal.tsx @@ -5,8 +5,8 @@ import { invalidStringInput, undefinedOnUnchagedEmptyString } from "components/Modals/PropertiesModalUtils"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import FormationMarker from "models/formationMarker"; import { itemStateTypes } from "models/itemStateTypes"; import MaxLength from "models/maxLength"; @@ -14,13 +14,7 @@ import Measure from "models/measure"; import MeasureWithDatum from "models/measureWithDatum"; import { ObjectType } from "models/objectType"; import StratigraphicStruct from "models/stratigraphicStruct"; -import React, { - ChangeEvent, - Dispatch, - SetStateAction, - useContext, - useState -} from "react"; +import React, { ChangeEvent, Dispatch, SetStateAction, useState } from "react"; import JobService, { JobType } from "services/jobService"; import { Layout } from "../StyledComponents/Layout"; @@ -66,7 +60,7 @@ const FormationMarkerPropertiesModal = ( props: FormationMarkerPropertiesModalProps ): React.ReactElement => { const { formationMarker } = props; - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const [editable, setEditable] = useState({}); const [isLoading, setIsLoading] = useState(false); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/GeologyIntervalPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/GeologyIntervalPropertiesModal.tsx index 5d7027e2b..30390ebf9 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/GeologyIntervalPropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/GeologyIntervalPropertiesModal.tsx @@ -7,8 +7,8 @@ import { invalidStringInput, undefinedOnUnchagedEmptyString } from "components/Modals/PropertiesModalUtils"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import GeologyInterval from "models/geologyInterval"; import ObjectReference from "models/jobs/objectReference"; import { lithologySources } from "models/lithologySources"; @@ -22,7 +22,6 @@ import React, { ChangeEvent, Dispatch, SetStateAction, - useContext, useEffect, useState } from "react"; @@ -75,7 +74,7 @@ const GeologyIntervalPropertiesModal = ( props: GeologyIntervalPropertiesModalInterface ): React.ReactElement => { const { geologyInterval, mudLog: selectedMudLog } = props; - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const [editable, setEditable] = useState({}); const [editableLithologies, setEditableLithologies] = useState< Record diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/LogComparisonModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/LogComparisonModal.tsx index d4fa8e647..f51fa7d66 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/LogComparisonModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/LogComparisonModal.tsx @@ -19,16 +19,16 @@ import ModalDialog, { ModalWidth } from "components/Modals/ModalDialog"; import ProgressSpinner from "components/ProgressSpinner"; -import OperationContext from "contexts/operationContext"; import { DispatchOperation } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import LogCurveInfo from "models/logCurveInfo"; import LogObject from "models/logObject"; import ObjectOnWellbore from "models/objectOnWellbore"; import { ObjectType } from "models/objectType"; import { Server } from "models/server"; -import { useContext, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import ComponentService from "services/componentService"; import styled from "styled-components"; import { Colors } from "styles/Colors"; @@ -53,7 +53,7 @@ const LogComparisonModal = ( } = props; const { operationState: { timeZone, colors, dateTimeFormat } - } = useContext(OperationContext); + } = useOperationState(); const [sourceLogCurveInfo, setSourceLogCurveInfo] = useState(null); const [targetLogCurveInfo, setTargetLogCurveInfo] = diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/LogCurveInfoBatchUpdateModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/LogCurveInfoBatchUpdateModal.tsx index 620e71cfc..d65a3b2c7 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/LogCurveInfoBatchUpdateModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/LogCurveInfoBatchUpdateModal.tsx @@ -1,8 +1,8 @@ import { Autocomplete, TextField } from "@equinor/eds-core-react"; import { Grid } from "@mui/material"; -import React, { ChangeEvent, useContext, useState } from "react"; +import { useOperationState } from "hooks/useOperationState"; +import React, { ChangeEvent, useState } from "react"; import styled from "styled-components"; -import OperationContext from "../../contexts/operationContext"; import OperationType from "../../contexts/operationType"; import BatchModifyLogCurveInfoJob from "../../models/jobs/batchModifyLogCurveInfoJob"; import LogCurveInfo, { EmptyLogCurveInfo } from "../../models/logCurveInfo"; @@ -26,7 +26,7 @@ const LogCurveInfoBatchUpdateModal = ( props: LogCurveInfoBatchUpdateModalProps ): React.ReactElement => { const { logCurveInfoRows, selectedLog } = props; - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const [editableLogCurveInfo, setEditableLogCurveInfo] = useState(EmptyLogCurveInfo); const [isLoading, setIsLoading] = useState(false); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/LogCurvePriorityModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/LogCurvePriorityModal.tsx index dd2958693..12b5bfc56 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/LogCurvePriorityModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/LogCurvePriorityModal.tsx @@ -1,8 +1,8 @@ import { Icon, TextField } from "@equinor/eds-core-react"; import { Button } from "components/StyledComponents/Button"; -import React, { ChangeEvent, useContext, useState } from "react"; +import { useOperationState } from "hooks/useOperationState"; +import React, { ChangeEvent, useState } from "react"; import styled from "styled-components"; -import OperationContext from "../../contexts/operationContext"; import { MousePosition } from "../../contexts/operationStateReducer"; import OperationType from "../../contexts/operationType"; import LogCurvePriorityService from "../../services/logCurvePriorityService"; @@ -34,7 +34,7 @@ export const LogCurvePriorityModal = ( const [updatedPrioritizedCurves, setUpdatedPrioritizedCurves] = useState(prioritizedCurves); const [newCurve, setNewCurve] = useState(""); - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const [position, setPosition] = useState({ mouseX: null, mouseY: null diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/LogDataImportModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/LogDataImportModal.tsx index 152f51bc0..acc4ab5cc 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/LogDataImportModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/LogDataImportModal.tsx @@ -5,9 +5,9 @@ import { StyledAccordionHeader } from "components/Modals/LogComparisonModal"; import ModalDialog from "components/Modals/ModalDialog"; import WarningBar from "components/WarningBar"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetComponents } from "hooks/query/useGetComponents"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import { IndexRange } from "models/jobs/deleteLogCurveValuesJob"; import ImportLogDataJob from "models/jobs/importLogDataJob"; @@ -15,7 +15,7 @@ import ObjectReference from "models/jobs/objectReference"; import LogCurveInfo from "models/logCurveInfo"; import LogObject from "models/logObject"; import { toObjectReference } from "models/objectOnWellbore"; -import React, { useCallback, useContext, useState } from "react"; +import React, { useCallback, useState } from "react"; import JobService, { JobType } from "services/jobService"; import styled from "styled-components"; @@ -42,7 +42,7 @@ const LogDataImportModal = ( const { dispatchOperation, operationState: { colors } - } = useContext(OperationContext); + } = useOperationState(); const { components: logCurveInfoList, isFetching: isFetchingLogCurveInfo } = useGetComponents( connectedServer, diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/LogHeaderDateTimeField.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/LogHeaderDateTimeField.tsx index 9b629bb50..85dc34a84 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/LogHeaderDateTimeField.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/LogHeaderDateTimeField.tsx @@ -3,9 +3,9 @@ import formatDateString, { getOffset, getOffsetFromTimeZone } from "components/DateFormatter"; -import OperationContext from "contexts/operationContext"; import { DateTimeFormat, TimeZone } from "contexts/operationStateReducer"; -import { ChangeEvent, useContext, useEffect, useState } from "react"; +import { useOperationState } from "hooks/useOperationState"; +import { ChangeEvent, useEffect, useState } from "react"; import styled from "styled-components"; interface DateTimeFieldProps { @@ -33,7 +33,7 @@ export const LogHeaderDateTimeField = ( const { value, label, updateObject, minValue, maxValue } = props; const { operationState: { timeZone } - } = useContext(OperationContext); + } = useOperationState(); const offset = timeZone === TimeZone.Raw ? getOffset(value) diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/LogPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/LogPropertiesModal.tsx index 1cf572bbf..da04b1035 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/LogPropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/LogPropertiesModal.tsx @@ -3,12 +3,12 @@ import { WITSML_INDEX_TYPE_DATE_TIME } from "components/Constants"; import formatDateString from "components/DateFormatter"; import ModalDialog from "components/Modals/ModalDialog"; import { PropertiesModalMode, validText } from "components/Modals/ModalParts"; -import OperationContext from "contexts/operationContext"; import { HideModalAction } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import LogObject from "models/logObject"; import { ObjectType } from "models/objectType"; -import React, { ChangeEvent, useContext, useEffect, useState } from "react"; +import React, { ChangeEvent, useEffect, useState } from "react"; import JobService, { JobType } from "services/jobService"; export enum IndexCurve { @@ -28,7 +28,7 @@ const LogPropertiesModal = ( const { mode, logObject, dispatchOperation } = props; const { operationState: { timeZone, dateTimeFormat } - } = useContext(OperationContext); + } = useOperationState(); const [editableLogObject, setEditableLogObject] = useState(null); const [isLoading, setIsLoading] = useState(false); const editMode = mode === PropertiesModalMode.Edit; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/MessageComparisonModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/MessageComparisonModal.tsx index 0b2ef9636..829262e38 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/MessageComparisonModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/MessageComparisonModal.tsx @@ -16,14 +16,14 @@ import ModalDialog, { ModalWidth } from "components/Modals/ModalDialog"; import ProgressSpinner from "components/ProgressSpinner"; -import OperationContext from "contexts/operationContext"; import { DispatchOperation } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import MessageObject from "models/messageObject"; import ObjectOnWellbore from "models/objectOnWellbore"; import { ObjectType } from "models/objectType"; import { Server } from "models/server"; -import { useContext, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import ObjectService from "services/objectService"; export interface MessageComparisonModalProps { @@ -46,7 +46,7 @@ const MessageComparisonModal = ( } = props; const { operationState: { timeZone, colors, dateTimeFormat } - } = useContext(OperationContext); + } = useOperationState(); const [targetMessage, setTargetMessage] = useState(null); const [differenceFound, setDifferenceFound] = useState(false); const [data, setData] = useState(null); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/MessagePropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/MessagePropertiesModal.tsx index c1043068b..6043a47f3 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/MessagePropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/MessagePropertiesModal.tsx @@ -2,12 +2,12 @@ import { TextField } from "@equinor/eds-core-react"; import formatDateString from "components/DateFormatter"; import ModalDialog from "components/Modals/ModalDialog"; import { PropertiesModalMode, validText } from "components/Modals/ModalParts"; -import OperationContext from "contexts/operationContext"; import { HideModalAction } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import MessageObject from "models/messageObject"; import { ObjectType } from "models/objectType"; -import React, { ChangeEvent, useContext, useEffect, useState } from "react"; +import React, { ChangeEvent, useEffect, useState } from "react"; import JobService, { JobType } from "services/jobService"; export interface MessagePropertiesModalProps { @@ -22,7 +22,7 @@ const MessagePropertiesModal = ( const { mode, messageObject, dispatchOperation } = props; const { operationState: { timeZone, dateTimeFormat } - } = useContext(OperationContext); + } = useOperationState(); const [editableMessageObject, setEditableMessageObject] = useState(null); const [isLoading, setIsLoading] = useState(false); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/MissingDataAgentModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/MissingDataAgentModal.tsx index 8c71d3800..b38c10a21 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/MissingDataAgentModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/MissingDataAgentModal.tsx @@ -15,15 +15,15 @@ import ModalDialog, { } from "components/Modals/ModalDialog"; import { ReportModal } from "components/Modals/ReportModal"; import { Button } from "components/StyledComponents/Button"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import useExport from "hooks/useExport"; import { useLocalStorageState } from "hooks/useLocalStorageState"; +import { useOperationState } from "hooks/useOperationState"; import MissingDataJob, { MissingDataCheck } from "models/jobs/missingDataJob"; import WellReference from "models/jobs/wellReference"; import WellboreReference from "models/jobs/wellboreReference"; import { ObjectType } from "models/objectType"; -import { useContext, useRef, useState } from "react"; +import { useRef, useState } from "react"; import JobService, { JobType } from "services/jobService"; import styled from "styled-components"; import { STORAGE_MISSING_DATA_AGENT_CHECKS_KEY } from "tools/localStorageHelpers"; @@ -47,7 +47,7 @@ const MissingDataAgentModal = ( const { dispatchOperation, operationState: { colors } - } = useContext(OperationContext); + } = useOperationState(); const [missingDataChecks, setMissingDataChecks] = useLocalStorageState< MissingDataCheck[] >(STORAGE_MISSING_DATA_AGENT_CHECKS_KEY, { diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/ModalDialog.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/ModalDialog.tsx index 87743cdb0..2cd0cf794 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/ModalDialog.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/ModalDialog.tsx @@ -1,7 +1,7 @@ import { Dialog, Progress, Typography } from "@equinor/eds-core-react"; import { Button } from "components/StyledComponents/Button"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import React, { ReactElement, useState } from "react"; import styled from "styled-components"; import { Colors, dark, light } from "styles/Colors"; @@ -46,7 +46,7 @@ const ModalDialog = (props: ModalDialogProps): React.ReactElement => { buttonPosition: ButtonPosition = ControlButtonPosition.BOTTOM, cancelText } = props; - const context = React.useContext(OperationContext); + const context = useOperationState(); const [confirmButtonIsFocused, setConfirmButtonIsFocused] = useState(false); const { operationState } = context; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/ModalPresenter.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/ModalPresenter.tsx index e4d45aa8e..18ca97e54 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/ModalPresenter.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/ModalPresenter.tsx @@ -1,8 +1,8 @@ -import { Fragment, ReactElement, useContext } from "react"; -import OperationContext from "contexts/operationContext"; +import { useOperationState } from "hooks/useOperationState"; +import { Fragment, ReactElement } from "react"; const ModalPresenter = (): ReactElement => { - const { operationState } = useContext(OperationContext); + const { operationState } = useOperationState(); const { modals } = operationState; return ( <> diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/MudLogPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/MudLogPropertiesModal.tsx index f20c3df50..98574170b 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/MudLogPropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/MudLogPropertiesModal.tsx @@ -5,14 +5,14 @@ import { invalidStringInput, undefinedOnUnchagedEmptyString } from "components/Modals/PropertiesModalUtils"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import { itemStateValues } from "models/commonData"; import MaxLength from "models/maxLength"; import MudLog from "models/mudLog"; import ObjectOnWellbore, { toObjectReference } from "models/objectOnWellbore"; import { ObjectType } from "models/objectType"; -import React, { ChangeEvent, useContext, useEffect, useState } from "react"; +import React, { ChangeEvent, useEffect, useState } from "react"; import JobService, { JobType } from "services/jobService"; import { Layout } from "../StyledComponents/Layout"; @@ -33,7 +33,7 @@ const MudLogPropertiesModal = ( const { operationState: { timeZone, dateTimeFormat }, dispatchOperation - } = useContext(OperationContext); + } = useOperationState(); const [editableMudLog, setEditableMudLog] = useState(null); const [isLoading, setIsLoading] = useState(false); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/ObjectPickerModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/ObjectPickerModal.tsx index 214d4d54c..ffde97068 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/ObjectPickerModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/ObjectPickerModal.tsx @@ -7,14 +7,14 @@ import ModalDialog, { import { Banner } from "components/StyledComponents/Banner"; import { Button } from "components/StyledComponents/Button"; import { Checkbox } from "components/StyledComponents/Checkbox"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; +import { useOperationState } from "hooks/useOperationState"; import MaxLength from "models/maxLength"; import ObjectOnWellbore from "models/objectOnWellbore"; import { ObjectType } from "models/objectType"; import { Server } from "models/server"; -import { ChangeEvent, useContext, useState } from "react"; +import { ChangeEvent, useState } from "react"; import ObjectService from "services/objectService"; import styled from "styled-components"; import Icon from "styles/Icons"; @@ -43,7 +43,7 @@ const ObjectPickerModal = ({ const { operationState: { colors }, dispatchOperation - } = useContext(OperationContext); + } = useOperationState(); const [targetServer, setTargetServer] = useState(); const [wellUid, setWellUid] = useState(sourceObject.wellUid); const [wellboreUid, setWellboreUid] = useState( diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/OffsetLogCurveModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/OffsetLogCurveModal.tsx index d0ea9f105..c09910364 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/OffsetLogCurveModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/OffsetLogCurveModal.tsx @@ -15,15 +15,15 @@ import AdjustNumberRangeModal from "components/Modals/TrimLogObject/AdjustNumber import { Checkbox } from "components/StyledComponents/Checkbox"; import WarningBar from "components/WarningBar"; import { useConnectedServer } from "contexts/connectedServerContext"; +import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import { createComponentReferences } from "models/jobs/componentReferences"; import { OffsetLogCurveJob } from "models/jobs/offsetLogCurveJob"; import LogObject from "models/logObject"; -import React, { ChangeEvent, ReactElement, useContext, useState } from "react"; +import React, { ChangeEvent, ReactElement, useState } from "react"; import JobService, { JobType } from "services/jobService"; import styled from "styled-components"; import { indexToNumber } from "tools/IndexHelpers"; -import OperationContext from "../../contexts/operationContext"; import OperationType from "../../contexts/operationType"; import ModalDialog from "./ModalDialog"; @@ -44,7 +44,7 @@ export const OffsetLogCurveModal = ( endIndex: initialEndIndex } = props; const { connectedServer } = useConnectedServer(); - const { operationState, dispatchOperation } = useContext(OperationContext); + const { operationState, dispatchOperation } = useOperationState(); const { colors } = operationState; const isDepthLog = selectedLog.indexType === WITSML_INDEX_TYPE_MD; const [isValidInterval, setIsValidInterval] = useState(); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/ReportModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/ReportModal.tsx index 41de8abb3..5daba5a31 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/ReportModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/ReportModal.tsx @@ -16,10 +16,10 @@ import ModalDialog, { ModalWidth } from "components/Modals/ModalDialog"; import { generateReport } from "components/ReportCreationHelper"; import { Banner } from "components/StyledComponents/Banner"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import useExport from "hooks/useExport"; import { useLiveJobProgress } from "hooks/useLiveJobProgress"; +import { useOperationState } from "hooks/useOperationState"; import BaseReport, { createReport } from "models/reports/BaseReport"; import React, { useEffect, useState } from "react"; import JobService from "services/jobService"; @@ -50,7 +50,7 @@ export const ReportModal = (props: ReportModal): React.ReactElement => { const { dispatchOperation, operationState: { colors } - } = React.useContext(OperationContext); + } = useOperationState(); const [report, setReport] = useState(reportProp); const fetchedReport = useGetReportOnJobFinished(jobId); const jobProgress = useLiveJobProgress(jobId); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/RigPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/RigPropertiesModal.tsx index 8a9cebce0..fe9651e53 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/RigPropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/RigPropertiesModal.tsx @@ -7,14 +7,14 @@ import { validPhoneNumber, validText } from "components/Modals/ModalParts"; -import OperationContext from "contexts/operationContext"; import { HideModalAction } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import { itemStateTypes } from "models/itemStateTypes"; import { ObjectType } from "models/objectType"; import Rig from "models/rig"; import { rigType } from "models/rigType"; -import React, { ChangeEvent, useContext, useState } from "react"; +import React, { ChangeEvent, useState } from "react"; import JobService, { JobType } from "services/jobService"; export interface RigPropertiesModalProps { @@ -29,7 +29,7 @@ const RigPropertiesModal = ( const { mode, rig, dispatchOperation } = props; const { operationState: { timeZone } - } = useContext(OperationContext); + } = useOperationState(); const [editableRig, setEditableRig] = useState({ ...rig }); const [isLoading, setIsLoading] = useState(false); const [dTimStartOpValid, setDTimStartOpValid] = useState(true); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/RiskPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/RiskPropertiesModal.tsx index 86b38975f..01d9c2d56 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/RiskPropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/RiskPropertiesModal.tsx @@ -3,9 +3,9 @@ import formatDateString from "components/DateFormatter"; import { DateTimeField } from "components/Modals/DateTimeField"; import ModalDialog from "components/Modals/ModalDialog"; import { PropertiesModalMode, validText } from "components/Modals/ModalParts"; -import OperationContext from "contexts/operationContext"; import { HideModalAction } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import { itemStateTypes } from "models/itemStateTypes"; import { ObjectType } from "models/objectType"; import { riskAffectedPersonnel } from "models/riskAffectedPersonnel"; @@ -13,7 +13,7 @@ import { riskCategory } from "models/riskCategory"; import RiskObject from "models/riskObject"; import { riskSubCategory } from "models/riskSubCategory"; import { riskType } from "models/riskType"; -import React, { ChangeEvent, useContext, useEffect, useState } from "react"; +import React, { ChangeEvent, useEffect, useState } from "react"; import JobService, { JobType } from "services/jobService"; export interface RiskPropertiesModalProps { @@ -28,7 +28,7 @@ const RiskPropertiesModal = ( const { mode, riskObject, dispatchOperation } = props; const { operationState: { timeZone, dateTimeFormat } - } = useContext(OperationContext); + } = useOperationState(); const [editableRiskObject, setEditableRiskObject] = useState(null); const [isLoading, setIsLoading] = useState(false); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/SelectIndexToDisplayModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/SelectIndexToDisplayModal.tsx index a7f380558..1c77f785c 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/SelectIndexToDisplayModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/SelectIndexToDisplayModal.tsx @@ -8,12 +8,12 @@ import AdjustDateTimeModal from "components/Modals/TrimLogObject/AdjustDateTimeM import AdjustNumberRangeModal from "components/Modals/TrimLogObject/AdjustNumberRangeModal"; import { Banner } from "components/StyledComponents/Banner"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetActiveRoute } from "hooks/useGetActiveRoute"; +import { useOperationState } from "hooks/useOperationState"; import LogObject from "models/logObject"; import { ObjectType } from "models/objectType"; -import React, { useContext, useState } from "react"; +import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; import { RouterLogType } from "routes/routerConstants"; import { @@ -40,7 +40,7 @@ const SelectIndexToDisplayModal = ( const { operationState: { colors }, dispatchOperation - } = useContext(OperationContext); + } = useOperationState(); const isTimeIndexed = log.indexType === WITSML_INDEX_TYPE_DATE_TIME; const [startIndex, setStartIndex] = useState( getStartIndex(log, logCurveInfoRows, isMultiLog) diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/ServerModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/ServerModal.tsx index 0777c2c24..6739b093b 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/ServerModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/ServerModal.tsx @@ -9,13 +9,13 @@ import UserCredentialsModal, { } from "components/Modals/UserCredentialsModal"; import { Button } from "components/StyledComponents/Button"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import { DisplayModalAction, HideModalAction } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; import { refreshServersQuery } from "hooks/query/queryRefreshHelpers"; +import { useOperationState } from "hooks/useOperationState"; import { Server } from "models/server"; import { msalEnabled } from "msal/MsalAuthProvider"; import React, { @@ -23,7 +23,6 @@ import React, { ChangeEvent, Dispatch, SetStateAction, - useContext, useState } from "react"; import AuthorizationService from "services/authorizationService"; @@ -39,7 +38,7 @@ export interface ServerModalProps { const ServerModal = (props: ServerModalProps): React.ReactElement => { const queryClient = useQueryClient(); - const { operationState, dispatchOperation } = useContext(OperationContext); + const { operationState, dispatchOperation } = useOperationState(); const { colors } = operationState; const [server, setServer] = useState(props.server); const [connectionVerified, setConnectionVerified] = useState(false); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/SettingsModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/SettingsModal.tsx index 0dfd85432..d6fd59a42 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/SettingsModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/SettingsModal.tsx @@ -4,7 +4,6 @@ import ModalDialog from "components/Modals/ModalDialog"; import { StyledNativeSelect } from "components/Select"; import { Button } from "components/StyledComponents/Button"; import { Checkbox } from "components/StyledComponents/Checkbox"; -import OperationContext from "contexts/operationContext"; import { DateTimeFormat, DecimalPreference, @@ -12,8 +11,9 @@ import { UserTheme } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import { getAccountInfo, msalEnabled, signOut } from "msal/MsalAuthProvider"; -import React, { CSSProperties, ChangeEvent, useContext, useState } from "react"; +import React, { CSSProperties, ChangeEvent, useState } from "react"; import AuthorizationService from "services/authorizationService"; import styled from "styled-components"; import { dark, light } from "styles/Colors"; @@ -54,7 +54,7 @@ const SettingsModal = (): React.ReactElement => { hotKeysEnabled }, dispatchOperation - } = useContext(OperationContext); + } = useOperationState(); const [checkedDecimalPreference, setCheckedDecimalPreference] = useState(() => { return decimals === DecimalPreference.Raw diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/ShowLogDataOnServerModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/ShowLogDataOnServerModal.tsx index 3551915ee..49ff03900 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/ShowLogDataOnServerModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/ShowLogDataOnServerModal.tsx @@ -7,11 +7,11 @@ import { import ModalDialog from "components/Modals/ModalDialog"; import { Banner } from "components/StyledComponents/Banner"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; +import { useOperationState } from "hooks/useOperationState"; import { Server } from "models/server"; -import { CSSProperties, ChangeEvent, useContext, useState } from "react"; +import { CSSProperties, ChangeEvent, useState } from "react"; import { useParams, useSearchParams } from "react-router-dom"; import { checkIsUrlTooLong } from "routes/utils/checkIsUrlTooLong"; import { createLogCurveValuesSearchParams } from "routes/utils/createLogCurveValuesSearchParams"; @@ -35,7 +35,7 @@ export function ShowLogDataOnServerModal() { const { operationState: { colors, theme }, dispatchOperation - } = useContext(OperationContext); + } = useOperationState(); const { wellUid, wellboreUid, objectGroup, objectUid, logType } = useParams(); const [isUrlTooLong, setIsUrlTooLong] = useState(false); const [searchParams] = useSearchParams(); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/SpliceLogsModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/SpliceLogsModal.tsx index 938acb09a..5fe4336a1 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/SpliceLogsModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/SpliceLogsModal.tsx @@ -2,8 +2,8 @@ import { Accordion, TextField, Typography } from "@equinor/eds-core-react"; import { StyledAccordionHeader } from "components/Modals/LogComparisonModal"; import ModalDialog, { ModalWidth } from "components/Modals/ModalDialog"; import { validText } from "components/Modals/ModalParts"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import SpliceLogsJob from "models/jobs/spliceLogsJob"; import LogObject from "models/logObject"; import ObjectOnWellbore, { toObjectReferences } from "models/objectOnWellbore"; @@ -12,7 +12,6 @@ import { ChangeEvent, DragEvent, ReactElement, - useContext, useEffect, useState } from "react"; @@ -32,7 +31,7 @@ const SpliceLogsModal = (props: SpliceLogsProps): ReactElement => { const { operationState: { colors }, dispatchOperation - } = useContext(OperationContext); + } = useOperationState(); const [draggedId, setDraggedId] = useState(null); const [draggedOverId, setDraggedOverId] = useState(null); const [orderedLogs, setOrderedLogs] = useState([]); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/TrajectoryPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/TrajectoryPropertiesModal.tsx index 444548579..00ff784f7 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/TrajectoryPropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/TrajectoryPropertiesModal.tsx @@ -2,12 +2,12 @@ import { DateTimeField } from "components/Modals/DateTimeField"; import ModalDialog from "components/Modals/ModalDialog"; import { PropertiesModalMode, validText } from "components/Modals/ModalParts"; -import OperationContext from "contexts/operationContext"; import { HideModalAction } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import { ObjectType } from "models/objectType"; import Trajectory, { aziRefValues } from "models/trajectory"; -import React, { ChangeEvent, useContext, useEffect, useState } from "react"; +import React, { ChangeEvent, useEffect, useState } from "react"; import JobService, { JobType } from "services/jobService"; import { itemStateTypes } from "../../models/itemStateTypes"; export interface TrajectoryPropertiesModalProps { @@ -22,7 +22,7 @@ const TrajectoryPropertiesModal = ( const { mode, trajectory, dispatchOperation } = props; const { operationState: { timeZone } - } = useContext(OperationContext); + } = useOperationState(); const [editableTrajectory, setEditableTrajectory] = useState(null); const [isLoading, setIsLoading] = useState(false); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/TrajectoryStationPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/TrajectoryStationPropertiesModal.tsx index 95d148d35..e7f6178e3 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/TrajectoryStationPropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/TrajectoryStationPropertiesModal.tsx @@ -2,15 +2,15 @@ import { TextField } from "@equinor/eds-core-react"; import formatDateString from "components/DateFormatter"; import { DateTimeField } from "components/Modals/DateTimeField"; import ModalDialog from "components/Modals/ModalDialog"; -import OperationContext from "contexts/operationContext"; import { HideModalAction } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import ObjectReference from "models/jobs/objectReference"; import { measureToString } from "models/measure"; import { toObjectReference } from "models/objectOnWellbore"; import Trajectory from "models/trajectory"; import TrajectoryStation from "models/trajectoryStation"; -import React, { ChangeEvent, useContext, useEffect, useState } from "react"; +import React, { ChangeEvent, useEffect, useState } from "react"; import JobService, { JobType } from "services/jobService"; export interface TrajectoryStationPropertiesModalInterface { @@ -25,7 +25,7 @@ const TrajectoryStationPropertiesModal = ( const { trajectoryStation, trajectory, dispatchOperation } = props; const { operationState: { timeZone, dateTimeFormat } - } = useContext(OperationContext); + } = useOperationState(); const [editableTrajectoryStation, setEditableTrajectoryStation] = useState(null); const [isLoading, setIsLoading] = useState(false); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/TrimLogObject/TrimLogObjectModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/TrimLogObject/TrimLogObjectModal.tsx index b63e05348..29bee51e6 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/TrimLogObject/TrimLogObjectModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/TrimLogObject/TrimLogObjectModal.tsx @@ -7,11 +7,11 @@ import ModalDialog from "components/Modals/ModalDialog"; import AdjustDateTimeModal from "components/Modals/TrimLogObject/AdjustDateTimeModal"; import AdjustNumberRangeModal from "components/Modals/TrimLogObject/AdjustNumberRangeModal"; import WarningBar from "components/WarningBar"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import { createTrimLogObjectJob } from "models/jobs/trimLogObjectJob"; import LogObject, { indexToNumber } from "models/logObject"; -import React, { useContext, useState } from "react"; +import React, { useState } from "react"; import JobService, { JobType } from "services/jobService"; export interface TrimLogObjectModalProps { @@ -22,7 +22,7 @@ const TrimLogObjectModal = ( props: TrimLogObjectModalProps ): React.ReactElement => { const { logObject } = props; - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const [log] = useState(logObject); const [isLoading, setIsLoading] = useState(false); const [startIndex, setStartIndex] = useState(); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/UserCredentialsModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/UserCredentialsModal.tsx index 5f601b3ff..5d5a8feea 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/UserCredentialsModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/UserCredentialsModal.tsx @@ -8,10 +8,10 @@ import ModalDialog, { ModalWidth } from "components/Modals/ModalDialog"; import { validText } from "components/Modals/ModalParts"; import { Button } from "components/StyledComponents/Button"; import { Checkbox } from "components/StyledComponents/Checkbox"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import { Server } from "models/server"; -import React, { ChangeEvent, useContext, useEffect, useState } from "react"; +import React, { ChangeEvent, useEffect, useState } from "react"; import AuthorizationService, { AuthorizationStatus, BasicServerCredentials, @@ -34,7 +34,7 @@ const UserCredentialsModal = ( const { operationState: { colors }, dispatchOperation - } = useContext(OperationContext); + } = useOperationState(); const [username, setUsername] = useState(); const [password, setPassword] = useState(); const [isLoading, setIsLoading] = useState(false); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/WbGeometryPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/WbGeometryPropertiesModal.tsx index fc5dc0cba..c968f8be1 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/WbGeometryPropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/WbGeometryPropertiesModal.tsx @@ -2,13 +2,13 @@ import { Autocomplete, TextField } from "@equinor/eds-core-react"; import formatDateString from "components/DateFormatter"; import ModalDialog from "components/Modals/ModalDialog"; import { PropertiesModalMode, validText } from "components/Modals/ModalParts"; -import OperationContext from "contexts/operationContext"; import { HideModalAction } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import { itemStateTypes } from "models/itemStateTypes"; import { ObjectType } from "models/objectType"; import WbGeometryObject from "models/wbGeometry"; -import React, { ChangeEvent, useContext, useEffect, useState } from "react"; +import React, { ChangeEvent, useEffect, useState } from "react"; import JobService, { JobType } from "services/jobService"; export interface WbGeometryPropertiesModalProps { @@ -23,7 +23,7 @@ const WbGeometryPropertiesModal = ( const { mode, wbGeometryObject, dispatchOperation } = props; const { operationState: { timeZone, dateTimeFormat } - } = useContext(OperationContext); + } = useOperationState(); const [editableWbGeometryObject, setEditableWbGeometryObject] = useState(null); const [isLoading, setIsLoading] = useState(false); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/WellPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/WellPropertiesModal.tsx index 0bc75b037..d2eef21df 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/WellPropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/WellPropertiesModal.tsx @@ -6,11 +6,11 @@ import { validText, validTimeZone } from "components/Modals/ModalParts"; -import OperationContext from "contexts/operationContext"; import { HideModalAction } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import Well from "models/well"; -import React, { ChangeEvent, useContext, useEffect, useState } from "react"; +import React, { ChangeEvent, useEffect, useState } from "react"; import JobService, { JobType } from "services/jobService"; export interface WellPropertiesModalProps { @@ -25,7 +25,7 @@ const WellPropertiesModal = ( const { mode, well, dispatchOperation } = props; const { operationState: { timeZone, dateTimeFormat } - } = useContext(OperationContext); + } = useOperationState(); const [editableWell, setEditableWell] = useState(null); const [isLoading, setIsLoading] = useState(false); const editMode = mode === PropertiesModalMode.Edit; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/WellborePropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/WellborePropertiesModal.tsx index 2c762e07c..3dfa79a0c 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/WellborePropertiesModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/WellborePropertiesModal.tsx @@ -4,12 +4,12 @@ import { DateTimeField } from "components/Modals/DateTimeField"; import ModalDialog from "components/Modals/ModalDialog"; import { PropertiesModalMode, validText } from "components/Modals/ModalParts"; import { invalidStringInput } from "components/Modals/PropertiesModalUtils"; -import OperationContext from "contexts/operationContext"; import { HideModalAction } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import MaxLength from "models/maxLength"; import Wellbore, { wellboreHasChanges } from "models/wellbore"; -import React, { ChangeEvent, useContext, useEffect, useState } from "react"; +import React, { ChangeEvent, useEffect, useState } from "react"; import JobService, { JobType } from "services/jobService"; import styled from "styled-components"; @@ -35,7 +35,7 @@ const WellborePropertiesModal = ( const { mode, wellbore, dispatchOperation } = props; const { operationState: { timeZone, dateTimeFormat } - } = useContext(OperationContext); + } = useOperationState(); const [editableWellbore, setEditableWellbore] = useState(null); const [pristineWellbore, setPristineWellbore] = useState(null); const [isLoading, setIsLoading] = useState(false); diff --git a/Src/WitsmlExplorer.Frontend/components/PageLayout.tsx b/Src/WitsmlExplorer.Frontend/components/PageLayout.tsx index 11d95d848..987174a92 100644 --- a/Src/WitsmlExplorer.Frontend/components/PageLayout.tsx +++ b/Src/WitsmlExplorer.Frontend/components/PageLayout.tsx @@ -8,14 +8,13 @@ import Nav from "components/Nav"; import PropertiesPanel from "components/PropertiesPanel"; import Sidebar from "components/Sidebar/Sidebar"; import { Button } from "components/StyledComponents/Button"; -import OperationContext from "contexts/operationContext"; import useDocumentDimensions from "hooks/useDocumentDimensions"; +import { useOperationState } from "hooks/useOperationState"; import { msalEnabled } from "msal/MsalAuthProvider"; import { ReactElement, createContext, useCallback, - useContext, useEffect, useRef, useState @@ -32,7 +31,7 @@ const PageLayout = (): ReactElement => { const [sidebarWidth, setSidebarWidth] = useState(316); const { width: documentWidth, height: documentHeight } = useDocumentDimensions(); - const { operationState } = useContext(OperationContext); + const { operationState } = useOperationState(); const { colors } = operationState; const startResizing = useCallback(() => { diff --git a/Src/WitsmlExplorer.Frontend/components/PropertiesPanel.tsx b/Src/WitsmlExplorer.Frontend/components/PropertiesPanel.tsx index 120d0108b..0eddf2e0c 100644 --- a/Src/WitsmlExplorer.Frontend/components/PropertiesPanel.tsx +++ b/Src/WitsmlExplorer.Frontend/components/PropertiesPanel.tsx @@ -1,20 +1,20 @@ import { Typography } from "@equinor/eds-core-react"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import { useGetObject } from "hooks/query/useGetObject"; import { useGetWell } from "hooks/query/useGetWell"; import { useGetWellbore } from "hooks/query/useGetWellbore"; +import { useOperationState } from "hooks/useOperationState"; import { getObjectOnWellboreProperties } from "models/objectOnWellbore"; import { ObjectType } from "models/objectType"; import { getWellProperties } from "models/well"; import { getWellboreProperties } from "models/wellbore"; -import React, { useContext } from "react"; +import React from "react"; import { useParams } from "react-router-dom"; const PropertiesPanel = (): React.ReactElement => { const { operationState: { colors } - } = useContext(OperationContext); + } = useOperationState(); const { connectedServer } = useConnectedServer(); const { wellUid, wellboreUid, objectGroup, objectUid } = useParams(); const { well } = useGetWell(connectedServer, wellUid); diff --git a/Src/WitsmlExplorer.Frontend/components/QueryEditor.tsx b/Src/WitsmlExplorer.Frontend/components/QueryEditor.tsx index 1faf52684..45d8aab7a 100644 --- a/Src/WitsmlExplorer.Frontend/components/QueryEditor.tsx +++ b/Src/WitsmlExplorer.Frontend/components/QueryEditor.tsx @@ -7,8 +7,7 @@ import "ace-builds/src-noconflict/theme-merbivore"; import "ace-builds/src-noconflict/theme-xcode"; import { customCommands, customCompleter } from "components/QueryEditorUtils"; import { updateLinesWithWidgets } from "components/QueryEditorWidgetUtils"; -import OperationContext from "contexts/operationContext"; -import { useContext } from "react"; +import { useOperationState } from "hooks/useOperationState"; import AceEditor from "react-ace"; import styled from "styled-components"; import { Colors, dark } from "styles/Colors"; @@ -23,7 +22,7 @@ export const QueryEditor = (props: QueryEditorProps) => { const { value, onChange, readonly } = props; const { operationState: { colors } - } = useContext(OperationContext); + } = useOperationState(); const onLoad = (editor: any) => { editor.renderer.setPadding(10); diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/FilterPanel.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/FilterPanel.tsx index d9dc31d4d..51e533254 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/FilterPanel.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/FilterPanel.tsx @@ -9,8 +9,8 @@ import { Checkbox } from "components/StyledComponents/Checkbox"; import { useConnectedServer } from "contexts/connectedServerContext"; import { useCurveThreshold } from "contexts/curveThresholdContext"; import { FilterContext, VisibilityStatus } from "contexts/filter"; -import OperationContext from "contexts/operationContext"; import { useGetCapObjects } from "hooks/query/useGetCapObjects"; +import { useOperationState } from "hooks/useOperationState"; import { ObjectType } from "models/objectType"; import React, { ChangeEvent, useContext } from "react"; import styled from "styled-components"; @@ -29,7 +29,7 @@ const FilterPanel = (): React.ReactElement => { const { selectedFilter, updateSelectedFilter } = useContext(FilterContext); const { operationState: { colors } - } = useContext(OperationContext); + } = useOperationState(); const { connectedServer } = useConnectedServer(); const { capObjects } = useGetCapObjects(connectedServer, { placeholderData: Object.entries(ObjectType) diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/LogItem.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/LogItem.tsx index dd5a5640b..6199d78e4 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/LogItem.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/LogItem.tsx @@ -5,10 +5,10 @@ import { import LogObjectContextMenu from "components/ContextMenus/LogObjectContextMenu"; import { ObjectContextMenuProps } from "components/ContextMenus/ObjectMenuItems"; import TreeItem from "components/Sidebar/TreeItem"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import LogObject from "models/logObject"; -import { MouseEvent, useContext } from "react"; +import { MouseEvent } from "react"; import { getNameOccurrenceSuffix } from "tools/logSameNamesHelper"; interface LogItemProps { @@ -28,7 +28,7 @@ export default function LogItem({ objectGrowing, to }: LogItemProps) { - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const onContextMenu = (event: MouseEvent, log: LogObject) => { preventContextMenuPropagation(event); diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/LogTypeItem.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/LogTypeItem.tsx index a77c7ec58..59051024e 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/LogTypeItem.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/LogTypeItem.tsx @@ -14,10 +14,10 @@ import { IndexCurve } from "components/Modals/LogPropertiesModal"; import LogItem from "components/Sidebar/LogItem"; import TreeItem from "components/Sidebar/TreeItem"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useGetWellbore } from "hooks/query/useGetWellbore"; +import { useOperationState } from "hooks/useOperationState"; import LogObject from "models/logObject"; import { calculateObjectNodeId } from "models/objectOnWellbore"; import { ObjectType } from "models/objectType"; @@ -29,7 +29,7 @@ import Wellbore, { calculateMultipleLogsNodeItem, calculateObjectNodeId as calculateWellboreObjectNodeId } from "models/wellbore"; -import { Fragment, MouseEvent, useContext } from "react"; +import { Fragment, MouseEvent } from "react"; import { useParams, useSearchParams } from "react-router-dom"; import { RouterLogType } from "routes/routerConstants"; import { @@ -48,7 +48,7 @@ export default function LogTypeItem({ wellUid, wellboreUid }: LogTypeItemProps) { - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const [searchParams] = useSearchParams(); const { servers } = useGetServers(); const { connectedServer } = useConnectedServer(); diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/ObjectGroupItem.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/ObjectGroupItem.tsx index 1dafb2f02..f84a6f800 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/ObjectGroupItem.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/ObjectGroupItem.tsx @@ -20,7 +20,6 @@ import { isObjectFilterType, objectFilterTypeToObjects } from "contexts/filter"; -import OperationContext from "contexts/operationContext"; import { OperationAction } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; import { useSidebar } from "contexts/sidebarContext"; @@ -29,6 +28,7 @@ import { useGetObjectCount } from "hooks/query/useGetObjectCount"; import { useGetObjects } from "hooks/query/useGetObjects"; import { useGetWellbore } from "hooks/query/useGetWellbore"; import { useObjectFilter } from "hooks/useObjectFilter"; +import { useOperationState } from "hooks/useOperationState"; import LogObject from "models/logObject"; import ObjectOnWellbore, { calculateObjectNodeId @@ -60,7 +60,7 @@ export default function ObjectGroupItem({ ObjectContextMenu, onGroupContextMenu }: ObjectGroupItemProps) { - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const { selectedFilter } = useContext(FilterContext); const { expandedTreeNodes } = useSidebar(); const { diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/ObjectOnWellboreItem.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/ObjectOnWellboreItem.tsx index 90ca49490..e5d5f169a 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/ObjectOnWellboreItem.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/ObjectOnWellboreItem.tsx @@ -5,12 +5,12 @@ import { import { ObjectContextMenuProps } from "components/ContextMenus/ObjectMenuItems"; import TreeItem from "components/Sidebar/TreeItem"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import ObjectOnWellbore from "models/objectOnWellbore"; import { ObjectType } from "models/objectType"; import { calculateObjectNodeId } from "models/wellbore"; -import { ComponentType, MouseEvent, useContext } from "react"; +import { ComponentType, MouseEvent } from "react"; import { useParams } from "react-router-dom"; import { getObjectViewPath } from "routes/utils/pathBuilder"; @@ -31,7 +31,7 @@ export default function ObjectOnWellboreItem({ wellUid, wellboreUid }: ObjectOnWellboreItemProps) { - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const { connectedServer } = useConnectedServer(); const { wellUid: urlWellUid, diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter.tsx index 183ff3784..0bf41640a 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter.tsx @@ -14,8 +14,8 @@ import { isObjectFilterType, isWellboreFilterType } from "contexts/filter"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; +import { useOperationState } from "hooks/useOperationState"; import React, { useContext, useEffect, useRef, useState } from "react"; import { createSearchParams, useNavigate } from "react-router-dom"; import { getSearchViewPath } from "routes/utils/pathBuilder"; @@ -26,13 +26,13 @@ import Icons from "styles/Icons"; const searchOptions = Object.values(FilterType); const SearchFilter = (): React.ReactElement => { - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const { selectedFilter, updateSelectedFilter } = useContext(FilterContext); const { connectedServer } = useConnectedServer(); const selectedOption = selectedFilter?.filterType; const { operationState: { colors } - } = useContext(OperationContext); + } = useOperationState(); const [expanded, setExpanded] = useState(false); const [nameFilter, setNameFilter] = useState(selectedFilter.name); const textFieldRef = useRef(null); diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/Sidebar.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/Sidebar.tsx index 7a6699576..b0b833598 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/Sidebar.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/Sidebar.tsx @@ -9,14 +9,14 @@ import ProgressSpinner from "components/ProgressSpinner"; import SearchFilter from "components/Sidebar/SearchFilter"; import WellItem from "components/Sidebar/WellItem"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import { UserTheme } from "contexts/operationStateReducer"; import { useSidebar } from "contexts/sidebarContext"; import { SidebarActionType } from "contexts/sidebarReducer"; import { useGetWells } from "hooks/query/useGetWells"; +import { useOperationState } from "hooks/useOperationState"; import { useWellFilter } from "hooks/useWellFilter"; import Well from "models/well"; -import { Fragment, useContext, useEffect, useRef } from "react"; +import { Fragment, useEffect, useRef } from "react"; import { useParams } from "react-router-dom"; import styled from "styled-components"; import Icon from "styles/Icons"; @@ -30,7 +30,7 @@ export default function Sidebar() { const isDeepLink = useRef(!!wellUid); const { operationState: { colors, theme } - } = useContext(OperationContext); + } = useOperationState(); const isCompactMode = theme === UserTheme.Compact; const filteredWells = useWellFilter(wells); const containerRef = useRef(null); diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/TreeItem.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/TreeItem.tsx index a2f4c520b..d0355195e 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/TreeItem.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/TreeItem.tsx @@ -1,9 +1,9 @@ import { DotProgress } from "@equinor/eds-core-react"; import { Tooltip } from "@mui/material"; import { TreeItem, TreeItemProps } from "@mui/x-tree-view"; -import OperationContext from "contexts/operationContext"; import { UserTheme } from "contexts/operationStateReducer"; -import React, { useContext } from "react"; +import { useOperationState } from "hooks/useOperationState"; +import React from "react"; import { NavLink } from "react-router-dom"; import styled from "styled-components"; import { Colors } from "styles/Colors"; @@ -22,12 +22,12 @@ const StyledTreeItem = (props: StyledTreeItemProps): React.ReactElement => { props; const { operationState: { theme } - } = useContext(OperationContext); + } = useOperationState(); const isCompactMode = theme === UserTheme.Compact; const { operationState: { colors } - } = useContext(OperationContext); + } = useOperationState(); return ( ((props, ref) => { const { operationState: { colors, theme } - } = useContext(OperationContext); + } = useOperationState(); if (!props.variant || props.variant === "contained") { return ; diff --git a/Src/WitsmlExplorer.Frontend/components/TopRightCornerMenu.tsx b/Src/WitsmlExplorer.Frontend/components/TopRightCornerMenu.tsx index ddcfdd432..39c41fe3d 100644 --- a/Src/WitsmlExplorer.Frontend/components/TopRightCornerMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/TopRightCornerMenu.tsx @@ -6,10 +6,9 @@ import { Button } from "components/StyledComponents/Button"; import { useConnectedServer } from "contexts/connectedServerContext"; import { useLoggedInUsernames } from "contexts/loggedInUsernamesContext"; import { LoggedInUsernamesActionType } from "contexts/loggedInUsernamesReducer"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import useDocumentDimensions from "hooks/useDocumentDimensions"; -import { useContext } from "react"; +import { useOperationState } from "hooks/useOperationState"; import { useNavigate } from "react-router-dom"; import { getJobsViewPath, getQueryViewPath } from "routes/utils/pathBuilder"; import AuthorizationService from "services/authorizationService"; @@ -17,7 +16,7 @@ import styled from "styled-components"; import Icon from "styles/Icons"; export default function TopRightCornerMenu() { - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const { width: documentWidth } = useDocumentDimensions(); const showLabels = documentWidth > 1180; const { connectedServer } = useConnectedServer(); diff --git a/Src/WitsmlExplorer.Frontend/contexts/MuiThemeProvider.tsx b/Src/WitsmlExplorer.Frontend/contexts/MuiThemeProvider.tsx new file mode 100644 index 000000000..ad3b4a721 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/contexts/MuiThemeProvider.tsx @@ -0,0 +1,18 @@ +import { ThemeProvider } from "@mui/material"; +import { useOperationState } from "hooks/useOperationState"; +import { ReactElement, ReactNode } from "react"; +import { getTheme } from "styles/material-eds"; + +interface MuiThemeProviderProps { + children: ReactNode; +} + +export const MuiThemeProvider = ({ + children +}: MuiThemeProviderProps): ReactElement => { + const { + operationState: { theme } + } = useOperationState(); + + return {children}; +}; diff --git a/Src/WitsmlExplorer.Frontend/contexts/operationStateProvider.tsx b/Src/WitsmlExplorer.Frontend/contexts/operationStateProvider.tsx new file mode 100644 index 000000000..d94a16507 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/contexts/operationStateProvider.tsx @@ -0,0 +1,108 @@ +import OperationContext from "contexts/operationContext"; +import { + DateTimeFormat, + DecimalPreference, + SetDateTimeFormatAction, + SetDecimalAction, + SetHotKeysEnabledAction, + SetModeAction, + SetThemeAction, + SetTimeZoneAction, + TimeZone, + UserTheme, + initOperationStateReducer +} from "contexts/operationStateReducer"; +import OperationType from "contexts/operationType"; +import { enableDarkModeDebug } from "debugUtils/darkModeDebug"; +import { ReactNode, useEffect } from "react"; +import { dark, light } from "styles/Colors"; +import { + STORAGE_DATETIMEFORMAT_KEY, + STORAGE_DECIMAL_KEY, + STORAGE_HOTKEYS_ENABLED_KEY, + STORAGE_MODE_KEY, + STORAGE_THEME_KEY, + STORAGE_TIMEZONE_KEY, + getLocalStorageItem +} from "tools/localStorageHelpers"; + +interface OperationStateProviderProps { + children: ReactNode; +} + +export const OperationStateProvider = ({ + children +}: OperationStateProviderProps) => { + const [operationState, dispatchOperation] = initOperationStateReducer(); + + useEffect(() => { + if (typeof localStorage != "undefined") { + const localStorageTheme = + getLocalStorageItem(STORAGE_THEME_KEY); + if (localStorageTheme) { + const action: SetThemeAction = { + type: OperationType.SetTheme, + payload: localStorageTheme + }; + dispatchOperation(action); + } + const storedTimeZone = + getLocalStorageItem(STORAGE_TIMEZONE_KEY); + if (storedTimeZone) { + const action: SetTimeZoneAction = { + type: OperationType.SetTimeZone, + payload: storedTimeZone + }; + dispatchOperation(action); + } + const storedMode = getLocalStorageItem<"light" | "dark">( + STORAGE_MODE_KEY + ); + if (storedMode) { + const action: SetModeAction = { + type: OperationType.SetMode, + payload: storedMode == "light" ? light : dark + }; + dispatchOperation(action); + } + const storedDateTimeFormat = getLocalStorageItem( + STORAGE_DATETIMEFORMAT_KEY + ); + if (storedDateTimeFormat) { + const action: SetDateTimeFormatAction = { + type: OperationType.SetDateTimeFormat, + payload: storedDateTimeFormat + }; + dispatchOperation(action); + } + const storedDecimals = + getLocalStorageItem(STORAGE_DECIMAL_KEY); + if (storedDecimals) { + const action: SetDecimalAction = { + type: OperationType.SetDecimal, + payload: storedDecimals + }; + dispatchOperation(action); + } + const storedHotKeysEnabled = getLocalStorageItem( + STORAGE_HOTKEYS_ENABLED_KEY + ); + if (storedHotKeysEnabled != null) { + const action: SetHotKeysEnabledAction = { + type: OperationType.SetHotKeysEnabled, + payload: storedHotKeysEnabled + }; + dispatchOperation(action); + } + } + if (import.meta.env.VITE_DARK_MODE_DEBUG) { + return enableDarkModeDebug(dispatchOperation); + } + }, []); + + return ( + + {children} + + ); +}; diff --git a/Src/WitsmlExplorer.Frontend/contexts/queryContext.tsx b/Src/WitsmlExplorer.Frontend/contexts/queryContext.tsx index 10a421890..72dbea9a2 100644 --- a/Src/WitsmlExplorer.Frontend/contexts/queryContext.tsx +++ b/Src/WitsmlExplorer.Frontend/contexts/queryContext.tsx @@ -1,5 +1,3 @@ -import React, { Dispatch, useEffect } from "react"; -import { v4 as uuid } from "uuid"; import { QueryTemplatePreset, ReturnElements, @@ -7,10 +5,12 @@ import { getQueryTemplateWithPreset } from "components/ContentViews/QueryViewUtils"; import { useLocalStorageState } from "hooks/useLocalStorageState"; +import React, { Dispatch, useEffect } from "react"; import { STORAGE_QUERYVIEW_DATA, getLocalStorageItem } from "tools/localStorageHelpers"; +import { v4 as uuid } from "uuid"; export interface QueryElement { query: string; diff --git a/Src/WitsmlExplorer.Frontend/hooks/query/queryRefreshHelpers.tsx b/Src/WitsmlExplorer.Frontend/hooks/query/queryRefreshHelpers.tsx index beb3f5867..ae6464ce4 100644 --- a/Src/WitsmlExplorer.Frontend/hooks/query/queryRefreshHelpers.tsx +++ b/Src/WitsmlExplorer.Frontend/hooks/query/queryRefreshHelpers.tsx @@ -1,19 +1,19 @@ import { QueryClient } from "@tanstack/react-query"; +import { ObjectFilterType } from "../../contexts/filter"; import { ComponentType, getParentType } from "../../models/componentType"; import { ObjectType } from "../../models/objectType"; import { QUERY_KEY_COMPONENTS, QUERY_KEY_JOB_INFO, QUERY_KEY_OBJECT, - QUERY_KEY_OBJECT_SEARCH, QUERY_KEY_OBJECTS, + QUERY_KEY_OBJECT_SEARCH, QUERY_KEY_SERVERS, QUERY_KEY_WELL, QUERY_KEY_WELLBORE, QUERY_KEY_WELLBORES, QUERY_KEY_WELLS } from "./queryKeys"; -import { ObjectFilterType } from "../../contexts/filter"; export const refreshServersQuery = (queryClient: QueryClient) => { queryClient.invalidateQueries({ diff --git a/Src/WitsmlExplorer.Frontend/hooks/useOpenInQueryView.tsx b/Src/WitsmlExplorer.Frontend/hooks/useOpenInQueryView.tsx index 99c572c03..51c552c13 100644 --- a/Src/WitsmlExplorer.Frontend/hooks/useOpenInQueryView.tsx +++ b/Src/WitsmlExplorer.Frontend/hooks/useOpenInQueryView.tsx @@ -1,8 +1,8 @@ import { QueryTemplatePreset } from "components/ContentViews/QueryViewUtils"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationContext from "contexts/operationContext"; import OperationType from "contexts/operationType"; import { QueryActionType, QueryContext } from "contexts/queryContext"; +import { useOperationState } from "hooks/useOperationState"; import { useCallback, useContext } from "react"; import { useNavigate } from "react-router-dom"; import { getQueryViewPath } from "routes/utils/pathBuilder"; @@ -11,7 +11,7 @@ export type OpenInQueryView = (templatePreset: QueryTemplatePreset) => void; export const useOpenInQueryView = () => { const { dispatchQuery } = useContext(QueryContext); - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const { connectedServer } = useConnectedServer(); const navigate = useNavigate(); diff --git a/Src/WitsmlExplorer.Frontend/hooks/useOperationState.tsx b/Src/WitsmlExplorer.Frontend/hooks/useOperationState.tsx new file mode 100644 index 000000000..363d72979 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/hooks/useOperationState.tsx @@ -0,0 +1,11 @@ +import OperationContext from "contexts/operationContext"; +import { useContext } from "react"; + +export function useOperationState() { + const context = useContext(OperationContext); + if (!context) + throw new Error( + `useOperationState() has to be used within ` + ); + return context; +} diff --git a/Src/WitsmlExplorer.Frontend/routes/AuthRoute.tsx b/Src/WitsmlExplorer.Frontend/routes/AuthRoute.tsx index 9236b67a0..3039168c1 100644 --- a/Src/WitsmlExplorer.Frontend/routes/AuthRoute.tsx +++ b/Src/WitsmlExplorer.Frontend/routes/AuthRoute.tsx @@ -1,14 +1,14 @@ import { useIsAuthenticated } from "@azure/msal-react"; import { useLoggedInUsernames } from "contexts/loggedInUsernamesContext"; import { LoggedInUsernamesActionType } from "contexts/loggedInUsernamesReducer"; -import { useContext, useEffect } from "react"; +import { useOperationState } from "hooks/useOperationState"; +import { useEffect } from "react"; import { Outlet, useNavigate, useParams } from "react-router-dom"; import { ItemNotFound } from "routes/ItemNotFound"; import UserCredentialsModal, { UserCredentialsModalProps } from "../components/Modals/UserCredentialsModal"; import { useConnectedServer } from "../contexts/connectedServerContext"; -import OperationContext from "../contexts/operationContext"; import OperationType from "../contexts/operationType"; import { useGetServers } from "../hooks/query/useGetServers"; import { Server } from "../models/server"; @@ -19,7 +19,7 @@ import AuthorizationService, { } from "../services/authorizationService"; export default function AuthRoute() { - const { dispatchOperation } = useContext(OperationContext); + const { dispatchOperation } = useOperationState(); const isAuthenticated = !msalEnabled || useIsAuthenticated(); const { servers } = useGetServers({ enabled: isAuthenticated }); const { serverUrl } = useParams(); diff --git a/Src/WitsmlExplorer.Frontend/routes/ItemNotFound.tsx b/Src/WitsmlExplorer.Frontend/routes/ItemNotFound.tsx index 031d74b99..80cd0b75f 100644 --- a/Src/WitsmlExplorer.Frontend/routes/ItemNotFound.tsx +++ b/Src/WitsmlExplorer.Frontend/routes/ItemNotFound.tsx @@ -1,7 +1,6 @@ import { Typography } from "@equinor/eds-core-react"; -import { useContext } from "react"; +import { useOperationState } from "hooks/useOperationState"; import styled from "styled-components"; -import OperationContext from "../contexts/operationContext"; import { Colors } from "../styles/Colors"; export interface ItemNotFoundProps { @@ -12,7 +11,7 @@ export function ItemNotFound(props: ItemNotFoundProps) { const { itemType } = props; const { operationState: { colors } - } = useContext(OperationContext); + } = useOperationState(); return ( <> {`${itemType} Not Found`} diff --git a/Src/WitsmlExplorer.Frontend/routes/PageNotFound.tsx b/Src/WitsmlExplorer.Frontend/routes/PageNotFound.tsx index 11f6f9918..d62dea244 100644 --- a/Src/WitsmlExplorer.Frontend/routes/PageNotFound.tsx +++ b/Src/WitsmlExplorer.Frontend/routes/PageNotFound.tsx @@ -1,12 +1,11 @@ -import { useContext } from "react"; +import { useOperationState } from "hooks/useOperationState"; import styled from "styled-components"; -import OperationContext from "../contexts/operationContext"; import { Colors } from "../styles/Colors"; export function PageNotFound() { const { operationState: { colors } - } = useContext(OperationContext); + } = useOperationState(); return ( <> 404 Not Found diff --git a/Src/WitsmlExplorer.Frontend/routes/Root.tsx b/Src/WitsmlExplorer.Frontend/routes/Root.tsx index 7c380b59c..89839c83e 100644 --- a/Src/WitsmlExplorer.Frontend/routes/Root.tsx +++ b/Src/WitsmlExplorer.Frontend/routes/Root.tsx @@ -1,124 +1,26 @@ import { InteractionType } from "@azure/msal-browser"; import { MsalAuthenticationTemplate, MsalProvider } from "@azure/msal-react"; -import { ThemeProvider } from "@mui/material"; +import ContextMenuPresenter from "components/ContextMenus/ContextMenuPresenter"; import { DesktopAppEventHandler } from "components/DesktopAppEventHandler"; +import { GlobalStylesWrapper } from "components/GlobalStyles"; import { HotKeyHandler } from "components/HotKeyHandler"; +import ModalPresenter from "components/Modals/ModalPresenter"; +import PageLayout from "components/PageLayout"; +import RefreshHandler from "components/RefreshHandler"; +import { Snackbar } from "components/Snackbar"; +import { MuiThemeProvider } from "contexts/MuiThemeProvider"; +import { ConnectedServerProvider } from "contexts/connectedServerContext"; +import { CurveThresholdProvider } from "contexts/curveThresholdContext"; +import { FilterContextProvider } from "contexts/filter"; import { LoggedInUsernamesProvider } from "contexts/loggedInUsernamesContext"; +import { OperationStateProvider } from "contexts/operationStateProvider"; +import { QueryContextProvider } from "contexts/queryContext"; +import { SidebarProvider } from "contexts/sidebarContext"; +import { authRequest, msalEnabled, msalInstance } from "msal/MsalAuthProvider"; import { SnackbarProvider } from "notistack"; -import { useEffect } from "react"; import { isDesktopApp } from "tools/desktopAppHelpers"; -import ContextMenuPresenter from "../components/ContextMenus/ContextMenuPresenter"; -import GlobalStyles from "../components/GlobalStyles"; -import ModalPresenter from "../components/Modals/ModalPresenter"; -import PageLayout from "../components/PageLayout"; -import RefreshHandler from "../components/RefreshHandler"; -import { Snackbar } from "../components/Snackbar"; -import { ConnectedServerProvider } from "../contexts/connectedServerContext"; -import { CurveThresholdProvider } from "../contexts/curveThresholdContext"; -import { FilterContextProvider } from "../contexts/filter"; -import OperationContext from "../contexts/operationContext"; -import { - DateTimeFormat, - DecimalPreference, - SetDateTimeFormatAction, - SetDecimalAction, - SetHotKeysEnabledAction, - SetModeAction, - SetThemeAction, - SetTimeZoneAction, - TimeZone, - UserTheme, - initOperationStateReducer -} from "../contexts/operationStateReducer"; -import OperationType from "../contexts/operationType"; -import { QueryContextProvider } from "../contexts/queryContext"; -import { SidebarProvider } from "../contexts/sidebarContext"; -import { enableDarkModeDebug } from "../debugUtils/darkModeDebug"; -import { - authRequest, - msalEnabled, - msalInstance -} from "../msal/MsalAuthProvider"; -import { dark, light } from "../styles/Colors"; -import { getTheme } from "../styles/material-eds"; -import { - STORAGE_DATETIMEFORMAT_KEY, - STORAGE_DECIMAL_KEY, - STORAGE_HOTKEYS_ENABLED_KEY, - STORAGE_MODE_KEY, - STORAGE_THEME_KEY, - STORAGE_TIMEZONE_KEY, - getLocalStorageItem -} from "../tools/localStorageHelpers"; export default function Root() { - const [operationState, dispatchOperation] = initOperationStateReducer(); - - useEffect(() => { - if (typeof localStorage != "undefined") { - const localStorageTheme = - getLocalStorageItem(STORAGE_THEME_KEY); - if (localStorageTheme) { - const action: SetThemeAction = { - type: OperationType.SetTheme, - payload: localStorageTheme - }; - dispatchOperation(action); - } - const storedTimeZone = - getLocalStorageItem(STORAGE_TIMEZONE_KEY); - if (storedTimeZone) { - const action: SetTimeZoneAction = { - type: OperationType.SetTimeZone, - payload: storedTimeZone - }; - dispatchOperation(action); - } - const storedMode = getLocalStorageItem<"light" | "dark">( - STORAGE_MODE_KEY - ); - if (storedMode) { - const action: SetModeAction = { - type: OperationType.SetMode, - payload: storedMode == "light" ? light : dark - }; - dispatchOperation(action); - } - const storedDateTimeFormat = getLocalStorageItem( - STORAGE_DATETIMEFORMAT_KEY - ); - if (storedDateTimeFormat) { - const action: SetDateTimeFormatAction = { - type: OperationType.SetDateTimeFormat, - payload: storedDateTimeFormat - }; - dispatchOperation(action); - } - const storedDecimals = - getLocalStorageItem(STORAGE_DECIMAL_KEY); - if (storedDecimals) { - const action: SetDecimalAction = { - type: OperationType.SetDecimal, - payload: storedDecimals - }; - dispatchOperation(action); - } - const storedHotKeysEnabled = getLocalStorageItem( - STORAGE_HOTKEYS_ENABLED_KEY - ); - if (storedHotKeysEnabled != null) { - const action: SetHotKeysEnabledAction = { - type: OperationType.SetHotKeysEnabled, - payload: storedHotKeysEnabled - }; - dispatchOperation(action); - } - } - if (import.meta.env.VITE_DARK_MODE_DEBUG) { - return enableDarkModeDebug(dispatchOperation); - } - }, []); - return ( {msalEnabled && ( @@ -127,9 +29,9 @@ export default function Root() { authenticationRequest={authRequest} /> )} - - - + + + @@ -151,8 +53,8 @@ export default function Root() { - - + + ); } From d9565cf3d1e1e25d2b63e1d177c758f5b257d0dc Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Fri, 14 Jun 2024 07:52:31 +0200 Subject: [PATCH 071/124] FIX-2259 Overlay loading status (#2477) --- .../components/ContentView.tsx | 1 + .../ContentViews/CurveValuesView.tsx | 11 +-- .../components/ContentViews/FluidsView.tsx | 31 ++++---- .../ContentViews/LogCurveInfoListView.tsx | 73 +++++++++--------- .../LogCurveInfoListViewUtils.tsx | 2 + .../components/ContentViews/LogsListView.tsx | 71 +++++++++--------- .../components/ContentViews/MudLogView.tsx | 29 ++++---- .../MultiLogsCurveInfoListView.tsx | 74 +++++++++---------- .../ContentViews/ObjectsListView.tsx | 21 +++--- .../ContentViews/TrajectoriesListView.tsx | 2 +- .../ContentViews/TrajectoryView.tsx | 27 ++++--- .../components/ContentViews/TubularView.tsx | 27 ++++--- .../ContentViews/TubularsListView.tsx | 2 +- .../ContentViews/WbGeometriesListView.tsx | 2 +- .../ContentViews/WbGeometryView.tsx | 27 ++++--- .../ContentViews/WellboresListView.tsx | 29 ++++---- .../components/ContentViews/WellsListView.tsx | 41 +++++----- .../components/ProgressSpinner.tsx | 42 ++++++++++- 18 files changed, 273 insertions(+), 239 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/components/ContentView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentView.tsx index 90085322c..19d5a8b24 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentView.tsx @@ -10,5 +10,6 @@ export default function ContentView() { } const ContentPanel = styled.div` + position: relative; height: 100%; `; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx index 74dab7310..6483bdcca 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx @@ -27,7 +27,7 @@ import formatDateString from "components/DateFormatter"; import ConfirmModal from "components/Modals/ConfirmModal"; import { ReportModal } from "components/Modals/ReportModal"; import { ShowLogDataOnServerModal } from "components/Modals/ShowLogDataOnServerModal"; -import ProgressSpinner from "components/ProgressSpinner"; +import { ProgressSpinnerOverlay } from "components/ProgressSpinner"; import { Button } from "components/StyledComponents/Button"; import { useConnectedServer } from "contexts/connectedServerContext"; import { DispatchOperation, UserTheme } from "contexts/operationStateReducer"; @@ -340,7 +340,6 @@ export const CurveValuesView = (): React.ReactElement => { }, [startIndex, endIndex, mnemonics, log]); const refreshData = () => { - setTableData([]); setIsLoading(true); setAutoRefresh(false); @@ -566,16 +565,15 @@ export const CurveValuesView = (): React.ReactElement => { ] ); - if (isFetching) { - return ; - } - if (isFetchedLog && !log) { return ; } return ( <> + {(isFetching || isLoading) && ( + + )} { )} - {isLoading && } {!isLoading && !tableData.length && ( No data diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsView.tsx index 1faddc987..b6edd7044 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsView.tsx @@ -8,7 +8,7 @@ import { getContextMenuPosition } from "components/ContextMenus/ContextMenu"; import FluidContextMenu, { FluidContextMenuProps } from "components/ContextMenus/FluidContextMenu"; -import ProgressSpinner from "components/ProgressSpinner"; +import { ProgressSpinnerOverlay } from "components/ProgressSpinner"; import { useConnectedServer } from "contexts/connectedServerContext"; import OperationType from "contexts/operationType"; import { useGetComponents } from "hooks/query/useGetComponents"; @@ -270,24 +270,25 @@ export default function FluidsView() { { property: "vis600Rpm", label: "vis600Rpm", type: ContentType.String } ]; - if (isFetching) { - return ; - } - if (isFetched && !fluidsReport) { return ; } return ( - + <> + {isFetching && ( + + )} + + ); } diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListView.tsx index 1d039ed6c..511a4f8d8 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListView.tsx @@ -4,7 +4,7 @@ import { getContextMenuPosition } from "components/ContextMenus/ContextMenu"; import LogCurveInfoContextMenu, { LogCurveInfoContextMenuProps } from "components/ContextMenus/LogCurveInfoContextMenu"; -import ProgressSpinner from "components/ProgressSpinner"; +import { ProgressSpinnerOverlay } from "components/ProgressSpinner"; import { CommonPanelContainer } from "components/StyledComponents/Container"; import { useConnectedServer } from "contexts/connectedServerContext"; import { useCurveThreshold } from "contexts/curveThresholdContext"; @@ -114,10 +114,6 @@ export default function LogCurveInfoListView() { }); }; - if (isFetching) { - return ; - } - if (isFetchedLog && !logObject) { return ; } @@ -145,37 +141,40 @@ export default function LogCurveInfoListView() { ]; return ( - logObjects && ( - - ) + <> + {isFetching && } + {logObject && ( + + )} + ); } diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListViewUtils.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListViewUtils.tsx index 1c36f9cc6..4145c0045 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListViewUtils.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListViewUtils.tsx @@ -118,6 +118,8 @@ export const getTableData = ( isDepthIndex: boolean, logUid: string = null ) => { + if (!logCurveInfoList) return []; + const isVisibleFunction = (isActive: boolean): (() => boolean) => { return () => { if (isDepthIndex) return true; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogsListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogsListView.tsx index 8eddec227..c5652f06c 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogsListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogsListView.tsx @@ -14,7 +14,7 @@ import { getContextMenuPosition } from "components/ContextMenus/ContextMenu"; import LogObjectContextMenu from "components/ContextMenus/LogObjectContextMenu"; import { ObjectContextMenuProps } from "components/ContextMenus/ObjectMenuItems"; import formatDateString from "components/DateFormatter"; -import ProgressSpinner from "components/ProgressSpinner"; +import { ProgressSpinnerOverlay } from "components/ProgressSpinner"; import { useConnectedServer } from "contexts/connectedServerContext"; import { UserTheme } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; @@ -151,47 +151,46 @@ export default function LogsListView() { navigate(encodeURIComponent(log.uid)); }; - if (isFetching) { - return ; - } - if (isFetchedWellbore && !wellbore) { return ; } return ( - - - - setShowGraph(!showGraph)} - size={theme === UserTheme.Compact ? "small" : "default"} + <> + {isFetching && } + + + + setShowGraph(!showGraph)} + size={theme === UserTheme.Compact ? "small" : "default"} + /> + + Gantt view{selectedRows.length > 0 && " (selected logs only)"} + + + + {showGraph ? ( + 0 ? selectedRows : logs} /> + ) : ( + + setSelectedRows(rows as LogObjectRow[]) + } + checkableRows + showRefresh + initiallySelectedRows={selectedRows} + downloadToCsvFileName={isTimeIndexed ? "TimeLogs" : "DepthLogs"} /> - - Gantt view{selectedRows.length > 0 && " (selected logs only)"} - - - - {showGraph ? ( - 0 ? selectedRows : logs} /> - ) : ( - - setSelectedRows(rows as LogObjectRow[]) - } - checkableRows - showRefresh - initiallySelectedRows={selectedRows} - downloadToCsvFileName={isTimeIndexed ? "TimeLogs" : "DepthLogs"} - /> - )} - + )} + + ); } diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/MudLogView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/MudLogView.tsx index 4a49c9113..964465af8 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/MudLogView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/MudLogView.tsx @@ -8,7 +8,7 @@ import { getContextMenuPosition } from "components/ContextMenus/ContextMenu"; import GeologyIntervalContextMenu, { GeologyIntervalContextMenuProps } from "components/ContextMenus/GeologyIntervalContextMenu"; -import ProgressSpinner from "components/ProgressSpinner"; +import { ProgressSpinnerOverlay } from "components/ProgressSpinner"; import { useConnectedServer } from "contexts/connectedServerContext"; import OperationType from "contexts/operationType"; import { useGetComponents } from "hooks/query/useGetComponents"; @@ -138,25 +138,24 @@ export default function MudLogView() { } ); - if (isFetching) { - return ; - } - if (isFetchedMudLog && !mudLog) { return ; } return ( - + <> + {isFetching && } + + ); } diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/MultiLogsCurveInfoListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/MultiLogsCurveInfoListView.tsx index 7e9c9bd03..05f0ad019 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/MultiLogsCurveInfoListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/MultiLogsCurveInfoListView.tsx @@ -6,7 +6,7 @@ import LogCurveInfoContextMenu, { } from "components/ContextMenus/LogCurveInfoContextMenu"; import { useOperationState } from "hooks/useOperationState"; -import ProgressSpinner from "components/ProgressSpinner"; +import { ProgressSpinnerOverlay } from "components/ProgressSpinner"; import { CommonPanelContainer } from "components/StyledComponents/Container"; import { useConnectedServer } from "contexts/connectedServerContext"; @@ -128,10 +128,6 @@ export default function MultiLogsCurveInfoListView() { }); }; - if (isFetching) { - return ; - } - const panelElements = [ - ) + <> + {isFetching && } + {logObjects && logCurveInfoList && ( + + )} + ); } diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/ObjectsListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/ObjectsListView.tsx index f05a25f8b..68255afc3 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/ObjectsListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/ObjectsListView.tsx @@ -5,7 +5,7 @@ import { useGetObjects } from "../../hooks/query/useGetObjects"; import { useGetWellbore } from "../../hooks/query/useGetWellbore"; import { ObjectType, pluralizeObjectType } from "../../models/objectType"; import { ItemNotFound } from "../../routes/ItemNotFound"; -import ProgressSpinner from "../ProgressSpinner"; +import { ProgressSpinnerOverlay } from "../ProgressSpinner"; import BhaRunsListView from "./BhaRunsListView"; import ChangeLogsListView from "./ChangeLogsListView"; import FluidsReportsListView from "./FluidsReportListView"; @@ -61,21 +61,22 @@ export function ObjectsListView() { objectGroup as ObjectType ); - if (isFetching) { - return ( - - ); - } - if (isFetchedWellbore && !wellbore) { return ( ); } - return getObjectListView(objectGroup); + return ( + <> + {isFetching && ( + + )} + {getObjectListView(objectGroup)} + + ); } const getObjectListView = (objectType: string) => { diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/TrajectoriesListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/TrajectoriesListView.tsx index 16c271836..54cffee01 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/TrajectoriesListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/TrajectoriesListView.tsx @@ -105,7 +105,7 @@ export default function TrajectoriesListView() { navigate(encodeURIComponent(trajectory.uid)); }; - const trajectoryRows = trajectories.map((trajectory) => { + const trajectoryRows = trajectories?.map((trajectory) => { return { ...trajectory, ...trajectory.commonData, diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/TrajectoryView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/TrajectoryView.tsx index b9f4fce21..b36573bef 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/TrajectoryView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/TrajectoryView.tsx @@ -9,7 +9,7 @@ import TrajectoryStationContextMenu, { TrajectoryStationContextMenuProps } from "components/ContextMenus/TrajectoryStationContextMenu"; import formatDateString from "components/DateFormatter"; -import ProgressSpinner from "components/ProgressSpinner"; +import { ProgressSpinnerOverlay } from "components/ProgressSpinner"; import { useConnectedServer } from "contexts/connectedServerContext"; import OperationType from "contexts/operationType"; import { useGetComponents } from "hooks/query/useGetComponents"; @@ -124,23 +124,22 @@ export default function TrajectoryView() { }; }); - if (isFetching) { - return ; - } - if (isFetchedTrajectory && !trajectory) { return ; } return ( - + <> + {isFetching && } + + ); } diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/TubularView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/TubularView.tsx index ae1c8d619..11e543a9b 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/TubularView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/TubularView.tsx @@ -8,7 +8,7 @@ import { getContextMenuPosition } from "components/ContextMenus/ContextMenu"; import TubularComponentContextMenu, { TubularComponentContextMenuProps } from "components/ContextMenus/TubularComponentContextMenu"; -import ProgressSpinner from "components/ProgressSpinner"; +import { ProgressSpinnerOverlay } from "components/ProgressSpinner"; import { useConnectedServer } from "contexts/connectedServerContext"; import OperationType from "contexts/operationType"; import { useGetComponents } from "hooks/query/useGetComponents"; @@ -138,23 +138,22 @@ export default function TubularView() { }; }); - if (isFetching) { - return ; - } - if (isFetchedTubular && !tubular) { return ; } return ( - + <> + {isFetching && } + + ); } diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/TubularsListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/TubularsListView.tsx index 57d7168f5..c01b587b4 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/TubularsListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/TubularsListView.tsx @@ -73,7 +73,7 @@ export default function TubularsListView() { navigate(encodeURIComponent(tubular.uid)); }; - const tubularRows = tubulars.map((tubular) => { + const tubularRows = tubulars?.map((tubular) => { return { ...tubular, dTimCreation: formatDateString( diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/WbGeometriesListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/WbGeometriesListView.tsx index 760496de9..67f9f0c82 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/WbGeometriesListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/WbGeometriesListView.tsx @@ -41,7 +41,7 @@ export default function WbGeometriesListView() { useExpandSidebarNodes(wellUid, wellboreUid, ObjectType.WbGeometry); const getTableData = () => { - return wbGeometries.map((wbGeometry) => { + return wbGeometries?.map((wbGeometry) => { return { ...wbGeometry, mdBottom: measureToString(wbGeometry.mdBottom), diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/WbGeometryView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/WbGeometryView.tsx index 0f3052703..e6d6b5387 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/WbGeometryView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/WbGeometryView.tsx @@ -8,7 +8,7 @@ import { getContextMenuPosition } from "components/ContextMenus/ContextMenu"; import WbGeometrySectionContextMenu, { WbGeometrySectionContextMenuProps } from "components/ContextMenus/WbGeometrySectionContextMenu"; -import ProgressSpinner from "components/ProgressSpinner"; +import { ProgressSpinnerOverlay } from "components/ProgressSpinner"; import { useConnectedServer } from "contexts/connectedServerContext"; import OperationType from "contexts/operationType"; import { useGetComponents } from "hooks/query/useGetComponents"; @@ -120,23 +120,22 @@ export default function WbGeometryView() { }; }); - if (isFetching) { - return ; - } - if (isFetchedWbGeometry && !wbGeometry) { return ; } return ( - + <> + {isFetching && } + + ); } diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboresListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboresListView.tsx index a1a994b5d..34f63e763 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboresListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboresListView.tsx @@ -9,7 +9,7 @@ import WellboreContextMenu, { WellboreContextMenuProps } from "components/ContextMenus/WellboreContextMenu"; import formatDateString from "components/DateFormatter"; -import ProgressSpinner from "components/ProgressSpinner"; +import { ProgressSpinnerOverlay } from "components/ProgressSpinner"; import { useConnectedServer } from "contexts/connectedServerContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; @@ -117,24 +117,23 @@ export default function WellboresListView() { ); }; - if (isFetching) { - return ; - } - if (isFetchedWell && !well) { return ; } return ( - + <> + {isFetching && } + + ); } diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/WellsListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/WellsListView.tsx index cdaec9146..8fdbfc5b9 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/WellsListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/WellsListView.tsx @@ -10,7 +10,7 @@ import WellContextMenu, { WellContextMenuProps } from "components/ContextMenus/WellContextMenu"; import formatDateString from "components/DateFormatter"; -import ProgressSpinner from "components/ProgressSpinner"; +import { ProgressSpinnerOverlay } from "components/ProgressSpinner"; import { useConnectedServer } from "contexts/connectedServerContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; @@ -94,24 +94,25 @@ export default function WellsListView() { }); }; - if (isFetching) { - return ( - - ); - } - - return wells?.length === 0 ? ( - No wells found. - ) : ( - + return ( + <> + {isFetching && ( + + )} + {!isFetching && wells?.length === 0 ? ( + No wells found. + ) : ( + + )} + ); } diff --git a/Src/WitsmlExplorer.Frontend/components/ProgressSpinner.tsx b/Src/WitsmlExplorer.Frontend/components/ProgressSpinner.tsx index 4200bd3fd..c9ff09ced 100644 --- a/Src/WitsmlExplorer.Frontend/components/ProgressSpinner.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ProgressSpinner.tsx @@ -1,6 +1,8 @@ import { CircularProgress, Typography } from "@equinor/eds-core-react"; -import React from "react"; +import OperationContext from "contexts/operationContext"; +import React, { useContext } from "react"; import styled from "styled-components"; +import { Colors, dark } from "styles/Colors"; type Props = { message?: string; @@ -21,6 +23,44 @@ const ProgressSpinner = ({ message }: Props): React.ReactElement => { ); }; +export const ProgressSpinnerOverlay = ({ + message +}: Props): React.ReactElement => { + const { + operationState: { colors } + } = useContext(OperationContext); + return ( + + + + + + ); +}; + +const Overlay = styled.div<{ colors: Colors }>` + position: absolute; + width: 100%; + height: 100%; + background: ${(props) => + props.colors === dark ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.1)"}; + z-index: 999; + display: flex; + justify-content: center; + align-items: center; +`; + +const InnerOverlay = styled.div<{ colors: Colors }>` + background-color: ${(props) => props.colors.ui.backgroundDefault}; + color: ${(props) => props.colors.text.staticIconsDefault}; + display: flex; + flex-direction: column; + align-items: center; + padding: 64px; + box-shadow: 1px 4px 5px rgba(0, 0, 0, 0.1); + border-radius: 4px; +`; + const ProgressLayout = styled.div` display: flex; flex-direction: column; From 0483a27189830751b2d8a07d23c5fb9f2b5b484d Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Fri, 14 Jun 2024 09:51:47 +0200 Subject: [PATCH 072/124] FIX-2444 Open credential modal when navigating back to another server (#2478) --- .../contexts/operationStateReducer.tsx | 10 +++++++++- Src/WitsmlExplorer.Frontend/routes/AuthRoute.tsx | 5 +++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/contexts/operationStateReducer.tsx b/Src/WitsmlExplorer.Frontend/contexts/operationStateReducer.tsx index 4da0ec316..ba1357d21 100644 --- a/Src/WitsmlExplorer.Frontend/contexts/operationStateReducer.tsx +++ b/Src/WitsmlExplorer.Frontend/contexts/operationStateReducer.tsx @@ -1,3 +1,4 @@ +import UserCredentialsModal from "components/Modals/UserCredentialsModal"; import OperationType from "contexts/operationType"; import { Dispatch, ReactElement, useReducer } from "react"; import { Colors, light } from "styles/Colors"; @@ -179,7 +180,14 @@ const displayModal = ( state: OperationState, { payload }: DisplayModalAction ) => { - const modals = state.modals.concat(payload); + const isUserCredentialsModal = payload.type === UserCredentialsModal; + + const modals = isUserCredentialsModal // We don't want to stack login modals + ? state.modals + .filter((modal) => modal.type !== UserCredentialsModal) + .concat(payload) + : state.modals.concat(payload); + return { ...state, contextMenu: EMPTY_CONTEXT_MENU, diff --git a/Src/WitsmlExplorer.Frontend/routes/AuthRoute.tsx b/Src/WitsmlExplorer.Frontend/routes/AuthRoute.tsx index 3039168c1..19bf483b1 100644 --- a/Src/WitsmlExplorer.Frontend/routes/AuthRoute.tsx +++ b/Src/WitsmlExplorer.Frontend/routes/AuthRoute.tsx @@ -59,11 +59,12 @@ export default function AuthRoute() { }, []); useEffect(() => { - if (servers && !connectedServer) { + if (servers && (!connectedServer || connectedServer.url != serverUrl)) { + setConnectedServer(null); const server = servers.find((server) => server.url === serverUrl); if (server) showCredentialsModal(server, true); } - }, [servers]); + }, [servers, serverUrl]); const showCredentialsModal = (server: Server, initialLogin: boolean) => { const userCredentialsModalProps: UserCredentialsModalProps = { From 0fada9db78aa5407f5cbf7569d4eef7610ae11bb Mon Sep 17 00:00:00 2001 From: Robert Basti Date: Mon, 17 Jun 2024 14:54:24 +0200 Subject: [PATCH 073/124] =?UTF-8?q?Missing=20Data=20Agent=20-=20can't=20al?= =?UTF-8?q?ways=20see=20the=20whole=20"select=20object"=20and=20=E2=80=9Cs?= =?UTF-8?q?elect=20properties=E2=80=9D=20list=20#2267=20(#2479)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/Modals/MissingDataAgentModal.tsx | 4 ++++ .../components/Modals/ModalDialog.tsx | 15 ++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/MissingDataAgentModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/MissingDataAgentModal.tsx index b38c10a21..0c0f04c88 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/MissingDataAgentModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/MissingDataAgentModal.tsx @@ -236,6 +236,8 @@ const MissingDataAgentModal = ( confirmText={`OK`} showCancelButton={true} width={ModalWidth.MEDIUM} + height="800px" + minHeight="650px" isLoading={false} content={ @@ -261,6 +263,7 @@ const MissingDataAgentModal = ( {missingDataChecks.map((missingDataCheck) => ( { @@ -41,10 +43,12 @@ const ModalDialog = (props: ModalDialogProps): React.ReactElement => { errorMessage, switchButtonPlaces, width = ModalWidth.MEDIUM, + height, showConfirmButton = true, showCancelButton = true, buttonPosition: ButtonPosition = ControlButtonPosition.BOTTOM, - cancelText + cancelText, + minHeight } = props; const context = useOperationState(); const [confirmButtonIsFocused, setConfirmButtonIsFocused] = @@ -158,6 +162,7 @@ const ModalDialog = (props: ModalDialogProps): React.ReactElement => { ); const dialogStyle = { width: width, + height: height, background: colors.ui.backgroundDefault, color: colors.text.staticIconsDefault }; @@ -165,7 +170,7 @@ const ModalDialog = (props: ModalDialogProps): React.ReactElement => { return ( {ButtonPosition == ControlButtonPosition.TOP ? top : header} - + {content} {errorMessage && {errorMessage}} @@ -196,9 +201,13 @@ export enum ControlButtonPosition { BOTTOM = "bottom" } -const Content = styled(Dialog.CustomContent)<{ colors: Colors }>` +const Content = styled(Dialog.CustomContent)<{ + colors: Colors; + minHeight: string; +}>` margin-top: 0.5em; max-height: 75vh; + min-height: ${(props) => (props.minHeight ? props.minHeight : "")}; overflow-y: auto; background-color: ${(props) => props.colors.ui.backgroundDefault}; color: ${(props) => props.colors.text.staticIconsDefault}; From 1a343922e3824274c6b14986f9abfc08c8491c01 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Mon, 17 Jun 2024 14:58:35 +0200 Subject: [PATCH 074/124] FIX-2234 Update server setup documentation (#2471) --- Docker/README.md | 2 +- Docker/Server/README.md | 38 +++++++++++++++++++++++++------- Docker/Server/docker-compose.yml | 20 +++++++++++++++++ Docs/AUTH.md | 2 +- Docs/OAUTH.md | 2 +- 5 files changed, 53 insertions(+), 11 deletions(-) diff --git a/Docker/README.md b/Docker/README.md index aa52ed4c9..5bb69ee2c 100644 --- a/Docker/README.md +++ b/Docker/README.md @@ -10,7 +10,7 @@ See [setting up a database in Azure](../Scripts/Azure) Build dockerfiles for frontend and backend (see [build_docker_images.sh](../build_docker_images.sh), [Dockerfile-api](../Dockerfile-api) and [Dockerfile-frontend](../Dockerfile-frontend)). -### **Build api and backend images** +### **Build api and frontend images** ```sh ❯ docker build -t witsmlexplorer-api:latest -f Dockerfile-api . Building witsmlexplorer-api... diff --git a/Docker/Server/README.md b/Docker/Server/README.md index 635c1dcd3..782ec8f7a 100644 --- a/Docker/Server/README.md +++ b/Docker/Server/README.md @@ -1,19 +1,41 @@ # Deploy on server +## Configure application + +Configure your application according to [this guide](../README.md#deploy-and-run-witsml-explorer) + +## OAUTH + +If you are planning on using OAUTH with Azure, see [OAUTH.md](../../Docs/OAUTH.md) + +## Configure docker-compose.yml + +You will find a [docker-compose-yml](./docker-compose.yml) that is similar to the one used for running docker-compose locally. The difference is that `web` and `api` containers do not expose their ports outside of docker, and that an nginx instance is running in front forwarding the requests. + +In `docker-compose.yml` ensure that the volumes that are mapped into the containers for nginx from the host OS have the correct host path. + +## Configure nginx Running on a server requires running an nginx in front of the api and the web containers. This is needed for configuring SSL. -You will find a [docker-compose-yml](./docker-compose-yml) that is similar to the one used for running docker-compose locally. +The volume `/etc/nginx` requires both an [`nginx.conf`](nginx.conf) file, and a `certs` directory. Here you need to place the certificate and key files. Ensure that the naming corresponds to what you configure in the `nginx.conf` file. + +## Build docker images -The difference is that `web` and `api` containers do not expose their ports outside of docker, and that an nginx instance is running in front forwarding the requests. +Follow [Run locally with docker](../README.md#build-api-and-frontend-images) to build the docker images. You can either clone the application and build the images locally on the server, or create a workflow to build the images and then push them to a container registry. Make sure you use the same name in the building process and in docker-compose.yml. -## docker-compose.yml and nginx.conf -You might need to do some changes to make this run on a server. -* In `docker-compose.yml` ensure that the volumes that are mapped into the containers for nginx from the host OS have the correct host path. -* The volume `/etc/nginx` requires both an `nginx.conf` file which you can find next to the `docker-compose.yml` file, and also a `certs` directory. Here you need to place the certificate and key files. Ensure that the naming corresponds to what you configure in the `nginx.conf` file. +## Start docker containers When the configuration is in place, run the following to pull fresh images and run them: -* `docker-compose pull` -* `docker-compose up`. Add `-d` if you want to run the application in the background +* `docker-compose pull`. +* `docker-compose up -d`. `-d` makes the application run in the background. * Go to `https://yourserver` to open the application * Use `docker-compose down` to stop the application + +## Update your application + +Before you update your application, make sure that no jobs are running. + +To update your application, build and pull your new containers, and re-run `docker-compose up -d`. + +Run `docker image prune --force --filter dangling=true` to remove dangling images. diff --git a/Docker/Server/docker-compose.yml b/Docker/Server/docker-compose.yml index ccfb5521a..952adef1f 100644 --- a/Docker/Server/docker-compose.yml +++ b/Docker/Server/docker-compose.yml @@ -1,17 +1,37 @@ version: '3.7' services: + + mongo: # Remove this if you are not using mongodb. + image: mongo:4.4.1 + container_name: witsmlexplorer-mongodb + restart: unless-stopped + ports: + - 27017:27017 + volumes: + - ./data:/data/db + environment: + - MONGO_INITDB_ROOT_USERNAME= # Configure this + - MONGO_INITDB_ROOT_PASSWORD= # Configure this + api: image: witsmlexplorer-api:latest + restart: unless-stopped container_name: witsmlexplorer-api volumes: - ./config.json:/app/config.json:ro #May be changed + depends_on: # Remove this if you are not using mongodb. + - mongo + links: # Remove this if you are not using mongodb. + - mongo:mongo web: image: witsmlexplorer-frontend:latest + restart: unless-stopped container_name: witsmlexplorer-frontend nginx: image: nginx:1.21-alpine + restart: unless-stopped container_name: witsmlexplorer-nginx ports: - 80:80 diff --git a/Docs/AUTH.md b/Docs/AUTH.md index d0fffc4c3..c140cfdb9 100644 --- a/Docs/AUTH.md +++ b/Docs/AUTH.md @@ -18,4 +18,4 @@ Accessing the API without a frontend is explained at the end of each of the docu Basic mode uses a session cookie to keep track of the user. This mode is used by setting `"OAuth2Enabled": false` in the API configuration and empty `VITE_MSALENABLED=` in the frontend environment. Basic flow is explained in [BASIC_AUTH.md](./BASIC_AUTH.md). -OAuth mode uses JWT authentication to keep track of the user. This mode is used by setting `"OAuth2Enabled": true` in the API configuration and `VITE_MSALENABLED=true` in the frontend environment. OAUTH flow is explained in [OAUTH.md](./OAUTH.md). +OAuth mode uses JWT authentication to keep track of the user. This mode is used by setting `"OAuth2Enabled": true` in the API configuration and `VITE_MSALENABLED=true` in the frontend environment. Current OAUTH implementation only supports Azure Entra ID. OAUTH flow is explained in [OAUTH.md](./OAUTH.md). diff --git a/Docs/OAUTH.md b/Docs/OAUTH.md index 123ac6110..c6126467b 100644 --- a/Docs/OAUTH.md +++ b/Docs/OAUTH.md @@ -30,7 +30,7 @@ Example `mysettings.json` file for API: } ``` -Required environment variables in frontend: +Required environment variables in frontend [Dockerfile](../Dockerfile-frontend). ```bash # To disable MSAL, leave VITE_MSALENABLED empty From a9aa7bb9b26b7057ef4cc14f83fd701a5fc4d513 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Wed, 19 Jun 2024 10:36:40 +0200 Subject: [PATCH 075/124] FIX-2482 Format dTim for fluidsReports (#2483) --- .../components/ContentViews/FluidsReportListView.tsx | 3 ++- .../components/ContentViews/FluidsView.tsx | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsReportListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsReportListView.tsx index 75be91593..407266f8b 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsReportListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsReportListView.tsx @@ -49,6 +49,7 @@ export default function FluidsReportsListView() { fluidsReport: fluidsReport, md: measureToString(fluidsReport.md), tvd: measureToString(fluidsReport.tvd), + dTim: formatDateString(fluidsReport.dTim, timeZone, dateTimeFormat), dTimCreation: formatDateString( fluidsReport.commonData.dTimCreation, timeZone, @@ -65,7 +66,7 @@ export default function FluidsReportsListView() { const columns: ContentTableColumn[] = [ { property: "name", label: "name", type: ContentType.String }, - { property: "dTim", label: "dTim", type: ContentType.String }, + { property: "dTim", label: "dTim", type: ContentType.DateTime }, { property: "md", label: "md", type: ContentType.Measure }, { property: "tvd", label: "tvd", type: ContentType.Measure }, { property: "numReport", label: "numReport", type: ContentType.String }, diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsView.tsx index b6edd7044..813849d58 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsView.tsx @@ -8,6 +8,7 @@ import { getContextMenuPosition } from "components/ContextMenus/ContextMenu"; import FluidContextMenu, { FluidContextMenuProps } from "components/ContextMenus/FluidContextMenu"; +import formatDateString from "components/DateFormatter"; import { ProgressSpinnerOverlay } from "components/ProgressSpinner"; import { useConnectedServer } from "contexts/connectedServerContext"; import OperationType from "contexts/operationType"; @@ -32,7 +33,10 @@ interface FluidsRow extends ContentTableRow, FluidAsStrings { } export default function FluidsView() { - const { dispatchOperation } = useOperationState(); + const { + operationState: { timeZone, dateTimeFormat }, + dispatchOperation + } = useOperationState(); const { wellUid, wellboreUid, objectUid } = useParams(); const { connectedServer } = useConnectedServer(); const { object: fluidsReport } = useGetObject( @@ -85,7 +89,7 @@ export default function FluidsView() { label: "locationSample", type: ContentType.String }, - { property: "dTim", label: "dTim", type: ContentType.String }, + { property: "dTim", label: "dTim", type: ContentType.DateTime }, { property: "md", label: "md", type: ContentType.Measure }, { property: "tvd", label: "tvd", type: ContentType.Measure }, { @@ -189,7 +193,7 @@ export default function FluidsView() { uid: fluid.uid, type: fluid.type, locationSample: fluid.locationSample, - dTim: fluid.dTim, + dTim: formatDateString(fluid.dTim, timeZone, dateTimeFormat), md: measureToString(fluid.md), tvd: measureToString(fluid.tvd), presBopRating: measureToString(fluid.presBopRating), From d665570d53280225f664a5b6f9c0e7bd4bafb590 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Wed, 19 Jun 2024 13:02:16 +0200 Subject: [PATCH 076/124] FIX-2094 Plot Improvements (#2480) --- .../ContentViews/CurveDataTransformation.ts | 126 +++++++++ .../ContentViews/CurveValuesPlot.tsx | 242 ++++++++++++++---- 2 files changed, 319 insertions(+), 49 deletions(-) create mode 100644 Src/WitsmlExplorer.Frontend/components/ContentViews/CurveDataTransformation.ts diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveDataTransformation.ts b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveDataTransformation.ts new file mode 100644 index 000000000..ada046c29 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveDataTransformation.ts @@ -0,0 +1,126 @@ +import { ExportableContentTableColumn } from "components/ContentViews/table"; +import { CurveSpecification } from "models/logData"; + +const calculateMean = (arr: number[]): number => + arr.reduce((a, b) => a + b, 0) / arr.length; + +const calculateStdDev = (arr: number[], mean: number): number => { + const variance = + arr.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / arr.length; + return Math.sqrt(variance); +}; + +const calculateZScore = (value: number, mean: number, stdDev: number): number => + (value - mean) / stdDev; + +interface ZScoreThreshold { + global: number; + local: number; + windowSize: number; +} + +export enum ThresholdLevel { + Low = "low", + Medium = "medium", + High = "high" +} + +const zScoreThresholds: Record = { + [ThresholdLevel.Low]: { + global: 2, + local: 2.5, + windowSize: 12 + }, + [ThresholdLevel.Medium]: { + global: 1.5, + local: 1.5, + windowSize: 20 + }, + [ThresholdLevel.High]: { + global: 0.7, + local: 0.5, + windowSize: 28 + } +}; + +/** + * Removes outliers from the provided data based on Z-score thresholds applied globally and locally within a moving window. + * + * @param {any[]} data - The dataset to process, where each element is an object containing various numeric properties. + * @param {ExportableContentTableColumn[]} columns - The columns specification, where the first column represents the index curve. + * @param {ZScoreThreshold} zScoreThreshold - An object containing `global` and `local` thresholds and 'windowSize' size for Z-score calculations. + * - `global`: The Z-score threshold to identify potential outliers globally across the entire dataset. + * - `local`: The Z-score threshold to identify outliers within the moving window. + * - `windowSize`: The size of the moving window. + * @returns {any[]} The transformed dataset with outliers removed. + */ +export const removeCurveDataOutliers = ( + data: any[], + columns: ExportableContentTableColumn[], + thresholdLevel: ThresholdLevel +): any[] => { + const transformedData = data.map((dataRow) => ({ ...dataRow })); + const indexCurve = columns[0].columnOf.mnemonic; + const zScoreThreshold = zScoreThresholds[thresholdLevel]; + + columns + .filter((col) => col.columnOf.mnemonic !== indexCurve) + .forEach((col) => { + const mnemonic = col.columnOf.mnemonic; + const originalIndices = data + .map((dataRow, index) => ({ value: dataRow[mnemonic], index })) + .filter( + (dataRow) => dataRow.value !== undefined && dataRow.value !== null + ); + const columnData = originalIndices.map((dataRow) => dataRow.value); + const mean = calculateMean(columnData); + const stdDev = calculateStdDev(columnData, mean); + if (stdDev) { + for (let i = 0; i < columnData.length; i++) { + const originalIndex = originalIndices[i].index; + const zScoreValue = calculateZScore(columnData[i], mean, stdDev); + if (Math.abs(zScoreValue) > zScoreThreshold.global) { + const start = Math.max( + 0, + i - Math.floor(zScoreThreshold.windowSize / 2) + ); + const end = Math.min( + columnData.length, + i + Math.ceil(zScoreThreshold.windowSize / 2) + ); + const window = columnData.slice(start, end); + const windowMean = calculateMean(window); + const windowStdDev = calculateStdDev(window, windowMean); + if (windowStdDev) { + const windowZScoreValue = calculateZScore( + columnData[i], + windowMean, + windowStdDev + ); + if (Math.abs(windowZScoreValue) > zScoreThreshold.local) { + delete transformedData[originalIndex][mnemonic]; + } + } + } + } + } + }); + + return transformedData; +}; + +export const transformCurveData = ( + data: any[], + columns: ExportableContentTableColumn[], + thresholdLevel: ThresholdLevel, + removeOutliers: boolean +) => { + let transformedData = data; + + if (removeOutliers) { + transformedData = removeCurveDataOutliers(data, columns, thresholdLevel); + } + // Other potential transformations should be added here. + + return transformedData; +}; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesPlot.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesPlot.tsx index a16084d24..ceedce8b3 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesPlot.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesPlot.tsx @@ -1,15 +1,33 @@ +import { EdsProvider, Switch, Typography } from "@equinor/eds-core-react"; +import { + ThresholdLevel, + transformCurveData +} from "components/ContentViews/CurveDataTransformation"; import { ContentType, ExportableContentTableColumn } from "components/ContentViews/table"; import formatDateString from "components/DateFormatter"; import { ContentViewDimensionsContext } from "components/PageLayout"; -import { DateTimeFormat, TimeZone } from "contexts/operationStateReducer"; +import { StyledNativeSelect } from "components/Select"; +import { CommonPanelContainer } from "components/StyledComponents/Container"; +import { + DateTimeFormat, + TimeZone, + UserTheme +} from "contexts/operationStateReducer"; import { ECharts } from "echarts"; import ReactEcharts from "echarts-for-react"; import { useOperationState } from "hooks/useOperationState"; import { CurveSpecification } from "models/logData"; -import React, { useContext, useEffect, useRef, useState } from "react"; +import React, { + ChangeEvent, + useContext, + useEffect, + useMemo, + useRef, + useState +} from "react"; import { useParams } from "react-router-dom"; import { RouterLogType } from "routes/routerConstants"; import { Colors } from "styles/Colors"; @@ -17,6 +35,7 @@ import { Colors } from "styles/Colors"; const COLUMN_WIDTH = 135; const MNEMONIC_LABEL_WIDTH = COLUMN_WIDTH - 10; const TOOLTIP_OFFSET_Y = 30; +const GAP_DISTANCE = 3; interface ControlledTooltipProps { visible: boolean; @@ -45,8 +64,12 @@ export const CurveValuesPlot = React.memo( (col, index) => col.type === ContentType.Number || index === 0 ); const { - operationState: { colors, dateTimeFormat } + operationState: { colors, dateTimeFormat, theme } } = useOperationState(); + const [enableScatter, setEnableScatter] = useState(false); + const [removeOutliers, setRemoveOutliers] = useState(false); + const [outliersThresholdLevel, setOutliersThresholdLevel] = + useState(ThresholdLevel.Medium); const chart = useRef(null); const selectedLabels = useRef>(null); const scrollIndex = useRef(0); @@ -65,6 +88,16 @@ export const CurveValuesPlot = React.memo( useState({ visible: false } as ControlledTooltipProps); + const transformedData = useMemo( + () => + transformCurveData( + data, + columns, + outliersThresholdLevel, + !autoRefresh && removeOutliers + ), + [data, columns, outliersThresholdLevel, removeOutliers, autoRefresh] + ); useEffect(() => { if (contentViewWidth) { @@ -78,7 +111,7 @@ export const CurveValuesPlot = React.memo( }, [contentViewWidth]); const chartOption = getChartOption( - data, + transformedData, columns, name, colors, @@ -90,7 +123,8 @@ export const CurveValuesPlot = React.memo( scrollIndex.current, horizontalZoom.current, verticalZoom.current, - isTimeLog + isTimeLog, + enableScatter ); const onMouseOver = (e: any) => { @@ -183,44 +217,99 @@ export const CurveValuesPlot = React.memo( }; return ( -
- (chart.current = c)} - style={{ - height: "100%", - minWidth: `${width}px`, - maxWidth: `${width}px` - }} - /> +
+ + + setEnableScatter(!enableScatter)} + size={theme === UserTheme.Compact ? "small" : "default"} + /> + + Scatter Plot + + {!autoRefresh && ( + <> + setRemoveOutliers(!removeOutliers)} + size={theme === UserTheme.Compact ? "small" : "default"} + /> + + Hide Outliers + + {removeOutliers && ( + <> + + Sensitivity: + + ) => + setOutliersThresholdLevel( + event.target.value as ThresholdLevel + ) + } + value={outliersThresholdLevel} + colors={colors} + > + {Object.values(ThresholdLevel).map((value) => { + return ( + + ); + })} + + + )} + + )} + +
- {controlledTooltip.content} + (chart.current = c)} + style={{ + height: "100%", + minWidth: `${width}px`, + maxWidth: `${width}px` + }} + /> +
+ {controlledTooltip.content} +
); @@ -241,7 +330,8 @@ const getChartOption = ( scrollIndex: number, horizontalZoom: [number, number], verticalZoom: [number, number], - isTimeLog: boolean + isTimeLog: boolean, + enableScatter: boolean ) => { const VALUE_OFFSET_FROM_COLUMN = 0.01; const AUTO_REFRESH_SIZE = 300; @@ -405,7 +495,7 @@ const getChartOption = ( start: verticalZoom[0], end: verticalZoom[1], orient: "vertical", - filterMode: "empty", + filterMode: enableScatter ? "empty" : "none", type: "slider", labelFormatter: () => "" }, @@ -437,14 +527,23 @@ const getChartOption = ( const minMaxValue = minMaxValues.find( (v) => v.curve == col.columnOf.mnemonic ); - return { - large: true, - symbolSize: data.length > 200 ? 2 : 5, - name: col.label, - type: "scatter", - data: data.map((row) => { + + const offsetData = data + .map((row, rowIndex) => { const index = row[indexCurve]; const value = row[col.columnOf.mnemonic]; + const isSmallGap = + !enableScatter && + value === undefined && + hasDataWithinRange( + data, + rowIndex, + col.columnOf.mnemonic, + GAP_DISTANCE + ); + if (isSmallGap) { + return null; // Return null and filter it away later to draw lines over small gaps. + } const normalizedValue = (value - minMaxValue.minValue) / (minMaxValue.maxValue - minMaxValue.minValue || 1); @@ -454,11 +553,56 @@ const getChartOption = ( i; return [offsetNormalizedValue, index]; }) + .filter((r) => r !== null); + + return { + large: true, + connectNulls: false, + symbolSize: enableScatter + ? data.length > 200 + ? 2 + : 5 + : (value: any, params: any) => { + const isIsolated = + value !== undefined && + isNaN(offsetData[Math.max(0, params.dataIndex - 1)][0]) && + isNaN( + offsetData[ + Math.min(offsetData.length - 1, params.dataIndex + 1) + ][0] + ); + return isIsolated ? 1 : 0; // Only isolated data points should have a symbol for the line plot + }, + emphasis: { + disabled: true + }, + name: col.label, + type: enableScatter ? "scatter" : "line", + showSymbol: true, + data: offsetData }; }) }; }; +const hasDataWithinRange = ( + data: any[], + valueIndex: number, + mnemonic: string, + distanceFromIndex: number +): boolean => { + const start = Math.max(0, valueIndex - distanceFromIndex); + const end = Math.min(data.length, valueIndex + distanceFromIndex); + const window = [ + ...data.slice(start, valueIndex), + ...data.slice(valueIndex + 1, end + 1) + ]; + if (window.some((d) => d[mnemonic] !== undefined)) { + return true; + } + return false; +}; + const timeFormatter = (params: number, dateTimeFormat: DateTimeFormat) => { const dateTime = new Date(Math.round(params)); return formatDateString( From edce3f62d449070e8e5e7befa3520a49d828c36c Mon Sep 17 00:00:00 2001 From: matusmlichsk <61700762+matusmlichsk@users.noreply.github.com> Date: Mon, 24 Jun 2024 08:28:33 +0200 Subject: [PATCH 077/124] FIX-2476 Inform user when no active wells are available and inactive are hidden (#2484) --- .../components/Sidebar/FilterPanel.tsx | 5 ++- .../InactiveWellsHiddenFilterHelper.tsx | 26 ++++++++++++ .../InactiveWellsHiddenFilterHelper/index.ts | 1 + .../components/Sidebar/Sidebar.tsx | 41 +++++++++++++------ .../components/StyledComponents/Chip/Chip.tsx | 38 +++++++++++++++++ .../components/StyledComponents/Chip/index.ts | 1 + 6 files changed, 97 insertions(+), 15 deletions(-) create mode 100644 Src/WitsmlExplorer.Frontend/components/Sidebar/InactiveWellsHiddenFilterHelper/InactiveWellsHiddenFilterHelper.tsx create mode 100644 Src/WitsmlExplorer.Frontend/components/Sidebar/InactiveWellsHiddenFilterHelper/index.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/StyledComponents/Chip/Chip.tsx create mode 100644 Src/WitsmlExplorer.Frontend/components/StyledComponents/Chip/index.ts diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/FilterPanel.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/FilterPanel.tsx index 51e533254..e6b30be61 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/FilterPanel.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/FilterPanel.tsx @@ -16,12 +16,12 @@ import React, { ChangeEvent, useContext } from "react"; import styled from "styled-components"; import { Colors } from "styles/Colors"; import { + setLocalStorageItem, STORAGE_FILTER_HIDDENOBJECTS_KEY, STORAGE_FILTER_INACTIVE_TIME_CURVES_KEY, STORAGE_FILTER_INACTIVE_TIME_CURVES_VALUE_KEY, STORAGE_FILTER_ISACTIVE_KEY, - STORAGE_FILTER_OBJECTGROWING_KEY, - setLocalStorageItem + STORAGE_FILTER_OBJECTGROWING_KEY } from "tools/localStorageHelpers"; const FilterPanel = (): React.ReactElement => { @@ -242,6 +242,7 @@ const StyledTextField = styled(TextField)<{ colors: Colors }>` label { color: ${(props) => props.colors.text.staticTextLabel}; } + div { background: ${(props) => props.colors.text.staticTextFieldDefault}; } diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/InactiveWellsHiddenFilterHelper/InactiveWellsHiddenFilterHelper.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/InactiveWellsHiddenFilterHelper/InactiveWellsHiddenFilterHelper.tsx new file mode 100644 index 000000000..d48f620a2 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/InactiveWellsHiddenFilterHelper/InactiveWellsHiddenFilterHelper.tsx @@ -0,0 +1,26 @@ +import React, { FC, useContext } from "react"; +import { FilterContext } from "../../../contexts/filter.tsx"; +import { + setLocalStorageItem, + STORAGE_FILTER_ISACTIVE_KEY +} from "../../../tools/localStorageHelpers.tsx"; +import { Icon } from "@equinor/eds-core-react"; +import { Chip } from "../../StyledComponents/Chip"; + +export const InactiveWellsHiddenFilterHelper: FC = () => { + const { selectedFilter, updateSelectedFilter } = useContext(FilterContext); + + const handleClearFilterProperty = () => { + setLocalStorageItem(STORAGE_FILTER_ISACTIVE_KEY, false); + updateSelectedFilter({ isActive: false }); + }; + + if (!selectedFilter.isActive) return null; + + return ( + + + Inactive Wells are hidden + + ); +}; diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/InactiveWellsHiddenFilterHelper/index.ts b/Src/WitsmlExplorer.Frontend/components/Sidebar/InactiveWellsHiddenFilterHelper/index.ts new file mode 100644 index 000000000..d5c3dc490 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/InactiveWellsHiddenFilterHelper/index.ts @@ -0,0 +1 @@ +export * from "./InactiveWellsHiddenFilterHelper"; diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/Sidebar.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/Sidebar.tsx index b0b833598..fe64e8e4c 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/Sidebar.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/Sidebar.tsx @@ -1,9 +1,9 @@ import { Divider, Typography } from "@equinor/eds-core-react"; import { TreeView } from "@mui/x-tree-view"; import { + useVirtualizer, VirtualItem, - Virtualizer, - useVirtualizer + Virtualizer } from "@tanstack/react-virtual"; import ProgressSpinner from "components/ProgressSpinner"; import SearchFilter from "components/Sidebar/SearchFilter"; @@ -16,11 +16,13 @@ import { useGetWells } from "hooks/query/useGetWells"; import { useOperationState } from "hooks/useOperationState"; import { useWellFilter } from "hooks/useWellFilter"; import Well from "models/well"; -import { Fragment, useEffect, useRef } from "react"; +import { Fragment, SyntheticEvent, useEffect, useRef } from "react"; import { useParams } from "react-router-dom"; import styled from "styled-components"; import Icon from "styles/Icons"; import { WellIndicator } from "../StyledComponents/WellIndicator"; +import { InactiveWellsHiddenFilterHelper } from "./InactiveWellsHiddenFilterHelper"; +import { Stack } from "@mui/material"; export default function Sidebar() { const { connectedServer } = useConnectedServer(); @@ -55,7 +57,7 @@ export default function Sidebar() { } }, [filteredWells]); - const onNodeToggle = (_: React.SyntheticEvent, nodeIds: string[]) => { + const onNodeToggle = (_: SyntheticEvent, nodeIds: string[]) => { if (nodeIds !== expandedTreeNodes) { dispatchSidebar({ type: SidebarActionType.SetTreeNodes, @@ -64,19 +66,29 @@ export default function Sidebar() { } }; + if (isFetching) + return ( + <> + + {!!connectedServer && ( + + + + )} + + ); + return ( {!!connectedServer && ( - {isFetching ? ( - - ) : ( - filteredWells && + {filteredWells && (filteredWells.length === 0 ? ( - - No wells match the current filter - + + No wells match the current filter + + ) : ( - )) - )} + ))} )} @@ -144,13 +155,17 @@ const SidebarTreeView = styled.div` height: 70%; padding-left: 1em; padding-right: 0.3em; + .MuiTreeItem-root { min-width: 0; + .MuiTreeItem-iconContainer { flex: none; } + .MuiTreeItem-label { min-width: 0; + p { white-space: nowrap; overflow: hidden; diff --git a/Src/WitsmlExplorer.Frontend/components/StyledComponents/Chip/Chip.tsx b/Src/WitsmlExplorer.Frontend/components/StyledComponents/Chip/Chip.tsx new file mode 100644 index 000000000..cb32ddbe4 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/StyledComponents/Chip/Chip.tsx @@ -0,0 +1,38 @@ +import React, { forwardRef } from "react"; +import { Chip as EquinorChip, ChipProps } from "@equinor/eds-core-react"; +import styled, { css } from "styled-components"; +import { Colors } from "../../../styles/Colors.tsx"; +import { useOperationState } from "../../../hooks/useOperationState.tsx"; + +type WithTheme = T & { colors: Colors }; + +export const Chip = forwardRef( + ({ variant = "default", ...props }, ref) => { + const { + operationState: { colors } + } = useOperationState(); + + const commonProps = { ref, variant, ...props }; + + switch (variant) { + case "default": + return ; + default: + return ; + } + } +); + +Chip.displayName = "WitsmlExplorerChip"; + +const WitsmlDefaultChip = styled(EquinorChip)` + ${({ colors: { ui, mode, interactive } }) => { + if (mode === "light") return; + + return css` + --eds_ui_background__light: ${ui.backgroundLight}; + --eds_interactive_primary__resting: ${interactive.primaryResting}; + --eds_interactive_primary__hover_alt: ${ui.backgroundDefault}; + `; + }} +`; diff --git a/Src/WitsmlExplorer.Frontend/components/StyledComponents/Chip/index.ts b/Src/WitsmlExplorer.Frontend/components/StyledComponents/Chip/index.ts new file mode 100644 index 000000000..ecfadf0ca --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/StyledComponents/Chip/index.ts @@ -0,0 +1 @@ +export * from "./Chip.tsx"; From d2b6fdf714dbbe2b935939eaad528932fe6c83af Mon Sep 17 00:00:00 2001 From: Robert Basti Date: Tue, 25 Jun 2024 10:26:50 +0200 Subject: [PATCH 078/124] New bump desktop version (#2491) --- Src/WitsmlExplorer.Desktop/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/WitsmlExplorer.Desktop/package.json b/Src/WitsmlExplorer.Desktop/package.json index 76b965bdb..54379940e 100644 --- a/Src/WitsmlExplorer.Desktop/package.json +++ b/Src/WitsmlExplorer.Desktop/package.json @@ -1,7 +1,7 @@ { "name": "WEx-Desktop", "description": "Witsml Explorer Desktop Edition", - "version": "0.2.0", + "version": "0.3.0", "private": true, "author": "Witsml Explorer Team", "repository": "https://github.com/equinor/witsml-explorer", From 91075ef7469fceff9fe8144ef667cad701176010 Mon Sep 17 00:00:00 2001 From: Libor Nikel <140812244+LibNik@users.noreply.github.com> Date: Wed, 26 Jun 2024 08:43:40 +0200 Subject: [PATCH 079/124] Create a new API method for downloading report data in csv format (#2481) --- .../Helpers/ReportHelper.cs | 57 +++++++++++++++++++ .../Models/Reports/BaseReport.cs | 1 + .../Services/LogObjectService.cs | 2 + .../Workers/DownloadAllLogDataWorker.cs | 7 ++- .../components/ContentViews/JobsView.tsx | 11 +--- .../components/Modals/ReportModal.tsx | 9 +-- .../components/ReportCreationHelper.ts | 2 +- .../models/reports/BaseReport.tsx | 7 ++- 8 files changed, 75 insertions(+), 21 deletions(-) create mode 100644 Src/WitsmlExplorer.Api/Helpers/ReportHelper.cs diff --git a/Src/WitsmlExplorer.Api/Helpers/ReportHelper.cs b/Src/WitsmlExplorer.Api/Helpers/ReportHelper.cs new file mode 100644 index 000000000..2dda2def2 --- /dev/null +++ b/Src/WitsmlExplorer.Api/Helpers/ReportHelper.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Linq; + +using WitsmlExplorer.Api.Models; + +namespace WitsmlExplorer.Api.Helpers +{ + /// + /// Content type enumeration + /// + public enum ContentType + { + String, + Number, + DateTime, + Measure, + Component + } + + /// + /// Helper class for generating reports + /// + public class ReportHelper + { + // csv separator character + const char Separator = ','; + // new line character (LineFeed only) + const char NewLineCharacter = '\n'; + + /// + /// Generates log report + /// + /// Collection of report log data + /// Report header string + /// Report header and report body as separate strings + public static (string header, string body) GenerateReport(ICollection> reportItems, string reportHeader) + { + var columns = reportItems.Count > 0 ? + reportItems.First().Keys.Select(key => new + { + Property = key, + Label = key, + Type = ContentType.String + }).ToList() + : []; + + var exportColumns = reportHeader ?? string.Join(Separator, columns.Select(column => column.Property)); + + var data = string.Join(NewLineCharacter, + reportItems.Select(row => + string.Join(Separator, + columns.Select(col => row.TryGetValue(col.Property, out LogDataValue value) ? value.Value.ToString() : string.Empty)))); + + return (exportColumns, data); + } + } +} diff --git a/Src/WitsmlExplorer.Api/Models/Reports/BaseReport.cs b/Src/WitsmlExplorer.Api/Models/Reports/BaseReport.cs index 858bbb03a..590d5c550 100644 --- a/Src/WitsmlExplorer.Api/Models/Reports/BaseReport.cs +++ b/Src/WitsmlExplorer.Api/Models/Reports/BaseReport.cs @@ -11,5 +11,6 @@ public class BaseReport public bool DownloadImmediately { get; init; } = false; public string ReportHeader { get; init; } public string JobDetails { get; init; } + public string ReportBody { get; init; } } } diff --git a/Src/WitsmlExplorer.Api/Services/LogObjectService.cs b/Src/WitsmlExplorer.Api/Services/LogObjectService.cs index d78d69320..71fd31b57 100644 --- a/Src/WitsmlExplorer.Api/Services/LogObjectService.cs +++ b/Src/WitsmlExplorer.Api/Services/LogObjectService.cs @@ -359,6 +359,7 @@ private async Task LoadData(List mnemonics, WitsmlLog log, In WitsmlLogs witsmlLogs = await _witsmlClient.GetFromStoreAsync(query, new OptionsIn(ReturnElements.All)); WitsmlLog witsmlLog = witsmlLogs.Logs?.FirstOrDefault(); + return witsmlLog; } @@ -366,6 +367,7 @@ private async Task LoadDataRecursive(List mnemonics, WitsmlLo { await using LogDataReader logDataReader = new(_witsmlClient, log, new List(mnemonics), null, startIndex, endIndex); WitsmlLogData logData = await logDataReader.GetNextBatch(cancellationToken); + var allLogData = logData; while (logData != null) { diff --git a/Src/WitsmlExplorer.Api/Workers/DownloadAllLogDataWorker.cs b/Src/WitsmlExplorer.Api/Workers/DownloadAllLogDataWorker.cs index 139aa3f5b..df4497c81 100644 --- a/Src/WitsmlExplorer.Api/Workers/DownloadAllLogDataWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/DownloadAllLogDataWorker.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; +using WitsmlExplorer.Api.Helpers; using WitsmlExplorer.Api.Jobs; using WitsmlExplorer.Api.Models; using WitsmlExplorer.Api.Models.Reports; @@ -43,6 +44,7 @@ public DownloadAllLogDataWorker( if (job.JobInfo != null) job.JobInfo.Progress = progress; }); var logData = await _logObjectService.ReadLogData(job.LogReference.WellUid, job.LogReference.WellboreUid, job.LogReference.Uid, job.Mnemonics.ToList(), job.StartIndexIsInclusive, job.LogReference.StartIndex, job.LogReference.EndIndex, true, cancellationToken, progressReporter); + return DownloadAllLogDataResult(job, logData.Data, logData.CurveSpecifications); } @@ -56,6 +58,8 @@ public DownloadAllLogDataWorker( private DownloadAllLogDataReport DownloadAllLogDataReport(ICollection> reportItems, LogObject logReference, string reportHeader) { + var result = ReportHelper.GenerateReport(reportItems, reportHeader); + return new DownloadAllLogDataReport { Title = $"{logReference.WellboreName} - {logReference.Name}", @@ -63,7 +67,8 @@ private DownloadAllLogDataReport DownloadAllLogDataReport(ICollection { const onClickReport = async (jobId: string) => { const report = await JobService.getReport(jobId); if (report.downloadImmediately === true) { - const reportProperties = generateReport( - report.reportItems, - report.reportHeader - ); - exportData( - report.title, - reportProperties.exportColumns, - reportProperties.data - ); + exportData(report.title, report.reportHeader, report.reportBody); } else { const reportModalProps = { report }; dispatchOperation({ diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/ReportModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/ReportModal.tsx index 5daba5a31..d61a473a6 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/ReportModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/ReportModal.tsx @@ -13,7 +13,6 @@ import { import { LabelsLayout } from "components/Modals/ComparisonModalStyles"; import { StyledAccordionHeader } from "components/Modals/LogComparisonModal"; import ModalDialog, { ModalWidth } from "components/Modals/ModalDialog"; -import { generateReport } from "components/ReportCreationHelper"; import { Banner } from "components/StyledComponents/Banner"; import { useConnectedServer } from "contexts/connectedServerContext"; import OperationType from "contexts/operationType"; @@ -226,14 +225,10 @@ export const useGetReportOnJobFinished = (jobId: string): BaseReport => { } else { setReport(report); if (report.downloadImmediately === true) { - const reportProperties = generateReport( - report.reportItems, - report.reportHeader - ); exportData( report.title, - reportProperties.exportColumns, - reportProperties.data + report.reportHeader, + report.reportBody ); } } diff --git a/Src/WitsmlExplorer.Frontend/components/ReportCreationHelper.ts b/Src/WitsmlExplorer.Frontend/components/ReportCreationHelper.ts index f37d5d538..3efbd8876 100644 --- a/Src/WitsmlExplorer.Frontend/components/ReportCreationHelper.ts +++ b/Src/WitsmlExplorer.Frontend/components/ReportCreationHelper.ts @@ -1,5 +1,5 @@ -import { ContentTableColumn, ContentType } from "./ContentViews/table"; import { defaultExportProperties } from "models/exportProperties"; +import { ContentTableColumn, ContentType } from "./ContentViews/table"; export interface ReportProperties { columns: string; diff --git a/Src/WitsmlExplorer.Frontend/models/reports/BaseReport.tsx b/Src/WitsmlExplorer.Frontend/models/reports/BaseReport.tsx index 9d2a3d4ed..4a98f4eb3 100644 --- a/Src/WitsmlExplorer.Frontend/models/reports/BaseReport.tsx +++ b/Src/WitsmlExplorer.Frontend/models/reports/BaseReport.tsx @@ -6,6 +6,7 @@ export default interface BaseReport { downloadImmediately?: boolean; reportHeader?: string; jobDetails?: string; + reportBody?: string; } export const createReport = ( @@ -15,7 +16,8 @@ export const createReport = ( warningMessage: string = null, downloadImmediately: boolean = null, reportHeader: string = null, - jobDetails: string = null + jobDetails: string = null, + reportBody: string = null ): BaseReport => { return { title, @@ -24,6 +26,7 @@ export const createReport = ( warningMessage, downloadImmediately, reportHeader, - jobDetails + jobDetails, + reportBody }; }; From bec70ac61f3b7e342bdddd2e7984dcdb43597197 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Wed, 26 Jun 2024 09:26:14 +0200 Subject: [PATCH 080/124] FIX-2493 requestObjectSelectionCapability workaround (#2495) --- .../Services/WellService.cs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/Src/WitsmlExplorer.Api/Services/WellService.cs b/Src/WitsmlExplorer.Api/Services/WellService.cs index cf58941d0..5d695a284 100644 --- a/Src/WitsmlExplorer.Api/Services/WellService.cs +++ b/Src/WitsmlExplorer.Api/Services/WellService.cs @@ -73,13 +73,21 @@ public async Task> GetWells() private async Task CanQueryByIsActive() { // send a request to see if the server is capable of querying by IsActive - WitsmlWellbores capabilityQuery = new WitsmlWellbores + try { - Wellbores = new WitsmlWellbore().AsItemInList() - }; - WitsmlWellbores capabilityResult = await _witsmlClient.GetFromStoreNullableAsync(capabilityQuery, new OptionsIn(RequestObjectSelectionCapability: true)); - WitsmlWellbore capabilities = capabilityResult?.Wellbores?.FirstOrDefault(); - return capabilities?.IsActive != null; + WitsmlWellbores capabilityQuery = new WitsmlWellbores + { + Wellbores = new WitsmlWellbore().AsItemInList() + }; + WitsmlWellbores capabilityResult = await _witsmlClient.GetFromStoreNullableAsync(capabilityQuery, new OptionsIn(RequestObjectSelectionCapability: true)); + WitsmlWellbore capabilities = capabilityResult?.Wellbores?.FirstOrDefault(); + return capabilities?.IsActive != null; + } + catch (Exception) + { + // The try/catch is used as a workaround for the servers that can't handle RequestObjectSelectionCapability. + return false; + } } private async Task SetWellIsActive(Well well) // Sets the IsActive property of the well to true if any of its wellbores are active From 5f376c00d158f5121f7406c632843450f5c1ade7 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Wed, 26 Jun 2024 13:16:59 +0200 Subject: [PATCH 081/124] FIX-2487 Add job progress for copying log curves (#2497) --- .../Workers/Copy/CopyComponentsWorker.cs | 5 +++++ .../Workers/ReplaceComponentsWorker.cs | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/Src/WitsmlExplorer.Api/Workers/Copy/CopyComponentsWorker.cs b/Src/WitsmlExplorer.Api/Workers/Copy/CopyComponentsWorker.cs index 9dc09ac48..3c31b8bcc 100644 --- a/Src/WitsmlExplorer.Api/Workers/Copy/CopyComponentsWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/Copy/CopyComponentsWorker.cs @@ -47,6 +47,11 @@ public CopyComponentsWorker(ILogger logger, IWitsmlClientProv { Source = job.Source, Target = job.Target, + ProgressReporter = new Progress(progress => + { + job.ProgressReporter?.Report(progress); + if (job.JobInfo != null) job.JobInfo.Progress = progress; + }) }, cancellationToken); } diff --git a/Src/WitsmlExplorer.Api/Workers/ReplaceComponentsWorker.cs b/Src/WitsmlExplorer.Api/Workers/ReplaceComponentsWorker.cs index 321412a9b..549c97781 100644 --- a/Src/WitsmlExplorer.Api/Workers/ReplaceComponentsWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/ReplaceComponentsWorker.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -40,6 +41,14 @@ public ReplaceComponentsWorker(ILogger logger, ICopyCompon { return result; } + + job.CopyJob.ProgressReporter = new Progress(progress => + { + job.ProgressReporter?.Report(progress); + if (job.JobInfo != null) job.JobInfo.Progress = progress; + } + ); + var copyResult = await _copyWorker.Execute(job.CopyJob, cancellationToken); replaceComponentReportItems.Add(new CommonCopyReportItem { From 38c43e0c8b31d815ebea48a93e4658c11768578d Mon Sep 17 00:00:00 2001 From: Robert Basti Date: Fri, 2 Aug 2024 14:51:53 +0200 Subject: [PATCH 082/124] maxdatapoints (#2499) --- Src/Witsml/CommonConstants.cs | 10 ++++ Src/WitsmlExplorer.Api/Workers/BaseWorker.cs | 37 +++++++++++++- .../Workers/Copy/CopyLogDataWorker.cs | 25 +++++---- .../Workers/ImportLogDataWorker.cs | 51 ++++++++----------- .../Workers/OffsetLogCurveWorker.cs | 31 +++-------- .../Workers/SpliceLogsWorker.cs | 33 +++--------- .../Workers/Tools/LogWorkerTools.cs | 23 +++++++++ .../Workers/CopyLogDataWorkerTests.cs | 2 + .../Workers/ImportLogDataWorkerTests.cs | 1 + .../Workers/LogUtils.cs | 31 +++++++++++ .../Workers/OffsetLogCurveWorkerTests.cs | 1 + .../Workers/SpliceLogsWorkerTests.cs | 1 + 12 files changed, 152 insertions(+), 94 deletions(-) diff --git a/Src/Witsml/CommonConstants.cs b/Src/Witsml/CommonConstants.cs index 3b2e56902..bd844de20 100644 --- a/Src/Witsml/CommonConstants.cs +++ b/Src/Witsml/CommonConstants.cs @@ -40,4 +40,14 @@ public static class Unit public const string Feet = "ft"; public const string Second = "s"; } + + public static class WitsmlQueryTypeName + { + public const string Log = "log"; + } + + public static class WitsmlFunctionType + { + public const string WMLSUpdateInStore = "WMLS_UpdateInStore"; + }; } diff --git a/Src/WitsmlExplorer.Api/Workers/BaseWorker.cs b/Src/WitsmlExplorer.Api/Workers/BaseWorker.cs index e2c125752..b473fde18 100644 --- a/Src/WitsmlExplorer.Api/Workers/BaseWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/BaseWorker.cs @@ -1,13 +1,16 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Witsml; +using Witsml.Data; +using Witsml.Extensions; using WitsmlExplorer.Api.Extensions; using WitsmlExplorer.Api.Jobs; @@ -21,19 +24,24 @@ public abstract class BaseWorker where T : Job protected ILogger Logger { get; } private IWitsmlClientProvider WitsmlClientProvider { get; } + private WitsmlCapServers _targetServerCapabilities; + public BaseWorker(ILogger logger = null) { Logger = logger; } + public BaseWorker(IWitsmlClientProvider witsmlClientProvider, ILogger logger = null) { Logger = logger; WitsmlClientProvider = witsmlClientProvider; } + protected IWitsmlClient GetTargetWitsmlClientOrThrow() { return WitsmlClientProvider.GetClient() ?? throw new WitsmlClientProviderException($"Missing Target WitsmlClient for {typeof(T)}", (int)HttpStatusCode.Unauthorized, ServerType.Target); } + protected IWitsmlClient GetSourceWitsmlClientOrThrow() { return WitsmlClientProvider.GetSourceClient() ?? throw new WitsmlClientProviderException($"Missing Source WitsmlClient for {typeof(T)}", (int)HttpStatusCode.Unauthorized, ServerType.Source); @@ -59,6 +67,33 @@ protected string CancellationReason() { return "The job was cancelled by the user."; } + protected async Task GetTargetServerCapabilities() + { + if (_targetServerCapabilities == null) + { + _targetServerCapabilities = await GetTargetWitsmlClientOrThrow().GetCap(); + } + return _targetServerCapabilities; + } + + protected async Task GetMaxBatchSize(int objectsCount, string functionType, string queryTypeName) + { + var targetServerCapabilities = await GetTargetServerCapabilities(); + var serverCapabilites = + targetServerCapabilities.ServerCapabilities; + + var functions = serverCapabilites.Select(x => x.Functions.Find(y => y.Name.Equals(functionType))); + var objectCapabilities = functions.Select(x => + x.DataObjects.Find(y => y.Name.Equals(queryTypeName))); + + var maxDataRows = objectCapabilities.FirstOrDefault().MaxDataNodes; + var maxDataPoints = objectCapabilities.FirstOrDefault().MaxDataPoints; + + var maxBatchSize = + Math.Min(maxDataRows, maxDataPoints / objectsCount); + return maxBatchSize; + } + public async Task<(Task<(WorkerResult, RefreshAction)>, Job)> SetupWorker(Stream jobStream, CancellationToken? cancellationToken = null) { T job = await jobStream.Deserialize(); diff --git a/Src/WitsmlExplorer.Api/Workers/Copy/CopyLogDataWorker.cs b/Src/WitsmlExplorer.Api/Workers/Copy/CopyLogDataWorker.cs index c2dfe1dee..a0bce1065 100644 --- a/Src/WitsmlExplorer.Api/Workers/Copy/CopyLogDataWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/Copy/CopyLogDataWorker.cs @@ -175,25 +175,28 @@ private async Task CopyLogData(WitsmlLog sourceLog, WitsmlLog target await using LogDataReader logDataReader = new(GetSourceWitsmlClientOrThrow(), sourceLog, mnemonics, Logger); WitsmlLogData sourceLogData = await logDataReader.GetNextBatch(); + var chunkMaxSize = await GetMaxBatchSize(mnemonics.Count, CommonConstants.WitsmlFunctionType.WMLSUpdateInStore, CommonConstants.WitsmlQueryTypeName.Log); + while (sourceLogData != null) { + var mnemonicList = targetLog.IndexCurve.Value + sourceLogData.MnemonicList[sourceLogData.MnemonicList.IndexOf(CommonConstants.DataSeparator, StringComparison.InvariantCulture)..]; + var updateLogDataQueries = LogWorkerTools.GetUpdateLogDataQueries(targetLog.Uid, targetLog.UidWell, targetLog.UidWellbore, sourceLogData, chunkMaxSize, mnemonicList); if (cancellationToken is { IsCancellationRequested: true }) { return new CopyResult { Success = false, NumberOfRowsCopied = numberOfDataRowsCopied, ErrorReason = CancellationReason() }; } - WitsmlLogs copyNewCurvesQuery = CreateCopyQuery(targetLog, sourceLogData); - QueryResult result = await RequestUtils.WithRetry(async () => await GetTargetWitsmlClientOrThrow().UpdateInStoreAsync(copyNewCurvesQuery), Logger); - if (result.IsSuccessful) - { - numberOfDataRowsCopied += sourceLogData.Data.Count; - UpdateJobProgress(job, sourceLog, sourceLogData); - } - else + foreach (var query in updateLogDataQueries) { - Logger.LogError("Failed to copy log data. - {Description} - Current index: {StartIndex}", job.Description(), logDataReader.StartIndex); - return new CopyResult { Success = false, NumberOfRowsCopied = numberOfDataRowsCopied, ErrorReason = result.Reason }; - } + var result = await RequestUtils.WithRetry(async () => await GetTargetWitsmlClientOrThrow().UpdateInStoreAsync(query), Logger); + if (!result.IsSuccessful) + { + Logger.LogError("Failed to copy log data. - {Description} - Current index: {StartIndex}", job.Description(), logDataReader.StartIndex); + return new CopyResult { Success = false, NumberOfRowsCopied = numberOfDataRowsCopied, ErrorReason = result.Reason }; + } + } + numberOfDataRowsCopied += sourceLogData.Data.Count; + UpdateJobProgress(job, sourceLog, sourceLogData); sourceLogData = await logDataReader.GetNextBatch(); } diff --git a/Src/WitsmlExplorer.Api/Workers/ImportLogDataWorker.cs b/Src/WitsmlExplorer.Api/Workers/ImportLogDataWorker.cs index 1f9725357..18a9f6470 100644 --- a/Src/WitsmlExplorer.Api/Workers/ImportLogDataWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/ImportLogDataWorker.cs @@ -26,7 +26,6 @@ public class ImportLogDataWorker : BaseWorker, IWorker public ImportLogDataWorker(ILogger logger, IWitsmlClientProvider witsmlClientProvider) : base(witsmlClientProvider, logger) { } public override async Task<(WorkerResult, RefreshAction)> Execute(ImportLogDataJob job, CancellationToken? cancellationToken = null) { - int chunkSize = 1000; int maxUpdateAttempts = 2; string wellUid = job.TargetLog.WellUid; string wellboreUid = job.TargetLog.WellboreUid; @@ -54,8 +53,24 @@ public ImportLogDataWorker(ILogger logger, IWitsmlClientProvid } } - //Todo: find a way to determine the maximum amount of rows that can be sent to the WITSML server then pass that amount to the CreateImportQueries method - WitsmlLogs[] queries = CreateImportQueries(job, chunkSize).ToArray(); + var dataRows = job.DataRows + .Where(d => d.Count() > 1) + .Select(row => new WitsmlData + { + Data = string.Join(CommonConstants.DataSeparator, row) + }); + + var logData = new WitsmlLogData() + { + Data = dataRows.ToList(), + UnitList = string.Join(CommonConstants.DataSeparator, job.Units) + }; + var mnemonicList = + string.Join(CommonConstants.DataSeparator, job.Mnemonics); + + var chunkMaxSize = await GetMaxBatchSize(job.Mnemonics.Count, CommonConstants.WitsmlFunctionType.WMLSUpdateInStore, CommonConstants.WitsmlQueryTypeName.Log); + + var queries = LogWorkerTools.GetUpdateLogDataQueries(witsmlLog.Uid, witsmlLog.UidWell, witsmlLog.UidWellbore, logData, chunkMaxSize, mnemonicList).ToArray(); for (int i = 0; i < queries.Length; i++) { @@ -73,10 +88,10 @@ public ImportLogDataWorker(ILogger logger, IWitsmlClientProvid job.Description(), i, maxUpdateAttempts, - chunkSize, + chunkMaxSize, queries.Length); - return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), result.IsSuccessful, $"Failed to import curve data from row: {i * chunkSize}", result.Reason, witsmlLog.GetDescription()), null); + return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), result.IsSuccessful, $"Failed to import curve data from row: {i * chunkMaxSize}", result.Reason, witsmlLog.GetDescription()), null); } } double progress = (i + 1) / (double)queries.Length; @@ -99,32 +114,6 @@ private async Task GetLogHeader(string wellUid, string wellboreUid, s return result?.Logs.FirstOrDefault(); } - private static IEnumerable CreateImportQueries(ImportLogDataJob job, int chunkSize) - { - return job.DataRows - .Where(d => d.Count() > 1) - .Select(row => new WitsmlData { Data = string.Join(CommonConstants.DataSeparator, row) }) - .Chunk(chunkSize) - .Select(logData => new WitsmlLogs - { - Logs = new List - { - new WitsmlLog - { - Uid = job.TargetLog.Uid, - UidWellbore = job.TargetLog.WellboreUid, - UidWell = job.TargetLog.WellUid, - LogData = new WitsmlLogData - { - Data = logData.ToList(), - MnemonicList = string.Join(CommonConstants.DataSeparator, job.Mnemonics), - UnitList = string.Join(CommonConstants.DataSeparator, job.Units) - } - } - }, - }); - } - private static WitsmlLogs CreateAddMnemonicsQuery(ImportLogDataJob job, WitsmlLog witsmlLog) { return new WitsmlLogs diff --git a/Src/WitsmlExplorer.Api/Workers/OffsetLogCurveWorker.cs b/Src/WitsmlExplorer.Api/Workers/OffsetLogCurveWorker.cs index b0c32184c..e0ba9eed3 100644 --- a/Src/WitsmlExplorer.Api/Workers/OffsetLogCurveWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/OffsetLogCurveWorker.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; +using Witsml; using Witsml.Data; using Witsml.Extensions; using Witsml.ServiceReference; @@ -176,7 +177,11 @@ private async Task DeleteLogData(WitsmlLog log, WitsmlLogCurveInfo logCurveInfo, private async Task UpdateLogData(WitsmlLog log, WitsmlLogCurveInfo logCurveinfo, WitsmlLogData offsetLogData) { - var queries = GetUpdateLogDataQueries(log, offsetLogData); + var mnemonics = offsetLogData.MnemonicList.Split(CommonConstants.DataSeparator).ToList(); + var chunkMaxSize = await GetMaxBatchSize(mnemonics.Count, CommonConstants.WitsmlFunctionType.WMLSUpdateInStore, CommonConstants.WitsmlQueryTypeName.Log); + var mnemonicList = offsetLogData.MnemonicList; + + var queries = LogWorkerTools.GetUpdateLogDataQueries(log.Uid, log.UidWell, log.UidWellbore, offsetLogData, chunkMaxSize, mnemonicList); foreach (var query in queries) { @@ -188,30 +193,6 @@ private async Task UpdateLogData(WitsmlLog log, WitsmlLogCurveInfo logCurveinfo, } } - private static List GetUpdateLogDataQueries(WitsmlLog log, WitsmlLogData offsetLogData) - { - int chunkSize = 5000; // TODO: Base this on maxDataNodes/maxDataPoints once issue #1957 is implemented. - List batchedQueries = offsetLogData.Data.Chunk(chunkSize).Select(chunk => - new WitsmlLogs - { - Logs = new WitsmlLog - { - Uid = log.Uid, - UidWell = log.UidWell, - UidWellbore = log.UidWellbore, - LogData = new WitsmlLogData - { - MnemonicList = offsetLogData.MnemonicList, - UnitList = offsetLogData.UnitList, - Data = chunk.ToList(), - } - }.AsItemInList() - } - ).ToList(); - - return batchedQueries; - } - private static WitsmlLogData OffsetLogData(WitsmlLogData logData, double depthOffset, TimeSpan timeOffset, bool isDepthLog) { List offsetLogData = new(); diff --git a/Src/WitsmlExplorer.Api/Workers/SpliceLogsWorker.cs b/Src/WitsmlExplorer.Api/Workers/SpliceLogsWorker.cs index ac742147e..4ec0f14e8 100644 --- a/Src/WitsmlExplorer.Api/Workers/SpliceLogsWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/SpliceLogsWorker.cs @@ -276,35 +276,16 @@ private static List GetNewLogCurveInfo(WitsmlLogs logHeaders private async Task AddDataToLog(string wellUid, string wellboreUid, string logUid, WitsmlLogData data) { - var batchSize = 5000; // Use maxDataNodes and maxDataPoints to calculate batchSize when supported by the API. - var dataRows = data.Data; - for (int i = 0; i < dataRows.Count; i += batchSize) + var mnemonics = data.MnemonicList.Split(CommonConstants.DataSeparator).ToList(); + var chunkMaxSize = await GetMaxBatchSize(mnemonics.Count, CommonConstants.WitsmlFunctionType.WMLSUpdateInStore, CommonConstants.WitsmlQueryTypeName.Log); + var mnemonicList = data.MnemonicList; + var queries = LogWorkerTools.GetUpdateLogDataQueries(logUid, wellUid, wellboreUid, data, chunkMaxSize, mnemonicList); + + foreach (var query in queries) { - var currentLogData = dataRows.Skip(i).Take(batchSize).ToList(); - WitsmlLogs copyNewCurvesQuery = CreateAddLogDataRowsQuery(wellUid, wellboreUid, logUid, data, currentLogData); - QueryResult result = await RequestUtils.WithRetry(async () => await GetTargetWitsmlClientOrThrow().UpdateInStoreAsync(copyNewCurvesQuery), Logger); + QueryResult result = await RequestUtils.WithRetry(async () => await GetTargetWitsmlClientOrThrow().UpdateInStoreAsync(query), Logger); if (!result.IsSuccessful) throw new ArgumentException($"Could not add log data to the new log. {result.Reason}"); } } - - private static WitsmlLogs CreateAddLogDataRowsQuery(string wellUid, string wellboreUid, string logUid, WitsmlLogData logData, List currentLogData) - { - return new() - { - Logs = new List { - new(){ - UidWell = wellUid, - UidWellbore = wellboreUid, - Uid = logUid, - LogData = new WitsmlLogData - { - MnemonicList = logData.MnemonicList, - UnitList = logData.UnitList, - Data = currentLogData - } - } - } - }; - } } } diff --git a/Src/WitsmlExplorer.Api/Workers/Tools/LogWorkerTools.cs b/Src/WitsmlExplorer.Api/Workers/Tools/LogWorkerTools.cs index 8343b3c98..6412a33fd 100644 --- a/Src/WitsmlExplorer.Api/Workers/Tools/LogWorkerTools.cs +++ b/Src/WitsmlExplorer.Api/Workers/Tools/LogWorkerTools.cs @@ -80,5 +80,28 @@ public static double CalculateProgressBasedOnIndex(WitsmlLog log, WitsmlLogData return (DateTime.Parse(index) - DateTime.Parse(startIndex)).TotalMilliseconds / (DateTime.Parse(endIndex) - DateTime.Parse(startIndex)).TotalMilliseconds; } } + + public static List GetUpdateLogDataQueries(string uid, string uidWell, string uidWellbore, WitsmlLogData logData, int chunkSize, string mnemonicList) + { + List batchedQueries = logData.Data.Chunk(chunkSize).Select(chunk => + new WitsmlLogs + { + Logs = new WitsmlLog + { + Uid = uid, + UidWell = uidWell, + UidWellbore = uidWellbore, + LogData = new WitsmlLogData + { + MnemonicList = mnemonicList, + UnitList = logData.UnitList, + Data = chunk.ToList(), + } + }.AsItemInList() + } + ).ToList(); + + return batchedQueries; + } } } diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/CopyLogDataWorkerTests.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/CopyLogDataWorkerTests.cs index 3d793d20f..1dc807909 100644 --- a/Tests/WitsmlExplorer.Api.Tests/Workers/CopyLogDataWorkerTests.cs +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/CopyLogDataWorkerTests.cs @@ -45,6 +45,8 @@ public CopyLogDataWorkerTests() _witsmlClient = new Mock(); witsmlClientProvider.Setup(provider => provider.GetClient()).Returns(_witsmlClient.Object); witsmlClientProvider.Setup(provider => provider.GetSourceClient()).Returns(_witsmlClient.Object); + LogUtils.SetUpGetServerCapabilites(_witsmlClient); + Mock> logger = new(); Mock> documentRepository = new(); documentRepository.Setup(client => client.GetDocumentsAsync()) diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/ImportLogDataWorkerTests.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/ImportLogDataWorkerTests.cs index b0e26b117..b3bb56178 100644 --- a/Tests/WitsmlExplorer.Api.Tests/Workers/ImportLogDataWorkerTests.cs +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/ImportLogDataWorkerTests.cs @@ -38,6 +38,7 @@ public ImportLogDataWorkerTests() witsmlClientProvider.Setup(provider => provider.GetClient()).Returns(_witsmlClient.Object); ILoggerFactory loggerFactory = new LoggerFactory(); loggerFactory.AddSerilog(Log.Logger); + LogUtils.SetUpGetServerCapabilites(_witsmlClient); ILogger logger = loggerFactory.CreateLogger(); _worker = new ImportLogDataWorker(logger, witsmlClientProvider.Object); } diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/LogUtils.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/LogUtils.cs index fb0428e58..99909168c 100644 --- a/Tests/WitsmlExplorer.Api.Tests/Workers/LogUtils.cs +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/LogUtils.cs @@ -413,5 +413,36 @@ public static void SetupGetDepthIndexed(Mock witsmlClient, Func witsmlClient) + { + var serverCapabalities = new WitsmlCapServers() + { + ServerCapabilities = new List() + { + new WitsmlServerCapabilities() + { + Functions = new List() + { + new WitsmlFunction() + { + DataObjects = new List() + { + new WitsmlFunctionDataObject() + { + MaxDataNodes = 10000, + MaxDataPoints = 8000000, + Name = "log" + } + }, + Name = "WMLS_UpdateInStore" + } + } + } + } + + }; + witsmlClient.Setup(client => client.GetCap()).ReturnsAsync(serverCapabalities); + } } } diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/OffsetLogCurveWorkerTests.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/OffsetLogCurveWorkerTests.cs index 659decdd7..41d665254 100644 --- a/Tests/WitsmlExplorer.Api.Tests/Workers/OffsetLogCurveWorkerTests.cs +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/OffsetLogCurveWorkerTests.cs @@ -40,6 +40,7 @@ public OffsetLogCurveWorkerTests() witsmlClientProvider.Setup(provider => provider.GetClient()).Returns(_witsmlClient.Object); ILoggerFactory loggerFactory = new LoggerFactory(); loggerFactory.AddSerilog(Log.Logger); + LogUtils.SetUpGetServerCapabilites(_witsmlClient); ILogger logger = loggerFactory.CreateLogger(); _worker = new OffsetLogCurveWorker(logger, witsmlClientProvider.Object, null, null); } diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/SpliceLogsWorkerTests.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/SpliceLogsWorkerTests.cs index 52ffdc9eb..00a3f8b8a 100644 --- a/Tests/WitsmlExplorer.Api.Tests/Workers/SpliceLogsWorkerTests.cs +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/SpliceLogsWorkerTests.cs @@ -41,6 +41,7 @@ public SpliceLogsWorkerTests() _logger = new Mock>(); _witsmlClientProvider.Setup(provider => provider.GetClient()).Returns(_witsmlClient.Object); _worker = new SpliceLogsWorker(_logger.Object, _witsmlClientProvider.Object); + LogUtils.SetUpGetServerCapabilites(_witsmlClient); } [Theory] From 5f13738354bcea3e6f017e98401bc001c9b598da Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Fri, 2 Aug 2024 14:55:54 +0200 Subject: [PATCH 083/124] FIX-2500 Properties modal refactoring (#2501) --- Src/WitsmlExplorer.Api/Jobs/CreateLogJob.cs | 29 -- .../Jobs/CreateMudLogJob.cs | 29 -- .../Jobs/CreateObjectOnWellboreJob.cs | 33 ++ Src/WitsmlExplorer.Api/Jobs/CreateRigJob.cs | 50 -- Src/WitsmlExplorer.Api/Jobs/CreateRiskJob.cs | 29 -- .../Jobs/CreateTrajectoryJob.cs | 50 -- .../Jobs/CreateWbGeometryJob.cs | 29 -- Src/WitsmlExplorer.Api/Models/JobType.cs | 7 +- Src/WitsmlExplorer.Api/Models/LogObject.cs | 2 +- .../Models/Measure/DayMeasure.cs | 13 + Src/WitsmlExplorer.Api/Models/Wellbore.cs | 4 +- .../Query/FluidsReportQueries.cs | 4 +- Src/WitsmlExplorer.Api/Query/RigQueries.cs | 2 + Src/WitsmlExplorer.Api/Query/RiskQueries.cs | 2 + .../Query/WellboreQueries.cs | 61 ++- .../Services/BhaRunService.cs | 6 +- .../Services/FluidsReportService.cs | 2 + .../Services/MudLogService.cs | 4 +- .../Services/WellboreService.cs | 83 ++- .../Workers/Create/CreateLogWorker.cs | 91 ---- .../Workers/Create/CreateMudLogWorker.cs | 78 --- .../Create/CreateObjectOnWellboreWorker.cs | 73 +++ .../Workers/Create/CreateRigWorker.cs | 63 --- .../Workers/Create/CreateRiskWorker.cs | 78 --- .../Workers/Create/CreateTrajectoryWorker.cs | 63 --- .../Workers/Create/CreateWbGeometryWorker.cs | 107 ---- .../BatchModifyObjectsOnWellboreWorker.cs | 5 +- .../Modify/ModifyObjectOnWellboreWorker.cs | 28 +- .../Workers/Modify/ModifyUtils.cs | 80 +-- .../components/Constants.tsx | 3 + .../ContentViews/WellboresListView.tsx | 8 +- .../ContextMenus/BatchModifyMenuItem.tsx | 79 +-- .../ContextMenus/BhaRunContextMenu.tsx | 28 +- .../ContextMenus/ContextMenuUtils.tsx | 9 +- .../ContextMenus/FluidsReportContextMenu.tsx | 24 +- .../FormationMarkerContextMenu.tsx | 26 +- .../GeologyIntervalContextMenu.tsx | 42 +- .../ContextMenus/LogCurveInfoContextMenu.tsx | 35 +- .../ContextMenus/LogObjectContextMenu.tsx | 25 +- .../ContextMenus/LogsContextMenu.tsx | 39 +- .../ContextMenus/MessageObjectContextMenu.tsx | 27 +- .../ContextMenus/MudLogContextMenu.tsx | 24 +- .../ContextMenus/ObjectMenuItems.tsx | 2 +- .../ObjectsSidebarContextMenu.tsx | 24 + .../ContextMenus/RigContextMenu.tsx | 28 +- .../ContextMenus/RigsContextMenu.tsx | 22 +- .../ContextMenus/RiskContextMenu.tsx | 28 +- .../ContextMenus/TrajectoriesContextMenu.tsx | 22 +- .../ContextMenus/TrajectoryContextMenu.tsx | 28 +- .../TrajectoryStationContextMenu.tsx | 38 +- .../TubularComponentContextMenu.tsx | 37 +- .../ContextMenus/TubularContextMenu.tsx | 25 +- .../ContextMenus/TubularsContextMenu.tsx | 26 + .../ContextMenus/WbGeometryContextMenu.tsx | 28 +- .../WbGeometrySectionContextMenu.tsx | 36 +- .../ContextMenus/WellContextMenu.tsx | 55 +- .../ContextMenus/WellboreContextMenu.tsx | 66 +-- .../Modals/BatchModifyPropertiesModal.tsx | 125 ----- .../Modals/BhaRunPropertiesModal.tsx | 449 ---------------- .../Modals/FormationMarkerPropertiesModal.tsx | 352 ------------- .../Modals/GeologyIntervalPropertiesModal.tsx | 430 ---------------- .../Modals/LogCurveInfoPropertiesModal.tsx | 162 ------ .../Modals/LogHeaderDateTimeField.tsx | 8 +- .../components/Modals/LogPropertiesModal.tsx | 290 ----------- .../Modals/MessagePropertiesModal.tsx | 167 ------ .../components/Modals/ModalDialog.tsx | 4 +- .../components/Modals/ModalParts.tsx | 55 +- .../Modals/MudLogPropertiesModal.tsx | 232 --------- .../PropertiesModal/NestedPropertyHelpers.ts | 38 ++ .../BatchModifyObjectOnWellboreProperties.ts | 40 ++ .../Properties/BhaRunProperties.ts | 140 +++++ .../CommonObjectOnWellboreProperties.ts | 45 ++ .../Properties/FormationMarkerProperties.ts | 110 ++++ .../Properties/GeologyIntervalProperties.ts | 157 ++++++ .../Properties/LogCurveInfoProperties.ts | 85 +++ .../Properties/LogObjectProperties.ts | 109 ++++ .../Properties/MessageProperties.ts | 30 ++ .../Properties/MudLogProperties.ts | 65 +++ .../Properties/ObjectOnWellboreProperties.ts | 70 +++ .../Properties/RigProperties.ts | 169 ++++++ .../Properties/RiskProperties.ts | 134 +++++ .../Properties/TrajectoryProperties.ts | 68 +++ .../Properties/TrajectoryStationProperties.ts | 228 +++++++++ .../Properties/TubularComponentProperties.ts | 107 ++++ .../Properties/TubularProperties.ts | 21 + .../Properties/WbGeometryProperties.ts | 75 +++ .../Properties/WbGeometrySectionProperties.ts | 105 ++++ .../Properties/WellProperties.ts | 73 +++ .../Properties/WellboreProperties.ts | 150 ++++++ .../Properties/getFluidsReportProperties.ts | 82 +++ .../PropertiesModal/PropertiesModal.tsx | 105 ++++ .../PropertiesModal/PropertiesRenderer.tsx | 295 +++++++++++ .../Modals/PropertiesModal/PropertyTypes.ts | 49 ++ .../PropertiesModal/ValidationHelpers.ts | 157 ++++++ .../LogCurveInfoPropertiesModal.test.tsx | 103 ++++ .../__tests__/PropertiesModal.test.tsx | 406 +++++++++++++++ .../PropertiesModal/openPropertiesHelpers.tsx | 109 ++++ .../orderPropertyJobHelpers.ts | 94 ++++ .../propertiesModalProperty.ts | 71 +++ .../components/Modals/PropertiesModalUtils.ts | 40 -- .../components/Modals/RigPropertiesModal.tsx | 396 -------------- .../components/Modals/RiskPropertiesModal.tsx | 433 ---------------- .../components/Modals/ServerModal.tsx | 2 +- .../Modals/TrajectoryPropertiesModal.tsx | 284 ---------- .../TrajectoryStationPropertiesModal.tsx | 453 ---------------- .../TubularComponentPropertiesModal.tsx | 357 ------------- .../Modals/TubularPropertiesModal.tsx | 117 ----- .../Modals/WbGeometryPropertiesModal.tsx | 282 ---------- .../WbGeometrySectionPropertiesModal.tsx | 273 ---------- .../components/Modals/WellPropertiesModal.tsx | 196 ------- .../Modals/WellborePropertiesModal.tsx | 484 ------------------ .../LogCurveInfoPropertiesModal.test.tsx | 96 ---- .../components/Sidebar/LogTypeItem.tsx | 10 +- .../models/bhaStatusTypes.ts | 1 + .../models/commonData.tsx | 2 - .../models/indexCurve.ts | 4 + .../models/levelIntegerCode.ts | 1 + .../models/typeTubularAssy.ts | 16 + .../models/wellbore.tsx | 8 +- .../models/wellborePurposeValues.ts | 9 + .../services/jobService.tsx | 4 +- .../Workers/CreateLogWorkerTests.cs | 71 +-- .../Workers/CreateRigWorkerTests.cs | 46 +- .../Workers/CreateRiskWorkerTests.cs | 38 +- .../Workers/CreateTrajectoryWorkerTests.cs | 47 +- .../Api/Workers/CreateLogWorkerTests.cs | 22 +- 126 files changed, 4363 insertions(+), 7032 deletions(-) delete mode 100644 Src/WitsmlExplorer.Api/Jobs/CreateLogJob.cs delete mode 100644 Src/WitsmlExplorer.Api/Jobs/CreateMudLogJob.cs create mode 100644 Src/WitsmlExplorer.Api/Jobs/CreateObjectOnWellboreJob.cs delete mode 100644 Src/WitsmlExplorer.Api/Jobs/CreateRigJob.cs delete mode 100644 Src/WitsmlExplorer.Api/Jobs/CreateRiskJob.cs delete mode 100644 Src/WitsmlExplorer.Api/Jobs/CreateTrajectoryJob.cs delete mode 100644 Src/WitsmlExplorer.Api/Jobs/CreateWbGeometryJob.cs delete mode 100644 Src/WitsmlExplorer.Api/Workers/Create/CreateLogWorker.cs delete mode 100644 Src/WitsmlExplorer.Api/Workers/Create/CreateMudLogWorker.cs create mode 100644 Src/WitsmlExplorer.Api/Workers/Create/CreateObjectOnWellboreWorker.cs delete mode 100644 Src/WitsmlExplorer.Api/Workers/Create/CreateRigWorker.cs delete mode 100644 Src/WitsmlExplorer.Api/Workers/Create/CreateRiskWorker.cs delete mode 100644 Src/WitsmlExplorer.Api/Workers/Create/CreateTrajectoryWorker.cs delete mode 100644 Src/WitsmlExplorer.Api/Workers/Create/CreateWbGeometryWorker.cs delete mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/BatchModifyPropertiesModal.tsx delete mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/BhaRunPropertiesModal.tsx delete mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/FormationMarkerPropertiesModal.tsx delete mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/GeologyIntervalPropertiesModal.tsx delete mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/LogCurveInfoPropertiesModal.tsx delete mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/LogPropertiesModal.tsx delete mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/MessagePropertiesModal.tsx delete mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/MudLogPropertiesModal.tsx create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/NestedPropertyHelpers.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/BatchModifyObjectOnWellboreProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/BhaRunProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/CommonObjectOnWellboreProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/FormationMarkerProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/GeologyIntervalProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/LogCurveInfoProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/LogObjectProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/MessageProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/MudLogProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/ObjectOnWellboreProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/RigProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/RiskProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/TrajectoryProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/TrajectoryStationProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/TubularComponentProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/TubularProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/WbGeometryProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/WbGeometrySectionProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/WellProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/WellboreProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/getFluidsReportProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/PropertiesModal.tsx create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/PropertiesRenderer.tsx create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/PropertyTypes.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/ValidationHelpers.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/__tests__/LogCurveInfoPropertiesModal.test.tsx create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/__tests__/PropertiesModal.test.tsx create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/openPropertiesHelpers.tsx create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/orderPropertyJobHelpers.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/propertiesModalProperty.ts delete mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModalUtils.ts delete mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/RigPropertiesModal.tsx delete mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/RiskPropertiesModal.tsx delete mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/TrajectoryPropertiesModal.tsx delete mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/TrajectoryStationPropertiesModal.tsx delete mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/TubularComponentPropertiesModal.tsx delete mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/TubularPropertiesModal.tsx delete mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/WbGeometryPropertiesModal.tsx delete mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/WbGeometrySectionPropertiesModal.tsx delete mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/WellPropertiesModal.tsx delete mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/WellborePropertiesModal.tsx delete mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/__tests__/LogCurveInfoPropertiesModal.test.tsx create mode 100644 Src/WitsmlExplorer.Frontend/models/bhaStatusTypes.ts create mode 100644 Src/WitsmlExplorer.Frontend/models/indexCurve.ts create mode 100644 Src/WitsmlExplorer.Frontend/models/levelIntegerCode.ts create mode 100644 Src/WitsmlExplorer.Frontend/models/typeTubularAssy.ts create mode 100644 Src/WitsmlExplorer.Frontend/models/wellborePurposeValues.ts diff --git a/Src/WitsmlExplorer.Api/Jobs/CreateLogJob.cs b/Src/WitsmlExplorer.Api/Jobs/CreateLogJob.cs deleted file mode 100644 index b7a48f98a..000000000 --- a/Src/WitsmlExplorer.Api/Jobs/CreateLogJob.cs +++ /dev/null @@ -1,29 +0,0 @@ -using WitsmlExplorer.Api.Models; - -namespace WitsmlExplorer.Api.Jobs -{ - public record CreateLogJob : Job - { - public LogObject LogObject { get; init; } - - public override string Description() - { - return $"Create Log - WellUid: {LogObject.WellUid}; WellboreUid: {LogObject.WellboreUid}; LogUid: {LogObject.Uid};"; - } - - public override string GetObjectName() - { - return LogObject.Name; - } - - public override string GetWellboreName() - { - return LogObject.WellboreName; - } - - public override string GetWellName() - { - return LogObject.WellName; - } - } -} diff --git a/Src/WitsmlExplorer.Api/Jobs/CreateMudLogJob.cs b/Src/WitsmlExplorer.Api/Jobs/CreateMudLogJob.cs deleted file mode 100644 index 283bf0ede..000000000 --- a/Src/WitsmlExplorer.Api/Jobs/CreateMudLogJob.cs +++ /dev/null @@ -1,29 +0,0 @@ -using WitsmlExplorer.Api.Models; - -namespace WitsmlExplorer.Api.Jobs -{ - public record CreateMudLogJob : Job - { - public MudLog MudLog { get; init; } - - public override string Description() - { - return $"Create MudLog - WellUid: {MudLog.WellUid}; WellboreUid: {MudLog.WellboreUid}; MudLogUid: {MudLog.Uid};"; - } - - public override string GetObjectName() - { - return MudLog.Name; - } - - public override string GetWellboreName() - { - return MudLog.WellboreName; - } - - public override string GetWellName() - { - return MudLog.WellName; - } - } -} diff --git a/Src/WitsmlExplorer.Api/Jobs/CreateObjectOnWellboreJob.cs b/Src/WitsmlExplorer.Api/Jobs/CreateObjectOnWellboreJob.cs new file mode 100644 index 000000000..e01984d29 --- /dev/null +++ b/Src/WitsmlExplorer.Api/Jobs/CreateObjectOnWellboreJob.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; + +using WitsmlExplorer.Api.Models; + +namespace WitsmlExplorer.Api.Jobs +{ + public record CreateObjectOnWellboreJob : Job + { + [JsonConverter(typeof(ObjectOnWellboreConverter))] + public ObjectOnWellbore Object { get; init; } + public EntityType ObjectType { get; init; } + + public override string Description() + { + return $"To Create - Type: {ObjectType}, Uid: {Object.Uid}."; + } + + public override string GetObjectName() + { + return Object.Name; + } + + public override string GetWellboreName() + { + return Object.WellboreName; + } + + public override string GetWellName() + { + return Object.WellName; + } + } +} diff --git a/Src/WitsmlExplorer.Api/Jobs/CreateRigJob.cs b/Src/WitsmlExplorer.Api/Jobs/CreateRigJob.cs deleted file mode 100644 index 08dd4b103..000000000 --- a/Src/WitsmlExplorer.Api/Jobs/CreateRigJob.cs +++ /dev/null @@ -1,50 +0,0 @@ -using WitsmlExplorer.Api.Models; - -namespace WitsmlExplorer.Api.Jobs; - -/// -/// Job for create rig with jobInfo. -/// -public record CreateRigJob : Job -{ - /// - /// Rig API model. - /// - public Rig Rig { get; init; } - - /// - /// Getting description of created rig. - /// - /// String of job info which provide WellUid, WellboreUid and RigUid. - public override string Description() - { - return $"Create Rig - Uid: {Rig.Uid}; Name: {Rig.Name}; WellUid: {Rig.WellUid}; WellboreUid: {Rig.WellboreUid};"; - } - - /// - /// Getting name of rig. - /// - /// String of rig name. - public override string GetObjectName() - { - return Rig.Name; - } - - /// - /// Getting name of wellbore. - /// - /// String of wellbore name. - public override string GetWellboreName() - { - return Rig.WellboreName; - } - - /// - /// Getting name of well. - /// - /// String of well name. - public override string GetWellName() - { - return Rig.WellName; - } -} diff --git a/Src/WitsmlExplorer.Api/Jobs/CreateRiskJob.cs b/Src/WitsmlExplorer.Api/Jobs/CreateRiskJob.cs deleted file mode 100644 index 767d98ab3..000000000 --- a/Src/WitsmlExplorer.Api/Jobs/CreateRiskJob.cs +++ /dev/null @@ -1,29 +0,0 @@ -using WitsmlExplorer.Api.Models; - -namespace WitsmlExplorer.Api.Jobs -{ - public record CreateRiskJob : Job - { - public Risk Risk { get; init; } - - public override string Description() - { - return $"Create Risk - WellUid: {Risk.WellUid}; WellboreUid: {Risk.WellboreUid}; RiskUid: {Risk.Uid};"; - } - - public override string GetObjectName() - { - return Risk.Name; - } - - public override string GetWellboreName() - { - return Risk.WellboreName; - } - - public override string GetWellName() - { - return Risk.WellName; - } - } -} diff --git a/Src/WitsmlExplorer.Api/Jobs/CreateTrajectoryJob.cs b/Src/WitsmlExplorer.Api/Jobs/CreateTrajectoryJob.cs deleted file mode 100644 index 2ba40901a..000000000 --- a/Src/WitsmlExplorer.Api/Jobs/CreateTrajectoryJob.cs +++ /dev/null @@ -1,50 +0,0 @@ -using WitsmlExplorer.Api.Models; - -namespace WitsmlExplorer.Api.Jobs; - -/// -/// Job for create trajectory with jobInfo. -/// -public record CreateTrajectoryJob : Job -{ - /// - /// Trajectory API model. - /// - public Trajectory Trajectory { get; init; } - - /// - /// Getting description of created trajectory. - /// - /// String of job info which provide Trajectory Uid and Name, WellUid, WellboreUid. - public override string Description() - { - return $"Create Trajectory - Uid: {Trajectory.Uid}; Name: {Trajectory.Name}; WellUid: {Trajectory.WellUid}; WellboreUid: {Trajectory.WellboreUid};"; - } - - /// - /// Getting name of trajectory. - /// - /// String of trajectory name. - public override string GetObjectName() - { - return Trajectory.Name; - } - - /// - /// Getting name of wellbore. - /// - /// String of wellbore name. - public override string GetWellboreName() - { - return Trajectory.WellboreName; - } - - /// - /// Getting name of well. - /// - /// String of well name. - public override string GetWellName() - { - return Trajectory.WellName; - } -} diff --git a/Src/WitsmlExplorer.Api/Jobs/CreateWbGeometryJob.cs b/Src/WitsmlExplorer.Api/Jobs/CreateWbGeometryJob.cs deleted file mode 100644 index 7e8d12a35..000000000 --- a/Src/WitsmlExplorer.Api/Jobs/CreateWbGeometryJob.cs +++ /dev/null @@ -1,29 +0,0 @@ -using WitsmlExplorer.Api.Models; - -namespace WitsmlExplorer.Api.Jobs -{ - public record CreateWbGeometryJob : Job - { - public WbGeometry WbGeometry { get; init; } - - public override string Description() - { - return $"Create WbGeometry - WellUid: {WbGeometry.WellUid}; WellboreUid: {WbGeometry.WellboreUid}; WbGeometryUid: {WbGeometry.Uid};"; - } - - public override string GetObjectName() - { - return WbGeometry.Name; - } - - public override string GetWellboreName() - { - return WbGeometry.WellboreName; - } - - public override string GetWellName() - { - return WbGeometry.WellName; - } - } -} diff --git a/Src/WitsmlExplorer.Api/Models/JobType.cs b/Src/WitsmlExplorer.Api/Models/JobType.cs index 5abeabf67..43f956661 100644 --- a/Src/WitsmlExplorer.Api/Models/JobType.cs +++ b/Src/WitsmlExplorer.Api/Models/JobType.cs @@ -25,14 +25,9 @@ public enum JobType ModifyWbGeometrySection, ModifyWell, ModifyWellbore, - CreateLogObject, CreateWell, CreateWellbore, - CreateRisk, - CreateMudLog, - CreateRig, - CreateTrajectory, - CreateWbGeometry, + CreateObjectOnWellbore, BatchModifyWell, ImportLogData, ReplaceComponents, diff --git a/Src/WitsmlExplorer.Api/Models/LogObject.cs b/Src/WitsmlExplorer.Api/Models/LogObject.cs index 346fa14f6..5523f91dc 100644 --- a/Src/WitsmlExplorer.Api/Models/LogObject.cs +++ b/Src/WitsmlExplorer.Api/Models/LogObject.cs @@ -9,7 +9,7 @@ public class LogObject : ObjectOnWellbore public string IndexType { get; set; } public string StartIndex { get; set; } public string EndIndex { get; set; } - public bool ObjectGrowing { get; init; } + public bool? ObjectGrowing { get; init; } public string ServiceCompany { get; init; } public string RunNumber { get; init; } public string IndexCurve { get; init; } diff --git a/Src/WitsmlExplorer.Api/Models/Measure/DayMeasure.cs b/Src/WitsmlExplorer.Api/Models/Measure/DayMeasure.cs index 82304a39e..d6cb6e693 100644 --- a/Src/WitsmlExplorer.Api/Models/Measure/DayMeasure.cs +++ b/Src/WitsmlExplorer.Api/Models/Measure/DayMeasure.cs @@ -1,7 +1,20 @@ +using System.Globalization; + +using Witsml.Data.Measures; + namespace WitsmlExplorer.Api.Models.Measure { public class DayMeasure : Measure { public int Value { get; init; } + + public WitsmlDayMeasure ToWitsml() + { + return new() + { + Uom = Uom, + Value = Value.ToString(CultureInfo.InvariantCulture) + }; + } } } diff --git a/Src/WitsmlExplorer.Api/Models/Wellbore.cs b/Src/WitsmlExplorer.Api/Models/Wellbore.cs index 335996463..3ea6aa2b2 100644 --- a/Src/WitsmlExplorer.Api/Models/Wellbore.cs +++ b/Src/WitsmlExplorer.Api/Models/Wellbore.cs @@ -11,8 +11,8 @@ public class Wellbore public string WellborePurpose { get; set; } public string WellboreParentUid { get; set; } public string WellboreParentName { get; set; } - public string WellStatus { get; set; } - public string WellType { get; set; } + public string WellboreStatus { get; set; } + public string WellboreType { get; set; } public bool? IsActive { get; set; } public string DateTimeCreation { get; set; } public string DateTimeLastChange { get; set; } diff --git a/Src/WitsmlExplorer.Api/Query/FluidsReportQueries.cs b/Src/WitsmlExplorer.Api/Query/FluidsReportQueries.cs index a7089b51c..193103186 100644 --- a/Src/WitsmlExplorer.Api/Query/FluidsReportQueries.cs +++ b/Src/WitsmlExplorer.Api/Query/FluidsReportQueries.cs @@ -25,7 +25,9 @@ public static WitsmlFluidsReports QueryByWellbore(string wellUid, string wellbor DTimLastChange = "", ItemState = "", Comments = "", - DefaultDatum = "" + DefaultDatum = "", + SourceName = "", + ServiceCategory = "" } }.AsItemInWitsmlList(); } diff --git a/Src/WitsmlExplorer.Api/Query/RigQueries.cs b/Src/WitsmlExplorer.Api/Query/RigQueries.cs index d7bba277d..0ad6182bd 100644 --- a/Src/WitsmlExplorer.Api/Query/RigQueries.cs +++ b/Src/WitsmlExplorer.Api/Query/RigQueries.cs @@ -23,6 +23,8 @@ public static WitsmlRigs GetWitsmlRig(string wellUid, string wellboreUid, string IsOffshore = "", Manufacturer = "", Name = "", + NameWell = "", + NameWellbore = "", NameContact = "", Owner = "", Registration = "", diff --git a/Src/WitsmlExplorer.Api/Query/RiskQueries.cs b/Src/WitsmlExplorer.Api/Query/RiskQueries.cs index 6dd37a4ef..805b3f922 100644 --- a/Src/WitsmlExplorer.Api/Query/RiskQueries.cs +++ b/Src/WitsmlExplorer.Api/Query/RiskQueries.cs @@ -16,6 +16,8 @@ public static WitsmlRisks GetWitsmlRiskByWellbore(string wellUid, string wellbor UidWell = wellUid, UidWellbore = wellboreUid, Name = "", + NameWell = "", + NameWellbore = "", Type = "", Category = "", SubCategory = "", diff --git a/Src/WitsmlExplorer.Api/Query/WellboreQueries.cs b/Src/WitsmlExplorer.Api/Query/WellboreQueries.cs index 96009a1c9..c76fe5ce1 100644 --- a/Src/WitsmlExplorer.Api/Query/WellboreQueries.cs +++ b/Src/WitsmlExplorer.Api/Query/WellboreQueries.cs @@ -143,35 +143,44 @@ public static WitsmlWellbores UpdateWitsmlWellbore(Wellbore wellbore) public static WitsmlWellbores CreateWitsmlWellbore(Wellbore wellbore) { - return !string.IsNullOrEmpty(wellbore.WellboreParentUid) - ? new WitsmlWellbores + WitsmlParentWellbore parentWellbore = (wellbore.WellboreParentUid == null && wellbore.WellboreParentName == null) ? null + : new WitsmlParentWellbore { - Wellbores = new WitsmlWellbore - { - Uid = wellbore.Uid, - Name = wellbore.Name, - UidWell = wellbore.WellUid, - NameWell = wellbore.WellName, - ParentWellbore = new WitsmlParentWellbore - { - UidRef = wellbore.WellboreParentUid, - Value = wellbore.WellboreParentName - }, - PurposeWellbore = wellbore.WellborePurpose - - }.AsItemInList() - } - : new WitsmlWellbores + UidRef = wellbore.WellboreParentUid, + Value = wellbore.WellboreParentName + }; + return new WitsmlWellbores + { + Wellbores = new WitsmlWellbore { - Wellbores = new WitsmlWellbore + Uid = wellbore.Uid, + Name = wellbore.Name, + UidWell = wellbore.WellUid, + NameWell = wellbore.WellName, + ParentWellbore = parentWellbore, + StatusWellbore = wellbore.WellboreStatus, + TypeWellbore = wellbore.WellboreType, + Number = wellbore.Number, + SuffixAPI = wellbore.SuffixAPI, + NumGovt = wellbore.NumGovt, + Shape = wellbore.Shape, + DTimKickoff = wellbore.DTimeKickoff, + PurposeWellbore = wellbore.WellborePurpose, + Md = wellbore.Md?.ToWitsml(), + Tvd = wellbore.Tvd?.ToWitsml(), + MdKickoff = wellbore.MdKickoff?.ToWitsml(), + TvdKickoff = wellbore.TvdKickoff?.ToWitsml(), + MdPlanned = wellbore.MdPlanned?.ToWitsml(), + TvdPlanned = wellbore.TvdPlanned?.ToWitsml(), + MdSubSeaPlanned = wellbore.MdSubSeaPlanned?.ToWitsml(), + TvdSubSeaPlanned = wellbore.TvdSubSeaPlanned?.ToWitsml(), + DayTarget = wellbore.DayTarget?.ToWitsml(), + CommonData = wellbore.Comments == null ? null : new WitsmlCommonData { - Uid = wellbore.Uid, - Name = wellbore.Name, - UidWell = wellbore.WellUid, - NameWell = wellbore.WellName, - PurposeWellbore = wellbore.WellborePurpose - }.AsItemInList() - }; + Comments = wellbore.Comments + } + }.AsItemInList() + }; } public static WitsmlWellbores DeleteWitsmlWellbore(string wellUid, string wellboreUid) diff --git a/Src/WitsmlExplorer.Api/Services/BhaRunService.cs b/Src/WitsmlExplorer.Api/Services/BhaRunService.cs index 096dd4abc..9edd88db8 100644 --- a/Src/WitsmlExplorer.Api/Services/BhaRunService.cs +++ b/Src/WitsmlExplorer.Api/Services/BhaRunService.cs @@ -45,10 +45,10 @@ private static BhaRun WitsmlToBhaRun(WitsmlBhaRun bhaRun) WellboreName = bhaRun.NameWellbore, WellboreUid = bhaRun.UidWellbore, NumStringRun = bhaRun.NumStringRun, - Tubular = new RefNameString + Tubular = (bhaRun.Tubular == null) ? null : new RefNameString { - UidRef = bhaRun.Tubular?.UidRef, - Value = bhaRun.Tubular?.Value + UidRef = bhaRun.Tubular.UidRef, + Value = bhaRun.Tubular.Value }, StatusBha = bhaRun.StatusBha ?? null, NumBitRun = bhaRun.NumBitRun, diff --git a/Src/WitsmlExplorer.Api/Services/FluidsReportService.cs b/Src/WitsmlExplorer.Api/Services/FluidsReportService.cs index ce602b79f..e491fccfa 100644 --- a/Src/WitsmlExplorer.Api/Services/FluidsReportService.cs +++ b/Src/WitsmlExplorer.Api/Services/FluidsReportService.cs @@ -141,6 +141,8 @@ private static FluidsReport WitsmlToFluidsReport(WitsmlFluidsReport fluidsReport ItemState = fluidsReport.CommonData.ItemState, Comments = fluidsReport.CommonData.Comments, DefaultDatum = fluidsReport.CommonData.DefaultDatum, + SourceName = fluidsReport.CommonData.SourceName, + ServiceCategory = fluidsReport.CommonData.ServiceCategory } }; } diff --git a/Src/WitsmlExplorer.Api/Services/MudLogService.cs b/Src/WitsmlExplorer.Api/Services/MudLogService.cs index 5987352f4..3ce66117e 100644 --- a/Src/WitsmlExplorer.Api/Services/MudLogService.cs +++ b/Src/WitsmlExplorer.Api/Services/MudLogService.cs @@ -74,8 +74,8 @@ private static List GetGeologyIntervals(List GetWellbore(string wellUid, string wellboreUid) WitsmlWellbore witsmlWellbore = result.Wellbores.FirstOrDefault(); return witsmlWellbore == null ? null - : new Wellbore - { - Uid = witsmlWellbore.Uid, - Name = witsmlWellbore.Name, - WellUid = witsmlWellbore.UidWell, - WellName = witsmlWellbore.NameWell, - Number = witsmlWellbore.Number, - SuffixAPI = witsmlWellbore.SuffixAPI, - NumGovt = witsmlWellbore.NumGovt, - WellStatus = witsmlWellbore.StatusWellbore, - IsActive = StringHelpers.ToBoolean(witsmlWellbore.IsActive), - WellborePurpose = witsmlWellbore.PurposeWellbore, - WellboreParentUid = witsmlWellbore.ParentWellbore?.UidRef, - WellboreParentName = witsmlWellbore.ParentWellbore?.Value, - WellType = witsmlWellbore.TypeWellbore, - Shape = witsmlWellbore.Shape, - DTimeKickoff = witsmlWellbore.DTimKickoff, - Md = (witsmlWellbore.Md == null) ? null : new LengthMeasure { Uom = witsmlWellbore.Md.Uom, Value = StringHelpers.ToDecimal(witsmlWellbore.Md.Value) }, - Tvd = (witsmlWellbore.Tvd == null) ? null : new LengthMeasure { Uom = witsmlWellbore.Tvd.Uom, Value = StringHelpers.ToDecimal(witsmlWellbore.Tvd.Value) }, - MdKickoff = (witsmlWellbore.MdKickoff == null) ? null : new LengthMeasure { Uom = witsmlWellbore.MdKickoff.Uom, Value = StringHelpers.ToDecimal(witsmlWellbore.MdKickoff.Value) }, - TvdKickoff = (witsmlWellbore.TvdKickoff == null) ? null : new LengthMeasure { Uom = witsmlWellbore.TvdKickoff.Uom, Value = StringHelpers.ToDecimal(witsmlWellbore.TvdKickoff.Value) }, - MdPlanned = (witsmlWellbore.MdPlanned == null) ? null : new LengthMeasure { Uom = witsmlWellbore.MdPlanned.Uom, Value = StringHelpers.ToDecimal(witsmlWellbore.MdPlanned.Value) }, - TvdPlanned = (witsmlWellbore.TvdPlanned == null) ? null : new LengthMeasure { Uom = witsmlWellbore.TvdPlanned.Uom, Value = StringHelpers.ToDecimal(witsmlWellbore.TvdPlanned.Value) }, - MdSubSeaPlanned = (witsmlWellbore.MdSubSeaPlanned == null) ? null : new LengthMeasure { Uom = witsmlWellbore.MdSubSeaPlanned.Uom, Value = StringHelpers.ToDecimal(witsmlWellbore.MdSubSeaPlanned.Value) }, - TvdSubSeaPlanned = (witsmlWellbore.TvdSubSeaPlanned == null) ? null : new LengthMeasure { Uom = witsmlWellbore.TvdSubSeaPlanned.Uom, Value = StringHelpers.ToDecimal(witsmlWellbore.TvdSubSeaPlanned.Value) }, - DayTarget = (witsmlWellbore.DayTarget == null) ? null : new DayMeasure { Uom = witsmlWellbore.DayTarget.Uom, Value = int.Parse(witsmlWellbore.DayTarget.Value) }, - DateTimeCreation = witsmlWellbore.CommonData.DTimCreation, - DateTimeLastChange = witsmlWellbore.CommonData.DTimLastChange, - ItemState = witsmlWellbore.CommonData.ItemState, - Comments = witsmlWellbore.CommonData.Comments - }; + : FromWitsml(witsmlWellbore); } public async Task> GetWellbores(string wellUid = "") @@ -68,25 +38,48 @@ public async Task> GetWellbores(string wellUid = "") return await MeasurementHelper.MeasureExecutionTimeAsync(async (timeMeasurer) => { WitsmlWellbores query = WellboreQueries.GetWitsmlWellboreByWell(wellUid); - WitsmlWellbores result = await _witsmlClient.GetFromStoreAsync(query, new OptionsIn(ReturnElements.Requested)); + WitsmlWellbores result = await _witsmlClient.GetFromStoreAsync(query, new OptionsIn(ReturnElements.All)); List wellbores = result.Wellbores - .Select(witsmlWellbore => - new Wellbore - { - Uid = witsmlWellbore.Uid, - Name = witsmlWellbore.Name, - WellUid = witsmlWellbore.UidWell, - WellName = witsmlWellbore.NameWell, - WellStatus = witsmlWellbore.StatusWellbore, - WellType = witsmlWellbore.TypeWellbore, - IsActive = StringHelpers.ToBoolean(witsmlWellbore.IsActive), - DateTimeLastChange = witsmlWellbore.CommonData.DTimLastChange, - DateTimeCreation = witsmlWellbore.CommonData.DTimCreation - }) + .Select(FromWitsml) .OrderBy(wellbore => wellbore.Name).ToList(); timeMeasurer.LogMessage = executionTime => $"Fetched {wellbores.Count} wellbores in {executionTime} ms."; return wellbores; }); } + + private Wellbore FromWitsml(WitsmlWellbore witsmlWellbore) + { + return new Wellbore + { + Uid = witsmlWellbore.Uid, + Name = witsmlWellbore.Name, + WellUid = witsmlWellbore.UidWell, + WellName = witsmlWellbore.NameWell, + Number = witsmlWellbore.Number, + SuffixAPI = witsmlWellbore.SuffixAPI, + NumGovt = witsmlWellbore.NumGovt, + WellboreStatus = witsmlWellbore.StatusWellbore, + IsActive = StringHelpers.ToBoolean(witsmlWellbore.IsActive), + WellborePurpose = witsmlWellbore.PurposeWellbore, + WellboreParentUid = witsmlWellbore.ParentWellbore?.UidRef, + WellboreParentName = witsmlWellbore.ParentWellbore?.Value, + WellboreType = witsmlWellbore.TypeWellbore, + Shape = witsmlWellbore.Shape, + DTimeKickoff = witsmlWellbore.DTimKickoff, + Md = (witsmlWellbore.Md == null) ? null : new LengthMeasure { Uom = witsmlWellbore.Md.Uom, Value = StringHelpers.ToDecimal(witsmlWellbore.Md.Value) }, + Tvd = (witsmlWellbore.Tvd == null) ? null : new LengthMeasure { Uom = witsmlWellbore.Tvd.Uom, Value = StringHelpers.ToDecimal(witsmlWellbore.Tvd.Value) }, + MdKickoff = (witsmlWellbore.MdKickoff == null) ? null : new LengthMeasure { Uom = witsmlWellbore.MdKickoff.Uom, Value = StringHelpers.ToDecimal(witsmlWellbore.MdKickoff.Value) }, + TvdKickoff = (witsmlWellbore.TvdKickoff == null) ? null : new LengthMeasure { Uom = witsmlWellbore.TvdKickoff.Uom, Value = StringHelpers.ToDecimal(witsmlWellbore.TvdKickoff.Value) }, + MdPlanned = (witsmlWellbore.MdPlanned == null) ? null : new LengthMeasure { Uom = witsmlWellbore.MdPlanned.Uom, Value = StringHelpers.ToDecimal(witsmlWellbore.MdPlanned.Value) }, + TvdPlanned = (witsmlWellbore.TvdPlanned == null) ? null : new LengthMeasure { Uom = witsmlWellbore.TvdPlanned.Uom, Value = StringHelpers.ToDecimal(witsmlWellbore.TvdPlanned.Value) }, + MdSubSeaPlanned = (witsmlWellbore.MdSubSeaPlanned == null) ? null : new LengthMeasure { Uom = witsmlWellbore.MdSubSeaPlanned.Uom, Value = StringHelpers.ToDecimal(witsmlWellbore.MdSubSeaPlanned.Value) }, + TvdSubSeaPlanned = (witsmlWellbore.TvdSubSeaPlanned == null) ? null : new LengthMeasure { Uom = witsmlWellbore.TvdSubSeaPlanned.Uom, Value = StringHelpers.ToDecimal(witsmlWellbore.TvdSubSeaPlanned.Value) }, + DayTarget = (witsmlWellbore.DayTarget == null) ? null : new DayMeasure { Uom = witsmlWellbore.DayTarget.Uom, Value = int.Parse(witsmlWellbore.DayTarget.Value) }, + DateTimeCreation = witsmlWellbore.CommonData.DTimCreation, + DateTimeLastChange = witsmlWellbore.CommonData.DTimLastChange, + ItemState = witsmlWellbore.CommonData.ItemState, + Comments = witsmlWellbore.CommonData.Comments + }; + } } } diff --git a/Src/WitsmlExplorer.Api/Workers/Create/CreateLogWorker.cs b/Src/WitsmlExplorer.Api/Workers/Create/CreateLogWorker.cs deleted file mode 100644 index 7c2288fa0..000000000 --- a/Src/WitsmlExplorer.Api/Workers/Create/CreateLogWorker.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.Extensions.Logging; - -using Witsml; -using Witsml.Data; -using Witsml.Data.Curves; -using Witsml.Extensions; -using Witsml.ServiceReference; - -using WitsmlExplorer.Api.Jobs; -using WitsmlExplorer.Api.Models; -using WitsmlExplorer.Api.Query; -using WitsmlExplorer.Api.Services; - -namespace WitsmlExplorer.Api.Workers.Create -{ - public class CreateLogWorker : BaseWorker, IWorker - { - public JobType JobType => JobType.CreateLogObject; - - public CreateLogWorker(ILogger logger, IWitsmlClientProvider witsmlClientProvider) : base(witsmlClientProvider, logger) { } - - public override async Task<(WorkerResult, RefreshAction)> Execute(CreateLogJob job, CancellationToken? cancellationToken = null) - { - WitsmlWellbore targetWellbore = await GetWellbore(GetTargetWitsmlClientOrThrow(), job.LogObject); - WitsmlLogs copyLogQuery = CreateLogQuery(job, targetWellbore); - QueryResult createLogResult = await GetTargetWitsmlClientOrThrow().AddToStoreAsync(copyLogQuery); - if (!createLogResult.IsSuccessful) - { - string errorMessage = "Failed to create log."; - Logger.LogError("{ErrorMessage}. {jobDescription}", errorMessage, job.Description()); - return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, errorMessage, createLogResult.Reason), null); - } - - Logger.LogInformation("Log object created. {jobDescription}", job.Description()); - RefreshObjects refreshAction = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), job.LogObject.WellUid, job.LogObject.WellboreUid, EntityType.Log); - WorkerResult workerResult = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), true, $"Log object {job.LogObject.Name} created for {targetWellbore.Name}"); - - return (workerResult, refreshAction); - } - - private static WitsmlLogs CreateLogQuery(CreateLogJob job, WitsmlWellbore targetWellbore) - { - IndexType indexType = job.LogObject.IndexCurve == IndexType.Depth.ToString() ? IndexType.Depth : IndexType.Time; - string unit = indexType == IndexType.Depth ? DepthUnit.Meter.ToString() : Unit.TimeUnit.ToString(); - return new WitsmlLogs - { - Logs = new WitsmlLog - { - UidWell = targetWellbore.UidWell, - NameWell = targetWellbore.NameWell, - UidWellbore = targetWellbore.Uid, - NameWellbore = targetWellbore.Name, - Uid = job.LogObject.Uid, - Name = job.LogObject.Name, - RunNumber = job.LogObject.RunNumber, - ServiceCompany = job.LogObject.ServiceCompany, - IndexType = indexType == IndexType.Depth ? WitsmlLog.WITSML_INDEX_TYPE_MD : WitsmlLog.WITSML_INDEX_TYPE_DATE_TIME, - IndexCurve = new WitsmlIndexCurve() - { - Value = indexType.ToString() - }, - LogCurveInfo = new WitsmlLogCurveInfo - { - Uid = Guid.NewGuid().ToString(), - Mnemonic = indexType.ToString(), - Unit = unit, - TypeLogData = indexType == IndexType.Depth ? WitsmlLogCurveInfo.LogDataTypeDouble : WitsmlLogCurveInfo.LogDataTypeDatetime - }.AsItemInList() - }.AsItemInList() - }; - } - - private enum IndexType - { - Depth, - Time - } - - private static async Task GetWellbore(IWitsmlClient client, LogObject logObject) - { - WitsmlWellbores query = WellboreQueries.GetWitsmlWellboreByUid(logObject.WellUid, logObject.WellboreUid); - WitsmlWellbores wellbores = await client.GetFromStoreAsync(query, new OptionsIn(ReturnElements.Requested)); - return wellbores.Wellbores.FirstOrDefault(); - } - } -} diff --git a/Src/WitsmlExplorer.Api/Workers/Create/CreateMudLogWorker.cs b/Src/WitsmlExplorer.Api/Workers/Create/CreateMudLogWorker.cs deleted file mode 100644 index ceee3a87b..000000000 --- a/Src/WitsmlExplorer.Api/Workers/Create/CreateMudLogWorker.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.Extensions.Logging; - -using Witsml; -using Witsml.Data.MudLog; -using Witsml.ServiceReference; - -using WitsmlExplorer.Api.Jobs; -using WitsmlExplorer.Api.Models; -using WitsmlExplorer.Api.Query; -using WitsmlExplorer.Api.Services; - -namespace WitsmlExplorer.Api.Workers.Create -{ - public class CreateMudLogWorker : BaseWorker, IWorker - { - public JobType JobType => JobType.CreateMudLog; - - public CreateMudLogWorker(ILogger logger, IWitsmlClientProvider witsmlClientProvider) : base(witsmlClientProvider, logger) { } - - public override async Task<(WorkerResult, RefreshAction)> Execute(CreateMudLogJob job, CancellationToken? cancellationToken = null) - { - MudLog mudLog = job.MudLog; - Verify(mudLog); - - WitsmlMudLogs mudLogToCreate = mudLog.ToWitsml(); - - QueryResult result = await GetTargetWitsmlClientOrThrow().AddToStoreAsync(mudLogToCreate); - if (result.IsSuccessful) - { - await WaitUntilMudLogHasBeenCreated(mudLog); - Logger.LogInformation("MudLog created. {jobDescription}", job.Description()); - WorkerResult workerResult = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), true, $"MudLog created ({mudLog.Name} [{mudLog.Uid}])"); - RefreshObjects refreshAction = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), mudLog.WellUid, mudLog.WellboreUid, EntityType.MudLog); - return (workerResult, refreshAction); - } - - EntityDescription description = new() { WellboreName = mudLog.WellboreName }; - string errorMessage = "Failed to create mudLog."; - Logger.LogError("{ErrorMessage}. {jobDescription}", errorMessage, job.Description()); - return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, errorMessage, result.Reason, description), null); - } - - private async Task WaitUntilMudLogHasBeenCreated(MudLog mudLog) - { - bool isMudLogCreated = false; - WitsmlMudLogs query = MudLogQueries.QueryById(mudLog.WellUid, mudLog.WellboreUid, new string[] { mudLog.Uid }); - int maxRetries = 30; - while (!isMudLogCreated) - { - if (--maxRetries == 0) - { - throw new InvalidOperationException($"Not able to read newly created MudLog with name {mudLog.Name} (id={mudLog.Uid})"); - } - Thread.Sleep(1000); - WitsmlMudLogs mudLogResult = await GetTargetWitsmlClientOrThrow().GetFromStoreAsync(query, new OptionsIn(ReturnElements.IdOnly)); - isMudLogCreated = mudLogResult.MudLogs.Any(); - } - } - - private static void Verify(MudLog mudLog) - { - if (string.IsNullOrEmpty(mudLog.Uid)) - { - throw new InvalidOperationException($"{nameof(mudLog.Uid)} cannot be empty"); - } - - if (string.IsNullOrEmpty(mudLog.Name)) - { - throw new InvalidOperationException($"{nameof(mudLog.Name)} cannot be empty"); - } - } - } -} diff --git a/Src/WitsmlExplorer.Api/Workers/Create/CreateObjectOnWellboreWorker.cs b/Src/WitsmlExplorer.Api/Workers/Create/CreateObjectOnWellboreWorker.cs new file mode 100644 index 000000000..3c0bc06c7 --- /dev/null +++ b/Src/WitsmlExplorer.Api/Workers/Create/CreateObjectOnWellboreWorker.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Witsml; +using Witsml.Data; +using Witsml.Data.Curves; +using Witsml.Extensions; + +using WitsmlExplorer.Api.Jobs; +using WitsmlExplorer.Api.Models; +using WitsmlExplorer.Api.Services; +using WitsmlExplorer.Api.Workers.Modify; + +namespace WitsmlExplorer.Api.Workers.Create +{ + public class CreateObjectOnWellboreWorker : BaseWorker, IWorker + { + public JobType JobType => JobType.CreateObjectOnWellbore; + + public CreateObjectOnWellboreWorker(ILogger logger, IWitsmlClientProvider witsmlClientProvider) : base(witsmlClientProvider, logger) { } + + public override async Task<(WorkerResult, RefreshAction)> Execute(CreateObjectOnWellboreJob job, CancellationToken? cancellationToken = null) + { + ObjectOnWellbore obj = job.Object; + EntityType objectType = job.ObjectType; + + Logger.LogInformation("Started {JobType}. {jobDescription}", JobType, job.Description()); + + try + { + ModifyUtils.VerifyCreationValues(obj); + } + catch (Exception e) + { + Logger.LogError("{JobType} - Job validation failed", JobType); + return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, e.Message, ""), null); + } + + IWitsmlQueryType query = obj.ToWitsml(); + + if (job.ObjectType == EntityType.Log) + { + LogObject log = (LogObject)job.Object; + ((WitsmlLogs)query).Logs.First().LogCurveInfo = new WitsmlLogCurveInfo + { + Uid = Guid.NewGuid().ToString(), + Mnemonic = log.IndexCurve, + Unit = log.IndexType == WitsmlLog.WITSML_INDEX_TYPE_MD ? DepthUnit.Meter.ToString() : Unit.TimeUnit.ToString(), + TypeLogData = log.IndexType == WitsmlLog.WITSML_INDEX_TYPE_MD ? WitsmlLogCurveInfo.LogDataTypeDouble : WitsmlLogCurveInfo.LogDataTypeDatetime + }.AsItemInList(); + } + + QueryResult createResult = await GetTargetWitsmlClientOrThrow().AddToStoreAsync(query); + + if (!createResult.IsSuccessful) + { + string errorMessage = $"Failed to create {objectType}"; + Logger.LogError("{ErrorMessage}. {jobDescription}", errorMessage, job.Description()); + return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, errorMessage, createResult.Reason), null); + } + + Logger.LogInformation("{objectType} created. {jobDescription}", objectType, job.Description()); + RefreshObjects refreshAction = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), obj.WellUid, obj.WellboreUid, objectType); + WorkerResult workerResult = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), true, $"{objectType} {obj.Name} updated for {obj.WellboreName}"); + + return (workerResult, refreshAction); + } + } +} diff --git a/Src/WitsmlExplorer.Api/Workers/Create/CreateRigWorker.cs b/Src/WitsmlExplorer.Api/Workers/Create/CreateRigWorker.cs deleted file mode 100644 index eeb7d2758..000000000 --- a/Src/WitsmlExplorer.Api/Workers/Create/CreateRigWorker.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.Extensions.Logging; - -using Witsml; -using Witsml.Data.Rig; - -using WitsmlExplorer.Api.Jobs; -using WitsmlExplorer.Api.Models; -using WitsmlExplorer.Api.Services; - -namespace WitsmlExplorer.Api.Workers.Create; - -/// -/// Worker for creating new rig by specific well and wellbore. -/// -public class CreateRigWorker : BaseWorker, IWorker -{ - public CreateRigWorker(ILogger logger, IWitsmlClientProvider witsmlClientProvider) : base(witsmlClientProvider, logger) { } - public JobType JobType => JobType.CreateRig; - - /// - /// Create new rig on wellbore for witsml client. - /// - /// Job info of created rig. - /// Task of workerResult with refresh objects. - public override async Task<(WorkerResult, RefreshAction)> Execute(CreateRigJob job, CancellationToken? cancellationToken = null) - { - Verify(job.Rig); - - WitsmlRigs rigToCreate = job.Rig.ToWitsml(); - - QueryResult addToStoreResult = await GetTargetWitsmlClientOrThrow().AddToStoreAsync(rigToCreate); - - if (!addToStoreResult.IsSuccessful) - { - string errorMessage = "Failed to create rig."; - Logger.LogError("{ErrorMessage}. {jobDescription}", errorMessage, job.Description()); - return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, errorMessage, addToStoreResult.Reason), null); - } - - Logger.LogInformation("Rig created. {jobDescription}", job.Description()); - RefreshObjects refreshAction = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), job.Rig.WellUid, job.Rig.WellboreUid, EntityType.Rig); - WorkerResult workerResult = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), true, $"Rig {job.Rig.Name} add for {job.Rig.WellboreName}"); - - return (workerResult, refreshAction); - } - - private static void Verify(Rig rig) - { - if (string.IsNullOrEmpty(rig.Uid)) - { - throw new InvalidOperationException($"{nameof(rig.Uid)} cannot be empty"); - } - - if (string.IsNullOrEmpty(rig.Name)) - { - throw new InvalidOperationException($"{nameof(rig.Name)} cannot be empty"); - } - } -} diff --git a/Src/WitsmlExplorer.Api/Workers/Create/CreateRiskWorker.cs b/Src/WitsmlExplorer.Api/Workers/Create/CreateRiskWorker.cs deleted file mode 100644 index 26553e6b5..000000000 --- a/Src/WitsmlExplorer.Api/Workers/Create/CreateRiskWorker.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.Extensions.Logging; - -using Witsml; -using Witsml.Data; -using Witsml.ServiceReference; - -using WitsmlExplorer.Api.Jobs; -using WitsmlExplorer.Api.Models; -using WitsmlExplorer.Api.Query; -using WitsmlExplorer.Api.Services; - -namespace WitsmlExplorer.Api.Workers.Create -{ - - public class CreateRiskWorker : BaseWorker, IWorker - { - public JobType JobType => JobType.CreateRisk; - - public CreateRiskWorker(ILogger logger, IWitsmlClientProvider witsmlClientProvider) : base(witsmlClientProvider, logger) { } - - public override async Task<(WorkerResult, RefreshAction)> Execute(CreateRiskJob job, CancellationToken? cancellationToken = null) - { - Risk risk = job.Risk; - Verify(risk); - - WitsmlRisks riskToCreate = risk.ToWitsml(); - - QueryResult result = await GetTargetWitsmlClientOrThrow().AddToStoreAsync(riskToCreate); - if (result.IsSuccessful) - { - await WaitUntilRiskHasBeenCreated(risk); - Logger.LogInformation("Risk created. {jobDescription}", job.Description()); - WorkerResult workerResult = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), true, $"Risk created ({risk.Name} [{risk.Uid}])"); - RefreshObjects refreshAction = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), risk.WellUid, risk.WellboreUid, EntityType.Risk); - return (workerResult, refreshAction); - } - - EntityDescription description = new() { WellboreName = risk.WellboreName }; - string errorMessage = "Failed to create Risk."; - Logger.LogError("{ErrorMessage}. {jobDescription}", errorMessage, job.Description()); - return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, errorMessage, result.Reason, description), null); - } - private async Task WaitUntilRiskHasBeenCreated(Risk risk) - { - bool isCreated = false; - WitsmlRisks query = RiskQueries.QueryById(risk.WellUid, risk.WellboreUid, risk.Uid); - int maxRetries = 30; - while (!isCreated) - { - if (--maxRetries == 0) - { - throw new InvalidOperationException($"Not able to read newly created Risk with name {risk.Name} (id={risk.Uid})"); - } - Thread.Sleep(1000); - WitsmlRisks riskResult = await GetTargetWitsmlClientOrThrow().GetFromStoreAsync(query, new OptionsIn(ReturnElements.IdOnly)); - isCreated = riskResult.Risks.Any(); - } - } - - private static void Verify(Risk risk) - { - if (string.IsNullOrEmpty(risk.Uid)) - { - throw new InvalidOperationException($"{nameof(risk.Uid)} cannot be empty"); - } - - if (string.IsNullOrEmpty(risk.Name)) - { - throw new InvalidOperationException($"{nameof(risk.Name)} cannot be empty"); - } - } - } -} diff --git a/Src/WitsmlExplorer.Api/Workers/Create/CreateTrajectoryWorker.cs b/Src/WitsmlExplorer.Api/Workers/Create/CreateTrajectoryWorker.cs deleted file mode 100644 index 302d295f5..000000000 --- a/Src/WitsmlExplorer.Api/Workers/Create/CreateTrajectoryWorker.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.Extensions.Logging; - -using Witsml; -using Witsml.Data; - -using WitsmlExplorer.Api.Jobs; -using WitsmlExplorer.Api.Models; -using WitsmlExplorer.Api.Services; - -namespace WitsmlExplorer.Api.Workers.Create; - -/// -/// Worker for creating new trajectory by specific well and wellbore. -/// -public class CreateTrajectoryWorker : BaseWorker, IWorker -{ - public CreateTrajectoryWorker(ILogger logger, IWitsmlClientProvider witsmlClientProvider) : base(witsmlClientProvider, logger) { } - public JobType JobType => JobType.CreateTrajectory; - - /// - /// Create new trajectory on wellbore for witsml client. - /// - /// Job info of created trajectory. - /// Task of workerResult with refresh objects. - public override async Task<(WorkerResult, RefreshAction)> Execute(CreateTrajectoryJob job, CancellationToken? cancellationToken = null) - { - Verify(job.Trajectory); - - WitsmlTrajectories trajectoryToCreate = job.Trajectory.ToWitsml(); - - QueryResult addToStoreResult = await GetTargetWitsmlClientOrThrow().AddToStoreAsync(trajectoryToCreate); - - if (!addToStoreResult.IsSuccessful) - { - string errorMessage = "Failed to create trajectory."; - Logger.LogError("{ErrorMessage}. {jobDescription}", errorMessage, job.Description()); - return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, errorMessage, addToStoreResult.Reason), null); - } - - Logger.LogInformation("Trajectory created. {jobDescription}", job.Description()); - RefreshObjects refreshAction = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), job.Trajectory.WellUid, job.Trajectory.WellboreUid, EntityType.Trajectory); - WorkerResult workerResult = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), true, $"Trajectory {job.Trajectory.Name} add for {job.Trajectory.WellboreName}"); - - return (workerResult, refreshAction); - } - - private static void Verify(Trajectory trajectory) - { - if (string.IsNullOrEmpty(trajectory.Uid)) - { - throw new InvalidOperationException($"{nameof(trajectory.Uid)} cannot be empty"); - } - - if (string.IsNullOrEmpty(trajectory.Name)) - { - throw new InvalidOperationException($"{nameof(trajectory.Name)} cannot be empty"); - } - } -} diff --git a/Src/WitsmlExplorer.Api/Workers/Create/CreateWbGeometryWorker.cs b/Src/WitsmlExplorer.Api/Workers/Create/CreateWbGeometryWorker.cs deleted file mode 100644 index 5f23fd23e..000000000 --- a/Src/WitsmlExplorer.Api/Workers/Create/CreateWbGeometryWorker.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.Extensions.Logging; - -using Witsml; -using Witsml.Data; -using Witsml.Data.Measures; -using Witsml.Extensions; -using Witsml.ServiceReference; - -using WitsmlExplorer.Api.Jobs; -using WitsmlExplorer.Api.Models; -using WitsmlExplorer.Api.Query; -using WitsmlExplorer.Api.Services; - -namespace WitsmlExplorer.Api.Workers.Create -{ - public class CreateWbGeometryWorker : BaseWorker, IWorker - { - public JobType JobType => JobType.CreateWbGeometry; - - public CreateWbGeometryWorker(ILogger logger, IWitsmlClientProvider witsmlClientProvider) : base(witsmlClientProvider, logger) { } - - public override async Task<(WorkerResult, RefreshAction)> Execute(CreateWbGeometryJob job, CancellationToken? cancellationToken = null) - { - WbGeometry wbGeometry = job.WbGeometry; - Verify(wbGeometry); - - WitsmlWbGeometrys wbGeometryToCreate = SetupWbGeometryToCreate(wbGeometry); - - QueryResult result = await GetTargetWitsmlClientOrThrow().AddToStoreAsync(wbGeometryToCreate); - if (result.IsSuccessful) - { - await WaitUntilWbGeometryHasBeenCreated(wbGeometry); - Logger.LogInformation("WbGeometry created. {jobDescription}", job.Description()); - WorkerResult workerResult = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), true, $"WbGeometry created ({wbGeometry.Name} [{wbGeometry.Uid}])"); - RefreshObjects refreshAction = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), wbGeometry.WellUid, wbGeometry.WellboreUid, EntityType.WbGeometry); - return (workerResult, refreshAction); - } - - EntityDescription description = new() { WellboreName = wbGeometry.WellboreName }; - string errorMessage = "Failed to create WbGeometry."; - Logger.LogError("{ErrorMessage}. {jobDescription}", errorMessage, job.Description()); - return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, errorMessage, result.Reason, description), null); - - } - private async Task WaitUntilWbGeometryHasBeenCreated(WbGeometry wbGeometry) - { - bool isCreated = false; - WitsmlWbGeometrys query = WbGeometryQueries.GetWitsmlWbGeometryIdOnly(wbGeometry.WellUid, wbGeometry.WellboreUid, wbGeometry.Uid); - int maxRetries = 30; - while (!isCreated) - { - if (--maxRetries == 0) - { - throw new InvalidOperationException($"Not able to read newly created WbGeometry with name {wbGeometry.Name} (id={wbGeometry.Uid})"); - } - Thread.Sleep(1000); - WitsmlWbGeometrys wbGeometryResult = await GetTargetWitsmlClientOrThrow().GetFromStoreAsync(query, new OptionsIn(ReturnElements.IdOnly)); - isCreated = wbGeometryResult.WbGeometrys.Any(); //Or WbGeometry - } - } - - private static WitsmlWbGeometrys SetupWbGeometryToCreate(WbGeometry wbGeometry) - { - return new WitsmlWbGeometrys - { - WbGeometrys = new WitsmlWbGeometry - { - UidWell = wbGeometry.WellUid, - UidWellbore = wbGeometry.WellboreUid, - Uid = wbGeometry.Name, - Name = wbGeometry.Name, - NameWell = wbGeometry.WellName, - NameWellbore = wbGeometry.WellboreName, - DTimReport = wbGeometry.DTimReport, - MdBottom = wbGeometry.MdBottom != null ? new WitsmlMeasuredDepthCoord { Uom = wbGeometry.MdBottom.Uom, Value = wbGeometry.MdBottom.Value.ToString(CultureInfo.InvariantCulture) } : null, - GapAir = wbGeometry.GapAir != null ? new WitsmlLengthMeasure { Uom = wbGeometry.GapAir.Uom, Value = wbGeometry.GapAir.Value.ToString(CultureInfo.InvariantCulture) } : null, - DepthWaterMean = wbGeometry.DepthWaterMean != null ? new WitsmlLengthMeasure { Uom = wbGeometry.DepthWaterMean.Uom, Value = wbGeometry.DepthWaterMean.Value.ToString(CultureInfo.InvariantCulture) } : null, - CommonData = new WitsmlCommonData - { - ItemState = wbGeometry.CommonData.ItemState, - SourceName = wbGeometry.CommonData.SourceName, - Comments = wbGeometry.CommonData.Comments, - }, - }.AsItemInList() - }; - } - - private static void Verify(WbGeometry wbGeometry) - { - if (string.IsNullOrEmpty(wbGeometry.Uid)) - { - throw new InvalidOperationException($"{nameof(wbGeometry.Uid)} cannot be empty"); - } - - if (string.IsNullOrEmpty(wbGeometry.Name)) - { - throw new InvalidOperationException($"{nameof(wbGeometry.Name)} cannot be empty"); - } - } - } -} diff --git a/Src/WitsmlExplorer.Api/Workers/Modify/BatchModifyObjectsOnWellboreWorker.cs b/Src/WitsmlExplorer.Api/Workers/Modify/BatchModifyObjectsOnWellboreWorker.cs index 2e850a26a..3f2d659fa 100644 --- a/Src/WitsmlExplorer.Api/Workers/Modify/BatchModifyObjectsOnWellboreWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/Modify/BatchModifyObjectsOnWellboreWorker.cs @@ -29,11 +29,10 @@ public BatchModifyObjectsOnWellboreWorker(ILogger ModifyUtils.PrepareModification(obj, objectType, Logger)).ToList(); - try { - objects.ForEach(ModifyUtils.VerifyModification); + objects.ForEach(obj => ModifyUtils.VerifyModificationProperties(obj, objectType, Logger)); + objects.ForEach(ModifyUtils.VerifyModificationValues); } catch (Exception e) { diff --git a/Src/WitsmlExplorer.Api/Workers/Modify/ModifyObjectOnWellboreWorker.cs b/Src/WitsmlExplorer.Api/Workers/Modify/ModifyObjectOnWellboreWorker.cs index b34c7f684..c5db362a4 100644 --- a/Src/WitsmlExplorer.Api/Workers/Modify/ModifyObjectOnWellboreWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/Modify/ModifyObjectOnWellboreWorker.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; using Witsml; using Witsml.Data; @@ -26,11 +27,10 @@ public ModifyObjectOnWellboreWorker(ILogger logger, I Logger.LogInformation("Started {JobType}. {jobDescription}", JobType, job.Description()); - obj = ModifyUtils.PrepareModification(obj, objectType, Logger); - try { - ModifyUtils.VerifyModification(obj); + ModifyUtils.VerifyModificationProperties(obj, objectType, Logger); + ModifyUtils.VerifyModificationValues(obj); } catch (Exception e) { @@ -38,6 +38,8 @@ public ModifyObjectOnWellboreWorker(ILogger logger, I return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, e.Message, ""), null); } + await PreModifyModifications(job); + IWitsmlQueryType query = obj.ToWitsml(); QueryResult modifyResult = await GetTargetWitsmlClientOrThrow().UpdateInStoreAsync(query); @@ -54,5 +56,25 @@ public ModifyObjectOnWellboreWorker(ILogger logger, I return (workerResult, refreshAction); } + + private async Task PreModifyModifications(ModifyObjectOnWellboreJob job) + { + if (job.ObjectType == EntityType.Risk) + { + Risk risk = (Risk)job.Object; + if (!risk.AffectedPersonnel.IsNullOrEmpty()) + { + // AffectedPersonnel can't be modified. So we delete it first, and then run the modification as usual. + WitsmlRisks test = new WitsmlRisk + { + Uid = risk.Uid, + UidWell = risk.WellUid, + UidWellbore = risk.WellboreUid, + AffectedPersonnel = [""] // Warning: The empty string must be included to ensure that AffectedPersonnel is serialized correctly and added to the query. Otherwise we risk deleting the entire risk object! + }.AsItemInWitsmlList(); + await GetTargetWitsmlClientOrThrow().DeleteFromStoreAsync(test); + } + } + } } } diff --git a/Src/WitsmlExplorer.Api/Workers/Modify/ModifyUtils.cs b/Src/WitsmlExplorer.Api/Workers/Modify/ModifyUtils.cs index 1d3122c24..cfdff8622 100644 --- a/Src/WitsmlExplorer.Api/Workers/Modify/ModifyUtils.cs +++ b/Src/WitsmlExplorer.Api/Workers/Modify/ModifyUtils.cs @@ -11,6 +11,14 @@ namespace WitsmlExplorer.Api.Workers.Modify { public static class ModifyUtils { + public static void VerifyNotNull(object value, string name) + { + if (value == null) + { + throw new InvalidOperationException($"{name} cannot be null"); + } + } + public static void VerifyMeasure(Measure measure, string name) { if (measure == null) @@ -47,6 +55,13 @@ public static void VerifyAllowedValues(string value, List allowedValues, } } + private static readonly List ObjectReferenceProperties = new List + { + nameof(ObjectOnWellbore.Uid), + nameof(ObjectOnWellbore.WellUid), + nameof(ObjectOnWellbore.WellboreUid) + }; + // Properties not used when converting to WITSML can safely be added here. private static readonly Dictionary> AllowedPropertiesToChange = new Dictionary> { @@ -74,6 +89,10 @@ public static void VerifyAllowedValues(string value, List allowedValues, EntityType.FluidsReport, new HashSet { nameof(FluidsReport.Name), + nameof(FluidsReport.DTim), + nameof(FluidsReport.Md), + nameof(FluidsReport.Tvd), + nameof(FluidsReport.NumReport), nameof(FormationMarker.CommonData) } }, @@ -161,6 +180,7 @@ public static void VerifyAllowedValues(string value, List allowedValues, nameof(Risk.DTimEnd), nameof(Risk.MdBitStart), nameof(Risk.MdBitEnd), + nameof(Risk.SeverityLevel), nameof(Risk.ProbabilityLevel), nameof(Risk.Summary), nameof(Risk.Details), @@ -196,53 +216,43 @@ public static void VerifyAllowedValues(string value, List allowedValues, }, }; - public static ObjectOnWellbore PrepareModification(ObjectOnWellbore obj, EntityType objectType, ILogger logger) + public static void VerifyModificationProperties(ObjectOnWellbore obj, EntityType objectType, ILogger logger) { if (!AllowedPropertiesToChange.TryGetValue(objectType, out var allowedProperties)) { throw new NotSupportedException($"ObjectType '{objectType}' is not supported"); } - return SetNotAllowedPropertiesToNull(obj, allowedProperties, logger); - } - - private static ObjectOnWellbore SetNotAllowedPropertiesToNull(ObjectOnWellbore obj, HashSet allowedPropertiesToChange, ILogger logger) - { - // The uids should not be changed, but are needed to identify the object - allowedPropertiesToChange.Add(nameof(obj.WellUid)); - allowedPropertiesToChange.Add(nameof(obj.WellboreUid)); - allowedPropertiesToChange.Add(nameof(obj.Uid)); - foreach (var property in obj.GetType().GetProperties()) { - if (!allowedPropertiesToChange.Contains(property.Name) && property.GetValue(obj) != null) + if (!allowedProperties.Contains(property.Name) && !ObjectReferenceProperties.Contains(property.Name) && property.GetValue(obj) != null) { - logger.LogWarning("Property '{propertyName}' should not be changed and will be set to null. If the change is intended, please update the AllowedPropertiesToChange list in ModifyUtils.cs", property.Name); - property.SetValue(obj, null); + throw new ArgumentException($"Modifying {property.Name} for a {objectType} is prohibited"); } } - - return obj; } - public static void VerifyModification(ObjectOnWellbore obj) + public static void VerifyCreationValues(ObjectOnWellbore obj) { - if (obj == null) - { - throw new InvalidOperationException("Object cannot be null"); - } - if (string.IsNullOrEmpty(obj.WellUid)) - { - throw new InvalidOperationException("WellUid cannot be empty"); - } - if (string.IsNullOrEmpty(obj.WellboreUid)) - { - throw new InvalidOperationException("WellboreUid cannot be empty"); - } - if (string.IsNullOrEmpty(obj.Uid)) + VerifyModificationValues(obj); + VerifyNotNull(obj.Name, nameof(obj.Name)); + switch (obj) { - throw new InvalidOperationException("Uid cannot be empty"); + case LogObject log: + VerifyLogCreation(log); + break; } + } + + public static void VerifyModificationValues(ObjectOnWellbore obj) + { + VerifyNotNull(obj, obj.GetType().Name); + VerifyNotNull(obj.WellUid, nameof(obj.WellUid)); + VerifyNotNull(obj.WellboreUid, nameof(obj.WellboreUid)); + VerifyNotNull(obj.Uid, nameof(obj.Uid)); + VerifyString(obj.WellUid, nameof(obj.WellUid)); + VerifyString(obj.WellboreUid, nameof(obj.WellboreUid)); + VerifyString(obj.Uid, nameof(obj.Uid)); VerifyString(obj.Name, nameof(obj.Name)); switch (obj) @@ -326,11 +336,19 @@ private static void VerifyFormationMarker(FormationMarker formationMarker) private static void VerifyLog(LogObject log) { + VerifyString(log.IndexType, nameof(log.IndexType)); + VerifyString(log.IndexCurve, nameof(log.IndexCurve)); VerifyString(log.ServiceCompany, nameof(log.ServiceCompany)); VerifyString(log.RunNumber, nameof(log.RunNumber), 16); VerifyAllowedValues(log.CommonData?.ItemState, _allowedItemStates, "CommonData.ItemState"); } + private static void VerifyLogCreation(LogObject log) + { + VerifyNotNull(log.IndexType, nameof(log.IndexType)); + VerifyNotNull(log.IndexCurve, nameof(log.IndexCurve)); + } + private static void VerifyMessage(MessageObject message) { VerifyAllowedValues(message.CommonData?.ItemState, _allowedItemStates, "CommonData.ItemState"); diff --git a/Src/WitsmlExplorer.Frontend/components/Constants.tsx b/Src/WitsmlExplorer.Frontend/components/Constants.tsx index 102ca5f4c..3954dcd87 100644 --- a/Src/WitsmlExplorer.Frontend/components/Constants.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Constants.tsx @@ -1,5 +1,8 @@ export const WITSML_INDEX_TYPE_MD = "measured depth"; export const WITSML_INDEX_TYPE_DATE_TIME = "date time"; +export type WITSML_INDEX_TYPE = + | typeof WITSML_INDEX_TYPE_MD + | typeof WITSML_INDEX_TYPE_DATE_TIME; export const WITSML_LOG_ORDERTYPE_DECREASING = "decreasing"; export const DateFormat = { diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboresListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboresListView.tsx index 34f63e763..633ae83c5 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboresListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboresListView.tsx @@ -50,9 +50,13 @@ export default function WellboresListView() { const columns: ContentTableColumn[] = [ { property: "name", label: "name", type: ContentType.String }, - { property: "wellType", label: "typeWellbore", type: ContentType.String }, { - property: "wellStatus", + property: "wellboreType", + label: "typeWellbore", + type: ContentType.String + }, + { + property: "wellboreStatus", label: "statusWellbore", type: ContentType.String }, diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/BatchModifyMenuItem.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/BatchModifyMenuItem.tsx index 961c38129..4915c35d5 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/BatchModifyMenuItem.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/BatchModifyMenuItem.tsx @@ -1,18 +1,14 @@ import { Typography } from "@equinor/eds-core-react"; import { MenuItem } from "@mui/material"; +import { getBatchModifyObjectOnWellboreProperties } from "components/Modals/PropertiesModal/Properties/BatchModifyObjectOnWellboreProperties"; +import { PropertiesModal } from "components/Modals/PropertiesModal/PropertiesModal"; import { useOperationState } from "hooks/useOperationState"; import { ReactElement, forwardRef } from "react"; import OperationType from "../../contexts/operationType"; import ObjectOnWellbore from "../../models/objectOnWellbore"; -import { ObjectType } from "../../models/objectType"; -import { rigType } from "../../models/rigType"; +import { ObjectType, ObjectTypeToModel } from "../../models/objectType"; import JobService, { JobType } from "../../services/jobService"; import { colors } from "../../styles/Colors"; -import { - BatchModifyPropertiesModal, - BatchModifyProperty -} from "../Modals/BatchModifyPropertiesModal"; -import { validText } from "../Modals/ModalParts"; import { ReportModal } from "../Modals/ReportModal"; import { StyledIcon, menuItemText } from "./ContextMenuUtils"; @@ -25,11 +21,13 @@ export const BatchModifyMenuItem = forwardRef( (props: BatchModifyMenuItemProps, ref: React.Ref): ReactElement => { const { checkedObjects, objectType } = props; const { dispatchOperation } = useOperationState(); - const batchModifyProperties = objectBatchModifyProperties[objectType]; + const batchModifyProperties = + getBatchModifyObjectOnWellboreProperties(objectType); const onSubmitBatchModify = async (batchUpdates: { [key: string]: string; }) => { + dispatchOperation({ type: OperationType.HideModal }); const objectsToModify = checkedObjects.map((object) => ({ uid: object.uid, wellboreUid: object.wellboreUid, @@ -59,12 +57,13 @@ export const BatchModifyMenuItem = forwardRef( dispatchOperation({ type: OperationType.HideContextMenu }); const batchModifyModalProps = { title: menuItemText("Batch update", objectType, checkedObjects), + object: {} as ObjectTypeToModel[typeof objectType], properties: batchModifyProperties, onSubmit: onSubmitBatchModify }; dispatchOperation({ type: OperationType.DisplayModal, - payload: + payload: }); }; @@ -85,65 +84,3 @@ export const BatchModifyMenuItem = forwardRef( ); BatchModifyMenuItem.displayName = "BatchModifyMenuItem"; - -// Note: Only add properties that can be updated directly (without having to create a new object and delete the old one) -export const objectBatchModifyProperties: { - [key in ObjectType]?: BatchModifyProperty[]; -} = { - [ObjectType.BhaRun]: [], - [ObjectType.ChangeLog]: [], - [ObjectType.FluidsReport]: [], - [ObjectType.FormationMarker]: [], - [ObjectType.Log]: [ - { - property: "name", - validator: (value: string) => validText(value, 0, 64), - helperText: "Name must be less than 64 characters" - }, - { - property: "runNumber", - validator: (value: string) => validText(value, 0, 16), - helperText: "Run number must be less than 16 characters" - }, - { - property: "commonData.comments" - } - ], - [ObjectType.Message]: [], - [ObjectType.MudLog]: [], - [ObjectType.Rig]: [ - { - property: "owner", - validator: (value: string) => validText(value, 0, 32), - helperText: "Owner must be less than 32 characters" - }, - { - property: "typeRig", - options: rigType - }, - { - property: "manufacturer", - validator: (value: string) => validText(value, 0, 64), - helperText: "Owner must be less than 64 characters" - }, - { - property: "classRig", - validator: (value: string) => validText(value, 0, 32), - helperText: "Owner must be less than 32 characters" - }, - { - property: "approvals", - validator: (value: string) => validText(value, 0, 64), - helperText: "Owner must be less than 64 characters" - }, - { - property: "registration", - validator: (value: string) => validText(value, 0, 32), - helperText: "Owner must be less than 32 characters" - } - ], - [ObjectType.Risk]: [], - [ObjectType.Trajectory]: [], - [ObjectType.Tubular]: [], - [ObjectType.WbGeometry]: [] -}; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/BhaRunContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/BhaRunContextMenu.tsx index c77c4d863..a1b76305f 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/BhaRunContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/BhaRunContextMenu.tsx @@ -7,12 +7,8 @@ import { ObjectContextMenuProps, ObjectMenuItems } from "components/ContextMenus/ObjectMenuItems"; -import BhaRunPropertiesModal, { - BhaRunPropertiesModalProps -} from "components/Modals/BhaRunPropertiesModal"; -import { PropertiesModalMode } from "components/Modals/ModalParts"; +import { openObjectOnWellboreProperties } from "components/Modals/PropertiesModal/openPropertiesHelpers"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; import { useOperationState } from "hooks/useOperationState"; @@ -31,20 +27,6 @@ const BhaRunContextMenu = ( const { connectedServer } = useConnectedServer(); const queryClient = useQueryClient(); - const onClickModify = async () => { - dispatchOperation({ type: OperationType.HideContextMenu }); - const mode = PropertiesModalMode.Edit; - const modifyBhaRunProps: BhaRunPropertiesModalProps = { - mode, - bhaRun: checkedObjects[0] as BhaRun, - dispatchOperation - }; - dispatchOperation({ - type: OperationType.DisplayModal, - payload: - }); - }; - return ( , + openObjectOnWellboreProperties( + ObjectType.BhaRun, + checkedObjects?.[0] as BhaRun, + dispatchOperation + ) + } disabled={checkedObjects.length !== 1} > { dispatchOperation({ type: OperationType.HideContextMenu }); let url = ""; - if (objectType === ObjectType.Log && indexCurve) { + if (objectType === ObjectType.Log && indexType) { const logTypePath = - indexCurve === IndexCurve.Depth + indexType === WITSML_INDEX_TYPE_MD ? RouterLogType.DEPTH : RouterLogType.TIME; url = getLogObjectsViewPath( diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/FluidsReportContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/FluidsReportContextMenu.tsx index aa1106357..37a3f0065 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/FluidsReportContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/FluidsReportContextMenu.tsx @@ -1,4 +1,4 @@ -import { Typography } from "@equinor/eds-core-react"; +import { Divider, Typography } from "@equinor/eds-core-react"; import { MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; import ContextMenu from "components/ContextMenus/ContextMenu"; @@ -12,11 +12,13 @@ import { ObjectMenuItems } from "components/ContextMenus/ObjectMenuItems"; import { useClipboardComponentReferencesOfType } from "components/ContextMenus/UseClipboardComponentReferences"; +import { openObjectOnWellboreProperties } from "components/Modals/PropertiesModal/openPropertiesHelpers"; import { useConnectedServer } from "contexts/connectedServerContext"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; +import FluidsReport from "models/fluidsReport"; import { ObjectType } from "models/objectType"; import React from "react"; import { colors } from "styles/Colors"; @@ -68,7 +70,25 @@ const FluidsReportContextMenu = ( queryClient, openInQueryView, extraMenuItems() - ) + ), + , + + openObjectOnWellboreProperties( + ObjectType.FluidsReport, + checkedObjects?.[0] as FluidsReport, + dispatchOperation + ) + } + disabled={checkedObjects.length !== 1} + > + + Properties + ]} /> ); diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/FormationMarkerContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/FormationMarkerContextMenu.tsx index 2a8c6eb74..1b80500ff 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/FormationMarkerContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/FormationMarkerContextMenu.tsx @@ -7,11 +7,8 @@ import { ObjectContextMenuProps, ObjectMenuItems } from "components/ContextMenus/ObjectMenuItems"; -import FormationMarkerPropertiesModal, { - FormationMarkerPropertiesModalProps -} from "components/Modals/FormationMarkerPropertiesModal"; +import { openObjectOnWellboreProperties } from "components/Modals/PropertiesModal/openPropertiesHelpers"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; import { useOperationState } from "hooks/useOperationState"; @@ -30,25 +27,18 @@ const FormationMarkerContextMenu = ( const queryClient = useQueryClient(); const { servers } = useGetServers(); - const onClickModify = async () => { - dispatchOperation({ type: OperationType.HideContextMenu }); - const modifyFormationMarkerProps: FormationMarkerPropertiesModalProps = { - formationMarker: checkedObjects[0] as FormationMarker - }; - dispatchOperation({ - type: OperationType.DisplayModal, - payload: ( - - ) - }); - }; - const extraMenuItems = (): React.ReactElement[] => { return [ , + openObjectOnWellboreProperties( + ObjectType.FormationMarker, + checkedObjects?.[0] as FormationMarker, + dispatchOperation + ) + } disabled={checkedObjects.length !== 1} > diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/GeologyIntervalContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/GeologyIntervalContextMenu.tsx index ee8c768b2..cfe7bd9af 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/GeologyIntervalContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/GeologyIntervalContextMenu.tsx @@ -12,7 +12,12 @@ import { pasteComponents } from "components/ContextMenus/CopyUtils"; import { useClipboardComponentReferencesOfType } from "components/ContextMenus/UseClipboardComponentReferences"; -import GeologyIntervalPropertiesModal from "components/Modals/GeologyIntervalPropertiesModal"; +import { PropertiesModalMode } from "components/Modals/ModalParts"; +import { getGeologyIntervalProperties } from "components/Modals/PropertiesModal/Properties/GeologyIntervalProperties"; +import { + PropertiesModal, + PropertiesModalProps +} from "components/Modals/PropertiesModal/PropertiesModal"; import { useConnectedServer } from "contexts/connectedServerContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; @@ -20,9 +25,11 @@ import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import GeologyInterval from "models/geologyInterval"; import { createComponentReferences } from "models/jobs/componentReferences"; +import ObjectReference from "models/jobs/objectReference"; import MudLog from "models/mudLog"; +import { toObjectReference } from "models/objectOnWellbore"; import React from "react"; -import { JobType } from "services/jobService"; +import JobService, { JobType } from "services/jobService"; import { colors } from "styles/Colors"; export interface GeologyIntervalContextMenuProps { @@ -43,17 +50,30 @@ const GeologyIntervalContextMenu = ( const onClickProperties = async () => { dispatchOperation({ type: OperationType.HideContextMenu }); - const geologyIntervalPropertiesModalProps = { - geologyInterval: checkedGeologyIntervals[0], - mudLog - }; + const geologyIntervalPropertiesModalProps: PropertiesModalProps = + { + title: `Edit properties for ${checkedGeologyIntervals[0].uid}`, + object: checkedGeologyIntervals[0], + properties: getGeologyIntervalProperties(PropertiesModalMode.Edit), + onSubmit: async (updates: Partial) => { + dispatchOperation({ type: OperationType.HideModal }); + const mudLogReference: ObjectReference = toObjectReference(mudLog); + const modifyGeologyIntervalJob = { + geologyInterval: { + uid: checkedGeologyIntervals[0].uid, + ...updates + }, + mudLogReference + }; + await JobService.orderJob( + JobType.ModifyGeologyInterval, + modifyGeologyIntervalJob + ); + } + }; dispatchOperation({ type: OperationType.DisplayModal, - payload: ( - - ) + payload: }); }; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurveInfoContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurveInfoContextMenu.tsx index 60d0cfb1d..acaaf7bf4 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurveInfoContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurveInfoContextMenu.tsx @@ -18,16 +18,17 @@ import AnalyzeGapModal, { import CopyRangeModal, { CopyRangeModalProps } from "components/Modals/CopyRangeModal"; -import LogCurveInfoPropertiesModal from "components/Modals/LogCurveInfoPropertiesModal"; import { LogCurvePriorityModal, LogCurvePriorityModalProps } from "components/Modals/LogCurvePriorityModal"; -import { IndexCurve } from "components/Modals/LogPropertiesModal"; +import { PropertiesModalMode } from "components/Modals/ModalParts"; import { OffsetLogCurveModal, OffsetLogCurveModalProps } from "components/Modals/OffsetLogCurveModal"; +import { getLogCurveInfoProperties } from "components/Modals/PropertiesModal/Properties/LogCurveInfoProperties"; +import { PropertiesModal } from "components/Modals/PropertiesModal/PropertiesModal"; import SelectIndexToDisplayModal from "components/Modals/SelectIndexToDisplayModal"; import { DisplayModalAction, @@ -36,12 +37,16 @@ import { } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; import { ComponentType } from "models/componentType"; +import { IndexCurve } from "models/indexCurve"; import { createComponentReferences } from "models/jobs/componentReferences"; +import ModifyLogCurveInfoJob from "models/jobs/modifyLogCurveInfoJob"; +import LogCurveInfo from "models/logCurveInfo"; import LogObject from "models/logObject"; +import { toObjectReference } from "models/objectOnWellbore"; import { ObjectType } from "models/objectType"; import { Server } from "models/server"; import React from "react"; -import { JobType } from "services/jobService"; +import JobService, { JobType } from "services/jobService"; import LogCurvePriorityService from "services/logCurvePriorityService"; import { colors } from "styles/Colors"; import LogCurveInfoBatchUpdateModal from "../Modals/LogCurveInfoBatchUpdateModal"; @@ -114,15 +119,27 @@ const LogCurveInfoContextMenu = ( dispatchOperation({ type: OperationType.HideContextMenu }); const logCurveInfo = checkedLogCurveInfoRows[0].logCurveInfo; const logCurveInfoPropertiesModalProps = { - logCurveInfo, - dispatchOperation, - selectedLog + title: `Edit properties for LogCurve: ${logCurveInfo.mnemonic}`, + properties: getLogCurveInfoProperties( + PropertiesModalMode.Edit, + logCurveInfo?.mnemonic === selectedLog?.indexCurve + ), + object: logCurveInfo, + onSubmit: async (updates: Partial) => { + dispatchOperation({ type: OperationType.HideModal }); + const job: ModifyLogCurveInfoJob = { + logReference: toObjectReference(selectedLog), + logCurveInfo: { + ...logCurveInfo, + ...updates + } + }; + await JobService.orderJob(JobType.ModifyLogCurveInfo, job); + } }; dispatchOperation({ type: OperationType.DisplayModal, - payload: ( - - ) + payload: }); }; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogObjectContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogObjectContextMenu.tsx index 1fa929a2f..91e7aeebf 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogObjectContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogObjectContextMenu.tsx @@ -28,11 +28,10 @@ import LogComparisonModal, { import LogDataImportModal, { LogDataImportModalProps } from "components/Modals/LogDataImportModal"; -import LogPropertiesModal from "components/Modals/LogPropertiesModal"; -import { PropertiesModalMode } from "components/Modals/ModalParts"; import ObjectPickerModal, { ObjectPickerProps } from "components/Modals/ObjectPickerModal"; +import { openObjectOnWellboreProperties } from "components/Modals/PropertiesModal/openPropertiesHelpers"; import { ReportModal } from "components/Modals/ReportModal"; import SpliceLogsModal from "components/Modals/SpliceLogsModal"; import TrimLogObjectModal from "components/Modals/TrimLogObject/TrimLogObjectModal"; @@ -78,20 +77,6 @@ const LogObjectContextMenu = ( const queryClient = useQueryClient(); const navigate = useNavigate(); - const onClickProperties = () => { - dispatchOperation({ type: OperationType.HideContextMenu }); - const logObject = checkedObjects[0]; - const logPropertiesModalProps = { - mode: PropertiesModalMode.Edit, - logObject, - dispatchOperation - }; - dispatchOperation({ - type: OperationType.DisplayModal, - payload: - }); - }; - const onClickTrimLogObject = () => { const logObject = checkedObjects[0]; dispatchOperation({ @@ -458,7 +443,13 @@ const LogObjectContextMenu = ( , + openObjectOnWellboreProperties( + ObjectType.Log, + checkedObjects?.[0] as LogObject, + dispatchOperation + ) + } disabled={checkedObjects.length !== 1} > diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogsContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogsContextMenu.tsx index a2181b41f..7cd2e3e23 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogsContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogsContextMenu.tsx @@ -1,6 +1,11 @@ import { Typography } from "@equinor/eds-core-react"; import { MenuItem } from "@mui/material"; import { useQueryClient } from "@tanstack/react-query"; +import { + WITSML_INDEX_TYPE, + WITSML_INDEX_TYPE_DATE_TIME, + WITSML_INDEX_TYPE_MD +} from "components/Constants"; import { StoreFunction, TemplateObjects @@ -15,19 +20,16 @@ import { import { pasteObjectOnWellbore } from "components/ContextMenus/CopyUtils"; import NestedMenuItem from "components/ContextMenus/NestedMenuItem"; import { useClipboardReferencesOfType } from "components/ContextMenus/UseClipboardReferences"; -import LogPropertiesModal, { - IndexCurve, - LogPropertiesModalInterface -} from "components/Modals/LogPropertiesModal"; import { PropertiesModalMode } from "components/Modals/ModalParts"; +import { openObjectOnWellboreProperties } from "components/Modals/PropertiesModal/openPropertiesHelpers"; import { useConnectedServer } from "contexts/connectedServerContext"; import { DisplayModalAction, HideContextMenuAction, HideModalAction } from "contexts/operationStateReducer"; -import OperationType from "contexts/operationType"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; +import { IndexCurve } from "models/indexCurve"; import { toWellboreReference } from "models/jobs/wellboreReference"; import LogObject from "models/logObject"; import { ObjectType } from "models/objectType"; @@ -43,11 +45,11 @@ export interface LogsContextMenuProps { ) => void; wellbore: Wellbore; servers: Server[]; - indexCurve?: IndexCurve; + indexType?: WITSML_INDEX_TYPE; } const LogsContextMenu = (props: LogsContextMenuProps): React.ReactElement => { - const { dispatchOperation, wellbore, servers, indexCurve } = props; + const { dispatchOperation, wellbore, servers, indexType } = props; const logReferences = useClipboardReferencesOfType(ObjectType.Log); const openInQueryView = useOpenInQueryView(); const { connectedServer } = useConnectedServer(); @@ -61,19 +63,18 @@ const LogsContextMenu = (props: LogsContextMenuProps): React.ReactElement => { wellName: wellbore.wellName, wellboreUid: wellbore.uid, wellboreName: wellbore.name, + indexType: indexType ?? WITSML_INDEX_TYPE_MD, indexCurve: - indexCurve === IndexCurve.Time ? IndexCurve.Time : IndexCurve.Depth - }; - const logPropertiesModalProps: LogPropertiesModalInterface = { - mode: PropertiesModalMode.New, - logObject: newLog, - dispatchOperation - }; - const action: DisplayModalAction = { - type: OperationType.DisplayModal, - payload: + indexType === WITSML_INDEX_TYPE_DATE_TIME + ? IndexCurve.Time + : IndexCurve.Depth }; - dispatchOperation(action); + openObjectOnWellboreProperties( + ObjectType.Log, + newLog, + dispatchOperation, + PropertiesModalMode.New + ); }; return ( @@ -129,7 +130,7 @@ const LogsContextMenu = (props: LogsContextMenuProps): React.ReactElement => { server, wellbore, ObjectType.Log, - indexCurve + indexType ) } > diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/MessageObjectContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/MessageObjectContextMenu.tsx index 928e1ce53..4ece3dbc9 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/MessageObjectContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/MessageObjectContextMenu.tsx @@ -13,13 +13,10 @@ import { import MessageComparisonModal, { MessageComparisonModalProps } from "components/Modals/MessageComparisonModal"; -import MessagePropertiesModal, { - MessagePropertiesModalProps -} from "components/Modals/MessagePropertiesModal"; -import { PropertiesModalMode } from "components/Modals/ModalParts"; import ObjectPickerModal, { ObjectPickerProps } from "components/Modals/ObjectPickerModal"; +import { openObjectOnWellboreProperties } from "components/Modals/PropertiesModal/openPropertiesHelpers"; import { useConnectedServer } from "contexts/connectedServerContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; @@ -42,20 +39,6 @@ const MessageObjectContextMenu = ( const queryClient = useQueryClient(); const { servers } = useGetServers(); - const onClickModify = async () => { - dispatchOperation({ type: OperationType.HideContextMenu }); - const mode = PropertiesModalMode.Edit; - const modifyMessageObjectProps: MessagePropertiesModalProps = { - mode, - messageObject: checkedObjects[0] as MessageObject, - dispatchOperation - }; - dispatchOperation({ - type: OperationType.DisplayModal, - payload: - }); - }; - const onClickCompare = () => { dispatchOperation({ type: OperationType.HideContextMenu }); const onPicked = (targetObject: ObjectOnWellbore, targetServer: Server) => { @@ -99,7 +82,13 @@ const MessageObjectContextMenu = ( , + openObjectOnWellboreProperties( + ObjectType.Message, + checkedObjects?.[0] as MessageObject, + dispatchOperation + ) + } disabled={checkedObjects.length !== 1} > diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/MudLogContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/MudLogContextMenu.tsx index 3601d829a..6dfd6a3d8 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/MudLogContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/MudLogContextMenu.tsx @@ -12,11 +12,8 @@ import { ObjectMenuItems } from "components/ContextMenus/ObjectMenuItems"; import { useClipboardComponentReferencesOfType } from "components/ContextMenus/UseClipboardComponentReferences"; -import MudLogPropertiesModal, { - MudLogPropertiesModalProps -} from "components/Modals/MudLogPropertiesModal"; +import { openObjectOnWellboreProperties } from "components/Modals/PropertiesModal/openPropertiesHelpers"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; import { useOperationState } from "hooks/useOperationState"; @@ -39,17 +36,6 @@ const MudLogContextMenu = ( const { connectedServer } = useConnectedServer(); const queryClient = useQueryClient(); - const onClickModify = async () => { - dispatchOperation({ type: OperationType.HideContextMenu }); - const modifyMudLogProps: MudLogPropertiesModalProps = { - mudLog: checkedObjects[0] as MudLog - }; - dispatchOperation({ - type: OperationType.DisplayModal, - payload: - }); - }; - const extraMenuItems = (): React.ReactElement[] => { return [ , + openObjectOnWellboreProperties( + ObjectType.MudLog, + checkedObjects?.[0] as MudLog, + dispatchOperation + ) + } disabled={checkedObjects.length !== 1} > diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectMenuItems.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectMenuItems.tsx index a71719875..6e07a0001 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectMenuItems.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectMenuItems.tsx @@ -20,9 +20,9 @@ import { } from "components/ContextMenus/CopyUtils"; import NestedMenuItem from "components/ContextMenus/NestedMenuItem"; import { useClipboardReferencesOfType } from "components/ContextMenus/UseClipboardReferences"; -import { IndexCurve } from "components/Modals/LogPropertiesModal"; import { DispatchOperation } from "contexts/operationStateReducer"; import { OpenInQueryView } from "hooks/useOpenInQueryView"; +import { IndexCurve } from "models/indexCurve"; import LogObject from "models/logObject"; import ObjectOnWellbore from "models/objectOnWellbore"; import { ObjectType } from "models/objectType"; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectsSidebarContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectsSidebarContextMenu.tsx index 431f2a14f..84331d19d 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectsSidebarContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectsSidebarContextMenu.tsx @@ -16,11 +16,14 @@ import { import { pasteObjectOnWellbore } from "components/ContextMenus/CopyUtils"; import NestedMenuItem from "components/ContextMenus/NestedMenuItem"; import { useClipboardReferencesOfType } from "components/ContextMenus/UseClipboardReferences"; +import { PropertiesModalMode } from "components/Modals/ModalParts"; +import { openObjectOnWellboreProperties } from "components/Modals/PropertiesModal/openPropertiesHelpers"; import { useConnectedServer } from "contexts/connectedServerContext"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; import { useOperationState } from "hooks/useOperationState"; import { toWellboreReference } from "models/jobs/wellboreReference"; +import ObjectOnWellbore from "models/objectOnWellbore"; import { ObjectType } from "models/objectType"; import { Server } from "models/server"; import Wellbore from "models/wellbore"; @@ -44,6 +47,23 @@ const ObjectsSidebarContextMenu = ( const { connectedServer } = useConnectedServer(); const queryClient = useQueryClient(); + const onClickNewObject = () => { + const newObject: ObjectOnWellbore = { + uid: uuid(), + name: "", + wellUid: wellbore.wellUid, + wellName: wellbore.wellName, + wellboreUid: wellbore.uid, + wellboreName: wellbore.name + }; + openObjectOnWellboreProperties( + objectType, + newObject, + dispatchOperation, + PropertiesModalMode.New + ); + }; + return ( , + + + New {objectType} + , diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigContextMenu.tsx index 940d073a0..5c47e3753 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigContextMenu.tsx @@ -8,12 +8,8 @@ import { ObjectContextMenuProps, ObjectMenuItems } from "components/ContextMenus/ObjectMenuItems"; -import { PropertiesModalMode } from "components/Modals/ModalParts"; -import RigPropertiesModal, { - RigPropertiesModalProps -} from "components/Modals/RigPropertiesModal"; +import { openObjectOnWellboreProperties } from "components/Modals/PropertiesModal/openPropertiesHelpers"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; import { useOperationState } from "hooks/useOperationState"; @@ -30,20 +26,6 @@ const RigContextMenu = (props: ObjectContextMenuProps): React.ReactElement => { const queryClient = useQueryClient(); const { servers } = useGetServers(); - const onClickModify = async () => { - dispatchOperation({ type: OperationType.HideContextMenu }); - const mode = PropertiesModalMode.Edit; - const modifyRigObjectProps: RigPropertiesModalProps = { - mode, - rig: checkedObjects[0] as Rig, - dispatchOperation - }; - dispatchOperation({ - type: OperationType.DisplayModal, - payload: - }); - }; - const extraMenuItems = (): React.ReactElement[] => { return [ , @@ -54,7 +36,13 @@ const RigContextMenu = (props: ObjectContextMenuProps): React.ReactElement => { />, + openObjectOnWellboreProperties( + ObjectType.Rig, + checkedObjects?.[0] as Rig, + dispatchOperation + ) + } disabled={checkedObjects.length !== 1} > diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigsContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigsContextMenu.tsx index bc51ad669..8c003d947 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigsContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/RigsContextMenu.tsx @@ -15,12 +15,8 @@ import { pasteObjectOnWellbore } from "components/ContextMenus/CopyUtils"; import NestedMenuItem from "components/ContextMenus/NestedMenuItem"; import { useClipboardReferencesOfType } from "components/ContextMenus/UseClipboardReferences"; import { PropertiesModalMode } from "components/Modals/ModalParts"; -import RigPropertiesModal, { - RigPropertiesModalProps -} from "components/Modals/RigPropertiesModal"; +import { openObjectOnWellboreProperties } from "components/Modals/PropertiesModal/openPropertiesHelpers"; import { useConnectedServer } from "contexts/connectedServerContext"; -import { DisplayModalAction } from "contexts/operationStateReducer"; -import OperationType from "contexts/operationType"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; import { useOperationState } from "hooks/useOperationState"; import { toWellboreReference } from "models/jobs/wellboreReference"; @@ -71,16 +67,12 @@ const RigsContextMenu = (props: RigsContextMenuProps): React.ReactElement => { typeRig: "unknown", yearEntService: null }; - const rigPropertiesModalProps: RigPropertiesModalProps = { - mode: PropertiesModalMode.New, - rig: newRig, - dispatchOperation - }; - const action: DisplayModalAction = { - type: OperationType.DisplayModal, - payload: - }; - dispatchOperation(action); + openObjectOnWellboreProperties( + ObjectType.Rig, + newRig, + dispatchOperation, + PropertiesModalMode.New + ); }; return ( diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/RiskContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/RiskContextMenu.tsx index 2aa72aaff..913a80f88 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/RiskContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/RiskContextMenu.tsx @@ -7,12 +7,8 @@ import { ObjectContextMenuProps, ObjectMenuItems } from "components/ContextMenus/ObjectMenuItems"; -import { PropertiesModalMode } from "components/Modals/ModalParts"; -import RiskPropertiesModal, { - RiskPropertiesModalProps -} from "components/Modals/RiskPropertiesModal"; +import { openObjectOnWellboreProperties } from "components/Modals/PropertiesModal/openPropertiesHelpers"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; import { useOperationState } from "hooks/useOperationState"; @@ -31,26 +27,18 @@ const RiskObjectContextMenu = ( const { connectedServer } = useConnectedServer(); const queryClient = useQueryClient(); - const onClickModify = async () => { - dispatchOperation({ type: OperationType.HideContextMenu }); - const mode = PropertiesModalMode.Edit; - const modifyRiskObjectProps: RiskPropertiesModalProps = { - mode, - riskObject: checkedObjects[0] as RiskObject, - dispatchOperation - }; - dispatchOperation({ - type: OperationType.DisplayModal, - payload: - }); - }; - const extraMenuItems = (): React.ReactElement[] => { return [ , + openObjectOnWellboreProperties( + ObjectType.Risk, + checkedObjects?.[0] as RiskObject, + dispatchOperation + ) + } disabled={checkedObjects.length !== 1} > diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoriesContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoriesContextMenu.tsx index 62aeafe0b..a31e6231c 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoriesContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoriesContextMenu.tsx @@ -15,12 +15,8 @@ import { pasteObjectOnWellbore } from "components/ContextMenus/CopyUtils"; import NestedMenuItem from "components/ContextMenus/NestedMenuItem"; import { useClipboardReferencesOfType } from "components/ContextMenus/UseClipboardReferences"; import { PropertiesModalMode } from "components/Modals/ModalParts"; -import TrajectoryPropertiesModal, { - TrajectoryPropertiesModalProps -} from "components/Modals/TrajectoryPropertiesModal"; +import { openObjectOnWellboreProperties } from "components/Modals/PropertiesModal/openPropertiesHelpers"; import { useConnectedServer } from "contexts/connectedServerContext"; -import { DisplayModalAction } from "contexts/operationStateReducer"; -import OperationType from "contexts/operationType"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; import { useOperationState } from "hooks/useOperationState"; import { toWellboreReference } from "models/jobs/wellboreReference"; @@ -66,16 +62,12 @@ const TrajectoriesContextMenu = ( trajectoryStations: [], commonData: null }; - const trajectoryPropertiesModalProps: TrajectoryPropertiesModalProps = { - mode: PropertiesModalMode.New, - trajectory: newTrajectory, - dispatchOperation - }; - const action: DisplayModalAction = { - type: OperationType.DisplayModal, - payload: - }; - dispatchOperation(action); + openObjectOnWellboreProperties( + ObjectType.Trajectory, + newTrajectory, + dispatchOperation, + PropertiesModalMode.New + ); }; return ( diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryContextMenu.tsx index e0e38edd0..7d8f7e336 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryContextMenu.tsx @@ -12,12 +12,8 @@ import { ObjectMenuItems } from "components/ContextMenus/ObjectMenuItems"; import { useClipboardComponentReferencesOfType } from "components/ContextMenus/UseClipboardComponentReferences"; -import { PropertiesModalMode } from "components/Modals/ModalParts"; -import TrajectoryPropertiesModal, { - TrajectoryPropertiesModalProps -} from "components/Modals/TrajectoryPropertiesModal"; +import { openObjectOnWellboreProperties } from "components/Modals/PropertiesModal/openPropertiesHelpers"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; import { useOperationState } from "hooks/useOperationState"; @@ -40,20 +36,6 @@ const TrajectoryContextMenu = ( const { connectedServer } = useConnectedServer(); const queryClient = useQueryClient(); - const onClickModify = async () => { - dispatchOperation({ type: OperationType.HideContextMenu }); - const mode = PropertiesModalMode.Edit; - const modifyObjectProps: TrajectoryPropertiesModalProps = { - mode, - trajectory: checkedObjects[0] as Trajectory, - dispatchOperation - }; - dispatchOperation({ - type: OperationType.DisplayModal, - payload: - }); - }; - const extraMenuItems = (): React.ReactElement[] => { return [ , + openObjectOnWellboreProperties( + ObjectType.Trajectory, + checkedObjects?.[0] as Trajectory, + dispatchOperation + ) + } disabled={checkedObjects.length !== 1} > diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryStationContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryStationContextMenu.tsx index b2727e980..f72a09841 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryStationContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryStationContextMenu.tsx @@ -15,18 +15,23 @@ import { } from "components/ContextMenus/CopyUtils"; import NestedMenuItem from "components/ContextMenus/NestedMenuItem"; import { useClipboardComponentReferencesOfType } from "components/ContextMenus/UseClipboardComponentReferences"; -import TrajectoryStationPropertiesModal from "components/Modals/TrajectoryStationPropertiesModal"; +import { PropertiesModalMode } from "components/Modals/ModalParts"; +import { getTrajectoryStationProperties } from "components/Modals/PropertiesModal/Properties/TrajectoryStationProperties"; +import { PropertiesModal } from "components/Modals/PropertiesModal/PropertiesModal"; import { useConnectedServer } from "contexts/connectedServerContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import { createComponentReferences } from "models/jobs/componentReferences"; +import ObjectReference from "models/jobs/objectReference"; +import { toObjectReference } from "models/objectOnWellbore"; import { ObjectType } from "models/objectType"; import { Server } from "models/server"; import Trajectory from "models/trajectory"; +import TrajectoryStation from "models/trajectoryStation"; import React from "react"; -import { JobType } from "services/jobService"; +import JobService, { JobType } from "services/jobService"; import { colors } from "styles/Colors"; export interface TrajectoryStationContextMenuProps { @@ -47,18 +52,31 @@ const TrajectoryStationContextMenu = ( const onClickProperties = async () => { dispatchOperation({ type: OperationType.HideContextMenu }); + const trajectoryStation = checkedTrajectoryStations[0].trajectoryStation; const trajectoryStationPropertiesModalProps = { - trajectoryStation: checkedTrajectoryStations[0].trajectoryStation, - trajectory, - dispatchOperation + title: `Edit properties for Trajectory Station for Trajectory ${trajectoryStation.uid} - ${trajectoryStation.typeTrajStation}`, + properties: getTrajectoryStationProperties(PropertiesModalMode.Edit), + object: trajectoryStation, + onSubmit: async (updates: Partial) => { + dispatchOperation({ type: OperationType.HideModal }); + const trajectoryReference: ObjectReference = + toObjectReference(trajectory); + const modifyTrajectoryStationJob = { + trajectoryStation: { + ...trajectoryStation, + ...updates + }, + trajectoryReference + }; + await JobService.orderJob( + JobType.ModifyTrajectoryStation, + modifyTrajectoryStationJob + ); + } }; dispatchOperation({ type: OperationType.DisplayModal, - payload: ( - - ) + payload: }); }; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularComponentContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularComponentContextMenu.tsx index 0855bf7ca..8952d31dc 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularComponentContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularComponentContextMenu.tsx @@ -17,18 +17,23 @@ import { } from "components/ContextMenus/CopyUtils"; import NestedMenuItem from "components/ContextMenus/NestedMenuItem"; import { useClipboardComponentReferencesOfType } from "components/ContextMenus/UseClipboardComponentReferences"; -import TubularComponentPropertiesModal from "components/Modals/TubularComponentPropertiesModal"; +import { PropertiesModalMode } from "components/Modals/ModalParts"; +import { getTubularComponentProperties } from "components/Modals/PropertiesModal/Properties/TubularComponentProperties"; +import { PropertiesModal } from "components/Modals/PropertiesModal/PropertiesModal"; import { useConnectedServer } from "contexts/connectedServerContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import { createComponentReferences } from "models/jobs/componentReferences"; +import ObjectReference from "models/jobs/objectReference"; +import { toObjectReference } from "models/objectOnWellbore"; import { ObjectType } from "models/objectType"; import { Server } from "models/server"; import Tubular from "models/tubular"; +import TubularComponent from "models/tubularComponent"; import React from "react"; -import { JobType } from "services/jobService"; +import JobService, { JobType } from "services/jobService"; import { colors } from "styles/Colors"; export interface TubularComponentContextMenuProps { @@ -50,18 +55,30 @@ const TubularComponentContextMenu = ( const onClickProperties = async () => { dispatchOperation({ type: OperationType.HideContextMenu }); + const tubularComponent = checkedTubularComponents[0].tubularComponent; const tubularComponentPropertiesModalProps = { - tubularComponent: checkedTubularComponents[0].tubularComponent, - tubular, - dispatchOperation + title: `Edit properties for Sequence ${tubularComponent.sequence} - ${tubularComponent.typeTubularComponent} - ${tubularComponent.uid}`, + properties: getTubularComponentProperties(PropertiesModalMode.Edit), + object: tubularComponent, + onSubmit: async (updates: Partial) => { + dispatchOperation({ type: OperationType.HideModal }); + const tubularReference: ObjectReference = toObjectReference(tubular); + const modifyTubularComponentJob = { + tubularComponent: { + ...tubularComponent, + ...updates + }, + tubularReference + }; + await JobService.orderJob( + JobType.ModifyTubularComponent, + modifyTubularComponentJob + ); + } }; dispatchOperation({ type: OperationType.DisplayModal, - payload: ( - - ) + payload: }); }; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularContextMenu.tsx index 17f5c1b00..3451b8d7f 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularContextMenu.tsx @@ -12,10 +12,8 @@ import { ObjectMenuItems } from "components/ContextMenus/ObjectMenuItems"; import { useClipboardComponentReferencesOfType } from "components/ContextMenus/UseClipboardComponentReferences"; -import { PropertiesModalMode } from "components/Modals/ModalParts"; -import TubularPropertiesModal from "components/Modals/TubularPropertiesModal"; +import { openObjectOnWellboreProperties } from "components/Modals/PropertiesModal/openPropertiesHelpers"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; import { useOperationState } from "hooks/useOperationState"; @@ -38,19 +36,6 @@ const TubularContextMenu = ( const { connectedServer } = useConnectedServer(); const queryClient = useQueryClient(); - const onClickProperties = async () => { - dispatchOperation({ type: OperationType.HideContextMenu }); - const tubularPropertiesModalProps = { - mode: PropertiesModalMode.Edit, - tubular: checkedObjects[0] as Tubular, - dispatchOperation - }; - dispatchOperation({ - type: OperationType.DisplayModal, - payload: - }); - }; - const extraMenuItems = (): React.ReactElement[] => { return [ , + openObjectOnWellboreProperties( + ObjectType.Tubular, + checkedObjects?.[0] as Tubular, + dispatchOperation + ) + } disabled={checkedObjects.length !== 1} > diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularsContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularsContextMenu.tsx index 2792adcca..8691a362a 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularsContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TubularsContextMenu.tsx @@ -14,12 +14,15 @@ import { import { pasteObjectOnWellbore } from "components/ContextMenus/CopyUtils"; import NestedMenuItem from "components/ContextMenus/NestedMenuItem"; import { useClipboardReferencesOfType } from "components/ContextMenus/UseClipboardReferences"; +import { PropertiesModalMode } from "components/Modals/ModalParts"; +import { openObjectOnWellboreProperties } from "components/Modals/PropertiesModal/openPropertiesHelpers"; import { useConnectedServer } from "contexts/connectedServerContext"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; import { useOperationState } from "hooks/useOperationState"; import { toWellboreReference } from "models/jobs/wellboreReference"; import { ObjectType } from "models/objectType"; import { Server } from "models/server"; +import Tubular from "models/tubular"; import Wellbore from "models/wellbore"; import React from "react"; import { colors } from "styles/Colors"; @@ -40,6 +43,25 @@ const TubularsContextMenu = ( const { connectedServer } = useConnectedServer(); const queryClient = useQueryClient(); + const onClickNewTubular = () => { + const newTubular: Tubular = { + uid: uuid(), + name: "", + wellUid: wellbore.wellUid, + wellName: wellbore.wellName, + wellboreUid: wellbore.uid, + wellboreName: wellbore.name, + typeTubularAssy: null, + commonData: null + }; + openObjectOnWellboreProperties( + ObjectType.Tubular, + newTubular, + dispatchOperation, + PropertiesModalMode.New + ); + }; + return ( Refresh tubulars , + + + New Tubular + , diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometryContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometryContextMenu.tsx index ddc1fbdf4..5b8e11987 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometryContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometryContextMenu.tsx @@ -12,12 +12,8 @@ import { ObjectMenuItems } from "components/ContextMenus/ObjectMenuItems"; import { useClipboardComponentReferencesOfType } from "components/ContextMenus/UseClipboardComponentReferences"; -import { PropertiesModalMode } from "components/Modals/ModalParts"; -import WbGeometryPropertiesModal, { - WbGeometryPropertiesModalProps -} from "components/Modals/WbGeometryPropertiesModal"; +import { openObjectOnWellboreProperties } from "components/Modals/PropertiesModal/openPropertiesHelpers"; import { useConnectedServer } from "contexts/connectedServerContext"; -import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOpenInQueryView } from "hooks/useOpenInQueryView"; import { useOperationState } from "hooks/useOperationState"; @@ -40,20 +36,6 @@ const WbGeometryObjectContextMenu = ( const { connectedServer } = useConnectedServer(); const queryClient = useQueryClient(); - const onClickModify = async () => { - dispatchOperation({ type: OperationType.HideContextMenu }); - const mode = PropertiesModalMode.Edit; - const modifyWbGeometryObjectProps: WbGeometryPropertiesModalProps = { - mode, - wbGeometryObject: checkedObjects[0] as WbGeometryObject, - dispatchOperation - }; - dispatchOperation({ - type: OperationType.DisplayModal, - payload: - }); - }; - const extraMenuItems = (): React.ReactElement[] => { return [ , + openObjectOnWellboreProperties( + ObjectType.WbGeometry, + checkedObjects?.[0] as WbGeometryObject, + dispatchOperation + ) + } disabled={checkedObjects.length !== 1} > diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometrySectionContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometrySectionContextMenu.tsx index e73caf1c1..f0db1151e 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometrySectionContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WbGeometrySectionContextMenu.tsx @@ -14,19 +14,23 @@ import { } from "components/ContextMenus/CopyUtils"; import NestedMenuItem from "components/ContextMenus/NestedMenuItem"; import { useClipboardComponentReferencesOfType } from "components/ContextMenus/UseClipboardComponentReferences"; -import WbGeometrySectionPropertiesModal from "components/Modals/WbGeometrySectionPropertiesModal"; +import { PropertiesModalMode } from "components/Modals/ModalParts"; +import { getWbGeometrySectionProperties } from "components/Modals/PropertiesModal/Properties/WbGeometrySectionProperties"; +import { PropertiesModal } from "components/Modals/PropertiesModal/PropertiesModal"; import { useConnectedServer } from "contexts/connectedServerContext"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOperationState } from "hooks/useOperationState"; import { ComponentType } from "models/componentType"; import { createComponentReferences } from "models/jobs/componentReferences"; +import ObjectReference from "models/jobs/objectReference"; +import { toObjectReference } from "models/objectOnWellbore"; import { ObjectType } from "models/objectType"; import { Server } from "models/server"; import WbGeometry from "models/wbGeometry"; import WbGeometrySection from "models/wbGeometrySection"; import React from "react"; -import { JobType } from "services/jobService"; +import JobService, { JobType } from "services/jobService"; import { colors } from "styles/Colors"; export interface WbGeometrySectionContextMenuProps { @@ -48,17 +52,29 @@ const WbGeometrySectionContextMenu = ( const onClickProperties = async () => { dispatchOperation({ type: OperationType.HideContextMenu }); const wbGeometrySectionPropertiesModalProps = { - wbGeometrySection: checkedWbGeometrySections[0], - wbGeometry, - dispatchOperation + title: `Edit properties for ${checkedWbGeometrySections[0].uid}`, + properties: getWbGeometrySectionProperties(PropertiesModalMode.Edit), + object: checkedWbGeometrySections[0], + onSubmit: async (updates: Partial) => { + dispatchOperation({ type: OperationType.HideModal }); + const wbGeometryReference: ObjectReference = + toObjectReference(wbGeometry); + const modifyWbGeometrySectionJob = { + wbGeometrySection: { + ...checkedWbGeometrySections[0], + ...updates + }, + wbGeometryReference + }; + await JobService.orderJob( + JobType.ModifyWbGeometrySection, + modifyWbGeometrySectionJob + ); + } }; dispatchOperation({ type: OperationType.DisplayModal, - payload: ( - - ) + payload: }); }; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx index ae18bece4..724a97f6b 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx @@ -17,15 +17,13 @@ import MissingDataAgentModal, { MissingDataAgentModalProps } from "components/Modals/MissingDataAgentModal"; import { PropertiesModalMode } from "components/Modals/ModalParts"; +import { + openWellProperties, + openWellboreProperties +} from "components/Modals/PropertiesModal/openPropertiesHelpers"; import WellBatchUpdateModal, { WellBatchUpdateModalProps } from "components/Modals/WellBatchUpdateModal"; -import WellPropertiesModal, { - WellPropertiesModalProps -} from "components/Modals/WellPropertiesModal"; -import WellborePropertiesModal, { - WellborePropertiesModalProps -} from "components/Modals/WellborePropertiesModal"; import { useConnectedServer } from "contexts/connectedServerContext"; import { DisplayModalAction, @@ -73,15 +71,7 @@ const WellContextMenu = (props: WellContextMenuProps): React.ReactElement => { country: "", timeZone: "" }; - const wellPropertiesModalProps: WellPropertiesModalProps = { - mode: PropertiesModalMode.New, - well: newWell, - dispatchOperation - }; - dispatchOperation({ - type: OperationType.DisplayModal, - payload: - }); + openWellProperties(newWell, dispatchOperation, PropertiesModalMode.New); }; const onClickRefresh = async () => { @@ -100,22 +90,18 @@ const WellContextMenu = (props: WellContextMenuProps): React.ReactElement => { name: "", wellUid: well.uid, wellName: well.name, - wellStatus: "", - wellType: "", + wellboreStatus: "", + wellboreType: "", isActive: false, wellboreParentUid: "", wellboreParentName: "", wellborePurpose: "unknown" }; - const wellborePropertiesModalProps: WellborePropertiesModalProps = { - mode: PropertiesModalMode.New, - wellbore: newWellbore, - dispatchOperation - }; - dispatchOperation({ - type: OperationType.DisplayModal, - payload: - }); + openWellboreProperties( + newWellbore, + dispatchOperation, + PropertiesModalMode.New + ); }; const deleteWell = async () => { @@ -183,18 +169,6 @@ const WellContextMenu = (props: WellContextMenuProps): React.ReactElement => { }); }; - const onClickProperties = () => { - const wellPropertiesModalProps: WellPropertiesModalProps = { - mode: PropertiesModalMode.Edit, - well, - dispatchOperation - }; - dispatchOperation({ - type: OperationType.DisplayModal, - payload: - }); - }; - const onClickShowOnServer = async (server: Server) => { dispatchOperation({ type: OperationType.HideContextMenu }); const wellboresViewPath = getWellboresViewPath(server.url, well.uid); @@ -332,7 +306,10 @@ const WellContextMenu = (props: WellContextMenuProps): React.ReactElement => { Missing Data Agent , , - + openWellProperties(well, dispatchOperation)} + > - }; - dispatchOperation(action); + openWellboreProperties( + newWellbore, + dispatchOperation, + PropertiesModalMode.New + ); }; const onClickNewLog = () => { @@ -102,18 +96,15 @@ const WellboreContextMenu = ( wellName: wellbore.wellName, wellboreUid: wellbore.uid, wellboreName: wellbore.name, + indexType: WITSML_INDEX_TYPE_MD, indexCurve: IndexCurve.Depth }; - const logPropertiesModalProps: LogPropertiesModalInterface = { - mode: PropertiesModalMode.New, - logObject: newLog, - dispatchOperation - }; - const action: DisplayModalAction = { - type: OperationType.DisplayModal, - payload: - }; - dispatchOperation(action); + openObjectOnWellboreProperties( + ObjectType.Log, + newLog, + dispatchOperation, + PropertiesModalMode.New + ); }; const deleteWellbore = async () => { @@ -197,18 +188,6 @@ const WellboreContextMenu = ( }); }; - const onClickProperties = async () => { - const wellborePropertiesModalProps: WellborePropertiesModalProps = { - mode: PropertiesModalMode.Edit, - wellbore, - dispatchOperation - }; - dispatchOperation({ - type: OperationType.DisplayModal, - payload: - }); - }; - const onClickShowOnServer = async (server: Server) => { dispatchOperation({ type: OperationType.HideContextMenu }); const objectGroupsViewPath = getObjectGroupsViewPath( @@ -367,7 +346,10 @@ const WellboreContextMenu = ( Missing Data Agent , , - + openWellboreProperties(wellbore, dispatchOperation)} + > boolean; - helperText?: string; -} - -export interface BatchModifyModalProps { - title: string; - properties: BatchModifyProperty[]; - onSubmit: (batchUpdates: { [key: string]: string }) => void; -} - -export const BatchModifyPropertiesModal = ( - props: BatchModifyModalProps -): ReactElement => { - const { title, properties, onSubmit } = props; - const { dispatchOperation } = useOperationState(); - const [batchUpdates, setBatchUpdates] = useState<{ [key: string]: string }>( - properties.reduce((acc, prop) => ({ ...acc, [prop.property]: "" }), {}) - ); - const allValid = properties.every( - (prop) => - !prop.validator || - !batchUpdates[prop.property] || - prop.validator(batchUpdates[prop.property]) - ); - const allEmpty = properties.every((prop) => !batchUpdates[prop.property]); - - const onChangeProperty = (property: string, value: string) => { - setBatchUpdates({ - ...batchUpdates, - [property]: value - }); - }; - - const onInternalSubmit = async () => { - dispatchOperation({ type: OperationType.HideModal }); - // Remove empty properties as they should not be updated - const filteredBatchUpdates = Object.fromEntries( - Object.entries(batchUpdates).filter(([, value]) => value !== "") - ); - - // Create nested objects for properties with . in the name (e.g. commonData.source.name) - const nestedBatchUpdates = Object.entries(filteredBatchUpdates).reduce<{ - [key: string]: any; - }>((acc, [property, value]) => { - const keys = property.split("."); - keys.reduce((obj, key, index) => { - if (index === keys.length - 1) { - obj[key] = value; - } else { - obj[key] = obj[key] || {}; - } - return obj[key]; - }, acc); - return acc; - }, {}); - - onSubmit(nestedBatchUpdates); - }; - - return ( - - {properties.length === 0 &&

No properties to update.

} - {properties.map((property) => - property.options ? ( - - onChangeProperty(property.property, selectedItems?.[0] ?? "") - } - /> - ) : ( - ) => - onChangeProperty(property.property, e.target.value) - } - /> - ) - )} - - } - confirmDisabled={!allValid || allEmpty} - onSubmit={onInternalSubmit} - isLoading={false} - /> - ); -}; - -const Layout = styled.div` - margin-top: 12px; - margin-bottom: 12px; - display: flex; - flex-direction: column; - gap: 8px; -`; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/BhaRunPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/BhaRunPropertiesModal.tsx deleted file mode 100644 index 809f4b1d7..000000000 --- a/Src/WitsmlExplorer.Frontend/components/Modals/BhaRunPropertiesModal.tsx +++ /dev/null @@ -1,449 +0,0 @@ -import { Autocomplete, TextField } from "@equinor/eds-core-react"; -import formatDateString from "components/DateFormatter"; -import { DateTimeField } from "components/Modals/DateTimeField"; -import ModalDialog from "components/Modals/ModalDialog"; -import { PropertiesModalMode, validText } from "components/Modals/ModalParts"; -import { - DateTimeFormat, - HideModalAction -} from "contexts/operationStateReducer"; -import OperationType from "contexts/operationType"; -import { useOperationState } from "hooks/useOperationState"; -import BhaRun from "models/bhaRun"; -import { itemStateTypes } from "models/itemStateTypes"; -import { ObjectType } from "models/objectType"; -import React, { ChangeEvent, useEffect, useState } from "react"; -import JobService, { JobType } from "services/jobService"; - -const typesOfBhaStatus = ["final", "progress", "plan", "unknown"]; - -export interface BhaRunPropertiesModalProps { - mode: PropertiesModalMode; - bhaRun: BhaRun; - dispatchOperation: (action: HideModalAction) => void; -} - -const BhaRunPropertiesModal = ( - props: BhaRunPropertiesModalProps -): React.ReactElement => { - const { mode, bhaRun, dispatchOperation } = props; - const { - operationState: { timeZone } - } = useOperationState(); - const [editableBhaRun, setEditableBhaRun] = useState(null); - const [dTimStartValid, setDTimStartValid] = useState(true); - const [dTimStopValid, setDTimStopValid] = useState(true); - const [dTimStartDrillingValid, setDTimStartDrillingValid] = - useState(true); - const [dTimStopDrillingValid, setDTimStopDrillingValid] = - useState(true); - const [isLoading, setIsLoading] = useState(false); - const editMode = mode === PropertiesModalMode.Edit; - - useEffect(() => { - setEditableBhaRun({ - ...bhaRun, - dTimStart: bhaRun.dTimStart - ? formatDateString(bhaRun.dTimStart, timeZone, DateTimeFormat.Raw) - : null, - dTimStop: bhaRun.dTimStop - ? formatDateString(bhaRun.dTimStop, timeZone, DateTimeFormat.Raw) - : null, - dTimStartDrilling: bhaRun.dTimStartDrilling - ? formatDateString( - bhaRun.dTimStartDrilling, - timeZone, - DateTimeFormat.Raw - ) - : null, - dTimStopDrilling: bhaRun.dTimStopDrilling - ? formatDateString( - bhaRun.dTimStopDrilling, - timeZone, - DateTimeFormat.Raw - ) - : null, - commonData: { - ...bhaRun.commonData, - dTimCreation: bhaRun.commonData.dTimCreation - ? formatDateString( - bhaRun.commonData.dTimCreation, - timeZone, - DateTimeFormat.Raw - ) - : null, - dTimLastChange: bhaRun.commonData.dTimLastChange - ? formatDateString( - bhaRun.commonData.dTimLastChange, - timeZone, - DateTimeFormat.Raw - ) - : null - } - }); - }, [bhaRun]); - - const onSubmit = async (updatedBhaRun: BhaRun) => { - setIsLoading(true); - const modifyJob = { - object: { ...updatedBhaRun, objectType: ObjectType.BhaRun }, - objectType: ObjectType.BhaRun - }; - await JobService.orderJob(JobType.ModifyObjectOnWellbore, modifyJob); - dispatchOperation({ type: OperationType.HideModal }); - }; - - const validBhaRunName = validText(editableBhaRun?.name, 1, 64); - - return ( - <> - {editableBhaRun && ( - - - - - - - ) => - setEditableBhaRun({ ...editableBhaRun, name: e.target.value }) - } - /> - ) => - setEditableBhaRun({ - ...editableBhaRun, - tubular: { - ...editableBhaRun.tubular, - value: e.target.value - } - }) - } - /> - ) => - setEditableBhaRun({ - ...editableBhaRun, - tubular: { - ...editableBhaRun.tubular, - uidRef: e.target.value - } - }) - } - /> - { - setEditableBhaRun({ ...editableBhaRun, dTimStart: dateTime }); - setDTimStartValid(valid); - }} - timeZone={timeZone} - /> - { - setEditableBhaRun({ ...editableBhaRun, dTimStop: dateTime }); - setDTimStopValid(valid); - }} - timeZone={timeZone} - /> - { - setEditableBhaRun({ - ...editableBhaRun, - dTimStartDrilling: dateTime - }); - setDTimStartDrillingValid(valid); - }} - timeZone={timeZone} - /> - { - setEditableBhaRun({ - ...editableBhaRun, - dTimStopDrilling: dateTime - }); - setDTimStopDrillingValid(valid); - }} - timeZone={timeZone} - /> - ) => - setEditableBhaRun({ - ...editableBhaRun, - planDogleg: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableBhaRun.planDogleg.uom - } - }) - } - /> - ) => - setEditableBhaRun({ - ...editableBhaRun, - actDogleg: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableBhaRun.actDogleg.uom - } - }) - } - /> - ) => - setEditableBhaRun({ - ...editableBhaRun, - actDoglegMx: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableBhaRun.actDoglegMx.uom - } - }) - } - /> - { - setEditableBhaRun({ - ...editableBhaRun, - statusBha: selectedItems[0] - }); - }} - /> - ) => - setEditableBhaRun({ - ...editableBhaRun, - numBitRun: e.target.value - }) - } - /> - ) => - setEditableBhaRun({ - ...editableBhaRun, - numStringRun: e.target.value - }) - } - /> - ) => - setEditableBhaRun({ - ...editableBhaRun, - reasonTrip: e.target.value - }) - } - /> - ) => - setEditableBhaRun({ - ...editableBhaRun, - objectiveBha: e.target.value - }) - } - /> - { - const commonData = { - ...editableBhaRun.commonData, - itemState: selectedItems[0] ?? null - }; - setEditableBhaRun({ ...editableBhaRun, commonData }); - }} - /> - - - ) => { - const commonData = { - ...editableBhaRun.commonData, - sourceName: e.target.value - }; - setEditableBhaRun({ ...editableBhaRun, commonData }); - }} - /> - ) => { - const commonData = { - ...editableBhaRun.commonData, - serviceCategory: e.target.value - }; - setEditableBhaRun({ ...editableBhaRun, commonData }); - }} - /> - ) => { - const commonData = { - ...editableBhaRun.commonData, - comments: e.target.value - }; - setEditableBhaRun({ ...editableBhaRun, commonData }); - }} - /> - ) => { - const commonData = { - ...editableBhaRun.commonData, - defaultDatum: e.target.value - }; - setEditableBhaRun({ ...editableBhaRun, commonData }); - }} - /> - - } - confirmDisabled={ - !validBhaRunName || - !dTimStopValid || - !dTimStartValid || - !dTimStartDrillingValid || - !dTimStopDrillingValid - } - onSubmit={() => onSubmit(editableBhaRun)} - isLoading={isLoading} - /> - )} - - ); -}; - -export default BhaRunPropertiesModal; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/FormationMarkerPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/FormationMarkerPropertiesModal.tsx deleted file mode 100644 index 1fa9ba384..000000000 --- a/Src/WitsmlExplorer.Frontend/components/Modals/FormationMarkerPropertiesModal.tsx +++ /dev/null @@ -1,352 +0,0 @@ -import { Autocomplete, TextField } from "@equinor/eds-core-react"; -import ModalDialog from "components/Modals/ModalDialog"; -import { - invalidMeasureInput, - invalidStringInput, - undefinedOnUnchagedEmptyString -} from "components/Modals/PropertiesModalUtils"; -import OperationType from "contexts/operationType"; -import { useOperationState } from "hooks/useOperationState"; -import FormationMarker from "models/formationMarker"; -import { itemStateTypes } from "models/itemStateTypes"; -import MaxLength from "models/maxLength"; -import Measure from "models/measure"; -import MeasureWithDatum from "models/measureWithDatum"; -import { ObjectType } from "models/objectType"; -import StratigraphicStruct from "models/stratigraphicStruct"; -import React, { ChangeEvent, Dispatch, SetStateAction, useState } from "react"; -import JobService, { JobType } from "services/jobService"; -import { Layout } from "../StyledComponents/Layout"; - -export interface FormationMarkerPropertiesModalProps { - formationMarker: FormationMarker; -} - -type PropertyFlags = { - [Property in keyof Type]: boolean; -}; - -interface EditableFormationMarker { - name?: string; - mdPrognosed?: MeasureWithDatum; - tvdPrognosed?: MeasureWithDatum; - mdTopSample?: MeasureWithDatum; - tvdTopSample?: MeasureWithDatum; - thicknessBed?: Measure; - thicknessApparent?: Measure; - thicknessPerpen?: Measure; - mdLogSample?: MeasureWithDatum; - tvdLogSample?: MeasureWithDatum; - dip?: Measure; - dipDirection?: Measure; - lithostratigraphic?: StratigraphicStruct; - chronostratigraphic?: StratigraphicStruct; - description?: string; - commonData?: { - itemState: string; - }; -} - -type InvalidProperties = PropertyFlags; - -/** - * Takes in the input to modify a formation marker by filling out an EditableFormationMarker object. - * For strings, an empty string represents an invalid value (on deletion of existing value), while undefined represents no change when the value was empty to begin with. - * For Measures, measure.value being NaN represents an invalid value. Only existing measures can be edited. - * @param props FormationMarker to modify - * @returns - */ -const FormationMarkerPropertiesModal = ( - props: FormationMarkerPropertiesModalProps -): React.ReactElement => { - const { formationMarker } = props; - const { dispatchOperation } = useOperationState(); - const [editable, setEditable] = useState({}); - const [isLoading, setIsLoading] = useState(false); - - const onSubmit = async () => { - setIsLoading(true); - const modifyJob = { - object: { - ...editable, - uid: formationMarker.uid, - wellboreUid: formationMarker.wellboreUid, - wellUid: formationMarker.wellUid, - objectType: ObjectType.FormationMarker - }, - objectType: ObjectType.FormationMarker - }; - await JobService.orderJob(JobType.ModifyObjectOnWellbore, modifyJob); - setIsLoading(false); - dispatchOperation({ type: OperationType.HideModal }); - }; - - const invalid: InvalidProperties = { - name: invalidStringInput( - formationMarker?.name, - editable.name, - MaxLength.Name - ), - mdPrognosed: invalidMeasureInput(editable.mdPrognosed), - tvdPrognosed: invalidMeasureInput(editable.tvdPrognosed), - mdTopSample: invalidMeasureInput(editable.mdTopSample), - tvdTopSample: invalidMeasureInput(editable.tvdTopSample), - thicknessBed: invalidMeasureInput(editable.thicknessBed), - thicknessApparent: invalidMeasureInput(editable.thicknessApparent), - thicknessPerpen: invalidMeasureInput(editable.thicknessPerpen), - mdLogSample: invalidMeasureInput(editable.mdLogSample), - tvdLogSample: invalidMeasureInput(editable.tvdLogSample), - dip: invalidMeasureInput(editable.dip), - dipDirection: invalidMeasureInput(editable.dipDirection), - lithostratigraphic: invalidStringInput( - formationMarker?.lithostratigraphic?.value, - editable.lithostratigraphic?.value, - MaxLength.Name - ), - chronostratigraphic: invalidStringInput( - formationMarker?.chronostratigraphic?.value, - editable.chronostratigraphic?.value, - MaxLength.Name - ), - description: invalidStringInput( - formationMarker?.description, - editable.description, - MaxLength.Comment - ) - }; - - return ( - <> - {editable && ( - - - ) => - setEditable({ ...editable, name: e.target.value }) - } - /> - { - setEditable({ - ...editable, - commonData: { itemState: selectedItems[0] } - }); - }} - hideClearButton={true} - /> - - - - - - - - - - - - - - ) => - setEditable({ - ...editable, - description: undefinedOnUnchagedEmptyString( - formationMarker.description, - e.target.value - ) - }) - } - multiline - /> - - } - confirmDisabled={ - Object.values(invalid).findIndex((value) => value === true) !== -1 - } - onSubmit={() => onSubmit()} - isLoading={isLoading} - /> - )} - - ); -}; - -interface StratigraphicFieldProps { - editable: EditableFormationMarker; - originalStruct: StratigraphicStruct; - invalid: InvalidProperties; - property: keyof InvalidProperties & keyof EditableFormationMarker; - setResult: Dispatch>; -} - -const StratigraphicField = ( - props: StratigraphicFieldProps -): React.ReactElement => { - const { editable, originalStruct, invalid, property, setResult } = props; - return ( - ) => { - setResult({ - ...editable, - [property]: { - ...originalStruct, - value: e.target.value - } - }); - }} - /> - ); -}; - -interface MeasureFieldProps { - editable: EditableFormationMarker; - originalMeasure: Measure; - invalid: InvalidProperties; - property: keyof InvalidProperties & keyof EditableFormationMarker; - setResult: Dispatch>; -} - -const MeasureField = (props: MeasureFieldProps): React.ReactElement => { - const { editable, originalMeasure, invalid, property, setResult } = props; - return ( - ) => { - setResult({ - ...editable, - [property]: { - ...originalMeasure, - value: parseFloat(e.target.value) - } - }); - }} - /> - ); -}; - -export default FormationMarkerPropertiesModal; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/GeologyIntervalPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/GeologyIntervalPropertiesModal.tsx deleted file mode 100644 index 30390ebf9..000000000 --- a/Src/WitsmlExplorer.Frontend/components/Modals/GeologyIntervalPropertiesModal.tsx +++ /dev/null @@ -1,430 +0,0 @@ -import { Autocomplete, TextField } from "@equinor/eds-core-react"; -import { Typography } from "@mui/material"; -import ModalDialog from "components/Modals/ModalDialog"; -import { - invalidMeasureInput, - invalidNumberInput, - invalidStringInput, - undefinedOnUnchagedEmptyString -} from "components/Modals/PropertiesModalUtils"; -import OperationType from "contexts/operationType"; -import { useOperationState } from "hooks/useOperationState"; -import GeologyInterval from "models/geologyInterval"; -import ObjectReference from "models/jobs/objectReference"; -import { lithologySources } from "models/lithologySources"; -import { lithologyTypes } from "models/lithologyTypes"; -import MaxLength from "models/maxLength"; -import Measure from "models/measure"; -import MeasureWithDatum from "models/measureWithDatum"; -import MudLog from "models/mudLog"; -import { toObjectReference } from "models/objectOnWellbore"; -import React, { - ChangeEvent, - Dispatch, - SetStateAction, - useEffect, - useState -} from "react"; -import JobService, { JobType } from "services/jobService"; -import { Layout } from "../StyledComponents/Layout"; - -export interface GeologyIntervalPropertiesModalInterface { - geologyInterval: GeologyInterval; - mudLog: MudLog; -} - -type PropertyFlags = { - [Property in keyof Type]: boolean; -}; - -interface EditableLithology { - type?: string; - codeLith?: string; - lithPc?: number; -} - -interface EditableGeologyInterval { - typeLithology?: string; - description?: string; - mdTop?: MeasureWithDatum; - mdBottom?: MeasureWithDatum; - tvdTop?: MeasureWithDatum; - tvdBase?: MeasureWithDatum; - ropAv?: Measure; - wobAv?: Measure; - tqAv?: Measure; - currentAv?: Measure; - rpmAv?: Measure; - wtMudAv?: Measure; - ecdTdAv?: Measure; - dxcAv?: number; -} - -type InvalidProperties = PropertyFlags; - -/** - * Takes in the input to modify a geology interval by filling out an EditableGeologyInterval object. - * For strings, an empty string represents an invalid value (on deletion of existing value), while undefined represents no change when the value was empty to begin with. - * For Measures, measure.value being NaN represents an invalid value. Only existing measures can be edited. - * For numbers, NaN represents an invalid value (empty input field on deletion). - * @param props GeologyInterval to modify - * @returns - */ -const GeologyIntervalPropertiesModal = ( - props: GeologyIntervalPropertiesModalInterface -): React.ReactElement => { - const { geologyInterval, mudLog: selectedMudLog } = props; - const { dispatchOperation } = useOperationState(); - const [editable, setEditable] = useState({}); - const [editableLithologies, setEditableLithologies] = useState< - Record - >({}); - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - if (geologyInterval != null) { - //the following properties are required by WITSML to be included in the update geology interval query - setEditable({ - typeLithology: geologyInterval.typeLithology, - mdTop: { ...geologyInterval.mdTop }, - mdBottom: { ...geologyInterval.mdBottom } - }); - } - }, [geologyInterval]); - - const onSubmit = async () => { - setIsLoading(true); - const mudLogReference: ObjectReference = toObjectReference(selectedMudLog); - const modifyGeologyIntervalJob = { - geologyInterval: { - ...editable, - uid: geologyInterval.uid, - dxcAv: editable.dxcAv?.toString(), - lithologies: Object.entries(editableLithologies).map((entry) => { - return { - ...entry[1], - uid: entry[0], - lithPc: entry[1].lithPc?.toString() - }; - }) - }, - mudLogReference - }; - await JobService.orderJob( - JobType.ModifyGeologyInterval, - modifyGeologyIntervalJob - ); - setIsLoading(false); - dispatchOperation({ type: OperationType.HideModal }); - }; - - const invalid: InvalidProperties = { - description: invalidStringInput( - geologyInterval?.description, - editable.description, - MaxLength.Comment - ), - mdTop: invalidMeasureInput(editable.mdTop), - mdBottom: invalidMeasureInput(editable.mdBottom), - tvdTop: invalidMeasureInput(editable.tvdTop), - tvdBase: invalidMeasureInput(editable.tvdBase), - ropAv: invalidMeasureInput(editable.ropAv), - wobAv: invalidMeasureInput(editable.wobAv), - tqAv: invalidMeasureInput(editable.tqAv), - currentAv: invalidMeasureInput(editable.currentAv), - rpmAv: invalidMeasureInput(editable.rpmAv), - wtMudAv: invalidMeasureInput(editable.wtMudAv), - ecdTdAv: invalidMeasureInput(editable.ecdTdAv), - dxcAv: invalidNumberInput(geologyInterval?.dxcAv, editable.dxcAv) - }; - - const invalidLithologies: Record< - string, - { lithPc: boolean; codeLith: boolean } - > = {}; - Object.entries(editableLithologies).forEach((entry) => { - const lithUid = entry[0]; - const lithology = entry[1]; - const originalLithology = geologyInterval.lithologies.find( - (lith) => lith.uid == lithUid - ); - invalidLithologies[lithUid] = { - lithPc: invalidNumberInput(originalLithology.lithPc, lithology.lithPc), - codeLith: invalidStringInput( - originalLithology.codeLith, - lithology.codeLith, - MaxLength.Str16 - ) - }; - }); - - const setLithology = ( - value: any, - property: keyof EditableLithology, - uid: string - ) => { - const editableLithology = - editableLithologies[uid] == null - ? { - type: geologyInterval.lithologies.find((lith) => lith.uid === uid) - .type - } // lithology.type is required by update query - : editableLithologies[uid]; - setEditableLithologies({ - ...editableLithologies, - [uid]: { - ...editableLithology, - [property]: value - } - }); - }; - - return ( - <> - {editable && ( - - - { - setEditable({ ...editable, typeLithology: selectedItems[0] }); - }} - hideClearButton={true} - onFocus={(e) => e.preventDefault()} - /> - ) => - setEditable({ - ...editable, - description: undefinedOnUnchagedEmptyString( - geologyInterval.description, - e.target.value - ) - }) - } - multiline - /> - - - - - - - - - - - - ) => - setEditable({ - ...editable, - dxcAv: parseFloat(e.target.value) - }) - } - /> - {geologyInterval?.lithologies?.map((lithology) => { - return ( - - - Lithology {lithology.uid} - - - setLithology(selectedItems[0], "type", lithology.uid) - } - hideClearButton={true} - /> - ) => - setLithology(e.target.value, "codeLith", lithology.uid) - } - /> - ) => - setLithology( - parseFloat(e.target.value), - "lithPc", - lithology.uid - ) - } - /> - - ); - })} - - } - confirmDisabled={ - Object.values(invalid).findIndex((value) => value === true) !== - -1 || - Object.values(invalidLithologies).findIndex( - (l) => l.lithPc || l.codeLith - ) !== -1 - } - onSubmit={() => onSubmit()} - isLoading={isLoading} - /> - )} - - ); -}; - -interface MeasureFieldProps { - editable: EditableGeologyInterval; - originalMeasure: Measure; - invalid: InvalidProperties; - property: keyof InvalidProperties & keyof EditableGeologyInterval; - setResult: Dispatch>; -} - -const MeasureField = (props: MeasureFieldProps): React.ReactElement => { - const { editable, originalMeasure, invalid, property, setResult } = props; - return ( - ) => { - setResult({ - ...editable, - [property]: { - ...originalMeasure, - value: parseFloat(e.target.value) - } - }); - }} - /> - ); -}; - -export default GeologyIntervalPropertiesModal; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/LogCurveInfoPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/LogCurveInfoPropertiesModal.tsx deleted file mode 100644 index b5cbe975e..000000000 --- a/Src/WitsmlExplorer.Frontend/components/Modals/LogCurveInfoPropertiesModal.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { TextField } from "@equinor/eds-core-react"; -import { Typography } from "@mui/material"; -import ModalDialog from "components/Modals/ModalDialog"; -import { validText } from "components/Modals/ModalParts"; -import { HideModalAction } from "contexts/operationStateReducer"; -import OperationType from "contexts/operationType"; -import ModifyLogCurveInfoJob from "models/jobs/modifyLogCurveInfoJob"; -import LogCurveInfo from "models/logCurveInfo"; -import LogObject from "models/logObject"; -import { toObjectReference } from "models/objectOnWellbore"; -import React, { ChangeEvent, useEffect, useState } from "react"; -import JobService, { JobType } from "services/jobService"; -import { Layout } from "../StyledComponents/Layout"; - -export interface LogCurveInfoPropertiesModalProps { - logCurveInfo: LogCurveInfo; - dispatchOperation: (action: HideModalAction) => void; - selectedLog: LogObject; -} - -const LogCurveInfoPropertiesModal = ( - props: LogCurveInfoPropertiesModalProps -): React.ReactElement => { - const { logCurveInfo, dispatchOperation, selectedLog } = props; - const [editableLogCurveInfo, setEditableLogCurveInfo] = - useState(null); - const [isLoading, setIsLoading] = useState(false); - const isIndexCurve = logCurveInfo?.mnemonic === selectedLog?.indexCurve; - - const onSubmit = async () => { - setIsLoading(true); - const job: ModifyLogCurveInfoJob = { - logReference: toObjectReference(selectedLog), - logCurveInfo: editableLogCurveInfo - }; - await JobService.orderJob(JobType.ModifyLogCurveInfo, job); - setIsLoading(false); - dispatchOperation({ type: OperationType.HideModal }); - }; - - useEffect(() => { - setEditableLogCurveInfo(logCurveInfo); - }, [logCurveInfo]); - - const validMnemonic = validText(editableLogCurveInfo?.mnemonic, 1, 64); - const validUnit = validText(editableLogCurveInfo?.unit, 1, 64); - - return ( - <> - {editableLogCurveInfo && ( - - - ) => - setEditableLogCurveInfo({ - ...editableLogCurveInfo, - mnemonic: e.target.value - }) - } - /> - ) => - setEditableLogCurveInfo({ - ...editableLogCurveInfo, - unit: e.target.value - }) - } - /> - ) => - setEditableLogCurveInfo({ - ...editableLogCurveInfo, - curveDescription: e.target.value - }) - } - /> - - - {logCurveInfo?.axisDefinitions?.map((axisDefinition) => { - return ( - - - AxisDefinition {axisDefinition.uid} - - - - - - ); - })} - - } - confirmDisabled={ - logCurveInfo.mnemonic == editableLogCurveInfo.mnemonic && - logCurveInfo.unit == editableLogCurveInfo.unit && - logCurveInfo.curveDescription == - editableLogCurveInfo.curveDescription - } - onSubmit={() => onSubmit()} - isLoading={isLoading} - /> - )} - - ); -}; - -export default LogCurveInfoPropertiesModal; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/LogHeaderDateTimeField.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/LogHeaderDateTimeField.tsx index 85dc34a84..057773658 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/LogHeaderDateTimeField.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/LogHeaderDateTimeField.tsx @@ -14,6 +14,7 @@ interface DateTimeFieldProps { updateObject: (dateTime: string) => void; minValue?: string; maxValue?: string; + disabled?: boolean; } /** @@ -30,7 +31,7 @@ interface DateTimeFieldProps { export const LogHeaderDateTimeField = ( props: DateTimeFieldProps ): React.ReactElement => { - const { value, label, updateObject, minValue, maxValue } = props; + const { disabled, value, label, updateObject, minValue, maxValue } = props; const { operationState: { timeZone } } = useOperationState(); @@ -85,11 +86,12 @@ export const LogHeaderDateTimeField = ( disabled style={{ fontFeatureSettings: '"tnum"', - width: "92px" + width: "94px" }} /> diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/LogPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/LogPropertiesModal.tsx deleted file mode 100644 index da04b1035..000000000 --- a/Src/WitsmlExplorer.Frontend/components/Modals/LogPropertiesModal.tsx +++ /dev/null @@ -1,290 +0,0 @@ -import { Autocomplete, TextField } from "@equinor/eds-core-react"; -import { WITSML_INDEX_TYPE_DATE_TIME } from "components/Constants"; -import formatDateString from "components/DateFormatter"; -import ModalDialog from "components/Modals/ModalDialog"; -import { PropertiesModalMode, validText } from "components/Modals/ModalParts"; -import { HideModalAction } from "contexts/operationStateReducer"; -import OperationType from "contexts/operationType"; -import { useOperationState } from "hooks/useOperationState"; -import LogObject from "models/logObject"; -import { ObjectType } from "models/objectType"; -import React, { ChangeEvent, useEffect, useState } from "react"; -import JobService, { JobType } from "services/jobService"; - -export enum IndexCurve { - Depth = "Depth", - Time = "Time" -} - -export interface LogPropertiesModalInterface { - mode: PropertiesModalMode; - logObject: LogObject; - dispatchOperation: (action: HideModalAction) => void; -} - -const LogPropertiesModal = ( - props: LogPropertiesModalInterface -): React.ReactElement => { - const { mode, logObject, dispatchOperation } = props; - const { - operationState: { timeZone, dateTimeFormat } - } = useOperationState(); - const [editableLogObject, setEditableLogObject] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const editMode = mode === PropertiesModalMode.Edit; - const validIndexCurves = [IndexCurve.Depth, IndexCurve.Time]; - - const validServiceCompany = () => { - if (mode === PropertiesModalMode.New) { - return validText(editableLogObject.serviceCompany, 0, 64); - } else if (mode === PropertiesModalMode.Edit) { - if ( - logObject.serviceCompany === null && - editableLogObject.serviceCompany === null - ) - return true; - return validText(editableLogObject.serviceCompany, 1, 64); - } - }; - - const getServiceCompanyHelperText = () => { - if (mode === PropertiesModalMode.New) { - return "A service company must be 0-64 characters"; - } else if (mode === PropertiesModalMode.Edit) { - return "A service company must be 1-64 characters"; - } - }; - - const validRunNumber = () => { - if (mode === PropertiesModalMode.New) { - return validText(editableLogObject.runNumber, 0, 16); - } else if (mode === PropertiesModalMode.Edit) { - if (logObject.runNumber === null && editableLogObject.runNumber === null) - return true; - return validText(editableLogObject.runNumber, 1, 16); - } - }; - - const getRunNumberHelperText = () => { - if (mode === PropertiesModalMode.New) { - return "A run number must be 0-16 characters"; - } else if (mode === PropertiesModalMode.Edit) { - return "A run number must be 1-16 characters"; - } - }; - - const onSubmit = async (updatedLog: LogObject) => { - setIsLoading(true); - if (editMode) { - const modifyJob = { - object: { ...updatedLog, objectType: ObjectType.Log }, - objectType: ObjectType.Log - }; - await JobService.orderJob(JobType.ModifyObjectOnWellbore, modifyJob); - } else { - const wellboreLogJob = { - logObject: updatedLog - }; - await JobService.orderJob(JobType.CreateLogObject, wellboreLogJob); - } - setIsLoading(false); - dispatchOperation({ type: OperationType.HideModal }); - }; - - const onChangeCurve = async (event: any) => { - const indexCurve = - event.selectedItems[0] === IndexCurve.Time - ? IndexCurve.Time - : IndexCurve.Depth; - setEditableLogObject({ ...editableLogObject, indexCurve }); - }; - - useEffect(() => { - const isTimeIndexed = logObject.indexType === WITSML_INDEX_TYPE_DATE_TIME; - setEditableLogObject({ - ...logObject, - startIndex: isTimeIndexed - ? formatDateString(logObject.startIndex, timeZone, dateTimeFormat) - : logObject.startIndex, - endIndex: isTimeIndexed - ? formatDateString(logObject.endIndex, timeZone, dateTimeFormat) - : logObject.endIndex - }); - }, [logObject]); - - return ( - <> - {editableLogObject && ( - - ) => - setEditableLogObject({ - ...editableLogObject, - uid: e.target.value - }) - } - /> - ) => - setEditableLogObject({ - ...editableLogObject, - name: e.target.value - }) - } - /> - - ) => - setEditableLogObject({ - ...editableLogObject, - serviceCompany: - e.target.value === "" ? null : e.target.value - }) - } - /> - ) => - setEditableLogObject({ - ...editableLogObject, - runNumber: e.target.value === "" ? null : e.target.value - }) - } - /> - - - - - - - - {mode !== PropertiesModalMode.New && ( - <> - - - - )} - - } - confirmDisabled={ - !validText(editableLogObject.uid) || - !validText(editableLogObject.name) || - !validText(editableLogObject.indexCurve) || - !validServiceCompany() || - !validRunNumber() - } - onSubmit={() => onSubmit(editableLogObject)} - isLoading={isLoading} - /> - )} - - ); -}; - -export default LogPropertiesModal; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/MessagePropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/MessagePropertiesModal.tsx deleted file mode 100644 index 6043a47f3..000000000 --- a/Src/WitsmlExplorer.Frontend/components/Modals/MessagePropertiesModal.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { TextField } from "@equinor/eds-core-react"; -import formatDateString from "components/DateFormatter"; -import ModalDialog from "components/Modals/ModalDialog"; -import { PropertiesModalMode, validText } from "components/Modals/ModalParts"; -import { HideModalAction } from "contexts/operationStateReducer"; -import OperationType from "contexts/operationType"; -import { useOperationState } from "hooks/useOperationState"; -import MessageObject from "models/messageObject"; -import { ObjectType } from "models/objectType"; -import React, { ChangeEvent, useEffect, useState } from "react"; -import JobService, { JobType } from "services/jobService"; - -export interface MessagePropertiesModalProps { - mode: PropertiesModalMode; - messageObject: MessageObject; - dispatchOperation: (action: HideModalAction) => void; -} - -const MessagePropertiesModal = ( - props: MessagePropertiesModalProps -): React.ReactElement => { - const { mode, messageObject, dispatchOperation } = props; - const { - operationState: { timeZone, dateTimeFormat } - } = useOperationState(); - const [editableMessageObject, setEditableMessageObject] = - useState(null); - const [isLoading, setIsLoading] = useState(false); - const editMode = mode === PropertiesModalMode.Edit; - - useEffect(() => { - setEditableMessageObject({ - ...messageObject, - commonData: { - ...messageObject.commonData, - dTimCreation: formatDateString( - messageObject.commonData.dTimCreation, - timeZone, - dateTimeFormat - ), - dTimLastChange: formatDateString( - messageObject.commonData.dTimLastChange, - timeZone, - dateTimeFormat - ) - } - }); - }, [messageObject]); - - const onSubmit = async (updatedMessage: MessageObject) => { - setIsLoading(true); - const modifyJob = { - object: { ...updatedMessage, objectType: ObjectType.Message }, - objectType: ObjectType.Message - }; - await JobService.orderJob(JobType.ModifyObjectOnWellbore, modifyJob); - setIsLoading(false); - dispatchOperation({ type: OperationType.HideModal }); - }; - - const validName = validText(editableMessageObject?.name, 1, 64); - const validMessageText = validText( - editableMessageObject?.messageText, - 1, - 4000 - ); - - return ( - <> - {editableMessageObject && ( - - - - - ) => - setEditableMessageObject({ - ...editableMessageObject, - name: e.target.value - }) - } - /> - ) => - setEditableMessageObject({ - ...editableMessageObject, - messageText: e.target.value - }) - } - /> - - - - - - } - confirmDisabled={!validName || !validMessageText} - onSubmit={() => onSubmit(editableMessageObject)} - isLoading={isLoading} - /> - )} - - ); -}; - -export default MessagePropertiesModal; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/ModalDialog.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/ModalDialog.tsx index d7a6c86a9..1e855b0a5 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/ModalDialog.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/ModalDialog.tsx @@ -213,14 +213,14 @@ const Content = styled(Dialog.CustomContent)<{ color: ${(props) => props.colors.text.staticIconsDefault}; div[class*="InputWrapper__Container"] { - label.dHhldd { + label { color: ${(props) => props.colors.text.staticTextLabel}; } } div[class*="Input__Container"][disabled] { background: ${(props) => props.colors.text.staticTextFieldDefault}; - border-bottom: 1px solid #9ca6ac; + border-bottom: 1px solid #575d63; } div[class*="Input__Container"] { diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/ModalParts.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/ModalParts.tsx index 36cc3693e..7d2c84b3c 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/ModalParts.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/ModalParts.tsx @@ -1,4 +1,9 @@ -export enum PropertiesModalMode { +import MaxLength from "models/maxLength"; +import Measure from "models/measure"; +import RefNameString from "models/refNameString"; +import StratigraphicStruct from "models/stratigraphicStruct"; + +export enum PropertiesModalMode { New, Edit } @@ -22,7 +27,7 @@ export const validTimeZone = (timeZone: string): boolean => { return timeZoneValidator.test(timeZone); }; -const validNumber = (num: string): boolean => { +export const validInteger = (num: string): boolean => { let result = true; if (num) { const arr: Array = num.split(""); @@ -35,10 +40,50 @@ const validNumber = (num: string): boolean => { return result; }; +export const validNumber = (num: string): boolean => { + return !isNaN(parseFloat(num)) && isFinite(parseFloat(num)); +}; + export const validPhoneNumber = (telnum: string): boolean => { - return validNumber(telnum) && validText(telnum, 8, 16); + return validInteger(telnum) && validText(telnum, 1, MaxLength.String32); +}; + +export const validMeasure = (measure: Measure): boolean => { + return ( + typeof measure.value === "number" && + !isNaN(measure.value) && + validText(measure.uom, 1, MaxLength.UomEnum) + ); +}; + +export const validBoolean = (value: any): boolean => { + return typeof value === "boolean"; +}; + +export const validStratigraphicStruct = ( + stratigraphicStruct: StratigraphicStruct +): boolean => { + return ( + validText(stratigraphicStruct.value, 1, MaxLength.Name) && + validText(stratigraphicStruct.kind, 1, MaxLength.Name) + ); +}; + +export const validRefNameString = (refNameString: RefNameString): boolean => { + return ( + validText(refNameString.value, 1, MaxLength.Name) && + validText(refNameString.uidRef, 1, MaxLength.Uid) + ); +}; + +export const validPositiveInteger = (num: string): boolean => { + return /^\+?(0|[1-9]\d*)$/.test(num); +}; + +export const validOption = (option: string, validOptions: string[]) => { + return validOptions.includes(option); }; -export const validFaxNumber = (faxnum: string): boolean => { - return validNumber(faxnum) && validText(faxnum, 0, 16); +export const validMultiOption = (options: string, validOptions: string[]) => { + return options.split(", ").every((option) => validOptions.includes(option)); }; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/MudLogPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/MudLogPropertiesModal.tsx deleted file mode 100644 index 98574170b..000000000 --- a/Src/WitsmlExplorer.Frontend/components/Modals/MudLogPropertiesModal.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import { Autocomplete, TextField } from "@equinor/eds-core-react"; -import formatDateString from "components/DateFormatter"; -import ModalDialog from "components/Modals/ModalDialog"; -import { - invalidStringInput, - undefinedOnUnchagedEmptyString -} from "components/Modals/PropertiesModalUtils"; -import OperationType from "contexts/operationType"; -import { useOperationState } from "hooks/useOperationState"; -import { itemStateValues } from "models/commonData"; -import MaxLength from "models/maxLength"; -import MudLog from "models/mudLog"; -import ObjectOnWellbore, { toObjectReference } from "models/objectOnWellbore"; -import { ObjectType } from "models/objectType"; -import React, { ChangeEvent, useEffect, useState } from "react"; -import JobService, { JobType } from "services/jobService"; -import { Layout } from "../StyledComponents/Layout"; - -export interface MudLogPropertiesModalProps { - mudLog: MudLog; -} - -interface EditableMudLog extends ObjectOnWellbore { - mudLogCompany?: string; - mudLogEngineers?: string; - itemState?: string; -} - -const MudLogPropertiesModal = ( - props: MudLogPropertiesModalProps -): React.ReactElement => { - const { mudLog } = props; - const { - operationState: { timeZone, dateTimeFormat }, - dispatchOperation - } = useOperationState(); - const [editableMudLog, setEditableMudLog] = useState(null); - const [isLoading, setIsLoading] = useState(false); - - const onSubmit = async (updatedMudLog: EditableMudLog) => { - setIsLoading(true); - const modifyMudLogJob = { - object: { - ...updatedMudLog, - commonData: updatedMudLog.itemState - ? { itemState: updatedMudLog.itemState } - : null, - objectType: ObjectType.MudLog - }, - objectType: ObjectType.MudLog - }; - await JobService.orderJob(JobType.ModifyObjectOnWellbore, modifyMudLogJob); - setIsLoading(false); - dispatchOperation({ type: OperationType.HideModal }); - }; - - useEffect(() => { - if (mudLog != null) { - setEditableMudLog(toObjectReference(mudLog)); - } - }, [mudLog]); - - const invalidName = invalidStringInput( - mudLog?.name, - editableMudLog?.name, - MaxLength.Name - ); - const invalidMudLogCompany = invalidStringInput( - mudLog?.mudLogCompany, - editableMudLog?.mudLogCompany, - MaxLength.Name - ); - const invalidMudLogEngineers = invalidStringInput( - mudLog?.mudLogEngineers, - editableMudLog?.mudLogEngineers, - MaxLength.Description - ); - return ( - <> - {editableMudLog && ( - - - - - - - - - - - - { - setEditableMudLog({ - ...editableMudLog, - itemState: selectedItems[0] - }); - }} - /> - - } - confirmDisabled={ - invalidName || invalidMudLogCompany || invalidMudLogEngineers - } - onSubmit={() => onSubmit(editableMudLog)} - isLoading={isLoading} - /> - )} - - ); -}; - -type Key = keyof EditableMudLog & keyof MudLog; -export interface EditableTextFieldProps { - property: Key; - invalid: boolean; - maxLength: number; - setter: React.Dispatch>; - originalObject: MudLog; - editableObject: EditableMudLog; -} - -const EditableTextField = ( - props: EditableTextFieldProps -): React.ReactElement => { - const { - property, - invalid, - maxLength, - setter, - originalObject, - editableObject - } = props; - const originalValue = originalObject[property]; - const value = editableObject[property]; - return ( - ) => - setter({ - ...editableObject, - [property]: undefinedOnUnchagedEmptyString( - originalValue, - e.target.value - ) - }) - } - /> - ); -}; - -export default MudLogPropertiesModal; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/NestedPropertyHelpers.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/NestedPropertyHelpers.ts new file mode 100644 index 000000000..7d8b83b33 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/NestedPropertyHelpers.ts @@ -0,0 +1,38 @@ +export const getNestedValue = (obj: any, path: string): any => { + return path.split(".").reduce((acc, key) => acc && acc[key], obj); +}; + +export const setNestedValue = (obj: any, path: string, value: any): any => { + const keys = path.split("."); + let currentLevel = obj; + + for (let i = 0; i < keys.length - 1; i++) { + if (!currentLevel[keys[i]]) { + currentLevel[keys[i]] = {}; + } + currentLevel = currentLevel[keys[i]]; + } + + currentLevel[keys[keys.length - 1]] = value; + return obj; +}; + +export const deleteNestedValue = (obj: any, path: string) => { + const keys = path.split("."); + let currentLevel = obj; + let deepestLevel = obj; + let deepestLevelProperty = keys[0]; + + for (let i = 0; i < keys.length - 1; i++) { + if (!currentLevel[keys[i]]) { + break; + } + if (Object.keys(currentLevel[keys[i]]).length > 1) { + deepestLevel = currentLevel[keys[i]]; + deepestLevelProperty = keys[i + 1]; + } + currentLevel = currentLevel[keys[i]]; + } + delete deepestLevel[deepestLevelProperty]; + return obj; +}; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/BatchModifyObjectOnWellboreProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/BatchModifyObjectOnWellboreProperties.ts new file mode 100644 index 000000000..8efa2211c --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/BatchModifyObjectOnWellboreProperties.ts @@ -0,0 +1,40 @@ +import { getBatchModifyLogObjectProperties } from "components/Modals/PropertiesModal/Properties/LogObjectProperties"; +import { getBatchModifyRigProperties } from "components/Modals/PropertiesModal/Properties/RigProperties"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import { ObjectType, ObjectTypeToModel } from "models/objectType"; + +// Note: Only add properties that can be updated directly (without having to create a new object and delete the old one) +export const getBatchModifyObjectOnWellboreProperties = ( + objectType: T +): PropertiesModalProperty[] => { + switch (objectType) { + case ObjectType.BhaRun: + return []; + case ObjectType.ChangeLog: + return []; + case ObjectType.FluidsReport: + return []; + case ObjectType.FormationMarker: + return []; + case ObjectType.Log: + return getBatchModifyLogObjectProperties() as PropertiesModalProperty< + ObjectTypeToModel[T] + >[]; + case ObjectType.Message: + return []; + case ObjectType.MudLog: + return []; + case ObjectType.Rig: + return getBatchModifyRigProperties() as PropertiesModalProperty< + ObjectTypeToModel[T] + >[]; + case ObjectType.Risk: + return []; + case ObjectType.Trajectory: + return []; + case ObjectType.Tubular: + return []; + case ObjectType.WbGeometry: + return []; + } +}; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/BhaRunProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/BhaRunProperties.ts new file mode 100644 index 000000000..4ba7211d6 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/BhaRunProperties.ts @@ -0,0 +1,140 @@ +import { + PropertiesModalMode, + validMeasure, + validOption, + validPositiveInteger, + validRefNameString, + validText +} from "components/Modals/ModalParts"; +import { getCommonObjectOnWellboreProperties } from "components/Modals/PropertiesModal/Properties/CommonObjectOnWellboreProperties"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { + getMaxLengthHelperText, + getMeasureHelperText, + getOptionHelperText, + getRefNameStringHelperText +} from "components/Modals/PropertiesModal/ValidationHelpers"; +import BhaRun from "models/bhaRun"; +import { bhaStatusTypes } from "models/bhaStatusTypes"; +import { itemStateTypes } from "models/itemStateTypes"; +import MaxLength from "models/maxLength"; + +export const getBhaRunProperties = ( + mode: PropertiesModalMode +): PropertiesModalProperty[] => [ + ...getCommonObjectOnWellboreProperties(mode), + { + property: "tubular", + propertyType: PropertyType.RefNameString, + validator: validRefNameString, + helperText: getRefNameStringHelperText("tubular") + }, + { + property: "dTimStart", + propertyType: PropertyType.DateTime + }, + { + property: "dTimStop", + propertyType: PropertyType.DateTime + }, + { + property: "dTimStartDrilling", + propertyType: PropertyType.DateTime + }, + { + property: "dTimStopDrilling", + propertyType: PropertyType.DateTime + }, + { + property: "planDogleg", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("planDogleg") + }, + { + property: "actDogleg", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("actDogleg") + }, + { + property: "actDoglegMx", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("actDoglegMx") + }, + { + property: "statusBha", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, bhaStatusTypes), + helperText: getOptionHelperText("statusBha"), + options: bhaStatusTypes + }, + { + property: "numBitRun", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("numBitRun", MaxLength.Name) + }, + { + property: "numStringRun", + propertyType: PropertyType.StringNumber, + validator: validPositiveInteger, + helperText: "numStringRun must be a positive integer" + }, + { + property: "reasonTrip", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Comment), + helperText: getMaxLengthHelperText("numBitRun", MaxLength.Comment) + }, + { + property: "objectiveBha", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Comment), + helperText: getMaxLengthHelperText("objectiveBha", MaxLength.Comment) + }, + { + property: "commonData.itemState", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, itemStateTypes), + helperText: getOptionHelperText("itemState"), + options: itemStateTypes + }, + { + property: "commonData.dTimCreation", + propertyType: PropertyType.DateTime, + disabled: true + }, + { + property: "commonData.dTimLastChange", + propertyType: PropertyType.DateTime, + disabled: true + }, + { + property: "commonData.sourceName", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("sourceName", MaxLength.Name) + }, + { + property: "commonData.serviceCategory", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Enum), + helperText: getMaxLengthHelperText("serviceCategory", MaxLength.Enum) + }, + { + property: "commonData.comments", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Comment), + helperText: getMaxLengthHelperText("comments", MaxLength.Comment), + multiline: true + }, + { + property: "commonData.defaultDatum", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("defaultDatum", MaxLength.Name) + } +]; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/CommonObjectOnWellboreProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/CommonObjectOnWellboreProperties.ts new file mode 100644 index 000000000..aa1b907e3 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/CommonObjectOnWellboreProperties.ts @@ -0,0 +1,45 @@ +import { PropertiesModalMode, validText } from "components/Modals/ModalParts"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { getMaxLengthHelperText } from "components/Modals/PropertiesModal/ValidationHelpers"; +import MaxLength from "models/maxLength"; +import ObjectOnWellbore from "models/objectOnWellbore"; + +export const getCommonObjectOnWellboreProperties = ( + mode: PropertiesModalMode +): PropertiesModalProperty[] => [ + { + property: "wellUid", + propertyType: PropertyType.String, + disabled: true + }, + { + property: "wellName", + propertyType: PropertyType.String, + disabled: true + }, + { + property: "wellboreUid", + propertyType: PropertyType.String, + disabled: true + }, + { + property: "wellboreName", + propertyType: PropertyType.String, + disabled: true + }, + { + property: "uid", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Uid), + helperText: getMaxLengthHelperText("uid", MaxLength.Uid), + disabled: mode === PropertiesModalMode.Edit + }, + { + property: "name", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("name", MaxLength.Name), + required: true + } +]; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/FormationMarkerProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/FormationMarkerProperties.ts new file mode 100644 index 000000000..e5a52bff3 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/FormationMarkerProperties.ts @@ -0,0 +1,110 @@ +import { + PropertiesModalMode, + validMeasure, + validOption, + validStratigraphicStruct, + validText +} from "components/Modals/ModalParts"; +import { getCommonObjectOnWellboreProperties } from "components/Modals/PropertiesModal/Properties/CommonObjectOnWellboreProperties"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { + getMaxLengthHelperText, + getMeasureHelperText, + getOptionHelperText, + getStratigraphicStructHelperText +} from "components/Modals/PropertiesModal/ValidationHelpers"; +import FormationMarker from "models/formationMarker"; +import { itemStateTypes } from "models/itemStateTypes"; +import MaxLength from "models/maxLength"; + +export const getFormationMarkerProperties = ( + mode: PropertiesModalMode +): PropertiesModalProperty[] => [ + ...getCommonObjectOnWellboreProperties(mode), + { + property: "commonData.itemState", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, itemStateTypes), + helperText: getOptionHelperText("itemState"), + options: itemStateTypes + }, + { + property: "mdPrognosed", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("mdPrognosed") + }, + { + property: "tvdPrognosed", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("tvdPrognosed") + }, + { + property: "mdTopSample", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("mdTopSample") + }, + { + property: "tvdTopSample", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("tvdTopSample") + }, + { + property: "thicknessBed", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("thicknessBed") + }, + { + property: "thicknessPerpen", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("thicknessPerpen") + }, + { + property: "mdLogSample", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("mdLogSample") + }, + { + property: "tvdLogSample", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("tvdLogSample") + }, + { + property: "dip", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("dip") + }, + { + property: "dipDirection", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("dipDirection") + }, + { + property: "lithostratigraphic", + propertyType: PropertyType.StratigraphicStruct, + validator: validStratigraphicStruct, + helperText: getStratigraphicStructHelperText("lithostratigraphic") + }, + { + property: "chronostratigraphic", + propertyType: PropertyType.StratigraphicStruct, + validator: validStratigraphicStruct, + helperText: getStratigraphicStructHelperText("chronostratigraphic") + }, + { + property: "description", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Description), + helperText: getMaxLengthHelperText("description", MaxLength.Description) + } +]; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/GeologyIntervalProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/GeologyIntervalProperties.ts new file mode 100644 index 000000000..d7e870c48 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/GeologyIntervalProperties.ts @@ -0,0 +1,157 @@ +import { + PropertiesModalMode, + validMeasure, + validNumber, + validOption, + validText +} from "components/Modals/ModalParts"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { + getMaxLengthHelperText, + getMeasureHelperText, + getNumberHelperText, + getOptionHelperText +} from "components/Modals/PropertiesModal/ValidationHelpers"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import GeologyInterval from "models/geologyInterval"; +import Lithology from "models/lithology"; +import { lithologySources } from "models/lithologySources"; +import { lithologyTypes } from "models/lithologyTypes"; +import MaxLength from "models/maxLength"; + +export const getGeologyIntervalProperties = ( + mode: PropertiesModalMode +): PropertiesModalProperty[] => [ + { + property: "uid", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Uid), + helperText: getMaxLengthHelperText("uid", MaxLength.Uid), + disabled: mode === PropertiesModalMode.Edit + }, + { + property: "typeLithology", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, lithologySources), + helperText: getOptionHelperText("typeLithology"), + options: lithologySources, + required: true + }, + { + property: "description", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Comment), + helperText: getMaxLengthHelperText("description", MaxLength.Comment) + }, + { + property: "mdTop", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("mdTop"), + required: true + }, + { + property: "mdBottom", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("mdBottom"), + required: true + }, + { + property: "tvdTop", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("tvdTop") + }, + { + property: "tvdBase", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("tvdBase") + }, + { + property: "ropAv", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("ropAv") + }, + { + property: "wobAv", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("wobAv") + }, + { + property: "tqAv", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("tqAv") + }, + { + property: "currentAv", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("currentAv") + }, + { + property: "rpmAv", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("rpmAv") + }, + { + property: "wtMudAv", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("wtMudAv") + }, + { + property: "ecdTdAv", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("ecdTdAv") + }, + { + property: "dxcAv", + propertyType: PropertyType.StringNumber, + validator: validNumber, + helperText: getNumberHelperText("dxcAv") + }, + { + property: "lithologies", + propertyType: PropertyType.List, + subProps: getLithologyProps(mode), + itemPrefix: "Lithology " + } +]; + +export const getLithologyProps = ( + mode: PropertiesModalMode +): PropertiesModalProperty[] => [ + { + property: "uid", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Uid), + helperText: getMaxLengthHelperText("uid", MaxLength.Uid), + disabled: mode === PropertiesModalMode.Edit + }, + { + property: "type", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, lithologyTypes), + helperText: getOptionHelperText("type"), + options: lithologyTypes + }, + { + property: "codeLith", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Str16), + helperText: getMaxLengthHelperText("codeLith", MaxLength.Str16) + }, + { + property: "lithPc", + propertyType: PropertyType.StringNumber, + validator: validNumber, + helperText: getNumberHelperText("lithPc") + } +]; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/LogCurveInfoProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/LogCurveInfoProperties.ts new file mode 100644 index 000000000..3b790db73 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/LogCurveInfoProperties.ts @@ -0,0 +1,85 @@ +import { PropertiesModalMode, validText } from "components/Modals/ModalParts"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { getMaxLengthHelperText } from "components/Modals/PropertiesModal/ValidationHelpers"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import AxisDefinition from "models/AxisDefinition"; +import LogCurveInfo from "models/logCurveInfo"; +import MaxLength from "models/maxLength"; + +export const getLogCurveInfoProperties = ( + mode: PropertiesModalMode, + isIndexCurve: boolean +): PropertiesModalProperty[] => [ + { + property: "uid", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Uid), + helperText: getMaxLengthHelperText("uid", MaxLength.Uid), + disabled: mode === PropertiesModalMode.Edit + }, + { + property: "mnemonic", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.String32), + helperText: getMaxLengthHelperText("mnemonic", MaxLength.String32), + disabled: isIndexCurve + }, + { + property: "unit", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.UomEnum), + helperText: getMaxLengthHelperText("unit", MaxLength.UomEnum) + }, + { + property: "curveDescription", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Description), + helperText: getMaxLengthHelperText( + "curveDescription", + MaxLength.Description + ) + }, + { + property: "typeLogData", + propertyType: PropertyType.String, + disabled: true + }, + { + property: "mnemAlias", + propertyType: PropertyType.String, + disabled: true + }, + { + property: "axisDefinitions", + propertyType: PropertyType.List, + subProps: getAxisDefinitionProps(mode), + itemPrefix: "Axis Definition " + } +]; + +export const getAxisDefinitionProps = ( + mode: PropertiesModalMode +): PropertiesModalProperty[] => [ + { + property: "uid", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Uid), + helperText: getMaxLengthHelperText("uid", MaxLength.Uid), + disabled: mode === PropertiesModalMode.Edit + }, + { + property: "order", + propertyType: PropertyType.Number, + disabled: true + }, + { + property: "count", + propertyType: PropertyType.Number, + disabled: true + }, + { + property: "doubleValues", + propertyType: PropertyType.String, + disabled: true + } +]; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/LogObjectProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/LogObjectProperties.ts new file mode 100644 index 000000000..5dc306c1a --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/LogObjectProperties.ts @@ -0,0 +1,109 @@ +import { + WITSML_INDEX_TYPE_DATE_TIME, + WITSML_INDEX_TYPE_MD +} from "components/Constants"; +import { + PropertiesModalMode, + validOption, + validText +} from "components/Modals/ModalParts"; +import { getCommonObjectOnWellboreProperties } from "components/Modals/PropertiesModal/Properties/CommonObjectOnWellboreProperties"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { + getMaxLengthHelperText, + getOptionHelperText +} from "components/Modals/PropertiesModal/ValidationHelpers"; +import { IndexCurve } from "models/indexCurve"; +import LogObject from "models/logObject"; +import MaxLength from "models/maxLength"; + +const indexTypeOptions = [WITSML_INDEX_TYPE_MD, WITSML_INDEX_TYPE_DATE_TIME]; + +export const getLogObjectProperties = ( + mode: PropertiesModalMode, + indexType: string +): PropertiesModalProperty[] => [ + ...getCommonObjectOnWellboreProperties(mode), + { + property: "indexCurve", + propertyType: PropertyType.Options, + helperText: "indexCurve cannot be empty", + options: Object.values(IndexCurve), + disabled: mode === PropertiesModalMode.Edit + }, + { + property: "indexType", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, indexTypeOptions), + helperText: getOptionHelperText("indexType"), + options: indexTypeOptions, + disabled: mode === PropertiesModalMode.Edit + }, + { + property: "runNumber", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Str16), + helperText: getMaxLengthHelperText("runNumber", MaxLength.Str16) + }, + { + property: "serviceCompany", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("serviceCompany", MaxLength.Name) + }, + { + property: "objectGrowing", + propertyType: PropertyType.String, + disabled: true + }, + { + property: "startIndex", + propertyType: + indexType === WITSML_INDEX_TYPE_DATE_TIME + ? PropertyType.DateTime + : PropertyType.String, + disabled: true + }, + { + property: "endIndex", + propertyType: + indexType === WITSML_INDEX_TYPE_DATE_TIME + ? PropertyType.DateTime + : PropertyType.String, + disabled: true + }, + { + property: "commonData.dTimCreation", + propertyType: PropertyType.DateTime, + disabled: true + }, + { + property: "commonData.dTimLastChange", + propertyType: PropertyType.DateTime, + disabled: true + } +]; + +export const getBatchModifyLogObjectProperties = + (): PropertiesModalProperty[] => [ + { + property: "name", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("name", MaxLength.Name) + }, + { + property: "runNumber", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Str16), + helperText: getMaxLengthHelperText("runNumber", MaxLength.Str16) + }, + { + property: "commonData.comments", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Comment), + helperText: getMaxLengthHelperText("comments", MaxLength.Comment), + multiline: true + } + ]; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/MessageProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/MessageProperties.ts new file mode 100644 index 000000000..1abaec62a --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/MessageProperties.ts @@ -0,0 +1,30 @@ +import { PropertiesModalMode, validText } from "components/Modals/ModalParts"; +import { getCommonObjectOnWellboreProperties } from "components/Modals/PropertiesModal/Properties/CommonObjectOnWellboreProperties"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { getMaxLengthHelperText } from "components/Modals/PropertiesModal/ValidationHelpers"; +import MaxLength from "models/maxLength"; +import MessageObject from "models/messageObject"; + +export const getMessageProperties = ( + mode: PropertiesModalMode +): PropertiesModalProperty[] => [ + ...getCommonObjectOnWellboreProperties(mode), + { + property: "messageText", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Comment), + helperText: getMaxLengthHelperText("messageText", MaxLength.Comment), + multiline: true + }, + { + property: "commonData.dTimCreation", + propertyType: PropertyType.DateTime, + disabled: true + }, + { + property: "commonData.dTimLastChange", + propertyType: PropertyType.DateTime, + disabled: true + } +]; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/MudLogProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/MudLogProperties.ts new file mode 100644 index 000000000..164363793 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/MudLogProperties.ts @@ -0,0 +1,65 @@ +import { + PropertiesModalMode, + validOption, + validText +} from "components/Modals/ModalParts"; +import { getCommonObjectOnWellboreProperties } from "components/Modals/PropertiesModal/Properties/CommonObjectOnWellboreProperties"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { + getMaxLengthHelperText, + getOptionHelperText +} from "components/Modals/PropertiesModal/ValidationHelpers"; +import { itemStateTypes } from "models/itemStateTypes"; +import MaxLength from "models/maxLength"; +import MudLog from "models/mudLog"; + +export const getMudLogProperties = ( + mode: PropertiesModalMode +): PropertiesModalProperty[] => [ + ...getCommonObjectOnWellboreProperties(mode), + { + property: "mudLogCompany", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("mudLogCompany", MaxLength.Name) + }, + { + property: "mudLogEngineers", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Description), + helperText: getMaxLengthHelperText("mudLogEngineers", MaxLength.Description) + }, + { + property: "objectGrowing", + propertyType: PropertyType.String, + disabled: true + }, + { + property: "startMd", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "endMd", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "commonData.itemState", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, itemStateTypes), + helperText: getOptionHelperText("itemState"), + options: itemStateTypes + }, + { + property: "commonData.dTimCreation", + propertyType: PropertyType.DateTime, + disabled: true + }, + { + property: "commonData.dTimLastChange", + propertyType: PropertyType.DateTime, + disabled: true + } +]; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/ObjectOnWellboreProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/ObjectOnWellboreProperties.ts new file mode 100644 index 000000000..4aa0f5cb2 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/ObjectOnWellboreProperties.ts @@ -0,0 +1,70 @@ +import { PropertiesModalMode } from "components/Modals/ModalParts"; +import { getBhaRunProperties } from "components/Modals/PropertiesModal/Properties/BhaRunProperties"; +import { getFormationMarkerProperties } from "components/Modals/PropertiesModal/Properties/FormationMarkerProperties"; +import { getLogObjectProperties } from "components/Modals/PropertiesModal/Properties/LogObjectProperties"; +import { getMessageProperties } from "components/Modals/PropertiesModal/Properties/MessageProperties"; +import { getMudLogProperties } from "components/Modals/PropertiesModal/Properties/MudLogProperties"; +import { getRigProperties } from "components/Modals/PropertiesModal/Properties/RigProperties"; +import { getRiskProperties } from "components/Modals/PropertiesModal/Properties/RiskProperties"; +import { getTrajectoryProperties } from "components/Modals/PropertiesModal/Properties/TrajectoryProperties"; +import { getTubularProperties } from "components/Modals/PropertiesModal/Properties/TubularProperties"; +import { getWbGeometryProperties } from "components/Modals/PropertiesModal/Properties/WbGeometryProperties"; +import { getFluidsReportProperties } from "components/Modals/PropertiesModal/Properties/getFluidsReportProperties"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import { ObjectType, ObjectTypeToModel } from "models/objectType"; + +// Note: Only add properties that can be updated directly (without having to create a new object and delete the old one) +export const getObjectOnWellboreProperties = ( + objectType: T, + mode: PropertiesModalMode, + indexType: string = null +): PropertiesModalProperty[] => { + switch (objectType) { + case ObjectType.BhaRun: + return getBhaRunProperties(mode) as PropertiesModalProperty< + ObjectTypeToModel[T] + >[]; + case ObjectType.ChangeLog: + return []; + case ObjectType.FluidsReport: + return getFluidsReportProperties(mode) as PropertiesModalProperty< + ObjectTypeToModel[T] + >[]; + case ObjectType.FormationMarker: + return getFormationMarkerProperties(mode) as PropertiesModalProperty< + ObjectTypeToModel[T] + >[]; + case ObjectType.Log: + return getLogObjectProperties(mode, indexType) as PropertiesModalProperty< + ObjectTypeToModel[T] + >[]; + case ObjectType.Message: + return getMessageProperties(mode) as PropertiesModalProperty< + ObjectTypeToModel[T] + >[]; + case ObjectType.MudLog: + return getMudLogProperties(mode) as PropertiesModalProperty< + ObjectTypeToModel[T] + >[]; + case ObjectType.Rig: + return getRigProperties(mode) as PropertiesModalProperty< + ObjectTypeToModel[T] + >[]; + case ObjectType.Risk: + return getRiskProperties(mode) as PropertiesModalProperty< + ObjectTypeToModel[T] + >[]; + case ObjectType.Trajectory: + return getTrajectoryProperties(mode) as PropertiesModalProperty< + ObjectTypeToModel[T] + >[]; + case ObjectType.Tubular: + return getTubularProperties(mode) as PropertiesModalProperty< + ObjectTypeToModel[T] + >[]; + case ObjectType.WbGeometry: + return getWbGeometryProperties(mode) as PropertiesModalProperty< + ObjectTypeToModel[T] + >[]; + } +}; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/RigProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/RigProperties.ts new file mode 100644 index 000000000..ae628527c --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/RigProperties.ts @@ -0,0 +1,169 @@ +import { + PropertiesModalMode, + validMeasure, + validOption, + validPhoneNumber, + validPositiveInteger, + validText +} from "components/Modals/ModalParts"; +import { getCommonObjectOnWellboreProperties } from "components/Modals/PropertiesModal/Properties/CommonObjectOnWellboreProperties"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { + getMaxLengthHelperText, + getMeasureHelperText, + getOptionHelperText, + getPhoneNumberHelperText +} from "components/Modals/PropertiesModal/ValidationHelpers"; +import { itemStateTypes } from "models/itemStateTypes"; +import MaxLength from "models/maxLength"; +import Rig from "models/rig"; +import { rigType } from "models/rigType"; + +export const getRigProperties = ( + mode: PropertiesModalMode +): PropertiesModalProperty[] => [ + ...getCommonObjectOnWellboreProperties(mode), + { + property: "typeRig", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, rigType), + helperText: getOptionHelperText("typeRig"), + options: rigType + }, + { + property: "dTimStartOp", + propertyType: PropertyType.DateTime + }, + { + property: "dTimEndOp", + propertyType: PropertyType.DateTime + }, + { + property: "yearEntService", + propertyType: PropertyType.StringNumber, + validator: (num: string) => + validPositiveInteger(num) && validText(num, 4, 4), + helperText: "yearEntService must be a 4 digit positive integer" + }, + { + property: "telNumber", + propertyType: PropertyType.String, + validator: validPhoneNumber, + helperText: getPhoneNumberHelperText("telNumber") + }, + { + property: "faxNumber", + propertyType: PropertyType.String, + validator: validPhoneNumber, + helperText: getPhoneNumberHelperText("faxNumber") + }, + { + property: "emailAddress", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("emailAddress", MaxLength.Name) + }, + { + property: "nameContact", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("nameContact", MaxLength.Name) + }, + { + property: "ratingDrillDepth", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("ratingDrillDepth") + }, + { + property: "ratingWaterDepth", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("ratingWaterDepth") + }, + { + property: "airGap", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("airGap") + }, + { + property: "owner", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.String32), + helperText: getMaxLengthHelperText("owner", MaxLength.String32) + }, + { + property: "manufacturer", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("manufacturer", MaxLength.Name) + }, + { + property: "classRig", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.String32), + helperText: getMaxLengthHelperText("classRig", MaxLength.String32) + }, + { + property: "approvals", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("approvals", MaxLength.Name) + }, + { + property: "registration", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.String32), + helperText: getMaxLengthHelperText("registration", MaxLength.String32) + }, + { + property: "commonData.itemState", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, itemStateTypes), + helperText: getOptionHelperText("itemState"), + options: itemStateTypes + } +]; + +export const getBatchModifyRigProperties = + (): PropertiesModalProperty[] => [ + { + property: "owner", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 0, MaxLength.String32), + helperText: getMaxLengthHelperText("owner", MaxLength.String32) + }, + { + property: "typeRig", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, rigType), + helperText: getOptionHelperText("typeRig"), + options: rigType + }, + { + property: "manufacturer", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 0, MaxLength.Name), + helperText: getMaxLengthHelperText("manufacturer", MaxLength.Name) + }, + { + property: "classRig", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 0, MaxLength.String32), + helperText: getMaxLengthHelperText("classRig", MaxLength.String32) + }, + { + property: "approvals", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 0, MaxLength.Name), + helperText: getMaxLengthHelperText("approvals", MaxLength.Name) + }, + { + property: "registration", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 0, MaxLength.String32), + helperText: getMaxLengthHelperText("registration", MaxLength.String32) + } + ]; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/RiskProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/RiskProperties.ts new file mode 100644 index 000000000..ce1660058 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/RiskProperties.ts @@ -0,0 +1,134 @@ +import { + PropertiesModalMode, + validMeasure, + validMultiOption, + validOption, + validText +} from "components/Modals/ModalParts"; +import { getCommonObjectOnWellboreProperties } from "components/Modals/PropertiesModal/Properties/CommonObjectOnWellboreProperties"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { + getMaxLengthHelperText, + getMeasureHelperText, + getOptionHelperText +} from "components/Modals/PropertiesModal/ValidationHelpers"; +import { itemStateTypes } from "models/itemStateTypes"; +import { levelIntegerCode } from "models/levelIntegerCode"; +import MaxLength from "models/maxLength"; +import { riskAffectedPersonnel } from "models/riskAffectedPersonnel"; +import { riskCategory } from "models/riskCategory"; +import RiskObject from "models/riskObject"; +import { riskSubCategory } from "models/riskSubCategory"; +import { riskType } from "models/riskType"; + +export const getRiskProperties = ( + mode: PropertiesModalMode +): PropertiesModalProperty[] => [ + ...getCommonObjectOnWellboreProperties(mode), + { + property: "commonData.dTimCreation", + propertyType: PropertyType.DateTime, + disabled: true + }, + { + property: "commonData.dTimLastChange", + propertyType: PropertyType.DateTime, + disabled: true + }, + { + property: "type", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, riskType), + helperText: getOptionHelperText("type"), + options: riskType + }, + { + property: "category", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, riskCategory), + helperText: getOptionHelperText("category"), + options: riskCategory + }, + { + property: "subCategory", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, riskSubCategory), + helperText: getOptionHelperText("subCategory"), + options: riskSubCategory + }, + { + property: "extendCategory", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Enum), + helperText: getMaxLengthHelperText("extendCategory", MaxLength.Enum) + }, + { + property: "affectedPersonnel", + propertyType: PropertyType.Options, + validator: (value: string) => + validMultiOption(value, riskAffectedPersonnel), + helperText: getOptionHelperText("affectedPersonnel"), + options: riskAffectedPersonnel, + multiSelect: true + }, + { + property: "dTimStart", + propertyType: PropertyType.DateTime + }, + { + property: "dTimEnd", + propertyType: PropertyType.DateTime + }, + { + property: "mdBitStart", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("mdBitStart") + }, + { + property: "mdBitEnd", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("mdBitEnd") + }, + { + property: "severityLevel", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, levelIntegerCode), + helperText: getOptionHelperText("severityLevel"), + options: levelIntegerCode + }, + { + property: "probabilityLevel", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, levelIntegerCode), + helperText: getOptionHelperText("severityLevel"), + options: levelIntegerCode + }, + { + property: "summary", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Description), + helperText: getMaxLengthHelperText("summary", MaxLength.Description) + }, + { + property: "details", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Description), + helperText: getMaxLengthHelperText("details", MaxLength.Description) + }, + { + property: "commonData.sourceName", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("sourceName", MaxLength.Name) + }, + { + property: "commonData.itemState", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, itemStateTypes), + helperText: getOptionHelperText("itemState"), + options: itemStateTypes + } +]; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/TrajectoryProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/TrajectoryProperties.ts new file mode 100644 index 000000000..881041f93 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/TrajectoryProperties.ts @@ -0,0 +1,68 @@ +import { + PropertiesModalMode, + validOption, + validText +} from "components/Modals/ModalParts"; +import { getCommonObjectOnWellboreProperties } from "components/Modals/PropertiesModal/Properties/CommonObjectOnWellboreProperties"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { + getMaxLengthHelperText, + getOptionHelperText +} from "components/Modals/PropertiesModal/ValidationHelpers"; +import { itemStateTypes } from "models/itemStateTypes"; +import MaxLength from "models/maxLength"; +import Trajectory, { aziRefValues } from "models/trajectory"; + +export const getTrajectoryProperties = ( + mode: PropertiesModalMode +): PropertiesModalProperty[] => [ + ...getCommonObjectOnWellboreProperties(mode), + { + property: "dTimTrajStart", + propertyType: PropertyType.DateTime, + disabled: mode === PropertiesModalMode.Edit + }, + { + property: "dTimTrajEnd", + propertyType: PropertyType.DateTime, + disabled: mode === PropertiesModalMode.Edit + }, + { + property: "serviceCompany", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("serviceCompany", MaxLength.Name) + }, + { + property: "mdMin", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "mdMax", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "aziRef", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, aziRefValues), + helperText: getOptionHelperText("aziRef"), + options: aziRefValues + }, + { + property: "commonData.sourceName", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("sourceName", MaxLength.Name), + disabled: mode === PropertiesModalMode.Edit + }, + { + property: "commonData.itemState", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, itemStateTypes), + helperText: getOptionHelperText("itemState"), + options: itemStateTypes + } +]; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/TrajectoryStationProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/TrajectoryStationProperties.ts new file mode 100644 index 000000000..10b36ce7d --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/TrajectoryStationProperties.ts @@ -0,0 +1,228 @@ +import { + PropertiesModalMode, + validMeasure, + validText +} from "components/Modals/ModalParts"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { + getMaxLengthHelperText, + getMeasureHelperText +} from "components/Modals/PropertiesModal/ValidationHelpers"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import MaxLength from "models/maxLength"; +import TrajectoryStation from "models/trajectoryStation"; + +export const getTrajectoryStationProperties = ( + mode: PropertiesModalMode +): PropertiesModalProperty[] => [ + { + property: "uid", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Uid), + helperText: getMaxLengthHelperText("uid", MaxLength.Uid), + disabled: mode === PropertiesModalMode.Edit + }, + { + property: "typeTrajStation", + propertyType: PropertyType.String, + disabled: true + }, + { + property: "dTimStn", + propertyType: PropertyType.DateTime + }, + { + property: "md", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("md") + }, + { + property: "tvd", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("tvd") + }, + { + property: "azi", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("azi") + }, + { + property: "incl", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("incl") + }, + { + property: "mtf", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "gtf", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "dispNs", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "dispEw", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "vertSect", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "dls", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "rateTurn", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "rateBuild", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "gravTotalUncert", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "dipAngleUncert", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "magTotalUncert", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "gravTotalFieldReference", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "magTotalFieldReference", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "magDipAngleReference", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "statusTrajStation", + propertyType: PropertyType.String, + disabled: true + }, + { + property: "gravAxialRaw", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "gravTran1Raw", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "gravTran2Raw", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "magAxialRaw", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "rawData.magTran1Raw", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "rawData.magTran2Raw", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "corUsed.gravAxialAccelCor", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "corUsed.gravTran1AccelCor", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "corUsed.gravTran2AccelCor", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "corUsed.magAxialDrlstrCor", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "corUsed.magTran1DrlstrCor", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "corUsed.magTran2DrlstrCor", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "corUsed.sagIncCor", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "corUsed.stnMagDeclUsed", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "corUsed.stnGridCorUsed", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "corUsed.dirSensorOffset", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "valid.magTotalFieldCalc", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "valid.magDipAngleCalc", + propertyType: PropertyType.Measure, + disabled: true + }, + { + property: "valid.gravTotalFieldCalc", + propertyType: PropertyType.Measure, + disabled: true + } +]; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/TubularComponentProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/TubularComponentProperties.ts new file mode 100644 index 000000000..f6ac4f50a --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/TubularComponentProperties.ts @@ -0,0 +1,107 @@ +import { + PropertiesModalMode, + validMeasure, + validOption, + validPositiveInteger, + validText +} from "components/Modals/ModalParts"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { + getMaxLengthHelperText, + getMeasureHelperText, + getOptionHelperText +} from "components/Modals/PropertiesModal/ValidationHelpers"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import { boxPinConfigTypes } from "models/boxPinConfigTypes"; +import { materialTypes } from "models/materialTypes"; +import MaxLength from "models/maxLength"; +import TubularComponent from "models/tubularComponent"; +import { tubularComponentTypes } from "models/tubularComponentTypes"; + +export const getTubularComponentProperties = ( + mode: PropertiesModalMode +): PropertiesModalProperty[] => [ + { + property: "uid", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Uid), + helperText: getMaxLengthHelperText("uid", MaxLength.Uid), + disabled: mode === PropertiesModalMode.Edit + }, + { + property: "sequence", + propertyType: PropertyType.Number, + validator: (value: string) => + validPositiveInteger(value) && parseInt(value) > 0, + helperText: "sequence must be a positive non-zero integer" + }, + { + property: "description", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Description), + helperText: getMaxLengthHelperText("description", MaxLength.Description) + }, + { + property: "typeTubularComponent", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, tubularComponentTypes), + helperText: getOptionHelperText("typeTubularComponent"), + options: tubularComponentTypes + }, + { + property: "id", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("id") + }, + { + property: "od", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("od") + }, + { + property: "len", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("len") + }, + { + property: "wtPerLen", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("wtPerLen") + }, + { + property: "numJointStand", + propertyType: PropertyType.Number, + validator: validPositiveInteger, + helperText: "numJointStand must be a positive integer" + }, + { + property: "configCon", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, boxPinConfigTypes), + helperText: getOptionHelperText("configCon"), + options: boxPinConfigTypes + }, + { + property: "typeMaterial", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, materialTypes), + helperText: getOptionHelperText("typeMaterial"), + options: materialTypes + }, + { + property: "vendor", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("vendor", MaxLength.Name) + }, + { + property: "model", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("model", MaxLength.Name) + } +]; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/TubularProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/TubularProperties.ts new file mode 100644 index 000000000..61cfd4297 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/TubularProperties.ts @@ -0,0 +1,21 @@ +import { PropertiesModalMode, validOption } from "components/Modals/ModalParts"; +import { getCommonObjectOnWellboreProperties } from "components/Modals/PropertiesModal/Properties/CommonObjectOnWellboreProperties"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { getOptionHelperText } from "components/Modals/PropertiesModal/ValidationHelpers"; +import Tubular from "models/tubular"; +import { typeTubularAssy } from "models/typeTubularAssy"; + +export const getTubularProperties = ( + mode: PropertiesModalMode +): PropertiesModalProperty[] => [ + ...getCommonObjectOnWellboreProperties(mode), + { + property: "typeTubularAssy", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, typeTubularAssy), + helperText: getOptionHelperText("typeTubularAssy"), + options: typeTubularAssy, + required: true + } +]; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/WbGeometryProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/WbGeometryProperties.ts new file mode 100644 index 000000000..5dc1e9b9f --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/WbGeometryProperties.ts @@ -0,0 +1,75 @@ +import { + PropertiesModalMode, + validMeasure, + validOption, + validText +} from "components/Modals/ModalParts"; +import { getCommonObjectOnWellboreProperties } from "components/Modals/PropertiesModal/Properties/CommonObjectOnWellboreProperties"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { + getMaxLengthHelperText, + getMeasureHelperText, + getOptionHelperText +} from "components/Modals/PropertiesModal/ValidationHelpers"; +import { itemStateTypes } from "models/itemStateTypes"; +import MaxLength from "models/maxLength"; +import WbGeometryObject from "models/wbGeometry"; + +export const getWbGeometryProperties = ( + mode: PropertiesModalMode +): PropertiesModalProperty[] => [ + ...getCommonObjectOnWellboreProperties(mode), + { + property: "commonData.dTimCreation", + propertyType: PropertyType.DateTime, + disabled: true + }, + { + property: "commonData.dTimLastChange", + propertyType: PropertyType.DateTime, + disabled: true + }, + { + property: "dTimReport", + propertyType: PropertyType.DateTime, + disabled: true + }, + { + property: "commonData.sourceName", + propertyType: PropertyType.String, + disabled: true + }, + { + property: "mdBottom", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("mdBottom") + }, + { + property: "gapAir", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("gapAir") + }, + { + property: "depthWaterMean", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("depthWaterMean") + }, + { + property: "commonData.comments", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Comment), + helperText: getMaxLengthHelperText("comments", MaxLength.Comment), + multiline: true + }, + { + property: "commonData.itemState", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, itemStateTypes), + helperText: getOptionHelperText("itemState"), + options: itemStateTypes + } +]; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/WbGeometrySectionProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/WbGeometrySectionProperties.ts new file mode 100644 index 000000000..0a6f640ee --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/WbGeometrySectionProperties.ts @@ -0,0 +1,105 @@ +import { + PropertiesModalMode, + validBoolean, + validMeasure, + validNumber, + validOption, + validText +} from "components/Modals/ModalParts"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { + getBooleanHelperText, + getMaxLengthHelperText, + getMeasureHelperText, + getNumberHelperText, + getOptionHelperText +} from "components/Modals/PropertiesModal/ValidationHelpers"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import { holeCasingTypes } from "models/holeCasingTypes"; +import MaxLength from "models/maxLength"; +import WbGeometrySection from "models/wbGeometrySection"; + +export const getWbGeometrySectionProperties = ( + mode: PropertiesModalMode +): PropertiesModalProperty[] => [ + { + property: "uid", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Uid), + helperText: getMaxLengthHelperText("uid", MaxLength.Uid), + disabled: mode === PropertiesModalMode.Edit + }, + { + property: "typeHoleCasing", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, holeCasingTypes), + helperText: getOptionHelperText("typeHoleCasing"), + options: holeCasingTypes + }, + { + property: "mdTop", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("mdTop") + }, + { + property: "mdBottom", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("mdBottom") + }, + { + property: "tvdTop", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("tvdTop") + }, + { + property: "tvdBottom", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("tvdBottom") + }, + { + property: "idSection", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("idSection") + }, + { + property: "odSection", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("odSection") + }, + { + property: "wtPerLen", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("wtPerLen") + }, + { + property: "curveConductor", + propertyType: PropertyType.Boolean, + validator: validBoolean, + helperText: getBooleanHelperText("curveConductor") + }, + { + property: "diaDrift", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("diaDrift") + }, + { + property: "grade", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.String32), + helperText: getMaxLengthHelperText("grade", MaxLength.String32) + }, + { + property: "factFric", + propertyType: PropertyType.Number, + validator: validNumber, + helperText: getNumberHelperText("factFric") + } +]; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/WellProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/WellProperties.ts new file mode 100644 index 000000000..157fdc7be --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/WellProperties.ts @@ -0,0 +1,73 @@ +import { + PropertiesModalMode, + validText, + validTimeZone +} from "components/Modals/ModalParts"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { + getMaxLengthHelperText, + getTimeZoneHelperText +} from "components/Modals/PropertiesModal/ValidationHelpers"; +import MaxLength from "models/maxLength"; +import Well from "models/well"; + +export const getWellProperties = ( + mode: PropertiesModalMode +): PropertiesModalProperty[] => [ + { + property: "uid", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Uid), + helperText: getMaxLengthHelperText("uid", MaxLength.Uid), + disabled: mode === PropertiesModalMode.Edit + }, + { + property: "name", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("name", MaxLength.Name), + required: true + }, + { + property: "field", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("field", MaxLength.Name) + }, + { + property: "country", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.String32), + helperText: getMaxLengthHelperText("country", MaxLength.String32) + }, + { + property: "operator", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("operator", MaxLength.Name) + }, + { + property: "timeZone", + propertyType: PropertyType.String, + validator: (value: string) => validTimeZone(value), + helperText: getTimeZoneHelperText("timeZone"), + required: true + }, + { + property: "numLicense", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("numLicense", MaxLength.Name) + }, + { + property: "dateTimeCreation", + propertyType: PropertyType.DateTime, + disabled: true + }, + { + property: "dateTimeLastChange", + propertyType: PropertyType.DateTime, + disabled: true + } +]; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/WellboreProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/WellboreProperties.ts new file mode 100644 index 000000000..d5bc49cae --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/WellboreProperties.ts @@ -0,0 +1,150 @@ +import { + PropertiesModalMode, + validMeasure, + validOption, + validText +} from "components/Modals/ModalParts"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { + getMaxLengthHelperText, + getMeasureHelperText, + getOptionHelperText +} from "components/Modals/PropertiesModal/ValidationHelpers"; +import MaxLength from "models/maxLength"; +import Wellbore from "models/wellbore"; +import { wellborePurposeValues } from "models/wellborePurposeValues"; + +export const getWellboreProperties = ( + mode: PropertiesModalMode +): PropertiesModalProperty[] => [ + { + property: "wellUid", + propertyType: PropertyType.String, + disabled: true + }, + { + property: "wellName", + propertyType: PropertyType.String, + disabled: true + }, + { + property: "wellboreParentName", + propertyType: PropertyType.String, + disabled: true + }, + { + property: "uid", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Uid), + helperText: getMaxLengthHelperText("uid", MaxLength.Uid), + disabled: mode === PropertiesModalMode.Edit + }, + { + property: "name", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("name", MaxLength.Name), + required: true + }, + { + property: "wellborePurpose", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, wellborePurposeValues), + helperText: getOptionHelperText("wellborePurpose"), + options: wellborePurposeValues + }, + { + property: "number", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.String32), + helperText: getMaxLengthHelperText("number", MaxLength.String32) + }, + { + property: "suffixAPI", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("suffixAPI", MaxLength.Name) + }, + { + property: "numGovt", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("numGovt", MaxLength.Name) + }, + { + property: "dTimeKickoff", + propertyType: PropertyType.DateTime + }, + { + property: "md", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("md") + }, + { + property: "tvd", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("tvd") + }, + { + property: "mdKickoff", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("mdKickoff") + }, + { + property: "tvdKickoff", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("tvdKickoff") + }, + { + property: "mdPlanned", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("mdPlanned") + }, + { + property: "tvdPlanned", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("tvdPlanned") + }, + { + property: "mdSubSeaPlanned", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("mdSubSeaPlanned") + }, + { + property: "tvdSubSeaPlanned", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("tvdSubSeaPlanned") + }, + { + property: "dayTarget", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("dayTarget") + }, + { + property: "dateTimeCreation", + propertyType: PropertyType.DateTime, + disabled: true + }, + { + property: "dateTimeLastChange", + propertyType: PropertyType.DateTime, + disabled: true + }, + { + property: "comments", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Comment), + helperText: getMaxLengthHelperText("comments", MaxLength.Comment), + multiline: true + } +]; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/getFluidsReportProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/getFluidsReportProperties.ts new file mode 100644 index 000000000..85f390cf2 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/getFluidsReportProperties.ts @@ -0,0 +1,82 @@ +import { + PropertiesModalMode, + validMeasure, + validOption, + validPositiveInteger, + validText +} from "components/Modals/ModalParts"; +import { getCommonObjectOnWellboreProperties } from "components/Modals/PropertiesModal/Properties/CommonObjectOnWellboreProperties"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { + getMaxLengthHelperText, + getMeasureHelperText, + getOptionHelperText +} from "components/Modals/PropertiesModal/ValidationHelpers"; +import FluidsReport from "models/fluidsReport"; +import { itemStateTypes } from "models/itemStateTypes"; +import MaxLength from "models/maxLength"; + +export const getFluidsReportProperties = ( + mode: PropertiesModalMode +): PropertiesModalProperty[] => [ + ...getCommonObjectOnWellboreProperties(mode), + { + property: "dTim", + propertyType: PropertyType.DateTime + }, + { + property: "md", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("md") + }, + { + property: "tvd", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("tvd") + }, + { + property: "numReport", + propertyType: PropertyType.StringNumber, + validator: validPositiveInteger, + helperText: "numReport must be a positive integer" + }, + { + property: "commonData.sourceName", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("sourceName", MaxLength.Name) + }, + { + property: "commonData.itemState", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, itemStateTypes), + helperText: getOptionHelperText("itemState"), + options: itemStateTypes + }, + { + property: "commonData.serviceCategory", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Enum), + helperText: getMaxLengthHelperText("serviceCategory", MaxLength.Enum) + }, + { + property: "commonData.comments", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Comment), + helperText: getMaxLengthHelperText("comments", MaxLength.Comment), + multiline: true + }, + { + property: "commonData.dTimCreation", + propertyType: PropertyType.DateTime, + disabled: true + }, + { + property: "commonData.dTimLastChange", + propertyType: PropertyType.DateTime, + disabled: true + } +]; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/PropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/PropertiesModal.tsx new file mode 100644 index 000000000..11e5bc3c1 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/PropertiesModal.tsx @@ -0,0 +1,105 @@ +import { getNestedValue } from "components/Modals/PropertiesModal/NestedPropertyHelpers"; +import { PropertiesRenderer } from "components/Modals/PropertiesModal/PropertiesRenderer"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { isPropertyValid } from "components/Modals/PropertiesModal/ValidationHelpers"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import { cloneDeep } from "lodash"; +import { ReactElement, useState } from "react"; +import styled from "styled-components"; +import ModalDialog, { ModalWidth } from "../ModalDialog"; + +export interface PropertiesModalProps { + title: string; + object: T; + properties: PropertiesModalProperty[]; + onSubmit: (updates: Partial) => void; +} + +/** + * PropertiesModal component + * + * A modal dialog for editing properties of a given object. It renders a list of properties, + * validates the input, and submits only the modified properties via the onSubmit callback. + * + * @template T - The type of the object whose properties are being edited. + * + * @param {PropertiesModalProps} props - The props for the PropertiesModal component. + * @param {string} props.title - The title of the modal dialog. + * @param {T} props.object - The object whose properties are to be edited. + * @param {PropertiesModalProperty[]} props.properties - The list of properties available for editing. + * @param {(updates: Partial) => void} props.onSubmit - Callback for submitting the modified properties. + * + * @returns {ReactElement} A React element representing the properties modal dialog. + * + * @remarks + * - The `onSubmit` callback only receives modified properties. + * - The modal validates each property before enabling the submit button. + */ + +export const PropertiesModal = ( + props: PropertiesModalProps +): ReactElement => { + const { title, object, properties, onSubmit } = props; + const [updates, setUpdates] = useState>({}); + const allValid = properties.every((prop) => + isPropertyValid(prop, object, updates) + ); + const anyUpdates = Object.keys(updates).length > 0; + + const getFullUpdateObject = () => { + const notPartialProperties = [ + PropertyType.Measure, + PropertyType.RefNameString, + PropertyType.StratigraphicStruct + ]; + const fullUpdates = cloneDeep(updates); + Object.keys(fullUpdates).forEach((property) => { + const propertyType = properties.find( + (prop) => prop.property === property + )?.propertyType; + if (notPartialProperties.includes(propertyType)) { + const originalValue = getNestedValue(object, property); + fullUpdates[property as keyof T] = { + ...originalValue, + ...updates[property as keyof T] + }; + } + }); + return fullUpdates; + }; + + const onInternalSubmit = async () => { + // Some datatypes like measures needs both the value and uom in order to update, so make sure the original is used if only partially modified by the user. + const fullUpdates = getFullUpdateObject(); + onSubmit(fullUpdates); + }; + + return ( + + {properties.length === 0 &&

No properties to update.

} + + + } + confirmDisabled={!allValid || !anyUpdates} + onSubmit={onInternalSubmit} + isLoading={false} + /> + ); +}; + +const Layout = styled.div` + margin-top: 12px; + margin-bottom: 12px; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; +`; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/PropertiesRenderer.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/PropertiesRenderer.tsx new file mode 100644 index 000000000..134cecd5e --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/PropertiesRenderer.tsx @@ -0,0 +1,295 @@ +import { Autocomplete, TextField, Typography } from "@equinor/eds-core-react"; +import { Stack } from "@mui/material"; +import { LogHeaderDateTimeField } from "components/Modals/LogHeaderDateTimeField"; +import { + deleteNestedValue, + getNestedValue, + setNestedValue +} from "components/Modals/PropertiesModal/NestedPropertyHelpers"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { + formatPropertyValue, + getHelperText, + getVariant, + hasPropertyChanged +} from "components/Modals/PropertiesModal/ValidationHelpers"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import { cloneDeep } from "lodash"; +import { ChangeEvent, Fragment, KeyboardEvent, ReactElement } from "react"; +import styled from "styled-components"; + +interface PropertiesRendererProps { + properties: PropertiesModalProperty[]; + object: T; + updates: Partial; + onChange: (updates: Partial) => void; +} + +export const PropertiesRenderer = ({ + properties, + object, + updates, + onChange +}: PropertiesRendererProps): ReactElement => { + const getInitialSelectedOptions = (prop: PropertiesModalProperty) => { + const optionsString = getNestedValue(object, prop.property)?.toString(); + if (!optionsString) return []; + return prop.multiSelect ? optionsString.split(", ") : [optionsString]; + }; + + const onOptionsChange = ( + prop: PropertiesModalProperty, + selectedItems: string[] + ) => { + const updatedValue = + (prop.multiSelect + ? selectedItems.sort().join(", ") + : selectedItems?.[0]) ?? ""; + onChangeProperty(prop.property, prop.propertyType, updatedValue); + }; + + const onChangeProperty = ( + property: string, + propertyType: PropertyType, + value: string + ) => { + const originalValue = getNestedValue(object, property); + const formattedValue = formatPropertyValue(property, propertyType, value); + if (hasPropertyChanged(propertyType, formattedValue, originalValue)) { + onChange(setNestedValue(cloneDeep(updates), property, formattedValue)); + } else { + onChange(deleteNestedValue(cloneDeep(updates), property)); + } + }; + + return ( + <> + {properties.map((prop) => { + switch (prop.propertyType) { + case PropertyType.List: { + const subObjectList = getNestedValue(object, prop.property) ?? []; + const subUpdatesList = getNestedValue(updates, prop.property) ?? []; + const onSubObjectChange = ( + subObject: any, + updates: Partial + ) => { + const fullSubUpdate = { ...cloneDeep(subObject), ...updates }; + const updateIndex = subUpdatesList.findIndex( + (update: any) => update.uid === subObject.uid + ); + let fullSubUpdateList = cloneDeep(subUpdatesList); + if (updateIndex >= 0) { + fullSubUpdateList[updateIndex] = fullSubUpdate; + } else { + fullSubUpdateList = [...fullSubUpdateList, fullSubUpdate]; + } + onChangeProperty( + prop.property, + prop.propertyType, + fullSubUpdateList + ); + }; + return ( + + {subObjectList.map((subObject: any, i: number) => ( + + + + {prop.itemPrefix + + (subObject.name || + subObject.mnemonic || + subObject.uid || + i.toString())} + + + subUpdate.uid === subObject.uid + ) ?? {} + } + onChange={(updates) => + onSubObjectChange(subObject, updates) + } + /> + + ))} + + ); + } + case PropertyType.Boolean: + case PropertyType.Options: { + const options = + (prop.propertyType === PropertyType.Boolean + ? ["true", "false"] + : prop.options) ?? []; + return ( + + onOptionsChange(prop, selectedItems) + } + onInputChange={(text: string) => { + if (!prop.multiSelect) { + onChangeProperty(prop.property, prop.propertyType, text); + } + }} + hideClearButton={!prop.multiSelect} + multiple={prop.multiSelect} + /> + ); + } + case PropertyType.DateTime: + return ( + { + onChangeProperty(prop.property, prop.propertyType, dateTime); + }} + /> + ); + case PropertyType.Measure: + return ( + + ) => + onChangeProperty( + `${prop.property}.value`, + prop.propertyType, + e.target.value + ) + } + /> + ) => + onChangeProperty( + `${prop.property}.uom`, + prop.propertyType, + e.target.value + ) + } + style={{ + width: "300px" + }} + /> + + ); + case PropertyType.RefNameString: + case PropertyType.StratigraphicStruct: { + const secondaryProperty = + prop.propertyType === PropertyType.RefNameString + ? "uidRef" + : "kind"; + return ( + + ) => + onChangeProperty( + `${prop.property}.value`, + prop.propertyType, + e.target.value + ) + } + /> + ) => + onChangeProperty( + `${prop.property}.${secondaryProperty}`, + prop.propertyType, + e.target.value + ) + } + style={{ + width: "300px" + }} + /> + + ); + } + case PropertyType.String: + case PropertyType.StringNumber: + case PropertyType.Number: + return ( + ) => { + if (prop.multiline && e.key === "Enter") e.stopPropagation(); + }} + disabled={prop.disabled} + defaultValue={getNestedValue(object, prop.property)} + helperText={getHelperText(prop, object, updates)} + variant={getVariant(prop, object, updates)} + onChange={(e: ChangeEvent) => + onChangeProperty( + prop.property, + prop.propertyType, + e.target.value + ) + } + /> + ); + } + })} + + ); +}; + +const ListHeaderLayout = styled.div` + grid-column: 1 / -1; + margin-top: 12px; +`; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/PropertyTypes.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/PropertyTypes.ts new file mode 100644 index 000000000..1b82c760c --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/PropertyTypes.ts @@ -0,0 +1,49 @@ +/** + * PropertyType options and details: + * + * PropertyType.String: + * - A simple text input. + * - Can be multi-line if `multiline` is set to true. + * + * PropertyType.StringNumber: + * - A numerical input that returns the number as a string. + * + * PropertyType.Number: + * - A numerical input. + * + * PropertyType.DateTime: + * - An input for date and time. + * + * PropertyType.Measure: + * - Combines a value and unit of measure. + * + * PropertyType.Options: + * - A dropdown or multi-select input. + * - Requires an `options` array. + * - Can be multi-select if `multiSelect` is set to true. + * + * PropertyType.RefNameString: + * - A reference name input, often used for linked data. + * + * PropertyType.StratigraphicStruct: + * - For complex stratigraphic structure inputs. + * + * PropertyType.Boolean: + * - A dropdown input for boolean values. + * + * PropertyType.List: + * - An input for a list of items. + * - Requires `subProps` defining the properties of list items. + */ +export enum PropertyType { + String, + StringNumber, + Number, + DateTime, + Measure, + Options, + RefNameString, + StratigraphicStruct, + Boolean, + List +} diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/ValidationHelpers.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/ValidationHelpers.ts new file mode 100644 index 000000000..6e7411698 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/ValidationHelpers.ts @@ -0,0 +1,157 @@ +import { Variants } from "@equinor/eds-core-react/dist/types/components/types"; +import { getNestedValue } from "components/Modals/PropertiesModal/NestedPropertyHelpers"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; + +const getActualValue = ( + propertyType: PropertyType, + value: any, + originalValue: any +) => { + switch (propertyType) { + case PropertyType.RefNameString: + case PropertyType.StratigraphicStruct: + case PropertyType.Measure: + return !value && !originalValue ? null : { ...originalValue, ...value }; + default: + return value === undefined ? originalValue : value; + } +}; + +export const isPropertyValid = ( + prop: PropertiesModalProperty, + originalObject: T, + updates: Partial +): boolean => { + const originalValue = getNestedValue(originalObject, prop.property); + const updatedValue = getNestedValue(updates, prop.property); + const actualValue = getActualValue( + prop.propertyType, + updatedValue, + originalValue + ); + if (prop.propertyType === PropertyType.List) { + if (updatedValue === undefined) return true; + return (updatedValue as any[]).every((subValue) => { + const originalSubObject = (originalValue as any[]).find( + (oV) => oV.uid === subValue.uid + ); + return prop.subProps.every((subProp) => + isPropertyValid(subProp, originalSubObject, subValue) + ); + }); + } + if (prop.validator && (actualValue || typeof actualValue === "boolean")) { + return prop.validator(actualValue, originalValue); + } + const isRequired = prop.required || !!originalValue; + if (isRequired && !actualValue) { + return false; + } + return true; +}; + +export const getHelperText = ( + prop: PropertiesModalProperty, + originalObject: T, + updates: Partial +) => { + return isPropertyValid(prop, originalObject, updates) ? "" : prop.helperText; +}; + +export const getVariant = ( + prop: PropertiesModalProperty, + originalObject: T, + updates: Partial +): Variants => { + return isPropertyValid(prop, originalObject, updates) ? undefined : "error"; +}; + +export const hasPropertyChanged = ( + propertyType: PropertyType, + value: any, + originalValue: any +) => { + switch (propertyType) { + case PropertyType.List: { + const updatedUids = (value as any[]).map((v) => v.uid); + const originalFiltered = (originalValue as any[]) + .map((obj) => + Object.fromEntries(Object.entries(obj).filter(([, v]) => v != null)) + ) + .filter((obj) => updatedUids.includes(obj.uid)); + const valueFiltered = (value as any[]).map((obj) => + Object.fromEntries(Object.entries(obj).filter(([, v]) => v != null)) + ); + return JSON.stringify(originalFiltered) !== JSON.stringify(valueFiltered); + } + case PropertyType.DateTime: + return Date.parse(originalValue) !== Date.parse(value); + default: + return ( + !( + value === "" && + (originalValue === null || originalValue === undefined) + ) && value !== originalValue + ); + } +}; + +export const formatPropertyValue = ( + property: string, + propertyType: PropertyType, + value: string +) => { + switch (propertyType) { + case PropertyType.Boolean: + if (!["true", "false"].includes(value)) return "null"; + return value === "true"; + case PropertyType.Number: + return parseFloat(value); + case PropertyType.Measure: + if ( + propertyType === PropertyType.Measure && + property.endsWith(".value") && + value !== "" + ) { + return parseFloat(value); + } + } + return value; +}; + +export const getMaxLengthHelperText = (property: string, maxLength: number) => { + return `${property} must be 1-${maxLength} characters`; +}; + +export const getTimeZoneHelperText = (property: string) => { + return `${property} has to be 'Z' or in the format -hh:mm or +hh:mm within the range (-12:00 to +14:00) and minutes has to be 00, 30 or 45`; +}; + +export const getPhoneNumberHelperText = (property: string) => { + return `${property} must be an integer of 1-32 characters. Whitespace, dash and plus is accepted`; +}; + +export const getNumberHelperText = (property: string) => { + return `${property} must be a valid number`; +}; + +export const getMeasureHelperText = (property: string) => { + return `${property} must have a valid number and unit`; +}; + +export const getBooleanHelperText = (property: string) => { + return `${property} must be true or false`; +}; + +export const getStratigraphicStructHelperText = (property: string) => { + return `${property} must have a valid value and kind`; +}; + +export const getRefNameStringHelperText = (property: string) => { + return `${property} must have a valid value and uidRef`; +}; + +export const getOptionHelperText = (property: string) => { + return `${property} must have a valid option`; +}; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/__tests__/LogCurveInfoPropertiesModal.test.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/__tests__/LogCurveInfoPropertiesModal.test.tsx new file mode 100644 index 000000000..48b318d6d --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/__tests__/LogCurveInfoPropertiesModal.test.tsx @@ -0,0 +1,103 @@ +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { mockEdsCoreReact } from "__testUtils__/mocks/EDSMocks"; +import { + getAxisDefinition, + getLogCurveInfo, + renderWithContexts +} from "__testUtils__/testUtils"; +import { PropertiesModalMode } from "components/Modals/ModalParts"; +import { getLogCurveInfoProperties } from "components/Modals/PropertiesModal/Properties/LogCurveInfoProperties"; +import { + PropertiesModal, + PropertiesModalProps +} from "components/Modals/PropertiesModal/PropertiesModal"; +import LogCurveInfo from "models/logCurveInfo"; +import { vi } from "vitest"; + +vi.mock("@equinor/eds-core-react", () => mockEdsCoreReact()); + +const simpleProps: PropertiesModalProps = { + title: `Edit LogCurveInfo properties`, + object: getLogCurveInfo(), + properties: getLogCurveInfoProperties(PropertiesModalMode.Edit, false), + onSubmit: () => {} +}; + +const propsWithAxisDefinition: PropertiesModalProps = { + ...simpleProps, + object: getLogCurveInfo({ + axisDefinitions: [getAxisDefinition()] + }) +}; + +describe("Tests for PropertiesModal for LogCurveInfo", () => { + it("Properties of a LogCurve should be shown in the modal", async () => { + const expectedLogCurveInfo = simpleProps.object; + + renderWithContexts(); + + const uidInput = screen.getByRole("textbox", { name: /uid/i }); + const mnemonicInput = screen.getByRole("textbox", { name: /mnemonic/i }); + + expect(uidInput).toHaveValue(expectedLogCurveInfo.uid); + expect(mnemonicInput).toHaveValue(expectedLogCurveInfo.mnemonic); + + expect(uidInput).toBeDisabled(); + expect(mnemonicInput).toBeEnabled(); + }); + + it("AxisDefinition should be shown disabled in the LogCurveInfo modal when included in the props", async () => { + const expectedLogCurveInfo = propsWithAxisDefinition.object; + const expectedAxisDefinition = expectedLogCurveInfo.axisDefinitions[0]; + + renderWithContexts(); + + const uidInput = screen.getByRole("textbox", { name: /uid/i }); + const mnemonicInput = screen.getByRole("textbox", { name: /mnemonic/i }); + const axisDefinitionLabel = screen.getByText(/axisdefinition/i); + const orderInput = screen.getByRole("spinbutton", { name: /order/i }); + const countInput = screen.getByRole("spinbutton", { name: /count/i }); + const doubleValuesInput = screen.getByRole("textbox", { + name: /doubleValues/i + }); + + expect(uidInput).toHaveValue(expectedLogCurveInfo.uid); + expect(mnemonicInput).toHaveValue(expectedLogCurveInfo.mnemonic); + expect(axisDefinitionLabel).toHaveTextContent(expectedAxisDefinition.uid); + expect(orderInput).toHaveValue(expectedAxisDefinition.order); + expect(countInput).toHaveValue(expectedAxisDefinition.count); + expect(doubleValuesInput).toHaveValue(expectedAxisDefinition.doubleValues); + + expect(uidInput).toBeDisabled(); + expect(mnemonicInput).toBeEnabled(); + expect(orderInput).toBeDisabled(); + expect(countInput).toBeDisabled(); + expect(doubleValuesInput).toBeDisabled(); + }); + + it("Saving edited properties of a LogCurve should call onSubmit with the changed parts of the object", async () => { + const user = userEvent.setup(); + const onSubmitMock = vi.fn(); + + const props = { + ...simpleProps, + onSubmit: onSubmitMock + }; + + renderWithContexts(); + + const mnemonicInput = screen.getByRole("textbox", { name: /mnemonic/i }); + const saveButton = screen.getByRole("button", { name: /save/i }); + + await user.clear(mnemonicInput); + await user.type(mnemonicInput, "editedMnemonic"); + + expect(saveButton).toBeEnabled(); + + await user.click(saveButton); + + expect(onSubmitMock).toHaveBeenCalledTimes(1); + expect(onSubmitMock).toHaveBeenCalledWith({ mnemonic: "editedMnemonic" }); + }); +}); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/__tests__/PropertiesModal.test.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/__tests__/PropertiesModal.test.tsx new file mode 100644 index 000000000..16e07ab74 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/__tests__/PropertiesModal.test.tsx @@ -0,0 +1,406 @@ +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { mockEdsCoreReact } from "__testUtils__/mocks/EDSMocks"; +import { + MockResizeObserver, + renderWithContexts +} from "__testUtils__/testUtils"; +import { + validBoolean, + validMeasure, + validOption, + validPositiveInteger, + validText +} from "components/Modals/ModalParts"; +import { + PropertiesModal, + PropertiesModalProps +} from "components/Modals/PropertiesModal/PropertiesModal"; +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; +import { + getBooleanHelperText, + getMaxLengthHelperText, + getMeasureHelperText, + getOptionHelperText +} from "components/Modals/PropertiesModal/ValidationHelpers"; +import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; +import MaxLength from "models/maxLength"; +import Measure from "models/measure"; +import { Mock, vi } from "vitest"; + +vi.mock("@equinor/eds-core-react", () => mockEdsCoreReact()); + +interface TestObject { + stringProperty: string; + numericStringProperty: string; + numberProperty: number; + measureProperty: Measure; + optionsProperty: string; + booleanProperty: boolean; + listProperty: SubObject[]; +} + +interface SubObject { + stringSubProperty: string; +} + +const initialObject: TestObject = { + stringProperty: "stringValue", + numericStringProperty: "3", + numberProperty: 5, + measureProperty: { + value: 3.2, + uom: "m" + }, + optionsProperty: "option 2", + booleanProperty: false, + listProperty: [ + { + stringSubProperty: "subStringValue" + } + ] +}; + +const testOptions = ["option 1", "option 2", "option 3"]; + +const subProperties: PropertiesModalProperty[] = [ + { + property: "stringSubProperty", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("stringSubProperty", MaxLength.Name) + } +]; + +const testProperties: PropertiesModalProperty[] = [ + { + property: "stringProperty", + propertyType: PropertyType.String, + validator: (value: string) => validText(value, 1, MaxLength.Name), + helperText: getMaxLengthHelperText("stringProperty", MaxLength.Name) + }, + { + property: "numericStringProperty", + propertyType: PropertyType.StringNumber, + validator: validPositiveInteger, + helperText: "numericStringProperty must be a positive integer" + }, + { + property: "numberProperty", + propertyType: PropertyType.Number, + validator: validPositiveInteger, + helperText: "numberProperty must be a positive integer" + }, + { + property: "measureProperty", + propertyType: PropertyType.Measure, + validator: validMeasure, + helperText: getMeasureHelperText("measureProperty") + }, + { + property: "optionsProperty", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, testOptions), + helperText: getOptionHelperText("optionsProperty"), + options: testOptions + }, + { + property: "booleanProperty", + propertyType: PropertyType.Boolean, + validator: validBoolean, + helperText: getBooleanHelperText("booleanProperty") + }, + { + property: "listProperty", + propertyType: PropertyType.List, + subProps: subProperties, + itemPrefix: "ListItem " + } +]; + +describe("Tests for PropertiesModal", () => { + window.ResizeObserver = MockResizeObserver; + let testProps: PropertiesModalProps; + let onSubmitMock: Mock; + + beforeEach(() => { + onSubmitMock = vi.fn(); + testProps = { + title: `Edit TestObject properties`, + object: initialObject, + properties: testProperties, + onSubmit: onSubmitMock + }; + }); + + it("Should show all properties", async () => { + renderWithContexts(); + + const stringInput = screen.getByRole("textbox", { + name: /stringProperty/i + }); + const numericStringInput = screen.getByRole("spinbutton", { + name: /numericStringProperty/i + }); + const numberInput = screen.getByRole("spinbutton", { + name: /numberProperty/i + }); + const measureValueInput = screen.getByRole("spinbutton", { + name: /measureProperty/i + }); + const measureUnitInput = screen.getByRole("textbox", { name: /unit/i }); + const optionsInput = screen.getByRole("combobox", { + name: /optionsProperty/i + }); + const booleanInput = screen.getByRole("combobox", { + name: /booleanProperty/i + }); + const listHeader = screen.getByText(/ListItem/i); + const subStringInput = screen.getByRole("textbox", { + name: /stringSubProperty/i + }); + + expect(stringInput).toHaveValue(initialObject.stringProperty); + expect(stringInput).toBeEnabled(); + expect(numericStringInput).toHaveValue( + parseInt(initialObject.numericStringProperty) + ); + expect(numericStringInput).toBeEnabled(); + expect(numberInput).toHaveValue(initialObject.numberProperty); + expect(numberInput).toBeEnabled(); + expect(measureValueInput).toHaveValue(initialObject.measureProperty.value); + expect(measureValueInput).toBeEnabled(); + expect(measureUnitInput).toHaveValue(initialObject.measureProperty.uom); + expect(measureUnitInput).toBeEnabled(); + expect(optionsInput).toHaveValue(initialObject.optionsProperty); + expect(optionsInput).toBeEnabled(); + expect(booleanInput).toHaveValue(initialObject.booleanProperty.toString()); + expect(booleanInput).toBeEnabled(); + expect(listHeader).toBeInTheDocument(); + expect(subStringInput).toHaveValue( + initialObject.listProperty[0].stringSubProperty + ); + expect(subStringInput).toBeEnabled(); + }); + + it("Should disable save when no values are changed", async () => { + renderWithContexts(); + + const saveButton = screen.getByRole("button", { name: /save/i }); + + expect(saveButton).toBeDisabled(); + }); + + it("Should enable save when a value is valid and changed", async () => { + const user = userEvent.setup(); + renderWithContexts(); + const newValue = "newStringValue"; + + const input = screen.getByRole("textbox", { name: /stringProperty/i }); + const saveButton = screen.getByRole("button", { name: /save/i }); + await user.clear(input); + await user.type(input, newValue); + + expect(input).toHaveValue(newValue); + expect(saveButton).toBeEnabled(); + }); + + it("Should disable save when all properties are reverted to their original value as no values are changed", async () => { + const user = userEvent.setup(); + renderWithContexts(); + + const stringInput = screen.getByRole("textbox", { + name: /stringProperty/i + }); + const measureValueInput = screen.getByRole("spinbutton", { + name: /measureProperty/i + }); + const subStringInput = screen.getByRole("textbox", { + name: /stringSubProperty/i + }); + const saveButton = screen.getByRole("button", { name: /save/i }); + + // Change the input fields + await user.type(stringInput, "a"); + await user.type(measureValueInput, "5"); + await user.type(subStringInput, "b"); + + // Revert changes + await user.type(stringInput, "{backspace}"); + await user.type(measureValueInput, "{backspace}"); + await user.type(subStringInput, "{backspace}"); + + expect(stringInput).toHaveValue(initialObject.stringProperty); + expect(measureValueInput).toHaveValue(initialObject.measureProperty.value); + expect(subStringInput).toHaveValue( + initialObject.listProperty[0].stringSubProperty + ); + expect(saveButton).toBeDisabled(); + }); + + it("Clicking cancel should not call the callback", async () => { + const user = userEvent.setup(); + renderWithContexts(); + const newValue = "newStringValue"; + + const input = screen.getByRole("textbox", { name: /stringProperty/i }); + const cancelButton = screen.getByRole("button", { name: /cancel/i }); + await user.clear(input); + await user.type(input, newValue); + await user.click(cancelButton); + + expect(onSubmitMock).not.toHaveBeenCalled(); + }); + + it("Saving edited string should call callback with the changed property", async () => { + const user = userEvent.setup(); + renderWithContexts(); + const newValue = "newStringValue"; + + const input = screen.getByRole("textbox", { name: /stringProperty/i }); + const saveButton = screen.getByRole("button", { name: /save/i }); + await user.clear(input); + await user.type(input, newValue); + await user.click(saveButton); + + expect(onSubmitMock).toHaveBeenCalledOnce(); + expect(onSubmitMock).toHaveBeenCalledWith({ stringProperty: newValue }); + }); + + it("Saving edited stringNumber should call callback with the changed number as a string", async () => { + const user = userEvent.setup(); + renderWithContexts(); + const newValue = "9"; + + const input = screen.getByRole("spinbutton", { + name: /numericStringProperty/i + }); + const saveButton = screen.getByRole("button", { name: /save/i }); + await user.clear(input); + await user.type(input, newValue); + await user.click(saveButton); + + expect(onSubmitMock).toHaveBeenCalledOnce(); + expect(onSubmitMock).toHaveBeenCalledWith({ + numericStringProperty: newValue + }); + }); + + it("Saving edited number should call callback with the changed number as a number", async () => { + const user = userEvent.setup(); + renderWithContexts(); + const newValue = 9; + + const input = screen.getByRole("spinbutton", { name: /numberProperty/i }); + const saveButton = screen.getByRole("button", { name: /save/i }); + await user.clear(input); + await user.type(input, newValue.toString()); + await user.click(saveButton); + + expect(onSubmitMock).toHaveBeenCalledOnce(); + expect(onSubmitMock).toHaveBeenCalledWith({ numberProperty: newValue }); + }); + + it("Saving edited boolean should call callback with the changed boolean", async () => { + const user = userEvent.setup(); + renderWithContexts(); + const newValue = true; + + const input = screen.getByRole("combobox", { name: /booleanProperty/i }); + const saveButton = screen.getByRole("button", { name: /save/i }); + await user.clear(input); + await user.type(input, newValue.toString()); + await user.click(saveButton); + + expect(onSubmitMock).toHaveBeenCalledOnce(); + expect(onSubmitMock).toHaveBeenCalledWith({ booleanProperty: newValue }); + }); + + it("Saving partially edited measure should call callback with the full measure", async () => { + const user = userEvent.setup(); + renderWithContexts(); + const newValue = 9.99; + + const input = screen.getByRole("spinbutton", { name: /measureProperty/i }); + const saveButton = screen.getByRole("button", { name: /save/i }); + await user.clear(input); + await user.type(input, newValue.toString()); + await user.click(saveButton); + + expect(onSubmitMock).toHaveBeenCalledOnce(); + expect(onSubmitMock).toHaveBeenCalledWith({ + measureProperty: { + value: newValue, + uom: initialObject.measureProperty.uom + } + }); + }); + + it("Invalid edited string should give an error", async () => { + const user = userEvent.setup(); + renderWithContexts(); + const newText = "editedInputWithTooLongText".repeat(10); + const expectedHelperText = testProps.properties.find( + (p) => p.property === "stringProperty" + ).helperText; + + const input = screen.getByRole("textbox", { name: /stringProperty/i }); + const saveButton = screen.getByRole("button", { name: /save/i }); + await user.clear(input); + await user.type(input, newText); + const helperText = screen.getByText(expectedHelperText); + + expect(input).toHaveValue(newText); + expect(helperText).toBeInTheDocument(); + expect(saveButton).toBeDisabled(); + }); + + it("Clearing the measure value should give an error", async () => { + const user = userEvent.setup(); + renderWithContexts(); + const expectedHelperText = testProps.properties.find( + (p) => p.property === "measureProperty" + ).helperText; + + const input = screen.getByRole("spinbutton", { name: /measureProperty/i }); + const saveButton = screen.getByRole("button", { name: /save/i }); + await user.clear(input); + const helperText = screen.getByText(expectedHelperText); + + expect(helperText).toBeInTheDocument(); + expect(saveButton).toBeDisabled(); + }); + + it("Clearing the measure unit should give an error", async () => { + const user = userEvent.setup(); + renderWithContexts(); + const expectedHelperText = testProps.properties.find( + (p) => p.property === "measureProperty" + ).helperText; + + const input = screen.getByRole("textbox", { name: /unit/i }); + const saveButton = screen.getByRole("button", { name: /save/i }); + await user.clear(input); + const helperText = screen.getByText(expectedHelperText); + + expect(helperText).toBeInTheDocument(); + expect(saveButton).toBeDisabled(); + }); + + it("Invalid option should give an error", async () => { + const user = userEvent.setup(); + renderWithContexts(); + const expectedHelperText = testProps.properties.find( + (p) => p.property === "optionsProperty" + ).helperText; + + const input = screen.getByRole("combobox", { name: /optionsProperty/i }); + const saveButton = screen.getByRole("button", { name: /save/i }); + await user.clear(input); + await user.type(input, "option 4"); + const helperText = screen.getByText(expectedHelperText); + + expect(helperText).toBeInTheDocument(); + expect(saveButton).toBeDisabled(); + }); +}); diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/openPropertiesHelpers.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/openPropertiesHelpers.tsx new file mode 100644 index 000000000..d07904513 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/openPropertiesHelpers.tsx @@ -0,0 +1,109 @@ +import { PropertiesModalMode } from "components/Modals/ModalParts"; +import { getObjectOnWellboreProperties } from "components/Modals/PropertiesModal/Properties/ObjectOnWellboreProperties"; +import { getWellProperties } from "components/Modals/PropertiesModal/Properties/WellProperties"; +import { getWellboreProperties } from "components/Modals/PropertiesModal/Properties/WellboreProperties"; +import { + PropertiesModal, + PropertiesModalProps +} from "components/Modals/PropertiesModal/PropertiesModal"; +import { + orderCreateObjectOnWellboreJob, + orderCreateWellJob, + orderCreateWellboreJob, + orderModifyObjectOnWellboreJob, + orderModifyWellJob, + orderModifyWellboreJob +} from "components/Modals/PropertiesModal/orderPropertyJobHelpers"; +import { DispatchOperation } from "contexts/operationStateReducer"; +import OperationType from "contexts/operationType"; +import LogObject from "models/logObject"; +import { ObjectType, ObjectTypeToModel } from "models/objectType"; +import Well from "models/well"; +import Wellbore from "models/wellbore"; + +export const openObjectOnWellboreProperties = async ( + objectType: T, + object: ObjectTypeToModel[T], + dispatchOperation: DispatchOperation, + mode: PropertiesModalMode = PropertiesModalMode.Edit +) => { + dispatchOperation({ type: OperationType.HideContextMenu }); + const indexType = + objectType === ObjectType.Log ? (object as LogObject).indexType : null; + const properties = getObjectOnWellboreProperties(objectType, mode, indexType); + + const propertyModalProps: PropertiesModalProps = { + title: + mode === PropertiesModalMode.Edit + ? `Edit properties for ${object.name}` + : `Create new ${objectType}`, + object, + properties, + onSubmit: async (updates) => { + dispatchOperation({ type: OperationType.HideModal }); + mode === PropertiesModalMode.Edit + ? orderModifyObjectOnWellboreJob(objectType, object, updates) + : orderCreateObjectOnWellboreJob(objectType, object, updates); + } + }; + dispatchOperation({ + type: OperationType.DisplayModal, + payload: + }); +}; + +export const openWellProperties = async ( + well: Well, + dispatchOperation: DispatchOperation, + mode: PropertiesModalMode = PropertiesModalMode.Edit +) => { + dispatchOperation({ type: OperationType.HideContextMenu }); + const properties = getWellProperties(mode); + + const propertyModalProps: PropertiesModalProps = { + title: + mode === PropertiesModalMode.Edit + ? `Edit properties for ${well.name}` + : "Create new Well", + object: well, + properties, + onSubmit: async (updates) => { + dispatchOperation({ type: OperationType.HideModal }); + mode === PropertiesModalMode.Edit + ? orderModifyWellJob(well, updates) + : orderCreateWellJob(well, updates); + } + }; + dispatchOperation({ + type: OperationType.DisplayModal, + payload: + }); +}; + +export const openWellboreProperties = async ( + wellbore: Wellbore, + dispatchOperation: DispatchOperation, + mode: PropertiesModalMode = PropertiesModalMode.Edit +) => { + dispatchOperation({ type: OperationType.HideContextMenu }); + const properties = getWellboreProperties(mode); + + const propertyModalProps: PropertiesModalProps = { + title: + mode === PropertiesModalMode.Edit + ? `Edit properties for ${wellbore.name}` + : "Create new Wellbore", + object: wellbore, + properties, + onSubmit: async (updates) => { + dispatchOperation({ type: OperationType.HideModal }); + mode === PropertiesModalMode.Edit + ? orderModifyWellboreJob(wellbore, updates) + : orderCreateWellboreJob(wellbore, updates); + } + }; + dispatchOperation({ + type: OperationType.DisplayModal, + payload: + }); +}; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/orderPropertyJobHelpers.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/orderPropertyJobHelpers.ts new file mode 100644 index 000000000..f6bd19236 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/orderPropertyJobHelpers.ts @@ -0,0 +1,94 @@ +import { ObjectType, ObjectTypeToModel } from "models/objectType"; +import Well from "models/well"; +import Wellbore from "models/wellbore"; +import JobService, { JobType } from "services/jobService"; + +export const orderModifyObjectOnWellboreJob = async ( + objectType: T, + object: ObjectTypeToModel[T], + updates: Partial +) => { + const modifyJob = { + object: { + // updates only contains modified properties, so we need to add uids for a correct reference to the object. + uid: object.uid, + wellUid: object.wellUid, + wellboreUid: object.wellboreUid, + ...updates, + objectType: objectType + }, + objectType: objectType + }; + await JobService.orderJob(JobType.ModifyObjectOnWellbore, modifyJob); +}; + +export const orderCreateObjectOnWellboreJob = async ( + objectType: T, + object: ObjectTypeToModel[T], + updates: Partial +) => { + const createJob = { + object: { + ...object, + ...updates, + objectType: objectType + }, + objectType: objectType + }; + await JobService.orderJob(JobType.CreateObjectOnWellbore, createJob); +}; + +export const orderModifyWellJob = async ( + object: Well, + updates: Partial +) => { + const modifyJob = { + well: { + // updates only contains modified properties, so we need to add uids for a correct reference to the object. + uid: object.uid, + ...updates + } + }; + await JobService.orderJob(JobType.ModifyWell, modifyJob); +}; + +export const orderCreateWellJob = async ( + object: Well, + updates: Partial +) => { + const createJob = { + well: { + ...object, + ...updates + } + }; + await JobService.orderJob(JobType.CreateWell, createJob); +}; + +export const orderModifyWellboreJob = async ( + object: Wellbore, + updates: Partial +) => { + const modifyJob = { + wellbore: { + // updates only contains modified properties, so we need to add uids for a correct reference to the object. + uid: object.uid, + wellUid: object.wellUid, + ...updates + } + }; + await JobService.orderJob(JobType.ModifyWellbore, modifyJob); +}; + +export const orderCreateWellboreJob = async ( + object: Wellbore, + updates: Partial +) => { + const createJob = { + wellbore: { + ...object, + ...updates + } + }; + await JobService.orderJob(JobType.CreateWellbore, createJob); +}; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/propertiesModalProperty.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/propertiesModalProperty.ts new file mode 100644 index 000000000..78c6611f2 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/propertiesModalProperty.ts @@ -0,0 +1,71 @@ +import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; + +// Helper-type to verify that a (nested) property exists on the given type, limited to depth D +type NestedKeys = D extends 0 + ? never + : { + [K in keyof T]: T[K] extends (infer U)[] + ? K extends string + ? `${K}` | `${K}[number]` | `${K}[number].${NestedKeys}` + : never + : T[K] extends object + ? K extends string + ? `${K}` | `${K}.${NestedKeys}` + : never + : K; + }[keyof T & string]; + +// Prev is used to limit NestedKeys to a max depth to avoid infinite typescript checks. +type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + +interface CommonProps { + property: NestedKeys; + propertyType: PropertyType; + validator?: (value: any, originalValue: string) => boolean; + helperText?: string; + required?: boolean; + disabled?: boolean; +} + +// options are only allowed for PropertyType.Options +type OptionsPropertyProps = + | { propertyType: PropertyType.Options; options: string[] } + | { + propertyType: Exclude; + options?: never; + }; + +// multiSelect is only allowed for PropertyType.Options +type MultiSelectOptionProps = + | { propertyType: PropertyType.Options; multiSelect?: boolean } + | { + propertyType: Exclude; + multiSelect?: never; + }; + +// multiline is only allowed for PropertyType.String +type MultiLineProps = + | { propertyType: PropertyType.String; multiline?: boolean } + | { + propertyType: Exclude; + multiline?: never; + }; + +// subproperties for list types +type SubProps = + | { + propertyType: PropertyType.List; + subProps: PropertiesModalProperty[]; + itemPrefix?: string; + } + | { + propertyType: Exclude; + subProperties?: never; + itemPrefix?: never; + }; + +export type PropertiesModalProperty = CommonProps & + OptionsPropertyProps & + MultiSelectOptionProps & + MultiLineProps & + SubProps; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModalUtils.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModalUtils.ts deleted file mode 100644 index 33e8c316f..000000000 --- a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModalUtils.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Measure from "models/measure"; - -export const undefinedOnUnchagedEmptyString = ( - original?: string, - edited?: string -): string | null => { - if (edited?.length > 0) { - return edited; - } - if (original == null || original.length == 0) { - return undefined; - } - return ""; -}; - -export const invalidStringInput = ( - original: string, - edited: string, - maxLength: number -): boolean => { - return ( - errorOnDeletion(original, edited) || - (edited != null && edited.length > maxLength) - ); -}; - -const errorOnDeletion = (original: string, edited: string): boolean => { - if (original == null || original.length == 0) { - return false; - } - return edited != null && edited.length == 0; -}; - -export const invalidMeasureInput = (edited: Measure): boolean => { - return edited != null && isNaN(edited.value); -}; - -export const invalidNumberInput = (original: any, edited: number): boolean => { - return original != null && edited != null && isNaN(edited); -}; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/RigPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/RigPropertiesModal.tsx deleted file mode 100644 index fe9651e53..000000000 --- a/Src/WitsmlExplorer.Frontend/components/Modals/RigPropertiesModal.tsx +++ /dev/null @@ -1,396 +0,0 @@ -import { Autocomplete, TextField } from "@equinor/eds-core-react"; -import { DateTimeField } from "components/Modals/DateTimeField"; -import ModalDialog from "components/Modals/ModalDialog"; -import { - PropertiesModalMode, - validFaxNumber, - validPhoneNumber, - validText -} from "components/Modals/ModalParts"; -import { HideModalAction } from "contexts/operationStateReducer"; -import OperationType from "contexts/operationType"; -import { useOperationState } from "hooks/useOperationState"; -import { itemStateTypes } from "models/itemStateTypes"; -import { ObjectType } from "models/objectType"; -import Rig from "models/rig"; -import { rigType } from "models/rigType"; -import React, { ChangeEvent, useState } from "react"; -import JobService, { JobType } from "services/jobService"; - -export interface RigPropertiesModalProps { - mode: PropertiesModalMode; - rig: Rig; - dispatchOperation: (action: HideModalAction) => void; -} - -const RigPropertiesModal = ( - props: RigPropertiesModalProps -): React.ReactElement => { - const { mode, rig, dispatchOperation } = props; - const { - operationState: { timeZone } - } = useOperationState(); - const [editableRig, setEditableRig] = useState({ ...rig }); - const [isLoading, setIsLoading] = useState(false); - const [dTimStartOpValid, setDTimStartOpValid] = useState(true); - const [dTimEndOpValid, setDTimEndOpValid] = useState(true); - const editMode = mode === PropertiesModalMode.Edit; - - const onSubmit = async (updatedRig: Rig) => { - setIsLoading(true); - if (editMode) { - const modifyJob = { - object: { ...updatedRig, objectType: ObjectType.Rig }, - objectType: ObjectType.Rig - }; - await JobService.orderJob(JobType.ModifyObjectOnWellbore, modifyJob); - } else { - const wellboreRigJob = { - rig: updatedRig - }; - await JobService.orderJob(JobType.CreateRig, wellboreRigJob); - } - setIsLoading(false); - dispatchOperation({ type: OperationType.HideModal }); - }; - - const yearEntServiceValid = - (!rig.yearEntService && !editableRig?.yearEntService) || - editableRig?.yearEntService?.length == 4; - const validTelNumber = - (!rig.telNumber && !editableRig?.telNumber) || - validPhoneNumber(editableRig.telNumber); - const faxNumberValid = - (!rig.faxNumber && !editableRig?.faxNumber) || - validFaxNumber(editableRig.faxNumber); - const validEmailAddress = - (!rig.emailAddress && !editableRig?.emailAddress) || - validText(editableRig.emailAddress, 1, 128); - const validNameContact = - (!rig.nameContact && !editableRig?.nameContact) || - validText(editableRig?.nameContact, 1, 64); - - const validRigUid = validText(editableRig?.uid, 1, 64); - const validRigName = validText(editableRig?.name, 1, 64); - - return ( - <> - {editableRig && ( - - ) => - setEditableRig({ ...editableRig, uid: e.target.value }) - } - /> - - - - - ) => - setEditableRig({ ...editableRig, name: e.target.value }) - } - /> - { - setEditableRig({ ...editableRig, typeRig: selectedItems[0] }); - }} - /> - { - setEditableRig({ ...editableRig, dTimStartOp: dateTime }); - setDTimStartOpValid(valid); - }} - timeZone={timeZone} - /> - { - setEditableRig({ ...editableRig, dTimEndOp: dateTime }); - setDTimEndOpValid(valid); - }} - timeZone={timeZone} - /> - ) => - setEditableRig({ - ...editableRig, - yearEntService: e.target.value - }) - } - /> - ) => - setEditableRig({ ...editableRig, telNumber: e.target.value }) - } - /> - ) => - setEditableRig({ ...editableRig, faxNumber: e.target.value }) - } - /> - ) => - setEditableRig({ - ...editableRig, - emailAddress: e.target.value - }) - } - /> - ) => - setEditableRig({ - ...editableRig, - nameContact: e.target.value - }) - } - /> - ) => - setEditableRig({ - ...editableRig, - ratingDrillDepth: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableRig.ratingDrillDepth.uom - } - }) - } - /> - ) => - setEditableRig({ - ...editableRig, - ratingWaterDepth: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableRig.ratingWaterDepth.uom - } - }) - } - /> - ) => { - const uom = - editableRig.airGap !== null ? editableRig.airGap.uom : "m"; - setEditableRig({ - ...editableRig, - airGap: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: uom - } - }); - }} - /> - ) => { - setEditableRig({ - ...editableRig, - owner: e.target.value - }); - }} - /> - ) => { - setEditableRig({ - ...editableRig, - manufacturer: e.target.value - }); - }} - /> - ) => { - setEditableRig({ - ...editableRig, - classRig: e.target.value - }); - }} - /> - ) => { - setEditableRig({ - ...editableRig, - approvals: e.target.value - }); - }} - /> - ) => { - setEditableRig({ - ...editableRig, - registration: e.target.value - }); - }} - /> - { - const commonData = { - ...editableRig.commonData, - itemState: selectedItems[0] ?? null - }; - setEditableRig({ ...editableRig, commonData }); - }} - /> - - } - confirmDisabled={ - !validRigUid || - !validRigName || - !dTimStartOpValid || - !dTimEndOpValid - } - onSubmit={() => onSubmit(editableRig)} - isLoading={isLoading} - /> - )} - - ); -}; - -export default RigPropertiesModal; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/RiskPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/RiskPropertiesModal.tsx deleted file mode 100644 index 01d9c2d56..000000000 --- a/Src/WitsmlExplorer.Frontend/components/Modals/RiskPropertiesModal.tsx +++ /dev/null @@ -1,433 +0,0 @@ -import { Autocomplete, TextField } from "@equinor/eds-core-react"; -import formatDateString from "components/DateFormatter"; -import { DateTimeField } from "components/Modals/DateTimeField"; -import ModalDialog from "components/Modals/ModalDialog"; -import { PropertiesModalMode, validText } from "components/Modals/ModalParts"; -import { HideModalAction } from "contexts/operationStateReducer"; -import OperationType from "contexts/operationType"; -import { useOperationState } from "hooks/useOperationState"; -import { itemStateTypes } from "models/itemStateTypes"; -import { ObjectType } from "models/objectType"; -import { riskAffectedPersonnel } from "models/riskAffectedPersonnel"; -import { riskCategory } from "models/riskCategory"; -import RiskObject from "models/riskObject"; -import { riskSubCategory } from "models/riskSubCategory"; -import { riskType } from "models/riskType"; -import React, { ChangeEvent, useEffect, useState } from "react"; -import JobService, { JobType } from "services/jobService"; - -export interface RiskPropertiesModalProps { - mode: PropertiesModalMode; - riskObject: RiskObject; - dispatchOperation: (action: HideModalAction) => void; -} - -const RiskPropertiesModal = ( - props: RiskPropertiesModalProps -): React.ReactElement => { - const { mode, riskObject, dispatchOperation } = props; - const { - operationState: { timeZone, dateTimeFormat } - } = useOperationState(); - const [editableRiskObject, setEditableRiskObject] = - useState(null); - const [isLoading, setIsLoading] = useState(false); - const [dTimStartValid, setDTimStartValid] = useState(true); - const [dTimEndValid, setDTimEndValid] = useState(true); - const editMode = mode === PropertiesModalMode.Edit; - - useEffect(() => { - setEditableRiskObject({ - ...riskObject, - dTimStart: formatDateString( - riskObject.dTimStart, - timeZone, - dateTimeFormat - ), - dTimEnd: formatDateString(riskObject.dTimEnd, timeZone, dateTimeFormat), - commonData: { - ...riskObject.commonData, - dTimCreation: formatDateString( - riskObject.commonData.dTimCreation, - timeZone, - dateTimeFormat - ), - dTimLastChange: formatDateString( - riskObject.commonData.dTimLastChange, - timeZone, - dateTimeFormat - ) - } - }); - }, [riskObject]); - - const onSubmit = async (updatedRisk: RiskObject) => { - setIsLoading(true); - const modifyJob = { - object: { ...updatedRisk, objectType: ObjectType.Risk }, - objectType: ObjectType.Risk - }; - await JobService.orderJob(JobType.ModifyObjectOnWellbore, modifyJob); - setIsLoading(false); - dispatchOperation({ type: OperationType.HideModal }); - }; - - const validRiskName = validText(editableRiskObject?.name, 1, 64); - const validRiskDetails = validText(editableRiskObject?.details, 1, 256); - - return ( - <> - {editableRiskObject && ( - - - - - - - - - ) => - setEditableRiskObject({ - ...editableRiskObject, - name: e.target.value - }) - } - /> - { - setEditableRiskObject({ - ...editableRiskObject, - type: selectedItems[0] - }); - }} - /> - { - setEditableRiskObject({ - ...editableRiskObject, - category: selectedItems[0] - }); - }} - /> - - { - setEditableRiskObject({ - ...editableRiskObject, - subCategory: selectedItems[0] - }); - }} - /> - ) => - setEditableRiskObject({ - ...editableRiskObject, - extendCategory: e.target.value - }) - } - /> - { - setEditableRiskObject({ - ...editableRiskObject, - affectedPersonnel: selectedItems.join(", ") - }); - }} - /> - { - setEditableRiskObject({ - ...editableRiskObject, - dTimStart: dateTime - }); - setDTimStartValid(valid); - }} - timeZone={timeZone} - /> - { - setEditableRiskObject({ - ...editableRiskObject, - dTimEnd: dateTime - }); - setDTimEndValid(valid); - }} - timeZone={timeZone} - /> - ) => - setEditableRiskObject({ - ...editableRiskObject, - mdBitStart: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableRiskObject.mdBitStart.uom - } - }) - } - /> - ) => - setEditableRiskObject({ - ...editableRiskObject, - mdBitEnd: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableRiskObject.mdBitEnd.uom - } - }) - } - /> - ) => - setEditableRiskObject({ - ...editableRiskObject, - severityLevel: e.target.value - }) - } - /> - ) => - setEditableRiskObject({ - ...editableRiskObject, - probabilityLevel: e.target.value - }) - } - /> - ) => - setEditableRiskObject({ - ...editableRiskObject, - summary: e.target.value - }) - } - /> - ) => - setEditableRiskObject({ - ...editableRiskObject, - details: e.target.value - }) - } - /> - ) => { - const commonData = { - ...editableRiskObject.commonData, - sourceName: e.target.value - }; - setEditableRiskObject({ ...editableRiskObject, commonData }); - }} - /> - { - const commonData = { - ...editableRiskObject.commonData, - itemState: selectedItems[0] ?? null - }; - setEditableRiskObject({ ...editableRiskObject, commonData }); - }} - /> - - } - confirmDisabled={!validRiskName || !dTimStartValid || !dTimEndValid} - onSubmit={() => onSubmit(editableRiskObject)} - isLoading={isLoading} - /> - )} - - ); -}; - -export default RiskPropertiesModal; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/ServerModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/ServerModal.tsx index 6739b093b..497a0ecd8 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/ServerModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/ServerModal.tsx @@ -222,7 +222,7 @@ const ServerModal = (props: ServerModalProps): React.ReactElement => { style={labelStyle} htmlFor="creds" /> - + void; -} - -const TrajectoryPropertiesModal = ( - props: TrajectoryPropertiesModalProps -): React.ReactElement => { - const { mode, trajectory, dispatchOperation } = props; - const { - operationState: { timeZone } - } = useOperationState(); - const [editableTrajectory, setEditableTrajectory] = - useState(null); - const [isLoading, setIsLoading] = useState(false); - const [, setDTimTrajStartValid] = useState(true); - const [, setDTimTrajEndValid] = useState(true); - const editMode = mode === PropertiesModalMode.Edit; - - useEffect(() => { - setEditableTrajectory({ - ...trajectory, - commonData: { - ...trajectory.commonData - } - }); - }, [trajectory]); - - const onSubmit = async (updatedTrajectory: Trajectory) => { - setIsLoading(true); - if (editMode) { - const modifyJob = { - object: { ...updatedTrajectory, objectType: ObjectType.Trajectory }, - objectType: ObjectType.Trajectory - }; - await JobService.orderJob(JobType.ModifyObjectOnWellbore, modifyJob); - } else { - const wellboreTrajectoryJob = { - trajectory: updatedTrajectory - }; - await JobService.orderJob( - JobType.CreateTrajectory, - wellboreTrajectoryJob - ); - } - setIsLoading(false); - dispatchOperation({ type: OperationType.HideModal }); - }; - - const validTrajectoryUid = validText(editableTrajectory?.uid, 1, 64); - const validTrajectoryName = validText(editableTrajectory?.name, 1, 64); - const validTrajectoryServiceCompany = editMode - ? validText(editableTrajectory?.serviceCompany, 1, 64) - : validText(editableTrajectory?.serviceCompany, 0, 64); - - return ( - <> - {editableTrajectory && ( - - ) => - setEditableTrajectory({ - ...editableTrajectory, - uid: e.target.value - }) - } - /> - - - - - ) => - setEditableTrajectory({ - ...editableTrajectory, - name: e.target.value - }) - } - /> - { - setEditableTrajectory({ - ...editableTrajectory, - dTimTrajStart: dateTime - }); - setDTimTrajStartValid(valid); - }} - timeZone={timeZone} - disabled={editMode} - /> - { - setEditableTrajectory({ - ...editableTrajectory, - dTimTrajEnd: dateTime - }); - setDTimTrajEndValid(valid); - }} - timeZone={timeZone} - disabled={editMode} - /> - ) => - setEditableTrajectory({ - ...editableTrajectory, - serviceCompany: e.target.value - }) - } - /> - ) => - setEditableTrajectory({ - ...editableTrajectory, - mdMin: { - ...editableTrajectory.mdMin, - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value) - } - }) - } - /> - ) => - setEditableTrajectory({ - ...editableTrajectory, - mdMax: { - ...editableTrajectory.mdMax, - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value) - } - }) - } - /> - { - setEditableTrajectory({ - ...editableTrajectory, - aziRef: selectedItems[0] - }); - }} - onFocus={(e) => e.preventDefault()} - /> - ) => { - const commonData = { - ...editableTrajectory.commonData, - sourceName: e.target.value - }; - setEditableTrajectory({ ...editableTrajectory, commonData }); - }} - /> - { - const commonData = { - ...editableTrajectory.commonData, - itemState: selectedItems[0] ?? null - }; - setEditableTrajectory({ ...editableTrajectory, commonData }); - }} - /> - - } - confirmDisabled={ - !validTrajectoryUid || - !validTrajectoryName || - !validTrajectoryServiceCompany - } - onSubmit={() => onSubmit(editableTrajectory)} - isLoading={isLoading} - /> - )} - - ); -}; - -export default TrajectoryPropertiesModal; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/TrajectoryStationPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/TrajectoryStationPropertiesModal.tsx deleted file mode 100644 index e7f6178e3..000000000 --- a/Src/WitsmlExplorer.Frontend/components/Modals/TrajectoryStationPropertiesModal.tsx +++ /dev/null @@ -1,453 +0,0 @@ -import { TextField } from "@equinor/eds-core-react"; -import formatDateString from "components/DateFormatter"; -import { DateTimeField } from "components/Modals/DateTimeField"; -import ModalDialog from "components/Modals/ModalDialog"; -import { HideModalAction } from "contexts/operationStateReducer"; -import OperationType from "contexts/operationType"; -import { useOperationState } from "hooks/useOperationState"; -import ObjectReference from "models/jobs/objectReference"; -import { measureToString } from "models/measure"; -import { toObjectReference } from "models/objectOnWellbore"; -import Trajectory from "models/trajectory"; -import TrajectoryStation from "models/trajectoryStation"; -import React, { ChangeEvent, useEffect, useState } from "react"; -import JobService, { JobType } from "services/jobService"; - -export interface TrajectoryStationPropertiesModalInterface { - trajectoryStation: TrajectoryStation; - trajectory: Trajectory; - dispatchOperation: (action: HideModalAction) => void; -} - -const TrajectoryStationPropertiesModal = ( - props: TrajectoryStationPropertiesModalInterface -): React.ReactElement => { - const { trajectoryStation, trajectory, dispatchOperation } = props; - const { - operationState: { timeZone, dateTimeFormat } - } = useOperationState(); - const [editableTrajectoryStation, setEditableTrajectoryStation] = - useState(null); - const [isLoading, setIsLoading] = useState(false); - const [dTimStnValid, setDTimStnValid] = useState(true); - - const onSubmit = async (updatedTrajectoryStation: TrajectoryStation) => { - setIsLoading(true); - const trajectoryReference: ObjectReference = toObjectReference(trajectory); - const modifyTrajectoryStationJob = { - trajectoryStation: updatedTrajectoryStation, - trajectoryReference - }; - await JobService.orderJob( - JobType.ModifyTrajectoryStation, - modifyTrajectoryStationJob - ); - setIsLoading(false); - dispatchOperation({ type: OperationType.HideModal }); - }; - - useEffect(() => { - setEditableTrajectoryStation({ - ...trajectoryStation, - dTimStn: formatDateString( - trajectoryStation.dTimStn, - timeZone, - dateTimeFormat - ) - }); - }, [trajectoryStation]); - return ( - <> - {editableTrajectoryStation && ( - - - - { - setEditableTrajectoryStation({ - ...editableTrajectoryStation, - dTimStn: dateTime - }); - setDTimStnValid(valid); - }} - timeZone={timeZone} - /> - ) => - setEditableTrajectoryStation({ - ...editableTrajectoryStation, - md: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableTrajectoryStation.md.uom - } - }) - } - /> - ) => - setEditableTrajectoryStation({ - ...editableTrajectoryStation, - tvd: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableTrajectoryStation.tvd.uom - } - }) - } - /> - ) => - setEditableTrajectoryStation({ - ...editableTrajectoryStation, - azi: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableTrajectoryStation.azi.uom - } - }) - } - /> - ) => - setEditableTrajectoryStation({ - ...editableTrajectoryStation, - incl: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableTrajectoryStation.incl.uom - } - }) - } - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - } - confirmDisabled={!dTimStnValid} - onSubmit={() => onSubmit(editableTrajectoryStation)} - isLoading={isLoading} - /> - )} - - ); -}; - -export default TrajectoryStationPropertiesModal; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/TubularComponentPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/TubularComponentPropertiesModal.tsx deleted file mode 100644 index f74e660e9..000000000 --- a/Src/WitsmlExplorer.Frontend/components/Modals/TubularComponentPropertiesModal.tsx +++ /dev/null @@ -1,357 +0,0 @@ -import { Autocomplete, TextField } from "@equinor/eds-core-react"; -import ModalDialog from "components/Modals/ModalDialog"; -import { validText } from "components/Modals/ModalParts"; -import { HideModalAction } from "contexts/operationStateReducer"; -import OperationType from "contexts/operationType"; -import { isInteger } from "lodash"; -import { boxPinConfigTypes } from "models/boxPinConfigTypes"; -import ObjectReference from "models/jobs/objectReference"; -import { materialTypes } from "models/materialTypes"; -import MaxLength from "models/maxLength"; -import { toObjectReference } from "models/objectOnWellbore"; -import Tubular from "models/tubular"; -import TubularComponent from "models/tubularComponent"; -import { tubularComponentTypes } from "models/tubularComponentTypes"; -import React, { ChangeEvent, useEffect, useState } from "react"; -import JobService, { JobType } from "services/jobService"; - -export interface TubularComponentPropertiesModalInterface { - tubularComponent: TubularComponent; - tubular: Tubular; - dispatchOperation: (action: HideModalAction) => void; -} - -const isInvalidSequence = (sequence: number) => { - return Number.isNaN(sequence) || sequence < 1 || !isInteger(sequence); -}; - -const TubularComponentPropertiesModal = ( - props: TubularComponentPropertiesModalInterface -): React.ReactElement => { - const { tubularComponent, tubular, dispatchOperation } = props; - const [editableTubularComponent, setEditableTubularComponent] = - useState(null); - const [isLoading, setIsLoading] = useState(false); - - const onSubmit = async (updatedTubularComponent: TubularComponent) => { - setIsLoading(true); - const tubularReference: ObjectReference = toObjectReference(tubular); - const modifyTubularComponentJob = { - tubularComponent: updatedTubularComponent, - tubularReference - }; - await JobService.orderJob( - JobType.ModifyTubularComponent, - modifyTubularComponentJob - ); - setIsLoading(false); - dispatchOperation({ type: OperationType.HideModal }); - }; - - useEffect(() => { - setEditableTubularComponent(tubularComponent); - }, [tubularComponent]); - - return ( - <> - {editableTubularComponent && ( - - - ) => - setEditableTubularComponent({ - ...editableTubularComponent, - sequence: parseFloat(e.target.value) - }) - } - variant={ - isInvalidSequence(editableTubularComponent.sequence) - ? "error" - : undefined - } - helperText={ - isInvalidSequence(editableTubularComponent.sequence) && - "Sequence must be a positive non-zero integer" - } - /> - ) => - setEditableTubularComponent({ - ...editableTubularComponent, - description: e.target.value - }) - } - variant={ - tubularComponent.description && - !validText( - editableTubularComponent.description, - 1, - MaxLength.Comment - ) - ? "error" - : undefined - } - helperText={ - tubularComponent.description && - !validText( - editableTubularComponent.description, - 1, - MaxLength.Comment - ) && - `Description must be 1-${MaxLength.Comment} characters` - } - /> - { - setEditableTubularComponent({ - ...editableTubularComponent, - typeTubularComponent: selectedItems[0] - }); - }} - hideClearButton={ - !!editableTubularComponent.typeTubularComponent - } - /> - ) => - setEditableTubularComponent({ - ...editableTubularComponent, - id: { - value: parseFloat(e.target.value), - uom: editableTubularComponent.id.uom - } - }) - } - /> - ) => - setEditableTubularComponent({ - ...editableTubularComponent, - od: { - value: parseFloat(e.target.value), - uom: editableTubularComponent.od.uom - } - }) - } - /> - ) => - setEditableTubularComponent({ - ...editableTubularComponent, - len: { - value: parseFloat(e.target.value), - uom: editableTubularComponent.len.uom - } - }) - } - /> - ) => - setEditableTubularComponent({ - ...editableTubularComponent, - wtPerLen: { - value: parseFloat(e.target.value), - uom: editableTubularComponent.wtPerLen.uom - } - }) - } - /> - ) => - setEditableTubularComponent({ - ...editableTubularComponent, - numJointStand: parseFloat(e.target.value) - }) - } - variant={ - Number.isNaN(editableTubularComponent.numJointStand) - ? "error" - : undefined - } - helperText={ - Number.isNaN(editableTubularComponent.numJointStand) && - "numJointStand must be a positive non-zero integer" - } - /> - { - setEditableTubularComponent({ - ...editableTubularComponent, - configCon: selectedItems[0] - }); - }} - hideClearButton={!!editableTubularComponent.configCon} - /> - { - setEditableTubularComponent({ - ...editableTubularComponent, - typeMaterial: selectedItems[0] - }); - }} - hideClearButton={!!editableTubularComponent.typeMaterial} - /> - ) => - setEditableTubularComponent({ - ...editableTubularComponent, - vendor: e.target.value - }) - } - variant={ - tubularComponent.vendor && - !validText(editableTubularComponent.vendor, 1, MaxLength.Name) - ? "error" - : undefined - } - helperText={ - tubularComponent.vendor && - !validText( - editableTubularComponent.vendor, - 1, - MaxLength.Name - ) && - `Vendor must be 1-${MaxLength.Name} characters` - } - /> - ) => - setEditableTubularComponent({ - ...editableTubularComponent, - model: e.target.value - }) - } - variant={ - tubularComponent.model && - !validText(editableTubularComponent.model, 1, MaxLength.Name) - ? "error" - : undefined - } - helperText={ - tubularComponent.model && - !validText( - editableTubularComponent.model, - 1, - MaxLength.Name - ) && - `Model must be 1-${MaxLength.Name} characters` - } - /> - - } - confirmDisabled={ - !validText(editableTubularComponent.typeTubularComponent) || - isInvalidSequence(editableTubularComponent.sequence) || - Number.isNaN(editableTubularComponent.numJointStand) || - Number.isNaN(editableTubularComponent.id.value) || - Number.isNaN(editableTubularComponent.od.value) || - Number.isNaN(editableTubularComponent.len.value) || - Number.isNaN(editableTubularComponent.wtPerLen.value) || - (tubularComponent.description && - !validText( - editableTubularComponent.description, - 1, - MaxLength.Comment - )) || - (tubularComponent.configCon && - !validText( - editableTubularComponent.configCon, - 1, - MaxLength.Enum - )) || - (tubularComponent.typeMaterial && - !validText( - editableTubularComponent.typeMaterial, - 1, - MaxLength.Enum - )) || - (tubularComponent.vendor && - !validText(editableTubularComponent.vendor, 1, MaxLength.Name)) || - (tubularComponent.model && - !validText(editableTubularComponent.model, 1, MaxLength.Name)) - } - onSubmit={() => onSubmit(editableTubularComponent)} - isLoading={isLoading} - /> - )} - - ); -}; - -export default TubularComponentPropertiesModal; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/TubularPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/TubularPropertiesModal.tsx deleted file mode 100644 index 0c8af0346..000000000 --- a/Src/WitsmlExplorer.Frontend/components/Modals/TubularPropertiesModal.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { Autocomplete, TextField } from "@equinor/eds-core-react"; -import ModalDialog from "components/Modals/ModalDialog"; -import { validText } from "components/Modals/ModalParts"; -import { HideModalAction } from "contexts/operationStateReducer"; -import OperationType from "contexts/operationType"; -import { ObjectType } from "models/objectType"; -import Tubular from "models/tubular"; -import React, { ChangeEvent, useEffect, useState } from "react"; -import JobService, { JobType } from "services/jobService"; - -const typeTubularAssy = [ - "drilling", - "directional drilling", - "fishing", - "condition mud", - "tubing conveyed logging", - "cementing", - "casing", - "clean out", - "completion or testing", - "coring", - "hole opening or underreaming", - "milling or dressing or cutting", - "wiper or check or reaming", - "unknown" -]; - -export interface TubularPropertiesModalInterface { - tubular: Tubular; - dispatchOperation: (action: HideModalAction) => void; -} - -const TubularPropertiesModal = ( - props: TubularPropertiesModalInterface -): React.ReactElement => { - const { tubular, dispatchOperation } = props; - const [editableTubular, setEditableTubular] = useState(null); - const [isLoading, setIsLoading] = useState(false); - - const onSubmit = async (updatedTubular: Tubular) => { - setIsLoading(true); - const wellboreTubularJob = { - object: { ...updatedTubular, objectType: ObjectType.Tubular }, - objectType: ObjectType.Tubular - }; - await JobService.orderJob( - JobType.ModifyObjectOnWellbore, - wellboreTubularJob - ); - setIsLoading(false); - dispatchOperation({ type: OperationType.HideModal }); - }; - - useEffect(() => { - setEditableTubular(tubular); - }, [tubular]); - - return ( - <> - {editableTubular && ( - - - ) => - setEditableTubular({ - ...editableTubular, - name: e.target.value - }) - } - /> - { - setEditableTubular({ - ...editableTubular, - typeTubularAssy: selectedItems[0] - }); - }} - hideClearButton={true} - /> - - } - confirmDisabled={ - !validText(editableTubular.uid) || - !validText(editableTubular.name) || - !validText(editableTubular.typeTubularAssy) - } - onSubmit={() => onSubmit(editableTubular)} - isLoading={isLoading} - /> - )} - - ); -}; - -export default TubularPropertiesModal; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/WbGeometryPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/WbGeometryPropertiesModal.tsx deleted file mode 100644 index c968f8be1..000000000 --- a/Src/WitsmlExplorer.Frontend/components/Modals/WbGeometryPropertiesModal.tsx +++ /dev/null @@ -1,282 +0,0 @@ -import { Autocomplete, TextField } from "@equinor/eds-core-react"; -import formatDateString from "components/DateFormatter"; -import ModalDialog from "components/Modals/ModalDialog"; -import { PropertiesModalMode, validText } from "components/Modals/ModalParts"; -import { HideModalAction } from "contexts/operationStateReducer"; -import OperationType from "contexts/operationType"; -import { useOperationState } from "hooks/useOperationState"; -import { itemStateTypes } from "models/itemStateTypes"; -import { ObjectType } from "models/objectType"; -import WbGeometryObject from "models/wbGeometry"; -import React, { ChangeEvent, useEffect, useState } from "react"; -import JobService, { JobType } from "services/jobService"; - -export interface WbGeometryPropertiesModalProps { - mode: PropertiesModalMode; - wbGeometryObject: WbGeometryObject; - dispatchOperation: (action: HideModalAction) => void; -} - -const WbGeometryPropertiesModal = ( - props: WbGeometryPropertiesModalProps -): React.ReactElement => { - const { mode, wbGeometryObject, dispatchOperation } = props; - const { - operationState: { timeZone, dateTimeFormat } - } = useOperationState(); - const [editableWbGeometryObject, setEditableWbGeometryObject] = - useState(null); - const [isLoading, setIsLoading] = useState(false); - const editMode = mode === PropertiesModalMode.Edit; - - useEffect(() => { - setEditableWbGeometryObject({ - ...wbGeometryObject, - dTimReport: formatDateString( - wbGeometryObject.dTimReport, - timeZone, - dateTimeFormat - ), - commonData: { - ...wbGeometryObject.commonData, - dTimCreation: formatDateString( - wbGeometryObject.commonData.dTimCreation, - timeZone, - dateTimeFormat - ), - dTimLastChange: formatDateString( - wbGeometryObject.commonData.dTimLastChange, - timeZone, - dateTimeFormat - ) - } - }); - }, [wbGeometryObject]); - - const onSubmit = async (updatedWbGeometry: WbGeometryObject) => { - setIsLoading(true); - const wellboreWbGeometryJob = { - object: { ...updatedWbGeometry, objectType: ObjectType.WbGeometry }, - objectType: ObjectType.WbGeometry - }; - await JobService.orderJob( - JobType.ModifyObjectOnWellbore, - wellboreWbGeometryJob - ); - setIsLoading(false); - dispatchOperation({ type: OperationType.HideModal }); - }; - - const validWbGeometryName = validText(editableWbGeometryObject?.name, 1, 64); - - return ( - <> - {editableWbGeometryObject && ( - - - - - - - - - - - ) => - setEditableWbGeometryObject({ - ...editableWbGeometryObject, - name: e.target.value - }) - } - /> - ) => - setEditableWbGeometryObject({ - ...editableWbGeometryObject, - mdBottom: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableWbGeometryObject.mdBottom.uom - } - }) - } - /> - ) => - setEditableWbGeometryObject({ - ...editableWbGeometryObject, - gapAir: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableWbGeometryObject.gapAir.uom - } - }) - } - /> - ) => - setEditableWbGeometryObject({ - ...editableWbGeometryObject, - depthWaterMean: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableWbGeometryObject.depthWaterMean.uom - } - }) - } - /> - ) => { - const commonData = { - ...editableWbGeometryObject.commonData, - comments: e.target.value - }; - setEditableWbGeometryObject({ - ...editableWbGeometryObject, - commonData - }); - }} - /> - { - const commonData = { - ...editableWbGeometryObject.commonData, - itemState: selectedItems[0] ?? null - }; - setEditableWbGeometryObject({ - ...editableWbGeometryObject, - commonData - }); - }} - /> - - } - confirmDisabled={!validWbGeometryName} - onSubmit={() => onSubmit(editableWbGeometryObject)} - isLoading={isLoading} - /> - )} - - ); -}; - -export default WbGeometryPropertiesModal; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/WbGeometrySectionPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/WbGeometrySectionPropertiesModal.tsx deleted file mode 100644 index 3a0de52ed..000000000 --- a/Src/WitsmlExplorer.Frontend/components/Modals/WbGeometrySectionPropertiesModal.tsx +++ /dev/null @@ -1,273 +0,0 @@ -import { Autocomplete, TextField } from "@equinor/eds-core-react"; -import ModalDialog from "components/Modals/ModalDialog"; -import { HideModalAction } from "contexts/operationStateReducer"; -import OperationType from "contexts/operationType"; -import { holeCasingTypes } from "models/holeCasingTypes"; -import ObjectReference from "models/jobs/objectReference"; -import Measure from "models/measure"; -import { toObjectReference } from "models/objectOnWellbore"; -import WbGeometryObject from "models/wbGeometry"; -import WbGeometrySection from "models/wbGeometrySection"; -import React, { - ChangeEvent, - Dispatch, - SetStateAction, - useEffect, - useState -} from "react"; -import JobService, { JobType } from "services/jobService"; -import styled from "styled-components"; - -export interface WbGeometrySectionPropertiesModalInterface { - wbGeometrySection: WbGeometrySection; - dispatchOperation: (action: HideModalAction) => void; - wbGeometry: WbGeometryObject; -} - -const WbGeometrySectionPropertiesModal = ( - props: WbGeometrySectionPropertiesModalInterface -): React.ReactElement => { - const { wbGeometrySection, dispatchOperation, wbGeometry } = props; - const [editableWbgs, setEditableWbGeometrySection] = - useState(null); - const [isLoading, setIsLoading] = useState(false); - - const onSubmit = async (updatedWbGeometrySection: WbGeometrySection) => { - setIsLoading(true); - const wbGeometryReference: ObjectReference = toObjectReference(wbGeometry); - const modifyWbGeometrySectionJob = { - wbGeometrySection: updatedWbGeometrySection, - wbGeometryReference - }; - await JobService.orderJob( - JobType.ModifyWbGeometrySection, - modifyWbGeometrySectionJob - ); - setIsLoading(false); - dispatchOperation({ type: OperationType.HideModal }); - }; - - useEffect(() => { - setEditableWbGeometrySection(JSON.parse(JSON.stringify(wbGeometrySection))); //deep copy - }, [wbGeometrySection]); - - const invalidGrade = - errorOnDeletion(wbGeometrySection.grade, editableWbgs?.grade) || - editableWbgs?.grade?.length > 32; - const invalidMdTop = - wbGeometrySection.mdTop && editableWbgs?.mdTop.value == undefined; - const invalidMdBottom = - wbGeometrySection.mdBottom && editableWbgs?.mdBottom.value == undefined; - const invalidTvdTop = - wbGeometrySection.tvdTop && editableWbgs?.tvdTop.value == undefined; - const invalidTvdBottom = - wbGeometrySection.tvdBottom && editableWbgs?.tvdBottom.value == undefined; - const invalidIdSection = - wbGeometrySection.idSection && editableWbgs?.idSection.value == undefined; - const invalidOdSection = - wbGeometrySection.odSection && editableWbgs?.odSection.value == undefined; - const invalidWtPerLen = - wbGeometrySection.wtPerLen && editableWbgs?.wtPerLen.value == undefined; - const invalidDiaDrift = - wbGeometrySection.diaDrift && editableWbgs?.diaDrift.value == undefined; - const invalidFactFric = - wbGeometrySection.factFric != null && editableWbgs?.factFric == undefined; - return ( - <> - {editableWbgs && ( - - - { - setEditableWbGeometrySection({ - ...editableWbgs, - typeHoleCasing: selectedItems[0] - }); - }} - hideClearButton={!!wbGeometrySection.typeHoleCasing} - onFocus={(e) => e.preventDefault()} - /> - - - - - - - - { - setEditableWbGeometrySection({ - ...editableWbgs, - curveConductor: selectedItems[0] == "false" ? false : true - }); - }} - hideClearButton={wbGeometrySection.curveConductor != null} - /> - - ) => - setEditableWbGeometrySection({ - ...editableWbgs, - grade: e.target.value - }) - } - /> - ) => { - setEditableWbGeometrySection({ - ...editableWbgs, - factFric: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value) - }); - }} - /> - - } - confirmDisabled={ - invalidGrade || - invalidMdTop || - invalidMdBottom || - invalidTvdTop || - invalidTvdBottom || - invalidIdSection || - invalidOdSection || - invalidWtPerLen || - invalidDiaDrift || - invalidFactFric - } - onSubmit={() => onSubmit(editableWbgs)} - isLoading={isLoading} - /> - )} - - ); -}; - -interface MeasureFieldProps { - measure: Measure; - editableWbgs: WbGeometrySection; - invalid: boolean; - name: string; - setResult: Dispatch>; -} - -const MeasureField = (props: MeasureFieldProps): React.ReactElement => { - const { measure, editableWbgs, invalid, name, setResult } = props; - return ( - ) => { - measure.value = isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value); - setResult({ - ...editableWbgs - }); - }} - /> - ); -}; - -const errorOnDeletion = (original?: string, edited?: string): boolean => { - if (!original) { - return false; - } - return edited == null || edited.length == 0; -}; - -const Layout = styled.div` - display: grid; - grid-template-columns: repeat(2, auto); - gap: 1rem; -`; - -export default WbGeometrySectionPropertiesModal; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/WellPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/WellPropertiesModal.tsx deleted file mode 100644 index d2eef21df..000000000 --- a/Src/WitsmlExplorer.Frontend/components/Modals/WellPropertiesModal.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import { TextField } from "@equinor/eds-core-react"; -import formatDateString from "components/DateFormatter"; -import ModalDialog from "components/Modals/ModalDialog"; -import { - PropertiesModalMode, - validText, - validTimeZone -} from "components/Modals/ModalParts"; -import { HideModalAction } from "contexts/operationStateReducer"; -import OperationType from "contexts/operationType"; -import { useOperationState } from "hooks/useOperationState"; -import Well from "models/well"; -import React, { ChangeEvent, useEffect, useState } from "react"; -import JobService, { JobType } from "services/jobService"; - -export interface WellPropertiesModalProps { - mode: PropertiesModalMode; - well: Well; - dispatchOperation: (action: HideModalAction) => void; -} - -const WellPropertiesModal = ( - props: WellPropertiesModalProps -): React.ReactElement => { - const { mode, well, dispatchOperation } = props; - const { - operationState: { timeZone, dateTimeFormat } - } = useOperationState(); - const [editableWell, setEditableWell] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const editMode = mode === PropertiesModalMode.Edit; - - const onSubmit = async (updatedWell: Well) => { - setIsLoading(true); - const wellJob = { - well: updatedWell - }; - await JobService.orderJob( - mode == PropertiesModalMode.New ? JobType.CreateWell : JobType.ModifyWell, - wellJob - ); - setIsLoading(false); - dispatchOperation({ type: OperationType.HideModal }); - }; - - useEffect(() => { - setEditableWell(well); - }, [well]); - - const validWellUid = validText(editableWell?.uid, 1, 64); - const validWellName = validText(editableWell?.name, 1, 64); - const validWellField = validText(editableWell?.field, 0, 64); - const validWellCountry = validText(editableWell?.country, 0, 32); - const validWellOperator = validText(editableWell?.operator, 0, 64); - - return ( - <> - {editableWell && ( - - ) => - setEditableWell({ ...editableWell, uid: e.target.value }) - } - /> - ) => - setEditableWell({ ...editableWell, name: e.target.value }) - } - /> - ) => - setEditableWell({ ...editableWell, field: e.target.value }) - } - /> - ) => - setEditableWell({ ...editableWell, country: e.target.value }) - } - /> - ) => - setEditableWell({ ...editableWell, operator: e.target.value }) - } - /> - ) => - setEditableWell({ ...editableWell, timeZone: e.target.value }) - } - /> - - {mode !== PropertiesModalMode.New && ( - <> - - - - )} - - } - confirmDisabled={ - !validWellName || - !validWellUid || - !validWellField || - !validWellOperator || - !validWellCountry || - !validTimeZone(editableWell.timeZone) - } - onSubmit={() => onSubmit(editableWell)} - isLoading={isLoading} - /> - )} - - ); -}; - -export default WellPropertiesModal; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/WellborePropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/WellborePropertiesModal.tsx deleted file mode 100644 index 3dfa79a0c..000000000 --- a/Src/WitsmlExplorer.Frontend/components/Modals/WellborePropertiesModal.tsx +++ /dev/null @@ -1,484 +0,0 @@ -import { Autocomplete, TextField } from "@equinor/eds-core-react"; -import formatDateString from "components/DateFormatter"; -import { DateTimeField } from "components/Modals/DateTimeField"; -import ModalDialog from "components/Modals/ModalDialog"; -import { PropertiesModalMode, validText } from "components/Modals/ModalParts"; -import { invalidStringInput } from "components/Modals/PropertiesModalUtils"; -import { HideModalAction } from "contexts/operationStateReducer"; -import OperationType from "contexts/operationType"; -import { useOperationState } from "hooks/useOperationState"; -import MaxLength from "models/maxLength"; -import Wellbore, { wellboreHasChanges } from "models/wellbore"; -import React, { ChangeEvent, useEffect, useState } from "react"; -import JobService, { JobType } from "services/jobService"; -import styled from "styled-components"; - -export interface WellborePropertiesModalProps { - mode: PropertiesModalMode; - wellbore: Wellbore; - dispatchOperation: (action: HideModalAction) => void; -} - -const purposeValues = [ - "appraisal", - "development", - "exploration", - "fluid storage", - "general srvc", - "mineral", - "unknown" -]; - -const WellborePropertiesModal = ( - props: WellborePropertiesModalProps -): React.ReactElement => { - const { mode, wellbore, dispatchOperation } = props; - const { - operationState: { timeZone, dateTimeFormat } - } = useOperationState(); - const [editableWellbore, setEditableWellbore] = useState(null); - const [pristineWellbore, setPristineWellbore] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [dTimeKickoffValid, setDTimeKickoffValid] = useState(true); - const editMode = mode === PropertiesModalMode.Edit; - - const onSubmit = async (updatedWellbore: Wellbore) => { - setIsLoading(true); - const wellboreJob = { - wellbore: updatedWellbore - }; - await JobService.orderJob( - mode == PropertiesModalMode.New - ? JobType.CreateWellbore - : JobType.ModifyWellbore, - wellboreJob - ); - setIsLoading(false); - dispatchOperation({ type: OperationType.HideModal }); - }; - - useEffect(() => { - setEditableWellbore({ - ...wellbore, - dTimeKickoff: formatDateString( - wellbore.dTimeKickoff, - timeZone, - dateTimeFormat - ) - }); - setPristineWellbore(wellbore); - }, [wellbore]); - - const validWellboreUid = validText(editableWellbore?.uid, 1, 64); - const validWellboreName = validText(editableWellbore?.name, 1, 64); - - return ( - <> - {editableWellbore && ( - - ) => - setEditableWellbore({ - ...editableWellbore, - uid: e.target.value - }) - } - /> - ) => - setEditableWellbore({ - ...editableWellbore, - name: e.target.value - }) - } - /> - - - - { - setEditableWellbore({ - ...editableWellbore, - wellborePurpose: selectedItems[0] - }); - }} - /> - {mode == PropertiesModalMode.Edit && ( - <> - - ) => - setEditableWellbore({ - ...editableWellbore, - number: e.target.value - }) - } - /> - ) => - setEditableWellbore({ - ...editableWellbore, - suffixAPI: e.target.value - }) - } - /> - - ) => - setEditableWellbore({ - ...editableWellbore, - numGovt: e.target.value - }) - } - /> - { - setEditableWellbore({ - ...editableWellbore, - dTimeKickoff: dateTime - }); - setDTimeKickoffValid(valid); - }} - timeZone={timeZone} - /> - - ) => - setEditableWellbore({ - ...editableWellbore, - md: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableWellbore.md.uom - } - }) - } - /> - ) => - setEditableWellbore({ - ...editableWellbore, - tvd: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableWellbore.tvd.uom - } - }) - } - /> - - - ) => - setEditableWellbore({ - ...editableWellbore, - mdKickoff: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableWellbore.mdKickoff.uom - } - }) - } - /> - ) => - setEditableWellbore({ - ...editableWellbore, - tvdKickoff: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableWellbore.tvdKickoff.uom - } - }) - } - /> - - - ) => - setEditableWellbore({ - ...editableWellbore, - mdPlanned: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableWellbore.mdPlanned.uom - } - }) - } - /> - ) => - setEditableWellbore({ - ...editableWellbore, - tvdPlanned: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableWellbore.tvdPlanned.uom - } - }) - } - /> - - - ) => - setEditableWellbore({ - ...editableWellbore, - mdSubSeaPlanned: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableWellbore.mdSubSeaPlanned.uom - } - }) - } - /> - ) => - setEditableWellbore({ - ...editableWellbore, - tvdSubSeaPlanned: { - value: isNaN(parseFloat(e.target.value)) - ? undefined - : parseFloat(e.target.value), - uom: editableWellbore.tvdSubSeaPlanned.uom - } - }) - } - /> - - ) => - setEditableWellbore({ - ...editableWellbore, - dayTarget: { - value: isNaN(parseInt(e.target.value)) - ? undefined - : parseInt(e.target.value), - uom: editableWellbore.dayTarget.uom - } - }) - } - /> - - - ) => { - setEditableWellbore({ - ...editableWellbore, - comments: e.target.value - }); - }} - /> - - )} - - } - confirmDisabled={ - !validWellboreUid || - !validWellboreName || - !dTimeKickoffValid || - !wellboreHasChanges(pristineWellbore, editableWellbore) - } - onSubmit={() => onSubmit(editableWellbore)} - isLoading={isLoading} - /> - )} - - ); -}; - -const Container = styled.div` - display: flex; -`; - -export default WellborePropertiesModal; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/__tests__/LogCurveInfoPropertiesModal.test.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/__tests__/LogCurveInfoPropertiesModal.test.tsx deleted file mode 100644 index 595b03ab5..000000000 --- a/Src/WitsmlExplorer.Frontend/components/Modals/__tests__/LogCurveInfoPropertiesModal.test.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { mockEdsCoreReact } from "__testUtils__/mocks/EDSMocks"; -import { - getAxisDefinition, - getLogCurveInfo, - getLogObject, - renderWithContexts -} from "__testUtils__/testUtils"; -import LogCurveInfoPropertiesModal, { - LogCurveInfoPropertiesModalProps -} from "components/Modals/LogCurveInfoPropertiesModal"; -import JobService from "services/jobService"; -import { vi } from "vitest"; - -vi.mock("services/jobService"); -vi.mock("@equinor/eds-core-react", () => mockEdsCoreReact()); - -const simpleProps: LogCurveInfoPropertiesModalProps = { - logCurveInfo: getLogCurveInfo(), - dispatchOperation: vi.fn(), - selectedLog: getLogObject() -}; - -const propsWithAxisDefinition: LogCurveInfoPropertiesModalProps = { - ...simpleProps, - logCurveInfo: getLogCurveInfo({ - axisDefinitions: [getAxisDefinition()] - }) -}; - -it("Properties of a LogCurve should be shown in the modal", async () => { - const expectedLogCurveInfo = simpleProps.logCurveInfo; - - renderWithContexts(); - - const uidInput = screen.getByRole("textbox", { name: /uid/i }); - const mnemonicInput = screen.getByRole("textbox", { name: /mnemonic/i }); - - expect(uidInput).toHaveValue(expectedLogCurveInfo.uid); - expect(mnemonicInput).toHaveValue(expectedLogCurveInfo.mnemonic); - - expect(uidInput).toBeDisabled(); - expect(mnemonicInput).toBeEnabled(); -}); - -it("AxisDefinition should be shown readonly in the LogCurveInfo modal when included in the props", async () => { - const expectedLogCurveInfo = propsWithAxisDefinition.logCurveInfo; - const expectedAxisDefinition = expectedLogCurveInfo.axisDefinitions[0]; - - renderWithContexts( - - ); - - const uidInput = screen.getByRole("textbox", { name: /uid/i }); - const mnemonicInput = screen.getByRole("textbox", { name: /mnemonic/i }); - const axisDefinitionLabel = screen.getByText(/axisdefinition/i); - const orderInput = screen.getByRole("textbox", { name: /order/i }); - const countInput = screen.getByRole("textbox", { name: /count/i }); - const doubleValuesInput = screen.getByRole("textbox", { - name: /doubleValues/i - }); - - expect(uidInput).toHaveValue(expectedLogCurveInfo.uid); - expect(mnemonicInput).toHaveValue(expectedLogCurveInfo.mnemonic); - expect(axisDefinitionLabel).toHaveTextContent(expectedAxisDefinition.uid); - expect(orderInput).toHaveValue(expectedAxisDefinition.order.toString()); - expect(countInput).toHaveValue(expectedAxisDefinition.count.toString()); - expect(doubleValuesInput).toHaveValue(expectedAxisDefinition.doubleValues); - - expect(uidInput).toBeDisabled(); - expect(mnemonicInput).toBeEnabled(); - expect(orderInput).toBeDisabled(); - expect(countInput).toBeDisabled(); - expect(doubleValuesInput).toBeDisabled(); -}); - -it("Saving edited properties of a LogCurve should result in the order of a job", async () => { - const mockedOrderJob = vi.fn(); - JobService.orderJob = mockedOrderJob; - - const user = userEvent.setup(); - - renderWithContexts(); - - const mnemonicInput = screen.getByRole("textbox", { name: /mnemonic/i }); - const saveButton = screen.getByRole("button", { name: /save/i }); - - await user.type(mnemonicInput, "editedMnemonic"); - - expect(saveButton).toBeEnabled(); - - await user.click(saveButton); - - expect(mockedOrderJob).toHaveBeenCalledTimes(1); -}); diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/LogTypeItem.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/LogTypeItem.tsx index 59051024e..79a0fe1c7 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/LogTypeItem.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/LogTypeItem.tsx @@ -1,4 +1,5 @@ import { + WITSML_INDEX_TYPE, WITSML_INDEX_TYPE_DATE_TIME, WITSML_INDEX_TYPE_MD } from "components/Constants"; @@ -10,7 +11,6 @@ import { import LogsContextMenu, { LogsContextMenuProps } from "components/ContextMenus/LogsContextMenu"; -import { IndexCurve } from "components/Modals/LogPropertiesModal"; import LogItem from "components/Sidebar/LogItem"; import TreeItem from "components/Sidebar/TreeItem"; import { useConnectedServer } from "contexts/connectedServerContext"; @@ -88,14 +88,14 @@ export default function LogTypeItem({ const onContextMenu = ( event: MouseEvent, wellbore: Wellbore, - indexCurve: IndexCurve + indexType: WITSML_INDEX_TYPE ) => { preventContextMenuPropagation(event); const contextMenuProps: LogsContextMenuProps = { dispatchOperation, wellbore, servers, - indexCurve + indexType }; const position = getContextMenuPosition(event); dispatchOperation({ @@ -255,7 +255,7 @@ export default function LogTypeItem({ nodeId={logTypeGroupDepth} to={getNavPath(logTypeGroupDepth)} onContextMenu={(event: MouseEvent) => - onContextMenu(event, wellbore, IndexCurve.Depth) + onContextMenu(event, wellbore, WITSML_INDEX_TYPE_MD) } isActive={depthLogs?.some((log) => log.objectGrowing)} selected={ @@ -279,7 +279,7 @@ export default function LogTypeItem({ labelText={"Time"} to={getNavPath(logTypeGroupTime)} onContextMenu={(event: MouseEvent) => - onContextMenu(event, wellbore, IndexCurve.Time) + onContextMenu(event, wellbore, WITSML_INDEX_TYPE_DATE_TIME) } isActive={timeLogs?.some((log) => log.objectGrowing)} selected={ diff --git a/Src/WitsmlExplorer.Frontend/models/bhaStatusTypes.ts b/Src/WitsmlExplorer.Frontend/models/bhaStatusTypes.ts new file mode 100644 index 000000000..f5c74a050 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/models/bhaStatusTypes.ts @@ -0,0 +1 @@ +export const bhaStatusTypes = ["final", "progress", "plan", "unknown"]; diff --git a/Src/WitsmlExplorer.Frontend/models/commonData.tsx b/Src/WitsmlExplorer.Frontend/models/commonData.tsx index 79da36fa8..625cb73f7 100644 --- a/Src/WitsmlExplorer.Frontend/models/commonData.tsx +++ b/Src/WitsmlExplorer.Frontend/models/commonData.tsx @@ -19,5 +19,3 @@ export function emptyCommonData(): CommonData { serviceCategory: "" }; } - -export const itemStateValues = ["actual", "model", "plan", "unknown"]; diff --git a/Src/WitsmlExplorer.Frontend/models/indexCurve.ts b/Src/WitsmlExplorer.Frontend/models/indexCurve.ts new file mode 100644 index 000000000..7df0be3f9 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/models/indexCurve.ts @@ -0,0 +1,4 @@ +export enum IndexCurve { + Depth = "Depth", + Time = "Time" +} diff --git a/Src/WitsmlExplorer.Frontend/models/levelIntegerCode.ts b/Src/WitsmlExplorer.Frontend/models/levelIntegerCode.ts new file mode 100644 index 000000000..ea7c29a8c --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/models/levelIntegerCode.ts @@ -0,0 +1 @@ +export const levelIntegerCode = ["1", "2", "3", "4", "5"]; diff --git a/Src/WitsmlExplorer.Frontend/models/typeTubularAssy.ts b/Src/WitsmlExplorer.Frontend/models/typeTubularAssy.ts new file mode 100644 index 000000000..913481927 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/models/typeTubularAssy.ts @@ -0,0 +1,16 @@ +export const typeTubularAssy = [ + "drilling", + "directional drilling", + "fishing", + "condition mud", + "tubing conveyed logging", + "cementing", + "casing", + "clean out", + "completion or testing", + "coring", + "hole opening or underreaming", + "milling or dressing or cutting", + "wiper or check or reaming", + "unknown" +]; diff --git a/Src/WitsmlExplorer.Frontend/models/wellbore.tsx b/Src/WitsmlExplorer.Frontend/models/wellbore.tsx index 2848ab7f4..8e6403993 100644 --- a/Src/WitsmlExplorer.Frontend/models/wellbore.tsx +++ b/Src/WitsmlExplorer.Frontend/models/wellbore.tsx @@ -26,8 +26,8 @@ export interface WellboreProperties { name: string; wellUid: string; wellName?: string; - wellStatus: string; - wellType: string; + wellboreStatus: string; + wellboreType: string; isActive: boolean; number?: string; suffixAPI?: string; @@ -78,8 +78,8 @@ export function emptyWellbore(): Wellbore { name: "", wellUid: "", wellName: "", - wellStatus: "", - wellType: "", + wellboreStatus: "", + wellboreType: "", isActive: false, wellboreParentUid: "", wellboreParentName: "", diff --git a/Src/WitsmlExplorer.Frontend/models/wellborePurposeValues.ts b/Src/WitsmlExplorer.Frontend/models/wellborePurposeValues.ts new file mode 100644 index 000000000..70391ee2d --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/models/wellborePurposeValues.ts @@ -0,0 +1,9 @@ +export const wellborePurposeValues = [ + "appraisal", + "development", + "exploration", + "fluid storage", + "general srvc", + "mineral", + "unknown" +]; diff --git a/Src/WitsmlExplorer.Frontend/services/jobService.tsx b/Src/WitsmlExplorer.Frontend/services/jobService.tsx index d2a569de7..2a76123b0 100644 --- a/Src/WitsmlExplorer.Frontend/services/jobService.tsx +++ b/Src/WitsmlExplorer.Frontend/services/jobService.tsx @@ -139,9 +139,7 @@ export enum JobType { CopyWithParent = "CopyWithParent", CopyObjectsWithParent = "CopyObjectsWithParent", CreateWellbore = "CreateWellbore", - CreateLogObject = "CreateLogObject", - CreateRig = "CreateRig", - CreateTrajectory = "CreateTrajectory", + CreateObjectOnWellbore = "CreateObjectOnWellbore", DeleteComponents = "DeleteComponents", DeleteCurveValues = "DeleteCurveValues", DeleteObjects = "DeleteObjects", diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/CreateLogWorkerTests.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/CreateLogWorkerTests.cs index 1be31ff3b..5182ab463 100644 --- a/Tests/WitsmlExplorer.Api.Tests/Workers/CreateLogWorkerTests.cs +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/CreateLogWorkerTests.cs @@ -30,7 +30,7 @@ public class CreateLogWorkerTests private const string WellboreUid = "B-5209671"; private const string WellboreName = "NO 34/10-A-25 C - Main Wellbore"; private readonly Mock _witsmlClient; - private readonly CreateLogWorker _worker; + private readonly CreateObjectOnWellboreWorker _worker; public CreateLogWorkerTests() { @@ -39,19 +39,18 @@ public CreateLogWorkerTests() witsmlClientProvider.Setup(provider => provider.GetClient()).Returns(_witsmlClient.Object); ILoggerFactory loggerFactory = new LoggerFactory(); loggerFactory.AddSerilog(Log.Logger); - ILogger logger = loggerFactory.CreateLogger(); - _worker = new CreateLogWorker(logger, witsmlClientProvider.Object); + ILogger logger = loggerFactory.CreateLogger(); + _worker = new CreateObjectOnWellboreWorker(logger, witsmlClientProvider.Object); } [Fact] public async Task CreateDepthIndexedLog_OK() { - CreateLogJob job = CreateJobTemplate(); - SetupGetWellbore(); + CreateObjectOnWellboreJob job = CreateJobTemplate(WitsmlLog.WITSML_INDEX_TYPE_MD); List createdLogs = new(); _witsmlClient.Setup(client => - client.AddToStoreAsync(It.IsAny())) - .Callback(createdLogs.Add) + client.AddToStoreAsync(It.IsAny())) + .Callback(logs => createdLogs.Add(logs as WitsmlLogs)) .ReturnsAsync(new QueryResult(true)); await _worker.Execute(job); @@ -73,12 +72,11 @@ public async Task CreateDepthIndexedLog_OK() [Fact] public async Task CreateTimeIndexedLog_OK() { - CreateLogJob job = CreateJobTemplate("Time"); - SetupGetWellbore(); + CreateObjectOnWellboreJob job = CreateJobTemplate(WitsmlLog.WITSML_INDEX_TYPE_DATE_TIME); List createdLogs = new(); _witsmlClient.Setup(client => - client.AddToStoreAsync(It.IsAny())) - .Callback(createdLogs.Add) + client.AddToStoreAsync(It.IsAny())) + .Callback(logs => createdLogs.Add(logs as WitsmlLogs)) .ReturnsAsync(new QueryResult(true)); await _worker.Execute(job); @@ -97,56 +95,23 @@ public async Task CreateTimeIndexedLog_OK() Assert.Equal(CommonConstants.Unit.Second, indexLogCurve.Unit); } - [Fact] - public async Task CreateTimeIndexedLog_UseTimeAsIndexIfUnknown() - { - CreateLogJob job = CreateJobTemplate("strange"); - SetupGetWellbore(); - List createdLogs = new(); - _witsmlClient.Setup(client => - client.AddToStoreAsync(It.IsAny())) - .Callback(createdLogs.Add) - .ReturnsAsync(new QueryResult(true)); - - await _worker.Execute(job); - WitsmlLog createdLog = createdLogs.First().Logs.First(); - WitsmlLogCurveInfo indexLogCurve = createdLog.LogCurveInfo.First(); - Assert.Equal("Time", indexLogCurve.Mnemonic); - Assert.Equal(CommonConstants.Unit.Second, indexLogCurve.Unit); - } - - private static CreateLogJob CreateJobTemplate(string indexCurve = "Depth") + private static CreateObjectOnWellboreJob CreateJobTemplate(string indexType) { - return new CreateLogJob + return new CreateObjectOnWellboreJob { - LogObject = new LogObject + Object = new LogObject { Uid = LogUid, Name = LogName, WellUid = WellUid, + WellName = WellName, WellboreUid = WellboreUid, - IndexCurve = indexCurve - } + WellboreName = WellboreName, + IndexCurve = indexType == WitsmlLog.WITSML_INDEX_TYPE_MD ? "Depth" : "Time", + IndexType = indexType + }, + ObjectType = EntityType.Log }; } - - private void SetupGetWellbore() - { - _witsmlClient.Setup(client => - client.GetFromStoreAsync(It.IsAny(), It.Is((ops) => ops.ReturnElements == ReturnElements.Requested))) - .ReturnsAsync(new WitsmlWellbores - { - Wellbores = new List - { - new WitsmlWellbore - { - UidWell = WellUid, - Uid = WellboreUid, - Name = WellboreName, - NameWell = WellName - } - } - }); - } } } diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/CreateRigWorkerTests.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/CreateRigWorkerTests.cs index 7ebdff046..5f99079e1 100644 --- a/Tests/WitsmlExplorer.Api.Tests/Workers/CreateRigWorkerTests.cs +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/CreateRigWorkerTests.cs @@ -10,6 +10,7 @@ using Serilog; using Witsml; +using Witsml.Data; using Witsml.Data.Rig; using WitsmlExplorer.Api.Jobs; @@ -33,7 +34,7 @@ public class CreateRigWorkerTests private const string WellboreUid = "wellboreUid"; private readonly Mock _witsmlClient; - private readonly CreateRigWorker _worker; + private readonly CreateObjectOnWellboreWorker _worker; public CreateRigWorkerTests() { @@ -42,31 +43,35 @@ public CreateRigWorkerTests() witsmlClientProvider.Setup(provider => provider.GetClient()).Returns(_witsmlClient.Object); ILoggerFactory loggerFactory = new LoggerFactory(); loggerFactory.AddSerilog(Log.Logger); - ILogger logger = loggerFactory.CreateLogger(); - _worker = new CreateRigWorker(logger, witsmlClientProvider.Object); + ILogger logger = loggerFactory.CreateLogger(); + _worker = new CreateObjectOnWellboreWorker(logger, witsmlClientProvider.Object); } [Fact] public async Task CreateRig_Execute_MissingUid_InvalidOperationException() { - CreateRigJob job = CreateJobTemplate(uid: null); - InvalidOperationException exception = await Assert.ThrowsAsync(() => _worker.Execute(job)); - Assert.Equal("Uid cannot be empty", exception.Message); + CreateObjectOnWellboreJob job = CreateJobTemplate(uid: null); + var (workerResult, _) = await _worker.Execute(job); + Assert.False(workerResult.IsSuccess); + Assert.Equal("Uid cannot be null", workerResult.Message); job = CreateJobTemplate(uid: string.Empty); - exception = await Assert.ThrowsAsync(() => _worker.Execute(job)); - Assert.Equal("Uid cannot be empty", exception.Message); + (workerResult, _) = await _worker.Execute(job); + Assert.False(workerResult.IsSuccess); + Assert.Equal("Uid cannot be empty", workerResult.Message); _witsmlClient.Verify(client => client.AddToStoreAsync(It.IsAny()), Times.Never); } [Fact] public async Task CreateRig_Execute_MissingName_InvalidOperationException() { - CreateRigJob job = CreateJobTemplate(name: null); - InvalidOperationException exception = await Assert.ThrowsAsync(() => _worker.Execute(job)); - Assert.Equal("Name cannot be empty", exception.Message); + CreateObjectOnWellboreJob job = CreateJobTemplate(name: null); + var (workerResult, _) = await _worker.Execute(job); + Assert.False(workerResult.IsSuccess); + Assert.Equal("Name cannot be null", workerResult.Message); job = CreateJobTemplate(name: string.Empty); - exception = await Assert.ThrowsAsync(() => _worker.Execute(job)); - Assert.Equal("Name cannot be empty", exception.Message); + (workerResult, _) = await _worker.Execute(job); + Assert.False(workerResult.IsSuccess); + Assert.Equal("Name cannot be empty", workerResult.Message); _witsmlClient.Verify(client => client.AddToStoreAsync(It.IsAny()), Times.Never); } @@ -74,12 +79,12 @@ public async Task CreateRig_Execute_MissingName_InvalidOperationException() [Fact] public async Task CreateRig_Execute_ValidResults() { - CreateRigJob job = CreateJobTemplate(); + CreateObjectOnWellboreJob job = CreateJobTemplate(); List createdRigs = new(); _witsmlClient.Setup(client => - client.AddToStoreAsync(It.IsAny())) - .Callback(rig => createdRigs.Add(rig)) + client.AddToStoreAsync(It.IsAny())) + .Callback(rig => createdRigs.Add(rig as WitsmlRigs)) .ReturnsAsync(new QueryResult(true)); await _worker.Execute(job); @@ -94,11 +99,11 @@ public async Task CreateRig_Execute_ValidResults() Assert.Equal(WellboreUid, createdRig.UidWellbore); } - private static CreateRigJob CreateJobTemplate(string uid = Uid, string name = Name, string wellUid = WellUid, string wellName = WellName, string wellboreUid = WellboreUid) + private static CreateObjectOnWellboreJob CreateJobTemplate(string uid = Uid, string name = Name, string wellUid = WellUid, string wellName = WellName, string wellboreUid = WellboreUid) { - return new CreateRigJob + return new CreateObjectOnWellboreJob { - Rig = new Rig() + Object = new Rig() { Uid = uid, Name = name, @@ -110,7 +115,8 @@ private static CreateRigJob CreateJobTemplate(string uid = Uid, string name = Na ItemState = "plan", SourceName = "sourceName" } - } + }, + ObjectType = EntityType.Rig }; } } diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/CreateRiskWorkerTests.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/CreateRiskWorkerTests.cs index ec9abc87e..7f0077df1 100644 --- a/Tests/WitsmlExplorer.Api.Tests/Workers/CreateRiskWorkerTests.cs +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/CreateRiskWorkerTests.cs @@ -26,9 +26,11 @@ public class CreateRiskWorkerTests private const string WellUid = "wellUid"; private const string WellName = "wellName"; private const string WellboreUid = "wellboreUid"; + private const string WellboreName = "wellboreName"; + private const string Uid = "riskUid"; private const string Name = "riskname"; private readonly Mock _witsmlClient; - private readonly CreateRiskWorker _worker; + private readonly CreateObjectOnWellboreWorker _worker; public CreateRiskWorkerTests() { @@ -37,22 +39,20 @@ public CreateRiskWorkerTests() witsmlClientProvider.Setup(provider => provider.GetClient()).Returns(_witsmlClient.Object); ILoggerFactory loggerFactory = new LoggerFactory(); loggerFactory.AddSerilog(Log.Logger); - ILogger logger = loggerFactory.CreateLogger(); - _worker = new CreateRiskWorker(logger, witsmlClientProvider.Object); + ILogger logger = loggerFactory.CreateLogger(); + _worker = new CreateObjectOnWellboreWorker(logger, witsmlClientProvider.Object); } [Fact] - public async Task ValidCreateRiskJobExecute() + public async Task ValidCreateObjectOnWellboreJobExecute() { - CreateRiskJob job = CreateJobTemplate(); + CreateObjectOnWellboreJob job = CreateJobTemplate(); List createdRisks = new(); _witsmlClient.Setup(client => - client.AddToStoreAsync(It.IsAny())) - .Callback(risk => createdRisks.Add(risk)) + client.AddToStoreAsync(It.IsAny())) + .Callback(risk => createdRisks.Add(risk as WitsmlRisks)) .ReturnsAsync(new QueryResult(true)); - _witsmlClient.Setup(client => client.GetFromStoreAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new WitsmlRisks() { Risks = new List() { new WitsmlRisk() } }); await _worker.Execute(job); @@ -64,23 +64,25 @@ public async Task ValidCreateRiskJobExecute() Assert.Equal(WellName, createdRisk.NameWell); } - private static CreateRiskJob CreateJobTemplate(string uid = WellboreUid, string name = Name, - string wellUid = WellUid, string wellName = WellName) + private static CreateObjectOnWellboreJob CreateJobTemplate() { - return new CreateRiskJob + return new CreateObjectOnWellboreJob { - Risk = new Risk + Object = new Risk { - Uid = uid, - Name = name, - WellUid = wellUid, - WellName = wellName, + Uid = Uid, + Name = Name, + WellUid = WellUid, + WellName = WellName, + WellboreUid = WellboreUid, + WellboreName = WellboreName, CommonData = new CommonData { ItemState = "model", SourceName = "SourceName" } - } + }, + ObjectType = EntityType.Risk }; } } diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/CreateTrajectoryWorkerTests.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/CreateTrajectoryWorkerTests.cs index e4d044de4..9401b97fa 100644 --- a/Tests/WitsmlExplorer.Api.Tests/Workers/CreateTrajectoryWorkerTests.cs +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/CreateTrajectoryWorkerTests.cs @@ -33,7 +33,7 @@ public class CreateTrajectoryWorkerTests private const string WellboreUid = "wellboreUid"; private readonly Mock _witsmlClient; - private readonly CreateTrajectoryWorker _worker; + private readonly CreateObjectOnWellboreWorker _worker; public CreateTrajectoryWorkerTests() { @@ -42,31 +42,35 @@ public CreateTrajectoryWorkerTests() witsmlClientProvider.Setup(provider => provider.GetClient()).Returns(_witsmlClient.Object); ILoggerFactory loggerFactory = new LoggerFactory(); loggerFactory.AddSerilog(Log.Logger); - ILogger logger = loggerFactory.CreateLogger(); - _worker = new CreateTrajectoryWorker(logger, witsmlClientProvider.Object); + ILogger logger = loggerFactory.CreateLogger(); + _worker = new CreateObjectOnWellboreWorker(logger, witsmlClientProvider.Object); } [Fact] public async Task CreateTrajectory_Execute_MissingUid_InvalidOperationException() { - var job = CreateJobTemplate(uid: null); - InvalidOperationException exception = await Assert.ThrowsAsync(() => _worker.Execute(job)); - Assert.Equal("Uid cannot be empty", exception.Message); - job = CreateJobTemplate(uid: ""); - exception = await Assert.ThrowsAsync(() => _worker.Execute(job)); - Assert.Equal("Uid cannot be empty", exception.Message); + CreateObjectOnWellboreJob job = CreateJobTemplate(uid: null); + var (workerResult, _) = await _worker.Execute(job); + Assert.False(workerResult.IsSuccess); + Assert.Equal("Uid cannot be null", workerResult.Message); + job = CreateJobTemplate(uid: string.Empty); + (workerResult, _) = await _worker.Execute(job); + Assert.False(workerResult.IsSuccess); + Assert.Equal("Uid cannot be empty", workerResult.Message); _witsmlClient.Verify(client => client.AddToStoreAsync(It.IsAny()), Times.Never); } [Fact] public async Task CreateTrajectory_Execute_MissingName_InvalidOperationException() { - var job = CreateJobTemplate(name: null); - InvalidOperationException exception = await Assert.ThrowsAsync(() => _worker.Execute(job)); - Assert.Equal("Name cannot be empty", exception.Message); + CreateObjectOnWellboreJob job = CreateJobTemplate(name: null); + var (workerResult, _) = await _worker.Execute(job); + Assert.False(workerResult.IsSuccess); + Assert.Equal("Name cannot be null", workerResult.Message); job = CreateJobTemplate(name: string.Empty); - exception = await Assert.ThrowsAsync(() => _worker.Execute(job)); - Assert.Equal("Name cannot be empty", exception.Message); + (workerResult, _) = await _worker.Execute(job); + Assert.False(workerResult.IsSuccess); + Assert.Equal("Name cannot be empty", workerResult.Message); _witsmlClient.Verify(client => client.AddToStoreAsync(It.IsAny()), Times.Never); } @@ -74,12 +78,12 @@ public async Task CreateTrajectory_Execute_MissingName_InvalidOperationException [Fact] public async Task CreateTrajectory_Execute_ValidResults() { - CreateTrajectoryJob job = CreateJobTemplate(); + CreateObjectOnWellboreJob job = CreateJobTemplate(); List createdTrajectories = new(); _witsmlClient.Setup(client => - client.AddToStoreAsync(It.IsAny())) - .Callback(trajectory => createdTrajectories.Add(trajectory)) + client.AddToStoreAsync(It.IsAny())) + .Callback(trajectory => createdTrajectories.Add(trajectory as WitsmlTrajectories)) .ReturnsAsync(new QueryResult(true)); await _worker.Execute(job); @@ -94,18 +98,19 @@ public async Task CreateTrajectory_Execute_ValidResults() Assert.Equal(WellboreUid, createdObject.UidWellbore); } - private static CreateTrajectoryJob CreateJobTemplate(string uid = Uid, string name = Name, string wellUid = WellUid, string wellName = WellName, string wellboreUid = WellboreUid) + private static CreateObjectOnWellboreJob CreateJobTemplate(string uid = Uid, string name = Name, string wellUid = WellUid, string wellName = WellName, string wellboreUid = WellboreUid) { - return new CreateTrajectoryJob + return new CreateObjectOnWellboreJob { - Trajectory = new Trajectory() + Object = new Trajectory() { Uid = uid, Name = name, WellUid = wellUid, WellName = wellName, WellboreUid = wellboreUid - } + }, + ObjectType = EntityType.Trajectory }; } } diff --git a/Tests/WitsmlExplorer.IntegrationTests/Api/Workers/CreateLogWorkerTests.cs b/Tests/WitsmlExplorer.IntegrationTests/Api/Workers/CreateLogWorkerTests.cs index 96370bb31..d25b3397d 100644 --- a/Tests/WitsmlExplorer.IntegrationTests/Api/Workers/CreateLogWorkerTests.cs +++ b/Tests/WitsmlExplorer.IntegrationTests/Api/Workers/CreateLogWorkerTests.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -9,7 +8,6 @@ using WitsmlExplorer.Api.Jobs; using WitsmlExplorer.Api.Models; using WitsmlExplorer.Api.Services; -using WitsmlExplorer.Api.Workers; using WitsmlExplorer.Api.Workers.Create; using Xunit; @@ -18,7 +16,7 @@ namespace WitsmlExplorer.IntegrationTests.Api.Workers { public class CreateLogWorkerTests { - private readonly CreateLogWorker _worker; + private readonly CreateObjectOnWellboreWorker _worker; private static readonly string WELL_UID = "fa53698b-0a19-4f02-bca5-001f5c31c0ca"; private static readonly string WELLBORE_UID = "eea43bf8-e3b7-42b6-b328-21b34cb505eb"; @@ -28,23 +26,24 @@ public CreateLogWorkerTests() var witsmlClientProvider = new WitsmlClientProvider(configuration); var loggerFactory = (ILoggerFactory)new LoggerFactory(); loggerFactory.AddSerilog(Log.Logger); - var logger = loggerFactory.CreateLogger(); - _worker = new CreateLogWorker(logger, witsmlClientProvider); + var logger = loggerFactory.CreateLogger(); + _worker = new CreateObjectOnWellboreWorker(logger, witsmlClientProvider); } [Fact(Skip = "Should only be run manually")] public async Task CreateLog_DepthIndexed() { - var job = new CreateLogJob + var job = new CreateObjectOnWellboreJob { - LogObject = new LogObject + Object = new LogObject { Uid = Guid.NewGuid().ToString(), Name = "Test depth", WellUid = WELL_UID, WellboreUid = WELLBORE_UID, IndexCurve = "Depth" - } + }, + ObjectType = EntityType.Log }; await _worker.Execute(job); @@ -53,16 +52,17 @@ public async Task CreateLog_DepthIndexed() [Fact(Skip = "Should only be run manually")] public async Task CreateLog_TimeIndexed() { - var job = new CreateLogJob + var job = new CreateObjectOnWellboreJob { - LogObject = new LogObject + Object = new LogObject { Uid = Guid.NewGuid().ToString(), Name = "Test time", WellUid = WELL_UID, WellboreUid = WELLBORE_UID, IndexCurve = "Time" - } + }, + ObjectType = EntityType.Log }; await _worker.Execute(job); From 00fcce931ca8a23056f13109f0381ae1a3518eb6 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Fri, 2 Aug 2024 15:28:18 +0200 Subject: [PATCH 084/124] FIX-2512 Fix time zone offset bug (#2513) --- Src/WitsmlExplorer.Frontend/components/DateFormatter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/WitsmlExplorer.Frontend/components/DateFormatter.ts b/Src/WitsmlExplorer.Frontend/components/DateFormatter.ts index b9466f36e..da8f7a1cb 100644 --- a/Src/WitsmlExplorer.Frontend/components/DateFormatter.ts +++ b/Src/WitsmlExplorer.Frontend/components/DateFormatter.ts @@ -55,7 +55,7 @@ export function getOffsetFromTimeZone(timeZone: TimeZone): string { } export function getOffset(dateString: string): string | null { - if (dateString.indexOf("Z") == dateString.length - 1) { + if (!dateString || dateString.indexOf("Z") == dateString.length - 1) { return "Z"; } From f9899ee96c5b36dbfd920069fbc995fbf046593e Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Mon, 5 Aug 2024 08:00:07 +0200 Subject: [PATCH 085/124] FIX-2503 Sort log curves in table by active (#2504) --- .../components/ContentViews/table/ColumnDef.tsx | 12 ++++++++---- .../components/ContentViews/table/ContentTable.tsx | 10 ++++++++++ .../ContentViews/table/contentTableUtils.ts | 1 + 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnDef.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnDef.tsx index 7cfbc7593..8580404f8 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnDef.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnDef.tsx @@ -9,6 +9,7 @@ import { } from "@tanstack/react-table"; import { activeId, + booleanSortingFn, calculateColumnWidth, componentSortingFn, expanderId, @@ -35,6 +36,7 @@ declare module "@tanstack/react-table" { interface SortingFns { [measureSortingFn]: SortingFn; [componentSortingFn]: SortingFn; + [booleanSortingFn]: SortingFn; } } @@ -65,7 +67,7 @@ export const useColumnDef = ( header: column.label, size: width, meta: { type: column.type }, - sortingFn: getSortingFn(column.type), + sortingFn: getSortingFn(column), enableColumnFilter: column.type !== ContentType.Component, filterFn: getFilterFn(column), ...addComponentCell(column.type), @@ -235,11 +237,13 @@ const getCheckableRowsColumnDef = ( }; }; -const getSortingFn = (contentType: ContentType) => { - if (contentType == ContentType.Measure) { +const getSortingFn = (column: ContentTableColumn) => { + if (column.type == ContentType.Measure) { return measureSortingFn; - } else if (contentType == ContentType.Number) { + } else if (column.type == ContentType.Number) { return "alphanumeric"; + } else if (column.label === activeId) { + return booleanSortingFn; } return "text"; }; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ContentTable.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ContentTable.tsx index 859dd2589..59db86001 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ContentTable.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ContentTable.tsx @@ -30,6 +30,7 @@ import { TableContainer } from "components/ContentViews/table/contentTableStyles"; import { + booleanSortingFn, calculateHorizontalSpace, calculateRowHeight, componentSortingFn, @@ -133,6 +134,15 @@ export const ContentTable = React.memo( const a = rowA.getValue(columnId) == null; const b = rowB.getValue(columnId) == null; return a === b ? 0 : a ? -1 : 1; + }, + [booleanSortingFn]: ( + rowA: Row, + rowB: Row, + columnId: string + ) => { + const a = rowA.getValue(columnId); + const b = rowB.getValue(columnId); + return a === b ? 0 : a ? 1 : -1; } }, columnResizeMode: "onChange", diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/contentTableUtils.ts b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/contentTableUtils.ts index 6eaae973c..3661992c4 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/contentTableUtils.ts +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/contentTableUtils.ts @@ -8,6 +8,7 @@ export const expanderId = "expander"; export const activeId = "active"; //implemented specifically for LogCurveInfoListView, needs rework if other views will also use filtering export const measureSortingFn = "measure"; export const componentSortingFn = "component"; +export const booleanSortingFn = "boolean"; export const constantTableOptions = { enableColumnResizing: true, From 20a6b2dced40fa349a077a609491beac8d53f6de Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Mon, 5 Aug 2024 10:05:28 +0200 Subject: [PATCH 086/124] FIX-2489 Query View - Implement "Open in table view" (#2510) --- .../components/QueryEditor.tsx | 9 +- .../components/QueryEditorUtils.tsx | 11 +- .../components/QueryEditorWidgetUtils.tsx | 283 ++++++++++++++++-- Src/WitsmlExplorer.Frontend/styles/Icons.tsx | 2 + 4 files changed, 282 insertions(+), 23 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/components/QueryEditor.tsx b/Src/WitsmlExplorer.Frontend/components/QueryEditor.tsx index 45d8aab7a..bb8566106 100644 --- a/Src/WitsmlExplorer.Frontend/components/QueryEditor.tsx +++ b/Src/WitsmlExplorer.Frontend/components/QueryEditor.tsx @@ -9,6 +9,7 @@ import { customCommands, customCompleter } from "components/QueryEditorUtils"; import { updateLinesWithWidgets } from "components/QueryEditorWidgetUtils"; import { useOperationState } from "hooks/useOperationState"; import AceEditor from "react-ace"; +import { useNavigate, useParams } from "react-router-dom"; import styled from "styled-components"; import { Colors, dark } from "styles/Colors"; @@ -20,6 +21,8 @@ export interface QueryEditorProps { export const QueryEditor = (props: QueryEditorProps) => { const { value, onChange, readonly } = props; + const navigate = useNavigate(); + const { serverUrl } = useParams(); const { operationState: { colors } } = useOperationState(); @@ -31,10 +34,10 @@ export const QueryEditor = (props: QueryEditorProps) => { editor.renderer.$cursorLayer.element.style.display = "none"; } else { editor.completers = [customCompleter]; - editor.renderer.on("afterRender", (_: any, renderer: any) => - updateLinesWithWidgets(editor, renderer) - ); } + editor.renderer.on("afterRender", (_: any, renderer: any) => + updateLinesWithWidgets(editor, renderer, navigate, serverUrl, readonly) + ); }; return ( diff --git a/Src/WitsmlExplorer.Frontend/components/QueryEditorUtils.tsx b/Src/WitsmlExplorer.Frontend/components/QueryEditorUtils.tsx index 4169cef03..3c91734ae 100644 --- a/Src/WitsmlExplorer.Frontend/components/QueryEditorUtils.tsx +++ b/Src/WitsmlExplorer.Frontend/components/QueryEditorUtils.tsx @@ -92,14 +92,19 @@ const getTemplateObject = (rows: string[]): TemplateObjects | null => { * * @param rows - An array of strings representing xml content. * @param currentRow - The row number for which to find the parent tag. - * @returns The parent tag or null if not found. + * @param fullLine - Wheter to return the full line, or only the tag (default). + * @returns The parent tag (or full line of text), or null if not found. */ -const getParentTag = (rows: string[], currentRow: number): string => { +export const getParentTag = ( + rows: string[], + currentRow: number, + fullLine: boolean = false +): string => { const openTags = []; for (let index = 0; index < currentRow; index++) { const tag = getTag(rows[index]); if (tag && !rows[index].includes("")) { - openTags.push(tag); + openTags.push(fullLine ? rows[index] : tag); } if (tag && /^\s*<\//.test(rows[index])) { openTags.pop(); diff --git a/Src/WitsmlExplorer.Frontend/components/QueryEditorWidgetUtils.tsx b/Src/WitsmlExplorer.Frontend/components/QueryEditorWidgetUtils.tsx index 6858198df..982ac47af 100644 --- a/Src/WitsmlExplorer.Frontend/components/QueryEditorWidgetUtils.tsx +++ b/Src/WitsmlExplorer.Frontend/components/QueryEditorWidgetUtils.tsx @@ -1,6 +1,26 @@ -import { Icon } from "@equinor/eds-core-react"; -import { getTag } from "components/QueryEditorUtils"; +import { Icon, Tooltip } from "@equinor/eds-core-react"; +import { + WITSML_INDEX_TYPE_DATE_TIME, + WITSML_INDEX_TYPE_MD, + WITSML_LOG_ORDERTYPE_DECREASING +} from "components/Constants"; +import { getParentTag, getTag } from "components/QueryEditorUtils"; +import { isExpandableGroupObject } from "components/Sidebar/ObjectGroupItem"; +import EntityType from "models/entityType"; +import { ObjectType } from "models/objectType"; +import { ReactElement } from "react"; import { createRoot } from "react-dom/client"; +import { NavigateFunction } from "react-router-dom"; +import { RouterLogType } from "routes/routerConstants"; +import { createLogCurveValuesSearchParams } from "routes/utils/createLogCurveValuesSearchParams"; +import { + getLogCurveValuesViewPath, + getLogObjectViewPath, + getObjectGroupsViewPath, + getObjectsViewPath, + getObjectViewPath, + getWellboresViewPath +} from "routes/utils/pathBuilder"; import styled from "styled-components"; /** @@ -33,24 +53,38 @@ let updateTimeout: NodeJS.Timeout | null = null; * @param editor The Ace editor instance. * @param renderer The Ace renderer instance. */ -export const updateLinesWithWidgets = (editor: any, renderer: any) => { +export const updateLinesWithWidgets = ( + editor: any, + renderer: any, + navigate: NavigateFunction, + serverUrl: string, + readonly: boolean +) => { // updateWidgets sometimes adds multiple widgets to the same line when scrolling fast, so we need to debounce it slightly. if (updateTimeout) { clearTimeout(updateTimeout); } updateTimeout = setTimeout(() => { - updateWidgets(editor, renderer); + updateWidgets(editor, renderer, navigate, serverUrl, readonly); updateTimeout = null; }, 10); }; /** - * Updates the widgets for opening tag icons in the QueryEditor. - * Adds a widget container with an opening tag icon to each line that contains a self-closing tag. + * Updates the widgets in the QueryEditor. + * Current widgets: + * Edit widget to open self-closing xml tags. + * Navigation widget to navigate to objects given the uids in the result editor. * @param editor The Ace editor instance. * @param renderer The Ace renderer instance. */ -const updateWidgets = (editor: any, renderer: any) => { +const updateWidgets = ( + editor: any, + renderer: any, + navigate: NavigateFunction, + serverUrl: string, + readonly: boolean +) => { const textLayer = renderer.$textLayer; const config = textLayer.config; const session = textLayer.session; @@ -67,6 +101,8 @@ const updateWidgets = (editor: any, renderer: any) => { const useGroups = textLayer.$useLineGroups(); + const fullQuery: string = editor.getValue(); + while (row <= lastRow) { if (row > foldStart) { row = foldLine.end.row + 1; @@ -78,7 +114,11 @@ const updateWidgets = (editor: any, renderer: any) => { if (lineElement) { if (useGroups) lineElement = lineElement.lastChild; const lineText = session.getLine(row); - if (lineText.match(/<.*\/>/) && !lineElement.querySelector("svg")) { + if ( + lineText.match(/<.*\/>/) && + !lineElement.querySelector("svg") && + !readonly + ) { const widget = ( { /> ); - const widgetContainer = document.createElement("span"); - widgetContainer.className = "widget"; - widgetContainer.style.marginTop = "-2px"; - widgetContainer.style.boxSizing = "border-box"; - widgetContainer.style.display = "inline-block"; - widgetContainer.style.verticalAlign = "middle"; - widgetContainer.style.pointerEvents = "auto"; + addWidgetToLine(lineElement, widget); + } - createRoot(widgetContainer).render(widget); + const uids = [ + ...lineText.matchAll(/\b(uid(?:Well|Wellbore)?|uid)="([^"]+)"/g) + ].reduce((acc, match) => { + acc[match[1]] = match[2]; + return acc; + }, {}); + if ( + Object.keys(uids).length && + !lineElement.querySelector("svg") && + readonly + ) { + const { pathname, searchParams } = getNavigationPath( + lineText, + row, + fullQuery, + serverUrl, + uids + ); + if (pathname) { + const widget = ( + + + navigate({ + pathname, + search: searchParams?.toString() + }) + } + /> + + ); - lineElement.appendChild(widgetContainer); + addWidgetToLine(lineElement, widget); + } } } row++; } }; +/** + * Extracts the navigation path and search parameters for the given line of text. + * @param lineText The text of the line to navigate from. + * @param row The row number of the line. + * @param fullQuery The entire query text. + * @param serverUrl The server URL. + * @param uids An object containing the uidWell, uidWellbore, and uid. + * @returns An object containing the pathname and search parameters. + */ +const getNavigationPath = ( + lineText: string, + row: number, + fullQuery: string, + serverUrl: string, + uids: Record +) => { + let { uidWell: wellUid, uidWellbore: wellboreUid, uid } = uids; + const tag = getTag(lineText); + const entity = tag.charAt(0).toUpperCase() + tag.slice(1); + let pathname = null; + let searchParams = null; + if (entity === EntityType.Well && uid) { + pathname = getWellboresViewPath(serverUrl, uid); + } else if (entity === EntityType.Wellbore && wellUid && uid) { + pathname = getObjectGroupsViewPath(serverUrl, wellUid, uid); + } else if ( + Object.values(ObjectType).includes(entity as ObjectType) && + wellUid && + wellboreUid && + uid + ) { + if (entity === ObjectType.Log) { + const logType = getRouterLogType(fullQuery, row); + if (logType) { + pathname = getLogObjectViewPath( + serverUrl, + wellUid, + wellboreUid, + entity, + logType, + uid + ); + } + } else if (isExpandableGroupObject(entity as ObjectType)) { + pathname = getObjectViewPath( + serverUrl, + wellUid, + wellboreUid, + entity, + uid + ); + } else { + pathname = getObjectsViewPath(serverUrl, wellUid, wellboreUid, entity); + } + } else if (entity === "LogCurveInfo") { + // We parse the data to find the corresponding mnemonic, start and end indexes as we need them for the search params. + const fullQueryRows = fullQuery.split("\n"); + const logType = getRouterLogType(fullQuery, row); + const parentLine = getParentTag(fullQueryRows, row, true); + const parentUidMatch = parentLine.match( + /") + ); + const { mnemonic, min, max } = extractLogCurveInfo(logCurveInfoContent); + const direction = getDirection(fullQuery, row); + const isDecreasing = direction === WITSML_LOG_ORDERTYPE_DECREASING; + searchParams = createLogCurveValuesSearchParams( + isDecreasing ? max : min, + isDecreasing ? min : max, + [mnemonic] + ); + pathname = getLogCurveValuesViewPath( + serverUrl, + wellUid, + wellboreUid, + ObjectType.Log, + logType, + uid + ); + } + } + return { pathname, searchParams }; +}; + +/** + * Determines the log type based on the index type within the log content surrounding the row from the given query. + * @param fullQuery The entire query text. + * @param row The row number of the log tag. + * @returns The log type as a string. + */ +const getRouterLogType = (fullQuery: string, row: number) => { + const rowPosition = fullQuery.split("\n").slice(0, row).join("\n").length; + const startTagIndex = fullQuery.lastIndexOf( + "", rowPosition); + if (startTagIndex === -1 || endTagIndex === -1) return null; + const logContent = fullQuery.slice(startTagIndex, endTagIndex); + const indexType = logContent.match(/([^<]+)<\/indexType>/)?.[1]; + const logType = + indexType === WITSML_INDEX_TYPE_MD + ? RouterLogType.DEPTH + : indexType === WITSML_INDEX_TYPE_DATE_TIME + ? RouterLogType.TIME + : null; + return logType; +}; + +/** + * Determines the direction based on the index type within the log content surrounding the row from the given query. + * @param fullQuery The entire query text. + * @param row The row number of the log tag. + * @returns The direction as a string. + */ +const getDirection = (fullQuery: string, row: number) => { + const rowPosition = fullQuery.split("\n").slice(0, row).join("\n").length; + const startTagIndex = fullQuery.lastIndexOf( + "", rowPosition); + if (startTagIndex === -1 || endTagIndex === -1) return null; + const logContent = fullQuery.slice(startTagIndex, endTagIndex); + const direction = logContent.match(/([^<]+)<\/direction>/)?.[1]; + return direction; +}; + +/** + * Extracts the mnemonic, min index, and max index from the logCurveInfo content. + * @param logCurveInfoContent The content of the logCurveInfo tag. + * @returns An object containing the mnemonic, min, and max values. + */ +const extractLogCurveInfo = (logCurveInfoContent: string) => { + const mnemonic = logCurveInfoContent.match( + /([^<]+)<\/mnemonic>/ + )?.[1]; + const minDateTimeIndex = logCurveInfoContent.match( + /([^<]+)<\/minDateTimeIndex>/ + )?.[1]; + const maxDateTimeIndex = logCurveInfoContent.match( + /([^<]+)<\/maxDateTimeIndex>/ + )?.[1]; + const minIndex = logCurveInfoContent.match( + /]*>([^<]+)<\/minIndex>/ + )?.[1]; + const maxIndex = logCurveInfoContent.match( + /]*>([^<]+)<\/maxIndex>/ + )?.[1]; + const min = minDateTimeIndex ?? minIndex; + const max = maxDateTimeIndex ?? maxIndex; + return { mnemonic, min, max }; +}; + +/** + * Adds a widget to a line element. + * @param lineElement The line element to which the widget is added. + * @param widget The React element representing the widget. + */ +const addWidgetToLine = (lineElement: any, widget: ReactElement) => { + const widgetContainer = document.createElement("span"); + widgetContainer.className = "widget"; + widgetContainer.style.marginTop = "-2px"; + widgetContainer.style.boxSizing = "border-box"; + widgetContainer.style.display = "inline-block"; + widgetContainer.style.verticalAlign = "middle"; + widgetContainer.style.pointerEvents = "auto"; + + createRoot(widgetContainer).render(widget); + + lineElement.appendChild(widgetContainer); +}; + const StyledEditIcon = styled(Icon)` display: inline-block; vertical-align: middle; diff --git a/Src/WitsmlExplorer.Frontend/styles/Icons.tsx b/Src/WitsmlExplorer.Frontend/styles/Icons.tsx index 6d38c3633..9e3e1ada7 100644 --- a/Src/WitsmlExplorer.Frontend/styles/Icons.tsx +++ b/Src/WitsmlExplorer.Frontend/styles/Icons.tsx @@ -34,6 +34,7 @@ import { filter_alt as filter, folder_open as folderOpen, format_line_spacing as formatLine, + go_to as goTo, in_progress as inProgress, info_circle as infoCircle, trending_up as isActive, @@ -89,6 +90,7 @@ const icons = { filter, folderOpen, formatLine, + goTo, infoCircle, inProgress, isActive, From 391684a98def85b9d467b01fdb556f43add56b7a Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Mon, 5 Aug 2024 11:02:18 +0200 Subject: [PATCH 087/124] FIX-2508 Disable paste when no trajectory station is copied (#2509) --- .../components/ContextMenus/TrajectoryStationContextMenu.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryStationContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryStationContextMenu.tsx index f72a09841..bf0fd29ba 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryStationContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoryStationContextMenu.tsx @@ -126,6 +126,7 @@ const TrajectoryStationContextMenu = ( trajectory ) } + disabled={trajectoryStationReferences === null} > From ed2605f03913b8594016b5da500741fba2b6c9db Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Mon, 5 Aug 2024 11:07:52 +0200 Subject: [PATCH 088/124] FIX-2506 Show heartbeat for active multi-logs (#2507) --- Src/WitsmlExplorer.Frontend/components/Sidebar/LogTypeItem.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/LogTypeItem.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/LogTypeItem.tsx index 79a0fe1c7..8d6a51f68 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/LogTypeItem.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/LogTypeItem.tsx @@ -202,6 +202,9 @@ export default function LogTypeItem({ labelText={subLogsNodeName(log.name)} key={getMultipleLogsNode(log.name)} nodeId={getMultipleLogsNode(log.name)} + isActive={logObjects + .filter((x) => x.name === log.name) + ?.some((log) => log.objectGrowing)} to={`${getNavPath( getLogTypeGroup(logType) )}?${createColumnFilterSearchParams(searchParams, { From f92b6e1acab8e1fbdf816ac80219bbcfc0d62bcb Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Mon, 5 Aug 2024 11:37:21 +0200 Subject: [PATCH 089/124] FIX-2494 Allow unspecified returnElements in queryView (#2505) --- Src/WitsmlExplorer.Api/Models/WitsmlQuery.cs | 2 +- .../components/ContentViews/QueryView.tsx | 3 ++- .../components/ContentViews/QueryViewUtils.tsx | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Src/WitsmlExplorer.Api/Models/WitsmlQuery.cs b/Src/WitsmlExplorer.Api/Models/WitsmlQuery.cs index 35f48eb2d..8fdd34b9e 100644 --- a/Src/WitsmlExplorer.Api/Models/WitsmlQuery.cs +++ b/Src/WitsmlExplorer.Api/Models/WitsmlQuery.cs @@ -7,7 +7,7 @@ namespace WitsmlExplorer.Api.Models public class WitsmlQuery { [JsonConverter(typeof(JsonStringEnumConverter))] - public ReturnElements ReturnElements { get; init; } + public ReturnElements? ReturnElements { get; init; } public string OptionsInString { get; init; } public string Body { get; init; } } diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView.tsx index 0a7c533a7..e8cb01615 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView.tsx @@ -53,7 +53,8 @@ const QueryView = (): React.ReactElement => { dispatchOperation?.({ type: OperationType.HideModal }); setIsLoading(true); const requestReturnElements = - storeFunction === StoreFunction.GetFromStore + storeFunction === StoreFunction.GetFromStore && + returnElements !== ReturnElements.None ? returnElements : undefined; let response = await QueryService.postQuery( diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryViewUtils.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryViewUtils.tsx index 706c26eb7..48864c547 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryViewUtils.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryViewUtils.tsx @@ -8,7 +8,8 @@ export enum ReturnElements { DataOnly = "dataOnly", StationLocationOnly = "stationLocationOnly", LatestChangeOnly = "latestChangeOnly", - Requested = "requested" + Requested = "requested", + None = "" } export enum StoreFunction { From fb57c7081604e61d7cc6055b8a0c0adff77275b2 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:39:55 +0200 Subject: [PATCH 090/124] FIX-2511 Ensure correct match between log header and data for downloads (#2514) --- .../Helpers/ReportHelper.cs | 57 ------------------- .../Workers/DownloadAllLogDataWorker.cs | 30 +++++++--- 2 files changed, 23 insertions(+), 64 deletions(-) delete mode 100644 Src/WitsmlExplorer.Api/Helpers/ReportHelper.cs diff --git a/Src/WitsmlExplorer.Api/Helpers/ReportHelper.cs b/Src/WitsmlExplorer.Api/Helpers/ReportHelper.cs deleted file mode 100644 index 2dda2def2..000000000 --- a/Src/WitsmlExplorer.Api/Helpers/ReportHelper.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -using WitsmlExplorer.Api.Models; - -namespace WitsmlExplorer.Api.Helpers -{ - /// - /// Content type enumeration - /// - public enum ContentType - { - String, - Number, - DateTime, - Measure, - Component - } - - /// - /// Helper class for generating reports - /// - public class ReportHelper - { - // csv separator character - const char Separator = ','; - // new line character (LineFeed only) - const char NewLineCharacter = '\n'; - - /// - /// Generates log report - /// - /// Collection of report log data - /// Report header string - /// Report header and report body as separate strings - public static (string header, string body) GenerateReport(ICollection> reportItems, string reportHeader) - { - var columns = reportItems.Count > 0 ? - reportItems.First().Keys.Select(key => new - { - Property = key, - Label = key, - Type = ContentType.String - }).ToList() - : []; - - var exportColumns = reportHeader ?? string.Join(Separator, columns.Select(column => column.Property)); - - var data = string.Join(NewLineCharacter, - reportItems.Select(row => - string.Join(Separator, - columns.Select(col => row.TryGetValue(col.Property, out LogDataValue value) ? value.Value.ToString() : string.Empty)))); - - return (exportColumns, data); - } - } -} diff --git a/Src/WitsmlExplorer.Api/Workers/DownloadAllLogDataWorker.cs b/Src/WitsmlExplorer.Api/Workers/DownloadAllLogDataWorker.cs index df4497c81..7c040ef82 100644 --- a/Src/WitsmlExplorer.Api/Workers/DownloadAllLogDataWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/DownloadAllLogDataWorker.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.Logging; -using WitsmlExplorer.Api.Helpers; using WitsmlExplorer.Api.Jobs; using WitsmlExplorer.Api.Models; using WitsmlExplorer.Api.Models.Reports; @@ -21,6 +20,8 @@ public class DownloadAllLogDataWorker : BaseWorker, IWork { public JobType JobType => JobType.DownloadAllLogData; private readonly ILogObjectService _logObjectService; + private readonly char _newLineCharacter = '\n'; + private readonly char _separator = ','; public DownloadAllLogDataWorker( ILogger logger, @@ -51,15 +52,15 @@ public DownloadAllLogDataWorker( private (WorkerResult, RefreshAction) DownloadAllLogDataResult(DownloadAllLogDataJob job, ICollection> reportItems, ICollection curveSpecifications) { Logger.LogInformation("Download of all data is done. {jobDescription}", job.Description()); - job.JobInfo.Report = DownloadAllLogDataReport(reportItems, job.LogReference, GetReportHeader(curveSpecifications)); + var reportHeader = GetReportHeader(curveSpecifications); + var reportBody = GetReportBody(reportItems, curveSpecifications); + job.JobInfo.Report = DownloadAllLogDataReport(reportItems, job.LogReference, reportHeader, reportBody); WorkerResult workerResult = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), true, $"Download of all data is ready, jobId: ", jobId: job.JobInfo.Id); return (workerResult, null); } - private DownloadAllLogDataReport DownloadAllLogDataReport(ICollection> reportItems, LogObject logReference, string reportHeader) + private DownloadAllLogDataReport DownloadAllLogDataReport(ICollection> reportItems, LogObject logReference, string reportHeader, string reportBody) { - var result = ReportHelper.GenerateReport(reportItems, reportHeader); - return new DownloadAllLogDataReport { Title = $"{logReference.WellboreName} - {logReference.Name}", @@ -67,8 +68,8 @@ private DownloadAllLogDataReport DownloadAllLogDataReport(ICollection curveSpecificatio } return string.Join(',', listOfHeaders); } + + private string GetReportBody(ICollection> reportItems, ICollection curveSpecifications) + { + var mnemonics = curveSpecifications.Select(spec => spec.Mnemonic).ToList(); + var body = string.Join(_newLineCharacter, + reportItems.Select(row => + string.Join(_separator, mnemonics.Select(mnemonic => + row.TryGetValue(mnemonic, out LogDataValue value) + ? value.Value.ToString() + : string.Empty + )) + ) + ); + return body; + } } From 907ce3694f43eb9cfb42e17d646c997d28b92b15 Mon Sep 17 00:00:00 2001 From: Marita Midthaug Date: Mon, 5 Aug 2024 12:55:31 +0200 Subject: [PATCH 091/124] [Snyk] Security upgrade echarts from 5.5.0 to 5.5.1 (#2498) Co-authored-by: snyk-bot Co-authored-by: Elias Bruvik --- Src/WitsmlExplorer.Frontend/package.json | 2 +- yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/package.json b/Src/WitsmlExplorer.Frontend/package.json index a16aec324..b4a33a08d 100644 --- a/Src/WitsmlExplorer.Frontend/package.json +++ b/Src/WitsmlExplorer.Frontend/package.json @@ -41,7 +41,7 @@ "buffer": "^6.0.3", "date-fns": "^2.29.3", "date-fns-tz": "^2.0.0", - "echarts": "^5.4.3", + "echarts": "^5.5.1", "echarts-for-react": "^3.0.2", "lodash.orderby": "^4.6.0", "memoize-one": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index cd2e20418..4aec83ee5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2754,13 +2754,13 @@ echarts-for-react@^3.0.2: fast-deep-equal "^3.1.3" size-sensor "^1.0.1" -echarts@^5.4.3: - version "5.5.0" - resolved "https://registry.yarnpkg.com/echarts/-/echarts-5.5.0.tgz#c13945a7f3acdd67c134d8a9ac67e917830113ac" - integrity sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw== +echarts@^5.5.1: + version "5.5.1" + resolved "https://registry.yarnpkg.com/echarts/-/echarts-5.5.1.tgz#8dc9c68d0c548934bedcb5f633db07ed1dd2101c" + integrity sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA== dependencies: tslib "2.3.0" - zrender "5.5.0" + zrender "5.6.0" ejs@^3.1.8: version "3.1.10" @@ -6363,9 +6363,9 @@ yocto-queue@^1.0.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== -zrender@5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.5.0.tgz#54d0d6c4eda81a96d9f60a9cd74dc48ea026bc1e" - integrity sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w== +zrender@5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.6.0.tgz#01325b0bb38332dd5e87a8dbee7336cafc0f4a5b" + integrity sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg== dependencies: tslib "2.3.0" From 5e581efc1d5a877fa70ef69d4e0cc954bf61fe81 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Mon, 5 Aug 2024 15:34:39 +0200 Subject: [PATCH 092/124] FIX-2515 Add required properties for creating new objects (#2516) --- .../Modals/LogHeaderDateTimeField.tsx | 6 ++++- .../Properties/BhaRunProperties.ts | 3 ++- .../Properties/FormationMarkerProperties.ts | 3 ++- .../Properties/MessageProperties.ts | 27 +++++++++++++++++-- .../Properties/RiskProperties.ts | 6 +++-- .../Properties/WbGeometryProperties.ts | 3 ++- .../Properties/getFluidsReportProperties.ts | 6 +++-- .../PropertiesModal/PropertiesRenderer.tsx | 1 + .../models/messageTypes.ts | 7 +++++ 9 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 Src/WitsmlExplorer.Frontend/models/messageTypes.ts diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/LogHeaderDateTimeField.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/LogHeaderDateTimeField.tsx index 057773658..34acbfd51 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/LogHeaderDateTimeField.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/LogHeaderDateTimeField.tsx @@ -15,6 +15,7 @@ interface DateTimeFieldProps { minValue?: string; maxValue?: string; disabled?: boolean; + required?: boolean; } /** @@ -31,7 +32,8 @@ interface DateTimeFieldProps { export const LogHeaderDateTimeField = ( props: DateTimeFieldProps ): React.ReactElement => { - const { disabled, value, label, updateObject, minValue, maxValue } = props; + const { disabled, required, value, label, updateObject, minValue, maxValue } = + props; const { operationState: { timeZone } } = useOperationState(); @@ -46,6 +48,7 @@ export const LogHeaderDateTimeField = ( }, []); const validate = (current: string) => { + if (required && !current) return false; return ( ((!minValue || current >= minValue) && (!maxValue || current <= maxValue)) || @@ -55,6 +58,7 @@ export const LogHeaderDateTimeField = ( const getHelperText = () => { if (!validate(value)) { + if (required && !value) return "This field is required"; if (!initiallyEmpty && (value == null || value === "")) { return "This field cannot be deleted."; } diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/BhaRunProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/BhaRunProperties.ts index 4ba7211d6..b34e80a4c 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/BhaRunProperties.ts +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/BhaRunProperties.ts @@ -28,7 +28,8 @@ export const getBhaRunProperties = ( property: "tubular", propertyType: PropertyType.RefNameString, validator: validRefNameString, - helperText: getRefNameStringHelperText("tubular") + helperText: getRefNameStringHelperText("tubular"), + required: true }, { property: "dTimStart", diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/FormationMarkerProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/FormationMarkerProperties.ts index e5a52bff3..db4b23b00 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/FormationMarkerProperties.ts +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/FormationMarkerProperties.ts @@ -45,7 +45,8 @@ export const getFormationMarkerProperties = ( property: "mdTopSample", propertyType: PropertyType.Measure, validator: validMeasure, - helperText: getMeasureHelperText("mdTopSample") + helperText: getMeasureHelperText("mdTopSample"), + required: true }, { property: "tvdTopSample", diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/MessageProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/MessageProperties.ts index 1abaec62a..45b2b2b3d 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/MessageProperties.ts +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/MessageProperties.ts @@ -1,15 +1,38 @@ -import { PropertiesModalMode, validText } from "components/Modals/ModalParts"; +import { + PropertiesModalMode, + validOption, + validText +} from "components/Modals/ModalParts"; import { getCommonObjectOnWellboreProperties } from "components/Modals/PropertiesModal/Properties/CommonObjectOnWellboreProperties"; import { PropertiesModalProperty } from "components/Modals/PropertiesModal/propertiesModalProperty"; import { PropertyType } from "components/Modals/PropertiesModal/PropertyTypes"; -import { getMaxLengthHelperText } from "components/Modals/PropertiesModal/ValidationHelpers"; +import { + getMaxLengthHelperText, + getOptionHelperText +} from "components/Modals/PropertiesModal/ValidationHelpers"; import MaxLength from "models/maxLength"; import MessageObject from "models/messageObject"; +import { messageTypes } from "models/messageTypes"; export const getMessageProperties = ( mode: PropertiesModalMode ): PropertiesModalProperty[] => [ ...getCommonObjectOnWellboreProperties(mode), + { + property: "dTim", + propertyType: PropertyType.DateTime, + required: true, + disabled: mode !== PropertiesModalMode.New + }, + { + property: "typeMessage", + propertyType: PropertyType.Options, + validator: (value: string) => validOption(value, messageTypes), + helperText: getOptionHelperText("typeMessage"), + options: messageTypes, + required: true, + disabled: mode !== PropertiesModalMode.New + }, { property: "messageText", propertyType: PropertyType.String, diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/RiskProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/RiskProperties.ts index ce1660058..02ff91e5f 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/RiskProperties.ts +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/RiskProperties.ts @@ -41,14 +41,16 @@ export const getRiskProperties = ( propertyType: PropertyType.Options, validator: (value: string) => validOption(value, riskType), helperText: getOptionHelperText("type"), - options: riskType + options: riskType, + required: true }, { property: "category", propertyType: PropertyType.Options, validator: (value: string) => validOption(value, riskCategory), helperText: getOptionHelperText("category"), - options: riskCategory + options: riskCategory, + required: true }, { property: "subCategory", diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/WbGeometryProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/WbGeometryProperties.ts index 5dc1e9b9f..1581c8e3c 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/WbGeometryProperties.ts +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/WbGeometryProperties.ts @@ -33,7 +33,8 @@ export const getWbGeometryProperties = ( { property: "dTimReport", propertyType: PropertyType.DateTime, - disabled: true + required: true, + disabled: mode !== PropertiesModalMode.New }, { property: "commonData.sourceName", diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/getFluidsReportProperties.ts b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/getFluidsReportProperties.ts index 85f390cf2..ed861323a 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/getFluidsReportProperties.ts +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/Properties/getFluidsReportProperties.ts @@ -23,13 +23,15 @@ export const getFluidsReportProperties = ( ...getCommonObjectOnWellboreProperties(mode), { property: "dTim", - propertyType: PropertyType.DateTime + propertyType: PropertyType.DateTime, + required: true }, { property: "md", propertyType: PropertyType.Measure, validator: validMeasure, - helperText: getMeasureHelperText("md") + helperText: getMeasureHelperText("md"), + required: true }, { property: "tvd", diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/PropertiesRenderer.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/PropertiesRenderer.tsx index 134cecd5e..81c40c976 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/PropertiesRenderer.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/PropertiesModal/PropertiesRenderer.tsx @@ -157,6 +157,7 @@ export const PropertiesRenderer = ({ } label={prop.property} disabled={prop.disabled} + required={prop.required} updateObject={(dateTime: string) => { onChangeProperty(prop.property, prop.propertyType, dateTime); }} diff --git a/Src/WitsmlExplorer.Frontend/models/messageTypes.ts b/Src/WitsmlExplorer.Frontend/models/messageTypes.ts new file mode 100644 index 000000000..884acff20 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/models/messageTypes.ts @@ -0,0 +1,7 @@ +export const messageTypes = [ + "alarm", + "event", + "informational", + "warning", + "unknown" +]; From c89225cce75a4944510a9d3e597e9741dbbfc38e Mon Sep 17 00:00:00 2001 From: matusmlichsk <61700762+matusmlichsk@users.noreply.github.com> Date: Wed, 7 Aug 2024 13:48:27 +0200 Subject: [PATCH 093/124] #2475 Command palette with better actions ux flow for query editor (#2517) --- .../components/ContentViews/QueryView.tsx | 323 ------------------ .../ContentViews/QueryView/QueryView.tsx | 122 +++++++ .../components/QueryOptions/QueryOptions.tsx | 228 +++++++++++++ .../TemplatePicker/TemplatePicker.tsx | 96 ++++++ .../QueryOptions/TemplatePicker/index.ts | 1 + .../components/QueryOptions/index.ts | 1 + .../ContentViews/QueryView/index.ts | 1 + .../components/QueryEditor.tsx | 92 +++-- .../components/StyledComponents/Chip/Chip.tsx | 3 +- Src/WitsmlExplorer.Frontend/styles/Icons.tsx | 9 +- 10 files changed, 518 insertions(+), 358 deletions(-) delete mode 100644 Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView.tsx create mode 100644 Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/QueryView.tsx create mode 100644 Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/QueryOptions.tsx create mode 100644 Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/TemplatePicker/TemplatePicker.tsx create mode 100644 Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/TemplatePicker/index.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/index.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/index.ts diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView.tsx deleted file mode 100644 index e8cb01615..000000000 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView.tsx +++ /dev/null @@ -1,323 +0,0 @@ -import { Menu, Tabs, TextField } from "@equinor/eds-core-react"; -import { - ReturnElements, - StoreFunction, - TemplateObjects, - formatXml, - getParserError, - getQueryTemplate -} from "components/ContentViews/QueryViewUtils"; -import ConfirmModal from "components/Modals/ConfirmModal"; -import { QueryEditor } from "components/QueryEditor"; -import { getTag } from "components/QueryEditorUtils"; -import { StyledNativeSelect } from "components/Select"; -import { Button } from "components/StyledComponents/Button"; -import { DispatchOperation } from "contexts/operationStateReducer"; -import OperationType from "contexts/operationType"; -import { QueryActionType, QueryContext } from "contexts/queryContext"; -import { useOperationState } from "hooks/useOperationState"; -import React, { ChangeEvent, useContext, useState } from "react"; -import QueryService from "services/queryService"; -import styled from "styled-components"; -import { Colors } from "styles/Colors"; -import Icon from "styles/Icons"; - -const QueryView = (): React.ReactElement => { - const { - operationState: { colors }, - dispatchOperation - } = useOperationState(); - const { - queryState: { queries, tabIndex }, - dispatchQuery - } = useContext(QueryContext); - const [isLoading, setIsLoading] = useState(false); - const [isTemplateMenuOpen, setIsTemplateMenuOpen] = useState(false); - const [menuAnchor, setMenuAnchor] = useState(null); - const { query, result, storeFunction, returnElements, optionsIn } = - queries[tabIndex]; - - const validateAndFormatQuery = (): boolean => { - const formattedQuery = formatXml(query); - const parserError = getParserError(formattedQuery); - if (parserError) { - dispatchQuery({ type: QueryActionType.SetResult, result: parserError }); - } else if (formattedQuery !== query) { - onQueryChange(formattedQuery); - } - return !parserError; - }; - - const sendQuery = () => { - const getResult = async (dispatchOperation?: DispatchOperation | null) => { - dispatchOperation?.({ type: OperationType.HideModal }); - setIsLoading(true); - const requestReturnElements = - storeFunction === StoreFunction.GetFromStore && - returnElements !== ReturnElements.None - ? returnElements - : undefined; - let response = await QueryService.postQuery( - query, - storeFunction, - requestReturnElements, - optionsIn?.trim() - ); - if (response.startsWith("<")) { - response = formatXml(response); - } - dispatchQuery({ type: QueryActionType.SetResult, result: response }); - setIsLoading(false); - }; - const isValid = validateAndFormatQuery(); - if (!isValid) return; - if (storeFunction === StoreFunction.DeleteFromStore) { - displayConfirmation( - () => getResult(dispatchOperation), - dispatchOperation - ); - } else { - getResult(); - } - }; - - const onQueryChange = (newValue: string) => { - dispatchQuery({ type: QueryActionType.SetQuery, query: newValue }); - }; - - const onFunctionChange = (event: ChangeEvent) => { - dispatchQuery({ - type: QueryActionType.SetStoreFunction, - storeFunction: event.target.value as StoreFunction - }); - }; - - const onReturnElementsChange = (event: ChangeEvent) => { - dispatchQuery({ - type: QueryActionType.SetReturnElements, - returnElements: event.target.value as ReturnElements - }); - }; - - const onOptionsInChange = (event: ChangeEvent) => { - dispatchQuery({ - type: QueryActionType.SetOptionsIn, - optionsIn: event.target.value - }); - }; - - const onTemplateSelect = (templateObject: TemplateObjects) => { - const template = getQueryTemplate(templateObject, returnElements); - if (template != undefined) { - dispatchQuery({ type: QueryActionType.SetQuery, query: template }); - } - setIsTemplateMenuOpen(false); - }; - - const onTabChange = (index: number) => { - if (index >= queries.length) { - dispatchQuery({ type: QueryActionType.AddTab }); - } else { - dispatchQuery({ type: QueryActionType.SetTabIndex, tabIndex: index }); - } - }; - - const onCloseTab = (event: React.MouseEvent, tabId: string) => { - event.stopPropagation(); - dispatchQuery({ type: QueryActionType.RemoveTab, tabId }); - }; - - const getTabName = (query: string) => { - return ( - getTag(query.split("\n")?.[0]) ?? (query.split("\n")?.[0] || "Empty") - ); - }; - - return ( - - - - {queries.map((query) => ( - - {getTabName(query.query)} - onCloseTab(event, query.tabId)} - /> - - ))} - - - - - -
-
- -
- - {Object.values(StoreFunction).map((value) => { - return ( - - ); - })} - - - {Object.values(ReturnElements).map((value) => { - return ( - - ); - })} - - - - setIsTemplateMenuOpen(false)} - anchorEl={menuAnchor} - colors={colors} - > - {Object.values(TemplateObjects).map((value) => { - return ( - onTemplateSelect(value)} - > - {value} - - ); - })} - - -
-
-
- -
-
-
- ); -}; - -const displayConfirmation = ( - onConfirm: () => void, - dispatchOperation: DispatchOperation -) => { - const confirmation = ( - Are you sure you want to delete this object?} - onConfirm={onConfirm} - confirmColor={"danger"} - confirmText={"Delete"} - switchButtonPlaces={true} - /> - ); - dispatchOperation({ - type: OperationType.DisplayModal, - payload: confirmation - }); -}; - -const StyledMenu = styled(Menu)<{ colors: Colors }>` - background: ${(props) => props.colors.ui.backgroundLight}; - padding: 0.25rem 0.5rem 0.25rem 0.5rem; - max-height: 80vh; - overflow-y: scroll; -`; - -const StyledMenuItem = styled(Menu.Item)<{ colors: Colors }>` - &&:hover { - background-color: ${(props) => - props.colors.interactive.contextMenuItemHover}; - } - color: ${(props) => props.colors.text.staticIconsDefault}; - padding: 4px; -`; - -const StyledTextField = styled(TextField)<{ colors: Colors }>` - label { - color: ${(props) => props.colors.text.staticIconsDefault}; - } - div { - background: ${(props) => props.colors.text.staticTextFieldDefault}; - } -`; - -const Layout = styled.div` - display: grid; - grid-template-rows: auto 1fr; - height: 100%; -`; - -const StyledClearIcon = styled(Icon)` - margin-left: 8px; - border-radius: 50%; - &:hover { - background-color: rgba(0, 0, 0, 0.1); - } -`; - -const StyledTab = styled(Tabs.Tab)<{ colors: Colors }>` - color: ${(props) => props.colors.infographic.primaryMossGreen}; -`; - -export default QueryView; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/QueryView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/QueryView.tsx new file mode 100644 index 000000000..bbd327840 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/QueryView.tsx @@ -0,0 +1,122 @@ +import { Tabs } from "@equinor/eds-core-react"; +import { QueryEditor } from "components/QueryEditor"; +import { getTag } from "components/QueryEditorUtils"; +import { QueryActionType, QueryContext } from "contexts/queryContext"; +import { useOperationState } from "hooks/useOperationState"; +import React, { useContext } from "react"; +import styled from "styled-components"; +import { Colors } from "styles/Colors"; +import Icon from "styles/Icons"; + +import { Box } from "@mui/material"; +import QueryOptions from "./components/QueryOptions"; + +const QueryView = (): React.ReactElement => { + const { + operationState: { colors } + } = useOperationState(); + const { + queryState: { queries, tabIndex }, + dispatchQuery + } = useContext(QueryContext); + + const { query, result } = queries[tabIndex]; + + const onQueryChange = (newValue: string) => { + dispatchQuery({ type: QueryActionType.SetQuery, query: newValue }); + }; + + const onTabChange = (index: number) => { + if (index >= queries.length) { + dispatchQuery({ type: QueryActionType.AddTab }); + } else { + dispatchQuery({ type: QueryActionType.SetTabIndex, tabIndex: index }); + } + }; + + const onCloseTab = (event: React.MouseEvent, tabId: string) => { + event.stopPropagation(); + dispatchQuery({ type: QueryActionType.RemoveTab, tabId }); + }; + + const getTabName = (query: string) => { + return ( + getTag(query.split("\n")?.[0]) ?? (query.split("\n")?.[0] || "Empty") + ); + }; + + return ( + + + + {queries.map((query) => ( + + {getTabName(query.query)} + onCloseTab(event, query.tabId)} + /> + + ))} + + + + + +
+ + + + +
+ +
+
+
+ ); +}; + +const Layout = styled.div` + display: grid; + grid-template-rows: auto 1fr; + height: 100%; + padding: 1rem; +`; + +const StyledClearIcon = styled(Icon)` + margin-left: 8px; + border-radius: 50%; + + &:hover { + background-color: rgba(0, 0, 0, 0.1); + } +`; + +const StyledTab = styled(Tabs.Tab)<{ colors: Colors }>` + color: ${(props) => props.colors.infographic.primaryMossGreen}; +`; + +export default QueryView; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/QueryOptions.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/QueryOptions.tsx new file mode 100644 index 000000000..2b58bbe36 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/QueryOptions.tsx @@ -0,0 +1,228 @@ +import React, { ChangeEvent, FC, useContext, useState } from "react"; +import { Box, Stack } from "@mui/material"; +import { StyledNativeSelect } from "../../../../Select.tsx"; +import { + formatXml, + getParserError, + ReturnElements, + StoreFunction +} from "../../../QueryViewUtils.tsx"; +import TemplatePicker from "./TemplatePicker"; +import { + QueryActionType, + QueryContext +} from "../../../../../contexts/queryContext.tsx"; +import styled, { css } from "styled-components"; +import { TextField } from "@equinor/eds-core-react"; +import { Colors } from "../../../../../styles/Colors.tsx"; +import { useOperationState } from "../../../../../hooks/useOperationState.tsx"; +import { Button } from "../../../../StyledComponents/Button.tsx"; +import Icon from "../../../../../styles/Icons.tsx"; +import { DispatchOperation } from "../../../../../contexts/operationStateReducer.tsx"; +import OperationType from "../../../../../contexts/operationType.ts"; +import QueryService from "../../../../../services/queryService.ts"; +import ConfirmModal from "../../../../Modals/ConfirmModal.tsx"; + +type QueryOptionsProps = { + onQueryChange: (newValue: string) => void; +}; + +const QueryOptions: FC = ({ onQueryChange }) => { + const { + dispatchQuery, + queryState: { queries, tabIndex } + } = useContext(QueryContext); + const { + operationState: { colors }, + dispatchOperation + } = useOperationState(); + + const [isLoading, setIsLoading] = useState(false); + + const { storeFunction, returnElements, optionsIn, query } = queries[tabIndex]; + + const onFunctionChange = (event: ChangeEvent) => { + dispatchQuery({ + type: QueryActionType.SetStoreFunction, + storeFunction: event.target.value as StoreFunction + }); + }; + + const validateAndFormatQuery = (): boolean => { + const formattedQuery = formatXml(query); + const parserError = getParserError(formattedQuery); + if (parserError) { + dispatchQuery({ type: QueryActionType.SetResult, result: parserError }); + } else if (formattedQuery !== query) { + onQueryChange(formattedQuery); + } + return !parserError; + }; + + const sendQuery = () => { + const getResult = async (dispatchOperation?: DispatchOperation | null) => { + dispatchOperation?.({ type: OperationType.HideModal }); + setIsLoading(true); + const requestReturnElements = + storeFunction === StoreFunction.GetFromStore && + returnElements !== ReturnElements.None + ? returnElements + : undefined; + let response = await QueryService.postQuery( + query, + storeFunction, + requestReturnElements, + optionsIn?.trim() + ); + if (response.startsWith("<")) { + response = formatXml(response); + } + dispatchQuery({ type: QueryActionType.SetResult, result: response }); + setIsLoading(false); + }; + const isValid = validateAndFormatQuery(); + if (!isValid) return; + if (storeFunction === StoreFunction.DeleteFromStore) { + displayConfirmation( + () => getResult(dispatchOperation), + dispatchOperation + ); + } else { + getResult(); + } + }; + + const onReturnElementsChange = (event: ChangeEvent) => { + dispatchQuery({ + type: QueryActionType.SetReturnElements, + returnElements: event.target.value as ReturnElements + }); + }; + + const onOptionsInChange = (event: ChangeEvent) => { + dispatchQuery({ + type: QueryActionType.SetOptionsIn, + optionsIn: event.target.value + }); + }; + + return ( + + + + + {Object.values(StoreFunction).map((value) => { + return ( + + ); + })} + + + + + + + + + {storeFunction === StoreFunction.GetFromStore && ( + + + + More options + + + + {Object.values(ReturnElements).map((value) => { + return ( + + ); + })} + + + + + + )} + + ); +}; + +const displayConfirmation = ( + onConfirm: () => void, + dispatchOperation: DispatchOperation +) => { + const confirmation = ( + Are you sure you want to delete this object?} + onConfirm={onConfirm} + confirmColor={"danger"} + confirmText={"Delete"} + switchButtonPlaces={true} + /> + ); + dispatchOperation({ + type: OperationType.DisplayModal, + payload: confirmation + }); +}; + +const StyledTextField = styled(TextField)<{ colors: Colors }>` + label { + color: ${(props) => props.colors.text.staticIconsDefault}; + } + + div { + background: ${(props) => props.colors.text.staticTextFieldDefault}; + } +`; + +const StyledSummary = styled(Box)<{ colors: Colors }>` + ${({ colors }) => css` + color: ${colors.interactive.primaryResting}; + `} +`; + +export default QueryOptions; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/TemplatePicker/TemplatePicker.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/TemplatePicker/TemplatePicker.tsx new file mode 100644 index 000000000..2ed864bc2 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/TemplatePicker/TemplatePicker.tsx @@ -0,0 +1,96 @@ +import React, { FC, useState } from "react"; +import { Button } from "../../../../../StyledComponents/Button.tsx"; +import Icon from "../../../../../../styles/Icons.tsx"; +import { + getQueryTemplate, + ReturnElements, + TemplateObjects +} from "../../../../QueryViewUtils.tsx"; +import { + DispatchQuery, + QueryActionType +} from "../../../../../../contexts/queryContext.tsx"; +import { useOperationState } from "../../../../../../hooks/useOperationState.tsx"; +import styled from "styled-components"; +import { Menu } from "@equinor/eds-core-react"; +import { Colors } from "../../../../../../styles/Colors.tsx"; + +type TemplatePickerProps = { + dispatchQuery: DispatchQuery; + returnElements: ReturnElements; +}; + +const TemplatePicker: FC = ({ + dispatchQuery, + returnElements +}) => { + const { + operationState: { colors } + } = useOperationState(); + + const [isTemplateMenuOpen, setIsTemplateMenuOpen] = useState(false); + const [menuAnchor, setMenuAnchor] = useState(null); + + const onTemplateSelect = (templateObject: TemplateObjects) => { + const template = getQueryTemplate(templateObject, returnElements); + if (template != undefined) { + dispatchQuery({ type: QueryActionType.SetQuery, query: template }); + } + setIsTemplateMenuOpen(false); + }; + + return ( + <> + + setIsTemplateMenuOpen(false)} + anchorEl={menuAnchor} + colors={colors} + > + {Object.values(TemplateObjects).map((value) => { + return ( + onTemplateSelect(value)} + > + {value} + + ); + })} + + + ); +}; + +export default TemplatePicker; + +const StyledMenu = styled(Menu)<{ colors: Colors }>` + background: ${(props) => props.colors.ui.backgroundLight}; + max-height: 80vh; + overflow-y: scroll; +`; + +const StyledMenuItem = styled(Menu.Item)<{ colors: Colors }>` + &&:hover { + background-color: ${(props) => + props.colors.interactive.contextMenuItemHover}; + } + + color: ${(props) => props.colors.text.staticIconsDefault}; + padding: 4px; +`; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/TemplatePicker/index.ts b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/TemplatePicker/index.ts new file mode 100644 index 000000000..ac3bad6fb --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/TemplatePicker/index.ts @@ -0,0 +1 @@ +export { default } from "./TemplatePicker.tsx"; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/index.ts b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/index.ts new file mode 100644 index 000000000..e6aa3c075 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/index.ts @@ -0,0 +1 @@ +export { default } from "./QueryOptions"; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/index.ts b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/index.ts new file mode 100644 index 000000000..329b5b066 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/index.ts @@ -0,0 +1 @@ +export { default } from "./QueryView.tsx"; diff --git a/Src/WitsmlExplorer.Frontend/components/QueryEditor.tsx b/Src/WitsmlExplorer.Frontend/components/QueryEditor.tsx index bb8566106..9812f7b02 100644 --- a/Src/WitsmlExplorer.Frontend/components/QueryEditor.tsx +++ b/Src/WitsmlExplorer.Frontend/components/QueryEditor.tsx @@ -12,56 +12,84 @@ import AceEditor from "react-ace"; import { useNavigate, useParams } from "react-router-dom"; import styled from "styled-components"; import { Colors, dark } from "styles/Colors"; +import React, { FC, useState } from "react"; +import { Chip } from "./StyledComponents/Chip"; +import Icon from "../styles/Icons.tsx"; +import { Stack } from "@mui/material"; export interface QueryEditorProps { value: string; onChange?: (newValue: string) => void; + showCommandPaletteOption?: boolean; readonly?: boolean; } -export const QueryEditor = (props: QueryEditorProps) => { - const { value, onChange, readonly } = props; +export const QueryEditor: FC = ({ + value, + onChange, + readonly, + showCommandPaletteOption +}) => { + const [ace, setAce] = useState(null); + const navigate = useNavigate(); const { serverUrl } = useParams(); const { operationState: { colors } } = useOperationState(); - const onLoad = (editor: any) => { + const onLoadInternal = (editor: any) => { editor.renderer.setPadding(10); editor.renderer.setScrollMargin(10); - if (readonly) { - editor.renderer.$cursorLayer.element.style.display = "none"; - } else { - editor.completers = [customCompleter]; - } - editor.renderer.on("afterRender", (_: any, renderer: any) => - updateLinesWithWidgets(editor, renderer, navigate, serverUrl, readonly) - ); + + if (readonly) editor.renderer.$cursorLayer.element.style.display = "none"; + else editor.completers = [customCompleter]; + + editor.renderer.on("afterRender", (_: any, renderer: any) => { + updateLinesWithWidgets(editor, renderer, navigate, serverUrl, readonly); + if (showCommandPaletteOption) setAce(editor); + }); }; + const canSeeCommandPalette = showCommandPaletteOption && ace && !readonly; + return ( - + <> + + {canSeeCommandPalette && ( + + { + ace.execCommand("openCommandPalette"); + }} + title="Show command palette [F1]" + > + + Command Palette + + + )} + ); }; diff --git a/Src/WitsmlExplorer.Frontend/components/StyledComponents/Chip/Chip.tsx b/Src/WitsmlExplorer.Frontend/components/StyledComponents/Chip/Chip.tsx index cb32ddbe4..1d2bf8b1e 100644 --- a/Src/WitsmlExplorer.Frontend/components/StyledComponents/Chip/Chip.tsx +++ b/Src/WitsmlExplorer.Frontend/components/StyledComponents/Chip/Chip.tsx @@ -26,13 +26,14 @@ export const Chip = forwardRef( Chip.displayName = "WitsmlExplorerChip"; const WitsmlDefaultChip = styled(EquinorChip)` - ${({ colors: { ui, mode, interactive } }) => { + ${({ colors: { ui, mode, interactive, text } }) => { if (mode === "light") return; return css` --eds_ui_background__light: ${ui.backgroundLight}; --eds_interactive_primary__resting: ${interactive.primaryResting}; --eds_interactive_primary__hover_alt: ${ui.backgroundDefault}; + --eds_interactive_primary__hover: ${text.staticPropertyValue}; `; }} `; diff --git a/Src/WitsmlExplorer.Frontend/styles/Icons.tsx b/Src/WitsmlExplorer.Frontend/styles/Icons.tsx index 9e3e1ada7..62508c79c 100644 --- a/Src/WitsmlExplorer.Frontend/styles/Icons.tsx +++ b/Src/WitsmlExplorer.Frontend/styles/Icons.tsx @@ -2,7 +2,6 @@ import { Icon } from "@equinor/eds-core-react"; import { accessible, account_circle as accountCircle, - filter_alt_active as activeFilter, add, arrow_down as arrowDown, arrow_drop_right as arrowDropRight, @@ -32,29 +31,33 @@ import { favorite_filled as favoriteFilled, favorite_outlined as favoriteOutlined, filter_alt as filter, + filter_alt_active as activeFilter, folder_open as folderOpen, format_line_spacing as formatLine, go_to as goTo, in_progress as inProgress, info_circle as infoCircle, - trending_up as isActive, keyboard, launch, more_vertical as moreVertical, new_alert as newAlert, paste, person, + play, refresh, save, search, settings, + style, sync, text_field as textField, + trending_up as isActive, tune, update, upload, world } from "@equinor/eds-icons"; + const icons = { accessible, accountCircle, @@ -100,10 +103,12 @@ const icons = { newAlert, paste, person, + play, refresh, save, search, settings, + style, sync, textField, tune, From ea895cb7cf3f50b5ed3e1216ffb0e3af436ab007 Mon Sep 17 00:00:00 2001 From: matusmlichsk <61700762+matusmlichsk@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:29:52 +0200 Subject: [PATCH 094/124] fix: support for using Button.Group in custom Button component (#2518) --- .../TrimLogObject/AdjustDateTimeModal.tsx | 2 +- .../TrimLogObject/AdjustNumberRangeModal.tsx | 3 ++- .../components/StyledComponents/Button.tsx | 21 ++++++++++++++++--- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/TrimLogObject/AdjustDateTimeModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/TrimLogObject/AdjustDateTimeModal.tsx index 76210c97e..7603455da 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/TrimLogObject/AdjustDateTimeModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/TrimLogObject/AdjustDateTimeModal.tsx @@ -1,8 +1,8 @@ -import { Button } from "@equinor/eds-core-react"; import { LogHeaderDateTimeField } from "components/Modals/LogHeaderDateTimeField"; import { addMilliseconds } from "date-fns"; import { toDate } from "date-fns-tz"; import React, { useEffect, useState } from "react"; +import { Button } from "../../StyledComponents/Button.tsx"; export interface AdjustDateTimeModelProps { minDate: string; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/TrimLogObject/AdjustNumberRangeModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/TrimLogObject/AdjustNumberRangeModal.tsx index 0e386c77e..9b5f45fd6 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/TrimLogObject/AdjustNumberRangeModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/TrimLogObject/AdjustNumberRangeModal.tsx @@ -1,5 +1,6 @@ -import { Button, TextField } from "@equinor/eds-core-react"; +import { TextField } from "@equinor/eds-core-react"; import React, { useEffect, useState } from "react"; +import { Button } from "../../StyledComponents/Button.tsx"; export interface AdjustNumberRangeModalProps { minValue: number; diff --git a/Src/WitsmlExplorer.Frontend/components/StyledComponents/Button.tsx b/Src/WitsmlExplorer.Frontend/components/StyledComponents/Button.tsx index 23aa0d484..04c27bc7a 100644 --- a/Src/WitsmlExplorer.Frontend/components/StyledComponents/Button.tsx +++ b/Src/WitsmlExplorer.Frontend/components/StyledComponents/Button.tsx @@ -4,7 +4,11 @@ import { } from "@equinor/eds-core-react"; import { UserTheme } from "contexts/operationStateReducer"; import { useOperationState } from "hooks/useOperationState"; -import React from "react"; +import React, { + forwardRef, + ForwardRefExoticComponent, + RefAttributes +} from "react"; import styled, { css } from "styled-components"; import { Colors } from "styles/Colors"; @@ -14,7 +18,13 @@ export interface ButtonProps extends Omit { export type Ref = HTMLButtonElement; -export const Button = React.forwardRef((props, ref) => { +type ComposedExoticButton = ForwardRefExoticComponent< + ButtonProps & RefAttributes +> & { + Group: typeof EdsButton.Group; +}; + +const ExoticButton = forwardRef((props, ref) => { const { operationState: { colors, theme } } = useOperationState(); @@ -46,7 +56,11 @@ export const Button = React.forwardRef((props, ref) => { } }); -Button.displayName = "WitsmlExplorerButton"; +ExoticButton.displayName = "WitsmlExplorerButton"; + +export const Button: ComposedExoticButton = Object.assign(ExoticButton, { + Group: EdsButton.Group +}); const ContainedButton = styled(EdsButton)<{ colors: Colors }>` ${(props) => @@ -84,6 +98,7 @@ const TableIconButton = styled(EdsButton)<{ css` height: 22px; width: 22px; + &::after { width: 22px; height: 22px; From ba756de5bbc520d86cc3db710878a7a06e4db749 Mon Sep 17 00:00:00 2001 From: matusmlichsk <61700762+matusmlichsk@users.noreply.github.com> Date: Mon, 19 Aug 2024 15:24:18 +0200 Subject: [PATCH 095/124] fix: expanded well overflow after filtering fixed to fit layout around (#2522) Updated @tanstack/react-virtual library --- .../ContentViews/table/contentTableUtils.ts | 3 +- .../components/Sidebar/Sidebar.tsx | 134 +++++++----------- .../SidebarVirtualItem/SidebarVirtualItem.tsx | 72 ++++++++++ .../Sidebar/SidebarVirtualItem/index.ts | 1 + Src/WitsmlExplorer.Frontend/package.json | 2 +- yarn.lock | 18 +-- 6 files changed, 135 insertions(+), 95 deletions(-) create mode 100644 Src/WitsmlExplorer.Frontend/components/Sidebar/SidebarVirtualItem/SidebarVirtualItem.tsx create mode 100644 Src/WitsmlExplorer.Frontend/components/Sidebar/SidebarVirtualItem/index.ts diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/contentTableUtils.ts b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/contentTableUtils.ts index 3661992c4..4f636c3fb 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/contentTableUtils.ts +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/contentTableUtils.ts @@ -27,6 +27,7 @@ export const constantTableOptions = { }; const sortingIconSize = 16; + export function calculateColumnWidth( label: string, isCompactMode: boolean, @@ -127,7 +128,7 @@ export const useInitFilterFns = (table: Table) => { }; export const calculateHorizontalSpace = ( - columnItems: VirtualItem[], + columnItems: VirtualItem[], totalSize: number, stickyLeftColumns: number ) => { diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/Sidebar.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/Sidebar.tsx index fe64e8e4c..a183e78ea 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/Sidebar.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/Sidebar.tsx @@ -1,13 +1,8 @@ -import { Divider, Typography } from "@equinor/eds-core-react"; +import { Typography } from "@equinor/eds-core-react"; import { TreeView } from "@mui/x-tree-view"; -import { - useVirtualizer, - VirtualItem, - Virtualizer -} from "@tanstack/react-virtual"; +import { useVirtualizer, Virtualizer } from "@tanstack/react-virtual"; import ProgressSpinner from "components/ProgressSpinner"; import SearchFilter from "components/Sidebar/SearchFilter"; -import WellItem from "components/Sidebar/WellItem"; import { useConnectedServer } from "contexts/connectedServerContext"; import { UserTheme } from "contexts/operationStateReducer"; import { useSidebar } from "contexts/sidebarContext"; @@ -15,16 +10,16 @@ import { SidebarActionType } from "contexts/sidebarReducer"; import { useGetWells } from "hooks/query/useGetWells"; import { useOperationState } from "hooks/useOperationState"; import { useWellFilter } from "hooks/useWellFilter"; -import Well from "models/well"; -import { Fragment, SyntheticEvent, useEffect, useRef } from "react"; +import { FC, SyntheticEvent, useEffect, useRef } from "react"; import { useParams } from "react-router-dom"; import styled from "styled-components"; import Icon from "styles/Icons"; -import { WellIndicator } from "../StyledComponents/WellIndicator"; import { InactiveWellsHiddenFilterHelper } from "./InactiveWellsHiddenFilterHelper"; import { Stack } from "@mui/material"; +import SidebarVirtualItem from "./SidebarVirtualItem"; +import { calculateWellNodeId } from "../../models/wellbore.tsx"; -export default function Sidebar() { +const Sidebar: FC = () => { const { connectedServer } = useConnectedServer(); const { wells, isFetching } = useGetWells(connectedServer); const { expandedTreeNodes, dispatchSidebar } = useSidebar(); @@ -34,7 +29,7 @@ export default function Sidebar() { operationState: { colors, theme } } = useOperationState(); const isCompactMode = theme === UserTheme.Compact; - const filteredWells = useWellFilter(wells); + const filteredWells = useWellFilter(wells) || []; const containerRef = useRef(null); const virtualizer = useVirtualizer({ getScrollElement: () => containerRef.current, @@ -79,75 +74,57 @@ export default function Sidebar() { ); return ( - + <> {!!connectedServer && ( - {filteredWells && - (filteredWells.length === 0 ? ( - - No wells match the current filter - - - ) : ( - + No wells match the current filter + + + ) : ( + + } + defaultExpandIcon={ + + } + defaultEndIcon={
} + expanded={expandedTreeNodes} + onNodeToggle={onNodeToggle} + virtualizer={virtualizer} + > + {virtualizer.getVirtualItems().map((virtualItem) => { + const well = filteredWells[virtualItem.index]; + return ( + - } - defaultExpandIcon={ - - } - defaultEndIcon={
} - expanded={expandedTreeNodes} - onNodeToggle={onNodeToggle} - virtualizer={virtualizer} - > - {virtualizer.getVirtualItems().map((virtualItem) => { - const well: Well = filteredWells[virtualItem.index]; - return ( - virtualizer.measureElement(node)} - virtualItem={virtualItem} - > - - - - - - - ); - })} - - ))} + ); + })} + + )} )} - + ); -} +}; -const WellListing = styled.div` - display: grid; - grid-template-columns: 1fr 18px; - justify-content: center; - align-content: stretch; -`; +export default Sidebar; const SidebarTreeView = styled.div` overflow-y: scroll; @@ -182,14 +159,3 @@ const StyledVirtualTreeView = styled(TreeView)<{ width: 100%; height: ${(props) => props.virtualizer.getTotalSize()}px; `; - -const StyledVirtualItem = styled.div.attrs<{ virtualItem: VirtualItem }>( - (props) => ({ - style: { - transform: `translateY(${props.virtualItem.start}px)` - } - }) -)<{ virtualItem: VirtualItem }>` - position: absolute; - width: 100%; -`; diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/SidebarVirtualItem/SidebarVirtualItem.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/SidebarVirtualItem/SidebarVirtualItem.tsx new file mode 100644 index 000000000..93f1a5925 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/SidebarVirtualItem/SidebarVirtualItem.tsx @@ -0,0 +1,72 @@ +import { FC, useLayoutEffect, useRef } from "react"; +import { VirtualItem, Virtualizer } from "@tanstack/react-virtual"; +import Well from "../../../models/well.tsx"; +import { useOperationState } from "../../../hooks/useOperationState.tsx"; +import { UserTheme } from "../../../contexts/operationStateReducer.tsx"; +import WellItem from "../WellItem.tsx"; +import { WellIndicator } from "../../StyledComponents/WellIndicator.tsx"; +import { Divider } from "@equinor/eds-core-react"; +import styled from "styled-components"; + +const SidebarVirtualItem: FC<{ + virtualItem: VirtualItem; + well: Well; + virtualizer: Virtualizer; + isExpanded: boolean; +}> = ({ virtualItem, well, virtualizer, isExpanded }) => { + const { + operationState: { colors, theme } + } = useOperationState(); + const rowRef = useRef(); + const isCompactMode = theme === UserTheme.Compact; + + useLayoutEffect(() => { + if (rowRef.current) virtualizer.measureElement(rowRef.current); + }, [isExpanded]); + + return ( + + + + + + + + ); +}; + +const WellListing = styled.div` + display: grid; + grid-template-columns: 1fr 18px; + justify-content: center; + align-content: stretch; +`; + +const StyledVirtualItem = styled.div.attrs<{ + virtualItem: VirtualItem; +}>((props) => ({ + style: { + transform: `translateY(${props.virtualItem.start}px)` + } +}))<{ virtualItem: VirtualItem }>` + position: absolute; + top: 0; + left: 0; + width: 100%; +`; + +export default SidebarVirtualItem; diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/SidebarVirtualItem/index.ts b/Src/WitsmlExplorer.Frontend/components/Sidebar/SidebarVirtualItem/index.ts new file mode 100644 index 000000000..baf48e363 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/SidebarVirtualItem/index.ts @@ -0,0 +1 @@ +export { default } from "./SidebarVirtualItem.tsx"; diff --git a/Src/WitsmlExplorer.Frontend/package.json b/Src/WitsmlExplorer.Frontend/package.json index b4a33a08d..1faa9a256 100644 --- a/Src/WitsmlExplorer.Frontend/package.json +++ b/Src/WitsmlExplorer.Frontend/package.json @@ -36,7 +36,7 @@ "@mui/x-tree-view": "^6.17.0", "@tanstack/react-query": "^5.18.0", "@tanstack/react-table": "^8.9.2", - "@tanstack/react-virtual": "^3.0.0-beta.54", + "@tanstack/react-virtual": "^3.9.0", "ace-builds": "^1.29.0", "buffer": "^6.0.3", "date-fns": "^2.29.3", diff --git a/yarn.lock b/yarn.lock index 4aec83ee5..4db3aba0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1197,12 +1197,12 @@ dependencies: "@tanstack/virtual-core" "3.0.0-beta.54" -"@tanstack/react-virtual@^3.0.0-beta.54": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.4.0.tgz#5dcc0ac7c9e35d5db12c3bbe4cbc075bad684d93" - integrity sha512-GZN4xn/Tg5w7gvYeVcMVCeL4pEyUhvg+Cp6KX2Z01C4FRNxIWMgIQ9ibgMarNQfo+gt0PVLcEER4A9sNv/jlow== +"@tanstack/react-virtual@^3.9.0": + version "3.9.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.9.0.tgz#728e3a1917cb98fb67a17f4190e75f531f1eb0de" + integrity sha512-5TeTSQBMV1PIFzBP9cduIX5klRaTvbOw+CxRx3LaUhwqiZLEZBZqz8anEIqG4eHNhDAe+BLarRDeNE9cNM1/EA== dependencies: - "@tanstack/virtual-core" "3.4.0" + "@tanstack/virtual-core" "3.9.0" "@tanstack/table-core@8.16.0": version "8.16.0" @@ -1214,10 +1214,10 @@ resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.0.0-beta.54.tgz#12259d007911ad9fce1388385c54a9141f4ecdc4" integrity sha512-jtkwqdP2rY2iCCDVAFuaNBH3fiEi29aTn2RhtIoky8DTTiCdc48plpHHreLwmv1PICJ4AJUUESaq3xa8fZH8+g== -"@tanstack/virtual-core@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.4.0.tgz#afd72bc5a839b71c2cda87a738eb4eb18451b80a" - integrity sha512-75jXqXxqq5M5Veb9KP1STi8kA5u408uOOAefk2ftHDGCpUk3RP6zX++QqfbmHJTBiU72NQ+ghgCZVts/Wocz8Q== +"@tanstack/virtual-core@3.9.0": + version "3.9.0" + resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.9.0.tgz#60db41fe8a19bb1a21873d86d8731416391e838a" + integrity sha512-Saga7/QRGej/IDCVP5BgJ1oDqlDT2d9rQyoflS3fgMS8ntJ8JGw/LBqK2GorHa06+VrNFc0tGz65XQHJQJetFQ== "@testing-library/dom@^10.0.0": version "10.1.0" From f930bbc525025b4cd79d1387a816f5580c1ea521 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Wed, 21 Aug 2024 16:57:43 +0200 Subject: [PATCH 096/124] Add WEx demo videos to Readme's (#2527) --- Media/README.md | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 5 ++- we-demo.png | Bin 422007 -> 0 bytes 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 Media/README.md delete mode 100644 we-demo.png diff --git a/Media/README.md b/Media/README.md new file mode 100644 index 000000000..802d0bd95 --- /dev/null +++ b/Media/README.md @@ -0,0 +1,82 @@ +# Witsml Explorer Demo & Tutorial Videos + +- [Overview](#overview) +- [Manage Servers](#manage-servers) +- [Desktop Installation](#desktop-installation) +- [Azure Integration](#azure-integration) +- [Settings](#settings) +- [Search and Filter](#search-and-filter) +- [Table Overview](#table-overview) +- [Query View](#query-view) +- [Deeplinking](#deeplinking) +- [Object Overview](#object-overview) +- [Log Overview](#log-overview) + - [Log Agents](#log-agents) + - [Export and Import](#export-and-import) + - [Splice](#splice) + - [Curve Offset](#curve-offset) +- [Copy and Paste](#copy-and-paste) + - [Copy Mnemonics](#copy-mnemonics) + - [Copy Other Objects](#copy-other-objects) + - [Copy With Range](#copy-with-range) + - [Copy Within Server](#copy-within-server) + +## Overview +https://github.com/user-attachments/assets/76b98f20-5ded-4ed8-9c21-a34625fcd355 + +## Manage Servers +https://github.com/user-attachments/assets/4e8bfb7c-b2c1-4989-a3dd-ef70357abd7f + +## Desktop Installation +https://github.com/user-attachments/assets/527260ca-76e7-4267-bbc9-5e35c2b0c257 + +## Azure Integration +https://github.com/user-attachments/assets/55b5e5b8-6603-4156-8d4c-f8dfdfde4d5e + +## Settings +https://github.com/user-attachments/assets/33947d09-577a-45b2-887b-01e04eebc17b + +## Search and Filter +https://github.com/user-attachments/assets/bbeff67f-bef9-4919-8efc-86d4ce3070f5 + +## Table Overview +https://github.com/user-attachments/assets/4055d50d-9996-4fee-83ae-1aeceb939274 + +## Query View +https://github.com/user-attachments/assets/3b3608ff-9587-4c34-90f2-432661350269 + +## Deeplinking +https://github.com/user-attachments/assets/b4ceb129-dd6e-4a3f-8878-15b1a6abade6 + +## Object Overview +https://github.com/user-attachments/assets/e86798ad-c8f9-4213-8f4f-b0e11bf30c57 + +## Log Overview +https://github.com/user-attachments/assets/28f4417c-45ed-4c16-acaf-99d2a103ddf6 + +### Log Agents +https://github.com/user-attachments/assets/6848c50d-3995-4bf7-a875-9a9be512413a + +### Export and Import +https://github.com/user-attachments/assets/e0a64ec7-4254-4976-902f-9088dc5d61b5 + +### Splice +https://github.com/user-attachments/assets/bd340c2f-56c1-4d37-a017-878a17f047ce + +### Curve Offset +https://github.com/user-attachments/assets/9a403460-8178-4b75-9eaf-72302f3a2b71 + +## Copy and Paste +https://github.com/user-attachments/assets/ab54c690-3fb0-45e5-a467-ce884935c65e + +### Copy Mnemonics +https://github.com/user-attachments/assets/3f7b9154-001c-49a7-8244-c22e1bb3e278 + +### Copy Other Objects +https://github.com/user-attachments/assets/32cedf8c-8811-436e-949a-9fc63f93ded2 + +### Copy With Range +https://github.com/user-attachments/assets/3dce1b20-8504-4df6-8e28-13761c17380e + +### Copy Within Server +https://github.com/user-attachments/assets/5aa1332c-3524-45dd-babe-18ae82078eba diff --git a/README.md b/README.md index 7c8ca8692..000664533 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,10 @@ Witsml Explorer is a data management tool used for browsing and editing data directly on [WITSML](https://en.wikipedia.org/wiki/Wellsite_information_transfer_standard_markup_language) servers. -witsml-explorer interface +https://github.com/user-attachments/assets/b1a8fdde-a129-4656-87dd-cd10b7a46fb0 + +## Demo Videos +Please see [Demo Videos](/Media/README.md) ## Key features * Runs directly in your browser, no need to install additional software. diff --git a/we-demo.png b/we-demo.png deleted file mode 100644 index 60b70f1d3e3853a75905113ee16f3cf4dad37a09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 422007 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

ni(2ao9~c*$Zkonai%1^J*5u+FhdeMo&G^#tcAj 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�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 From f6d3133f0adfdb2b358131b5834316f52e521cee Mon Sep 17 00:00:00 2001 From: Robert Basti Date: Fri, 23 Aug 2024 14:03:32 +0200 Subject: [PATCH 097/124] Api.Workers tests #1907 (#2523) --- .../Workers/Delete/DeleteObjectsWorker.cs | 2 +- .../Workers/DeleteWellWorkerTests.cs | 76 +++++++ .../Workers/DeleteWellboreWorkerTests.cs | 79 +++++++ .../Workers/LogWorkerToolsTests.cs | 172 +++++++++++++++ .../ModifyTrajectoryStationWorkerTest.cs | 109 ++++++++++ .../Workers/ModifyTrajectoryWorkerTests.cs | 4 +- .../ModifyWbGeometrySectionWorkerTest.cs | 150 +++++++++++++ .../Workers/ReplaceComponentsWorkerTests.cs | 197 +++++++++++++++++ .../Workers/ReplaceObjectsWorkerTests.cs | 201 ++++++++++++++++++ .../Workers/WorkerToolsTests.cs | 80 +++++++ 10 files changed, 1066 insertions(+), 4 deletions(-) create mode 100644 Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellWorkerTests.cs create mode 100644 Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellboreWorkerTests.cs create mode 100644 Tests/WitsmlExplorer.Api.Tests/Workers/LogWorkerToolsTests.cs create mode 100644 Tests/WitsmlExplorer.Api.Tests/Workers/ModifyTrajectoryStationWorkerTest.cs create mode 100644 Tests/WitsmlExplorer.Api.Tests/Workers/ModifyWbGeometrySectionWorkerTest.cs create mode 100644 Tests/WitsmlExplorer.Api.Tests/Workers/ReplaceComponentsWorkerTests.cs create mode 100644 Tests/WitsmlExplorer.Api.Tests/Workers/ReplaceObjectsWorkerTests.cs create mode 100644 Tests/WitsmlExplorer.Api.Tests/Workers/WorkerToolsTests.cs diff --git a/Src/WitsmlExplorer.Api/Workers/Delete/DeleteObjectsWorker.cs b/Src/WitsmlExplorer.Api/Workers/Delete/DeleteObjectsWorker.cs index 8c20eafc0..b33c56104 100644 --- a/Src/WitsmlExplorer.Api/Workers/Delete/DeleteObjectsWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/Delete/DeleteObjectsWorker.cs @@ -86,7 +86,7 @@ await Task.WhenAll(queries.Select(async (query) => string successString = successUids.Count > 0 ? $"Deleted {witsmlObjectOnWellbore?.GetType().Name}s: {string.Join(", ", successUids)}." : ""; return !error ? (new WorkerResult(witsmlClient.GetServerHostname(), true, successString), refreshAction) - : (new WorkerResult(witsmlClient.GetServerHostname(), false, $"{successString} Failed to delete some {witsmlObjectOnWellbore?.GetType().Name}s", errorReason, null), successUids.Count > 0 ? refreshAction : null); + : (new WorkerResult(witsmlClient.GetServerHostname(), false, $"{successString}Failed to delete some {witsmlObjectOnWellbore?.GetType().Name}s", errorReason, null), successUids.Count > 0 ? refreshAction : null); } } } diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellWorkerTests.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellWorkerTests.cs new file mode 100644 index 000000000..33fb96f21 --- /dev/null +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellWorkerTests.cs @@ -0,0 +1,76 @@ +using System.Linq; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Moq; + +using Serilog; + +using Witsml; +using Witsml.Data; + +using WitsmlExplorer.Api.Jobs; +using WitsmlExplorer.Api.Models; +using WitsmlExplorer.Api.Services; +using WitsmlExplorer.Api.Workers; +using WitsmlExplorer.Api.Workers.Delete; + +using Xunit; + +namespace WitsmlExplorer.Api.Tests.Workers +{ + public class DeleteWellWorkerTests + { + private readonly DeleteWellWorker _worker; + private readonly Mock _witsmlClient; + private const string WellUid = "wellUid"; + + public DeleteWellWorkerTests() + { + Mock witsmlClientProvider = new(); + _witsmlClient = new(); + witsmlClientProvider.Setup(provider => provider.GetClient()).Returns(_witsmlClient.Object); + ILoggerFactory loggerFactory = new LoggerFactory(); + loggerFactory.AddSerilog(Log.Logger); + ILogger logger = loggerFactory.CreateLogger(); + _worker = new DeleteWellWorker(logger, witsmlClientProvider.Object); + } + + private static DeleteWellJob CreateJob() + { + return new() + { + ToDelete = new() + { + WellUid = WellUid + } + }; + } + + [Fact] + public async Task Execute_DeleteWell_RefreshAction() + { + _witsmlClient.Setup(client => client.DeleteFromStoreAsync(It.IsAny())) + .ReturnsAsync(new QueryResult(true)); + + (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob()); + Assert.True(result.IsSuccess); + Assert.True(((RefreshWell)refreshAction).WellUid == WellUid); + } + + [Fact] + public async Task Execute_DeleteWell_ReturnResult() + { + WitsmlWells query = null; + _witsmlClient.Setup(client => client.DeleteFromStoreAsync(It.IsAny())) + .Callback((wells) => query = wells) + .ReturnsAsync(new QueryResult(true)); + + (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob()); + Assert.True(result.IsSuccess); + Assert.Single(query.Wells); + Assert.Equal(WellUid, query.Wells.First().Uid); + } + } +} diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellboreWorkerTests.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellboreWorkerTests.cs new file mode 100644 index 000000000..ad446bef7 --- /dev/null +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellboreWorkerTests.cs @@ -0,0 +1,79 @@ +using System.Linq; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Moq; + +using Serilog; + +using Witsml; +using Witsml.Data; + +using WitsmlExplorer.Api.Jobs; +using WitsmlExplorer.Api.Models; +using WitsmlExplorer.Api.Services; +using WitsmlExplorer.Api.Workers; +using WitsmlExplorer.Api.Workers.Delete; + +using Xunit; + +namespace WitsmlExplorer.Api.Tests.Workers +{ + public class DeleteWellboreWorkerTests + { + private readonly DeleteWellboreWorker _worker; + private readonly Mock _witsmlClient; + private const string WellboreUid = "wellboreUid"; + private const string WellUid = "wellUid"; + + public DeleteWellboreWorkerTests() + { + Mock witsmlClientProvider = new(); + _witsmlClient = new(); + witsmlClientProvider.Setup(provider => provider.GetClient()).Returns(_witsmlClient.Object); + ILoggerFactory loggerFactory = new LoggerFactory(); + loggerFactory.AddSerilog(Log.Logger); + ILogger logger = loggerFactory.CreateLogger(); + _worker = new DeleteWellboreWorker(logger, witsmlClientProvider.Object); + } + + private static DeleteWellboreJob CreateJob() + { + return new() + { + ToDelete = new() + { + WellboreUid = WellboreUid, + WellUid = WellUid + } + }; + } + + [Fact] + public async Task Execute_DeleteWellbore_RefreshAction() + { + _witsmlClient.Setup(client => client.DeleteFromStoreAsync(It.IsAny())) + .ReturnsAsync(new QueryResult(true)); + + (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob()); + Assert.True(result.IsSuccess); + Assert.True(((RefreshWellbore)refreshAction).WellboreUid == WellboreUid); + Assert.True(((RefreshWellbore)refreshAction).WellUid == WellUid); + } + + [Fact] + public async Task Execute_DeleteWellbore_ReturnResult() + { + WitsmlWellbores query = null; + _witsmlClient.Setup(client => client.DeleteFromStoreAsync(It.IsAny())) + .Callback((wellBores) => query = wellBores) + .ReturnsAsync(new QueryResult(true)); + + (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob()); + Assert.True(result.IsSuccess); + Assert.Single(query.Wellbores); + Assert.Equal(WellboreUid, query.Wellbores.First().Uid); + } + } +} diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/LogWorkerToolsTests.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/LogWorkerToolsTests.cs new file mode 100644 index 000000000..87643aa37 --- /dev/null +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/LogWorkerToolsTests.cs @@ -0,0 +1,172 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Moq; + +using Serilog; + +using Witsml; +using Witsml.Data; +using Witsml.ServiceReference; + +using WitsmlExplorer.Api.Jobs; +using WitsmlExplorer.Api.Models; +using WitsmlExplorer.Api.Services; +using WitsmlExplorer.Api.Workers; + +using Xunit; + +namespace WitsmlExplorer.Api.Tests.Workers +{ + public class LogWorkerToolsTests + { + private const string LogUid = "51bb71c2-5e6f-4e15-ae5f-0fbc866bdad6"; + private const string LogName = "Test log"; + private const string WellUid = "welluid"; + private const string WellName = "wellname"; + private const string WellboreUid = "wellboreuid"; + private const string WellboreName = "wellborename"; + private readonly Mock _witsmlClient; + + public LogWorkerToolsTests() + { + Mock witsmlClientProvider = new(); + _witsmlClient = new Mock(); + witsmlClientProvider.Setup(provider => provider.GetClient()).Returns(_witsmlClient.Object); + ILoggerFactory loggerFactory = new LoggerFactory(); + loggerFactory.AddSerilog(Log.Logger); + } + + [Fact] + public async Task GetLogTest_OK() + { + WitsmlLog expectedLog = CreateLog(); + CreateObjectOnWellboreJob job = CreateJobTemplate(WitsmlLog.WITSML_INDEX_TYPE_MD); + _witsmlClient.Setup(client => + client.GetFromStoreAsync(It.IsAny(), It.Is((ops) => ops.ReturnElements == ReturnElements.HeaderOnly))).ReturnsAsync(expectedLog.AsItemInWitsmlList()); + + var log = await LogWorkerTools.GetLog(_witsmlClient.Object, job.Object, ReturnElements.HeaderOnly); + Assert.Equal(expectedLog.Uid, log.Uid); + Assert.Equal(expectedLog.EndDateTimeIndex, log.EndDateTimeIndex); + Assert.Equal(expectedLog.StartDateTimeIndex, log.StartDateTimeIndex); + Assert.Equal(expectedLog.LogCurveInfo.Count, log.LogCurveInfo.Count); + } + + [Fact] + public async Task GetLogDataForCurveTest_OK() + { + WitsmlLog expectedLog = CreateLog(); + _witsmlClient.Setup(client => + client.GetFromStoreAsync(It.IsAny(), It.Is((ops) => ops.ReturnElements == ReturnElements.HeaderOnly))).ReturnsAsync(expectedLog.AsItemInWitsmlList); + WitsmlLog log = LogUtils.GetSourceLogs(WitsmlLog.WITSML_INDEX_TYPE_MD, 123.11, 123.12, "Depth").Logs.First(); + LogUtils.SetupGetDepthIndexed(_witsmlClient, (logs) => logs.Logs.First().StartIndex?.Value == "123.11", + new() { new() { Data = "123.11,1," }, new() { Data = "123.12,,2" } }); + + LogUtils.SetupGetDepthIndexed(_witsmlClient, (logs) => logs.Logs.First().StartIndex?.Value == "123.12", + new() { new() { Data = "123.12,,2" } }); + var sourceLog = await LogWorkerTools.GetLogDataForCurve(_witsmlClient.Object, log, "mnemonic", null); + Assert.Equal("123.11,1,", sourceLog.Data[0].Data); + Assert.Equal("123.12,,2", sourceLog.Data[1].Data); + Assert.Equal("data", sourceLog.Data[0].TypeName); + Assert.Equal("data", sourceLog.Data[1].TypeName); + Assert.Equal("Depth,DepthBit,DepthHole", sourceLog.MnemonicList); + Assert.Equal("data", sourceLog.TypeName); + Assert.Equal("m,m,m", sourceLog.UnitList); + } + + [Fact] + public void CalculateProgressBasedOnIndexTest_OK() + { + var log = CreateLog(); + string mnemonicList = "Depth,BPOS"; + var witmslLogData = GetTestLogData(mnemonicList); + var result = LogWorkerTools.CalculateProgressBasedOnIndex(log, witmslLogData); + Assert.Equal(0.5, result); + } + + [Fact] + public void GetUpdateLogDataQueriesTest_OK() + { + string mnemonicList = "Depth,BPOS"; + var witmslLogData = GetTestLogData(mnemonicList); + var batchedQuueries = LogWorkerTools.GetUpdateLogDataQueries("uid", + "uidwell", "uidwellbore", witmslLogData, 2, mnemonicList); + Assert.Equal(5, batchedQuueries.Count); + } + + private static WitsmlLog CreateLog() + { + return new WitsmlLog + { + UidWell = WellUid, + UidWellbore = WellboreUid, + Uid = LogUid, + StartDateTimeIndex = "2023-04-19T00:00:00Z", + EndDateTimeIndex = "2023-04-19T00:00:20Z", + LogCurveInfo = new List + { + new WitsmlLogCurveInfo + { + Mnemonic = "Time", + Unit = "date time" + }, + new WitsmlLogCurveInfo + { + Mnemonic = "mnemo1", + Unit = + CommonConstants.Unit + .Unitless + }, + new WitsmlLogCurveInfo + { + Mnemonic = "mnemo2", + Unit = CommonConstants.Unit + .Unitless + } + } + }; + } + + private static WitsmlLogData GetTestLogData(string mnemonicList) + { + var data = new List() + { + new() { Data = "2023-04-19T00:00:00Z,101" }, + new() { Data = "2023-04-19T00:00:01Z,102" }, + new() { Data = "2023-04-19T00:00:02Z,103" }, + new() { Data = "2023-04-19T00:00:03Z,104" }, + new() { Data = "2023-04-19T00:00:04Z,105" }, + new() { Data = "2023-04-19T00:00:05Z,106" }, + new() { Data = "2023-04-19T00:00:07Z,107" }, + new() { Data = "2023-04-19T00:00:08Z,108" }, + new() { Data = "2023-04-19T00:00:09Z,109" }, + new() { Data = "2023-04-19T00:00:10Z,110" }, + }; + + + + return new WitsmlLogData() { MnemonicList = mnemonicList, Data = data }; + } + private static CreateObjectOnWellboreJob CreateJobTemplate(string indexType) + { + return new CreateObjectOnWellboreJob + { + Object = new LogObject + { + Uid = LogUid, + Name = LogName, + WellUid = WellUid, + WellName = WellName, + WellboreUid = WellboreUid, + WellboreName = WellboreName, + IndexCurve = indexType == WitsmlLog.WITSML_INDEX_TYPE_MD ? "Depth" : "Time", + IndexType = indexType + }, + ObjectType = EntityType.Log + }; + } + } +} diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/ModifyTrajectoryStationWorkerTest.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/ModifyTrajectoryStationWorkerTest.cs new file mode 100644 index 000000000..064b6d75d --- /dev/null +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/ModifyTrajectoryStationWorkerTest.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Moq; + +using Serilog; + +using Witsml; +using Witsml.Data; + +using WitsmlExplorer.Api.Jobs; +using WitsmlExplorer.Api.Jobs.Common; +using WitsmlExplorer.Api.Models; +using WitsmlExplorer.Api.Models.Measure; +using WitsmlExplorer.Api.Services; +using WitsmlExplorer.Api.Workers.Modify; + +using Xunit; + +namespace WitsmlExplorer.Api.Tests.Workers +{ + public class ModifyTrajectoryStationWorkerTest + { + private readonly Mock _witsmlClient; + private readonly ModifyTrajectoryStationWorker _worker; + private const string WellUid = "wellUid"; + private const string WellboreUid = "wellboreUid"; + private const string TsUid = "ts_uid"; + private const string Uom = "uom"; + private const string GeometrySectionUid = "GeometrySectionUid"; + private const decimal Value = 20; + + public ModifyTrajectoryStationWorkerTest() + { + Mock witsmlClientProvider = new(); + _witsmlClient = new Mock(); + witsmlClientProvider.Setup(provider => provider.GetClient()).Returns(_witsmlClient.Object); + ILoggerFactory loggerFactory = new LoggerFactory(); + loggerFactory.AddSerilog(Log.Logger); + ILogger logger = loggerFactory.CreateLogger(); + _worker = new ModifyTrajectoryStationWorker(logger, witsmlClientProvider.Object); + } + + [Fact] + public async Task Update_TrajectoryStation_ResultOK() + { + ModifyTrajectoryStationJob job = CreateJobTemplate(); + List updatedTrajectories = new(); + _witsmlClient.Setup(client => + client.UpdateInStoreAsync(It.IsAny())).Callback(trajectories => updatedTrajectories.Add(trajectories)) + .ReturnsAsync(new QueryResult(true)); + + var (workerResult, _) = await _worker.Execute(job); + Assert.True(workerResult.IsSuccess); + Assert.Single(updatedTrajectories); + Assert.NotNull(updatedTrajectories); + var ts = updatedTrajectories.FirstOrDefault().Trajectories.FirstOrDefault().TrajectoryStations.FirstOrDefault(); + Assert.NotNull(ts); + Assert.Equal(Uom, ts.Md.Uom); + Assert.Equal(Value.ToString(), ts.Md.Value); + Assert.Equal(Uom, ts.Tvd.Uom); + Assert.Equal(Value.ToString(), ts.Tvd.Value); + Assert.Equal(Uom, ts.Incl.Uom); + Assert.Equal(Value.ToString(), ts.Incl.Value); + Assert.Equal(Uom, ts.Azi.Uom); + Assert.Equal(Value.ToString(), ts.Azi.Value); + } + + private static ModifyTrajectoryStationJob CreateJobTemplate() + { + return new ModifyTrajectoryStationJob + { + TrajectoryStation = new TrajectoryStation() + { + Uid = TsUid, + Md = new LengthMeasure() + { + Uom = Uom, + Value = Value + }, + Tvd = new LengthMeasure() + { + Uom = Uom, + Value = Value + }, + Incl = new LengthMeasure() + { + Uom = Uom, + Value = Value + }, + Azi = new LengthMeasure() + { + Uom = Uom, + Value = Value + } + }, + TrajectoryReference = new ObjectReference() + { + WellUid = WellUid, + WellboreUid = WellboreUid, + Uid = GeometrySectionUid + } + }; + } + } +} diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/ModifyTrajectoryWorkerTests.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/ModifyTrajectoryWorkerTests.cs index 86a0832a6..b1b686a39 100644 --- a/Tests/WitsmlExplorer.Api.Tests/Workers/ModifyTrajectoryWorkerTests.cs +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/ModifyTrajectoryWorkerTests.cs @@ -44,10 +44,8 @@ public async Task ModifyTrajectory_ValidResults() const string expectedNewName = "NewName"; ModifyObjectOnWellboreJob job = CreateJobTemplate(TrajectoryUid, expectedNewName); Trajectory trajectory = (Trajectory)job.Object; - List updatedTrajectories = new(); - _witsmlClient.Setup(client => - client.UpdateInStoreAsync(It.IsAny())).Callback(trajectories => updatedTrajectories.Add(trajectories as WitsmlTrajectories)) + _witsmlClient.Setup(client => client.UpdateInStoreAsync(It.IsAny())).Callback(trajectories => updatedTrajectories.Add(trajectories as WitsmlTrajectories)) .ReturnsAsync(new QueryResult(true)); await _worker.Execute(job); diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/ModifyWbGeometrySectionWorkerTest.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/ModifyWbGeometrySectionWorkerTest.cs new file mode 100644 index 000000000..293dbaf3c --- /dev/null +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/ModifyWbGeometrySectionWorkerTest.cs @@ -0,0 +1,150 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Moq; + +using Serilog; + +using Witsml; +using Witsml.Data; + +using WitsmlExplorer.Api.Jobs; +using WitsmlExplorer.Api.Jobs.Common; +using WitsmlExplorer.Api.Models; +using WitsmlExplorer.Api.Models.Measure; +using WitsmlExplorer.Api.Services; +using WitsmlExplorer.Api.Workers.Modify; + +using Xunit; + +namespace WitsmlExplorer.Api.Tests.Workers +{ + public class ModifyWbGeometrySectionWorkerTest + { + private readonly Mock _witsmlClient; + private readonly ModifyWbGeometrySectionWorker _worker; + private readonly static string uid = "gs_uid"; + private readonly static string grade = "a"; + private readonly static string uom = "uom"; + private readonly static double value = 1.2; + private readonly static decimal decimal_value = 1.2m; + private readonly static string datum = "2023-04-19T00:00:04Z"; + private readonly static string fastFabric = "1.2"; + public ModifyWbGeometrySectionWorkerTest() + { + Mock witsmlClientProvider = new(); + _witsmlClient = new Mock(); + witsmlClientProvider.Setup(provider => provider.GetClient()).Returns(_witsmlClient.Object); + ILoggerFactory loggerFactory = new LoggerFactory(); + loggerFactory.AddSerilog(Log.Logger); + ILogger logger = loggerFactory.CreateLogger(); + _worker = new ModifyWbGeometrySectionWorker(logger, witsmlClientProvider.Object); + } + + [Fact] + public async Task Update_GeometryStation() + { + ModifyWbGeometrySectionJob job = CreateJobTemplate(); + List updatedGeometrys = await MockJob(job); + Assert.Single(updatedGeometrys); + var wbGeometrySection = updatedGeometrys.First().WbGeometrys.First() + .WbGeometrySections.First(); + Assert.Equal(grade, wbGeometrySection.Grade); + Assert.Equal(uom, wbGeometrySection.DiaDrift.Uom); + Assert.Equal(value.ToString(CultureInfo.InvariantCulture), wbGeometrySection.DiaDrift.Value); + Assert.Equal(uom, wbGeometrySection.MdBottom.Uom); + Assert.Equal(value.ToString(CultureInfo.InvariantCulture), wbGeometrySection.MdBottom.Value); + Assert.Equal(datum, wbGeometrySection.MdBottom.Datum); + Assert.Equal(uom, wbGeometrySection.MdTop.Uom); + Assert.Equal(value.ToString(CultureInfo.InvariantCulture), wbGeometrySection.MdTop.Value); + Assert.Equal(datum, wbGeometrySection.MdTop.Datum); + Assert.Equal(uom, wbGeometrySection.TvdBottom.Uom); + Assert.Equal(value.ToString(CultureInfo.InvariantCulture), wbGeometrySection.TvdBottom.Value); + Assert.Equal(datum, wbGeometrySection.TvdBottom.Datum); + Assert.Equal(uom, wbGeometrySection.TvdTop.Uom); + Assert.Equal(value.ToString(CultureInfo.InvariantCulture), wbGeometrySection.TvdTop.Value); + Assert.Equal(datum, wbGeometrySection.TvdTop.Datum); + Assert.Equal(uom, wbGeometrySection.OdSection.Uom); + Assert.Equal(value.ToString(CultureInfo.InvariantCulture), wbGeometrySection.OdSection.Value); + Assert.Equal(uom, wbGeometrySection.WtPerLen.Uom); + Assert.Equal(value.ToString(CultureInfo.InvariantCulture), wbGeometrySection.WtPerLen.Value); + Assert.Equal(fastFabric, wbGeometrySection.FactFric); + Assert.Equal(uid, wbGeometrySection.Uid); + } + + private async Task> MockJob(ModifyWbGeometrySectionJob job) + { + List updatedWbGeometrys = new(); + _witsmlClient.Setup(client => + client.UpdateInStoreAsync(It.IsAny())).Callback(geometrys => updatedWbGeometrys.Add(geometrys)) + .ReturnsAsync(new QueryResult(true)); + + await _worker.Execute(job); + return updatedWbGeometrys; + } + + private static ModifyWbGeometrySectionJob CreateJobTemplate() + { + return new ModifyWbGeometrySectionJob + { + WbGeometrySection = new WbGeometrySection() + { + Uid = "gs_uid", + Grade = grade, + TypeHoleCasing = "typeholecasing", + MdTop = new MeasureWithDatum() + { + Datum = datum, + Uom = uom, + Value = value + }, + MdBottom = new MeasureWithDatum() + { + Datum = datum, + Uom = uom, + Value = value + }, + TvdBottom = new MeasureWithDatum() + { + Datum = datum, + Uom = uom, + Value = value + }, + DiaDrift = new LengthMeasure() + { + Uom = uom, + Value = decimal_value + }, + OdSection = new LengthMeasure() + { + Uom = uom, + Value = decimal_value + }, + TvdTop = new MeasureWithDatum + { + Datum = datum, + Uom = uom, + Value = value + }, + WtPerLen = new LengthMeasure() + { + Uom = uom, + Value = decimal_value + }, + FactFric = value + }, + + WbGeometryReference = new ObjectReference() + { + WellUid = "welluid", + WellboreUid = "wellboreuid", + Uid = "geometrysectionuid" + } + }; + } + } +} diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/ReplaceComponentsWorkerTests.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/ReplaceComponentsWorkerTests.cs new file mode 100644 index 000000000..11a618811 --- /dev/null +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/ReplaceComponentsWorkerTests.cs @@ -0,0 +1,197 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Moq; + +using Witsml; +using Witsml.Data; +using Witsml.ServiceReference; + +using WitsmlExplorer.Api.Jobs; +using WitsmlExplorer.Api.Jobs.Common; +using WitsmlExplorer.Api.Models; +using WitsmlExplorer.Api.Query; +using WitsmlExplorer.Api.Services; +using WitsmlExplorer.Api.Workers; +using WitsmlExplorer.Api.Workers.Copy; +using WitsmlExplorer.Api.Workers.Delete; + +using Xunit; + +namespace WitsmlExplorer.Api.Tests.Workers +{ + public class ReplaceComponentsWorkerTests + { + private const string WellboreUid = "wellboreUid"; + private const string TargetUid = "targetUid"; + private const string SourceUid = "sourceUid"; + private const string Uid2 = "Uid2"; + private const string Uid3 = "Uid3"; + private const string Uid4 = "Uid4"; + + private readonly ReplaceComponentsWorker _replaceComponentsWorker; + private readonly Mock _witsmlClient; + private const string WellUid = "wellUid"; + private const string ObjectUid = "objectUid"; + private static readonly string[] ComponentUids = new string[] { "componentUid1", "componentUid2" }; + + public ReplaceComponentsWorkerTests() + { + Mock witsmlClientProvider = new(); + _witsmlClient = new Mock(); + witsmlClientProvider.Setup(provider => provider.GetClient()).Returns(_witsmlClient.Object); + witsmlClientProvider.Setup(provider => provider.GetSourceClient()).Returns(_witsmlClient.Object); + Mock> copyComponentLogger = new(); + + CopyComponentsWorker copyComponentsWorker = new(copyComponentLogger.Object, witsmlClientProvider.Object, new Mock().Object); + ILogger deleteComponentLogger = new Mock>().Object; + DeleteComponentsWorker deleteComponentsWorker = new(deleteComponentLogger, witsmlClientProvider.Object); + ILogger replaceComponentLogger = new Mock>().Object; + _replaceComponentsWorker = new ReplaceComponentsWorker(replaceComponentLogger, copyComponentsWorker, deleteComponentsWorker); + } + + [Fact] + public async Task Execute_ReplaceTubularComponents_Delete_Failure() + { + CopyComponentsJob copyTubularComponentJob = CreateJobTemplate(new string[] { Uid2, Uid3 }, ComponentType.TubularComponent); + SetupGetFromStoreAsync(ComponentType.TubularComponent, copyTubularComponentJob.Source.ComponentUids, new string[] { Uid4 }); + SetUpStoreForDelete(false); + + var deleteObjectsJob = CreateJob(ComponentType.TubularComponent); + var replaceObjectsJob = new ReplaceComponentsJob() + { + CopyJob = copyTubularComponentJob, + DeleteJob = deleteObjectsJob + }; + + (WorkerResult workerResult, RefreshAction refreshAction) = await _replaceComponentsWorker.Execute(replaceObjectsJob); + Assert.False(workerResult.IsSuccess); + Assert.Equal("Failed to delete tubularcomponents", workerResult.Message); + } + + [Fact] + public async Task Execute_ReplaceTubularComponents_CopyFailure() + { + SetupUpdateInStoreAsync(); + string missingUid = "uidOfMissingTubularComponent123123"; + CopyComponentsJob copyTubularComponentJob = CreateJobTemplate(new string[] { Uid2, Uid3, missingUid }, ComponentType.TubularComponent); + SetupGetFromStoreAsync(ComponentType.TubularComponent, new string[] { Uid2, Uid3 }, new string[] { Uid4 }); + SetUpStoreForDelete(); + + var deleteObjectsJob = CreateJob(ComponentType.TubularComponent); + var replaceObjectsJob = new ReplaceComponentsJob() + { + CopyJob = copyTubularComponentJob, + DeleteJob = deleteObjectsJob, + JobInfo = new() + }; + + (WorkerResult workerResult, RefreshAction refreshAction) = await _replaceComponentsWorker.Execute(replaceObjectsJob); + Assert.False(workerResult.IsSuccess); + Assert.Equal("Failed to copy tubularcomponents.", workerResult.Message); + } + + [Fact] + public async Task Execute_ReplaceTubularComponents_IsSuccess() + { + SetupUpdateInStoreAsync(); + CopyComponentsJob copyTubularComponentJob = CreateJobTemplate(new string[] { Uid2, Uid3 }, ComponentType.TubularComponent); + SetupGetFromStoreAsync(ComponentType.TubularComponent, copyTubularComponentJob.Source.ComponentUids, new string[] { Uid4 }); + SetUpStoreForDelete(); + + var deleteObjectsJob = CreateJob(ComponentType.TubularComponent); + var replaceObjectsJob = new ReplaceComponentsJob() + { + CopyJob = copyTubularComponentJob, + DeleteJob = deleteObjectsJob, + JobInfo = new() + }; + + (WorkerResult workerResult, RefreshAction refreshAction) = await _replaceComponentsWorker.Execute(replaceObjectsJob); + Assert.True(workerResult.IsSuccess); + Assert.Equal(EntityType.Tubular, refreshAction.EntityType); + } + + private void SetUpStoreForDelete(bool deleteResult = true) + { + List deleteQueries = new(); + _witsmlClient.Setup(client => client.DeleteFromStoreAsync(It.IsAny())) + .Callback(deleteQueries.Add) + .ReturnsAsync(new QueryResult(deleteResult)); + } + + private List SetupUpdateInStoreAsync() + { + List updateQueries = new(); + _witsmlClient.Setup(client => client.UpdateInStoreAsync(It.IsAny())) + .Callback(updateQueries.Add) + .ReturnsAsync(new QueryResult(true)); + return updateQueries; + } + + private void SetupGetFromStoreAsync(ComponentType componentType, string[] sourceComponentUids, string[] targetComponentUids) + { + _witsmlClient.Setup(client => + client.GetFromStoreNullableAsync(It.Is(query => query.Objects.First().Uid == SourceUid), It.Is((ops) => ops.ReturnElements == ReturnElements.All))) + .ReturnsAsync(GetWitsmlObject(sourceComponentUids, SourceUid, componentType)); + _witsmlClient.Setup(client => + client.GetFromStoreNullableAsync(It.Is(query => query.Objects.First().Uid == TargetUid), It.Is((ops) => ops.ReturnElements == ReturnElements.Requested))) + .ReturnsAsync(GetWitsmlObject(targetComponentUids, TargetUid, componentType)); + } + + private static DeleteComponentsJob CreateJob(ComponentType componentType) + { + return new() + { + ToDelete = new ComponentReferences() + { + ComponentUids = ComponentUids, + ComponentType = componentType, + Parent = new ObjectReference() + { + WellUid = WellUid, + WellboreUid = WellboreUid, + Uid = ObjectUid, + } + } + }; + } + + private static CopyComponentsJob CreateJobTemplate(string[] toCopyUids, ComponentType componentType) + { + return new CopyComponentsJob + { + Source = new ComponentReferences + { + Parent = new ObjectReference + { + Uid = SourceUid, + WellboreUid = WellboreUid, + WellUid = WellUid + }, + ComponentUids = toCopyUids, + ComponentType = componentType + }, + Target = new ObjectReference + { + WellUid = WellUid, + WellboreUid = WellboreUid, + Uid = TargetUid + } + }; + } + + private static IWitsmlObjectList GetWitsmlObject(string[] componentUids, string uid, ComponentType componentType) + { + WitsmlObjectOnWellbore source = EntityTypeHelper.ToObjectOnWellbore(componentType.ToParentType()); + source.UidWell = WellUid; + source.UidWellbore = WellboreUid; + source.Uid = uid; + ObjectQueries.SetComponents(source, componentType, componentUids); + return (IWitsmlObjectList)source.AsItemInWitsmlList(); + } + } +} diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/ReplaceObjectsWorkerTests.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/ReplaceObjectsWorkerTests.cs new file mode 100644 index 000000000..a1485c24e --- /dev/null +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/ReplaceObjectsWorkerTests.cs @@ -0,0 +1,201 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Moq; + +using Witsml; +using Witsml.Data; +using Witsml.Data.Tubular; +using Witsml.ServiceReference; + +using WitsmlExplorer.Api.Jobs; +using WitsmlExplorer.Api.Jobs.Common; +using WitsmlExplorer.Api.Models; +using WitsmlExplorer.Api.Services; +using WitsmlExplorer.Api.Workers; +using WitsmlExplorer.Api.Workers.Copy; +using WitsmlExplorer.Api.Workers.Delete; + +using Xunit; + +namespace WitsmlExplorer.Api.Tests.Workers +{ + public class ReplaceObjectsWorkerTests + { + private readonly Mock _witsmlClientProvider; + private readonly ReplaceObjectsWorker _replaceObjectWorker; + private readonly CopyObjectsWorker _copyObjectsWorker; + private readonly DeleteObjectsWorker _deleteObjectsWorker; + private readonly Mock _witsmlClient; + + private const string WellUid = "wellUid"; + private const string SourceWellboreUid = "sourceWellboreUid"; + private const string TargetWellboreUid = "targetWellboreUid"; + private const string ObjectUid = "objectUid"; + private static readonly string[] ObjectUids = { "objectUid1", "objectUid2" }; + + public ReplaceObjectsWorkerTests() + { + _witsmlClientProvider = new(); + _witsmlClient = new Mock(); + _witsmlClientProvider.Setup(provider => provider.GetClient()).Returns(_witsmlClient.Object); + _witsmlClientProvider.Setup(provider => provider.GetSourceClient()).Returns(_witsmlClient.Object); + + Mock> copyObjectsLogger = new(); + CopyUtils copyUtils = new(new Mock>().Object); + CopyLogWorker copyLogWorker = new(new Mock>().Object, _witsmlClientProvider.Object); + _copyObjectsWorker = new CopyObjectsWorker(copyObjectsLogger.Object, _witsmlClientProvider.Object, copyUtils, copyLogWorker); + + ILogger deleteObjectsLogger = new Mock>().Object; + _deleteObjectsWorker = new DeleteObjectsWorker(deleteObjectsLogger, _witsmlClientProvider.Object); + ILogger replaceObjectsLogger = new Mock>().Object; + _replaceObjectWorker = new ReplaceObjectsWorker(replaceObjectsLogger, _copyObjectsWorker, _deleteObjectsWorker); + } + + [Fact] + public async Task Execute_Delete_Part_ThrowsError() + { + SetUpStoreForCopy(true); + SetUpStoreForDelete(false); + ReplaceObjectsJob replaceObjectsJob = SetUpReplaceObjectsJob(); + (WorkerResult workerResult, RefreshAction refreshAction) = await _replaceObjectWorker.Execute(replaceObjectsJob); + Assert.False(workerResult.IsSuccess); + Assert.Equal("Failed to delete some WitsmlTubulars", workerResult.Message); + } + + [Fact] + public async Task Execute_Copy_Part_ThrowsError() + { + SetUpStoreForCopy(true); + SetUpStoreForDelete(); + ReplaceObjectsJob replaceObjectsJob = SetUpReplaceObjectsJob(); + (WorkerResult workerResult, RefreshAction refreshAction) = await _replaceObjectWorker.Execute(replaceObjectsJob); + Assert.False(workerResult.IsSuccess); + Assert.Equal("Could not find any objects to copy", workerResult.Message); + } + + [Fact] + public async Task Execute_CopyOneTubular_IsSuccess() + { + SetUpStoreForCopy(); + SetUpStoreForDelete(); + ReplaceObjectsJob replaceObjectsJob = SetUpReplaceObjectsJob(); + (WorkerResult workerResult, RefreshAction refreshAction) = await _replaceObjectWorker.Execute(replaceObjectsJob); + Assert.True(workerResult.IsSuccess); + Assert.Equal(EntityType.Tubular, refreshAction.EntityType); + Assert.Equal("Copied WitsmlTubulars: objectUid.", workerResult.Message); + } + + private static ReplaceObjectsJob SetUpReplaceObjectsJob() + { + var copyObjectsJob = CreateJobTemplate(); + var deleteObjectsJob = CreateJob(EntityType.Tubular); + var replaceObjectsJob = new ReplaceObjectsJob() + { + CopyJob = copyObjectsJob, + DeleteJob = deleteObjectsJob + }; + return replaceObjectsJob; + } + + private void SetUpStoreForDelete(bool queryResult = true) + { + List deleteQueries = new(); + _witsmlClient.Setup(client => client.DeleteFromStoreAsync(It.IsAny())) + .Callback(deleteQueries.Add) + .ReturnsAsync(new QueryResult(true)); + + _witsmlClient.Setup(client => client.DeleteFromStoreAsync( + Match.Create(o => + ((WitsmlTubulars)o).Tubulars.First().UidWell == WellUid && + ((WitsmlTubulars)o).Tubulars.First().UidWellbore == TargetWellboreUid))) + .ReturnsAsync(new QueryResult(queryResult)); + } + + private void SetUpStoreForCopy(bool emptyResult = false) + { + _witsmlClient.Setup(client => + client.GetFromStoreNullableAsync(It.Is(witsmlObjects => witsmlObjects.Objects.First().Uid == ObjectUid), It.Is((ops) => ops.ReturnElements == ReturnElements.All))) + .ReturnsAsync(emptyResult ? GetEmptySourceObjects() : GetSourceObjects()); + SetupGetWellbore(); + CopyTestsUtils.SetupAddInStoreAsync(_witsmlClient); + } + + private void SetupGetWellbore() + { + _witsmlClient.Setup(client => + client.GetFromStoreAsync(It.IsAny(), It.Is((ops) => ops.ReturnElements == ReturnElements.Requested))) + .ReturnsAsync(new WitsmlWellbores + { + Wellbores = new List + { + new WitsmlWellbore + { + UidWell = "Well1", + Uid = "wellbore1", + Name = "Wellbore 1", + NameWell = "Well 1" + } + } + }); + } + + private static DeleteObjectsJob CreateJob(EntityType objectType) + { + return new() + { + ToDelete = new ObjectReferences() + { + WellUid = WellUid, + WellboreUid = TargetWellboreUid, + ObjectUids = ObjectUids, + ObjectType = objectType + } + }; + } + + private static CopyObjectsJob CreateJobTemplate(string targetWellboreUid = TargetWellboreUid) + { + return new CopyObjectsJob + { + Source = new ObjectReferences + { + WellUid = WellUid, + WellboreUid = SourceWellboreUid, + ObjectUids = new string[] { ObjectUid }, + ObjectType = EntityType.Tubular + }, + Target = new WellboreReference + { + WellUid = WellUid, + WellboreUid = targetWellboreUid + } + }; + } + + private static IWitsmlObjectList GetSourceObjects() + { + WitsmlTubular witsmlObject = new() + { + UidWell = WellUid, + UidWellbore = SourceWellboreUid, + Uid = ObjectUid, + }; + return new WitsmlTubulars + { + Objects = new List { witsmlObject } + }; + } + + private static IWitsmlObjectList GetEmptySourceObjects() + { + return new WitsmlTubulars + { + Objects = new List() + }; + } + } +} diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/WorkerToolsTests.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/WorkerToolsTests.cs new file mode 100644 index 000000000..c585bee81 --- /dev/null +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/WorkerToolsTests.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Threading.Tasks; + +using Moq; + +using Witsml; +using Witsml.Data; +using Witsml.ServiceReference; + +using WitsmlExplorer.Api.Jobs.Common; +using WitsmlExplorer.Api.Services; +using WitsmlExplorer.Api.Workers; + +using Xunit; + +namespace WitsmlExplorer.Api.Tests.Workers +{ + public class WorkerToolsTests + { + private const string WellName = "NO 34/10-A-25 C"; + private const string WellboreName = "NO 34/10-A-25 C - Main Wellbore"; + private readonly Mock _witsmlClient; + + public WorkerToolsTests() + { + Mock witsmlClientProvider = new(); + _witsmlClient = new Mock(); + witsmlClientProvider.Setup(provider => provider.GetClient()).Returns(_witsmlClient.Object); + } + + [Fact] + public async Task GetWellTest_OK() + { + _witsmlClient.Setup(client => + client.GetFromStoreAsync(It.IsAny(), It.Is((ops) => ops.ReturnElements == ReturnElements.HeaderOnly))).ReturnsAsync(CreateWells()); + var wellReference = new WellReference() { WellName = WellName }; + var well = await WorkerTools.GetWell(_witsmlClient.Object, wellReference, ReturnElements.HeaderOnly); + Assert.Equal(WellName, well.Name); + } + + [Fact] + public async Task GetWellBoreTest_OK() + { + _witsmlClient.Setup(client => + client.GetFromStoreAsync(It.IsAny(), It.Is((ops) => ops.ReturnElements == ReturnElements.HeaderOnly))).ReturnsAsync(CreateWellbores()); + var wellboreReference = new WellboreReference() { WellName = WellName, WellboreName = WellboreName }; + var wellbore = await WorkerTools.GetWellbore(_witsmlClient.Object, wellboreReference, ReturnElements.HeaderOnly); + Assert.Equal(WellboreName, wellbore.Name); + } + + private static WitsmlWells CreateWells() + { + return new WitsmlWells() + { + Wells = new List() + { + new WitsmlWell() + { + Name = WellName + } + } + }; + } + + private static WitsmlWellbores CreateWellbores() + { + return new WitsmlWellbores() + { + Wellbores = new List() + { + new WitsmlWellbore() + { + Name = WellboreName, + } + } + }; + } + } +} From d02b5400bb7f210b2e45ea92dcdb78dbf8e4e46d Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:49:49 +0200 Subject: [PATCH 098/124] Video fix (#2531) --- Media/README.md | 1 - README.md | 2 -- 2 files changed, 3 deletions(-) diff --git a/Media/README.md b/Media/README.md index 802d0bd95..aadfeb0fb 100644 --- a/Media/README.md +++ b/Media/README.md @@ -22,7 +22,6 @@ - [Copy Within Server](#copy-within-server) ## Overview -https://github.com/user-attachments/assets/76b98f20-5ded-4ed8-9c21-a34625fcd355 ## Manage Servers https://github.com/user-attachments/assets/4e8bfb7c-b2c1-4989-a3dd-ef70357abd7f diff --git a/README.md b/README.md index 000664533..7f56f3797 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,6 @@ Witsml Explorer is a data management tool used for browsing and editing data directly on [WITSML](https://en.wikipedia.org/wiki/Wellsite_information_transfer_standard_markup_language) servers. -https://github.com/user-attachments/assets/b1a8fdde-a129-4656-87dd-cd10b7a46fb0 - ## Demo Videos Please see [Demo Videos](/Media/README.md) From 18612582ce49eb98e25bb160899876e6d6ecef70 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Fri, 23 Aug 2024 15:09:56 +0200 Subject: [PATCH 099/124] Video fix (#2532) --- Media/README.md | 1 + README.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Media/README.md b/Media/README.md index aadfeb0fb..c6649f671 100644 --- a/Media/README.md +++ b/Media/README.md @@ -22,6 +22,7 @@ - [Copy Within Server](#copy-within-server) ## Overview +https://github.com/user-attachments/assets/dc0d1668-03c8-47c8-8601-98daab5bb915 ## Manage Servers https://github.com/user-attachments/assets/4e8bfb7c-b2c1-4989-a3dd-ef70357abd7f diff --git a/README.md b/README.md index 7f56f3797..317308f6a 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Witsml Explorer is a data management tool used for browsing and editing data directly on [WITSML](https://en.wikipedia.org/wiki/Wellsite_information_transfer_standard_markup_language) servers. +https://github.com/user-attachments/assets/dc0d1668-03c8-47c8-8601-98daab5bb915 + ## Demo Videos Please see [Demo Videos](/Media/README.md) From bf5d04dd702f1401c139178754465edc7c268adc Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:43:47 +0200 Subject: [PATCH 100/124] FIX-2540 Import LAS - Depth Logs (#2541) --- .../ContextMenus/LogObjectContextMenu.tsx | 2 +- .../components/Modals/LogDataImportModal.tsx | 189 ++++++++++++++---- .../tools/lasFileTools.ts | 81 ++++++++ 3 files changed, 235 insertions(+), 37 deletions(-) create mode 100644 Src/WitsmlExplorer.Frontend/tools/lasFileTools.ts diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogObjectContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogObjectContextMenu.tsx index 91e7aeebf..c043826c7 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogObjectContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogObjectContextMenu.tsx @@ -438,7 +438,7 @@ const LogObjectContextMenu = ( disabled={checkedObjects.length === 0} > - Import log data from .csv + Import log data , , (""); const [isLoading, setIsLoading] = useState(false); const separator = ","; - const hasOverlap = checkOverlap( - targetLog, - uploadedFileColumns, - uploadedFileData, - logCurveInfoList - ); const validate = (fileColumns: ImportColumn[]) => { setError(""); @@ -76,6 +85,13 @@ const LogDataImportModal = ( } }; + const hasOverlap = checkOverlap( + targetLog, + uploadedFileColumns, + uploadedFileData, + logCurveInfoList + ); + const onSubmit = async () => { setIsLoading(true); @@ -96,19 +112,35 @@ const LogDataImportModal = ( async (e: React.ChangeEvent): Promise => { const file = e.target.files.item(0); if (!file) return; - const text = await file.text(); - const header = text.split("\n", 1)[0]; - const data = text.split("\n").slice(1); + let header: ImportColumn[] = null; + let data: string[] = null; + + if (text.startsWith("~V")) { + // LAS files should start with ~V. + const curveSection = extractLASSection( + text, + "CURVE INFORMATION", + "Curve" + ); + const dataSection = extractLASSection(text, "ASCII", "A"); + header = parseLASHeader(curveSection); + data = parseLASData(dataSection); + } else { + const headerLine = text.split("\n", 1)[0]; + header = parseCSVHeader(headerLine); + data = text.split("\n").slice(1); + } + validate(header); setUploadedFile(file); - updateUploadedFileColumns(header); + setUploadedFileColumns(header); setUploadedFileData(data); }, [] ); - const updateUploadedFileColumns = (header: string): void => { + const parseCSVHeader = (header: string) => { const unitRegex = /(?<=\[)(.*)(?=\]){1}/; const fileColumns = header.split(separator).map((col, index) => { const columnName = col.substring(0, col.indexOf("[")); @@ -118,10 +150,19 @@ const LogDataImportModal = ( unit: unitRegex.exec(col) ? unitRegex.exec(col)[0] : UNITLESS_UNIT }; }); - setUploadedFileColumns(fileColumns); - validate(fileColumns); + return fileColumns; }; + const contentTableColumns: ContentTableColumn[] = useMemo( + () => + uploadedFileColumns.map((col) => ({ + property: col.name, + label: `${col.name}[${col.unit}]`, + type: ContentType.String + })), + [uploadedFileColumns] + ); + return ( <> { @@ -139,7 +180,7 @@ const LogDataImportModal = ( Upload File @@ -159,6 +200,13 @@ const LogDataImportModal = ( style={{ backgroundColor: colors.ui.backgroundLight }} > + Supported filetypes: csv, las. + + Supported logs: depth (csv + las), time (csv). + + + Only curve names, units and data is imported. + Currently, only double values are supported as TypeLogData. @@ -175,15 +223,66 @@ const LogDataImportModal = ( + + The las is expected to have these sections: + + + ~CURVE INFORMATION (or ~C) +
+ [...] +
+ IndexCurve .unit [...] +
+ Curve1 .unit [...] +
+ [...] +
+ ~ASCII (or ~A) +
+ 195.99 -999.25 2500 +
+ 196.00 1 2501 +
+
+
+ {uploadedFileColumns?.length && + uploadedFileData?.length && + targetLog?.indexCurve && + !error && ( + + + Preview + + +

+ + + )} {hasOverlap && ( )} } + width={ModalWidth.LARGE} confirmDisabled={!uploadedFile || !!error || isFetchingLogCurveInfo} confirmText={"Import"} onSubmit={() => onSubmit()} @@ -213,20 +312,15 @@ const Container = styled.div` const checkOverlap = ( targetLog: LogObject, - uploadedFileColumns: ImportColumn[], - uploadedFileData: string[], + columns: ImportColumn[], + data: string[], logCurveInfoList: LogCurveInfo[] ) => { - if (!uploadedFileColumns || !uploadedFileData || !logCurveInfoList) - return false; - const importDataRanges = getDataRanges( - targetLog, - uploadedFileColumns, - uploadedFileData - ); + if (!columns || !data || !logCurveInfoList) return false; + const importDataRanges = getDataRanges(targetLog, columns, data); - for (let index = 1; index < uploadedFileColumns.length; index++) { - const mnemonic = uploadedFileColumns[index].name; + for (let index = 1; index < columns.length; index++) { + const mnemonic = columns[index].name; const logCurveInfo = logCurveInfoList.find( (lci) => lci.mnemonic === mnemonic ); @@ -266,24 +360,28 @@ const checkOverlap = ( const getDataRanges = ( targetLog: LogObject, - uploadedFileColumns: ImportColumn[], - uploadedFileData: string[] + columns: ImportColumn[], + data: string[] ): IndexRange[] => { const dataRanges: IndexRange[] = []; + const indexCurveColumn = columns.find( + (col) => col.name === targetLog.indexCurve + )?.index; - for (let index = 0; index < uploadedFileColumns.length; index++) { - const firstRowWithData = uploadedFileData.find((dataRow) => { + for (let index = 0; index < columns.length; index++) { + const firstRowWithData = data.find((dataRow) => { const data = dataRow.split(",")[index]; if (data) return true; }); - const lastRowWithData = uploadedFileData.findLast((dataRow) => { + const lastRowWithData = data.findLast((dataRow) => { const data = dataRow.split(",")[index]; if (data) return true; }); - const firstRowWithDataIndex = firstRowWithData?.split(",")[0]; - const lastRowWithDataIndex = lastRowWithData?.split(",")[0]; + const firstRowWithDataIndex = + firstRowWithData?.split(",")[indexCurveColumn]; + const lastRowWithDataIndex = lastRowWithData?.split(",")[indexCurveColumn]; if ( targetLog.indexType === WITSML_INDEX_TYPE_MD && @@ -304,4 +402,23 @@ const getDataRanges = ( return dataRanges; }; +const getTableData = ( + data: string[], + columns: ImportColumn[], + indexCurve: string +): ContentTableCustomRow[] => { + const indexCurveColumn = columns.find((col) => col.name === indexCurve); + if (!indexCurveColumn) return []; + return data?.map((dataLine) => { + const dataCells = dataLine.split(","); + const result: ContentTableCustomRow = { + id: dataCells[indexCurveColumn.index] + }; + columns.forEach((col, i) => { + result[col.name] = dataCells[i]; + }); + return result; + }); +}; + export default LogDataImportModal; diff --git a/Src/WitsmlExplorer.Frontend/tools/lasFileTools.ts b/Src/WitsmlExplorer.Frontend/tools/lasFileTools.ts new file mode 100644 index 000000000..0f2c86c5b --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/tools/lasFileTools.ts @@ -0,0 +1,81 @@ +/** + * Parses LAS header section data to extract curve names and units. + * + * @param sectionData - A string containing the LAS header section data. + * @returns An array of objects, each containing the index, curve name, and unit. + * Each object represents a curve from the LAS header section. + */ +export const parseLASHeader = (sectionData: string) => { + const lines = sectionData + .split("\n") + .filter((line) => line.trim() != "" && !line.startsWith("#")); + const headerData = lines.map((line, index) => { + const endOfCurveNameIndex = line.indexOf("."); + const curveName = line.slice(0, endOfCurveNameIndex).trim(); + const unit = line.slice(endOfCurveNameIndex + 1).split(/\s+/)[0]; + return { + index, + name: curveName, + unit: unit + }; + }); + return headerData; +}; + +/** + * Parses LAS data section to extract log data. + * + * @param sectionData - A string containing the LAS data section. + * @returns An array of strings where each string represents a line of log data with comma-separated values. + */ +export const parseLASData = (sectionData: string) => { + const lines = sectionData + .split("\n") + .map((line) => line.trim()) + .filter((line) => line != "" && !line.startsWith("#")); + const logData = lines.map((line) => { + // Split the lines on whitespaces, but keep entries if surrounded by "". + const matches = line.match(/"[^"]*"|\S+/g) || []; + const cells = matches.map((cell) => cell.replace(/^"|"$/g, "")); + return cells.join(","); + }); + return logData; +}; + +/** + * Extracts the content of the first matching section from a LAS data string based on the provided section names. + * + * @param lasData - A string containing the entire LAS data. + * @param sectionNames - One or more section names to search for, e.g., 'CURVE INFORMATION'. + * The function will return the content of the first section that matches. + * If no exact line matches are found, it will look for matches that starts on the given strings. + * @returns The content of the first matching section as a string. Returns an empty string + * if none of the specified sections are found or if the section is incorrectly formatted. + */ +export const extractLASSection = ( + lasData: string, + ...sectionNames: string[] +): string => { + const getSection = (exactMatch: boolean) => { + for (const sectionName of sectionNames) { + const searchString = exactMatch + ? `\n~${sectionName}\\s*$` + : `\n~${sectionName}`; + const sectionPattern = new RegExp(searchString, "m"); + const sectionMatch = sectionPattern.exec(lasData); + if (sectionMatch) { + const sectionIndex = sectionMatch.index; + const startIndex = + lasData.indexOf("\n", sectionIndex + sectionMatch[0].length) + 1; + if (startIndex === -1) return ""; + const endIndex = lasData.indexOf("\n~", startIndex); + const sectionEndIndex = endIndex !== -1 ? endIndex : undefined; + const sectionData = lasData.slice(startIndex, sectionEndIndex); + return sectionData; + } + } + return null; + }; + + return getSection(true) ?? getSection(false) ?? ""; +}; From 46b175dc69a3f572ae795acbd716abd95d5f44d8 Mon Sep 17 00:00:00 2001 From: Robert Basti Date: Fri, 13 Sep 2024 14:25:14 +0200 Subject: [PATCH 101/124] Global log curve priority (#2544) --- .../HttpHandlers/LogCurvePriorityHandler.cs | 26 ++- Src/WitsmlExplorer.Api/Routes.cs | 6 +- .../Services/LogCurvePriorityService.cs | 81 ++++++--- .../ContentViews/LogCurveInfoListView.tsx | 37 +++- .../MultiLogsCurveInfoListView.tsx | 37 +++- .../ContextMenus/LogCurveInfoContextMenu.tsx | 169 +++++++++++++----- .../Modals/LogCurvePriorityModal.tsx | 93 ++++++++-- .../services/logCurvePriorityService.tsx | 35 ++-- .../Services/LogCurvePriorityServiceTests.cs | 20 +-- 9 files changed, 372 insertions(+), 132 deletions(-) diff --git a/Src/WitsmlExplorer.Api/HttpHandlers/LogCurvePriorityHandler.cs b/Src/WitsmlExplorer.Api/HttpHandlers/LogCurvePriorityHandler.cs index 12b9ccf03..ad1e0e3b3 100644 --- a/Src/WitsmlExplorer.Api/HttpHandlers/LogCurvePriorityHandler.cs +++ b/Src/WitsmlExplorer.Api/HttpHandlers/LogCurvePriorityHandler.cs @@ -10,17 +10,31 @@ namespace WitsmlExplorer.Api.HttpHandlers { public static class LogCurvePriorityHandler { - [Produces(typeof(List))] - public static async Task GetPrioritizedCurves(string wellUid, string wellboreUid, ILogCurvePriorityService logCurvePriorityService) + [Produces(typeof(string[]))] + public static async Task GetPrioritizedLocalCurves(string wellUid, string wellboreUid, ILogCurvePriorityService logCurvePriorityService) + { + var prioritizedCurves = await logCurvePriorityService.GetPrioritizedLocalCurves(wellUid, wellboreUid) ?? new List(); + return TypedResults.Ok(prioritizedCurves); + } + + [Produces(typeof(string[]))] + public static async Task GetPrioritizedUniversalCurves(ILogCurvePriorityService logCurvePriorityService) { - var prioritizedCurves = await logCurvePriorityService.GetPrioritizedCurves(wellUid, wellboreUid) ?? new List(); + var prioritizedCurves = await logCurvePriorityService.GetPrioritizedUniversalCurves() ?? new List(); return TypedResults.Ok(prioritizedCurves); } - [Produces(typeof(IList))] - public static async Task SetPrioritizedCurves(string wellUid, string wellboreUid, IList prioritizedCurves, ILogCurvePriorityService logCurvePriorityService) + [Produces(typeof(List))] + public static async Task SetPrioritizedLocalCurves(string wellUid, string wellboreUid, IList prioritizedCurves, ILogCurvePriorityService logCurvePriorityService) + { + var createdPrioritizedCurves = await logCurvePriorityService.SetPrioritizedLocalCurves(wellUid, wellboreUid, prioritizedCurves) ?? new List(); + return TypedResults.Ok(createdPrioritizedCurves); + } + + [Produces(typeof(List))] + public static async Task SetPrioritizedUniversalCurves(List prioritizedCurves, ILogCurvePriorityService logCurvePriorityService) { - var createdPrioritizedCurves = await logCurvePriorityService.SetPrioritizedCurves(wellUid, wellboreUid, prioritizedCurves) ?? new List(); + var createdPrioritizedCurves = await logCurvePriorityService.SetPrioritizedUniversalCurves(prioritizedCurves) ?? new List(); return TypedResults.Ok(createdPrioritizedCurves); } } diff --git a/Src/WitsmlExplorer.Api/Routes.cs b/Src/WitsmlExplorer.Api/Routes.cs index 6e84bb55d..69acd7fa3 100644 --- a/Src/WitsmlExplorer.Api/Routes.cs +++ b/Src/WitsmlExplorer.Api/Routes.cs @@ -39,8 +39,10 @@ public static void ConfigureApi(this WebApplication app, IConfiguration configur app.MapGet("/wells/{wellUid}/wellbores/{wellboreUid}/idonly/{objectType}/{objectUid}", ObjectHandler.GetObjectIdOnly, useOAuth2); app.MapGet("/wells/{wellUid}/wellbores/{wellboreUid}/countexpandable", ObjectHandler.GetExpandableObjectsCount, useOAuth2); - app.MapGet("/wells/{wellUid}/wellbores/{wellboreUid}/logCurvePriority", LogCurvePriorityHandler.GetPrioritizedCurves, useOAuth2); - app.MapPost("/wells/{wellUid}/wellbores/{wellboreUid}/logCurvePriority", LogCurvePriorityHandler.SetPrioritizedCurves, useOAuth2); + app.MapGet("/wells/{wellUid}/wellbores/{wellboreUid}/logCurvePriority", LogCurvePriorityHandler.GetPrioritizedLocalCurves, useOAuth2); + app.MapGet("/universal/logCurvePriority", LogCurvePriorityHandler.GetPrioritizedUniversalCurves, useOAuth2); + app.MapPost("/universal/logCurvePriority", LogCurvePriorityHandler.SetPrioritizedUniversalCurves, useOAuth2); + app.MapPost("/wells/{wellUid}/wellbores/{wellboreUid}/logCurvePriority", LogCurvePriorityHandler.SetPrioritizedLocalCurves, useOAuth2); Dictionary types = EntityTypeHelper.ToPluralLowercase(); Dictionary routes = types.ToDictionary(entry => entry.Key, entry => "/wells/{wellUid}/wellbores/{wellboreUid}/" + entry.Value); diff --git a/Src/WitsmlExplorer.Api/Services/LogCurvePriorityService.cs b/Src/WitsmlExplorer.Api/Services/LogCurvePriorityService.cs index 7337d1aca..04492085c 100644 --- a/Src/WitsmlExplorer.Api/Services/LogCurvePriorityService.cs +++ b/Src/WitsmlExplorer.Api/Services/LogCurvePriorityService.cs @@ -1,7 +1,4 @@ - -using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.IdentityModel.Tokens; @@ -13,27 +10,26 @@ namespace WitsmlExplorer.Api.Services { public interface ILogCurvePriorityService { - Task> GetPrioritizedCurves(string wellUid, string wellboreUid); - Task> SetPrioritizedCurves(string wellUid, string wellboreUid, IList prioritizedCurves); + Task> GetPrioritizedLocalCurves(string wellUid, string wellboreUid); + Task> SetPrioritizedLocalCurves(string wellUid, string wellboreUid, IList prioritizedCurves); + Task> SetPrioritizedUniversalCurves(List prioritizedCurves); + Task> GetPrioritizedUniversalCurves(); } - public class LogCurvePriorityService : ILogCurvePriorityService + public class LogCurvePriorityService( + IDocumentRepository + logCurvePriorityRepository) + : ILogCurvePriorityService { - private readonly IDocumentRepository logCurvePriorityRepository; - - public LogCurvePriorityService(IDocumentRepository logCurvePriorityRepository) - { - this.logCurvePriorityRepository = logCurvePriorityRepository; - } - - public async Task> GetPrioritizedCurves(string wellUid, string wellboreUid) + private const string UniversalDbId = "universal"; + public async Task> GetPrioritizedLocalCurves(string wellUid, string wellboreUid) { string logCurvePriorityId = GetLogCurvePriorityId(wellUid, wellboreUid); LogCurvePriority logCurvePriority = await logCurvePriorityRepository.GetDocumentAsync(logCurvePriorityId); return logCurvePriority?.PrioritizedCurves; } - public async Task> SetPrioritizedCurves(string wellUid, string wellboreUid, IList prioritizedCurves) + public async Task> SetPrioritizedLocalCurves(string wellUid, string wellboreUid, IList prioritizedCurves) { if (prioritizedCurves.IsNullOrEmpty()) { @@ -41,34 +37,50 @@ public async Task> SetPrioritizedCurves(string wellUid, string wel return null; } - IList currentPrioritizedCurves = await GetPrioritizedCurves(wellUid, wellboreUid); - if (currentPrioritizedCurves == null) + string logCurvePriorityId = GetLogCurvePriorityId(wellUid, wellboreUid); + LogCurvePriority logCurvePriority = await logCurvePriorityRepository.GetDocumentAsync(logCurvePriorityId); + if (logCurvePriority == null) { return await CreatePrioritizedCurves(wellUid, wellboreUid, prioritizedCurves); } - string logCurvePriorityId = GetLogCurvePriorityId(wellUid, wellboreUid); - LogCurvePriority logCurvePriorityToUpdate = CreateLogCurvePriorityObject(wellUid, wellboreUid, prioritizedCurves); + LogCurvePriority logCurvePriorityToUpdate = CreateLogCurvePriorityLocalObject(wellUid, wellboreUid, prioritizedCurves); LogCurvePriority updatedLogCurvePriority = await logCurvePriorityRepository.UpdateDocumentAsync(logCurvePriorityId, logCurvePriorityToUpdate); return updatedLogCurvePriority.PrioritizedCurves; } + public async Task> GetPrioritizedUniversalCurves() + { + LogCurvePriority logCurvePriorityGlobal = await logCurvePriorityRepository.GetDocumentAsync(UniversalDbId); + return logCurvePriorityGlobal?.PrioritizedCurves; + } + + public async Task> SetPrioritizedUniversalCurves(List prioritizedCurves) + { + var globalDocument = await logCurvePriorityRepository.GetDocumentAsync(UniversalDbId); + if (globalDocument == null) + { + return await CreatePrioritizedUniversalCurves(prioritizedCurves); + } + return await UpdatePrioritizedUniversalCurves(prioritizedCurves); + } + + private async Task> CreatePrioritizedCurves(string wellUid, string wellboreUid, IList prioritizedCurves) { - LogCurvePriority logCurvePriorityToCreate = CreateLogCurvePriorityObject(wellUid, wellboreUid, prioritizedCurves); + LogCurvePriority logCurvePriorityToCreate = CreateLogCurvePriorityLocalObject(wellUid, wellboreUid, prioritizedCurves); LogCurvePriority inserted = await logCurvePriorityRepository.CreateDocumentAsync(logCurvePriorityToCreate); return inserted.PrioritizedCurves; } private async Task DeleteLogCurvePriorityObject(string wellUid, string wellboreUid) { - IList currentPrioritizedCurves = await GetPrioritizedCurves(wellUid, wellboreUid); + IList currentPrioritizedCurves = await GetPrioritizedLocalCurves(wellUid, wellboreUid); if (currentPrioritizedCurves != null) { string logCurvePriorityId = GetLogCurvePriorityId(wellUid, wellboreUid); await logCurvePriorityRepository.DeleteDocumentAsync(logCurvePriorityId); } - return; } private string GetLogCurvePriorityId(string wellUid, string wellboreUid) @@ -76,7 +88,7 @@ private string GetLogCurvePriorityId(string wellUid, string wellboreUid) return $"{wellUid}-{wellboreUid}"; } - private LogCurvePriority CreateLogCurvePriorityObject(string wellUid, string wellboreUid, IList prioritizedCurves) + private LogCurvePriority CreateLogCurvePriorityLocalObject(string wellUid, string wellboreUid, IList prioritizedCurves) { string logCurvePriorityId = GetLogCurvePriorityId(wellUid, wellboreUid); LogCurvePriority logCurvePriorityObject = new(logCurvePriorityId) @@ -85,5 +97,28 @@ private LogCurvePriority CreateLogCurvePriorityObject(string wellUid, string wel }; return logCurvePriorityObject; } + + private async Task> CreatePrioritizedUniversalCurves(List logCurvePriorities) + { + LogCurvePriority logCurvePriorityToCreate = CreateLogCurveUniversalPriorityObject(logCurvePriorities); + LogCurvePriority inserted = await logCurvePriorityRepository.CreateDocumentAsync(logCurvePriorityToCreate); + return inserted.PrioritizedCurves; + } + + private async Task> UpdatePrioritizedUniversalCurves(List logCurvePriorities) + { + LogCurvePriority logCurvePriorityToCreate = CreateLogCurveUniversalPriorityObject(logCurvePriorities); + LogCurvePriority updated = await logCurvePriorityRepository.UpdateDocumentAsync(UniversalDbId, logCurvePriorityToCreate); + return updated.PrioritizedCurves; + } + + private LogCurvePriority CreateLogCurveUniversalPriorityObject(List prioritizedCurves) + { + LogCurvePriority logCurvePriorityObject = new(UniversalDbId) + { + PrioritizedCurves = prioritizedCurves + }; + return logCurvePriorityObject; + } } } diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListView.tsx index 511a4f8d8..24aee8012 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListView.tsx @@ -61,10 +61,19 @@ export default function LogCurveInfoListView() { const [hideEmptyMnemonics, setHideEmptyMnemonics] = useState(false); const [showOnlyPrioritizedCurves, setShowOnlyPrioritizedCurves] = useState(false); - const [prioritizedCurves, setPrioritizedCurves] = useState([]); + const [prioritizedLocalCurves, setPrioritizedLocalCurves] = useState< + string[] + >([]); + const [prioritizedUniversalCurves, setPrioritizedUniversalCurves] = useState< + string[] + >([]); const logObjects = new Map([[objectUid, logObject]]); const isDepthIndex = logType === RouterLogType.DEPTH; const isFetching = isFetchingLog || isFetchingLogCurveInfo; + const allPrioritizedCurves = [ + ...prioritizedLocalCurves, + ...prioritizedUniversalCurves + ].filter((value, index, self) => self.indexOf(value) === index); useExpandSidebarNodes( wellUid, @@ -76,16 +85,24 @@ export default function LogCurveInfoListView() { useEffect(() => { if (logObject) { - const getLogCurvePriority = async () => { + const getLogCurveLocalPriority = async () => { const prioritizedCurves = await LogCurvePriorityService.getPrioritizedCurves( + false, wellUid, wellboreUid ); - setPrioritizedCurves(prioritizedCurves); + setPrioritizedLocalCurves(prioritizedCurves); }; - getLogCurvePriority().catch(truncateAbortHandler); + const getLogCurveUniversalPriority = async () => { + const prioritizedCurves = + await LogCurvePriorityService.getPrioritizedCurves(true); + setPrioritizedUniversalCurves(prioritizedCurves); + }; + + getLogCurveLocalPriority().catch(truncateAbortHandler); + getLogCurveUniversalPriority().catch(truncateAbortHandler); setShowOnlyPrioritizedCurves(false); } }, [logObject]); @@ -101,8 +118,10 @@ export default function LogCurveInfoListView() { selectedLog: logObject, selectedServer: connectedServer, servers, - prioritizedCurves, - setPrioritizedCurves + prioritizedLocalCurves, + setPrioritizedLocalCurves, + prioritizedUniversalCurves, + setPrioritizedUniversalCurves }; const position = getContextMenuPosition(event); dispatchOperation({ @@ -130,7 +149,9 @@ export default function LogCurveInfoListView() { setShowOnlyPrioritizedCurves(!showOnlyPrioritizedCurves) } @@ -154,7 +175,7 @@ export default function LogCurveInfoListView() { columns={getColumns( isDepthIndex, showOnlyPrioritizedCurves, - prioritizedCurves, + allPrioritizedCurves, logObjects, hideEmptyMnemonics, true diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/MultiLogsCurveInfoListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/MultiLogsCurveInfoListView.tsx index 05f0ad019..70db3f5d1 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/MultiLogsCurveInfoListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/MultiLogsCurveInfoListView.tsx @@ -48,7 +48,16 @@ export default function MultiLogsCurveInfoListView() { const [hideEmptyMnemonics, setHideEmptyMnemonics] = useState(false); const [showOnlyPrioritizedCurves, setShowOnlyPrioritizedCurves] = useState(false); - const [prioritizedCurves, setPrioritizedCurves] = useState([]); + const [prioritizedLocalCurves, setPrioritizedLocalCurves] = useState< + string[] + >([]); + const [prioritizedUniversalCurves, setPrioritizedUniversalCurves] = useState< + string[] + >([]); + const allPrioritizedCurves = [ + ...prioritizedLocalCurves, + ...prioritizedUniversalCurves + ].filter((value, index, self) => self.indexOf(value) === index); const { objects: allLogs, isFetching: isFetchingLogs } = useGetObjects( connectedServer, wellUid, @@ -87,16 +96,24 @@ export default function MultiLogsCurveInfoListView() { }; getMnemonics(); - const getLogCurvePriority = async () => { + const getLogCurveLocalPriority = async () => { const prioritizedCurves = await LogCurvePriorityService.getPrioritizedCurves( + false, wellUid, wellboreUid ); - setPrioritizedCurves(prioritizedCurves); + setPrioritizedLocalCurves(prioritizedCurves); }; - getLogCurvePriority().catch(truncateAbortHandler); + const getLogCurveUniversalPriority = async () => { + const prioritizedCurves = + await LogCurvePriorityService.getPrioritizedCurves(true); + setPrioritizedUniversalCurves(prioritizedCurves); + }; + + getLogCurveLocalPriority().catch(truncateAbortHandler); + getLogCurveUniversalPriority().catch(truncateAbortHandler); setShowOnlyPrioritizedCurves(false); } }, [allLogs]); @@ -114,8 +131,10 @@ export default function MultiLogsCurveInfoListView() { selectedLog: selectedLog, selectedServer: connectedServer, servers, - prioritizedCurves, - setPrioritizedCurves, + prioritizedLocalCurves, + setPrioritizedLocalCurves, + prioritizedUniversalCurves, + setPrioritizedUniversalCurves, isMultiLog }; const position = getContextMenuPosition(event); @@ -140,7 +159,9 @@ export default function MultiLogsCurveInfoListView() { setShowOnlyPrioritizedCurves(!showOnlyPrioritizedCurves) } @@ -164,7 +185,7 @@ export default function MultiLogsCurveInfoListView() { columns={getColumns( isDepthIndex, showOnlyPrioritizedCurves, - prioritizedCurves, + allPrioritizedCurves, logObjects, hideEmptyMnemonics )} diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurveInfoContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurveInfoContextMenu.tsx index acaaf7bf4..6bcc33487 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurveInfoContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurveInfoContextMenu.tsx @@ -59,8 +59,10 @@ export interface LogCurveInfoContextMenuProps { selectedLog: LogObject; selectedServer: Server; servers: Server[]; - prioritizedCurves: string[]; - setPrioritizedCurves: (prioritizedCurves: string[]) => void; + prioritizedLocalCurves: string[]; + setPrioritizedLocalCurves: (prioritizedLocalCurves: string[]) => void; + prioritizedUniversalCurves: string[]; + setPrioritizedUniversalCurves: (prioritizedUniversalCurves: string[]) => void; isMultiLog?: boolean; } @@ -73,17 +75,26 @@ const LogCurveInfoContextMenu = ( selectedLog, selectedServer, servers, - prioritizedCurves, - setPrioritizedCurves, + prioritizedLocalCurves, + setPrioritizedLocalCurves, + prioritizedUniversalCurves, + setPrioritizedUniversalCurves, isMultiLog = false } = props; const onlyPrioritizedCurvesAreChecked = checkedLogCurveInfoRows.every( (row, index) => - prioritizedCurves.includes(row.mnemonic) || + prioritizedLocalCurves.includes(row.mnemonic) || (checkedLogCurveInfoRows.length > 1 && index === 0) ); + const onlyPrioritizedUniversalCurvesAreChecked = + checkedLogCurveInfoRows.every( + (row, index) => + prioritizedUniversalCurves.includes(row.mnemonic) || + (checkedLogCurveInfoRows.length > 1 && index === 0) + ); + const checkedLogCurveInfoRowsWithoutIndexCurve = checkedLogCurveInfoRows.filter( (lc) => lc.mnemonic !== selectedLog.indexCurve @@ -174,8 +185,22 @@ const LogCurveInfoContextMenu = ( const logCurvePriorityModalProps: LogCurvePriorityModalProps = { wellUid: selectedLog.wellUid, wellboreUid: selectedLog.wellboreUid, - prioritizedCurves, - setPrioritizedCurves + prioritizedCurves: prioritizedLocalCurves, + setPrioritizedCurves: setPrioritizedLocalCurves, + isUniversal: false + }; + dispatchOperation({ + type: OperationType.DisplayModal, + payload: + }); + }; + + const onClickEditUniversalPriority = () => { + dispatchOperation({ type: OperationType.HideContextMenu }); + const logCurvePriorityModalProps: LogCurvePriorityModalProps = { + prioritizedCurves: prioritizedUniversalCurves, + setPrioritizedCurves: setPrioritizedUniversalCurves, + isUniversal: true }; dispatchOperation({ type: OperationType.DisplayModal, @@ -199,36 +224,52 @@ const LogCurveInfoContextMenu = ( }); }; - const onClickSetPriority = async () => { + const onClickSetPriority = async (isUniversal: boolean) => { dispatchOperation({ type: OperationType.HideContextMenu }); const newCurvesToPrioritize = checkedLogCurveInfoRows.map( (lc) => lc.mnemonic ); - const curvesToPrioritize = Array.from( - new Set(prioritizedCurves.concat(newCurvesToPrioritize)) - ); + const curvesToPrioritize = isUniversal + ? Array.from( + new Set(prioritizedUniversalCurves.concat(newCurvesToPrioritize)) + ) + : Array.from( + new Set(prioritizedLocalCurves.concat(newCurvesToPrioritize)) + ); const newPrioritizedCurves = await LogCurvePriorityService.setPrioritizedCurves( + curvesToPrioritize, + isUniversal, selectedLog.wellUid, selectedLog.wellboreUid, - curvesToPrioritize + null ); - setPrioritizedCurves(newPrioritizedCurves); + isUniversal + ? setPrioritizedUniversalCurves(newPrioritizedCurves) + : setPrioritizedLocalCurves(newPrioritizedCurves); }; - const onClickRemovePriority = async () => { + const onClickRemovePriority = async (isUniversal: boolean) => { dispatchOperation({ type: OperationType.HideContextMenu }); const curvesToDelete = checkedLogCurveInfoRows.map((lc) => lc.mnemonic); - const curvesToPrioritize = prioritizedCurves.filter( - (curve) => !curvesToDelete.includes(curve) - ); + const curvesToPrioritize = isUniversal + ? prioritizedUniversalCurves.filter( + (curve) => !curvesToDelete.includes(curve) + ) + : prioritizedLocalCurves.filter( + (curve) => !curvesToDelete.includes(curve) + ); const newPrioritizedCurves = await LogCurvePriorityService.setPrioritizedCurves( + curvesToPrioritize, + isUniversal, selectedLog.wellUid, selectedLog.wellboreUid, - curvesToPrioritize + null ); - setPrioritizedCurves(newPrioritizedCurves); + isUniversal + ? setPrioritizedUniversalCurves(newPrioritizedCurves) + : setPrioritizedLocalCurves(newPrioritizedCurves); }; const toDelete = createComponentReferences( @@ -379,35 +420,73 @@ const LogCurveInfoContextMenu = ( )} , - - onlyPrioritizedCurvesAreChecked - ? onClickRemovePriority() - : onClickSetPriority() - } + - onlyPrioritizedCurvesAreChecked - ? "favoriteFilled" - : "favoriteOutlined" + ? onClickRemovePriority(false) + : onClickSetPriority(false) } - color={colors.interactive.primaryResting} - /> - - {onlyPrioritizedCurvesAreChecked - ? "Remove Priority" - : "Set Priority"} - - , - - - Edit Priority - , + > + + + {onlyPrioritizedCurvesAreChecked + ? "Remove Local Priority" + : "Set Local Priority"} + + + + onlyPrioritizedUniversalCurvesAreChecked + ? onClickRemovePriority(true) + : onClickSetPriority(true) + } + > + + + {onlyPrioritizedUniversalCurvesAreChecked + ? "Remove Universal Priority" + : "Set Universal Priority"} + + + + + Edit Local Priority + + + + Edit Universal Priority + + , , void; + isUniversal: boolean; } export interface LogCurvePriorityRow { @@ -41,12 +43,13 @@ export const LogCurvePriorityModal = ( }); const [checkedCurves, setCheckedCurves] = useState([]); + const [uploadedFile, setUploadedFile] = useState(null); const columns = [ { property: "mnemonic", label: "mnemonic", type: ContentType.String, - width: 500 + width: 440 } ]; @@ -78,14 +81,30 @@ export const LogCurvePriorityModal = ( const onSubmit = async () => { await LogCurvePriorityService.setPrioritizedCurves( + updatedPrioritizedCurves, + props.isUniversal, wellUid, - wellboreUid, - updatedPrioritizedCurves + wellboreUid ); dispatchOperation({ type: OperationType.HideModal }); setPrioritizedCurves(updatedPrioritizedCurves); }; + const handleFileChange = async ( + e: React.ChangeEvent + ): Promise => { + const file = e.target.files.item(0); + if (!file) return; + const text = (await file.text()).replace(/(\r)/gm, "").trim(); + const data = text.split("\n").slice(1); + const mergedArray = [...data, ...updatedPrioritizedCurves]; + const uniqueArray = mergedArray.filter( + (value, index, self) => self.indexOf(value) === index && value !== "" + ); + setUpdatedPrioritizedCurves(uniqueArray); + setUploadedFile(file); + }; + const addCurve = () => { setUpdatedPrioritizedCurves([...updatedPrioritizedCurves, newCurve]); setNewCurve(""); @@ -93,7 +112,11 @@ export const LogCurvePriorityModal = ( return ( @@ -101,13 +124,18 @@ export const LogCurvePriorityModal = ( ) => - setNewCurve(e.target.value) - } + onKeyDown={(e: KeyboardEvent) => { + if (e.key === "Enter") { + e.stopPropagation(); + addCurve(); + } + }} + onChange={(e: ChangeEvent) => { + setNewCurve(e.target.value); + }} value={newCurve} /> + + + + + {uploadedFile?.name ?? "No file chosen"} + + + @@ -143,7 +196,7 @@ const Layout = styled.div` display: grid; grid-template-rows: 1fr auto; max-height: 100%; - gap: 20px; + gap: 40px; `; const AddItemLayout = styled.div` @@ -152,3 +205,13 @@ const AddItemLayout = styled.div` gap: 10px; align-items: end; `; + +const FileContainer = styled.div` + display: flex; + flex-direction: row; + gap: 1rem; + align-items: center; + .MuiButton-root { + min-width: 160px; + } +`; diff --git a/Src/WitsmlExplorer.Frontend/services/logCurvePriorityService.tsx b/Src/WitsmlExplorer.Frontend/services/logCurvePriorityService.tsx index 32fa17bbc..b03ef16ca 100644 --- a/Src/WitsmlExplorer.Frontend/services/logCurvePriorityService.tsx +++ b/Src/WitsmlExplorer.Frontend/services/logCurvePriorityService.tsx @@ -2,40 +2,45 @@ import { ApiClient } from "./apiClient"; export default class LogCurvePriorityService { public static async getPrioritizedCurves( - wellUid: string, - wellboreUid: string, + isUniversal: boolean, + wellUid?: string, + wellboreUid?: string, abortSignal?: AbortSignal ): Promise { - const response = await ApiClient.get( - `/api/wells/${encodeURIComponent(wellUid)}/wellbores/${encodeURIComponent( - wellboreUid - )}/logCurvePriority`, - abortSignal - ); + const path = isUniversal + ? `/api/universal/logCurvePriority` + : `/api/wells/${encodeURIComponent( + wellUid + )}/wellbores/${encodeURIComponent(wellboreUid)}/logCurvePriority`; + const response = await ApiClient.get(path, abortSignal); if (response.ok) { return response.json(); } else { - return []; + return null; } } public static async setPrioritizedCurves( - wellUid: string, - wellboreUid: string, prioritizedCurves: string[], + isUniversal: boolean, + wellUid?: string, + wellboreUid?: string, abortSignal?: AbortSignal ): Promise { + const path = isUniversal + ? `/api/universal/logCurvePriority` + : `/api/wells/${encodeURIComponent( + wellUid + )}/wellbores/${encodeURIComponent(wellboreUid)}/logCurvePriority`; const response = await ApiClient.post( - `/api/wells/${encodeURIComponent(wellUid)}/wellbores/${encodeURIComponent( - wellboreUid - )}/logCurvePriority`, + path, JSON.stringify(prioritizedCurves), abortSignal ); if (response.ok) { return response.json(); } else { - return []; + return null; } } } diff --git a/Tests/WitsmlExplorer.Api.Tests/Services/LogCurvePriorityServiceTests.cs b/Tests/WitsmlExplorer.Api.Tests/Services/LogCurvePriorityServiceTests.cs index 3f7d54aab..6ea4559ea 100644 --- a/Tests/WitsmlExplorer.Api.Tests/Services/LogCurvePriorityServiceTests.cs +++ b/Tests/WitsmlExplorer.Api.Tests/Services/LogCurvePriorityServiceTests.cs @@ -52,31 +52,31 @@ public LogCurvePriorityServiceTests() } [Fact] - public async Task GetPrioritizedCurves_CorrectIds_ReturnsExpectedCurves() + public async Task GetPrioritizedLocalCurves_CorrectIds_ReturnsExpectedCurves() { var wellUid = "well1"; var wellboreUid = "wellbore1"; - var result = await _logCurvePriorityService.GetPrioritizedCurves(wellUid, wellboreUid); + var result = await _logCurvePriorityService.GetPrioritizedLocalCurves(wellUid, wellboreUid); Assert.Equal(_prioritizedCurvesWell1Wellbore1, result); _repository.Verify(repo => repo.GetDocumentAsync(It.Is(id => id == $"{wellUid}-{wellboreUid}")), Times.Once); } [Fact] - public async Task GetPrioritizedCurves_IncorrectId_ReturnsNull() + public async Task GetPrioritizedLocalCurves_IncorrectId_ReturnsNull() { var wellUid = "well"; var wellboreUid = "wellbore"; - var result = await _logCurvePriorityService.GetPrioritizedCurves(wellUid, wellboreUid); + var result = await _logCurvePriorityService.GetPrioritizedLocalCurves(wellUid, wellboreUid); Assert.Null(result); _repository.Verify(repo => repo.GetDocumentAsync(It.Is(id => id == $"{wellUid}-{wellboreUid}")), Times.Once); } [Fact] - public async Task SetPrioritizedCurves_NoExistingPriority_CreatesNewPriority() + public async Task SetPrioritizedLocalCurves_NoExistingPriority_CreatesNewPriority() { var wellUid = "well3"; var wellboreUid = "wellbore3"; @@ -86,7 +86,7 @@ public async Task SetPrioritizedCurves_NoExistingPriority_CreatesNewPriority() PrioritizedCurves = curves }; - var result = await _logCurvePriorityService.SetPrioritizedCurves(wellUid, wellboreUid, curves); + var result = await _logCurvePriorityService.SetPrioritizedLocalCurves(wellUid, wellboreUid, curves); Assert.Equal(curves, result); _repository.Verify(repo => repo.UpdateDocumentAsync(It.IsAny(), It.IsAny()), Times.Never); @@ -94,7 +94,7 @@ public async Task SetPrioritizedCurves_NoExistingPriority_CreatesNewPriority() } [Fact] - public async Task SetPrioritizedCurves_ExistingPriority_ReplacesPriority() + public async Task SetPrioritizedLocalCurves_ExistingPriority_ReplacesPriority() { var wellUid = "well1"; var wellboreUid = "wellbore2"; @@ -104,20 +104,20 @@ public async Task SetPrioritizedCurves_ExistingPriority_ReplacesPriority() PrioritizedCurves = curves }; - var result = await _logCurvePriorityService.SetPrioritizedCurves(wellUid, wellboreUid, curves); + var result = await _logCurvePriorityService.SetPrioritizedLocalCurves(wellUid, wellboreUid, curves); Assert.Equal(curves, result); _repository.Verify(repo => repo.UpdateDocumentAsync($"{wellUid}-{wellboreUid}", It.Is(lcp => lcp.PrioritizedCurves.SequenceEqual(curves))), Times.Once); } [Fact] - public async Task SetPrioritizedCurves_EmptyInput_RemovesPriorityObject() + public async Task SetPrioritizedLocalCurves_EmptyInput_RemovesPriorityObject() { var wellUid = "well2"; var wellboreUid = "wellbore1"; var curves = new List(); - var result = await _logCurvePriorityService.SetPrioritizedCurves(wellUid, wellboreUid, curves); + var result = await _logCurvePriorityService.SetPrioritizedLocalCurves(wellUid, wellboreUid, curves); Assert.Null(result); _repository.Verify(repo => repo.UpdateDocumentAsync(It.IsAny(), It.IsAny()), Times.Never); From 2503924e9b3f0a5b5d04ed716df16d62acdf30b8 Mon Sep 17 00:00:00 2001 From: Robert Basti Date: Mon, 16 Sep 2024 14:43:46 +0200 Subject: [PATCH 102/124] Database should be designed to prevent of duplicate prioritized log curves (#2546) --- Src/WitsmlExplorer.Api/Services/LogCurvePriorityService.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Src/WitsmlExplorer.Api/Services/LogCurvePriorityService.cs b/Src/WitsmlExplorer.Api/Services/LogCurvePriorityService.cs index 04492085c..48877d0e1 100644 --- a/Src/WitsmlExplorer.Api/Services/LogCurvePriorityService.cs +++ b/Src/WitsmlExplorer.Api/Services/LogCurvePriorityService.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.IdentityModel.Tokens; @@ -37,6 +38,7 @@ public async Task> SetPrioritizedLocalCurves(string wellUid, strin return null; } + prioritizedCurves = prioritizedCurves.Distinct().ToList(); string logCurvePriorityId = GetLogCurvePriorityId(wellUid, wellboreUid); LogCurvePriority logCurvePriority = await logCurvePriorityRepository.GetDocumentAsync(logCurvePriorityId); if (logCurvePriority == null) @@ -57,6 +59,7 @@ public async Task> GetPrioritizedUniversalCurves() public async Task> SetPrioritizedUniversalCurves(List prioritizedCurves) { + prioritizedCurves = prioritizedCurves.Distinct().ToList(); var globalDocument = await logCurvePriorityRepository.GetDocumentAsync(UniversalDbId); if (globalDocument == null) { From 244c36a250eeece1c61b74350f87504bbbbb7f24 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Tue, 17 Sep 2024 10:41:34 +0200 Subject: [PATCH 103/124] Add more demo videos (#2547) --- Media/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Media/README.md b/Media/README.md index c6649f671..bab047d9d 100644 --- a/Media/README.md +++ b/Media/README.md @@ -7,10 +7,12 @@ - [Settings](#settings) - [Search and Filter](#search-and-filter) - [Table Overview](#table-overview) +- [Missing Data Agent](#missing-data-agent) - [Query View](#query-view) - [Deeplinking](#deeplinking) - [Object Overview](#object-overview) - [Log Overview](#log-overview) + - [Multi-log functionality](#multi-log-functionality) - [Log Agents](#log-agents) - [Export and Import](#export-and-import) - [Splice](#splice) @@ -42,6 +44,9 @@ https://github.com/user-attachments/assets/bbeff67f-bef9-4919-8efc-86d4ce3070f5 ## Table Overview https://github.com/user-attachments/assets/4055d50d-9996-4fee-83ae-1aeceb939274 +## Missing Data Agent +https://github.com/user-attachments/assets/2de1f0f5-d805-4b03-a41b-985701b402bf + ## Query View https://github.com/user-attachments/assets/3b3608ff-9587-4c34-90f2-432661350269 @@ -54,6 +59,9 @@ https://github.com/user-attachments/assets/e86798ad-c8f9-4213-8f4f-b0e11bf30c57 ## Log Overview https://github.com/user-attachments/assets/28f4417c-45ed-4c16-acaf-99d2a103ddf6 +### Multi-log functionality +https://github.com/user-attachments/assets/651f3040-e82d-4107-8713-62b0ad29bcef + ### Log Agents https://github.com/user-attachments/assets/6848c50d-3995-4bf7-a875-9a9be512413a From 69b5f0b9213906a11b31c51350b7bff83be23a88 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:55:20 +0200 Subject: [PATCH 104/124] update readme (#2549) --- CONTRIBUTING.md | 7 +++---- README.md | 8 +++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f2deafd8b..b2df87894 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,14 +11,13 @@ There are two levels of issues - userstory and issue ### Userstory A userstory is describing a functional (high level) enhancement of the WitsmlExplorer. -Here is the project board of [proposed and ongoing userstories](https://github.com/equinor/witsml-explorer/projects/2), and here is an [overview of the main contributors](https://github.com/equinor/witsml-explorer/wiki) participating on each userstory. -Proposals for new user stories are submitted as new issue - [use the Userstory template](https://github.com/equinor/witsml-explorer/issues/new/choose) . +Proposals for new user stories are submitted as new issue - [use the Userstory template](https://github.com/equinor/witsml-explorer/issues/new/choose). ### Issues -Issues are the feature (code level) enhancement of WitsmlExplorer. Here is the project board for [proposed and ongoing issues](https://github.com/equinor/witsml-explorer/projects/1). +Issues are the feature (code level) enhancement of WitsmlExplorer. Here is the [issue list](https://github.com/equinor/witsml-explorer/issues) and project board for [proposed and ongoing issues](https://github.com/orgs/equinor/projects/789). Feel free to file new issues for bugs, suggest feature requests or improvements and so on. Please relate the issue to an UserStory if possible. -If you wish to contribute with coding please have a look at our [Issues board](https://github.com/equinor/witsml-explorer/projects/1). +If you wish to contribute with coding please have a look at our [Issues board](https://github.com/orgs/equinor/projects/789). Issues that are in the TODO column should be ready to go, assign it to yourself and start working on it :computer: Especially the ones labeled as a `good first issue` might be a good start. Other issues might need some discussion or clarification before it can be started on, give us your thoughts or suggestions on something you would like to work on, and we can take it from there :smiley: diff --git a/README.md b/README.md index 317308f6a..22f6ee4c7 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,14 @@ Please see [Demo Videos](/Media/README.md) ## Key features * Runs directly in your browser, no need to install additional software. +* Local desktop version with a simple installer also available. * An intuitive and easy to use interface. * Connect to any WITSML server running version 1.4.1.1. -* Supported WITSML objects includes: wells, wellbores, bharuns, log objects, curves, messages, rigs, risks, trajectories, trajectory stations, tubulars, tubularcomponents, and wbgeometries. -* Trim log objects and individual curves. +* Supported WITSML objects: wells, wellbores, bharuns, changelogs, fluidsreports, fluids, formation markers, log objects, curves, messages, mudlogs, geology intervals, rigs, risks, trajectories, trajectory stations, tubulars, tubularcomponents, wbgeometries and wbgeometry sections. * Copy objects and sub objects (also between different servers!). -* URL deep linking directly to objects +* URL deep linking directly to objects. +* WITSML query editor. +* Perform QA/QC jobs on logs and curves: edit, splice, compare, analyze gaps, trim, offset and more. ## Witsml as a Nuget package Please see [nuget_witsml.md](/Docs/nuget_witsml.md) From 5092c182745c78335bf944cb223e57cf7cefe30d Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:06:15 +0200 Subject: [PATCH 105/124] Bump desktop version (#2550) --- Src/WitsmlExplorer.Desktop/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/WitsmlExplorer.Desktop/package.json b/Src/WitsmlExplorer.Desktop/package.json index 54379940e..fb3ca944a 100644 --- a/Src/WitsmlExplorer.Desktop/package.json +++ b/Src/WitsmlExplorer.Desktop/package.json @@ -1,7 +1,7 @@ { "name": "WEx-Desktop", "description": "Witsml Explorer Desktop Edition", - "version": "0.3.0", + "version": "0.4.0", "private": true, "author": "Witsml Explorer Team", "repository": "https://github.com/equinor/witsml-explorer", From f34d0b39925758e50e0edf2cb6ba3d0e1d4bb1c7 Mon Sep 17 00:00:00 2001 From: Robert Basti Date: Tue, 17 Sep 2024 20:43:09 +0200 Subject: [PATCH 106/124] Unit tests for universal priority curves #2545 (#2548) --- .../Services/LogCurvePriorityServiceTests.cs | 86 +++++++++++++++++-- 1 file changed, 80 insertions(+), 6 deletions(-) diff --git a/Tests/WitsmlExplorer.Api.Tests/Services/LogCurvePriorityServiceTests.cs b/Tests/WitsmlExplorer.Api.Tests/Services/LogCurvePriorityServiceTests.cs index 6ea4559ea..fa698f870 100644 --- a/Tests/WitsmlExplorer.Api.Tests/Services/LogCurvePriorityServiceTests.cs +++ b/Tests/WitsmlExplorer.Api.Tests/Services/LogCurvePriorityServiceTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -19,9 +20,11 @@ public class LogCurvePriorityServiceTests private readonly List _prioritizedCurvesWell1Wellbore1 = new() { "curve1", "curve2" }; private readonly List _prioritizedCurvesWell1Wellbore2 = new() { "A", "B", "C" }; private readonly List _prioritizedCurvesWell2Wellbore1 = new() { "1", "2", "3", "4" }; + private readonly List _prioritizedCurvesUniversal = new() { "A", "B", "C" }; private readonly LogCurvePriority _logCurvePriorityWell1Wellbore1; private readonly LogCurvePriority _logCurvePriorityWell1Wellbore2; private readonly LogCurvePriority _logCurvePriorityWell2Wellbore1; + private readonly LogCurvePriority _logCurvePriorityUniversal; public LogCurvePriorityServiceTests() { @@ -39,6 +42,10 @@ public LogCurvePriorityServiceTests() { PrioritizedCurves = _prioritizedCurvesWell2Wellbore1 }; + _logCurvePriorityUniversal = new LogCurvePriority("universal") + { + PrioritizedCurves = _prioritizedCurvesUniversal + }; _repository.Setup(repo => repo.GetDocumentAsync(It.IsAny())).ReturnsAsync((LogCurvePriority)null); _repository.Setup(repo => repo.GetDocumentAsync("well1-wellbore1")).ReturnsAsync(_logCurvePriorityWell1Wellbore1); @@ -63,6 +70,16 @@ public async Task GetPrioritizedLocalCurves_CorrectIds_ReturnsExpectedCurves() _repository.Verify(repo => repo.GetDocumentAsync(It.Is(id => id == $"{wellUid}-{wellboreUid}")), Times.Once); } + [Fact] + public async Task GetPrioritizedUniversalCurves_CorrectIds_ReturnsExpectedCurves() + { + _repository.Setup(repo => repo.GetDocumentAsync("universal")).ReturnsAsync(_logCurvePriorityUniversal); + var result = await _logCurvePriorityService.GetPrioritizedUniversalCurves(); + + Assert.Equal(_prioritizedCurvesUniversal, result); + _repository.Verify(repo => repo.GetDocumentAsync(It.Is(id => id == "universal")), Times.Once); + } + [Fact] public async Task GetPrioritizedLocalCurves_IncorrectId_ReturnsNull() { @@ -75,12 +92,22 @@ public async Task GetPrioritizedLocalCurves_IncorrectId_ReturnsNull() _repository.Verify(repo => repo.GetDocumentAsync(It.Is(id => id == $"{wellUid}-{wellboreUid}")), Times.Once); } + [Fact] + public async Task GetPrioritizedUniversalCurves_IncorrectId_ReturnsNull() + { + var result = await _logCurvePriorityService.GetPrioritizedUniversalCurves(); + + Assert.Null(result); + _repository.Verify(repo => repo.GetDocumentAsync(It.Is(id => id == $"universal")), Times.Once); + } + [Fact] public async Task SetPrioritizedLocalCurves_NoExistingPriority_CreatesNewPriority() { var wellUid = "well3"; var wellboreUid = "wellbore3"; - var curves = new List { "AA", "AB" }; + var curves = new List { "AA", "AB", "AA" }; + var expectedUniqueCurves = new List { "AA", "AB" }; var logCurvePriority = new LogCurvePriority($"{wellUid}-{wellboreUid}") { PrioritizedCurves = curves @@ -88,9 +115,25 @@ public async Task SetPrioritizedLocalCurves_NoExistingPriority_CreatesNewPriorit var result = await _logCurvePriorityService.SetPrioritizedLocalCurves(wellUid, wellboreUid, curves); - Assert.Equal(curves, result); + Assert.Equal(expectedUniqueCurves, result); + _repository.Verify(repo => repo.UpdateDocumentAsync(It.IsAny(), It.IsAny()), Times.Never); + _repository.Verify(repo => repo.CreateDocumentAsync(It.Is(lcp => lcp.Id == $"{wellUid}-{wellboreUid}" && lcp.PrioritizedCurves.SequenceEqual(expectedUniqueCurves))), Times.Once); + } + + [Fact] + public async Task SetPrioritizedUniversalCurves_NoExistingPriority_CreatesNewPriority() + { + var curves = new List { "AA", "AB", "AA" }; + var expectedUniqueCurves = new List { "AA", "AB" }; + var logCurvePriority = new LogCurvePriority($"universal") + { + PrioritizedCurves = curves + }; + + var result = await _logCurvePriorityService.SetPrioritizedUniversalCurves(curves); + Assert.Equal(expectedUniqueCurves, result); _repository.Verify(repo => repo.UpdateDocumentAsync(It.IsAny(), It.IsAny()), Times.Never); - _repository.Verify(repo => repo.CreateDocumentAsync(It.Is(lcp => lcp.Id == $"{wellUid}-{wellboreUid}" && lcp.PrioritizedCurves.SequenceEqual(curves))), Times.Once); + _repository.Verify(repo => repo.CreateDocumentAsync(It.Is(lcp => lcp.Id == $"universal" && lcp.PrioritizedCurves.SequenceEqual(expectedUniqueCurves))), Times.Once); } [Fact] @@ -98,7 +141,8 @@ public async Task SetPrioritizedLocalCurves_ExistingPriority_ReplacesPriority() { var wellUid = "well1"; var wellboreUid = "wellbore2"; - var curves = new List { "D", "E" }; + var curves = new List { "D", "E", "D" }; + var expectedUniqueCurves = new List { "D", "E" }; var logCurvePriority = new LogCurvePriority($"{wellUid}-{wellboreUid}") { PrioritizedCurves = curves @@ -106,8 +150,25 @@ public async Task SetPrioritizedLocalCurves_ExistingPriority_ReplacesPriority() var result = await _logCurvePriorityService.SetPrioritizedLocalCurves(wellUid, wellboreUid, curves); - Assert.Equal(curves, result); - _repository.Verify(repo => repo.UpdateDocumentAsync($"{wellUid}-{wellboreUid}", It.Is(lcp => lcp.PrioritizedCurves.SequenceEqual(curves))), Times.Once); + Assert.Equal(expectedUniqueCurves, result); + _repository.Verify(repo => repo.UpdateDocumentAsync($"{wellUid}-{wellboreUid}", It.Is(lcp => lcp.PrioritizedCurves.SequenceEqual(expectedUniqueCurves))), Times.Once); + } + + [Fact] + public async Task SetPrioritizedUniversalCurves_ExistingPriority_ReplacesPriority() + { + _repository.Setup(repo => repo.GetDocumentAsync("universal")).ReturnsAsync(_logCurvePriorityUniversal); + var curves = new List { "D", "E", "D" }; + var expectedUniqueCurves = new List { "D", "E" }; + var logCurvePriority = new LogCurvePriority($"universal") + { + PrioritizedCurves = curves + }; + + var result = await _logCurvePriorityService.SetPrioritizedUniversalCurves(curves); + + Assert.Equal(expectedUniqueCurves, result); + _repository.Verify(repo => repo.UpdateDocumentAsync($"universal", It.Is(lcp => lcp.PrioritizedCurves.SequenceEqual(expectedUniqueCurves))), Times.Once); } [Fact] @@ -123,5 +184,18 @@ public async Task SetPrioritizedLocalCurves_EmptyInput_RemovesPriorityObject() _repository.Verify(repo => repo.UpdateDocumentAsync(It.IsAny(), It.IsAny()), Times.Never); _repository.Verify(repo => repo.DeleteDocumentAsync($"{wellUid}-{wellboreUid}"), Times.Once); } + + [Fact] + public async Task SetPrioritizedUniversalCurves_EmptyInput_RemovesPriorityObject() + { + var expectedResult = 0; + _repository.Setup(repo => repo.GetDocumentAsync("universal")).ReturnsAsync(_logCurvePriorityUniversal); + var curves = new List(); + + var result = await _logCurvePriorityService.SetPrioritizedUniversalCurves(curves); + + Assert.Equal(result.Count, expectedResult); + _repository.Verify(repo => repo.UpdateDocumentAsync(It.IsAny(), It.IsAny()), Times.Once); + } } } From c0bd0dbcaeecfd1c64ba1791928a47ec47268a75 Mon Sep 17 00:00:00 2001 From: Robert Basti Date: Tue, 24 Sep 2024 15:30:24 +0200 Subject: [PATCH 107/124] Cascade deletes well (#2556) --- Src/Witsml/ServiceReference/OptionsIn.cs | 5 ++ Src/Witsml/WitsmlClient.cs | 15 +++- Src/WitsmlExplorer.Api/Jobs/DeleteJobs.cs | 10 ++- .../Workers/Delete/DeleteWellWorker.cs | 3 +- .../Workers/Delete/DeleteWellboreWorker.cs | 3 +- .../ContextMenus/WellContextMenu.tsx | 40 +++++------ .../ContextMenus/WellboreContextMenu.tsx | 40 +++++------ .../Modals/ConfirmDeletionModal.tsx | 71 +++++++++++++++++++ .../models/jobs/deleteJobs.ts | 2 + .../ServiceReference/OptionsInTests.cs | 2 +- .../Workers/DeleteWellWorkerTests.cs | 37 ++++++++-- .../Workers/DeleteWellboreWorkerTests.cs | 38 ++++++++-- 12 files changed, 211 insertions(+), 55 deletions(-) create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/ConfirmDeletionModal.tsx diff --git a/Src/Witsml/ServiceReference/OptionsIn.cs b/Src/Witsml/ServiceReference/OptionsIn.cs index 0e05926ad..fde082912 100644 --- a/Src/Witsml/ServiceReference/OptionsIn.cs +++ b/Src/Witsml/ServiceReference/OptionsIn.cs @@ -10,6 +10,7 @@ public record OptionsIn( int? MaxReturnNodes = null, int? RequestLatestValues = null, bool? RequestObjectSelectionCapability = null, + bool? CascadedDelete = null, string OptionsInString = null) { public string OptionsInString { get; init; } = ValidateOptionsInString(OptionsInString); @@ -34,6 +35,10 @@ public string GetKeywords() { keywords.Add($"requestObjectSelectionCapability=true"); } + if (CascadedDelete == true) + { + keywords.Add($"cascadedDelete=true"); + } if (!string.IsNullOrEmpty(OptionsInString)) { keywords.Add(OptionsInString); diff --git a/Src/Witsml/WitsmlClient.cs b/Src/Witsml/WitsmlClient.cs index b02852bff..94b355894 100644 --- a/Src/Witsml/WitsmlClient.cs +++ b/Src/Witsml/WitsmlClient.cs @@ -26,6 +26,7 @@ public interface IWitsmlClient Task UpdateInStoreAsync(T query) where T : IWitsmlQueryType; Task UpdateInStoreAsync(string query, OptionsIn optionsIn = null); Task DeleteFromStoreAsync(T query) where T : IWitsmlQueryType; + Task DeleteFromStoreAsync(T query, OptionsIn optionsIn) where T : IWitsmlQueryType; Task DeleteFromStoreAsync(string query, OptionsIn optionsIn = null); Task TestConnectionAsync(); Task GetCap(); @@ -367,7 +368,17 @@ public async Task UpdateInStoreAsync(string query, OptionsIn optionsIn = throw new Exception($"Error while adding to store: {response.Result} - {errorResponse.Result}. {response.SuppMsgOut}"); } - public async Task DeleteFromStoreAsync(T query) where T : IWitsmlQueryType + public Task DeleteFromStoreAsync(T query) where T : IWitsmlQueryType + { + return DeleteFromStoreAsyncImplementation(query); + } + + public Task DeleteFromStoreAsync(T query, OptionsIn optionsIn) where T : IWitsmlQueryType + { + return DeleteFromStoreAsyncImplementation(query, optionsIn); + } + + private async Task DeleteFromStoreAsyncImplementation(T query, OptionsIn optionsIn = null) where T : IWitsmlQueryType { try { @@ -375,7 +386,7 @@ public async Task DeleteFromStoreAsync(T query) where T : IWitsm { WMLtypeIn = query.TypeName, QueryIn = XmlHelper.Serialize(query), - OptionsIn = string.Empty, + OptionsIn = optionsIn == null ? string.Empty : optionsIn.GetKeywords(), CapabilitiesIn = _clientCapabilities }; diff --git a/Src/WitsmlExplorer.Api/Jobs/DeleteJobs.cs b/Src/WitsmlExplorer.Api/Jobs/DeleteJobs.cs index e3eaf7ec0..9bba2a383 100644 --- a/Src/WitsmlExplorer.Api/Jobs/DeleteJobs.cs +++ b/Src/WitsmlExplorer.Api/Jobs/DeleteJobs.cs @@ -4,6 +4,12 @@ namespace WitsmlExplorer.Api.Jobs { public record DeleteComponentsJob : IDeleteJob { } public record DeleteObjectsJob : IDeleteJob { } - public record DeleteWellboreJob : IDeleteJob { } - public record DeleteWellJob : IDeleteJob { } + public record DeleteWellboreJob : IDeleteJob + { + public bool CascadedDelete { get; init; } + } + public record DeleteWellJob : IDeleteJob + { + public bool CascadedDelete { get; init; } + } } diff --git a/Src/WitsmlExplorer.Api/Workers/Delete/DeleteWellWorker.cs b/Src/WitsmlExplorer.Api/Workers/Delete/DeleteWellWorker.cs index 35231ab25..efa495271 100644 --- a/Src/WitsmlExplorer.Api/Workers/Delete/DeleteWellWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/Delete/DeleteWellWorker.cs @@ -23,10 +23,11 @@ public DeleteWellWorker(ILogger logger, IWitsmlClientProvider wit public override async Task<(WorkerResult, RefreshAction)> Execute(DeleteWellJob job, CancellationToken? cancellationToken = null) { + bool cascadedDelete = job.CascadedDelete; string wellUid = job.ToDelete.WellUid; WitsmlWells witsmlWell = WellQueries.DeleteWitsmlWell(wellUid); - QueryResult result = await GetTargetWitsmlClientOrThrow().DeleteFromStoreAsync(witsmlWell); + QueryResult result = cascadedDelete ? await GetTargetWitsmlClientOrThrow().DeleteFromStoreAsync(witsmlWell, new OptionsIn(CascadedDelete: true)) : await GetTargetWitsmlClientOrThrow().DeleteFromStoreAsync(witsmlWell); if (result.IsSuccessful) { Logger.LogInformation("Deleted well. WellUid: {WellUid}", wellUid); diff --git a/Src/WitsmlExplorer.Api/Workers/Delete/DeleteWellboreWorker.cs b/Src/WitsmlExplorer.Api/Workers/Delete/DeleteWellboreWorker.cs index 18348a6d8..665b5d726 100644 --- a/Src/WitsmlExplorer.Api/Workers/Delete/DeleteWellboreWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/Delete/DeleteWellboreWorker.cs @@ -23,11 +23,12 @@ public DeleteWellboreWorker(ILogger logger, IWitsmlClientProv public override async Task<(WorkerResult, RefreshAction)> Execute(DeleteWellboreJob job, CancellationToken? cancellationToken = null) { + bool cascadedDelete = job.CascadedDelete; string wellUid = job.ToDelete.WellUid; string wellboreUid = job.ToDelete.WellboreUid; WitsmlWellbores witsmlWellbore = WellboreQueries.DeleteWitsmlWellbore(wellUid, wellboreUid); - QueryResult result = await GetTargetWitsmlClientOrThrow().DeleteFromStoreAsync(witsmlWellbore); + QueryResult result = cascadedDelete ? await GetTargetWitsmlClientOrThrow().DeleteFromStoreAsync(witsmlWellbore, new OptionsIn(CascadedDelete: true)) : await GetTargetWitsmlClientOrThrow().DeleteFromStoreAsync(witsmlWellbore); if (result.IsSuccessful) { Logger.LogInformation("Deleted wellbore. WellUid: {WellUid}, WellboreUid: {WellboreUid}", diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx index 724a97f6b..f6f723f55 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx @@ -9,7 +9,9 @@ import { WellRow } from "components/ContentViews/WellsListView"; import ContextMenu from "components/ContextMenus/ContextMenu"; import { StyledIcon } from "components/ContextMenus/ContextMenuUtils"; import NestedMenuItem from "components/ContextMenus/NestedMenuItem"; -import ConfirmModal from "components/Modals/ConfirmModal"; +import ConfirmDeletionModal, { + ConfirmDeletionModalProps +} from "components/Modals/ConfirmDeletionModal"; import DeleteEmptyMnemonicsModal, { DeleteEmptyMnemonicsModalProps } from "components/Modals/DeleteEmptyMnemonicsModal"; @@ -104,37 +106,31 @@ const WellContextMenu = (props: WellContextMenuProps): React.ReactElement => { ); }; - const deleteWell = async () => { + const deleteWell = async (cascadedDelete: boolean) => { dispatchOperation({ type: OperationType.HideContextMenu }); dispatchOperation({ type: OperationType.HideModal }); const job: DeleteWellJob = { toDelete: { wellUid: well.uid, wellName: well.name - } + }, + cascadedDelete }; await JobService.orderJob(JobType.DeleteWell, job); }; const onClickDelete = async () => { - const confirmation = ( - - This will permanently delete {well.name} with uid:{" "} - {well.uid} - - } - onConfirm={deleteWell} - confirmColor={"danger"} - confirmText={"Delete well"} - switchButtonPlaces={true} - /> - ); + const userCredentialsModalProps: ConfirmDeletionModalProps = { + componentType: "well", + objectName: well.name, + objectUid: well.uid, + onSubmit(cascadedDelete) { + deleteWell(cascadedDelete); + } + }; dispatchOperation({ type: OperationType.DisplayModal, - payload: confirmation + payload: }); }; @@ -219,7 +215,11 @@ const WellContextMenu = (props: WellContextMenuProps): React.ReactElement => { New Wellbore , - + { + const deleteWellbore = async (cascadedDelete: boolean) => { dispatchOperation({ type: OperationType.HideContextMenu }); dispatchOperation({ type: OperationType.HideModal }); const job: DeleteWellboreJob = { @@ -116,30 +118,24 @@ const WellboreContextMenu = ( wellboreUid: wellbore.uid, wellName: wellbore.wellName, wellboreName: wellbore.name - } + }, + cascadedDelete }; await JobService.orderJob(JobType.DeleteWellbore, job); }; const onClickDelete = async () => { - const confirmation = ( - - This will permanently delete {wellbore.name} with - uid: {wellbore.uid} - - } - onConfirm={deleteWellbore} - confirmColor={"danger"} - confirmText={"Delete wellbore"} - switchButtonPlaces={true} - /> - ); + const userCredentialsModalProps: ConfirmDeletionModalProps = { + componentType: "wellbore", + objectName: wellbore.name, + objectUid: wellbore.uid, + onSubmit(cascadedDelete) { + deleteWellbore(cascadedDelete); + } + }; dispatchOperation({ type: OperationType.DisplayModal, - payload: confirmation + payload: }); }; @@ -237,7 +233,11 @@ const WellboreContextMenu = ( )} , - + void; +} + +const ConfirmDeletionModal = ( + props: ConfirmDeletionModalProps +): React.ReactElement => { + const { + operationState: { colors }, + dispatchOperation + } = useOperationState(); + + const [cascadedDelete, setCascadedDelete] = useState(false); + + const onConfirmClick = async () => { + props.onSubmit(cascadedDelete); + dispatchOperation({ type: OperationType.HideModal }); + }; + + return ( + + + + This will permanently delete {props.componentType}{" "} + {props.objectName} with uid:{" "} + {props.objectUid} + + + ) => { + setCascadedDelete(e.target.checked); + }} + colors={colors} + /> + + {cascadedDelete && ( + + )} + + + } + onConfirm={onConfirmClick} + confirmColor={"danger"} + confirmText={"Delete " + props.componentType} + switchButtonPlaces={true} + /> + ); +}; + +export default ConfirmDeletionModal; diff --git a/Src/WitsmlExplorer.Frontend/models/jobs/deleteJobs.ts b/Src/WitsmlExplorer.Frontend/models/jobs/deleteJobs.ts index 01679aefa..ce78f7d17 100644 --- a/Src/WitsmlExplorer.Frontend/models/jobs/deleteJobs.ts +++ b/Src/WitsmlExplorer.Frontend/models/jobs/deleteJobs.ts @@ -12,6 +12,7 @@ export interface DeleteComponentsJob { export interface DeleteWellboreJob { toDelete: WellboreReference; + cascadedDelete: boolean; } export interface DeleteWellJob { @@ -19,4 +20,5 @@ export interface DeleteWellJob { wellUid: string; wellName: string; }; + cascadedDelete: boolean; } diff --git a/Tests/Witsml.Tests/ServiceReference/OptionsInTests.cs b/Tests/Witsml.Tests/ServiceReference/OptionsInTests.cs index 2d6bea24c..44d53857b 100644 --- a/Tests/Witsml.Tests/ServiceReference/OptionsInTests.cs +++ b/Tests/Witsml.Tests/ServiceReference/OptionsInTests.cs @@ -62,7 +62,7 @@ public void GetKeywords_OptionsInString_MultipleKeywords_ReturnsCorrectValue() [Fact] public void GetKeywords_OptionsInStringAndOtherOptions_ReturnsCorrectValue() { - OptionsIn optionsIn = new(ReturnElements.DataOnly, 50, 100, true, "foo=bar;baz=qux"); + OptionsIn optionsIn = new(ReturnElements.DataOnly, 50, 100, true, false, "foo=bar;baz=qux"); Assert.Equal("returnElements=data-only;maxReturnNodes=50;requestLatestValues=100;requestObjectSelectionCapability=true;foo=bar;baz=qux", optionsIn.GetKeywords()); } diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellWorkerTests.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellWorkerTests.cs index 33fb96f21..0ed840eb1 100644 --- a/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellWorkerTests.cs +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellWorkerTests.cs @@ -9,6 +9,7 @@ using Witsml; using Witsml.Data; +using Witsml.ServiceReference; using WitsmlExplorer.Api.Jobs; using WitsmlExplorer.Api.Models; @@ -37,14 +38,15 @@ public DeleteWellWorkerTests() _worker = new DeleteWellWorker(logger, witsmlClientProvider.Object); } - private static DeleteWellJob CreateJob() + private static DeleteWellJob CreateJob(bool cascadedDelete) { return new() { ToDelete = new() { WellUid = WellUid - } + }, + CascadedDelete = cascadedDelete }; } @@ -54,7 +56,18 @@ public async Task Execute_DeleteWell_RefreshAction() _witsmlClient.Setup(client => client.DeleteFromStoreAsync(It.IsAny())) .ReturnsAsync(new QueryResult(true)); - (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob()); + (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob(false)); + Assert.True(result.IsSuccess); + Assert.True(((RefreshWell)refreshAction).WellUid == WellUid); + } + + [Fact] + public async Task Execute_CascadedDeleteWell_RefreshAction() + { + _witsmlClient.Setup(client => client.DeleteFromStoreAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new QueryResult(true)); + + (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob(true)); Assert.True(result.IsSuccess); Assert.True(((RefreshWell)refreshAction).WellUid == WellUid); } @@ -67,10 +80,26 @@ public async Task Execute_DeleteWell_ReturnResult() .Callback((wells) => query = wells) .ReturnsAsync(new QueryResult(true)); - (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob()); + (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob(false)); + Assert.True(result.IsSuccess); + Assert.Single(query.Wells); + Assert.Equal(WellUid, query.Wells.First().Uid); + _witsmlClient.Verify(client => client.DeleteFromStoreAsync(It.IsAny(), It.Is(options => options.CascadedDelete == true)), Times.Never); + } + + [Fact] + public async Task Execute_CascadedDeleteWell_ReturnResult() + { + WitsmlWells query = null; + _witsmlClient.Setup(client => client.DeleteFromStoreAsync(It.IsAny(), It.IsAny())) + .Callback((wells, _) => query = wells) + .ReturnsAsync(new QueryResult(true)); + + (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob(true)); Assert.True(result.IsSuccess); Assert.Single(query.Wells); Assert.Equal(WellUid, query.Wells.First().Uid); + _witsmlClient.Verify(client => client.DeleteFromStoreAsync(It.IsAny(), It.Is(options => options.CascadedDelete == true)), Times.Once); } } } diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellboreWorkerTests.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellboreWorkerTests.cs index ad446bef7..ca0c588ec 100644 --- a/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellboreWorkerTests.cs +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellboreWorkerTests.cs @@ -9,6 +9,7 @@ using Witsml; using Witsml.Data; +using Witsml.ServiceReference; using WitsmlExplorer.Api.Jobs; using WitsmlExplorer.Api.Models; @@ -38,7 +39,7 @@ public DeleteWellboreWorkerTests() _worker = new DeleteWellboreWorker(logger, witsmlClientProvider.Object); } - private static DeleteWellboreJob CreateJob() + private static DeleteWellboreJob CreateJob(bool cascadedDelete) { return new() { @@ -46,7 +47,8 @@ private static DeleteWellboreJob CreateJob() { WellboreUid = WellboreUid, WellUid = WellUid - } + }, + CascadedDelete = cascadedDelete }; } @@ -56,7 +58,19 @@ public async Task Execute_DeleteWellbore_RefreshAction() _witsmlClient.Setup(client => client.DeleteFromStoreAsync(It.IsAny())) .ReturnsAsync(new QueryResult(true)); - (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob()); + (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob(false)); + Assert.True(result.IsSuccess); + Assert.True(((RefreshWellbore)refreshAction).WellboreUid == WellboreUid); + Assert.True(((RefreshWellbore)refreshAction).WellUid == WellUid); + } + + [Fact] + public async Task Execute_CascadedDeleteWellbore_RefreshAction() + { + _witsmlClient.Setup(client => client.DeleteFromStoreAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new QueryResult(true)); + + (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob(true)); Assert.True(result.IsSuccess); Assert.True(((RefreshWellbore)refreshAction).WellboreUid == WellboreUid); Assert.True(((RefreshWellbore)refreshAction).WellUid == WellUid); @@ -70,10 +84,26 @@ public async Task Execute_DeleteWellbore_ReturnResult() .Callback((wellBores) => query = wellBores) .ReturnsAsync(new QueryResult(true)); - (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob()); + (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob(false)); + Assert.True(result.IsSuccess); + Assert.Single(query.Wellbores); + Assert.Equal(WellboreUid, query.Wellbores.First().Uid); + _witsmlClient.Verify(client => client.DeleteFromStoreAsync(It.IsAny(), It.Is(options => options.CascadedDelete == true)), Times.Never); + } + + [Fact] + public async Task Execute_CascadedDeleteWellbore_ReturnResult() + { + WitsmlWellbores query = null; + _witsmlClient.Setup(client => client.DeleteFromStoreAsync(It.IsAny(), It.IsAny())) + .Callback((wellBores, _) => query = wellBores) + .ReturnsAsync(new QueryResult(true)); + + (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob(true)); Assert.True(result.IsSuccess); Assert.Single(query.Wellbores); Assert.Equal(WellboreUid, query.Wellbores.First().Uid); + _witsmlClient.Verify(client => client.DeleteFromStoreAsync(It.IsAny(), It.Is(options => options.CascadedDelete == true)), Times.Once); } } } From 4250d4a1d01fc2517fdfa9f1fb5322216d46c660 Mon Sep 17 00:00:00 2001 From: Robert Basti Date: Wed, 25 Sep 2024 13:02:25 +0200 Subject: [PATCH 108/124] =?UTF-8?q?Incorrect=20URL=20when=20opening=20a=20?= =?UTF-8?q?log=20from=20the=20depth=20overview=20table=F0=9F=90=9B=20#2552?= =?UTF-8?q?=20(#2555)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ContentViews/Charts/ReactLogChart.tsx | 19 +++++++++++++++-- .../ContentViews/FluidsReportListView.tsx | 11 +++++++++- .../ContentViews/LogTypeListView.tsx | 13 +++++++++++- .../components/ContentViews/LogsListView.tsx | 14 ++++++++++++- .../ContentViews/MudLogsListView.tsx | 11 +++++++++- .../ContentViews/TrajectoriesListView.tsx | 11 +++++++++- .../ContentViews/TubularsListView.tsx | 11 +++++++++- .../ContentViews/WbGeometriesListView.tsx | 11 +++++++++- .../WellboreObjectTypesListView.tsx | 21 +++++++++++++++---- .../ContentViews/WellboresListView.tsx | 8 +++++-- .../components/ContentViews/WellsListView.tsx | 4 ++-- 11 files changed, 117 insertions(+), 17 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/Charts/ReactLogChart.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/Charts/ReactLogChart.tsx index c4c3b9920..ce004e936 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/Charts/ReactLogChart.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/Charts/ReactLogChart.tsx @@ -1,9 +1,12 @@ import { DataItem } from "components/ContentViews/Charts/LogsGraph"; +import { useConnectedServer } from "contexts/connectedServerContext"; import type { ECharts, EChartsOption, SetOptionOpts } from "echarts"; import { getInstanceByDom, init } from "echarts"; +import { ObjectType } from "models/objectType"; import type { CSSProperties } from "react"; import { useEffect, useRef } from "react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; +import { getLogObjectViewPath } from "routes/utils/pathBuilder"; export interface ReactEChartsProps { option: EChartsOption; @@ -25,7 +28,10 @@ export function ReactLogChart({ height }: ReactEChartsProps): JSX.Element { const chartRef = useRef(null); + const { wellUid, wellboreUid, logType } = useParams(); + const navigate = useNavigate(); + const { connectedServer } = useConnectedServer(); useEffect(() => { // Initialize chart @@ -36,7 +42,16 @@ export function ReactLogChart({ chart?.on("click", (params) => { const uid = (params.data as DataItem).uid; - navigate(encodeURIComponent(uid)); + navigate( + getLogObjectViewPath( + connectedServer?.url, + wellUid, + wellboreUid, + ObjectType.Log, + logType, + uid + ) + ); }); // Add chart resize listener diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsReportListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsReportListView.tsx index 407266f8b..198e42ff9 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsReportListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/FluidsReportListView.tsx @@ -18,6 +18,7 @@ import { measureToString } from "models/measure"; import { ObjectType } from "models/objectType"; import { MouseEvent } from "react"; import { useNavigate, useParams } from "react-router-dom"; +import { getObjectViewPath } from "routes/utils/pathBuilder"; export interface FluidsReportRow extends ContentTableRow, FluidsReport { fluidsReport: FluidsReport; @@ -90,7 +91,15 @@ export default function FluidsReportsListView() { ]; const onSelect = (fluidsReportRow: FluidsReportRow) => { - navigate(encodeURIComponent(fluidsReportRow.fluidsReport.uid)); + navigate( + getObjectViewPath( + connectedServer?.url, + wellUid, + wellboreUid, + ObjectType.FluidsReport, + fluidsReportRow.fluidsReport.uid + ) + ); }; const onContextMenu = ( diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogTypeListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogTypeListView.tsx index a7fda6ef6..903249f18 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogTypeListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogTypeListView.tsx @@ -3,10 +3,12 @@ import { ContentTableColumn, ContentType } from "components/ContentViews/table"; +import { useConnectedServer } from "contexts/connectedServerContext"; import { useExpandSidebarNodes } from "hooks/useExpandObjectGroupNodes"; import { ObjectType } from "models/objectType"; import { useNavigate, useParams } from "react-router-dom"; import { RouterLogType } from "routes/routerConstants"; +import { getLogObjectsViewPath } from "routes/utils/pathBuilder"; interface LogType { uid: number; @@ -16,6 +18,7 @@ interface LogType { export default function LogTypeListView() { const navigate = useNavigate(); const { wellUid, wellboreUid } = useParams(); + const { connectedServer } = useConnectedServer(); const columns: ContentTableColumn[] = [ { property: "name", label: "Name", type: ContentType.String } @@ -29,8 +32,16 @@ export default function LogTypeListView() { useExpandSidebarNodes(wellUid, wellboreUid, ObjectType.Log); const onSelect = async (logType: any) => { + const logTypePath = + logType.uid === 0 ? RouterLogType.DEPTH : RouterLogType.TIME; navigate( - `${logType.uid === 0 ? RouterLogType.DEPTH : RouterLogType.TIME}/objects` + getLogObjectsViewPath( + connectedServer?.url, + wellUid, + wellboreUid, + ObjectType.Log, + logTypePath + ) ); }; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogsListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogsListView.tsx index c5652f06c..91f67e6dc 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogsListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogsListView.tsx @@ -33,6 +33,7 @@ import { CommonPanelContainer, ContentContainer } from "../StyledComponents/Container"; +import { getLogObjectViewPath } from "routes/utils/pathBuilder"; export interface LogObjectRow extends ContentTableRow, LogObject { logObject: LogObject; @@ -148,7 +149,18 @@ export default function LogsListView() { ]; const onSelect = (log: LogObjectRow) => { - navigate(encodeURIComponent(log.uid)); + navigate( + getLogObjectViewPath( + connectedServer.url, + log.wellUid, + log.wellboreUid, + ObjectType.Log, + (log as LogObject)?.indexType === WITSML_INDEX_TYPE_MD + ? RouterLogType.DEPTH + : RouterLogType.TIME, + log.uid + ) + ); }; if (isFetchedWellbore && !wellbore) { diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/MudLogsListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/MudLogsListView.tsx index ead398aa8..954349737 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/MudLogsListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/MudLogsListView.tsx @@ -18,6 +18,7 @@ import MudLog from "models/mudLog"; import { ObjectType } from "models/objectType"; import { MouseEvent } from "react"; import { useNavigate, useParams } from "react-router-dom"; +import { getObjectViewPath } from "routes/utils/pathBuilder"; export interface MudLogRow extends ContentTableRow { mudLog: MudLog; @@ -49,7 +50,15 @@ export default function MudLogsListView() { useExpandSidebarNodes(wellUid, wellboreUid, ObjectType.MudLog); const onSelect = (mudLogRow: MudLogRow) => { - navigate(encodeURIComponent(mudLogRow.mudLog.uid)); + navigate( + getObjectViewPath( + connectedServer?.url, + wellUid, + wellboreUid, + ObjectType.MudLog, + mudLogRow.mudLog.uid + ) + ); }; const getTableData = (): MudLogRow[] => { diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/TrajectoriesListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/TrajectoriesListView.tsx index 54cffee01..588450043 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/TrajectoriesListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/TrajectoriesListView.tsx @@ -17,6 +17,7 @@ import { ObjectType } from "models/objectType"; import Trajectory from "models/trajectory"; import { MouseEvent } from "react"; import { useNavigate, useParams } from "react-router-dom"; +import { getObjectViewPath } from "routes/utils/pathBuilder"; export default function TrajectoriesListView() { const { @@ -102,7 +103,15 @@ export default function TrajectoriesListView() { ]; const onSelect = (trajectory: any) => { - navigate(encodeURIComponent(trajectory.uid)); + navigate( + getObjectViewPath( + connectedServer?.url, + wellUid, + wellboreUid, + ObjectType.Trajectory, + trajectory.uid + ) + ); }; const trajectoryRows = trajectories?.map((trajectory) => { diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/TubularsListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/TubularsListView.tsx index c01b587b4..9df141b7e 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/TubularsListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/TubularsListView.tsx @@ -16,6 +16,7 @@ import { ObjectType } from "models/objectType"; import Tubular from "models/tubular"; import { MouseEvent } from "react"; import { useNavigate, useParams } from "react-router-dom"; +import { getObjectViewPath } from "routes/utils/pathBuilder"; export default function TubularsListView() { const { @@ -70,7 +71,15 @@ export default function TubularsListView() { ]; const onSelect = (tubular: any) => { - navigate(encodeURIComponent(tubular.uid)); + navigate( + getObjectViewPath( + connectedServer?.url, + wellUid, + wellboreUid, + ObjectType.Tubular, + tubular.uid + ) + ); }; const tubularRows = tubulars?.map((tubular) => { diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/WbGeometriesListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/WbGeometriesListView.tsx index 67f9f0c82..63fe0b5a1 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/WbGeometriesListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/WbGeometriesListView.tsx @@ -18,6 +18,7 @@ import { ObjectType } from "models/objectType"; import WbGeometryObject from "models/wbGeometry"; import { MouseEvent } from "react"; import { useNavigate, useParams } from "react-router-dom"; +import { getObjectViewPath } from "routes/utils/pathBuilder"; export interface WbGeometryObjectRow extends ContentTableRow, WbGeometryObject { wbGeometry: WbGeometryObject; @@ -69,7 +70,15 @@ export default function WbGeometriesListView() { }; const onSelect = (wbGeometry: any) => { - navigate(encodeURIComponent(wbGeometry.uid)); + navigate( + getObjectViewPath( + connectedServer?.url, + wellUid, + wellboreUid, + ObjectType.WbGeometry, + wbGeometry.uid + ) + ); }; const columns: ContentTableColumn[] = [ diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboreObjectTypesListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboreObjectTypesListView.tsx index 75b699cd2..102d23b9a 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboreObjectTypesListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboreObjectTypesListView.tsx @@ -14,7 +14,10 @@ import { ObjectType, pluralizeObjectType } from "models/objectType"; import { useContext } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { ItemNotFound } from "routes/ItemNotFound"; -import { LOG_TYPES_PATH, OBJECTS_PATH } from "routes/routerConstants"; +import { + getLogTypesViewPath, + getObjectsViewPath +} from "routes/utils/pathBuilder"; interface ObjectTypeRow extends ContentTableRow { uid: string; @@ -61,9 +64,19 @@ export default function WellboreObjectTypesListView() { const onSelect = async (row: ObjectTypeRow) => { navigate( - `${row.objectType}/${ - row.objectType === ObjectType.Log ? LOG_TYPES_PATH : OBJECTS_PATH - }` + row.objectType == ObjectType.Log + ? getLogTypesViewPath( + connectedServer?.url, + wellbore.wellUid, + wellbore.uid, + ObjectType.Log + ) + : getObjectsViewPath( + connectedServer?.url, + wellbore.wellUid, + wellbore.uid, + row.objectType + ) ); }; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboresListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboresListView.tsx index 633ae83c5..656f606df 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboresListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/WellboresListView.tsx @@ -22,7 +22,7 @@ import Wellbore from "models/wellbore"; import React from "react"; import { useNavigate, useParams } from "react-router-dom"; import { ItemNotFound } from "routes/ItemNotFound"; -import { OBJECT_GROUPS_PATH } from "routes/routerConstants"; +import { getObjectGroupsViewPath } from "routes/utils/pathBuilder"; export interface WellboreRow extends ContentTableRow, Wellbore {} @@ -117,7 +117,11 @@ export default function WellboresListView() { const onSelect = async (wellboreRow: any) => { navigate( - `${encodeURIComponent(wellboreRow.wellbore.uid)}/${OBJECT_GROUPS_PATH}` + getObjectGroupsViewPath( + connectedServer?.url, + wellboreRow.wellUid, + wellboreRow.uid + ) ); }; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/WellsListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/WellsListView.tsx index 8fdbfc5b9..6592ac38d 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/WellsListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/WellsListView.tsx @@ -19,7 +19,7 @@ import { useOperationState } from "hooks/useOperationState"; import Well from "models/well"; import React from "react"; import { useNavigate } from "react-router-dom"; -import { WELLBORES_PATH } from "routes/routerConstants"; +import { getWellboresViewPath } from "routes/utils/pathBuilder"; export interface WellRow extends ContentTableRow, Well {} @@ -54,7 +54,7 @@ export default function WellsListView() { ]; const onSelect = (well: any) => { - navigate(`${encodeURIComponent(well.uid)}/${WELLBORES_PATH}`); + navigate(getWellboresViewPath(connectedServer?.url, well.uid)); }; const onContextMenu = ( From 010251ffda7338ea0e1164d35637f1d8c274ad99 Mon Sep 17 00:00:00 2001 From: matusmlichsk <61700762+matusmlichsk@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:57:20 +0200 Subject: [PATCH 109/124] Eslint v8.* to v9.* flat-configuration update with depending packages (#2566) --- .eslintrc.json | 44 -- .github/workflows/ci_desktop.yml | 3 + .prettierignore | 2 +- Src/WitsmlExplorer.Desktop/package.json | 3 +- Src/WitsmlExplorer.Desktop/src/main/main.ts | 1 + .../UseClipboardComponentReferences.ts | 5 +- .../ContextMenus/UseClipboardReferences.ts | 5 +- .../components/DateFormatter.ts | 2 + .../Modals/MissingDataAgentModal.tsx | 3 +- .../components/Modals/ObjectPickerModal.tsx | 1 + .../components/RefreshHandler.tsx | 5 +- .../components/Sidebar/SearchFilter.tsx | 5 +- .../contexts/filter.tsx | 2 +- Src/WitsmlExplorer.Frontend/package.json | 18 +- .../services/apiClient.tsx | 1 + Src/WitsmlExplorer.Frontend/tsconfig.json | 2 +- eslint.config.js | 68 +++ yarn.lock | 475 +++++++++--------- 18 files changed, 352 insertions(+), 293 deletions(-) delete mode 100644 .eslintrc.json create mode 100644 eslint.config.js diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 8fbdb049a..000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "settings": { - "react": { - "version": "detect" - } - }, - "env": { - "browser": true, - "es2021": true, - "es6": true - }, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react/recommended", - "prettier" - ], - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint", - "react" - ], - "rules": { - // suppress errors for missing 'import React' in files - "react/react-in-jsx-scope": "off", - // allow jsx syntax in js files (for next.js project) - "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }], - "react-hooks/exhaustive-deps": "off", - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/explicit-member-accessibility": 0, - "@typescript-eslint/explicit-function-return-type": 0, - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-use-before-define": 0, - "@typescript-eslint/no-unused-vars": ["error"], - "no-console": [ - "error", - { "allow": ["warn", "error"] } - ], - "react/prop-types": 1, - "no-unused-vars": "off", - "no-empty-pattern": "off" - } -} - diff --git a/.github/workflows/ci_desktop.yml b/.github/workflows/ci_desktop.yml index 0cb7109a1..5bc958a16 100644 --- a/.github/workflows/ci_desktop.yml +++ b/.github/workflows/ci_desktop.yml @@ -22,6 +22,9 @@ jobs: - name: Install dependencies run: yarn --network-timeout 100000 working-directory: ./Src/WitsmlExplorer.Desktop + - name: Linting + run: yarn lint + working-directory: ./Src/WitsmlExplorer.Desktop - name: Package run: yarn electron:pack working-directory: ./Src/WitsmlExplorer.Desktop diff --git a/.prettierignore b/.prettierignore index c1456abaa..97201e9e9 100644 --- a/.prettierignore +++ b/.prettierignore @@ -13,4 +13,4 @@ Src/WitsmlExplorer.Frontend/bin *.yml .prettierignore .github -.eslintrc.json +eslint.config.js diff --git a/Src/WitsmlExplorer.Desktop/package.json b/Src/WitsmlExplorer.Desktop/package.json index fb3ca944a..b1502a119 100644 --- a/Src/WitsmlExplorer.Desktop/package.json +++ b/Src/WitsmlExplorer.Desktop/package.json @@ -13,7 +13,8 @@ "preview": "yarn build:api && electron-vite preview", "electron:pack": "yarn build && electron-builder --dir -c electron-builder.json", "electron:dist": "yarn build && electron-builder -c electron-builder.json", - "test:pack": "cross-env ELECTRON_IS_TEST=true playwright test" + "test:pack": "cross-env ELECTRON_IS_TEST=true playwright test", + "lint": "eslint . --report-unused-disable-directives --max-warnings 0" }, "main": "./dist/main/main.js", "lint-staged": { diff --git a/Src/WitsmlExplorer.Desktop/src/main/main.ts b/Src/WitsmlExplorer.Desktop/src/main/main.ts index 42f172242..f78d1381c 100644 --- a/Src/WitsmlExplorer.Desktop/src/main/main.ts +++ b/Src/WitsmlExplorer.Desktop/src/main/main.ts @@ -51,6 +51,7 @@ function readOrCreateAppConfig() { // Merge the configs to ensure that new properties are added to the existing config. config = { ...defaultConfig, ...existingConfig }; } catch (err) { + console.error(err); config = defaultConfig; } diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/UseClipboardComponentReferences.ts b/Src/WitsmlExplorer.Frontend/components/ContextMenus/UseClipboardComponentReferences.ts index 9e99f578d..0b86615f8 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/UseClipboardComponentReferences.ts +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/UseClipboardComponentReferences.ts @@ -15,6 +15,7 @@ const useClipboardComponentReferences: () => ComponentReferences | null = parseStringToComponentReferences(clipboardText); setReferences(componentReferences); } catch (e) { + console.error(e); //Not a valid object on the clipboard? That is fine, we won't use it. } }; @@ -39,8 +40,8 @@ export function parseStringToComponentReferences( let jsonObject: ComponentReferences; try { jsonObject = JSON.parse(input); - } catch (error) { - throw new Error("Invalid input given."); + } catch (e) { + throw new Error("Invalid input given.", e); } verifyRequiredProperties(jsonObject); return jsonObject; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/UseClipboardReferences.ts b/Src/WitsmlExplorer.Frontend/components/ContextMenus/UseClipboardReferences.ts index 7727050b8..2da571c85 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/UseClipboardReferences.ts +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/UseClipboardReferences.ts @@ -13,6 +13,7 @@ export const useClipboardReferences = ( const objectReferences = parseStringToReferences(clipboardText); setReferences(objectReferences); } catch (e) { + console.error(e); //Not a valid object on the clipboard? That is fine, we won't use it. } }; @@ -45,8 +46,8 @@ export function parseStringToReferences(input: string): ObjectReferences { let jsonObject: ObjectReferences; try { jsonObject = JSON.parse(input); - } catch (error) { - throw new Error("Invalid input given."); + } catch (e) { + throw new Error("Invalid input given.", e); } verifyRequiredProperties(jsonObject); return jsonObject; diff --git a/Src/WitsmlExplorer.Frontend/components/DateFormatter.ts b/Src/WitsmlExplorer.Frontend/components/DateFormatter.ts index da8f7a1cb..dba32f0e8 100644 --- a/Src/WitsmlExplorer.Frontend/components/DateFormatter.ts +++ b/Src/WitsmlExplorer.Frontend/components/DateFormatter.ts @@ -40,9 +40,11 @@ function formatDateString( const offset = getOffsetFromTimeZone(timeZone); return formatInTimeZone(parsed, offset, dateTimeFormat); } catch (e) { + console.error(e); return "Invalid date"; } } + export default formatDateString; export function getOffsetFromTimeZone(timeZone: TimeZone): string { diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/MissingDataAgentModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/MissingDataAgentModal.tsx index 0c0f04c88..645621674 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/MissingDataAgentModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/MissingDataAgentModal.tsx @@ -67,7 +67,8 @@ const MissingDataAgentModal = ( ? checksObj : []; return checks.map((check) => ({ ...check, id: uuid() })); - } catch (error) { + } catch (e) { + console.error(e); return []; } }; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/ObjectPickerModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/ObjectPickerModal.tsx index ffde97068..3b67c7cea 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/ObjectPickerModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/ObjectPickerModal.tsx @@ -109,6 +109,7 @@ const ObjectPickerModal = ({ setFetchError(`The target ${objectType} was not found`); } } catch (e) { + console.error(e); setFetchError("Failed to fetch"); } finally { setIsLoading(false); diff --git a/Src/WitsmlExplorer.Frontend/components/RefreshHandler.tsx b/Src/WitsmlExplorer.Frontend/components/RefreshHandler.tsx index 2841f4efd..4456a17ab 100644 --- a/Src/WitsmlExplorer.Frontend/components/RefreshHandler.tsx +++ b/Src/WitsmlExplorer.Frontend/components/RefreshHandler.tsx @@ -53,9 +53,10 @@ const RefreshHandler = (): React.ReactElement => { } refreshSearchQueries(refreshAction); } - } catch (error) { + } catch (e) { console.error( - `Unable to perform refresh action for action: ${refreshAction.refreshType} and entity: ${refreshAction.entityType}` + `Unable to perform refresh action for action: ${refreshAction.refreshType} and entity: ${refreshAction.entityType}`, + e ); } } diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter.tsx index 0bf41640a..a4232b9a4 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter.tsx @@ -10,6 +10,7 @@ import { useConnectedServer } from "contexts/connectedServerContext"; import { FilterContext, FilterType, + FilterTypes, getFilterTypeInformation, isObjectFilterType, isWellboreFilterType @@ -23,7 +24,7 @@ import styled, { CSSProp } from "styled-components"; import { Colors } from "styles/Colors"; import Icons from "styles/Icons"; -const searchOptions = Object.values(FilterType); +const searchOptions = Object.values(FilterTypes); const SearchFilter = (): React.ReactElement => { const { dispatchOperation } = useOperationState(); @@ -219,9 +220,11 @@ const SearchIconLayout = styled.div` const SearchBarContainer = styled.div` width: 85%; + .small-padding-left { padding-left: 4px; } + .small-padding-right { padding-right: 4px; } diff --git a/Src/WitsmlExplorer.Frontend/contexts/filter.tsx b/Src/WitsmlExplorer.Frontend/contexts/filter.tsx index 712187c0a..6176df3c9 100644 --- a/Src/WitsmlExplorer.Frontend/contexts/filter.tsx +++ b/Src/WitsmlExplorer.Frontend/contexts/filter.tsx @@ -113,7 +113,7 @@ export type FilterType = | WellboreFilterType | WellPropertyFilterType | ObjectFilterType; -export const FilterType = { +export const FilterTypes = { ...WellFilterType, ...WellboreFilterType, ...WellPropertyFilterType, diff --git a/Src/WitsmlExplorer.Frontend/package.json b/Src/WitsmlExplorer.Frontend/package.json index 1faa9a256..aba25068b 100644 --- a/Src/WitsmlExplorer.Frontend/package.json +++ b/Src/WitsmlExplorer.Frontend/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint": "eslint . --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", "test": "vitest run", "test:watch": "vitest watch" @@ -17,11 +17,6 @@ "prettier -w" ] }, - "eslintIgnore": [ - "node_modules/", - "dist/", - "out/" - ], "dependencies": { "@azure/msal-browser": "^2.28.3", "@azure/msal-react": "^1.4.7", @@ -61,6 +56,7 @@ "uuid": "^9.0.0" }, "devDependencies": { + "@eslint/js": "^9.12.0", "@testing-library/jest-dom": "^6.4.5", "@testing-library/react": "^15.0.7", "@testing-library/user-event": "^14.4.3", @@ -73,16 +69,18 @@ "@types/react-window": "^1.8.5", "@types/styled-components": "^5.1.26", "@types/uuidv4": "^5.0.0", - "@typescript-eslint/eslint-plugin": "^7.3.1", - "@typescript-eslint/parser": "^7.3.1", + "@typescript-eslint/eslint-plugin": "^8.8.1", + "@typescript-eslint/parser": "^8.8.1", "@vitejs/plugin-react": "^4.2.1", - "eslint": "^8.57.0", + "eslint": "^9.12.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^27.9.0", - "eslint-plugin-react": "^7.34.1", + "eslint-plugin-react": "^7.37.1", + "globals": "^15.11.0", "jsdom": "^24.0.0", "lint-staged": "^13.0.3", "typescript": "^5.4.3", + "typescript-eslint": "^8.8.1", "vite": "^5.2.8", "vite-tsconfig-paths": "^4.3.2", "vitest": "^1.6.0" diff --git a/Src/WitsmlExplorer.Frontend/services/apiClient.tsx b/Src/WitsmlExplorer.Frontend/services/apiClient.tsx index b6ad0f4b7..af2d987af 100644 --- a/Src/WitsmlExplorer.Frontend/services/apiClient.tsx +++ b/Src/WitsmlExplorer.Frontend/services/apiClient.tsx @@ -249,6 +249,7 @@ export async function getBaseUrl(): Promise { baseUrl = new URL(`${protocol}://${host}${port}`); } } catch (e) { + console.error(e); baseUrl = new URL("http://localhost"); } return baseUrl; diff --git a/Src/WitsmlExplorer.Frontend/tsconfig.json b/Src/WitsmlExplorer.Frontend/tsconfig.json index fa830058d..abebf9ba8 100644 --- a/Src/WitsmlExplorer.Frontend/tsconfig.json +++ b/Src/WitsmlExplorer.Frontend/tsconfig.json @@ -23,7 +23,7 @@ "noFallthroughCasesInSwitch": true, "types": ["vitest/globals"] }, - "exclude": ["node_modules"], + "exclude": ["node_modules", "eslint.config.js"], "include": ["vite-env.d.ts", "**/*.ts", "**/*.tsx", "."], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..70275fbc2 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,68 @@ +const eslint = require("@eslint/js"); +const tsEslint = require("typescript-eslint"); +const globals = require("globals"); +const tsEslintParser = require("@typescript-eslint/parser"); +const tsEslintPlugin = require("@typescript-eslint/eslint-plugin"); +const reactPlugin = require("eslint-plugin-react"); + +module.exports = tsEslint.config( + eslint.configs.recommended, + ...tsEslint.configs.recommended, + { + settings: { + react: { + version: "detect" + } + }, + + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + parser: tsEslintParser, + globals: { + ...globals.browser, + ...globals.node, + ...globals.jest, + React: "readonly", + HeadersInit: "readonly", + RequestInit: "readonly", + NodeJS: "readonly", + JSX: "readonly", + vi: "readonly" + } + }, + files: ["**/*.ts", "**/*.tsx"], + plugins: { + "@typescript-eslint": tsEslintPlugin, + "react": reactPlugin + }, + rules: { + "react-hooks/exhaustive-deps": "off", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/explicit-member-accessibility": "off", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/no-unused-expressions": "off", + "@typescript-eslint/no-duplicate-enum-values": "off", + "no-unused-vars": "off", //we want to ignore this and handle unused vars by @typescript-eslint plugin rule + "no-console": ["error", { allow: ["warn", "error"] }], + "react/prop-types": 1, + "no-empty-pattern": "off" + } + }, + // standalone global ignores config due to default behaviour of minimal matching strategy + { + ignores: [ + "**/*.config.js", + "**/*.config.ts", + "node_modules", + "**/bin", + "**/.idea", + "**/dist", + "**/out", + "**/obj" + ] + } +); diff --git a/yarn.lock b/yarn.lock index 4db3aba0d..714b453f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -720,30 +720,61 @@ dependencies: eslint-visitor-keys "^3.3.0" -"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1": +"@eslint-community/regexpp@^4.10.0": version "4.10.0" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== -"@eslint/eslintrc@^2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" - integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== +"@eslint-community/regexpp@^4.11.0": + version "4.11.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.1.tgz#a547badfc719eb3e5f4b556325e542fbe9d7a18f" + integrity sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q== + +"@eslint/config-array@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.18.0.tgz#37d8fe656e0d5e3dbaea7758ea56540867fd074d" + integrity sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw== + dependencies: + "@eslint/object-schema" "^2.1.4" + debug "^4.3.1" + minimatch "^3.1.2" + +"@eslint/core@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.6.0.tgz#9930b5ba24c406d67a1760e94cdbac616a6eb674" + integrity sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg== + +"@eslint/eslintrc@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.1.0.tgz#dbd3482bfd91efa663cbe7aa1f506839868207b6" + integrity sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.6.0" - globals "^13.19.0" + espree "^10.0.1" + globals "^14.0.0" ignore "^5.2.0" import-fresh "^3.2.1" js-yaml "^4.1.0" minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.57.0": - version "8.57.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" - integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== +"@eslint/js@9.12.0", "@eslint/js@^9.12.0": + version "9.12.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.12.0.tgz#69ca3ca9fab9a808ec6d67b8f6edb156cbac91e1" + integrity sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA== + +"@eslint/object-schema@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.4.tgz#9e69f8bb4031e11df79e03db09f9dbbae1740843" + integrity sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ== + +"@eslint/plugin-kit@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz#8712dccae365d24e9eeecb7b346f85e750ba343d" + integrity sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig== + dependencies: + levn "^0.4.1" "@floating-ui/core@^1.0.0": version "1.6.0" @@ -781,24 +812,28 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== -"@humanwhocodes/config-array@^0.11.14": - version "0.11.14" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" - integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== +"@humanfs/core@^0.19.0": + version "0.19.0" + resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.0.tgz#08db7a8c73bb07673d9ebd925f2dad746411fcec" + integrity sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw== + +"@humanfs/node@^0.16.5": + version "0.16.5" + resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.5.tgz#a9febb7e7ad2aff65890fdc630938f8d20aa84ba" + integrity sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg== dependencies: - "@humanwhocodes/object-schema" "^2.0.2" - debug "^4.3.1" - minimatch "^3.0.5" + "@humanfs/core" "^0.19.0" + "@humanwhocodes/retry" "^0.3.0" "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^2.0.2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" - integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== +"@humanwhocodes/retry@^0.3.0", "@humanwhocodes/retry@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a" + integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA== "@isaacs/cliui@^8.0.2": version "8.0.2" @@ -1044,7 +1079,7 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== -"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": +"@nodelib/fs.walk@^1.2.3": version "1.2.8" resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== @@ -1348,6 +1383,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/estree@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + "@types/fs-extra@9.0.13", "@types/fs-extra@^9.0.11": version "9.0.13" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45" @@ -1506,7 +1546,7 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A== -"@types/semver@^7.3.12", "@types/semver@^7.5.8": +"@types/semver@^7.3.12": version "7.5.8" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== @@ -1561,32 +1601,30 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^7.3.1": - version "7.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.0.tgz#bf34a02f221811505b8bf2f31060c8560c1bb0a3" - integrity sha512-GJWR0YnfrKnsRoluVO3PRb9r5aMZriiMMM/RHj5nnTrBy1/wIgk76XCtCKcnXGjpZQJQRFtGV9/0JJ6n30uwpQ== +"@typescript-eslint/eslint-plugin@8.8.1", "@typescript-eslint/eslint-plugin@^8.8.1": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz#9364b756d4d78bcbdf6fd3e9345e6924c68ad371" + integrity sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "7.7.0" - "@typescript-eslint/type-utils" "7.7.0" - "@typescript-eslint/utils" "7.7.0" - "@typescript-eslint/visitor-keys" "7.7.0" - debug "^4.3.4" + "@typescript-eslint/scope-manager" "8.8.1" + "@typescript-eslint/type-utils" "8.8.1" + "@typescript-eslint/utils" "8.8.1" + "@typescript-eslint/visitor-keys" "8.8.1" graphemer "^1.4.0" ignore "^5.3.1" natural-compare "^1.4.0" - semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/parser@^7.3.1": - version "7.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.7.0.tgz#6b1b3ce76c5de002c43af8ae933613b0f2b4bcc6" - integrity sha512-fNcDm3wSwVM8QYL4HKVBggdIPAy9Q41vcvC/GtDobw3c4ndVT3K6cqudUmjHPw8EAp4ufax0o58/xvWaP2FmTg== +"@typescript-eslint/parser@8.8.1", "@typescript-eslint/parser@^8.8.1": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.8.1.tgz#5952ba2a83bd52024b872f3fdc8ed2d3636073b8" + integrity sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow== dependencies: - "@typescript-eslint/scope-manager" "7.7.0" - "@typescript-eslint/types" "7.7.0" - "@typescript-eslint/typescript-estree" "7.7.0" - "@typescript-eslint/visitor-keys" "7.7.0" + "@typescript-eslint/scope-manager" "8.8.1" + "@typescript-eslint/types" "8.8.1" + "@typescript-eslint/typescript-estree" "8.8.1" + "@typescript-eslint/visitor-keys" "8.8.1" debug "^4.3.4" "@typescript-eslint/scope-manager@5.62.0": @@ -1597,21 +1635,21 @@ "@typescript-eslint/types" "5.62.0" "@typescript-eslint/visitor-keys" "5.62.0" -"@typescript-eslint/scope-manager@7.7.0": - version "7.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.7.0.tgz#3f0db079b275bb8b0cb5be7613fb3130cfb5de77" - integrity sha512-/8INDn0YLInbe9Wt7dK4cXLDYp0fNHP5xKLHvZl3mOT5X17rK/YShXaiNmorl+/U4VKCVIjJnx4Ri5b0y+HClw== +"@typescript-eslint/scope-manager@8.8.1": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.8.1.tgz#b4bea1c0785aaebfe3c4ab059edaea1c4977e7ff" + integrity sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA== dependencies: - "@typescript-eslint/types" "7.7.0" - "@typescript-eslint/visitor-keys" "7.7.0" + "@typescript-eslint/types" "8.8.1" + "@typescript-eslint/visitor-keys" "8.8.1" -"@typescript-eslint/type-utils@7.7.0": - version "7.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.7.0.tgz#36792ff4209a781b058de61631a48df17bdefbc5" - integrity sha512-bOp3ejoRYrhAlnT/bozNQi3nio9tIgv3U5C0mVDdZC7cpcQEDZXvq8inrHYghLVwuNABRqrMW5tzAv88Vy77Sg== +"@typescript-eslint/type-utils@8.8.1": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.8.1.tgz#31f59ec46e93a02b409fb4d406a368a59fad306e" + integrity sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA== dependencies: - "@typescript-eslint/typescript-estree" "7.7.0" - "@typescript-eslint/utils" "7.7.0" + "@typescript-eslint/typescript-estree" "8.8.1" + "@typescript-eslint/utils" "8.8.1" debug "^4.3.4" ts-api-utils "^1.3.0" @@ -1620,10 +1658,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== -"@typescript-eslint/types@7.7.0": - version "7.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.7.0.tgz#23af4d24bf9ce15d8d301236e3e3014143604f27" - integrity sha512-G01YPZ1Bd2hn+KPpIbrAhEWOn5lQBrjxkzHkWvP6NucMXFtfXoevK82hzQdpfuQYuhkvFDeQYbzXCjR1z9Z03w== +"@typescript-eslint/types@8.8.1": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.8.1.tgz#ebe85e0fa4a8e32a24a56adadf060103bef13bd1" + integrity sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q== "@typescript-eslint/typescript-estree@5.62.0": version "5.62.0" @@ -1638,32 +1676,29 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@7.7.0": - version "7.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.0.tgz#b5dd6383b4c6a852d7b256a37af971e8982be97f" - integrity sha512-8p71HQPE6CbxIBy2kWHqM1KGrC07pk6RJn40n0DSc6bMOBBREZxSDJ+BmRzc8B5OdaMh1ty3mkuWRg4sCFiDQQ== +"@typescript-eslint/typescript-estree@8.8.1": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.1.tgz#34649f4e28d32ee49152193bc7dedc0e78e5d1ec" + integrity sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg== dependencies: - "@typescript-eslint/types" "7.7.0" - "@typescript-eslint/visitor-keys" "7.7.0" + "@typescript-eslint/types" "8.8.1" + "@typescript-eslint/visitor-keys" "8.8.1" debug "^4.3.4" - globby "^11.1.0" + fast-glob "^3.3.2" is-glob "^4.0.3" minimatch "^9.0.4" semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/utils@7.7.0": - version "7.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.7.0.tgz#3d2b6606a60ac34f3c625facfb3b3ab7e126f58d" - integrity sha512-LKGAXMPQs8U/zMRFXDZOzmMKgFv3COlxUQ+2NMPhbqgVm6R1w+nU1i4836Pmxu9jZAuIeyySNrN/6Rc657ggig== +"@typescript-eslint/utils@8.8.1": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.8.1.tgz#9e29480fbfa264c26946253daa72181f9f053c9d" + integrity sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@types/json-schema" "^7.0.15" - "@types/semver" "^7.5.8" - "@typescript-eslint/scope-manager" "7.7.0" - "@typescript-eslint/types" "7.7.0" - "@typescript-eslint/typescript-estree" "7.7.0" - semver "^7.6.0" + "@typescript-eslint/scope-manager" "8.8.1" + "@typescript-eslint/types" "8.8.1" + "@typescript-eslint/typescript-estree" "8.8.1" "@typescript-eslint/utils@^5.10.0": version "5.62.0" @@ -1687,19 +1722,14 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@7.7.0": - version "7.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.0.tgz#950148cf1ac11562a2d903fdf7acf76714a2dc9e" - integrity sha512-h0WHOj8MhdhY8YWkzIF30R379y0NqyOHExI9N9KCzvmu05EgG4FumeYa3ccfKUSphyWkWQE1ybVrgz/Pbam6YA== +"@typescript-eslint/visitor-keys@8.8.1": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.1.tgz#0fb1280f381149fc345dfde29f7542ff4e587fc5" + integrity sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag== dependencies: - "@typescript-eslint/types" "7.7.0" + "@typescript-eslint/types" "8.8.1" eslint-visitor-keys "^3.4.3" -"@ungap/structured-clone@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" - integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== - "@vitejs/plugin-react@^4.2.1": version "4.2.1" resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz#744d8e4fcb120fc3dbaa471dadd3483f5a304bb9" @@ -1782,11 +1812,16 @@ acorn-walk@^8.3.2: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== -acorn@^8.11.3, acorn@^8.9.0: +acorn@^8.11.3: version "8.11.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== +acorn@^8.12.0: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -1915,7 +1950,7 @@ array-buffer-byte-length@^1.0.1: call-bind "^1.0.5" is-array-buffer "^3.0.4" -array-includes@^3.1.6, array-includes@^3.1.7: +array-includes@^3.1.6, array-includes@^3.1.8: version "3.1.8" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== @@ -1932,7 +1967,7 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array.prototype.findlast@^1.2.4: +array.prototype.findlast@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== @@ -1964,25 +1999,15 @@ array.prototype.flatmap@^1.3.2: es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" -array.prototype.toreversed@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz#b989a6bf35c4c5051e1dc0325151bf8088954eba" - integrity sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" - -array.prototype.tosorted@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz#c8c89348337e51b8a3c48a9227f9ce93ceedcba8" - integrity sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg== +array.prototype.tosorted@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" + integrity sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== dependencies: - call-bind "^1.0.5" + call-bind "^1.0.7" define-properties "^1.2.1" - es-abstract "^1.22.3" - es-errors "^1.1.0" + es-abstract "^1.23.3" + es-errors "^1.3.0" es-shim-unscopables "^1.0.2" arraybuffer.prototype.slice@^1.0.3: @@ -2695,13 +2720,6 @@ doctrine@^2.1.0: dependencies: esutils "^2.0.2" -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - dom-accessibility-api@^0.5.9: version "0.5.16" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" @@ -2890,7 +2908,7 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2: +es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: version "1.23.3" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== @@ -2949,29 +2967,29 @@ es-define-property@^1.0.0: dependencies: get-intrinsic "^1.2.4" -es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0: +es-errors@^1.2.1, es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== -es-iterator-helpers@^1.0.17: - version "1.0.18" - resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.18.tgz#4d3424f46b24df38d064af6fbbc89274e29ea69d" - integrity sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA== +es-iterator-helpers@^1.0.19: + version "1.1.0" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.1.0.tgz#f6d745d342aea214fe09497e7152170dc333a7a6" + integrity sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw== dependencies: call-bind "^1.0.7" define-properties "^1.2.1" - es-abstract "^1.23.0" + es-abstract "^1.23.3" es-errors "^1.3.0" es-set-tostringtag "^2.0.3" function-bind "^1.1.2" get-intrinsic "^1.2.4" - globalthis "^1.0.3" + globalthis "^1.0.4" has-property-descriptors "^1.0.2" has-proto "^1.0.3" has-symbols "^1.0.3" internal-slot "^1.0.7" - iterator.prototype "^1.1.2" + iterator.prototype "^1.1.3" safe-array-concat "^1.1.2" es-object-atoms@^1.0.0: @@ -3101,29 +3119,29 @@ eslint-plugin-jest@^27.9.0: dependencies: "@typescript-eslint/utils" "^5.10.0" -eslint-plugin-react@^7.34.1: - version "7.34.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz#6806b70c97796f5bbfb235a5d3379ece5f4da997" - integrity sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw== +eslint-plugin-react@^7.37.1: + version "7.37.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz#56493d7d69174d0d828bc83afeffe96903fdadbd" + integrity sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg== dependencies: - array-includes "^3.1.7" - array.prototype.findlast "^1.2.4" + array-includes "^3.1.8" + array.prototype.findlast "^1.2.5" array.prototype.flatmap "^1.3.2" - array.prototype.toreversed "^1.1.2" - array.prototype.tosorted "^1.1.3" + array.prototype.tosorted "^1.1.4" doctrine "^2.1.0" - es-iterator-helpers "^1.0.17" + es-iterator-helpers "^1.0.19" estraverse "^5.3.0" + hasown "^2.0.2" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.1.2" - object.entries "^1.1.7" - object.fromentries "^2.0.7" - object.hasown "^1.1.3" - object.values "^1.1.7" + object.entries "^1.1.8" + object.fromentries "^2.0.8" + object.values "^1.2.0" prop-types "^15.8.1" resolve "^2.0.0-next.5" semver "^6.3.1" - string.prototype.matchall "^4.0.10" + string.prototype.matchall "^4.0.11" + string.prototype.repeat "^1.0.0" eslint-scope@^5.1.1: version "5.1.1" @@ -3133,76 +3151,78 @@ eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" - integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== +eslint-scope@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.1.0.tgz#70214a174d4cbffbc3e8a26911d8bf51b9ae9d30" + integrity sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@^8.57.0: - version "8.57.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" - integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== +eslint-visitor-keys@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz#1f785cc5e81eb7534523d85922248232077d2f8c" + integrity sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg== + +eslint@^9.12.0: + version "9.12.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.12.0.tgz#54fcba2876c90528396da0fa44b6446329031e86" + integrity sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw== dependencies: "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.6.1" - "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.57.0" - "@humanwhocodes/config-array" "^0.11.14" + "@eslint-community/regexpp" "^4.11.0" + "@eslint/config-array" "^0.18.0" + "@eslint/core" "^0.6.0" + "@eslint/eslintrc" "^3.1.0" + "@eslint/js" "9.12.0" + "@eslint/plugin-kit" "^0.2.0" + "@humanfs/node" "^0.16.5" "@humanwhocodes/module-importer" "^1.0.1" - "@nodelib/fs.walk" "^1.2.8" - "@ungap/structured-clone" "^1.2.0" + "@humanwhocodes/retry" "^0.3.1" + "@types/estree" "^1.0.6" + "@types/json-schema" "^7.0.15" ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" debug "^4.3.2" - doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.2.2" - eslint-visitor-keys "^3.4.3" - espree "^9.6.1" - esquery "^1.4.2" + eslint-scope "^8.1.0" + eslint-visitor-keys "^4.1.0" + espree "^10.2.0" + esquery "^1.5.0" esutils "^2.0.2" fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" + file-entry-cache "^8.0.0" find-up "^5.0.0" glob-parent "^6.0.2" - globals "^13.19.0" - graphemer "^1.4.0" ignore "^5.2.0" imurmurhash "^0.1.4" is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" lodash.merge "^4.6.2" minimatch "^3.1.2" natural-compare "^1.4.0" optionator "^0.9.3" - strip-ansi "^6.0.1" text-table "^0.2.0" -espree@^9.6.0, espree@^9.6.1: - version "9.6.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" - integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== +espree@^10.0.1, espree@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.2.0.tgz#f4bcead9e05b0615c968e85f83816bc386a45df6" + integrity sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g== dependencies: - acorn "^8.9.0" + acorn "^8.12.0" acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.4.1" + eslint-visitor-keys "^4.1.0" -esquery@^1.4.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" - integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== +esquery@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== dependencies: estraverse "^5.1.0" @@ -3312,7 +3332,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.9: +fast-glob@^3.2.9, fast-glob@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== @@ -3355,12 +3375,12 @@ fetch-cookie@^2.0.3: set-cookie-parser "^2.4.8" tough-cookie "^4.0.0" -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== dependencies: - flat-cache "^3.0.4" + flat-cache "^4.0.0" filelist@^1.0.4: version "1.0.4" @@ -3389,14 +3409,13 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -flat-cache@^3.0.4: - version "3.2.0" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" - integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== dependencies: flatted "^3.2.9" - keyv "^4.5.3" - rimraf "^3.0.2" + keyv "^4.5.4" flatted@^3.2.9: version "3.3.1" @@ -3574,7 +3593,7 @@ glob@^10.3.10: minipass "^7.0.4" path-scurry "^1.10.2" -glob@^7.1.3, glob@^7.1.6: +glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -3603,12 +3622,15 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.19.0: - version "13.24.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" - integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== - dependencies: - type-fest "^0.20.2" +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== + +globals@^15.11.0: + version "15.11.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-15.11.0.tgz#b96ed4c6998540c6fb824b24b5499216d2438d6e" + integrity sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw== globalthis@^1.0.1, globalthis@^1.0.3: version "1.0.3" @@ -3617,6 +3639,14 @@ globalthis@^1.0.1, globalthis@^1.0.3: dependencies: define-properties "^1.1.3" +globalthis@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" + globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" @@ -4002,11 +4032,6 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-path-inside@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - is-potential-custom-element-name@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" @@ -4098,10 +4123,10 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -iterator.prototype@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" - integrity sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w== +iterator.prototype@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.3.tgz#016c2abe0be3bbdb8319852884f60908ac62bf9c" + integrity sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ== dependencies: define-properties "^1.2.1" get-intrinsic "^1.2.1" @@ -4355,7 +4380,7 @@ jss@10.10.0, jss@^10.10.0: object.assign "^4.1.4" object.values "^1.1.6" -keyv@^4.0.0, keyv@^4.5.3: +keyv@^4.0.0, keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== @@ -4605,7 +4630,7 @@ min-indent@^1.0.0: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -4753,7 +4778,7 @@ object.assign@^4.1.4, object.assign@^4.1.5: has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.7: +object.entries@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== @@ -4762,7 +4787,7 @@ object.entries@^1.1.7: define-properties "^1.2.1" es-object-atoms "^1.0.0" -object.fromentries@^2.0.7: +object.fromentries@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== @@ -4772,16 +4797,7 @@ object.fromentries@^2.0.7: es-abstract "^1.23.2" es-object-atoms "^1.0.0" -object.hasown@^1.1.3: - version "1.1.4" - resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.4.tgz#e270ae377e4c120cdcb7656ce66884a6218283dc" - integrity sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg== - dependencies: - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-object-atoms "^1.0.0" - -object.values@^1.1.6, object.values@^1.1.7: +object.values@^1.1.6, object.values@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== @@ -5288,13 +5304,6 @@ rfdc@^1.3.0: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f" integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg== -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - roarr@^2.15.3: version "2.15.4" resolved "https://registry.yarnpkg.com/roarr/-/roarr-2.15.4.tgz#f5fe795b7b838ccfe35dc608e0282b9eba2e7afd" @@ -5629,7 +5638,7 @@ string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" -string.prototype.matchall@^4.0.10: +string.prototype.matchall@^4.0.11: version "4.0.11" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== @@ -5647,6 +5656,14 @@ string.prototype.matchall@^4.0.10: set-function-name "^2.0.2" side-channel "^1.0.6" +string.prototype.repeat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz#e90872ee0308b29435aa26275f6e1b762daee01a" + integrity sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + string.prototype.trim@^1.2.9: version "1.2.9" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" @@ -5929,11 +5946,6 @@ type-fest@^0.13.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - type-fest@^1.0.2: version "1.4.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" @@ -5983,6 +5995,15 @@ typed-array-length@^1.0.6: is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" +typescript-eslint@^8.8.1: + version "8.8.1" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.8.1.tgz#b375c877b2184d883b6228170bc66f0fca847c9a" + integrity sha512-R0dsXFt6t4SAFjUSKFjMh4pXDtq04SsFKCVGDP3ZOzNP7itF0jBcZYU4fMsZr4y7O7V7Nc751dDeESbe4PbQMQ== + dependencies: + "@typescript-eslint/eslint-plugin" "8.8.1" + "@typescript-eslint/parser" "8.8.1" + "@typescript-eslint/utils" "8.8.1" + typescript@^5.3.3, typescript@^5.4.3: version "5.4.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" From 3c587adf0931a2eb9568492a51b0dc34764925fe Mon Sep 17 00:00:00 2001 From: Robert Basti Date: Mon, 14 Oct 2024 10:35:18 +0200 Subject: [PATCH 110/124] Package 'System.Runtime.Caching' 8.0.0 high severity vulnerability (#2570) --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 153fabb78..cbc306a13 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -30,7 +30,7 @@ - + @@ -40,4 +40,4 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + \ No newline at end of file From e9bf55ae8afd99cb87515967a10c0e8c397607a0 Mon Sep 17 00:00:00 2001 From: matusmlichsk <61700762+matusmlichsk@users.noreply.github.com> Date: Wed, 16 Oct 2024 14:51:10 +0200 Subject: [PATCH 111/124] Real compact UI mode feature (#2569) --- .prettierignore | 1 + .../components/Breadcrumbs.tsx | 38 +++- .../ContentViews/CurveValuesPlot.tsx | 3 +- .../ContentViews/CurveValuesView.tsx | 3 +- .../ContentViews/EditSelectedLogCurveInfo.tsx | 3 +- .../components/ContentViews/LogsListView.tsx | 3 +- .../ContentViews/MultiLogCurveValuesView.tsx | 3 +- .../TemplatePicker/TemplatePicker.tsx | 59 +++--- .../ContentViews/table/ContentTable.tsx | 38 +++- .../components/ContentViews/table/Panel.tsx | 3 +- .../components/ContextMenus/ContextMenu.tsx | 24 +-- .../LogCurvePriorityContextMenu.tsx | 7 +- .../ContextMenus/NestedMenuItem.tsx | 3 +- .../components/Modals/DateTimeField.tsx | 27 ++- .../components/Modals/LogComparisonModal.tsx | 11 +- .../Modals/LogCurvePriorityModal.tsx | 25 +-- .../components/Modals/LogDataImportModal.tsx | 7 +- .../Modals/MissingDataAgentModal.tsx | 5 +- .../components/Modals/OffsetLogCurveModal.tsx | 5 +- .../components/Modals/ReportModal.tsx | 5 +- .../components/Modals/SettingsModal.tsx | 67 ++++--- .../Modals/ShowLogDataOnServerModal.tsx | 5 +- .../components/Modals/SpliceLogsModal.tsx | 5 +- .../components/QueryEditor.tsx | 7 +- .../EndAdornment/EndAdornment.tsx | 31 +++ .../SearchFilter/EndAdornment/index.ts | 1 + .../SearchFilter/FilterIcon/FilterIcon.tsx | 40 ++++ .../Sidebar/SearchFilter/FilterIcon/index.ts | 1 + .../{ => SearchFilter}/SearchFilter.tsx | 179 +++++++++--------- .../StartAdornment/StartAdornment.tsx | 29 +++ .../SearchFilter/StartAdornment/index.ts | 1 + .../components/Sidebar/SearchFilter/index.ts | 1 + .../components/Sidebar/Sidebar.tsx | 4 +- .../SidebarVirtualItem/SidebarVirtualItem.tsx | 6 +- .../components/Sidebar/TreeItem.tsx | 4 +- .../Sidebar/{ => WellItem}/WellItem.tsx | 90 +++++---- .../components/Sidebar/WellItem/index.ts | 1 + .../components/Sidebar/WellItem/styles.ts | 14 ++ .../components/Sidebar/WellboreItem.tsx | 40 ++-- .../components/StyledComponents/Button.tsx | 85 +++++++-- .../StyledAccordion/StyledAccordion.tsx | 44 +++++ .../StyledComponents/StyledAccordion/index.ts | 1 + .../StyledMenu/StyledMenu.tsx | 84 ++++++++ .../StyledComponents/StyledMenu/index.ts | 1 + .../StyledComponents/WellIndicator.tsx | 24 ++- .../CompactEdsProvider/CompactEdsProvider.tsx | 22 +++ .../contexts/CompactEdsProvider/index.ts | 1 + .../contexts/operationStateReducer.tsx | 3 +- Src/WitsmlExplorer.Frontend/routes/Root.tsx | 37 ++-- .../styles/material-eds.tsx | 37 +++- .../tools/themeHelpers.ts | 11 ++ 51 files changed, 775 insertions(+), 374 deletions(-) create mode 100644 Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/EndAdornment/EndAdornment.tsx create mode 100644 Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/EndAdornment/index.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/FilterIcon/FilterIcon.tsx create mode 100644 Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/FilterIcon/index.ts rename Src/WitsmlExplorer.Frontend/components/Sidebar/{ => SearchFilter}/SearchFilter.tsx (53%) create mode 100644 Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/StartAdornment/StartAdornment.tsx create mode 100644 Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/StartAdornment/index.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/index.ts rename Src/WitsmlExplorer.Frontend/components/Sidebar/{ => WellItem}/WellItem.tsx (63%) create mode 100644 Src/WitsmlExplorer.Frontend/components/Sidebar/WellItem/index.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/Sidebar/WellItem/styles.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/StyledComponents/StyledAccordion/StyledAccordion.tsx create mode 100644 Src/WitsmlExplorer.Frontend/components/StyledComponents/StyledAccordion/index.ts create mode 100644 Src/WitsmlExplorer.Frontend/components/StyledComponents/StyledMenu/StyledMenu.tsx create mode 100644 Src/WitsmlExplorer.Frontend/components/StyledComponents/StyledMenu/index.ts create mode 100644 Src/WitsmlExplorer.Frontend/contexts/CompactEdsProvider/CompactEdsProvider.tsx create mode 100644 Src/WitsmlExplorer.Frontend/contexts/CompactEdsProvider/index.ts create mode 100644 Src/WitsmlExplorer.Frontend/tools/themeHelpers.ts diff --git a/.prettierignore b/.prettierignore index 97201e9e9..8823cdbd6 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,6 +6,7 @@ Src/WitsmlExplorer.Console Src/WitsmlExplorer.Frontend/build Src/WitsmlExplorer.Frontend/obj Src/WitsmlExplorer.Frontend/bin +Src/WitsmlExplorer.Frontend/dist .vscode/* diff --git a/Src/WitsmlExplorer.Frontend/components/Breadcrumbs.tsx b/Src/WitsmlExplorer.Frontend/components/Breadcrumbs.tsx index c09036f99..53ff8536f 100644 --- a/Src/WitsmlExplorer.Frontend/components/Breadcrumbs.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Breadcrumbs.tsx @@ -16,32 +16,33 @@ import Well from "models/well"; import Wellbore from "models/wellbore"; import { useEffect, useState } from "react"; import { - NavLink, - NavigateFunction, createSearchParams, + NavigateFunction, + NavLink, useNavigate, useParams, useSearchParams } from "react-router-dom"; import { - getLogObjectViewPath, getLogObjectsViewPath, + getLogObjectViewPath, getLogTypesViewPath, getMultiLogCurveInfoListViewPath, getObjectGroupsViewPath, - getObjectViewPath, getObjectsViewPath, + getObjectViewPath, getWellboresViewPath, getWellsViewPath } from "routes/utils/pathBuilder"; -import styled from "styled-components"; +import styled, { css } from "styled-components"; import { colors } from "styles/Colors"; import Icon from "styles/Icons"; import { v4 as uuid } from "uuid"; +import { UserTheme } from "../contexts/operationStateReducer.tsx"; export function Breadcrumbs() { const { - operationState: { colors } + operationState: { colors, theme } } = useOperationState(); const navigate = useNavigate(); const { @@ -150,7 +151,12 @@ export function Breadcrumbs() { style={{ minWidth: "18" }} /> )} - + {breadcrumbContent.map((breadCrumb, index: number) => ( ` padding-top: 0.2em; height: 1.5rem; overflow: clip; -`; + + ${({ isCompact }) => + !isCompact + ? "" + : css` + li > span { + font-size: 0.8rem; + } + + li > p { + padding-left: 0.5rem; + padding-right: 0.5rem; + } + `} +}`; const Title = styled.p` line-height: 1rem; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesPlot.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesPlot.tsx index ceedce8b3..b10212626 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesPlot.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesPlot.tsx @@ -31,6 +31,7 @@ import React, { import { useParams } from "react-router-dom"; import { RouterLogType } from "routes/routerConstants"; import { Colors } from "styles/Colors"; +import { normaliseThemeForEds } from "../../tools/themeHelpers.ts"; const COLUMN_WIDTH = 135; const MNEMONIC_LABEL_WIDTH = COLUMN_WIDTH - 10; @@ -219,7 +220,7 @@ export const CurveValuesPlot = React.memo( return (
- + setEnableScatter(!enableScatter)} diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx index 6483bdcca..bcd995e4f 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx @@ -69,6 +69,7 @@ import { CommonPanelContainer, ContentContainer } from "../StyledComponents/Container"; +import { normaliseThemeForEds } from "../../tools/themeHelpers.ts"; const TIME_INDEX_START_OFFSET = SECONDS_IN_MINUTE * 20; // offset before log end index that defines the start index for streaming (in seconds). const DEPTH_INDEX_START_OFFSET = 20; // offset before log end index that defines the start index for streaming. @@ -583,7 +584,7 @@ export const CurveValuesView = (): React.ReactElement => { overrideEndIndex={autoRefresh ? getCurrentMaxIndex() : null} onClickRefresh={() => refreshData()} /> - + setShowPlot(!showPlot)} diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/EditSelectedLogCurveInfo.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/EditSelectedLogCurveInfo.tsx index b8a34e0ff..e4e337d7b 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/EditSelectedLogCurveInfo.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/EditSelectedLogCurveInfo.tsx @@ -35,6 +35,7 @@ import { checkIsUrlTooLong } from "routes/utils/checkIsUrlTooLong"; import styled from "styled-components"; import { Colors, dark } from "styles/Colors"; import { createLogCurveValuesSearchParams } from "../../routes/utils/createLogCurveValuesSearchParams"; +import { normaliseThemeForEds } from "../../tools/themeHelpers.ts"; interface EditSelectedLogCurveInfoProps { disabled?: boolean; @@ -156,7 +157,7 @@ const EditSelectedLogCurveInfo = ( return ( selectedMnemonics && ( - + diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogsListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogsListView.tsx index 91f67e6dc..ede0a6975 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogsListView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogsListView.tsx @@ -33,6 +33,7 @@ import { CommonPanelContainer, ContentContainer } from "../StyledComponents/Container"; +import { normaliseThemeForEds } from "../../tools/themeHelpers.ts"; import { getLogObjectViewPath } from "routes/utils/pathBuilder"; export interface LogObjectRow extends ContentTableRow, LogObject { @@ -171,7 +172,7 @@ export default function LogsListView() { <> {isFetching && } - + { <> - + setShowPlot(!showPlot)} diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/TemplatePicker/TemplatePicker.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/TemplatePicker/TemplatePicker.tsx index 2ed864bc2..43ee55e66 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/TemplatePicker/TemplatePicker.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/TemplatePicker/TemplatePicker.tsx @@ -10,10 +10,9 @@ import { DispatchQuery, QueryActionType } from "../../../../../../contexts/queryContext.tsx"; +import StyledMenu from "../../../../../StyledComponents/StyledMenu"; +import { MenuItem } from "@mui/material"; import { useOperationState } from "../../../../../../hooks/useOperationState.tsx"; -import styled from "styled-components"; -import { Menu } from "@equinor/eds-core-react"; -import { Colors } from "../../../../../../styles/Colors.tsx"; type TemplatePickerProps = { dispatchQuery: DispatchQuery; @@ -24,9 +23,7 @@ const TemplatePicker: FC = ({ dispatchQuery, returnElements }) => { - const { - operationState: { colors } - } = useOperationState(); + const { interactive, text } = useOperationState().operationState.colors; const [isTemplateMenuOpen, setIsTemplateMenuOpen] = useState(false); const [menuAnchor, setMenuAnchor] = useState(null); @@ -59,38 +56,32 @@ const TemplatePicker: FC = ({ aria-labelledby="anchor-default" onClose={() => setIsTemplateMenuOpen(false)} anchorEl={menuAnchor} - colors={colors} + slotProps={{ + paper: { + sx: { + maxHeight: "80vh", + overflowY: "scroll" + } + } + }} > - {Object.values(TemplateObjects).map((value) => { - return ( - onTemplateSelect(value)} - > - {value} - - ); - })} + {Object.values(TemplateObjects).map((value) => ( + onTemplateSelect(value)} + sx={{ + "&:hover": { + backgroundColor: interactive.contextMenuItemHover + }, + "color": text.staticIconsDefault + }} + > + {value} + + ))} ); }; export default TemplatePicker; - -const StyledMenu = styled(Menu)<{ colors: Colors }>` - background: ${(props) => props.colors.ui.backgroundLight}; - max-height: 80vh; - overflow-y: scroll; -`; - -const StyledMenuItem = styled(Menu.Item)<{ colors: Colors }>` - &&:hover { - background-color: ${(props) => - props.colors.interactive.contextMenuItemHover}; - } - - color: ${(props) => props.colors.text.staticIconsDefault}; - padding: 4px; -`; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ContentTable.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ContentTable.tsx index 59db86001..63e827e4b 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ContentTable.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ContentTable.tsx @@ -1,16 +1,16 @@ import { TableBody, TableHead } from "@mui/material"; import { ColumnSizingState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, Header, Row, RowData, RowSelectionState, SortDirection, Table, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getSortedRowModel, useReactTable } from "@tanstack/react-table"; import { defaultRangeExtractor, useVirtualizer } from "@tanstack/react-virtual"; @@ -63,6 +63,30 @@ declare module "@tanstack/react-table" { } } +type TableStyleProps = { + cellHeight: number; + headCellHeight: number; + fontSize?: string; +}; + +const sizes: { [key in UserTheme]: TableStyleProps } = { + [UserTheme.Comfortable]: { + cellHeight: 53, + headCellHeight: 55, + fontSize: undefined + }, + [UserTheme.SemiCompact]: { + cellHeight: 30, + headCellHeight: 35, + fontSize: undefined + }, + [UserTheme.Compact]: { + cellHeight: 26, + headCellHeight: 30, + fontSize: "0.8rem" + } +}; + export const ContentTable = React.memo( (contentTableProps: ContentTableProps): React.ReactElement => { const { @@ -96,9 +120,8 @@ export const ContentTable = React.memo( initializeColumnVisibility(viewId) ); const [columnSizing, setColumnSizing] = useState({}); - const isCompactMode = theme === UserTheme.Compact; - const cellHeight = isCompactMode ? 30 : 53; - const headCellHeight = isCompactMode ? 35 : 55; + const cellHeight = sizes[theme].cellHeight; + const headCellHeight = sizes[theme].headCellHeight; const noData = useMemo(() => [], []); const columnDef = useColumnDef( @@ -388,6 +411,7 @@ export const ContentTable = React.memo( style={{ width: cell.column.getSize(), height: cellHeight, + fontSize: sizes[theme].fontSize, left: column.index < stickyLeftColumns ? column.start diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/Panel.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/Panel.tsx index 6ef2312bd..ed446b1e9 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/Panel.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/Panel.tsx @@ -17,6 +17,7 @@ import { useParams } from "react-router-dom"; import styled from "styled-components"; import Icon from "styles/Icons"; import { ContentTableColumn } from "."; +import { normaliseThemeForEds } from "../../../tools/themeHelpers.ts"; export interface PanelProps { checkableRows: boolean; @@ -122,7 +123,7 @@ const Panel = (props: PanelProps) => { return ( - + {selectedItemsText} {selectedColumnsStatus} { const { operationState, dispatchOperation } = useOperationState(); - const { contextMenu, colors } = operationState; + const { contextMenu } = operationState; const handleClose = () => { dispatchOperation({ type: OperationType.HideContextMenu }); @@ -48,28 +46,10 @@ const ContextMenu = (props: ContextMenuProps): React.ReactElement => { : undefined } onContextMenu={preventContextMenuPropagation} - colors={colors} > {props.menuItems} ); }; -export const StyledMenu = styled(Menu)<{ colors: Colors }>` - .MuiPaper-root { - background: ${(props) => props.colors.ui.backgroundLight}; - p { - color: ${(props) => props.colors.infographic.primaryMossGreen}; - } - svg { - fill: ${(props) => props.colors.infographic.primaryMossGreen}; - } - .MuiMenuItem-root:hover { - text-decoration: none; - background-color: ${(props) => - props.colors.interactive.contextMenuItemHover}; - } - } -`; - export default ContextMenu; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurvePriorityContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurvePriorityContextMenu.tsx index 44f55e6ba..30cb5127a 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurvePriorityContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurvePriorityContextMenu.tsx @@ -3,8 +3,9 @@ import { MenuItem } from "@mui/material"; import { useOperationState } from "hooks/useOperationState"; import React from "react"; import { MousePosition } from "../../contexts/operationStateReducer"; -import { StyledMenu, preventContextMenuPropagation } from "./ContextMenu"; +import { preventContextMenuPropagation } from "./ContextMenu"; import { StyledIcon } from "./ContextMenuUtils"; +import StyledMenu from "../StyledComponents/StyledMenu"; export interface LogCurvePriorityContextMenuProps { onDelete: () => void; @@ -17,8 +18,7 @@ export const LogCurvePriorityContextMenu = ( props: LogCurvePriorityContextMenuProps ): React.ReactElement => { const { onDelete, onClose, position, open } = props; - const { operationState } = useOperationState(); - const { colors } = operationState; + const { colors } = useOperationState().operationState; const onClickDelete = async () => { onDelete(); @@ -41,7 +41,6 @@ export const LogCurvePriorityContextMenu = ( : undefined } onContextMenu={preventContextMenuPropagation} - colors={colors} > { label: string; @@ -146,7 +146,6 @@ const NestedMenuItem = React.forwardRef< onClose={() => { setIsSubMenuOpen(false); }} - colors={colors} >
{children} diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/DateTimeField.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/DateTimeField.tsx index 0ef321fcc..fde57a976 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/DateTimeField.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/DateTimeField.tsx @@ -2,10 +2,16 @@ import { TextField } from "@equinor/eds-core-react"; import formatDateString, { validateIsoDateString } from "components/DateFormatter"; -import { DateTimeFormat, TimeZone } from "contexts/operationStateReducer"; +import { + DateTimeFormat, + TimeZone, + UserTheme +} from "contexts/operationStateReducer"; import { useEffect, useState } from "react"; import styled from "styled-components"; import Icon from "styles/Icons"; +import { Box } from "@mui/material"; +import { useOperationState } from "../../hooks/useOperationState.tsx"; interface DateTimeFieldProps { value: string; @@ -32,6 +38,10 @@ export const DateTimeField = ( const { value, label, updateObject, timeZone, disabled } = props; const [initiallyEmpty, setInitiallyEmpty] = useState(false); const isFirefox = navigator.userAgent.includes("Firefox"); + const { + operationState: { theme } + } = useOperationState(); + const isCompact = theme === UserTheme.Compact; useEffect(() => { setInitiallyEmpty(value == null || value === ""); @@ -68,7 +78,13 @@ export const DateTimeField = ( /> {disabled ? null : ( <> - + + + - + How are the logs compared? @@ -280,7 +281,7 @@ const LogComparisonModal = ( - + {!indexTypesMatch && ( Unable to compare the logs due to different log types. Source @@ -357,9 +358,11 @@ export const StyledAccordionHeader = styled(Accordion.Header)<{ colors: Colors; }>` background-color: ${(props) => props.colors.ui.backgroundDefault}; + &:hover { background-color: ${(props) => props.colors.ui.backgroundLight}; } + span { color: ${(props) => props.colors.infographic.primaryMossGreen}; } diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/LogCurvePriorityModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/LogCurvePriorityModal.tsx index 43523bff0..f38e42971 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/LogCurvePriorityModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/LogCurvePriorityModal.tsx @@ -11,9 +11,10 @@ import { getContextMenuPosition, preventContextMenuPropagation } from "../ContextMenus/ContextMenu"; +import { Button as MuiButton, Stack } from "@mui/material"; import { LogCurvePriorityContextMenu } from "../ContextMenus/LogCurvePriorityContextMenu"; import ModalDialog from "./ModalDialog"; -import { Button } from "@mui/material"; +import { Button } from "../StyledComponents/Button.tsx"; export interface LogCurvePriorityModalProps { wellUid?: string; @@ -120,7 +121,7 @@ export const LogCurvePriorityModal = ( content={ <> - + - + - + {uploadedFile?.name ?? "No file chosen"} @@ -199,18 +200,12 @@ const Layout = styled.div` gap: 40px; `; -const AddItemLayout = styled.div` - display: grid; - grid-template-columns: 1fr 0.2fr; - gap: 10px; - align-items: end; -`; - const FileContainer = styled.div` display: flex; flex-direction: row; gap: 1rem; align-items: center; + .MuiButton-root { min-width: 160px; } diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/LogDataImportModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/LogDataImportModal.tsx index 5c1535d88..4a78fde6d 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/LogDataImportModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/LogDataImportModal.tsx @@ -29,10 +29,12 @@ import { parseLASData, parseLASHeader } from "tools/lasFileTools"; +import StyledAccordion from "../StyledComponents/StyledAccordion"; export interface LogDataImportModalProps { targetLog: LogObject; } + interface ImportColumn { index: number; name: string; @@ -191,7 +193,7 @@ const LogDataImportModal = ( - + Limitations @@ -276,7 +278,7 @@ const LogDataImportModal = ( )} - + {hasOverlap && ( )} @@ -299,6 +301,7 @@ const FileContainer = styled.div` flex-direction: row; gap: 1rem; align-items: center; + .MuiButton-root { min-width: 160px; } diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/MissingDataAgentModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/MissingDataAgentModal.tsx index 645621674..1bceb5139 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/MissingDataAgentModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/MissingDataAgentModal.tsx @@ -28,6 +28,7 @@ import JobService, { JobType } from "services/jobService"; import styled from "styled-components"; import { STORAGE_MISSING_DATA_AGENT_CHECKS_KEY } from "tools/localStorageHelpers"; import { v4 as uuid } from "uuid"; +import StyledAccordion from "../StyledComponents/StyledAccordion"; export interface MissingDataAgentModalProps { wellReferences: WellReference[]; @@ -242,7 +243,7 @@ const MissingDataAgentModal = ( isLoading={false} content={ - + Missing Data Agent @@ -260,7 +261,7 @@ const MissingDataAgentModal = ( - + {missingDataChecks.map((missingDataCheck) => ( - + How are the curves offset? @@ -106,7 +107,7 @@ export const OffsetLogCurveModal = ( {isDepthLog ? : } - + {isDepthLog ? ( <> { )} {report.summary?.includes("\n") ? ( - + {report.summary.split("\n")[0]} @@ -163,7 +164,7 @@ export const ReportModal = (props: ReportModal): React.ReactElement => { - + ) : ( {report.summary} )} diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/SettingsModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/SettingsModal.tsx index d6fd59a42..30ec1f098 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/SettingsModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/SettingsModal.tsx @@ -13,21 +13,27 @@ import { import OperationType from "contexts/operationType"; import { useOperationState } from "hooks/useOperationState"; import { getAccountInfo, msalEnabled, signOut } from "msal/MsalAuthProvider"; -import React, { CSSProperties, ChangeEvent, useState } from "react"; +import React, { ChangeEvent, CSSProperties, FC, useState } from "react"; import AuthorizationService from "services/authorizationService"; import styled from "styled-components"; import { dark, light } from "styles/Colors"; import Icon from "styles/Icons"; import { + setLocalStorageItem, STORAGE_DATETIMEFORMAT_KEY, STORAGE_DECIMAL_KEY, STORAGE_HOTKEYS_ENABLED_KEY, STORAGE_MODE_KEY, STORAGE_THEME_KEY, - STORAGE_TIMEZONE_KEY, - setLocalStorageItem + STORAGE_TIMEZONE_KEY } from "tools/localStorageHelpers"; +const iconSizes: { [key in UserTheme]: 24 | 32 | 16 | 18 | 40 } = { + [UserTheme.Compact]: 24, + [UserTheme.SemiCompact]: 32, + [UserTheme.Comfortable]: 32 +}; + const timeZoneLabels: Record = { [TimeZone.Local]: `${getOffsetFromTimeZone(TimeZone.Local)} Local Time`, [TimeZone.Raw]: "Original Time", @@ -147,11 +153,7 @@ const SettingsModal = (): React.ReactElement => { content={
- + { colors={colors} > + - + { - + { - + {
- +
- + { ); }; +type RowIconType = { name: string; style?: CSSProperties }; + +const RowIcon: FC = ({ style, name }) => { + const { + operationState: { theme, colors } + } = useOperationState(); + + return ( + + ); +}; + const HorizontalLayout = styled.div` && { display: flex; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/ShowLogDataOnServerModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/ShowLogDataOnServerModal.tsx index 49ff03900..58771191b 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/ShowLogDataOnServerModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/ShowLogDataOnServerModal.tsx @@ -11,7 +11,7 @@ import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useOperationState } from "hooks/useOperationState"; import { Server } from "models/server"; -import { CSSProperties, ChangeEvent, useState } from "react"; +import { ChangeEvent, CSSProperties, useState } from "react"; import { useParams, useSearchParams } from "react-router-dom"; import { checkIsUrlTooLong } from "routes/utils/checkIsUrlTooLong"; import { createLogCurveValuesSearchParams } from "routes/utils/createLogCurveValuesSearchParams"; @@ -20,6 +20,7 @@ import styled from "styled-components"; import { Colors } from "styles/Colors"; import Icon from "styles/Icons"; import { openRouteInNewWindow } from "tools/windowHelpers"; +import { normaliseThemeForEds } from "../../tools/themeHelpers.ts"; enum IndexRangeOptions { Full = "Full", @@ -134,7 +135,7 @@ export function ShowLogDataOnServerModal() { + { width={ModalWidth.LARGE} content={ <> - + How are the logs spliced? @@ -100,7 +101,7 @@ const SpliceLogsModal = (props: SpliceLogsProps): ReactElement => { - + Priority: diff --git a/Src/WitsmlExplorer.Frontend/components/QueryEditor.tsx b/Src/WitsmlExplorer.Frontend/components/QueryEditor.tsx index 9812f7b02..0b2985be9 100644 --- a/Src/WitsmlExplorer.Frontend/components/QueryEditor.tsx +++ b/Src/WitsmlExplorer.Frontend/components/QueryEditor.tsx @@ -16,6 +16,7 @@ import React, { FC, useState } from "react"; import { Chip } from "./StyledComponents/Chip"; import Icon from "../styles/Icons.tsx"; import { Stack } from "@mui/material"; +import { UserTheme } from "../contexts/operationStateReducer.tsx"; export interface QueryEditorProps { value: string; @@ -35,9 +36,11 @@ export const QueryEditor: FC = ({ const navigate = useNavigate(); const { serverUrl } = useParams(); const { - operationState: { colors } + operationState: { colors, theme } } = useOperationState(); + const isCompact = theme === UserTheme.Compact; + const onLoadInternal = (editor: any) => { editor.renderer.setPadding(10); editor.renderer.setScrollMargin(10); @@ -63,7 +66,7 @@ export const QueryEditor: FC = ({ onChange={onChange} onLoad={onLoadInternal} readOnly={readonly} - fontSize={13} + fontSize={isCompact ? "0.75rem" : 13} showPrintMargin={false} highlightActiveLine={false} setOptions={{ diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/EndAdornment/EndAdornment.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/EndAdornment/EndAdornment.tsx new file mode 100644 index 000000000..d6ca67df3 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/EndAdornment/EndAdornment.tsx @@ -0,0 +1,31 @@ +import { Button } from "../../../StyledComponents/Button.tsx"; +import { Icon } from "@equinor/eds-core-react"; +import { Box } from "@mui/material"; +import React, { FC } from "react"; + +type EndAdornmentProps = { + searchActive: boolean; + onResetFilter: () => void; + onOpenSearch: () => void; + color: string; +}; + +const EndAdornment: FC = ({ + searchActive, + onOpenSearch, + onResetFilter, + color +}) => ( + + {searchActive && ( + + )} + + +); + +export default EndAdornment; diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/EndAdornment/index.ts b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/EndAdornment/index.ts new file mode 100644 index 000000000..747369e82 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/EndAdornment/index.ts @@ -0,0 +1 @@ +export { default } from "./EndAdornment.tsx"; diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/FilterIcon/FilterIcon.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/FilterIcon/FilterIcon.tsx new file mode 100644 index 000000000..2e927f1af --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/FilterIcon/FilterIcon.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import FilterPanel from "../../FilterPanel.tsx"; +import { Box } from "@mui/material"; +import { Icon } from "@equinor/eds-core-react"; +import { Button } from "../../../StyledComponents/Button.tsx"; + +type FilterIconProps = { + color: string; + expanded: boolean; + onClick: () => void; +}; + +const FilterIcon = ({ onClick, expanded, color }: FilterIconProps) => { + return ( + + ); +}; + +FilterIcon.Popup = () => ( + + + +); + +export default FilterIcon; diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/FilterIcon/index.ts b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/FilterIcon/index.ts new file mode 100644 index 000000000..5e27c7b02 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/FilterIcon/index.ts @@ -0,0 +1 @@ +export { default } from "./FilterIcon.tsx"; diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/SearchFilter.tsx similarity index 53% rename from Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter.tsx rename to Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/SearchFilter.tsx index a4232b9a4..f520b0dc3 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/SearchFilter.tsx @@ -1,11 +1,14 @@ -import { EdsProvider, Icon } from "@equinor/eds-core-react"; -import { Divider, TextField } from "@mui/material"; +import { EdsProvider } from "@equinor/eds-core-react"; +import { + inputBaseClasses, + inputLabelClasses, + Stack, + TextField +} from "@mui/material"; import { pluralize } from "components/ContextMenus/ContextMenuUtils"; import OptionsContextMenu, { OptionsContextMenuProps } from "components/ContextMenus/OptionsContextMenu"; -import FilterPanel from "components/Sidebar/FilterPanel"; -import { Button } from "components/StyledComponents/Button"; import { useConnectedServer } from "contexts/connectedServerContext"; import { FilterContext, @@ -17,37 +20,43 @@ import { } from "contexts/filter"; import OperationType from "contexts/operationType"; import { useOperationState } from "hooks/useOperationState"; -import React, { useContext, useEffect, useRef, useState } from "react"; +import React, { + ChangeEventHandler, + KeyboardEventHandler, + ReactElement, + useContext, + useEffect, + useRef, + useState +} from "react"; import { createSearchParams, useNavigate } from "react-router-dom"; import { getSearchViewPath } from "routes/utils/pathBuilder"; -import styled, { CSSProp } from "styled-components"; +import styled, { css } from "styled-components"; import { Colors } from "styles/Colors"; -import Icons from "styles/Icons"; +import EndAdornment from "./EndAdornment"; +import StartAdornment from "./StartAdornment"; +import FilterIcon from "./FilterIcon"; +import { UserTheme } from "../../../contexts/operationStateReducer.tsx"; const searchOptions = Object.values(FilterTypes); -const SearchFilter = (): React.ReactElement => { +const SearchFilter = (): ReactElement => { const { dispatchOperation } = useOperationState(); const { selectedFilter, updateSelectedFilter } = useContext(FilterContext); const { connectedServer } = useConnectedServer(); const selectedOption = selectedFilter?.filterType; const { - operationState: { colors } + operationState: { colors, theme } } = useOperationState(); + + const isCompact = theme === UserTheme.Compact; + const iconColor = colors.interactive.primaryResting; + const [expanded, setExpanded] = useState(false); const [nameFilter, setNameFilter] = useState(selectedFilter.name); const textFieldRef = useRef(null); const navigate = useNavigate(); - const FilterPopup: CSSProp = { - zIndex: 10, - position: "absolute", - width: "inherit", - top: "6.3rem", - minWidth: "174px", - paddingRight: "0.1em" - }; - useEffect(() => { const dispatch = setTimeout(() => { if ( @@ -87,7 +96,7 @@ const SearchFilter = (): React.ReactElement => { setNameFilter(""); }; - const openOptions = () => { + const handleOpenOptions = () => { const contextMenuProps: OptionsContextMenuProps = { dispatchOperation, options: searchOptions, @@ -108,66 +117,56 @@ const SearchFilter = (): React.ReactElement => { }); }; + const handleFilterReset = () => setNameFilter(""); + const handleSearchOpen = () => openSearchView(selectedOption); + + const handleEnterPress: KeyboardEventHandler = ({ key }) => + key == "Enter" ? openSearchView(selectedOption) : null; + + const handleInputChange: ChangeEventHandler = ({ + target + }) => setNameFilter(target.value ?? ""); + + const handleExpandFiltersClick = () => setExpanded(!expanded); + return ( <> - + setNameFilter(event.target.value ?? "")} + fullWidth id="searchField" ref={textFieldRef} + value={nameFilter} + onChange={handleInputChange} variant="outlined" colors={colors} size="small" label={`Search ${pluralize(selectedOption)}`} - onKeyDown={(e) => - e.key == "Enter" ? openSearchView(selectedOption) : null - } + onKeyDown={handleEnterPress} + isCompact={isCompact} InputProps={{ startAdornment: ( - - - + ), endAdornment: ( - - {nameFilter && ( - - )} - - + ), classes: { adornedStart: "small-padding-left", @@ -177,45 +176,37 @@ const SearchFilter = (): React.ReactElement => { /> - setExpanded(!expanded)} - name={expanded ? "activeFilter" : "filter"} - color={colors.interactive.primaryResting} - size={32} - style={{ cursor: "pointer" }} + - - {expanded ? ( -
- -
- ) : ( - <> - )} - + + {expanded ? : null} ); }; -const SearchField = styled(TextField)<{ colors: Colors }>` +const SearchField = styled(TextField)<{ colors: Colors; isCompact: boolean }>` &&& > div > fieldset { - border-color: ${(props) => props.colors.interactive.primaryResting}; + border-color: ${({ colors }) => colors.interactive.primaryResting}; } -`; -const SearchLayout = styled.div<{ colors: Colors }>` - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.6rem 0.375rem 0.5rem 1rem; - position: relative; - padding-right: 6px; - border-bottom: 1px solid ${(props) => props.colors.interactive.disabledBorder}; -`; + ${({ isCompact }) => + !isCompact + ? "" + : css` + .${inputLabelClasses.root} { + font-size: 0.9rem; + } + + .${inputBaseClasses.input} { + padding: 8px 0; + font-size: 0.9rem; + } + `} +} -const SearchIconLayout = styled.div` - display: flex; - align-items: center; `; const SearchBarContainer = styled.div` diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/StartAdornment/StartAdornment.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/StartAdornment/StartAdornment.tsx new file mode 100644 index 000000000..0e598518a --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/StartAdornment/StartAdornment.tsx @@ -0,0 +1,29 @@ +import { Box } from "@mui/material"; +import { Button } from "../../../StyledComponents/Button.tsx"; +import { Icon } from "@equinor/eds-core-react"; +import React, { FC } from "react"; + +type StartAdornmentProps = { + onOpenOptions: () => void; + color: string; + disabled: boolean; +}; + +const StartAdornment: FC = ({ + onOpenOptions, + disabled, + color +}) => ( + + + +); + +export default StartAdornment; diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/StartAdornment/index.ts b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/StartAdornment/index.ts new file mode 100644 index 000000000..e1dd0562e --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/StartAdornment/index.ts @@ -0,0 +1 @@ +export { default } from "./StartAdornment.tsx"; diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/index.ts b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/index.ts new file mode 100644 index 000000000..a88291d0b --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/SearchFilter/index.ts @@ -0,0 +1 @@ +export { default } from "./SearchFilter.tsx"; diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/Sidebar.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/Sidebar.tsx index a183e78ea..f7ddd2c58 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/Sidebar.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/Sidebar.tsx @@ -4,7 +4,6 @@ import { useVirtualizer, Virtualizer } from "@tanstack/react-virtual"; import ProgressSpinner from "components/ProgressSpinner"; import SearchFilter from "components/Sidebar/SearchFilter"; import { useConnectedServer } from "contexts/connectedServerContext"; -import { UserTheme } from "contexts/operationStateReducer"; import { useSidebar } from "contexts/sidebarContext"; import { SidebarActionType } from "contexts/sidebarReducer"; import { useGetWells } from "hooks/query/useGetWells"; @@ -18,6 +17,7 @@ import { InactiveWellsHiddenFilterHelper } from "./InactiveWellsHiddenFilterHelp import { Stack } from "@mui/material"; import SidebarVirtualItem from "./SidebarVirtualItem"; import { calculateWellNodeId } from "../../models/wellbore.tsx"; +import { isInAnyCompactMode } from "../../tools/themeHelpers.ts"; const Sidebar: FC = () => { const { connectedServer } = useConnectedServer(); @@ -28,7 +28,7 @@ const Sidebar: FC = () => { const { operationState: { colors, theme } } = useOperationState(); - const isCompactMode = theme === UserTheme.Compact; + const isCompactMode = isInAnyCompactMode(theme); const filteredWells = useWellFilter(wells) || []; const containerRef = useRef(null); const virtualizer = useVirtualizer({ diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/SidebarVirtualItem/SidebarVirtualItem.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/SidebarVirtualItem/SidebarVirtualItem.tsx index 93f1a5925..7f0a18b38 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/SidebarVirtualItem/SidebarVirtualItem.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/SidebarVirtualItem/SidebarVirtualItem.tsx @@ -2,11 +2,10 @@ import { FC, useLayoutEffect, useRef } from "react"; import { VirtualItem, Virtualizer } from "@tanstack/react-virtual"; import Well from "../../../models/well.tsx"; import { useOperationState } from "../../../hooks/useOperationState.tsx"; -import { UserTheme } from "../../../contexts/operationStateReducer.tsx"; -import WellItem from "../WellItem.tsx"; import { WellIndicator } from "../../StyledComponents/WellIndicator.tsx"; import { Divider } from "@equinor/eds-core-react"; import styled from "styled-components"; +import WellItem from "../WellItem"; const SidebarVirtualItem: FC<{ virtualItem: VirtualItem; @@ -18,7 +17,6 @@ const SidebarVirtualItem: FC<{ operationState: { colors, theme } } = useOperationState(); const rowRef = useRef(); - const isCompactMode = theme === UserTheme.Compact; useLayoutEffect(() => { if (rowRef.current) virtualizer.measureElement(rowRef.current); @@ -34,7 +32,7 @@ const SidebarVirtualItem: FC<{ diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/TreeItem.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/TreeItem.tsx index d0355195e..06848d3d7 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/TreeItem.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/TreeItem.tsx @@ -1,13 +1,13 @@ import { DotProgress } from "@equinor/eds-core-react"; import { Tooltip } from "@mui/material"; import { TreeItem, TreeItemProps } from "@mui/x-tree-view"; -import { UserTheme } from "contexts/operationStateReducer"; import { useOperationState } from "hooks/useOperationState"; import React from "react"; import { NavLink } from "react-router-dom"; import styled from "styled-components"; import { Colors } from "styles/Colors"; import Icon from "styles/Icons"; +import { isInAnyCompactMode } from "../../tools/themeHelpers.ts"; interface StyledTreeItemProps extends TreeItemProps { labelText: string; @@ -23,7 +23,7 @@ const StyledTreeItem = (props: StyledTreeItemProps): React.ReactElement => { const { operationState: { theme } } = useOperationState(); - const isCompactMode = theme === UserTheme.Compact; + const isCompactMode = isInAnyCompactMode(theme); const { operationState: { colors } diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/WellItem.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/WellItem/WellItem.tsx similarity index 63% rename from Src/WitsmlExplorer.Frontend/components/Sidebar/WellItem.tsx rename to Src/WitsmlExplorer.Frontend/components/Sidebar/WellItem/WellItem.tsx index 10f7c0e4e..e0e253e34 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/WellItem.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/WellItem/WellItem.tsx @@ -18,19 +18,23 @@ import { useOperationState } from "hooks/useOperationState"; import { useWellboreFilter } from "hooks/useWellboreFilter"; import Well from "models/well"; import Wellbore, { - calculateWellNodeId, - calculateWellboreNodeId + calculateWellboreNodeId, + calculateWellNodeId } from "models/wellbore"; import React, { MouseEvent } from "react"; import { useParams } from "react-router-dom"; import { getWellboresViewPath } from "routes/utils/pathBuilder"; +import { getStyles } from "./styles.ts"; interface WellItemProps { wellUid: string; } export default function WellItem({ wellUid }: WellItemProps) { - const { dispatchOperation } = useOperationState(); + const { + dispatchOperation, + operationState: { theme } + } = useOperationState(); const { servers } = useGetServers(); const { wellUid: urlWellUid, wellboreUid: urlWellboreUid } = useParams(); const { connectedServer } = useConnectedServer(); @@ -45,6 +49,9 @@ export default function WellItem({ wellUid }: WellItemProps) { wellUid, { enabled: isExpanded } ); + + const generatedSx = getStyles(theme); + const filteredWellbores = useWellboreFilter(wellbores); const isFetching = isFetchingWell || isFetchingWellbores; @@ -68,44 +75,45 @@ export default function WellItem({ wellUid }: WellItemProps) { }); }; + if (!well) return null; + return ( - well && ( - ) => - onContextMenu(event, well) - } - selected={ - calculateWellNodeId(wellUid) === calculateWellNodeId(urlWellUid) - } - key={wellUid} - labelText={well?.name} - nodeId={calculateWellNodeId(wellUid)} - isLoading={isFetching} - to={getWellboresViewPath(connectedServer?.url, wellUid)} - > - {filteredWellbores?.length > 0 ? ( - filteredWellbores.map((wellbore: Wellbore) => ( - - )) - ) : ( - - )} - - ) + ) => + onContextMenu(event, well) + } + selected={ + calculateWellNodeId(wellUid) === calculateWellNodeId(urlWellUid) + } + key={wellUid} + labelText={well?.name} + nodeId={calculateWellNodeId(wellUid)} + isLoading={isFetching} + to={getWellboresViewPath(connectedServer?.url, wellUid)} + sx={generatedSx} + > + {filteredWellbores?.length > 0 ? ( + filteredWellbores.map((wellbore: Wellbore) => ( + + )) + ) : ( + + )} + ); } diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/WellItem/index.ts b/Src/WitsmlExplorer.Frontend/components/Sidebar/WellItem/index.ts new file mode 100644 index 000000000..f0aa6c17d --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/WellItem/index.ts @@ -0,0 +1 @@ +export { default } from "./WellItem"; diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/WellItem/styles.ts b/Src/WitsmlExplorer.Frontend/components/Sidebar/WellItem/styles.ts new file mode 100644 index 000000000..9260b4564 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/WellItem/styles.ts @@ -0,0 +1,14 @@ +import { UserTheme } from "../../../contexts/operationStateReducer.tsx"; +import { SxProps } from "@mui/material"; +import { treeItemClasses } from "@mui/x-tree-view"; + +export const getStyles = (theme: UserTheme): SxProps => { + if (theme === UserTheme.Compact) + return { + [`.${treeItemClasses.label} p`]: { + p: "0.3rem 0.3rem 0.3rem 0rem" + } + }; + + return {}; +}; diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/WellboreItem.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/WellboreItem.tsx index 640272f35..ecbbd7984 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/WellboreItem.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/WellboreItem.tsx @@ -26,7 +26,6 @@ import WellboreContextMenu, { import ObjectGroupItem from "components/Sidebar/ObjectGroupItem"; import TreeItem from "components/Sidebar/TreeItem"; import { useConnectedServer } from "contexts/connectedServerContext"; -import { UserTheme } from "contexts/operationStateReducer"; import OperationType from "contexts/operationType"; import { useGetServers } from "hooks/query/useGetServers"; import { useGetWellbore } from "hooks/query/useGetWellbore"; @@ -45,6 +44,13 @@ interface WellboreItemProps { nodeId: string; } +type ContextEventType = MouseEvent; + +type ContextMenuActionHandler = ( + e: ContextEventType, + wellbore: Wellbore +) => void; + export default function WellboreItem({ wellUid, wellboreUid, @@ -56,7 +62,7 @@ export default function WellboreItem({ dispatchOperation, operationState: { theme } } = useOperationState(); - const isCompactMode = theme === UserTheme.Compact; + const { operationState: { colors } } = useOperationState(); @@ -67,10 +73,7 @@ export default function WellboreItem({ wellboreUid ); - const onContextMenu = ( - event: MouseEvent, - wellbore: Wellbore - ) => { + const onContextMenu: ContextMenuActionHandler = (event, wellbore) => { preventContextMenuPropagation(event); const contextMenuProps: WellboreContextMenuProps = { servers, @@ -86,10 +89,7 @@ export default function WellboreItem({ }); }; - const onLogsContextMenu = ( - event: MouseEvent, - wellbore: Wellbore - ) => { + const onLogsContextMenu: ContextMenuActionHandler = (event, wellbore) => { preventContextMenuPropagation(event); const contextMenuProps: LogsContextMenuProps = { dispatchOperation, @@ -106,10 +106,7 @@ export default function WellboreItem({ }); }; - const onRigsContextMenu = ( - event: MouseEvent, - wellbore: Wellbore - ) => { + const onRigsContextMenu: ContextMenuActionHandler = (event, wellbore) => { preventContextMenuPropagation(event); const contextMenuProps: RigsContextMenuProps = { wellbore, @@ -125,10 +122,7 @@ export default function WellboreItem({ }); }; - const onTubularsContextMenu = ( - event: MouseEvent, - wellbore: Wellbore - ) => { + const onTubularsContextMenu: ContextMenuActionHandler = (event, wellbore) => { preventContextMenuPropagation(event); const contextMenuProps: TubularsContextMenuProps = { wellbore, @@ -144,9 +138,9 @@ export default function WellboreItem({ }); }; - const onTrajectoryContextMenu = ( - event: MouseEvent, - wellbore: Wellbore + const onTrajectoryContextMenu: ContextMenuActionHandler = ( + event, + wellbore ) => { preventContextMenuPropagation(event); const contextMenuProps: TrajectoriesContextMenuProps = { @@ -170,7 +164,7 @@ export default function WellboreItem({ return ( ) => + onContextMenu={(event: ContextEventType) => onContextMenu(event, wellbore) } key={nodeId} @@ -255,7 +249,7 @@ export default function WellboreItem({ /> diff --git a/Src/WitsmlExplorer.Frontend/components/StyledComponents/Button.tsx b/Src/WitsmlExplorer.Frontend/components/StyledComponents/Button.tsx index 04c27bc7a..e4bb4b953 100644 --- a/Src/WitsmlExplorer.Frontend/components/StyledComponents/Button.tsx +++ b/Src/WitsmlExplorer.Frontend/components/StyledComponents/Button.tsx @@ -24,21 +24,68 @@ type ComposedExoticButton = ForwardRefExoticComponent< Group: typeof EdsButton.Group; }; +const StyledEdsButton = styled(EdsButton)<{ + isCompact: boolean; +}>` + ${({ isCompact }) => + !isCompact + ? "" + : css` + --eds_button__padding_x: 0.5rem; + --eds_button__padding_y_compact: 2px; + + & > span > svg { + height: 18px !important; + width: 18px !important; + } + `} +`; + const ExoticButton = forwardRef((props, ref) => { const { operationState: { colors, theme } } = useOperationState(); + const isCompact = theme === UserTheme.Compact; if (!props.variant || props.variant === "contained") { - return ; + return ( + + ); } else if (props.variant === "contained_icon") { - return ; + return ( + + ); } else if (props.variant === "outlined") { - return ; + return ( + + ); } else if (props.variant === "ghost") { - return ; + return ( + + ); } else if (props.variant === "ghost_icon") { - return ; + return ( + + ); } else if (props.variant === "table_icon") { return ( @@ -48,6 +95,7 @@ const ExoticButton = forwardRef((props, ref) => { variant="ghost_icon" colors={colors} userTheme={theme} + isCompact={isCompact} /> ); @@ -62,29 +110,30 @@ export const Button: ComposedExoticButton = Object.assign(ExoticButton, { Group: EdsButton.Group }); -const ContainedButton = styled(EdsButton)<{ colors: Colors }>` +const ContainedButton = styled(StyledEdsButton)<{ colors: Colors }>` ${(props) => props?.colors?.mode === "dark" - ? ` - &&:disabled { - background: #565656; - border:1px solid #565656; - color:#9CA6AC; - }` + ? css` + &&:disabled { + background: #565656; + border: 1px solid #565656; + color: #9ca6ac; + } + ` : ""}; `; -const GhostButton = styled(EdsButton)<{ colors: Colors }>` +const GhostButton = styled(StyledEdsButton)<{ colors: Colors }>` white-space: nowrap; color: ${(props) => props.colors.infographic.primaryMossGreen}; `; -const GhostIconButton = styled(EdsButton)<{ colors: Colors }>` +const GhostIconButton = styled(StyledEdsButton)<{ colors: Colors }>` white-space: nowrap; color: ${(props) => props.colors.infographic.primaryMossGreen}; `; -const TableIconButton = styled(EdsButton)<{ +const TableIconButton = styled(StyledEdsButton)<{ colors: Colors; userTheme: UserTheme; }>` @@ -93,8 +142,8 @@ const TableIconButton = styled(EdsButton)<{ top: 50%; left: 50%; transform: translate(-50%, -55%); - ${(props) => - props.userTheme === UserTheme.Compact && + ${({ isCompact }) => + isCompact && css` height: 22px; width: 22px; @@ -111,7 +160,7 @@ const TableIconButton = styled(EdsButton)<{ `} `; -const OutlinedButton = styled(EdsButton)<{ colors: Colors }>` +const OutlinedButton = styled(StyledEdsButton)<{ colors: Colors }>` white-space: nowrap; color: ${(props) => props.colors.infographic.primaryMossGreen}; `; diff --git a/Src/WitsmlExplorer.Frontend/components/StyledComponents/StyledAccordion/StyledAccordion.tsx b/Src/WitsmlExplorer.Frontend/components/StyledComponents/StyledAccordion/StyledAccordion.tsx new file mode 100644 index 000000000..03a7dc854 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/StyledComponents/StyledAccordion/StyledAccordion.tsx @@ -0,0 +1,44 @@ +import { Accordion } from "@equinor/eds-core-react"; +import styled, { css } from "styled-components"; +import { useOperationState } from "../../../hooks/useOperationState.tsx"; +import { UserTheme } from "../../../contexts/operationStateReducer.tsx"; +import { CSSProperties, FC, ReactNode } from "react"; + +type StyledAccordionProps = { + children: ReactNode; + style?: CSSProperties; +}; + +const StyledAccordion: FC = ({ children, style }) => { + const { + operationState: { theme } + } = useOperationState(); + const isCompact = theme === UserTheme.Compact; + + return ( + + {children} + + ); +}; + +export default StyledAccordion; + +const StyledRawAccordion = styled(Accordion)<{ + isCompact: boolean; +}>` + ${({ isCompact }) => { + if (isCompact) + return css` + h2 { + height: auto; + padding: 0; + + button { + padding: 0.4rem 0.8rem; + } + } + `; + return ""; + }} +`; diff --git a/Src/WitsmlExplorer.Frontend/components/StyledComponents/StyledAccordion/index.ts b/Src/WitsmlExplorer.Frontend/components/StyledComponents/StyledAccordion/index.ts new file mode 100644 index 000000000..280831225 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/StyledComponents/StyledAccordion/index.ts @@ -0,0 +1 @@ +export { default } from "./StyledAccordion.tsx"; diff --git a/Src/WitsmlExplorer.Frontend/components/StyledComponents/StyledMenu/StyledMenu.tsx b/Src/WitsmlExplorer.Frontend/components/StyledComponents/StyledMenu/StyledMenu.tsx new file mode 100644 index 000000000..342ee5441 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/StyledComponents/StyledMenu/StyledMenu.tsx @@ -0,0 +1,84 @@ +import { FC } from "react"; +import styled, { css } from "styled-components"; +import { Colors } from "../../../styles/Colors.tsx"; +import { + buttonBaseClasses, + Menu as MuiMenu, + menuItemClasses, + MenuProps, + paperClasses +} from "@mui/material"; +import { useOperationState } from "../../../hooks/useOperationState.tsx"; +import { UserTheme } from "../../../contexts/operationStateReducer.tsx"; + +const StyledMenu: FC = (props) => { + const { colors, theme } = useOperationState().operationState; + const sizeVariant: SizeVariant = + theme === UserTheme.Compact ? UserTheme.Compact : "default"; + + return ; +}; + +export default StyledMenu; + +type SizeVariant = UserTheme.Compact | "default"; + +const sizes: { + [key in "default" | UserTheme.Compact]: { + buttonBaseMargin?: string; + menuItemFontSize?: string; + menuItemIconSize?: string; + }; +} = { + default: { + buttonBaseMargin: undefined, + menuItemFontSize: undefined, + menuItemIconSize: undefined + }, + compact: { + buttonBaseMargin: "0", + menuItemFontSize: "0.8rem", + menuItemIconSize: "20px" + } +}; + +const RawStyledMenu = styled(MuiMenu)<{ + colors: Colors; + sizeVariant: SizeVariant; +}>` + ${({ colors, sizeVariant }) => { + const { infographic, ui, interactive } = colors; + return css` + .${paperClasses.root} { + background: ${ui.backgroundLight}; + + p { + color: ${infographic.primaryMossGreen}; + } + + svg { + fill: ${infographic.primaryMossGreen}; + } + + .${buttonBaseClasses.root}.${menuItemClasses.root} { + margin: ${sizes[sizeVariant].buttonBaseMargin}; + + &, + p { + font-size: ${sizes[sizeVariant].menuItemFontSize}; + } + + svg { + height: ${sizes[sizeVariant].menuItemIconSize}; + width: ${sizes[sizeVariant].menuItemIconSize}; + } + + &:hover { + text-decoration: none; + background-color: ${interactive.contextMenuItemHover}; + } + } + } + `; + }} +`; diff --git a/Src/WitsmlExplorer.Frontend/components/StyledComponents/StyledMenu/index.ts b/Src/WitsmlExplorer.Frontend/components/StyledComponents/StyledMenu/index.ts new file mode 100644 index 000000000..b42cf2bee --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/StyledComponents/StyledMenu/index.ts @@ -0,0 +1 @@ +export { default } from "./StyledMenu.tsx"; diff --git a/Src/WitsmlExplorer.Frontend/components/StyledComponents/WellIndicator.tsx b/Src/WitsmlExplorer.Frontend/components/StyledComponents/WellIndicator.tsx index a76f2e0d4..cbf3eab91 100644 --- a/Src/WitsmlExplorer.Frontend/components/StyledComponents/WellIndicator.tsx +++ b/Src/WitsmlExplorer.Frontend/components/StyledComponents/WellIndicator.tsx @@ -1,18 +1,24 @@ import styled from "styled-components"; import { Colors } from "../../styles/Colors"; +import { UserTheme } from "../../contexts/operationStateReducer.tsx"; + +const dotPaddingTops: { [key in UserTheme]: string } = { + [UserTheme.Comfortable]: "1.125rem", + [UserTheme.SemiCompact]: "0.625rem", + [UserTheme.Compact]: "0.55rem" +}; export const WellIndicator = styled.div<{ - compactMode: boolean; + themeMode: UserTheme; active: boolean; colors: Colors; }>` - width: 10px; - height: 10px; + width: 0.5em; + height: 0.5em; border-radius: 50%; - margin: ${(props) => - props.compactMode ? "0.625rem 0 0 0.5rem" : "1.125rem 0 0 0.5rem"}; - ${(props) => - props.active - ? `background-color: ${props.colors.interactive.successHover};` - : `border: 2px solid ${props.colors.text.staticIconsTertiary};`} + margin: ${({ themeMode }) => `${dotPaddingTops[themeMode]} 0.3rem`}; + ${({ colors, active }) => + active + ? `background-color: ${colors.interactive.successHover};` + : `border: 2px solid ${colors.text.staticIconsTertiary};`} `; diff --git a/Src/WitsmlExplorer.Frontend/contexts/CompactEdsProvider/CompactEdsProvider.tsx b/Src/WitsmlExplorer.Frontend/contexts/CompactEdsProvider/CompactEdsProvider.tsx new file mode 100644 index 000000000..df8542f9b --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/contexts/CompactEdsProvider/CompactEdsProvider.tsx @@ -0,0 +1,22 @@ +import React, { FC, ReactNode } from "react"; +import { useOperationState } from "../../hooks/useOperationState.tsx"; +import { EdsProvider as NativeEdsProvider } from "@equinor/eds-core-react"; +import { normaliseThemeForEds } from "../../tools/themeHelpers.ts"; +import { UserTheme } from "../operationStateReducer.tsx"; + +const CompactEdsProvider: FC<{ children: ReactNode }> = ({ children }) => { + const { + operationState: { theme } + } = useOperationState(); + + if (theme === UserTheme.Compact) + return ( + + {children} + + ); + + return children; +}; + +export default CompactEdsProvider; diff --git a/Src/WitsmlExplorer.Frontend/contexts/CompactEdsProvider/index.ts b/Src/WitsmlExplorer.Frontend/contexts/CompactEdsProvider/index.ts new file mode 100644 index 000000000..da5dc5dc7 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/contexts/CompactEdsProvider/index.ts @@ -0,0 +1 @@ +export { default } from "./CompactEdsProvider.tsx"; diff --git a/Src/WitsmlExplorer.Frontend/contexts/operationStateReducer.tsx b/Src/WitsmlExplorer.Frontend/contexts/operationStateReducer.tsx index ba1357d21..bd520d062 100644 --- a/Src/WitsmlExplorer.Frontend/contexts/operationStateReducer.tsx +++ b/Src/WitsmlExplorer.Frontend/contexts/operationStateReducer.tsx @@ -5,6 +5,7 @@ import { Colors, light } from "styles/Colors"; export enum UserTheme { Compact = "compact", + SemiCompact = "semiCompact", Comfortable = "comfortable" } @@ -126,7 +127,7 @@ export const initOperationStateReducer = (): [ contextMenu: EMPTY_CONTEXT_MENU, progressIndicatorValue: 0, modals: [], - theme: UserTheme.Compact, + theme: UserTheme.SemiCompact, timeZone: TimeZone.Raw, colors: Light, dateTimeFormat: DateTimeFormat.Raw, diff --git a/Src/WitsmlExplorer.Frontend/routes/Root.tsx b/Src/WitsmlExplorer.Frontend/routes/Root.tsx index 89839c83e..5e49dcf0c 100644 --- a/Src/WitsmlExplorer.Frontend/routes/Root.tsx +++ b/Src/WitsmlExplorer.Frontend/routes/Root.tsx @@ -19,6 +19,7 @@ import { SidebarProvider } from "contexts/sidebarContext"; import { authRequest, msalEnabled, msalInstance } from "msal/MsalAuthProvider"; import { SnackbarProvider } from "notistack"; import { isDesktopApp } from "tools/desktopAppHelpers"; +import CompactEdsProvider from "../contexts/CompactEdsProvider"; export default function Root() { return ( @@ -34,23 +35,25 @@ export default function Root() { - - - - - {isDesktopApp() && } - - - - - - - - - - - - + + + + + + {isDesktopApp() && } + + + + + + + + + + + + + diff --git a/Src/WitsmlExplorer.Frontend/styles/material-eds.tsx b/Src/WitsmlExplorer.Frontend/styles/material-eds.tsx index 4ac1a60f2..db830682b 100644 --- a/Src/WitsmlExplorer.Frontend/styles/material-eds.tsx +++ b/Src/WitsmlExplorer.Frontend/styles/material-eds.tsx @@ -1,6 +1,12 @@ -import { createTheme, Theme } from "@mui/material"; +import { + buttonClasses, + createTheme, + Theme, + typographyClasses +} from "@mui/material"; import { UserTheme } from "contexts/operationStateReducer"; import { colors } from "styles/Colors"; +import { isInAnyCompactMode } from "../tools/themeHelpers.ts"; const EquinorRegular = { fontFamily: "EquinorRegular" @@ -170,7 +176,7 @@ const edsTheme = createTheme({ const getTheme = (theme: UserTheme): Theme => { let themeOverrides = {}; - if (theme === UserTheme.Compact) { + if (isInAnyCompactMode(theme)) { themeOverrides = { ...edsTheme, components: { @@ -193,15 +199,38 @@ const getTheme = (theme: UserTheme): Theme => { padding: "0.25rem" } } + }, + MuiButton: { + ...edsTheme.components.MuiButton, + styleOverrides: + theme !== UserTheme.Compact + ? edsTheme.components.MuiButton.styleOverrides + : { + root: { + // @ts-ignore + ...edsTheme.components.MuiButton.styleOverrides.root, + padding: "0.2rem 1rem", + textTransform: "initial", + width: "fit-content !important", + minWidth: "fit-content !important", + fontSize: "0.85rem", + whiteSpace: "nowrap", + [`.${typographyClasses.root}`]: { + fontSize: "0.85rem" + }, + [`span.${buttonClasses.icon} svg`]: { + height: "20px", + width: "20px" + } + } + } } } }; } - return createTheme({ ...edsTheme, ...themeOverrides }); }; - export { getTheme }; diff --git a/Src/WitsmlExplorer.Frontend/tools/themeHelpers.ts b/Src/WitsmlExplorer.Frontend/tools/themeHelpers.ts new file mode 100644 index 000000000..dd5f19e83 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/tools/themeHelpers.ts @@ -0,0 +1,11 @@ +import { UserTheme } from "../contexts/operationStateReducer.tsx"; + +export const normaliseThemeForEds = ( + theme: UserTheme +): UserTheme.Compact | UserTheme.Comfortable => { + if (theme === UserTheme.Comfortable) return UserTheme.Comfortable; + return UserTheme.Compact; +}; + +export const isInAnyCompactMode = (theme: UserTheme): boolean => + theme !== UserTheme.Comfortable; From 2f86cda79eda3997c4abeb43dfe5114358c74433 Mon Sep 17 00:00:00 2001 From: Libor Nikel <140812244+LibNik@users.noreply.github.com> Date: Thu, 17 Oct 2024 13:06:37 +0200 Subject: [PATCH 112/124] 2486 log object duplication (#2572) --- Src/WitsmlExplorer.Api/Jobs/CopyJobs.cs | 11 ++ .../Workers/Copy/CopyLogWorker.cs | 18 +++ .../ContextMenus/ObjectMenuItems.tsx | 30 +++++ .../Modals/DuplicateObjectModal.tsx | 114 ++++++++++++++++++ .../models/jobs/copyJobs.ts | 2 + 5 files changed, 175 insertions(+) create mode 100644 Src/WitsmlExplorer.Frontend/components/Modals/DuplicateObjectModal.tsx diff --git a/Src/WitsmlExplorer.Api/Jobs/CopyJobs.cs b/Src/WitsmlExplorer.Api/Jobs/CopyJobs.cs index 1b95e2c7c..3b5f168d8 100644 --- a/Src/WitsmlExplorer.Api/Jobs/CopyJobs.cs +++ b/Src/WitsmlExplorer.Api/Jobs/CopyJobs.cs @@ -16,6 +16,17 @@ public record CopyObjectsJob : ICopyJob /// Indicates, if the job can be cancelled /// public override bool IsCancelable => true; + + /// + /// Target object Uid - only for log duplication purposes + /// + public string TargetObjectUid { get; init; } + + /// + /// Target object name - only for log duplication purposes + /// + public string TargetObjectName { get; init; } + } public record CopyLogDataJob : ICopyJob diff --git a/Src/WitsmlExplorer.Api/Workers/Copy/CopyLogWorker.cs b/Src/WitsmlExplorer.Api/Workers/Copy/CopyLogWorker.cs index 24a695221..7a79f62d7 100644 --- a/Src/WitsmlExplorer.Api/Workers/Copy/CopyLogWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/Copy/CopyLogWorker.cs @@ -38,8 +38,18 @@ public CopyLogWorker(ILogger logger, IWitsmlClientProvider witsm public override async Task<(WorkerResult, RefreshAction)> Execute(CopyObjectsJob job, CancellationToken? cancellationToken = null) { + var duplicate = job.TargetObjectUid != null; + (WitsmlLog[] sourceLogs, WitsmlWellbore targetWellbore) = await FetchSourceLogsAndTargetWellbore(job); ICollection copyLogsQuery = ObjectQueries.CopyObjectsQuery(sourceLogs, targetWellbore); + + // if duplicationg log, set the only one source log uid and name to the new values + if (duplicate) + { + copyLogsQuery.First().Uid = job.TargetObjectUid; + copyLogsQuery.First().Name = job.TargetObjectName; + } + List> copyLogTasks = copyLogsQuery.Select(logToCopy => GetTargetWitsmlClientOrThrow().AddToStoreAsync(logToCopy.AsItemInWitsmlList())).ToList(); Task copyLogTasksResult = Task.WhenAll(copyLogTasks); @@ -59,6 +69,14 @@ public CopyLogWorker(ILogger logger, IWitsmlClientProvider witsm cancellationToken?.ThrowIfCancellationRequested(); ConcurrentDictionary progressDict = new ConcurrentDictionary(); IEnumerable copyLogDataJobs = sourceLogs.Select(log => CreateCopyLogDataJob(job, log, progressDict)); + + if (duplicate) + { + var data = copyLogDataJobs.ToList()[0]; + data.Source.Parent.Uid = job.Source.ObjectUids[0]; + copyLogDataJobs = new List { data }; + } + List> copyLogDataTasks = copyLogDataJobs.Select(x => _copyLogDataWorker.Execute(x, cancellationToken)).ToList(); Task<(WorkerResult Result, RefreshAction)[]> copyLogDataResultTask = Task.WhenAll(copyLogDataTasks); diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectMenuItems.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectMenuItems.tsx index 6e07a0001..d0c1b9533 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectMenuItems.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectMenuItems.tsx @@ -30,6 +30,8 @@ import { Server } from "models/server"; import React from "react"; import { colors } from "styles/Colors"; import { v4 as uuid } from "uuid"; +import DuplicateObjectModal from "../Modals/DuplicateObjectModal"; +import OperationType from "../../contexts/operationType"; export interface ObjectContextMenuProps { checkedObjects: ObjectOnWellbore[]; @@ -47,6 +49,20 @@ export const ObjectMenuItems = ( ): React.ReactElement[] => { const objectReferences = useClipboardReferencesOfType(objectType); + const onClickDuplicateObjectOnWellbore = () => { + dispatchOperation({ type: OperationType.HideContextMenu }); + dispatchOperation({ + type: OperationType.DisplayModal, + payload: ( + + ) + }); + }; + return [ , , + 1 || + objectType !== ObjectType.Log + } + > + + + {menuItemText("duplicate", objectType, null)} + + , diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/DuplicateObjectModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/DuplicateObjectModal.tsx new file mode 100644 index 000000000..097f4f69d --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/DuplicateObjectModal.tsx @@ -0,0 +1,114 @@ +import { TextField } from "@equinor/eds-core-react"; +import OperationType from "contexts/operationType"; +import ObjectOnWellbore, { + toObjectReferences +} from "../../models/objectOnWellbore"; +import { ObjectType } from "../../models/objectType"; +import { ModalContentLayout } from "../StyledComponents/ModalContentLayout"; +import AuthorizationService from "services/authorizationService"; +import { ChangeEvent, useState } from "react"; +import { useOperationState } from "../../hooks/useOperationState"; +import { validText } from "./ModalParts"; +import ModalDialog from "./ModalDialog"; +import { CopyObjectsJob } from "../../models/jobs/copyJobs"; +import JobService, { JobType } from "../../services/jobService"; +import { v4 as uuid } from "uuid"; +import { onClickPaste } from "../ContextMenus/CopyUtils"; +import { Server } from "../../models/server"; + +export interface DuplicateObjectModalProps { + servers: Server[]; + objectsOnWellbore: ObjectOnWellbore[]; + objectType: ObjectType; +} + +const DuplicateObjectModal = ( + props: DuplicateObjectModalProps +): React.ReactElement => { + const duplicateNameSuffix = "_copy"; + const maxLogNameLength = 64; + + const { servers, objectsOnWellbore, objectType } = props; + const { dispatchOperation } = useOperationState(); + + const toDuplicateTypeName = objectType.toString(); + const [duplicateName, setDuplicateName] = useState( + objectsOnWellbore[0].name.slice( + 0, + maxLogNameLength - duplicateNameSuffix.length + ) + duplicateNameSuffix + ); + + const wellbore = objectsOnWellbore[0].wellboreName; + + const onConfirm = async () => { + dispatchOperation({ type: OperationType.HideModal }); + + const orderCopyJob = () => { + const copyJob: CopyObjectsJob = { + source: toObjectReferences(objectsOnWellbore, ObjectType.Log), + target: { + wellUid: objectsOnWellbore[0].wellUid, + wellboreUid: objectsOnWellbore[0].wellboreUid, + wellName: objectsOnWellbore[0].wellName, + wellboreName: objectsOnWellbore[0].wellboreName + }, + targetObjectUid: uuid(), + targetObjectName: duplicateName + }; + JobService.orderJob(JobType.CopyObjects, copyJob); + }; + onClickPaste( + servers, + AuthorizationService.selectedServer?.url, + orderCopyJob + ); + }; + + return ( + + + + + ) => + setDuplicateName(e.target.value) + } + /> + + } + /> + ); +}; + +export default DuplicateObjectModal; diff --git a/Src/WitsmlExplorer.Frontend/models/jobs/copyJobs.ts b/Src/WitsmlExplorer.Frontend/models/jobs/copyJobs.ts index 0f9356b17..180985662 100644 --- a/Src/WitsmlExplorer.Frontend/models/jobs/copyJobs.ts +++ b/Src/WitsmlExplorer.Frontend/models/jobs/copyJobs.ts @@ -17,6 +17,8 @@ export interface CopyWellboreJob { export interface CopyObjectsJob { source: ObjectReferences; target: WellboreReference; + targetObjectUid?: string; + targetObjectName?: string; } export interface CopyComponentsJob { From c1809d0c7399ae51bc560b41d81c1a2c9696a6a4 Mon Sep 17 00:00:00 2001 From: Robert Basti Date: Mon, 21 Oct 2024 09:13:30 +0200 Subject: [PATCH 113/124] Import LAS files #1999 (#2564) --- .../components/Modals/LogDataImportModal.tsx | 346 ++++++++++++++++-- 1 file changed, 319 insertions(+), 27 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/LogDataImportModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/LogDataImportModal.tsx index 4a78fde6d..42e6a5dde 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/LogDataImportModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/LogDataImportModal.tsx @@ -1,6 +1,16 @@ -import { Accordion, Icon, List } from "@equinor/eds-core-react"; +import { + Accordion, + Autocomplete, + Icon, + Label, + List, + TextField +} from "@equinor/eds-core-react"; import { Button, Tooltip, Typography } from "@mui/material"; -import { WITSML_INDEX_TYPE_MD } from "components/Constants"; +import { + WITSML_INDEX_TYPE_DATE_TIME, + WITSML_INDEX_TYPE_MD +} from "components/Constants"; import { ContentTable, ContentTableColumn, @@ -12,8 +22,11 @@ import ModalDialog, { ModalWidth } from "components/Modals/ModalDialog"; import WarningBar from "components/WarningBar"; import { useConnectedServer } from "contexts/connectedServerContext"; import OperationType from "contexts/operationType"; +import { parse } from "date-fns"; +import { zonedTimeToUtc } from "date-fns-tz"; import { useGetComponents } from "hooks/query/useGetComponents"; import { useOperationState } from "hooks/useOperationState"; +import { countBy, Dictionary } from "lodash"; import { ComponentType } from "models/componentType"; import { IndexRange } from "models/jobs/deleteLogCurveValuesJob"; import ImportLogDataJob from "models/jobs/importLogDataJob"; @@ -21,9 +34,10 @@ import ObjectReference from "models/jobs/objectReference"; import LogCurveInfo from "models/logCurveInfo"; import LogObject from "models/logObject"; import { toObjectReference } from "models/objectOnWellbore"; -import React, { useCallback, useMemo, useState } from "react"; +import React, { ChangeEvent, useCallback, useMemo, useState } from "react"; import JobService, { JobType } from "services/jobService"; -import styled from "styled-components"; +import styled, { CSSProperties } from "styled-components"; +import { Colors } from "styles/Colors"; import { extractLASSection, parseLASData, @@ -70,27 +84,70 @@ const LogDataImportModal = ( ); const [uploadedFile, setUploadedFile] = useState(null); const [uploadedFileData, setUploadedFileData] = useState([]); + const [allUploadedFileData, setAllUploadedFileData] = useState([]); const [uploadedFileColumns, setUploadedFileColumns] = useState< ImportColumn[] >([]); + const [allFileColumns, setAllFileColumns] = useState([]); + const [selectedMnemonics, setSelectedMnemonics] = useState([]); + const [allMnemonics, setAllMnemonics] = useState([]); const [error, setError] = useState(""); + const [duplicityWarning, setDuplicityWarning] = useState(""); const [isLoading, setIsLoading] = useState(false); + const [dateTimeFormat, setDateTimeFormat] = useState(null); + const [contentTableId, setContentTableId] = useState( + "listOfSelectedMmenomics" + ); const separator = ","; - const validate = (fileColumns: ImportColumn[]) => { - setError(""); + const validate = (fileColumns: ImportColumn[], parseError?: string) => { + if (parseError) setError(parseError); if (fileColumns.length) { if (fileColumns.map((col) => col.name).some((value) => value === "")) setError(IMPORT_FORMAT_INVALID); - if (!fileColumns.map((col) => col.name).includes(targetLog.indexCurve)) + if ( + !fileColumns + .map((col) => col.name.toUpperCase()) + .includes(targetLog.indexCurve.toUpperCase()) + ) setError(MISSING_INDEX_CURVE); } }; + const getParsedData = () => { + if ( + uploadedFileData && + uploadedFileColumns && + targetLog?.indexType === WITSML_INDEX_TYPE_DATE_TIME + ) { + try { + return parseDateTimeColumn(uploadedFileData, 0, dateTimeFormat); + } catch (error) { + validate( + uploadedFileColumns, + dateTimeFormat ? `Unable to parse data. ${error}` : null + ); + return null; + } + } + return uploadedFileData; + }; + + const parsedData = useMemo( + () => getParsedData(), + [ + uploadedFileData, + uploadedFileColumns, + targetLog, + dateTimeFormat, + selectedMnemonics + ] + ); + const hasOverlap = checkOverlap( targetLog, uploadedFileColumns, - uploadedFileData, + parsedData, logCurveInfoList ); @@ -102,7 +159,10 @@ const LogDataImportModal = ( targetLog: logReference, mnemonics: uploadedFileColumns.map((col) => col.name), units: uploadedFileColumns.map((col) => col.unit), - dataRows: uploadedFileData.map((line) => line.split(separator)) + dataRows: + parsedData !== null + ? parsedData.map((line) => line.split(separator)) + : uploadedFileData.map((line) => line.split(separator)) }; await JobService.orderJob(JobType.ImportLogData, job); @@ -118,6 +178,7 @@ const LogDataImportModal = ( let header: ImportColumn[] = null; let data: string[] = null; + setDuplicityWarning(""); if (text.startsWith("~V")) { // LAS files should start with ~V. @@ -128,23 +189,51 @@ const LogDataImportModal = ( ); const dataSection = extractLASSection(text, "ASCII", "A"); header = parseLASHeader(curveSection); + const groupedByNum = countBy(header, "name"); + createWarningOfDuplicities(groupedByNum); + header = countOccurrences(header, "name"); + validate(header); data = parseLASData(dataSection); + const indexCurveColumn = header.find( + (x) => x.name.toLowerCase() === targetLog.indexCurve.toLowerCase() + )?.index; + header[indexCurveColumn].name = targetLog.indexCurve; + if ( + targetLog.indexType === WITSML_INDEX_TYPE_DATE_TIME && + indexCurveColumn !== null + ) { + // las file time + const dateTimeFormat = findDateTimeFormat(data, indexCurveColumn); + data = swapFirstColumn(data, indexCurveColumn); + setUploadedFileData(data); + setAllUploadedFileData(data); + swapArrayElements(header, 0, indexCurveColumn); + setDateTimeFormat(dateTimeFormat); + } else { + // las file depth + setUploadedFileData(data); + setAllUploadedFileData(data); + } } else { + // csv files const headerLine = text.split("\n", 1)[0]; header = parseCSVHeader(headerLine); data = text.split("\n").slice(1); + setUploadedFileData(data); + setAllUploadedFileData(data); } - validate(header); - setUploadedFile(file); setUploadedFileColumns(header); - setUploadedFileData(data); + setAllMnemonics(header.map((col) => col.name)); + setAllFileColumns(header); + setSelectedMnemonics(header.map((col) => col.name)); + setUploadedFile(file); }, [] ); - const parseCSVHeader = (header: string) => { + const parseCSVHeader = (headerr: string) => { const unitRegex = /(?<=\[)(.*)(?=\]){1}/; - const fileColumns = header.split(separator).map((col, index) => { + const fileColumns = headerr.split(separator).map((col, index) => { const columnName = col.substring(0, col.indexOf("[")); return { index: index, @@ -157,14 +246,71 @@ const LogDataImportModal = ( const contentTableColumns: ContentTableColumn[] = useMemo( () => - uploadedFileColumns.map((col) => ({ - property: col.name, - label: `${col.name}[${col.unit}]`, - type: ContentType.String - })), + uploadedFileColumns + .filter((x) => selectedMnemonics.find((y) => y === x.name)) + .map((col) => ({ + col, + property: col.name, + label: `${col.name}[${col.unit}]`, + type: ContentType.String + })), [uploadedFileColumns] ); + const onMnemonicsChange = ({ + selectedItems + }: { + selectedItems: string[]; + }) => { + if ( + selectedItems.find( + (option) => option.toUpperCase() === targetLog.indexCurve.toUpperCase() + ) + ) { + setSelectedMnemonics(selectedItems); + const reducedData = updateColumns( + allUploadedFileData, + selectedItems, + allFileColumns + ); + + const reducedHeader = updateHeader(allFileColumns, selectedItems); + const timestamp = new Date().getTime(); + setContentTableId(timestamp.toString()); + setUploadedFileColumns(reducedHeader); + setUploadedFileData(reducedData); + } + }; + + const countOccurrences = (arr: any[], property: string) => { + return arr.reduce((acc, obj) => { + const key = obj[property]; + if (key) { + acc[key] = (acc[key] || 0) + 1; + if (acc[key] > 1) obj.name = obj.name + "(" + acc[key] + ")"; + } + return arr; + }, {}); + }; + + const createWarningOfDuplicities = (mnemonics: Dictionary) => { + let foundDuplicity = false; + let warningText = "Found multiple mnemonics with the same name: "; + for (const key in mnemonics) { + const value = mnemonics[key]; + if (value > 1) { + warningText = warningText + key + "(" + mnemonics[key] + ") "; + foundDuplicity = true; + } + } + if (foundDuplicity) { + warningText = + warningText + + ". Duplicate names were automatically changed by adding numbers as suffix in parenthesis."; + setDuplicityWarning(warningText); + } + }; + return ( <> { @@ -193,6 +339,43 @@ const LogDataImportModal = ( + {uploadedFile && ( + <> + + e.preventDefault()} + onOptionsChange={onMnemonicsChange} + style={ + { + "--eds-input-background": colors.ui.backgroundDefault + } as CSSProperties + } + dropdownHeight={700} + colors={colors} + /> + + )} + {targetLog?.indexType === WITSML_INDEX_TYPE_DATE_TIME && + !!uploadedFileData?.length && ( + ) => { + setDateTimeFormat(e.target.value); + }} + /> + )} @@ -203,9 +386,6 @@ const LogDataImportModal = ( > Supported filetypes: csv, las. - - Supported logs: depth (csv + las), time (csv). - Only curve names, units and data is imported. @@ -251,7 +431,6 @@ const LogDataImportModal = ( {uploadedFileColumns?.length && - uploadedFileData?.length && targetLog?.indexCurve && !error && ( @@ -266,13 +445,16 @@ const LogDataImportModal = ( >
@@ -282,9 +464,12 @@ const LogDataImportModal = ( {hasOverlap && ( )} + {duplicityWarning && } } width={ModalWidth.LARGE} + height="800px" + minHeight="650px" confirmDisabled={!uploadedFile || !!error || isFetchingLogCurveInfo} confirmText={"Import"} onSubmit={() => onSubmit()} @@ -296,6 +481,19 @@ const LogDataImportModal = ( ); }; +const StyledAutocomplete = styled(Autocomplete)<{ colors: Colors }>` + button { + color: ${(props) => props.colors.infographic.primaryMossGreen}; + } +`; + +const StyledLabel = styled(Label)<{ colors: Colors }>` + color: ${(props) => props.colors.infographic.primaryMossGreen}; + white-space: nowrap; + align-items: center; + font-style: italic; +`; + const FileContainer = styled.div` display: flex; flex-direction: row; @@ -410,7 +608,9 @@ const getTableData = ( columns: ImportColumn[], indexCurve: string ): ContentTableCustomRow[] => { - const indexCurveColumn = columns.find((col) => col.name === indexCurve); + const indexCurveColumn = columns.find( + (col) => col.name.toUpperCase() === indexCurve.toUpperCase() + ); if (!indexCurveColumn) return []; return data?.map((dataLine) => { const dataCells = dataLine.split(","); @@ -420,8 +620,100 @@ const getTableData = ( columns.forEach((col, i) => { result[col.name] = dataCells[i]; }); + return result; }); }; +function swapColumns( + matrix: string[][], + col1: number, + col2: number +): string[][] { + for (const row of matrix) { + [row[col1], row[col2]] = [row[col2], row[col1]]; + } + return matrix; +} + +const swapFirstColumn = (data: string[], selectedColumn: number) => { + const splitData = data.map((obj) => obj.split(",")); + const tempData = swapColumns(splitData, 0, selectedColumn); + const result = tempData.map((obj) => obj.join(",")); + return result; +}; + +const updateColumns = ( + data: string[], + mnemonics: string[], + allMnemonics: ImportColumn[] +) => { + let splitData = data.map((obj) => obj.split(",")); + + for (let i = allMnemonics.length - 1; i >= 0; i--) { + const toRemove = allMnemonics[i]; + if (mnemonics.indexOf(toRemove.name) === -1) { + splitData = removeColumn(splitData, i); + } + } + return splitData.map((obj) => obj.join(",")); +}; + +const updateHeader = (columns: ImportColumn[], mnemonics: string[]) => { + const output = columns.filter((x) => mnemonics.indexOf(x.name) > -1); + return output; +}; + +function removeColumn(arr: any[][], colIndex: number): any[][] { + return arr.map((row) => row.filter((_, index) => index !== colIndex)); +} + +function swapArrayElements(arr: T[], i: number, j: number): void { + [arr[i], arr[j]] = [arr[j], arr[i]]; +} + +const inputDateFormats: string[] = [ + "YYYY-MM-DDTHH:mm:ss.sssZ", // ISO 8601 format + "HH:mm:ss/dd-MMM-yyyy" +]; + +const findDateTimeFormat = ( + data: string[], + selectedColumn: number +): string | null => { + const dateString = data[0].split(",")[selectedColumn]; + for (const format of inputDateFormats) { + try { + parseDateFromFormat(dateString, format); + return format; + } catch { + // Ignore error, try next format. + } + } + return null; +}; + +const parseDateTimeColumn = ( + data: string[], + selectedColumn: number, + inputFormat: string +) => { + const dataWithISOTimeColumn = data.map((dataRow) => { + const rowValues = dataRow.split(","); + rowValues[selectedColumn] = parseDateFromFormat( + rowValues[selectedColumn], + inputFormat + ); + return rowValues.join(","); + }); + return dataWithISOTimeColumn; +}; + +const parseDateFromFormat = (dateString: string, format: string) => { + const parsed = parse(dateString, format, new Date()); + if (parsed.toString() === "Invalid Date") + throw new Error(`Unable to parse date ${dateString} with format ${format}`); + return zonedTimeToUtc(parsed, "UTC").toISOString(); +}; + export default LogDataImportModal; From f56f66ef514a314ad9edd05186f57194ac990652 Mon Sep 17 00:00:00 2001 From: Robert Basti Date: Mon, 21 Oct 2024 15:31:04 +0200 Subject: [PATCH 114/124] Object Duplication #2574 (#2575) --- Src/WitsmlExplorer.Api/Workers/Copy/CopyObjectsWorker.cs | 6 ++++++ .../components/ContextMenus/ObjectMenuItems.tsx | 6 +----- .../components/Modals/DuplicateObjectModal.tsx | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Src/WitsmlExplorer.Api/Workers/Copy/CopyObjectsWorker.cs b/Src/WitsmlExplorer.Api/Workers/Copy/CopyObjectsWorker.cs index 1dcdc3616..0cae33e15 100644 --- a/Src/WitsmlExplorer.Api/Workers/Copy/CopyObjectsWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/Copy/CopyObjectsWorker.cs @@ -43,6 +43,7 @@ public CopyObjectsWorker(ILogger logger, IWitsmlClientProvider w private async Task<(WorkerResult, RefreshAction)> GenericCopy(CopyObjectsJob job, CancellationToken? cancellationToken = null) { + var duplicate = job.TargetObjectUid != null; Witsml.IWitsmlClient targetClient = GetTargetWitsmlClientOrThrow(); Witsml.IWitsmlClient sourceClient = GetSourceWitsmlClientOrThrow(); IWitsmlObjectList fetchObjectsQuery = ObjectQueries.GetWitsmlObjectsByIds(job.Source.WellUid, job.Source.WellboreUid, job.Source.ObjectUids, job.Source.ObjectType); @@ -66,6 +67,11 @@ public CopyObjectsWorker(ILogger logger, IWitsmlClientProvider w } ICollection queries = ObjectQueries.CopyObjectsQuery(objectsToCopy.Objects, targetWellbore); + if (duplicate) + { + queries.First().Uid = job.TargetObjectUid; + queries.First().Name = job.TargetObjectName; + } RefreshObjects refreshAction = new(targetClient.GetServerHostname(), job.Target.WellUid, job.Target.WellboreUid, job.Source.ObjectType); return await _copyUtils.CopyObjectsOnWellbore(targetClient, sourceClient, queries, refreshAction, job.Source.WellUid, job.Source.WellboreUid); } diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectMenuItems.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectMenuItems.tsx index d0c1b9533..331066948 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectMenuItems.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/ObjectMenuItems.tsx @@ -87,11 +87,7 @@ export const ObjectMenuItems = ( 1 || - objectType !== ObjectType.Log - } + disabled={checkedObjects.length === 0 || checkedObjects.length > 1} > diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/DuplicateObjectModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/DuplicateObjectModal.tsx index 097f4f69d..46d789f10 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/DuplicateObjectModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/DuplicateObjectModal.tsx @@ -46,7 +46,7 @@ const DuplicateObjectModal = ( const orderCopyJob = () => { const copyJob: CopyObjectsJob = { - source: toObjectReferences(objectsOnWellbore, ObjectType.Log), + source: toObjectReferences(objectsOnWellbore, props.objectType), target: { wellUid: objectsOnWellbore[0].wellUid, wellboreUid: objectsOnWellbore[0].wellboreUid, From 98b064358cf4de0a6cccce8f912791b4cd73f0d7 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Tue, 22 Oct 2024 11:56:40 +0200 Subject: [PATCH 115/124] FIX-2485 Query data grid (#2577) --- .../components/ContentViews/QueryDataGrid.tsx | 370 +++++ .../ContentViews/QueryView/QueryView.tsx | 24 +- .../components/QueryOptions/QueryOptions.tsx | 55 +- .../ContentViews/table/ColumnDef.tsx | 82 +- .../ContentViews/table/ColumnOptionsMenu.tsx | 50 +- .../ContentViews/table/ContentTable.tsx | 93 +- .../components/ContentViews/table/Panel.tsx | 5 +- .../ContentViews/table/contentTableUtils.ts | 10 +- .../ContentViews/table/tableParts.ts | 12 +- Src/WitsmlExplorer.Frontend/package.json | 1 + .../templates/dataGrid/DataGridProperty.ts | 8 + .../templates/dataGrid/DataGridTemplates.ts | 65 + .../dataGrid/objects/DataGridAttachment.ts | 136 ++ .../dataGrid/objects/DataGridBhaRun.ts | 421 ++++++ .../dataGrid/objects/DataGridCementJob.ts | 1229 +++++++++++++++++ .../dataGrid/objects/DataGridChangeLog.ts | 155 +++ .../dataGrid/objects/DataGridConvCore.ts | 182 +++ .../dataGrid/objects/DataGridFluidsReport.ts | 434 ++++++ .../objects/DataGridFormationMarker.ts | 141 ++ .../templates/dataGrid/objects/DataGridLog.ts | 401 ++++++ .../dataGrid/objects/DataGridMessage.ts | 124 ++ .../dataGrid/objects/DataGridMudLog.ts | 180 +++ .../templates/dataGrid/objects/DataGridRig.ts | 443 ++++++ .../dataGrid/objects/DataGridRisk.ts | 166 +++ .../dataGrid/objects/DataGridTrajectory.ts | 167 +++ .../dataGrid/objects/DataGridTubular.ts | 772 +++++++++++ .../dataGrid/objects/DataGridWbGeometry.ts | 64 + .../dataGrid/objects/DataGridWell.ts | 286 ++++ .../dataGrid/objects/DataGridWellbore.ts | 167 +++ .../objects/common/DataGridCommonData.ts | 73 + .../objects/common/DataGridCustomData.ts | 7 + .../objects/common/DataGridExtensionAny.ts | 6 + .../common/DataGridExtensionNameValue.ts | 72 + .../properties/DataGridBitRecordProperties.ts | 139 ++ .../properties/DataGridBopProperties.ts | 224 +++ .../DataGridCentrifugeProperties.ts | 51 + ...aGridChronostratigraphyStructProperties.ts | 9 + .../properties/DataGridCostProperties.ts | 9 + .../properties/DataGridDegasserProperties.ts | 112 ++ .../properties/DataGridFootageDirection.ts | 19 + .../DataGridGeologyIntervalProperties.ts | 723 ++++++++++ .../DataGridGrpWbGeometryProperties.ts | 98 ++ .../DataGridHydrocycloneProperties.ts | 48 + .../DataGridIndexedObjectProperties.ts | 30 + ...taGridLithostratigraphyStructProperties.ts | 9 + .../properties/DataGridLocationProperties.ts | 87 ++ .../DataGridMeasuredDepthCoordProperties.ts | 15 + .../DataGridNameStructProperties.ts | 13 + .../properties/DataGridNameTagProperties.ts | 47 + .../properties/DataGridPitProperties.ts | 51 + .../properties/DataGridPumpProperties.ts | 112 ++ .../DataGridRefNameStringProperties.ts | 10 + .../DataGridRefObjectStringProperties.ts | 16 + .../DataGridRefWellWellboreProperties.ts | 16 + .../DataGridRefWellWellboreRigProperties.ts | 22 + ...DataGridRefWellboreTrajectoryProperties.ts | 16 + .../properties/DataGridSensorProperties.ts | 29 + .../properties/DataGridShakerProperties.ts | 77 ++ .../DataGridSurfaceEquipmentProperties.ts | 194 +++ .../DataGridTrajectoryStationProperties.ts | 462 +++++++ .../properties/DataGridUomProperties.ts | 9 + .../properties/DataGridWellCRSProperties.ts | 330 +++++ .../properties/DataGridWellDatumProperties.ts | 81 ++ .../DataGridWellElevationCoordProperties.ts | 16 + .../DataGridWellKnownNameStructProperties.ts | 14 + ...ataGridWellVerticalDepthCoordProperties.ts | 15 + yarn.lock | 12 + 67 files changed, 9448 insertions(+), 68 deletions(-) create mode 100644 Src/WitsmlExplorer.Frontend/components/ContentViews/QueryDataGrid.tsx create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/DataGridProperty.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/DataGridTemplates.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/DataGridAttachment.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/DataGridBhaRun.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/DataGridCementJob.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/DataGridChangeLog.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/DataGridConvCore.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/DataGridFluidsReport.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/DataGridFormationMarker.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/DataGridLog.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/DataGridMessage.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/DataGridMudLog.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/DataGridRig.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/DataGridRisk.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/DataGridTrajectory.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/DataGridTubular.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/DataGridWbGeometry.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/DataGridWell.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/DataGridWellbore.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/DataGridCommonData.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/DataGridCustomData.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/DataGridExtensionAny.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/DataGridExtensionNameValue.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridBitRecordProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridBopProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridCentrifugeProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridChronostratigraphyStructProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridCostProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridDegasserProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridFootageDirection.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridGeologyIntervalProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridGrpWbGeometryProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridHydrocycloneProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridIndexedObjectProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridLithostratigraphyStructProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridLocationProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridMeasuredDepthCoordProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridNameStructProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridNameTagProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridPitProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridPumpProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridRefNameStringProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridRefObjectStringProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridRefWellWellboreProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridRefWellWellboreRigProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridRefWellboreTrajectoryProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridSensorProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridShakerProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridSurfaceEquipmentProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridTrajectoryStationProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridUomProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridWellCRSProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridWellDatumProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridWellElevationCoordProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridWellKnownNameStructProperties.ts create mode 100644 Src/WitsmlExplorer.Frontend/templates/dataGrid/objects/common/properties/DataGridWellVerticalDepthCoordProperties.ts diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryDataGrid.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryDataGrid.tsx new file mode 100644 index 000000000..21a9c35b9 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryDataGrid.tsx @@ -0,0 +1,370 @@ +import { TextField, Typography } from "@equinor/eds-core-react"; +import { ExpandedState, RowSelectionState } from "@tanstack/react-table"; +import { + formatXml, + TemplateObjects +} from "components/ContentViews/QueryViewUtils"; +import { + ContentTable, + ContentTableColumn, + ContentTableRow, + ContentType +} from "components/ContentViews/table"; +import { pluralize } from "components/ContextMenus/ContextMenuUtils"; +import { QueryActionType, QueryContext } from "contexts/queryContext"; +import { XMLBuilder, XMLParser } from "fast-xml-parser"; +import { cloneDeep, debounce } from "lodash"; +import { + ChangeEvent, + MouseEvent, + useCallback, + useContext, + useEffect, + useMemo, + useState +} from "react"; +import styled from "styled-components"; +import { DataGridProperty } from "templates/dataGrid/DataGridProperty"; +import { getDataGridTemplate } from "templates/dataGrid/DataGridTemplates"; + +const parserOptions = { + ignoreAttributes: false, + attributeNamePrefix: "@_" +}; + +export default function QueryDataGrid() { + const { + queryState: { queries, tabIndex }, + dispatchQuery + } = useContext(QueryContext); + + const { query, tabId } = queries[tabIndex]; + const [expanded, setExpanded] = useState({}); + + const parser = new XMLParser(parserOptions); + const builder = new XMLBuilder(parserOptions); + const queryObj = parser.parse(query); + const templateObject = Object.keys(queryObj)?.[0]?.slice(0, -1); + const template = getDataGridTemplate(templateObject as TemplateObjects); + const data = mergeTemplateWithQuery(template, queryObj); + const rowSelection = getRowSelectionFromQuery(data); + + useEffect(() => { + const newExpanded = getExpandedRowsFromQuery(data); + setExpanded(newExpanded); + }, [tabIndex]); + + const columns: ContentTableColumn[] = useMemo( + () => [ + { + property: "name", + label: "variable", + type: ContentType.String + }, + { + property: "value", + label: "value", + type: ContentType.Component + }, + { + property: "documentation", + label: "documentation", + type: ContentType.String + } + ], + [] + ); + + const onRowSelectionChange = (rows: QueryGridDataRow[]) => { + const dataClone = cloneDeep(data); + const selectedRowIds = new Set(rows.map((row) => row.id)); + const rowWasAdded = rows.length > Object.keys(rowSelection).length; + + const updatePresentInQuery = ( + node: QueryGridDataRow, + parentRows: QueryGridDataRow[] + ) => { + if (selectedRowIds.has(node.id)) { + node.presentInQuery = true; + if (rowWasAdded) { + parentRows.forEach((row) => { + row.presentInQuery = true; + }); + } + } else { + node.presentInQuery = false; + node.value = null; + } + + if (node.children && node.children.length > 0) { + node.children.forEach((child) => + updatePresentInQuery(child, [...parentRows, node]) + ); + } + }; + + dataClone.forEach((node) => updatePresentInQuery(node, [])); + + updateQueryFromData(dataClone); + }; + + const updateQueryFromData = (data: QueryGridDataRow[]) => { + const generatedQueryObj = extractQueryFromData(data); + const generatedQuery = builder.build(generatedQueryObj); + const formattedGeneratedQuery = generatedQuery + ? formatXml(generatedQuery) + : ""; + + dispatchQuery({ + type: QueryActionType.SetQuery, + query: formattedGeneratedQuery + }); + }; + + const handleChangeDebounced = useCallback( + debounce((e: ChangeEvent, rowId: string) => { + const dataClone = cloneDeep(data); + const value = e.target.value; + + const updateRowValueById = ( + rows: QueryGridDataRow[], + parentRows: QueryGridDataRow[] + ) => { + for (const row of rows) { + if (row.id === rowId) { + row.value = value; + row.presentInQuery = true; + parentRows.forEach((row) => { + row.presentInQuery = true; + }); + return; + } + + if (row.children) { + updateRowValueById(row.children, [...parentRows, row]); + } + } + }; + updateRowValueById(dataClone, []); + updateQueryFromData(dataClone); + }, 700), + [data] + ); + + const tableData = useMemo(() => { + const getTableData = (dataRows: QueryGridDataRow[]): QueryGridDataRow[] => { + return dataRows.map((row) => ({ + ...row, + value: ( + ) => + handleChangeDebounced(e, row.id) + } + onClick={(e: MouseEvent) => e.stopPropagation()} + disabled={row.isContainer} + /> + ), + children: row.children ? getTableData(row.children) : undefined + })); + }; + + return getTableData(data); + }, [data]); + + return tableData?.length > 0 ? ( +
+ +
+ ) : ( + + {Object.values(TemplateObjects).includes( + templateObject as TemplateObjects + ) && !template + ? `Data Grid is not yet supported for ${pluralize(templateObject)}.` + : "Unable to parse query."} + + ); +} + +interface QueryGridDataRow extends ContentTableRow { + name: string; + documentation: string; + value: any; + isAttribute?: boolean; + isContainer?: boolean; + isMultiple?: boolean; + children?: QueryGridDataRow[]; + presentInQuery?: boolean; +} + +const mergeTemplateWithQuery = ( + template: DataGridProperty, + queryObj: any +): QueryGridDataRow[] => { + const processTemplate = ( + templateNode: DataGridProperty, + queryNode: any, + parentId: string = "", + index: number | null = null + ): QueryGridDataRow => { + const { name, documentation, isContainer, isAttribute, properties } = + templateNode; + + const uniqueId = + (parentId ? `${parentId}--` : "") + + name + + (index != null ? `[${index}]` : ""); + + let value = null; + const children: QueryGridDataRow[] = []; + const presentInQuery = queryNode !== undefined; + + if (!isContainer) { + value = typeof queryNode === "object" ? queryNode?.["#text"] : queryNode; + } + + if (properties) { + properties.forEach((prop) => { + const propQueryName = prop.isAttribute ? `@_${prop.name}` : prop.name; + const childQueryNode = queryNode?.[propQueryName]; + if (prop.isMultiple && Array.isArray(childQueryNode)) { + const multiChildren = childQueryNode.map((child, index) => + processTemplate(prop, child, uniqueId, index) + ); + children.push(...multiChildren); + } else { + children.push(processTemplate(prop, childQueryNode, uniqueId)); + } + }); + } + + return { + id: uniqueId, + name, + documentation, + value, + isAttribute, + isContainer, + presentInQuery, + children: children.length > 0 ? children : undefined + }; + }; + + if (!template) return []; + + const mergedRows = [processTemplate(template, queryObj[template.name])]; + + return mergedRows; +}; + +const extractQueryFromData = (data: QueryGridDataRow[]): any => { + function buildQuery(row: QueryGridDataRow): any { + const { value, isAttribute, children, presentInQuery } = row; + + if (!presentInQuery) { + return undefined; + } + + const queryNode: any = {}; + + if (isAttribute) { + return value ?? ""; + } else { + queryNode["#text"] = value ?? ""; + } + + children?.forEach((child) => { + const childQuery = buildQuery(child); + if (childQuery !== undefined) { + const childName = child.isAttribute ? `@_${child.name}` : child.name; + if (Array.isArray(queryNode[childName])) { + queryNode[childName].push(childQuery); + } else if (queryNode[childName]) { + // If we have multiple of the same object, move the items to an array. + queryNode[childName] = [queryNode[childName], childQuery]; + } else { + queryNode[childName] = childQuery; + } + } + }); + + return Object.keys(queryNode).length ? queryNode : undefined; + } + + const queryObj: any = {}; + + data.forEach((row) => { + const rowQuery = buildQuery(row); + if (rowQuery !== undefined) { + queryObj[row.name] = rowQuery; + } + }); + + return queryObj; +}; + +const getRowSelectionFromQuery = ( + data: QueryGridDataRow[] +): RowSelectionState => { + const result: RowSelectionState = {}; + + const traverse = (rows: QueryGridDataRow[]) => { + for (const row of rows) { + if (row.presentInQuery) { + result[row.id] = true; + } + if (row.children) { + traverse(row.children); + } + } + }; + + traverse(data); + return result; +}; + +const getExpandedRowsFromQuery = ( + data: QueryGridDataRow[], + expandLevels: number = 2 // By default expand the first two levels (the main object - logs and each log) +): ExpandedState => { + const result: ExpandedState = {}; + + const traverse = (rows: QueryGridDataRow[], level: number) => { + for (const row of rows) { + if (row.children) { + result[row.id] = true; + if (level < expandLevels) { + traverse(row.children, level + 1); + } + } + } + }; + traverse(data, 1); + return result; +}; + +const StyledTextField = styled(TextField)` + display: flex; + align-items: center; + div { + background-color: transparent; + } + width: 100%; + height: 100%; +`; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/QueryView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/QueryView.tsx index bbd327840..8ce26b747 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/QueryView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/QueryView.tsx @@ -3,12 +3,14 @@ import { QueryEditor } from "components/QueryEditor"; import { getTag } from "components/QueryEditorUtils"; import { QueryActionType, QueryContext } from "contexts/queryContext"; import { useOperationState } from "hooks/useOperationState"; -import React, { useContext } from "react"; +import React, { useContext, useState } from "react"; import styled from "styled-components"; import { Colors } from "styles/Colors"; import Icon from "styles/Icons"; import { Box } from "@mui/material"; +import QueryDataGrid from "components/ContentViews/QueryDataGrid"; +import { QueryEditorTypes } from "components/ContentViews/QueryView/components/QueryOptions/QueryOptions"; import QueryOptions from "./components/QueryOptions"; const QueryView = (): React.ReactElement => { @@ -19,6 +21,9 @@ const QueryView = (): React.ReactElement => { queryState: { queries, tabIndex }, dispatchQuery } = useContext(QueryContext); + const [editorType, setEditorType] = useState( + QueryEditorTypes.AceEditor + ); const { query, result } = queries[tabIndex]; @@ -84,12 +89,19 @@ const QueryView = (): React.ReactElement => { height="100%" pr="2px" > - - + {editorType === QueryEditorTypes.DataGrid ? ( + + ) : ( + + )}
diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/QueryOptions.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/QueryOptions.tsx index 2b58bbe36..936c65444 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/QueryOptions.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/QueryOptions.tsx @@ -1,6 +1,21 @@ -import React, { ChangeEvent, FC, useContext, useState } from "react"; +import { Switch, TextField, Typography } from "@equinor/eds-core-react"; import { Box, Stack } from "@mui/material"; +import { CommonPanelContainer } from "components/StyledComponents/Container.tsx"; +import { ChangeEvent, FC, useContext, useState } from "react"; +import styled, { css } from "styled-components"; +import { DispatchOperation } from "../../../../../contexts/operationStateReducer.tsx"; +import OperationType from "../../../../../contexts/operationType.ts"; +import { + QueryActionType, + QueryContext +} from "../../../../../contexts/queryContext.tsx"; +import { useOperationState } from "../../../../../hooks/useOperationState.tsx"; +import QueryService from "../../../../../services/queryService.ts"; +import { Colors } from "../../../../../styles/Colors.tsx"; +import Icon from "../../../../../styles/Icons.tsx"; +import ConfirmModal from "../../../../Modals/ConfirmModal.tsx"; import { StyledNativeSelect } from "../../../../Select.tsx"; +import { Button } from "../../../../StyledComponents/Button.tsx"; import { formatXml, getParserError, @@ -8,26 +23,21 @@ import { StoreFunction } from "../../../QueryViewUtils.tsx"; import TemplatePicker from "./TemplatePicker"; -import { - QueryActionType, - QueryContext -} from "../../../../../contexts/queryContext.tsx"; -import styled, { css } from "styled-components"; -import { TextField } from "@equinor/eds-core-react"; -import { Colors } from "../../../../../styles/Colors.tsx"; -import { useOperationState } from "../../../../../hooks/useOperationState.tsx"; -import { Button } from "../../../../StyledComponents/Button.tsx"; -import Icon from "../../../../../styles/Icons.tsx"; -import { DispatchOperation } from "../../../../../contexts/operationStateReducer.tsx"; -import OperationType from "../../../../../contexts/operationType.ts"; -import QueryService from "../../../../../services/queryService.ts"; -import ConfirmModal from "../../../../Modals/ConfirmModal.tsx"; + +export enum QueryEditorTypes { + AceEditor = "AceEditor", + DataGrid = "DataGrid" +} type QueryOptionsProps = { onQueryChange: (newValue: string) => void; + onChangeEditorType: (type: QueryEditorTypes) => void; }; -const QueryOptions: FC = ({ onQueryChange }) => { +const QueryOptions: FC = ({ + onQueryChange, + onChangeEditorType +}) => { const { dispatchQuery, queryState: { queries, tabIndex } @@ -137,6 +147,19 @@ const QueryOptions: FC = ({ onQueryChange }) => { gap="0.5rem" direction="row" > + + + onChangeEditorType( + event.target.checked + ? QueryEditorTypes.DataGrid + : QueryEditorTypes.AceEditor + ) + } + size="small" + /> + Data Grid + { @@ -56,11 +57,17 @@ export const useColumnDef = ( const savedWidths = getLocalStorageItem<{ [label: string]: number }>( viewId + STORAGE_CONTENTTABLE_WIDTH_KEY ); - let columnDef: ColumnDef[] = columns.map((column) => { + let columnDef: ColumnDef[] = columns.map((column, index) => { + const isPrimaryNestedColumn = nested && index === 0; const width = column.width ?? savedWidths?.[column.label] ?? - calculateColumnWidth(column.label, isCompactMode, column.type); + calculateColumnWidth( + column.label, + isCompactMode, + column.type, + isPrimaryNestedColumn + ); return { id: column.property, accessorFn: (data) => data[column.property], @@ -72,7 +79,8 @@ export const useColumnDef = ( filterFn: getFilterFn(column), ...addComponentCell(column.type), ...addActiveCurveFiltering(column.label), - ...addDecimalPreference(column.type, decimals) + ...addDecimalPreference(column.type, decimals), + ...addNestedCell(column, isPrimaryNestedColumn) }; }); @@ -98,7 +106,7 @@ export const useColumnDef = ( ]; const firstToggleableIndex = Math.max( - (checkableRows ? 1 : 0) + (insetColumns ? 1 : 0), + (checkableRows ? 1 : 0) + (insetColumns || nested ? 1 : 0), stickyLeftColumns ); @@ -159,6 +167,72 @@ const addDecimalPreference = ( : {}; }; +const addNestedCell = ( + column: ContentTableColumn, + isPrimaryNestedColumn: boolean +): Partial> => { + return isPrimaryNestedColumn + ? { + enableHiding: false, + enableSorting: false, + header: ({ table }: { table: Table }) => ( + <> + + table.toggleAllRowsExpanded(!table.getIsSomeRowsExpanded()) + } + size="small" + style={{ margin: 0, padding: 0 }} + > + + + {column.label} + + ), + cell: ({ row, table, getValue }) => { + return ( +
+ {row.getCanExpand() && ( + { + event.stopPropagation(); + row.getToggleExpandedHandler()(); + }} + size="small" + style={{ margin: 0, padding: 0 }} + > + + + )} +
+ {getValue()} +
+
+ ); + } + } + : {}; +}; + const getExpanderColumnDef = (isCompactMode: boolean): ColumnDef => { return { id: expanderId, diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnOptionsMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnOptionsMenu.tsx index 0ddf32630..bf5e6b81c 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnOptionsMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ColumnOptionsMenu.tsx @@ -46,6 +46,7 @@ export const ColumnOptionsMenu = (props: { stickyLeftColumns: number; selectedColumnsStatus: string; firstToggleableIndex: number; + disableFilters: boolean; }): React.ReactElement => { const { table, @@ -55,7 +56,8 @@ export const ColumnOptionsMenu = (props: { columns, stickyLeftColumns, selectedColumnsStatus, - firstToggleableIndex + firstToggleableIndex, + disableFilters } = props; const { operationState: { colors, theme } @@ -72,6 +74,7 @@ export const ColumnOptionsMenu = (props: { const isCompactMode = theme === UserTheme.Compact; useEffect(() => { + if (disableFilters) return; const filterString = searchParams.get("filter"); const initialFilter = JSON.parse(filterString); const bothEmpty = @@ -267,7 +270,7 @@ export const ColumnOptionsMenu = (props: { column.id != selectId && column.id != expanderId && index >= stickyLeftColumns && ( - + - - ) => - onChangeColumnFilter(e, column) - } - style={{ minWidth: "100px", maxHeight: "25px" }} - /> - + {!disableFilters && ( + + ) => + onChangeColumnFilter(e, column) + } + style={{ minWidth: "100px", maxHeight: "25px" }} + /> + + )} ) ); @@ -335,7 +340,7 @@ export const ColumnOptionsMenu = (props: { table.setColumnOrder([ ...(checkableRows ? [selectId] : []), ...(expandableRows ? [expanderId] : []), - ...columns.map((column) => column.label) + ...columns.map((column) => column.property) ]); if (viewId) removeLocalStorageItem(viewId + STORAGE_CONTENTTABLE_ORDER_KEY); @@ -344,16 +349,19 @@ export const ColumnOptionsMenu = (props: { Reset ordering Reset sizing - Reset filter + {!disableFilters && ( + Reset filter + )} ); }; -const OrderingRow = styled.div` +const OrderingRow = styled.div<{ disableFilters: boolean }>` display: grid; - grid-template-columns: 20px 25px 25px 1fr 1.5fr; + grid-template-columns: ${(props) => + props.disableFilters ? "20px 25px 25px 1fr" : "20px 25px 25px 1fr 1.5fr"}; align-items: center; `; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ContentTable.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ContentTable.tsx index 63e827e4b..dae3837db 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ContentTable.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/ContentTable.tsx @@ -1,8 +1,10 @@ import { TableBody, TableHead } from "@mui/material"; import { ColumnSizingState, + ExpandedState, flexRender, getCoreRowModel, + getExpandedRowModel, getFilteredRowModel, getSortedRowModel, Header, @@ -96,6 +98,8 @@ export const ContentTable = React.memo( onContextMenu, checkableRows, insetColumns, + nested, + nestedProperty, panelElements, showPanel = true, showRefresh = false, @@ -103,8 +107,12 @@ export const ContentTable = React.memo( viewId, downloadToCsvFileName = null, onRowSelectionChange, + onExpandedChange, initiallySelectedRows = [], - autoRefresh = false + rowSelection: controlledRowSelection = null, + expanded: controlledExpansionState = null, + autoRefresh = false, + disableFilters = false } = contentTableProps; const { operationState: { colors, theme } @@ -116,6 +124,7 @@ export const ContentTable = React.memo( ...initiallySelectedRows.map((row) => ({ [row.id]: true })) ) ); + const [expanded, setExpanded] = useState({}); const [columnVisibility, setColumnVisibility] = useState( initializeColumnVisibility(viewId) ); @@ -128,6 +137,7 @@ export const ContentTable = React.memo( viewId, columns, insetColumns, + nested, checkableRows, stickyLeftColumns ); @@ -135,7 +145,8 @@ export const ContentTable = React.memo( data: data ?? noData, columns: columnDef, state: { - rowSelection, + rowSelection: controlledRowSelection ?? rowSelection, + expanded: controlledExpansionState ?? expanded, columnVisibility, columnSizing }, @@ -172,6 +183,9 @@ export const ContentTable = React.memo( getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), + getExpandedRowModel: getExpandedRowModel(), + getSubRows: (originalRow) => + nested && nestedProperty ? originalRow[nestedProperty] : undefined, getRowId: (originalRow, index) => originalRow.id ?? index, getRowCanExpand: insetColumns != null @@ -179,8 +193,18 @@ export const ContentTable = React.memo( : undefined, onColumnVisibilityChange: setColumnVisibility, onColumnSizingChange: setColumnSizing, + onExpandedChange: (updaterOrValue) => { + const newExpanded = + updaterOrValue instanceof Function + ? updaterOrValue(controlledExpansionState ?? expanded) + : updaterOrValue; + setExpanded(newExpanded); + onExpandedChange?.(newExpanded); + }, onRowSelectionChange: (updaterOrValue) => { - const prevSelection = checkableRows ? rowSelection : {}; + const prevSelection = checkableRows + ? controlledRowSelection ?? rowSelection + : {}; let newRowSelection = updaterOrValue instanceof Function ? updaterOrValue(prevSelection) @@ -188,8 +212,27 @@ export const ContentTable = React.memo( if (!checkableRows && Object.keys(newRowSelection).length == 0) newRowSelection = rowSelection; setRowSelection(newRowSelection); + + const flattenDataRecursively = (dataRow: any) => { + return dataRow[nestedProperty] + ? [ + dataRow, + ...dataRow[nestedProperty].flatMap((nestedRow: any) => + flattenDataRecursively(nestedRow) + ) + ] + : [dataRow]; + }; + + const flattenedData = + nested && nestedProperty + ? data.flatMap((dataRow) => flattenDataRecursively(dataRow)) + : data; + onRowSelectionChange?.( - data.filter((dataRow, index) => newRowSelection[dataRow.id ?? index]) + flattenedData.filter( + (dataRow, index) => newRowSelection[dataRow.id ?? index] + ) ); }, meta: { @@ -197,7 +240,7 @@ export const ContentTable = React.memo( setPreviousIndex, colors }, - enableExpanding: insetColumns != null, + enableExpanding: insetColumns != null || nested, enableRowSelection: checkableRows, ...constantTableOptions }); @@ -255,6 +298,17 @@ export const ContentTable = React.memo( stickyLeftColumns ); + const flattenDataRecursively = (dataRow: any) => { + return dataRow[nestedProperty] + ? [ + dataRow, + ...dataRow[nestedProperty].flatMap((nestedRow: any) => + flattenDataRecursively(nestedRow) + ) + ] + : [dataRow]; + }; + const onHeaderClick = ( e: React.MouseEvent, header: Header @@ -301,7 +355,12 @@ export const ContentTable = React.memo( numberOfCheckedItems={ table.getFilteredSelectedRowModel().flatRows.length } - numberOfItems={data?.length} + numberOfItems={ + nested && nestedProperty + ? data?.flatMap((dataRow) => flattenDataRecursively(dataRow)) + .length + : data?.length + } table={table} viewId={viewId} columns={columns} @@ -309,6 +368,7 @@ export const ContentTable = React.memo( showRefresh={showRefresh} downloadToCsvFileName={downloadToCsvFileName} stickyLeftColumns={stickyLeftColumns} + disableFilters={disableFilters || nested} /> ) : null}
- {row.getIsExpanded() && row.original.inset?.length != 0 && ( - - )} + {row.getIsExpanded() && + (row.original.inset?.length || 0) !== 0 && ( + + )} ); })} diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/Panel.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/Panel.tsx index ed446b1e9..523b273d4 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/table/Panel.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/table/Panel.tsx @@ -31,6 +31,7 @@ export interface PanelProps { expandableRows?: boolean; stickyLeftColumns?: number; downloadToCsvFileName?: string; + disableFilters?: boolean; } const csvIgnoreColumns = ["select", "expander"]; //Ids of the columns that should be ignored when downloading as csv @@ -47,7 +48,8 @@ const Panel = (props: PanelProps) => { columns, expandableRows = false, downloadToCsvFileName = null, - stickyLeftColumns + stickyLeftColumns, + disableFilters = false } = props; const { operationState: { theme } @@ -135,6 +137,7 @@ const Panel = (props: PanelProps) => { stickyLeftColumns={stickyLeftColumns} selectedColumnsStatus={selectedColumnsStatus} firstToggleableIndex={firstToggleableIndex} + disableFilters={disableFilters} /> {showRefresh && ( ], - [ - isLoading, - exportSelectedDataPoints, - exportSelectedIndexRange, - selectedRows, - colors.mode, - theme - ] + [isLoading, exportSelectedDataPoints, selectedRows, colors.mode, theme] ); if (isFetchedLog && !log) { diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/LogDataImportModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/LogDataImportModal.tsx index 42e6a5dde..14722c7af 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/LogDataImportModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/LogDataImportModal.tsx @@ -599,7 +599,6 @@ const getDataRanges = ( }); } } - return dataRanges; }; diff --git a/Src/WitsmlExplorer.Frontend/models/jobs/downloadAllLogDataJob.tsx b/Src/WitsmlExplorer.Frontend/models/jobs/downloadLogDataJob.tsx similarity index 58% rename from Src/WitsmlExplorer.Frontend/models/jobs/downloadAllLogDataJob.tsx rename to Src/WitsmlExplorer.Frontend/models/jobs/downloadLogDataJob.tsx index 1d086fd02..8bf59db81 100644 --- a/Src/WitsmlExplorer.Frontend/models/jobs/downloadAllLogDataJob.tsx +++ b/Src/WitsmlExplorer.Frontend/models/jobs/downloadLogDataJob.tsx @@ -1,7 +1,9 @@ import LogObject from "models/logObject"; -export default interface DownloadAllLogDataJob { +export default interface DownloadLogDataJob { logReference: LogObject; mnemonics: string[]; startIndexIsInclusive: boolean; + startIndex?: string; + endIndex?: string; } diff --git a/Src/WitsmlExplorer.Frontend/services/jobService.tsx b/Src/WitsmlExplorer.Frontend/services/jobService.tsx index 2a76123b0..cf5a84405 100644 --- a/Src/WitsmlExplorer.Frontend/services/jobService.tsx +++ b/Src/WitsmlExplorer.Frontend/services/jobService.tsx @@ -166,6 +166,6 @@ export enum JobType { SpliceLogs = "SpliceLogs", CompareLogData = "CompareLogData", CountLogDataRows = "CountLogDataRows", - DownloadAllLogData = "DownloadAllLogData", + DownloadLogData = "DownloadLogData", OffsetLogCurves = "OffsetLogCurves" } From 78c23b887de7a43ee4b99e0b781030435b817f15 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Wed, 30 Oct 2024 10:25:33 +0100 Subject: [PATCH 118/124] FIX-2582 Fix wellbore creation bug (#2583) --- .../__testUtils__/testUtils.tsx | 7 +- .../components/Breadcrumbs.tsx | 2 +- .../ContextMenus/WellContextMenu.tsx | 5 - .../ContextMenus/WellboreContextMenu.tsx | 3 - .../hooks/query/useGetWellboreSearch.ts | 6 +- .../models/wellbore.tsx | 106 +----------------- .../services/objectService.tsx | 22 +--- 7 files changed, 14 insertions(+), 137 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/__testUtils__/testUtils.tsx b/Src/WitsmlExplorer.Frontend/__testUtils__/testUtils.tsx index 24ce2865f..3382c458f 100644 --- a/Src/WitsmlExplorer.Frontend/__testUtils__/testUtils.tsx +++ b/Src/WitsmlExplorer.Frontend/__testUtils__/testUtils.tsx @@ -43,7 +43,7 @@ import Trajectory from "models/trajectory"; import Tubular from "models/tubular"; import WbGeometryObject from "models/wbGeometry"; import Well, { emptyWell } from "models/well"; -import Wellbore, { emptyWellbore } from "models/wellbore"; +import Wellbore from "models/wellbore"; import { SnackbarProvider } from "notistack"; import React from "react"; import { MemoryRouter } from "react-router-dom"; @@ -170,7 +170,10 @@ export function getWell(overrides?: Partial): Well { export function getWellbore(overrides?: Partial): Wellbore { return { - ...emptyWellbore(), + name: "wellboreName", + uid: "wellboreUid", + wellName: "wellName", + wellUid: "wellUid", ...overrides }; } diff --git a/Src/WitsmlExplorer.Frontend/components/Breadcrumbs.tsx b/Src/WitsmlExplorer.Frontend/components/Breadcrumbs.tsx index 53ff8536f..fcb4d97b8 100644 --- a/Src/WitsmlExplorer.Frontend/components/Breadcrumbs.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Breadcrumbs.tsx @@ -418,7 +418,7 @@ const StyledBreadcrumbs = styled(EdsBreadcrumbs)<{ isCompact: boolean }>` padding-right: 0.5rem; } `} -}`; +`; const Title = styled.p` line-height: 1rem; diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx index f6f723f55..752edd04f 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx @@ -92,11 +92,6 @@ const WellContextMenu = (props: WellContextMenuProps): React.ReactElement => { name: "", wellUid: well.uid, wellName: well.name, - wellboreStatus: "", - wellboreType: "", - isActive: false, - wellboreParentUid: "", - wellboreParentName: "", wellborePurpose: "unknown" }; openWellboreProperties( diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellboreContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellboreContextMenu.tsx index b07565f36..830bc6672 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellboreContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellboreContextMenu.tsx @@ -76,9 +76,6 @@ const WellboreContextMenu = ( name: "", wellUid: wellbore.wellUid, wellName: wellbore.wellName, - wellboreStatus: "", - wellboreType: "", - isActive: false, wellboreParentUid: wellbore.uid, wellboreParentName: wellbore.name, wellborePurpose: "unknown" diff --git a/Src/WitsmlExplorer.Frontend/hooks/query/useGetWellboreSearch.ts b/Src/WitsmlExplorer.Frontend/hooks/query/useGetWellboreSearch.ts index 8e330b9ad..a872f37d5 100644 --- a/Src/WitsmlExplorer.Frontend/hooks/query/useGetWellboreSearch.ts +++ b/Src/WitsmlExplorer.Frontend/hooks/query/useGetWellboreSearch.ts @@ -1,6 +1,6 @@ import { QueryObserverResult } from "@tanstack/react-query"; import { useGetWellbores } from "hooks/query/useGetWellbores"; -import Wellbore, { WellboreProperties } from "models/wellbore"; +import Wellbore from "models/wellbore"; import { useMemo } from "react"; import { WellboreFilterType, @@ -28,9 +28,7 @@ export const useGetWellboreSearch = ( const filteredData = useMemo(() => { const regex = getSearchRegex(value, true); - const property = filterTypeToProperty[ - filterType - ] as keyof WellboreProperties; + const property = filterTypeToProperty[filterType] as keyof Wellbore; return ( wellbores?.filter( (result) => diff --git a/Src/WitsmlExplorer.Frontend/models/wellbore.tsx b/Src/WitsmlExplorer.Frontend/models/wellbore.tsx index 8e6403993..30e82b361 100644 --- a/Src/WitsmlExplorer.Frontend/models/wellbore.tsx +++ b/Src/WitsmlExplorer.Frontend/models/wellbore.tsx @@ -2,33 +2,17 @@ import { WITSML_INDEX_TYPE_DATE_TIME, WITSML_INDEX_TYPE_MD } from "components/Constants"; -import BhaRun from "models/bhaRun"; -import ChangeLog from "models/changeLog"; -import FluidsReport from "models/fluidsReport"; -import FormationMarker from "models/formationMarker"; -import LogObject from "models/logObject"; import Measure from "models/measure"; -import MessageObject from "models/messageObject"; -import MudLog from "models/mudLog"; -import { - ObjectType, - ObjectTypeToModel, - pluralizeObjectType -} from "models/objectType"; -import Rig from "models/rig"; -import RiskObject from "models/riskObject"; -import Trajectory from "models/trajectory"; -import Tubular from "models/tubular"; -import WbGeometryObject from "models/wbGeometry"; +import { ObjectType } from "models/objectType"; -export interface WellboreProperties { +export default interface Wellbore { uid: string; name: string; wellUid: string; wellName?: string; - wellboreStatus: string; - wellboreType: string; - isActive: boolean; + wellboreStatus?: string; + wellboreType?: string; + isActive?: boolean; number?: string; suffixAPI?: string; numGovt?: string; @@ -53,63 +37,8 @@ export interface WellboreProperties { objectCount?: ExpandableObjectsCount; } -export interface WellboreObjects { - bhaRuns?: BhaRun[]; - changeLogs?: ChangeLog[]; - fluidsReports?: FluidsReport[]; - formationMarkers?: FormationMarker[]; - logs?: LogObject[]; - rigs?: Rig[]; - trajectories?: Trajectory[]; - messages?: MessageObject[]; - mudLogs?: MudLog[]; - tubulars?: Tubular[]; - risks?: RiskObject[]; - wbGeometries?: WbGeometryObject[]; -} - export type ExpandableObjectsCount = Partial>; -export default interface Wellbore extends WellboreProperties, WellboreObjects {} - -export function emptyWellbore(): Wellbore { - return { - uid: "", - name: "", - wellUid: "", - wellName: "", - wellboreStatus: "", - wellboreType: "", - isActive: false, - wellboreParentUid: "", - wellboreParentName: "", - wellborePurpose: "unknown", - dateTimeCreation: "", - dateTimeLastChange: "", - itemState: "", - bhaRuns: [], - changeLogs: [], - fluidsReports: [], - formationMarkers: [], - logs: [], - rigs: [], - trajectories: [], - tubulars: [], - messages: [], - mudLogs: [], - risks: [], - wbGeometries: [], - objectCount: null - }; -} - -export function wellboreHasChanges( - wellbore: WellboreProperties, - updatedWellbore: WellboreProperties -): boolean { - return JSON.stringify(wellbore) !== JSON.stringify(updatedWellbore); -} - export const calculateWellNodeId = (wellUid: string): string => { return `w=${wellUid};`; }; @@ -183,28 +112,3 @@ export const getWellboreProperties = ( ["UID Wellbore", wellbore.uid] ]); }; - -export function objectTypeToWellboreObjects( - objectType: ObjectType -): keyof WellboreObjects { - return (objectType.charAt(0).toLowerCase() + - pluralizeObjectType(objectType).slice(1)) as keyof WellboreObjects; -} - -export function getObjectsFromWellbore( - wellbore: Wellbore, - objectType: Key -): ObjectTypeToModel[Key][] { - return wellbore[ - objectTypeToWellboreObjects(objectType) - ] as ObjectTypeToModel[Key][]; -} - -export function getObjectFromWellbore( - wellbore: Wellbore, - uid: string, - objectType: Key -): ObjectTypeToModel[Key] { - const objects = getObjectsFromWellbore(wellbore, objectType); - return objects?.find((object) => object.uid === uid); -} diff --git a/Src/WitsmlExplorer.Frontend/services/objectService.tsx b/Src/WitsmlExplorer.Frontend/services/objectService.tsx index 796d0523f..20d8fdeea 100644 --- a/Src/WitsmlExplorer.Frontend/services/objectService.tsx +++ b/Src/WitsmlExplorer.Frontend/services/objectService.tsx @@ -7,10 +7,7 @@ import { pluralizeObjectType } from "models/objectType"; import { Server } from "models/server"; -import Wellbore, { - ExpandableObjectsCount, - getObjectsFromWellbore -} from "models/wellbore"; +import { ExpandableObjectsCount } from "models/wellbore"; import { ApiClient, throwError } from "services/apiClient"; export default class ObjectService { @@ -105,23 +102,6 @@ export default class ObjectService { } } - public static async getObjectsIfMissing( - wellbore: Wellbore, - objectType: Key, - abortSignal?: AbortSignal - ): Promise { - const objects = getObjectsFromWellbore(wellbore, objectType); - if (objects == null || objects.length == 0) { - return await ObjectService.getObjects( - wellbore.wellUid, - wellbore.uid, - objectType, - abortSignal - ); - } - return null; - } - public static async getExpandableObjectsCount( wellUid: string, wellboreUid: string, From c466b3fbcc9e2f4427f0771a944ad9dd0d8cb9ce Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:42:51 +0100 Subject: [PATCH 119/124] FIX-2580 Fix server casing problem (#2585) --- .../routes/AuthRoute.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/routes/AuthRoute.tsx b/Src/WitsmlExplorer.Frontend/routes/AuthRoute.tsx index 19bf483b1..81bba85c3 100644 --- a/Src/WitsmlExplorer.Frontend/routes/AuthRoute.tsx +++ b/Src/WitsmlExplorer.Frontend/routes/AuthRoute.tsx @@ -59,9 +59,15 @@ export default function AuthRoute() { }, []); useEffect(() => { - if (servers && (!connectedServer || connectedServer.url != serverUrl)) { + if ( + servers && + (!connectedServer || + connectedServer.url.toLowerCase() != serverUrl.toLowerCase()) + ) { setConnectedServer(null); - const server = servers.find((server) => server.url === serverUrl); + const server = servers.find( + (server) => server.url.toLowerCase() === serverUrl.toLowerCase() + ); if (server) showCredentialsModal(server, true); } }, [servers, serverUrl]); @@ -91,7 +97,12 @@ export default function AuthRoute() { }); }; - if (servers && !servers.find((server) => server.url === serverUrl)) { + if ( + servers && + !servers.find( + (server) => server.url.toLowerCase() === serverUrl.toLowerCase() + ) + ) { return ; } From ab45b5e3102b00b73f585dd4c6d068b6c50124a8 Mon Sep 17 00:00:00 2001 From: Robert Basti Date: Wed, 30 Oct 2024 13:56:08 +0100 Subject: [PATCH 120/124] Import Log LAS Data - replace space with underscore (#2584) --- Src/WitsmlExplorer.Frontend/tools/lasFileTools.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Src/WitsmlExplorer.Frontend/tools/lasFileTools.ts b/Src/WitsmlExplorer.Frontend/tools/lasFileTools.ts index 0f2c86c5b..57102a647 100644 --- a/Src/WitsmlExplorer.Frontend/tools/lasFileTools.ts +++ b/Src/WitsmlExplorer.Frontend/tools/lasFileTools.ts @@ -11,7 +11,10 @@ export const parseLASHeader = (sectionData: string) => { .filter((line) => line.trim() != "" && !line.startsWith("#")); const headerData = lines.map((line, index) => { const endOfCurveNameIndex = line.indexOf("."); - const curveName = line.slice(0, endOfCurveNameIndex).trim(); + const curveName = line + .slice(0, endOfCurveNameIndex) + .trim() + .replaceAll(" ", "_"); const unit = line.slice(endOfCurveNameIndex + 1).split(/\s+/)[0]; return { index, From 8a4684b798dd8d021aa00f2dae4946c9b6591a23 Mon Sep 17 00:00:00 2001 From: Robert Basti Date: Fri, 1 Nov 2024 11:14:53 +0100 Subject: [PATCH 121/124] =?UTF-8?q?Query=20View=20-=20Enable=20the=20Optio?= =?UTF-8?q?ns=20In=20field=20for=20DeleteFromStore=20=F0=9F=90=9B=20#2538?= =?UTF-8?q?=20(#2587)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/QueryOptions/QueryOptions.tsx | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/QueryOptions.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/QueryOptions.tsx index 936c65444..061aa48fd 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/QueryOptions.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/QueryOptions/QueryOptions.tsx @@ -171,7 +171,8 @@ const QueryOptions: FC = ({ - {storeFunction === StoreFunction.GetFromStore && ( + {(storeFunction === StoreFunction.GetFromStore || + storeFunction === StoreFunction.DeleteFromStore) && ( = ({ More options - - {Object.values(ReturnElements).map((value) => { - return ( - - ); - })} - + {storeFunction === StoreFunction.GetFromStore && ( + + {Object.values(ReturnElements).map((value) => { + return ( + + ); + })} + + )} Date: Thu, 7 Nov 2024 10:25:26 +0100 Subject: [PATCH 122/124] FIX-2589 Add object count for the result in the query view (#2591) --- .../ContentViews/QueryView/QueryView.tsx | 12 +++++- .../components/ResultMeta/ResultMeta.tsx | 42 +++++++++++++++++++ .../QueryView/components/ResultMeta/index.ts | 1 + 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/ResultMeta/ResultMeta.tsx create mode 100644 Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/ResultMeta/index.ts diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/QueryView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/QueryView.tsx index 8ce26b747..2c740857a 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/QueryView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/QueryView.tsx @@ -11,6 +11,7 @@ import Icon from "styles/Icons"; import { Box } from "@mui/material"; import QueryDataGrid from "components/ContentViews/QueryDataGrid"; import { QueryEditorTypes } from "components/ContentViews/QueryView/components/QueryOptions/QueryOptions"; +import ResultMeta from "components/ContentViews/QueryView/components/ResultMeta"; import QueryOptions from "./components/QueryOptions"; const QueryView = (): React.ReactElement => { @@ -103,9 +104,16 @@ const QueryView = (): React.ReactElement => { /> )} -
+ + -
+
); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/ResultMeta/ResultMeta.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/ResultMeta/ResultMeta.tsx new file mode 100644 index 000000000..362d77937 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/ResultMeta/ResultMeta.tsx @@ -0,0 +1,42 @@ +import { Typography } from "@equinor/eds-core-react"; +import { QueryContext } from "contexts/queryContext"; +import { XMLParser } from "fast-xml-parser"; +import { FC, useContext } from "react"; +import styled from "styled-components"; + +const ResultMeta: FC = () => { + const { + queryState: { queries, tabIndex } + } = useContext(QueryContext); + + const { result } = queries[tabIndex]; + const parser = new XMLParser(); + const resultObj = parser.parse(result); + const templateObject = Object.keys(resultObj)?.[0]?.slice(0, -1); + const resultCount = countItemsAtDepth2(resultObj, templateObject); + + return ( + + {resultCount > 0 && ( + {`Number of ${templateObject}s: ${resultCount}`} + )} + + ); +}; + +const Layout = styled.div` + padding-left: 46px; +`; + +function countItemsAtDepth2(obj: any, templateObject: string) { + try { + const depth1object = Object.values(obj)[0] as any; + const depth2objects = depth1object[templateObject]; + const depth2length = depth2objects.length; + return depth2length; + } catch { + return null; + } +} + +export default ResultMeta; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/ResultMeta/index.ts b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/ResultMeta/index.ts new file mode 100644 index 000000000..25e8f3dec --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView/components/ResultMeta/index.ts @@ -0,0 +1 @@ +export { default } from "./ResultMeta"; From f0911298a91ebadb309b63f84a21ea30a4b70ee1 Mon Sep 17 00:00:00 2001 From: Elias Kristoffer Bruvik <70512270+eliasbruvik@users.noreply.github.com> Date: Thu, 7 Nov 2024 13:43:22 +0100 Subject: [PATCH 123/124] FIX-2496 Rework log file download (#2590) --- .../HttpHandlers/JobHandler.cs | 23 ++++++++++++ Src/WitsmlExplorer.Api/Jobs/JobInfo.cs | 2 +- .../Models/Reports/BaseReport.cs | 13 +++++-- Src/WitsmlExplorer.Api/Routes.cs | 1 + .../Workers/DownloadLogDataWorker.cs | 24 ++++++++----- .../__testUtils__/testUtils.tsx | 3 +- .../components/ContentViews/JobsView.tsx | 7 ++-- .../components/Modals/ReportModal.tsx | 16 +++------ .../components/ReportCreationHelper.ts | 35 ------------------- .../models/reports/BaseReport.tsx | 16 +++------ .../services/jobService.tsx | 23 ++++++++++++ 11 files changed, 86 insertions(+), 77 deletions(-) delete mode 100644 Src/WitsmlExplorer.Frontend/components/ReportCreationHelper.ts diff --git a/Src/WitsmlExplorer.Api/HttpHandlers/JobHandler.cs b/Src/WitsmlExplorer.Api/HttpHandlers/JobHandler.cs index c1b5ef6a7..8cb237e73 100644 --- a/Src/WitsmlExplorer.Api/HttpHandlers/JobHandler.cs +++ b/Src/WitsmlExplorer.Api/HttpHandlers/JobHandler.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; +using System.IO; using System.Linq; using System.Threading.Tasks; @@ -120,5 +121,27 @@ public static IResult GetReport(string jobId, IJobCache jobCache, HttpRequest ht } return TypedResults.Ok(job.Report); } + + [Produces("application/octet-stream")] + public static IResult DownloadFile(string jobId, IJobCache jobCache, HttpRequest httpRequest, IConfiguration configuration, ICredentialsService credentialsService) + { + EssentialHeaders eh = new(httpRequest); + bool useOAuth2 = StringHelpers.ToBoolean(configuration[ConfigConstants.OAuth2Enabled]); + string userName = useOAuth2 ? credentialsService.GetClaimFromToken(eh.GetBearerToken(), "upn") : eh.TargetUsername; + if (!useOAuth2) + { + credentialsService.VerifyUserIsLoggedIn(eh, ServerType.Target); + } + JobInfo job = jobCache.GetJobInfoById(jobId); + if (job.Username != userName && (!useOAuth2 || !IsAdminOrDeveloper(eh.GetBearerToken()))) + { + return TypedResults.Forbid(); + } + BaseReport report = job.Report; + byte[] byteArray = System.Text.Encoding.UTF8.GetBytes(report.FileData.FileContent); + var stream = new MemoryStream(byteArray); + httpRequest.HttpContext.Response.Headers["Access-Control-Expose-Headers"] = "Content-Disposition"; + return TypedResults.File(stream, "application/octet-stream", report.FileData.FileName); + } } } diff --git a/Src/WitsmlExplorer.Api/Jobs/JobInfo.cs b/Src/WitsmlExplorer.Api/Jobs/JobInfo.cs index c5b7b3e51..711dd026a 100644 --- a/Src/WitsmlExplorer.Api/Jobs/JobInfo.cs +++ b/Src/WitsmlExplorer.Api/Jobs/JobInfo.cs @@ -83,7 +83,7 @@ public ReportType ReportType { return ReportType.None; } - if (Report?.DownloadImmediately == true) + if (Report?.HasFile == true) { return ReportType.File; } diff --git a/Src/WitsmlExplorer.Api/Models/Reports/BaseReport.cs b/Src/WitsmlExplorer.Api/Models/Reports/BaseReport.cs index 590d5c550..6e988a5c8 100644 --- a/Src/WitsmlExplorer.Api/Models/Reports/BaseReport.cs +++ b/Src/WitsmlExplorer.Api/Models/Reports/BaseReport.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text.Json.Serialization; namespace WitsmlExplorer.Api.Models.Reports { @@ -8,9 +9,15 @@ public class BaseReport public string Summary { get; init; } public IEnumerable ReportItems { get; init; } public string WarningMessage { get; init; } - public bool DownloadImmediately { get; init; } = false; - public string ReportHeader { get; init; } + public bool HasFile { get; init; } = false; public string JobDetails { get; init; } - public string ReportBody { get; init; } + [JsonIgnore] + public ReportFileData FileData { get; init; } + } + + public class ReportFileData + { + public string FileName { get; init; } + public string FileContent { get; init; } } } diff --git a/Src/WitsmlExplorer.Api/Routes.cs b/Src/WitsmlExplorer.Api/Routes.cs index 69acd7fa3..ec3fcd22a 100644 --- a/Src/WitsmlExplorer.Api/Routes.cs +++ b/Src/WitsmlExplorer.Api/Routes.cs @@ -101,6 +101,7 @@ public static void ConfigureApi(this WebApplication app, IConfiguration configur app.MapGet("/jobs/alljobinfos", JobHandler.GetAllJobInfos, useOAuth2, AuthorizationPolicyRoles.ADMINORDEVELOPER); app.MapPost("/jobs/cancel/{jobId}", JobHandler.CancelJob, useOAuth2); app.MapGet("/jobs/report/{jobId}", JobHandler.GetReport, useOAuth2); + app.MapGet("/jobs/download/{jobId}", JobHandler.DownloadFile, useOAuth2); app.MapGet("/credentials/authorize", AuthorizeHandler.Authorize, useOAuth2); app.MapGet("/credentials/deauthorize", AuthorizeHandler.Deauthorize, useOAuth2); diff --git a/Src/WitsmlExplorer.Api/Workers/DownloadLogDataWorker.cs b/Src/WitsmlExplorer.Api/Workers/DownloadLogDataWorker.cs index 137613da1..9d1701874 100644 --- a/Src/WitsmlExplorer.Api/Workers/DownloadLogDataWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/DownloadLogDataWorker.cs @@ -63,27 +63,33 @@ public DownloadLogDataWorker( private (WorkerResult, RefreshAction) DownloadLogDataResult(DownloadLogDataJob job, ICollection> reportItems, ICollection curveSpecifications) { Logger.LogInformation("Download of all data is done. {jobDescription}", job.Description()); - var reportHeader = GetReportHeader(curveSpecifications); - var reportBody = GetReportBody(reportItems, curveSpecifications); - job.JobInfo.Report = DownloadLogDataReport(reportItems, job.LogReference, reportHeader, reportBody); + string content = GetCsvFileContent(reportItems, curveSpecifications); + job.JobInfo.Report = DownloadLogDataReport(job.LogReference, content, "csv"); WorkerResult workerResult = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), true, $"Download of all data is ready, jobId: ", jobId: job.JobInfo.Id); return (workerResult, null); } - private DownloadLogDataReport DownloadLogDataReport(ICollection> reportItems, LogObject logReference, string reportHeader, string reportBody) + private DownloadLogDataReport DownloadLogDataReport(LogObject logReference, string fileContent, string fileExtension) { return new DownloadLogDataReport { Title = $"{logReference.WellboreName} - {logReference.Name}", - Summary = "You can download the report as csv file", + Summary = "The download will start automatically. You can also access the download link in the Jobs view.", LogReference = logReference, - ReportItems = reportItems, - DownloadImmediately = true, - ReportHeader = reportHeader, - ReportBody = reportBody + HasFile = true, + FileData = new ReportFileData + { + FileName = $"{logReference.WellboreName}-{logReference.Name}.{fileExtension}", + FileContent = fileContent + } }; } + private string GetCsvFileContent(ICollection> reportItems, ICollection curveSpecifications) + { + return $"{GetReportHeader(curveSpecifications)}\n{GetReportBody(reportItems, curveSpecifications)}"; + } + private string GetReportHeader(ICollection curveSpecifications) { var listOfHeaders = new List(); diff --git a/Src/WitsmlExplorer.Frontend/__testUtils__/testUtils.tsx b/Src/WitsmlExplorer.Frontend/__testUtils__/testUtils.tsx index 3382c458f..aab96cc8b 100644 --- a/Src/WitsmlExplorer.Frontend/__testUtils__/testUtils.tsx +++ b/Src/WitsmlExplorer.Frontend/__testUtils__/testUtils.tsx @@ -209,8 +209,7 @@ export function getReport(overrides?: Partial): BaseReport { summary: "testSummary", reportItems: [], warningMessage: "", - downloadImmediately: false, - reportHeader: "", + hasFile: false, ...overrides }; } diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/JobsView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/JobsView.tsx index 557b77a8c..39a22857c 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/JobsView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/JobsView.tsx @@ -17,7 +17,6 @@ import OperationType from "contexts/operationType"; import { refreshJobInfoQuery } from "hooks/query/queryRefreshHelpers"; import { useGetJobInfo } from "hooks/query/useGetJobInfo"; import { useGetServers } from "hooks/query/useGetServers"; -import useExport from "hooks/useExport"; import { useOperationState } from "hooks/useOperationState"; import JobStatus from "models/jobStatus"; import JobInfo from "models/jobs/jobInfo"; @@ -49,8 +48,6 @@ export const JobsView = (): React.ReactElement => { ? new Date(dataUpdatedAt).toLocaleTimeString() : ""; - const { exportData } = useExport(); - const [cancellingJobs, setCancellingJobs] = useState([]); const onContextMenu = ( @@ -94,8 +91,8 @@ export const JobsView = (): React.ReactElement => { }; const onClickReport = async (jobId: string) => { const report = await JobService.getReport(jobId); - if (report.downloadImmediately === true) { - exportData(report.title, report.reportHeader, report.reportBody); + if (report.hasFile === true) { + await JobService.downloadFile(jobId); } else { const reportModalProps = { report }; dispatchOperation({ diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/ReportModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/ReportModal.tsx index c302a4c29..e9332b241 100644 --- a/Src/WitsmlExplorer.Frontend/components/Modals/ReportModal.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Modals/ReportModal.tsx @@ -16,7 +16,6 @@ import ModalDialog, { ModalWidth } from "components/Modals/ModalDialog"; import { Banner } from "components/StyledComponents/Banner"; import { useConnectedServer } from "contexts/connectedServerContext"; import OperationType from "contexts/operationType"; -import useExport from "hooks/useExport"; import { useLiveJobProgress } from "hooks/useLiveJobProgress"; import { useOperationState } from "hooks/useOperationState"; import BaseReport, { createReport } from "models/reports/BaseReport"; @@ -25,8 +24,8 @@ import JobService from "services/jobService"; import NotificationService from "services/notificationService"; import styled from "styled-components"; import { Colors } from "styles/Colors"; -import ConfirmModal from "./ConfirmModal"; import StyledAccordion from "../StyledComponents/StyledAccordion"; +import ConfirmModal from "./ConfirmModal"; export interface ReportModal { report?: BaseReport; @@ -76,7 +75,7 @@ export const ReportModal = (props: ReportModal): React.ReactElement => { const columns: ContentTableColumn[] = React.useMemo( () => - report && report.reportItems.length > 0 + report && report.reportItems?.length > 0 ? Object.keys(report.reportItems[0]).map((key) => ({ property: key, label: key, @@ -168,7 +167,7 @@ export const ReportModal = (props: ReportModal): React.ReactElement => { ) : ( {report.summary} )} - {columns.length > 0 && report.downloadImmediately !== true && ( + {columns.length > 0 && report.hasFile !== true && ( { export const useGetReportOnJobFinished = (jobId: string): BaseReport => { const { connectedServer } = useConnectedServer(); const [report, setReport] = useState(null); - const { exportData } = useExport(); if (!jobId) return null; @@ -225,12 +223,8 @@ export const useGetReportOnJobFinished = (jobId: string): BaseReport => { ); } else { setReport(report); - if (report.downloadImmediately === true) { - exportData( - report.title, - report.reportHeader, - report.reportBody - ); + if (report.hasFile === true) { + await JobService.downloadFile(jobId); } } } diff --git a/Src/WitsmlExplorer.Frontend/components/ReportCreationHelper.ts b/Src/WitsmlExplorer.Frontend/components/ReportCreationHelper.ts deleted file mode 100644 index 3efbd8876..000000000 --- a/Src/WitsmlExplorer.Frontend/components/ReportCreationHelper.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { defaultExportProperties } from "models/exportProperties"; -import { ContentTableColumn, ContentType } from "./ContentViews/table"; - -export interface ReportProperties { - columns: string; - data: string; -} - -export const generateReport = (reportItems: any[], reportHeader: string) => { - const columns: ContentTableColumn[] = - reportItems.length > 0 - ? Object.keys(reportItems[0]).map((key) => ({ - property: key, - label: key, - type: ContentType.String - })) - : []; - - const exportColumns = - reportHeader !== null - ? reportHeader - : columns - .map((column) => `${column.property}`) - .join(defaultExportProperties.separator); - - const data = reportItems - .map((row) => - columns - .map((col) => row[col.property] as string) - .join(defaultExportProperties.separator) - ) - .join(defaultExportProperties.newLineCharacter); - - return { exportColumns: exportColumns, data: data }; -}; diff --git a/Src/WitsmlExplorer.Frontend/models/reports/BaseReport.tsx b/Src/WitsmlExplorer.Frontend/models/reports/BaseReport.tsx index 4a98f4eb3..6d376183c 100644 --- a/Src/WitsmlExplorer.Frontend/models/reports/BaseReport.tsx +++ b/Src/WitsmlExplorer.Frontend/models/reports/BaseReport.tsx @@ -3,10 +3,8 @@ export default interface BaseReport { summary: string; reportItems: any[]; warningMessage?: string; - downloadImmediately?: boolean; - reportHeader?: string; + hasFile?: boolean; jobDetails?: string; - reportBody?: string; } export const createReport = ( @@ -14,19 +12,15 @@ export const createReport = ( summary = "", reportItems: any[] = [], warningMessage: string = null, - downloadImmediately: boolean = null, - reportHeader: string = null, - jobDetails: string = null, - reportBody: string = null + hasFile: boolean = null, + jobDetails: string = null ): BaseReport => { return { title, summary, reportItems, warningMessage, - downloadImmediately, - reportHeader, - jobDetails, - reportBody + hasFile, + jobDetails }; }; diff --git a/Src/WitsmlExplorer.Frontend/services/jobService.tsx b/Src/WitsmlExplorer.Frontend/services/jobService.tsx index cf5a84405..2cbb58456 100644 --- a/Src/WitsmlExplorer.Frontend/services/jobService.tsx +++ b/Src/WitsmlExplorer.Frontend/services/jobService.tsx @@ -126,6 +126,29 @@ export default class JobService { return null; } } + + public static async downloadFile(jobId: string): Promise { + const response = await ApiClient.get(`/api/jobs/download/${jobId}`); + + if (response.ok) { + const blob = await response.blob(); + + const contentDisposition = response.headers.get("Content-Disposition"); + const filename = + contentDisposition?.match(/filename="?([^";]+)"?/)?.[1] || "report"; + + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", filename); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + } else { + throwError(response.status, response.statusText); + } + } } export enum JobType { From 192cd092025fe08c6030bc66569ee3aef178f70c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sindre=20B=C3=B8rsheim?= Date: Mon, 11 Nov 2024 10:46:48 +0100 Subject: [PATCH 124/124] update workflow --- .github/workflows/ci_frontend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_frontend.yml b/.github/workflows/ci_frontend.yml index de81d9eac..6c03d8f40 100644 --- a/.github/workflows/ci_frontend.yml +++ b/.github/workflows/ci_frontend.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: - node-version: '16' + node-version: '20' - name: Install dependencies run: yarn working-directory: ./Src/WitsmlExplorer.Frontend