Skip to content

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

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

feat(admin): add database panel with size on disk and retention sugge…#2549
karlitschek wants to merge 1 commit intomasterfrom
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
Copy link
Copy Markdown

codecov Bot commented May 2, 2026

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
Copy link
Copy Markdown

cypress Bot commented May 2, 2026

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,

Copy link
Copy Markdown

Copilot AI left a comment

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants