dashboard/shell/components/Inactivity.vue

241 lines
6.8 KiB
Vue

<script>
import ModalWithCard from '@shell/components/ModalWithCard';
import { Banner } from '@components/Banner';
import PercentageBar from '@shell/components/PercentageBar.vue';
import throttle from 'lodash/throttle';
import { MANAGEMENT } from '@shell/config/types';
import { DEFAULT_PERF_SETTING, SETTING } from '@shell/config/settings';
let globalId;
export default {
name: 'Inactivity',
components: {
ModalWithCard, Banner, PercentageBar
},
data() {
return {
enabled: null,
isOpen: false,
isInactive: false,
showModalAfter: null,
inactivityTimeoutId: null,
courtesyTimer: null,
courtesyTimerId: null,
courtesyCountdown: null,
trackInactivity: throttle(this._trackInactivity, 1000),
id: null,
};
},
async mounted() {
// Info: normally, this is done in the fetch hook but for some reasons while awaiting for things that will take a while, it won't be ready by the time mounted() is called, pending for investigation.
let settings;
try {
const settingsString = await this.$store.dispatch('management/find', { type: MANAGEMENT.SETTING, id: SETTING.UI_PERFORMANCE });
settings = settingsString?.value ? JSON.parse(settingsString.value) : DEFAULT_PERF_SETTING;
} catch { }
if (!settings || !settings?.inactivity || !settings?.inactivity.enabled) {
return;
}
this.enabled = settings?.inactivity?.enabled || false;
// Total amount of time before the user's session is lost
const thresholdToSeconds = settings?.inactivity?.threshold * 60;
// Amount of time the user sees the inactivity warning
this.courtesyTimer = Math.floor(thresholdToSeconds * 0.1);
this.courtesyTimer = Math.min(this.courtesyTimer, 60 * 5); // Never show the modal more than 5 minutes
// Amount of time before the user sees the inactivity warning
// Note - time before warning is shown + time warning is shown = settings threshold (total amount of time)
this.showModalAfter = thresholdToSeconds - this.courtesyTimer;
console.debug(`Inactivity modal will show after ${ this.showModalAfter / 60 }(m) and be shown for ${ this.courtesyTimer / 60 }(m)`); // eslint-disable-line no-console
this.courtesyCountdown = this.courtesyTimer;
if (settings?.inactivity.enabled) {
this.trackInactivity();
this.addIdleListeners();
}
},
beforeDestroy() {
this.removeEventListener();
this.clearAllTimeouts();
},
methods: {
_trackInactivity() {
if (this.isInactive || this.isOpen || !this.showModalAfter) {
return;
}
this.clearAllTimeouts();
const endTime = Date.now() + this.showModalAfter * 1000;
this.id = endTime;
globalId = endTime;
const checkInactivityTimer = () => {
const now = Date.now();
if (this.id !== globalId) {
return;
}
if (now >= endTime) {
this.isOpen = true;
this.startCountdown();
this.$modal.show('inactivityModal');
} else {
this.inactivityTimeoutId = setTimeout(checkInactivityTimer, 1000);
}
};
checkInactivityTimer();
},
startCountdown() {
const endTime = Date.now() + (this.courtesyCountdown * 1000);
const checkCountdown = () => {
const now = Date.now();
if (now >= endTime) {
this.isInactive = true;
this.unsubscribe();
this.clearAllTimeouts();
} else {
this.courtesyCountdown = Math.floor((endTime - now) / 1000);
this.courtesyTimerId = setTimeout(checkCountdown, 1000);
}
};
checkCountdown();
},
addIdleListeners() {
document.addEventListener('mousemove', this.trackInactivity);
document.addEventListener('mousedown', this.trackInactivity);
document.addEventListener('keypress', this.trackInactivity);
document.addEventListener('touchmove', this.trackInactivity);
document.addEventListener('visibilitychange', this.trackInactivity);
},
removeEventListener() {
document.removeEventListener('mousemove', this.trackInactivity);
document.removeEventListener('mousedown', this.trackInactivity);
document.removeEventListener('keypress', this.trackInactivity);
document.removeEventListener('touchmove', this.trackInactivity);
document.removeEventListener('visibilitychange', this.trackInactivity);
},
resume() {
this.isInactive = false;
this.isOpen = false;
this.courtesyCountdown = this.courtesyTimer;
this.clearAllTimeouts();
this.$modal.hide('inactivityModal');
},
refresh() {
window.location.reload();
},
unsubscribe() {
console.debug('Unsubscribing from all websocket events'); // eslint-disable-line no-console
this.$store.dispatch('unsubscribe');
},
clearAllTimeouts() {
clearTimeout(this.inactivityTimeoutId);
clearTimeout(this.courtesyTimerId);
}
},
computed: {
isInactiveTexts() {
return this.isInactive ? {
title: this.t('inactivity.titleExpired'),
banner: this.t('inactivity.bannerExpired'),
content: this.t('inactivity.contentExpired'),
} : {
title: this.t('inactivity.title'),
banner: this.t('inactivity.banner'),
content: this.t('inactivity.content'),
};
},
timerPercentageLeft() {
return Math.floor((this.courtesyCountdown / this.courtesyTimer ) * 100);
},
colorStops() {
return {
0: '--info', 30: '--info', 70: '--info'
};
},
}
};
</script>
<template>
<ModalWithCard
ref="inactivityModal"
name="inactivityModal"
save-text="Continue"
:v-if="isOpen"
@finish="resume"
>
<template #title>
{{ isInactiveTexts.title }}
</template>
<span>{{ courtesyCountdown }}</span>
<template #content>
<Banner color="info">
{{ isInactiveTexts.banner }}
</Banner>
<p>
{{ isInactiveTexts.content }}
</p>
<PercentageBar
v-if="!isInactive"
class="mt-20"
:value="timerPercentageLeft"
:color-stops="colorStops"
/>
</template>
<template
#footer
>
<div class="card-actions">
<button
v-if="!isInactive"
class="btn role-tertiary bg-primary"
@click.prevent="resume"
>
<t k="inactivity.cta" />
</button>
<button
v-if="isInactive"
class="btn role-tertiary bg-primary"
@click.prevent="refresh"
>
<t k="inactivity.ctaExpired" />
</button>
</div>
</template>
</ModalWithCard>
</template>
<style lang="scss" scoped>
.card-actions {
display: flex;
width: 100%;
justify-content: flex-end;
}
</style>