Skip to content
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

Pacs progress websockets #1256

Closed
wants to merge 29 commits into from
Closed
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5dc1248
Add LonkClient
jennydaman Sep 16, 2024
288aea3
Add new fp-ts PfdcmClient
jennydaman Sep 16, 2024
86e791c
Begin rewriting Pacs
jennydaman Sep 16, 2024
5991eda
Fix font styling conflict by antd App component
jennydaman Sep 17, 2024
0419bb7
Change LonkClient to accept handlers via init
jennydaman Sep 21, 2024
f71c38c
Merge staging (switch from npm to pnpm)
jennydaman Sep 21, 2024
c0556a5
Migrate .../Pacs/components/input.tsx to antd
jennydaman Sep 21, 2024
d2074e2
Merge remote-tracking branch 'upstream/staging' into pacs-progress-we…
jennydaman Sep 21, 2024
707d10e
LonkClient.unsubscribeAll
jennydaman Sep 21, 2024
0c5b27d
Merge staging #1249
jennydaman Sep 22, 2024
2a72864
Reorganize Pacs to be MVC-ish
jennydaman Sep 22, 2024
2ad8b55
Add helpers for testing components which use redux
jennydaman Sep 22, 2024
75ed5fa
Merge staging #1250
jennydaman Sep 22, 2024
5e3fd36
Implement PACS query
jennydaman Sep 22, 2024
6bc5f3e
Implement PACS study details
jennydaman Sep 23, 2024
8628407
Replace Radio.Group with Segmented
jennydaman Sep 23, 2024
7c530d5
Change Pacs studies to use Collapse instead of List
jennydaman Sep 23, 2024
e2cdc0e
Add AccessionNumber to study title
jennydaman Sep 23, 2024
66a0f0b
SeriesList
jennydaman Sep 23, 2024
a20b1d0
Merge remote-tracking branch 'upstream/staging' into pacs-progress-we…
jennydaman Sep 23, 2024
5b3a2c4
Remove redux from Pacs code
jennydaman Sep 23, 2024
c4a2b39
WIP
jennydaman Sep 24, 2024
5bb10d8
ReceiveState
jennydaman Sep 24, 2024
f3057a3
Fix pfdcm tests since removal of fp-ts monads
jennydaman Sep 24, 2024
02d7e39
useLonk
jennydaman Sep 24, 2024
1a66b2a
Rename things + more documentation
jennydaman Sep 24, 2024
e39a50a
Merge staging
jennydaman Sep 25, 2024
0811d4e
PACS pull is working, progress messages aren't though.
jennydaman Sep 25, 2024
76c5a75
Pull study works, except for the last part.
jennydaman Sep 25, 2024
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
Prev Previous commit
Next Next commit
Remove redux from Pacs code
  • Loading branch information
jennydaman committed Sep 23, 2024
commit 5b3a2c48d8f33f548d50e9cff53170795d48804d
2 changes: 1 addition & 1 deletion src/api/chrisapiclient.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import { Cookies } from "react-cookie";
* passed the token, declare process.env variables, etc.
*/

