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

Implement a faster query for sorted samples with search enabled and editing disabled #164

Merged
merged 23 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
44b11a6
Optimized sample dashboard query (#162)
mandawilson Sep 26, 2024
1827069
Update configs to work with new Neo4j version
qu8n Sep 20, 2024
0e9524c
Move nested fields returned in the new Samples query to the root level
qu8n Sep 23, 2024
d50ee8b
Create a placeholder custom query and type for the new Samples query
qu8n Sep 23, 2024
d7c406f
Populate cancerType and cancerTypeDetailed fields with the new Sample…
qu8n Sep 23, 2024
f86ddc6
Enable searching in the all-samples view of Samples page
qu8n Sep 24, 2024
0a33a3e
Populate recipe with the new Samples query
qu8n Sep 24, 2024
331493f
Add better error handling when getting oncotree data from cache
qu8n Sep 24, 2024
4026763
Search ct and ctDetailed fields more efficiently
qu8n Sep 24, 2024
4d84ffb
Create a separate custom schema for dashboard samples
qu8n Sep 25, 2024
fe7861a
Add a context arg to the new samples query & enable WES view
qu8n Sep 25, 2024
dc6b86c
Use the new samples query in the Request Samples view
qu8n Sep 25, 2024
8968f77
Use the new samples query in the Patient Samples view
qu8n Sep 26, 2024
8c3a70b
Use the new samples query in the Cohort Samples view
qu8n Sep 26, 2024
79bcb48
Resolve sample count using the new samples query
qu8n Sep 26, 2024
49b8519
Clean up the old sample query's WHERE variables
qu8n Sep 26, 2024
16094a6
Remove unnecessary React state 'parsedSearchVals' in Samples view
qu8n Sep 26, 2024
272fe85
Remove dataloader package
qu8n Sep 26, 2024
2495685
Trigger search when search form is cleared
qu8n Sep 26, 2024
363bda6
Trigger search when search form is cleared
qu8n Sep 26, 2024
92b21c6
Temporarily handle table element diffs between SamplesList and Record…
qu8n Sep 27, 2024
88b9774
Add Investigator Sample ID column to WES Samples view
qu8n Sep 27, 2024
b09494b
Increase sample polling interval from 2s to 5s and move investigatorS…
qu8n Sep 27, 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
2 changes: 1 addition & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default function App() {
<PatientsPage userEmail={userEmail} setUserEmail={setUserEmail} />
}
>
<Route path=":smilePatientId" />
<Route path=":patientId" />
</Route>
<Route path="/samples" element={<SamplesPage />} />
<Route
Expand Down
21 changes: 10 additions & 11 deletions frontend/src/components/RecordsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ import {
IServerSideGetRowsRequest,
} from "ag-grid-community";
import { DataName, useHookLazyGeneric } from "../shared/types";
import SamplesList from "./SamplesList";
import { Sample, SampleWhere, SortDirection } from "../generated/graphql";
import SamplesList, { SampleContext } from "./SamplesList";
import { Sample, SortDirection } from "../generated/graphql";
import { defaultColDef } from "../shared/helpers";
import { PatientIdsTriplet } from "../pages/patients/PatientsPage";
import { ErrorMessage, LoadingSpinner, Toolbar } from "../shared/tableElements";
import {
ErrorMessage,
LoadingSpinner,
Toolbar,
} from "../shared/tableElements-records";
import { AgGridReact as AgGridReactType } from "ag-grid-react/lib/agGridReact";
import { BreadCrumb } from "../shared/components/BreadCrumb";
import { Title } from "../shared/components/Title";
Expand Down Expand Up @@ -50,10 +54,7 @@ interface IRecordsListProps {
setShowDownloadModal: Dispatch<SetStateAction<boolean>>;
handleDownload: () => void;
samplesColDefs: ColDef[];
samplesParentWhereVariables: SampleWhere;
samplesRefetchWhereVariables: (
samplesParsedSearchVals: string[]
) => SampleWhere;
sampleContext?: SampleContext;
sampleKeyForUpdate?: keyof Sample;
userEmail?: string | null;
setUserEmail?: Dispatch<SetStateAction<string | null>>;
Expand All @@ -79,8 +80,7 @@ export default function RecordsList({
setShowDownloadModal,
handleDownload,
samplesColDefs,
samplesParentWhereVariables,
samplesRefetchWhereVariables,
sampleContext,
sampleKeyForUpdate,
userEmail,
setUserEmail,
Expand Down Expand Up @@ -270,8 +270,7 @@ export default function RecordsList({
<SamplesList
columnDefs={samplesColDefs}
parentDataName={dataName}
parentWhereVariables={samplesParentWhereVariables}
refetchWhereVariables={samplesRefetchWhereVariables}
sampleContext={sampleContext}
setUnsavedChanges={setUnsavedChanges}
sampleKeyForUpdate={sampleKeyForUpdate}
userEmail={userEmail}
Expand Down
148 changes: 54 additions & 94 deletions frontend/src/components/SamplesList.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
import {
SortDirection,
Sample,
SampleWhere,
useSamplesListQuery,
} from "../generated/graphql";
import { Sample, useDashboardSamplesQuery } from "../generated/graphql";
import AutoSizer from "react-virtualized-auto-sizer";
import { Button, Col, Container } from "react-bootstrap";
import { Dispatch, SetStateAction, useEffect, useMemo, useRef } from "react";
import { Dispatch, SetStateAction, useRef } from "react";
import { DownloadModal } from "./DownloadModal";
import { UpdateModal } from "./UpdateModal";
// import { UpdateModal } from "./UpdateModal";
import { AlertModal } from "./AlertModal";
import { buildTsvString } from "../utils/stringBuilders";
import {
SampleChange,
defaultColDef,
getSamplePopupParamId,
handleSearch,
isValidCostCenter,
} from "../shared/helpers";
import { AgGridReact } from "ag-grid-react";
Expand All @@ -25,35 +18,36 @@ import "ag-grid-community/styles/ag-grid.css";
import "ag-grid-community/styles/ag-theme-alpine.css";
import "ag-grid-enterprise";
import { CellValueChangedEvent, ColDef } from "ag-grid-community";
import { ErrorMessage, LoadingSpinner, Toolbar } from "../shared/tableElements";
import { ErrorMessage, Toolbar } from "../shared/tableElements";
import styles from "./records.module.scss";
import { getUserEmail } from "../utils/getUserEmail";
import { openLoginPopup } from "../utils/openLoginPopup";
import { Title } from "../shared/components/Title";
import { BreadCrumb } from "../shared/components/BreadCrumb";
import { useParams } from "react-router-dom";
import { DataName } from "../shared/types";
import { parseUserSearchVal } from "../utils/parseSearchQueries";

const POLLING_INTERVAL = 2000;
const POLLING_INTERVAL = 5000; // 5s
const MAX_ROWS_TABLE = 500;
const MAX_ROWS_EXPORT = 5000;
const MAX_ROWS_SCROLLED_ALERT =
"You've reached the maximum number of samples that can be displayed. Please refine your search to see more samples.";
const MAX_ROWS_EXPORT_EXCEED_ALERT =
"You can only download up to 5,000 rows of data at a time. Please refine your search and try again. If you need the full dataset, contact the SMILE team at cmosmile@mskcc.org";
"You can only download up to 5,000 rows of data at a time. Please refine your search and try again. If you need the full dataset, contact the SMILE team at cmosmile@mskcc.org.";
const COST_CENTER_VALIDATION_ALERT =
"Please update your Cost Center/Fund Number input as #####/##### (5 digits, a forward slash, then 5 digits). For example: 12345/12345.";
const TEMPO_EVENT_OPTIONS = {
sort: [{ date: SortDirection.Desc }],
limit: 1,
};

export interface SampleContext {
fieldName: string;
values: string[];
}

interface ISampleListProps {
columnDefs: ColDef[];
setUnsavedChanges?: (unsavedChanges: boolean) => void;
parentDataName?: DataName;
parentWhereVariables?: SampleWhere;
refetchWhereVariables: (parsedSearchVals: string[]) => SampleWhere;
sampleContext?: SampleContext;
sampleKeyForUpdate?: keyof Sample;
userEmail?: string | null;
setUserEmail?: Dispatch<SetStateAction<string | null>>;
Expand All @@ -63,34 +57,14 @@ interface ISampleListProps {
export default function SamplesList({
columnDefs,
parentDataName,
parentWhereVariables,
refetchWhereVariables,
sampleContext,
setUnsavedChanges,
sampleKeyForUpdate = "hasMetadataSampleMetadata",
userEmail,
setUserEmail,
customToolbarUI,
}: ISampleListProps) {
const { loading, error, data, startPolling, stopPolling, refetch } =
useSamplesListQuery({
variables: {
where: parentWhereVariables || {},
options: {
limit: MAX_ROWS_TABLE,
},
sampleMetadataOptions: {
sort: [{ importDate: SortDirection.Desc }],
limit: 1,
},
bamCompletesOptions: TEMPO_EVENT_OPTIONS,
mafCompletesOptions: TEMPO_EVENT_OPTIONS,
qcCompletesOptions: TEMPO_EVENT_OPTIONS,
},
pollInterval: POLLING_INTERVAL,
});

const [userSearchVal, setUserSearchVal] = useState<string>("");
const [parsedSearchVals, setParsedSearchVals] = useState<string[]>([]);
const [showDownloadModal, setShowDownloadModal] = useState(false);
const [showAlertModal, setShowAlertModal] = useState(false);
const [showUpdateModal, setShowUpdateModal] = useState(false);
Expand All @@ -101,36 +75,30 @@ export default function SamplesList({

const gridRef = useRef<AgGridReactType>(null);
const params = useParams();
const hasParams = Object.keys(params).length > 0;

const { loading, error, data, startPolling, stopPolling, refetch } =
useDashboardSamplesQuery({
variables: {
searchVals: [],
sampleContext,
},
pollInterval: POLLING_INTERVAL,
});

useEffect(() => {
const samples = data?.dashboardSamples;

if (error) return <ErrorMessage error={error} />;

function handleSearch(userSearchVal: string) {
gridRef.current?.api?.showLoadingOverlay();
refetch({
where: refetchWhereVariables(parsedSearchVals),
searchVals: parseUserSearchVal(userSearchVal),
sampleContext,
}).then(() => {
gridRef.current?.api?.hideOverlay();
});
}, [parsedSearchVals, columnDefs, refetchWhereVariables, refetch]);

useEffect(() => {
setSampleCount(data?.samplesConnection.totalCount || 0);
}, [data]);

const samples = data?.samples;

const popupParamId = useMemo(() => {
if (parentWhereVariables && samples && params) {
return getSamplePopupParamId(
parentWhereVariables,
samples,
Object.values(params)?.[0]!
);
}
return undefined;
}, [parentWhereVariables, params, samples]);

if (loading) return <LoadingSpinner />;

if (error) return <ErrorMessage error={error} />;
}

async function onCellValueChanged(params: CellValueChangedEvent) {
if (!editMode) return;
Expand Down Expand Up @@ -249,15 +217,14 @@ export default function SamplesList({
return (
<>
<Container fluid>
{!parentWhereVariables && <BreadCrumb currPageTitle="samples" />}
{!hasParams && <BreadCrumb currPageTitle="samples" />}
<Title
text={
popupParamId
? `Viewing ${parentDataName?.slice(
0,
-1
)} ${popupParamId}'s samples`
: "samples"
hasParams
? `Viewing ${parentDataName?.slice(0, -1)} ${
Object.values(params)?.[0]
}'s samples`
: "Samples"
}
/>
</Container>
Expand All @@ -273,35 +240,27 @@ export default function SamplesList({
gridRef.current?.columnApi?.getAllGridColumns()
)
)
: refetch({
options: {
limit: MAX_ROWS_EXPORT,
},
}).then((result) =>
buildTsvString(result.data.samples, columnDefs)
: refetch().then((result) =>
buildTsvString(result.data.dashboardSamples!, columnDefs)
);
}}
onComplete={() => {
setShowDownloadModal(false);
// Reset the limit back to the default value of MAX_ROWS_TABLE.
// Otherwise, polling will use the most recent value MAX_ROWS_EXPORT
refetch({
options: {
limit: MAX_ROWS_TABLE,
},
});
refetch();
}}
exportFileName={[
parentDataName?.slice(0, -1),
popupParamId,
Object.values(params)?.[0],
"samples.tsv",
]
.filter(Boolean)
.join("_")}
/>
)}

{showUpdateModal && (
{/* {showUpdateModal && (
<UpdateModal
changes={changes}
samples={samples!}
Expand All @@ -310,7 +269,7 @@ export default function SamplesList({
onOpen={() => stopPolling()}
sampleKeyForUpdate={sampleKeyForUpdate}
/>
)}
)} */}

<AlertModal
show={showAlertModal}
Expand All @@ -325,12 +284,10 @@ export default function SamplesList({
dataName={"samples"}
userSearchVal={userSearchVal}
setUserSearchVal={setUserSearchVal}
handleSearch={() => handleSearch(userSearchVal, setParsedSearchVals)}
clearUserSearchVal={() => {
setUserSearchVal("");
setParsedSearchVals([]);
}}
matchingResultsCount={`${sampleCount.toLocaleString()} matching samples`}
handleSearch={(userSearchVal) => handleSearch(userSearchVal)}
matchingResultsCount={`${
sampleCount ? sampleCount.toLocaleString() : "Loading"
} matching samples`}
handleDownload={() => {
if (sampleCount > MAX_ROWS_EXPORT) {
setAlertContent(MAX_ROWS_EXPORT_EXCEED_ALERT);
Expand Down Expand Up @@ -372,9 +329,7 @@ export default function SamplesList({
{({ width }) => (
<div
className={`ag-theme-alpine ${
parentWhereVariables
? styles.popupTableHeight
: styles.tableHeight
hasParams ? styles.popupTableHeight : styles.tableHeight
}`}
style={{ width: width }}
>
Expand Down Expand Up @@ -423,6 +378,11 @@ export default function SamplesList({
onFilterChanged={(params) => {
setSampleCount(params.api.getDisplayedRowCount());
}}
onRowDataUpdated={() => {
setSampleCount(data?.dashboardSampleCount?.totalCount || 0);
}}
onGridColumnsChanged={() => handleSearch(userSearchVal)}
suppressClickEdit={true} // temporarily disable cell editing
/>
</div>
)}
Expand Down
13 changes: 8 additions & 5 deletions frontend/src/components/UpdateModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import "ag-grid-community/styles/ag-grid.css";
import "ag-grid-community/styles/ag-theme-alpine.css";
import { ChangesByPrimaryId, SampleChange } from "../shared/helpers";
import {
Sample,
SamplesListQuery,
DashboardSample,
DashboardSamplesQuery,
SampleUpdateInput,
SampleWhere,
useUpdateSamplesMutation,
Expand All @@ -20,9 +20,9 @@ interface UpdateModalProps {
changes: SampleChange[];
onSuccess: () => void;
onHide: () => void;
samples: SamplesListQuery["samples"];
samples: DashboardSamplesQuery["dashboardSamples"];
onOpen?: () => void;
sampleKeyForUpdate: keyof Sample;
sampleKeyForUpdate: keyof DashboardSample;
}

export function UpdateModal({
Expand Down Expand Up @@ -58,7 +58,10 @@ export function UpdateModal({
}

const updatedSamples = _.cloneDeep(samples);

updatedSamples?.forEach((s) => {
if (!s) return; // TODO: fix this

const primaryId = s.primaryId as string;
if (primaryId in changesByPrimaryId) {
s.revisable = false;
Expand Down Expand Up @@ -92,7 +95,7 @@ export function UpdateModal({
},
optimisticResponse: {
updateSamples: {
samples: updatedSamples,
samples: updatedSamples as any, // TODO: fix this
},
},
});
Expand Down
Loading