2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-16 20:15:44 +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:
Lukas
2024-02-14 15:13:47 +01:00
committed by GitHub
parent aed7754bc2
commit aa7eaaab3a
50 changed files with 4243 additions and 61 deletions

View File

@ -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}
/>

View File

@ -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>

View File

@ -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>
);

View File

@ -0,0 +1,5 @@
import { IconAlertCircle } from '@tabler/icons-react';
export function UnavailableIndicator() {
return <IconAlertCircle size={18} color="red" />;
}

View File

@ -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 />

View File

@ -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>
);

View File

@ -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} />;
}

View File

@ -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/',

View File

@ -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 />
}
];
}, []);

View File

@ -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>
);
}

View File

@ -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
*/

View File

@ -79,6 +79,7 @@ export interface Setting {
typ: SettingTyp;
plugin?: string;
method?: string;
required?: boolean;
}
export interface SettingChoice {

View 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
}))
: []
}
]
}}
/>
</>
);
}

View 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
}
}}
/>
</>
);
}