// biome-ignore lint/complexity/noStaticOnlyClass: <explanation>
// biome-ignore lint/complexity/noStaticOnlyClass: Singleton pattern
class ChrisAPIClient {
private static client: Client;
private static isTokenAuthorized: boolean;
113 changes: 47 additions & 66 deletions src/api/pfdcm/client.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/**
* Wrapper for the OpenAPI-generated client, providing better typing
* and a fp-ts API.
* Wrapper for the OpenAPI-generated client, providing better typing.
*
* Note to developers: we want to use some types from fp-ts such as
* `ReadonlyNonEmptyArray` but we don't use `TaskEither` because
* traditional promises and `throw` are more compatible with TanStack
* Query.
*/

import * as TE from "fp-ts/TaskEither";
import * as E from "fp-ts/Either";
import {
Configuration,
PACSPypxApiV1PACSSyncPypxPostRequest,
@@ -14,18 +16,14 @@ import {
PACSServiceHandlerApiV1PACSThreadPypxPostRequest,
PACSasync,
} from "./generated";
import { flow, pipe } from "fp-ts/function";
import { pipe } from "fp-ts/function";
import { PypxFind, PypxTag, Series, StudyAndSeries } from "./models.ts";
import { parse as parseDate } from "date-fns";
import {
ReadonlyNonEmptyArray,
fromArray as readonlyNonEmptyArrayFromArray,
} from "fp-ts/ReadonlyNonEmptyArray";

const validateNonEmptyStringArray = flow(
readonlyNonEmptyArrayFromArray<string>,
TE.fromOption(() => new Error("PFDCM returned an empty list for services")),
);
import { match as matchOption } from "fp-ts/Option";

/**
* PFDCM client.
@@ -41,23 +39,25 @@ class PfdcmClient {
/**
* Get list of PACS services which this PFDCM is configured to speak with.
*/
public getPacsServices(): TE.TaskEither<
Error,
ReadonlyNonEmptyArray<string>
> {
public async getPacsServices(): Promise<ReadonlyNonEmptyArray<string>> {
const services =
await this.servicesClient.serviceListGetApiV1PACSserviceListGet();
return pipe(
TE.tryCatch(
() => this.servicesClient.serviceListGetApiV1PACSserviceListGet(),
E.toError,
// default service is a useless option added by pfdcm
services.filter((s) => s !== "default"),
readonlyNonEmptyArrayFromArray,
matchOption(
() => {
throw new Error(
`PFDCM is not configured with any services (besides "default")`,
);
},
(some) => some,
),
TE.flatMap(validateNonEmptyStringArray),
);
}

private find(
service: string,
query: PACSqueryCore,
): TE.TaskEither<Error, PypxFind> {
private async find(service: string, query: PACSqueryCore): Promise<PypxFind> {
const params: PACSPypxApiV1PACSSyncPypxPostRequest = {
bodyPACSPypxApiV1PACSSyncPypxPost: {
pACSservice: {
@@ -69,32 +69,31 @@ class PfdcmClient {
pACSdirective: query,
},
};
return pipe(
TE.tryCatch(
() => this.qrClient.pACSPypxApiV1PACSSyncPypxPost(params),
E.toError,
),
TE.flatMap(validateFindResponseData),
TE.flatMap(validateStatusIsTrue),
);
const data = await this.qrClient.pACSPypxApiV1PACSSyncPypxPost(params);
if (!isFindResponseData(data)) {
throw new Error("Unrecognizable response from PFDCM");
}
raiseForBadStatus(data);
return data;
}

/**
* Search for PACS data.
* @param service which PACS service to search for. See {@link PfdcmClient.getPacsServices}
* @param query PACS query
*/
public query(
public async query(
service: string,
query: PACSqueryCore,
): TE.TaskEither<Error, ReadonlyArray<StudyAndSeries>> {
return pipe(this.find(service, query), TE.map(simplifyResponse));
): Promise<ReadonlyArray<StudyAndSeries>> {
const data = await this.find(service, query);
return simplifyResponse(data);
}

public retrieve(
public async retrieve(
service: string,
query: PACSqueryCore,
): TE.TaskEither<Error, PACSasync> {
): Promise<PACSasync> {
const params: PACSServiceHandlerApiV1PACSThreadPypxPostRequest = {
bodyPACSServiceHandlerApiV1PACSThreadPypxPost: {
pACSservice: {
@@ -110,29 +109,14 @@ class PfdcmClient {
},
},
};
return pipe(
TE.tryCatch(
() => this.qrClient.pACSServiceHandlerApiV1PACSThreadPypxPost(params),
E.toError,
),
TE.flatMap((data) => {
// @ts-ignore OpenAPI spec of PFDCM is incomplete
if (data.response.job.status) {
return TE.right(data);
}
const error = new Error("PYPX job status is missing or false");
return TE.left(error);
}),
);
}
}

function validateFindResponseData(data: any): TE.TaskEither<Error, PypxFind> {
if (isFindResponseData(data)) {
return TE.right(data);
const res =
await this.qrClient.pACSServiceHandlerApiV1PACSThreadPypxPost(params);
// @ts-expect-error PFDCM OpenAPI spec is incomplete
if (!res.response?.job?.status) {
throw new Error("PYPX job status is missing or false");
}
return res;
}
const error = new Error("Invalid response from PFDCM");
return TE.left(error);
}

function isFindResponseData(data: any): data is PypxFind {
@@ -142,33 +126,30 @@ function isFindResponseData(data: any): data is PypxFind {
}

/**
* Validate that all the "status" fields are `true`
* Throw an error for any "status" field that is not `true`
* (this is a convention that Rudolph uses for error handling
* instead of HTTP status codes, exceptions, and/or monads).
*/
function validateStatusIsTrue(data: PypxFind): TE.TaskEither<Error, PypxFind> {
function raiseForBadStatus(data: PypxFind): PypxFind {
if (!data.status) {
const error = new Error("PFDCM response status=false");
return TE.left(error);
throw new Error("PFDCM response status=false");
}
if (data.pypx.status !== "success") {
const error = new Error("PFDCM response pypx.status=false");
return TE.left(error);
throw new Error("PFDCM response pypx.status=false");
}
for (const study of data.pypx.data) {
if (!Array.isArray(study.series)) {
continue;
}
for (const series of study.series) {
if (series.status.value !== "success") {
const error = new Error(
throw new Error(
`PFDCM response pypx...status is false for SeriesInstanceUID=${series?.SeriesInstanceUID?.value}`,
);
return TE.left(error);
}
}
}
return TE.right(data);
return data;
}

/**
Loading