Skip to content

Commit 737729e

Browse files
committed
feat(portal): Cells & Operations CRUD (UI + services); DAB permissions; fix AppHost SDK approach; docs update
1 parent de0c063 commit 737729e

File tree

7 files changed

+204
-7
lines changed

7 files changed

+204
-7
lines changed

management-portal/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@ Run locally with one command or via the AppHost. Defaults to in-memory data; set
2222
- Set DAB_GRAPHQL_URL to use GraphQL
2323

2424
## CRUD
25-
- Tenants page supports create/update/delete when GraphQL is enabled (DAB provides mutations).
25+
- Tenants, Cells, and Operations pages support create/update/delete when GraphQL is enabled (DAB provides mutations).
2626
- For in-memory mode, operations affect only the running process.

management-portal/dab/dab-config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@
2929
"graphql": {
3030
"type": { "singular": "Cell", "plural": "Cells" }
3131
},
32-
"permissions": [ { "role": "anonymous", "actions": [ "read" ] } ]
32+
"permissions": [ { "role": "anonymous", "actions": [ "read", "create", "update", "delete" ] } ]
3333
},
3434
"Operation": {
3535
"source": "operations",
3636
"graphql": {
3737
"type": { "singular": "Operation", "plural": "Operations" }
3838
},
39-
"permissions": [ { "role": "anonymous", "actions": [ "read" ] } ]
39+
"permissions": [ { "role": "anonymous", "actions": [ "read", "create", "update", "delete" ] } ]
4040
}
4141
}
4242
}

management-portal/src/Portal/Pages/Cells.razor

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,25 @@
11
@page "/cells"
2+
@using Microsoft.AspNetCore.Components.Forms
3+
@using System.ComponentModel.DataAnnotations
24
@inject Stamps.ManagementPortal.Services.IDataService Data
35

46
<h1>Cells</h1>
57

8+
<EditForm Model="edit" OnValidSubmit="OnSaveAsync">
9+
<DataAnnotationsValidator />
10+
<div class="form-grid">
11+
<input @bind="edit.Id" placeholder="Id" />
12+
<input @bind="edit.Region" placeholder="Region" />
13+
<input @bind="edit.AvailabilityZone" placeholder="AZ" />
14+
<input @bind="edit.Status" placeholder="Status" />
15+
<input type="number" @bind-value="edit.CapacityUsed" placeholder="Used" />
16+
<input type="number" @bind-value="edit.CapacityTotal" placeholder="Total" />
17+
</div>
18+
<button type="submit">Save</button>
19+
<button type="button" @onclick="NewAsync">New</button>
20+
<span class="hint">CRUD requires GraphQL/DAB to be enabled</span>
21+
</EditForm>
22+
623
@if (cells is null)
724
{
825
<p>Loading...</p>
@@ -22,6 +39,10 @@ else
2239
<td>@c.AvailabilityZone</td>
2340
<td>@c.Status</td>
2441
<td>@c.CapacityUsed/@c.CapacityTotal</td>
42+
<td>
43+
<button @onclick="() => Edit(c)">Edit</button>
44+
<button @onclick="() => DeleteAsync(c)">Delete</button>
45+
</td>
2546
</tr>
2647
}
2748
</tbody>
@@ -30,8 +51,39 @@ else
3051

3152
@code {
3253
private IReadOnlyList<Stamps.ManagementPortal.Models.Cell>? cells;
54+
private CellEdit edit = new();
55+
56+
class CellEdit
57+
{
58+
public string Id { get; set; } = string.Empty;
59+
public string Region { get; set; } = string.Empty;
60+
public string AvailabilityZone { get; set; } = string.Empty;
61+
public string Status { get; set; } = string.Empty;
62+
public int CapacityUsed { get; set; }
63+
public int CapacityTotal { get; set; }
64+
}
3365
protected override async Task OnInitializedAsync()
3466
{
3567
cells = await Data.GetCellsAsync();
3668
}
69+
70+
void Edit(Stamps.ManagementPortal.Models.Cell c)
71+
=> edit = new CellEdit { Id = c.Id, Region = c.Region, AvailabilityZone = c.AvailabilityZone, Status = c.Status, CapacityUsed = c.CapacityUsed, CapacityTotal = c.CapacityTotal };
72+
Task NewAsync() { edit = new CellEdit(); return Task.CompletedTask; }
73+
74+
async Task OnSaveAsync()
75+
{
76+
var rec = new Stamps.ManagementPortal.Models.Cell(edit.Id, edit.Region, edit.AvailabilityZone, edit.Status, edit.CapacityUsed, edit.CapacityTotal);
77+
if (cells?.Any(x => x.Id == rec.Id) == true)
78+
await Data.UpdateCellAsync(rec);
79+
else
80+
await Data.CreateCellAsync(rec);
81+
cells = await Data.GetCellsAsync();
82+
}
83+
84+
async Task DeleteAsync(Stamps.ManagementPortal.Models.Cell c)
85+
{
86+
await Data.DeleteCellAsync(c.Id, c.Id);
87+
cells = await Data.GetCellsAsync();
88+
}
3789
}

