Skip to content

Commit

Permalink
Merge pull request #2072 from eliasbruvik/FIX-2071
Browse files Browse the repository at this point in the history
FIX-2071 Add freetext optionsIn for query view
  • Loading branch information
eliasbruvik authored Oct 6, 2023
2 parents 69e7e95 + b93d33e commit ddbbbed
Show file tree
Hide file tree
Showing 15 changed files with 155 additions and 49 deletions.
21 changes: 20 additions & 1 deletion Src/Witsml/ServiceReference/OptionsIn.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;

namespace Witsml.ServiceReference
{
public record OptionsIn(
ReturnElements? ReturnElements = null,
int? MaxReturnNodes = null,
int? RequestLatestValues = null,
bool? RequestObjectSelectionCapability = null)
bool? RequestObjectSelectionCapability = null,
string OptionsInString = null)
{
public string OptionsInString { get; init; } = ValidateOptionsInString(OptionsInString);
private static readonly string OptionsInRegexPattern = @"^([A-Za-z]+=[^=;]+)(;[A-Za-z]+=[^=;]+)*$";

public string GetKeywords()
{
List<string> keywords = new();
Expand All @@ -28,9 +34,22 @@ public string GetKeywords()
{
keywords.Add($"requestObjectSelectionCapability=true");
}
if (!string.IsNullOrEmpty(OptionsInString))
{
keywords.Add(OptionsInString);
}

return string.Join(";", keywords);
}

private static string ValidateOptionsInString(string optionsInString)
{
if (!string.IsNullOrEmpty(optionsInString) && !Regex.IsMatch(optionsInString, OptionsInRegexPattern))
{
throw new ArgumentException("OptionsInString does not match the required pattern.");
}
return optionsInString;
}
}

public enum ReturnElements
Expand Down
8 changes: 4 additions & 4 deletions Src/WitsmlExplorer.Api/HttpHandlers/WitsmlQueryHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public static async Task<IResult> AddToStore(IWitsmlClientProvider witsmlClientP
try
{
WitsmlQuery query = await httpRequest.Body.Deserialize<WitsmlQuery>();
string result = await witsmlClient.AddToStoreAsync(query.Body);
string result = await witsmlClient.AddToStoreAsync(query.Body, new OptionsIn(OptionsInString: query.OptionsInString));
return TypedResults.Ok(result);
}
catch (Exception e)
Expand All @@ -42,7 +42,7 @@ public static async Task<IResult> DeleteFromStore(IWitsmlClientProvider witsmlCl
try
{
WitsmlQuery query = await httpRequest.Body.Deserialize<WitsmlQuery>();
string result = await witsmlClient.DeleteFromStoreAsync(query.Body);
string result = await witsmlClient.DeleteFromStoreAsync(query.Body, new OptionsIn(OptionsInString: query.OptionsInString));
return TypedResults.Ok(result);
}
catch (Exception e)
Expand All @@ -58,7 +58,7 @@ public static async Task<IResult> GetFromStore(IWitsmlClientProvider witsmlClien
try
{
WitsmlQuery query = await httpRequest.Body.Deserialize<WitsmlQuery>();
string result = await witsmlClient.GetFromStoreAsync(query.Body, new OptionsIn(query.ReturnElements));
string result = await witsmlClient.GetFromStoreAsync(query.Body, new OptionsIn(query.ReturnElements, OptionsInString: query.OptionsInString));
return TypedResults.Ok(result);
}
catch (Exception e)
Expand All @@ -74,7 +74,7 @@ public static async Task<IResult> UpdateInStore(IWitsmlClientProvider witsmlClie
try
{
WitsmlQuery query = await httpRequest.Body.Deserialize<WitsmlQuery>();
string result = await witsmlClient.UpdateInStoreAsync(query.Body);
string result = await witsmlClient.UpdateInStoreAsync(query.Body, new OptionsIn(OptionsInString: query.OptionsInString));
return TypedResults.Ok(result);
}
catch (Exception e)
Expand Down
1 change: 1 addition & 0 deletions Src/WitsmlExplorer.Api/Models/WitsmlQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public class WitsmlQuery
{
[JsonConverter(typeof(JsonStringEnumConverter))]
public ReturnElements ReturnElements { get; init; }
public string OptionsInString { get; init; }
public string Body { get; init; }
}
}
40 changes: 33 additions & 7 deletions Src/WitsmlExplorer.Frontend/components/ContentViews/QueryView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Button, Menu, TextField } from "@equinor/eds-core-react";
import React, { useContext, useEffect, useRef, useState } from "react";
import React, { ChangeEvent, useContext, useEffect, useRef, useState } from "react";
import styled from "styled-components";
import OperationContext from "../../contexts/operationContext";
import { DispatchOperation } from "../../contexts/operationStateReducer";
Expand Down Expand Up @@ -67,6 +67,7 @@ const QueryView = (): React.ReactElement => {
const [isLoading, setIsLoading] = useState(false);
const [isXmlResponse, setIsXmlResponse] = useState(false);
const [returnElements, setReturnElements] = useState(ReturnElements.All);
const [optionsIn, setOptionsIn] = useState<string>("");
const [storeFunction, setStoreFunction] = useState(StoreFunction.GetFromStore);
const inputRef = useRef<HTMLTextAreaElement>(null);
const [isTemplateMenuOpen, setIsTemplateMenuOpen] = useState<boolean>(false);
Expand All @@ -77,7 +78,7 @@ const QueryView = (): React.ReactElement => {
dispatchOperation?.({ type: OperationType.HideModal });
setIsLoading(true);
const requestReturnElements = storeFunction == StoreFunction.GetFromStore ? returnElements : undefined;
let response = await QueryService.postQuery(query, storeFunction, requestReturnElements);
let response = await QueryService.postQuery(query, storeFunction, requestReturnElements, optionsIn.trim());
if (response.startsWith("<")) {
response = formatXml(response);
}
Expand Down Expand Up @@ -107,12 +108,19 @@ const QueryView = (): React.ReactElement => {
<>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem", height: "100%", padding: "1rem" }}>
<div style={{ display: "grid", gridTemplateRows: "1fr auto", gap: "1rem", height: "100%" }}>
<StyledTextField id="input" multiline colors={colors} onChange={(e: any) => setQuery(e.target.value)} defaultValue={query} textareaRef={inputRef} />
<StyledLargeTextField
id="input"
multiline
colors={colors}
onChange={(e: ChangeEvent<HTMLInputElement>) => setQuery(e.target.value)}
defaultValue={query}
textareaRef={inputRef}
/>
<div style={{ display: "flex", alignItems: "flex-end", gap: "1rem" }}>
<StyledNativeSelect
label="Function"
id="function"
onChange={(event: any) => setStoreFunction(event.target.value)}
onChange={(event: ChangeEvent<HTMLSelectElement>) => setStoreFunction(event.target.value as StoreFunction)}
defaultValue={StoreFunction.GetFromStore}
colors={colors}
>
Expand All @@ -127,7 +135,7 @@ const QueryView = (): React.ReactElement => {
<StyledNativeSelect
label="Return elements"
id="return-elements"
onChange={(event: any) => setReturnElements(event.target.value)}
onChange={(event: ChangeEvent<HTMLSelectElement>) => setReturnElements(event.target.value as ReturnElements)}
defaultValue={ReturnElements.All}
colors={colors}
>
Expand All @@ -139,6 +147,15 @@ const QueryView = (): React.ReactElement => {
);
})}
</StyledNativeSelect>
<StyledTextField
id="optionsIn"
label="Options In"
value={optionsIn}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
setOptionsIn(e.target.value);
}}
colors={colors}
/>
<Button
ref={setMenuAnchor}
id="anchor-default"
Expand Down Expand Up @@ -183,7 +200,7 @@ const QueryView = (): React.ReactElement => {
</div>
</div>
<div>
<StyledTextField id="output" multiline colors={colors} readOnly value={result} textWrap={!isXmlResponse} />
<StyledLargeTextField id="output" multiline colors={colors} readOnly value={result} textWrap={!isXmlResponse} />
</div>
</div>
</>
Expand Down Expand Up @@ -261,7 +278,7 @@ const formatXml = (xml: string) => {
return new XMLSerializer().serializeToString(resultDoc);
};

const StyledTextField = styled(TextField)<{ colors: Colors; textWrap?: boolean }>`
const StyledLargeTextField = styled(TextField)<{ colors: Colors; textWrap?: boolean }>`
border: 1px solid ${(props) => props.colors.interactive.tableBorder};
height: 100%;
&&& > div {
Expand Down Expand Up @@ -299,4 +316,13 @@ const StyledMenuItem = styled(Menu.Item)<{ colors: Colors }>`
padding: 4px;
`;

const StyledTextField = styled(TextField)<{ colors: Colors }>`
label {
color: ${(props) => props.colors.text.staticIconsDefault};
}
div {
background: ${(props) => props.colors.text.staticTextFieldDefault};
}
`;

export default QueryView;
5 changes: 3 additions & 2 deletions Src/WitsmlExplorer.Frontend/services/queryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { ReturnElements, StoreFunction } from "../components/ContentViews/QueryV
import { ApiClient } from "./apiClient";

export default class QueryService {
public static async postQuery(query: string, storeFunction: StoreFunction, returnElements?: ReturnElements, abortSignal?: AbortSignal): Promise<string> {
public static async postQuery(query: string, storeFunction: StoreFunction, returnElements?: ReturnElements, optionsIn?: string, abortSignal?: AbortSignal): Promise<string> {
const payload = {
body: query,
...(returnElements ? { returnElements } : {})
...(returnElements ? { returnElements } : {}),
...(optionsIn ? { optionsInString: optionsIn } : {})
};
const response = await ApiClient.post(`/api/query/${storeFunction.toLowerCase()}`, JSON.stringify(payload), abortSignal);
if (response.ok) {
Expand Down
59 changes: 59 additions & 0 deletions Tests/Witsml.Tests/ServiceReference/OptionsInTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;

using Witsml.ServiceReference;

using Xunit;
Expand Down Expand Up @@ -35,5 +37,62 @@ public void GetKeyWords_ReturnLatestValues10_ReturnsCorrectValue()
OptionsIn optionsIn = new(ReturnElements.DataOnly, null, 10);
Assert.Equal("returnElements=data-only;requestLatestValues=10", optionsIn.GetKeywords());
}

[Fact]
public void GetKeywords_OptionsInString_ReturnsCorrectValue()
{
OptionsIn optionsIn = new(OptionsInString: "foo=bar");
Assert.Equal("foo=bar", optionsIn.GetKeywords());
}

[Fact]
public void GetKeywords_OptionsInString_WithSymbols_ReturnsCorrectValue()
{
OptionsIn optionsIn = new(OptionsInString: "requestElements=data-only");
Assert.Equal("requestElements=data-only", optionsIn.GetKeywords());
}

[Fact]
public void GetKeywords_OptionsInString_MultipleKeywords_ReturnsCorrectValue()
{
OptionsIn optionsIn = new(OptionsInString: "foo=bar;baz=qux;quux=corge;grault=garply");
Assert.Equal("foo=bar;baz=qux;quux=corge;grault=garply", optionsIn.GetKeywords());
}

[Fact]
public void GetKeywords_OptionsInStringAndOtherOptions_ReturnsCorrectValue()
{
OptionsIn optionsIn = new(ReturnElements.DataOnly, 50, 100, true, "foo=bar;baz=qux");
Assert.Equal("returnElements=data-only;maxReturnNodes=50;requestLatestValues=100;requestObjectSelectionCapability=true;foo=bar;baz=qux", optionsIn.GetKeywords());
}

[Fact]
public void OptionsInString_InvalidValue_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new OptionsIn(OptionsInString: "invalid"));
Assert.Throws<ArgumentException>(() => new OptionsIn(OptionsInString: "invalid="));
Assert.Throws<ArgumentException>(() => new OptionsIn(OptionsInString: "invalid=true;"));
Assert.Throws<ArgumentException>(() => new OptionsIn(OptionsInString: ";invalid=true"));
Assert.Throws<ArgumentException>(() => new OptionsIn(OptionsInString: "invalid-key=true"));
Assert.Throws<ArgumentException>(() => new OptionsIn(OptionsInString: "=true"));
Assert.Throws<ArgumentException>(() => new OptionsIn(OptionsInString: "foo=bar,baz=qux"));
Assert.Throws<ArgumentException>(() => new OptionsIn(OptionsInString: "foo=bar;;baz=qux"));
Assert.Throws<ArgumentException>(() => new OptionsIn(OptionsInString: "foo=baz=qux"));
Assert.Throws<ArgumentException>(() => new OptionsIn(OptionsInString: "foo=bar;invalid;baz=qux"));
}

[Fact]
public void GetKeywords_OptionsInString_EmptyValue_ReturnsEmpty()
{
OptionsIn optionsIn = new(OptionsInString: "");
Assert.Equal("", optionsIn.GetKeywords());
}

[Fact]
public void GetKeywords_OptionsInString_NullValue_ReturnsEmpty()
{
OptionsIn optionsIn = new(OptionsInString: null);
Assert.Equal("", optionsIn.GetKeywords());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,10 @@ private List<IWitsmlQueryType> SetupUpdateInStoreAsync()
private void SetupGetFromStoreAsync(ComponentType componentType, string[] sourceComponentUids, string[] targetComponentUids)
{
_witsmlClient.Setup(client =>
client.GetFromStoreNullableAsync(It.Is<IWitsmlObjectList>(query => query.Objects.First().Uid == SourceUid), new OptionsIn(ReturnElements.All, null, null, null)))
client.GetFromStoreNullableAsync(It.Is<IWitsmlObjectList>(query => query.Objects.First().Uid == SourceUid), It.Is<OptionsIn>((ops) => ops.ReturnElements == ReturnElements.All)))
.ReturnsAsync(GetWitsmlObject(sourceComponentUids, SourceUid, componentType));
_witsmlClient.Setup(client =>
client.GetFromStoreNullableAsync(It.Is<IWitsmlObjectList>(query => query.Objects.First().Uid == TargetUid), new OptionsIn(ReturnElements.Requested, null, null, null)))
client.GetFromStoreNullableAsync(It.Is<IWitsmlObjectList>(query => query.Objects.First().Uid == TargetUid), It.Is<OptionsIn>((ops) => ops.ReturnElements == ReturnElements.Requested)))
.ReturnsAsync(GetWitsmlObject(targetComponentUids, TargetUid, componentType));
}

Expand Down
12 changes: 6 additions & 6 deletions Tests/WitsmlExplorer.Api.Tests/Workers/CopyLogDataWorkerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public async Task CopyLogData_TimeIndexed()
LogUtils.SetupTargetLog(WitsmlLog.WITSML_INDEX_TYPE_DATE_TIME, _witsmlClient);
List<WitsmlLogs> updatedLogs = LogUtils.SetupUpdateInStoreAsync(_witsmlClient);
WitsmlLogs query = null;
_witsmlClient.Setup(client => client.GetFromStoreAsync(It.IsAny<WitsmlLogs>(), new OptionsIn(ReturnElements.DataOnly, null, null, null)))
_witsmlClient.Setup(client => client.GetFromStoreAsync(It.IsAny<WitsmlLogs>(), It.Is<OptionsIn>((ops) => ops.ReturnElements == ReturnElements.DataOnly)))
.Callback<WitsmlLogs, OptionsIn>((logs, _) => query = logs)
.ReturnsAsync(() => LogUtils.GetSourceLogData(query.Logs.First().StartDateTimeIndex, query.Logs.First().EndDateTimeIndex));

Expand Down Expand Up @@ -109,7 +109,7 @@ public async Task CopyLogData_DepthIndexed_SelectedMnemonics()
LogUtils.SetupSourceLog(WitsmlLog.WITSML_INDEX_TYPE_MD, _witsmlClient);
LogUtils.SetupTargetLog(WitsmlLog.WITSML_INDEX_TYPE_MD, _witsmlClient);
WitsmlLogs query = null;
_witsmlClient.Setup(client => client.GetFromStoreAsync(It.IsAny<WitsmlLogs>(), new OptionsIn(ReturnElements.DataOnly, null, null, null)))
_witsmlClient.Setup(client => client.GetFromStoreAsync(It.IsAny<WitsmlLogs>(), It.Is<OptionsIn>((ops) => ops.ReturnElements == ReturnElements.DataOnly)))
.Callback<WitsmlLogs, OptionsIn>((logs, _) => query = logs)
.ReturnsAsync(() =>
{
Expand Down Expand Up @@ -139,7 +139,7 @@ public async Task CopyLogData_DepthIndexed_AddsIndexMnemonicIfNotIncludedInJob()
LogUtils.SetupTargetLog(WitsmlLog.WITSML_INDEX_TYPE_MD, _witsmlClient);

WitsmlLogs query = null;
_witsmlClient.Setup(client => client.GetFromStoreAsync(It.IsAny<WitsmlLogs>(), new OptionsIn(ReturnElements.DataOnly, null, null, null)))
_witsmlClient.Setup(client => client.GetFromStoreAsync(It.IsAny<WitsmlLogs>(), It.Is<OptionsIn>((ops) => ops.ReturnElements == ReturnElements.DataOnly)))
.Callback<WitsmlLogs, OptionsIn>((logs, _) => query = logs)
.ReturnsAsync(() =>
{
Expand Down Expand Up @@ -184,7 +184,7 @@ public async Task CopyLogData_DepthIndexed_AllowIndexCurveNamesThatOnlyDifferInC
LogUtils.SetupTargetLog(WitsmlLog.WITSML_INDEX_TYPE_MD, _witsmlClient);

WitsmlLogs query = null;
_witsmlClient.Setup(client => client.GetFromStoreAsync(It.IsAny<WitsmlLogs>(), new OptionsIn(ReturnElements.DataOnly, null, null, null)))
_witsmlClient.Setup(client => client.GetFromStoreAsync(It.IsAny<WitsmlLogs>(), It.Is<OptionsIn>((ops) => ops.ReturnElements == ReturnElements.DataOnly)))
.Callback<WitsmlLogs, OptionsIn>((logs, _) => query = logs)
.ReturnsAsync(() =>
{
Expand Down Expand Up @@ -215,7 +215,7 @@ public async Task Execute_DifferentIndexMnemonics_CopyWithTargetIndex()
LogUtils.SetupTargetLog(WitsmlLog.WITSML_INDEX_TYPE_MD, _witsmlClient, targetLog);

WitsmlLogs query = null;
_witsmlClient.Setup(client => client.GetFromStoreAsync(It.IsAny<WitsmlLogs>(), new OptionsIn(ReturnElements.DataOnly, null, null, null)))
_witsmlClient.Setup(client => client.GetFromStoreAsync(It.IsAny<WitsmlLogs>(), It.Is<OptionsIn>((ops) => ops.ReturnElements == ReturnElements.DataOnly)))
.Callback<WitsmlLogs, OptionsIn>((logs, _) => query = logs)
.ReturnsAsync(() =>
{
Expand Down Expand Up @@ -285,7 +285,7 @@ public async Task Execute_TimeRange_CopiedCorrectly()
LogUtils.SetupTargetLog(WitsmlLog.WITSML_INDEX_TYPE_DATE_TIME, _witsmlClient);
List<WitsmlLogs> updatedLogs = LogUtils.SetupUpdateInStoreAsync(_witsmlClient);
WitsmlLogs query = null;
_witsmlClient.Setup(client => client.GetFromStoreAsync(It.IsAny<WitsmlLogs>(), new OptionsIn(ReturnElements.DataOnly, null, null, null)))
_witsmlClient.Setup(client => client.GetFromStoreAsync(It.IsAny<WitsmlLogs>(), It.Is<OptionsIn>((ops) => ops.ReturnElements == ReturnElements.DataOnly)))
.Callback<WitsmlLogs, OptionsIn>((logs, _) => query = logs)
.ReturnsAsync(() => LogUtils.GetSourceLogData(query.Logs.First().StartDateTimeIndex, query.Logs.First().EndDateTimeIndex));

Expand Down
Loading

0 comments on commit ddbbbed

Please sign in to comment.