Skip to content

feat(admin): add database panel with size on disk and retention sugge…#2549

Open
karlitschek wants to merge 1 commit into
masterfrom
feat/admin-db-panel
Open

feat(admin): add database panel with size on disk and retention sugge…#2549
karlitschek wants to merge 1 commit into
masterfrom
feat/admin-db-panel

Conversation

@karlitschek

Copy link
Copy Markdown
Member

…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.
@codecov

codecov Bot commented May 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.61538% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/views/AdminSettings.vue 84.61% 8 Missing ⚠️

📢 Thoughts on this report? Let us know!

@cypress

cypress Bot commented May 2, 2026

Copy link
Copy Markdown

Activity    Run #3671

Run Properties:  status check passed Passed #3671  •  git commit 35b28db6b7: feat(admin): add database panel with size on disk and retention sugge…
Project Activity
Branch Review feat/admin-db-panel
Run status status check passed Passed #3671
Run duration 01m 59s
Commit git commit 35b28db6b7: feat(admin): add database panel with size on disk and retention sugge…
Committer Frank Karlitschek
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 1
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 9
View all changes introduced in this branch ↗︎
@miaulalala miaulalala force-pushed the feat/admin-db-panel branch 2 times, most recently from 97d9808 to ac46b1c Compare May 5, 2026 19:30
@miaulalala

Copy link
Copy Markdown
Collaborator

/compile amend

…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>
@miaulalala miaulalala force-pushed the feat/admin-db-panel branch from 8e4d483 to 97fefe6 Compare May 5, 2026 19:38
@miaulalala

Copy link
Copy Markdown
Collaborator

@artonge can you check out this PR? I added Oracle support and some test coverage, in addition to Frank's changes,

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 DatabaseStats backend 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 thread lib/DatabaseStats.php
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')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment