Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 24 additions & 15 deletions .github/workflows/test-webui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,50 @@ on:

jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04 # pin for consistency (22.04 is also fine)
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x

# Restore all projects
- name: Cache NuGet
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/packages.lock.json') }}
restore-keys: |
${{ runner.os }}-nuget-

- name: Cache Playwright browsers (Chromium)
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-pw-chromium-${{ hashFiles('**/*.csproj') }}
restore-keys: |
${{ runner.os }}-pw-chromium-

- name: Restore
run: dotnet restore

# Build solution
- name: Build
run: dotnet build --no-restore --configuration Release

# Install Playwright CLI
- name: Install Playwright CLI
run: dotnet tool install --global Microsoft.Playwright.CLI

# Install browser binaries + Linux deps
- name: Install Playwright Browsers
# Prefer the Playwright script that comes with the NuGet package (no global tool install)
- name: Install Playwright Browsers (Chromium only)
shell: bash
run: |
playwright install --with-deps
pwsh Tests.E2e/bin/Release/net*/playwright.ps1 install --with-deps chromium

# Build Docker image used by Testcontainers
- name: Build Docker Image
run: |
docker build \
-t rackpeek:ci \
-f RackPeek.Web/Dockerfile \
.

# Run E2E tests
- name: Run E2E Tests
run: dotnet test Tests.E2e --configuration Release --verbosity normal
run: dotnet test Tests.E2e --configuration Release --verbosity normal
108 changes: 85 additions & 23 deletions Shared.Rcl/Services/ServiceCardComponent.razor
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,24 @@
}
else if (Service.Network?.Port.HasValue == true)
{
<div class="text-zinc-300"
data-testid="service-port-value">
@Service.Network.Port
</div>
var href = GetBrowsableHref();

if (href is not null)
{
<a href="@href"
data-testid="service-port-value"
target="_blank"
rel="noopener noreferrer"
class="text-emerald-400 hover:text-emerald-300 hover:underline transition">
@Service.Network.Port
</a>
}
else
{
<div class="text-zinc-300" data-testid="service-port-value">
@Service.Network.Port
</div>
}
}
</div>

Expand Down Expand Up @@ -334,32 +348,59 @@
async Task HandleParentSelected(string? name)
{
SelectedParentName = name;
await UpdateUseCase.ExecuteAsync(
Service.Name,
Service.Network?.Ip,
Service.Network?.Port,
Service.Network?.Protocol,
Service.Network?.Url,
new List<string>{name},
Service.Notes);

var runsOn = (_isEditing ? _edit.RunsOn : Service.RunsOn) ?? new List<string>();
runsOn = runsOn.Where(x => !string.IsNullOrWhiteSpace(x)).ToList();

if (!string.IsNullOrWhiteSpace(name) && !runsOn.Contains(name))
runsOn.Add(name);

var ip = _isEditing ? _edit.Ip : Service.Network?.Ip;
var port = _isEditing ? _edit.Port : Service.Network?.Port;
var protocol = _isEditing ? _edit.Protocol : Service.Network?.Protocol;
var url = _isEditing ? _edit.Url : Service.Network?.Url;
var notes = _isEditing ? _edit.Notes : Service.Notes;

await UpdateUseCase.ExecuteAsync(Service.Name, ip, port, protocol, url, runsOn, notes);

// Refresh service from backend (optional, but if you do it, DO NOT nuke the edit buffer)
Service = await GetByNameUseCase.ExecuteAsync(Service.Name);
_edit = ServiceEditModel.From(Service);

if (_isEditing)
{
// keep whatever the user typed; just sync RunsOn
_edit.RunsOn = runsOn;
}
else
{
_edit = ServiceEditModel.From(Service);
}
}

async Task HandleParentDeleted(string? name)
{
if (string.IsNullOrWhiteSpace(name))
return;

SelectedParentName = name;
Service.RunsOn.Remove(SelectedParentName);
await UpdateUseCase.ExecuteAsync(
Service.Name,
Service.Network?.Ip,
Service.Network?.Port,
Service.Network?.Protocol,
Service.Network?.Url,
Service.RunsOn,
Service.Notes);

var runsOn = (_isEditing ? _edit.RunsOn : Service.RunsOn) ?? new List<string>();
runsOn = runsOn.Where(x => !string.IsNullOrWhiteSpace(x) && x != name).ToList();

var ip = _isEditing ? _edit.Ip : Service.Network?.Ip;
var port = _isEditing ? _edit.Port : Service.Network?.Port;
var protocol = _isEditing ? _edit.Protocol : Service.Network?.Protocol;
var url = _isEditing ? _edit.Url : Service.Network?.Url;
var notes = _isEditing ? _edit.Notes : Service.Notes;

await UpdateUseCase.ExecuteAsync(Service.Name, ip, port, protocol, url, runsOn, notes);

Service = await GetByNameUseCase.ExecuteAsync(Service.Name);
_edit = ServiceEditModel.From(Service);

if (_isEditing)
_edit.RunsOn = runsOn;
else
_edit = ServiceEditModel.From(Service);
}

}
Expand Down Expand Up @@ -401,4 +442,25 @@
Nav.NavigateTo($"resources/services/{Uri.EscapeDataString(newName)}");
}

private string? GetBrowsableHref()
{
var ip = Service.Network?.Ip;
var port = Service.Network?.Port;

if (string.IsNullOrWhiteSpace(ip) || port is null)
return null;

var proto = Service.Network?.Protocol?.Trim().ToLowerInvariant();

var scheme = proto switch
{
"https" => "https",
"http" => "http",
_ => "http"
};

// Build a correct absolute URL
var ub = new UriBuilder(scheme, ip) { Port = port.Value };
return ub.Uri.ToString();
}
}
117 changes: 79 additions & 38 deletions Shared.Rcl/Systems/SystemCardComponent.razor
Original file line number Diff line number Diff line change
Expand Up @@ -180,24 +180,27 @@

@if (_isEditing)
{
@if (System.RunsOn?.Count > 0)
{
@foreach(var parent in System.RunsOn)
{
@if (_edit.RunsOn?.Count > 0)
{
@foreach (var parent in _edit.RunsOn)
{
<button
type="button"
class="hover:text-emerald-400"
title="Edit Runs On"
@onclick="() => _selectParentOpen = true">
@($"{parent}")
@parent
</button>

<button
type="button"
class="text-red-400 hover:text-red-300 pr-4"
title="Remove"
@onclick="() => HandleParentDeleted(parent)">
@($"✕")
@onclick="async () => await HandleParentDeleted(parent)">
</button>
}
}
}
}
}
else if (System.RunsOn?.Count > 0)
{
Expand Down Expand Up @@ -290,15 +293,15 @@


<ConfirmModal
IsOpen="_confirmDeleteOpen"
IsOpen="@_confirmDeleteOpen"
IsOpenChanged="v => _confirmDeleteOpen = v"
Title="Delete system"
ConfirmText="Delete"
ConfirmClass="bg-red-600 hover:bg-red-500"
OnConfirm="DeleteServer"
TestIdPrefix="system-delete">
Are you sure you want to delete <strong>@System.Name</strong>?
<br/>
<br />
This will detach all dependent systems.
</ConfirmModal>

Expand Down Expand Up @@ -341,36 +344,74 @@
bool _selectParentOpen;
string? SelectedParentName;

async Task HandleParentSelected(string? name)
{
SelectedParentName = name;
await UpdateUseCase.ExecuteAsync(
System.Name,
System.Type,
System.Os,
System.Cores,
System.Ram,
new List<string>{name},
System.Notes);
System = await GetByNameUseCase.ExecuteAsync(System.Name);
async Task HandleParentSelected(string? name)
{
SelectedParentName = name;

// Start from the edit buffer when editing, otherwise from the persisted model
var runsOn = (_isEditing ? _edit.RunsOn : System.RunsOn) ?? new List<string>();
runsOn = runsOn.Where(x => !string.IsNullOrWhiteSpace(x)).ToList();

if (!string.IsNullOrWhiteSpace(name) && !runsOn.Contains(name))
runsOn.Add(name);

// IMPORTANT: use _edit values when editing so we don't wipe typed fields
var type = _isEditing ? _edit.Type : System.Type;
var os = _isEditing ? _edit.Os : System.Os;
var cores = _isEditing ? _edit.Cores : System.Cores;
var ram = _isEditing ? _edit.Ram : System.Ram;
var notes = _isEditing ? _edit.Notes : System.Notes;

await UpdateUseCase.ExecuteAsync(
System.Name,
type,
os,
cores,
ram,
runsOn,
notes);

System = await GetByNameUseCase.ExecuteAsync(System.Name);

if (_isEditing)
_edit.RunsOn = runsOn;
else
_edit = SystemEditModel.From(System);
}
}

async Task HandleParentDeleted(string? name)
{
SelectedParentName = name;
System.RunsOn.Remove(SelectedParentName);
await UpdateUseCase.ExecuteAsync(
System.Name,
System.Type,
System.Os,
System.Cores,
System.Ram,
System.RunsOn,
System.Notes);
System = await GetByNameUseCase.ExecuteAsync(System.Name);
async Task HandleParentDeleted(string? name)
{
if (string.IsNullOrWhiteSpace(name))
return;

SelectedParentName = name;

var runsOn = (_isEditing ? _edit.RunsOn : System.RunsOn) ?? new List<string>();
System.RunsOn = runsOn = runsOn.Where(x => !string.IsNullOrWhiteSpace(x) && x != name).ToList();

var type = _isEditing ? _edit.Type : System.Type;
var os = _isEditing ? _edit.Os : System.Os;
var cores = _isEditing ? _edit.Cores : System.Cores;
var ram = _isEditing ? _edit.Ram : System.Ram;
var notes = _isEditing ? _edit.Notes : System.Notes;

await UpdateUseCase.ExecuteAsync(
System.Name,
type,
os,
cores,
ram,
runsOn,
notes);

System = await GetByNameUseCase.ExecuteAsync(System.Name);

if (_isEditing)
_edit.RunsOn = runsOn;
else
_edit = SystemEditModel.From(System);
}
}



#region Drives
Expand Down
10 changes: 9 additions & 1 deletion Shared.Rcl/Systems/SystemEditModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ namespace Shared.Rcl.Systems;
public sealed class SystemEditModel
{
public string Name { get; init; } = default!;
public string? Type { get; set; }
private string? _type;
public string? Type
{
get => _type;
set => _type = string.IsNullOrWhiteSpace(value)
? null
: value.Trim().ToLowerInvariant();
}

public string? Os { get; set; }
public int? Cores { get; set; }
public double? Ram { get; set; }
Expand Down
Loading