Skip to content

Commit

Permalink
feat(app, robot-server): add protocol receipt toasts and protocol ids…
Browse files Browse the repository at this point in the history
… endpoint (#12725)

closes RCORE-490 and RCORE-714
  • Loading branch information
shlokamin authored May 22, 2023
1 parent 86b3298 commit bdb2bce
Show file tree
Hide file tree
Showing 14 changed files with 260 additions and 8 deletions.
11 changes: 11 additions & 0 deletions api-client/src/protocols/getProtocolIds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { GET, request } from '../request'

import type { ResponsePromise } from '../request'
import type { HostConfig } from '../types'
import type { ProtocolsIds } from './types'

export function getProtocolIds(
config: HostConfig
): ResponsePromise<ProtocolsIds> {
return request<ProtocolsIds>(GET, `/protocols/ids`, null, config)
}
1 change: 1 addition & 0 deletions api-client/src/protocols/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { getProtocolAnalyses } from './getProtocolAnalyses'
export { deleteProtocol } from './deleteProtocol'
export { createProtocol } from './createProtocol'
export { getProtocols } from './getProtocols'
export { getProtocolIds } from './getProtocolIds'

export * from './types'
export * from './utils'
Expand Down
5 changes: 5 additions & 0 deletions api-client/src/protocols/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,8 @@ export interface Protocols {
links?: ResourceLinks
data: ProtocolResource[]
}

export interface ProtocolsIds {
links?: ResourceLinks
data: string[]
}
9 changes: 8 additions & 1 deletion app/src/App/OnDeviceDisplayApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { InitialLoadingScreen } from '../pages/OnDeviceDisplay/InitialLoadingScr
import { PortalRoot as ModalPortalRoot } from './portal'
import { getOnDeviceDisplaySettings, updateConfigValue } from '../redux/config'
import { SLEEP_NEVER_MS } from './constants'
import { useCurrentRunRoute } from './hooks'
import { useCurrentRunRoute, useProtocolReceiptToast } from './hooks'

import type { Dispatch } from '../redux/types'
import type { RouteProps } from './types'
Expand Down Expand Up @@ -250,6 +250,7 @@ export const OnDeviceDisplayApp = (): JSX.Element => {
<SleepScreen />
) : (
<ToasterOven>
<ProtocolReceiptToasts />
<Switch>
{onDeviceDisplayRoutes.map(
({ Component, exact, path }: RouteProps) => {
Expand All @@ -276,7 +277,13 @@ export const OnDeviceDisplayApp = (): JSX.Element => {
function TopLevelRedirects(): JSX.Element | null {
const runRouteMatch = useRouteMatch({ path: '/runs/:runId' })
const currentRunRoute = useCurrentRunRoute()

if (runRouteMatch != null && currentRunRoute == null)
return <Redirect to="/dashboard" />
return currentRunRoute != null ? <Redirect to={currentRunRoute} /> : null
}

function ProtocolReceiptToasts(): null {
useProtocolReceiptToast()
return null
}
9 changes: 8 additions & 1 deletion app/src/App/__tests__/OnDeviceDisplayApp.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { NameRobot } from '../../pages/OnDeviceDisplay/NameRobot'
import { InitialLoadingScreen } from '../../pages/OnDeviceDisplay/InitialLoadingScreen'
import { getOnDeviceDisplaySettings } from '../../redux/config'
import { getIsShellReady } from '../../redux/shell'
import { useCurrentRunRoute } from '../hooks'
import { useCurrentRunRoute, useProtocolReceiptToast } from '../hooks'

import type { OnDeviceDisplaySettings } from '../../redux/config/types'

Expand Down Expand Up @@ -101,6 +101,9 @@ const mockgetIsShellReady = getIsShellReady as jest.MockedFunction<
const mockUseCurrentRunRoute = useCurrentRunRoute as jest.MockedFunction<
typeof useCurrentRunRoute
>
const mockUseProtocolReceiptToasts = useProtocolReceiptToast as jest.MockedFunction<
typeof useProtocolReceiptToast
>

const render = (path = '/') => {
return renderWithProviders(
Expand Down Expand Up @@ -211,4 +214,8 @@ describe('OnDeviceDisplayApp', () => {
mockgetIsShellReady.mockReturnValue(true)
getByText('Mock Loading')
})
it('renders protocol receipt toasts', () => {
render('/')
expect(mockUseProtocolReceiptToasts).toHaveBeenCalled()
})
})
86 changes: 82 additions & 4 deletions app/src/App/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
import * as React from 'react'
import difference from 'lodash/difference'
import { useTranslation } from 'react-i18next'
import { useQueryClient } from 'react-query'
import { useDispatch } from 'react-redux'
import { useInterval } from '@opentrons/components'
import { checkShellUpdate } from '../redux/shell'

import type { Dispatch } from '../redux/types'
import { useAllRunsQuery } from '@opentrons/react-api-client'
import {
useAllProtocolIdsQuery,
useAllRunsQuery,
useHost,
} from '@opentrons/react-api-client'
import {
getProtocol,
RUN_ACTION_TYPE_PLAY,
RUN_STATUS_BLOCKED_BY_OPEN_DOOR,
RUN_STATUS_IDLE,
RUN_STATUS_STOPPED,
RUN_STATUS_FAILED,
RUN_STATUS_SUCCEEDED,
} from '@opentrons/api-client'
import { checkShellUpdate } from '../redux/shell'
import { useToaster } from '../organisms/ToasterOven'

import type { Dispatch } from '../redux/types'

const CURRENT_RUN_POLL = 5000
const UPDATE_RECHECK_INTERVAL_MS = 60000
const PROTOCOL_IDS_RECHECK_INTERVAL_MS = 3000

export function useSoftwareUpdatePoll(): void {
const dispatch = useDispatch<Dispatch>()
Expand All @@ -25,6 +35,74 @@ export function useSoftwareUpdatePoll(): void {
useInterval(checkAppUpdate, UPDATE_RECHECK_INTERVAL_MS)
}

export function useProtocolReceiptToast(): void {
const host = useHost()
const { t } = useTranslation('protocol_info')
const { makeToast } = useToaster()
const queryClient = useQueryClient()
const protocolIdsQuery = useAllProtocolIdsQuery(
{
refetchInterval: PROTOCOL_IDS_RECHECK_INTERVAL_MS,
},
true
)
const protocolIds = protocolIdsQuery.data?.data ?? []
const protocolIdsRef = React.useRef(protocolIds)
const hasRefetched = React.useRef(true)

if (protocolIdsQuery.isRefetching === true) {
hasRefetched.current = false
}

React.useEffect(() => {
const newProtocolIds = difference(protocolIds, protocolIdsRef.current)
if (!hasRefetched.current && newProtocolIds.length > 0) {
Promise.all(
newProtocolIds.map(protocolId => {
if (host != null) {
return (
getProtocol(host, protocolId).then(
data =>
data.data.data.metadata.protocolName ??
data.data.data.files[0].name
) ?? ''
)
} else {
return Promise.reject(
new Error(
'no host provider info inside of useProtocolReceiptToast'
)
)
}
})
)
.then(protocolNames => {
protocolNames.forEach(name => {
makeToast(
t('protocol_added', {
protocol_name: name,
}),
'success'
)
})
})
.then(() => {
queryClient
.invalidateQueries([host, 'protocols'])
.catch((e: Error) =>
console.error(`error invalidating protocols query: ${e.message}`)
)
})
.catch((e: Error) => {
console.error(e)
})
}
protocolIdsRef.current = protocolIds
// dont want this hook to rerun when other deps change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [protocolIds])
}

export function useCurrentRunRoute(): string | null {
const { data: allRuns } = useAllRunsQuery(
{ pageLength: 1 },
Expand Down
1 change: 1 addition & 0 deletions app/src/assets/localization/en/protocol_info.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"pin_protocol": "Pin protocol",
"pinned_protocol": "Pinned protocol",
"pinned_protocols": "Pinned Protocols",
"protocol_added": "Successfully received {{protocol_name}}",
"protocol_finishing": "Finishing Protocol On {{robot_name}}",
"protocol_loading": "Opening Protocol On {{robot_name}}",
"protocol_name_title": "Protocol Name",
Expand Down
1 change: 1 addition & 0 deletions react-api-client/src/protocols/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { useAllProtocolsQuery } from './useAllProtocolsQuery'
export { useAllProtocolIdsQuery } from './useAllProtocolIdsQuery'
export { useProtocolQuery } from './useProtocolQuery'
export { useProtocolAnalysesQuery } from './useProtocolAnalysesQuery'
export { useCreateProtocolMutation } from './useCreateProtocolMutation'
Expand Down
29 changes: 29 additions & 0 deletions react-api-client/src/protocols/useAllProtocolIdsQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { UseQueryResult, useQuery } from 'react-query'
import { getProtocolIds } from '@opentrons/api-client'
import { useHost } from '../api'
import type { HostConfig, ProtocolsIds } from '@opentrons/api-client'
import type { UseQueryOptions } from 'react-query'

const POLLING_INTERVAL = 1000

export function useAllProtocolIdsQuery(
options?: UseQueryOptions<ProtocolsIds>,
enablePolling?: boolean
): UseQueryResult<ProtocolsIds | null> {
const host = useHost()
const allOptions: UseQueryOptions<ProtocolsIds> = {
...options,
enabled: host !== null && (enablePolling == null || enablePolling),
refetchInterval:
enablePolling != null
? options?.refetchInterval ?? POLLING_INTERVAL
: false,
}
const query = useQuery<ProtocolsIds>(
[host, 'protocols', 'ids'],
() => getProtocolIds(host as HostConfig).then(response => response.data),
allOptions
)

return query
}
9 changes: 9 additions & 0 deletions robot-server/robot_server/protocols/protocol_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,14 @@ def get_all(self) -> List[ProtocolResource]:
for r in all_sql_resources
]

@lru_cache(maxsize=_CACHE_ENTRIES)
def get_all_ids(self) -> List[str]:
"""Get all protocol ids currently saved in this store."""
select_ids = sqlalchemy.select(protocol_table.c.id).order_by(sqlite_rowid)
with self._sql_engine.begin() as transaction:
protocol_ids = transaction.execute(select_ids).scalars().all()
return protocol_ids

def get_id_by_hash(self, hash: str) -> Optional[str]:
"""Get all protocol hashes keyed by protocol id."""
for p in self.get_all():
Expand Down Expand Up @@ -359,6 +367,7 @@ def _sql_remove(self, protocol_id: str) -> None:

def _clear_caches(self) -> None:
self.get.cache_clear()
self.get_all_ids.cache_clear()
self.get_all.cache_clear()
self.has.cache_clear()

Expand Down
23 changes: 22 additions & 1 deletion robot-server/robot_server/protocols/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
FileHasher,
)
from opentrons_shared_data.robot.dev_types import RobotType

from robot_server.errors import ErrorDetails, ErrorBody
from robot_server.hardware import get_robot_type
from robot_server.service.task_runner import TaskRunner, get_task_runner
Expand Down Expand Up @@ -306,6 +305,28 @@ async def get_protocols(
)


@protocols_router.get(
path="/protocols/ids",
summary="Get uploaded protocol ids",
responses={status.HTTP_200_OK: {"model": SimpleMultiBody[str]}},
)
async def get_protocol_ids(
protocol_store: ProtocolStore = Depends(get_protocol_store),
) -> PydanticResponse[SimpleMultiBody[str]]:
"""Get a list of all protocol ids stored on the server.
Args:
protocol_store: In-memory database of protocol resources.
"""
protocol_ids = protocol_store.get_all_ids()

meta = MultiBodyMeta(cursor=0, totalLength=len(protocol_ids))

return await PydanticResponse.create(
content=SimpleMultiBody.construct(data=protocol_ids, meta=meta)
)


@protocols_router.get(
path="/protocols/{protocolId}",
summary="Get an uploaded protocol",
Expand Down
2 changes: 1 addition & 1 deletion robot-server/robot_server/service/json_api/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class ResourceModel(BaseModel):
id: str = Field(..., description="Unique identifier of the resource.")


ResponseDataT = TypeVar("ResponseDataT", bound=BaseModel)
ResponseDataT = TypeVar("ResponseDataT")
ResponseLinksT = TypeVar("ResponseLinksT")


Expand Down
49 changes: 49 additions & 0 deletions robot-server/tests/protocols/test_protocol_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,3 +371,52 @@ def test_get_referencing_run_ids(
run_store.remove(run_id="run-id-2")

assert subject.get_referencing_run_ids("protocol-id-1") == []


def test_get_protocol_ids(
subject: ProtocolStore,
) -> None:
"""It should return a list of protocol ids."""
protocol_resource_1 = ProtocolResource(
protocol_id="protocol-id-1",
created_at=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc),
source=ProtocolSource(
directory=None,
main_file=Path("/dev/null"),
config=JsonProtocolConfig(schema_version=123),
files=[],
metadata={},
robot_type="OT-2 Standard",
content_hash="abc1",
),
protocol_key=None,
)

protocol_resource_2 = ProtocolResource(
protocol_id="protocol-id-2",
created_at=datetime(year=2021, month=1, day=2, tzinfo=timezone.utc),
source=ProtocolSource(
directory=None,
main_file=Path("/dev/null"),
config=JsonProtocolConfig(schema_version=123),
files=[],
metadata={},
robot_type="OT-2 Standard",
content_hash="abc2",
),
protocol_key=None,
)

assert subject.get_all_ids() == []

subject.insert(protocol_resource_1)

assert subject.get_all_ids() == ["protocol-id-1"]

subject.insert(protocol_resource_2)
assert subject.get_all_ids() == ["protocol-id-1", "protocol-id-2"]

subject.remove(protocol_id="protocol-id-1")
subject.remove(protocol_id="protocol-id-2")

assert subject.get_all_ids() == []
Loading

0 comments on commit bdb2bce

Please sign in to comment.