diff --git a/Src/WitsmlExplorer.Api/Jobs/CreateTrajectoryJob.cs b/Src/WitsmlExplorer.Api/Jobs/CreateTrajectoryJob.cs new file mode 100644 index 000000000..2ba40901a --- /dev/null +++ b/Src/WitsmlExplorer.Api/Jobs/CreateTrajectoryJob.cs @@ -0,0 +1,50 @@ +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/Models/JobType.cs b/Src/WitsmlExplorer.Api/Models/JobType.cs index d8c65a4ed..466bfce0d 100644 --- a/Src/WitsmlExplorer.Api/Models/JobType.cs +++ b/Src/WitsmlExplorer.Api/Models/JobType.cs @@ -37,6 +37,7 @@ public enum JobType CreateRisk, CreateMudLog, CreateRig, + CreateTrajectory, CreateWbGeometry, BatchModifyWell, ImportLogData, diff --git a/Src/WitsmlExplorer.Api/Models/Trajectory.cs b/Src/WitsmlExplorer.Api/Models/Trajectory.cs index 313220934..da8eb9bbf 100644 --- a/Src/WitsmlExplorer.Api/Models/Trajectory.cs +++ b/Src/WitsmlExplorer.Api/Models/Trajectory.cs @@ -6,14 +6,14 @@ namespace WitsmlExplorer.Api.Models { public class Trajectory : ObjectOnWellbore { - public decimal? MdMin { get; internal init; } - public decimal? MdMax { get; internal init; } - public string AziRef { get; internal init; } - public string DTimTrajStart { get; internal init; } - public string DTimTrajEnd { get; internal init; } - public List TrajectoryStations { get; internal init; } - public string ServiceCompany { get; internal init; } - public string DateTimeCreation { get; internal init; } - public string DateTimeLastChange { get; internal init; } + public decimal? MdMin { get; init; } + public decimal? MdMax { get; init; } + public string AziRef { get; init; } + public string DTimTrajStart { get; init; } + public string DTimTrajEnd { get; init; } + public List TrajectoryStations { get; init; } + public string ServiceCompany { get; init; } + public string DateTimeCreation { get; init; } + public string DateTimeLastChange { get; init; } } } diff --git a/Src/WitsmlExplorer.Api/Query/TrajectoryQueries.cs b/Src/WitsmlExplorer.Api/Query/TrajectoryQueries.cs index 87afdf3f1..dfb83295a 100644 --- a/Src/WitsmlExplorer.Api/Query/TrajectoryQueries.cs +++ b/Src/WitsmlExplorer.Api/Query/TrajectoryQueries.cs @@ -90,6 +90,34 @@ public static WitsmlTrajectories DeleteTrajectoryStations(string wellUid, string }.AsSingletonList() }; } + + /// + /// Create trajectories witsml model. + /// + /// API model of trajectory data. + /// New instance of WitsmlTrajectories model with added trajectory data. + public static WitsmlTrajectories CreateTrajectory(Trajectory trajectory) + { + return new() + { + Trajectories = new WitsmlTrajectory + { + UidWell = trajectory.WellUid, + NameWell = trajectory.WellName, + NameWellbore = trajectory.WellboreName, + Uid = trajectory.Uid, + Name = trajectory.Name, + UidWellbore = trajectory.WellboreUid, + MdMin = trajectory.MdMin != null ? new WitsmlMeasuredDepthCoord() { Value = trajectory.MdMin.Value.ToString(CultureInfo.InvariantCulture) } : null, + MdMax = trajectory.MdMax != null ? new WitsmlMeasuredDepthCoord() { Value = trajectory.MdMax.Value.ToString(CultureInfo.InvariantCulture) } : null, + AziRef = trajectory.AziRef.NullIfEmpty(), + ServiceCompany = trajectory.ServiceCompany.NullIfEmpty(), + DTimTrajStart = StringHelpers.ToUniversalDateTimeString(trajectory.DTimTrajStart), + DTimTrajEnd = StringHelpers.ToUniversalDateTimeString(trajectory.DTimTrajEnd), + }.AsSingletonList() + }; + } + public static WitsmlTrajectories UpdateTrajectoryStation(TrajectoryStation trajectoryStation, ObjectReference trajectoryReference) { WitsmlTrajectoryStation ts = new() diff --git a/Src/WitsmlExplorer.Api/Workers/Create/CreateTrajectoryWorker.cs b/Src/WitsmlExplorer.Api/Workers/Create/CreateTrajectoryWorker.cs new file mode 100644 index 000000000..a10481a6f --- /dev/null +++ b/Src/WitsmlExplorer.Api/Workers/Create/CreateTrajectoryWorker.cs @@ -0,0 +1,63 @@ +using System; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Witsml; +using Witsml.Data; + +using WitsmlExplorer.Api.Jobs; +using WitsmlExplorer.Api.Models; +using WitsmlExplorer.Api.Query; +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) + { + Verify(job.Trajectory); + + WitsmlTrajectories trajectoryToCreate = TrajectoryQueries.CreateTrajectory(job.Trajectory); + + 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.Frontend/components/ContextMenus/TrajectoriesContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoriesContextMenu.tsx new file mode 100644 index 000000000..1a73bd356 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoriesContextMenu.tsx @@ -0,0 +1,79 @@ +import { Typography } from "@equinor/eds-core-react"; +import { MenuItem } from "@material-ui/core"; +import React, { useContext } from "react"; +import { v4 as uuid } from "uuid"; +import { DisplayModalAction, HideContextMenuAction, HideModalAction } from "../../contexts/operationStateReducer"; +import OperationType from "../../contexts/operationType"; +import Wellbore from "../../models/wellbore"; +import { colors } from "../../styles/Colors"; +import { PropertiesModalMode } from "../Modals/ModalParts"; +import ContextMenu from "./ContextMenu"; +import { menuItemText, onClickRefresh, StyledIcon } from "./ContextMenuUtils"; +import TrajectoryPropertiesModal, { TrajectoryPropertiesModalProps } from "../Modals/TrajectoryPropertiesModal"; +import { ObjectType } from "../../models/objectType"; +import { pasteObjectOnWellbore } from "./CopyUtils"; +import { useClipboardReferencesOfType } from "./UseClipboardReferences"; +import NavigationContext from "../../contexts/navigationContext"; +import { Server } from "../../models/server"; +import Trajectory from "../../models/trajectory"; + +export interface TrajectoriesContextMenuProps { + dispatchOperation: (action: DisplayModalAction | HideModalAction | HideContextMenuAction) => void; + wellbore: Wellbore; + servers: Server[]; + setIsLoading?: (arg: boolean) => void; +} + +const TrajectoriesContextMenu = (props: TrajectoriesContextMenuProps): React.ReactElement => { + const { dispatchOperation, wellbore, servers, setIsLoading } = props; + const { dispatchNavigation } = useContext(NavigationContext); + const trajectoryReferences = useClipboardReferencesOfType(ObjectType.Trajectory); + + const onClickNewTrajectory = () => { + const newTrajectory: Trajectory = { + uid: uuid(), + name: "", + wellUid: wellbore.wellUid, + wellName: wellbore.wellName, + wellboreUid: wellbore.uid, + wellboreName: wellbore.name, + serviceCompany: "", + aziRef: "", + dTimTrajEnd: "", + dTimTrajStart: "", + mdMax: null, + mdMin: null, + trajectoryStations: [] + }; + const trajectoryPropertiesModalProps: TrajectoryPropertiesModalProps = { mode: PropertiesModalMode.New, trajectory: newTrajectory, dispatchOperation }; + const action: DisplayModalAction = { type: OperationType.DisplayModal, payload: }; + dispatchOperation(action); + }; + + return ( + onClickRefresh(dispatchOperation, dispatchNavigation, wellbore.wellUid, wellbore.uid, ObjectType.Trajectory, setIsLoading)}> + + {`Refresh Trajectories`} + + ) : null, + + + New Trajectory + , + pasteObjectOnWellbore(servers, trajectoryReferences, dispatchOperation, wellbore)} + disabled={trajectoryReferences === null} + > + + {menuItemText("paste", "trajectory", trajectoryReferences?.objectUids)} + + ]} + /> + ); +}; + +export default TrajectoriesContextMenu; diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/TrajectoryPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/TrajectoryPropertiesModal.tsx new file mode 100644 index 000000000..c4e963993 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/Modals/TrajectoryPropertiesModal.tsx @@ -0,0 +1,155 @@ +import { TextField } from "@material-ui/core"; +import React, { useContext, useEffect, useState } from "react"; +import { HideModalAction } from "../../contexts/operationStateReducer"; +import OperationType from "../../contexts/operationType"; +import JobService, { JobType } from "../../services/jobService"; +import ModalDialog from "./ModalDialog"; +import { PropertiesModalMode, validText } from "./ModalParts"; +import Trajectory from "../../models/trajectory"; +import { DateTimeField } from "./DateTimeField"; +import OperationContext from "../../contexts/operationContext"; +export interface TrajectoryPropertiesModalProps { + mode: PropertiesModalMode; + trajectory: Trajectory; + dispatchOperation: (action: HideModalAction) => void; +} + +const TrajectoryPropertiesModal = (props: TrajectoryPropertiesModalProps): React.ReactElement => { + const { mode, trajectory, dispatchOperation } = props; + const { + operationState: { timeZone } + } = useContext(OperationContext); + 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 + }); + }, [trajectory]); + + const onSubmit = async (updatedTrajectory: Trajectory) => { + setIsLoading(true); + const wellboreTrajectoryJob = { + trajectory: updatedTrajectory + }; + await JobService.orderJob(editMode ? JobType.ModifyTrajectory : JobType.CreateTrajectory, wellboreTrajectoryJob); + setIsLoading(false); + dispatchOperation({ type: OperationType.HideModal }); + }; + + return ( + <> + {editableTrajectory && ( + + setEditableTrajectory({ ...editableTrajectory, uid: e.target.value })} + /> + + + + + setEditableTrajectory({ ...editableTrajectory, name: e.target.value })} + /> + { + setEditableTrajectory({ ...editableTrajectory, dTimTrajStart: dateTime }); + setDTimTrajStartValid(valid); + }} + timeZone={timeZone} + /> + { + setEditableTrajectory({ ...editableTrajectory, dTimTrajEnd: dateTime }); + setDTimTrajEndValid(valid); + }} + timeZone={timeZone} + /> + setEditableTrajectory({ ...editableTrajectory, serviceCompany: e.target.value })} + /> + + setEditableTrajectory({ + ...editableTrajectory, + mdMin: isNaN(parseFloat(e.target.value)) ? undefined : parseFloat(e.target.value) + }) + } + /> + + setEditableTrajectory({ + ...editableTrajectory, + mdMax: isNaN(parseFloat(e.target.value)) ? undefined : parseFloat(e.target.value) + }) + } + /> + setEditableTrajectory({ ...editableTrajectory, aziRef: e.target.value })} + /> + + } + confirmDisabled={!validText(editableTrajectory.uid) || !validText(editableTrajectory.name)} + onSubmit={() => onSubmit(editableTrajectory)} + isLoading={isLoading} + /> + )} + + ); +}; + +export default TrajectoryPropertiesModal; diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/WellboreItem.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/WellboreItem.tsx index e9e0ddf9e..5d9334c3f 100644 --- a/Src/WitsmlExplorer.Frontend/components/Sidebar/WellboreItem.tsx +++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/WellboreItem.tsx @@ -22,6 +22,7 @@ import TubularContextMenu from "../ContextMenus/TubularContextMenu"; import TubularsContextMenu, { TubularsContextMenuProps } from "../ContextMenus/TubularsContextMenu"; import WbGeometryObjectContextMenu from "../ContextMenus/WbGeometryContextMenu"; import WellboreContextMenu, { WellboreContextMenuProps } from "../ContextMenus/WellboreContextMenu"; +import TrajectoriesContextMenu, { TrajectoriesContextMenuProps } from "../ContextMenus/TrajectoriesContextMenu"; import { IndexCurve } from "../Modals/LogPropertiesModal"; import LogTypeItem from "./LogTypeItem"; import ObjectGroupItem from "./ObjectGroupItem"; @@ -82,6 +83,13 @@ const WellboreItem = (props: WellboreItemProps): React.ReactElement => { dispatchOperation({ type: OperationType.DisplayContextMenu, payload: { component: , position } }); }; + const onTrajectoryContextMenu = (event: React.MouseEvent, wellbore: Wellbore, setIsLoading?: (arg: boolean) => void) => { + preventContextMenuPropagation(event); + const contextMenuProps: TrajectoriesContextMenuProps = { dispatchOperation, wellbore, servers, setIsLoading }; + const position = getContextMenuPosition(event); + dispatchOperation({ type: OperationType.DisplayContextMenu, payload: { component: , position } }); + }; + const getExpandableObjectCount = useCallback(async () => { if (wellbore.objectCount == null) { setIsFetchingCount(true); @@ -136,7 +144,12 @@ const WellboreItem = (props: WellboreItemProps): React.ReactElement => { onGroupContextMenu={(event, _, setIsLoading) => onRigsContextMenu(event, wellbore, setIsLoading)} /> - + onTrajectoryContextMenu(event, wellbore, setIsLoading)} + /> +/// Create new trajectory tests. +/// +public class CreateTrajectoryWorkerTests +{ + private const string Uid = "newTrajectoryUid"; + private const string Name = "newTrajectoryName"; + private const string WellUid = "wellUid"; + private const string WellName = "wellName"; + private const string WellboreUid = "wellboreUid"; + + private readonly Mock _witsmlClient; + private readonly CreateTrajectoryWorker _worker; + + public CreateTrajectoryWorkerTests() + { + 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 CreateTrajectoryWorker(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); + _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); + job = CreateJobTemplate(name: ""); + exception = await Assert.ThrowsAsync(() => _worker.Execute(job)); + Assert.Equal("Name cannot be empty", exception.Message); + _witsmlClient.Verify(client => client.AddToStoreAsync(It.IsAny()), Times.Never); + } + + + [Fact] + public async Task CreateTrajectory_Execute_ValidResults() + { + CreateTrajectoryJob job = CreateJobTemplate(); + List createdTrajectories = new(); + + _witsmlClient.Setup(client => + client.AddToStoreAsync(It.IsAny())) + .Callback(trajectory => createdTrajectories.Add(trajectory)) + .ReturnsAsync(new QueryResult(true)); + + await _worker.Execute(job); + + Assert.Single(createdTrajectories); + Assert.Single(createdTrajectories.First().Trajectories); + WitsmlTrajectory createdObject = createdTrajectories.First().Trajectories.First(); + Assert.Equal(Uid, createdObject.Uid); + Assert.Equal(Name, createdObject.Name); + Assert.Equal(WellUid, createdObject.UidWell); + Assert.Equal(WellName, createdObject.NameWell); + Assert.Equal(WellboreUid, createdObject.UidWellbore); + } + + private static CreateTrajectoryJob CreateJobTemplate(string uid = Uid, string name = Name, string wellUid = WellUid, string wellName = WellName, string wellboreUid = WellboreUid) + { + return new CreateTrajectoryJob + { + Trajectory = new Trajectory() + { + Uid = uid, + Name = name, + WellUid = wellUid, + WellName = wellName, + WellboreUid = wellboreUid + } + }; + } +}