mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
290 lines
7.6 KiB
Vue
290 lines
7.6 KiB
Vue
<template>
|
|
<BCard no-body id="console">
|
|
<!-- HISTORY BAR -->
|
|
<BCardHeader
|
|
role="button"
|
|
tabindex="0"
|
|
:aria-expanded="open ? 'true' : 'false'"
|
|
aria-controls="console-collapse"
|
|
header-tag="header"
|
|
:header-bg-variant="open ? 'best' : 'white'"
|
|
:class="{ 'text-white': open }"
|
|
class="d-flex align-items-center"
|
|
@mousedown.left.prevent="onHistoryBarClick"
|
|
@keyup.space.enter.prevent="onHistoryBarKey"
|
|
>
|
|
<h5 class="m-0">
|
|
<YIcon iname="history" />
|
|
<span class="d-none d-sm-inline font-weight-bold">
|
|
{{ $t('history.title') }}
|
|
</span>
|
|
</h5>
|
|
|
|
<!-- CURRENT/LAST ACTION -->
|
|
<BButton
|
|
v-if="lastAction"
|
|
size="sm"
|
|
pill
|
|
class="ml-auto py-0"
|
|
:variant="open ? 'light' : 'best'"
|
|
@click.prevent="onLastActionClick"
|
|
@keyup.enter.space.prevent="onLastActionClick"
|
|
>
|
|
<small>{{ $t('history.last_action') }}</small>
|
|
</BButton>
|
|
<QueryHeader
|
|
v-if="lastAction"
|
|
:request="lastAction"
|
|
class="w-auto ml-2 xs-hide"
|
|
/>
|
|
</BCardHeader>
|
|
|
|
<BCollapse id="console-collapse" v-model="open">
|
|
<div class="accordion" role="tablist" id="history" ref="history">
|
|
<p v-if="history.length === 0" class="alert m-0 px-2 py-1">
|
|
{{ $t('history.is_empty') }}
|
|
</p>
|
|
|
|
<!-- ACTION LIST -->
|
|
<BCard
|
|
v-for="(action, i) in history"
|
|
:key="i"
|
|
no-body
|
|
class="rounded-0 rounded-top border-left-0 border-right-0"
|
|
>
|
|
<!-- ACTION -->
|
|
<BCardHeader
|
|
header-tag="header"
|
|
header-bg-variant="white"
|
|
class="sticky-top d-flex"
|
|
>
|
|
<!-- ACTION DESC -->
|
|
<QueryHeader
|
|
role="tab"
|
|
v-b-toggle="
|
|
action.messages.length ? 'messages-collapse-' + i : false
|
|
"
|
|
:request="action"
|
|
show-time
|
|
show-error
|
|
/>
|
|
</BCardHeader>
|
|
|
|
<!-- ACTION MESSAGES -->
|
|
<BCollapse
|
|
v-if="action.messages.length"
|
|
:id="'messages-collapse-' + i"
|
|
accordion="my-accordion"
|
|
role="tabpanel"
|
|
@shown="scrollToAction(i)"
|
|
@hide="scrollToAction(i)"
|
|
>
|
|
<MessageListGroup :messages="action.messages" />
|
|
</BCollapse>
|
|
</BCard>
|
|
</div>
|
|
</BCollapse>
|
|
</BCard>
|
|
</template>
|
|
|
|
<script>
|
|
import { mapGetters } from 'vuex'
|
|
|
|
import QueryHeader from '@/components/QueryHeader.vue'
|
|
import MessageListGroup from '@/components/MessageListGroup.vue'
|
|
|
|
export default {
|
|
name: 'HistoryConsole',
|
|
|
|
components: {
|
|
QueryHeader,
|
|
MessageListGroup,
|
|
},
|
|
|
|
props: {
|
|
value: { type: Boolean, default: false },
|
|
height: { type: [Number, String], default: 30 },
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
open: false,
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
...mapGetters(['history', 'lastAction', 'waiting', 'error']),
|
|
},
|
|
|
|
methods: {
|
|
scrollToAction(actionIndex) {
|
|
const actionCard = this.$el.querySelector(
|
|
'#messages-collapse-' + actionIndex,
|
|
).parentElement
|
|
const headerOffset = actionCard.firstElementChild.offsetHeight
|
|
// Can't use `scrollIntoView()` here since it will also scroll in the main content.
|
|
this.$refs.history.scrollTop = actionCard.offsetTop - headerOffset
|
|
},
|
|
|
|
async onLastActionClick() {
|
|
if (!this.open) {
|
|
this.open = true
|
|
await this.$nextTick()
|
|
}
|
|
const historyElem = this.$refs.history
|
|
const lastActionCard = historyElem.lastElementChild
|
|
const lastCollapsable = lastActionCard.querySelector('.collapse')
|
|
|
|
if (lastCollapsable && !lastCollapsable.classList.contains('show')) {
|
|
this.$root.$emit('bv::toggle::collapse', lastCollapsable.id)
|
|
// `scrollToAction` will be triggered and will handle the scrolling.
|
|
} else {
|
|
const headerOffset = lastActionCard.firstElementChild.offsetHeight
|
|
historyElem.scrollTop = lastActionCard.offsetTop - headerOffset
|
|
}
|
|
},
|
|
|
|
onHistoryBarKey(e) {
|
|
// FIXME interactive element in another is not valid, need to find another way.
|
|
if (
|
|
e.target.nodeName === 'BUTTON' ||
|
|
e.target.parentElement.nodeName === 'BUTTON'
|
|
)
|
|
return
|
|
this.open = !this.open
|
|
},
|
|
|
|
onHistoryBarClick(e) {
|
|
// FIXME interactive element in another is not valid, need to find another way.
|
|
if (
|
|
e.target.nodeName === 'BUTTON' ||
|
|
e.target.parentElement.nodeName === 'BUTTON'
|
|
)
|
|
return
|
|
|
|
const historyElem = this.$refs.history
|
|
let mousePos = e.clientY
|
|
|
|
const onMouseMove = ({ clientY }) => {
|
|
if (!this.open) {
|
|
historyElem.style.height = '0px'
|
|
this.open = true
|
|
}
|
|
const currentHeight = historyElem.offsetHeight
|
|
const move = mousePos - clientY
|
|
const nextSize = currentHeight + move
|
|
if (nextSize < 10 && nextSize < currentHeight) {
|
|
// Close the console and reset its size if the user reduce it to less than 10px.
|
|
mousePos = e.clientY
|
|
historyElem.style.height = ''
|
|
onMouseUp()
|
|
} else {
|
|
historyElem.style.height = nextSize + 'px'
|
|
// Simulate scroll when reducing the box so the content doesn't move.
|
|
if (nextSize < currentHeight) {
|
|
historyElem.scrollBy(0, -move)
|
|
}
|
|
mousePos = clientY
|
|
}
|
|
}
|
|
|
|
// Delay the mouse move listener to distinguish a click from a drag.
|
|
const listenToMouseMove = setTimeout(() => {
|
|
historyElem.style.height = historyElem.offsetHeight + 'px'
|
|
historyElem.classList.add('no-max')
|
|
window.addEventListener('mousemove', onMouseMove)
|
|
}, 200)
|
|
|
|
const onMouseUp = () => {
|
|
// Toggle opening if no mouse movement.
|
|
if (mousePos === e.clientY) {
|
|
// remove the free height class if the box's height is not custom
|
|
if (!historyElem.style.height) {
|
|
historyElem.classList.remove('no-max')
|
|
}
|
|
this.open = !this.open
|
|
}
|
|
clearTimeout(listenToMouseMove)
|
|
window.removeEventListener('mousemove', onMouseMove)
|
|
window.removeEventListener('mouseup', onMouseUp)
|
|
}
|
|
|
|
window.addEventListener('mouseup', onMouseUp)
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
// reset default style
|
|
.card + .card {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.card-header {
|
|
padding: $tooltip-padding-y $tooltip-padding-x;
|
|
}
|
|
|
|
#console {
|
|
position: sticky;
|
|
z-index: 15;
|
|
bottom: 0;
|
|
|
|
width: calc(100% + 3rem);
|
|
margin-left: -1.5rem;
|
|
border-bottom: 0;
|
|
border-bottom-right-radius: 0;
|
|
border-bottom-left-radius: 0;
|
|
font-size: $font-size-sm;
|
|
|
|
& > header {
|
|
cursor: ns-resize;
|
|
}
|
|
|
|
.btn {
|
|
height: 1.25rem;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
@include media-breakpoint-down(xs) {
|
|
margin-left: -15px;
|
|
width: calc(100% + 30px);
|
|
|
|
& > .card-header {
|
|
border-radius: 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Hacky disable of collapse animation
|
|
.collapsing {
|
|
transition: none !important;
|
|
height: auto !important;
|
|
display: block !important;
|
|
position: static !important;
|
|
}
|
|
|
|
#history {
|
|
overflow-y: auto;
|
|
max-height: 20vh;
|
|
|
|
&.no-max {
|
|
max-height: none;
|
|
}
|
|
|
|
> .card {
|
|
// reset bootstrap's `overflow: hidden` that prevent sticky headers to work properly.
|
|
overflow: visible;
|
|
|
|
&:first-of-type {
|
|
// hide first top border that conflicts with the console header's bottom border.
|
|
margin-top: -1px;
|
|
}
|
|
}
|
|
|
|
[aria-controls] {
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
</style>
|