management-portal/src/Portal/Pages/Operations.razor

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
11
@page "/operations"
2+
@using Microsoft.AspNetCore.Components.Forms
3+
@using System.ComponentModel.DataAnnotations
24
@inject Stamps.ManagementPortal.Services.IDataService Data
35

46
<h1>Operations</h1>
57

8+
<EditForm Model="edit" OnValidSubmit="OnSaveAsync">
9+
<DataAnnotationsValidator />
10+
<div class="form-grid">
11+
<input @bind="edit.Id" placeholder="Id" />
12+
<input @bind="edit.TenantId" placeholder="Tenant Id" />
13+
<input @bind="edit.Type" placeholder="Type" />
14+
<input @bind="edit.Status" placeholder="Status" />
15+
<input type="datetime-local" @bind-value="edit.CreatedAtLocal" />
16+
</div>
17+
<button type="submit">Save</button>
18+
<button type="button" @onclick="NewAsync">New</button>
19+
<span class="hint">CRUD requires GraphQL/DAB to be enabled</span>
20+
</EditForm>
21+
622
@if (ops is null)
723
{
824
<p>Loading...</p>
@@ -22,6 +38,10 @@ else
2238
<td>@o.Type</td>
2339
<td>@o.Status</td>
2440
<td>@o.CreatedAt.ToLocalTime()</td>
41+
<td>
42+
<button @onclick="() => Edit(o)">Edit</button>
43+
<button @onclick="() => DeleteAsync(o)">Delete</button>
44+
</td>
2545
</tr>
2646
}
2747
</tbody>
@@ -30,8 +50,43 @@ else
3050

3151
@code {
3252
private IReadOnlyList<Stamps.ManagementPortal.Models.Operation>? ops;
53+
private OperationEdit edit = new();
54+
55+
class OperationEdit
56+
{
57+
public string Id { get; set; } = string.Empty;
58+
public string TenantId { get; set; } = string.Empty;
59+
public string Type { get; set; } = string.Empty;
60+
public string Status { get; set; } = string.Empty;
61+
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
62+
public DateTime CreatedAtLocal
63+
{
64+
get => CreatedAt.LocalDateTime;
65+
set => CreatedAt = new DateTimeOffset(value, DateTimeOffset.Now.Offset);
66+
}
67+
}
3368
protected override async Task OnInitializedAsync()
3469
{
3570
ops = await Data.GetOperationsAsync();
3671
}
72+
73+
void Edit(Stamps.ManagementPortal.Models.Operation o)
74+
=> edit = new OperationEdit { Id = o.Id, TenantId = o.TenantId, Type = o.Type, Status = o.Status, CreatedAt = o.CreatedAt };
75+
Task NewAsync() { edit = new OperationEdit(); return Task.CompletedTask; }
76+
77+
async Task OnSaveAsync()
78+
{
79+
var rec = new Stamps.ManagementPortal.Models.Operation(edit.Id, edit.TenantId, edit.Type, edit.Status, edit.CreatedAt);
80+
if (ops?.Any(x => x.Id == rec.Id) == true)
81+
await Data.UpdateOperationAsync(rec);
82+
else
83+
await Data.CreateOperationAsync(rec);
84+
ops = await Data.GetOperationsAsync();
85+
}
86+
87+
async Task DeleteAsync(Stamps.ManagementPortal.Models.Operation o)
88+
{
89+
await Data.DeleteOperationAsync(o.Id, o.TenantId);
90+
ops = await Data.GetOperationsAsync();
91+
}
3792
}

