• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

HicServices / RDMP / 13242776802

10 Feb 2025 02:09PM UTC coverage: 57.396% (-0.006%) from 57.402%
13242776802

push

github

web-flow
Merge pull request #2132 from HicServices/bugfix/RDMP-280-delete-cohort-versions

fix up cohort freezing and warning UI alert

11342 of 21308 branches covered (53.23%)

Branch coverage included in aggregate %.

1 of 11 new or added lines in 5 files covered. (9.09%)

1 existing line in 1 file now uncovered.

32238 of 54621 relevant lines covered (59.02%)

17081.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

6.33
/Rdmp.Core/CommandLine/Interactive/ConsoleInputManager.cs
1
// Copyright (c) The University of Dundee 2018-2019
2
// This file is part of the Research Data Management Platform (RDMP).
3
// RDMP is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
4
// RDMP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
5
// You should have received a copy of the GNU General Public License along with RDMP. If not, see <https://www.gnu.org/licenses/>.
6

7
using System;
8
using System.Collections.Generic;
9
using System.Data;
10
using System.Diagnostics;
11
using System.IO;
12
using System.Linq;
13
using System.Text;
14
using System.Threading;
15
using System.Threading.Tasks;
16
using FAnsi.Discovery;
17
using Rdmp.Core.CommandExecution;
18
using Rdmp.Core.CommandLine.Interactive.Picking;
19
using Rdmp.Core.Curation.Data.Aggregation;
20
using Rdmp.Core.Curation.Data.DataLoad;
21
using Rdmp.Core.DataExport.DataExtraction;
22
using Rdmp.Core.DataViewing;
23
using Rdmp.Core.MapsDirectlyToDatabaseTable;
24
using Rdmp.Core.Repositories;
25
using Rdmp.Core.ReusableLibraryCode;
26
using Rdmp.Core.ReusableLibraryCode.Checks;
27
using Rdmp.Core.ReusableLibraryCode.DataAccess;
28
using Spectre.Console;
29

30
namespace Rdmp.Core.CommandLine.Interactive;
31

