diff --git a/packages/app/src/admin/SuperAdminPage.tsx b/packages/app/src/admin/SuperAdminPage.tsx index d3f13f8700..1877fddde3 100644 --- a/packages/app/src/admin/SuperAdminPage.tsx +++ b/packages/app/src/admin/SuperAdminPage.tsx @@ -1,6 +1,8 @@ -import { Button, Divider, NativeSelect, PasswordInput, Stack, TextInput, Title } from '@mantine/core'; +import { Button, Divider, Modal, NativeSelect, PasswordInput, Stack, TextInput, Title } from '@mantine/core'; +import { useDisclosure } from '@mantine/hooks'; import { notifications, showNotification } from '@mantine/notifications'; import { MedplumClient, MedplumRequestOptions, forbidden, normalizeErrorString } from '@medplum/core'; +import { Parameters } from '@medplum/fhirtypes'; import { DateTimeInput, Document, @@ -11,9 +13,13 @@ import { useMedplum, } from '@medplum/react'; import { IconCheck, IconX } from '@tabler/icons-react'; +import { ReactNode, useState } from 'react'; export function SuperAdminPage(): JSX.Element { const medplum = useMedplum(); + const [opened, { open, close }] = useDisclosure(false); + const [modalTitle, setModalTitle] = useState(''); + const [modalContent, setModalContent] = useState(); if (!medplum.isLoading() && !medplum.isSuperAdmin()) { return ; @@ -61,7 +67,14 @@ export function SuperAdminPage(): JSX.Element { } function getDatabaseStats(): void { - startAsyncJob(medplum, 'Get Database Stats', 'admin/super/dbstats', {}); + medplum + .post('fhir/R4/$db-stats', {}) + .then((params: Parameters) => { + setModalTitle('Database Stats'); + setModalContent(
{params.parameter?.find((p) => p.name === 'tableString')?.valueString}
); + open(); + }) + .catch((err) => showNotification({ color: 'red', message: normalizeErrorString(err), autoClose: false })); } return ( @@ -169,6 +182,9 @@ export function SuperAdminPage(): JSX.Element {
+ + {modalContent} + ); } diff --git a/packages/server/src/admin/super.ts b/packages/server/src/admin/super.ts index ab1e62823e..96d7069cde 100644 --- a/packages/server/src/admin/super.ts +++ b/packages/server/src/admin/super.ts @@ -22,7 +22,6 @@ import { rebuildR4SearchParameters } from '../seeds/searchparameters'; import { rebuildR4StructureDefinitions } from '../seeds/structuredefinitions'; import { rebuildR4ValueSets } from '../seeds/valuesets'; import { removeBullMQJobByKey } from '../workers/cron'; -import { Communication } from '@medplum/fhirtypes'; export const superAdminRouter = Router(); superAdminRouter.use(authenticateRequest); @@ -220,41 +219,6 @@ superAdminRouter.post( }) ); -// POST to /admin/super/dbstats -// to query database statistics. -superAdminRouter.post( - '/dbstats', - asyncWrap(async (req: Request, res: Response) => { - requireSuperAdmin(); - requireAsync(req); - - await sendAsyncResponse(req, res, async () => { - const systemRepo = getSystemRepo(); - const client = getDatabasePool(); - const sql = ` - SELECT * FROM ( - SELECT table_schema, table_name, pg_relation_size('"'||table_schema||'"."'||table_name||'"') AS table_size - FROM information_schema.tables - ) tables - WHERE table_size > 0 - ORDER BY table_size DESC; - `; - - const result = await client.query(sql); - - const contentString = result.rows - .map((row) => `${row.table_schema}.${row.table_name}: ${row.table_size}`) - .join('\n'); - - await systemRepo.createResource({ - resourceType: 'Communication', - status: 'completed', - payload: [{ contentString }], - }); - }); - }) -); - export function requireSuperAdmin(): AuthenticatedRequestContext { const ctx = getAuthenticatedContext(); if (!ctx.project.superAdmin) { diff --git a/packages/server/src/fhir/operations/dbstats.ts b/packages/server/src/fhir/operations/dbstats.ts new file mode 100644 index 0000000000..b3dc435011 --- /dev/null +++ b/packages/server/src/fhir/operations/dbstats.ts @@ -0,0 +1,39 @@ +import { allOk } from '@medplum/core'; +import { FhirRequest, FhirResponse } from '@medplum/fhir-router'; +import { OperationDefinition } from '@medplum/fhirtypes'; +import { requireSuperAdmin } from '../../admin/super'; +import { getDatabasePool } from '../../database'; +import { buildOutputParameters } from './utils/parameters'; + +const operation: OperationDefinition = { + resourceType: 'OperationDefinition', + name: 'db-stats', + status: 'active', + kind: 'operation', + code: 'status', + experimental: true, + system: true, + type: false, + instance: false, + parameter: [{ use: 'out', name: 'tableString', type: 'string', min: 1, max: '1' }], +}; + +export async function dbStatsHandler(_req: FhirRequest): Promise { + requireSuperAdmin(); + + const client = getDatabasePool(); + const sql = ` + SELECT * FROM ( + SELECT table_schema, table_name, pg_relation_size('"'||table_schema||'"."'||table_name||'"') AS table_size + FROM information_schema.tables + ) tables + WHERE table_size > 0 + ORDER BY table_size DESC; + `; + + const result = await client.query(sql); + + const tableString = result.rows.map((row) => `${row.table_schema}.${row.table_name}: ${row.table_size}`).join('\n'); + + return [allOk, buildOutputParameters(operation, { tableString })]; +} diff --git a/packages/server/src/fhir/routes.ts b/packages/server/src/fhir/routes.ts index 0ae3ac8188..e42255fd14 100644 --- a/packages/server/src/fhir/routes.ts +++ b/packages/server/src/fhir/routes.ts @@ -18,6 +18,7 @@ import { codeSystemLookupHandler } from './operations/codesystemlookup'; import { codeSystemValidateCodeHandler } from './operations/codesystemvalidatecode'; import { conceptMapTranslateHandler } from './operations/conceptmaptranslate'; import { csvHandler } from './operations/csv'; +import { dbStatsHandler } from './operations/dbstats'; import { deployHandler } from './operations/deploy'; import { evaluateMeasureHandler } from './operations/evaluatemeasure'; import { executeHandler } from './operations/execute'; @@ -247,6 +248,9 @@ function initInternalFhirRouter(): FhirRouter { return [allOk]; }); + // Super admin operations + router.add('POST', '/$db-stats', dbStatsHandler); + router.addEventListener('warn', (e: any) => { const ctx = getAuthenticatedContext(); ctx.logger.warn(e.message, { ...e.data, project: ctx.project.id });