Skip to main content
Glama
UploadDataPage.tsx7.9 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { Button, LoadingOverlay } from '@mantine/core'; import { showNotification } from '@mantine/notifications'; import { capitalize, getReferenceString, isOk, normalizeErrorString } from '@medplum/core'; import type { MedplumClient, WithId } from '@medplum/core'; import type { Binary, Bot, Bundle, BundleEntry, Practitioner, Questionnaire, Resource } from '@medplum/fhirtypes'; import { Document, useMedplum, useMedplumProfile } from '@medplum/react'; import { IconCircleCheck, IconCircleOff } from '@tabler/icons-react'; import { useCallback, useContext, useState } from 'react'; import type { JSX } from 'react'; import { useNavigate, useParams } from 'react-router'; import patientIntakeQuestionnaireData from '../../data/core/patient-intake-questionnaire.json'; import valuesetsData from '../../data/core/valuesets.json'; import exampleData from '../../data/example/example-organization-data.json'; import { IntakeQuestionnaireContext } from '../Questionnaire.context'; type UploadFunction = ( medplum: MedplumClient, profile: Practitioner, questionnaire: WithId<Questionnaire> ) => Promise<void>; export function UploadDataPage(): JSX.Element { const medplum = useMedplum(); const profile = useMedplumProfile(); const navigate = useNavigate(); const [pageDisabled, setPageDisabled] = useState<boolean>(false); const { questionnaire } = useContext(IntakeQuestionnaireContext); const { dataType } = useParams(); const dataTypeDisplay = dataType ? capitalize(dataType) : ''; const buttonDisabled = dataType === 'bots' && (!checkQuestionnairesUploaded(medplum) || checkBotsUploaded(medplum)); const handleUpload = useCallback(() => { if (!profile) { return; } setPageDisabled(true); let uploadFunction: UploadFunction; switch (dataType) { case 'core': uploadFunction = uploadCoreData; break; case 'questionnaire': uploadFunction = uploadQuestionnaires; break; case 'bots': uploadFunction = uploadExampleBots; break; case 'example': uploadFunction = uploadExampleData; break; default: throw new Error(`Invalid upload type: ${dataType}`); } uploadFunction(medplum, profile as Practitioner, questionnaire as WithId<Questionnaire>) .then(() => navigate('/')) .catch((error) => { showNotification({ color: 'red', icon: <IconCircleOff />, title: 'Error', message: normalizeErrorString(error), }); }) .finally(() => setPageDisabled(false)); }, [medplum, profile, questionnaire, dataType, navigate]); return ( <Document> <LoadingOverlay visible={pageDisabled} /> <Button disabled={buttonDisabled} onClick={handleUpload}> Upload {dataTypeDisplay} data </Button> </Document> ); } async function uploadCoreData(medplum: MedplumClient): Promise<void> { const batch = valuesetsData as Bundle; const result = await medplum.executeBatch(batch); if (result.entry?.every((entry) => entry.response?.outcome && isOk(entry.response?.outcome))) { await setTimeout( () => showNotification({ icon: <IconCircleCheck />, title: 'Success', message: 'Uploaded Core Data', }), 1000 ); } else { throw new Error('Error uploading core data'); } } async function uploadQuestionnaires(medplum: MedplumClient): Promise<void> { const batch = patientIntakeQuestionnaireData as Bundle; const result = await medplum.executeBatch(batch); if (result.entry?.every((entry) => entry.response?.outcome && isOk(entry.response?.outcome))) { await setTimeout( () => showNotification({ icon: <IconCircleCheck />, title: 'Success', message: 'Uploaded Questionnaire Data', }), 1000 ); } else { throw new Error('Error uploading questionnaire data'); } } async function uploadExampleData(medplum: MedplumClient): Promise<void> { const batch = exampleData as Bundle; const result = await medplum.executeBatch(batch); if (result.entry?.every((entry) => entry.response?.outcome && isOk(entry.response?.outcome))) { await setTimeout( () => showNotification({ icon: <IconCircleCheck />, title: 'Success', message: 'Uploaded Example Data', }), 1000 ); } else { throw new Error('Error uploading example data'); } } const EXAMPLE_BOTS_JSON = '../../data/core/example-bots.json'; async function uploadExampleBots( medplum: MedplumClient, profile: Practitioner, questionnaire: WithId<Questionnaire> ): Promise<void> { let exampleBotData: Bundle; try { exampleBotData = await import(/* @vite-ignore */ EXAMPLE_BOTS_JSON); } catch (err) { console.log(err); if (err instanceof TypeError && err.message.includes('Failed to fetch')) { throw new Error('Error loading bot data. Run `npm run build:bots` and try again.'); } throw err; } let transactionString = JSON.stringify(exampleBotData); const botEntries: BundleEntry[] = (exampleBotData as Bundle).entry?.filter((e: any) => (e.resource as Resource)?.resourceType === 'Bot') || []; const botNames = botEntries.map((e) => (e.resource as Bot).name ?? ''); const botIds: Record<string, string> = {}; for (const botName of botNames) { let existingBot = await medplum.searchOne('Bot', { name: botName }); // Create a new Bot if it doesn't already exist if (!existingBot) { const projectId = profile.meta?.project; const createBotUrl = new URL('admin/projects/' + (projectId as string) + '/bot', medplum.getBaseUrl()); existingBot = (await medplum.post(createBotUrl, { name: botName, })) as WithId<Bot>; } botIds[botName] = existingBot.id as string; // Replace the Bot id placeholder in the bundle transactionString = transactionString .replaceAll(`$bot-${botName}-reference`, getReferenceString(existingBot)) .replaceAll(`$bot-${botName}-id`, existingBot.id as string); } transactionString = transactionString.replaceAll(`$${questionnaire.name}`, getReferenceString(questionnaire)); // Execute the transaction to upload / update the bot const transaction = JSON.parse(transactionString); await medplum.executeBatch(transaction); // Deploy the new bots for (const entry of botEntries) { const botName = (entry?.resource as Bot)?.name as string; const distUrl = (entry.resource as Bot).executableCode?.url; const distBinaryEntry = exampleBotData.entry?.find((e: any) => e.fullUrl === distUrl) as | BundleEntry<Binary> | undefined; if (!distBinaryEntry) { throw new Error('Error finding Bundle entry with fullUrl: ' + distUrl); } if (!distBinaryEntry.resource?.data) { throw new Error('Could not find encoded code for bot: ' + botName); } // Decode the base64 encoded code and deploy const code = atob(distBinaryEntry.resource.data); await medplum.post(medplum.fhirUrl('Bot', botIds[botName], '$deploy'), { code }); } showNotification({ icon: <IconCircleCheck />, title: 'Success', message: 'Deployed Example Bots', }); } function checkQuestionnairesUploaded(medplum: MedplumClient): boolean { let check = false; const questionnairesToCheck = medplum.searchResources('Questionnaire', { name: 'patient-intake' }).read(); if (questionnairesToCheck.length === 1) { check = true; } return check; } function checkBotsUploaded(medplum: MedplumClient): boolean { const exampleBots = medplum.searchResources('Bot', { name: 'intake-form' }).read(); if (exampleBots.length === 1) { return true; } return false; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server