32
/// <summary>
33
/// Implementation of <see cref="IBasicActivateItems"/> that handles object selection and message notification but is <see cref="IsInteractive"/>=false and throws <see cref="InputDisallowedException"/> on any attempt to illicit user feedback
34
/// </summary>
35
public class ConsoleInputManager : BasicActivateItems
36
{
37
    /// <inheritdoc/>
38
    public override bool IsInteractive => !DisallowInput;
20,336✔
39

40
    /// <summary>
41
    /// Set to true to throw on any blocking input methods (e.g. <see cref="TypeText"/>)
42
    /// </summary>
43
    public bool DisallowInput { get; set; }
20,620✔
44

45
    /// <summary>
46
    /// Creates a new instance connected to the provided RDMP platform databases
47
    /// </summary>
48
    /// <param name="repositoryLocator">The databases to connect to</param>
49
    /// <param name="globalErrorCheckNotifier">The global error provider for non fatal issues</param>
50
    public ConsoleInputManager(IRDMPPlatformRepositoryServiceLocator repositoryLocator,
51
        ICheckNotifier globalErrorCheckNotifier) : base(repositoryLocator, globalErrorCheckNotifier)
296✔
52
    {
53
    }
296✔
54

55
    public override void Show(string title, string message)
56
    {
57
        Console.WriteLine(message);
8✔
58
    }
8✔
59

60
    public override void ShowWarning(string message)
61
    {
NEW
62
        Console.WriteLine(message);
×
NEW
63
    }
×
64

65
    public override bool TypeText(DialogArgs args, int maxLength, string initialText, out string text,
66
        bool requireSaneHeaderText)
67
    {
68
        text = AnsiConsole.Prompt(
×
69
            new TextPrompt<string>(GetPromptFor(args))
×
70
                .AllowEmpty()
×
71
        );
×
72

73
        if (string.Equals(text, "Cancel", StringComparison.CurrentCultureIgnoreCase))
×
74
            // user does not want to type any text
75
            return false;
×
76

77
        // user typed "null" or some spaces or something
78
        if (text.IsBasicallyNull())
×
79
            text = null;
×
80

81
        // that's still an affirmative choice
82
        return true;
×
83
    }
84

85
    public override DiscoveredDatabase SelectDatabase(bool allowDatabaseCreation, string taskDescription)
86
    {
87
        if (DisallowInput)
×
88
            throw new InputDisallowedException($"Value required for '{taskDescription}'");
×
89

90
        var value = ReadLineWithAuto(new DialogArgs { WindowTitle = taskDescription }, new PickDatabase());
×
91
        return value.Database;
×
92
    }
93

94
    public override DiscoveredTable SelectTable(bool allowDatabaseCreation, string taskDescription)
95
    {
96
        if (DisallowInput)
×
97
            throw new InputDisallowedException($"Value required for '{taskDescription}'");
×
98

99
        var value = ReadLineWithAuto(new DialogArgs { WindowTitle = taskDescription }, new PickTable());
×
100
        return value.Table;
×
101
    }
102

103
    public override void ShowException(string errorText, Exception exception)
104
    {
105
        throw exception ?? new Exception(errorText);
×
106
    }
107

108
    public override bool SelectEnum(DialogArgs args, Type enumType, out Enum chosen)
109
    {
110
        if (DisallowInput)
×
111
            throw new InputDisallowedException($"Value required for '{args}'");
×
112

113
        var chosenStr = GetString(args, Enum.GetNames(enumType).ToList());
×
114
        try
115
        {
116
            chosen = (Enum)Enum.Parse(enumType, chosenStr);
×
117
        }
×
118
        catch (Exception)
×
119
        {
120
            Console.WriteLine(
×
121
                $"Could not parse value.  Valid Enum values are:{Environment.NewLine}{string.Join(Environment.NewLine, Enum.GetNames(enumType))}");
×
122
            throw;
×
123
        }
124

125
        return true;
×
126
    }
127

128
    public override bool SelectType(DialogArgs args, Type[] available, out Type chosen)
129
    {
130
        if (DisallowInput)
×
131
            throw new InputDisallowedException($"Value required for '{args}'");
×
132

133
        var chosenStr = GetString(args, available.Select(t => t.Name).ToList());
×
134

135
        if (string.IsNullOrWhiteSpace(chosenStr))
×
136
        {
137
            chosen = null;
×
138
            return false;
×
139
        }
140

141
        chosen = available.SingleOrDefault(t => t.Name.Equals(chosenStr));
×
142

143
        return chosen == null ? throw new Exception($"Unknown or incompatible Type '{chosen}'") : true;
×
144
    }
145

146

147
    public override IMapsDirectlyToDatabaseTable[] SelectMany(DialogArgs args, Type arrayElementType,
148
        IMapsDirectlyToDatabaseTable[] availableObjects)
149
    {
150
        var value = ReadLineWithAuto(args, new PickObjectBase[]
×
151
            { new PickObjectByID(this), new PickObjectByName(this) });
×
152

153
        var unavailable = value.DatabaseEntities.Except(availableObjects).ToArray();
×
154

155
        return unavailable.Any()
×
156
            ? throw new Exception(
×
157
                $"The following objects were not among the listed available objects {string.Join(",", unavailable.Select(o => o.ToString()))}")
×
158
            : value.DatabaseEntities.ToArray();
×
159
    }
160

161
    /// <summary>
162
    /// Displays the text described in the prompt theming <paramref name="args"/>
163
    /// </summary>
164
    /// <param name="args"></param>
165
    /// <param name="entryLabel"></param>
166
    /// <param name="pickers"></param>
167
    /// <exception cref="InputDisallowedException">Thrown if <see cref="DisallowInput"/> is true</exception>
168
    private string GetPromptFor(DialogArgs args, bool entryLabel = true, params PickObjectBase[] pickers)
169
    {
170
        if (DisallowInput)
2!
171
            throw new InputDisallowedException($"Value required for '{args}'");
2✔
172

173
        var sb = new StringBuilder();
×
174

175
        if (!string.IsNullOrWhiteSpace(args.WindowTitle))
×
176
        {
177
            sb.Append(Markup.Escape(args.WindowTitle));
×
178

179
            if (entryLabel && !string.IsNullOrWhiteSpace(args.EntryLabel)) sb.Append(" - ");
×
180
        }
181

182
        if (entryLabel && !string.IsNullOrWhiteSpace(args.EntryLabel))
×
183
            sb.Append($"[green]{Markup.Escape(args.EntryLabel)}[/]");
×
184

185
        if (!string.IsNullOrWhiteSpace(args.TaskDescription))
×
186
        {
187
            sb.AppendLine();
×
188
            sb.Append($"[grey]{Markup.Escape(args.TaskDescription)}[/]");
×
189
        }
190

191
        foreach (var picker in pickers)
×
192
        {
193
            sb.AppendLine();
×
194
            sb.Append($"Format:[grey]{Markup.Escape(picker.Format)}[/]");
×
195

196
            if (picker.Examples.Any())
×
197
            {
198
                sb.AppendLine();
×
199
                sb.Append("Examples:");
×
200
                foreach (var example in picker.Examples)
×
201
                {
202
                    sb.AppendLine();
×
203
                    sb.Append($"[grey]{Markup.Escape(example)}[/]");
×
204
                }
205
            }
206

207
            sb.AppendLine();
×
208
            sb.Append(':');
×
209
        }
210

211
        return sb.ToString();
×
212
    }
213

214
    public override IMapsDirectlyToDatabaseTable SelectOne(DialogArgs args,
215
        IMapsDirectlyToDatabaseTable[] availableObjects)
216
    {
217
        if (DisallowInput)
×
218
            return args.AllowAutoSelect && availableObjects.Length == 1
×
219
                ? availableObjects[0]
×
220
                : throw new InputDisallowedException($"Value required for '{args}'");
×
221

222
        if (availableObjects.Length == 0)
×
223
            throw new Exception("No available objects found");
×
224

225
        //handle auto selection when there is one object
226
        if (availableObjects.Length == 1 && args.AllowAutoSelect)
×
227
            return availableObjects[0];
×
228

229
        Console.WriteLine(args.WindowTitle);
×
230

231
        Console.Write(args.EntryLabel);
×
232

233
        var value = ReadLineWithAuto(args, new PickObjectBase[]
×
234
            { new PickObjectByID(this), new PickObjectByName(this) });
×
235

236
        var chosen = value.DatabaseEntities?.SingleOrDefault();
×
237

238
        if (chosen == null)
×
239
            return null;
×
240

241
        return !availableObjects.Contains(chosen)
×
242
            ? throw new Exception("Picked object was not one of the listed available objects")
×
243
            : chosen;
×
244
    }
245

246
    public override bool SelectObject<T>(DialogArgs args, T[] available, out T selected)
247
    {
248
        for (var i = 0; i < available.Length; i++) Console.WriteLine($"{i}:{available[i]}");
×
249

250
        Console.Write(args.EntryLabel);
×
251

252
        var result = Console.ReadLine();
×
253

254
        if (int.TryParse(result, out var idx))
×
255
            if (idx >= 0 && idx < available.Length)
×
256
            {
257
                selected = available[idx];
×
258
                return true;
×
259
            }
260

261
        selected = default;
×
262
        return false;
×
263
    }
264

265
    private CommandLineObjectPickerArgumentValue ReadLineWithAuto(DialogArgs args, params PickObjectBase[] pickers)
266
    {
267
        if (DisallowInput)
×
268
            throw new InputDisallowedException("Value required");
×
269

270
        var line = AnsiConsole.Prompt(
×
271
            new TextPrompt<string>(
×
272
                GetPromptFor(args, true, pickers).Trim()));
×
273

274

275
        var cli = new CommandLineObjectPicker(new[] { line }, pickers);
×
276
        return cli[0];
×
277
    }
278

279
    public override DirectoryInfo SelectDirectory(string prompt)
280
    {
281
        if (DisallowInput)
×
282
            throw new InputDisallowedException($"Value required for '{prompt}'");
×
283

284
        var result = AnsiConsole.Prompt<string>(
×
285
            new TextPrompt<string>(
×
286
                    GetPromptFor(new DialogArgs
×
287
                    {
×
288
                        WindowTitle = "Select Directory",
×
289
                        EntryLabel = prompt
×
290
                    }))
×
291
                .AllowEmpty());
×
292

293
        return result.IsBasicallyNull() ? null : new DirectoryInfo(result);
×
294
    }
295

296
    public override FileInfo SelectFile(string prompt) => DisallowInput
×
297
        ? throw new InputDisallowedException($"Value required for '{prompt}'")
×
298
        : SelectFile(prompt, null, null);
×
299

300
    public override FileInfo SelectFile(string prompt, string patternDescription, string pattern)
301
    {
302
        if (DisallowInput)
×
303
            throw new InputDisallowedException($"Value required for '{prompt}'");
×
304

305
        var result = AnsiConsole.Prompt<string>(
×
306
            new TextPrompt<string>(
×
307
                    GetPromptFor(new DialogArgs
×
308
                    {
×
309
                        WindowTitle = "Select File",
×
310
                        EntryLabel = prompt
×
311
                    }))
×
312
                .AllowEmpty());
×
313

314
        return result.IsBasicallyNull() ? null : new FileInfo(result);
×
315
    }
316

317
    public override FileInfo[] SelectFiles(string prompt, string patternDescription, string pattern)
318
    {
319
        if (DisallowInput)
×
320
            throw new InputDisallowedException($"Value required for '{prompt}'");
×
321

322
        var file = AnsiConsole.Prompt<string>(
×
323
            new TextPrompt<string>(
×
324
                    GetPromptFor(new DialogArgs
×
325
                    {
×
326
                        WindowTitle = "Select File(s)",
×
327
                        TaskDescription = patternDescription,
×
328
                        EntryLabel = prompt
×
329
                    }))
×
330
                .AllowEmpty());
×
331

332
        if (file.IsBasicallyNull())
×
333
            return null;
×
334

335
        var asteriskIdx = file.IndexOf('*');
×
336

337
        if (asteriskIdx != -1)
×
338
        {
339
            var idxLastSlash =
×
340
                file.LastIndexOfAny(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar });
×
341

342
            if (idxLastSlash == -1 || asteriskIdx < idxLastSlash)
×
343
                throw new Exception("Wildcards are only supported at the file level");
×
344

345
            var searchPattern = file[(idxLastSlash + 1)..];
×
346
            var dirStr = file[..idxLastSlash];
×
347

348
            var dir = new DirectoryInfo(dirStr);
×
349

350
            return !dir.Exists
×
351
                ? throw new DirectoryNotFoundException($"Could not find directory:{dirStr}")
×
352
                : dir.GetFiles(searchPattern).ToArray();
×
353
        }
354

355
        return new[] { new FileInfo(file) };
×
356
    }