management-portal/src/Portal/Services/GraphQLDataService.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,48 @@ public async Task DeleteTenantAsync(string id, string partitionKey, Cancellation
7171
await MutationAsync<object>(mutation, variables, "deleteTenant", ct);
7272
}
7373

74+
public async Task<Cell> CreateCellAsync(Cell cell, CancellationToken ct = default)
75+
{
76+
var mutation = @"mutation($input: CreateCellInput!) { createCell(input: $input) { id region availabilityZone status capacityUsed capacityTotal } }";
77+
var variables = new { input = new { id = cell.Id, pk = cell.Id, region = cell.Region, availabilityZone = cell.AvailabilityZone, status = cell.Status, capacityUsed = cell.CapacityUsed, capacityTotal = cell.CapacityTotal } };
78+
return await MutationAsync<Cell>(mutation, variables, "createCell", ct);
79+
}
80+
81+
public async Task<Cell> UpdateCellAsync(Cell cell, CancellationToken ct = default)
82+
{
83+
var mutation = @"mutation($id: ID!, $input: UpdateCellInput!) { updateCell(id: $id, input: $input) { id region availabilityZone status capacityUsed capacityTotal } }";
84+
var variables = new { id = cell.Id, input = new { region = cell.Region, availabilityZone = cell.AvailabilityZone, status = cell.Status, capacityUsed = cell.CapacityUsed, capacityTotal = cell.CapacityTotal } };
85+
return await MutationAsync<Cell>(mutation, variables, "updateCell", ct);
86+
}
87+
88+
public async Task DeleteCellAsync(string id, string partitionKey, CancellationToken ct = default)
89+
{
90+
var mutation = @"mutation($id: ID!, $pk: String!) { deleteCell(id: $id, partitionKeyValue: $pk) }";
91+
var variables = new { id, pk = partitionKey };
92+
await MutationAsync<object>(mutation, variables, "deleteCell", ct);
93+
}
94+
95+
public async Task<Operation> CreateOperationAsync(Operation op, CancellationToken ct = default)
96+
{
97+
var mutation = @"mutation($input: CreateOperationInput!) { createOperation(input: $input) { id tenantId type status createdAt } }";
98+
var variables = new { input = new { id = op.Id, pk = op.TenantId, tenantId = op.TenantId, type = op.Type, status = op.Status, createdAt = op.CreatedAt } };
99+
return await MutationAsync<Operation>(mutation, variables, "createOperation", ct);
100+
}
101+
102+
public async Task<Operation> UpdateOperationAsync(Operation op, CancellationToken ct = default)
103+
{
104+
var mutation = @"mutation($id: ID!, $input: UpdateOperationInput!) { updateOperation(id: $id, input: $input) { id tenantId type status createdAt } }";
105+
var variables = new { id = op.Id, input = new { tenantId = op.TenantId, type = op.Type, status = op.Status, createdAt = op.CreatedAt } };
106+
return await MutationAsync<Operation>(mutation, variables, "updateOperation", ct);
107+
}
108+
109+
public async Task DeleteOperationAsync(string id, string partitionKey, CancellationToken ct = default)
110+
{
111+
var mutation = @"mutation($id: ID!, $pk: String!) { deleteOperation(id: $id, partitionKeyValue: $pk) }";
112+
var variables = new { id, pk = partitionKey };
113+
await MutationAsync<object>(mutation, variables, "deleteOperation", ct);
114+
}
115+
74116
private async Task<IReadOnlyList<T>> QueryAsync<T>(string query, string rootField, CancellationToken ct)
75117
{
76118
var payload = new { query };

management-portal/src/Portal/Services/IDataService.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,14 @@ public interface IDataService
1212
Task<Tenant> CreateTenantAsync(Tenant tenant, CancellationToken ct = default);
1313
Task<Tenant> UpdateTenantAsync(Tenant tenant, CancellationToken ct = default);
1414
Task DeleteTenantAsync(string id, string partitionKey, CancellationToken ct = default);
15+
16+
// Cell CRUD
17+
Task<Cell> CreateCellAsync(Cell cell, CancellationToken ct = default);
18+
Task<Cell> UpdateCellAsync(Cell cell, CancellationToken ct = default);
19+
Task DeleteCellAsync(string id, string partitionKey, CancellationToken ct = default);
20+
21+
// Operation CRUD
22+
Task<Operation> CreateOperationAsync(Operation op, CancellationToken ct = default);
23+
Task<Operation> UpdateOperationAsync(Operation op, CancellationToken ct = default);
24+
Task DeleteOperationAsync(string id, string partitionKey, CancellationToken ct = default);
1525
}

