style(web): filled buttons, bolder icons, table/row alignment fixes, brand icon
- Two filled button kinds, no neutral/outline third state: every action is either "normal" (#4382c2) or destructive (#cb4b4c); border is the same neutral gray as every table/card border, not the fill colour. - Icons: bolder stroke site-wide (2 -> 2.5) and +1px, except inside buttons (2.25, no size bump) where a bold icon next to now-unbolded button text was too heavy. - tbody row hover: flat gray -> a transparent wash of the link/accent blue. - The Actions <td> in Services/Templates was itself a flex container; some engines size that box's content slightly differently from a plain-text sibling cell, which showed up as the row's collapsed bottom border sitting 1px higher under Actions than its neighbours. The flex row now lives on an inner <div class="actions-row"> instead, with its <form> children pinned to content width (a bare <form> is display:flex by the site's own rule, which was letting one form claim the whole row and push the next to wrap). - Navbar brand gets an icon (a server rack -- this dashboard orchestrates hosts/VMs, not generic settings).
This commit is contained in:
@@ -91,10 +91,16 @@ dd code, dd .mono { overflow-wrap: anywhere; }
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
/* Gear: automation/pipeline, fits an orchestrator's brand mark better than a
|
||||
* subject-specific glyph (server/box/etc. already belong to their own card). */
|
||||
.navbar-brand .icon { width: 1.3em; height: 1.3em; }
|
||||
.navbar-brand:hover { color: var(--accent-hover); text-decoration: none; }
|
||||
|
||||
.navbar-links {
|
||||
@@ -120,63 +126,16 @@ dd code, dd .mono { overflow-wrap: anywhere; }
|
||||
#action-slot:empty { display: none; }
|
||||
#action-slot:not(:empty) { margin-bottom: 1.25rem; }
|
||||
|
||||
/* ── status header (A3) ──────────────────────────────────────────────── */
|
||||
/* ── resource gauges inside cards (Stage F) ──────────────────────────── */
|
||||
|
||||
.status-header {
|
||||
display: grid;
|
||||
/* Six tiles (VMs, Runners, CPU, RAM, Disk, Cache) -- a smaller min track
|
||||
* lets all six sit on one row on a wide screen, wrapping gracefully below. */
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
/* The Host card's CPU / RAM / disk readouts. These lived in the retired
|
||||
* status-header tiles; the `tile-*` level class names survive as what the
|
||||
* gauge fill colour keys off. Stage G dropped the level word + check-icon
|
||||
* strip that used to follow each gauge: the state is stated as a number
|
||||
* ("42%", "20.0 / 62 GB") right above the bar, and the level word moved into
|
||||
* the gauge's aria-label -- so no reading of this card depends on colour. */
|
||||
|
||||
.stat-tile {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 3px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.9rem 1rem;
|
||||
}
|
||||
|
||||
.stat-tile > .icon { flex-shrink: 0; margin-top: 0.15rem; color: var(--text-dim); }
|
||||
|
||||
.stat-tile-body { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; flex: 1; }
|
||||
|
||||
.stat-tile-label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.stat-tile-value { font-size: 1.15rem; font-weight: 600; color: var(--text); }
|
||||
.stat-tile-sub { font-size: 0.8rem; font-weight: 400; color: var(--warn); }
|
||||
|
||||
.stat-tile-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
/* Health must never rely on color alone -- the border accent + icon +
|
||||
* label all move together, so a color-blind viewer still reads the state
|
||||
* from the text/icon. */
|
||||
.tile-ok { border-left-color: var(--success); }
|
||||
.tile-ok .stat-tile-status { color: var(--success); }
|
||||
.tile-warn { border-left-color: var(--warn); }
|
||||
.tile-warn .stat-tile-status { color: var(--warn); }
|
||||
.tile-bad { border-left-color: var(--danger); }
|
||||
.tile-bad .stat-tile-status { color: var(--danger); }
|
||||
.tile-unknown { border-left-color: var(--border); }
|
||||
.tile-unknown .stat-tile-status { color: var(--text-dim); }
|
||||
.resource-value { display: block; font-variant-numeric: tabular-nums; }
|
||||
|
||||
.gauge {
|
||||
width: 100%;
|
||||
@@ -184,24 +143,30 @@ dd code, dd .mono { overflow-wrap: anywhere; }
|
||||
background: var(--panel-2);
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
margin: 0.15rem 0;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
.gauge-fill { height: 100%; background: var(--success); }
|
||||
.tile-warn .gauge-fill { background: var(--warn); }
|
||||
.tile-bad .gauge-fill { background: var(--danger); }
|
||||
.tile-bad .gauge-fill { background: var(--danger); }
|
||||
|
||||
/* Sub-line under a gauge (Host: the load average under CPU), same role as the
|
||||
* dim "GB free" beside the disk percentage. */
|
||||
.resource-sub {
|
||||
display: block;
|
||||
font-size: 0.78rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
/* FIXED four-column track, NOT auto-fit. A `.card--wide` spans `1 / -1`,
|
||||
* i.e. occupies every column on its row -- with `auto-fit` that means no
|
||||
* track is ever globally empty, so auto-fit can't collapse the compact
|
||||
* row's spare tracks and the four info cards stayed narrow with dead
|
||||
* columns on the right. A fixed count makes the four compact cards fill
|
||||
* their row exactly, and the wide tables still span all four via
|
||||
* `grid-column: 1 / -1`. minmax(0,1fr) lets a card shrink below its
|
||||
* content width so a wide table scrolls inside its own .table-wrap
|
||||
* instead of forcing the page to scroll. Column count steps down with
|
||||
* width (below). */
|
||||
* track is ever globally empty, so auto-fit could not collapse a compact
|
||||
* row's spare tracks. Since Stage F every card is `.card--wide` (the four
|
||||
* compact info cards became the one Platform card), so the track count only
|
||||
* matters if a compact card is ever added back. minmax(0,1fr) lets a card
|
||||
* shrink below its content width so a wide table scrolls inside its own
|
||||
* .table-wrap instead of forcing the page to scroll. */
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 1.25rem;
|
||||
/* stretch: every card in a row shares the row's height, so the grid reads
|
||||
@@ -285,6 +250,19 @@ dd code, dd .mono { overflow-wrap: anywhere; }
|
||||
* the bar's right end. Compact and case-normal so it reads as a control rather
|
||||
* than part of the uppercase heading text, and short enough not to grow the
|
||||
* bar's height. */
|
||||
/* Right-hand end of a card header bar: a single near-static fact that would
|
||||
* otherwise cost a whole <dl> row (Host: uptime). `text-transform` must be
|
||||
* reset -- the h2 uppercases, and "26H 03M" is not a duration anyone reads. */
|
||||
.head-meta {
|
||||
margin-left: auto;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 400;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.head-select {
|
||||
margin-left: auto;
|
||||
padding: 0.1rem 0.35rem;
|
||||
@@ -295,7 +273,10 @@ dd code, dd .mono { overflow-wrap: anywhere; }
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Per-template expandable backup list (Templates panel). */
|
||||
/* Per-template expandable backup list (Templates panel). Each row is its own
|
||||
* bordered line (not just a wrapping flex list run into the next) so a
|
||||
* multi-backup template reads as a small table, not a paragraph the delete
|
||||
* button happened to float in. */
|
||||
.backups-details > summary {
|
||||
cursor: pointer;
|
||||
color: var(--link);
|
||||
@@ -306,14 +287,19 @@ dd code, dd .mono { overflow-wrap: anywhere; }
|
||||
.backups-list { list-style: none; margin: 0; padding: 0; }
|
||||
.backups-list li {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
align-items: baseline;
|
||||
padding: 0.2rem 0;
|
||||
font-size: 0.82rem;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.45rem 0;
|
||||
font-size: 0.85rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.backups-list li:first-child { border-top: none; }
|
||||
/* Name + size/date as one shrinkable unit, so the row wraps as a block
|
||||
* instead of the delete button drifting away from a name that wrapped. */
|
||||
.backup-info { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.4rem; min-width: 0; flex: 1 1 auto; }
|
||||
.backup-info code { overflow-wrap: anywhere; }
|
||||
/* Per-backup delete button pinned to the right end of its row. */
|
||||
.backup-del { margin-left: auto; align-self: center; }
|
||||
.backup-del { margin-left: auto; flex: none; }
|
||||
|
||||
.card h2 {
|
||||
display: flex;
|
||||
@@ -339,7 +325,9 @@ dd code, dd .mono { overflow-wrap: anywhere; }
|
||||
dl { display: grid; grid-template-columns: auto 1fr; gap: 0 0.9rem; }
|
||||
dt, dd { padding: 0.45rem 0; border-bottom: 1px solid var(--border); }
|
||||
dt { color: var(--text-dim); font-weight: 500; }
|
||||
dd { margin: 0; }
|
||||
/* Long unbreakable values (a cache root, a kernel string, a CPU model)
|
||||
* wrap inside the card rather than widening its grid track. */
|
||||
dd { margin: 0; overflow-wrap: anywhere; }
|
||||
|
||||
/* ── tables ───────────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -347,7 +335,13 @@ dd { margin: 0; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
|
||||
|
||||
th, td { text-align: left; padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); }
|
||||
/* vertical-align:middle is explicit, not left to the UA default (baseline) --
|
||||
* keeps every cell in a row anchored the same way regardless of its content
|
||||
* (a row of buttons vs. one line of text). The Actions-column border step
|
||||
* this was chasing turned out to be .actions-row's flex box sitting directly
|
||||
* on the <td> (see the comment there); this rule stays for alignment, not as
|
||||
* that fix. */
|
||||
th, td { text-align: left; padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); vertical-align: middle; }
|
||||
|
||||
th {
|
||||
color: var(--text-dim);
|
||||
@@ -358,7 +352,7 @@ th {
|
||||
}
|
||||
|
||||
tbody tr { transition: background-color 120ms ease; }
|
||||
tbody tr:hover { background: var(--panel-2); }
|
||||
tbody tr:hover { background: rgba(65, 131, 196, 0.12); }
|
||||
|
||||
tr.row-orphan td { color: var(--warn); }
|
||||
tr.row-alert td { color: var(--danger); }
|
||||
@@ -396,8 +390,37 @@ tr.row-alert td { color: var(--danger); }
|
||||
.badge-info { background: rgba(65, 131, 196, 0.15); color: var(--accent); border: 1px solid rgba(65, 131, 196, 0.4); }
|
||||
.badge-muted { background: var(--panel-2); color: var(--text-dim); border: 1px solid var(--border); }
|
||||
|
||||
/* A badge sitting right before a dim sub-state ("loaded" + "kernel module",
|
||||
* "active" + "running") only had the markup's one collapsed whitespace
|
||||
* character between them -- reads as glued together. */
|
||||
.badge + .text-dim { margin-left: 0.4rem; }
|
||||
|
||||
/* VMware/Runners (Stage H): "loaded"/"not loaded", "active"/"down" are
|
||||
* different lengths, so the pill in each <dd> row was a different width from
|
||||
* its neighbours. Levelled to fit the longest word in either card, same
|
||||
* technique as the Executions Status column. */
|
||||
#vmware-card .badge,
|
||||
#runner-card .badge {
|
||||
min-width: 5rem;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* Executions card: the Status column mixes run outcomes (success/failure/
|
||||
* queued/cancelled...) and VM-only states (orphan/running) of very different
|
||||
* word lengths -- a ragged column of differently-sized pills. Both row kinds
|
||||
* put their badge in the 8th <td> (Run, Repo, Workflow, Branch, Commit,
|
||||
* Trigger, Actor, Status, ...), so one rule levels every pill in the column
|
||||
* to "cancelled"'s width (the longest word) with room to spare, and
|
||||
* `justify-content` keeps icon+text hugging the left edge of the now-wider
|
||||
* pill rather than centering. */
|
||||
#executions-card td:nth-child(8) { width: 5rem; }
|
||||
#executions-card td:nth-child(8) .badge { width: 100%; justify-content: flex-start; }
|
||||
|
||||
/* ── buttons ──────────────────────────────────────────────────────────── */
|
||||
|
||||
/* Two filled button kinds, no neutral/outline third state: every action is
|
||||
* either "normal" (blue) or destructive (red) -- the confirm gate is still
|
||||
* the real safeguard on a destructive action, this is just legibility. */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -406,26 +429,31 @@ tr.row-alert td { color: var(--danger); }
|
||||
border-radius: var(--radius);
|
||||
padding: 0.45rem 0.9rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
font-weight: 400;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
background: var(--panel-2);
|
||||
color: var(--text);
|
||||
background: #4382c2;
|
||||
color: #ffffff;
|
||||
/* Same neutral gray as every table/card border, not the fill colour --
|
||||
* a solid-colour border read as an outline duplicating the fill. */
|
||||
border-color: var(--border);
|
||||
transition: background-color 120ms ease, border-color 120ms ease, transform 80ms ease;
|
||||
}
|
||||
.btn:hover { background: var(--border); }
|
||||
/* Hover tint: +25 per channel, same lighten-by magnitude as --accent-hover.
|
||||
* Border stays the neutral gray -- only the fill shifts on hover. */
|
||||
.btn:hover { background: #5c9bdb; }
|
||||
.btn:active { transform: translateY(1px); }
|
||||
|
||||
.btn-primary { background: var(--accent); color: #ffffff; border-color: var(--accent); }
|
||||
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
||||
/* A plain-weight button label leaves its icon looking thin/washed-out next
|
||||
* to a solid fill -- bumped stroke-width and a couple px of extra size make
|
||||
* it read as clearly as the bold text used to. */
|
||||
.btn .icon { stroke-width: 2.25; width: 1.15em; height: 1.15em; }
|
||||
|
||||
/* Destructive actions: a NEUTRAL button at rest (same panel surface + border
|
||||
* as .btn) that only reads as red through its text/icon -- no red border or
|
||||
* fill glaring on the page. Danger becomes fully visible on hover (red border
|
||||
* + soft red wash). The real safeguard is the two-step confirm gate, not the
|
||||
* colour. */
|
||||
.btn-danger { background: var(--panel-2); color: var(--danger); border-color: var(--border); }
|
||||
.btn-danger:hover { background: rgba(248, 81, 73, 0.14); color: var(--danger); border-color: var(--danger); }
|
||||
.btn-primary { background: #4382c2; color: #ffffff; border-color: var(--border); }
|
||||
.btn-primary:hover { background: #5c9bdb; }
|
||||
|
||||
.btn-danger { background: #cb4b4c; color: #ffffff; border-color: var(--border); }
|
||||
.btn-danger:hover { background: #e46465; }
|
||||
|
||||
/* Compact variant for buttons that live inside a table row (VM remove,
|
||||
* per-service start/stop/restart) so a row of them stays low-profile. */
|
||||
@@ -458,8 +486,17 @@ input[type="text"], input[type="number"], select {
|
||||
/* Multiple one-button action forms side-by-side in one table cell (Stage D
|
||||
* services panel: Start/Stop/Restart). Overrides the default form's
|
||||
* margin-top so the row stays compact inside a <td>. */
|
||||
.service-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
|
||||
.service-actions form { margin-top: 0; }
|
||||
/* Wraps the row of action buttons INSIDE the td rather than making the td
|
||||
* itself a flex box -- a table-cell that is also a flex container computes
|
||||
* its content-box height slightly differently from a plain-text sibling
|
||||
* cell in some engines, which showed up as the row's collapsed bottom
|
||||
* border sitting 1px higher under this column than its neighbours. */
|
||||
.actions-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
|
||||
/* Each <form> is itself display:flex (global form rule) -- as a flex item of
|
||||
* .actions-row that can size to fill the row instead of its own content,
|
||||
* pushing the next form onto a new line. flex:none pins it to content width
|
||||
* so multiple forms actually sit side by side. */
|
||||
.actions-row form { margin-top: 0; flex: none; }
|
||||
|
||||
/* Muted inline text (e.g. a relative-time suffix next to a file name). */
|
||||
.text-dim { color: var(--text-dim); }
|
||||
@@ -600,18 +637,26 @@ input[type="text"], input[type="number"], select {
|
||||
* references render. currentColor lets every icon pick up whatever text
|
||||
* color its container already has -- no per-icon color rules needed. */
|
||||
.icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
/* calc(...+1px) rather than a bigger em multiplier: a flat 1px bump reads
|
||||
* the same at every context size (a 0.75rem badge icon and a 15px body
|
||||
* icon both just gain one pixel), where a fixed em delta would swing by
|
||||
* context. .btn .icon overrides this with a plain em value -- no +1px --
|
||||
* so buttons are unaffected. */
|
||||
width: calc(1em + 1px);
|
||||
height: calc(1em + 1px);
|
||||
vertical-align: -0.15em;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
/* Bolder than the sprite's own stroke-width="2" attribute -- CSS wins over
|
||||
* the SVG presentation attribute. Button icons dial this back down, see
|
||||
* .btn .icon below. */
|
||||
stroke-width: 2.5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card h2 .icon,
|
||||
.stat-tile > .icon {
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
.card h2 .icon {
|
||||
width: calc(1.1em + 1px);
|
||||
height: calc(1.1em + 1px);
|
||||
}
|
||||
|
||||
/* ── accessibility ────────────────────────────────────────────────────── */
|
||||
@@ -637,5 +682,4 @@ input[type="text"], input[type="number"], select {
|
||||
.navbar-inner { flex-direction: column; align-items: flex-start; gap: 0.5rem; }
|
||||
form { flex-direction: column; align-items: flex-start; }
|
||||
dl { grid-template-columns: 1fr; }
|
||||
.status-header { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
{% set logged_in_user = request.session.get("user") %}
|
||||
<nav class="navbar">
|
||||
<div class="navbar-inner">
|
||||
<a class="navbar-brand" href="/">ci-orchestrator</a>
|
||||
<a class="navbar-brand" href="/"><svg class="icon" aria-hidden="true"><use href="#i-server"></use></svg>ci-orchestrator</a>
|
||||
<div class="navbar-links">
|
||||
<a href="/audit">Audit log</a>
|
||||
{% if gitea_url %}
|
||||
|
||||
@@ -25,26 +25,28 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ state.sub_state }}</td>
|
||||
<td class="service-actions">
|
||||
{% if not state.active %}
|
||||
<form method="post" action="/services/{{ state.service.id }}/start"
|
||||
hx-post="/services/{{ state.service.id }}/start" hx-target="#action-slot" hx-swap="innerHTML show:top">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn btn-primary btn-sm" aria-label="Start {{ state.service.label }}">Start</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if state.active and state.service.allow_stop %}
|
||||
<form method="post" action="/services/{{ state.service.id }}/stop"
|
||||
hx-post="/services/{{ state.service.id }}/stop" hx-target="#action-slot" hx-swap="innerHTML show:top">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn btn-danger btn-sm" aria-label="Stop {{ state.service.label }}">Stop</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="post" action="/services/{{ state.service.id }}/restart"
|
||||
hx-post="/services/{{ state.service.id }}/restart" hx-target="#action-slot" hx-swap="innerHTML show:top">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn btn-sm" aria-label="Restart {{ state.service.label }}">Restart</button>
|
||||
</form>
|
||||
<td>
|
||||
<div class="actions-row">
|
||||
{% if not state.active %}
|
||||
<form method="post" action="/services/{{ state.service.id }}/start"
|
||||
hx-post="/services/{{ state.service.id }}/start" hx-target="#action-slot" hx-swap="innerHTML show:top">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn btn-primary btn-sm" aria-label="Start {{ state.service.label }}">Start</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if state.active and state.service.allow_stop %}
|
||||
<form method="post" action="/services/{{ state.service.id }}/stop"
|
||||
hx-post="/services/{{ state.service.id }}/stop" hx-target="#action-slot" hx-swap="innerHTML show:top">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn btn-danger btn-sm" aria-label="Stop {{ state.service.label }}">Stop</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="post" action="/services/{{ state.service.id }}/restart"
|
||||
hx-post="/services/{{ state.service.id }}/restart" hx-target="#action-slot" hx-swap="innerHTML show:top">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn btn-sm" aria-label="Restart {{ state.service.label }}">Restart</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
Reference in New Issue
Block a user