add YnhConsole component to display history

This commit is contained in:
Axolotle 2020-10-12 15:21:01 +02:00
parent 23aa8bba64
commit a86e90e49a
4 changed files with 223 additions and 32 deletions

View file

@ -3,7 +3,10 @@
<!-- HEADER --> <!-- HEADER -->
<header> <header>
<b-navbar> <b-navbar>
<b-navbar-brand :to="{ name: 'home' }" exact exact-active-class="active"> <b-navbar-brand
:to="{ name: 'home' }" :disabled="waiting"
exact exact-active-class="active"
>
<img alt="Yunohost logo" src="./assets/logo.png"> <img alt="Yunohost logo" src="./assets/logo.png">
</b-navbar-brand> </b-navbar-brand>
@ -40,8 +43,12 @@
<router-view v-else class="static" /> <router-view v-else class="static" />
</main> </main>
</api-wait-overlay> </api-wait-overlay>
<!-- CONSOLE/HISTORY -->
<ynh-console @height-changed="consoleHeight = $event" class="mt-auto" />
<!-- FOOTER --> <!-- FOOTER -->
<footer> <footer :style="'padding-bottom: ' + consoleHeight + 'px;'">
<nav> <nav>
<b-nav class="justify-content-center"> <b-nav class="justify-content-center">
<b-nav-item href="https://yunohost.org/docs" target="_blank" link-classes="text-secondary"> <b-nav-item href="https://yunohost.org/docs" target="_blank" link-classes="text-secondary">
@ -79,22 +86,27 @@
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import ApiWaitOverlay from '@/components/ApiWaitOverlay' import ApiWaitOverlay from '@/components/ApiWaitOverlay'
import YnhConsole from '@/components/YnhConsole'
export default { export default {
name: 'App', name: 'App',
data: () => ({ data () {
transitionName: null return {
}), transitionName: null,
// Value used to add padding to the footer so the opened console never hides content
consoleHeight: 0
}
},
computed: { computed: {
...mapGetters(['connected', 'yunohost', 'transitions']) ...mapGetters(['connected', 'yunohost', 'transitions', 'waiting'])
}, },
watch: { watch: {
// Set the css class to animate the components transition // Set the css class to animate the components transition
'$route' (to, from) { '$route' (to, from) {
if (!this.transitions) return if (!this.transitions || from.name === null) return
// Use the breadcrumb array length as a direction indicator // Use the breadcrumb array length as a direction indicator
const toDepth = to.meta.breadcrumb.length const toDepth = to.meta.breadcrumb.length
const fromDepth = from.meta.breadcrumb.length const fromDepth = from.meta.breadcrumb.length
@ -109,7 +121,8 @@ export default {
}, },
components: { components: {
ApiWaitOverlay ApiWaitOverlay,
YnhConsole
}, },
// This hook is only triggered at page first load // This hook is only triggered at page first load
@ -126,9 +139,19 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
// Global import of Bootstrap and custom styles
@import '@/scss/main.scss'; @import '@/scss/main.scss';
</style>
#app > header { <style lang="scss" scoped>
::v-deep#app {
display: flex;
flex-direction: column;
height: 100%;
}
header {
border-bottom: $thin-border; border-bottom: $thin-border;
padding-top: 1rem; padding-top: 1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
@ -146,28 +169,6 @@ export default {
li { li {
margin: .2rem 0; margin: .2rem 0;
} }
icon {
margin-left: .5rem;
}
}
}
}
#app > footer {
padding: 1rem 0;
border-top: 1px solid #eee;
font-size: 0.875rem;
margin-top: 2rem;
.nav-item {
& + .nav-item a::before {
content: "•";
width: 1rem;
display: inline-block;
margin-left: -1.15rem;
}
&:first-child {
margin-left: -1rem;
} }
} }
} }
@ -197,4 +198,33 @@ main {
display: none; display: none;
} }
} }
#console {
// Allows the console to be tabbed before the footer links while remaining visually
// the last element of the page
order: 3;
}
footer {
border-top: 1px solid #eee;
font-size: $font-size-sm;
margin-top: 2rem;
padding-bottom: 3rem;
.nav {
padding: 1rem 0 3rem 0;
}
.nav-item {
& + .nav-item a::before {
content: "•";
width: 1rem;
display: inline-block;
margin-left: -1.15rem;
}
&:first-child {
margin-left: -1rem;
}
}
}
</style> </style>

View file

