diff --git a/Src/Witsml/CommonConstants.cs b/Src/Witsml/CommonConstants.cs
index 615ddc1dd..3b2e56902 100644
--- a/Src/Witsml/CommonConstants.cs
+++ b/Src/Witsml/CommonConstants.cs
@@ -11,6 +11,8 @@ public static class CommonConstants
public const string NewLine = "\n";
public const int DefaultClientRequestTimeOutSeconds = 90;
public const int DefaultReloadIntervalMinutes = 15;
+ public const string Yes = "Yes";
+ public const string No = "No";
public static class DepthIndex
{
diff --git a/Src/Witsml/Helpers/EnumHelpers.cs b/Src/Witsml/Helpers/EnumHelpers.cs
new file mode 100644
index 000000000..20ed17da7
--- /dev/null
+++ b/Src/Witsml/Helpers/EnumHelpers.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+
+namespace Witsml.Helpers;
+
+///
+/// Helper class for working with Enum.
+///
+public static class EnumHelper
+{
+ ///
+ /// Gets a list of descriptions for all values in the specified Enum.
+ ///
+ /// The Enum type.
+ /// A list of string descriptions for all Enum values.
+ public static List GetEnumDescriptions() where TEnum : Enum
+ {
+ return Enum.GetValues(typeof(TEnum))
+ .Cast()
+ .Select(value => GetEnumDescription(value))
+ .ToList();
+ }
+
+ ///
+ /// Gets the description of a specific Enum value. If the description is not found, then return the enum value.
+ ///
+ /// The Enum value.
+ /// The Enum type.
+ /// The description of the Enum value.
+ public static string GetEnumDescription(TEnum value)
+ where TEnum : Enum
+ {
+ var field = typeof(TEnum).GetField(value.ToString());
+ return field != null &&
+ Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute))
+ is DescriptionAttribute descriptionAttribute
+ ? descriptionAttribute.Description
+ : value.ToString();
+ }
+}
diff --git a/Src/WitsmlExplorer.Api/Jobs/BatchModifyLogCurveInfoJob.cs b/Src/WitsmlExplorer.Api/Jobs/BatchModifyLogCurveInfoJob.cs
new file mode 100644
index 000000000..71438b660
--- /dev/null
+++ b/Src/WitsmlExplorer.Api/Jobs/BatchModifyLogCurveInfoJob.cs
@@ -0,0 +1,69 @@
+using System.Collections.Generic;
+using System.Linq;
+
+using WitsmlExplorer.Api.Jobs.Common;
+using WitsmlExplorer.Api.Models;
+
+namespace WitsmlExplorer.Api.Jobs
+{
+ ///
+ /// Job for batch modification of logCurveInfo.
+ ///
+ public record BatchModifyLogCurveInfoJob : Job
+ {
+ ///
+ /// WellboreReference API model.
+ ///
+ public WellboreReference WellboreReference { get; init; }
+
+ ///
+ /// Edited logCurveInfo API model.
+ ///
+ public LogCurveInfo EditedLogCurveInfo { get; init; }
+
+ ///
+ /// Collection of logCurveInfos and log Uids API models.
+ ///
+ public ICollection LogCurveInfoBatchItems
+ {
+ get;
+ init;
+ }
+
+ ///
+ /// Getting a description of batch-modified LogCurveInfos.
+ ///
+ ///
+ public override string Description()
+ {
+ return $"To Batch Modify - Uids: {string.Join(", ", LogCurveInfoBatchItems.Select(batchItem => batchItem.LogCurveInfoUid))}";
+ }
+
+ ///
+ /// Getting name of logCurveInfo.
+ ///
+ /// null
+ public override string GetObjectName()
+ {
+ return null;
+ }
+
+ ///
+ /// Getting name of wellbore.
+ ///
+ /// String of wellbore name.
+ public override string GetWellboreName()
+ {
+ return WellboreReference.WellboreName;
+ }
+
+ ///
+ /// Getting name of well.
+ ///
+ /// String of well name.
+ public override string GetWellName()
+ {
+ return WellboreReference.WellName;
+ }
+ }
+}
diff --git a/Src/WitsmlExplorer.Api/Models/JobType.cs b/Src/WitsmlExplorer.Api/Models/JobType.cs
index 71aca222c..4cf7589a4 100644
--- a/Src/WitsmlExplorer.Api/Models/JobType.cs
+++ b/Src/WitsmlExplorer.Api/Models/JobType.cs
@@ -42,6 +42,7 @@ public enum JobType
AnalyzeGaps,
SpliceLogs,
CompareLogData,
- CountLogDataRows
+ CountLogDataRows,
+ BatchModifyLogCurveInfo
}
}
diff --git a/Src/WitsmlExplorer.Api/Models/LogCurveInfo.cs b/Src/WitsmlExplorer.Api/Models/LogCurveInfo.cs
index 83d9cce5e..2360055ee 100644
--- a/Src/WitsmlExplorer.Api/Models/LogCurveInfo.cs
+++ b/Src/WitsmlExplorer.Api/Models/LogCurveInfo.cs
@@ -19,5 +19,13 @@ public class LogCurveInfo
public List AxisDefinitions { get; init; }
public string CurveDescription { get; init; }
public string TypeLogData { get; init; }
+ public string TraceState { get; init; }
+ public string NullValue { get; init; }
+ }
+
+ public class LogCurveInfoBatchItem
+ {
+ public string LogUid { get; init; }
+ public string LogCurveInfoUid { get; init; }
}
}
diff --git a/Src/WitsmlExplorer.Api/Models/LogTraceState.cs b/Src/WitsmlExplorer.Api/Models/LogTraceState.cs
new file mode 100644
index 000000000..5228ad630
--- /dev/null
+++ b/Src/WitsmlExplorer.Api/Models/LogTraceState.cs
@@ -0,0 +1,24 @@
+using System.ComponentModel;
+
+namespace WitsmlExplorer.Api.Models;
+
+public enum LogTraceState
+{
+ [Description("depth adjusted")]
+ DepthAdjusted,
+
+ [Description("edited")]
+ Edited,
+
+ [Description("joined")]
+ Joined,
+
+ [Description("processed")]
+ Processed,
+
+ [Description("raw")]
+ Raw,
+
+ [Description("unknown")]
+ Unknown
+}
diff --git a/Src/WitsmlExplorer.Api/Models/Reports/BatchModifyLogCurveInfoReport.cs b/Src/WitsmlExplorer.Api/Models/Reports/BatchModifyLogCurveInfoReport.cs
new file mode 100644
index 000000000..f02073da1
--- /dev/null
+++ b/Src/WitsmlExplorer.Api/Models/Reports/BatchModifyLogCurveInfoReport.cs
@@ -0,0 +1,19 @@
+namespace WitsmlExplorer.Api.Models.Reports;
+
+///
+/// Batch modification report for logCurveInfos.
+///
+public class BatchModifyLogCurveInfoReport : BaseReport
+{
+}
+
+///
+/// The item information about a batch modification is extended with LogUid.
+///
+public class BatchModifyLogCurveInfoReportItem : BatchModifyReportItem
+{
+ ///
+ /// Log unique identifier.
+ ///
+ public string LogUid { get; init; }
+}
diff --git a/Src/WitsmlExplorer.Api/Services/LogObjectService.cs b/Src/WitsmlExplorer.Api/Services/LogObjectService.cs
index 447ce6e25..6feb7712e 100644
--- a/Src/WitsmlExplorer.Api/Services/LogObjectService.cs
+++ b/Src/WitsmlExplorer.Api/Services/LogObjectService.cs
@@ -134,6 +134,8 @@ public async Task> GetLogCurveInfo(string wellUid, str
Unit = logCurveInfo.Unit,
CurveDescription = logCurveInfo.CurveDescription,
TypeLogData = logCurveInfo.TypeLogData,
+ TraceState = logCurveInfo.TraceState,
+ NullValue = logCurveInfo.NullValue,
AxisDefinitions = logCurveInfo.AxisDefinitions?.Select(a => new AxisDefinition()
{
Uid = a.Uid,
diff --git a/Src/WitsmlExplorer.Api/Workers/Modify/BatchModifyLogCurveInfoWorker.cs b/Src/WitsmlExplorer.Api/Workers/Modify/BatchModifyLogCurveInfoWorker.cs
new file mode 100644
index 000000000..498274685
--- /dev/null
+++ b/Src/WitsmlExplorer.Api/Workers/Modify/BatchModifyLogCurveInfoWorker.cs
@@ -0,0 +1,153 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+using Microsoft.Extensions.Logging;
+
+using Witsml;
+using Witsml.Data;
+using Witsml.Data.Measures;
+using Witsml.Helpers;
+using Witsml.ServiceReference;
+
+using WitsmlExplorer.Api.Jobs;
+using WitsmlExplorer.Api.Models;
+using WitsmlExplorer.Api.Models.Reports;
+using WitsmlExplorer.Api.Services;
+
+namespace WitsmlExplorer.Api.Workers.Modify;
+
+///
+/// Worker for batch modification of LogCurveInfo.
+///
+public class BatchModifyLogCurveInfoWorker : BaseWorker, IWorker
+{
+ public JobType JobType => JobType.BatchModifyLogCurveInfo;
+
+ public BatchModifyLogCurveInfoWorker(ILogger logger, IWitsmlClientProvider witsmlClientProvider) : base(witsmlClientProvider, logger) { }
+
+ ///
+ /// Executes a batch modification job for LogCurveInfoItem properties.
+ ///
+ /// The job model contains batch modification parameters for logCurveInfo.
+ /// Task of the worker Result in a report with a result of batch modification.
+ public override async Task<(WorkerResult, RefreshAction)> Execute(BatchModifyLogCurveInfoJob job)
+ {
+ Verify(job);
+
+ Logger.LogInformation("Started {JobType}. {jobDescription}", JobType, job.Description());
+
+ WitsmlLogs logHeaders = await GetLogHeaders(job.WellboreReference.WellUid, job.WellboreReference.WellboreUid, job.LogCurveInfoBatchItems.Select(x => x.LogUid).Distinct().ToArray());
+
+ IList<(string logUid, WitsmlLogCurveInfo logCurveInfo)> originalLogCurveInfoData = job.LogCurveInfoBatchItems
+ .SelectMany(batchItem =>
+ {
+ WitsmlLog logHeader = logHeaders.Logs.Find(l => l.Uid == batchItem.LogUid);
+ var curveInfo = logHeader?.LogCurveInfo.FirstOrDefault(c => c.Uid == batchItem.LogCurveInfoUid);
+ return curveInfo != null ? new[] { (logHeader.Uid, curveInfo) } : Array.Empty<(string, WitsmlLogCurveInfo)>();
+ })
+ .ToList();
+
+ IList logCurveInfosToUpdateQueries = originalLogCurveInfoData
+ .Select(obj => GetModifyLogCurveInfoQuery(job, obj)).ToList();
+ List modifyResults = logCurveInfosToUpdateQueries
+ .Select(async query => await GetTargetWitsmlClientOrThrow().UpdateInStoreAsync(query))
+ .Select(updateTask => updateTask.Result).ToList();
+
+ var report = CreateReport(job, originalLogCurveInfoData, modifyResults);
+ job.JobInfo.Report = report;
+
+ if (modifyResults.Any(result => !result.IsSuccessful))
+ {
+ string errorMessage = $"Failed to modify some LogCurveInfos";
+ var reason = "Inspect the report for details";
+ Logger.LogError("{ErrorMessage}. {jobDescription}", errorMessage, job.Description());
+ return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, errorMessage, reason, null, job.JobInfo.Id), null);
+ }
+
+ Logger.LogInformation("{JobType} - Job successful", GetType().Name);
+ WorkerResult workerResult = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), true, $"The LogCurveInfo properties have been updated in the batch.", jobId: job.JobInfo.Id);
+ RefreshObjects refreshAction = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), job.WellboreReference.WellUid, job.WellboreReference.WellboreUid, EntityType.Log);
+ return (workerResult, refreshAction);
+ }
+
+ private BatchModifyLogCurveInfoReport CreateReport(BatchModifyLogCurveInfoJob job, IList<(string logUid, WitsmlLogCurveInfo logCurveInfo)> logCurveInfoData, IList results)
+ {
+ var reportItems = logCurveInfoData.Select((obj, index) => new BatchModifyLogCurveInfoReportItem
+ {
+ WellUid = job.WellboreReference.WellUid,
+ WellboreUid = job.WellboreReference.WellboreUid,
+ LogUid = obj.logUid,
+ Uid = obj.logCurveInfo.Uid,
+ IsSuccessful = results[index].IsSuccessful ? CommonConstants.Yes : CommonConstants.No,
+ FailureReason = results[index].IsSuccessful ? string.Empty : results[index].Reason
+ }).ToList();
+
+ return new BatchModifyLogCurveInfoReport()
+ {
+ Title = "Batch Update LogCurveInfo Report",
+ Summary = $"Updated {logCurveInfoData.Count} objects",
+ WarningMessage = results.Any(result => !result.IsSuccessful) ? "Some logCurveInfos were not modified. Inspect the reasons below." : null,
+ ReportItems = reportItems
+ };
+ }
+
+ private WitsmlLogs GetModifyLogCurveInfoQuery(BatchModifyLogCurveInfoJob job, (string logUid, WitsmlLogCurveInfo logCurveInfo) originalLogCurveInfoData)
+ {
+ if (!string.IsNullOrEmpty(job.EditedLogCurveInfo.TraceState))
+ {
+ originalLogCurveInfoData.logCurveInfo.TraceState = job.EditedLogCurveInfo.TraceState;
+ }
+
+ originalLogCurveInfoData.logCurveInfo.SensorOffset = job.EditedLogCurveInfo.SensorOffset?.ToWitsml();
+
+ if (!string.IsNullOrEmpty(job.EditedLogCurveInfo.NullValue))
+ {
+ originalLogCurveInfoData.logCurveInfo.NullValue = job.EditedLogCurveInfo.NullValue;
+ }
+
+ return new()
+ {
+ Logs = new List
+ {
+ new()
+ {
+ UidWell = job.WellboreReference.WellUid,
+ UidWellbore = job.WellboreReference.WellboreUid,
+ Uid = originalLogCurveInfoData.logUid,
+ LogCurveInfo = new List()
+ {
+ originalLogCurveInfoData.logCurveInfo
+ }
+ }
+ }
+ };
+ }
+
+ private async Task GetLogHeaders(string wellUid, string wellboreUid, string[] logUids)
+ {
+ return await LogWorkerTools.GetLogsByIds(GetTargetWitsmlClientOrThrow(), wellUid, wellboreUid, logUids, ReturnElements.HeaderOnly);
+ }
+
+ private void Verify(BatchModifyLogCurveInfoJob job)
+ {
+ if (!job.LogCurveInfoBatchItems.Any())
+ {
+ throw new InvalidOperationException("LogCurveInfoBatchItems must be specified");
+ }
+
+ if (string.IsNullOrEmpty(job.WellboreReference.WellUid))
+ {
+ throw new InvalidOperationException("WellUid cannot be empty");
+ }
+
+ if (string.IsNullOrEmpty(job.WellboreReference.WellboreUid))
+ {
+ throw new InvalidOperationException("WellboreUid cannot be empty");
+ }
+
+ ModifyUtils.VerifyMeasure(job.EditedLogCurveInfo.SensorOffset, nameof(job.EditedLogCurveInfo.SensorOffset));
+ ModifyUtils.VerifyAllowedValues(job.EditedLogCurveInfo.TraceState, EnumHelper.GetEnumDescriptions(), nameof(job.EditedLogCurveInfo.TraceState));
+ }
+}
diff --git a/Src/WitsmlExplorer.Frontend/__testUtils__/testUtils.tsx b/Src/WitsmlExplorer.Frontend/__testUtils__/testUtils.tsx
index 7db795f13..0af7e1c41 100644
--- a/Src/WitsmlExplorer.Frontend/__testUtils__/testUtils.tsx
+++ b/Src/WitsmlExplorer.Frontend/__testUtils__/testUtils.tsx
@@ -450,6 +450,8 @@ export function getLogCurveInfo(
curveDescription: "curveDescription",
typeLogData: "typeLogData",
sensorOffset: getMeasure(),
+ nullValue: "123",
+ traceState: "raw",
...overrides
};
}
diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListView.tsx
index 30950e61c..e34bea2ae 100644
--- a/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListView.tsx
+++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/LogCurveInfoListView.tsx
@@ -40,7 +40,6 @@ export interface LogCurveInfoRow extends ContentTableRow {
isActive: boolean;
logCurveInfo: LogCurveInfo;
}
-
export const LogCurveInfoListView = (): React.ReactElement => {
const { navigationState, dispatchNavigation } = useContext(NavigationContext);
const {
@@ -179,6 +178,8 @@ export const LogCurveInfoListView = (): React.ReactElement => {
unit: logCurveInfo.unit,
sensorOffset: measureToString(logCurveInfo.sensorOffset),
mnemAlias: logCurveInfo.mnemAlias,
+ traceState: logCurveInfo.traceState,
+ nullValue: logCurveInfo.nullValue,
logUid: selectedLog.uid,
wellUid: selectedWell.uid,
wellboreUid: selectedWellbore.uid,
@@ -250,6 +251,8 @@ export const LogCurveInfoListView = (): React.ReactElement => {
type: ContentType.Measure
},
{ property: "mnemAlias", label: "mnemAlias", type: ContentType.String },
+ { property: "traceState", label: "traceState", type: ContentType.String },
+ { property: "nullValue", label: "nullValue", type: ContentType.String },
{ property: "uid", label: "uid", type: ContentType.String }
];
diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurveInfoContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurveInfoContextMenu.tsx
index a7bc7bb36..313e82875 100644
--- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurveInfoContextMenu.tsx
+++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/LogCurveInfoContextMenu.tsx
@@ -39,6 +39,7 @@ import React from "react";
import { JobType } from "services/jobService";
import LogCurvePriorityService from "services/logCurvePriorityService";
import { colors } from "styles/Colors";
+import LogCurveInfoBatchUpdateModal from "../Modals/LogCurveInfoBatchUpdateModal";
export interface LogCurveInfoContextMenuProps {
checkedLogCurveInfoRows: LogCurveInfoRow[];
@@ -119,6 +120,21 @@ const LogCurveInfoContextMenu = (
});
};
+ const onClickBatchUpdate = () => {
+ const logCurveInfoRows = checkedLogCurveInfoRows;
+ const logCurveInfoBatchUpdateModalProps = {
+ logCurveInfoRows,
+ selectedLog
+ };
+ dispatchOperation({
+ type: OperationType.DisplayModal,
+ payload: (
+
+ )
+ });
+ dispatchOperation({ type: OperationType.HideContextMenu });
+ };
+
const onClickAnalyzeGaps = () => {
dispatchOperation({ type: OperationType.HideContextMenu });
const logObject = selectedLog;
@@ -332,6 +348,17 @@ const LogCurveInfoContextMenu = (
color={colors.interactive.primaryResting}
/>
Properties
+ ,
+
]}
/>
diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/LogCurveInfoBatchUpdateModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/LogCurveInfoBatchUpdateModal.tsx
new file mode 100644
index 000000000..f81da6422
--- /dev/null
+++ b/Src/WitsmlExplorer.Frontend/components/Modals/LogCurveInfoBatchUpdateModal.tsx
@@ -0,0 +1,179 @@
+import { Grid } from "@material-ui/core";
+import React, { useContext, useState } from "react";
+import styled from "styled-components";
+import LogCurveInfo, { EmptyLogCurveInfo } from "../../models/logCurveInfo";
+import LogObject from "../../models/logObject";
+import { toObjectReference } from "../../models/objectOnWellbore";
+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 { 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[];
+ selectedLog: LogObject;
+}
+
+const LogCurveInfoBatchUpdateModal = (
+ props: LogCurveInfoBatchUpdateModalProps
+): React.ReactElement => {
+ const { logCurveInfoRows, selectedLog } = props;
+ const { dispatchOperation } = useContext(OperationContext);
+ const [editableLogCurveInfo, setEditableLogCurveInfo] =
+ useState(EmptyLogCurveInfo);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const onSubmit = async () => {
+ setIsLoading(true);
+ const job: BatchModifyLogCurveInfoJob = {
+ wellboreReference: toObjectReference(selectedLog),
+ editedLogCurveInfo: editableLogCurveInfo,
+ logCurveInfoBatchItems: logCurveInfoRows.map(
+ (logCurveInfoRow: LogCurveInfoRow) => {
+ return {
+ logUid: logCurveInfoRow.logUid,
+ logCurveInfoUid: logCurveInfoRow.logCurveInfo.uid
+ };
+ }
+ )
+ };
+ const jobId = await JobService.orderJob(
+ JobType.BatchModifyLogCurveInfo,
+ job
+ );
+ setIsLoading(false);
+ dispatchOperation({ type: OperationType.HideModal });
+ if (jobId) {
+ const reportModalProps = { jobId };
+ dispatchOperation({
+ type: OperationType.DisplayModal,
+ payload:
+ });
+ }
+ };
+
+ const validateSensorOffset = (edited: Measure): boolean => {
+ return (
+ edited?.value !== undefined &&
+ edited?.uom !== undefined &&
+ !isNaN(edited?.value) &&
+ validText(edited?.uom)
+ );
+ };
+
+ const isSensorOffsetValid = validateSensorOffset(
+ editableLogCurveInfo.sensorOffset
+ );
+
+ const validateSensorOffsetWithValueInvalid = (sensorOffset: Measure) =>
+ (sensorOffset?.value !== undefined || sensorOffset?.uom !== undefined) &&
+ !isSensorOffsetValid;
+
+ const isSensorOffsetWithValueInvalid = validateSensorOffsetWithValueInvalid(
+ editableLogCurveInfo.sensorOffset
+ );
+
+ return (
+ <>
+ {editableLogCurveInfo && (
+
+ {
+ setEditableLogCurveInfo({
+ ...editableLogCurveInfo,
+ traceState: selectedItems[0]
+ });
+ }}
+ />
+
+
+
+
+ setEditableLogCurveInfo({
+ ...editableLogCurveInfo,
+ sensorOffset: {
+ value: isNaN(parseFloat(e.target.value))
+ ? undefined
+ : parseFloat(e.target.value),
+ uom: editableLogCurveInfo.sensorOffset?.uom
+ }
+ })
+ }
+ />
+
+
+ {
+ setEditableLogCurveInfo({
+ ...editableLogCurveInfo,
+ sensorOffset: {
+ value: editableLogCurveInfo.sensorOffset?.value,
+ uom: selectedItems[0]
+ }
+ });
+ }}
+ />
+
+
+
+
+ setEditableLogCurveInfo({
+ ...editableLogCurveInfo,
+ nullValue: e.target.value
+ })
+ }
+ />
+
+ }
+ confirmDisabled={
+ (!validText(editableLogCurveInfo.traceState) &&
+ !validText(editableLogCurveInfo.nullValue) &&
+ !isSensorOffsetValid) ||
+ isSensorOffsetWithValueInvalid
+ }
+ onSubmit={() => onSubmit()}
+ isLoading={isLoading}
+ />
+ )}
+ >
+ );
+};
+
+const Layout = styled.div`
+ display: grid;
+ grid-template-columns: repeat(1, auto);
+ gap: 1rem;
+`;
+
+export default LogCurveInfoBatchUpdateModal;
diff --git a/Src/WitsmlExplorer.Frontend/models/jobs/batchModifyLogCurveInfoJob.ts b/Src/WitsmlExplorer.Frontend/models/jobs/batchModifyLogCurveInfoJob.ts
new file mode 100644
index 000000000..a62076d3e
--- /dev/null
+++ b/Src/WitsmlExplorer.Frontend/models/jobs/batchModifyLogCurveInfoJob.ts
@@ -0,0 +1,7 @@
+import LogCurveInfo, { LogCurveInfoBatchItem } from "models/logCurveInfo";
+import WellboreReference from "./wellboreReference";
+export default interface BatchModifyLogCurveInfoJob {
+ wellboreReference: WellboreReference;
+ editedLogCurveInfo: LogCurveInfo;
+ logCurveInfoBatchItems: LogCurveInfoBatchItem[];
+}
diff --git a/Src/WitsmlExplorer.Frontend/models/logCurveInfo.ts b/Src/WitsmlExplorer.Frontend/models/logCurveInfo.ts
index 9c6e36352..0ff13ff8d 100644
--- a/Src/WitsmlExplorer.Frontend/models/logCurveInfo.ts
+++ b/Src/WitsmlExplorer.Frontend/models/logCurveInfo.ts
@@ -15,6 +15,33 @@ export default interface LogCurveInfo {
typeLogData: string;
mnemAlias: string;
axisDefinitions: AxisDefinition[];
+ traceState: string;
+ nullValue: string;
+}
+
+export function EmptyLogCurveInfo(): LogCurveInfo {
+ return {
+ uid: "",
+ mnemonic: "",
+ minDateTimeIndex: null,
+ minDepthIndex: null,
+ maxDateTimeIndex: null,
+ maxDepthIndex: null,
+ classWitsml: null,
+ unit: null,
+ sensorOffset: null,
+ curveDescription: "",
+ typeLogData: "",
+ mnemAlias: "",
+ axisDefinitions: [],
+ traceState: null,
+ nullValue: ""
+ };
+}
+
+export interface LogCurveInfoBatchItem {
+ logCurveInfoUid: string;
+ logUid: string;
}
export const NULL_DEPTH_INDEX = "-999.25";
diff --git a/Src/WitsmlExplorer.Frontend/models/logTraceState.ts b/Src/WitsmlExplorer.Frontend/models/logTraceState.ts
new file mode 100644
index 000000000..38ea9848e
--- /dev/null
+++ b/Src/WitsmlExplorer.Frontend/models/logTraceState.ts
@@ -0,0 +1,8 @@
+export const logTraceState = [
+ "depth adjusted",
+ "edited",
+ "joined",
+ "processed",
+ "raw",
+ "unknown"
+].sort((a, b) => a.localeCompare(b));
diff --git a/Src/WitsmlExplorer.Frontend/models/unitType.ts b/Src/WitsmlExplorer.Frontend/models/unitType.ts
new file mode 100644
index 000000000..b43b4b46d
--- /dev/null
+++ b/Src/WitsmlExplorer.Frontend/models/unitType.ts
@@ -0,0 +1,3 @@
+export const unitType = ["m", "ft", "cm", "in"].sort((a, b) =>
+ a.localeCompare(b)
+);
diff --git a/Src/WitsmlExplorer.Frontend/services/jobService.tsx b/Src/WitsmlExplorer.Frontend/services/jobService.tsx
index 513273930..ebca01d07 100644
--- a/Src/WitsmlExplorer.Frontend/services/jobService.tsx
+++ b/Src/WitsmlExplorer.Frontend/services/jobService.tsx
@@ -115,6 +115,7 @@ export enum JobType {
MissingData = "MissingData",
ModifyGeologyInterval = "ModifyGeologyInterval",
ModifyLogCurveInfo = "ModifyLogCurveInfo",
+ BatchModifyLogCurveInfo = "BatchModifyLogCurveInfo",
DeleteEmptyMnemonics = "DeleteEmptyMnemonics",
ModifyTrajectoryStation = "ModifyTrajectoryStation",
ModifyTubularComponent = "ModifyTubularComponent",
diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/BatchModifyLogCurveInfoTests.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/BatchModifyLogCurveInfoTests.cs
new file mode 100644
index 000000000..6c76e7e2f
--- /dev/null
+++ b/Tests/WitsmlExplorer.Api.Tests/Workers/BatchModifyLogCurveInfoTests.cs
@@ -0,0 +1,204 @@
+using System;
+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.Helpers;
+using Witsml.ServiceReference;
+
+using WitsmlExplorer.Api.Jobs;
+using WitsmlExplorer.Api.Jobs.Common;
+using WitsmlExplorer.Api.Models;
+using WitsmlExplorer.Api.Models.Measure;
+using WitsmlExplorer.Api.Models.Reports;
+using WitsmlExplorer.Api.Services;
+using WitsmlExplorer.Api.Workers.Modify;
+
+using Xunit;
+
+namespace WitsmlExplorer.Api.Tests.Workers;
+
+///
+/// Unit tests for the functionality of batch modifying log curve information.
+///
+public class BatchModifyLogCurveInfoTests
+{
+ private readonly Mock _witsmlClient;
+ private readonly BatchModifyLogCurveInfoWorker _worker;
+
+ private const string WellUid = "wellUid";
+ private const string WellboreUid = "wellboreUid";
+ private const string LogUid1 = "logUid1";
+ private const string LogUid2 = "logUid2";
+ private const string LogUid3 = "logUid3";
+ private const string LogCurveInfoUid1 = "LogCurveInfoUid1";
+ private const string LogCurveInfoUid2 = "LogCurveInfoUid2";
+ private const string LogCurveInfoUid3 = "LogCurveInfoUid3";
+ private const string NullValue = "123";
+
+ public BatchModifyLogCurveInfoTests()
+ {
+ 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 BatchModifyLogCurveInfoWorker(logger, witsmlClientProvider.Object);
+ }
+
+ [Fact]
+ public async Task BatchModifyLogCurveInfo_CorrectData_IsValid()
+ {
+ BatchModifyLogCurveInfoJob job = GetJobTemplate();
+ JobInfo jobInfo = new();
+ job.JobInfo = jobInfo;
+
+ _witsmlClient.Setup(client =>
+ client.GetFromStoreAsync(It.IsAny(), It.IsAny()))
+ .Returns(Task.FromResult(GetTestWitsmlLogs()));
+
+ List updatedLogs = new();
+ _witsmlClient.Setup(client =>
+ client.UpdateInStoreAsync(It.IsAny())).Callback(logs => updatedLogs.Add(logs as WitsmlLogs))
+ .ReturnsAsync(new QueryResult(true));
+
+ (_, _) = await _worker.Execute(job);
+
+ BatchModifyLogCurveInfoReport report = (BatchModifyLogCurveInfoReport)job.JobInfo.Report;
+ IEnumerable reportItems = (IEnumerable)report.ReportItems;
+
+ Assert.Equal(3, updatedLogs.Count);
+ Assert.Equal(WellUid, updatedLogs.FirstOrDefault()?.Logs.FirstOrDefault()?.UidWell);
+ Assert.Equal(WellboreUid, updatedLogs.FirstOrDefault()?.Logs.FirstOrDefault()?.UidWellbore);
+ Assert.Equal(LogCurveInfoUid1, updatedLogs.FirstOrDefault()?.Logs.FirstOrDefault()?.LogCurveInfo
+ .FirstOrDefault()?.Uid);
+ Assert.Equal(LogUid1, updatedLogs.FirstOrDefault()?.Logs.FirstOrDefault()?.Uid);
+ Assert.Equal(LogCurveInfoUid3, updatedLogs.LastOrDefault()?.Logs.LastOrDefault()?.LogCurveInfo
+ .LastOrDefault()?.Uid);
+ Assert.Equal(LogUid3, updatedLogs.LastOrDefault()?.Logs.LastOrDefault()?.Uid);
+ Assert.Equal(3, reportItems.Count());
+ Assert.Equal(CommonConstants.Yes, reportItems.FirstOrDefault()?.IsSuccessful);
+ Assert.Equal(CommonConstants.Yes, reportItems.LastOrDefault().IsSuccessful);
+ }
+
+ [Fact]
+ public async Task BatchModifyLogCurveInfo_NoCurveInfoItems_InvalidOperationException()
+ {
+ BatchModifyLogCurveInfoJob job = GetJobTemplate(null, emptyLogCurveInfoItems: true);
+ JobInfo jobInfo = new();
+ job.JobInfo = jobInfo;
+
+ InvalidOperationException exception = await Assert.ThrowsAsync(() => _worker.Execute(job));
+ Assert.Equal("LogCurveInfoBatchItems must be specified", exception.Message);
+ _witsmlClient.Verify(client => client.UpdateInStoreAsync(It.IsAny()), Times.Never);
+ }
+
+ [Fact]
+ public async Task BatchModifyLogCurveInfo_EmptyWellUid_InvalidOperationException()
+ {
+ BatchModifyLogCurveInfoJob job = GetJobTemplate();
+ job.WellboreReference.WellUid = string.Empty;
+ JobInfo jobInfo = new();
+ job.JobInfo = jobInfo;
+
+ InvalidOperationException exception = await Assert.ThrowsAsync(() => _worker.Execute(job));
+ Assert.Equal("WellUid cannot be empty", exception.Message);
+ _witsmlClient.Verify(client => client.UpdateInStoreAsync(It.IsAny()), Times.Never);
+ }
+
+ [Fact]
+ public async Task BatchModifyLogCurveInfo_TraceState_InvalidOperationException()
+ {
+ BatchModifyLogCurveInfoJob job = GetJobTemplate("WrongTraceStateType");
+ JobInfo jobInfo = new();
+ job.JobInfo = jobInfo;
+
+ InvalidOperationException exception = await Assert.ThrowsAsync(() => _worker.Execute(job));
+ Assert.Equal($"WrongTraceStateType is not an allowed value for TraceState", exception.Message);
+ _witsmlClient.Verify(client => client.UpdateInStoreAsync(It.IsAny()), Times.Never);
+ }
+
+ private static WitsmlLogs GetTestWitsmlLogs()
+ {
+ return new WitsmlLogs
+ {
+ Logs = new List()
+ {
+ new()
+ {
+ UidWell = WellUid,
+ UidWellbore = WellboreUid,
+ Uid = LogUid1,
+ LogCurveInfo = new List()
+ {
+ new() { Uid = LogCurveInfoUid1 },
+ }
+ },
+ new()
+ {
+ UidWell = WellUid,
+ UidWellbore = WellboreUid,
+ Uid = LogUid2,
+ LogCurveInfo = new List()
+ {
+ new() { Uid = LogCurveInfoUid2 },
+ }
+ },
+ new()
+ {
+ UidWell = WellUid,
+ UidWellbore = WellboreUid,
+ Uid = LogUid3,
+ LogCurveInfo = new List()
+ {
+ new() { Uid = LogCurveInfoUid3 },
+ }
+ },
+ }
+ };
+ }
+
+ private static BatchModifyLogCurveInfoJob GetJobTemplate(
+ string logTraceState = null, bool emptyLogCurveInfoItems = false)
+ {
+ var logCurveInfoBatchItems = new List()
+ {
+ new() { LogUid = LogUid1, LogCurveInfoUid = LogCurveInfoUid1 },
+ new() { LogUid = LogUid2, LogCurveInfoUid = LogCurveInfoUid2 },
+ new() { LogUid = LogUid3, LogCurveInfoUid = LogCurveInfoUid3 }
+ };
+
+ return new BatchModifyLogCurveInfoJob()
+ {
+ WellboreReference = new WellboreReference()
+ {
+ WellUid = WellUid,
+ WellboreUid = WellboreUid
+ },
+ EditedLogCurveInfo = new LogCurveInfo()
+ {
+ TraceState =
+ logTraceState ??
+ EnumHelper.GetEnumDescription(LogTraceState.Raw),
+ SensorOffset = new LengthMeasure()
+ {
+ Value = 22,
+ Uom = CommonConstants.Unit.Meter
+ },
+ NullValue = NullValue
+ },
+ LogCurveInfoBatchItems = emptyLogCurveInfoItems
+ ? new List()
+ : logCurveInfoBatchItems
+ };
+ }
+}