feat(manager/devices): Add device management features including creation and editing
This commit is contained in:
210
src/pages/Manager/Device/components/CreateDevice.tsx
Normal file
210
src/pages/Manager/Device/components/CreateDevice.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import TreeSelectedGroup from '@/components/shared/TreeSelectedGroup';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
ModalForm,
|
||||
ProForm,
|
||||
ProFormInstance,
|
||||
ProFormSelect,
|
||||
ProFormText,
|
||||
} from '@ant-design/pro-components';
|
||||
import { FormattedMessage, useIntl } from '@umijs/max';
|
||||
import { Button } from 'antd';
|
||||
import { MessageInstance } from 'antd/es/message/interface';
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
type CreateDeviceProps = {
|
||||
message: MessageInstance;
|
||||
onSuccess?: (isSuccess: boolean) => void;
|
||||
};
|
||||
|
||||
type CreateDeviceFormValues = {
|
||||
name: string;
|
||||
external_id: string;
|
||||
type: string;
|
||||
address?: string;
|
||||
group_id?: string;
|
||||
};
|
||||
|
||||
const CreateDevice = ({ message, onSuccess }: CreateDeviceProps) => {
|
||||
const formRef = useRef<ProFormInstance<CreateDeviceFormValues>>();
|
||||
const intl = useIntl();
|
||||
const [group_id, setGroupId] = useState<string | string[] | null>(null);
|
||||
|
||||
const handleGroupSelect = (group: string | string[] | null) => {
|
||||
setGroupId(group);
|
||||
formRef.current?.setFieldsValue({
|
||||
group_id: Array.isArray(group) ? group.join(',') : group || undefined,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalForm<CreateDeviceFormValues>
|
||||
title={intl.formatMessage({
|
||||
id: 'master.devices.register.title',
|
||||
defaultMessage: 'Register New Device',
|
||||
})}
|
||||
formRef={formRef}
|
||||
trigger={
|
||||
<Button type="primary" key="primary" icon={<PlusOutlined />}>
|
||||
<FormattedMessage
|
||||
id="master.devices.register"
|
||||
defaultMessage="Register"
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
autoFocusFirstInput
|
||||
onFinish={async (values: CreateDeviceFormValues) => {
|
||||
// TODO: Implement API call to create device
|
||||
console.log('Create device with values:', values);
|
||||
|
||||
try {
|
||||
// Placeholder for API call
|
||||
// const body = {
|
||||
// name: values.name,
|
||||
// metadata: {
|
||||
// external_id: values.external_id,
|
||||
// type: values.type,
|
||||
// address: values.address,
|
||||
// group_id: values.group_id,
|
||||
// },
|
||||
// };
|
||||
// const resp = await apiCreateDevice(body);
|
||||
|
||||
message.success(
|
||||
intl.formatMessage({
|
||||
id: 'master.devices.create.success',
|
||||
defaultMessage: 'Create device successfully',
|
||||
}),
|
||||
);
|
||||
formRef.current?.resetFields();
|
||||
onSuccess?.(true);
|
||||
return true;
|
||||
} catch (error) {
|
||||
message.error(
|
||||
intl.formatMessage({
|
||||
id: 'master.devices.create.error',
|
||||
defaultMessage: 'Failed to create device',
|
||||
}),
|
||||
);
|
||||
onSuccess?.(false);
|
||||
return false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ProFormText
|
||||
name={'name'}
|
||||
label={intl.formatMessage({
|
||||
id: 'master.devices.name',
|
||||
defaultMessage: 'Name',
|
||||
})}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'master.devices.name.placeholder',
|
||||
defaultMessage: 'Enter device name',
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'master.devices.name.required',
|
||||
defaultMessage: 'The device name is required',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<ProFormText
|
||||
name={'external_id'}
|
||||
label={intl.formatMessage({
|
||||
id: 'master.devices.external_id',
|
||||
defaultMessage: 'External ID',
|
||||
})}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'master.devices.external_id.placeholder',
|
||||
defaultMessage: 'Enter external ID',
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'master.devices.external_id.required',
|
||||
defaultMessage: 'The external ID is required',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<ProFormSelect
|
||||
name="type"
|
||||
label={intl.formatMessage({
|
||||
id: 'master.devices.type',
|
||||
defaultMessage: 'Type',
|
||||
})}
|
||||
options={[
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'master.devices.type.gms',
|
||||
defaultMessage: 'GMS',
|
||||
}),
|
||||
value: 'gms',
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'master.devices.type.sgw',
|
||||
defaultMessage: 'SGW',
|
||||
}),
|
||||
value: 'sgw',
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'master.devices.type.spole',
|
||||
defaultMessage: 'SPOLE',
|
||||
}),
|
||||
value: 'spole',
|
||||
},
|
||||
]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'master.devices.type.required',
|
||||
defaultMessage: 'Please select a device type',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<ProFormText
|
||||
name={'address'}
|
||||
label={intl.formatMessage({
|
||||
id: 'master.devices.address',
|
||||
defaultMessage: 'Address',
|
||||
})}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'master.devices.address.placeholder',
|
||||
defaultMessage: 'Enter device address',
|
||||
})}
|
||||
/>
|
||||
|
||||
<ProForm.Item
|
||||
name="group_id"
|
||||
label={intl.formatMessage({
|
||||
id: 'master.devices.groups',
|
||||
defaultMessage: 'Groups',
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'master.devices.groups.required',
|
||||
defaultMessage: 'Please select groups!',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TreeSelectedGroup groupIds={group_id} onSelected={handleGroupSelect} />
|
||||
</ProForm.Item>
|
||||
</ModalForm>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateDevice;
|
||||
133
src/pages/Manager/Device/components/EditDeviceModal.tsx
Normal file
133
src/pages/Manager/Device/components/EditDeviceModal.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Form, Input, Modal } from 'antd';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
device: MasterModel.Thing | null;
|
||||
onCancel: () => void;
|
||||
onSubmit: (values: {
|
||||
name: string;
|
||||
external_id: string;
|
||||
address?: string;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
const EditDeviceModal: React.FC<Props> = ({
|
||||
visible,
|
||||
device,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const intl = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
if (device) {
|
||||
form.setFieldsValue({
|
||||
name: device.name,
|
||||
external_id: device?.metadata?.external_id,
|
||||
address: device?.metadata?.address,
|
||||
});
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [device, form]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={intl.formatMessage({
|
||||
id: 'master.devices.update.title',
|
||||
defaultMessage: 'Update device',
|
||||
})}
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={() => form.submit()}
|
||||
okText={intl.formatMessage({
|
||||
id: 'master.devices.ok',
|
||||
defaultMessage: 'OK',
|
||||
})}
|
||||
cancelText={intl.formatMessage({
|
||||
id: 'master.devices.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={onSubmit} preserve={false}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={intl.formatMessage({
|
||||
id: 'master.devices.name',
|
||||
defaultMessage: 'Name',
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'master.devices.name.required',
|
||||
defaultMessage: 'Please enter device name',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'master.devices.name.placeholder',
|
||||
defaultMessage: 'Enter device name',
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="external_id"
|
||||
label={intl.formatMessage({
|
||||
id: 'master.devices.external_id',
|
||||
defaultMessage: 'External ID',
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'master.devices.external_id.required',
|
||||
defaultMessage: 'Please enter external ID',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'master.devices.external_id.placeholder',
|
||||
defaultMessage: 'Enter external ID',
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="address"
|
||||
label={intl.formatMessage({
|
||||
id: 'master.devices.address',
|
||||
defaultMessage: 'Address',
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'master.devices.address.required',
|
||||
defaultMessage: 'Please enter address',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'master.devices.address.placeholder',
|
||||
defaultMessage: 'Enter address',
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditDeviceModal;
|
||||
@@ -1,5 +1,319 @@
|
||||
import IconFont from '@/components/IconFont';
|
||||
import TreeGroup from '@/components/shared/TreeGroup';
|
||||
import { DEFAULT_PAGE_SIZE } from '@/constants';
|
||||
import { apiSearchThings } from '@/services/master/ThingController';
|
||||
import { EditOutlined, EnvironmentOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
ActionType,
|
||||
ProCard,
|
||||
ProColumns,
|
||||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { FormattedMessage, useIntl } from '@umijs/max';
|
||||
import { Button, Divider, Grid, Space, Tag, theme } from 'antd';
|
||||
import message from 'antd/es/message';
|
||||
import Paragraph from 'antd/lib/typography/Paragraph';
|
||||
import { useRef, useState } from 'react';
|
||||
import CreateDevice from './components/CreateDevice';
|
||||
import EditDeviceModal from './components/EditDeviceModal';
|
||||
|
||||
const ManagerDevicePage = () => {
|
||||
return <div>ManagerDevicePage</div>;
|
||||
const { useBreakpoint } = Grid;
|
||||
const intl = useIntl();
|
||||
const screens = useBreakpoint();
|
||||
const { token } = theme.useToken();
|
||||
const actionRef = useRef<ActionType | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const [selectedRowsState, setSelectedRowsState] = useState<
|
||||
MasterModel.Thing[]
|
||||
>([]);
|
||||
const [groupCheckedKeys, setGroupCheckedKeys] = useState<
|
||||
string | string[] | null
|
||||
>(null);
|
||||
const [isEditModalVisible, setIsEditModalVisible] = useState<boolean>(false);
|
||||
const [editingDevice, setEditingDevice] = useState<MasterModel.Thing | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const handleClickAssign = (device: MasterModel.Thing) => {
|
||||
console.log('Device ', device);
|
||||
};
|
||||
|
||||
const handleEdit = (device: MasterModel.Thing) => {
|
||||
setEditingDevice(device);
|
||||
setIsEditModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEditCancel = () => {
|
||||
setIsEditModalVisible(false);
|
||||
setEditingDevice(null);
|
||||
};
|
||||
|
||||
const handleEditSubmit = async (values: any) => {
|
||||
// TODO: call update API here if available. For now just simulate success.
|
||||
console.log('Update values for', editingDevice?.id, values);
|
||||
messageApi.success('Cập nhật thành công');
|
||||
setIsEditModalVisible(false);
|
||||
setEditingDevice(null);
|
||||
actionRef.current?.reload();
|
||||
};
|
||||
|
||||
const columns: ProColumns<MasterModel.Thing>[] = [
|
||||
{
|
||||
key: 'name',
|
||||
title: (
|
||||
<FormattedMessage id="master.devices.name" defaultMessage="Name" />
|
||||
),
|
||||
tip: intl.formatMessage({
|
||||
id: 'master.devices.name.tip',
|
||||
defaultMessage: 'The device name',
|
||||
}),
|
||||
dataIndex: 'name',
|
||||
render: (_, record) => (
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: 0,
|
||||
verticalAlign: 'middle',
|
||||
display: 'inline-block',
|
||||
color: token.colorText,
|
||||
}}
|
||||
copyable
|
||||
>
|
||||
{record?.name}
|
||||
</Paragraph>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
key: 'external_id',
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="master.devices.external_id"
|
||||
defaultMessage="External ID"
|
||||
/>
|
||||
),
|
||||
tip: intl.formatMessage({
|
||||
id: 'master.devices.external_id.tip',
|
||||
defaultMessage: 'The external identifier',
|
||||
}),
|
||||
responsive: ['lg', 'md'],
|
||||
dataIndex: ['metadata', 'external_id'],
|
||||
render: (_, record) =>
|
||||
record?.metadata?.external_id ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: 0,
|
||||
verticalAlign: 'middle',
|
||||
display: 'inline-block',
|
||||
color: token.colorText,
|
||||
}}
|
||||
copyable
|
||||
>
|
||||
{record?.metadata?.external_id}
|
||||
</Paragraph>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
key: 'type',
|
||||
hideInSearch: true,
|
||||
title: (
|
||||
<FormattedMessage id="master.devices.type" defaultMessage="Type" />
|
||||
),
|
||||
tip: intl.formatMessage({
|
||||
id: 'master.devices.type.tip',
|
||||
defaultMessage: 'The device type',
|
||||
}),
|
||||
dataIndex: ['metadata', 'type'],
|
||||
render: (_, record) => record?.metadata?.type || '...',
|
||||
},
|
||||
|
||||
{
|
||||
key: 'connected',
|
||||
hideInSearch: true,
|
||||
title: <FormattedMessage id="common.status" defaultMessage="Status" />,
|
||||
dataIndex: ['metadata', 'connected'],
|
||||
render: (_, record) => (
|
||||
<Tag color={record?.metadata?.connected ? 'green' : 'red'}>
|
||||
{record?.metadata?.connected
|
||||
? intl.formatMessage({
|
||||
id: 'master.devices.online',
|
||||
defaultMessage: 'Online',
|
||||
})
|
||||
: intl.formatMessage({
|
||||
id: 'master.devices.offline',
|
||||
defaultMessage: 'Offline',
|
||||
})}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage id="common.actions" defaultMessage="Operating" />
|
||||
),
|
||||
hideInSearch: true,
|
||||
render: (_, device) => {
|
||||
return (
|
||||
<Space
|
||||
size={5}
|
||||
split={<Divider type="vertical" style={{ margin: '0 4px' }} />}
|
||||
>
|
||||
<Button
|
||||
shape="default"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(device)}
|
||||
/>
|
||||
<Button
|
||||
shape="default"
|
||||
size="small"
|
||||
icon={<EnvironmentOutlined />}
|
||||
// onClick={() => handleClickAssign(device)}
|
||||
/>
|
||||
<Button
|
||||
shape="default"
|
||||
size="small"
|
||||
icon={<IconFont type="icon-camera" />}
|
||||
// onClick={() => handleClickAssign(device)}
|
||||
/>
|
||||
{device?.metadata?.type === 'gmsv6' && (
|
||||
<Button
|
||||
shape="default"
|
||||
size="small"
|
||||
icon={<IconFont type="icon-terminal" />}
|
||||
// onClick={() => handleClickAssign(device)}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditDeviceModal
|
||||
visible={isEditModalVisible}
|
||||
device={editingDevice}
|
||||
onCancel={handleEditCancel}
|
||||
onSubmit={handleEditSubmit}
|
||||
/>
|
||||
{contextHolder}
|
||||
<ProCard split={screens.md ? 'vertical' : 'horizontal'}>
|
||||
<ProCard colSpan={{ xs: 24, sm: 24, md: 6, lg: 6, xl: 6 }}>
|
||||
<TreeGroup
|
||||
disable={isLoading}
|
||||
multiple={true}
|
||||
groupIds={groupCheckedKeys}
|
||||
onSelected={(value: string | string[] | null) => {
|
||||
setGroupCheckedKeys(value);
|
||||
if (actionRef.current) {
|
||||
actionRef.current.reload();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ProCard>
|
||||
<ProCard colSpan={{ xs: 24, sm: 24, md: 18, lg: 18, xl: 18 }}>
|
||||
<ProTable<MasterModel.Thing>
|
||||
columns={columns}
|
||||
tableLayout="auto"
|
||||
actionRef={actionRef}
|
||||
rowKey="id"
|
||||
search={{
|
||||
layout: 'vertical',
|
||||
defaultCollapsed: false,
|
||||
}}
|
||||
dateFormatter="string"
|
||||
rowSelection={{
|
||||
selectedRowKeys: selectedRowsState.map((row) => row.id!),
|
||||
onChange: (_: React.Key[], selectedRows: MasterModel.Thing[]) => {
|
||||
setSelectedRowsState(selectedRows);
|
||||
},
|
||||
}}
|
||||
pagination={{
|
||||
defaultPageSize: DEFAULT_PAGE_SIZE * 2,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '15', '20'],
|
||||
showTotal: (total, range) =>
|
||||
`${range[0]}-${range[1]}
|
||||
${intl.formatMessage({
|
||||
id: 'common.paginations.of',
|
||||
defaultMessage: 'of',
|
||||
})}
|
||||
${total} ${intl.formatMessage({
|
||||
id: 'master.devices.table.pagination',
|
||||
defaultMessage: 'devices',
|
||||
})}`,
|
||||
}}
|
||||
request={async (params = {}) => {
|
||||
const { current = 1, pageSize, name, external_id } = params;
|
||||
const size = pageSize || DEFAULT_PAGE_SIZE * 2;
|
||||
const offset = current === 1 ? 0 : (current - 1) * size;
|
||||
setIsLoading(true);
|
||||
|
||||
const metadata: Partial<MasterModel.ThingMetadata> = {};
|
||||
if (external_id) metadata.external_id = external_id;
|
||||
|
||||
// Add group filter if groups are selected
|
||||
if (groupCheckedKeys && groupCheckedKeys.length > 0) {
|
||||
const groupId = Array.isArray(groupCheckedKeys)
|
||||
? groupCheckedKeys.join(',')
|
||||
: groupCheckedKeys;
|
||||
metadata.group_id = groupId;
|
||||
}
|
||||
|
||||
const query: MasterModel.SearchThingPaginationBody = {
|
||||
offset: offset,
|
||||
limit: size,
|
||||
order: 'name',
|
||||
dir: 'asc',
|
||||
};
|
||||
if (name) query.name = name;
|
||||
if (Object.keys(metadata).length > 0) query.metadata = metadata;
|
||||
|
||||
try {
|
||||
const response = await apiSearchThings(query);
|
||||
setIsLoading(false);
|
||||
return {
|
||||
data: response.things || [],
|
||||
success: true,
|
||||
total: response.total || 0,
|
||||
};
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
return {
|
||||
data: [],
|
||||
success: false,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
search: false,
|
||||
setting: false,
|
||||
density: false,
|
||||
reload: true,
|
||||
}}
|
||||
toolBarRender={() => [
|
||||
<CreateDevice
|
||||
message={messageApi}
|
||||
onSuccess={(isSuccess) => {
|
||||
if (isSuccess) {
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
}}
|
||||
key="create-device"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</ProCard>
|
||||
</ProCard>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManagerDevicePage;
|
||||
|
||||
Reference in New Issue
Block a user