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