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/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}) => { 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 } 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 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..f5bda4cf 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 = ({ ) : ( <> - + 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