From c1bd7bba86a2e0f392e9900a16171aa1132081a7 Mon Sep 17 00:00:00 2001 From: John Mendez Date: Sun, 12 Jul 2020 13:52:12 -0400 Subject: [PATCH] Perform a query against the selected class view (#84) When the user changes the class, the app should reinitialize with the new query. This commit will update the query controller, default constraints, pie chart, bar graph, and table with the new results. Closes: #83 Squashed commits: * Display only a Templates and ClassView tab * Display the fetched model classes from the mine * Use imjs to fetch models instead of axios request * Update class tab when user selects a new one * Reset all constraints when the class changes * Make class list searchable --- src/actionConstants.js | 1 + src/components/App.jsx | 48 +++++++++++---- src/components/Constraints/SelectPopup.jsx | 8 +-- .../Constraints/createConstraintMachine.js | 3 +- src/components/NavBar/NavBar.jsx | 59 ++++++++++++++----- .../QueryController/queryControllerMachine.js | 2 + src/components/Selects.jsx | 2 +- src/fetchSummary.js | 15 +++++ src/utils.js | 8 +++ 9 files changed, 110 insertions(+), 36 deletions(-) diff --git a/src/actionConstants.js b/src/actionConstants.js index 0cc82611..7e2002fd 100644 --- a/src/actionConstants.js +++ b/src/actionConstants.js @@ -38,3 +38,4 @@ export const SET_INITIAL_ORGANISMS = 'pieChart/fetch/initial' * Supervisor */ export const CHANGE_MINE = 'supervisor/mine/change' +export const CHANGE_CLASS = 'supervisor/class/change' diff --git a/src/components/App.jsx b/src/components/App.jsx index 3ae9faad..12049fbb 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -2,9 +2,10 @@ import '@emotion/core' import { assign } from '@xstate/immer' -import axios from 'axios' +import FlexSearch from 'flexsearch' import React, { useEffect } from 'react' -import { CHANGE_MINE, FETCH_INITIAL_SUMMARY } from 'src/actionConstants' +import { CHANGE_CLASS, CHANGE_MINE, FETCH_INITIAL_SUMMARY } from 'src/actionConstants' +import { fetchClasses, fetchInstances } from 'src/fetchSummary' import { sendToBus, SupervisorServiceContext, useMachineBus } from 'src/machineBus' import { Machine } from 'xstate' @@ -15,20 +16,22 @@ import { Header } from './Layout/Header' const supervisorMachine = Machine( { id: 'Supervisor', - initial: 'init', + initial: 'loading', context: { classView: 'Gene', intermines: [], + modelClasses: [], + classSearchIndex: null, selectedMine: { rootUrl: 'https://www.humanmine.org/humanmine', name: 'HumanMine', }, }, states: { - init: { + loading: { invoke: { id: 'fetchMines', - src: 'fetchMines', + src: 'fetchMinesAndClasses', onDone: { target: 'idle', actions: 'setIntermines', @@ -43,6 +46,7 @@ const supervisorMachine = Machine( idle: { on: { [CHANGE_MINE]: { actions: 'changeMine' }, + [CHANGE_CLASS]: { actions: 'changeClass' }, }, }, }, @@ -54,20 +58,40 @@ const supervisorMachine = Machine( ctx.selectedMine = ctx.intermines.find((mine) => mine.name === newMine) }), // @ts-ignore + changeClass: assign((ctx, { newClass }) => { + ctx.classView = newClass + }), + // @ts-ignore setIntermines: assign((ctx, { data }) => { ctx.intermines = data.intermines + ctx.modelClasses = data.modelClasses.sort() + + // @ts-ignore + const searchIndex = new FlexSearch({ + encode: 'advanced', + tokenize: 'reverse', + suggest: true, + cache: true, + }) + + ctx.modelClasses.forEach((item) => { + // @ts-ignore + searchIndex.add(item.displayName, item.displayName) + }) + + ctx.classSearchIndex = searchIndex }), }, services: { - fetchMines: async (ctx, event) => { - const results = await axios.get('https://registry.intermine.org/service/instances', { - params: { - mine: 'prod', - }, - }) + fetchMinesAndClasses: async (ctx, event) => { + const [instancesResult, classesResult] = await Promise.all([ + fetchInstances(), + fetchClasses(ctx.selectedMine.rootUrl), + ]) return { - intermines: results.data.instances.map((mine) => ({ + modelClasses: Object.entries(classesResult.classes).map(([_key, value]) => value), + intermines: instancesResult.data.instances.map((mine) => ({ name: mine.name, rootUrl: mine.url, })), diff --git a/src/components/Constraints/SelectPopup.jsx b/src/components/Constraints/SelectPopup.jsx index ddbd6922..9802d174 100644 --- a/src/components/Constraints/SelectPopup.jsx +++ b/src/components/Constraints/SelectPopup.jsx @@ -14,6 +14,7 @@ import React, { useEffect, useRef, useState } from 'react' import { FixedSizeList as List } from 'react-window' import { ADD_CONSTRAINT, REMOVE_CONSTRAINT } from 'src/actionConstants' import { generateId } from 'src/generateId' +import { pluralizeFilteredCount } from 'src/utils' import { useServiceContext } from '../../machineBus' import { NoValuesProvided } from './NoValuesProvided' @@ -47,12 +48,7 @@ const VirtualizedMenu = ({ handleItemSelect, }) => { const listRef = useRef(null) - - const isPlural = filteredItems.length > 1 ? 's' : '' - const infoText = - query === '' - ? `Showing ${filteredItems.length} Item${isPlural}` - : `Found ${filteredItems.length} item${isPlural} matching "${query}"` + const infoText = pluralizeFilteredCount(filteredItems, query) useEffect(() => { if (listRef?.current) { diff --git a/src/components/Constraints/createConstraintMachine.js b/src/components/Constraints/createConstraintMachine.js index 71e7c5f9..65889307 100644 --- a/src/components/Constraints/createConstraintMachine.js +++ b/src/components/Constraints/createConstraintMachine.js @@ -116,9 +116,10 @@ export const createConstraintMachine = ({ }), // @ts-ignore setAvailableValues: assign((ctx, { data }) => { - // @ts-ignore ctx.availableValues = data.items ctx.classView = data.classView + ctx.selectedValues = [] + ctx.searchIndex = null if (ctx.type === 'select') { // prebuild search index for the dropdown select menu diff --git a/src/components/NavBar/NavBar.jsx b/src/components/NavBar/NavBar.jsx index 35141f9b..18458677 100644 --- a/src/components/NavBar/NavBar.jsx +++ b/src/components/NavBar/NavBar.jsx @@ -1,24 +1,49 @@ -import { Button, ButtonGroup, Classes, Navbar, Tab, Tabs } from '@blueprintjs/core' +import { Button, ButtonGroup, Classes, Menu, MenuItem, Navbar, Tab, Tabs } from '@blueprintjs/core' import { IconNames } from '@blueprintjs/icons' import { Select } from '@blueprintjs/select' import React, { useState } from 'react' +import { CHANGE_CLASS } from 'src/actionConstants' +import { useServiceContext } from 'src/machineBus' +import { pluralizeFilteredCount } from 'src/utils' import { NumberedSelectMenuItems } from '../Selects' import { Mine } from './MineSelect' -export const NavigationBar = () => { - const [visibleClasses, setVisibleClasses] = useState([{ name: 'Gene' }, { name: 'Protein' }]) - const [hiddenClasses, setHiddenClasses] = useState([ - { name: 'Enhancer' }, - { name: 'Chromosomal Duplication' }, - { name: 'GWAS' }, - ]) +const renderMenu = ({ filteredItems, itemsParentRef, query, renderItem }) => { + const renderedItems = filteredItems.map(renderItem) + const infoText = pluralizeFilteredCount(filteredItems, query) + return ( + + + {renderedItems} + + ) +} + +export const NavigationBar = () => { const [selectedTheme, changeTheme] = useState('light') const isLightTheme = selectedTheme === 'light' - const handleClassSelect = (newClass) => { - setVisibleClasses([...visibleClasses, newClass]) - setHiddenClasses(hiddenClasses.filter((c) => c.name !== newClass.name)) + + const [state, send] = useServiceContext('supervisor') + const { classView, modelClasses, classSearchIndex } = state.context + + const classDisplayName = + modelClasses.find((model) => model.name === classView)?.displayName ?? 'Gene' + + const handleClassSelect = ({ name }) => { + send({ type: CHANGE_CLASS, newClass: name }) + } + + const filterQuery = (query, items) => { + if (query === '') { + return items + } + + // flexSearch's default result limit is set 1000, so we set it to the length of all items + const results = classSearchIndex.search(query, modelClasses.length) + + return results.map((name) => ({ name })) } return ( @@ -30,6 +55,7 @@ export const NavigationBar = () => { */} { }, }} > - {visibleClasses.map((c) => ( - - ))} + +