diff --git a/app/src/assets/localization/en/device_details.json b/app/src/assets/localization/en/device_details.json index 7bf132d898a8..f84554282ca1 100644 --- a/app/src/assets/localization/en/device_details.json +++ b/app/src/assets/localization/en/device_details.json @@ -57,7 +57,17 @@ "deck_fixture_setup_modal_top_description": "First, unscrew and remove the deck slot where you'll install a fixture. Then put the fixture in place and attach it as needed.", "deck_hardware": "deck hardware", "deck_slot": "deck slot {{slot}}", + "delete_all_logs": "Delete all logs?", + "delete_all_logs_description": "Deleting all logs will permanently remove them from the robot. This action cannot be undone.", + "delete_all_logs_recommendation": "We recommend downloading all log files before proceeding.", + "delete_all_run_records": "Delete all protocol run records?", + "delete_all_run_records_description": "Deleting all protocol run records will permanently remove them from the robot, along with all associated files. This action cannot be undone.", + "delete_all_run_records_recommendation": "We recommend downloading all protocol files before proceeding.", "delete_run": "Delete protocol run record", + "delete_selected": "Delete selected", + "delete_selected_run_records": "Delete selected protocol run records?", + "delete_selected_run_records_description": "Deleting the selected protocol run records will permanently remove them from the robot, along with all associated files. This action cannot be undone.", + "delete_selected_run_records_recommendation": "We recommend downloading all protocol files before proceeding.", "detach_gripper": "Detach gripper", "detach_pipette": "Detach pipette", "disable_camera": "Disable camera", diff --git a/app/src/organisms/Desktop/Devices/DeleteRecordsModal/__tests__/DeleteRecordsModal.test.tsx b/app/src/organisms/Desktop/Devices/DeleteRecordsModal/__tests__/DeleteRecordsModal.test.tsx new file mode 100644 index 000000000000..1ac646b5aba1 --- /dev/null +++ b/app/src/organisms/Desktop/Devices/DeleteRecordsModal/__tests__/DeleteRecordsModal.test.tsx @@ -0,0 +1,50 @@ +import { screen } from '@testing-library/react' +import { describe, it, vi } from 'vitest' + +import { renderWithProviders } from '/app/__testing-utils__' +import { i18n } from '/app/i18n' + +import { DeleteRecordsModal } from '..' + +const render = ( + type: React.ComponentProps['type'] +) => + renderWithProviders( + , + { i18nInstance: i18n } + ) + +describe('DeleteRecordsModal', () => { + it('renders allRuns copy', () => { + render('allRuns') + screen.getByText('Delete all protocol run records?') + screen.getByText( + 'Deleting all protocol run records will permanently remove them from the robot, along with all associated files. This action cannot be undone.' + ) + screen.getByText( + 'We recommend downloading all protocol files before proceeding.' + ) + }) + + it('renders selectedRuns copy', () => { + render('selectedRuns') + screen.getByText('Delete selected protocol run records?') + screen.getByText( + 'Deleting the selected protocol run records will permanently remove them from the robot, along with all associated files. This action cannot be undone.' + ) + screen.getByText( + 'We recommend downloading all protocol files before proceeding.' + ) + }) + + it('renders allLogs copy', () => { + render('allLogs') + screen.getByText('Delete all logs?') + screen.getByText( + 'Deleting all logs will permanently remove them from the robot. This action cannot be undone.' + ) + screen.getByText( + 'We recommend downloading all log files before proceeding.' + ) + }) +}) diff --git a/app/src/organisms/Desktop/Devices/DeleteRecordsModal/deleterecordsmodal.module.css b/app/src/organisms/Desktop/Devices/DeleteRecordsModal/deleterecordsmodal.module.css new file mode 100644 index 000000000000..373be325d02c --- /dev/null +++ b/app/src/organisms/Desktop/Devices/DeleteRecordsModal/deleterecordsmodal.module.css @@ -0,0 +1,19 @@ +.button_row { + display: flex; + height: 100%; + align-items: center; + justify-content: flex-end; + gap: var(--spacing-8); +} + +.description { + display: flex; + flex-direction: column; + gap: var(--spacing-8); +} + +.modal_content { + display: flex; + flex-direction: column; + gap: var(--spacing-24); +} diff --git a/app/src/organisms/Desktop/Devices/DeleteRecordsModal/index.tsx b/app/src/organisms/Desktop/Devices/DeleteRecordsModal/index.tsx new file mode 100644 index 000000000000..63b7b45b7827 --- /dev/null +++ b/app/src/organisms/Desktop/Devices/DeleteRecordsModal/index.tsx @@ -0,0 +1,80 @@ +import { useTranslation } from 'react-i18next' + +import { + AlertPrimaryButton, + Modal, + SecondaryButton, + StyledText, +} from '@opentrons/components' + +import styles from './deleterecordsmodal.module.css' + +import type { DeleteRecordsType } from './types' + +interface DeleteRecordsModalProps { + onClose: () => void + onConfirm: () => void + type: DeleteRecordsType +} + +export function DeleteRecordsModal( + props: DeleteRecordsModalProps +): JSX.Element { + const { onClose, onConfirm, type } = props + const { t } = useTranslation(['device_details', 'shared']) + const { title, description, recommendation } = ((): { + title: string + description: string + recommendation: string + } => { + switch (type) { + case 'allRuns': + return { + title: t('device_details:delete_all_run_records'), + description: t('device_details:delete_all_run_records_description'), + recommendation: t( + 'device_details:delete_all_run_records_recommendation' + ), + } + case 'selectedRuns': + return { + title: t('device_details:delete_selected_run_records'), + description: t( + 'device_details:delete_selected_run_records_description' + ), + recommendation: t( + 'device_details:delete_selected_run_records_recommendation' + ), + } + case 'allLogs': + return { + title: t('device_details:delete_all_logs'), + description: t('device_details:delete_all_logs_description'), + recommendation: t('device_details:delete_all_logs_recommendation'), + } + } + })() + + return ( + +
+
+ + {description} + + + {recommendation} + +
+
+ + {t('shared:cancel')} + + + {t('delete_all')} + +
+
+
+ ) +} diff --git a/app/src/organisms/Desktop/Devices/DeleteRecordsModal/types.ts b/app/src/organisms/Desktop/Devices/DeleteRecordsModal/types.ts new file mode 100644 index 000000000000..8898f3376eaa --- /dev/null +++ b/app/src/organisms/Desktop/Devices/DeleteRecordsModal/types.ts @@ -0,0 +1 @@ +export type DeleteRecordsType = 'allRuns' | 'selectedRuns' | 'allLogs' diff --git a/app/src/organisms/Desktop/Devices/HistoricalProtocolRun.module.css b/app/src/organisms/Desktop/Devices/HistoricalProtocolRun.module.css deleted file mode 100644 index ed04d468ce62..000000000000 --- a/app/src/organisms/Desktop/Devices/HistoricalProtocolRun.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.protocol_name { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } diff --git a/app/src/organisms/Desktop/Devices/RecentProtocolRuns.tsx b/app/src/organisms/Desktop/Devices/RecentProtocolRuns.tsx deleted file mode 100644 index c109587f3446..000000000000 --- a/app/src/organisms/Desktop/Devices/RecentProtocolRuns.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import { useTranslation } from 'react-i18next' - -import { - ALIGN_CENTER, - ALIGN_FLEX_START, - BasicButton, - BORDERS, - COLORS, - DIRECTION_COLUMN, - DISPLAY_FLEX, - Flex, - InfoScreen, - JUSTIFY_FLEX_END, - JUSTIFY_FLEX_START, - JUSTIFY_SPACE_BETWEEN, - LegacyStyledText, - SIZE_4, - SPACING, - StyledText, -} from '@opentrons/components' -import { useAllProtocolsQuery } from '@opentrons/react-api-client' - -import { useIsRobotViewable } from '/app/redux-resources/robots' -import { - useCurrentRunId, - useNotifyAllRunsQuery, - useRunStatuses, -} from '/app/resources/runs' - -import { RECENT_PROTOCOL_RUNS_HEADER } from './constants' -import { HistoricalProtocolRun } from './HistoricalProtocolRun' - -interface RecentProtocolRunsProps { - robotName: string -} - -export function RecentProtocolRuns({ - robotName, -}: RecentProtocolRunsProps): JSX.Element | null { - const { t } = useTranslation(['device_details', 'shared']) - const isRobotViewable = useIsRobotViewable(robotName) - const runsQueryResponse = useNotifyAllRunsQuery() - const runs = runsQueryResponse?.data?.data - const protocols = useAllProtocolsQuery() - const currentRunId = useCurrentRunId() - const { isRunTerminal } = useRunStatuses() - const robotIsBusy = currentRunId != null ? !isRunTerminal : false - const allRunsMutable = [...(runs ?? [])] - return ( - - - - {t('run_history')} - - - {}} - iconName="download" - > - {t('download_all')} - - {}}>{t('delete_all')} - - - - {isRobotViewable && allRunsMutable && allRunsMutable?.length > 0 && ( - <> - - - {t('run_date')} - - - {t('protocol')} - - - {t('status')} - - - {t('files')} - - - {t('run_duration')} - - - - {allRunsMutable - .sort( - (a, b) => - new Date(b.createdAt).getTime() - - new Date(a.createdAt).getTime() - ) - - .map((run, index) => { - const protocol = protocols?.data?.data.find( - protocol => protocol.id === run.protocolId - ) - const protocolName = - protocol?.metadata.protocolName ?? - protocol?.files[0].name ?? - t('shared:loading') ?? - '' - - return ( - - ) - })} - - - )} - {!isRobotViewable && ( - - )} - {isRobotViewable && allRunsMutable?.length === 0 && ( - - {t('no_protocol_runs')} - - )} - - - ) -} diff --git a/app/src/organisms/Desktop/Devices/HistoricalProtocolRun.tsx b/app/src/organisms/Desktop/Devices/RecentProtocolRuns/HistoricalProtocolRun.tsx similarity index 94% rename from app/src/organisms/Desktop/Devices/HistoricalProtocolRun.tsx rename to app/src/organisms/Desktop/Devices/RecentProtocolRuns/HistoricalProtocolRun.tsx index c947407f3421..650ba1a254c3 100644 --- a/app/src/organisms/Desktop/Devices/HistoricalProtocolRun.tsx +++ b/app/src/organisms/Desktop/Devices/RecentProtocolRuns/HistoricalProtocolRun.tsx @@ -15,12 +15,14 @@ import { EMPTY_TIMESTAMP } from '/app/resources/runs' import { formatInterval } from '/app/transformations/commands' import { formatTimestamp } from '/app/transformations/runs' -import { RECENT_PROTOCOL_RUNS_HEADER } from './constants' -import styles from './HistoricalProtocolRun.module.css' import { HistoricalProtocolRunOverflowMenu as OverflowMenu } from './HistoricalProtocolRunOverflowMenu' +import styles from './recentprotocolruns.module.css' import type { RunData } from '@opentrons/api-client' +// inclusive of overflow menu button +const RECENT_PROTOCOL_RUNS_COLUMNS = '30% 25% 16% 5% 14% 10%' + interface HistoricalProtocolRunProps { run: RunData protocolName: string @@ -72,7 +74,7 @@ export function HistoricalProtocolRun( diff --git a/app/src/organisms/Desktop/Devices/HistoricalProtocolRunDrawer.tsx b/app/src/organisms/Desktop/Devices/RecentProtocolRuns/HistoricalProtocolRunDrawer.tsx similarity index 98% rename from app/src/organisms/Desktop/Devices/HistoricalProtocolRunDrawer.tsx rename to app/src/organisms/Desktop/Devices/RecentProtocolRuns/HistoricalProtocolRunDrawer.tsx index 472bc40809ac..f0c0f2c25dc8 100644 --- a/app/src/organisms/Desktop/Devices/HistoricalProtocolRunDrawer.tsx +++ b/app/src/organisms/Desktop/Devices/RecentProtocolRuns/HistoricalProtocolRunDrawer.tsx @@ -48,9 +48,9 @@ import { useIsFlex, useRobotType } from '/app/redux-resources/robots' import { useRunGeneratedDataFiles } from '/app/resources/dataFiles/useRunGeneratedDataFiles' import { useMostRecentCompletedAnalysis } from '/app/resources/runs' -import { OffsetTag } from '../../LabwarePositionCheck' -import { DownloadCsvFileLink } from './DownloadCsvFileLink' -import { useDeckCalibrationData } from './hooks' +import { OffsetTag } from '../../../LabwarePositionCheck' +import { DownloadCsvFileLink } from '../DownloadCsvFileLink' +import { useDeckCalibrationData } from '../hooks' import type { LabwareOffset, RunData } from '@opentrons/api-client' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' diff --git a/app/src/organisms/Desktop/Devices/HistoricalProtocolRunOverflowMenu.tsx b/app/src/organisms/Desktop/Devices/RecentProtocolRuns/HistoricalProtocolRunOverflowMenu.tsx similarity index 99% rename from app/src/organisms/Desktop/Devices/HistoricalProtocolRunOverflowMenu.tsx rename to app/src/organisms/Desktop/Devices/RecentProtocolRuns/HistoricalProtocolRunOverflowMenu.tsx index 4e760f6ec725..78ebe5e99298 100644 --- a/app/src/organisms/Desktop/Devices/HistoricalProtocolRunOverflowMenu.tsx +++ b/app/src/organisms/Desktop/Devices/RecentProtocolRuns/HistoricalProtocolRunOverflowMenu.tsx @@ -55,7 +55,7 @@ import { import { useIsRobotOnWrongVersionOfSoftware } from '/app/redux/robot-update' import { useIsEstopNotDisengaged } from '/app/resources/devices' -import { useDownloadRunLog } from './hooks' +import { useDownloadRunLog } from '../hooks' import type { MouseEventHandler } from 'react' import type { Run } from '@opentrons/api-client' diff --git a/app/src/organisms/Desktop/Devices/__tests__/HistoricalProtocolRun.test.tsx b/app/src/organisms/Desktop/Devices/RecentProtocolRuns/__tests__/HistoricalProtocolRun.test.tsx similarity index 100% rename from app/src/organisms/Desktop/Devices/__tests__/HistoricalProtocolRun.test.tsx rename to app/src/organisms/Desktop/Devices/RecentProtocolRuns/__tests__/HistoricalProtocolRun.test.tsx diff --git a/app/src/organisms/Desktop/Devices/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx b/app/src/organisms/Desktop/Devices/RecentProtocolRuns/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx similarity index 97% rename from app/src/organisms/Desktop/Devices/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx rename to app/src/organisms/Desktop/Devices/RecentProtocolRuns/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx index 866c9c84ba64..5f3afec730f6 100644 --- a/app/src/organisms/Desktop/Devices/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx +++ b/app/src/organisms/Desktop/Devices/RecentProtocolRuns/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx @@ -26,9 +26,9 @@ import { useIsRobotOnWrongVersionOfSoftware } from '/app/redux/robot-update' import { useIsEstopNotDisengaged } from '/app/resources/devices' import { useNotifyAllCommandsQuery } from '/app/resources/runs' +import { useDownloadRunLog } from '../../hooks' +import runRecord from '../../ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__fixtures__/runRecord.json' import { HistoricalProtocolRunOverflowMenu } from '../HistoricalProtocolRunOverflowMenu' -import { useDownloadRunLog } from '../hooks' -import runRecord from '../ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/__fixtures__/runRecord.json' import type { ComponentProps } from 'react' import type { UseQueryResult } from 'react-query' @@ -37,7 +37,7 @@ import type { CommandsData } from '@opentrons/api-client' vi.mock('/app/redux/analytics') vi.mock('/app/redux/robot-update/selectors') vi.mock('/app/redux-resources/robots') -vi.mock('../hooks') +vi.mock('../../hooks') vi.mock('/app/organisms/RunTimeControl') vi.mock('/app/redux/analytics') vi.mock('/app/redux/config') diff --git a/app/src/organisms/Desktop/Devices/__tests__/RecentProtocolRuns.test.tsx b/app/src/organisms/Desktop/Devices/RecentProtocolRuns/__tests__/RecentProtocolRuns.test.tsx similarity index 98% rename from app/src/organisms/Desktop/Devices/__tests__/RecentProtocolRuns.test.tsx rename to app/src/organisms/Desktop/Devices/RecentProtocolRuns/__tests__/RecentProtocolRuns.test.tsx index 1e828514f654..f42b8d123238 100644 --- a/app/src/organisms/Desktop/Devices/__tests__/RecentProtocolRuns.test.tsx +++ b/app/src/organisms/Desktop/Devices/RecentProtocolRuns/__tests__/RecentProtocolRuns.test.tsx @@ -10,8 +10,8 @@ import { i18n } from '/app/i18n' import { useIsRobotViewable } from '/app/redux-resources/robots' import { useNotifyAllRunsQuery, useRunStatuses } from '/app/resources/runs' +import { RecentProtocolRuns } from '../../RecentProtocolRuns' import { HistoricalProtocolRun } from '../HistoricalProtocolRun' -import { RecentProtocolRuns } from '../RecentProtocolRuns' import type { AxiosError } from 'axios' import type { UseQueryResult } from 'react-query' diff --git a/app/src/organisms/Desktop/Devices/RecentProtocolRuns/index.tsx b/app/src/organisms/Desktop/Devices/RecentProtocolRuns/index.tsx new file mode 100644 index 000000000000..b49ed6cb739a --- /dev/null +++ b/app/src/organisms/Desktop/Devices/RecentProtocolRuns/index.tsx @@ -0,0 +1,156 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' + +import { BasicButton, InfoScreen, StyledText } from '@opentrons/components' +import { useAllProtocolsQuery } from '@opentrons/react-api-client' + +import { useIsRobotViewable } from '/app/redux-resources/robots' +import { + useCurrentRunId, + useNotifyAllRunsQuery, + useRunStatuses, +} from '/app/resources/runs' + +import { DeleteRecordsModal } from '../DeleteRecordsModal' +import { HistoricalProtocolRun } from './HistoricalProtocolRun' +import styles from './recentprotocolruns.module.css' + +interface RecentProtocolRunsProps { + robotName: string +} + +export function RecentProtocolRuns({ + robotName, +}: RecentProtocolRunsProps): JSX.Element | null { + const { t } = useTranslation(['device_details', 'shared']) + const isRobotViewable = useIsRobotViewable(robotName) + const runsQueryResponse = useNotifyAllRunsQuery() + const runs = runsQueryResponse?.data?.data + const protocols = useAllProtocolsQuery() + const currentRunId = useCurrentRunId() + const { isRunTerminal } = useRunStatuses() + const [showDeleteRecordsModal, setShowDeleteRecordsModal] = + useState(false) + + // TODO: wire up delete runs handler + const handleConfirmDeleteRuns = (): void => { + setShowDeleteRecordsModal(false) + } + + const robotIsBusy = currentRunId != null ? !isRunTerminal : false + + // TODO (nd, 06/25/2026): audit this once full run delete endpoint is created + const allRunsMutable = [...(runs ?? [])] + + return ( + <> + {showDeleteRecordsModal ? ( + { + setShowDeleteRecordsModal(false) + }} + onConfirm={handleConfirmDeleteRuns} + /> + ) : null} +
+
+ + {t('run_history')} + +
+ { + setShowDeleteRecordsModal(true) + }} + iconName="download" + > + {t('download_all')} + + { + setShowDeleteRecordsModal(true) + }} + > + {t('delete_all')} + +
+
+
+ {isRobotViewable && allRunsMutable && allRunsMutable?.length > 0 && ( + <> +
+ + {t('run_date')} + + + {t('protocol')} + + + {t('status')} + + + {t('files')} + + + {t('run_duration')} + +
+
+ {allRunsMutable + .sort( + (a, b) => + new Date(b.createdAt).getTime() - + new Date(a.createdAt).getTime() + ) + .map((run, index) => { + const protocol = protocols?.data?.data.find( + protocol => protocol.id === run.protocolId + ) + const protocolName = + protocol?.metadata.protocolName ?? + protocol?.files[0].name ?? + t('shared:loading') ?? + '' + + return ( + + ) + })} +
+ + )} + {!isRobotViewable && ( + + )} + {isRobotViewable && allRunsMutable?.length === 0 && ( + + )} +
+
+ + ) +} diff --git a/app/src/organisms/Desktop/Devices/RecentProtocolRuns/recentprotocolruns.module.css b/app/src/organisms/Desktop/Devices/RecentProtocolRuns/recentprotocolruns.module.css new file mode 100644 index 000000000000..99c844e38e82 --- /dev/null +++ b/app/src/organisms/Desktop/Devices/RecentProtocolRuns/recentprotocolruns.module.css @@ -0,0 +1,59 @@ +.container { + display: flex; + width: 100%; + flex-direction: column; + align-items: flex-start; + padding: 0 0 6rem; + border-radius: var(--border-radius-8); + background-color: var(--white); +} + +.header { + display: flex; + width: 100%; + align-items: center; + justify-content: space-between; + padding: 1rem; + border-bottom: 1px solid var(--grey-30); + gap: var(--spacing-8); +} + +.header_actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--spacing-8); +} + +.content { + display: flex; + width: 100%; + min-height: 8rem; + flex-direction: column; + align-items: center; + padding: 1rem; +} + +.column_headers { + display: grid; + width: 88%; + justify-content: flex-start; + padding: var(--spacing-8); + margin-right: 12%; + color: var(--grey-60); + gap: var(--spacing-20); + grid-template-columns: 30% 25% 16% 5% 24%; +} + +.protocol_name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.runs_list { + display: flex; + width: 100%; + flex-direction: column; + gap: var(--spacing-4); +} diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/RobotSettingsFileManager/__tests__/RobotSettingsFileManagers.test.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/RobotSettingsFileManager/__tests__/RobotSettingsFileManagers.test.tsx deleted file mode 100644 index fd63c1ee4d52..000000000000 --- a/app/src/organisms/Desktop/Devices/RobotSettings/RobotSettingsFileManager/__tests__/RobotSettingsFileManagers.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { screen } from '@testing-library/react' -import { beforeEach, describe, it, vi } from 'vitest' - -import { renderWithProviders } from '/app/__testing-utils__' - -import { RobotSettingsFileManager } from '../index' - -vi.mock('../RobotStorage', () => ({ - RobotStorage: () =>
mock robot storage
, -})) - -const render = () => renderWithProviders() - -describe('RobotSettingsFileManager', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('renders robot storage', () => { - render() - screen.getByText('mock robot storage') - }) -}) diff --git a/app/src/organisms/Desktop/Devices/constants.ts b/app/src/organisms/Desktop/Devices/constants.ts index df2c25e3a360..0ffc96e3b302 100644 --- a/app/src/organisms/Desktop/Devices/constants.ts +++ b/app/src/organisms/Desktop/Devices/constants.ts @@ -29,8 +29,3 @@ export function getDefaultTiprackDefForPipetteName( } return null } - -export const RECENT_PROTOCOL_RUNS_HEADER = '30% 25% 16% 5% 24%' - -// inclusive of overflow menu button -export const RECENT_PROTOCOL_RUNS_COLUMNS = '30% 25% 16% 5% 14% 10%'