2025-07-17 17:06:37 +02:00
< script setup >
2025-07-17 19:27:09 +02:00
import { prettyDecimalSize } from '@cloudron/pankow/utils' ;
import SystemModel from '../models/SystemModel.js' ;
const systemModel = SystemModel . create ( ) ;
2025-07-17 17:06:37 +02:00
const props = defineProps ( {
filesystem : Object
} ) ;
2025-07-17 19:27:09 +02:00
// 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 ) ;
}
2025-07-17 17:06:37 +02:00
2025-07-17 19:27:09 +02:00
// https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array
function shuffle ( a ) {
for ( let i = a . length - 1 ; i > 0 ; i -- ) {
const j = Math . floor ( Math . random ( ) * ( i + 1 ) ) ;
[ a [ i ] , a [ j ] ] = [ a [ j ] , a [ i ] ] ;
}
return a ;
}
2025-07-17 17:06:37 +02:00
2025-07-17 19:27:09 +02:00
let colorIndex = 0 ;
let colors = [ ] ;
function resetColors ( n ) {
colorIndex = 0 ;
colors = [ ] ;
for ( let i = 0 ; i < n ; i ++ ) colors . push ( rainbow ( n , i ) ) ;
shuffle ( colors ) ;
}
2025-07-17 17:06:37 +02:00
2025-07-17 19:27:09 +02:00
function getNextColor ( ) {
return colors [ colorIndex ++ ] ;
}
2025-07-17 17:06:37 +02:00
// 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
// });
// }
// });
// }
2025-07-17 19:27:09 +02:00
import { ref } from 'vue' ;
const isExpanded = ref ( false ) ;
const percent = ref ( 0 ) ;
const contents = ref ( [ ] ) ;
const speed = ref ( - 1 ) ;
async function onExpand ( ) {
if ( isExpanded . value ) return ;
resetColors ( 50 ) ;
isExpanded . value = true ;
const [ error , eventSource ] = await systemModel . filesystemUsage ( props . filesystem . filesystem ) ;
if ( error ) return console . error ( error ) ;
eventSource . addEventListener ( 'message' , function ( message ) {
const payload = JSON . parse ( message . data ) ;
if ( payload . type === 'done' ) {
percent . value = 100 ;
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 ( ) ;
contents . value . push ( payload . content ) ;
}
else console . error ( 'Unkown data' , payload ) ;
}
} ) ;
eventSource . addEventListener ( 'error' , function ( error ) {
console . log ( 'error: errored' , error ) ;
eventSource . close ( ) ;
} ) ;
}
2025-07-17 17:06:37 +02:00
< / script >
< template >
2025-07-17 19:27:09 +02:00
< div class = "disk-item" : style = "{ cursor: isExpanded ? null : 'pointer' }" @click ="onExpand()" >
2025-07-17 17:06:37 +02:00
< div class = "disk-item-title" > { { filesystem . mountpoint } } < / div >
< div > { { filesystem . type } } < / div >
< div class = "usage-label" >
< div > { { prettyDecimalSize ( filesystem . size ) } } total < / div >
< div > { { prettyDecimalSize ( filesystem . available ) } } available < / div >
< / div >
2025-07-17 19:27:09 +02:00
< div class = "disk-size" v-if = "!isExpanded" >
2025-07-17 17:06:37 +02:00
< div class = "disk-used" : style = "{ width: parseInt(filesystem.capacity*100) + '%' }" > < / div >
< / div >
2025-07-17 19:27:09 +02:00
< div class = "disk-size" v-else-if = "percent < 100" >
< div class = "disk-used disk-used-busy" > < / div >
< / div >
< div class = "disk-size" v-else >
< div class = "disk-used" v-for = "content in contents" :key="content.id" :style="{ 'background-color': content.color, width: 100*content.usage/filesystem.size + '%' }" > < / div >
< / div >
< div v-if = "isExpanded" >
< div v-if = "percent < 100" style="text-align: center;" > Calculating disk usage... {{ percent }} % < / div >
< div v-else >
< table style = "width: 100%" >
< tr v-for = "content in contents" :key="content.id" >
2025-07-17 19:44:55 +02:00
< td style = "width: 20px" > < div class = "content-color-indicator" : style = "{ backgroundColor: content.color }" > < / div > < / td >
2025-07-17 19:27:09 +02:00
< td > { { content . id } } < / td >
< td style = "text-align: right" > { { prettyDecimalSize ( content . usage ) } } < / td >
< / tr >
< / table >
2025-07-17 17:06:37 +02:00
< / div >
< / div >
< / div >
< / template >
< style scoped >
. disk - item {
position : relative ;
display : flex ;
flex - direction : column ;
flex - grow : 1 ;
margin : 10 px ;
max - width : 620 px ;
padding : 10 px 16 px ;
overflow : hidden ;
border - radius : 10 px ;
background - color : var ( -- card - background ) ;
}
. disk - item : focus ,
. disk - item : hover {
box - shadow : 0 px 2 px 5 px rgba ( 0 , 0 , 0 , 0.1 ) ;
background - color : var ( -- pankow - color - background - hover ) ! important ;
text - decoration : none ;
}
. disk - item > div {
margin : 6 px 0 ;
}
. disk - item - title {
font - weight : bold ;
font - size : 18 px ;
}
. usage - label {
display : flex ;
justify - content : space - between ;
}
. disk - size {
2025-07-17 19:27:09 +02:00
display : flex ;
position : relative ;
2025-07-17 17:06:37 +02:00
background - color : white ;
border - radius : calc ( var ( -- pankow - border - radius ) / 1.5 ) ;
2025-07-17 19:27:09 +02:00
height : 6 px ;
overflow : hidden ;
2025-07-17 17:06:37 +02:00
}
. disk - used {
2025-07-17 19:27:09 +02:00
display : inline - block ;
2025-07-17 17:06:37 +02:00
background - color : var ( -- pankow - color - primary ) ;
white - space : nowrap ;
height : 6 px ;
}
2025-07-17 19:27:09 +02:00
. disk - used - busy : : after {
content : '' ;
width : 90 % ;
height : 100 % ;
background : var ( -- pankow - color - primary ) ;
position : absolute ;
top : 0 ;
left : 0 ;
box - sizing : border - box ;
animation : pankow - progress - bar - indeterminate - animation 1.5 s ease - in - out infinite ;
border - radius : calc ( var ( -- pankow - border - radius ) / 1.5 ) ;
}
. content - legend - item {
display : flex ;
align - items : center ;
}
. content - color - indicator {
height : 16 px ;
width : 16 px ;
2025-07-17 19:44:55 +02:00
border - radius : 4 px ;
}
. td {
vertical - align : middle ;
2025-07-17 19:27:09 +02:00
}
2025-07-17 17:06:37 +02:00
< / style >