CRUD-Operation für Server implementiert

This commit is contained in:
2025-06-15 14:20:21 +02:00
parent f362dc9e3a
commit 1f1a1fddbd
10 changed files with 507 additions and 74 deletions

View File

@@ -41,11 +41,8 @@ public class ServerController : Controller
{ {
Name = vm.Name, Name = vm.Name,
IPAddress = vm.IPAddress, IPAddress = vm.IPAddress,
Hostname = vm.Hostname,
Status = vm.Status,
Type = vm.Type, Type = vm.Type,
IsOnline = vm.IsOnline, IsOnline = vm.IsOnline,
CreatedAt = DateTime.UtcNow
}; };
_context.Servers.Add(server); _context.Servers.Add(server);
@@ -53,4 +50,57 @@ public class ServerController : Controller
return RedirectToAction("Overview"); return RedirectToAction("Overview");
} }
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id)
{
var server = await _context.Servers.FindAsync(id);
if (server == null)
{
return NotFound();
}
_context.Servers.Remove(server);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Overview));
}
// GET: Server/Edit/5
public async Task<IActionResult> EditServer(int id)
{
var server = await _context.Servers.FindAsync(id);
if (server == null) return NotFound();
var vm = new EditServerViewModel
{
Name = server.Name,
IPAddress = server.IPAddress,
Type = server.Type
};
return View(vm);
}
// POST: Server/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditServer(int id, EditServerViewModel vm)
{
if (ModelState.IsValid)
{
var server = await _context.Servers.FindAsync(id);
if (server == null) return NotFound();
server.Name = vm.Name;
server.IPAddress = vm.IPAddress;
server.Type = vm.Type;
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Overview));
}
return View(vm);
}
} }

View File

@@ -0,0 +1,288 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Watcher.Data;
#nullable disable
namespace Watcher.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20250615114821_ServerCleanUp")]
partial class ServerCleanUp
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.6")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("Watcher.Models.Container", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Hostname")
.IsRequired()
.HasColumnType("longtext");
b.Property<int?>("ImageId")
.HasColumnType("int");
b.Property<bool>("IsRunning")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("longtext");
b.Property<int?>("TagId")
.HasColumnType("int");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("ImageId");
b.HasIndex("TagId");
b.ToTable("Containers");
});
modelBuilder.Entity("Watcher.Models.Image", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Name")
.HasColumnType("longtext");
b.Property<string>("Tag")
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("Images");
});
modelBuilder.Entity("Watcher.Models.LogEvent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int?>("ContainerId")
.HasColumnType("int");
b.Property<string>("Level")
.HasColumnType("longtext");
b.Property<string>("Message")
.HasColumnType("longtext");
b.Property<int?>("ServerId")
.HasColumnType("int");
b.Property<DateTime>("Timestamp")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.HasIndex("ContainerId");
b.HasIndex("ServerId");
b.ToTable("LogEvents");
});
modelBuilder.Entity("Watcher.Models.Metric", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int?>("ContainerId")
.HasColumnType("int");
b.Property<int?>("ServerId")
.HasColumnType("int");
b.Property<DateTime>("Timestamp")
.HasColumnType("datetime(6)");
b.Property<string>("Type")
.HasColumnType("longtext");
b.Property<double>("Value")
.HasColumnType("double");
b.HasKey("Id");
b.HasIndex("ContainerId");
b.HasIndex("ServerId");
b.ToTable("Metrics");
});
modelBuilder.Entity("Watcher.Models.Server", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.HasColumnType("longtext");
b.Property<string>("IPAddress")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsOnline")
.HasColumnType("tinyint(1)");
b.Property<DateTime>("LastSeen")
.HasColumnType("datetime(6)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int?>("TagId")
.HasColumnType("int");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("TagId");
b.ToTable("Servers");
});
modelBuilder.Entity("Watcher.Models.Tag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Name")
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("Tags");
});
modelBuilder.Entity("Watcher.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Email")
.HasColumnType("longtext");
b.Property<DateTime>("LastLogin")
.HasColumnType("datetime(6)");
b.Property<string>("PocketId")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("PreferredUsername")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("Watcher.Models.Container", b =>
{
b.HasOne("Watcher.Models.Image", "Image")
.WithMany("Containers")
.HasForeignKey("ImageId");
b.HasOne("Watcher.Models.Tag", null)
.WithMany("Containers")
.HasForeignKey("TagId");
b.Navigation("Image");
});
modelBuilder.Entity("Watcher.Models.LogEvent", b =>
{
b.HasOne("Watcher.Models.Container", "Container")
.WithMany()
.HasForeignKey("ContainerId");
b.HasOne("Watcher.Models.Server", "Server")
.WithMany()
.HasForeignKey("ServerId");
b.Navigation("Container");
b.Navigation("Server");
});
modelBuilder.Entity("Watcher.Models.Metric", b =>
{
b.HasOne("Watcher.Models.Container", "Container")
.WithMany()
.HasForeignKey("ContainerId");
b.HasOne("Watcher.Models.Server", "Server")
.WithMany()
.HasForeignKey("ServerId");
b.Navigation("Container");
b.Navigation("Server");
});
modelBuilder.Entity("Watcher.Models.Server", b =>
{
b.HasOne("Watcher.Models.Tag", null)
.WithMany("Servers")
.HasForeignKey("TagId");
});
modelBuilder.Entity("Watcher.Models.Image", b =>
{
b.Navigation("Containers");
});
modelBuilder.Entity("Watcher.Models.Tag", b =>
{
b.Navigation("Containers");
b.Navigation("Servers");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,51 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Watcher.Migrations
{
/// <inheritdoc />
public partial class ServerCleanUp : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Hostname",
table: "Servers");
migrationBuilder.DropColumn(
name: "Status",
table: "Servers");
migrationBuilder.AddColumn<string>(
name: "Description",
table: "Servers",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Description",
table: "Servers");
migrationBuilder.AddColumn<string>(
name: "Hostname",
table: "Servers",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<string>(
name: "Status",
table: "Servers",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
}
}
}

View File

@@ -148,8 +148,7 @@ namespace Watcher.Migrations
b.Property<DateTime>("CreatedAt") b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)"); .HasColumnType("datetime(6)");
b.Property<string>("Hostname") b.Property<string>("Description")
.IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<string>("IPAddress") b.Property<string>("IPAddress")
@@ -166,10 +165,6 @@ namespace Watcher.Migrations
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("longtext");
b.Property<int?>("TagId") b.Property<int?>("TagId")
.HasColumnType("int"); .HasColumnType("int");

