import { createApp } from './vue.esm-browser.js'; createApp({ components: { Cluster, Host }, data() { return { forms: { store: {}, storeUpload: {}, delKey: {}, hostFromTemplate: {}, hostFromTemplateDel: "", }, view: "", viewFilter: "", session: {}, error: null, publicState: null, serverVersion: null, uiHash: null, watchingState: false, state: null, toasts: [], toastId: 0, } }, mounted() { this.session = JSON.parse(sessionStorage.state || "{}") this.watchPublicState() document.addEventListener('keydown', (e) => this.onKeydown(e)) }, unmounted() { document.removeEventListener('keydown', (e) => this.onKeydown(e)) }, watch: { session: { deep: true, handler(v) { sessionStorage.state = JSON.stringify(v) if (v.token && !this.watchingState) { this.watchState() this.watchingState = true } } }, publicState: { deep: true, handler(v) { if (v) { this.serverVersion = v.ServerVersion if (this.uiHash && v.UIHash != this.uiHash) { console.log("reloading") location.reload() } else { this.uiHash = v.UIHash } } }, } }, computed: { adminViews() { return [{type: "actions", name: "admin", title: "Admin actions"}]; }, clusterViews() { return (this.state.Clusters||[]).map((c) => ({type: "cluster", name: c.Name, title: c.Name})); }, hostViews() { return (this.state.Hosts||[]) .filter((h) => h.Name.includes(this.viewFilter)) .map((h) => ({type: "host", name: h.Name, title: h.Name})); }, viewObj() { if (this.view) { if (this.view.type == "cluster") { return this.state.Clusters.find((c) => c.Name == this.view.name); } if (this.view.type == "host") { return this.state.Hosts.find((h) => h.Name == this.view.name); } } return undefined; }, hostsFromTemplate() { return (this.state.Hosts||[]).filter((h) => h.Template); }, }, methods: { any(array) { return array && array.length != 0; }, isActive(v) { return this.view && this.view.type == v.type && this.view.name == v.name }, onKeydown(e) { if ((e.ctrlKey || e.metaKey) && e.key == 'k') { e.preventDefault() this.$nextTick(() => { const el = document.querySelector('.sidebar input[type="search"]') if (el) el.focus() }) return } if (e.key == 'Escape') { this.viewFilter = '' return } }, copyText(text) { event.preventDefault() try { window.navigator.clipboard.writeText(text) this.toast('copied!', 'info') } catch (e) { this.toast('copy failed: ' + e, 'error') } }, setToken() { event.preventDefault() this.session.token = this.forms.setToken this.forms.setToken = null }, uploadStore() { event.preventDefault() this.apiPost('/public/store.tar', this.$refs.storeUpload.files[0], (v) => { this.forms.store = {} }, "application/tar") }, namedPassphrase(name, passphrase) { return {Name: this.forms.store.name, Passphrase: btoa(this.forms.store.pass1)} }, storeAddKey() { const params = this.namedPassphrase(); if (this.forms.store.byHash) { params.Hash = this.forms.store.hash; } this.apiPost('/store/add-key', params, (v) => { this.forms.store = {} }) }, storeDelKey() { event.preventDefault() let name = this.forms.delKey.name if (!confirm("Remove key named "+JSON.stringify(name)+"?")) { return } this.apiPost('/store/delete-key', name , (v) => { this.forms.delKey = {} }) }, unlockStore() { this.apiPost('/public/unlock-store', this.namedPassphrase(), (v) => { this.forms.store = {} if (v) { this.session.token = v if (!this.watchingState) { this.watchState() this.watchingState = true } } }) }, uploadConfig() { const file = this.$refs.configUpload.files[0]; let contentType = "text/vnd.yaml"; if (file.name.endsWith(".json")) { contentType = "application/json"; } this.apiPost('/configs', file, (v) => {}, contentType, true) }, hostFromTemplateAdd() { let v = this.forms.hostFromTemplate; this.apiPost('/hosts-from-template/'+v.name, v, (v) => { this.forms.hostFromTemplate = {} }); }, hostFromTemplateDel() { event.preventDefault() let v = this.forms.hostFromTemplateDel; if (!confirm("delete host template instance "+v+"?")) { return } this.apiDelete('/hosts-from-template/'+v, (v) => { this.forms.hostFromTemplateDel = "" }); }, apiPost(action, data, onload, contentType = 'application/json', raw = false) { event.preventDefault() if (data === undefined) { throw("action " + action + ": no data") } /* TODO fetch(action, { method: 'POST', body: JSON.stringify(data), }) .then((response) => response.json()) .then((result) => onload) // */ var xhr = new XMLHttpRequest() xhr.responseType = 'json' xhr.onerror = () => {} xhr.onload = (r) => { if (xhr.status != 200) { this.toast((xhr.response && xhr.response.message) || 'request failed', 'error') return } this.error = null if (onload) { onload(xhr.response) } } xhr.open("POST", action) xhr.setRequestHeader('Accept', 'application/json') xhr.setRequestHeader('Content-Type', contentType) if (this.session.token) { xhr.setRequestHeader('Authorization', 'Bearer '+this.session.token) } if (!raw && contentType == "application/json") { xhr.send(JSON.stringify(data)) } else { xhr.send(data) } }, apiDelete(action, data, onload) { event.preventDefault() var xhr = new XMLHttpRequest() xhr.onload = (r) => { if (xhr.status != 200) { this.error = xhr.response return } this.error = null if (onload) { onload(xhr.response) } } xhr.open("DELETE", action) xhr.setRequestHeader('Accept', 'application/json') if (this.session.token) { xhr.setRequestHeader('Authorization', 'Bearer '+this.session.token) } xhr.send() }, download(url) { event.target.target = '_blank' event.target.href = this.downloadLink(url) }, downloadLink(url) { // TODO once-shot download link return url + '?token=' + this.session.token }, toast(message, kind) { const id = ++this.toastId this.toasts.push({id, message, kind: kind || 'info'}) setTimeout(() => this.dismissToast(id), 4000) }, dismissToast(id) { this.toasts = this.toasts.filter(t => t.id != id) }, watchPublicState() { this.watchStream('publicState', '/public-state') }, watchState() { this.watchStream('state', '/state', true) }, watchStream(field, path, withToken) { let evtSrc = new EventSource(path + (withToken ? '?token='+this.session.token : '')); evtSrc.onmessage = (e) => { let update = JSON.parse(e.data) console.log("watch "+path+":", update) if (update.err) { console.log("watch error from server:", err) } if (update.set) { this[field] = update.set } if (update.p) { // patch new jsonpatch.JSONPatch(update.p, true).apply(this[field]) } } evtSrc.onerror = (e) => { // console.log("event source " + path + " error:", e) if (evtSrc) evtSrc.close() this[field] = null window.setTimeout(() => { this.watchStream(field, path, withToken) }, 1000) } }, } }).mount('#app');