357

358

359
    protected override bool SelectValueTypeImpl(DialogArgs args, Type paramType, object initialValue, out object chosen)
360
    {
361
        chosen = UsefulStuff.ChangeType(AnsiConsole.Ask<string>(GetPromptFor(args)), paramType);
×
362
        return true;
×
363
    }
364

365
    public override bool YesNo(DialogArgs args, out bool chosen)
366
    {
367
        var result = GetString(args, new List<string> { "Yes", "No", "Cancel" });
×
368
        chosen = result == "Yes";
×
369
        //user made a non-cancel decision?
370
        return result != "Cancel" && !string.IsNullOrWhiteSpace(result);
×
371
    }
372

373
    public string GetString(DialogArgs args, List<string> options)
374
    {
375
        var chosen = AnsiConsole.Prompt(
2✔
376
            new SelectionPrompt<string>()
2✔
377
                .PageSize(10)
2✔
378
                .Title(GetPromptFor(args))
2✔
379
                .AddChoices(options)
2✔
380
        );
2✔
381

382
        return chosen;
×
383
    }
384

385
    public override void ShowData(IViewSQLAndResultsCollection collection)
386
    {
387
        var point = collection.GetDataAccessPoint();
×
388
        var db = DataAccessPortal.ExpectDatabase(point, DataAccessContext.InternalDataProcessing);
×
389

390
        var sql = collection.GetSql();
×
391

392
        var logger = NLog.LogManager.GetCurrentClassLogger();
×
393
        logger.Trace($"About to ShowData from Query:{Environment.NewLine}{sql}");
×
394

395
        var toRun = new ExtractTableVerbatim(db.Server, sql, Console.OpenStandardOutput(), ",", null);
×
396
        toRun.DoExtraction();
×
397
    }