management-portal/src/Portal/Services/InMemoryDataService.cs

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,20 @@ public class InMemoryDataService : IDataService
99
new("contoso","Contoso","contoso.com","enterprise","active","cell-eastus-1"),
1010
new("fabrikam","Fabrikam","fabrikam.io","smb","active","cell-westus-1")
1111
};
12-
private static readonly IReadOnlyList<Cell> Cells = new List<Cell>
12+
private static readonly List<Cell> Cells = new()
1313
{
1414
new("cell-eastus-1","eastus","1","healthy",60,100),
1515
new("cell-westus-1","westus","2","healthy",40,100)
1616
};
17-
private static readonly IReadOnlyList<Operation> Operations = new List<Operation>
17+
private static readonly List<Operation> Operations = new()
1818
{
1919
new("op-001","contoso","migrate","running", DateTimeOffset.UtcNow.AddMinutes(-12)),
2020
new("op-002","fabrikam","suspend","completed", DateTimeOffset.UtcNow.AddDays(-1))
2121
};
2222

2323
public Task<IReadOnlyList<Tenant>> GetTenantsAsync(CancellationToken ct = default) => Task.FromResult((IReadOnlyList<Tenant>)Tenants.ToList());
24-
public Task<IReadOnlyList<Cell>> GetCellsAsync(CancellationToken ct = default) => Task.FromResult(Cells);
25-
public Task<IReadOnlyList<Operation>> GetOperationsAsync(CancellationToken ct = default) => Task.FromResult(Operations);
24+
public Task<IReadOnlyList<Cell>> GetCellsAsync(CancellationToken ct = default) => Task.FromResult((IReadOnlyList<Cell>)Cells.ToList());
25+
public Task<IReadOnlyList<Operation>> GetOperationsAsync(CancellationToken ct = default) => Task.FromResult((IReadOnlyList<Operation>)Operations.ToList());
2626

2727
public Task<Tenant> CreateTenantAsync(Tenant tenant, CancellationToken ct = default)
2828
{
@@ -42,4 +42,42 @@ public Task DeleteTenantAsync(string id, string partitionKey, CancellationToken
4242
Tenants.RemoveAll(t => t.Id == id);
4343
return Task.CompletedTask;
4444
}
45+
46+
public Task<Cell> CreateCellAsync(Cell cell, CancellationToken ct = default)
47+
{
48+
Cells.Add(cell);
49+
return Task.FromResult(cell);
50+
}
51+
52+
public Task<Cell> UpdateCellAsync(Cell cell, CancellationToken ct = default)
53+
{
54+
var idx = Cells.FindIndex(c => c.Id == cell.Id);
55+
if (idx >= 0) Cells[idx] = cell;
56+
return Task.FromResult(cell);
57+
}
58+
59+
public Task DeleteCellAsync(string id, string partitionKey, CancellationToken ct = default)
60+
{
61+
Cells.RemoveAll(c => c.Id == id);
62+
return Task.CompletedTask;
63+
}
64+
65+
public Task<Operation> CreateOperationAsync(Operation op, CancellationToken ct = default)
66+
{
67+
Operations.Add(op);
68+
return Task.FromResult(op);
69+
}
70+
71+
public Task<Operation> UpdateOperationAsync(Operation op, CancellationToken ct = default)
72+
{
73+
var idx = Operations.FindIndex(o => o.Id == op.Id);
74+
if (idx >= 0) Operations[idx] = op;
75+
return Task.FromResult(op);
76+
}
77+
78+
public Task DeleteOperationAsync(string id, string partitionKey, CancellationToken ct = default)
79+
{
80+
Operations.RemoveAll(o => o.Id == id);
81+
return Task.CompletedTask;
82+
}
4583
}

0 commit comments

Comments
 (0)