Skip to main content
Glama
UploadDataPage.tsx10.7 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, createReference, getReferenceString, isOk, normalizeErrorString } from '@medplum/core'; import type { MedplumClient, WithId } from '@medplum/core'; import type { Binary, Bot, Bundle, BundleEntry, Coding, Practitioner, ValueSet } from '@medplum/fhirtypes'; import { Document, useMedplum, useMedplumProfile } from '@medplum/react'; import { IconCircleCheck, IconCircleOff } from '@tabler/icons-react'; import { useCallback, useState } from 'react'; import type { JSX } from 'react'; import { useNavigate, useParams } from 'react-router'; import businessStatusValueSet from '../../data/core/business-status-valueset.json'; import practitionerRoleValueSet from '../../data/core/practitioner-role-valueset.json'; import taskTypeValueSet from '../../data/core/task-type-valueset.json'; import exampleMessageData from '../../data/example/example-messages.json'; import exampleRoleData from '../../data/example/example-practitioner-role.json'; import exampleReportData from '../../data/example/example-reports.json'; import exampleTaskData from '../../data/example/example-tasks.json'; type UploadFunction = | ((medplum: MedplumClient, profile: WithId<Practitioner>) => Promise<void>) | ((medplum: MedplumClient) => Promise<void>); export function UploadDataPage(): JSX.Element { const medplum = useMedplum(); const profile = useMedplumProfile(); const { dataType } = useParams(); const navigate = useNavigate(); const [pageDisabled, setPageDisabled] = useState<boolean>(false); const dataTypeDisplay = dataType ? capitalize(dataType) : ''; const handleUpload = useCallback(() => { setPageDisabled(true); let uploadFunction: UploadFunction; switch (dataType) { case 'core': uploadFunction = uploadCoreData; break; case 'task': uploadFunction = uploadExampleTaskData; break; case 'role': uploadFunction = uploadExampleRoleData; break; case 'message': uploadFunction = uploadExampleMessageData; break; case 'report': uploadFunction = uploadExampleReportData; break; case 'qualifications': uploadFunction = uploadExampleQualifications; break; case 'bots': uploadFunction = uploadExampleBots; break; default: throw new Error(`Invalid upload type '${dataType}'`); } uploadFunction(medplum, profile as WithId<Practitioner>) .then(() => navigate(-1)) .catch((error) => { showNotification({ color: 'red', icon: <IconCircleOff />, title: 'Error', message: normalizeErrorString(error), }); }) .finally(() => setPageDisabled(false)); }, [medplum, profile, dataType, navigate]); return ( <Document> <LoadingOverlay visible={pageDisabled} /> <Button disabled={pageDisabled} onClick={handleUpload}>{`Upload ${dataTypeDisplay} Data`}</Button> </Document> ); } async function uploadCoreData(medplum: MedplumClient): Promise<void> { // Upload all the core ValueSets in a single batch request const valueSets: ValueSet[] = [ businessStatusValueSet as ValueSet, taskTypeValueSet as ValueSet, practitionerRoleValueSet as ValueSet, ]; // Upsert the ValueSet (see: https://www.medplum.com/docs/fhir-datastore/fhir-batch-requests#performing-upserts) const batch: Bundle = { resourceType: 'Bundle', type: 'transaction', entry: valueSets.flatMap((valueSet): BundleEntry[] => { const tempId = valueSet.id as string; return [ { fullUrl: tempId, request: { method: 'POST', url: valueSet.resourceType, ifNoneExist: `url=${valueSet.url}` }, resource: valueSet, }, { request: { method: 'PUT', url: tempId }, resource: { id: tempId, ...valueSet }, }, ]; }), }; console.log(batch); const result = await medplum.executeBatch(batch); console.log(result); showNotification({ icon: <IconCircleCheck />, title: 'Success', message: 'Uploaded Business Statuses', }); if (result.entry?.every((entry) => entry.response?.outcome && isOk(entry.response?.outcome))) { await setTimeout( () => showNotification({ icon: <IconCircleCheck />, title: 'Success', message: 'Uploaded Business Statuses', }), 1000 ); } else { throw new Error('Error uploading core data'); } } async function uploadExampleMessageData(medplum: MedplumClient): Promise<void> { await medplum.executeBatch(exampleMessageData as Bundle); showNotification({ icon: <IconCircleCheck />, title: 'Success', message: 'Uploaded Example Messages', }); } async function uploadExampleReportData(medplum: MedplumClient): Promise<void> { await medplum.executeBatch(exampleReportData as Bundle); showNotification({ icon: <IconCircleCheck />, title: 'Success', message: 'Uploaded Example Report', }); } async function uploadExampleTaskData(medplum: MedplumClient): Promise<void> { await medplum.executeBatch(exampleTaskData as Bundle); showNotification({ icon: <IconCircleCheck />, title: 'Success', message: 'Uploaded Example Tasks', }); } async function uploadExampleQualifications(medplum: MedplumClient, profile: Practitioner): Promise<void> { if (!profile) { return; } const states: Coding[] = [ { code: 'NY', display: 'State of New York', system: 'https://www.usps.com/' }, { code: 'CA', display: 'State of California', system: 'https://www.usps.com/' }, { code: 'TX', display: 'State of Texas', system: 'https://www.usps.com/' }, ]; await medplum.patchResource(profile.resourceType, profile.id as string, [ { path: '/qualification', // JSON patch does not have an upsert operation. If the user already has qualifications, we should just replace them with these licences op: profile.qualification ? 'replace' : 'add', value: states.map((state) => ({ code: { coding: [ { system: 'http://terminology.hl7.org/CodeSystem/v2-0360', code: 'MD', }, ], text: 'MD', }, // Medical License Issuer: State of New York issuer: { display: state.display, }, // Extension: Medical License Valid in NY extension: [ { url: 'http://hl7.org/fhir/us/davinci-pdex-plan-net/StructureDefinition/practitioner-qualification', extension: [ { url: 'whereValid', valueCodeableConcept: { coding: [state], }, }, ], }, ], })), }, ]); showNotification({ icon: <IconCircleCheck />, title: 'Success', message: 'Uploaded Example Qualifications', }); } async function uploadExampleRoleData(medplum: MedplumClient, profile: WithId<Practitioner>): Promise<void> { // Update the suffix of the current user to highlight the change if (!profile?.name?.[0]?.suffix) { await medplum.patchResource(profile.resourceType, profile.id as string, [ { op: 'add', path: '/name/0/suffix', value: ['MD'], }, ]); } const bundleString = JSON.stringify(exampleRoleData, null, 2) .replaceAll('$practitionerReference', getReferenceString(profile)) .replaceAll('"$practitioner"', JSON.stringify(createReference(profile))); const transaction = JSON.parse(bundleString) as Bundle; // Create the practitioner role await medplum.executeBatch(transaction); showNotification({ icon: <IconCircleCheck />, title: 'Success', message: 'Uploaded Example Qualifications', }); } const EXAMPLE_BOTS_JSON = '../../data/example/example-bots.json'; async function uploadExampleBots(medplum: MedplumClient, profile: Practitioner): 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) => e.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); } // 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', }); }

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