feat: demo-data parity for databases and cron tab

Adds demoDatabases fixtures (postgres/redis/clickhouse) wired into
/databases?demo=1 and demoCronJobsByService fixtures for the service
Cron tab, so demo mode no longer renders empty sections on the new
surfaces.

Authored By: TDvorak <[email protected]>

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-17 18:28:26 +02:00
1 parent d5e6bfc40d
commit 6d237eccf7
4 files changed
+112 -8

No files matched your search

+5 -3
View File
@@ -440,9 +440,11 @@ security scans produce real reports, not empty tables.
events live in process memory — a backend restart wipes them and events live in process memory — a backend restart wipes them and
reverts replicas to policy minimums on next registration. Persist reverts replicas to policy minimums on next registration. Persist
to Postgres before leaving beta. to Postgres before leaving beta.
- [ ] **Demo-data parity for new pages**: `demo-data.ts` covers Projects/ - [x] **Demo-data parity for new pages**: `demoDatabases` fixture wired
Templates but the new Databases page and service Cron tab have no demo into `/databases?demo=1`; `demoCronJobsByService` fixtures wired into
fixtures — add them so `?demo=1` doesn't show empty sections. the service Cron tab (worker + api demo services). Scaling, Previews,
HA and Security sections still show empty in demo mode — acceptable:
those are interaction surfaces, not dashboards.
- [ ] **CI**: GitHub Actions — build, lint, typecheck, tests, compose boot - [ ] **CI**: GitHub Actions — build, lint, typecheck, tests, compose boot
smoke; publish container images to GHCR with semver tags. smoke; publish container images to GHCR with semver tags.
- [ ] **Release**: `v1.0.0` tag, changelog, release notes, screenshots refresh - [ ] **Release**: `v1.0.0` tag, changelog, release notes, screenshots refresh
@@ -47,6 +47,7 @@ import {
type DatabaseEntity, type DatabaseEntity,
type FailoverPolicy, type FailoverPolicy,
} from '@/lib/api-client'; } from '@/lib/api-client';
import { demoDatabases } from '@/lib/demo-data';
import { formatRelative } from '@/lib/time'; import { formatRelative } from '@/lib/time';
import { getAuthBaseUrl, signOutAuthSession } from '@/lib/auth-client'; import { getAuthBaseUrl, signOutAuthSession } from '@/lib/auth-client';
import { useBuildUpdates } from '@/lib/use-build-updates'; import { useBuildUpdates } from '@/lib/use-build-updates';
@@ -1535,12 +1536,15 @@ const dbStatusClass: Record<string, string> = {
export function DatabasesPage() { export function DatabasesPage() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [searchParams] = useSearchParams();
const isDemoMode = searchParams.get('demo') === '1';
const [expandedId, setExpandedId] = useState<string | null>(null); const [expandedId, setExpandedId] = useState<string | null>(null);
const [bindDb, setBindDb] = useState<DatabaseEntity | null>(null); const [bindDb, setBindDb] = useState<DatabaseEntity | null>(null);
const databasesQuery = useQuery({ const databasesQuery = useQuery({
queryKey: ['databases'], queryKey: ['databases'],
queryFn: listDatabases, queryFn: listDatabases,
refetchInterval: 15_000, refetchInterval: 15_000,
enabled: !isDemoMode,
}); });
const invalidate = () => queryClient.invalidateQueries({ queryKey: ['databases'] }); const invalidate = () => queryClient.invalidateQueries({ queryKey: ['databases'] });
@@ -1559,7 +1563,7 @@ export function DatabasesPage() {
onSuccess: () => window.setTimeout(invalidate, 3000), onSuccess: () => window.setTimeout(invalidate, 3000),
}); });
const databases = databasesQuery.data ?? []; const databases = isDemoMode ? demoDatabases : databasesQuery.data ?? [];
return ( return (
<div> <div>
@@ -30,7 +30,7 @@ import {
updateServiceVariables, updateServiceVariables,
type CronJobEntity, type CronJobEntity,
} from '@/lib/api-client'; } from '@/lib/api-client';
import { getDemoProjectById, getDemoServiceById } from '@/lib/demo-data'; import { getDemoProjectById, getDemoServiceById, getDemoCronJobsByService } from '@/lib/demo-data';
import { parseDotenv, validateVariableRows, type VariableDraft } from '../variable-utils'; import { parseDotenv, validateVariableRows, type VariableDraft } from '../variable-utils';
import { formatBytes, formatDate, formatRelative, seededMetric } from '@/lib/time'; import { formatBytes, formatDate, formatRelative, seededMetric } from '@/lib/time';
import { EnhancedMetricCard, LineAreaChart, DonutChart } from '@/shared/components'; import { EnhancedMetricCard, LineAreaChart, DonutChart } from '@/shared/components';
@@ -1290,14 +1290,14 @@ export function ServiceDetailPage() {
<div className="flex items-center justify-center py-12 text-[var(--text-muted)]"> <div className="flex items-center justify-center py-12 text-[var(--text-muted)]">
<Loader2 size={20} className="animate-spin" /> <Loader2 size={20} className="animate-spin" />
</div> </div>
) : (cronJobsQuery.data ?? []).length === 0 ? ( ) : (isDemoMode ? getDemoCronJobsByService(serviceId) : cronJobsQuery.data ?? []).length === 0 ? (
<div className="panel-soft p-8 text-center"> <div className="panel-soft p-8 text-center">
<p className="text-sm text-[var(--text-secondary)]">No cron jobs configured.</p> <p className="text-sm text-[var(--text-secondary)]">No cron jobs configured.</p>
<p className="mt-1 text-xs text-[var(--text-muted)]">Create a job to run commands on a schedule inside this service's container.</p> <p className="mt-1 text-xs text-[var(--text-muted)]">Create a job to run commands on a schedule inside this service's container.</p>
</div> </div>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{(cronJobsQuery.data ?? []).map((job) => ( {(isDemoMode ? getDemoCronJobsByService(serviceId) : cronJobsQuery.data ?? []).map((job) => (
<div key={job.id} className="panel-soft p-3"> <div key={job.id} className="panel-soft p-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button <button
+99 -1
View File
@@ -1,4 +1,4 @@
import type { ProjectEntity, ServiceEntity } from '@/lib/api-client'; import type { CronJobEntity, DatabaseEntity, ProjectEntity, ServiceEntity } from '@/lib/api-client';
import type { ServiceVariable } from '@/features/workspace/auto-connections'; import type { ServiceVariable } from '@/features/workspace/auto-connections';
export const demoProjects: ProjectEntity[] = [ export const demoProjects: ProjectEntity[] = [
@@ -231,3 +231,101 @@ export function getDemoServiceById(serviceId: string): ServiceEntity | undefined
export function getDemoVariablesByProject(projectId: string): Record<string, ServiceVariable[]> { export function getDemoVariablesByProject(projectId: string): Record<string, ServiceVariable[]> {
return demoVariablesByProject[projectId] ?? {}; return demoVariablesByProject[projectId] ?? {};
} }
export const demoDatabases: DatabaseEntity[] = [
{
id: 'demo-db-postgres',
name: 'core-postgres',
type: 'postgresql',
status: 'running',
connection_url: 'postgres://demo:***@core-postgres.internal:5432/app',
version: '16.4',
plan: 'standard',
region: 'local',
backup_schedule: '0 3 * * *',
next_backup_at: '2026-04-01T03:00:00Z',
created_at: '2026-03-12T10:05:00Z',
},
{
id: 'demo-db-redis',
name: 'cache-redis',
type: 'redis',
status: 'running',
connection_url: 'redis://:***@cache-redis.internal:6379',
version: '7.4',
plan: 'hobby',
region: 'local',
created_at: '2026-03-12T10:12:00Z',
},
{
id: 'demo-db-analytics',
name: 'analytics-clickhouse',
type: 'clickhouse',
status: 'stopped',
connection_url: 'clickhouse://analytics.internal:9000/default',
version: '25.3',
plan: 'business',
region: 'local',
created_at: '2026-02-18T08:00:00Z',
},
];
export const demoCronJobsByService: Record<string, CronJobEntity[]> = {
'demo-svc-worker': [
{
id: 'demo-cron-cleanup',
project_id: 'demo-project-core',
service_id: 'demo-svc-worker',
name: 'Nightly cleanup',
schedule: '0 2 * * *',
command: 'npm run cleanup:expired',
timezone: 'UTC',
enabled: true,
last_run_at: '2026-03-31T02:00:00Z',
next_run_at: '2026-04-01T02:00:00Z',
last_status: 'success',
retention: 50,
created_at: '2026-03-12T10:20:00Z',
updated_at: '2026-03-31T02:00:00Z',
},
{
id: 'demo-cron-report',
project_id: 'demo-project-core',
service_id: 'demo-svc-worker',
name: 'Hourly metrics rollup',
schedule: '15 * * * *',
command: 'npm run metrics:rollup',
timezone: 'UTC',
enabled: true,
last_run_at: '2026-03-31T09:15:00Z',
next_run_at: '2026-03-31T10:15:00Z',
last_status: 'success',
retention: 50,
created_at: '2026-03-12T10:21:00Z',
updated_at: '2026-03-31T09:15:00Z',
},
],
'demo-svc-api': [
{
id: 'demo-cron-warmup',
project_id: 'demo-project-core',
service_id: 'demo-svc-api',
name: 'Cache warmup',
schedule: '*/10 * * * *',
command: 'curl -sf http://localhost:8080/warm',
timezone: 'UTC',
enabled: false,
last_run_at: '2026-03-30T22:10:00Z',
next_run_at: '2026-03-30T22:20:00Z',
last_status: 'failed',
last_output: 'curl: (7) Failed to connect to localhost port 8080',
retention: 50,
created_at: '2026-03-12T10:22:00Z',
updated_at: '2026-03-30T22:10:00Z',
},
],
};
export function getDemoCronJobsByService(serviceId: string): CronJobEntity[] {
return demoCronJobsByService[serviceId] ?? [];
}