feat(config + admin panel): handle features activated
- review admin dashboard to reflect features enabled/disabled - hide mount option if feature is disabled - remove QUOTA option as it is not wired
This commit is contained in:
@@ -416,11 +416,15 @@ export interface AdminDashboard {
|
||||
* `online_sessions / online_users` is the multi-device factor
|
||||
* (browser + desktop + phone). */
|
||||
online_sessions: number;
|
||||
/** Currently-connected message-bus WebSocket sessions — one per
|
||||
* open browser tab that reached a folder view. Reported as `0`
|
||||
* when `OXICLOUD_MESSAGEBUS_ENABLE=false` (no live sessions
|
||||
* possible); the dashboard hides the card in that case since
|
||||
* the value would be misleading. */
|
||||
active_ws_sessions: number;
|
||||
server_version: string;
|
||||
drive_usage: DriveKindUsage[];
|
||||
auth_enabled: boolean;
|
||||
oidc_configured: boolean;
|
||||
quotas_enabled: boolean;
|
||||
registration_enabled?: boolean;
|
||||
users_over_80_percent: number;
|
||||
users_over_quota: number;
|
||||
|
||||
@@ -893,7 +893,10 @@ export interface ServerFeatures {
|
||||
trash: boolean;
|
||||
search: boolean;
|
||||
sharing: boolean;
|
||||
quotas: boolean;
|
||||
// NOTE: `quotas` was intentionally NOT exposed — see the Rust
|
||||
// `FeaturesDto` doc for why (dormant server flag with zero
|
||||
// consumers). Add it back once it actually gates FE-visible
|
||||
// behavior.
|
||||
music: boolean;
|
||||
places: boolean;
|
||||
faces: boolean;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import { dateTimeFormatFor, iconNameFromClass } from '$lib/utils/display';
|
||||
import { userInitials, avatarColorIndex } from '$lib/utils/avatar';
|
||||
import { i18n, LANGUAGES, setLocale, t, type Locale } from '$lib/i18n/index.svelte';
|
||||
import { serverConfig } from '$lib/stores/serverConfig.svelte';
|
||||
import { serverStatus } from '$lib/stores/serverStatus.svelte';
|
||||
import { apiFetch } from '$lib/api/client';
|
||||
import { dialogs } from '$lib/stores/dialogs.svelte';
|
||||
@@ -74,68 +75,81 @@
|
||||
// strip; that was displaced here so the section navigation
|
||||
// scales past ~7 items and matches deep-link URLs from the
|
||||
// address bar.
|
||||
const ADMIN_LINKS: NavLink[] = [
|
||||
{
|
||||
href: '/admin',
|
||||
label: t('admin.dashboard', 'Dashboard'),
|
||||
icon: 'chart-pie',
|
||||
section: 'admin-dashboard'
|
||||
},
|
||||
{
|
||||
href: '/admin/users',
|
||||
label: t('admin.users', 'Users'),
|
||||
icon: 'users',
|
||||
section: 'admin-users'
|
||||
},
|
||||
{
|
||||
href: '/admin/sessions',
|
||||
label: t('admin.sessions', 'Sessions'),
|
||||
icon: 'key',
|
||||
section: 'admin-sessions'
|
||||
},
|
||||
{
|
||||
href: '/admin/drives',
|
||||
label: t('admin.drives', 'Drives'),
|
||||
icon: 'hdd',
|
||||
section: 'admin-drives'
|
||||
},
|
||||
{
|
||||
href: '/admin/mounts',
|
||||
label: t('admin.mounts', 'External Mounts'),
|
||||
icon: 'folder',
|
||||
section: 'admin-mounts'
|
||||
},
|
||||
{
|
||||
href: '/admin/oidc',
|
||||
label: t('admin.oidc', 'OIDC / SSO'),
|
||||
icon: 'building-shield',
|
||||
section: 'admin-oidc'
|
||||
},
|
||||
{
|
||||
href: '/admin/storage',
|
||||
label: t('admin.storage_tab', 'Storage'),
|
||||
icon: 'database',
|
||||
section: 'admin-storage'
|
||||
},
|
||||
{
|
||||
href: '/admin/smtp',
|
||||
label: t('admin.smtp', 'Email (SMTP)'),
|
||||
icon: 'envelope',
|
||||
section: 'admin-smtp'
|
||||
},
|
||||
{
|
||||
href: '/admin/plugins',
|
||||
label: t('admin.plugins', 'Plugins'),
|
||||
icon: 'layer-group',
|
||||
section: 'admin-plugins'
|
||||
},
|
||||
{
|
||||
href: '/admin/jobs',
|
||||
label: t('admin.jobs.tab', 'Background tasks'),
|
||||
icon: 'cogs',
|
||||
section: 'admin-jobs'
|
||||
// `$derived` so feature-flag gating drops entries when a feature is
|
||||
// disabled server-side. Server-side the admin CRUD routes are also
|
||||
// gated (matching the message-bus pattern) — hiding the link here
|
||||
// keeps the sidebar consistent with what the backend actually
|
||||
// serves; a stale link would land on a 404. See
|
||||
// `$lib/stores/serverConfig.svelte.ts`.
|
||||
const ADMIN_LINKS = $derived.by<NavLink[]>(() => {
|
||||
const links: NavLink[] = [
|
||||
{
|
||||
href: '/admin',
|
||||
label: t('admin.dashboard', 'Dashboard'),
|
||||
icon: 'chart-pie',
|
||||
section: 'admin-dashboard'
|
||||
},
|
||||
{
|
||||
href: '/admin/users',
|
||||
label: t('admin.users', 'Users'),
|
||||
icon: 'users',
|
||||
section: 'admin-users'
|
||||
},
|
||||
{
|
||||
href: '/admin/sessions',
|
||||
label: t('admin.sessions', 'Sessions'),
|
||||
icon: 'key',
|
||||
section: 'admin-sessions'
|
||||
},
|
||||
{
|
||||
href: '/admin/drives',
|
||||
label: t('admin.drives', 'Drives'),
|
||||
icon: 'hdd',
|
||||
section: 'admin-drives'
|
||||
}
|
||||
];
|
||||
if (serverConfig.features.external_mounts) {
|
||||
links.push({
|
||||
href: '/admin/mounts',
|
||||
label: t('admin.mounts', 'External Mounts'),
|
||||
icon: 'folder',
|
||||
section: 'admin-mounts'
|
||||
});
|
||||
}
|
||||
];
|
||||
links.push(
|
||||
{
|
||||
href: '/admin/oidc',
|
||||
label: t('admin.oidc', 'OIDC / SSO'),
|
||||
icon: 'building-shield',
|
||||
section: 'admin-oidc'
|
||||
},
|
||||
{
|
||||
href: '/admin/storage',
|
||||
label: t('admin.storage_tab', 'Storage'),
|
||||
icon: 'database',
|
||||
section: 'admin-storage'
|
||||
},
|
||||
{
|
||||
href: '/admin/smtp',
|
||||
label: t('admin.smtp', 'Email (SMTP)'),
|
||||
icon: 'envelope',
|
||||
section: 'admin-smtp'
|
||||
},
|
||||
{
|
||||
href: '/admin/plugins',
|
||||
label: t('admin.plugins', 'Plugins'),
|
||||
icon: 'layer-group',
|
||||
section: 'admin-plugins'
|
||||
},
|
||||
{
|
||||
href: '/admin/jobs',
|
||||
label: t('admin.jobs.tab', 'Background tasks'),
|
||||
icon: 'cogs',
|
||||
section: 'admin-jobs'
|
||||
}
|
||||
);
|
||||
return links;
|
||||
});
|
||||
|
||||
const isAdmin = $derived(session.user?.role === 'admin');
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ const DEFAULT_FEATURES: ServerFeatures = {
|
||||
trash: true,
|
||||
search: true,
|
||||
sharing: true,
|
||||
quotas: false,
|
||||
music: true,
|
||||
places: true,
|
||||
faces: false,
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
} from '$lib/api/types';
|
||||
import { shortUserAgent } from '$lib/utils/userAgent';
|
||||
import { triggerJob } from '$lib/api/endpoints/adminJobs';
|
||||
import { serverConfig } from '$lib/stores/serverConfig.svelte';
|
||||
import { serverStatus } from '$lib/stores/serverStatus.svelte';
|
||||
import AdminJobsPanel from '$lib/components/AdminJobsPanel.svelte';
|
||||
import Icon from '$lib/icons/Icon.svelte';
|
||||
@@ -224,6 +225,44 @@
|
||||
// `$effect` loop is even possible.
|
||||
const tab = $derived<Tab>(parseTab(page.params.tab));
|
||||
|
||||
/**
|
||||
* Feature-flag matrix for the dashboard "System" section.
|
||||
* Data-driven from `serverConfig.features` (populated at boot from
|
||||
* `GET /api/config`). Each entry becomes one card; adding a
|
||||
* feature server-side flows through this list automatically —
|
||||
* label lookup falls back to the raw key so a missing translation
|
||||
* won't hide the card.
|
||||
*
|
||||
* Uses `unknown` bracket-key reads (rather than a rigid mapping
|
||||
* over hard-coded keys) so the FE doesn't need a code change when
|
||||
* the backend adds a new feature flag. The i18n key namespace
|
||||
* `admin.features.<key>` keeps translations discoverable.
|
||||
*/
|
||||
interface FeatureRow {
|
||||
key: string;
|
||||
label: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
const FEATURE_LABELS: Record<string, string> = {
|
||||
message_bus: 'Message bus',
|
||||
trash: 'Trash',
|
||||
search: 'Search',
|
||||
sharing: 'Sharing',
|
||||
music: 'Music',
|
||||
places: 'Places (photo map)',
|
||||
faces: 'People (faces)',
|
||||
video_thumbnails: 'Video thumbnails',
|
||||
external_mounts: 'External mounts'
|
||||
};
|
||||
const featureRows = $derived.by<FeatureRow[]>(() => {
|
||||
const raw = serverConfig.features as unknown as Record<string, boolean>;
|
||||
return Object.entries(raw).map(([key, enabled]) => ({
|
||||
key,
|
||||
label: t(`admin.features.${key}`, FEATURE_LABELS[key] ?? key),
|
||||
enabled
|
||||
}));
|
||||
});
|
||||
|
||||
/**
|
||||
* Human-readable label for the current section — feeds the
|
||||
* page title (`Admin › Jobs · OxiCloud`) and the h1. Kept in
|
||||
@@ -1905,33 +1944,56 @@
|
||||
</span>
|
||||
{t('admin.online_sessions', 'Online sessions')}
|
||||
</div>
|
||||
<!-- WS-sessions card only rendered when the message bus is
|
||||
enabled server-side. With `OXICLOUD_MESSAGEBUS_ENABLE=false`
|
||||
the count is unconditionally 0, and showing "0 Live
|
||||
WS sessions" reads like a bug when the feature simply
|
||||
isn't running. `serverConfig.features.message_bus`
|
||||
comes from `/api/config` at boot. -->
|
||||
{#if serverConfig.features.message_bus}
|
||||
<div
|
||||
class="ds-card"
|
||||
title={t(
|
||||
'admin.active_ws_sessions_tooltip',
|
||||
'Currently-connected message-bus WebSocket sessions — one per open browser tab reaching a folder view'
|
||||
)}
|
||||
>
|
||||
<span class="ds-num ds-num--live">
|
||||
<span class="presence-dot presence-dot--online" aria-hidden="true"></span>
|
||||
{dashboard.active_ws_sessions}
|
||||
</span>
|
||||
{t('admin.active_ws_sessions', 'Live WS sessions')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Section 3: System — deployment flags + version. -->
|
||||
<!-- Section 3: System — deployment flags + version.
|
||||
Feature cards are data-driven from `/api/config`
|
||||
(see `serverConfig.features` + `featureRows` derived).
|
||||
Auth / OIDC / version still come from the dashboard
|
||||
endpoint since those are per-deployment "system"
|
||||
concerns not exposed on the public config surface.
|
||||
Adding a new feature server-side flows into this grid
|
||||
automatically — no template change needed. -->
|
||||
<h2 class="ds-section-title">{t('admin.section_system', 'System')}</h2>
|
||||
<div class="ds-grid">
|
||||
<div class="ds-card">
|
||||
<span class="ds-flag" class:ds-flag--on={dashboard.auth_enabled}>
|
||||
{dashboard.auth_enabled
|
||||
? t('admin.enabled', 'Enabled')
|
||||
: t('admin.disabled', 'Disabled')}
|
||||
</span>
|
||||
{t('admin.auth', 'Authentication')}
|
||||
</div>
|
||||
<div class="ds-card">
|
||||
<span class="ds-flag" class:ds-flag--on={dashboard.oidc_configured}>
|
||||
{dashboard.oidc_configured ? t('admin.active', 'Active') : t('admin.off', 'Off')}
|
||||
</span>
|
||||
{t('admin.oidc', 'OIDC / SSO')}
|
||||
</div>
|
||||
<div class="ds-card">
|
||||
<span class="ds-flag" class:ds-flag--on={dashboard.quotas_enabled}>
|
||||
{dashboard.quotas_enabled
|
||||
? t('admin.enabled', 'Enabled')
|
||||
: t('admin.disabled', 'Disabled')}
|
||||
</span>
|
||||
{t('admin.quotas', 'Quotas')}
|
||||
</div>
|
||||
{#each featureRows as row (row.key)}
|
||||
<div class="ds-card" data-testid="admin-feature-card-{row.key}">
|
||||
<span class="ds-flag" class:ds-flag--on={row.enabled}>
|
||||
{row.enabled ? t('admin.enabled', 'Enabled') : t('admin.disabled', 'Disabled')}
|
||||
</span>
|
||||
{row.label}
|
||||
</div>
|
||||
{/each}
|
||||
<!-- Version card intentionally last — tertiary build
|
||||
metadata (like a footer), least useful at a glance
|
||||
compared to the feature-toggle cards above. -->
|
||||
<div class="ds-card">
|
||||
<span class="ds-num">v{dashboard.server_version}</span>{t('admin.version', 'Version')}
|
||||
</div>
|
||||
|
||||
@@ -89,9 +89,8 @@ const dashboard = {
|
||||
total_used_bytes: 100,
|
||||
total_quota_bytes: 1000,
|
||||
storage_usage_percent: 10,
|
||||
auth_enabled: true,
|
||||
oidc_configured: false,
|
||||
quotas_enabled: true,
|
||||
active_ws_sessions: 0,
|
||||
registration_enabled: true,
|
||||
users_over_80_percent: 0,
|
||||
users_over_quota: 0
|
||||
|
||||
Reference in New Issue
Block a user