Redesigned config panel UI.

This commit is contained in:
Fabio Manganiello 2023-08-06 18:49:03 +02:00
parent d3fce6d922
commit 237e0c47cb
Signed by untrusted user: blacklight
GPG key ID: D90FBA7F76362774
8 changed files with 233 additions and 48 deletions

File diff suppressed because one or more lines are too long

View file

@ -5,7 +5,29 @@
<span class="hostname" v-if="hostname" v-text="hostname" /> <span class="hostname" v-if="hostname" v-text="hostname" />
</div> </div>
<ul class="plugins"> <ul class="plugins" v-if="selectedPanel === 'settings'">
<li class="entry" title="Home" @click="onItemClick('entities')">
<a href="/#">
<i class="fas fa-home" />
<span class="name" v-if="!collapsed">Home</span>
</a>
</li>
<li v-for="config, name in configSections" :key="name" class="entry"
:class="{selected: name === selectedConfigPanel}"
:title="config.name" @click="$emit('select-config', name)">
<a href="/#settings">
<span class="icon">
<i :class="config.icon['class']" v-if="config.icon?.['class']" />
<img :src="config.icon?.imgUrl" v-else-if="config.icon?.imgUrl" alt="name"/>
<i class="fas fa-puzzle-piece" v-else />
</span>
<span class="name" v-if="!collapsed" v-text="config.name" />
</a>
</li>
</ul>
<ul class="plugins" v-else>
<li v-for="name in panelNames" :key="name" class="entry" :class="{selected: name === selectedPanel}" <li v-for="name in panelNames" :key="name" class="entry" :class="{selected: name === selectedPanel}"
:title="name" @click="onItemClick(name)"> :title="name" @click="onItemClick(name)">
<a :href="`/#${name}`"> <a :href="`/#${name}`">
@ -21,7 +43,6 @@
<ul class="footer"> <ul class="footer">
<li :class="{selected: selectedPanel === 'settings'}" title="Settings" @click="onItemClick('settings')"> <li :class="{selected: selectedPanel === 'settings'}" title="Settings" @click="onItemClick('settings')">
<!--suppress HtmlUnknownAnchorTarget -->
<a href="/#settings"> <a href="/#settings">
<span class="icon"> <span class="icon">
<i class="fa fa-cog" /> <i class="fa fa-cog" />
@ -31,7 +52,6 @@
</li> </li>
<li title="Logout" @click="onItemClick('logout')"> <li title="Logout" @click="onItemClick('logout')">
<!--suppress HtmlUnknownTarget -->
<a href="/logout"> <a href="/logout">
<span class="icon"> <span class="icon">
<i class="fas fa-sign-out-alt" /> <i class="fas fa-sign-out-alt" />
@ -46,10 +66,11 @@
<script> <script>
import icons from '@/assets/icons.json' import icons from '@/assets/icons.json'
import Utils from "@/Utils"; import Utils from "@/Utils";
import configSections from '@/components/panels/Settings/sections.json';
export default { export default {
name: "Nav", name: "Nav",
emits: ['select'], emits: ['select', 'select-config'],
mixins: [Utils], mixins: [Utils],
props: { props: {
panels: { panels: {
@ -61,6 +82,10 @@ export default {
type: String, type: String,
}, },
selectedConfigPanel: {
type: String,
},
hostname: { hostname: {
type: String, type: String,
}, },
@ -94,6 +119,7 @@ export default {
collapsed: true, collapsed: true,
icons: icons, icons: icons,
host: null, host: null,
configSections: configSections,
} }
}, },
@ -217,7 +243,6 @@ nav {
.footer { .footer {
height: $footer-expanded-height; height: $footer-expanded-height;
background: $nav-footer-bg; background: $nav-footer-bg;
box-shadow: $nav-footer-shadow;
padding: 0; padding: 0;
margin: 0; margin: 0;

View file

@ -0,0 +1,73 @@
<template>
<div class="floating-btn" :class="className">
<button type="button" class="btn btn-primary" :title="title" @click="$emit('click', $event)">
<Icon :class="iconClass" :url="iconUrl" />
</button>
</div>
</template>
<script>
import Icon from "@/components/elements/Icon";
export default {
name: "FloatingButton",
components: {Icon},
emits: ["click"],
props: {
iconClass: {
type: String,
},
iconUrl: {
type: String,
},
class: {
type: String,
},
title: {
type: String,
},
},
computed: {
className() {
return this.class
}
}
}
</script>
<style lang="scss" scoped>
.floating-btn {
position: fixed;
bottom: 0;
right: 0;
margin: auto 1em 1em auto;
button {
background: $active-glow-bg-2 !important;
color: $selected-fg !important;
width: 4em;
height: 4em;
border-radius: 2em;
border: none !important;
padding: 0;
box-shadow: $border-shadow-bottom-right;
&:hover {
background: $hover-bg-2 !important;
color: $hover-fg !important;
}
}
:deep(button) {
.icon-container {
width: 4em;
.icon {
margin: auto;
}
}
}
}
</style>

View file

@ -1,48 +1,32 @@
<template> <template>
<div class="settings-container"> <div class="settings-container">
<header>
<div class="col-8">
<Dropdown title="Select a category" icon-class="fa fa-ellipsis-h">
<DropdownItem text="Users" icon-class="fa fa-user"
:item-class="{selected: selectedView === 'users'}"
@click="selectedView = 'users'" />
<DropdownItem text="Generate a token" icon-class="fa fa-key"
:item-class="{selected: selectedView === 'token'}"
@click="selectedView = 'token'" />
</Dropdown>
</div>
<div class="col-4 pull-right">
<button title="Add User" @click="$refs.usersView.$refs.addUserModal.show()" v-if="selectedView === 'users'">
<i class="fa fa-plus" />
</button>
</div>
</header>
<main> <main>
<Users :session-token="sessionToken" :current-user="currentUser" <Users :session-token="sessionToken" :current-user="currentUser"
v-if="selectedView === 'users'" ref="usersView" /> v-if="selectedPanel === 'users' && currentUser" />
<Token :session-token="sessionToken" :current-user="currentUser" <Token :session-token="sessionToken" :current-user="currentUser"
v-else-if="selectedView === 'token'" ref="tokenView" /> v-else-if="selectedPanel === 'tokens' && currentUser" />
</main> </main>
</div> </div>
</template> </template>
<script> <script>
import Dropdown from "@/components/elements/Dropdown";
import DropdownItem from "@/components/elements/DropdownItem";
import Token from "@/components/panels/Settings/Token"; import Token from "@/components/panels/Settings/Token";
import Users from "@/components/panels/Settings/Users"; import Users from "@/components/panels/Settings/Users";
import Utils from "@/Utils"; import Utils from "@/Utils";
export default { export default {
name: "Settings", name: "Settings",
components: {Dropdown, DropdownItem, Users, Token}, components: {Users, Token},
mixins: [Utils], mixins: [Utils],
props: {
selectedPanel: {
type: String,
},
},
data() { data() {
return { return {
selectedView: 'users',
currentUser: null, currentUser: null,
sessionToken: null, sessionToken: null,
} }

View file

@ -7,25 +7,66 @@
<label> <label>
This is your generated token. Treat it carefully and do not share it with untrusted parties.<br/> This is your generated token. Treat it carefully and do not share it with untrusted parties.<br/>
Also, make sure to save it - it WILL NOT be displayed again. Also, make sure to save it - it WILL NOT be displayed again.
<textarea class="token" v-text="token" @focus="onTokenSelect" />
</label> </label>
<textarea class="token" v-text="token" @focus="onTokenSelect" />
</div>
</Modal>
<Modal ref="sessionTokenModal">
<div class="token-container">
<label>
This is your current session token.
It will be invalidated once you log out of the current session.
</label>
<textarea class="token" v-text="sessionToken" @focus="onTokenSelect" />
</div> </div>
</Modal> </Modal>
<div class="body"> <div class="body">
<div class="description"> <div class="description">
<p>Generate a JWT authentication token that can be used for API calls to the <tt>/execute</tt> endpoint.</p><br/> <p>
Platypush provides two types of tokens:
<ul>
<li>
<b>JWT tokens</b> are bearer-only, and they contain encrypted
authentication information.<br/>
They can be used as permanent or time-based tokens to
authenticate with the Platypush API.
</li>
<li>
<b>Session tokens</b> are randomly generated tokens stored on the
application database. A session token generated in this session
will expire when you log out of it.
</li>
</ul>
</p>
<p>Generate a JWT authentication token that can be used for API calls to the <code>/execute</code> endpoint.</p><br/>
<p>You can include the token in your requests in any of the following ways:</p> <p>You can include the token in your requests in any of the following ways:</p>
<ul> <ul>
<li>Specify it on the <tt>Authorization: Bearer</tt> header;</li> <li>Specify it on the <code>Authorization: Bearer</code> header;</li>
<li>Specify it on the <tt>X-Token</tt> header;</li> <li>Specify it on the <code>X-Token</code> header;</li>
<li>Specify it as a URL parameter: <tt>http://site:8008/execute?token=...</tt>;</li> <li>
<li>Specify it on the body of your JSON request: <tt>{"type":"request", "action", "...", "token":"..."}</tt>.</li> Specify it as a URL parameter: <code>http://site:8008/execute?token=...</code>
for a JWT token and <code>...?session_token=...</code> for a
session token;
</li>
<li>Specify it on the body of your JSON request:
<code>{"type":"request", "action", "...", "token":"..."}</code> for
a JWT token, or <code>"session_token"</code> for a session token.
</li>
</ul> </ul>
Confirm your credentials in order to generate a new token. <p>Confirm your credentials in order to generate a new JWT token.</p>
<p>
<i>Show session token</i> will instead show the token cookie associated
to the current session.
</p>
</div> </div>
<div class="form-container"> <div class="form-container">
@ -50,13 +91,19 @@
<input type="text" name="validityDays"> <input type="text" name="validityDays">
</span> </span>
<span class="note"> <span class="note">
Decimal values are also supported (e.g. <i>0.5</i> to identify 6 hours). An empty or zero value means that Decimal values are also supported - e.g. <i>0.5</i> means half a
the token has no expiry date. day (12 hours). An empty or zero value means that the token has
no expiry date.
</span> </span>
</label> </label>
<label> <label>
<input type="submit" class="btn btn-primary" value="Generate token"> <input type="submit" class="btn btn-primary" value="Generate JWT token">
</label>
<label>
<input type="button" class="btn btn-default" value="Show session token"
@click.stop="$refs.sessionTokenModal.show()">
</label> </label>
</form> </form>
</div> </div>
@ -80,6 +127,11 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
sessionToken: {
type: String,
required: true,
},
}, },
data() { data() {
@ -137,8 +189,13 @@ export default {
.token-container { .token-container {
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column;
margin-top: .15em; margin-top: .15em;
label {
width: 100%;
}
.body { .body {
background: $background-color; background: $background-color;
display: flex; display: flex;
@ -203,9 +260,13 @@ export default {
} }
textarea { textarea {
width: 100%;
height: 10em; height: 10em;
margin-top: 1em; margin-top: 1em;
border-radius: 1em; border-radius: 1em;
border: none;
background: $active-glow-bg-1;
padding: 1em;
} }
} }
} }

View file

@ -43,15 +43,17 @@
<li v-for="user in users" :key="user.user_id" class="item user" @click="selectedUser = user.username"> <li v-for="user in users" :key="user.user_id" class="item user" @click="selectedUser = user.username">
<div class="name col-8" v-text="user.username" /> <div class="name col-8" v-text="user.username" />
<div class="actions pull-right col-4"> <div class="actions pull-right col-4">
<Dropdown title="User Actions" icon-class="fa fa-cog"> <Dropdown title="User Actions" icon-class="fa fa-ellipsis">
<DropdownItem text="Change Password" :disabled="commandRunning" icon-class="fa fa-key" <DropdownItem text="Change Password" :disabled="commandRunning" icon-class="fa fa-key"
@click="selectedUser = user.username; $refs.changePasswordModal.show()" /> @click="showChangePasswordModal(user)" />
<DropdownItem text="Delete User" :disabled="commandRunning" icon-class="fa fa-trash" <DropdownItem text="Delete User" :disabled="commandRunning" icon-class="fa fa-trash"
@click="deleteUser(user)" /> @click="deleteUser(user)" />
</Dropdown> </Dropdown>
</div> </div>
</li> </li>
</ul> </ul>
<FloatingButton icon-class="fa fa-plus" text="Add User" @click="showAddUserModal" />
</div> </div>
</template> </template>
@ -61,10 +63,11 @@ import Modal from "@/components/Modal";
import Loading from "@/components/Loading"; import Loading from "@/components/Loading";
import Utils from "@/Utils"; import Utils from "@/Utils";
import DropdownItem from "@/components/elements/DropdownItem"; import DropdownItem from "@/components/elements/DropdownItem";
import FloatingButton from "@/components/elements/FloatingButton";
export default { export default {
name: "Users", name: "Users",
components: {DropdownItem, Loading, Modal, Dropdown}, components: {Dropdown, DropdownItem, FloatingButton, Loading, Modal},
mixins: [Utils], mixins: [Utils],
props: { props: {
@ -218,6 +221,22 @@ export default {
await this.refresh() await this.refresh()
}, },
showAddUserModal() {
this.$refs.addUserModal.show()
this.$nextTick(() => {
this.$refs.addUserForm.reset()
this.$refs.addUserForm.username.focus()
})
},
showChangePasswordModal(user) {
this.$refs.changePasswordModal.show()
this.$nextTick(() => {
this.$refs.changePasswordForm.password.focus()
this.selectedUser = user.username
})
},
}, },
mounted() { mounted() {
@ -268,6 +287,7 @@ export default {
justify-content: right; justify-content: right;
button { button {
background: none !important;
width: min-content; width: min-content;
} }
} }

View file

@ -0,0 +1,15 @@
{
"users": {
"name": "Users",
"icon": {
"class": "fas fa-user"
}
},
"tokens": {
"name": "Tokens",
"icon": {
"class": "fas fa-key"
}
}
}

View file

@ -1,12 +1,18 @@
<template> <template>
<main> <main>
<Loading v-if="loading" /> <Loading v-if="loading" />
<Nav :panels="components" :selected-panel="selectedPanel" :hostname="hostname" <Nav :panels="components"
@select="selectedPanel = $event" v-else /> :selected-panel="selectedPanel"
:selected-config-panel="selectedConfigPanel"
:hostname="hostname"
@select="selectedPanel = $event"
@select-config="selectedConfigPanel = $event"
v-else
/>
<div class="canvas" v-if="selectedPanel === 'settings'"> <div class="canvas" v-if="selectedPanel === 'settings'">
<div class="panel"> <div class="panel">
<Settings /> <Settings :selected-panel="selectedConfigPanel" />
</div> </div>
</div> </div>
@ -39,6 +45,7 @@ export default {
components: {}, components: {},
hostname: undefined, hostname: undefined,
selectedPanel: undefined, selectedPanel: undefined,
selectedConfigPanel: 'users',
} }
}, },