Skip to content

Commit 2eb8ef9

Browse files
committed
feat(files): Quota in navigation
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
1 parent d389b54 commit 2eb8ef9

25 files changed

Lines changed: 400 additions & 109 deletions

.eslintrc.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,15 @@ module.exports = {
88
oc_userconfig: true,
99
dayNames: true,
1010
firstDay: true,
11+
'cypress/globals': true,
1112
},
12-
extends: ['@nextcloud'],
13+
plugins: [
14+
'cypress',
15+
],
16+
extends: [
17+
'@nextcloud',
18+
'plugin:cypress/recommended',
19+
],
1320
rules: {
1421
'no-tabs': 'warn',
1522
// TODO: make sure we fix this as this is bad vue coding style.

apps/files/appinfo/routes.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,6 @@
6161
'verb' => 'GET',
6262
'root' => '',
6363
],
64-
[
65-
'name' => 'ajax#getStorageStats',
66-
'url' => '/ajax/getstoragestats',
67-
'verb' => 'GET',
68-
],
6964
[
7065
'name' => 'API#getThumbnail',
7166
'url' => '/api/v1/thumbnail/{x}/{y}/{file}',
@@ -83,6 +78,11 @@
8378
'url' => '/api/v1/recent/',
8479
'verb' => 'GET'
8580
],
81+
[
82+
'name' => 'API#getStorageStats',
83+
'url' => '/api/v1/stats',
84+
'verb' => 'GET'
85+
],
8686
[
8787
'name' => 'API#setConfig',
8888
'url' => '/api/v1/config/{key}',

apps/files/lib/Controller/AjaxController.php

Lines changed: 0 additions & 57 deletions
This file was deleted.

apps/files/lib/Controller/ApiController.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,20 @@ public function getRecentFiles() {
257257
return new DataResponse(['files' => $files]);
258258
}
259259

260+
261+
/**
262+
* Returns the current logged-in user's storage stats.
263+
*
264+
* @NoAdminRequired
265+
*
266+
* @param ?string $dir the directory to get the storage stats from
267+
* @return JSONResponse
268+
*/
269+
public function getStorageStats($dir = '/'): JSONResponse {
270+
$storageInfo = \OC_Helper::getStorageInfo($dir ?: '/');
271+
return new JSONResponse(['message' => 'ok', 'data' => $storageInfo]);
272+
}
273+
260274
/**
261275
* Change the default sort mode
262276
*

apps/files/lib/Controller/ViewController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ public function index($dir = '', $view = '', $fileid = null, $fileNotFound = fal
254254
$contentItems = [];
255255

256256
$this->initialState->provideInitialState('navigation', $navItems);
257+
$this->initialState->provideInitialState('storageStats', \OC_Helper::getStorageInfo($view === 'files' ? ($dir ?: '/') : '/' ));
257258
$this->initialState->provideInitialState('config', $this->userConfig->getConfigs());
258259

259260
// render the container content for every navigation item
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
<template>
2+
<NcAppNavigationItem v-if="storageStats"
3+
:aria-label="t('files', 'Storage informations')"
4+
:class="{ 'app-navigation-entry__settings-quota--not-unlimited': storageStats.quota >= 0}"
5+
:loading="loadingStorageStats"
6+
:name="storageStatsTitle"
7+
:title="storageStatsTooltip"
8+
class="app-navigation-entry__settings-quota"
9+
data-cy-files-navigation-settings-quota
10+
@click.stop.prevent="debounceUpdateStorageStats">
11+
<ChartPie slot="icon" :size="20" />
12+
13+
<!-- Progress bar -->
14+
<NcProgressBar v-if="storageStats.quota >= 0"
15+
slot="extra"
16+
:error="storageStats.relative > 80"
17+
:value="Math.min(storageStats.relative, 100)" />
18+
</NcAppNavigationItem>
19+
</template>
20+
21+
<script>
22+
import { formatFileSize } from '@nextcloud/files'
23+
import { generateUrl } from '@nextcloud/router'
24+
import { loadState } from '@nextcloud/initial-state'
25+
import { showError } from '@nextcloud/dialogs'
26+
import { debounce, throttle } from 'throttle-debounce'
27+
import { translate } from '@nextcloud/l10n'
28+
import axios from '@nextcloud/axios'
29+
import ChartPie from 'vue-material-design-icons/ChartPie.vue'
30+
import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js'
31+
import NcProgressBar from '@nextcloud/vue/dist/Components/NcProgressBar.js'
32+
33+
import logger from '../logger.js'
34+
import { subscribe } from '@nextcloud/event-bus'
35+
36+
export default {
37+
name: 'NavigationQuota',
38+
39+
components: {
40+
ChartPie,
41+
NcAppNavigationItem,
42+
NcProgressBar,
43+
},
44+
45+
data() {
46+
return {
47+
loadingStorageStats: false,
48+
storageStats: loadState('files', 'storageStats', null),
49+
}
50+
},
51+
52+
computed: {
53+
storageStatsTitle() {
54+
const usedQuotaByte = formatFileSize(this.storageStats?.used)
55+
const quotaByte = formatFileSize(this.storageStats?.quota)
56+
57+
// If no quota set
58+
if (this.storageStats?.quota < 0) {
59+
return this.t('files', '{usedQuotaByte} used', { usedQuotaByte })
60+
}
61+
62+
return this.t('files', '{used} of {quota} used', {
63+
used: usedQuotaByte,
64+
quota: quotaByte,
65+
})
66+
},
67+
storageStatsTooltip() {
68+
if (!this.storageStats.relative) {
69+
return ''
70+
}
71+
72+
return this.t('files', '{relative}% used', this.storageStats)
73+
},
74+
},
75+
76+
beforeMount() {
77+
/**
78+
* Update storage stats every minute
79+
* TODO: remove when all views are migrated to Vue
80+
*/
81+
setInterval(this.throttleUpdateStorageStats, 60 * 1000)
82+
83+
subscribe('files:file:created', this.throttleUpdateStorageStats)
84+
subscribe('files:file:deleted', this.throttleUpdateStorageStats)
85+
subscribe('files:file:moved', this.throttleUpdateStorageStats)
86+
subscribe('files:file:updated', this.throttleUpdateStorageStats)
87+
88+
subscribe('files:folder:created', this.throttleUpdateStorageStats)
89+
subscribe('files:folder:deleted', this.throttleUpdateStorageStats)
90+
subscribe('files:folder:moved', this.throttleUpdateStorageStats)
91+
subscribe('files:folder:updated', this.throttleUpdateStorageStats)
92+
},
93+
94+
methods: {
95+
// From user input
96+
debounceUpdateStorageStats: debounce(200, function(event) {
97+
this.updateStorageStats(event)
98+
}),
99+
// From interval or event bus
100+
throttleUpdateStorageStats: throttle(1000, function(event) {
101+
this.updateStorageStats(event)
102+
}),
103+
104+
/**
105+
* Update the storage stats
106+
* Throttled at max 1 refresh per minute
107+
*
108+
* @param {Event} [event = null] if user interaction
109+
*/
110+
async updateStorageStats(event = null) {
111+
if (this.loadingStorageStats) {
112+
return
113+
}
114+
115+
this.loadingStorageStats = true
116+
try {
117+
const response = await axios.get(generateUrl('/apps/files/api/v1/stats'))
118+
if (!response?.data?.data) {
119+
throw new Error('Invalid storage stats')
120+
}
121+
this.storageStats = response.data.data
122+
} catch (error) {
123+
logger.error('Could not refresh storage stats', error)
124+
// Only show to the user if it was manually triggered
125+
if (event) {
126+
showError(t('files', 'Could not refresh storage stats'))
127+
}
128+
} finally {
129+
this.loadingStorageStats = false
130+
}
131+
},
132+
133+
t: translate,
134+
},
135+
}
136+
</script>
137+
138+
<style lang="scss" scoped>
139+
// User storage stats display
140+
.app-navigation-entry__settings-quota {
141+
// Align title with progress and icon
142+
&--not-unlimited::v-deep .app-navigation-entry__title {
143+
margin-top: -4px;
144+
}
145+
146+
progress {
147+
position: absolute;
148+
bottom: 10px;
149+
margin-left: 44px;
150+
width: calc(100% - 44px - 22px);
151+
}
152+
}
153+
</style>

0 commit comments

Comments
 (0)