feat: refine cloud console UI
This commit is contained in:
@@ -18,9 +18,9 @@ export function VMCreate({ token, projectID, tenantID, onCreated }: Props) {
|
|||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [templateID, setTemplateID] = useState("");
|
const [templateID, setTemplateID] = useState("");
|
||||||
const [node, setNode] = useState("");
|
const [node, setNode] = useState("");
|
||||||
const [vcpu, setVCPU] = useState(2);
|
const [vcpu, setVCPU] = useState(4);
|
||||||
const [ramMB, setRAMMB] = useState(2048);
|
const [ramMB, setRAMMB] = useState(6144);
|
||||||
const [diskGB, setDiskGB] = useState(10);
|
const [diskGB, setDiskGB] = useState(30);
|
||||||
const [sshKeyID, setSSHKeyID] = useState("");
|
const [sshKeyID, setSSHKeyID] = useState("");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
@@ -92,14 +92,17 @@ export function VMCreate({ token, projectID, tenantID, onCreated }: Props) {
|
|||||||
|
|
||||||
if (!open) {
|
if (!open) {
|
||||||
return (
|
return (
|
||||||
<button className="primary-button" onClick={() => setOpen(true)}>
|
<div className="create-launch">
|
||||||
Neue VM erstellen
|
<span>Server aus einem Template bereitstellen</span>
|
||||||
</button>
|
<button className="primary-button" onClick={() => setOpen(true)}>
|
||||||
|
Server erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panel">
|
<div className="provision-box">
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">Provisioning</p>
|
<p className="eyebrow">Provisioning</p>
|
||||||
@@ -110,7 +113,7 @@ export function VMCreate({ token, projectID, tenantID, onCreated }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{error ? <p className="form-error">{error}</p> : null}
|
{error ? <p className="form-error">{error}</p> : null}
|
||||||
<div className="metric-list">
|
<div className="provision-grid">
|
||||||
<label>
|
<label>
|
||||||
Name
|
Name
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -48,59 +48,66 @@ export function VMList({ token, projectID }: Props) {
|
|||||||
return <p className="metric-text">Keine VMs in diesem Projekt.</p>;
|
return <p className="metric-text">Keine VMs in diesem Projekt.</p>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="vm-grid">
|
<div className="table-wrap">
|
||||||
{vms.map((vm) => (
|
<table className="resource-table">
|
||||||
<div className="vm-card" key={vm.id}>
|
<thead>
|
||||||
<div className="vm-card-header">
|
<tr>
|
||||||
<strong>{vm.name}</strong>
|
<th>Status</th>
|
||||||
<span className={`pill ${vm.status === "running" ? "ok" : ""}`}>
|
<th>Name</th>
|
||||||
{vm.status}
|
<th>Node</th>
|
||||||
</span>
|
<th>Ressourcen</th>
|
||||||
</div>
|
<th>VMID</th>
|
||||||
<div className="vm-card-body">
|
<th>Aktionen</th>
|
||||||
<div className="metric">
|
</tr>
|
||||||
<span>Node</span>
|
</thead>
|
||||||
<strong>{vm.node}</strong>
|
<tbody>
|
||||||
</div>
|
{vms.map((vm) => (
|
||||||
<div className="metric">
|
<tr key={vm.id}>
|
||||||
<span>vCPU / RAM / Disk</span>
|
<td>
|
||||||
<strong>
|
<span className={`pill ${vm.status === "running" ? "ok" : ""}`}>
|
||||||
{vm.vcpu} / {vm.ram_mb} MB / {vm.disk_gb} GB
|
{vm.status}
|
||||||
</strong>
|
</span>
|
||||||
</div>
|
</td>
|
||||||
<div className="metric">
|
<td>
|
||||||
<span>Proxmox VMID</span>
|
<strong className="resource-name">{vm.name}</strong>
|
||||||
<strong>{vm.proxmox_vmid}</strong>
|
</td>
|
||||||
</div>
|
<td>{vm.node}</td>
|
||||||
</div>
|
<td>
|
||||||
<div className="vm-card-actions">
|
{vm.vcpu} vCPU · {vm.ram_mb} MB · {vm.disk_gb} GB
|
||||||
<button
|
</td>
|
||||||
className="primary-button"
|
<td>{vm.proxmox_vmid}</td>
|
||||||
onClick={() => handlePower(vm.id, "start")}
|
<td>
|
||||||
>
|
<div className="resource-actions">
|
||||||
Start
|
<button
|
||||||
</button>
|
className="ghost-button compact"
|
||||||
<button
|
onClick={() => handlePower(vm.id, "start")}
|
||||||
className="ghost-button"
|
>
|
||||||
onClick={() => handlePower(vm.id, "stop")}
|
Start
|
||||||
>
|
</button>
|
||||||
Stop
|
<button
|
||||||
</button>
|
className="ghost-button compact"
|
||||||
<button
|
onClick={() => handlePower(vm.id, "stop")}
|
||||||
className="ghost-button"
|
>
|
||||||
onClick={() => handlePower(vm.id, "reboot")}
|
Stop
|
||||||
>
|
</button>
|
||||||
Reboot
|
<button
|
||||||
</button>
|
className="ghost-button compact"
|
||||||
<button
|
onClick={() => handlePower(vm.id, "reboot")}
|
||||||
className="ghost-button danger"
|
>
|
||||||
onClick={() => handleDelete(vm.id)}
|
Reboot
|
||||||
>
|
</button>
|
||||||
Löschen
|
<button
|
||||||
</button>
|
className="ghost-button compact danger"
|
||||||
</div>
|
onClick={() => handleDelete(vm.id)}
|
||||||
</div>
|
>
|
||||||
))}
|
Löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+47
-12
@@ -27,10 +27,10 @@ const supabase =
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const navItems: Array<{ key: ViewKey; label: string }> = [
|
const navItems: Array<{ key: ViewKey; label: string }> = [
|
||||||
{ key: "overview", label: "Uebersicht" },
|
{ key: "overview", label: "Projekt" },
|
||||||
{ key: "vms", label: "VMs" },
|
{ key: "vms", label: "Server" },
|
||||||
{ key: "ssh", label: "SSH-Keys" },
|
{ key: "ssh", label: "SSH-Keys" },
|
||||||
{ key: "audit", label: "Audit" },
|
{ key: "audit", label: "Audit Logs" },
|
||||||
{ key: "console", label: "Konsole" },
|
{ key: "console", label: "Konsole" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -121,6 +121,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const connected = Boolean(session && backendPrincipal);
|
const connected = Boolean(session && backendPrincipal);
|
||||||
|
const activeTitle = pageTitle(activeView);
|
||||||
|
|
||||||
if (loadingSession) {
|
if (loadingSession) {
|
||||||
return (
|
return (
|
||||||
@@ -152,8 +153,19 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<main className="app-frame">
|
<main className="app-frame">
|
||||||
<aside className="sidebar">
|
<aside className="sidebar">
|
||||||
<div className="brand-mark">P</div>
|
<div className="sidebar-brand">
|
||||||
|
<div className="brand-mark">P</div>
|
||||||
|
<div className="brand-copy">
|
||||||
|
<strong>ProxUI</strong>
|
||||||
|
<span>Cloud Console</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="project-chip">
|
||||||
|
<span>Projekt</span>
|
||||||
|
<strong>{selectedProject?.name ?? "Nicht gewählt"}</strong>
|
||||||
|
</div>
|
||||||
<nav className="main-nav" aria-label="Hauptnavigation">
|
<nav className="main-nav" aria-label="Hauptnavigation">
|
||||||
|
<span className="nav-section-label">Resources</span>
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<button
|
<button
|
||||||
className={
|
className={
|
||||||
@@ -173,7 +185,7 @@ function App() {
|
|||||||
<header className="topbar">
|
<header className="topbar">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">ProxUI</p>
|
<p className="eyebrow">ProxUI</p>
|
||||||
<h1>Proxmox Multi-Tenant Console</h1>
|
<h1>{activeTitle}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="session-box">
|
<div className="session-box">
|
||||||
<span className={connected ? "status-dot ok" : "status-dot"} />
|
<span className={connected ? "status-dot ok" : "status-dot"} />
|
||||||
@@ -209,7 +221,9 @@ function App() {
|
|||||||
activeView={activeView}
|
activeView={activeView}
|
||||||
token={session.access_token}
|
token={session.access_token}
|
||||||
tenantID={selectedTenant?.id ?? ""}
|
tenantID={selectedTenant?.id ?? ""}
|
||||||
|
tenantName={selectedTenant?.name ?? ""}
|
||||||
projectID={selectedProject?.id ?? ""}
|
projectID={selectedProject?.id ?? ""}
|
||||||
|
projectName={selectedProject?.name ?? ""}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
@@ -295,12 +309,16 @@ function ViewPanel({
|
|||||||
activeView,
|
activeView,
|
||||||
token,
|
token,
|
||||||
tenantID,
|
tenantID,
|
||||||
|
tenantName,
|
||||||
projectID,
|
projectID,
|
||||||
|
projectName,
|
||||||
}: {
|
}: {
|
||||||
activeView: ViewKey;
|
activeView: ViewKey;
|
||||||
token: string;
|
token: string;
|
||||||
tenantID: string;
|
tenantID: string;
|
||||||
|
tenantName: string;
|
||||||
projectID: string;
|
projectID: string;
|
||||||
|
projectName: string;
|
||||||
}) {
|
}) {
|
||||||
const [vmRefreshKey, setVMRefreshKey] = useState(0);
|
const [vmRefreshKey, setVMRefreshKey] = useState(0);
|
||||||
|
|
||||||
@@ -315,7 +333,12 @@ function ViewPanel({
|
|||||||
return (
|
return (
|
||||||
<section className="panel view-panel">
|
<section className="panel view-panel">
|
||||||
{activeView === "overview" && (
|
{activeView === "overview" && (
|
||||||
<Overview tenantID={tenantID} projectID={projectID} />
|
<Overview
|
||||||
|
tenantID={tenantID}
|
||||||
|
tenantName={tenantName}
|
||||||
|
projectID={projectID}
|
||||||
|
projectName={projectName}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{activeView === "vms" && (
|
{activeView === "vms" && (
|
||||||
<VMView
|
<VMView
|
||||||
@@ -339,27 +362,31 @@ function ViewPanel({
|
|||||||
|
|
||||||
function Overview({
|
function Overview({
|
||||||
tenantID,
|
tenantID,
|
||||||
|
tenantName,
|
||||||
projectID,
|
projectID,
|
||||||
|
projectName,
|
||||||
}: {
|
}: {
|
||||||
tenantID: string;
|
tenantID: string;
|
||||||
|
tenantName: string;
|
||||||
projectID: string;
|
projectID: string;
|
||||||
|
projectName: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">Status</p>
|
<p className="eyebrow">Projekt</p>
|
||||||
<h2>Arbeitsbereich</h2>
|
<h2>Arbeitsbereich</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="placeholder-table">
|
<div className="placeholder-table">
|
||||||
<div className="placeholder-row">
|
<div className="placeholder-row">
|
||||||
<span>Tenant</span>
|
<span>Mandant</span>
|
||||||
<strong>{tenantID}</strong>
|
<strong>{tenantName || tenantID}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="placeholder-row">
|
<div className="placeholder-row">
|
||||||
<span>Projekt</span>
|
<span>Projekt</span>
|
||||||
<strong>{projectID || "Kein Projekt ausgewählt"}</strong>
|
<strong>{projectName || projectID || "Kein Projekt ausgewählt"}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -385,8 +412,8 @@ function VMView({
|
|||||||
<>
|
<>
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">VMs</p>
|
<p className="eyebrow">Compute</p>
|
||||||
<h2>Virtuelle Maschinen</h2>
|
<h2>Server</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<VMCreate
|
<VMCreate
|
||||||
@@ -400,6 +427,14 @@ function VMView({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pageTitle(view: ViewKey) {
|
||||||
|
if (view === "vms") return "Server";
|
||||||
|
if (view === "ssh") return "SSH Keys";
|
||||||
|
if (view === "audit") return "Audit Logs";
|
||||||
|
if (view === "console") return "Konsole";
|
||||||
|
return "Projektübersicht";
|
||||||
|
}
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
|
|||||||
+395
-169
@@ -1,9 +1,11 @@
|
|||||||
:root {
|
:root {
|
||||||
color: #172026;
|
color: #e8edf0;
|
||||||
background: #f4f7fb;
|
background: #0b0d0e;
|
||||||
font-family:
|
font-family:
|
||||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||||
sans-serif;
|
sans-serif;
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -12,10 +14,17 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
background:
|
||||||
|
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px),
|
||||||
|
#0b0d0e;
|
||||||
|
background-size: 32px 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
input {
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,26 +43,27 @@ button {
|
|||||||
.loader {
|
.loader {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border: 3px solid #cfdae7;
|
border: 3px solid #263135;
|
||||||
border-top-color: #147d64;
|
border-top-color: #ff583d;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
animation: spin 900ms linear infinite;
|
animation: spin 900ms linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-panel {
|
.auth-panel {
|
||||||
width: min(420px, 100%);
|
width: min(420px, 100%);
|
||||||
border: 1px solid #d6dee8;
|
border: 1px solid #283236;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #ffffff;
|
background: #111517;
|
||||||
padding: 28px;
|
padding: 28px;
|
||||||
box-shadow: 0 18px 50px rgba(23, 32, 38, 0.08);
|
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.34);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-panel h1,
|
.auth-panel h1,
|
||||||
.topbar h1,
|
.topbar h1,
|
||||||
.panel h2 {
|
.panel h2,
|
||||||
|
.provision-box h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1.15;
|
line-height: 1.12;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,21 +77,43 @@ button {
|
|||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-form label {
|
.auth-form label,
|
||||||
display: grid;
|
.metric-list label,
|
||||||
gap: 8px;
|
.context-selects label {
|
||||||
color: #4d5c68;
|
color: #9aa6aa;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-form input {
|
.auth-form label,
|
||||||
width: 100%;
|
.metric-list label {
|
||||||
border: 1px solid #c7d2de;
|
display: grid;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form input,
|
||||||
|
.metric-list input,
|
||||||
|
.metric-list select,
|
||||||
|
.ssh-form input,
|
||||||
|
.ssh-form textarea,
|
||||||
|
.context-selects select {
|
||||||
|
min-height: 38px;
|
||||||
|
border: 1px solid #313c41;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: #172026;
|
color: #e8edf0;
|
||||||
background: #ffffff;
|
background: #0d1113;
|
||||||
padding: 11px 12px;
|
padding: 8px 10px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form input:focus,
|
||||||
|
.metric-list input:focus,
|
||||||
|
.metric-list select:focus,
|
||||||
|
.ssh-form input:focus,
|
||||||
|
.ssh-form textarea:focus,
|
||||||
|
.context-selects select:focus {
|
||||||
|
border-color: #ff583d;
|
||||||
|
box-shadow: 0 0 0 3px rgba(255, 88, 61, 0.16);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-actions,
|
.auth-actions,
|
||||||
@@ -101,13 +133,17 @@ button {
|
|||||||
min-height: 38px;
|
min-height: 38px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 0 14px;
|
padding: 0 14px;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-button {
|
.primary-button {
|
||||||
border: 1px solid #147d64;
|
border: 1px solid #ff583d;
|
||||||
color: #ffffff;
|
color: #140d0b;
|
||||||
background: #147d64;
|
background: #ff583d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-button:hover {
|
||||||
|
background: #ff6b53;
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-button:disabled {
|
.primary-button:disabled {
|
||||||
@@ -116,14 +152,28 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ghost-button {
|
.ghost-button {
|
||||||
border: 1px solid #c7d2de;
|
border: 1px solid #313c41;
|
||||||
color: #23313a;
|
color: #d5dcdf;
|
||||||
background: #ffffff;
|
background: #121719;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghost-button:hover {
|
||||||
|
border-color: #4a575c;
|
||||||
|
background: #171d20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghost-button.danger {
|
||||||
|
border-color: #793a31;
|
||||||
|
color: #ff8b78;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghost-button.danger:hover {
|
||||||
|
background: #221312;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-error {
|
.form-error {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #a13c21;
|
color: #ff8b78;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
@@ -131,82 +181,142 @@ button {
|
|||||||
.app-frame {
|
.app-frame {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 224px minmax(0, 1fr);
|
grid-template-columns: 236px minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
border-right: 1px solid #dbe3ec;
|
border-right: 1px solid #20282b;
|
||||||
background: #ffffff;
|
background: rgba(12, 15, 16, 0.94);
|
||||||
padding: 20px 14px;
|
padding: 22px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-mark {
|
.brand-mark {
|
||||||
width: 40px;
|
width: 42px;
|
||||||
height: 40px;
|
height: 42px;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
|
border: 1px solid #2e3b3f;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: #ffffff;
|
color: #ff583d;
|
||||||
background: #147d64;
|
background: #101416;
|
||||||
font-weight: 800;
|
font-weight: 900;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(255, 88, 61, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-copy strong {
|
||||||
|
color: #f3f6f7;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-copy span,
|
||||||
|
.project-chip span,
|
||||||
|
.nav-section-label {
|
||||||
|
color: #6f7b80;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-chip {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
border: 1px solid #242e32;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #0d1113;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-chip strong {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #e8edf0;
|
||||||
|
font-size: 13px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-nav {
|
.main-nav {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 5px;
|
||||||
margin-top: 28px;
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-section-label {
|
||||||
|
display: block;
|
||||||
|
padding: 0 10px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0;
|
border: 1px solid transparent;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: #40515d;
|
color: #9aa6aa;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 10px 12px;
|
padding: 11px 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
font-weight: 750;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
color: #e8edf0;
|
||||||
|
background: #121719;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item.active {
|
.nav-item.active {
|
||||||
color: #0f2c26;
|
border-color: #314047;
|
||||||
background: #e5f4ef;
|
color: #ffffff;
|
||||||
font-weight: 800;
|
background: #182024;
|
||||||
|
box-shadow: inset 3px 0 0 #ff583d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace {
|
.workspace {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 28px;
|
padding: 24px 32px 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar {
|
.topbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar h1 {
|
.topbar h1 {
|
||||||
font-size: 28px;
|
max-width: 780px;
|
||||||
|
font-size: 26px;
|
||||||
|
color: #f3f6f7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eyebrow {
|
.eyebrow {
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
color: #147d64;
|
color: #ff583d;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 800;
|
font-weight: 900;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-box {
|
.session-box {
|
||||||
max-width: 520px;
|
max-width: 520px;
|
||||||
min-width: 280px;
|
min-width: 292px;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
border: 1px solid #d6dee8;
|
border: 1px solid #283236;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #ffffff;
|
background: #111517;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,11 +333,12 @@ button {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
color: #f3f6f7;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-box span {
|
.session-box span {
|
||||||
color: #60717d;
|
color: #8e9aa0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,17 +347,21 @@ button {
|
|||||||
height: 10px;
|
height: 10px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: #d99b33;
|
background: #c9952e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot.ok {
|
.status-dot.ok {
|
||||||
background: #147d64;
|
background: #28c28b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-grid {
|
.context-bar,
|
||||||
display: grid;
|
.panel,
|
||||||
grid-template-columns: 1fr;
|
.provision-box,
|
||||||
gap: 18px;
|
.vm-card {
|
||||||
|
border: 1px solid #283236;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(17, 21, 23, 0.96);
|
||||||
|
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-bar {
|
.context-bar {
|
||||||
@@ -254,9 +369,6 @@ button {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
border: 1px solid #d6dee8;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #ffffff;
|
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
}
|
}
|
||||||
@@ -265,35 +377,210 @@ button {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-selects label {
|
.context-selects label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
color: #4d5c68;
|
}
|
||||||
|
|
||||||
|
.content-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-panel {
|
||||||
|
min-height: 168px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel h2,
|
||||||
|
.provision-box h2 {
|
||||||
|
color: #f3f6f7;
|
||||||
|
font-size: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-list,
|
||||||
|
.placeholder-table {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric,
|
||||||
|
.placeholder-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
border: 1px solid #222b2f;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #0d1113;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric span,
|
||||||
|
.placeholder-row span,
|
||||||
|
.metric-text {
|
||||||
|
color: #8e9aa0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric strong,
|
||||||
|
.placeholder-row strong {
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
color: #edf2f3;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill {
|
||||||
|
border: 1px solid #7d5a20;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #e5b559;
|
||||||
|
background: #1d170d;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill.ok {
|
||||||
|
border-color: #2a795f;
|
||||||
|
color: #52d7a7;
|
||||||
|
background: #0d201a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provision-box {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
margin: 14px 0 16px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-launch {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
border: 1px solid #242e32;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #0d1113;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-launch span {
|
||||||
|
color: #9aa6aa;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provision-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provision-grid label {
|
||||||
|
display: grid;
|
||||||
|
gap: 7px;
|
||||||
|
color: #9aa6aa;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-selects select {
|
.provision-grid input,
|
||||||
border: 1px solid #c7d2de;
|
.provision-grid select {
|
||||||
|
min-height: 38px;
|
||||||
|
border: 1px solid #313c41;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 7px 10px;
|
color: #e8edf0;
|
||||||
font: inherit;
|
background: #0d1113;
|
||||||
background: #ffffff;
|
padding: 8px 10px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provision-grid input:focus,
|
||||||
|
.provision-grid select:focus {
|
||||||
|
border-color: #ff583d;
|
||||||
|
box-shadow: 0 0 0 3px rgba(255, 88, 61, 0.16);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-grid {
|
.vm-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-card {
|
.table-wrap {
|
||||||
border: 1px solid #d6dee8;
|
overflow-x: auto;
|
||||||
|
border: 1px solid #242e32;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #ffffff;
|
background: #0d1113;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-table {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 820px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-table th,
|
||||||
|
.resource-table td {
|
||||||
|
border-bottom: 1px solid #20282b;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-table th {
|
||||||
|
color: #7b878c;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-table td {
|
||||||
|
color: #c9d2d6;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-table tbody tr:hover {
|
||||||
|
background: #111719;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-table tbody tr:last-child td {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-name {
|
||||||
|
color: #f3f6f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact {
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-card {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,12 +588,21 @@ button {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-card-header strong {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #f3f6f7;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-card-body {
|
.vm-card-body {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,103 +618,18 @@ button {
|
|||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ssh-form input,
|
|
||||||
.ssh-form textarea {
|
|
||||||
border: 1px solid #c7d2de;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 8px 10px;
|
|
||||||
font: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-text {
|
.metric-text {
|
||||||
color: #60717d;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-list label {
|
code {
|
||||||
display: grid;
|
display: block;
|
||||||
gap: 4px;
|
|
||||||
color: #4d5c68;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-list input {
|
|
||||||
border: 1px solid #c7d2de;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 8px 10px;
|
|
||||||
font: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ghost-button.danger {
|
|
||||||
border-color: #da8a7a;
|
|
||||||
color: #a13c21;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ghost-button.danger:hover {
|
|
||||||
background: #fdf3f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
min-width: 0;
|
|
||||||
border: 1px solid #d6dee8;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #ffffff;
|
|
||||||
padding: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-header {
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel h2 {
|
|
||||||
font-size: 21px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-list,
|
|
||||||
.placeholder-table {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric,
|
|
||||||
.placeholder-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
border: 1px solid #edf1f5;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric span,
|
|
||||||
.placeholder-row span {
|
|
||||||
color: #60717d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric strong,
|
|
||||||
.placeholder-row strong {
|
|
||||||
min-width: 0;
|
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
text-align: right;
|
border: 1px solid #283236;
|
||||||
}
|
border-radius: 6px;
|
||||||
|
color: #d7f7ec;
|
||||||
.pill {
|
background: #090c0d;
|
||||||
border: 1px solid #d7a44d;
|
padding: 10px;
|
||||||
border-radius: 999px;
|
|
||||||
color: #8a5c0b;
|
|
||||||
background: #fff8e8;
|
|
||||||
padding: 5px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill.ok {
|
|
||||||
border-color: #6fb79f;
|
|
||||||
color: #126b55;
|
|
||||||
background: #e8f6f1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
@@ -434,13 +645,18 @@ button {
|
|||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
border-bottom: 1px solid #dbe3ec;
|
border-bottom: 1px solid #20282b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-nav {
|
.main-nav {
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-chip,
|
||||||
|
.nav-section-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.topbar,
|
.topbar,
|
||||||
.content-grid {
|
.content-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -470,6 +686,16 @@ button {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.create-launch,
|
||||||
|
.context-bar {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provision-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.metric strong,
|
.metric strong,
|
||||||
.placeholder-row strong {
|
.placeholder-row strong {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|||||||
Reference in New Issue
Block a user