diff --git a/html/ui/Cluster-34a012ee0910827c.js b/html/ui/Cluster-34a012ee0910827c.js new file mode 100644 index 0000000..e68982a --- /dev/null +++ b/html/ui/Cluster-34a012ee0910827c.js @@ -0,0 +1,27 @@ +const Cluster = { + components: { ClusterAccess, ClusterCAs, GetCopy }, + props: [ 'cluster', 'token', 'state' ], + template: ` +
+ Access + +
+ +
+ CAs + +
+ +
+ Secrets +

Tokens

+ +

Passwords

+ +
+` +} diff --git a/html/ui/Cluster-361bc6b76a062d88.js b/html/ui/Cluster-361bc6b76a062d88.js deleted file mode 100644 index 6d7cdf2..0000000 --- a/html/ui/Cluster-361bc6b76a062d88.js +++ /dev/null @@ -1,19 +0,0 @@ -const Cluster = { - components: { ClusterAccess, ClusterCAs, GetCopy }, - props: [ 'cluster', 'token', 'state' ], - template: ` - - - - -

Secrets

-

Tokens

- -

Passwords

- -` -} diff --git a/html/ui/ClusterAccess-e67af72c976d642c.js b/html/ui/ClusterAccess-8b4165435ba4fcac.js similarity index 73% rename from html/ui/ClusterAccess-e67af72c976d642c.js rename to html/ui/ClusterAccess-8b4165435ba4fcac.js index 926c3fe..f9c8421 100644 --- a/html/ui/ClusterAccess-e67af72c976d642c.js +++ b/html/ui/ClusterAccess-8b4165435ba4fcac.js @@ -17,6 +17,8 @@ const ClusterAccess = { kubeUserCert: null, downloadSet: null, selectedAssets: {}, + loading: false, + msg: null, }; }, computed: { @@ -44,40 +46,43 @@ const ClusterAccess = { hostCount() { return (this.state.Hosts||[]).filter(h => h.Cluster == this.cluster.Name).length }, - }, + }, methods: { sshCASign() { event.preventDefault(); + this.loading = true; this.msg = null; fetch(`/clusters/${this.cluster.Name}/ssh/user-ca/sign`, { method: 'POST', body: JSON.stringify({ ...this.sshSignReq, Validity: this.signReqValidity }), headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }, }).then((resp) => { if (resp.ok) { - resp.blob().then((cert) => { this.sshUserCert = URL.createObjectURL(cert) }) + resp.blob().then((cert) => { this.sshUserCert = URL.createObjectURL(cert); this.loading = false }) } else { - resp.json().then((resp) => alert('failed to sign: '+resp.message)) + resp.json().then((resp) => { this.msg = 'failed to sign: '+resp.message; this.loading = false }) } }) - .catch((e) => { alert('failed to sign: '+e); }) + .catch((e) => { this.msg = 'failed to sign: '+e; this.loading = false }) }, kubeCASign() { event.preventDefault(); + this.loading = true; this.msg = null; fetch(`/clusters/${this.cluster.Name}/kube/sign`, { method: 'POST', body: JSON.stringify({ ...this.kubeSignReq, Validity: this.signReqValidity }), headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }, }).then((resp) => { if (resp.ok) { - resp.blob().then((cert) => { this.kubeUserCert = URL.createObjectURL(cert) }) + resp.blob().then((cert) => { this.kubeUserCert = URL.createObjectURL(cert); this.loading = false }) } else { - resp.json().then((resp) => alert('failed to sign: '+resp.message)) + resp.json().then((resp) => { this.msg = 'failed to sign: '+resp.message; this.loading = false }) } }) - .catch((e) => { alert('failed to sign: '+e); }) + .catch((e) => { this.msg = 'failed to sign: '+e; this.loading = false }) }, generateDownloadSet() { event.preventDefault() + this.loading = true; this.msg = null; const items = [{ Kind: "host", @@ -91,18 +96,18 @@ const ClusterAccess = { headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }, }).then((resp) => { if (resp.ok) { - resp.json().then((set) => { this.downloadSet = set }) + resp.json().then((set) => { this.downloadSet = set; this.loading = false }) } else { - resp.json().then((resp) => alert('failed to generate: '+resp.message)) + resp.json().then((resp) => { this.msg = 'failed to generate: '+resp.message; this.loading = false }) } - }).catch((e) => { alert('failed to generate: '+e) }) + }).catch((e) => { this.msg = 'failed to generate: '+e; this.loading = false }) }, readFile(e, onload) { const file = e.target.files[0]; if (!file) { return; } const reader = new FileReader(); reader.onload = () => { onload(reader.result) }; - reader.onerror = () => { alert("error reading file"); }; + reader.onerror = () => { this.msg = "error reading file" }; reader.readAsText(file); }, loadPubKey(e) { @@ -110,6 +115,14 @@ const ClusterAccess = { this.sshSignReq.PubKey = v; }); }, + copyDownloadSetUrl() { + try { + navigator.clipboard.writeText(window.location.origin+'/public/download-set?set='+this.downloadSet) + this.$root.toast('copied!', 'info') + } catch (e) { + this.$root.toast('copy failed', 'error') + } + }, loadCSR(e) { this.readFile(e, (v) => { this.kubeSignReq.CSR = v; @@ -117,8 +130,6 @@ const ClusterAccess = { }, }, template: ` -

Access

-

Grant access to: @@ -127,6 +138,8 @@ const ClusterAccess = {

Validity: time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.

+

{{ msg }}

+

Grant SSH access

User:

@@ -134,7 +147,7 @@ const ClusterAccess = {

-

+

@@ -149,9 +162,9 @@ const ClusterAccess = {

-

+

@@ -166,12 +179,12 @@ const ClusterAccess = { {{" "}}

-

-

- Open download set page +

+
+ /public/download-set?set={{ downloadSet }}
- -

+ +
` } diff --git a/html/ui/ClusterCAs-d6eba07c367b6306.js b/html/ui/ClusterCAs-4f0a381bfb0c1b88.js similarity index 97% rename from html/ui/ClusterCAs-d6eba07c367b6306.js rename to html/ui/ClusterCAs-4f0a381bfb0c1b88.js index 75e520f..eb05771 100644 --- a/html/ui/ClusterCAs-d6eba07c367b6306.js +++ b/html/ui/ClusterCAs-4f0a381bfb0c1b88.js @@ -2,7 +2,6 @@ const ClusterCAs = { components: { GetCopy }, props: [ 'cluster', 'token' ], template: ` -

CAs

diff --git a/html/ui/Downloads-25b2e1f4aeaaff61.js b/html/ui/Downloads-3c8cba0572aebfae.js similarity index 89% rename from html/ui/Downloads-25b2e1f4aeaaff61.js rename to html/ui/Downloads-3c8cba0572aebfae.js index 09b94ce..558d58c 100644 --- a/html/ui/Downloads-25b2e1f4aeaaff61.js +++ b/html/ui/Downloads-3c8cba0572aebfae.js @@ -1,7 +1,7 @@ const Downloads = { props: [ 'kind', 'name', 'token', 'state' ], data() { - return { createDisabled: false, selectedAssets: {} } + return { createDisabled: false, selectedAssets: {}, msg: null } }, computed: { availableAssets() { @@ -50,11 +50,12 @@ const Downloads = { headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }, }).then((resp) => resp.json()) .then((token) => { this.selectedAssets = {}; this.createDisabled = false }) - .catch((e) => { alert('failed to create link'); this.createDisabled = false }) + .catch((e) => { this.msg = 'failed to create link'; this.createDisabled = false }) }, }, template: `

Available assets

+

{{ msg }}

diff --git a/html/ui/style.css b/html/ui/style.css index 664d9a1..1ebf588 100644 --- a/html/ui/style.css +++ b/html/ui/style.css @@ -1,35 +1,231 @@ :root { - --bg: #eee; - --color: black; - --bevel-dark: darkgray; - --bevel-light: lightgray; - --link: blue; - --input-bg: #ddd; - --input-text: white; - --btn-bg: #eee; -} - -@media (prefers-color-scheme: dark) { - :root { - --bg: black; - --color: orange; - --bevel-dark: #402900; - --bevel-light: #805300; + --bg: #000; + --color: #ddd; + --accent: #d4a017; + --dim: #555; --link: #31b0fa; - --input-bg: #111; - --input-text: #ddd; - --btn-bg: #222; - } + --input-bg: #1a1a1a; + --border: #333; + --hover: #222; } body { background: var(--bg); color: var(--color); + font-family: "Courier New", Courier, monospace; + font-size: 14px; + line-height: 1.5; + margin: 0; + padding: 0; } -button[disabled] { - opacity: 0.5; +/* Layout */ + +html, body { + height: 100%; + margin: 0; + padding: 0; } +#app { + display: grid; + height: 100vh; + grid-template: auto 1fr / 18em 1fr; + grid-template-areas: "h h" "s m"; +} +header { + grid-area: h; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5em 1em; + border-bottom: 1px solid var(--border); +} +#logo > img { + vertical-align: middle; + margin-right: 0.5em; +} +header .utils > * { + margin-left: 1em; +} + +.sidebar { + grid-area: s; + border-right: 1px solid var(--border); + padding: 0.5em; + overflow-y: auto; +} +.sidebar input[type="search"] { + width: 100%; + box-sizing: border-box; + margin-bottom: 0.5em; + background: var(--input-bg); + color: var(--color); + border: 1px solid var(--border); + padding: 0.3em 0.5em; + font-family: inherit; +} + +.content { + grid-area: m; + padding: 1em; + overflow-y: auto; +} + +/* Sidebar nav sections */ + +.nav-section { + margin: 0.5em 0 0.2em; + padding: 0.2em 0.5em; + color: var(--dim); + text-transform: uppercase; + letter-spacing: 0.1em; + font-size: 0.85em; + border-bottom: 1px solid var(--border); +} + +/* Sidebar nav items */ + +.view-links { + margin: 0; + padding: 0; +} +.sidebar:focus-within .view-links > button { + color: var(--color); +} +.view-links > button { + display: block; + width: 100%; + padding: 0.2em 0.5em; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--dim); + border: none; + border-bottom: 1px solid var(--border); + background: none; + font: inherit; + text-align: left; + border-radius: 0; +} +.view-links > button:last-child { + border-bottom: none; +} +.view-links > button:hover { + background: var(--hover); + color: var(--color); +} +.view-links > button.selected { + color: var(--accent) !important; + border-bottom-color: var(--accent); +} + +/* Headings */ + +h2 { + color: var(--accent); + border-bottom: 1px solid var(--border); + padding-bottom: 0.3em; + margin-top: 0; +} +h3 { + color: var(--accent); + margin: 1em 0 0.5em; +} +h3::before { + content: "── "; +} +h3::after { + content: " ──"; +} +h4 { + color: var(--color); + margin: 0.5em 0 0.3em; + font-weight: bold; +} + +/* Details / summary (collapsible sections) */ + +details { + margin: 0.5em 0; + border: 1px solid var(--border); +} +details[open] { + border-color: var(--accent); +} +summary { + cursor: pointer; + padding: 0.3em 0.5em; + background: var(--hover); + color: var(--accent); + user-select: none; +} +summary:hover { + background: #2a2a2a; +} +details[open] summary { + border-bottom: 1px solid var(--border); +} + +/* Tables */ + +table { + border-collapse: collapse; + width: 100%; +} +th, td { + border: 1px solid var(--border); + padding: 0.3em 0.5em; + text-align: left; +} +th { + color: var(--accent); + font-weight: bold; +} + +/* Form elements */ + +textarea, select, input { + background: var(--input-bg); + color: var(--color); + border: 1px solid var(--border); + padding: 0.3em 0.5em; + font-family: inherit; + font-size: inherit; + box-sizing: border-box; +} +textarea:focus, select:focus, input:focus { + outline: none; + border-color: var(--accent); +} + +button, input[type=button], input[type=submit], ::file-selector-button { + background: var(--input-bg); + color: var(--color); + border: 1px solid var(--border); + padding: 0.3em 0.7em; + cursor: pointer; + font-family: inherit; + font-size: inherit; +} +button:hover, input[type=button]:hover, input[type=submit]:hover { + border-color: var(--accent); + color: var(--accent); +} +button:active, input[type=button]:active, input[type=submit]:active { + background: var(--accent); + color: var(--bg); +} +button[disabled] { + opacity: 0.4; + cursor: default; +} +button[disabled]:hover { + border-color: var(--border); + color: var(--color); +} + +/* Links */ a[href], a[href]:visited, button.link { border: none; @@ -37,140 +233,153 @@ a[href], a[href]:visited, button.link { background: none; cursor: pointer; text-decoration: none; + font-family: inherit; + font-size: inherit; +} +a[href]:hover { + text-decoration: underline; } -table { - border-collapse: collapse; +/* Radio pills */ + +label.radio { + display: inline-block; + margin-right: 0.5em; + margin-bottom: 0.5em; + padding: 0.3em 0.7em; + border: 1px solid var(--border); + cursor: pointer; + user-select: none; } -th, td { - border-left: dotted 1pt; - border-right: dotted 1pt; - border-bottom: dotted 1pt; - padding: 2pt 4pt; +label.radio:has(input:checked) { + color: var(--accent); + border-color: var(--accent); } -tr:first-child { - th, td { - border-top: dotted 1pt; - } +label.radio input { + position: absolute; + opacity: 0; + width: 0; + height: 0; } -th, tr:last-child > td { - border-bottom: solid 1pt; +label.radio:focus-within { + color: var(--accent); + border-color: var(--accent); } +/* Asset checkboxes → [x] / [ ] style */ + +.downloads label { + display: inline-block; + margin-right: 0.5em; + margin-bottom: 0.5em; + padding: 0.2em 0; + cursor: pointer; + user-select: none; + font-family: inherit; + border-bottom: 1px solid transparent; +} +.downloads label.selected { + color: var(--accent); + border-bottom-color: var(--accent); +} +.downloads label input[type="checkbox"] { + position: absolute; + opacity: 0; + width: 0; + height: 0; +} +.downloads label:focus-within { + color: var(--accent); + border-bottom-color: var(--accent); +} +.downloads label::before { + content: "[ ] "; + color: var(--dim); +} +.downloads label.selected::before { + content: "[x] "; + color: var(--accent); +} + +/* Toast notifications */ + +.toasts { + position: fixed; + bottom: 1em; + right: 1em; + z-index: 1000; + display: flex; + flex-direction: column; + gap: 0.3em; +} +.toast { + background: #1a1a1a; + border: 1px solid var(--border); + padding: 0.5em 1em; + max-width: 30em; + animation: fadein 0.2s; + font-family: inherit; + opacity: 1; +} +.toast.info { + border-color: var(--accent); + color: var(--accent); +} +.toast.error { + background: #100; + border-color: #c00; + color: #c00; +} +@keyframes fadein { + from { opacity: 0; transform: translateY(0.5em); } + to { opacity: 1; transform: translateY(0); } +} + +/* Terminal output block */ + +.term { + background: #000; + border: 1px solid var(--border); + padding: 0.5em; + word-break: break-all; + font-family: inherit; + white-space: pre-wrap; +} +.term a { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--link); +} + +/* Misc */ + +.green { color: #0a0; } +.red { color: #c00; } +.copy { font-size: small; } + +.links > * { margin-left: 1ex; } +.links > *:first-child { margin-left: 0; } + .flat > * { margin-left: 1ex; } .flat > *:first-child { margin-left: 0; } -.green { color: green; } -.red { color: red; } - -@media (prefers-color-scheme: dark) { - .red { color: #c00; } -} - -textarea, select, input { - background: var(--input-bg); - color: var(--input-text); - border: solid 1pt; - border-color: var(--bevel-light); - border-top-color: var(--bevel-dark); - border-left-color: var(--bevel-dark); - margin: 1pt; - - &:focus { - outline: solid 1pt var(--color); - } - } - -button, input[type=button], input[type=submit], ::file-selector-button { - background: var(--btn-bg); - color: var(--color); - border: solid 2pt; - border-color: var(--bevel-dark); - border-top-color: var(--bevel-light); - border-left-color: var(--bevel-light); - - &:hover { - background: var(--bevel-dark); - } - &:active { - background: var(--bevel-dark); - border-color: var(--bevel-light); - } -} - -header { - display: flex; - align-items: center; - border-bottom: 2pt solid; - margin: 0 0 1em 0; - padding: 1ex; - justify-content: space-between; -} -#logo > img { - vertical-align: middle; -} -header .utils > * { - margin-left: 1ex; -} - .error { display: flex; - position: relative; - background: rgba(255,0,0,0.2); - border: 1pt solid red; + background: rgba(200,0,0,0.15); + border: 1px solid #c00; + padding: 0.5em; + margin: 0.5em 0; justify-content: space-between; align-items: center; } -.error .btn-close, -.error .code { +.error .btn-close { background: #600; color: white; - font-weight: bold; border: none; - align-self: stretch; - padding: 1ex 1em; -} -.error .code { - order: 1; - - display: flex; - align-items: center; - text-align: center; -} -.error .message { - order: 2; - padding: 1ex 2em; -} -.error .btn-close { - order: 3; -} - -.sheets { - display: flex; - align-items: stretch; -} -.sheets > div { - margin: 0 1ex; - border: 1pt solid; - border-radius: 6pt; -} -.sheets .title { - text-align: center; - font-weight: bold; - font-size: large; - padding: 2pt 6pt; - background: rgba(127,127,127,0.5); -} -.sheets .section { - padding: 2pt 6pt 2pt 6pt; - font-weight: bold; - border-top: 1px dotted; -} -.sheets section { - margin: 2pt 6pt 6pt 6pt; -} -.sheets > *:last-child > table:last-child > tr:last-child > td { - border-bottom: none; + padding: 0.3em 0.7em; + cursor: pointer; } .notif { @@ -180,34 +389,21 @@ header .utils > * { .notif > div:first-child { position: absolute; min-width: 100%; height: 100%; - background: white; - opacity: 75%; + background: var(--bg); + opacity: 85%; text-align: center; } -.links > * { margin-left: 1ex; } -.links > *:first-child { margin-left: 0; } - -@media (prefers-color-scheme: dark) { - .notif > div:first-child { - background: black; - } +.text-and-file { + position: relative; + display: block; } - -.copy { font-size: small; } - -label.radio { - display: inline-block; - margin-right: 1ex; - margin-bottom: 1ex; - padding: 0.5ex; - border: 1pt solid; - border-radius: 1ex; - cursor: pointer; +.text-and-file textarea { + width: 100%; + box-sizing: border-box; } -label.radio:has(input:checked) { - color: var(--link); -} -label.radio input { - display: none; +.text-and-file input[type="file"] { + position: absolute; + bottom: 0; + right: 0; } diff --git a/ui/app.css b/ui/app.css index bee1bd9..9b90cae 100644 --- a/ui/app.css +++ b/ui/app.css @@ -1,12 +1,7 @@ .view-links > span { - display: inline-block; + display: block; white-space: nowrap; - margin-right: 1ex; - margin-bottom: 1ex; - padding: 0.5ex; - border: 1pt solid; - border-radius: 1ex; cursor: pointer; } @@ -15,9 +10,6 @@ display: inline-block; margin-right: 1ex; margin-bottom: 1ex; - padding: 0.5ex; - border: 1px solid !important; - border-radius: 1ex; cursor: pointer; } } @@ -37,7 +29,7 @@ position:relative; textarea { - width: 64em; + width: 100%; } input[type="file"] { diff --git a/ui/index.html b/ui/index.html index dad1bf0..cd446c7 100644 --- a/ui/index.html +++ b/ui/index.html @@ -21,21 +21,16 @@

- -
+ + Logged in - server {{ serverVersion || '-----' }} ui {{ uiHash || '-----' }} - 🗲 -
+
@@ -44,11 +39,13 @@
{{ error.message }}
- +
{{ t.message }}
- + - + - +
+ diff --git a/ui/js/Cluster.js b/ui/js/Cluster.js index 6d7cdf2..e68982a 100644 --- a/ui/js/Cluster.js +++ b/ui/js/Cluster.js @@ -2,18 +2,26 @@ const Cluster = { components: { ClusterAccess, ClusterCAs, GetCopy }, props: [ 'cluster', 'token', 'state' ], template: ` - +
+ Access + +
- +
+ CAs + +
-

Secrets

-

Tokens

- -

Passwords

- +
+ Secrets +

Tokens

+ +

Passwords

+ +
` } diff --git a/ui/js/ClusterAccess.js b/ui/js/ClusterAccess.js index 926c3fe..f9c8421 100644 --- a/ui/js/ClusterAccess.js +++ b/ui/js/ClusterAccess.js @@ -17,6 +17,8 @@ const ClusterAccess = { kubeUserCert: null, downloadSet: null, selectedAssets: {}, + loading: false, + msg: null, }; }, computed: { @@ -44,40 +46,43 @@ const ClusterAccess = { hostCount() { return (this.state.Hosts||[]).filter(h => h.Cluster == this.cluster.Name).length }, - }, + }, methods: { sshCASign() { event.preventDefault(); + this.loading = true; this.msg = null; fetch(`/clusters/${this.cluster.Name}/ssh/user-ca/sign`, { method: 'POST', body: JSON.stringify({ ...this.sshSignReq, Validity: this.signReqValidity }), headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }, }).then((resp) => { if (resp.ok) { - resp.blob().then((cert) => { this.sshUserCert = URL.createObjectURL(cert) }) + resp.blob().then((cert) => { this.sshUserCert = URL.createObjectURL(cert); this.loading = false }) } else { - resp.json().then((resp) => alert('failed to sign: '+resp.message)) + resp.json().then((resp) => { this.msg = 'failed to sign: '+resp.message; this.loading = false }) } }) - .catch((e) => { alert('failed to sign: '+e); }) + .catch((e) => { this.msg = 'failed to sign: '+e; this.loading = false }) }, kubeCASign() { event.preventDefault(); + this.loading = true; this.msg = null; fetch(`/clusters/${this.cluster.Name}/kube/sign`, { method: 'POST', body: JSON.stringify({ ...this.kubeSignReq, Validity: this.signReqValidity }), headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }, }).then((resp) => { if (resp.ok) { - resp.blob().then((cert) => { this.kubeUserCert = URL.createObjectURL(cert) }) + resp.blob().then((cert) => { this.kubeUserCert = URL.createObjectURL(cert); this.loading = false }) } else { - resp.json().then((resp) => alert('failed to sign: '+resp.message)) + resp.json().then((resp) => { this.msg = 'failed to sign: '+resp.message; this.loading = false }) } }) - .catch((e) => { alert('failed to sign: '+e); }) + .catch((e) => { this.msg = 'failed to sign: '+e; this.loading = false }) }, generateDownloadSet() { event.preventDefault() + this.loading = true; this.msg = null; const items = [{ Kind: "host", @@ -91,18 +96,18 @@ const ClusterAccess = { headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }, }).then((resp) => { if (resp.ok) { - resp.json().then((set) => { this.downloadSet = set }) + resp.json().then((set) => { this.downloadSet = set; this.loading = false }) } else { - resp.json().then((resp) => alert('failed to generate: '+resp.message)) + resp.json().then((resp) => { this.msg = 'failed to generate: '+resp.message; this.loading = false }) } - }).catch((e) => { alert('failed to generate: '+e) }) + }).catch((e) => { this.msg = 'failed to generate: '+e; this.loading = false }) }, readFile(e, onload) { const file = e.target.files[0]; if (!file) { return; } const reader = new FileReader(); reader.onload = () => { onload(reader.result) }; - reader.onerror = () => { alert("error reading file"); }; + reader.onerror = () => { this.msg = "error reading file" }; reader.readAsText(file); }, loadPubKey(e) { @@ -110,6 +115,14 @@ const ClusterAccess = { this.sshSignReq.PubKey = v; }); }, + copyDownloadSetUrl() { + try { + navigator.clipboard.writeText(window.location.origin+'/public/download-set?set='+this.downloadSet) + this.$root.toast('copied!', 'info') + } catch (e) { + this.$root.toast('copy failed', 'error') + } + }, loadCSR(e) { this.readFile(e, (v) => { this.kubeSignReq.CSR = v; @@ -117,8 +130,6 @@ const ClusterAccess = { }, }, template: ` -

Access

-

Grant access to: @@ -127,6 +138,8 @@ const ClusterAccess = {

Validity: time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.

+

{{ msg }}

+

Grant SSH access

User:

@@ -134,7 +147,7 @@ const ClusterAccess = {

-

+

@@ -149,9 +162,9 @@ const ClusterAccess = {

-

+

@@ -166,12 +179,12 @@ const ClusterAccess = { {{" "}}

-

-

- Open download set page +

+
+ /public/download-set?set={{ downloadSet }}
- -

+ +
` } diff --git a/ui/js/ClusterCAs.js b/ui/js/ClusterCAs.js index 75e520f..eb05771 100644 --- a/ui/js/ClusterCAs.js +++ b/ui/js/ClusterCAs.js @@ -2,7 +2,6 @@ const ClusterCAs = { components: { GetCopy }, props: [ 'cluster', 'token' ], template: ` -

CAs

NameCertificateSigned certificates
{{ ca.Name }}
diff --git a/ui/js/ClusterDownloadSet.js b/ui/js/ClusterDownloadSet.js deleted file mode 100644 index 64af00d..0000000 --- a/ui/js/ClusterDownloadSet.js +++ /dev/null @@ -1,43 +0,0 @@ -const ClusterDownloadSet = { - props: [ 'cluster', 'token', 'state' ], - data() { - return { - signReqValidity: "1d", - downloadSet: null, - }; - }, - methods: { - generateDownloadSet() { - event.preventDefault() - - const hosts = (this.state.Hosts||[]).filter(h => h.Cluster == this.cluster.Name) - const items = hosts.map(h => ({ - Kind: "host", - Name: h.Name, - Assets: ["kernel", "initrd", "uki", "bootstrap.tar", "boot.img.gz", "boot.img", "boot.qcow2", "boot.iso", "boot.tar", "bootstrap-config", "config", "config.json", "ipxe"], - })) - - fetch('/sign-download-set', { - method: 'POST', - body: JSON.stringify({Expiry: this.signReqValidity, Items: items}), - headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }, - }).then((resp) => { - if (resp.ok) { - resp.json().then((set) => { this.downloadSet = set }) - } else { - resp.json().then((resp) => alert('failed to generate: '+resp.message)) - } - }).catch((e) => { alert('failed to generate: '+e) }) - }, - }, - template: ` -

Download set

-

Validity: time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.

-

-

- Open download set page -
- -

-` -} diff --git a/ui/js/Downloads.js b/ui/js/Downloads.js index 09b94ce..558d58c 100644 --- a/ui/js/Downloads.js +++ b/ui/js/Downloads.js @@ -1,7 +1,7 @@ const Downloads = { props: [ 'kind', 'name', 'token', 'state' ], data() { - return { createDisabled: false, selectedAssets: {} } + return { createDisabled: false, selectedAssets: {}, msg: null } }, computed: { availableAssets() { @@ -50,11 +50,12 @@ const Downloads = { headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }, }).then((resp) => resp.json()) .then((token) => { this.selectedAssets = {}; this.createDisabled = false }) - .catch((e) => { alert('failed to create link'); this.createDisabled = false }) + .catch((e) => { this.msg = 'failed to create link'; this.createDisabled = false }) }, }, template: `

Available assets

+

{{ msg }}

NameCertificateSigned certificates
{{ ca.Name }}