Skip to content

Commit

Permalink
Perform a query against the selected class view (#84)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
JM-Mendez authored Jul 12, 2020
1 parent 7e8f640 commit c1bd7bb
Show file tree
Hide file tree
Showing 9 changed files with 110 additions and 36 deletions.
1 change: 1 addition & 0 deletions src/actionConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
48 changes: 36 additions & 12 deletions src/components/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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',
Expand All @@ -43,6 +46,7 @@ const supervisorMachine = Machine(
idle: {
on: {
[CHANGE_MINE]: { actions: 'changeMine' },
[CHANGE_CLASS]: { actions: 'changeClass' },
},
},
},
Expand All @@ -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,
})),
Expand Down
8 changes: 2 additions & 6 deletions src/components/Constraints/SelectPopup.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion src/components/Constraints/createConstraintMachine.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 43 additions & 16 deletions src/components/NavBar/NavBar.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<Menu ulRef={itemsParentRef}>
<MenuItem disabled={true} text={infoText} />
{renderedItems}
</Menu>
)
}

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 (
Expand All @@ -30,6 +55,7 @@ export const NavigationBar = () => {
*/}
<Tabs
id="classes-tab"
selectedTabId={classView}
// @ts-ignore
css={{
marginLeft: 'auto',
Expand All @@ -40,21 +66,22 @@ export const NavigationBar = () => {
},
}}
>
{visibleClasses.map((c) => (
<Tab key={c.name} id={c.name} title={c.name} />
))}
<Tab key="Templates" id="Templates" title="Templates" />
<Tab key={classView} id={classView} title={classDisplayName} />
</Tabs>
<Select
items={hiddenClasses}
items={state.context.modelClasses}
filterable={true}
itemRenderer={NumberedSelectMenuItems}
onItemSelect={handleClassSelect}
itemListRenderer={renderMenu}
itemListPredicate={filterQuery}
>
<Button
aria-label="select the views you'd like to query"
// used to override `Blueprintjs` styles for a small button
small={true}
text="add view"
text="change view"
alignText="left"
rightIcon={IconNames.CARET_DOWN}
/>
Expand Down
2 changes: 2 additions & 0 deletions src/components/QueryController/queryControllerMachine.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export const queryControllerMachine = Machine(
actions: {
// @ts-ignore
initializeMachine: assign((ctx, { globalConfig }) => {
ctx.currentConstraints = []
ctx.selectedPaths = []
ctx.classView = globalConfig.classView
ctx.rootUrl = globalConfig.rootUrl
}),
Expand Down
2 changes: 1 addition & 1 deletion src/components/Selects.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const NumberedSelectMenuItems = (item, props) => {
return (
<MenuItem
key={item.name}
text={`${props.index + 1}. ${item.name}`}
text={`${props.index + 1}. ${item?.displayName ?? item.name}`}
active={props.modifiers.active}
onClick={props.handleClick}
/>
Expand Down
15 changes: 15 additions & 0 deletions src/fetchSummary.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import axios from 'axios'
import imjs from 'imjs'

import { formatConstraintPath } from './utils'
Expand All @@ -16,3 +17,17 @@ export const fetchTable = async ({ rootUrl, query, page }) => {

return await service.tableRows(query, page)
}

export const fetchInstances = async () => {
return axios.get('https://registry.intermine.org/service/instances', {
params: {
mine: 'prod',
},
})
}

export const fetchClasses = async (url) => {
const service = new imjs.Service({ root: url })

return await service.fetchModel()
}
8 changes: 8 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
export const noop = () => {}

export const formatConstraintPath = ({ classView, path }) => `${classView}.${path}`

export const pluralizeFilteredCount = (filteredItems, query) => {
const isPlural = filteredItems.length > 1 ? 's' : ''

return query === ''
? `Showing ${filteredItems.length} Item${isPlural}`
: `Found ${filteredItems.length} item${isPlural} matching "${query}"`
}

0 comments on commit c1bd7bb

Please sign in to comment.