×
398

399
    public override void ShowLogs(ILoggedActivityRootObject rootObject)
400
    {
401
        foreach (var load in GetLogs(rootObject).OrderByDescending(l => l.StartTime))
×
402
        {
403
            Console.WriteLine(load.Description);
×
404
            Console.WriteLine(load.StartTime);
×
405

406
            Console.WriteLine($"Errors:{load.Errors.Count}");
×
407

408
            foreach (var error in load.Errors)
×
409
            {
410
                error.GetSummary(out var title, out var body, out _, out _);
×
411

412
                Console.WriteLine($"\t{title}");
×
413
                Console.WriteLine($"\t{body}");
×
414
            }
415

416
            Console.WriteLine("Tables Loaded:");
×
417

418
            foreach (var t in load.TableLoadInfos)
×
419
            {
420
                Console.WriteLine($"\t{t}: I={t.Inserts:N0} U={t.Updates:N0} D={t.Deletes:N0}");
×
421

422
                foreach (var source in t.DataSources)
×
423
                    Console.WriteLine($"\t\tSource:{source.Source}");
×
424
            }
425

426
            Console.WriteLine("Progress:");
×
427

428
            foreach (var p in load.Progress) Console.WriteLine($"\t{p.Date} {p.Description}");
×
429
        }
430
    }
