diff --git a/dashboard/src/components/DiskUsageItem.vue b/dashboard/src/components/DiskUsageItem.vue index 2d8a1f790..28fa68cf2 100644 --- a/dashboard/src/components/DiskUsageItem.vue +++ b/dashboard/src/components/DiskUsageItem.vue @@ -3,36 +3,18 @@ import { ref, onUnmounted } from 'vue'; import { Button } from '@cloudron/pankow'; import { prettyDecimalSize } from '@cloudron/pankow/utils'; +import AppsModel from '../models/AppsModel.js'; +import VolumesModel from '../models/VolumesModel.js'; import SystemModel from '../models/SystemModel.js'; +const appsModel = AppsModel.create(); +const volumesModel = VolumesModel.create(); const systemModel = SystemModel.create(); const props = defineProps({ filesystem: Object }); -// https://stackoverflow.com/questions/1484506/random-color-generator -function rainbow(numOfSteps, step) { - // This function generates vibrant, "evenly spaced" colours (i.e. no clustering). This is ideal for creating easily distinguishable vibrant markers in Google Maps and other apps. - // Adam Cole, 2011-Sept-14 - // HSV to RBG adapted from: http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript - var r, g, b; - var h = step / numOfSteps; - var i = ~~(h * 6); - var f = h * 6 - i; - var q = 1 - f; - switch(i % 6){ - case 0: r = 1; g = f; b = 0; break; - case 1: r = q; g = 1; b = 0; break; - case 2: r = 0; g = 1; b = f; break; - case 3: r = 0; g = q; b = 1; break; - case 4: r = f; g = 0; b = 1; break; - case 5: r = 1; g = 0; b = q; break; - } - var c = '#' + ('00' + (~ ~(r * 255)).toString(16)).slice(-2) + ('00' + (~ ~(g * 255)).toString(16)).slice(-2) + ('00' + (~ ~(b * 255)).toString(16)).slice(-2); - return (c); -} - // https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array function shuffle(a) { for (let i = a.length - 1; i > 0; i--) { @@ -42,109 +24,49 @@ function shuffle(a) { return a; } +function hue(numOfSteps, step) { + const deg = 360/numOfSteps; + return `hsl(${deg*step} 70% 50%)`; +} + let colorIndex = 0; let colors = []; function resetColors(n) { - colorIndex = 0; + colorIndex = 7; colors = []; - for (let i = 0; i < n; i++) colors.push(rainbow(n, i)); - shuffle(colors); + for (let i = 0; i < n; i++) colors.push(hue(n, i)); } function getNextColor() { return colors[colorIndex++]; } -// async function refresh() { -// let [error, result] = await appsModel.list(); -// if (error) return console.error(error); - -// const appsById = {}; -// result.forEach(a => { appsById[a.id] = a; }); - -// [error, result] = await volumesModel.list(); -// if (error) return console.error(error); - -// const volumesById = {}; -// result.forEach(v => { volumesById[v.id] = v; }); - -// [error, result] = await systemModel.diskUsage(); -// if (error) return console.error(error); - -// lastUpdated.value = result.ts; - -// // [ { filesystem, type, size, used, available, capacity, mountpoint }] -// disks.value = Object.keys(result.filesystems).map(k => result.filesystems[k]); // convert object to array... - -// disks.value.forEach(disk => { -// let usageOther = disk.used; - -// resetColors(disk.contents.length); - -// // if this disk is a volume amend it and remove it from contents -// disk.contents.forEach(function (content) { if (content.path === disk.mountpoint) disk.volume = volumesById[content.id]; }); -// disk.contents = disk.contents.filter(function (content) { return content.path !== disk.mountpoint; }); - -// // only show old backups if the size is significant -// disk.contents = disk.contents.filter(function (content) { return content.id !== 'cloudron-backup-default' || content.usage > 1024*1024*1024; }); - -// disk.contents.forEach(function (content) { -// content.color = getNextColor(); - -// if (content.type === 'app') { -// content.app = appsById[content.id]; -// if (!content.app) content.uninstalled = true; -// else content.label = content.app.label || content.app.fqdn; -// } else if (content.type === 'volume') { -// content.volume = volumesById[content.id]; -// content.label = content.volume ? content.volume.name : 'Removed volume'; -// } - -// // ensure a label for ui -// content.label = content.label || content.id; - -// usageOther -= content.usage; -// }); - -// disk.contents.sort((x, y) => { return y.usage - x.usage; }); // sort by usage - -// if (disks.value[0] === disk) { // the root mount point is the first disk. keep this 'contains' in the end -// disk.contents.push({ -// type: 'standard', -// label: 'Everything else (Ubuntu, etc)', -// id: 'other', -// color: '#555555', -// usage: usageOther -// }); -// } else { -// disk.contents.push({ -// type: 'standard', -// label: 'Used', -// id: 'other', -// color: '#555555', -// usage: usageOther -// }); -// } -// }); -// } - const isExpanded = ref(false); const percent = ref(0); const contents = ref([]); const speed = ref(-1); +const highlight = ref(null); let eventSource = null; -async function onExpand() { - if (isExpanded.value) return; - - resetColors(50); - - isExpanded.value = true; - - const [error, result] = await systemModel.filesystemUsage(props.filesystem.filesystem); +async function refresh() { + let [error, result] = await appsModel.list(); if (error) return console.error(error); + const appsById = {}; + result.forEach(a => { appsById[a.id] = a; }); + + [error, result] = await volumesModel.list(); + if (error) return console.error(error); + + const volumesById = {}; + result.forEach(v => { volumesById[v.id] = v; }); + + [error, result] = await systemModel.filesystemUsage(props.filesystem.filesystem); + if (error) return console.error(error); + + contents.value = []; + eventSource = result; eventSource.addEventListener('message', function (message) { @@ -152,16 +74,44 @@ async function onExpand() { if (payload.type === 'done') { percent.value = 100; + + // we first 8 colors are reserved for known system contents + resetColors(contents.value.length + 8); + contents.value.forEach(content => { + // assign fixed colors for known entries + if (content.id === 'platformdata') content.color = colors[0]; + else if (content.id === 'boxdata') content.color = colors[1]; + else if (content.id === 'maildata') content.color = colors[2]; + else if (content.id === 'cloudron-backup-default') content.color = colors[3]; + else if (content.id === 'docker') content.color = colors[4]; + else if (content.id === 'docker-volumes') content.color = colors[5]; + else if (content.id === '/apps.swap') content.color = colors[6]; + else if (content.id === 'os') content.color = colors[7]; + else content.color = getNextColor(); + }); + contents.value.sort((a, b) => b.usage - a.usage); + eventSource.close(); } else if (payload.type === 'progress') { percent.value = payload.percent; } else { - if (payload.speed) speed.value = payload.speed; - else if (payload.content) { - payload.content.color = getNextColor(); + if (payload.speed) { + speed.value = payload.speed; + } else if (payload.content) { + if (payload.content.type === 'app') { + payload.content.app = appsById[payload.content.id]; + if (!payload.content.app) payload.content.uninstalled = true; + else payload.content.label = payload.content.app.label || payload.content.app.fqdn; + } else if (payload.content.type === 'volume') { + payload.content.volume = volumesById[payload.content.id]; + payload.content.label = payload.content.volume ? `Volume ${payload.content.volume.name}` : 'Removed volume'; + } else { + payload.content.label = payload.content.id; + } contents.value.push(payload.content); + } else { + console.error('Unkown data', payload); } - else console.error('Unkown data', payload); } }); @@ -171,45 +121,55 @@ async function onExpand() { }); } +async function onExpand() { + if (isExpanded.value) return; + + isExpanded.value = true; + + refresh(); +} + onUnmounted(() => { if (eventSource) eventSource.close(); }); - @@ -240,10 +200,17 @@ onUnmounted(() => { } .disk-item-title { + display: flex; + justify-content: space-between; font-weight: bold; font-size: 18px; } +.disk-item-type-and-speed { + display: flex; + justify-content: space-between; +} + .usage-label { display: flex; justify-content: space-between; @@ -256,6 +223,7 @@ onUnmounted(() => { border-radius: var(--pankow-border-radius); height: 12px; overflow: hidden; + margin-bottom: 12px; } .disk-used { @@ -263,10 +231,16 @@ onUnmounted(() => { background-color: var(--pankow-color-primary); white-space: nowrap; height: 12px; + min-width: 2px; + transition: all 250ms; } .disk-used.highlight { - transform: scale(1.2); + transform: scaleY(2); +} + +.disk-used-busy { + min-width: 0; } .disk-used-busy::after { @@ -297,4 +271,8 @@ onUnmounted(() => { vertical-align: middle; } +tr.highlight { + font-weight: bold; +} + diff --git a/src/system.js b/src/system.js index c088f240b..8a13d292f 100644 --- a/src/system.js +++ b/src/system.js @@ -259,7 +259,7 @@ class FilesystemUsageTask extends AsyncTask { this.emitData({ content }); } - if (mountpoint === '/') this.emitData({ type: 'standard', id: 'os', usage: used-usage }); + if (mountpoint === '/') this.emitData({ content: { type: 'standard', id: 'os', usage: used-usage }}); } }