chore(webui): redesign the states page

This commit is contained in:
Julien Dessaux 2025-04-05 23:59:56 +02:00
parent ff14a71b39
commit f12a54b760
Signed by: adyxax
GPG key ID: F92E51B86E07177E
2 changed files with 111 additions and 77 deletions

View file

@ -1,69 +1,46 @@
{{ define "main" }} {{ define "main" }}
<div> <h1>States</h1>
<div class="tabs"> <div class="flex-row" style="justify-content: space-between;">
<a data-ui="#explorer"{{ if eq .ActiveTab 0 }} class="active"{{ end }}>States</a> <div style="min-width: 240px;">
<a data-ui="#new"{{ if eq .ActiveTab 1 }} class="active"{{ end }}>Create New State</a> <p>TfStated is currently managing {{ len .States }} states.</p>
<p>Use this page to inspect the existing states.</p>
<p>You also have the option to upload a state file in order to create a new one. This is equivalent to using the <code>state push</code> command of OpenTofu/Terraform on a brand new state.</p>
</div> </div>
<div id="explorer" class="page padding{{ if eq .ActiveTab 0 }} active{{ end }}">
<table class="clickable-rows no-space">
<thead>
<tr>
<th>Path</th>
<th>Updated</th>
<th>Locked</th>
</tr>
</thead>
<tbody>
{{ range .States }}
<tr>
<td><a href="/states/{{ .Id }}">{{ .Path }}</a></td>
<td><a href="/states/{{ .Id }}">{{ .Updated }}</a></td>
<td>
<a href="/states/{{ .Id }}">
{{ if eq .Lock nil }}no{{ else }}
<span>yes</span>
<div class="tooltip left max">
<b>Lock</b>
<p>{{ .Lock }}</p>
</div>
{{ end }}
</a>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<div id="new" class="page padding{{ if eq .ActiveTab 1 }} active{{ end }}">
<form action="/states" enctype="multipart/form-data" method="post"> <form action="/states" enctype="multipart/form-data" method="post">
<fieldset> <fieldset>
<div class="field border label{{ if .PathError }} invalid{{ end }}"> <legend>New State</legend>
<div class="grid-2">
<label for="path">Path</label>
<input autofocus <input autofocus
class="flex-stretch"
id="path" id="path"
name="path" name="path"
required required
type="text" type="text"
value="{{ .Path }}"> value="{{ .Path }}">
<label for="path">Path</label>
{{ if .PathDuplicate }} {{ if .PathDuplicate }}
<span class="error">This path already exist</span> <span class="error">This path already exist</span>
{{ else if .PathError }} {{ else if .PathError }}
<span class="error">Invalid path</span> <span class="error">A valid URL path beginning with a / is expected.</span>
{{ else }}
<span class="helper">Valid URL path beginning with a /</span>
{{ end }} {{ end }}
</div> <label for="file" style="min-width: 120px">JSON state file</label>
<div class="field label border"> <input id="file"
<input name="file" name="file"
required required
type="file"> type="file">
<input type="text">
<label>File</label>
<span class="helper">JSON state file</span>
</div> </div>
<button class="small-round" type="submit" value="submit">New</button> <button class="primary" type="submit" value="submit">Upload and Create State</button>
</fieldset> </fieldset>
</form> </form>
</div> </div>
</div> <article aria-role="table" class="grid-3" style="margin-top: 16px; justify-items: stretch;">
<span>Path</span>
<span>Updated</span>
<span>Locked</span>
{{ range .States }}
<a href="/states/{{ .Id }}">{{ .Path }}</a>
<a href="/states/{{ .Id }}">{{ .Updated }}</a>
<a href="/states/{{ .Id }}" style="text-align: center;">{{ if eq .Lock nil }}no{{ else }}yes{{ end }}</a>
{{ end }}
</article>
{{ end }} {{ end }}

View file

@ -105,7 +105,7 @@ header {
#main { #main {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 8px; gap: 16px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
padding: 8px 0px; padding: 8px 0px;
@ -130,17 +130,22 @@ header {
text-align: center; text-align: center;
text-decoration: none; text-decoration: none;
} }
#main aside a.active { #main aside a:hover {
background-color: var(--bg-2); background-color: var(--br_orange);
color: var(--red);
} }
main { main {
background-color: var(--bg-1); background-color: var(--bg-1);
margin-right: auto; margin-right: auto;
padding: 8px;
overflow-wrap: anywhere; overflow-wrap: anywhere;
scrollbar-gutter: stable both-edges; scrollbar-gutter: stable both-edges;
max-width: 776px; max-width: 776px;
min-width: 776px; min-width: 776px;
} }
hr {
width: 50%;
}
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
font-family: open, serif; font-family: open, serif;
font-weight: 700; font-weight: 700;
@ -225,14 +230,62 @@ ul {
ul li { ul li {
margin-bottom: 4px; margin-bottom: 4px;
} }
.fullwidth { article, fieldset {
width: 100%; border: 1px solid var(--fg-0);
border-radius: 4px;
padding: 8px;
} }
img[src*='#center'] { button {
display: block; background-color: var(--bg-2);
margin: auto; border: 1px solid var(--orange);
color: var(--orange);
align-items: center;
border: 1px solid var(--fg-1);
border-radius: 8px;
display: inline-flex;
font-size: 16px;
gap: 8px;
margin: 4px 2px;
padding: 4px 4px;
text-align: center;
text-decoration: none;
}
button:hover {
background-color: var(--br_orange);
border: 1px solid var(--red);
color: var(--red);
}
.primary {
background-color: var(--orange);
color: var(--fg-1);
}
.flex-column {
display: flex;
flex-direction: column;
gap: 4px;
}
.flex-row {
align-items: start;
display: flex;
flex-direction: row;
gap: 16px;
}
.flex-stretch {
flex: 1;
}
.grid-2 {
align-items: center;
display: grid;
grid-template-columns: min-content 1fr;
}
.grid-3 {
align-items: center;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
.grid-3 span {
text-align: center;
} }
a { a {
color: var(--yellow); color: var(--yellow);
} }
@ -262,9 +315,9 @@ footer a {
color: var(--green); color: var(--green);
} }
@media only screen and (640px <= width < 960px) { /*864*/ @media only screen and (640px <= width < 968px) { /*856*/
header { header {
width: calc(100vw - 16px); width: calc(100vw - 32px); /* 8x2 padding + 16 gap */
} }
#main { #main {
/* These 16px account for the vertical scrollbar */ /* These 16px account for the vertical scrollbar */
@ -280,13 +333,16 @@ footer a {
display: none; display: none;
} }
main { main {
max-width: calc(100vw - 16px - 64px); max-width: calc(100vw - 32px - 64px);
min-width: calc(100vw - 16px - 64px); min-width: calc(100vw - 32px - 64px);
} }
} }
@media only screen and (width < 640px) { @media only screen and (width < 640px) {
header { header {
width: calc(100vw - 16px); width: calc(100vw - 16px);
}
#main aside hr {
width: auto;
} }
#main { #main {
display: block; display: block;
@ -298,6 +354,7 @@ footer a {
min-width: calc(100vw - 16px); min-width: calc(100vw - 16px);
} }
#main aside { #main aside {
background-color: var(--bg-0);
bottom: 0; bottom: 0;
flex-direction: row; flex-direction: row;
position: fixed; position: fixed;