Skip to content

Commit

Permalink
feat(app, labware-library, app-shell): Labware creator in app prototy…
Browse files Browse the repository at this point in the history
…pe (#15485)

This is the _InT3rNaL pRoTOtyP3_ of putting Labware Creator in the app and it is behind the feature flag
  • Loading branch information
jerader authored Jul 2, 2024
1 parent 8ecdddb commit 0e7effd
Show file tree
Hide file tree
Showing 32 changed files with 880 additions and 81 deletions.
4 changes: 4 additions & 0 deletions app-shell/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
ADD_CUSTOM_LABWARE_TYPE,
ADD_CUSTOM_LABWARE_FILE_TYPE,
ADD_CUSTOM_LABWARE_FAILURE_TYPE,
ADD_CUSTOM_LABWARE_FILE_FROM_CREATOR_TYPE,
CLEAR_ADD_CUSTOM_LABWARE_FAILURE_TYPE,
ADD_NEW_LABWARE_NAME_TYPE,
CLEAR_NEW_LABWARE_NAME_TYPE,
Expand Down Expand Up @@ -114,6 +115,9 @@ export const OPEN_CUSTOM_LABWARE_DIRECTORY: OPEN_CUSTOM_LABWARE_DIRECTORY_TYPE =

export const DELETE_CUSTOM_LABWARE_FILE: DELETE_CUSTOM_LABWARE_FILE_TYPE =
'labware:DELETE_CUSTOM_LABWARE_FILE'
export const ADD_CUSTOM_LABWARE_FILE_FROM_CREATOR: ADD_CUSTOM_LABWARE_FILE_FROM_CREATOR_TYPE =
'labware:ADD_CUSTOM_LABWARE_FILE_BLOB'

// action meta literals

export const POLL: POLL_TYPE = 'poll'
Expand Down
58 changes: 46 additions & 12 deletions app-shell/src/labware/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,40 @@ export function readLabwareDirectory(dir: string): Promise<string[]> {
}

export function parseLabwareFiles(
files: string[]
filesOrContent: string | string[]
): Promise<UncheckedLabwareFile[]> {
const tasks = files.map(f => {
const readTask = fs.readJson(f, { throws: false })
const statTask = fs.stat(f)

return Promise.all([readTask, statTask]).then(([data, stats]) => ({
filename: f,
modified: stats.mtimeMs,
data,
}))
})
if (typeof filesOrContent === 'string') {
return new Promise((resolve, reject) => {
try {
const data = JSON.parse(filesOrContent)
const modified = Date.now()
const filename = `${data.parameters?.loadName}.json` ?? 'unknown_file'

resolve([{ filename, modified, data }])
} catch (error) {
reject(error)
}
})
} else if (Array.isArray(filesOrContent)) {
const tasks = filesOrContent.map(f => {
const readTask = fs.readJson(f, { throws: false })
const statTask = fs.stat(f)

return Promise.all(tasks)
return Promise.all([readTask, statTask]).then(([data, stats]) => ({
filename: f,
modified: stats.mtimeMs,
data,
}))
})

return Promise.all(tasks)
} else {
return Promise.reject(
new Error(
'Invalid input: expected an inported file or data from App Labware Creator'
)
)
}
}

// get a filename, adding an incrementor to avoid collisions
Expand Down Expand Up @@ -74,6 +94,20 @@ export function addLabwareFile(file: string, dir: string): Promise<void> {
)
}

export function addLabwareFileFromCreator(
fileContent: string,
dir: string,
fileName: string
): Promise<void> {
const extname = path.extname(fileName)
const basename = path.basename(fileName, extname)

return getFileName(dir, basename, extname).then(destName => {
const data = JSON.parse(fileContent)
return fs.outputJson(destName, data)
})
}

