diff --git a/.husky/pre-commit b/.husky/pre-commit index e260cff8a..f1d227f25 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ /bin/sh . "$(dirname "$0")/_/husky.sh" -npm exec pretty-quick --staged && npm exec concurrently npm:test npm:lint +npm exec pretty-quick --staged && npm exec concurrently "npm:lint" "npm:test -- --silent" diff --git a/src/api/useCqlParsingService.ts b/src/api/useCqlParsingService.ts new file mode 100644 index 000000000..dd6ebadd4 --- /dev/null +++ b/src/api/useCqlParsingService.ts @@ -0,0 +1,39 @@ +import axios from "axios"; +import useServiceConfig from "./useServiceConfig"; +import { ServiceConfig } from "./ServiceContext"; +import { useOktaTokens } from "@madie/madie-util"; +import { CqlDefinitionCallstack } from "../components/editTestCase/groupCoverage/QiCoreGroupCoverage"; + +export class CqlParsingService { + constructor(private baseUrl: string, private getAccessToken: () => string) {} + + async getDefinitionCallstacks(cql: string): Promise { + try { + const response = await axios.put( + `${this.baseUrl}/cql/callstacks`, + cql, + { + headers: { + Authorization: `Bearer ${this.getAccessToken()}`, + "Content-Type": "text/plain", + }, + } + ); + return response.data as unknown as CqlDefinitionCallstack; + } catch (err) { + const message = `Unable to retrieve used definition references`; + throw new Error(message); + } + } +} + +const useCqlParsingService = (): CqlParsingService => { + const serviceConfig: ServiceConfig = useServiceConfig(); + const { getAccessToken } = useOktaTokens(); + return new CqlParsingService( + serviceConfig?.elmTranslationService.baseUrl, + getAccessToken + ); +}; + +export default useCqlParsingService; diff --git a/src/components/editTestCase/groupCoverage/QiCoreGroupCoverage.tsx b/src/components/editTestCase/groupCoverage/QiCoreGroupCoverage.tsx index 4000ccc73..d1a5bdd24 100644 --- a/src/components/editTestCase/groupCoverage/QiCoreGroupCoverage.tsx +++ b/src/components/editTestCase/groupCoverage/QiCoreGroupCoverage.tsx @@ -19,9 +19,31 @@ import { } from "../../../util/GroupCoverageHelpers"; import "./QiCoreGroupCoverage.scss"; +export interface CqlDefinitionExpression { + id?: string; + definitionName: string; + definitionLogic: string; + context: string; + supplDataElement: boolean; + popDefinition: boolean; + commentString: string; + returnType: string | null; + parentLibrary: string | null; + libraryDisplayName: string | null; + libraryVersion: string | null; + function: boolean; + name: string; + logic: string; +} + +export interface CqlDefinitionCallstack { + [key: string]: Array; +} + interface Props { groupPopulations: GroupPopulation[]; mappedCalculationResults: MappedCalculationResults; + cqlDefinitionCallstack?: CqlDefinitionCallstack; } interface Statement { @@ -29,6 +51,7 @@ interface Statement { relevance: Relevance; statementLevelHTML?: string | undefined; pretty?: string; + name?: string; } interface PopulationStatement extends Statement { @@ -50,6 +73,7 @@ const allDefinitions = [ const QiCoreGroupCoverage = ({ groupPopulations, mappedCalculationResults, + cqlDefinitionCallstack, }: Props) => { // selected group/criteria const [selectedCriteria, setSelectedCriteria] = useState(""); @@ -115,6 +139,7 @@ const QiCoreGroupCoverage = ({ populationName: FHIR_POPULATION_CODES[relevantPopulations[key].populationType], id: relevantPopulations[key].populationId, + name: key, }; return output; }, {}); @@ -224,6 +249,39 @@ const QiCoreGroupCoverage = ({ } }; + const generateCallstackText = (selectedDefinition: Statement): string => { + let text = ""; + cqlDefinitionCallstack[selectedDefinition.name]?.forEach( + (calledDefinition) => { + // Get Highlighted HTML from execution results + text += + mappedCalculationResults[selectedCriteria]["statementResults"][ + calledDefinition.name + ].statementLevelHTML; + // Get the callstack for each definition called by the parent statement + getCallstack(calledDefinition.id).forEach((name) => { + text += + mappedCalculationResults[selectedCriteria]["statementResults"][name] + .statementLevelHTML; + }); + } + ); + return text; + }; + + const getCallstack = (defId: string): string[] => { + let calledDefinitions: string[] = []; + cqlDefinitionCallstack[defId]?.forEach((calledDefinition) => { + calledDefinitions.push(calledDefinition.name); + if (cqlDefinitionCallstack[calledDefinition.id]) { + calledDefinitions = calledDefinitions.concat( + getCallstack(calledDefinition.id) + ); + } + }); + return calledDefinitions; + }; + return ( <>
@@ -300,12 +358,38 @@ const QiCoreGroupCoverage = ({ data-testid={`${selectedHighlightingTab.abbreviation}-highlighting`} > {selectedPopulationDefinitionResults ? ( -
- {parse(selectedPopulationDefinitionResults?.statementLevelHTML)} - -
+ <> +
+ {parse( + selectedPopulationDefinitionResults?.statementLevelHTML + )} + +
+
+ Definition(s) Used +
+
+ {parse( + generateCallstackText(selectedPopulationDefinitionResults) + )} +
+ ) : ( "No results available" )} diff --git a/src/components/editTestCase/qiCore/EditTestCase.test.tsx b/src/components/editTestCase/qiCore/EditTestCase.test.tsx index 1d85c4c4b..23b0512ba 100644 --- a/src/components/editTestCase/qiCore/EditTestCase.test.tsx +++ b/src/components/editTestCase/qiCore/EditTestCase.test.tsx @@ -41,6 +41,7 @@ import { TestCaseValidator } from "../../../validators/TestCaseValidator"; import { checkUserCanEdit } from "@madie/madie-util"; import { PopulationType as FqmPopulationType } from "fqm-execution/build/types/Enums"; import { addValues } from "../../../util/DefaultValueProcessor"; +import { defineElm } from "../../../__mocks__/define-elm-fixture"; //temporary solution (after jest updated to version 27) for error: thrown: "Exceeded timeout of 5000 ms for a test. jest.setTimeout(60000); diff --git a/src/components/editTestCase/qiCore/EditTestCase.tsx b/src/components/editTestCase/qiCore/EditTestCase.tsx index 64dd1d017..1ead83bc1 100644 --- a/src/components/editTestCase/qiCore/EditTestCase.tsx +++ b/src/components/editTestCase/qiCore/EditTestCase.tsx @@ -75,6 +75,8 @@ import { Bundle } from "fhir/r4"; import { Allotment } from "allotment"; import ElementsTab from "./LeftPanel/ElementsTab/ElementsTab"; import { QiCoreResourceProvider } from "../../../util/QiCorePatientProvider"; +import useCqlParsingService from "../../../api/useCqlParsingService"; +import { CqlDefinitionCallstack } from "../groupCoverage/QiCoreGroupCoverage"; const TestCaseForm = tw.form`m-3`; const ValidationErrorsButton = tw.button` @@ -205,6 +207,7 @@ const EditTestCase = (props: EditTestCaseProps) => { // Avoid infinite dependency render. May require additional error handling for timeouts. const testCaseService = useRef(useTestCaseServiceApi()); const calculation = useRef(calculationService()); + const cqlParsingService = useRef(useCqlParsingService()); const [alert, setAlert] = useState(null); const { errors, setErrors } = props; if (!errors) { @@ -261,6 +264,7 @@ const EditTestCase = (props: EditTestCaseProps) => { const [groupPopulations, setGroupPopulations] = useState( [] ); + const [callstackMap, setCallstackMap] = useState(); const { measureState, @@ -415,6 +419,17 @@ const EditTestCase = (props: EditTestCaseProps) => { load.current = +1; loadTestCase(); } + + if (_.isNil(callstackMap) && measure?.cql) { + cqlParsingService.current + .getDefinitionCallstacks(measure.cql) + .then((callstack: CqlDefinitionCallstack) => { + setCallstackMap(callstack); + }) + .catch((error) => { + console.error(error); + }); + } }, [ id, measureId, @@ -886,6 +901,7 @@ const EditTestCase = (props: EditTestCaseProps) => { calculationResults={populationGroupResults} calculationErrors={calculationErrors} groupPopulations={groupPopulations} + cqlDefinitionCallstack={callstackMap} /> )}
diff --git a/src/components/editTestCase/qiCore/calculationResults/CalculationResults.tsx b/src/components/editTestCase/qiCore/calculationResults/CalculationResults.tsx index 5a2013a21..c5ab36282 100644 --- a/src/components/editTestCase/qiCore/calculationResults/CalculationResults.tsx +++ b/src/components/editTestCase/qiCore/calculationResults/CalculationResults.tsx @@ -7,8 +7,10 @@ import { DetailedPopulationGroupResult } from "fqm-execution/build/types/Calcula import { MadieAlert } from "@madie/madie-design-system/dist/react"; import { GroupPopulation, PopulationType } from "@madie/madie-models"; import { useFeatureFlags } from "@madie/madie-util"; -import QiCoreGroupCoverage from "../../groupCoverage/QiCoreGroupCoverage"; import { Relevance } from "fqm-execution"; +import QiCoreGroupCoverage, { + CqlDefinitionCallstack, +} from "../../groupCoverage/QiCoreGroupCoverage"; type ErrorProps = { status?: "success" | "warning" | "error" | "info" | "meta"; @@ -19,6 +21,7 @@ type CalculationResultType = { calculationResults: DetailedPopulationGroupResult[]; calculationErrors: ErrorProps; groupPopulations: GroupPopulation[]; + cqlDefinitionCallstack?: CqlDefinitionCallstack; }; export interface MappedCalculationResults { @@ -45,6 +48,7 @@ const CalculationResults = ({ calculationResults, calculationErrors, groupPopulations, + cqlDefinitionCallstack = {}, }: CalculationResultType) => { // template for group name coming from execution engine const originalGroupName = (name) => { @@ -140,6 +144,7 @@ const CalculationResults = ({ )} {!featureFlags.highlightingTabs && coverageHtmls && (