Skip to content

Add ability to specify default FHIR server #576

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions flyway/sql/V13_01__add_default_fhir_server.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ALTER TABLE fhir_servers
ADD COLUMN default_server BOOLEAN NOT NULL DEFAULT FALSE;

-- Add a unique constraint that only applies when default_server is TRUE
CREATE UNIQUE INDEX idx_fhir_servers_single_default
ON fhir_servers (default_server)
WHERE default_server = TRUE;
143 changes: 85 additions & 58 deletions src/app/(pages)/fhirServers/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { Icon, Label, TextInput } from "@trussworks/react-uswds";
import { Icon, Label, Tag, TextInput } from "@trussworks/react-uswds";
import {
insertFhirServer,
updateFhirServer,
Expand Down Expand Up @@ -48,6 +48,7 @@ const FhirServers: React.FC = () => {
const [tokenEndpoint, setTokenEndpoint] = useState("");
const [scopes, setScopes] = useState("");
const [disableCertValidation, setDisableCertValidation] = useState(false);
const [defaultServer, setDefaultServer] = useState(false);
const [connectionStatus, setConnectionStatus] = useState<
"idle" | "success" | "error"
>("idle");
Expand Down Expand Up @@ -86,6 +87,8 @@ const FhirServers: React.FC = () => {
setTokenEndpoint("");
setScopes("");
setConnectionStatus("idle");
setDisableCertValidation(false);
setDefaultServer(false);
setErrorMessage("");
setSelectedServer(null);
};
Expand All @@ -98,6 +101,7 @@ const FhirServers: React.FC = () => {
setServerUrl(server.hostname);
setConnectionStatus("idle");
setDisableCertValidation(server.disableCertValidation);
setDefaultServer(server.defaultServer);

// Set auth method and corresponding fields based on server data
if (server.authType) {
Expand Down Expand Up @@ -205,6 +209,7 @@ const FhirServers: React.FC = () => {
serverName,
serverUrl,
disableCertValidation,
defaultServer,
connectionResult.success,
authData,
);
Expand All @@ -224,6 +229,7 @@ const FhirServers: React.FC = () => {
serverName,
serverUrl,
disableCertValidation,
defaultServer,
connectionResult.success,
authData,
);
Expand Down Expand Up @@ -461,64 +467,78 @@ const FhirServers: React.FC = () => {
</tr>
</thead>
<tbody>
{fhirServers.map((fhirServer) => (
<tr
key={fhirServer.id}
className={classNames(styles.tableRowHover)}
>
<td>{fhirServer.name}</td>
<td>{fhirServer.hostname}</td>
<td>
{fhirServer.authType ||
(fhirServer.headers?.Authorization ? "basic" : "none")}
</td>
<td width={480}>
<div className="grid-container grid-row padding-0 display-flex flex-align-center">
{fhirServer.lastConnectionSuccessful ? (
<>
<Icon.Check
size={3}
className="usa-icon margin-right-05"
aria-label="Connected"
color="green"
/>
Connected
</>
) : (
<>
<Icon.Close
size={3}
className="usa-icon margin-right-05"
aria-label="Not connected"
color="red"
/>
Not connected
</>
)}
<span className={styles.lastChecked}>
(last checked:{" "}
{fhirServer.lastConnectionAttempt
? new Date(
fhirServer.lastConnectionAttempt,
).toLocaleString()
: "unknown"}
)
</span>
<button
className={classNames(
styles.editButton,
"usa-button usa-button--unstyled",
{fhirServers
.slice()
.sort((a, b) =>
b.defaultServer === true
? 1
: a.defaultServer === true
? -1
: 0,
)
.map((fhirServer) => (
<tr
key={fhirServer.id}
className={classNames(styles.tableRowHover)}
>
<td>
{fhirServer.name}{" "}
{fhirServer.defaultServer ? (
<Tag className="margin-left-2">DEFAULT</Tag>
) : null}
</td>
<td>{fhirServer.hostname}</td>
<td>
{fhirServer.authType ||
(fhirServer.headers?.Authorization ? "basic" : "none")}
</td>
<td width={480}>
<div className="grid-container grid-row padding-0 display-flex flex-align-center">
{fhirServer.lastConnectionSuccessful ? (
<>
<Icon.Check
size={3}
className="usa-icon margin-right-05"
aria-label="Connected"
color="green"
/>
Connected
</>
) : (
<>
<Icon.Close
size={3}
className="usa-icon margin-right-05"
aria-label="Not connected"
color="red"
/>
Not connected
</>
)}
onClick={() => handleOpenModal("edit", fhirServer)}
aria-label={`Edit ${fhirServer.name}`}
>
<Icon.Edit aria-label="edit" size={3} />
Edit
</button>
</div>
</td>
</tr>
))}
<span className={styles.lastChecked}>
(last checked:{" "}
{fhirServer.lastConnectionAttempt
? new Date(
fhirServer.lastConnectionAttempt,
).toLocaleString()
: "unknown"}
)
</span>
<button
className={classNames(
styles.editButton,
"usa-button usa-button--unstyled",
)}
onClick={() => handleOpenModal("edit", fhirServer)}
aria-label={`Edit ${fhirServer.name}`}
>
<Icon.Edit aria-label="edit" size={3} />
Edit
</button>
</div>
</td>
</tr>
))}
</tbody>
</Table>
<Modal
Expand Down Expand Up @@ -584,6 +604,13 @@ const FhirServers: React.FC = () => {
checked={disableCertValidation}
onChange={(e) => setDisableCertValidation(e.target.checked)}
/>

<Checkbox
id="default-server"
label="Default server?"
checked={defaultServer}
onChange={(e) => setDefaultServer(e.target.checked)}
/>
</Modal>
</div>
</WithAuth>
Expand Down
7 changes: 0 additions & 7 deletions src/app/(pages)/query/components/searchForm/SearchForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,11 @@ const SearchForm: React.FC<SearchFormProps> = function SearchForm({
// Fills fields with sample data based on the selected
const fillFields = useCallback(
(highlightAutofilled = true) => {
const defaultFhirServer = fhirServers.includes(
hyperUnluckyPatient.FhirServer,
)
? hyperUnluckyPatient.FhirServer
: fhirServers[0];

setFirstName(hyperUnluckyPatient.FirstName);
setLastName(hyperUnluckyPatient.LastName);
setDOB(hyperUnluckyPatient.DOB);
setMRN(hyperUnluckyPatient.MRN);
setPhone(hyperUnluckyPatient.Phone);
setFhirServer(defaultFhirServer);
setAutofilled(highlightAutofilled);
},
[fhirServers],
Expand Down
7 changes: 3 additions & 4 deletions src/app/(pages)/query/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import ResultsView from "./components/ResultsView";
import PatientSearchResults from "./components/PatientSearchResults";
import SearchForm from "./components/searchForm/SearchForm";
import SelectQuery from "./components/SelectQuery";
import { DEFAULT_DEMO_FHIR_SERVER, Mode } from "../../shared/constants";
import { Mode } from "../../shared/constants";
import StepIndicator, {
CUSTOMIZE_QUERY_STEPS,
} from "./components/stepIndicator/StepIndicator";
Expand Down Expand Up @@ -36,15 +36,14 @@ const Query: React.FC = () => {
);
const [mode, setMode] = useState<Mode>("search");
const [loading, setLoading] = useState<boolean>(false);
const [fhirServer, setFhirServer] = useState<string>(
DEFAULT_DEMO_FHIR_SERVER,
);
const [fhirServer, setFhirServer] = useState<string>("");
const [fhirServers, setFhirServers] = useState<string[]>([]);
const ctx = useContext(DataContext);

async function fetchFHIRServerNames() {
const servers = await getFhirServerNames();
setFhirServers(servers);
setFhirServer(servers[0]);
}

useEffect(() => {
Expand Down
25 changes: 18 additions & 7 deletions src/app/backend/dbServices/fhir-servers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ class FhirServerConfigService extends FhirServerConfigServiceInternal {

static async getFhirServerNames(): Promise<string[]> {
const configs = await super.getFhirServerConfigs();
// Sort so that the default server is always first
configs.sort((a, b) =>
b.defaultServer === true ? 1 : a.defaultServer === true ? -1 : 0,
);
Copy link
Collaborator

@fzhao99 fzhao99 May 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noticing on dev startup that since the default isn't initialized any way outside of user input, that Public HAPI ends up being the default server on a fresh run of the app (ie without anyone marking a default) since it shows up first. We could just ask devs to mark Aidbox as default manually each time the start the app up fresh, but in self interest wondering if we could come up with something else 😅

Screen.Recording.2025-05-02.at.10.29.04.AM.mov

Can we think of a way between the way we're returning the servers here and the way we're setting the default on the frontend to still have Aidbox populate? Maybe by also alphabetizing the list as well?

return configs.map((config) => config.name);
}

Expand Down Expand Up @@ -111,6 +115,7 @@ class FhirServerConfigService extends FhirServerConfigServiceInternal {
* @param name - The new name of the FHIR server
* @param hostname - The new URL/hostname of the FHIR server
* @param disableCertValidation - Whether to disable certificate validation
* @param defaultServer - Whether this is the default server
* @param lastConnectionSuccessful - Optional boolean indicating if the last connection was successful
* @param authData - Authentication data including auth type and credentials
* @returns An object indicating success or failure with optional error message
Expand All @@ -122,6 +127,7 @@ class FhirServerConfigService extends FhirServerConfigServiceInternal {
name: string,
hostname: string,
disableCertValidation: boolean,
defaultServer: boolean,
lastConnectionSuccessful?: boolean,
authData?: AuthData,
) {
Expand All @@ -134,13 +140,14 @@ class FhirServerConfigService extends FhirServerConfigServiceInternal {
last_connection_successful = $4,
headers = $5,
disable_cert_validation = $6,
auth_type = $7,
client_id = $8,
client_secret = $9,
token_endpoint = $10,
scopes = $11,
access_token = $12,
token_expiry = $13
default_server = $7,
auth_type = $8,
client_id = $9,
client_secret = $10,
token_endpoint = $11,
scopes = $12,
access_token = $13,
token_expiry = $14
WHERE id = $1
RETURNING *;
`;
Expand Down Expand Up @@ -183,6 +190,7 @@ class FhirServerConfigService extends FhirServerConfigServiceInternal {
lastConnectionSuccessful,
headers,
disableCertValidation,
defaultServer,
authType,
authData?.clientId || null,
authData?.clientSecret || null,
Expand Down Expand Up @@ -220,6 +228,7 @@ class FhirServerConfigService extends FhirServerConfigServiceInternal {
* @param name - The name of the FHIR server
* @param hostname - The URL/hostname of the FHIR server
* @param disableCertValidation - Whether to disable certificate validation
* @param defaultServer - Whether this is the default server
* @param lastConnectionSuccessful - Optional boolean indicating if the last connection was successful
* @param authData - Authentication data including auth type and credentials
* @returns An object indicating success or failure with optional error message
Expand All @@ -231,6 +240,7 @@ class FhirServerConfigService extends FhirServerConfigServiceInternal {
name: string,
hostname: string,
disableCertValidation: boolean,
defaultServer: boolean,
lastConnectionSuccessful?: boolean,
authData?: AuthData,
) {
Expand All @@ -255,6 +265,7 @@ class FhirServerConfigService extends FhirServerConfigServiceInternal {
lastConnectionSuccessful,
headers,
disableCertValidation,
defaultServer,
authType,
authData?.clientId || null,
authData?.clientSecret || null,
Expand Down
3 changes: 2 additions & 1 deletion src/app/backend/dbServices/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ INSERT INTO fhir_servers (
last_connection_successful,
headers,
disable_cert_validation,
default_server,
auth_type,
client_id,
client_secret,
Expand All @@ -14,7 +15,7 @@ INSERT INTO fhir_servers (
access_token,
token_expiry
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
RETURNING *;
`;

Expand Down
1 change: 1 addition & 0 deletions src/app/models/entities/fhir-servers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface FhirServerConfig {
lastConnectionAttempt?: string;
lastConnectionSuccessful?: boolean;
disableCertValidation: boolean;
defaultServer: boolean;
authType?: "none" | "basic" | "client_credentials" | "SMART";
clientId?: string;
clientSecret?: string;
Expand Down
3 changes: 0 additions & 3 deletions src/app/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,8 @@ export type DemoDataFields = {
DOB: string;
MRN: string;
Phone: string;
FhirServer: string;
};

export const DEFAULT_DEMO_FHIR_SERVER = "Aidbox";
export const HYPER_UNLUCKY_DEFAULT_ID = "f288c654-6885-4f48-999c-48d776dc06af";
/*
* Common "Hyper Unlucky" patient data used for all non-newborn screening use cases
Expand All @@ -60,7 +58,6 @@ export const hyperUnluckyPatient: DemoDataFields = {
DOB: "1975-12-06",
MRN: "8692756",
Phone: "517-425-1398",
FhirServer: DEFAULT_DEMO_FHIR_SERVER,
};

/*Labels and values for the state options dropdown on the query page*/
Expand Down
2 changes: 2 additions & 0 deletions src/app/shared/fhirClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class FHIRClient {
name: "test",
hostname: url,
disableCertValidation: disableCertValidation,
defaultServer: false,
headers: authData?.headers || {},
};

Expand Down Expand Up @@ -288,6 +289,7 @@ class FHIRClient {
this.serverConfig.name,
this.serverConfig.hostname,
this.serverConfig.disableCertValidation,
this.serverConfig.defaultServer,
this.serverConfig.lastConnectionSuccessful,
{
authType: this.serverConfig.authType as
Expand Down
Loading