Skip to content

Commit

Permalink
Implement optimistic response for Tempo field updates
Browse files Browse the repository at this point in the history
  • Loading branch information
qu8n authored and ao508 committed May 21, 2024
1 parent b1c4b19 commit ca5ff71
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 124 deletions.
4 changes: 1 addition & 3 deletions frontend/src/components/SamplesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import { ErrorMessage, LoadingSpinner, Toolbar } from "../shared/tableElements";
import styles from "./records.module.scss";
import { getUserEmail } from "../utils/getUserEmail";
import { openLoginPopup } from "../utils/openLoginPopup";
import _ from "lodash";

const POLLING_INTERVAL = 2000;
const max_rows = 500;
Expand Down Expand Up @@ -140,8 +139,7 @@ export default function SamplesList({

// prevent registering a change if no actual changes are made
const noChangeInVal = rowNode.data[fieldName] === newValue;
const noChangeInEmptyCell =
_.isEmpty(rowNode.data[fieldName]) && _.isEmpty(newValue);
const noChangeInEmptyCell = !rowNode.data[fieldName] && !newValue;
if (noChangeInVal || noChangeInEmptyCell) {
const updatedChanges = changes.filter(
(c) => !(c.primaryId === primaryId && c.fieldName === fieldName)
Expand Down
21 changes: 9 additions & 12 deletions frontend/src/components/UpdateModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect, useMemo } from "react";
import { useEffect, useMemo } from "react";
import { Button } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import { AgGridReact } from "ag-grid-react";
Expand Down Expand Up @@ -27,23 +27,18 @@ export function UpdateModal({
samples,
sampleKeyForUpdate,
}: UpdateModalProps) {
const [rowData, setRowData] = useState(changes);
const [columnDefs] = useState([
const columnDefs = [
{ field: "primaryId", rowGroup: true, hide: true },
{ field: "fieldName" },
{ field: "oldValue" },
{ field: "newValue" },
]);
];

useEffect(() => {
onOpen && onOpen();
// eslint-disable-next-line
}, []);

useEffect(() => {
setRowData(changes);
}, [changes]);

const [updateSamplesMutation] = useUpdateSamplesMutation();

async function handleSubmitUpdates() {
Expand All @@ -69,21 +64,23 @@ export function UpdateModal({
}
});

for (const [key, value] of Object.entries(changesByPrimaryId)) {
for (const [primaryId, changedFields] of Object.entries(
changesByPrimaryId
)) {
updateSamplesMutation({
variables: {
where: {
hasMetadataSampleMetadataConnection_SOME: {
node: {
primaryId: key,
primaryId: primaryId,
},
},
},
update: {
[sampleKeyForUpdate]: [
{
update: {
node: value!,
node: changedFields!,
},
},
],
Expand Down Expand Up @@ -126,7 +123,7 @@ export function UpdateModal({
<p>Are you sure you want to submit the following changes?</p>
<div className="ag-theme-alpine" style={{ height: 350 }}>
<AgGridReact
rowData={rowData}
rowData={changes}
columnDefs={columnDefs}
groupRemoveSingleChildren={true}
autoGroupColumnDef={autoGroupColumnDef}
Expand Down
7 changes: 3 additions & 4 deletions frontend/src/shared/helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import { StatusTooltip } from "./components/StatusToolTip";
import { parseUserSearchVal } from "../utils/parseSearchQueries";
import { Dispatch, SetStateAction } from "react";
import moment from "moment";
import _ from "lodash";

export interface SampleMetadataExtended extends SampleMetadata {
revisable: boolean;
Expand Down Expand Up @@ -1012,7 +1011,7 @@ export function prepareSampleCohortDataForAgGrid(samples: Sample[]) {
});
});
const deliveryDate = cohortDates?.sort()[0]; // earliest cohort date
var embargoDateAsDate = new Date(deliveryDate);
let embargoDateAsDate = new Date(deliveryDate);
embargoDateAsDate.setMonth(embargoDateAsDate.getMonth() + 18); // embargo date is 18 months post earliest delivery date
const embargoDate = moment(embargoDateAsDate).format("YYYY-MM-DD");

Expand Down Expand Up @@ -1076,8 +1075,8 @@ function formatCohortRelatedDate(date: string) {
}

export function isValidCostCenter(costCenter: string): boolean {
if (_.isEmpty(costCenter)) return true;
if (costCenter && costCenter.length !== 11) return false;
if (!costCenter) return true;
if (costCenter.length !== 11) return false;
const validCostCenter = new RegExp("^\\d{5}/\\d{5}$");
return validCostCenter.test(costCenter);
}
184 changes: 79 additions & 105 deletions graphql-server/src/schemas/neo4j.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,24 @@ import { Neo4jGraphQL } from "@neo4j/graphql";
import { OGM } from "@neo4j/graphql-ogm";
import { toGraphQLTypeDefs } from "@neo4j/introspector";
import { createHttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";
import { InMemoryCache, NormalizedCacheObject } from "apollo-cache-inmemory";
import { props } from "../utils/constants";
import {
FindSamplesByInputValueDocument,
SampleHasMetadataSampleMetadataUpdateFieldInput,
SampleHasTempoTemposUpdateFieldInput,
SampleMetadata,
SampleMetadataUpdateInput,
SampleUpdateInput,
SampleWhere,
SamplesDocument,
SortDirection,
Tempo,
UpdateSamplesMutationResponse,
} from "../generated/graphql";
import { connect, headers, StringCodec } from "nats";
const fetch = require("node-fetch");
const request = require("request-promise-native");
const ApolloClient = require("apollo-client").ApolloClient;
import { ApolloClient, ApolloQueryResult } from "apollo-client";

export async function buildNeo4jDbSchema() {
const driver = neo4j.driver(
Expand Down Expand Up @@ -51,41 +58,85 @@ export async function buildNeo4jDbSchema() {
return neo4jDbSchema;
}

function buildResolvers(ogm: OGM, apolloClient: typeof ApolloClient) {
function buildResolvers(
ogm: OGM,
apolloClient: ApolloClient<NormalizedCacheObject>
) {
return {
Mutation: {
async updateSamples(_source: any, { where, update }: any) {
async updateSamples(
_source: any,
{ where, update }: { where: SampleWhere; update: SampleUpdateInput }
) {
// Grab data passed in from the frontend
const primaryId =
where.hasMetadataSampleMetadataConnection_SOME.node.primaryId;
where.hasMetadataSampleMetadataConnection_SOME!.node!.primaryId!;

const sampleKeyForUpdate = Object.keys(
update
)[0] as keyof SampleUpdateInput;

const changedFields = (
update[sampleKeyForUpdate] as Array<
| SampleHasMetadataSampleMetadataUpdateFieldInput
| SampleHasTempoTemposUpdateFieldInput
>
)[0].update!.node!;

// Get sample manifest from SMILE API /sampleById and update fields that were changed
const sampleManifest = await request(
props.smile_sample_endpoint + primaryId,
{
json: true,
}
);

const sampleKeyForUpdate = Object.keys(update)[0];

const changesByPrimaryId = update[sampleKeyForUpdate][0].update.node;
Object.keys(changedFields).forEach((changedField) => {
const key = changedField as keyof SampleMetadataUpdateInput;
sampleManifest[key] =
changedFields[key as keyof typeof changedFields];
});

// Get the sample data from the database and update the fields that were changed
const updatedSamples: ApolloQueryResult<UpdateSamplesMutationResponse> =
await apolloClient.query({
query: SamplesDocument,
variables: {
where: {
smileSampleId: sampleManifest.smileSampleId,
},
hasMetadataSampleMetadataOptions2: {
sort: [{ importDate: SortDirection.Desc }],
limit: 1,
},
},
});

Object.keys(changedFields).forEach((key) => {
const sample = updatedSamples.data.samples[0][sampleKeyForUpdate] as
| SampleMetadata[]
| Tempo[];
if (Array.isArray(sample) && sample.length > 0) {
sample[0][key as keyof typeof sample[0]] =
changedFields[key as keyof typeof changedFields];
}
});

let updatedSamples: any;
// Publish the data updates to NATS
if ("hasMetadataSampleMetadata" in update) {
updatedSamples = await updateSampleMetadata(
sampleManifest,
changesByPrimaryId,
ogm,
apolloClient
);
} else {
updatedSamples = await updateSampleBilling(
sampleManifest,
await publishNatsMessageForSampleMetadataUpdates(sampleManifest, ogm);
} else if ("hasTempoTempos" in update) {
await publishNatsMessageForSampleBillingUpdates(
primaryId,
changesByPrimaryId,
apolloClient
updatedSamples
);
} else {
throw new Error("Unknown update field");
}

// Return the updated samples data to enable optimistic UI updates.
// The shape of the data returned here doesn't fully match the shape of the data
// in the frontend, but it has all the fields being updated
return {
samples: updatedSamples.data.samples,
};
Expand All @@ -94,16 +145,10 @@ function buildResolvers(ogm: OGM, apolloClient: typeof ApolloClient) {
};
}

async function updateSampleMetadata(
async function publishNatsMessageForSampleMetadataUpdates(
sampleManifest: any,
changesByPrimaryId: any,
ogm: OGM,
apolloClient: typeof ApolloClient
ogm: OGM
) {
Object.keys(changesByPrimaryId).forEach((primaryId: string) => {
sampleManifest[primaryId] = changesByPrimaryId[primaryId];
});

// remove 'status' from sample metadata to ensure validator and label
// generator use latest status data added during validation process
delete sampleManifest["status"];
Expand All @@ -121,73 +166,23 @@ async function updateSampleMetadata(
rd[0]["isCmoRequest"].toString();
}

// fire and forget
publishNatsMessage(
props.pub_validate_sample_update,
JSON.stringify(sampleManifest)
);

let sample = ogm.model("Sample");

await sample.update({
await ogm.model("Sample").update({
where: { smileSampleId: sampleManifest.smileSampleId },
update: { revisable: false },
});

const updatedSamples = await apolloClient.query({
query: SamplesDocument,
variables: {
where: {
smileSampleId: sampleManifest.smileSampleId,
},
hasMetadataSampleMetadataOptions2: {
sort: [{ importDate: SortDirection.Desc }],
limit: 1,
},
},
});

Object.keys(changesByPrimaryId).forEach((key: string) => {
updatedSamples.data.samples[0].hasMetadataSampleMetadata[0][key] =
changesByPrimaryId[key];
});

return updatedSamples;
}

async function updateSampleBilling(
sampleManifest: any,
async function publishNatsMessageForSampleBillingUpdates(
primaryId: string,
changesByPrimaryId: any,
apolloClient: typeof ApolloClient
updatedSamples: ApolloQueryResult<UpdateSamplesMutationResponse>
) {
const sampleData = await apolloClient.query({
query: FindSamplesByInputValueDocument,
variables: {
where: {
smileSampleId: sampleManifest.smileSampleId,
},
sampleMetadataOptions: {
sort: [{ importDate: SortDirection.Desc }],
limit: 1,
},
bamCompletesOptions: {
sort: [{ date: SortDirection.Desc }],
limit: 1,
},
mafCompletesOptions: {
sort: [{ date: SortDirection.Desc }],
limit: 1,
},
qcCompletesOptions: {
sort: [{ date: SortDirection.Desc }],
limit: 1,
},
},
});

const { billed, billedBy, costCenter } =
sampleData.data.samplesConnection.edges[0].node.hasTempoTempos[0];
updatedSamples.data.samples[0].hasTempoTempos[0];

const dataForTempoBillingUpdate = {
primaryId,
Expand All @@ -196,34 +191,13 @@ async function updateSampleBilling(
costCenter,
};

for (const primaryId in changesByPrimaryId) {
dataForTempoBillingUpdate[
primaryId as keyof typeof dataForTempoBillingUpdate
] = changesByPrimaryId[primaryId];
}

publishNatsMessage(
props.pub_tempo_sample_billing,
JSON.stringify(dataForTempoBillingUpdate)
);

const updatedSamples = await apolloClient.query({
query: SamplesDocument,
variables: {
where: {
smileSampleId: sampleManifest.smileSampleId,
},
hasMetadataSampleMetadataOptions2: {
sort: [{ importDate: SortDirection.Desc }],
limit: 1,
},
},
});

return updatedSamples;
}

async function publishNatsMessage(topic: string, message: any) {
async function publishNatsMessage(topic: string, message: string) {
const sc = StringCodec();

const tlsOptions = {
Expand Down

0 comments on commit ca5ff71

Please sign in to comment.