@ -0,0 +1,145 @@
<template>
<div id="console">
<b-list-group>
<!-- HISTORY BAR -->
<b-list-group-item class="d-flex align-items-center" :class="{ 'bg-best text-white': open }">
<h6 class="m-0">
<icon iname="history" /> {{ $t('history.title') }}
</h6>
<div class="ml-auto">
<!-- LAST ACTION -->
<small v-if="lastAction">
<u v-t="'history.last_action'" />
{{ lastAction.uri | readableUri }} ({{ $t('history.methods.' + lastAction.method) }})
</small>
<b-button
v-b-toggle:collapse
class="ml-2 px-1 py-0" size="sm" :variant="open ? 'light' : 'outline-dark'"
>
<icon iname="chevron-right" /><span class="sr-only">{{ $t('words.collapse') }}</span>
</b-button>
</div>
</b-list-group-item>
<!-- ACTION LIST -->
<b-collapse id="collapse" v-model="open">
<b-list-group-item class="p-0" id="history" ref="history">
<!-- ACTION -->
<b-list-group v-for="(action, i) in history" :key="i" flush>
<!-- ACTION DESC -->
<b-list-group-item class="sticky-top d-flex align-items-center" variant="dark">
<div>
<strong>{{ $t('action') }}:</strong>
{{ action.uri | readableUri }}
<small>({{ $t('history.methods.' + action.method) }})</small>
</div>
<time :datetime="action.date | hour" class="ml-auto">{{ action.date | hour }}</time>
</b-list-group-item>
<!-- ACTION MESSAGE -->
<b-list-group-item v-for="({ type, text }, j) in action.messages" :key="j">
<icon iname="comment" :class="'text-' + type" /> <span v-html="text" />
</b-list-group-item>
</b-list-group>
</b-list-group-item>
</b-collapse>
</b-list-group>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'YnhConsole',
props: {
value: { type: Boolean, default: false },
height: { type: [Number, String], default: 30 }
},
data () {
return {
open: false
}
},
watch: {
async open (value) {
// In case it is needed.
this.$emit('toggle', value)
if (!value) {
// Reset footer padding.
this.$emit('height-changed', 0)
} else {
// Wait for DOM update.
await this.$nextTick()
// Send history's elem height so the footer can update its padding.
this.$emit('height-changed', this.$refs.history.clientHeight)
// Scroll to the last action.
const lastActionItem = document.querySelector('#history > .list-group:last-of-type')
if (lastActionItem) {
lastActionItem.scrollIntoView()
}
}
},
'lastAction.messages' (a, b) {
if (!this.open) return
this.$nextTick(() => {
document.querySelector('#history > .list-group:last-of-type').scrollIntoView()
})
}
},
computed: mapGetters(['history', 'lastAction']),
filters: {
readableUri (uri) {
return uri.split('?')[0].replace('/', ' > ')
},
hour (date) {
return new Date(date).toLocaleTimeString()
}
}
}
</script>
<style lang="scss" scoped>
#console {
position: sticky;
z-index: 10;
bottom: 0;
margin-left: -1.5rem;
width: calc(100% + 3rem);
@include media-breakpoint-down(xs) {
margin-left: -15px;
width: calc(100% + 30px);
& > .list-group {
border-radius: 0;
}
}
}
#history {
overflow-y: auto;
max-height: 30vh;
}
#collapse {
// disable collapse animation
transition: none !important;
}
.list-group-item {
font-size: $font-size-sm;
padding: $tooltip-padding-y $tooltip-padding-x;
}
</style>

View file

@ -7,6 +7,7 @@
"all": "All", "all": "All",
"all_apps": "All apps", "all_apps": "All apps",
"api_not_responding": "The YunoHost API is not responding. Maybe 'yunohost-api' is down or got restarted?", "api_not_responding": "The YunoHost API is not responding. Maybe 'yunohost-api' is down or got restarted?",
"api_waiting": "Waiting for the server's response...",
"app_actions": "Actions", "app_actions": "Actions",
"app_actions_label": "Perform actions", "app_actions_label": "Perform actions",
"app_change_label": "Change Label", "app_change_label": "Change Label",
@ -166,6 +167,16 @@
"groups_and_permissions": "Groups and permissions", "groups_and_permissions": "Groups and permissions",
"groups_and_permissions_manage": "Manage groups and permissions", "groups_and_permissions_manage": "Manage groups and permissions",
"permissions": "Permissions", "permissions": "Permissions",
"history": {
"title": "History",
"last_action": "Last action:",
"action": "Action:",
"methods": {
"POST": "create/execute",
"PUT": "modify",
"DELETE": "delete"
}
},
"home": "Home", "home": "Home",
"hook_adminjs_group_configuration": "System configurations", "hook_adminjs_group_configuration": "System configurations",
"hook_conf_cron": "Automatic tasks", "hook_conf_cron": "Automatic tasks",
@ -245,6 +256,7 @@
"passwords_dont_match": "Passwords don't match", "passwords_dont_match": "Passwords don't match",
"passwords_too_short": "Password is too short", "passwords_too_short": "Password is too short",
"path": "Path", "path": "Path",
"perform": "Perform",
"placeholder": { "placeholder": {
"username": "johndoe", "username": "johndoe",
"firstname": "John", "firstname": "John",
@ -294,6 +306,7 @@
"restore": "Restore", "restore": "Restore",
"restart": "Restart", "restart": "Restart",
"run": "Run", "run": "Run",
"running": "Running",
"save": "Save", "save": "Save",
"search": { "search": {
"domain": "Search for domains...", "domain": "Search for domains...",

View file

@ -17,6 +17,10 @@
// Style overrides happens after dependencies imports // Style overrides happens after dependencies imports
// Bootstrap overrides // Bootstrap overrides
html, body {
height: 100%;
overflow-x: hidden;
}
body { body {
margin: 0; margin: 0;
font-family: "Source Sans Pro", "Helvetica Neue", "Fira Sans", Helvetica, Arial, sans-serif; font-family: "Source Sans Pro", "Helvetica Neue", "Fira Sans", Helvetica, Arial, sans-serif;
@ -104,7 +108,6 @@ body {
top: 2px; top: 2px;
} }
// Fork-awesome overrides // Fork-awesome overrides
.fa-fw { .fa-fw {
width: 1.25em !important; width: 1.25em !important;