View File

@@ -8,16 +8,16 @@ public class Server
public string IPAddress { get; set; } = string.Empty; public string IPAddress { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public string Hostname { get; set; } = string.Empty;
// z.B. "VPS", "standalone", "VM", etc. // z.B. "VPS", "standalone", "VM", etc.
public string Type { get; set; } = "VPS"; public string Type { get; set; } = "VPS";
public Boolean IsOnline { get; set; } = false; public Boolean IsOnline { get; set; } = false;
public DateTime LastSeen { get; set; } public DateTime LastSeen { get; set; }
public string? Description { get; set; }
} }

View File

@@ -0,0 +1,10 @@
namespace Watcher.ViewModels;
public class EditServerViewModel
{
public string Name { get; set; } = string.Empty;
public string IPAddress { get; set; } = string.Empty;
public string Type { get; set; } = "VPS";
}

View File

@@ -1,42 +1,42 @@
@using Watcher.ViewModels @model Watcher.ViewModels.AddServerViewModel
@model AddServerViewModel
@{ @{
ViewData["Title"] = "Neuen Server hinzufügen"; ViewData["Title"] = "Neuen Server hinzufügen";
} }
<h1 class="text-2xl font-bold mb-4">Neuen Server hinzufügen</h1> <div class="max-w-2xl mx-auto mt-10 bg-white rounded-2xl shadow-md p-8">
<h1 class="text-2xl font-bold text-gray-800 mb-6">Neuen Server hinzufügen</h1>
<form asp-action="Add" method="post" class="space-y-4 max-w-xl"> <form asp-action="AddServer" method="post" class="space-y-5">
<div> <div>
<label asp-for="Name" class="block font-medium">Name</label> <label asp-for="Name" class="block text-sm font-medium text-gray-700">Name</label>
<input asp-for="Name" class="w-full border rounded px-3 py-2" /> <input asp-for="Name" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500" />
<span asp-validation-for="Name" class="text-red-600 text-sm"></span> <span asp-validation-for="Name" class="text-red-500 text-sm" />
</div> </div>
<div> <div>
<label asp-for="Hostname" class="block font-medium">Hostname</label> <label asp-for="IPAddress" class="block text-sm font-medium text-gray-700">IP-Adresse</label>
<input asp-for="Hostname" class="w-full border rounded px-3 py-2" /> <input asp-for="IPAddress" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500" />
<span asp-validation-for="IPAddress" class="text-red-500 text-sm" />
</div> </div>
<div> <div>
<label asp-for="IPAddress" class="block font-medium">IP-Adresse</label> <label asp-for="Type" class="block text-sm font-medium text-gray-700">Typ</label>
<input asp-for="IPAddress" class="w-full border rounded px-3 py-2" /> <select asp-for="Type" class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label asp-for="Status" class="block font-medium">Status</label>
<input asp-for="Status" class="w-full border rounded px-3 py-2" />
</div>
<div>
<label asp-for="Type" class="block font-medium">Typ</label>
<select asp-for="Type" class="w-full border rounded px-3 py-2">
<option>VPS</option> <option>VPS</option>
<option>VM</option> <option>VM</option>
<option>Container</option> <option>Standalone</option>
<option>Physisch</option>
</select> </select>
</div> </div>
<div class="flex items-center">
<input asp-for="IsOnline" class="mr-2" type="checkbox" /> <div class="flex justify-end">
<label asp-for="IsOnline">Online?</label> <a asp-action="Overview" class="mr-3 inline-block px-4 py-2 text-gray-600 hover:text-blue-600">Abbrechen</a>
<button type="submit" class="px-5 py-2 bg-blue-600 text-white font-semibold rounded hover:bg-blue-700">
Speichern
</button>
</div> </div>
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">Speichern</button> </form>
</form> </div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@@ -0,0 +1,31 @@
@model Watcher.ViewModels.EditServerViewModel;
@{
ViewData["Title"] = "Server bearbeiten";
}
<h2>Server bearbeiten</h2>
<form asp-action="EditServer" method="post">
@Html.AntiForgeryToken()
<div class="mb-3">
<label asp-for="Name"></label>
<input asp-for="Name" class="form-control" />
</div>
<div class="mb-3">
<label asp-for="IPAddress"></label>
<input asp-for="IPAddress" class="form-control" />
</div>
</div>
<div class="mb-3">
<label asp-for="Type"></label>
<input asp-for="Type" class="form-control" />
</div>
<button type="submit" class="btn btn-primary mt-3">Speichern</button>
<a asp-action="Overview" class="btn btn-secondary mt-3">Abbrechen</a>
</form>

View File

@@ -5,7 +5,8 @@
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold">Serverübersicht</h1> <h1 class="text-2xl font-bold">Serverübersicht</h1>
<a asp-controller="Server" asp-action="AddServer" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"> <a asp-controller="Server" asp-action="AddServer"
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
+ Server hinzufügen + Server hinzufügen
</a> </a>
</div> </div>
@@ -13,13 +14,12 @@
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach (var s in Model.Servers) @foreach (var s in Model.Servers)
{ {
<div class="bg-white rounded-xl shadow p-4 border border-gray-200"> <div class="bg-white rounded-xl shadow p-4 border border-gray-200">
<div class="flex justify-between items-start"> <div class="flex justify-between items-start">
<div> <div>
<h2 class="text-lg font-semibold">@s.Name</h2> <h2 class="text-lg font-semibold">@s.Name</h2>
<p class="text-sm text-gray-500">@s.Hostname</p>
</div> </div>
<span class="text-sm px-2 py-1 rounded <span class="text-sm px-2 py-1 rounded
@(s.IsOnline ? "bg-green-100 text-green-700" : "bg-red-100 text-red-700")"> @(s.IsOnline ? "bg-green-100 text-green-700" : "bg-red-100 text-red-700")">
@@ -28,16 +28,21 @@
</div> </div>
<div class="mt-4 text-sm space-y-1 text-gray-700"> <div class="mt-4 text-sm space-y-1 text-gray-700">
<div><strong>IP:</strong> Ip</div> <div><strong>IP:</strong> @s.IPAddress</div>
<div><strong>Typ:</strong> @s.Type</div> <div><strong>Typ:</strong> @s.Type</div>
<div><strong>Status:</strong> @(string.IsNullOrEmpty(s.Status) ? "-" : s.Status)</div> <div><strong>Status:</strong> @((s.IsOnline) ? "Online" : "Offline")</div>
<div><strong>Erstellt:</strong> @s.CreatedAt.ToLocalTime().ToString("dd.MM.yyyy HH:mm")</div> <div><strong>Erstellt:</strong> @s.CreatedAt.ToLocalTime().ToString("dd.MM.yyyy HH:mm")</div>
<div><strong>Last-Seen:</strong> @(s.LastSeen.ToLocalTime().ToString("dd.MM.yyyy HH:mm") ?? "Never")</div>
</div> </div>
<div class="mt-4 flex justify-end gap-2"> <div class="mt-4 flex justify-end gap-2">
<a asp-action="Edit" asp-route-id="@s.Id" class="text-blue-600 hover:underline text-sm">Bearbeiten</a> <a asp-action="EditServer" asp-route-id="@s.Id" class="btn btn-sm btn-primary">Bearbeiten</a>
<a asp-action="Delete" asp-route-id="@s.Id" class="text-red-600 hover:underline text-sm">Löschen</a>
<form asp-action="Delete" asp-route-id="@s.Id" method="post"
onsubmit="return confirm('Diesen Server wirklich löschen?');">
<button type="submit" class="text-red-600 hover:text-red-800 font-semibold">Löschen</button>
</form>
</div> </div>
</div> </div>
} }
</div> </div>

View File

@@ -103,6 +103,9 @@
</main> </main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
@RenderSection("Scripts", required: false)
</body> </body>
</html> </html>