×
431

432
    public override void ShowGraph(AggregateConfiguration aggregate)
433
    {
434
        ShowData(new ViewAggregateExtractUICollection(aggregate));
×
435
    }
×
436

437
    public override bool SelectObjects<T>(DialogArgs args, T[] available, out T[] selected)
438
    {
439
        if (available.Length == 0)
×
440
        {
441
            selected = Array.Empty<T>();
×
442
            return true;
×
443
        }
444

445
        for (var i = 0; i < available.Length; i++) Console.WriteLine($"{i}:{available[i]}");
×
446

447
        var result = Console.ReadLine();
×
448

449
        if (string.IsNullOrWhiteSpace(result))
×
450
        {
451
            // selecting none is a valid user selection
452
            selected = Array.Empty<T>();
×
453
            return true;
×
454
        }
455

456
        var selectIdx = result.Split(",", StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
×
457

458
        selected = available.Where((e, idx) => selectIdx.Contains(idx)).ToArray();
×
459
        return true;
×
460
    }
461

462
    public override void LaunchSubprocess(ProcessStartInfo startInfo)
463
    {
464
        throw new NotSupportedException();
×
465
    }
466

467
    public override void Wait(string title, Task task, CancellationTokenSource cts)
468
    {
469
        AnsiConsole.Status()
4✔
470
            .Spinner(Spinner.Known.Star)
4✔
471
            .Start(title, ctx =>
4✔
472
                base.Wait(title, task, cts)
4✔
473
            );
4✔
474
    }
4✔
475

476
    public override void ShowData(DataTable collection)
477
    {
478
        var tbl = new Table();
×
479

480
        foreach (DataColumn c in collection.Columns) tbl.AddColumn(c.ColumnName);
×
481

482
        foreach (DataRow row in collection.Rows) tbl.AddRow(row.ItemArray.Select(i => i.ToString()).ToArray());
×
483
        AnsiConsole.Write(tbl);
×
484
    }
×
485
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc