Compare commits
2 Commits
c255f1f196
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9aa942c99d | |||
| 38ad620759 |
@@ -13,6 +13,20 @@ import (
|
||||
"novit.tech/direktil/pkg/localconfig"
|
||||
)
|
||||
|
||||
func wsFetchConfig(req *restful.Request, resp *restful.Response) {
|
||||
cfg, err := readConfig()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
wsNotFound(resp)
|
||||
} else {
|
||||
wsError(resp, httperr.Internal(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.WriteEntity(cfg)
|
||||
}
|
||||
|
||||
func wsUploadConfig(req *restful.Request, resp *restful.Response) {
|
||||
cfg := &localconfig.Config{}
|
||||
if err := req.ReadEntity(cfg); err != nil {
|
||||
|
||||
@@ -73,6 +73,9 @@ func registerWS(rest *restful.Container) {
|
||||
Doc("Sign a download set"))
|
||||
|
||||
// - configs API
|
||||
ws.Route(ws.GET("/config").To(wsFetchConfig).
|
||||
Produces(mime.JSON, mime.YAML).
|
||||
Doc("Fetch the current configuration"))
|
||||
ws.Route(ws.POST("/configs").To(wsUploadConfig).
|
||||
Consumes(mime.YAML, mime.JSON).Param(ws.BodyParameter("config", "The new full configuration")).
|
||||
Produces(mime.JSON).Writes(true).
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
position:relative;
|
||||
|
||||
textarea {
|
||||
width: 64em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
|
||||
+14
-21
@@ -21,21 +21,16 @@
|
||||
|
||||
<div id="app">
|
||||
<header>
|
||||
<div id="logo">
|
||||
<img src="favicon.ico" />
|
||||
<span>Direktil Local Server</span>
|
||||
</div>
|
||||
<div class="utils">
|
||||
<span id="logo"><img src="favicon.ico" /> Direktil Local Server</span>
|
||||
<span class="utils">
|
||||
<span id="login-hdr" v-if="session.token">
|
||||
Logged in
|
||||
<button class="link" @click="copyText(session.token)">🗐</button>
|
||||
</span>
|
||||
|
||||
<span>server <code>{{ serverVersion || '-----' }}</code></span>
|
||||
<span>ui <code>{{ uiHash || '-----' }}</code></span>
|
||||
|
||||
<span :class="publicState ? 'green' : 'red'">🗲</span>
|
||||
</div>
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div class="error" v-if="error">
|
||||
@@ -46,11 +41,11 @@
|
||||
|
||||
<div class="toasts"><div v-for="t in toasts" :class="'toast '+t.kind" @click="dismissToast(t.id)">{{ t.message }}</div></div>
|
||||
|
||||
<template v-if="!publicState">
|
||||
<main class="content" v-if="!publicState">
|
||||
<p>Not connected.</p>
|
||||
</template>
|
||||
</main>
|
||||
|
||||
<template v-else-if="publicState.Store.New">
|
||||
<main class="content" v-else-if="publicState.Store.New">
|
||||
<p>Store is new.</p>
|
||||
<p>Option 1: initialize a new store</p>
|
||||
<form @submit="unlockStore">
|
||||
@@ -64,17 +59,17 @@
|
||||
<input type="file" ref="storeUpload" />
|
||||
<input type="submit" value="upload" />
|
||||
</form>
|
||||
</template>
|
||||
</main>
|
||||
|
||||
<template v-else-if="!publicState.Store.Open">
|
||||
<main class="content" v-else-if="!publicState.Store.Open">
|
||||
<p>Store is not open.</p>
|
||||
<form @submit="unlockStore">
|
||||
<input type="password" name="passphrase" v-model="forms.store.pass1" required placeholder="Passphrase" />
|
||||
<input type="submit" value="unlock" :disabled="!forms.store.pass1" />
|
||||
</form>
|
||||
</template>
|
||||
</main>
|
||||
|
||||
<template v-else-if="!state">
|
||||
<main class="content" v-else-if="!state">
|
||||
<p v-if="!session.token">Not logged in.</p>
|
||||
<p v-else>Invalid token</p>
|
||||
|
||||
@@ -82,18 +77,17 @@
|
||||
<input type="password" v-model="forms.store.pass1" required placeholder="Passphrase" />
|
||||
<input type="submit" value="log in"/>
|
||||
</form>
|
||||
</template>
|
||||
</main>
|
||||
|
||||
<template v-else>
|
||||
<div class="layout">
|
||||
<nav class="sidebar">
|
||||
<input type="search" placeholder="Filter" v-model="viewFilter"/>
|
||||
<p class="nav-section">Admin</p>
|
||||
<p class="view-links"><span v-for="v in adminViews" @click="view = v" :class="{selected: isActive(v)}">{{v.title}}</span></p>
|
||||
<p class="view-links"><button type="button" v-for="v in adminViews" @click="view = v" :class="{selected: isActive(v)}">{{v.title}}</button></p>
|
||||
<p class="nav-section" v-if="clusterViews.length">Clusters</p>
|
||||
<p class="view-links" v-if="clusterViews.length"><span v-for="v in clusterViews" @click="view = v" :class="{selected: isActive(v)}">{{v.title}}</span></p>
|
||||
<p class="view-links" v-if="clusterViews.length"><button type="button" v-for="v in clusterViews" @click="view = v" :class="{selected: isActive(v)}">{{v.title}}</button></p>
|
||||
<p class="nav-section" v-if="hostViews.length">Hosts</p>
|
||||
<p class="view-links" v-if="hostViews.length"><span v-for="v in hostViews" @click="view = v" :class="{selected: isActive(v)}">{{v.title}}</span></p>
|
||||
<p class="view-links" v-if="hostViews.length"><button type="button" v-for="v in hostViews" @click="view = v" :class="{selected: isActive(v)}">{{v.title}}</button></p>
|
||||
</nav>
|
||||
<main class="content">
|
||||
<h2 v-if="view">{{ view.type == 'cluster' ? 'Cluster ' : view.type == 'host' ? 'Host ' : '' }}{{view.title}}</h2>
|
||||
@@ -158,7 +152,6 @@
|
||||
</template>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
+33
-15
@@ -21,31 +21,38 @@ body {
|
||||
|
||||
/* 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;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0.5em 1em;
|
||||
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;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
min-height: calc(100vh - 3em);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 18em;
|
||||
grid-area: s;
|
||||
border-right: 1px solid var(--border);
|
||||
padding: 0.5em;
|
||||
overflow-y: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sidebar input[type="search"] {
|
||||
width: 100%;
|
||||
@@ -59,7 +66,7 @@ header .utils > * {
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
grid-area: m;
|
||||
padding: 1em;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -82,25 +89,34 @@ header .utils > * {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.view-links > span {
|
||||
.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 > span:last-child {
|
||||
.view-links > button:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.view-links > span:hover {
|
||||
.view-links > button:hover {
|
||||
background: var(--hover);
|
||||
color: var(--color);
|
||||
}
|
||||
.view-links > span.selected {
|
||||
color: var(--accent);
|
||||
.view-links > button.selected {
|
||||
color: var(--accent) !important;
|
||||
border-bottom-color: var(--accent);
|
||||
}
|
||||
|
||||
@@ -176,6 +192,7 @@ textarea, select, input {
|
||||
padding: 0.3em 0.5em;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
textarea:focus, select:focus, input:focus {
|
||||
outline: none;
|
||||
@@ -379,6 +396,7 @@ label.radio:focus-within {
|
||||
|
||||
.text-and-file {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
.text-and-file textarea {
|
||||
width: 100%;
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
position:relative;
|
||||
|
||||
textarea {
|
||||
width: 64em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
|
||||
+14
-21
@@ -21,21 +21,16 @@
|
||||
|
||||
<div id="app">
|
||||
<header>
|
||||
<div id="logo">
|
||||
<img src="favicon.ico" />
|
||||
<span>Direktil Local Server</span>
|
||||
</div>
|
||||
<div class="utils">
|
||||
<span id="logo"><img src="favicon.ico" /> Direktil Local Server</span>
|
||||
<span class="utils">
|
||||
<span id="login-hdr" v-if="session.token">
|
||||
Logged in
|
||||
<button class="link" @click="copyText(session.token)">🗐</button>
|
||||
</span>
|
||||
|
||||
<span>server <code>{{ serverVersion || '-----' }}</code></span>
|
||||
<span>ui <code>{{ uiHash || '-----' }}</code></span>
|
||||
|
||||
<span :class="publicState ? 'green' : 'red'">🗲</span>
|
||||
</div>
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div class="error" v-if="error">
|
||||
@@ -46,11 +41,11 @@
|
||||
|
||||
<div class="toasts"><div v-for="t in toasts" :class="'toast '+t.kind" @click="dismissToast(t.id)">{{ t.message }}</div></div>
|
||||
|
||||
<template v-if="!publicState">
|
||||
<main class="content" v-if="!publicState">
|
||||
<p>Not connected.</p>
|
||||
</template>
|
||||
</main>
|
||||
|
||||
<template v-else-if="publicState.Store.New">
|
||||
<main class="content" v-else-if="publicState.Store.New">
|
||||
<p>Store is new.</p>
|
||||
<p>Option 1: initialize a new store</p>
|
||||
<form @submit="unlockStore">
|
||||
@@ -64,17 +59,17 @@
|
||||
<input type="file" ref="storeUpload" />
|
||||
<input type="submit" value="upload" />
|
||||
</form>
|
||||
</template>
|
||||
</main>
|
||||
|
||||
<template v-else-if="!publicState.Store.Open">
|
||||
<main class="content" v-else-if="!publicState.Store.Open">
|
||||
<p>Store is not open.</p>
|
||||
<form @submit="unlockStore">
|
||||
<input type="password" name="passphrase" v-model="forms.store.pass1" required placeholder="Passphrase" />
|
||||
<input type="submit" value="unlock" :disabled="!forms.store.pass1" />
|
||||
</form>
|
||||
</template>
|
||||
</main>
|
||||
|
||||
<template v-else-if="!state">
|
||||
<main class="content" v-else-if="!state">
|
||||
<p v-if="!session.token">Not logged in.</p>
|
||||
<p v-else>Invalid token</p>
|
||||
|
||||
@@ -82,18 +77,17 @@
|
||||
<input type="password" v-model="forms.store.pass1" required placeholder="Passphrase" />
|
||||
<input type="submit" value="log in"/>
|
||||
</form>
|
||||
</template>
|
||||
</main>
|
||||
|
||||
<template v-else>
|
||||
<div class="layout">
|
||||
<nav class="sidebar">
|
||||
<input type="search" placeholder="Filter" v-model="viewFilter"/>
|
||||
<p class="nav-section">Admin</p>
|
||||
<p class="view-links"><span v-for="v in adminViews" @click="view = v" :class="{selected: isActive(v)}">{{v.title}}</span></p>
|
||||
<p class="view-links"><button type="button" v-for="v in adminViews" @click="view = v" :class="{selected: isActive(v)}">{{v.title}}</button></p>
|
||||
<p class="nav-section" v-if="clusterViews.length">Clusters</p>
|
||||
<p class="view-links" v-if="clusterViews.length"><span v-for="v in clusterViews" @click="view = v" :class="{selected: isActive(v)}">{{v.title}}</span></p>
|
||||
<p class="view-links" v-if="clusterViews.length"><button type="button" v-for="v in clusterViews" @click="view = v" :class="{selected: isActive(v)}">{{v.title}}</button></p>
|
||||
<p class="nav-section" v-if="hostViews.length">Hosts</p>
|
||||
<p class="view-links" v-if="hostViews.length"><span v-for="v in hostViews" @click="view = v" :class="{selected: isActive(v)}">{{v.title}}</span></p>
|
||||
<p class="view-links" v-if="hostViews.length"><button type="button" v-for="v in hostViews" @click="view = v" :class="{selected: isActive(v)}">{{v.title}}</button></p>
|
||||
</nav>
|
||||
<main class="content">
|
||||
<h2 v-if="view">{{ view.type == 'cluster' ? 'Cluster ' : view.type == 'host' ? 'Host ' : '' }}{{view.title}}</h2>
|
||||
@@ -158,7 +152,6 @@
|
||||
</template>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
+33
-15
@@ -21,31 +21,38 @@ body {
|
||||
|
||||
/* 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;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0.5em 1em;
|
||||
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;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
min-height: calc(100vh - 3em);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 18em;
|
||||
grid-area: s;
|
||||
border-right: 1px solid var(--border);
|
||||
padding: 0.5em;
|
||||
overflow-y: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sidebar input[type="search"] {
|
||||
width: 100%;
|
||||
@@ -59,7 +66,7 @@ header .utils > * {
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
grid-area: m;
|
||||
padding: 1em;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -82,25 +89,34 @@ header .utils > * {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.view-links > span {
|
||||
.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 > span:last-child {
|
||||
.view-links > button:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.view-links > span:hover {
|
||||
.view-links > button:hover {
|
||||
background: var(--hover);
|
||||
color: var(--color);
|
||||
}
|
||||
.view-links > span.selected {
|
||||
color: var(--accent);
|
||||
.view-links > button.selected {
|
||||
color: var(--accent) !important;
|
||||
border-bottom-color: var(--accent);
|
||||
}
|
||||
|
||||
@@ -176,6 +192,7 @@ textarea, select, input {
|
||||
padding: 0.3em 0.5em;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
textarea:focus, select:focus, input:focus {
|
||||
outline: none;
|
||||
@@ -379,6 +396,7 @@ label.radio:focus-within {
|
||||
|
||||
.text-and-file {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
.text-and-file textarea {
|
||||
width: 100%;
|
||||
|
||||
Reference in New Issue
Block a user