diff --git a/resources/assets/scripts/components/core/Icon.ts b/resources/assets/scripts/components/core/Icon.ts index 25c06e51f..242935506 100644 --- a/resources/assets/scripts/components/core/Icon.ts +++ b/resources/assets/scripts/components/core/Icon.ts @@ -9,6 +9,6 @@ export default Vue.component('icon', { replace(); }, template: ` - + `, }); diff --git a/resources/assets/scripts/components/core/Navigation.ts b/resources/assets/scripts/components/core/Navigation.ts index 436ac24a6..a25287199 100644 --- a/resources/assets/scripts/components/core/Navigation.ts +++ b/resources/assets/scripts/components/core/Navigation.ts @@ -32,12 +32,11 @@ export default Vue.component('navigation', { }, methods: { - search: debounce(function (): void { - // @todo why is this not liked? - // if (this.searchTerm.length >= 3) { - // this.loadingResults = true; - // this.gatherSearchResults(); - // } + search: debounce(function (this: any): void { + if (this.searchTerm.length >= 3) { + this.loadingResults = true; + this.gatherSearchResults(); + } }, 500), gatherSearchResults: function (): void { diff --git a/resources/assets/scripts/components/dashboard/Dashboard.ts b/resources/assets/scripts/components/dashboard/Dashboard.ts new file mode 100644 index 000000000..d4e02a281 --- /dev/null +++ b/resources/assets/scripts/components/dashboard/Dashboard.ts @@ -0,0 +1,125 @@ +import Vue from 'vue'; +import { debounce, isObject } from 'lodash'; +import { mapState } from 'vuex'; +import Flash from "./../Flash"; +import Navigation from "./../core/Navigation"; +import {AxiosError} from "axios"; +import ServerBox from "./ServerBox"; + +type DataStructure = { + backgroundedAt: Date, + documentVisible: boolean, + loading: boolean, + servers?: Array, + searchTerm?: string, +} + +export default Vue.component('dashboard', { + components: { + ServerBox, + Navigation, + Flash + }, + + data: function (): DataStructure { + return { + backgroundedAt: new Date(), + documentVisible: true, + loading: false, + } + }, + + /** + * Start loading the servers before the DOM $.el is created. If we already have servers + * stored in vuex shows those and don't fire another API call just to load them again. + */ + created: function () { + if (!this.servers || this.servers.length === 0) { + this.loadServers(); + } + }, + + /** + * Once the page is mounted set a function to run every 10 seconds that will + * iterate through the visible servers and fetch their resource usage. + */ + mounted: function () { + (this.$refs.search as HTMLElement).focus(); + }, + + computed: { + ...mapState('dashboard', ['servers']), + searchTerm: { + get: function (): string { + return this.$store.getters['dashboard/getSearchTerm']; + }, + set: function (value: string): void { + this.$store.dispatch('dashboard/setSearchTerm', value); + }, + }, + }, + + methods: { + /** + * Load the user's servers and render them onto the dashboard. + */ + loadServers: function () { + this.loading = true; + this.$flash.clear(); + + this.$store.dispatch('dashboard/loadServers') + .then(() => { + if (!this.servers || this.servers.length === 0) { + this.$flash.info(this.$t('dashboard.index.no_matches')); + } + }) + .catch((err: AxiosError) => { + console.error(err); + const response = err.response; + if (response && isObject(response.data.errors)) { + response.data.errors.forEach((error: any) => { + this.$flash.error(error.detail); + }); + } + }) + .then(() => this.loading = false); + }, + + /** + * Handle a search for servers but only call the search function every 500ms + * at the fastest. + */ + onChange: debounce(function (this: any): void { + this.loadServers(); + }, 500), + }, + + template: ` + + + + + + + + + + + + + + + + + + ` +}); diff --git a/resources/assets/scripts/components/dashboard/Dashboard.vue b/resources/assets/scripts/components/dashboard/Dashboard.vue deleted file mode 100644 index ae436826d..000000000 --- a/resources/assets/scripts/components/dashboard/Dashboard.vue +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/resources/assets/scripts/components/dashboard/ServerBox.ts b/resources/assets/scripts/components/dashboard/ServerBox.ts new file mode 100644 index 000000000..fdf2df18a --- /dev/null +++ b/resources/assets/scripts/components/dashboard/ServerBox.ts @@ -0,0 +1,194 @@ +import Vue from 'vue'; +import { get } from 'lodash'; +import { differenceInSeconds } from 'date-fns'; +import {AxiosError, AxiosResponse} from "axios"; + +type DataStructure = { + backgroundedAt: Date, + documentVisible: boolean, + resources: null | { [s: string]: any }, + cpu: number, + memory: number, + status: string, + link: { name: string, params: { id: string } }, + dataGetTimeout: undefined | number, +} + +export default Vue.component('server-box', { + props: { + server: { type: Object, required: true }, + }, + + data: function (): DataStructure { + return { + backgroundedAt: new Date(), + documentVisible: true, + resources: null, + cpu: 0, + memory: 0, + status: '', + link: { name: 'server', params: { id: this.server.identifier }}, + dataGetTimeout: undefined, + }; + }, + + watch: { + /** + * Watch the documentVisible item and perform actions when it is changed. If it becomes + * true, we want to check how long ago the last poll was, if it was more than 30 seconds + * we want to immediately trigger the resourceUse api call, otherwise we just want to restart + * the time. + * + * If it is now false, we want to clear the timer that checks resource use, since we know + * we won't be doing anything with them anyways. Might as well avoid extraneous resource + * usage by the browser. + */ + documentVisible: function (value) { + if (!value) { + window.clearTimeout(this.dataGetTimeout); + return; + } + + if (differenceInSeconds(new Date(), this.backgroundedAt) >= 30) { + this.getResourceUse(); + } + + this.dataGetTimeout = window.setInterval(() => { + this.getResourceUse(); + }, 10000); + }, + }, + + /** + * Grab the initial resource usage for this specific server instance and add a listener + * to monitor when this window is no longer visible. We don't want to needlessly poll the + * API when we aren't looking at the page. + */ + created: function () { + this.getResourceUse(); + document.addEventListener('visibilitychange', this._visibilityChange.bind(this)); + }, + + /** + * Poll the API for changes every 10 seconds when the component is mounted. + */ + mounted: function () { + this.dataGetTimeout = window.setInterval(() => { + this.getResourceUse(); + }, 10000); + }, + + /** + * Clear the timer and event listeners when we destroy the component. + */ + beforeDestroy: function () { + window.clearInterval(this.$data.dataGetTimeout); + document.removeEventListener('visibilitychange', this._visibilityChange.bind(this), false); + }, + + methods: { + /** + * Query the resource API to determine what this server's state and resource usage is. + */ + getResourceUse: function () { + window.axios.get(this.route('api.client.servers.resources', { server: this.server.identifier })) + .then((response: AxiosResponse) => { + if (!(response.data instanceof Object)) { + throw new Error('Received an invalid response object back from status endpoint.'); + } + + this.resources = response.data.attributes; + this.status = this.getServerStatus(); + this.memory = parseInt(parseFloat(get(this.resources, 'memory.current', '0')).toFixed(0)); + this.cpu = this._calculateCpu( + parseFloat(get(this.resources, 'cpu.current', '0')), + parseFloat(this.server.limits.cpu) + ); + }) + .catch((err: AxiosError) => console.warn('Error fetching server resource usage', { ...err })); + }, + + /** + * Set the CSS to use for displaying the server's current status. + */ + getServerStatus: function () { + if (!this.resources || !this.resources.installed || this.resources.suspended) { + return ''; + } + + switch (this.resources.state) { + case 'off': + return 'offline'; + case 'on': + case 'starting': + case 'stopping': + return 'online'; + default: + return ''; + } + }, + + /** + * Calculate the CPU usage for a given server relative to their set maximum. + * + * @private + */ + _calculateCpu: function (current: number, max: number) { + if (max === 0) { + return parseFloat(current.toFixed(1)); + } + + return parseFloat((current / max * 100).toFixed(1)); + }, + + /** + * Handle document visibility changes. + * + * @private + */ + _visibilityChange: function () { + this.documentVisible = document.visibilityState === 'visible'; + + if (!this.documentVisible) { + this.backgroundedAt = new Date(); + } + }, + }, + + template: ` + + + + + + + {{ server.name[0] }} + + {{ server.name }} + + + + {{ server.description }} + + + {{ server.node }} + {{ server.allocation.ip }}:{{ server.allocation.port }} + + + + + + + ` +}); diff --git a/resources/assets/scripts/components/dashboard/ServerBox.vue b/resources/assets/scripts/components/dashboard/ServerBox.vue deleted file mode 100644 index 90a816f04..000000000 --- a/resources/assets/scripts/components/dashboard/ServerBox.vue +++ /dev/null @@ -1,195 +0,0 @@ - - - - - - - - {{ server.name[0] }} - - {{ server.name }} - - - - {{ server.description }} - - - {{ server.node }} - {{ server.allocation.ip }}:{{ server.allocation.port }} - - - - - - - - - diff --git a/resources/assets/scripts/mixins/flash.ts b/resources/assets/scripts/mixins/flash.ts index 49ddcb1c0..c90b98641 100644 --- a/resources/assets/scripts/mixins/flash.ts +++ b/resources/assets/scripts/mixins/flash.ts @@ -1,18 +1,19 @@ import {ComponentOptions} from "vue"; import {Vue} from "vue/types/vue"; +import {TranslateResult} from "vue-i18n"; export interface FlashInterface { - flash(message: string, title: string, severity: string): void; + flash(message: string | TranslateResult, title: string, severity: string): void; clear(): void, - success(message: string): void, + success(message: string | TranslateResult): void, - info(message: string): void, + info(message: string | TranslateResult): void, - warning(message: string): void, + warning(message: string | TranslateResult): void, - error(message: string): void, + error(message: string | TranslateResult): void, } class Flash implements FlashInterface { diff --git a/resources/assets/scripts/router.ts b/resources/assets/scripts/router.ts index 9c2973c46..7e76c60c4 100644 --- a/resources/assets/scripts/router.ts +++ b/resources/assets/scripts/router.ts @@ -5,7 +5,7 @@ const route = require('./../../../vendor/tightenco/ziggy/src/js/route').default; // Base Vuejs Templates import Login from './components/auth/Login'; -import Dashboard from './components/dashboard/Dashboard.vue'; +import Dashboard from './components/dashboard/Dashboard'; import Account from './components/dashboard/Account.vue'; import ResetPassword from './components/auth/ResetPassword'; import User from './models/user';
{{ server.description }}