diff --git a/front/src/App.vue b/front/src/App.vue index 673f8386460ecba32737c129e3421adc06881f04..91cf29843429eb06e55d1aefd861b7eb3f81f1b4 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -1,6 +1,7 @@ <template> <div id="app"> <sidebar></sidebar> + <service-messages v-if="messages.length > 0" /> <router-view :key="$route.fullPath"></router-view> <div class="ui fitted divider"></div> <div id="footer" class="ui vertical footer segment"> @@ -44,9 +45,11 @@ <script> import axios from 'axios' import _ from 'lodash' +import {mapState} from 'vuex' import Sidebar from '@/components/Sidebar' import Raven from '@/components/Raven' +import ServiceMessages from '@/components/ServiceMessages' import PlaylistModal from '@/components/playlists/PlaylistModal' @@ -55,7 +58,8 @@ export default { components: { Sidebar, Raven, - PlaylistModal + PlaylistModal, + ServiceMessages }, data () { return { @@ -80,6 +84,9 @@ export default { } }, computed: { + ...mapState({ + messages: state => state.ui.messages + }), version () { if (!this.nodeinfo) { return null @@ -115,6 +122,14 @@ html, body { } transform: none !important; } +.service-messages { + position: fixed; + bottom: 1em; + left: 1em; + @include media(">desktop") { + left: 350px; + } +} .main-pusher { padding: 1.5rem 0; } diff --git a/front/src/components/ServiceMessages.vue b/front/src/components/ServiceMessages.vue new file mode 100644 index 0000000000000000000000000000000000000000..90f6f06791785d5fb675f84c7a21647b2077178f --- /dev/null +++ b/front/src/components/ServiceMessages.vue @@ -0,0 +1,83 @@ +<template> + <div class="service-messages"> + <message v-for="message in displayedMessages" :key="String(message.date)" :class="['large', getLevel(message)]"> + <p>{{ message.content }}</p> + </message> + </div> +</template> + +<script> +import _ from 'lodash' +import {mapState} from 'vuex' + +export default { + data () { + return { + date: new Date(), + interval: null + } + }, + created () { + this.setupInterval() + }, + destroyed () { + if (this.interval) { + clearInterval(this.interval) + } + }, + computed: { + ...mapState({ + messages: state => state.ui.messages, + displayDuration: state => state.ui.messageDisplayDuration + }), + displayedMessages () { + let now = this.date + let interval = this.displayDuration + let toDisplay = this.messages.filter(m => { + return now - m.date <= interval + }) + return _.reverse(toDisplay).slice(0, 5) + } + }, + methods: { + setupInterval () { + if (this.interval) { + return + } + let self = this + this.interval = setInterval(() => { + if (self.displayedMessages.length === 0) { + clearInterval(self.interval) + this.interval = null + } + self.date = new Date() + }, 1000) + }, + getLevel (message) { + return message.level || 'blue' + } + }, + watch: { + messages: { + handler (v) { + if (v.length > 0 && !this.interval) { + this.setupInterval() + } + }, + deep: true + } + } +} +</script> + +<style> +.service-messages { + z-index: 9999; + margin-left: 1em; + min-width: 20em; + max-width: 40em; +} +.service-messages .message:last-child { + margin-bottom: 0; +} +</style> diff --git a/front/src/components/common/Message.vue b/front/src/components/common/Message.vue new file mode 100644 index 0000000000000000000000000000000000000000..772071db78c98881dab84405b2bc0903beff8106 --- /dev/null +++ b/front/src/components/common/Message.vue @@ -0,0 +1,36 @@ +<template> + <div class="ui message"> + <div class="content"> + <slot></slot> + </div> + <i class="close icon"></i> + </div> +</template> +<script> +import $ from 'jquery' + +export default { + mounted () { + let self = this + $(this.$el).find('.close.icon').on('click', function () { + $(self.$el).transition('fade', 125) + }) + $(this.$el).on('click', function () { + $(self.$el).transition('fade', 125) + }) + } +} +</script> +<style scoped> +.ui.message .content { + padding-right: 0.5em; + cursor: pointer; +} +.ui.message .content :first-child { + margin-top: 0; +} + +.ui.message .content :last-child { + margin-bottom: 0; +} +</style> diff --git a/front/src/components/globals.js b/front/src/components/globals.js index 79bbcf1b93a4a74ecf3c3b3d3d4e724870d7120c..4ad09f70425a987fd99e86f6e4277cd07d797605 100644 --- a/front/src/components/globals.js +++ b/front/src/components/globals.js @@ -12,4 +12,8 @@ import DangerousButton from '@/components/common/DangerousButton' Vue.component('dangerous-button', DangerousButton) +import Message from '@/components/common/Message' + +Vue.component('message', Message) + export default {} diff --git a/front/src/store/ui.js b/front/src/store/ui.js index f0935e491bb1a48abc5f90d3482e3d5eab7ee9a5..be744afe51ad954a4bae722f9442a9d71ad85730 100644 --- a/front/src/store/ui.js +++ b/front/src/store/ui.js @@ -2,11 +2,20 @@ export default { namespaced: true, state: { - lastDate: new Date() + lastDate: new Date(), + maxMessages: 100, + messageDisplayDuration: 10000, + messages: [] }, mutations: { computeLastDate: (state) => { state.lastDate = new Date() + }, + addMessage (state, message) { + state.messages.push(message) + if (state.messages.length > state.maxMessages) { + state.messages.shift() + } } } } diff --git a/front/test/unit/specs/store/ui.spec.js b/front/test/unit/specs/store/ui.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..adcfa87d8f34bd600f113fc4100c0c6318bc730e --- /dev/null +++ b/front/test/unit/specs/store/ui.spec.js @@ -0,0 +1,18 @@ +import store from '@/store/ui' + +import { testAction } from '../../utils' + +describe('store/ui', () => { + describe('mutations', () => { + it('addMessage', () => { + const state = {maxMessages: 100, messages: []} + store.mutations.addMessage(state, 'hello') + expect(state.messages).to.deep.equal(['hello']) + }) + it('addMessage', () => { + const state = {maxMessages: 1, messages: ['hello']} + store.mutations.addMessage(state, 'world') + expect(state.messages).to.deep.equal(['world']) + }) + }) +})