Refactor form structure and add collapsible components
Improved the user interface on the TargetsPage by replacing static form headers with collapsible sections, enhancing maintainability and user experience. Updated styles for consistency, added hover effects, and ensured accessibility. Also replaced German special characters for uniform encoding.
This commit is contained in:
@@ -51,7 +51,7 @@ export function TargetsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteTarget = async (id) => {
|
const deleteTarget = async (id) => {
|
||||||
if (!confirm("Target löschen?")) return;
|
if (!confirm("Target loeschen?")) return;
|
||||||
try {
|
try {
|
||||||
await apiFetch(`/targets/${id}`, { method: "DELETE" }, tokens, refresh);
|
await apiFetch(`/targets/${id}`, { method: "DELETE" }, tokens, refresh);
|
||||||
await load();
|
await load();
|
||||||
@@ -64,62 +64,76 @@ export function TargetsPage() {
|
|||||||
<div className="targets-page">
|
<div className="targets-page">
|
||||||
<h2>Targets Management</h2>
|
<h2>Targets Management</h2>
|
||||||
{error && <div className="card error">{error}</div>}
|
{error && <div className="card error">{error}</div>}
|
||||||
|
|
||||||
{canManage && (
|
{canManage && (
|
||||||
<form className="card target-form grid two" onSubmit={createTarget}>
|
<details className="card collapsible" open>
|
||||||
<div className="target-form-header">
|
<summary className="collapse-head">
|
||||||
<h3>Neues Target</h3>
|
<div>
|
||||||
<p>Verbindungsdaten fuer eine PostgreSQL-Instanz.</p>
|
<h3>Neues Target</h3>
|
||||||
</div>
|
<p>Verbindungsdaten fuer eine PostgreSQL-Instanz.</p>
|
||||||
<div className="field">
|
</div>
|
||||||
<label>Name</label>
|
<span className="collapse-chevron" aria-hidden="true">?</span>
|
||||||
<input placeholder="z.B. Prod-DB" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} required />
|
</summary>
|
||||||
<small>Eindeutiger Anzeigename im Dashboard.</small>
|
|
||||||
</div>
|
<form className="target-form grid two" onSubmit={createTarget}>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label>Host</label>
|
<label>Name</label>
|
||||||
<input placeholder="z.B. 172.16.0.106 oder db.internal" value={form.host} onChange={(e) => setForm({ ...form, host: e.target.value })} required />
|
<input placeholder="z.B. Prod-DB" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} required />
|
||||||
<small>Wichtig: Muss vom Backend-Container aus erreichbar sein.</small>
|
<small>Eindeutiger Anzeigename im Dashboard.</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label>Port</label>
|
<label>Host</label>
|
||||||
<input placeholder="5432" value={form.port} onChange={(e) => setForm({ ...form, port: Number(e.target.value) })} type="number" required />
|
<input placeholder="z.B. 172.16.0.106 oder db.internal" value={form.host} onChange={(e) => setForm({ ...form, host: e.target.value })} required />
|
||||||
<small>Standard PostgreSQL Port ist 5432 (oder gemappter Host-Port).</small>
|
<small>Wichtig: Muss vom Backend-Container aus erreichbar sein.</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label>DB Name</label>
|
<label>Port</label>
|
||||||
<input placeholder="z.B. postgres oder appdb" value={form.dbname} onChange={(e) => setForm({ ...form, dbname: e.target.value })} required />
|
<input placeholder="5432" value={form.port} onChange={(e) => setForm({ ...form, port: Number(e.target.value) })} type="number" required />
|
||||||
<small>Name der Datenbank, die überwacht werden soll.</small>
|
<small>Standard PostgreSQL Port ist 5432 (oder gemappter Host-Port).</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label>Username</label>
|
<label>DB Name</label>
|
||||||
<input placeholder="z.B. postgres" value={form.username} onChange={(e) => setForm({ ...form, username: e.target.value })} required />
|
<input placeholder="z.B. postgres oder appdb" value={form.dbname} onChange={(e) => setForm({ ...form, dbname: e.target.value })} required />
|
||||||
<small>DB User mit Leserechten auf Stats-Views.</small>
|
<small>Name der Datenbank, die ueberwacht werden soll.</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label>Password</label>
|
<label>Username</label>
|
||||||
<input placeholder="Passwort" type="password" value={form.password} onChange={(e) => setForm({ ...form, password: e.target.value })} required />
|
<input placeholder="z.B. postgres" value={form.username} onChange={(e) => setForm({ ...form, username: e.target.value })} required />
|
||||||
<small>Wird verschlüsselt in der Core-DB gespeichert.</small>
|
<small>DB User mit Leserechten auf Stats-Views.</small>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label>SSL Mode</label>
|
<label>Password</label>
|
||||||
<select value={form.sslmode} onChange={(e) => setForm({ ...form, sslmode: e.target.value })}>
|
<input placeholder="Passwort" type="password" value={form.password} onChange={(e) => setForm({ ...form, password: e.target.value })} required />
|
||||||
<option value="disable">disable</option>
|
<small>Wird verschluesselt in der Core-DB gespeichert.</small>
|
||||||
<option value="prefer">prefer</option>
|
</div>
|
||||||
<option value="require">require</option>
|
<div className="field">
|
||||||
</select>
|
<label>SSL Mode</label>
|
||||||
<small>
|
<select value={form.sslmode} onChange={(e) => setForm({ ...form, sslmode: e.target.value })}>
|
||||||
Bei Fehler "rejected SSL upgrade" auf <code>disable</code> stellen.
|
<option value="disable">disable</option>
|
||||||
</small>
|
<option value="prefer">prefer</option>
|
||||||
</div>
|
<option value="require">require</option>
|
||||||
<div className="field">
|
</select>
|
||||||
<label> </label>
|
<small>
|
||||||
<button className="primary-btn">Target anlegen</button>
|
Bei Fehler "rejected SSL upgrade" auf <code>disable</code> stellen.
|
||||||
</div>
|
</small>
|
||||||
</form>
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label> </label>
|
||||||
|
<button className="primary-btn">Target anlegen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{canManage && (
|
{canManage && (
|
||||||
<div className="card tips">
|
<details className="card collapsible tips">
|
||||||
<strong>Troubleshooting</strong>
|
<summary className="collapse-head">
|
||||||
|
<div>
|
||||||
|
<h3>Troubleshooting</h3>
|
||||||
|
<p>Typische Verbindungsfehler schnell erkennen.</p>
|
||||||
|
</div>
|
||||||
|
<span className="collapse-chevron" aria-hidden="true">?</span>
|
||||||
|
</summary>
|
||||||
<p>
|
<p>
|
||||||
<code>Connection refused</code>: Host/Port falsch oder DB nicht erreichbar.
|
<code>Connection refused</code>: Host/Port falsch oder DB nicht erreichbar.
|
||||||
</p>
|
</p>
|
||||||
@@ -129,8 +143,9 @@ export function TargetsPage() {
|
|||||||
<p>
|
<p>
|
||||||
<code>localhost</code> im Target zeigt aus Backend-Container-Sicht auf den Container selbst.
|
<code>localhost</code> im Target zeigt aus Backend-Container-Sicht auf den Container selbst.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</details>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="card targets-table">
|
<div className="card targets-table">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p>Lade Targets...</p>
|
<p>Lade Targets...</p>
|
||||||
|
|||||||
@@ -195,36 +195,59 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.target-form {
|
.target-form {
|
||||||
position: relative;
|
margin-top: 12px;
|
||||||
padding-top: 56px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.target-form-header {
|
.primary-btn {
|
||||||
position: absolute;
|
font-weight: 650;
|
||||||
top: 14px;
|
letter-spacing: 0.01em;
|
||||||
left: 16px;
|
border-color: #4467ab;
|
||||||
|
background: linear-gradient(180deg, #1a2f56, #142643);
|
||||||
|
box-shadow: inset 0 1px 0 #5f8de144;
|
||||||
}
|
}
|
||||||
|
|
||||||
.target-form-header h3 {
|
.primary-btn:hover {
|
||||||
|
border-color: #6b9ee9;
|
||||||
|
background: linear-gradient(180deg, #1d3766, #183053);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible {
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-head {
|
||||||
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: -2px 0 0 0;
|
||||||
|
padding: 0 2px 8px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-head::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-head h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.target-form-header p {
|
.collapse-head p {
|
||||||
margin: 2px 0 0 0;
|
margin: 2px 0 0 0;
|
||||||
color: #92a7cc;
|
color: #92a7cc;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-btn {
|
.collapse-chevron {
|
||||||
font-weight: 700;
|
font-size: 22px;
|
||||||
border-color: #3e73d4;
|
color: #89a7d8;
|
||||||
background: linear-gradient(90deg, #2e7cd4, #265fb4);
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-btn:hover {
|
details[open] .collapse-chevron {
|
||||||
border-color: #6aa8ff;
|
transform: rotate(180deg);
|
||||||
filter: brightness(1.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.targets-table table tbody tr:hover {
|
.targets-table table tbody tr:hover {
|
||||||
|
|||||||
Reference in New Issue
Block a user