feat(admin): add database panel with size on disk and retention sugge…#2549
Open
karlitschek wants to merge 1 commit intomasterfrom
Open
feat(admin): add database panel with size on disk and retention sugge…#2549karlitschek wants to merge 1 commit intomasterfrom
karlitschek wants to merge 1 commit intomasterfrom
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Activity
|
||||||||||||||||||||||||||||
| Project |
Activity
|
| Branch Review |
feat/admin-db-panel
|
| Run status |
|
| Run duration | 01m 59s |
| Commit |
|
| Committer | Frank Karlitschek |
| View all properties for this run ↗︎ | |
| Test results | |
|---|---|
|
|
0
|
|
|
0
|
|
|
1
|
|
|
0
|
|
|
9
|
| View all changes introduced in this branch ↗︎ | |
97d9808 to
ac46b1c
Compare
Collaborator
|
/compile amend |
ac46b1c to
8e4d483
Compare
…stion The activity log can grow into hundreds of GB on busy installations without the admin noticing — there is no built-in signal that something has gotten out of hand. Surface the relevant numbers in the Activity admin section. New `DatabaseStats` service queries on-disk size against the activity-scoped connection (the dedicated one if `activity_db*` is configured, otherwise the main DB): - MySQL/MariaDB: `information_schema.tables` (data_length + index_length). - PostgreSQL: `pg_total_relation_size()` (table + indexes + toast). - SQLite: returns null per table; admin UI explains the limitation. The same service produces a conservative retention suggestion — when the total size crosses 1/5/10 GB and `activity_expire_days` is still at the 365-day default, recommend lowering it to 180/90/30 days. Recommendations only ever shorten; admins who already tuned retention down are not second-guessed. Suggestions are surfaced as a warning card in the new "Database" section of the admin UI; nothing is auto-applied — admins copy the `'activity_expire_days' => N,` snippet into `config.php` themselves so config-as-code workflows stay in control. Wired through: - `lib/DatabaseStats.php` — new, uses `IQueryBuilder::PARAM_STR_ARRAY`, honours `activity_dbtableprefix` / `dbtableprefix`. - `lib/AppInfo/Application.php` — register service against `ActivityConnectionAdapter` so it queries the right DB. - `lib/Settings/Admin.php` — inject and expose via initial state (`database_stats` key carries dedicated_connection + tables + retention_suggestion). - `src/views/AdminSettings.vue` — new "Database" section with per-table size table, total row, contextual description, and the retention suggestion as an `NcNoteCard` with "Copy config snippet" action. Signed-off-by: Frank Karlitschek <frank@nextcloud.com>
8e4d483 to
97fefe6
Compare
Collaborator
|
@artonge can you check out this PR? I added Oracle support and some test coverage, in addition to Frank's changes, |
There was a problem hiding this comment.
Pull request overview
Adds activity-database visibility to the admin settings flow by computing table sizes/retention guidance on the backend and surfacing that data in the admin UI.
Changes:
- Add a new
DatabaseStatsbackend service and expose its output through admin initial state. - Extend the admin settings Vue view with a new “Database” section, totals, and a retention suggestion card.
- Add unit tests and generated frontend/build artifacts related to the new admin UI and supporting dependencies.
Reviewed changes
Copilot reviewed 22 out of 38 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
vite.config.ts |
Adjusts Vitest config to ignore CSS imports in tests. |
tests/psalm-baseline.xml |
Adds Psalm baseline entry for the new DB platform class reference. |
tests/DatabaseStatsTest.php |
Adds backend tests for table sizing, retention suggestions, and dedicated-DB detection. |
src/views/AdminSettings.vue |
Implements the new admin Database section and copy-snippet action. |
src/__tests__/AdminSettings.test.ts |
Adds Vue tests for database stats rendering and retention suggestion UI. |
lib/Settings/Admin.php |
Injects DatabaseStats and publishes database_stats initial state. |
lib/DatabaseStats.php |
New service for DB table sizes, retention suggestion logic, and dedicated-connection detection. |
lib/AppInfo/Application.php |
Registers DatabaseStats in the app container with the activity DB adapter. |
js/translation-DoG5ZELJ-DZn9HrMY.chunk.mjs.license |
Generated license metadata update. |
js/settings-store-CX3hEB-M.chunk.mjs.map |
Generated source map for rebuilt frontend chunk. |
js/settings-store-CX3hEB-M.chunk.mjs.license |
Generated license metadata for rebuilt chunk. |
js/settings-store-CX3hEB-M.chunk.mjs |
Generated JS chunk update. |
js/mdi-CpchYUUV-DyQi4TYO.chunk.mjs.map |
Generated source map for new icon chunk. |
js/mdi-CpchYUUV-DyQi4TYO.chunk.mjs.license |
Generated license metadata for icon chunk. |
js/mdi-CpchYUUV-DyQi4TYO.chunk.mjs |
Generated icon chunk update. |
js/index-DQB7NaKz.chunk.mjs.license |
Generated license metadata update. |
js/index-DJLpEI0G.chunk.mjs.license |
Generated license metadata update. |
js/index-C1xmmKTZ-wIpZ60yn.chunk.mjs.license |
Generated license metadata update. |
js/ContentCopy-DN5i-3PD.chunk.mjs.map |
Generated source map for copy-icon chunk. |
js/ContentCopy-DN5i-3PD.chunk.mjs.license |
Generated license metadata for copy-icon chunk. |
js/ContentCopy-DN5i-3PD.chunk.mjs |
Generated copy-icon chunk. |
js/ActivityTab-Dv1gyT8t.chunk.mjs.map |
Generated source map update for sidebar activity bundle. |
js/ActivityTab-Dv1gyT8t.chunk.mjs.license |
Generated license metadata for sidebar activity bundle. |
js/ActivityTab-Dv1gyT8t.chunk.mjs |
Generated sidebar activity bundle update. |
js/ActivityComponent.vue_vue_type_script_setup_true_lang-C4VJG6jM.chunk.mjs.license |
Generated license metadata update. |
js/_plugin-vue_export-helper-CI9KtCO6.chunk.mjs.license |
Generated license metadata update. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+100
to
+107
| return null; | ||
| } | ||
| $totalBytes = array_sum($values); | ||
|
|
||
| $current = (int)$this->config->getSystemValue('activity_expire_days', self::DEFAULT_RETENTION_DAYS); | ||
| if ($current < self::DEFAULT_RETENTION_DAYS) { | ||
| // Admin has already turned retention down — don't second-guess them. | ||
| return null; |
Comment on lines
+17
to
+57
| <NcSettingsSection | ||
| :name="t('activity', 'Database')" | ||
| :description="databaseDescription"> | ||
| <table class="activity-database-table"> | ||
| <thead> | ||
| <tr> | ||
| <th>{{ t('activity', 'Table') }}</th> | ||
| <th class="activity-database-table__size"> | ||
| {{ t('activity', 'Size on disk') }} | ||
| </th> | ||
| </tr> | ||
| </thead> | ||
| <tbody> | ||
| <tr v-for="row in tableRows" :key="row.table"> | ||
| <td><code>{{ row.table }}</code></td> | ||
| <td class="activity-database-table__size">{{ row.formatted }}</td> | ||
| </tr> | ||
| <tr v-if="totalBytes !== null" class="activity-database-table__total"> | ||
| <th>{{ t('activity', 'Total') }}</th> | ||
| <th class="activity-database-table__size">{{ formatBytes(totalBytes) }}</th> | ||
| </tr> | ||
| </tbody> | ||
| </table> | ||
| <p v-if="!sizesAvailable" class="activity-database-table__hint"> | ||
| {{ t('activity', 'Per-table size is only available on MySQL/MariaDB and PostgreSQL.') }} | ||
| </p> | ||
|
|
||
| <NcNoteCard | ||
| v-if="retentionSuggestion" | ||
| type="warning" | ||
| class="activity-database-suggestion"> | ||
| <p>{{ retentionSuggestionTitle }}</p> | ||
| <p>{{ retentionSuggestionDetail }}</p> | ||
| <pre class="activity-database-suggestion__snippet">{{ retentionSuggestionSnippet }}</pre> | ||
| <template #actions> | ||
| <NcButton @click="copySuggestionSnippet"> | ||
| <template #icon><IconContentCopy :size="16" /></template> | ||
| {{ t('activity', 'Copy config snippet') }} | ||
| </NcButton> | ||
| </template> | ||
| </NcNoteCard> |
Comment on lines
+172
to
+177
| async copySuggestionSnippet() { | ||
| try { | ||
| await navigator.clipboard.writeText(this.retentionSuggestionSnippet) | ||
| showSuccess(t('activity', 'Copied. Paste into config/config.php.')) | ||
| } catch (e) { | ||
| showError(t('activity', 'Could not copy to clipboard.')) |
Comment on lines
+166
to
+203
| $platform = $this->createMock(OraclePlatform::class); | ||
| $this->connection->method('getDatabasePlatform')->willReturn($platform); | ||
|
|
||
| $this->config->method('getSystemValue') | ||
| ->willReturnMap([ | ||
| ['activity_dbtableprefix', 'oc_', 'oc_'], | ||
| ['dbtableprefix', 'oc_', 'oc_'], | ||
| ]); | ||
|
|
||
| $result = $this->createMock(\OCP\DB\IResult::class); | ||
| $result->method('fetch')->willReturn(['size_bytes' => null]); | ||
| $result->method('closeCursor')->willReturn(true); | ||
|
|
||
| $this->connection->expects($this->exactly(2)) | ||
| ->method('executeQuery') | ||
| ->with($this->anything(), $this->callback(static fn (array $params) => $params[0] === 'OC_ACTIVITY' || $params[0] === 'OC_ACTIVITY_MQ')) | ||
| ->willReturn($result); | ||
|
|
||
| $this->stats->getTableSizesInBytes(); | ||
| } | ||
|
|
||
| public function testGetTableSizesFallsBackToNullForSQLite(): void { | ||
| // Any platform class without MySQL or PostgreSQL in its name → null sizes | ||
| $platform = $this->createMock(AbstractPlatform::class); | ||
|
|
||
| $this->connection->method('getDatabasePlatform')->willReturn($platform); | ||
|
|
||
| $sizes = $this->stats->getTableSizesInBytes(); | ||
|
|
||
| $this->assertNull($sizes['activity']); | ||
| $this->assertNull($sizes['activity_mq']); | ||
| } | ||
|
|
||
| public function testGetTableSizesCatchesExceptionAndReturnsNull(): void { | ||
| $platform = $this->createMock(MySQLPlatform::class); | ||
| $this->connection->method('getDatabasePlatform')->willReturn($platform); | ||
|
|
||
| $this->connection->method('getQueryBuilder') |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
…stion
The activity log can grow into hundreds of GB on busy installations without the admin noticing — there is no built-in signal that something has gotten out of hand. Surface the relevant numbers in the Activity admin section.
New
DatabaseStatsservice queries on-disk size against the activity-scoped connection (the dedicated one ifactivity_db*is configured, otherwise the main DB):information_schema.tables(data_length + index_length).pg_total_relation_size()(table + indexes + toast).The same service produces a conservative retention suggestion — when the total size crosses 1/5/10 GB and
activity_expire_daysis still at the 365-day default, recommend lowering it to 180/90/30 days. Recommendations only ever shorten; admins who already tuned retention down are not second-guessed. Suggestions are surfaced as a warning card in the new "Database" section of the admin UI; nothing is auto-applied — admins copy the'activity_expire_days' => N,snippet intoconfig.phpthemselves so config-as-code workflows stay in control.Wired through:
lib/DatabaseStats.php— new, usesIQueryBuilder::PARAM_STR_ARRAY, honoursactivity_dbtableprefix/dbtableprefix.lib/AppInfo/Application.php— register service againstActivityConnectionAdapterso it queries the right DB.lib/Settings/Admin.php— inject and expose via initial state (database_statskey carries dedicated_connection + tables + retention_suggestion).src/views/AdminSettings.vue— new "Database" section with per-table size table, total row, contextual description, and the retention suggestion as anNcNoteCardwith "Copy config snippet" action.