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 + , + + + Batch Update ]} /> 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 + }; + } +}