export function removeLabwareFile(file: string): Promise<void> {
return shell.trashItem(file).catch(() => fs.unlink(file))
}
66 changes: 60 additions & 6 deletions app-shell/src/labware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { showOpenDirectoryDialog, showOpenFileDialog } from '../dialogs'
import {
ADD_CUSTOM_LABWARE,
ADD_CUSTOM_LABWARE_FILE,
ADD_CUSTOM_LABWARE_FILE_FROM_CREATOR,
ADD_LABWARE,
CHANGE_CUSTOM_LABWARE_DIRECTORY,
CHANGE_DIRECTORY,
Expand Down Expand Up @@ -42,23 +43,44 @@ import {

const ensureDir: (dir: string) => Promise<void> = fse.ensureDir

const fetchCustomLabware = (): Promise<UncheckedLabwareFile[]> => {
const fetchCustomLabware = (
inMemoryFile?: string
): Promise<UncheckedLabwareFile[]> => {
const { labware: config } = getFullConfig()

return ensureDir(config.directory)
.then(() => Definitions.readLabwareDirectory(config.directory))
.then(Definitions.parseLabwareFiles)
.then(filePaths => {
const tasks = []

if (inMemoryFile) {
tasks.push(Definitions.parseLabwareFiles(inMemoryFile))
}
tasks.push(Definitions.parseLabwareFiles(filePaths))

return Promise.all(tasks)
})
.then(parsedFilesArrays => {
const parsedFiles = parsedFilesArrays.reduce(
(acc, curr) => acc.concat(curr),
[]
)
return parsedFiles
})
}

const fetchValidatedCustomLabware = (): Promise<CheckedLabwareFile[]> => {
return fetchCustomLabware().then(validateLabwareFiles)
const fetchValidatedCustomLabware = (
inMemoryFile?: string
): Promise<CheckedLabwareFile[]> => {
return fetchCustomLabware(inMemoryFile).then(validateLabwareFiles)
}

const fetchAndValidateCustomLabware = (
dispatch: Dispatch,
source: ListSource
source: ListSource,
inMemoryFile?: string
): Promise<void> => {
return fetchValidatedCustomLabware()
return fetchValidatedCustomLabware(inMemoryFile)
.then(payload => {
dispatch(customLabwareList(payload, source))
})
Expand Down Expand Up @@ -112,6 +134,30 @@ const copyLabware = (
})
}

const copyLabwareFromCreator = (
dispatch: Dispatch,
file: string
): Promise<void> => {
return Promise.all([
fetchCustomLabware(),
Definitions.parseLabwareFiles(file),
]).then(([existingFiles, [newFile]]) => {
const existing = validateLabwareFiles(existingFiles)
const next = validateNewLabwareFile(existing, newFile)
const dir = getFullConfig().labware.directory

if (next.type !== VALID_LABWARE_FILE) {
dispatch(addCustomLabwareFailure(next))
return
}
return Definitions.addLabwareFileFromCreator(file, dir, next.filename)
.then(() => fetchAndValidateCustomLabware(dispatch, ADD_LABWARE, file))
.then(() => {
dispatch(addNewLabwareName(newFile.filename))
})
})
}

const deleteLabware = (dispatch: Dispatch, filePath: string): Promise<void> => {
return Definitions.removeLabwareFile(filePath).then(() =>
fetchAndValidateCustomLabware(dispatch, DELETE_LABWARE)
Expand Down Expand Up @@ -193,6 +239,14 @@ export function registerLabware(
break
}

case ADD_CUSTOM_LABWARE_FILE_FROM_CREATOR: {
const file = action.payload.file
copyLabwareFromCreator(dispatch, file).catch((error: Error) => {
dispatch(addCustomLabwareFailure(null, error.message))
})
break
}

case ADD_CUSTOM_LABWARE_FILE: {
const filePath = action.payload.filePath
copyLabware(dispatch, [filePath]).catch((error: Error) => {
Expand Down
3 changes: 0 additions & 3 deletions app-shell/src/labware/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,9 @@ export function validateLabwareFiles(
): CheckedLabwareFile[] {
const validated = files.map<CheckedLabwareFile>(file => {
const { filename, data, modified } = file

// check file against the schema
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
const definition = data && validateLabwareDefinition(data)

if (definition === null) {
return { filename, modified, type: INVALID_LABWARE_FILE }
}
Expand All @@ -60,7 +58,6 @@ export function validateLabwareFiles(
return { type: DUPLICATE_LABWARE_FILE, ...props }
}
}

return v
})
}
Expand Down
1 change: 1 addition & 0 deletions app-shell/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type CHANGE_CUSTOM_LABWARE_DIRECTORY_TYPE = 'labware:CHANGE_CUSTOM_LABWAR
export type ADD_CUSTOM_LABWARE_TYPE = 'labware:ADD_CUSTOM_LABWARE'
export type ADD_CUSTOM_LABWARE_FILE_TYPE = 'labware:ADD_CUSTOM_LABWARE_FILE'
export type ADD_CUSTOM_LABWARE_FAILURE_TYPE = 'labware:ADD_CUSTOM_LABWARE_FAILURE'
export type ADD_CUSTOM_LABWARE_FILE_FROM_CREATOR_TYPE = 'labware:ADD_CUSTOM_LABWARE_FILE_BLOB'
export type CLEAR_ADD_CUSTOM_LABWARE_FAILURE_TYPE = 'labware:CLEAR_ADD_CUSTOM_LABWARE_FAILURE'
export type ADD_NEW_LABWARE_NAME_TYPE = 'labware:ADD_NEW_LABWARE_NAME'
export type CLEAR_NEW_LABWARE_NAME_TYPE = 'labware:CLEAR_NEW_LABWARE_NAME'
Expand Down
1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@fontsource/public-sans": "5.0.3",
"@opentrons/api-client": "link:../api-client",
"@opentrons/components": "link:../components",
"@opentrons/labware-library": "link:../labware-library",
"@opentrons/react-api-client": "link:../react-api-client",
"@opentrons/shared-data": "link:../shared-data",
"@opentrons/step-generation": "link:../step-generation",
Expand Down
1 change: 1 addition & 0 deletions app/src/assets/localization/en/app_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"__dev_internal__enableRunNotes": "Display Notes During a Protocol Run",
"__dev_internal__enableQuickTransfer": "Enable Quick Transfer",
"__dev_internal__enableCsvFile": "Enable CSV File",
"__dev_internal__enableLabwareCreator": "Enable App Labware Creator",
"add_folder_button": "Add labware source folder",
"add_ip_button": "Add",
"add_ip_error": "Enter an IP Address or Hostname",
Expand Down
47 changes: 38 additions & 9 deletions app/src/pages/Labware/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react'
import { useTranslation } from 'react-i18next'
import startCase from 'lodash/startCase'
import { css } from 'styled-components'
import { useDispatch } from 'react-redux'

import {
ALIGN_CENTER,
Expand All @@ -16,24 +17,27 @@ import {
JUSTIFY_SPACE_BETWEEN,
Link,
POSITION_ABSOLUTE,
PrimaryButton,
SecondaryButton,
SPACING,
LegacyStyledText,
TYPOGRAPHY,
useOnClickOutside,
} from '@opentrons/components'

import { LabwareCreator } from '@opentrons/labware-library'
import { ERROR_TOAST, SUCCESS_TOAST } from '../../atoms/Toast'
import { MenuItem } from '../../atoms/MenuList/MenuItem'
import {
useTrackEvent,
ANALYTICS_OPEN_LABWARE_CREATOR_FROM_BOTTOM_OF_LABWARE_LIBRARY_LIST,
} from '../../redux/analytics'
import { addCustomLabwareFileFromCreator } from '../../redux/custom-labware'
import { DropdownMenu } from '../../atoms/MenuList/DropdownMenu'
import { LabwareCard } from '../../organisms/LabwareCard'
import { AddCustomLabwareSlideout } from '../../organisms/AddCustomLabwareSlideout'
import { LabwareDetails } from '../../organisms/LabwareDetails'
import { useToaster } from '../../organisms/ToasterOven'
import { useFeatureFlag } from '../../redux/config'
import { useAllLabware, useLabwareFailure, useNewLabwareName } from './hooks'

import type { DropdownOption } from '../../atoms/MenuList/DropdownMenu'
Expand Down Expand Up @@ -73,12 +77,14 @@ const SORT_BY_BUTTON_STYLE = css`

export function Labware(): JSX.Element {
const { t } = useTranslation(['labware_landing', 'shared'])

const enableLabwareCreator = useFeatureFlag('enableLabwareCreator')
const [sortBy, setSortBy] = React.useState<LabwareSort>('alphabetical')
const [showSortByMenu, setShowSortByMenu] = React.useState<boolean>(false)
const toggleSetShowSortByMenu = (): void => {
setShowSortByMenu(!showSortByMenu)
}
const dispatch = useDispatch()
const [showLC, setShowLC] = React.useState<boolean>(false)
const trackEvent = useTrackEvent()
const [filterBy, setFilterBy] = React.useState<LabwareFilter>('all')
const { makeToast } = useToaster()
Expand All @@ -89,6 +95,7 @@ export function Labware(): JSX.Element {
const [showAddLabwareSlideout, setShowAddLabwareSlideout] = React.useState(
false
)

const [
currentLabwareDef,
setCurrentLabwareDef,
Expand Down Expand Up @@ -122,6 +129,17 @@ export function Labware(): JSX.Element {

return (
<>
{showLC ? (
<LabwareCreator
goBack={() => {
setShowLC(false)
}}
save={(file: string) => {
dispatch(addCustomLabwareFileFromCreator(file))
}}
isOnRunApp
/>
) : null}
<Box paddingX={SPACING.spacing16} paddingY={SPACING.spacing16}>
<Flex
flexDirection={DIRECTION_ROW}
Expand All @@ -135,13 +153,24 @@ export function Labware(): JSX.Element {
>
{t('labware')}
</LegacyStyledText>
<SecondaryButton
onClick={() => {
setShowAddLabwareSlideout(true)
}}
>
{t('import')}
</SecondaryButton>
<Flex flexDirection={DIRECTION_ROW} gridGap={SPACING.spacing4}>
<SecondaryButton
onClick={() => {
setShowAddLabwareSlideout(true)
}}
>
{t('import')}
</SecondaryButton>
{enableLabwareCreator ? (
<PrimaryButton
onClick={() => {
setShowLC(true)
}}
>
Open Labware Creator
</PrimaryButton>
) : null}
</Flex>
</Flex>
<Flex
flexDirection={DIRECTION_ROW}
Expand Down
1 change: 1 addition & 0 deletions app/src/redux/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const DEV_INTERNAL_FLAGS: DevInternalFlag[] = [
'enableQuickTransfer',
'protocolTimeline',
'enableCsvFile',
'enableLabwareCreator',
]

// action type constants
Expand Down
1 change: 1 addition & 0 deletions app/src/redux/config/schema-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type DevInternalFlag =
| 'enableQuickTransfer'
| 'protocolTimeline'
| 'enableCsvFile'
| 'enableLabwareCreator'

export type FeatureFlags = Partial<Record<DevInternalFlag, boolean | undefined>>

Expand Down
9 changes: 8 additions & 1 deletion app/src/redux/custom-labware/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export const ADD_CUSTOM_LABWARE: 'labware:ADD_CUSTOM_LABWARE' =

export const ADD_CUSTOM_LABWARE_FILE: 'labware:ADD_CUSTOM_LABWARE_FILE' =
'labware:ADD_CUSTOM_LABWARE_FILE'

export const ADD_CUSTOM_LABWARE_FILE_FROM_CREATOR: 'labware:ADD_CUSTOM_LABWARE_FILE_BLOB' =
'labware:ADD_CUSTOM_LABWARE_FILE_BLOB'
export const ADD_CUSTOM_LABWARE_FAILURE: 'labware:ADD_CUSTOM_LABWARE_FAILURE' =
'labware:ADD_CUSTOM_LABWARE_FAILURE'

Expand Down Expand Up @@ -99,6 +100,12 @@ export const addCustomLabwareFile = (
meta: { shell: true },
})

export const addCustomLabwareFileFromCreator = (file: string): any => ({
type: ADD_CUSTOM_LABWARE_FILE_FROM_CREATOR,
payload: { file },
meta: { shell: true },
})

export const deleteCustomLabwareFile = (
filePath: string
): Types.DeleteCustomLabwareFileAction => ({
Expand Down
1 change: 1 addition & 0 deletions app/src/redux/custom-labware/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const customLabwareReducer: Reducer<CustomLabwareState, Action> = (

case Actions.ADD_CUSTOM_LABWARE:
case Actions.ADD_CUSTOM_LABWARE_FILE:
case Actions.ADD_CUSTOM_LABWARE_FILE_FROM_CREATOR:
case Actions.CLEAR_ADD_CUSTOM_LABWARE_FAILURE: {
return { ...state, addFailureFile: null, addFailureMessage: null }
}
Expand Down
Loading

0 comments on commit 0e7effd

Please sign in to comment.