yunohost-admin/app/src/views/_partials/HistoryConsole.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>