From aa206d30e674be6484da774eea13e9b52a42e523 Mon Sep 17 00:00:00 2001 From: Anna Guo Date: Fri, 9 Jul 2021 11:32:14 -0400 Subject: [PATCH 1/6] add savedPlotQueries field to the schema --- database/schema/run.js | 48 +++++++++++++++++++++++++++ graphql/schema/run/typeDefinitions.js | 43 ++++++++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/database/schema/run.js b/database/schema/run.js index 523212a5..6317d2af 100644 --- a/database/schema/run.js +++ b/database/schema/run.js @@ -36,6 +36,49 @@ const UploadNamesSchema = new mongoose.Schema ({ } }) +const PlotQuerySchema = new mongoose.Schema ({ + activeResult: { + type: String, + default: null + }, + selectedQC: { + type: String, + default: null + }, + selectedFeature: { + type: String, + default: null + }, + selectedFeatures: { + type: [String], + default: null + }, + selectedScaleBy: { + type: String, + default: null + }, + selectedExpRange: { + type: [String], + default: null + }, + selectedGroup: { + type: String, + default: null + }, + selectedAssay: { + type: String, + default: null + }, + selectedDiffExpression: { + type: String, + default: null + }, + selectedQCDataset: { + type: mongoose.Schema.Types.ObjectId, + default: null + }, +}) + const RunSchema = new mongoose.Schema({ runID: { type: mongoose.Schema.Types.ObjectId, @@ -109,6 +152,11 @@ const RunSchema = new mongoose.Schema({ uploadNames: { type: UploadNamesSchema, default: {metadata: null, gsva: null}, + }, + + savedPlotQueries: { + type: [PlotQuerySchema], + default: [] } }) diff --git a/graphql/schema/run/typeDefinitions.js b/graphql/schema/run/typeDefinitions.js index 27431dfb..8d954d4c 100644 --- a/graphql/schema/run/typeDefinitions.js +++ b/graphql/schema/run/typeDefinitions.js @@ -17,6 +17,34 @@ const typeDefs = gql` metadata: String } + type PlotQuery { + id: ID + activeResult: String + selectedQC: String + selectedFeature: String + selectedFeatures: [String] + selectedScaleBy: String + selectedExpRange: [String] + selectedGroup: String + selectedAssay: String + selectedDiffExpression: String + selectedQCDataset: ID + } + + input PlotQueryInput { + plotQueryID: ID + activeResult: String + selectedQC: String + selectedFeature: String + selectedFeatures: [String] + selectedScaleBy: String + selectedExpRange: [String] + selectedGroup: String + selectedAssay: String + selectedDiffExpression: String + selectedQCDataset: ID + } + type Run { runID: ID createdOn: Date @@ -43,6 +71,8 @@ const typeDefs = gql` datasets: [Dataset] # Datasets selected within a run to act as reference/anchors for CWL referenceDatasets: [Dataset] + + savedPlotQueries: [PlotQuery] } type Query { allRuns: [Run] @@ -113,6 +143,19 @@ const typeDefs = gql` updateRunName( runID: ID! newName: String! + savePlotQuery( + runID: ID!, + input: PlotQueryInput! + ): Run + + updateSavedPlotQuery( + runID: ID!, + input: PlotQueryInput! + ): Run + + removeSavedPlotQuery( + runID: ID!, + plotQueryID: ID! ): Run } From 9f294bea93b9446e5a19936523ca94dce1c498e2 Mon Sep 17 00:00:00 2001 From: Anna Guo Date: Fri, 9 Jul 2021 11:33:41 -0400 Subject: [PATCH 2/6] add graphql mutations to manipulate saved plot queries --- graphql/schema/run/resolvers.js | 43 ++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/graphql/schema/run/resolvers.js b/graphql/schema/run/resolvers.js index ab9624da..bef50453 100644 --- a/graphql/schema/run/resolvers.js +++ b/graphql/schema/run/resolvers.js @@ -240,7 +240,48 @@ const resolvers = { run.name = newName await run.save() return run - } + }, + + savePlotQuery: async (parent, {runID, input}, {Runs}) => { + try { + const run = await Runs.findOne({runID}) + run.savedPlotQueries = [...run.savedPlotQueries, R.omit(['plotQueryID'], input)] + await run.save() + return run + } catch (err) { + console.log(err) + } + }, + + updateSavedPlotQuery: async (parent, {runID, input}, {Runs}) => { + try { + const run = await Runs.findOne({runID}) + const plotQueryIndex = R.findIndex(R.propEq('id', input.plotQueryID))(run.savedPlotQueries); + const fieldsToChange = R.compose( + R.reduce((obj, field) => { + obj[`savedPlotQueries.${plotQueryIndex}.${field}`] = input[field] + return obj + }, {}), + R.keys(R.__), + R.omit(['plotQueryID']) + )(input) + await Runs.updateOne({runID}, {$set: fieldsToChange}) + return run + } catch (err) { + console.log(err) + } + }, + + removeSavedPlotQuery: async (parent, {runID, plotQueryID}, {Runs}) => { + try { + const run = await Runs.findOne({runID}) + run.savedPlotQueries = R.filter(query => !R.equals(query.id, plotQueryID), run.savedPlotQueries) + await run.save() + return run + } catch (err) { + console.log(err) + } + }, }, Run: { createdBy: async({createdBy}, variables, {Users}) => { From ea2f6bfe31e37c2e75250d7082a461abccd02186 Mon Sep 17 00:00:00 2001 From: Anna Guo Date: Fri, 9 Jul 2021 11:35:50 -0400 Subject: [PATCH 3/6] add actions and reducers for loading/update plot queries on the frontend --- react/src/redux/actions/resultsPage.js | 16 +++++++++++++++- react/src/redux/reducers/resultsPage.js | 23 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/react/src/redux/actions/resultsPage.js b/react/src/redux/actions/resultsPage.js index 53ffb660..4974fee5 100644 --- a/react/src/redux/actions/resultsPage.js +++ b/react/src/redux/actions/resultsPage.js @@ -137,6 +137,18 @@ const toggleSidebarCollapsed = () => ({ const resetResultsPage = R.always({type: 'resultsPage/reset'}) +const addSavedPlots = ({value}) =>({ + type: 'resultsPage/addSavedPlots', + payload: { + value + } +}) + +const setPlotQueryID = ({value}) => ({ + type: 'resultsPage/setPlotQueryID', + payload: {value} +}) + export { setActiveSidebarTab, setActiveDataAction, @@ -157,5 +169,7 @@ export { resetResultsPage, addPlot, setActivePlot, - toggleSidebarCollapsed + toggleSidebarCollapsed, + addSavedPlots, + setPlotQueryID } \ No newline at end of file diff --git a/react/src/redux/reducers/resultsPage.js b/react/src/redux/reducers/resultsPage.js index 85115b37..38f3f1a5 100644 --- a/react/src/redux/reducers/resultsPage.js +++ b/react/src/redux/reducers/resultsPage.js @@ -20,6 +20,7 @@ const initialPlotQuery = { selectedDiffExpression: 'All', selectedQCDataset: null, service: null, + plotQueryID: null, } const initialState = { @@ -237,5 +238,27 @@ export default createReducer( 'resultsPage/reset': R.always(initialState), + + 'resultsPage/addSavedPlots': (state, payload) => { + const {value} = payload + // function for converting selectedExpRange to float and remove __typename + const cleanUpPlotQuery = R.compose( + R.map(R.omit(['__typename'])), + R.map(R.evolve({selectedExpRange: R.map(str => parseFloat(str))})) + ) + return R.evolve({ + plotQueries: R.isEmpty(value) + ? [R.always(initialPlotQuery)] + : R.always(cleanUpPlotQuery(value)) + })(state) + }, + + 'resultsPage/setPlotQueryID': (state, payload) => { + const {value} = payload + const {activePlot} = state + return R.evolve({ + plotQueries: evolveAtIndex({plotQueryID: R.always(value)}, activePlot) + })(state) + }, } ) \ No newline at end of file From fdcb6190dddc0fe2ac5c85f04c504db8521dba9b Mon Sep 17 00:00:00 2001 From: Anna Guo Date: Fri, 9 Jul 2021 11:37:03 -0400 Subject: [PATCH 4/6] add and modify hooks for querying/adding/removing/updating plot queries --- react/src/apollo/hooks/run/index.js | 7 ++++ .../run/useRemoveSavedPlotQueryMutation.js | 36 ++++++++++++++++++ .../apollo/hooks/run/useRunDetailsQuery.js | 14 +++++++ .../hooks/run/useSavePlotQueryMutation.js | 37 +++++++++++++++++++ .../run/useUpdateSavedPlotQueryMutation.js | 26 +++++++++++++ 5 files changed, 120 insertions(+) create mode 100644 react/src/apollo/hooks/run/useRemoveSavedPlotQueryMutation.js create mode 100644 react/src/apollo/hooks/run/useSavePlotQueryMutation.js create mode 100644 react/src/apollo/hooks/run/useUpdateSavedPlotQueryMutation.js diff --git a/react/src/apollo/hooks/run/index.js b/react/src/apollo/hooks/run/index.js index 1b12a55b..1cd5478f 100644 --- a/react/src/apollo/hooks/run/index.js +++ b/react/src/apollo/hooks/run/index.js @@ -13,6 +13,10 @@ import useUploadRunMetadataMutation from './useUploadRunMetadataMutation' import useUploadRunGenesetMutation from './useUploadRunGenesetMutation' import useUpdateRunReferenceDatasetsMutation from './useUpdateRunReferenceDatasetsMutation' import useEditRunDetailsMutation from './useEditRunDetailsMutation' +import useSavePlotQueryMutation from './useSavePlotQueryMutation' +import useRemoveSavedPlotQueryMutation from './useRemoveSavedPlotQueryMutation' +import useUpdateSavedPlotQueryMutation from './useUpdateSavedPlotQueryMutation' + export { useRunDetailsQuery, @@ -30,4 +34,7 @@ export { useUploadRunGenesetMutation, useUpdateRunReferenceDatasetsMutation, useEditRunDetailsMutation, + useSavePlotQueryMutation, + useRemoveSavedPlotQueryMutation, + useUpdateSavedPlotQueryMutation } \ No newline at end of file diff --git a/react/src/apollo/hooks/run/useRemoveSavedPlotQueryMutation.js b/react/src/apollo/hooks/run/useRemoveSavedPlotQueryMutation.js new file mode 100644 index 00000000..6d5a3a77 --- /dev/null +++ b/react/src/apollo/hooks/run/useRemoveSavedPlotQueryMutation.js @@ -0,0 +1,36 @@ +import { useMutation } from '@apollo/react-hooks' +import { gql } from 'apollo-boost' +import * as RA from 'ramda-adjunct' + +export default function useRemoveSavedPlotQueryMutation( + runID, + plotQueryID, + updatePlotQueryID +) { + const [removeSavedPlotQuery, {loading, error}] = useMutation(gql` + mutation RemoveSavedPlotQuery( + $runID: ID!, + $plotQueryID: ID! + ) { + removeSavedPlotQuery( + runID: $runID, + plotQueryID: $plotQueryID + ) { + runID + savedPlotQueries { + id + } + } + } + `, { + variables: { + runID, plotQueryID + }, + onCompleted: ({removeSavedPlotQuery}) => { + if (RA.isNotNil(removeSavedPlotQuery)) { + updatePlotQueryID() + } + } + }) + return {removeSavedPlotQuery, loading} +} \ No newline at end of file diff --git a/react/src/apollo/hooks/run/useRunDetailsQuery.js b/react/src/apollo/hooks/run/useRunDetailsQuery.js index 45b822e4..613a15d6 100644 --- a/react/src/apollo/hooks/run/useRunDetailsQuery.js +++ b/react/src/apollo/hooks/run/useRunDetailsQuery.js @@ -21,6 +21,20 @@ export default function useRunDetails(runID) { status + savedPlotQueries { + id + activeResult + selectedQC + selectedFeature + selectedFeatures + selectedGroup + selectedAssay + selectedDiffExpression + selectedQCDataset + selectedScaleBy + selectedExpRange + } + secondaryRuns { wesID status diff --git a/react/src/apollo/hooks/run/useSavePlotQueryMutation.js b/react/src/apollo/hooks/run/useSavePlotQueryMutation.js new file mode 100644 index 00000000..140512d1 --- /dev/null +++ b/react/src/apollo/hooks/run/useSavePlotQueryMutation.js @@ -0,0 +1,37 @@ +import { useMutation } from '@apollo/react-hooks' +import { gql } from 'apollo-boost' +import * as RA from 'ramda-adjunct' +import * as R from 'ramda' + +export default function useSavePlotQueryMutation( + runID, + input, + updatePlotQueryID +) { + const [savePlotQuery, {loading, error}] = useMutation(gql` + mutation SavePlotQuery( + $runID: ID!, + $input: PlotQueryInput! + ) { + savePlotQuery( + runID: $runID, + input: $input + ) { + runID + savedPlotQueries { + id + } + } + } + `, { + variables: { + runID, input + }, + onCompleted: ({savePlotQuery}) => { + if (RA.isNotNil(savePlotQuery)) { + updatePlotQueryID(R.last(savePlotQuery.savedPlotQueries).id) + } + } + }) + return {savePlotQuery, loading} +} \ No newline at end of file diff --git a/react/src/apollo/hooks/run/useUpdateSavedPlotQueryMutation.js b/react/src/apollo/hooks/run/useUpdateSavedPlotQueryMutation.js new file mode 100644 index 00000000..82f7e739 --- /dev/null +++ b/react/src/apollo/hooks/run/useUpdateSavedPlotQueryMutation.js @@ -0,0 +1,26 @@ +import { useMutation } from '@apollo/react-hooks' +import { gql } from 'apollo-boost' + +export default function useUpdateSavedPlotQueryMutation( + runID, + input, +) { + const [updateSavedPlotQuery, {loading, error}] = useMutation(gql` + mutation UpdateSavedPlotQuery( + $runID: ID!, + $input: PlotQueryInput! + ) { + updateSavedPlotQuery( + runID: $runID, + input: $input + ) { + runID + } + } + `, { + variables: { + runID, input + }, + }) + return {updateSavedPlotQuery, loading} +} \ No newline at end of file From df391d345762f10644124cdd20bf35b27145de5a Mon Sep 17 00:00:00 2001 From: Anna Guo Date: Fri, 9 Jul 2021 11:43:14 -0400 Subject: [PATCH 5/6] add frontend components for handling plot queries --- .../src/components/main/resultsPage/index.js | 6 +- .../visualizations/QualityControlMenu.js | 2 +- .../visualizations/VisualizationsSidebar.js | 69 ++++++++++++++----- 3 files changed, 58 insertions(+), 19 deletions(-) diff --git a/react/src/components/main/resultsPage/index.js b/react/src/components/main/resultsPage/index.js index a94be321..2eb06660 100644 --- a/react/src/components/main/resultsPage/index.js +++ b/react/src/components/main/resultsPage/index.js @@ -8,7 +8,7 @@ import SidebarComponent from './sidebar' import ParametersComponent from './parameters' import VisualizationsComponent from './visualizations' -import {resetResultsPage, setActiveSidebarTab} from '../../../redux/actions/resultsPage' +import {resetResultsPage, setActiveSidebarTab, addSavedPlots} from '../../../redux/actions/resultsPage' import {useCrescentContext} from '../../../redux/hooks' import {useDispatch} from 'react-redux' import {useRunDetailsQuery} from '../../../apollo/hooks/run' @@ -30,12 +30,14 @@ const ResultsPageComponent = ({ const runIsIncomplete = R.includes(status, ['pending']) const sidebarTab = runIsIncomplete ? 'parameters' : 'visualizations' //'parameters' replaced by 'data', is disabled unless run.referenceDatasets is nonempty dispatch(setActiveSidebarTab({sidebarTab})) + // add the saved plot queries + dispatch(addSavedPlots({value: R.map(plotQuery => RA.renameKeys({ id: 'plotQueryID'})(plotQuery), run.savedPlotQueries)})) } }, [run]) if (R.isNil(run)) { return null } - + return ( diff --git a/react/src/components/main/resultsPage/visualizations/QualityControlMenu.js b/react/src/components/main/resultsPage/visualizations/QualityControlMenu.js index 2ff12ce2..29f648d2 100644 --- a/react/src/components/main/resultsPage/visualizations/QualityControlMenu.js +++ b/react/src/components/main/resultsPage/visualizations/QualityControlMenu.js @@ -22,7 +22,7 @@ const QualityControlMenu = ({ const datasetsOptions = useRunDatasetsDropdownQuery(runID, { onNonEmptyOptions: options => { const [{value}] = options - dispatch(setSelectedQCDataset({value})) + if (!selectedQCDataset) dispatch(setSelectedQCDataset({value})) } }) const [current, send] = useMachineService() diff --git a/react/src/components/main/resultsPage/visualizations/VisualizationsSidebar.js b/react/src/components/main/resultsPage/visualizations/VisualizationsSidebar.js index e5f5e869..0420d562 100644 --- a/react/src/components/main/resultsPage/visualizations/VisualizationsSidebar.js +++ b/react/src/components/main/resultsPage/visualizations/VisualizationsSidebar.js @@ -6,9 +6,10 @@ import * as RA from 'ramda-adjunct' import {useDispatch} from 'react-redux' import {useCrescentContext, useResultsPage} from '../../../../redux/hooks' -import {useRunDetailsQuery} from '../../../../apollo/hooks/run' +import {useRunDetailsQuery, useSavePlotQueryMutation, useRemoveSavedPlotQueryMutation, useUpdateSavedPlotQueryMutation} from '../../../../apollo/hooks/run' import {useResultsAvailableQuery} from '../../../../apollo/hooks/results' -import {setActiveResult, addPlot, setActivePlot} from '../../../../redux/actions/resultsPage' +import {useResultsPagePlotQuery} from '../../../../redux/hooks/useResultsPage'; +import {setActiveResult, addPlot, setActivePlot, setPlotQueryID} from '../../../../redux/actions/resultsPage' import VisualizationMenu from '../../resultsPage/visualizations/VisualizationMenu' import DotPlotVisualizationMenu from '../../resultsPage/visualizations/DotPlotVisualizationMenu' @@ -16,26 +17,62 @@ import HeatmapVisualizationMenu from '../../resultsPage/visualizations/HeatmapVi import QualityControlMenu from '../../resultsPage/visualizations/QualityControlMenu' const MultiPlotSelector = ({ - + runID }) => { const [numPlots, setNumPlots] = useState(1) const dispatch = useDispatch() const {activePlot, plotQueries} = useResultsPage() + const {plotQueryID} = useResultsPagePlotQuery(activePlot) + const {savePlotQuery, loading: savePlotQueryLoading} = useSavePlotQueryMutation( + runID, + R.compose( + R.over(R.lensProp('selectedExpRange'), R.map(num => num.toString()), R.__), + R.omit(['plotQueryID']) + )(plotQueries[activePlot]), + (id) => dispatch(setPlotQueryID({value: id})) + ) + const { removeSavedPlotQuery, loading: removePlotQueryLoading } = useRemoveSavedPlotQueryMutation(runID, plotQueryID, () => dispatch(setPlotQueryID({value: null}))) + const { updateSavedPlotQuery, loading: updatePlotQueryLoading } = useUpdateSavedPlotQueryMutation( + runID, + R.over(R.lensProp('selectedExpRange'), R.map(num => num.toString()), plotQueries[activePlot]) + ) return ( - - { - R.compose( - R.addIndex(R.map)((plot, idx) => ( - dispatch(setActivePlot({value: idx}))} - /> - )) - )(plotQueries) - } - dispatch(addPlot())} /> - + <> + + { + R.compose( + R.addIndex(R.map)((plot, idx) => ( + dispatch(setActivePlot({value: idx}))}> + {R.inc(idx)} + {plot.plotQueryID && ( + + )) + )(plotQueries) + } + dispatch(addPlot())} /> + + { plotQueryID ? ( + + + + + ) : ( + + )} + ) } @@ -94,7 +131,7 @@ const VisualizationsSidebar = ({ ) : ( <> - + From 73cce4697b97406ab1f9519e18eaa77c52963146 Mon Sep 17 00:00:00 2001 From: Anna Guo Date: Mon, 12 Jul 2021 14:15:30 -0400 Subject: [PATCH 6/6] change add and update buttons color to violet --- .../resultsPage/visualizations/VisualizationsSidebar.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/react/src/components/main/resultsPage/visualizations/VisualizationsSidebar.js b/react/src/components/main/resultsPage/visualizations/VisualizationsSidebar.js index 0420d562..f5bda4cf 100644 --- a/react/src/components/main/resultsPage/visualizations/VisualizationsSidebar.js +++ b/react/src/components/main/resultsPage/visualizations/VisualizationsSidebar.js @@ -47,7 +47,7 @@ const MultiPlotSelector = ({ dispatch(setActivePlot({value: idx}))}> {R.inc(idx)} {plot.plotQueryID && ( - )) @@ -57,7 +57,7 @@ const MultiPlotSelector = ({ { plotQueryID ? ( - @@ -67,7 +67,7 @@ const MultiPlotSelector = ({ ) : ( -