From d9824420cf59f51745f606370aea77ef30f65e92 Mon Sep 17 00:00:00 2001 From: AndrewThien Date: Tue, 17 Feb 2026 15:16:45 +0000 Subject: [PATCH 01/42] add new wizard --- .../SubmissionWizardNewViewComponent.cs | 60 +++++++ .../Project/SubmissionProjectSqlNew.cshtml | 13 ++ .../SubmissionWizardNew/Default.cshtml | 161 ++++++++++++++++++ 3 files changed, 234 insertions(+) create mode 100644 Submission/Submission.Web/ViewComponents/SubmissionWizardNewViewComponent.cs create mode 100644 Submission/Submission.Web/Views/Project/SubmissionProjectSqlNew.cshtml create mode 100644 Submission/Submission.Web/Views/Shared/Components/SubmissionWizardNew/Default.cshtml diff --git a/Submission/Submission.Web/ViewComponents/SubmissionWizardNewViewComponent.cs b/Submission/Submission.Web/ViewComponents/SubmissionWizardNewViewComponent.cs new file mode 100644 index 00000000..36753e8a --- /dev/null +++ b/Submission/Submission.Web/ViewComponents/SubmissionWizardNewViewComponent.cs @@ -0,0 +1,60 @@ +using FiveSafesTes.Core.Models; +using FiveSafesTes.Core.Models.ViewModels; +using FiveSafesTes.Core.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace Submission.Web.ViewComponents +{ + public class SubmissionWizardNewViewComponent : ViewComponent + { + private readonly IDareClientHelper _clientHelper; + public SubmissionWizardNewViewComponent(IDareClientHelper client) + { + _clientHelper = client; + } + public async Task InvokeAsync(int projectId) + { + var paramlist = new Dictionary(); + paramlist.Add("projectId", projectId.ToString()); + var project = _clientHelper.CallAPIWithoutModel( + "/api/Project/GetProject/", paramlist).Result; + var SelectTresOptions = project.Tres.ToList(); + List treInfoList = new List(); + foreach (var param in SelectTresOptions) + { + var isOnline = param.IsOnline(); + + var treInfo = new TreInfo() + { + Name = param.Name, + IsSelected = false, + IsOnline = isOnline + }; + treInfoList.Add(treInfo); + } + var userItems2 = project.Users; + var treItems2 = project.Tres; + + var userItems = userItems2 + .Select(p => new SelectListItem { Value = p.Id.ToString(), Text = p.FullName != "" ? p.FullName : p.Name }) + .ToList(); + var treItems = treItems2 + .Select(p => new SelectListItem { Value = p.Id.ToString(), Text = p.Name }) + .ToList(); + + var model = new AddiSubmissionWizard() + { + ProjectId = project.Id, + ProjectName = project.Name, + SelectTresOptions = project.Tres.Select(x => x.Name).ToList(), + TreRadios = treInfoList, + Submissions = project.Submissions.ToList(), + UserItemList = userItems, + TreItemList = treItems + }; + + return View(model); + } + } +} diff --git a/Submission/Submission.Web/Views/Project/SubmissionProjectSqlNew.cshtml b/Submission/Submission.Web/Views/Project/SubmissionProjectSqlNew.cshtml new file mode 100644 index 00000000..7755d428 --- /dev/null +++ b/Submission/Submission.Web/Views/Project/SubmissionProjectSqlNew.cshtml @@ -0,0 +1,13 @@ +@model FiveSafesTes.Core.Models.ViewModels.ProjectUserTre +@{ + string pageName = Model.Name; + + ViewData["Title"] = @pageName + " - SQL TES Wizard"; + int pId = Model.Id; +} +
+ @if (ViewBag.UserCanDoSubmissions) + { + @await Component.InvokeAsync("SubmissionWizardNew", new {projectId=pId}) + } +
diff --git a/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardNew/Default.cshtml b/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardNew/Default.cshtml new file mode 100644 index 00000000..142e5298 --- /dev/null +++ b/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardNew/Default.cshtml @@ -0,0 +1,161 @@ +@using FiveSafesTes.Core.Models.ViewModels +@using Submission.Web.Formatters +@model FiveSafesTes.Core.Models.ViewModels.AddiSubmissionWizard +@{ + ViewData["Title"] = "Submission Wizard"; +} + + +@using (Html.BeginForm("", "", FormMethod.Post, new { enctype = "multipart/form-data", id = "frmMain2" })) + { + @Html.HiddenFor(x => x.ProjectId) + { +

+ SQL TES Wizard
+

+
+ @Html.LabelFor(x => x.TESName,new {@class="form-label fw-bold"}) + @Html.TextBoxFor(x => x.TESName, new {@class="form-control"}) +
+ +
+ @Html.LabelFor(x => x.TESDescription,new {@class="form-label fw-bold"}) + @Html.TextAreaFor(x => x.TESDescription, new {@class="form-control"}) +
+
+ + @for (var i = 0; i < Model.TreRadios.Count(); i++) + { +
+
+ @Html.HiddenFor(x => x.TreRadios[i].Name) + @Html.CheckBoxFor(x=>x.TreRadios[i].IsSelected, new {@class = "form-check-input", @id = "tre_" + i}) + +
+ @if (Model.TreRadios[i].IsOnline) + { + + Online + + } + else + { + + Offline + + } +
+ } + @if (Model.TreRadios.Count() < 1) + { +

No available TREs

+ } +
+
+ @Html.LabelFor(x => x.Query,new {@class="form-label fw-bold"}) + @Html.TextAreaFor(x => x.Query, new {@class="form-control"}) +
+ + +
+ + + + + + + + + + + + + + + @if (Model.Executors != null) + { + @foreach (var executor in Model.Executors) + { + + + + + + } + } + + +
ImageCommandEnvironment Variables
+ + +
+
+ +

Separate commands with Enter ↵ key

+
+ Add +
+
+
+
+ +

Type in your environment variable (e.g MyENV=MyVal) then press Enter ↵ key

+
+ Add +
+
@executor.Image@executor.Command@executor.ENV
+
+ + +
+ + + + + + + + + + + + + +
PathType
+
+ +
+
+
+ +
+
+
+ + + +
+ @Html.TextAreaFor(m => m.RawInput, new {@class="form-control"}) +
+ +
+ + Cancel +
+
+
+
+ } + } + + + + + + + From 4a547cbfff94a8b97984ca90e77e0e9cc26e5c48 Mon Sep 17 00:00:00 2001 From: AndrewThien Date: Tue, 17 Feb 2026 15:20:31 +0000 Subject: [PATCH 02/42] add new wizard to project controller --- .../Controllers/ProjectController.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Submission/Submission.Web/Controllers/ProjectController.cs b/Submission/Submission.Web/Controllers/ProjectController.cs index 5f55cd3a..f01cb504 100644 --- a/Submission/Submission.Web/Controllers/ProjectController.cs +++ b/Submission/Submission.Web/Controllers/ProjectController.cs @@ -121,7 +121,30 @@ public async Task GetProject(int id) return View(projectView); } + public IActionResult SubmissionProjectSqlNew(int id) + { + if (!ModelState.IsValid) // SonarQube security + { + return View("/"); + } + + var paramlist = new Dictionary(); + paramlist.Add("projectId", id.ToString()); + var project = _clientHelper.CallAPIWithoutModel( + "/api/Project/GetProjectUI/", paramlist).Result; + + ViewBag.UserCanDoSubmissions = IsUserOnProject(project); + + var projectView = new ProjectUserTre() + { + Id = project.Id, + Name = project.Name, + Submissions = project.Submissions.Where(x => x.HasParent == false).ToList(), + }; + + return View(projectView); + } public IActionResult SubmissionProjectSQL(int id) { if (!ModelState.IsValid) // SonarQube security From 44db6ad85cfc7e7ae316eb70128a1cdb25dfd34f Mon Sep 17 00:00:00 2001 From: AndrewThien Date: Tue, 17 Feb 2026 15:20:55 +0000 Subject: [PATCH 03/42] add link to new wizard --- Submission/Submission.Web/Views/Project/GetProject.cshtml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Submission/Submission.Web/Views/Project/GetProject.cshtml b/Submission/Submission.Web/Views/Project/GetProject.cshtml index b029d2ef..5e5f9f9b 100644 --- a/Submission/Submission.Web/Views/Project/GetProject.cshtml +++ b/Submission/Submission.Web/Views/Project/GetProject.cshtml @@ -137,7 +137,10 @@ Wizard
  • GraphQL - TES Wizard
  • + TES Wizard +
  • SQL + TES Wizard New
  • Date: Tue, 17 Feb 2026 15:33:58 +0000 Subject: [PATCH 04/42] change frame name for the new wizard --- .../SubmissionWizardNew/Default.cshtml | 2 +- .../Views/Shared/_Layout.cshtml | 129 ++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardNew/Default.cshtml b/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardNew/Default.cshtml index 142e5298..ff1aebd0 100644 --- a/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardNew/Default.cshtml +++ b/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardNew/Default.cshtml @@ -6,7 +6,7 @@ } -@using (Html.BeginForm("", "", FormMethod.Post, new { enctype = "multipart/form-data", id = "frmMain2" })) +@using (Html.BeginForm("", "", FormMethod.Post, new { enctype = "multipart/form-data", id = "frmMainNewWizard" })) { @Html.HiddenFor(x => x.ProjectId) { diff --git a/Submission/Submission.Web/Views/Shared/_Layout.cshtml b/Submission/Submission.Web/Views/Shared/_Layout.cshtml index bb94f03b..39492645 100644 --- a/Submission/Submission.Web/Views/Shared/_Layout.cshtml +++ b/Submission/Submission.Web/Views/Shared/_Layout.cshtml @@ -485,7 +485,136 @@ }); }); + + + From f5211ac0f44fa4682abe4cdaa13c89da865c0510 Mon Sep 17 00:00:00 2001 From: AndrewThien Date: Mon, 23 Feb 2026 09:43:32 +0000 Subject: [PATCH 24/42] update type --- Submission/Submission.Web/Controllers/SubmissionController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Submission/Submission.Web/Controllers/SubmissionController.cs b/Submission/Submission.Web/Controllers/SubmissionController.cs index d1f7289f..5e373f4c 100644 --- a/Submission/Submission.Web/Controllers/SubmissionController.cs +++ b/Submission/Submission.Web/Controllers/SubmissionController.cs @@ -558,7 +558,7 @@ public async Task SubmissionWizardAction(SubmissionWizardV2 model, if (string.IsNullOrEmpty(Executors) == false && Executors != "null") { - List executorsList = JsonConvert.DeserializeObject>(Executors); + List executorsList = JsonConvert.DeserializeObject>(Executors); foreach (var ex in executorsList) { if (string.IsNullOrEmpty(ex.Image)) continue; From 7f9cf5dc3c451b9bfc90ae9a79c40265e4364019 Mon Sep 17 00:00:00 2001 From: AndrewThien Date: Mon, 23 Feb 2026 10:11:06 +0000 Subject: [PATCH 25/42] monaco editor --- .../SubmissionWizardV2/Default.cshtml | 139 ++++++++++++++++-- .../Views/Shared/_Layout.cshtml | 5 +- 2 files changed, 133 insertions(+), 11 deletions(-) diff --git a/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardV2/Default.cshtml b/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardV2/Default.cshtml index d7509f5b..6ddf06a5 100644 --- a/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardV2/Default.cshtml +++ b/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardV2/Default.cshtml @@ -50,9 +50,9 @@
    - +
    @@ -133,7 +133,7 @@
    } + + + + + diff --git a/Submission/Submission.Web/Views/Shared/_Layout.cshtml b/Submission/Submission.Web/Views/Shared/_Layout.cshtml index f8709735..fb9d5115 100644 --- a/Submission/Submission.Web/Views/Shared/_Layout.cshtml +++ b/Submission/Submission.Web/Views/Shared/_Layout.cshtml @@ -595,7 +595,10 @@ formData.append('SQL', 'true'); } else if (selectedMode === 'raw') { - // Raw mode: validate JSON input + // Raw mode: sync Monaco → hidden textarea before reading + if (window.monacoEditor) { + document.getElementById('RawInput').value = window.monacoEditor.getValue(); + } var rawInput = $('#RawInput').val(); if (!rawInput || rawInput.trim() === '') { $('#error').text('Please enter a valid TES task JSON for raw mode.').css('color', 'red'); From 25a3d55cd3e9d70fa84ba2217c280305940ac866 Mon Sep 17 00:00:00 2001 From: AndrewThien Date: Mon, 23 Feb 2026 10:50:47 +0000 Subject: [PATCH 26/42] add * --- .../Shared/Components/SubmissionWizardV2/Default.cshtml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardV2/Default.cshtml b/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardV2/Default.cshtml index 6ddf06a5..ec3704e7 100644 --- a/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardV2/Default.cshtml +++ b/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardV2/Default.cshtml @@ -80,7 +80,8 @@
    - + + *
    @@ -92,7 +93,8 @@
    - + + * @for (var i = 0; i < Model.TreRadios.Count(); i++) {
    From bca34bf17c21cef71b2ad6ee4d5ab97a14703b9f Mon Sep 17 00:00:00 2001 From: AndrewThien Date: Mon, 23 Feb 2026 10:53:59 +0000 Subject: [PATCH 27/42] clean up for now, keep todo --- .../SubmissionWizardV2/Default.cshtml | 27 ++----------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardV2/Default.cshtml b/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardV2/Default.cshtml index ec3704e7..d1bc6ba5 100644 --- a/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardV2/Default.cshtml +++ b/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardV2/Default.cshtml @@ -204,31 +204,8 @@
    - @* TODO: understand the data input first *@ - @* *@ - @*
    *@ - @* *@ - @* *@ - @* *@ - @* *@ - @* *@ - @* *@ - @* *@ - @* *@ - @* *@ - @* *@ - @* *@ - @* *@ - @* *@ - @*
    PathType
    *@ - @* *@ - @* *@ - @* *@ - @*
    *@ - @*
    *@ + @* TODO: understand the data input and how it works then add it here *@ +
    From d97a966759d496c9bb41ccd70821511104b8c69f Mon Sep 17 00:00:00 2001 From: AndrewThien Date: Mon, 23 Feb 2026 10:56:44 +0000 Subject: [PATCH 28/42] add note --- .../Views/Shared/Components/SubmissionWizardV2/Default.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardV2/Default.cshtml b/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardV2/Default.cshtml index d1bc6ba5..3f683ef7 100644 --- a/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardV2/Default.cshtml +++ b/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardV2/Default.cshtml @@ -217,7 +217,7 @@ Notes:
  • The JSON should follow the request body schema here
  • -
  • Tags Project (target project) and tres (list of target TREs) are required
  • +
  • Tags Project (target project) and tres (list of target TREs, separated by "|") are required
  • From f123cfc9c4f8a4ff41b825b33c15f5766185bab5 Mon Sep 17 00:00:00 2001 From: AndrewThien Date: Mon, 23 Feb 2026 11:27:06 +0000 Subject: [PATCH 29/42] first clean up --- .../Controllers/SubmissionController.cs | 123 ++++++++---------- .../Views/Shared/_Layout.cshtml | 33 ++--- 2 files changed, 66 insertions(+), 90 deletions(-) diff --git a/Submission/Submission.Web/Controllers/SubmissionController.cs b/Submission/Submission.Web/Controllers/SubmissionController.cs index 5e373f4c..3cd51303 100644 --- a/Submission/Submission.Web/Controllers/SubmissionController.cs +++ b/Submission/Submission.Web/Controllers/SubmissionController.cs @@ -448,7 +448,7 @@ public async Task AddiSubmissionWizard(AddiSubmissionWizard model, [HttpPost] [Authorize] - public async Task SubmissionWizardAction(SubmissionWizardV2 model, string? Executors, string? SQL, string? SimpleMode) + public async Task SubmissionSQLWizardAction(SubmissionWizardV2 model, string? Executors, string Mode) { if (!ModelState.IsValid) // SonarQube security { @@ -471,11 +471,22 @@ public async Task SubmissionWizardAction(SubmissionWizardV2 model, var tes = new TesTask(); var tesExecutors = new List(); + // ------------------------------------------------------------------ + // Raw mode + // ------------------------------------------------------------------ + if (Mode == "Raw") + { + if (string.IsNullOrEmpty(model.RawInput) == false) + { + tes = JsonConvert.DeserializeObject(model.RawInput); + + } + } // ------------------------------------------------------------------ // SIMPLE MODE: build the full TES message from defaults + query only // ------------------------------------------------------------------ - if (SimpleMode == "true") + if (Mode == "Simple") { if (string.IsNullOrWhiteSpace(model.Query)) return BadRequest("A SQL query is required in Simple mode."); @@ -553,64 +564,55 @@ public async Task SubmissionWizardAction(SubmissionWizardV2 model, } // ------------------------------------------------------------------ - // CUSTOM / RAW modes + // CUSTOM mode // ------------------------------------------------------------------ - if (string.IsNullOrEmpty(Executors) == false && Executors != "null") - { - List executorsList = JsonConvert.DeserializeObject>(Executors); + if (Mode == "Custom") + if (string.IsNullOrEmpty(Executors) == false && Executors != "null") + { List executorsList = JsonConvert.DeserializeObject>(Executors); foreach (var ex in executorsList) { - if (string.IsNullOrEmpty(ex.Image)) continue; - - Dictionary EnvVars = new Dictionary(); - foreach (var anENV in ex.ENV) - { - var keyval = anENV.Split('=', 2); - EnvVars[keyval[0]] = keyval[1]; - } - - // Normalise each command argument — replace literal \n with real newlines - var normalizedCommands = ex.Command? - .Select(c => c - .Replace("\\r\\n", "\n") - .Replace("\\r", "\n") - .Replace("\\n", "\n")) - .ToList() ?? new List(); - - var exet = new TesExecutor() - { - Image = ex.Image, - Command = normalizedCommands, - Env = EnvVars - }; - tesExecutors.Add(exet); - } - } - - var TreDataTreData = model.TreRadios.Where(x => x.IsSelected == true).ToList(); - - if (TreDataTreData.Count == 0) - { - var paramList = new Dictionary(); - paramList.Add("projectId", model.ProjectId.ToString()); - var tre = await _clientHelper.CallAPIWithoutModel>("/api/Project/GetTresInProject/", - paramList); - List namesList = tre.Select(t => t.Name).ToList(); - listOfTre = string.Join("|", namesList); - } - else - { - listOfTre = string.Join("|", - model.TreRadios.Where(info => info.IsSelected).Select(info => info.Name)); - } + if (string.IsNullOrEmpty(ex.Image)) continue; + + Dictionary EnvVars = new Dictionary(); + foreach (var anENV in ex.ENV) + { + var keyval = anENV.Split('=', 2); + EnvVars[keyval[0]] = keyval[1]; + } + + // Normalise each command argument — replace literal \n with real newlines + var normalizedCommands = ex.Command? + .Select(c => c + .Replace("\\r\\n", "\n") + .Replace("\\r", "\n") + .Replace("\\n", "\n")) + .ToList() ?? new List(); + + var exet = new TesExecutor() + { + Image = ex.Image, + Command = normalizedCommands, + Env = EnvVars + }; + tesExecutors.Add(exet); + }} + else + { + return BadRequest("Executors data is required in Custom mode."); + } + + + + + + listOfTre = string.Join("|", + model.TreRadios.Where(info => info.IsSelected).Select(info => info.Name)); + tes = new TesTask(); - if (string.IsNullOrEmpty(model.RawInput) == false) - { - tes = JsonConvert.DeserializeObject(model.RawInput); - } + if (string.IsNullOrEmpty(model.TESName) == false) { @@ -652,20 +654,7 @@ public async Task SubmissionWizardAction(SubmissionWizardV2 model, } }; - if (SQL == "true") - { - QueryExecutor.Image = _URLSettingsFrontEnd.QueryImageSQL; - QueryExecutor.Command = new List() - { - "/bin/bash", - "/workspace/entrypoint.sh", - $"--Query={queryNormalized}" - }; - QueryExecutor.Env = new Dictionary() - { - ["LOCATION"] = "/workspace/data/results.csv", - }; - } + if (tes.Executors == null) diff --git a/Submission/Submission.Web/Views/Shared/_Layout.cshtml b/Submission/Submission.Web/Views/Shared/_Layout.cshtml index fb9d5115..d10adcf4 100644 --- a/Submission/Submission.Web/Views/Shared/_Layout.cshtml +++ b/Submission/Submission.Web/Views/Shared/_Layout.cshtml @@ -539,9 +539,9 @@ $('#frmSQLWizardV2 button[type="submit"]').prop('disabled', false).text('Submit TES Task'); return; } - formData.append('SimpleMode', 'true'); - formData.append('Executors', JSON.stringify([])); - formData.append('SQL', 'true'); + formData.append('Mode', 'Simple'); + formData.append('CustomExecutors', JSON.stringify([])); + } else if (selectedMode === 'custom') { // Custom mode: collect executor data from form @@ -591,8 +591,8 @@ } }); - formData.append('Executors', JSON.stringify(executorData)); - formData.append('SQL', 'true'); + formData.append('CustomExecutors', JSON.stringify(executorData)); + formData.append('Mode', 'Custom'); } else if (selectedMode === 'raw') { // Raw mode: sync Monaco → hidden textarea before reading @@ -615,28 +615,15 @@ } // Send empty executors for raw mode since executors are in the JSON - formData.append('Executors', JSON.stringify([])); + formData.append('CustomExecutors', JSON.stringify([])); + formData.append('Mode', 'Raw'); } - // Collect TRE data - var treData = []; - $('.col-md-4.mb-4 .form-check-input').each(function () { - var name = $(this).siblings('input[type="hidden"]').val(); - var isSelected = $(this).prop('checked'); - treData.push({ Name: name, IsSelected: isSelected }); - }); - - formData.append('TreData', JSON.stringify(treData)); - // Debug logging - console.log('Selected mode:', selectedMode); - console.log('TRE data:', treData); - for (var pair of formData.entries()) { - console.log(pair[0] + ':', pair[1]); - } + $.ajax({ - url: '@Url.Action("SubmissionWizardAction", "Submission")', + url: '@Url.Action("SubmissionSQLWizardAction", "Submission")', type: 'POST', data: formData, contentType: false, @@ -665,7 +652,7 @@ if (xhr.responseText) { errorMessage += xhr.responseText; } else if (xhr.status === 0) { - errorMessage += "Unable to connect to server. Please check your internet connection."; + errorMessage += "Please make sure the the TES message is valid and the server is reachable."; } else { errorMessage += error || "Unknown error occurred"; } From d77d8ae0ca8435ba1b363507e0eec758c278be3d Mon Sep 17 00:00:00 2001 From: AndrewThien Date: Mon, 23 Feb 2026 11:51:22 +0000 Subject: [PATCH 30/42] main refactoring --- .../Controllers/SubmissionController.cs | 403 +++++++----------- .../Views/Shared/_Layout.cshtml | 2 +- 2 files changed, 148 insertions(+), 257 deletions(-) diff --git a/Submission/Submission.Web/Controllers/SubmissionController.cs b/Submission/Submission.Web/Controllers/SubmissionController.cs index 3cd51303..0fb35d01 100644 --- a/Submission/Submission.Web/Controllers/SubmissionController.cs +++ b/Submission/Submission.Web/Controllers/SubmissionController.cs @@ -21,7 +21,6 @@ public class SubmissionController : Controller private readonly URLSettingsFrontEnd _URLSettingsFrontEnd; private readonly IKeyCloakService _IKeyCloakService; - public SubmissionController(IDareClientHelper client, IConfiguration configuration, URLSettingsFrontEnd URLSettingsFrontEnd, IKeyCloakService IKeyCloakService) { @@ -31,15 +30,12 @@ public SubmissionController(IDareClientHelper client, IConfiguration configurati _IKeyCloakService = IKeyCloakService; } - public IActionResult Instructions() { var url = _configuration["DareAPISettings:HelpAddress"]; return View(model: url); } - - [HttpPost] [Authorize] public async Task SubmissionWizard(SubmissionWizard model) @@ -50,7 +46,7 @@ public async Task SubmissionWizard(SubmissionWizard model) .Where(x => x.Value.Errors.Count > 0) .Select(x => new { Field = x.Key, Errors = x.Value.Errors.Select(e => e.ErrorMessage) }) .ToList(); - + return BadRequest($"Model validation failed: {string.Join(", ", errors.SelectMany(e => e.Errors))}"); } @@ -63,7 +59,6 @@ public async Task SubmissionWizard(SubmissionWizard model) var project = await _clientHelper.CallAPIWithoutModel( "/api/Project/GetProject/", paramlist); - if (model.TreRadios == null) { var paramList = new Dictionary(); @@ -86,23 +81,20 @@ public async Task SubmissionWizard(SubmissionWizard model) else { var paramss = new Dictionary(); - paramss.Add("bucketName", project.SubmissionBucket); if (model.File != null) { - var uplodaResultTest = - await _clientHelper.CallAPIToSendFile("/api/Project/UploadToMinio", "file", - model.File, paramss); + await _clientHelper.CallAPIToSendFile("/api/Project/UploadToMinio", "file", + model.File, paramss); } var minioEndpoint = await _clientHelper.CallAPIWithoutModel("/api/Project/GetMinioEndPoint"); - //Don't add http:// minioEndpoint.Url already has it. And if not it should! - imageUrl = /*"http://" +*/ minioEndpoint.Url + "/browser/" + project.SubmissionBucket + "/" + - model.File.FileName; + // Don't add http:// — minioEndpoint.Url already has it. + imageUrl = minioEndpoint.Url + "/browser/" + project.SubmissionBucket + "/" + model.File.FileName; } - var TesTask = new TesTask() + var tesTask = new TesTask() { Name = model.TESName, Executors = new List() @@ -120,11 +112,8 @@ await _clientHelper.CallAPIToSendFile("/api/Project/UploadToMinio", " } }; - - var result = await _clientHelper.CallAPI("/v1/tasks", TesTask); - + var result = await _clientHelper.CallAPI("/v1/tasks", tesTask); return RedirectToAction("GetASubmission", new { id = result.Id }); - //return Ok(); } catch (Exception e) { @@ -155,13 +144,9 @@ public IActionResult GetAllSubmissions() ViewBag.minioendpoint = minio?.Url; ViewBag.URLBucket = _URLSettingsFrontEnd.MinioUrl; - List displaySubmissionsList = new List(); var res = _clientHelper.CallAPIWithoutModel>("/api/Submission/GetAllSubmissions/").Result .Where(x => x.Parent == null).ToList(); - res = res.Where(x => x.Parent == null).ToList(); - - return View(res); } @@ -175,18 +160,18 @@ public IActionResult GetASubmission(int id) var res = _clientHelper.CallAPIWithoutModel($"/api/Submission/GetASubmission/{id}").Result; - var minio = _clientHelper.CallAPIWithoutModel("/api/Project/GetMinioEndPoint").Result; ViewBag.minioendpoint = minio?.Url; ViewBag.URLBucket = _URLSettingsFrontEnd.MinioUrl; - var test = new SubmissionInfo() + + var model = new SubmissionInfo() { Submission = res, Stages = _clientHelper.CallAPIWithoutModel("/api/Submission/StageTypes/").Result }; - return View(test); - } + return View(model); + } [HttpPost] [Authorize] @@ -204,6 +189,7 @@ public async Task SubmitDemoTes(AddiSubmissionWizard model, string paramlist.Add("projectId", model.ProjectId.ToString()); var project = await _clientHelper.CallAPIWithoutModel( "/api/Project/GetProject/", paramlist) ?? throw new NullReferenceException("Project not found"); + var treSelection = model.TreRadios.Where(x => x.IsSelected).ToList(); if (treSelection.Count == 0) { @@ -225,6 +211,7 @@ public async Task SubmitDemoTes(AddiSubmissionWizard model, string { tesTask = JsonConvert.DeserializeObject(model.JsonData) ?? throw new NullReferenceException("Json data not returned"); + if (tesTask.Tags == null || tesTask.Tags.Count == 0) { tesTask.Tags = new Dictionary() @@ -237,10 +224,8 @@ public async Task SubmitDemoTes(AddiSubmissionWizard model, string } await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); - await _clientHelper.CallAPI("/v1/tasks", tesTask); - return Ok(); } catch (Exception e) @@ -250,7 +235,6 @@ public async Task SubmitDemoTes(AddiSubmissionWizard model, string } } - [HttpPost] [Authorize] public async Task AddiSubmissionWizard(AddiSubmissionWizard model, string Executors, string SQL) @@ -445,274 +429,152 @@ public async Task AddiSubmissionWizard(AddiSubmissionWizard model, return BadRequest(e.Message); } } - + [HttpPost] [Authorize] - public async Task SubmissionSQLWizardAction(SubmissionWizardV2 model, string? Executors, string Mode) + public async Task SubmissionSqlWizardAction( + SubmissionWizardV2 model, string? CustomExecutors, string Mode) { - if (!ModelState.IsValid) // SonarQube security + if (!ModelState.IsValid) { var errors = ModelState .Where(x => x.Value.Errors.Count > 0) - .Select(x => new { Field = x.Key, Errors = x.Value.Errors.Select(e => e.ErrorMessage) }) - .ToList(); - - return BadRequest($"Model validation failed: {string.Join(", ", errors.SelectMany(e => e.Errors))}"); + .SelectMany(x => x.Value.Errors.Select(e => e.ErrorMessage)); + return BadRequest($"Model validation failed: {string.Join(", ", errors)}"); } try { - var listOfTre = ""; - - var paramlist = new Dictionary(); - paramlist.Add("projectId", model.ProjectId.ToString()); - var project = await _clientHelper.CallAPIWithoutModel( - "/api/Project/GetProject/", paramlist); + TesTask tes; - var tes = new TesTask(); - var tesExecutors = new List(); - // ------------------------------------------------------------------ - // Raw mode - // ------------------------------------------------------------------ + // ── RAW mode ───────────────────────────────────────────────────────── if (Mode == "Raw") { - if (string.IsNullOrEmpty(model.RawInput) == false) - { - tes = JsonConvert.DeserializeObject(model.RawInput); - - } - } + if (string.IsNullOrWhiteSpace(model.RawInput)) + return BadRequest("Raw JSON input is required in Raw mode."); - // ------------------------------------------------------------------ - // SIMPLE MODE: build the full TES message from defaults + query only - // ------------------------------------------------------------------ - if (Mode == "Simple") + tes = JsonConvert.DeserializeObject(model.RawInput) + ?? throw new InvalidOperationException("Failed to deserialise TES JSON."); + } + // ── SIMPLE or CUSTOM modes ──────────────────────────────────────────── + else if (Mode == "Simple" || Mode == "Custom") { - if (string.IsNullOrWhiteSpace(model.Query)) - return BadRequest("A SQL query is required in Simple mode."); - - // Normalise the query: replace literal \n / \r\n escape sequences - // (produced when the user pastes JSON-style text) with real newlines. - var normalizedQuery = model.Query - .Replace("\\r\\n", "\n") - .Replace("\\r", "\n") - .Replace("\\n", "\n"); - - // Resolve TRE list - var selectedTres = model.TreRadios?.Where(x => x.IsSelected).Select(x => x.Name).ToList() - ?? new List(); + // 1. Resolve project + var project = await _clientHelper.CallAPIWithoutModel( + "/api/Project/GetProject/", + new Dictionary { { "projectId", model.ProjectId.ToString() } }); + + // 2. Resolve TRE list (fall back to all TREs in the project) + var selectedTres = model.TreRadios? + .Where(x => x.IsSelected) + .Select(x => x.Name) + .ToList() ?? new List(); + if (selectedTres.Count == 0) { - var paramList2 = new Dictionary(); - paramList2.Add("projectId", model.ProjectId.ToString()); - var allTres = await _clientHelper.CallAPIWithoutModel>("/api/Project/GetTresInProject/", paramList2); - selectedTres = allTres.Select(t => t.Name).ToList(); + return BadRequest("No Tres selected."); } - listOfTre = string.Join("|", selectedTres); - tes = new TesTask + var listOfTre = string.Join("|", selectedTres); + + // 3. Common TES metadata + var tesName = string.IsNullOrWhiteSpace(model.TESName) ? "SQL Query Task" : model.TESName; + var tesDescription = string.IsNullOrWhiteSpace(model.TESDescription) ? "Federated analysis task" : model.TESDescription; + + // 4. Build executors depending on mode + List executors; + + if (Mode == "Simple") { - State = 0, - Name = string.IsNullOrWhiteSpace(model.TESName) ? "SQL Query Task" : model.TESName, - Description = string.IsNullOrWhiteSpace(model.TESDescription) - ? "Federated analysis task" - : model.TESDescription, - Inputs = null, - Outputs = new List - { - new TesOutput - { - Name = "workdir", - Description = "analysis test output", - Url = "s3://", - Path = "/outputs", - Type = TesFileType.DIRECTORYEnum - } - }, - Resources = null, - Executors = new List + if (string.IsNullOrWhiteSpace(model.Query)) + return BadRequest("A SQL query is required in Simple mode."); + + var query = NormaliseText(model.Query); + + executors = new List { new TesExecutor { - Image = _URLSettingsFrontEnd.QueryImageSQL, + Image = _URLSettingsFrontEnd.QueryImageSQL, Command = new List { "--Output=/outputs/output.csv", - $"--Query={normalizedQuery}" + $"--Query={query}" }, Workdir = "/app", - Stdin = null, - Stdout = null, - Stderr = null, - Env = new Dictionary() + Stdin = null, + Stdout = null, + Stderr = null, + Env = new Dictionary() } - }, - Volumes = null, - Tags = new Dictionary - { - { "Project", project?.Name ?? "Testing" }, - { "tres", listOfTre } - }, - Logs = null, - CreationTime = null - }; - - var context2 = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); - await _IKeyCloakService.RefreshUserToken(context2); - await _clientHelper.CallAPI("/v1/tasks", tes); - return Ok(); - } - - // ------------------------------------------------------------------ - // CUSTOM mode - // ------------------------------------------------------------------ - - if (Mode == "Custom") - if (string.IsNullOrEmpty(Executors) == false && Executors != "null") - { List executorsList = JsonConvert.DeserializeObject>(Executors); - foreach (var ex in executorsList) - { - if (string.IsNullOrEmpty(ex.Image)) continue; - - Dictionary EnvVars = new Dictionary(); - foreach (var anENV in ex.ENV) - { - var keyval = anENV.Split('=', 2); - EnvVars[keyval[0]] = keyval[1]; - } - - // Normalise each command argument — replace literal \n with real newlines - var normalizedCommands = ex.Command? - .Select(c => c - .Replace("\\r\\n", "\n") - .Replace("\\r", "\n") - .Replace("\\n", "\n")) - .ToList() ?? new List(); - - var exet = new TesExecutor() - { - Image = ex.Image, - Command = normalizedCommands, - Env = EnvVars - }; - tesExecutors.Add(exet); - }} - else - { - return BadRequest("Executors data is required in Custom mode."); - } - - - - - - listOfTre = string.Join("|", - model.TreRadios.Where(info => info.IsSelected).Select(info => info.Name)); - - - tes = new TesTask(); - - - - if (string.IsNullOrEmpty(model.TESName) == false) - { - tes.Name = model.TESName; - } - - if (string.IsNullOrEmpty(model.TESDescription) == false) - { - tes.Description = model.TESDescription; - } - - if (tesExecutors.Count > 0) - { - if (tes.Executors == null || tes.Executors.Count == 0) - { - tes.Executors = tesExecutors; - } - else - { - tes.Executors.AddRange(tesExecutors); + }; } - } - - if (string.IsNullOrEmpty(model.Query) == false) - { - var queryNormalized = model.Query - .Replace("\\r\\n", "\n") - .Replace("\\r", "\n") - .Replace("\\n", "\n"); - - var QueryExecutor = new TesExecutor() + else // Custom { - Image = _URLSettingsFrontEnd.QueryImageGraphQL, - Command = new List - { - "/usr/bin/dotnet", - "/app/Tre-Hasura.dll", - "--Query_" + queryNormalized - } - }; - - + if (string.IsNullOrWhiteSpace(CustomExecutors) || CustomExecutors == "null") + return BadRequest("Executors data is required in Custom mode."); + var executorDtos = JsonConvert.DeserializeObject>(CustomExecutors) + ?? new List(); - if (tes.Executors == null) - { - tes.Executors = new List(); - tes.Executors.Add(QueryExecutor); - } - else - { - tes.Executors.Insert(0, QueryExecutor); + executors = executorDtos + .Where(ex => !string.IsNullOrEmpty(ex.Image)) + .Select(ex => new TesExecutor + { + Image = ex.Image, + Command = ex.Command? + .Select(NormaliseText) + .ToList() ?? new List(), + Workdir = "/app", + Stdin = null, + Stdout = null, + Stderr = null, + Env = ParseEnvList(ex.ENV) + }) + .ToList(); + + if (executors.Count == 0) + return BadRequest("At least one valid executor with an image is required in Custom mode."); } - } - if (tes.Outputs == null || tes.Outputs.Count == 0) - { - // Same output structure as simple mode - tes.Outputs = new List() + // 5. Assemble the TES message (same shape for both Simple and Custom) + tes = new TesTask { - new TesOutput() + State = 0, + Name = tesName, + Description = tesDescription, + Inputs = null, + Outputs = new List { - Url = "s3://", - Name = "workdir", - Description = "analysis test output", - Path = "/outputs", - Type = TesFileType.DIRECTORYEnum, - } - }; - } - - if (tes.Tags == null || tes.Tags.Count == 0) - { - tes.Tags = new Dictionary() - { - { "project", project.Name }, - { "tres", listOfTre }, - { "author", HttpContext.User.FindFirst("name").Value } + new TesOutput + { + Name = "workdir", + Description = "analysis test output", + Url = "s3://", + Path = "/outputs", + Type = TesFileType.DIRECTORYEnum + } + }, + Resources = null, + Executors = executors, + Volumes = null, + Tags = new Dictionary + { + { "Project", project?.Name ?? string.Empty }, + { "tres", listOfTre } + }, + Logs = null, + CreationTime = null }; } - - if (string.IsNullOrEmpty(model.DataInputPath) == false) + else { - if (tes.Inputs == null) - { - tes.Inputs = new List(); - } - tes.Inputs.Add(new TesInput() - { - Path = model.DataInputPath, - Type = Enum.Parse(model.DataInputType), - Name = "", - Description = "", - Url = "", - Content = "" - }); + return BadRequest($"Unknown submission mode: '{Mode}'. Expected Simple, Custom, or Raw."); } - var context = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); - await _IKeyCloakService.RefreshUserToken(context); + // ── Submit to TES API ───────────────────────────────────────────────── + var authContext = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await _IKeyCloakService.RefreshUserToken(authContext); await _clientHelper.CallAPI("/v1/tasks", tes); return Ok(); @@ -723,6 +585,35 @@ public async Task SubmissionSQLWizardAction(SubmissionWizardV2 mod return BadRequest(e.Message); } } - + + // ── Private helpers ─────────────────────────────────────────────────────────── + + /// + /// Replaces literal escape sequences (\n, \r\n, \r) with real newline characters. + /// This handles text that arrives as JSON-encoded strings with visible backslash-n. + /// + private static string NormaliseText(string text) => + text.Replace("\\r\\n", "\n") + .Replace("\\r", "\n") + .Replace("\\n", "\n"); + + /// + /// Converts a list of "KEY=value" strings into a dictionary. + /// + private static Dictionary ParseEnvList(IEnumerable? envLines) + { + var result = new Dictionary(); + if (envLines == null) return result; + + foreach (var line in envLines) + { + if (string.IsNullOrWhiteSpace(line)) continue; + var parts = line.Split('=', 2); + if (parts.Length == 2) + result[parts[0].Trim()] = parts[1].Trim(); + } + + return result; + } } } diff --git a/Submission/Submission.Web/Views/Shared/_Layout.cshtml b/Submission/Submission.Web/Views/Shared/_Layout.cshtml index d10adcf4..b72c7a8f 100644 --- a/Submission/Submission.Web/Views/Shared/_Layout.cshtml +++ b/Submission/Submission.Web/Views/Shared/_Layout.cshtml @@ -623,7 +623,7 @@ $.ajax({ - url: '@Url.Action("SubmissionSQLWizardAction", "Submission")', + url: '@Url.Action("SubmissionSqlWizardAction", "Submission")', type: 'POST', data: formData, contentType: false, From e4eaf9089b7b19c3d44f80543182106209f7d1a9 Mon Sep 17 00:00:00 2001 From: AndrewThien Date: Mon, 23 Feb 2026 15:55:41 +0000 Subject: [PATCH 31/42] fix sonar qube --- .../SubmissionWizardV2/Default.cshtml | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardV2/Default.cshtml b/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardV2/Default.cshtml index 3f683ef7..213ed164 100644 --- a/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardV2/Default.cshtml +++ b/Submission/Submission.Web/Views/Shared/Components/SubmissionWizardV2/Default.cshtml @@ -49,10 +49,10 @@

    SQL TES Wizard V2

    -
    -
    +
    - + *
    - + (optional)
    -
    - - * +
    + + Select TREs * + @for (var i = 0; i < Model.TreRadios.Count(); i++) {
    @@ -128,7 +129,7 @@ {

    No available TREs

    } -
    +
    @@ -142,7 +143,7 @@
    - +
    @@ -156,7 +157,7 @@ - +
    Executors
    @@ -215,15 +216,15 @@ Raw JSON Mode: Build your complete TES task JSON here. This will override all other settings.
    Notes: -
    +
    • The JSON should follow the request body schema here
    • Tags Project (target project) and tres (list of target TREs, separated by "|") are required
    • -
    +
    - +
    TES Task JSON