forked from platypush/platypush
Redesigned config panel UI.
This commit is contained in:
parent
d3fce6d922
commit
237e0c47cb
8 changed files with 233 additions and 48 deletions
File diff suppressed because one or more lines are too long
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"users": {
|
||||||
|
"name": "Users",
|
||||||
|
"icon": {
|
||||||
|
"class": "fas fa-user"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"tokens": {
|
||||||
|
"name": "Tokens",
|
||||||
|
"icon": {
|
||||||
|
"class": "fas fa-key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue