mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	Machine integration (#4824)
* Added initial draft for machines * refactor: isPluginRegistryLoaded check into own ready function * Added suggestions from codereview * Refactor: base_drivers -> machine_types * Use new BaseInvenTreeSetting unique interface * Fix Django not ready error * Added get_machines function to driver - get_machines function on driver - get_machine function on driver - initialized attribute on machine * Added error handeling for driver and machine type * Extended get_machines functionality * Export everything from plugin module * Fix spelling mistakes * Better states handeling, BaseMachineType is now used instead of Machine Model * Use uuid as pk * WIP: machine termination hook * Remove termination hook as this does not work with gunicorn * Remove machine from registry after delete * Added ClassProviderMixin * Check for slug dupplication * Added config_type to MachineSettings to define machine/driver settings * Refactor helper mixins into own file in InvenTree app * Fixed typing and added required_attributes for BaseDriver * fix: generic status import * Added first draft for machine states * Added convention for status codes * Added update_machine hook * Removed unnecessary _key suffix from machine config model * Initil draft for machine API * Refactored BaseInvenTreeSetting all_items and allValues method * Added required to InvenTreeBaseSetting and check_settings method * check if all required machine settings are defined and refactor: use getattr * Fix: comment * Fix initialize error and python 3.9 compability * Make machine states available through the global states api * Added basic PUI machine admin implementation that is still in dev * Added basic machine setting UI to PUI * Added machine detail view to PUI admin center * Fix merge issues * Fix style issues * Added machine type,machine driver,error stack tables * Fix style in machine/serializers.py * Added pui link from machine to machine type/driver drawer * Removed only partially working django admin in favor of the PUI admin center implementation * Added required field to settings item * Added machine restart function * Added restart requird badge to machine table/drawer * Added driver init function * handle error functions for machines and registry * Added driver errors * Added machine table to driver drawer * Added back button to detail drawer component * Fix auto formatable pre-commit * fix: style * Fix deepsource * Removed slug field from table, added more links between drawers, remove detail drawer blur * Added initial docs * Removed description from driver/machine type select and fixed disabled driver select if no machine type is selected * Added basic label printing implementation * Remove translated column names because they are now retrieved from the api * Added printer location setting * Save last 10 used printer machine per user and sort them in the printing dialog * Added BasePrintingOptionsSerializer for common options * Fix not printing_options are not properly casted to its internal value * Fix type * Improved machine docs * Fix docs * Added UNKNOWN status code to label printer status * Skip machine loading when running migrations * Fix testing? * Fix: tests? * Fix: tests? * Disable docs check precommit * Disable docs check precommit * First draft for tests * fix test * Add type ignore * Added API tests * Test ci? * Add more tests * Added more tests * Bump api version * Changed driver/base driver naming schema * Added more tests * Fix tests * Added setting choice with kwargs and get_machines with initialized=None * Refetch table after deleting machine * Fix test --------- Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
		| @@ -61,6 +61,8 @@ export function ChoiceField({ | ||||
|       label={definition.label} | ||||
|       description={definition.description} | ||||
|       placeholder={definition.placeholder} | ||||
|       required={definition.required} | ||||
|       disabled={definition.disabled} | ||||
|       icon={definition.icon} | ||||
|       withinPortal={true} | ||||
|     /> | ||||
|   | ||||
| @@ -1,5 +1,11 @@ | ||||
| import { t } from '@lingui/macro'; | ||||
| import { ActionIcon, Menu, Tooltip } from '@mantine/core'; | ||||
| import { | ||||
|   ActionIcon, | ||||
|   Indicator, | ||||
|   IndicatorProps, | ||||
|   Menu, | ||||
|   Tooltip | ||||
| } from '@mantine/core'; | ||||
| import { | ||||
|   IconCopy, | ||||
|   IconEdit, | ||||
| @@ -18,6 +24,7 @@ export type ActionDropdownItem = { | ||||
|   tooltip?: string; | ||||
|   disabled?: boolean; | ||||
|   onClick?: () => void; | ||||
|   indicator?: Omit<IndicatorProps, 'children'>; | ||||
| }; | ||||
|  | ||||
| /** | ||||
| @@ -37,34 +44,45 @@ export function ActionDropdown({ | ||||
|   const hasActions = useMemo(() => { | ||||
|     return actions.some((action) => !action.disabled); | ||||
|   }, [actions]); | ||||
|   const indicatorProps = useMemo(() => { | ||||
|     return actions.find((action) => action.indicator); | ||||
|   }, [actions]); | ||||
|  | ||||
|   return hasActions ? ( | ||||
|     <Menu position="bottom-end"> | ||||
|       <Menu.Target> | ||||
|         <Tooltip label={tooltip} hidden={!tooltip}> | ||||
|           <ActionIcon size="lg" radius="sm" variant="outline"> | ||||
|             {icon} | ||||
|           </ActionIcon> | ||||
|         </Tooltip> | ||||
|       </Menu.Target> | ||||
|       <Indicator disabled={!indicatorProps} {...indicatorProps?.indicator}> | ||||
|         <Menu.Target> | ||||
|           <Tooltip label={tooltip} hidden={!tooltip}> | ||||
|             <ActionIcon size="lg" radius="sm" variant="outline"> | ||||
|               {icon} | ||||
|             </ActionIcon> | ||||
|           </Tooltip> | ||||
|         </Menu.Target> | ||||
|       </Indicator> | ||||
|       <Menu.Dropdown> | ||||
|         {actions.map((action) => | ||||
|           action.disabled ? null : ( | ||||
|             <Tooltip label={action.tooltip} key={action.name}> | ||||
|               <Menu.Item | ||||
|                 icon={action.icon} | ||||
|                 onClick={() => { | ||||
|                   if (action.onClick != undefined) { | ||||
|                     action.onClick(); | ||||
|                   } else { | ||||
|                     notYetImplemented(); | ||||
|                   } | ||||
|                 }} | ||||
|                 disabled={action.disabled} | ||||
|               > | ||||
|                 {action.name} | ||||
|               </Menu.Item> | ||||
|             </Tooltip> | ||||
|             <Indicator | ||||
|               disabled={!action.indicator} | ||||
|               {...action.indicator} | ||||
|               key={action.name} | ||||
|             > | ||||
|               <Tooltip label={action.tooltip}> | ||||
|                 <Menu.Item | ||||
|                   icon={action.icon} | ||||
|                   onClick={() => { | ||||
|                     if (action.onClick != undefined) { | ||||
|                       action.onClick(); | ||||
|                     } else { | ||||
|                       notYetImplemented(); | ||||
|                     } | ||||
|                   }} | ||||
|                   disabled={action.disabled} | ||||
|                 > | ||||
|                   {action.name} | ||||
|                 </Menu.Item> | ||||
|               </Tooltip> | ||||
|             </Indicator> | ||||
|           ) | ||||
|         )} | ||||
|       </Menu.Dropdown> | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Trans } from '@lingui/macro'; | ||||
| import { Flex, Group, Text } from '@mantine/core'; | ||||
| import { Code, Flex, Group, Text } from '@mantine/core'; | ||||
| import { Link, To } from 'react-router-dom'; | ||||
|  | ||||
| import { YesNoButton } from './YesNoButton'; | ||||
|  | ||||
| @@ -7,13 +8,37 @@ export function InfoItem({ | ||||
|   name, | ||||
|   children, | ||||
|   type, | ||||
|   value | ||||
|   value, | ||||
|   link | ||||
| }: { | ||||
|   name: string; | ||||
|   children?: React.ReactNode; | ||||
|   type?: 'text' | 'boolean'; | ||||
|   type?: 'text' | 'boolean' | 'code'; | ||||
|   value?: any; | ||||
|   link?: To; | ||||
| }) { | ||||
|   function renderComponent() { | ||||
|     if (value === undefined) return null; | ||||
|  | ||||
|     if (type === 'text') { | ||||
|       return <Text>{value || <Trans>None</Trans>}</Text>; | ||||
|     } | ||||
|  | ||||
|     if (type === 'boolean') { | ||||
|       return <YesNoButton value={value || false} />; | ||||
|     } | ||||
|  | ||||
|     if (type === 'code') { | ||||
|       return ( | ||||
|         <Code style={{ wordWrap: 'break-word', maxWidth: '400px' }}> | ||||
|           {value} | ||||
|         </Code> | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Group position="apart"> | ||||
|       <Text fz="sm" fw={700}> | ||||
| @@ -21,13 +46,7 @@ export function InfoItem({ | ||||
|       </Text> | ||||
|       <Flex> | ||||
|         {children} | ||||
|         {value !== undefined && type === 'text' ? ( | ||||
|           <Text>{value || <Trans>None</Trans>}</Text> | ||||
|         ) : type === 'boolean' ? ( | ||||
|           <YesNoButton value={value || false} /> | ||||
|         ) : ( | ||||
|           '' | ||||
|         )} | ||||
|         {link ? <Link to={link}>{renderComponent()}</Link> : renderComponent()} | ||||
|       </Flex> | ||||
|     </Group> | ||||
|   ); | ||||
|   | ||||
| @@ -0,0 +1,5 @@ | ||||
| import { IconAlertCircle } from '@tabler/icons-react'; | ||||
|  | ||||
| export function UnavailableIndicator() { | ||||
|   return <IconAlertCircle size={18} color="red" />; | ||||
| } | ||||
| @@ -1,4 +1,13 @@ | ||||
| import { Divider, Drawer, MantineNumberSize, Stack, Text } from '@mantine/core'; | ||||
| import { | ||||
|   ActionIcon, | ||||
|   Divider, | ||||
|   Drawer, | ||||
|   Group, | ||||
|   MantineNumberSize, | ||||
|   Stack, | ||||
|   Text | ||||
| } from '@mantine/core'; | ||||
| import { IconChevronLeft } from '@tabler/icons-react'; | ||||
| import { useMemo } from 'react'; | ||||
| import { Route, Routes, useNavigate, useParams } from 'react-router-dom'; | ||||
|  | ||||
| @@ -35,11 +44,15 @@ function DetailDrawerComponent({ | ||||
|       position={position} | ||||
|       size={size} | ||||
|       title={ | ||||
|         <Text size="xl" fw={600} variant="gradient"> | ||||
|           {title} | ||||
|         </Text> | ||||
|         <Group> | ||||
|           <ActionIcon variant="outline" onClick={() => navigate(-1)}> | ||||
|             <IconChevronLeft /> | ||||
|           </ActionIcon> | ||||
|           <Text size="xl" fw={600} variant="gradient"> | ||||
|             {title} | ||||
|           </Text> | ||||
|         </Group> | ||||
|       } | ||||
|       overlayProps={{ opacity: 0.5, blur: 4 }} | ||||
|     > | ||||
|       <Stack spacing={'xs'}> | ||||
|         <Divider /> | ||||
|   | ||||
| @@ -26,10 +26,12 @@ import { ApiFormFieldType } from '../forms/fields/ApiFormField'; | ||||
|  */ | ||||
| function SettingValue({ | ||||
|   settingsState, | ||||
|   setting | ||||
|   setting, | ||||
|   onChange | ||||
| }: { | ||||
|   settingsState: SettingsStateProps; | ||||
|   setting: Setting; | ||||
|   onChange?: () => void; | ||||
| }) { | ||||
|   // Callback function when a boolean value is changed | ||||
|   function onToggle(value: boolean) { | ||||
| @@ -45,6 +47,7 @@ function SettingValue({ | ||||
|           color: 'green' | ||||
|         }); | ||||
|         settingsState.fetchSettings(); | ||||
|         onChange?.(); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         showNotification({ | ||||
| @@ -97,6 +100,7 @@ function SettingValue({ | ||||
|           color: 'green' | ||||
|         }); | ||||
|         settingsState.fetchSettings(); | ||||
|         onChange?.(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| @@ -153,11 +157,13 @@ function SettingValue({ | ||||
| export function SettingItem({ | ||||
|   settingsState, | ||||
|   setting, | ||||
|   shaded | ||||
|   shaded, | ||||
|   onChange | ||||
| }: { | ||||
|   settingsState: SettingsStateProps; | ||||
|   setting: Setting; | ||||
|   shaded: boolean; | ||||
|   onChange?: () => void; | ||||
| }) { | ||||
|   const theme = useMantineTheme(); | ||||
|  | ||||
| @@ -173,10 +179,17 @@ export function SettingItem({ | ||||
|     <Paper style={style}> | ||||
|       <Group position="apart" p="3"> | ||||
|         <Stack spacing="2" p="4px"> | ||||
|           <Text>{setting.name}</Text> | ||||
|           <Text> | ||||
|             {setting.name} | ||||
|             {setting.required ? ' *' : ''} | ||||
|           </Text> | ||||
|           <Text size="xs">{setting.description}</Text> | ||||
|         </Stack> | ||||
|         <SettingValue settingsState={settingsState} setting={setting} /> | ||||
|         <SettingValue | ||||
|           settingsState={settingsState} | ||||
|           setting={setting} | ||||
|           onChange={onChange} | ||||
|         /> | ||||
|       </Group> | ||||
|     </Paper> | ||||
|   ); | ||||
|   | ||||
| @@ -1,9 +1,11 @@ | ||||
| import { Trans } from '@lingui/macro'; | ||||
| import { Stack, Text } from '@mantine/core'; | ||||
| import React, { useEffect, useMemo, useRef } from 'react'; | ||||
| import { useStore } from 'zustand'; | ||||
|  | ||||
| import { | ||||
|   SettingsStateProps, | ||||
|   createMachineSettingsState, | ||||
|   createPluginSettingsState, | ||||
|   useGlobalSettingsState, | ||||
|   useUserSettingsState | ||||
| @@ -15,10 +17,12 @@ import { SettingItem } from './SettingItem'; | ||||
|  */ | ||||
| export function SettingList({ | ||||
|   settingsState, | ||||
|   keys | ||||
|   keys, | ||||
|   onChange | ||||
| }: { | ||||
|   settingsState: SettingsStateProps; | ||||
|   keys?: string[]; | ||||
|   onChange?: () => void; | ||||
| }) { | ||||
|   useEffect(() => { | ||||
|     settingsState.fetchSettings(); | ||||
| @@ -44,6 +48,7 @@ export function SettingList({ | ||||
|                   settingsState={settingsState} | ||||
|                   setting={setting} | ||||
|                   shaded={i % 2 === 0} | ||||
|                   onChange={onChange} | ||||
|                 /> | ||||
|               ) : ( | ||||
|                 <Text size="sm" italic color="red"> | ||||
| @@ -53,6 +58,11 @@ export function SettingList({ | ||||
|             </React.Fragment> | ||||
|           ); | ||||
|         })} | ||||
|         {(keys || allKeys).length === 0 && ( | ||||
|           <Text italic> | ||||
|             <Trans>No settings specified</Trans> | ||||
|           </Text> | ||||
|         )} | ||||
|       </Stack> | ||||
|     </> | ||||
|   ); | ||||
| @@ -78,3 +88,23 @@ export function PluginSettingList({ pluginPk }: { pluginPk: string }) { | ||||
|  | ||||
|   return <SettingList settingsState={pluginSettings} />; | ||||
| } | ||||
|  | ||||
| export function MachineSettingList({ | ||||
|   machinePk, | ||||
|   configType, | ||||
|   onChange | ||||
| }: { | ||||
|   machinePk: string; | ||||
|   configType: 'M' | 'D'; | ||||
|   onChange?: () => void; | ||||
| }) { | ||||
|   const machineSettingsStore = useRef( | ||||
|     createMachineSettingsState({ | ||||
|       machine: machinePk, | ||||
|       configType: configType | ||||
|     }) | ||||
|   ).current; | ||||
|   const machineSettings = useStore(machineSettingsStore); | ||||
|  | ||||
|   return <SettingList settingsState={machineSettings} onChange={onChange} />; | ||||
| } | ||||
|   | ||||
| @@ -100,6 +100,15 @@ export enum ApiEndpoints { | ||||
|   plugin_activate = 'plugins/:id/activate/', | ||||
|   plugin_uninstall = 'plugins/:id/uninstall/', | ||||
|  | ||||
|   // Machine API endpoints | ||||
|   machine_types_list = 'machine/types/', | ||||
|   machine_driver_list = 'machine/drivers/', | ||||
|   machine_registry_status = 'machine/status/', | ||||
|   machine_list = 'machine/', | ||||
|   machine_restart = 'machine/:machine/restart/', | ||||
|   machine_setting_list = 'machine/:machine/settings/', | ||||
|   machine_setting_detail = 'machine/:machine/settings/:config_type/', | ||||
|  | ||||
|   // Miscellaneous API endpoints | ||||
|   error_report_list = 'error-report/', | ||||
|   project_code_list = 'project-code/', | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { Trans, t } from '@lingui/macro'; | ||||
| import { Divider, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core'; | ||||
| import { | ||||
|   IconCpu, | ||||
|   IconDevicesPc, | ||||
|   IconExclamationCircle, | ||||
|   IconList, | ||||
|   IconListDetails, | ||||
| @@ -29,6 +30,10 @@ const PluginManagementPanel = Loadable( | ||||
|   lazy(() => import('./PluginManagementPanel')) | ||||
| ); | ||||
|  | ||||
| const MachineManagementPanel = Loadable( | ||||
|   lazy(() => import('./MachineManagementPanel')) | ||||
| ); | ||||
|  | ||||
| const ErrorReportTable = Loadable( | ||||
|   lazy(() => import('../../../../tables/settings/ErrorTable')) | ||||
| ); | ||||
| @@ -95,6 +100,12 @@ export default function AdminCenter() { | ||||
|         label: t`Plugins`, | ||||
|         icon: <IconPlugConnected />, | ||||
|         content: <PluginManagementPanel /> | ||||
|       }, | ||||
|       { | ||||
|         name: 'machine', | ||||
|         label: t`Machines`, | ||||
|         icon: <IconDevicesPc />, | ||||
|         content: <MachineManagementPanel /> | ||||
|       } | ||||
|     ]; | ||||
|   }, []); | ||||
|   | ||||
| @@ -0,0 +1,76 @@ | ||||
| import { Trans } from '@lingui/macro'; | ||||
| import { | ||||
|   ActionIcon, | ||||
|   Code, | ||||
|   Group, | ||||
|   List, | ||||
|   Space, | ||||
|   Stack, | ||||
|   Text, | ||||
|   Title | ||||
| } from '@mantine/core'; | ||||
| import { IconRefresh } from '@tabler/icons-react'; | ||||
| import { useQuery } from '@tanstack/react-query'; | ||||
|  | ||||
| import { api } from '../../../../App'; | ||||
| import { ApiEndpoints } from '../../../../enums/ApiEndpoints'; | ||||
| import { apiUrl } from '../../../../states/ApiState'; | ||||
| import { MachineListTable } from '../../../../tables/machine/MachineListTable'; | ||||
| import { MachineTypeListTable } from '../../../../tables/machine/MachineTypeTable'; | ||||
|  | ||||
| interface MachineRegistryStatusI { | ||||
|   registry_errors: { message: string }[]; | ||||
| } | ||||
|  | ||||
| export default function MachineManagementPanel() { | ||||
|   const { data: registryStatus, refetch } = useQuery<MachineRegistryStatusI>({ | ||||
|     queryKey: ['machine-registry-status'], | ||||
|     queryFn: () => | ||||
|       api | ||||
|         .get(apiUrl(ApiEndpoints.machine_registry_status)) | ||||
|         .then((res) => res.data), | ||||
|     staleTime: 10 * 1000 | ||||
|   }); | ||||
|  | ||||
|   return ( | ||||
|     <Stack> | ||||
|       <MachineListTable props={{}} /> | ||||
|  | ||||
|       <Space h="10px" /> | ||||
|  | ||||
|       <Stack spacing={'xs'}> | ||||
|         <Title order={5}> | ||||
|           <Trans>Machine types</Trans> | ||||
|         </Title> | ||||
|         <MachineTypeListTable props={{}} /> | ||||
|       </Stack> | ||||
|  | ||||
|       <Space h="10px" /> | ||||
|  | ||||
|       <Stack spacing={'xs'}> | ||||
|         <Group> | ||||
|           <Title order={5}> | ||||
|             <Trans>Machine Error Stack</Trans> | ||||
|           </Title> | ||||
|           <ActionIcon variant="outline" onClick={() => refetch()}> | ||||
|             <IconRefresh /> | ||||
|           </ActionIcon> | ||||
|         </Group> | ||||
|         {registryStatus?.registry_errors && | ||||
|         registryStatus.registry_errors.length === 0 ? ( | ||||
|           <Text italic> | ||||
|             <Trans>There are no machine registry errors.</Trans> | ||||
|           </Text> | ||||
|         ) : ( | ||||
|           <List> | ||||
|             {registryStatus?.registry_errors?.map((error, i) => ( | ||||
|               <List.Item key={i}> | ||||
|                 <Code>{error.message}</Code> | ||||
|               </List.Item> | ||||
|             ))} | ||||
|           </List> | ||||
|         )} | ||||
|       </Stack> | ||||
|     </Stack> | ||||
|   ); | ||||
| } | ||||
| @@ -132,6 +132,54 @@ export const createPluginSettingsState = ({ | ||||
|   })); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * State management for machine settings | ||||
|  */ | ||||
| interface CreateMachineSettingStateProps { | ||||
|   machine: string; | ||||
|   configType: 'M' | 'D'; | ||||
| } | ||||
|  | ||||
| export const createMachineSettingsState = ({ | ||||
|   machine, | ||||
|   configType | ||||
| }: CreateMachineSettingStateProps) => { | ||||
|   const pathParams: PathParams = { machine, config_type: configType }; | ||||
|  | ||||
|   return createStore<SettingsStateProps>()((set, get) => ({ | ||||
|     settings: [], | ||||
|     lookup: {}, | ||||
|     endpoint: ApiEndpoints.machine_setting_detail, | ||||
|     pathParams, | ||||
|     fetchSettings: async () => { | ||||
|       await api | ||||
|         .get(apiUrl(ApiEndpoints.machine_setting_list, undefined, { machine })) | ||||
|         .then((response) => { | ||||
|           const settings = response.data.filter( | ||||
|             (s: any) => s.config_type === configType | ||||
|           ); | ||||
|           set({ | ||||
|             settings, | ||||
|             lookup: generate_lookup(settings) | ||||
|           }); | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error( | ||||
|             `Error fetching machine settings for machine ${machine} with type ${configType}:`, | ||||
|             error | ||||
|           ); | ||||
|         }); | ||||
|     }, | ||||
|     getSetting: (key: string, default_value?: string) => { | ||||
|       return get().lookup[key] ?? default_value ?? ''; | ||||
|     }, | ||||
|     isSet: (key: string, default_value?: boolean) => { | ||||
|       let value = get().lookup[key] ?? default_value ?? 'false'; | ||||
|       return isTrue(value); | ||||
|     } | ||||
|   })); | ||||
| }; | ||||
|  | ||||
| /* | ||||
|   return a lookup dictionary for the value of the provided Setting list | ||||
| */ | ||||
|   | ||||
| @@ -79,6 +79,7 @@ export interface Setting { | ||||
|   typ: SettingTyp; | ||||
|   plugin?: string; | ||||
|   method?: string; | ||||
|   required?: boolean; | ||||
| } | ||||
|  | ||||
| export interface SettingChoice { | ||||
|   | ||||
							
								
								
									
										604
									
								
								src/frontend/src/tables/machine/MachineListTable.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										604
									
								
								src/frontend/src/tables/machine/MachineListTable.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,604 @@ | ||||
| import { Trans, t } from '@lingui/macro'; | ||||
| import { | ||||
|   ActionIcon, | ||||
|   Badge, | ||||
|   Box, | ||||
|   Card, | ||||
|   Code, | ||||
|   Flex, | ||||
|   Group, | ||||
|   Indicator, | ||||
|   List, | ||||
|   LoadingOverlay, | ||||
|   Space, | ||||
|   Stack, | ||||
|   Text, | ||||
|   Title | ||||
| } from '@mantine/core'; | ||||
| import { notifications } from '@mantine/notifications'; | ||||
| import { IconCheck, IconDots, IconRefresh } from '@tabler/icons-react'; | ||||
| import { useQuery } from '@tanstack/react-query'; | ||||
| import { useCallback, useMemo, useState } from 'react'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import { Link } from 'react-router-dom'; | ||||
|  | ||||
| import { api } from '../../App'; | ||||
| import { AddItemButton } from '../../components/buttons/AddItemButton'; | ||||
| import { | ||||
|   ActionDropdown, | ||||
|   DeleteItemAction, | ||||
|   EditItemAction | ||||
| } from '../../components/items/ActionDropdown'; | ||||
| import { InfoItem } from '../../components/items/InfoItem'; | ||||
| import { UnavailableIndicator } from '../../components/items/UnavailableIndicator'; | ||||
| import { YesNoButton } from '../../components/items/YesNoButton'; | ||||
| import { DetailDrawer } from '../../components/nav/DetailDrawer'; | ||||
| import { | ||||
|   StatusRenderer, | ||||
|   TableStatusRenderer | ||||
| } from '../../components/render/StatusRenderer'; | ||||
| import { MachineSettingList } from '../../components/settings/SettingList'; | ||||
| import { ApiEndpoints } from '../../enums/ApiEndpoints'; | ||||
| import { openDeleteApiForm, openEditApiForm } from '../../functions/forms'; | ||||
| import { useCreateApiFormModal } from '../../hooks/UseForm'; | ||||
| import { useTable } from '../../hooks/UseTable'; | ||||
| import { apiUrl } from '../../states/ApiState'; | ||||
| import { TableColumn } from '../Column'; | ||||
| import { BooleanColumn } from '../ColumnRenderers'; | ||||
| import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable'; | ||||
| import { MachineDriverI, MachineTypeI } from './MachineTypeTable'; | ||||
|  | ||||
| interface MachineI { | ||||
|   pk: string; | ||||
|   name: string; | ||||
|   machine_type: string; | ||||
|   driver: string; | ||||
|   initialized: boolean; | ||||
|   active: boolean; | ||||
|   status: number; | ||||
|   status_model: string; | ||||
|   status_text: string; | ||||
|   machine_errors: string[]; | ||||
|   is_driver_available: boolean; | ||||
|   restart_required: boolean; | ||||
| } | ||||
|  | ||||
| function MachineStatusIndicator({ machine }: { machine: MachineI }) { | ||||
|   const sx = { marginLeft: '4px' }; | ||||
|  | ||||
|   // machine is not active, show a gray dot | ||||
|   if (!machine.active) { | ||||
|     return ( | ||||
|       <Indicator sx={sx} color="gray"> | ||||
|         <Box></Box> | ||||
|       </Indicator> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   // determine the status color | ||||
|   let color = 'green'; | ||||
|   const hasErrors = | ||||
|     machine.machine_errors.length > 0 || !machine.is_driver_available; | ||||
|  | ||||
|   if (hasErrors || machine.status >= 300) color = 'red'; | ||||
|   else if (machine.status >= 200) color = 'orange'; | ||||
|  | ||||
|   // determine if the machine is running | ||||
|   const processing = | ||||
|     machine.initialized && machine.status > 0 && machine.status < 300; | ||||
|  | ||||
|   return ( | ||||
|     <Indicator processing={processing} sx={sx} color={color}> | ||||
|       <Box></Box> | ||||
|     </Indicator> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export function useMachineTypeDriver({ | ||||
|   includeTypes = true, | ||||
|   includeDrivers = true | ||||
| }: { includeTypes?: boolean; includeDrivers?: boolean } = {}) { | ||||
|   const { | ||||
|     data: machineTypes, | ||||
|     isFetching: isMachineTypesFetching, | ||||
|     refetch: refreshMachineTypes | ||||
|   } = useQuery<MachineTypeI[]>({ | ||||
|     enabled: includeTypes, | ||||
|     queryKey: ['machine-types'], | ||||
|     queryFn: () => | ||||
|       api.get(apiUrl(ApiEndpoints.machine_types_list)).then((res) => res.data), | ||||
|     staleTime: 10 * 1000 | ||||
|   }); | ||||
|   const { | ||||
|     data: machineDrivers, | ||||
|     isFetching: isMachineDriversFetching, | ||||
|     refetch: refreshDrivers | ||||
|   } = useQuery<MachineDriverI[]>({ | ||||
|     enabled: includeDrivers, | ||||
|     queryKey: ['machine-drivers'], | ||||
|     queryFn: () => | ||||
|       api.get(apiUrl(ApiEndpoints.machine_driver_list)).then((res) => res.data), | ||||
|     staleTime: 10 * 1000 | ||||
|   }); | ||||
|  | ||||
|   const refresh = useCallback(() => { | ||||
|     refreshMachineTypes(); | ||||
|     refreshDrivers(); | ||||
|   }, [refreshDrivers, refreshMachineTypes]); | ||||
|  | ||||
|   return { | ||||
|     machineTypes, | ||||
|     machineDrivers, | ||||
|     isFetching: isMachineTypesFetching || isMachineDriversFetching, | ||||
|     refresh | ||||
|   }; | ||||
| } | ||||
|  | ||||
| function MachineDrawer({ | ||||
|   machinePk, | ||||
|   refreshTable | ||||
| }: { | ||||
|   machinePk: string; | ||||
|   refreshTable: () => void; | ||||
| }) { | ||||
|   const navigate = useNavigate(); | ||||
|   const { | ||||
|     data: machine, | ||||
|     refetch, | ||||
|     isFetching: isMachineFetching | ||||
|   } = useQuery<MachineI>({ | ||||
|     enabled: true, | ||||
|     queryKey: ['machine-detail', machinePk], | ||||
|     queryFn: () => | ||||
|       api | ||||
|         .get(apiUrl(ApiEndpoints.machine_list, machinePk)) | ||||
|         .then((res) => res.data) | ||||
|   }); | ||||
|   const { | ||||
|     machineTypes, | ||||
|     machineDrivers, | ||||
|     isFetching: isMachineTypeDriverFetching | ||||
|   } = useMachineTypeDriver(); | ||||
|  | ||||
|   const isFetching = isMachineFetching || isMachineTypeDriverFetching; | ||||
|  | ||||
|   const machineType = useMemo( | ||||
|     () => | ||||
|       machineTypes && machine | ||||
|         ? machineTypes.find((t) => t.slug === machine.machine_type) | ||||
|         : undefined, | ||||
|     [machine?.machine_type, machineTypes] | ||||
|   ); | ||||
|  | ||||
|   const machineDriver = useMemo( | ||||
|     () => | ||||
|       machineDrivers && machine | ||||
|         ? machineDrivers.find((d) => d.slug === machine.driver) | ||||
|         : undefined, | ||||
|     [machine?.driver, machineDrivers] | ||||
|   ); | ||||
|  | ||||
|   const refreshAll = useCallback(() => { | ||||
|     refetch(); | ||||
|     refreshTable(); | ||||
|   }, [refetch, refreshTable]); | ||||
|  | ||||
|   const restartMachine = useCallback( | ||||
|     (machinePk: string) => { | ||||
|       api | ||||
|         .post( | ||||
|           apiUrl(ApiEndpoints.machine_restart, undefined, { | ||||
|             machine: machinePk | ||||
|           }) | ||||
|         ) | ||||
|         .then(() => { | ||||
|           refreshAll(); | ||||
|           notifications.show({ | ||||
|             message: t`Machine restarted`, | ||||
|             color: 'green', | ||||
|             icon: <IconCheck size="1rem" /> | ||||
|           }); | ||||
|         }); | ||||
|     }, | ||||
|     [refreshAll] | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <Stack spacing="xs"> | ||||
|       <Group position="apart"> | ||||
|         <Box></Box> | ||||
|  | ||||
|         <Group> | ||||
|           {machine && <MachineStatusIndicator machine={machine} />} | ||||
|           <Title order={4}>{machine?.name}</Title> | ||||
|         </Group> | ||||
|  | ||||
|         <Group> | ||||
|           {machine?.restart_required && ( | ||||
|             <Badge color="red"> | ||||
|               <Trans>Restart required</Trans> | ||||
|             </Badge> | ||||
|           )} | ||||
|           <ActionDropdown | ||||
|             tooltip={t`Machine Actions`} | ||||
|             icon={<IconDots />} | ||||
|             actions={[ | ||||
|               EditItemAction({ | ||||
|                 tooltip: t`Edit machine`, | ||||
|                 onClick: () => { | ||||
|                   openEditApiForm({ | ||||
|                     title: t`Edit machine`, | ||||
|                     url: ApiEndpoints.machine_list, | ||||
|                     pk: machinePk, | ||||
|                     fields: { | ||||
|                       name: {}, | ||||
|                       active: {} | ||||
|                     }, | ||||
|                     onClose: () => refreshAll() | ||||
|                   }); | ||||
|                 } | ||||
|               }), | ||||
|               DeleteItemAction({ | ||||
|                 tooltip: t`Delete machine`, | ||||
|                 onClick: () => { | ||||
|                   openDeleteApiForm({ | ||||
|                     title: t`Delete machine`, | ||||
|                     successMessage: t`Machine successfully deleted.`, | ||||
|                     url: ApiEndpoints.machine_list, | ||||
|                     pk: machinePk, | ||||
|                     preFormContent: ( | ||||
|                       <Text>{t`Are you sure you want to remove the machine "${machine?.name}"?`}</Text> | ||||
|                     ), | ||||
|                     onFormSuccess: () => { | ||||
|                       refreshTable(); | ||||
|                       navigate(-1); | ||||
|                     } | ||||
|                   }); | ||||
|                 } | ||||
|               }), | ||||
|               { | ||||
|                 icon: <IconRefresh />, | ||||
|                 name: t`Restart`, | ||||
|                 tooltip: | ||||
|                   t`Restart machine` + | ||||
|                   (machine?.restart_required | ||||
|                     ? ' (' + t`manual restart required` + ')' | ||||
|                     : ''), | ||||
|                 indicator: machine?.restart_required | ||||
|                   ? { color: 'red' } | ||||
|                   : undefined, | ||||
|                 onClick: () => machine && restartMachine(machine?.pk) | ||||
|               } | ||||
|             ]} | ||||
|           /> | ||||
|         </Group> | ||||
|       </Group> | ||||
|  | ||||
|       <Card withBorder> | ||||
|         <Stack spacing="md"> | ||||
|           <Group position="apart"> | ||||
|             <Title order={4}> | ||||
|               <Trans>Machine information</Trans> | ||||
|             </Title> | ||||
|             <ActionIcon variant="outline" onClick={() => refetch()}> | ||||
|               <IconRefresh /> | ||||
|             </ActionIcon> | ||||
|           </Group> | ||||
|           <Stack pos="relative" spacing="xs"> | ||||
|             <LoadingOverlay visible={isFetching} overlayOpacity={0} /> | ||||
|             <InfoItem name={t`Machine Type`}> | ||||
|               <Group spacing="xs"> | ||||
|                 {machineType ? ( | ||||
|                   <Link to={`../type-${machine?.machine_type}`}> | ||||
|                     <Text>{machineType.name}</Text> | ||||
|                   </Link> | ||||
|                 ) : ( | ||||
|                   <Text>{machine?.machine_type}</Text> | ||||
|                 )} | ||||
|                 {machine && !machineType && <UnavailableIndicator />} | ||||
|               </Group> | ||||
|             </InfoItem> | ||||
|             <InfoItem name={t`Machine Driver`}> | ||||
|               <Group spacing="xs"> | ||||
|                 {machineDriver ? ( | ||||
|                   <Link to={`../driver-${machine?.driver}`}> | ||||
|                     <Text>{machineDriver.name}</Text> | ||||
|                   </Link> | ||||
|                 ) : ( | ||||
|                   <Text>{machine?.driver}</Text> | ||||
|                 )} | ||||
|                 {!machine?.is_driver_available && <UnavailableIndicator />} | ||||
|               </Group> | ||||
|             </InfoItem> | ||||
|             <InfoItem name={t`Initialized`}> | ||||
|               <YesNoButton value={machine?.initialized || false} /> | ||||
|             </InfoItem> | ||||
|             <InfoItem name={t`Active`}> | ||||
|               <YesNoButton value={machine?.active || false} /> | ||||
|             </InfoItem> | ||||
|             <InfoItem name={t`Status`}> | ||||
|               <Flex direction="column"> | ||||
|                 {machine?.status === -1 ? ( | ||||
|                   <Text fz="xs">No status</Text> | ||||
|                 ) : ( | ||||
|                   StatusRenderer({ | ||||
|                     status: `${machine?.status || -1}`, | ||||
|                     type: `MachineStatus__${machine?.status_model}` as any | ||||
|                   }) | ||||
|                 )} | ||||
|                 <Text fz="sm">{machine?.status_text}</Text> | ||||
|               </Flex> | ||||
|             </InfoItem> | ||||
|             <Group position="apart" spacing="xs"> | ||||
|               <Text fz="sm" fw={700}> | ||||
|                 <Trans>Errors</Trans>: | ||||
|               </Text> | ||||
|               {machine && machine?.machine_errors.length > 0 ? ( | ||||
|                 <Badge color="red" sx={{ marginLeft: '10px' }}> | ||||
|                   {machine?.machine_errors.length} | ||||
|                 </Badge> | ||||
|               ) : ( | ||||
|                 <Text fz="xs"> | ||||
|                   <Trans>No errors reported</Trans> | ||||
|                 </Text> | ||||
|               )} | ||||
|               <List w="100%"> | ||||
|                 {machine?.machine_errors.map((error, i) => ( | ||||
|                   <List.Item key={i}> | ||||
|                     <Code>{error}</Code> | ||||
|                   </List.Item> | ||||
|                 ))} | ||||
|               </List> | ||||
|             </Group> | ||||
|           </Stack> | ||||
|         </Stack> | ||||
|       </Card> | ||||
|       <Space h="10px" /> | ||||
|  | ||||
|       {machine?.is_driver_available && ( | ||||
|         <> | ||||
|           <Card withBorder> | ||||
|             <Title order={5} pb={4}> | ||||
|               <Trans>Machine Settings</Trans> | ||||
|             </Title> | ||||
|             <MachineSettingList | ||||
|               machinePk={machinePk} | ||||
|               configType="M" | ||||
|               onChange={refreshAll} | ||||
|             /> | ||||
|           </Card> | ||||
|  | ||||
|           <Card withBorder> | ||||
|             <Title order={5} pb={4}> | ||||
|               <Trans>Driver Settings</Trans> | ||||
|             </Title> | ||||
|             <MachineSettingList | ||||
|               machinePk={machinePk} | ||||
|               configType="D" | ||||
|               onChange={refreshAll} | ||||
|             /> | ||||
|           </Card> | ||||
|         </> | ||||
|       )} | ||||
|     </Stack> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Table displaying list of available plugins | ||||
|  */ | ||||
| export function MachineListTable({ | ||||
|   props, | ||||
|   renderMachineDrawer = true, | ||||
|   createProps | ||||
| }: { | ||||
|   props: InvenTreeTableProps; | ||||
|   renderMachineDrawer?: boolean; | ||||
|   createProps?: { machine_type?: string; driver?: string }; | ||||
| }) { | ||||
|   const { machineTypes, machineDrivers } = useMachineTypeDriver(); | ||||
|  | ||||
|   const table = useTable('machine'); | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const machineTableColumns = useMemo<TableColumn<MachineI>[]>( | ||||
|     () => [ | ||||
|       { | ||||
|         accessor: 'name', | ||||
|         sortable: true, | ||||
|         render: function (record) { | ||||
|           return ( | ||||
|             <Group position="left" noWrap> | ||||
|               <MachineStatusIndicator machine={record} /> | ||||
|               <Text>{record.name}</Text> | ||||
|               {record.restart_required && ( | ||||
|                 <Badge color="red"> | ||||
|                   <Trans>Restart required</Trans> | ||||
|                 </Badge> | ||||
|               )} | ||||
|             </Group> | ||||
|           ); | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         accessor: 'machine_type', | ||||
|         sortable: true, | ||||
|         render: (record) => { | ||||
|           const machineType = machineTypes?.find( | ||||
|             (m) => m.slug === record.machine_type | ||||
|           ); | ||||
|           return ( | ||||
|             <Group spacing="xs"> | ||||
|               <Text> | ||||
|                 {machineType ? machineType.name : record.machine_type} | ||||
|               </Text> | ||||
|               {machineTypes && !machineType && <UnavailableIndicator />} | ||||
|             </Group> | ||||
|           ); | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         accessor: 'driver', | ||||
|         sortable: true, | ||||
|         render: (record) => { | ||||
|           const driver = machineDrivers?.find((d) => d.slug === record.driver); | ||||
|           return ( | ||||
|             <Group spacing="xs"> | ||||
|               <Text>{driver ? driver.name : record.driver}</Text> | ||||
|               {!record.is_driver_available && <UnavailableIndicator />} | ||||
|             </Group> | ||||
|           ); | ||||
|         } | ||||
|       }, | ||||
|       BooleanColumn({ | ||||
|         accessor: 'initialized' | ||||
|       }), | ||||
|       BooleanColumn({ | ||||
|         accessor: 'active' | ||||
|       }), | ||||
|       { | ||||
|         accessor: 'status', | ||||
|         sortable: false, | ||||
|         render: (record) => { | ||||
|           const renderer = TableStatusRenderer( | ||||
|             `MachineStatus__${record.status_model}` as any | ||||
|           ); | ||||
|           if (renderer && record.status !== -1) { | ||||
|             return renderer(record); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     ], | ||||
|     [machineTypes] | ||||
|   ); | ||||
|  | ||||
|   const [createFormMachineType, setCreateFormMachineType] = useState< | ||||
|     null | string | ||||
|   >(null); | ||||
|   const createFormDriverOptions = useMemo(() => { | ||||
|     if (!machineDrivers) return []; | ||||
|  | ||||
|     return machineDrivers | ||||
|       .filter((d) => d.machine_type === createFormMachineType) | ||||
|       .map((d) => ({ | ||||
|         value: d.slug, | ||||
|         display_name: d.name | ||||
|       })); | ||||
|   }, [machineDrivers, createFormMachineType]); | ||||
|  | ||||
|   const createMachineForm = useCreateApiFormModal({ | ||||
|     title: t`Create machine`, | ||||
|     url: ApiEndpoints.machine_list, | ||||
|     fields: { | ||||
|       name: {}, | ||||
|       machine_type: { | ||||
|         hidden: !!createProps?.machine_type, | ||||
|         ...(createProps?.machine_type | ||||
|           ? { value: createProps.machine_type } | ||||
|           : {}), | ||||
|         field_type: 'choice', | ||||
|         choices: machineTypes | ||||
|           ? machineTypes.map((t) => ({ | ||||
|               value: t.slug, | ||||
|               display_name: t.name | ||||
|             })) | ||||
|           : [], | ||||
|         onValueChange: (value) => setCreateFormMachineType(value) | ||||
|       }, | ||||
|       driver: { | ||||
|         hidden: !!createProps?.driver, | ||||
|         ...(createProps?.driver ? { value: createProps.driver } : {}), | ||||
|         field_type: 'choice', | ||||
|         disabled: !createFormMachineType, | ||||
|         choices: createFormDriverOptions | ||||
|       }, | ||||
|       active: {} | ||||
|     }, | ||||
|     onFormSuccess: (data) => { | ||||
|       table.refreshTable(); | ||||
|       navigate( | ||||
|         renderMachineDrawer ? `machine-${data.pk}/` : `../machine-${data.pk}/` | ||||
|       ); | ||||
|     }, | ||||
|     onClose: () => { | ||||
|       setCreateFormMachineType(null); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   const tableActions = useMemo(() => { | ||||
|     return [ | ||||
|       <AddItemButton | ||||
|         variant="outline" | ||||
|         onClick={() => { | ||||
|           setCreateFormMachineType(null); | ||||
|           createMachineForm.open(); | ||||
|         }} | ||||
|       /> | ||||
|     ]; | ||||
|   }, [createMachineForm.open]); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       {createMachineForm.modal} | ||||
|       {renderMachineDrawer && ( | ||||
|         <DetailDrawer | ||||
|           title={t`Machine detail`} | ||||
|           size={'lg'} | ||||
|           renderContent={(id) => { | ||||
|             if (!id || !id.startsWith('machine-')) return false; | ||||
|             return ( | ||||
|               <MachineDrawer | ||||
|                 machinePk={id.replace('machine-', '')} | ||||
|                 refreshTable={table.refreshTable} | ||||
|               /> | ||||
|             ); | ||||
|           }} | ||||
|         /> | ||||
|       )} | ||||
|       <InvenTreeTable | ||||
|         url={apiUrl(ApiEndpoints.machine_list)} | ||||
|         tableState={table} | ||||
|         columns={machineTableColumns} | ||||
|         props={{ | ||||
|           ...props, | ||||
|           enableDownload: false, | ||||
|           onRowClick: (machine) => | ||||
|             navigate( | ||||
|               renderMachineDrawer | ||||
|                 ? `machine-${machine.pk}/` | ||||
|                 : `../machine-${machine.pk}/` | ||||
|             ), | ||||
|           tableActions, | ||||
|           params: { | ||||
|             ...props.params | ||||
|           }, | ||||
|           tableFilters: [ | ||||
|             { | ||||
|               name: 'active', | ||||
|               type: 'boolean' | ||||
|             }, | ||||
|             { | ||||
|               name: 'machine_type', | ||||
|               type: 'choice', | ||||
|               choiceFunction: () => | ||||
|                 machineTypes | ||||
|                   ? machineTypes.map((t) => ({ value: t.slug, label: t.name })) | ||||
|                   : [] | ||||
|             }, | ||||
|             { | ||||
|               name: 'driver', | ||||
|               type: 'choice', | ||||
|               choiceFunction: () => | ||||
|                 machineDrivers | ||||
|                   ? machineDrivers.map((d) => ({ | ||||
|                       value: d.slug, | ||||
|                       label: d.name | ||||
|                     })) | ||||
|                   : [] | ||||
|             } | ||||
|           ] | ||||
|         }} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										364
									
								
								src/frontend/src/tables/machine/MachineTypeTable.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										364
									
								
								src/frontend/src/tables/machine/MachineTypeTable.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,364 @@ | ||||
| import { Trans, t } from '@lingui/macro'; | ||||
| import { | ||||
|   ActionIcon, | ||||
|   Badge, | ||||
|   Card, | ||||
|   Code, | ||||
|   Group, | ||||
|   List, | ||||
|   LoadingOverlay, | ||||
|   Stack, | ||||
|   Text, | ||||
|   Title | ||||
| } from '@mantine/core'; | ||||
| import { IconRefresh } from '@tabler/icons-react'; | ||||
| import { useMemo } from 'react'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
|  | ||||
| import { InfoItem } from '../../components/items/InfoItem'; | ||||
| import { DetailDrawer } from '../../components/nav/DetailDrawer'; | ||||
| import { ApiEndpoints } from '../../enums/ApiEndpoints'; | ||||
| import { useTable } from '../../hooks/UseTable'; | ||||
| import { apiUrl } from '../../states/ApiState'; | ||||
| import { TableColumn } from '../Column'; | ||||
| import { BooleanColumn } from '../ColumnRenderers'; | ||||
| import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable'; | ||||
| import { MachineListTable, useMachineTypeDriver } from './MachineListTable'; | ||||
|  | ||||
| export interface MachineTypeI { | ||||
|   slug: string; | ||||
|   name: string; | ||||
|   description: string; | ||||
|   provider_file: string; | ||||
|   provider_plugin: { slug: string; name: string; pk: number | null } | null; | ||||
|   is_builtin: boolean; | ||||
| } | ||||
|  | ||||
| export interface MachineDriverI { | ||||
|   slug: string; | ||||
|   name: string; | ||||
|   description: string; | ||||
|   provider_file: string; | ||||
|   provider_plugin: { slug: string; name: string; pk: number | null } | null; | ||||
|   is_builtin: boolean; | ||||
|   machine_type: string; | ||||
|   driver_errors: string[]; | ||||
| } | ||||
|  | ||||
| function MachineTypeDrawer({ machineTypeSlug }: { machineTypeSlug: string }) { | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const { machineTypes, refresh, isFetching } = useMachineTypeDriver({ | ||||
|     includeDrivers: false | ||||
|   }); | ||||
|   const machineType = useMemo( | ||||
|     () => machineTypes?.find((m) => m.slug === machineTypeSlug), | ||||
|     [machineTypes, machineTypeSlug] | ||||
|   ); | ||||
|  | ||||
|   const table = useTable('machineDrivers'); | ||||
|  | ||||
|   const machineDriverTableColumns = useMemo<TableColumn<MachineDriverI>[]>( | ||||
|     () => [ | ||||
|       { | ||||
|         accessor: 'name', | ||||
|         title: t`Name` | ||||
|       }, | ||||
|       { | ||||
|         accessor: 'description', | ||||
|         title: t`Description` | ||||
|       }, | ||||
|       BooleanColumn({ | ||||
|         accessor: 'is_builtin', | ||||
|         title: t`Builtin driver` | ||||
|       }) | ||||
|     ], | ||||
|     [] | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <Stack> | ||||
|       <Group position="center"> | ||||
|         <Title order={4}> | ||||
|           {machineType ? machineType.name : machineTypeSlug} | ||||
|         </Title> | ||||
|       </Group> | ||||
|  | ||||
|       {!machineType && ( | ||||
|         <Text italic> | ||||
|           <Trans>Machine type not found.</Trans> | ||||
|         </Text> | ||||
|       )} | ||||
|  | ||||
|       <Card withBorder> | ||||
|         <Stack spacing="md"> | ||||
|           <Group position="apart"> | ||||
|             <Title order={4}> | ||||
|               <Trans>Machine type information</Trans> | ||||
|             </Title> | ||||
|             <ActionIcon variant="outline" onClick={() => refresh()}> | ||||
|               <IconRefresh /> | ||||
|             </ActionIcon> | ||||
|           </Group> | ||||
|  | ||||
|           <Stack pos="relative" spacing="xs"> | ||||
|             <LoadingOverlay visible={isFetching} overlayOpacity={0} /> | ||||
|             <InfoItem name={t`Name`} value={machineType?.name} type="text" /> | ||||
|             <InfoItem name={t`Slug`} value={machineType?.slug} type="text" /> | ||||
|             <InfoItem | ||||
|               name={t`Description`} | ||||
|               value={machineType?.description} | ||||
|               type="text" | ||||
|             /> | ||||
|             {!machineType?.is_builtin && ( | ||||
|               <InfoItem | ||||
|                 name={t`Provider plugin`} | ||||
|                 value={machineType?.provider_plugin?.name} | ||||
|                 type="text" | ||||
|                 link={ | ||||
|                   machineType?.provider_plugin?.pk !== null | ||||
|                     ? `../../plugin/${machineType?.provider_plugin?.pk}/` | ||||
|                     : undefined | ||||
|                 } | ||||
|               /> | ||||
|             )} | ||||
|             <InfoItem | ||||
|               name={t`Provider file`} | ||||
|               value={machineType?.provider_file} | ||||
|               type="code" | ||||
|             /> | ||||
|             <InfoItem | ||||
|               name={t`Builtin`} | ||||
|               value={machineType?.is_builtin} | ||||
|               type="boolean" | ||||
|             /> | ||||
|           </Stack> | ||||
|         </Stack> | ||||
|       </Card> | ||||
|  | ||||
|       <Card withBorder> | ||||
|         <Stack spacing="md"> | ||||
|           <Title order={4}> | ||||
|             <Trans>Available drivers</Trans> | ||||
|           </Title> | ||||
|  | ||||
|           <InvenTreeTable | ||||
|             url={apiUrl(ApiEndpoints.machine_driver_list)} | ||||
|             tableState={table} | ||||
|             columns={machineDriverTableColumns} | ||||
|             props={{ | ||||
|               dataFormatter: (data: any) => { | ||||
|                 return data.filter( | ||||
|                   (d: any) => d.machine_type === machineTypeSlug | ||||
|                 ); | ||||
|               }, | ||||
|               enableDownload: false, | ||||
|               enableSearch: false, | ||||
|               onRowClick: (machine) => navigate(`../driver-${machine.slug}/`) | ||||
|             }} | ||||
|           /> | ||||
|         </Stack> | ||||
|       </Card> | ||||
|     </Stack> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function MachineDriverDrawer({ | ||||
|   machineDriverSlug | ||||
| }: { | ||||
|   machineDriverSlug: string; | ||||
| }) { | ||||
|   const { machineDrivers, machineTypes, refresh, isFetching } = | ||||
|     useMachineTypeDriver(); | ||||
|   const machineDriver = useMemo( | ||||
|     () => machineDrivers?.find((d) => d.slug === machineDriverSlug), | ||||
|     [machineDrivers, machineDriverSlug] | ||||
|   ); | ||||
|   const machineType = useMemo( | ||||
|     () => machineTypes?.find((t) => t.slug === machineDriver?.machine_type), | ||||
|     [machineDrivers, machineTypes] | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <Stack> | ||||
|       <Group position="center"> | ||||
|         <Title order={4}> | ||||
|           {machineDriver ? machineDriver.name : machineDriverSlug} | ||||
|         </Title> | ||||
|       </Group> | ||||
|  | ||||
|       {!machineDriver && ( | ||||
|         <Text italic> | ||||
|           <Trans>Machine driver not found.</Trans> | ||||
|         </Text> | ||||
|       )} | ||||
|  | ||||
|       <Card withBorder> | ||||
|         <Stack spacing="md"> | ||||
|           <Group position="apart"> | ||||
|             <Title order={4}> | ||||
|               <Trans>Machine driver information</Trans> | ||||
|             </Title> | ||||
|             <ActionIcon variant="outline" onClick={() => refresh()}> | ||||
|               <IconRefresh /> | ||||
|             </ActionIcon> | ||||
|           </Group> | ||||
|  | ||||
|           <Stack pos="relative" spacing="xs"> | ||||
|             <LoadingOverlay visible={isFetching} overlayOpacity={0} /> | ||||
|             <InfoItem name={t`Name`} value={machineDriver?.name} type="text" /> | ||||
|             <InfoItem name={t`Slug`} value={machineDriver?.slug} type="text" /> | ||||
|             <InfoItem | ||||
|               name={t`Description`} | ||||
|               value={machineDriver?.description} | ||||
|               type="text" | ||||
|             /> | ||||
|             <InfoItem | ||||
|               name={t`Machine type`} | ||||
|               value={ | ||||
|                 machineType ? machineType.name : machineDriver?.machine_type | ||||
|               } | ||||
|               type="text" | ||||
|               link={ | ||||
|                 machineType | ||||
|                   ? `../type-${machineDriver?.machine_type}` | ||||
|                   : undefined | ||||
|               } | ||||
|             /> | ||||
|             {!machineDriver?.is_builtin && ( | ||||
|               <InfoItem | ||||
|                 name={t`Provider plugin`} | ||||
|                 value={machineDriver?.provider_plugin?.name} | ||||
|                 type="text" | ||||
|                 link={ | ||||
|                   machineDriver?.provider_plugin?.pk !== null | ||||
|                     ? `../../plugin/${machineDriver?.provider_plugin?.pk}/` | ||||
|                     : undefined | ||||
|                 } | ||||
|               /> | ||||
|             )} | ||||
|             <InfoItem | ||||
|               name={t`Provider file`} | ||||
|               value={machineDriver?.provider_file} | ||||
|               type="code" | ||||
|             /> | ||||
|             <InfoItem | ||||
|               name={t`Builtin`} | ||||
|               value={machineDriver?.is_builtin} | ||||
|               type="boolean" | ||||
|             /> | ||||
|             <Group position="apart" spacing="xs"> | ||||
|               <Text fz="sm" fw={700}> | ||||
|                 <Trans>Errors</Trans>: | ||||
|               </Text> | ||||
|               {machineDriver && machineDriver?.driver_errors.length > 0 ? ( | ||||
|                 <Badge color="red" sx={{ marginLeft: '10px' }}> | ||||
|                   {machineDriver.driver_errors.length} | ||||
|                 </Badge> | ||||
|               ) : ( | ||||
|                 <Text fz="xs"> | ||||
|                   <Trans>No errors reported</Trans> | ||||
|                 </Text> | ||||
|               )} | ||||
|               <List w="100%"> | ||||
|                 {machineDriver?.driver_errors.map((error, i) => ( | ||||
|                   <List.Item key={i}> | ||||
|                     <Code>{error}</Code> | ||||
|                   </List.Item> | ||||
|                 ))} | ||||
|               </List> | ||||
|             </Group> | ||||
|           </Stack> | ||||
|         </Stack> | ||||
|       </Card> | ||||
|  | ||||
|       <Card withBorder> | ||||
|         <Stack spacing="md"> | ||||
|           <Title order={4}> | ||||
|             <Trans>Machines</Trans> | ||||
|           </Title> | ||||
|  | ||||
|           <MachineListTable | ||||
|             props={{ params: { driver: machineDriverSlug } }} | ||||
|             renderMachineDrawer={false} | ||||
|             createProps={{ | ||||
|               machine_type: machineDriver?.machine_type, | ||||
|               driver: machineDriverSlug | ||||
|             }} | ||||
|           /> | ||||
|         </Stack> | ||||
|       </Card> | ||||
|     </Stack> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Table displaying list of available machine types | ||||
|  */ | ||||
| export function MachineTypeListTable({ | ||||
|   props | ||||
| }: { | ||||
|   props: InvenTreeTableProps; | ||||
| }) { | ||||
|   const table = useTable('machineTypes'); | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const machineTypeTableColumns = useMemo<TableColumn<MachineTypeI>[]>( | ||||
|     () => [ | ||||
|       { | ||||
|         accessor: 'name', | ||||
|         title: t`Name` | ||||
|       }, | ||||
|       { | ||||
|         accessor: 'description', | ||||
|         title: t`Description` | ||||
|       }, | ||||
|       BooleanColumn({ | ||||
|         accessor: 'is_builtin', | ||||
|         title: t`Builtin type` | ||||
|       }) | ||||
|     ], | ||||
|     [] | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <DetailDrawer | ||||
|         title={t`Machine type detail`} | ||||
|         size={'lg'} | ||||
|         renderContent={(id) => { | ||||
|           if (!id || !id.startsWith('type-')) return false; | ||||
|           return ( | ||||
|             <MachineTypeDrawer machineTypeSlug={id.replace('type-', '')} /> | ||||
|           ); | ||||
|         }} | ||||
|       /> | ||||
|       <DetailDrawer | ||||
|         title={t`Machine driver detail`} | ||||
|         size={'lg'} | ||||
|         renderContent={(id) => { | ||||
|           if (!id || !id.startsWith('driver-')) return false; | ||||
|           return ( | ||||
|             <MachineDriverDrawer | ||||
|               machineDriverSlug={id.replace('driver-', '')} | ||||
|             /> | ||||
|           ); | ||||
|         }} | ||||
|       /> | ||||
|       <InvenTreeTable | ||||
|         url={apiUrl(ApiEndpoints.machine_types_list)} | ||||
|         tableState={table} | ||||
|         columns={machineTypeTableColumns} | ||||
|         props={{ | ||||
|           ...props, | ||||
|           enableDownload: false, | ||||
|           enableSearch: false, | ||||
|           onRowClick: (machine) => navigate(`type-${machine.slug}/`), | ||||
|           params: { | ||||
|             ...props.params | ||||
|           } | ||||
|         }} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user