feat(master/device-detail && alarm): Enhance device detail page with alarm list and binary sensors integration, update iconfont URLs, and improve alarm confirmation handling

This commit is contained in:
Tran Anh Tuan
2026-01-27 20:56:54 +07:00
parent ed5751002b
commit ea07d0c99e
16 changed files with 497 additions and 104 deletions

View File

@@ -86,7 +86,7 @@ export const layout: RunTimeLayoutConfig = ({ initialState }) => {
contentWidth: 'Fluid', contentWidth: 'Fluid',
navTheme: isDark ? 'realDark' : 'light', navTheme: isDark ? 'realDark' : 'light',
splitMenus: true, splitMenus: true,
iconfontUrl: '//at.alicdn.com/t/c/font_5096559_gjd5149497o.js', iconfontUrl: '//at.alicdn.com/t/c/font_5096559_qeg2471go4g.js',
contentStyle: { contentStyle: {
padding: 0, padding: 0,
margin: 0, margin: 0,

View File

@@ -1,7 +1,7 @@
import { createFromIconfontCN } from '@ant-design/icons'; import { createFromIconfontCN } from '@ant-design/icons';
const IconFont = createFromIconfontCN({ const IconFont = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_5096559_gjd5149497o.js', scriptUrl: '//at.alicdn.com/t/c/font_5096559_qeg2471go4g.js',
}); });
export default IconFont; export default IconFont;

View File

@@ -0,0 +1,82 @@
import { HTTPSTATUS } from '@/constants';
import { apiUnconfirmAlarm } from '@/services/master/AlarmController';
import { CloseOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Popconfirm } from 'antd';
import { MessageInstance } from 'antd/es/message/interface';
type AlarmUnConfirmButtonProps = {
alarm: MasterModel.Alarm;
onFinish?: (isReload: boolean) => void;
message: MessageInstance;
button?: React.ReactNode;
};
const AlarmUnConfirmButton = ({
alarm,
message,
button,
onFinish,
}: AlarmUnConfirmButtonProps) => {
const intl = useIntl();
return (
<Popconfirm
title={intl.formatMessage({
id: 'master.alarms.unconfirm.body',
defaultMessage: 'Are you sure you want to unconfirm this alarm?',
})}
okText={intl.formatMessage({
id: 'common.yes',
defaultMessage: 'Yes',
})}
cancelText={intl.formatMessage({
id: 'common.no',
defaultMessage: 'No',
})}
onConfirm={async () => {
const body: MasterModel.ConfirmAlarmRequest = {
id: alarm.id,
thing_id: alarm.thing_id,
time: alarm.time,
};
try {
const resp = await apiUnconfirmAlarm(body);
if (resp.status === HTTPSTATUS.HTTP_SUCCESS) {
message.success({
content: intl.formatMessage({
id: 'master.alarms.unconfirm.success',
defaultMessage: 'Confirm alarm successfully',
}),
});
onFinish?.(true);
} else if (resp.status === HTTPSTATUS.HTTP_NOTFOUND) {
message.warning({
content: intl.formatMessage({
id: 'master.alarms.not_found',
defaultMessage: 'Alarm has expired or does not exist',
}),
});
onFinish?.(true);
} else {
throw new Error('Failed to confirm alarm');
}
} catch (error) {
console.error('Error when unconfirm alarm: ', error);
message.error({
content: intl.formatMessage({
id: 'master.alarms.unconfirm.fail',
defaultMessage: 'Unconfirm alarm failed',
}),
});
}
}}
>
{button ? (
button
) : (
<Button danger icon={<CloseOutlined />} size="small" />
)}
</Popconfirm>
);
};
export default AlarmUnConfirmButton;

View File

@@ -0,0 +1,158 @@
import { DATE_TIME_FORMAT, DEFAULT_PAGE_SIZE, DURATION_POLLING } from '@/const';
import AlarmDescription from '@/pages/Alarm/components/AlarmDescription';
import AlarmFormConfirm from '@/pages/Alarm/components/AlarmFormConfirm';
import { apiGetAlarms } from '@/services/master/AlarmController';
import { CheckOutlined } from '@ant-design/icons';
import { ActionType, ProList, ProListMetas } from '@ant-design/pro-components';
import { useIntl } from '@umijs/max';
import { Button, message, theme, Typography } from 'antd';
import moment from 'moment';
import { useRef, useState } from 'react';
import AlarmUnConfirmButton from './Alarm/AlarmUnConfirmButton';
import { getBadgeStatus, getTitleColorByDeviceStateLevel } from './ThingShared';
type DeviceAlarmListProps = {
thingId: string;
};
const DeviceAlarmList = ({ thingId }: DeviceAlarmListProps) => {
const { token } = theme.useToken();
const [messageApi, contextHolder] = message.useMessage();
const intl = useIntl();
const actionRef = useRef<ActionType>();
const [loading, setIsLoading] = useState<boolean>(false);
const [alarmConfirmed, setAlarmConfirmed] = useState<
MasterModel.Alarm | undefined
>(undefined);
const columns: ProListMetas<MasterModel.Alarm> = {
title: {
dataIndex: 'name',
render(_, item) {
return (
<Typography.Text
style={{
color: getTitleColorByDeviceStateLevel(item.level || 0, token),
}}
>
{item.name}
</Typography.Text>
);
},
},
avatar: {
render: (_, item) => getBadgeStatus(item.level || 0),
},
description: {
dataIndex: 'time',
render: (_, item) => {
return (
<>
{item.confirmed ? (
<AlarmDescription alarm={item} size="small" />
) : (
<div>{moment.unix(item?.time || 0).format(DATE_TIME_FORMAT)}</div>
)}
</>
);
},
},
actions: {
render: (_, entity) => [
entity.confirmed ? (
<AlarmUnConfirmButton
key="unconfirm"
alarm={entity}
message={messageApi}
onFinish={(isReload) => {
if (isReload) actionRef.current?.reload();
}}
/>
) : (
<Button
key="confirm"
icon={<CheckOutlined />}
type="dashed"
className="green-btn"
style={{ color: 'green', borderColor: 'green' }}
size="small"
onClick={() => setAlarmConfirmed(entity)}
></Button>
),
],
},
};
return (
<>
{contextHolder}
<AlarmFormConfirm
isOpen={!!alarmConfirmed}
setIsOpen={(v) => {
if (!v) setAlarmConfirmed(undefined);
}}
alarm={alarmConfirmed || ({} as MasterModel.Alarm)}
trigger={<></>}
message={messageApi}
onFinish={(isReload) => {
if (isReload) actionRef.current?.reload();
}}
/>
<ProList<MasterModel.Alarm>
rowKey={(record) => `${record.thing_id}_${record.id}`}
bordered
actionRef={actionRef}
metas={columns}
polling={DURATION_POLLING}
loading={loading}
search={false}
dateFormatter="string"
cardProps={{
bodyStyle: { paddingInline: 16, paddingBlock: 8 },
}}
pagination={{
defaultPageSize: DEFAULT_PAGE_SIZE * 2,
showSizeChanger: false,
pageSizeOptions: ['5', '10', '15', '20'],
showTotal: (total, range) =>
`${range[0]}-${range[1]}
${intl.formatMessage({
id: 'common.paginations.of',
defaultMessage: 'of',
})}
${total} ${intl.formatMessage({
id: 'master.alarms.table.pagination',
defaultMessage: 'alarms',
})}`,
}}
request={async (params) => {
setIsLoading(true);
try {
const { current, pageSize } = params;
const offset = current === 1 ? 0 : (current! - 1) * pageSize!;
const body: MasterModel.SearchAlarmPaginationBody = {
limit: pageSize,
offset: offset,
thing_id: thingId,
dir: 'desc',
};
const res = await apiGetAlarms(body);
return {
data: res.alarms,
total: res.total,
success: true,
};
} catch (error) {
return {
data: [],
total: 0,
success: false,
};
} finally {
setIsLoading(false);
}
}}
/>
</>
);
};
export default DeviceAlarmList;

View File

@@ -4,7 +4,7 @@ import {
STATUS_SOS, STATUS_SOS,
STATUS_WARNING, STATUS_WARNING,
} from '@/constants'; } from '@/constants';
import { Badge } from 'antd'; import { Badge, GlobalToken } from 'antd';
import IconFont from '../IconFont'; import IconFont from '../IconFont';
export const getBadgeStatus = (status: number) => { export const getBadgeStatus = (status: number) => {
@@ -30,3 +30,21 @@ export const getBadgeConnection = (online: boolean) => {
return <IconFont type="icon-cloud-disconnect" />; return <IconFont type="icon-cloud-disconnect" />;
} }
}; };
export const getTitleColorByDeviceStateLevel = (
level: number,
token: GlobalToken,
) => {
switch (level) {
case STATUS_NORMAL:
return token.colorSuccess;
case STATUS_WARNING:
return token.colorWarning;
case STATUS_DANGEROUS:
return token.colorError;
case STATUS_SOS:
return token.colorError;
default:
return token.colorText;
}
};

View File

@@ -4,6 +4,7 @@ import masterGroupEn from './master-group-en';
import masterSysLogEn from './master-log-en'; import masterSysLogEn from './master-log-en';
import masterMenuEn from './master-menu-en'; import masterMenuEn from './master-menu-en';
import masterMenuProfileEn from './master-profile-en'; import masterMenuProfileEn from './master-profile-en';
import masterThingDetailEn from './master-thing-detail-en';
import masterThingEn from './master-thing-en'; import masterThingEn from './master-thing-en';
import masterUserEn from './master-user-en'; import masterUserEn from './master-user-en';
export default { export default {
@@ -16,4 +17,5 @@ export default {
...masterSysLogEn, ...masterSysLogEn,
...masterUserEn, ...masterUserEn,
...masterGroupEn, ...masterGroupEn,
...masterThingDetailEn,
}; };

View File

@@ -0,0 +1 @@
export default { 'master.thing.detail.alarmList.title': 'Alarm List' };

View File

@@ -0,0 +1,3 @@
export default {
'master.thing.detail.alarmList.title': 'Danh sách cảnh báo',
};

View File

@@ -4,6 +4,7 @@ import masterGroupVi from './master-group-vi';
import masterSysLogVi from './master-log-vi'; import masterSysLogVi from './master-log-vi';
import masterMenuVi from './master-menu-vi'; import masterMenuVi from './master-menu-vi';
import masterProfileVi from './master-profile-vi'; import masterProfileVi from './master-profile-vi';
import masterThingDetailVi from './master-thing-detail-vi';
import masterThingVi from './master-thing-vi'; import masterThingVi from './master-thing-vi';
import masterUserVi from './master-user-vi'; import masterUserVi from './master-user-vi';
export default { export default {
@@ -16,4 +17,5 @@ export default {
...masterSysLogVi, ...masterSysLogVi,
...masterUserVi, ...masterUserVi,
...masterGroupVi, ...masterGroupVi,
...masterThingDetailVi,
}; };

View File

@@ -4,21 +4,23 @@ import {
UserOutlined, UserOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Space, Typography } from 'antd'; import { Space, Typography } from 'antd';
import { SpaceSize } from 'antd/es/space';
import moment from 'moment'; import moment from 'moment';
const { Text } = Typography; const { Text } = Typography;
interface AlarmDescriptionProps { interface AlarmDescriptionProps {
alarm: MasterModel.Alarm; alarm: MasterModel.Alarm;
size?: SpaceSize;
} }
const AlarmDescription = ({ alarm }: AlarmDescriptionProps) => { const AlarmDescription = ({ alarm, size = 'large' }: AlarmDescriptionProps) => {
if (!alarm?.confirmed) { if (!alarm?.confirmed) {
return null; return null;
} }
return ( return (
<Space size="large" wrap> <Space size={size} wrap>
<Space align="baseline" size={10}> <Space align="baseline" size={10}>
<CheckCircleOutlined style={{ color: '#52c41a' }} /> <CheckCircleOutlined style={{ color: '#52c41a' }} />
<Text type="secondary" style={{ fontSize: 15 }}> <Text type="secondary" style={{ fontSize: 15 }}>

View File

@@ -11,7 +11,7 @@ import { FormattedMessage, useIntl } from '@umijs/max';
import { Button, Flex } from 'antd'; import { Button, Flex } from 'antd';
import { MessageInstance } from 'antd/es/message/interface'; import { MessageInstance } from 'antd/es/message/interface';
import moment from 'moment'; import moment from 'moment';
import { useRef, useState } from 'react'; import React, { useRef } from 'react';
type AlarmForm = { type AlarmForm = {
name: string; name: string;
@@ -19,22 +19,26 @@ type AlarmForm = {
description: string; description: string;
}; };
type AlarmFormConfirmProps = { type AlarmFormConfirmProps = {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
trigger?: React.ReactNode;
alarm: MasterModel.Alarm; alarm: MasterModel.Alarm;
message: MessageInstance; message: MessageInstance;
onFinish?: (reload: boolean) => void; onFinish?: (reload: boolean) => void;
}; };
const AlarmFormConfirm = ({ const AlarmFormConfirm = ({
isOpen,
setIsOpen,
trigger,
alarm, alarm,
message, message,
onFinish, onFinish,
}: AlarmFormConfirmProps) => { }: AlarmFormConfirmProps) => {
const [modalVisit, setModalVisit] = useState(false);
const formRef = useRef<ProFormInstance<AlarmForm>>(); const formRef = useRef<ProFormInstance<AlarmForm>>();
const intl = useIntl(); const intl = useIntl();
return ( return (
<ModalForm<AlarmForm> <ModalForm<AlarmForm>
open={modalVisit} open={isOpen}
formRef={formRef} formRef={formRef}
title={ title={
<Flex align="center" justify="center"> <Flex align="center" justify="center">
@@ -45,8 +49,9 @@ const AlarmFormConfirm = ({
layout="horizontal" layout="horizontal"
modalProps={{ modalProps={{
destroyOnHidden: true, destroyOnHidden: true,
// maskStyle: { backgroundColor: 'rgba(0,0,0,0.1)' },
}} }}
onOpenChange={setModalVisit} onOpenChange={setIsOpen}
request={async () => { request={async () => {
return { return {
name: alarm.name ?? '', name: alarm.name ?? '',
@@ -95,17 +100,20 @@ const AlarmFormConfirm = ({
return true; return true;
}} }}
trigger={ trigger={
React.isValidElement(trigger) ? (
trigger
) : (
<Button <Button
size="small" size="small"
variant="solid" type="primary"
color="green"
icon={<CheckOutlined />} icon={<CheckOutlined />}
onClick={() => setModalVisit(true)} onClick={() => setIsOpen(true)}
> >
{intl.formatMessage({ {intl.formatMessage({
id: 'common.confirm', id: 'common.confirm',
})} })}
</Button> </Button>
)
} }
> >
<ProForm.Group> <ProForm.Group>

View File

@@ -1,9 +1,7 @@
import AlarmUnConfirmButton from '@/components/shared/Alarm/AlarmUnConfirmButton';
import ThingsFilter from '@/components/shared/ThingFilterModal'; import ThingsFilter from '@/components/shared/ThingFilterModal';
import { DATE_TIME_FORMAT, DEFAULT_PAGE_SIZE, HTTPSTATUS } from '@/constants'; import { DATE_TIME_FORMAT, DEFAULT_PAGE_SIZE } from '@/constants';
import { import { apiGetAlarms } from '@/services/master/AlarmController';
apiGetAlarms,
apiUnconfirmAlarm,
} from '@/services/master/AlarmController';
import { import {
CloseOutlined, CloseOutlined,
DeleteOutlined, DeleteOutlined,
@@ -11,7 +9,7 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components'; import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
import { FormattedMessage, useIntl, useModel } from '@umijs/max'; import { FormattedMessage, useIntl, useModel } from '@umijs/max';
import { Button, Flex, message, Popconfirm, Progress, Tooltip } from 'antd'; import { Button, Flex, message, Progress, Tooltip } from 'antd';
import moment from 'moment'; import moment from 'moment';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import AlarmDescription from './components/AlarmDescription'; import AlarmDescription from './components/AlarmDescription';
@@ -26,7 +24,7 @@ const AlarmPage = () => {
const [messageApi, contextHolder] = message.useMessage(); const [messageApi, contextHolder] = message.useMessage();
const { initialState } = useModel('@@initialState'); const { initialState } = useModel('@@initialState');
const { currentUserProfile } = initialState || {}; const { currentUserProfile } = initialState || {};
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState<boolean>(false);
const columns: ProColumns<MasterModel.Alarm>[] = [ const columns: ProColumns<MasterModel.Alarm>[] = [
{ {
title: intl.formatMessage({ title: intl.formatMessage({
@@ -202,65 +200,22 @@ const AlarmPage = () => {
return ( return (
<Flex gap={10}> <Flex gap={10}>
{alarm?.confirmed || false ? ( {alarm?.confirmed || false ? (
<Popconfirm <AlarmUnConfirmButton
title={intl.formatMessage({ alarm={alarm}
id: 'master.alarms.unconfirm.body', message={messageApi}
defaultMessage: button={
'Are you sure you want to unconfirm this alarm?',
})}
okText={intl.formatMessage({
id: 'common.yes',
defaultMessage: 'Yes',
})}
cancelText={intl.formatMessage({
id: 'common.no',
defaultMessage: 'No',
})}
onConfirm={async () => {
const body: MasterModel.ConfirmAlarmRequest = {
id: alarm.id,
thing_id: alarm.thing_id,
time: alarm.time,
};
try {
const resp = await apiUnconfirmAlarm(body);
if (resp.status === HTTPSTATUS.HTTP_SUCCESS) {
message.success({
content: intl.formatMessage({
id: 'master.alarms.unconfirm.success',
defaultMessage: 'Confirm alarm successfully',
}),
});
tableRef.current?.reload();
} else if (resp.status === HTTPSTATUS.HTTP_NOTFOUND) {
message.warning({
content: intl.formatMessage({
id: 'master.alarms.not_found',
defaultMessage:
'Alarm has expired or does not exist',
}),
});
tableRef.current?.reload();
} else {
throw new Error('Failed to confirm alarm');
}
} catch (error) {
console.error('Error when unconfirm alarm: ', error);
message.error({
content: intl.formatMessage({
id: 'master.alarms.unconfirm.fail',
defaultMessage: 'Unconfirm alarm failed',
}),
});
}
}}
>
<Button danger icon={<DeleteOutlined />} size="small"> <Button danger icon={<DeleteOutlined />} size="small">
<FormattedMessage id="master.alarms.unconfirm.title" /> <FormattedMessage id="master.alarms.unconfirm.title" />
</Button> </Button>
</Popconfirm> }
onFinish={(isReload) => {
if (isReload) tableRef.current?.reload();
}}
/>
) : ( ) : (
<AlarmFormConfirm <AlarmFormConfirm
isOpen={isConfirmModalOpen}
setIsOpen={setIsConfirmModalOpen}
alarm={alarm} alarm={alarm}
message={messageApi} message={messageApi}
onFinish={(isReload) => { onFinish={(isReload) => {

View File

@@ -0,0 +1,105 @@
import IconFont from '@/components/IconFont';
import { StatisticCard } from '@ant-design/pro-components';
import { Flex, GlobalToken, Grid, theme } from 'antd';
type BinarySensorsProps = {
nodeConfigs: MasterModel.NodeConfig[];
};
export const getBinaryEntities = (
nodeConfigs: MasterModel.NodeConfig[] = [],
): MasterModel.Entity[] =>
nodeConfigs.flatMap((nodeConfig) =>
nodeConfig.type === 'din'
? nodeConfig.entities.filter(
(entity) => entity.type === 'bin' && entity.active === 1,
)
: [],
);
interface IconTypeResult {
iconType: string;
color: string;
}
const getIconTypeByEntity = (
entity: MasterModel.Entity,
token: GlobalToken,
): IconTypeResult => {
if (!entity.config || entity.config.length === 0) {
return {
iconType: 'icon-device-setting',
color: token.colorPrimary,
};
}
switch (entity.config[0].subType) {
case 'smoke':
return {
iconType: 'icon-fire',
color: entity.value === 0 ? token.colorSuccess : token.colorWarning,
};
case 'heat':
return {
iconType: 'icon-fire',
color: entity.value === 0 ? token.colorSuccess : token.colorWarning,
};
case 'motion':
return {
iconType: 'icon-motion',
color: entity.value === 0 ? token.colorTextBase : token.colorInfoActive,
};
case 'flood':
return {
iconType: 'icon-water-ingress',
color: entity.value === 0 ? token.colorSuccess : token.colorWarning,
};
case 'door':
return {
iconType: entity.value === 0 ? 'icon-door' : 'icon-door-open',
color: entity.value === 0 ? token.colorText : token.colorWarning,
};
case 'button':
return {
iconType: 'icon-alarm-button',
color: entity.value === 0 ? token.colorText : token.colorSuccess,
};
default:
return {
iconType: 'icon-door',
color: token.colorPrimary,
};
}
};
const StatisticCardItem = (entity: MasterModel.Entity, token: GlobalToken) => {
const { iconType, color } = getIconTypeByEntity(entity, token);
return (
<StatisticCard
key={entity.entityId}
statistic={{
title: entity.name,
icon: (
<IconFont type={iconType} style={{ color: color, fontSize: 24 }} />
),
value: entity.active === 1 ? 'Active' : 'Inactive',
}}
/>
);
};
const BinarySensors = ({ nodeConfigs }: BinarySensorsProps) => {
const binarySensors = getBinaryEntities(nodeConfigs);
console.log('BinarySensor: ', binarySensors);
const { token } = theme.useToken();
const { useBreakpoint } = Grid;
const screens = useBreakpoint();
return (
<Flex wrap="wrap">
<StatisticCard.Group direction={screens.sm ? 'row' : 'column'}>
{binarySensors.map((entity) => StatisticCardItem(entity, token))}
</StatisticCard.Group>
</Flex>
);
};
export default BinarySensors;

View File

@@ -1,10 +1,13 @@
import DeviceAlarmList from '@/components/shared/DeviceAlarmList';
import TooltipIconFontButton from '@/components/shared/TooltipIconFontButton'; import TooltipIconFontButton from '@/components/shared/TooltipIconFontButton';
import { ROUTER_HOME } from '@/constants/routes'; import { ROUTER_HOME } from '@/constants/routes';
import { apiQueryMessage } from '@/services/master/MessageController'; import { apiQueryNodeConfigMessage } from '@/services/master/MessageController';
import { apiGetThingDetail } from '@/services/master/ThingController'; import { apiGetThingDetail } from '@/services/master/ThingController';
import { PageContainer } from '@ant-design/pro-components'; import { PageContainer, ProCard } from '@ant-design/pro-components';
import { history, useModel, useParams } from '@umijs/max'; import { history, useIntl, useModel, useParams } from '@umijs/max';
import { Grid } from 'antd';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import BinarySensors from './components/BinarySensors';
import ThingTitle from './components/ThingTitle'; import ThingTitle from './components/ThingTitle';
const DetailDevicePage = () => { const DetailDevicePage = () => {
@@ -12,6 +15,10 @@ const DetailDevicePage = () => {
const [thing, setThing] = useState<MasterModel.Thing | null>(null); const [thing, setThing] = useState<MasterModel.Thing | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const { initialState } = useModel('@@initialState'); const { initialState } = useModel('@@initialState');
const { useBreakpoint } = Grid;
const screens = useBreakpoint();
const intl = useIntl();
const [nodeConfigs, setNodeConfigs] = useState<MasterModel.NodeConfig[]>([]);
const getThingDetail = async () => { const getThingDetail = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
@@ -25,7 +32,7 @@ const DetailDevicePage = () => {
}; };
const getDeviceConfig = async () => { const getDeviceConfig = async () => {
try { try {
const resp = await apiQueryMessage( const resp = await apiQueryNodeConfigMessage(
thing?.metadata?.data_channel_id || '', thing?.metadata?.data_channel_id || '',
initialState?.currentUserProfile?.metadata?.frontend_thing_key || '', initialState?.currentUserProfile?.metadata?.frontend_thing_key || '',
{ {
@@ -34,7 +41,11 @@ const DetailDevicePage = () => {
subtopic: `config.${thing?.metadata?.type}.node`, subtopic: `config.${thing?.metadata?.type}.node`,
}, },
); );
console.log('Device Config:', resp.messages![0].string_value_parsed); if (resp.messages && resp.messages.length > 0) {
console.log('Node Configs: ', resp.messages[0].string_value_parsed);
setNodeConfigs(resp.messages[0].string_value_parsed ?? []);
}
} catch (error) {} } catch (error) {}
}; };
useEffect(() => { useEffect(() => {
@@ -82,7 +93,21 @@ const DetailDevicePage = () => {
/>, />,
]} ]}
> >
Thing ID: {thingId} <ProCard split={screens.md ? 'vertical' : 'horizontal'}>
<ProCard
title={intl.formatMessage({
id: 'master.thing.detail.alarmList.title',
})}
colSpan={{ xs: 24, sm: 24, md: 24, lg: 6, xl: 6 }}
bodyStyle={{ paddingInline: 0, paddingBlock: 0 }}
bordered
>
<DeviceAlarmList key="thing-alarms-key" thingId={thingId || ''} />
</ProCard>
<ProCard>
<BinarySensors nodeConfigs={nodeConfigs} />
</ProCard>
</ProCard>
</PageContainer> </PageContainer>
); );
}; };

View File

@@ -111,21 +111,20 @@ export function transformRawNodeConfig(
}; };
} }
export async function apiQueryMessage( export async function apiQueryNodeConfigMessage(
dataChanelId: string, dataChanelId: string,
authorization: string, authorization: string,
params: MasterModel.SearchMessagePaginationBody, params: MasterModel.SearchMessagePaginationBody,
) { ) {
const resp = await request<MasterModel.MesageReaderResponse>( const resp = await request<
`${API_READER}/${dataChanelId}/messages`, MasterModel.MesageReaderResponse<MasterModel.NodeConfig[]>
{ >(`${API_READER}/${dataChanelId}/messages`, {
method: 'GET', method: 'GET',
headers: { headers: {
Authorization: authorization, Authorization: authorization,
}, },
params: params, params: params,
}, });
);
// Process messages to add string_value_parsed // Process messages to add string_value_parsed
if (resp.messages) { if (resp.messages) {

View File

@@ -8,7 +8,7 @@ declare namespace MasterModel {
type LogTypeRequest = 'user_logs' | undefined; type LogTypeRequest = 'user_logs' | undefined;
interface MesageReaderResponse { interface MesageReaderResponse<T = MessageDataType> {
offset?: number; offset?: number;
limit?: number; limit?: number;
publisher?: string; publisher?: string;
@@ -16,10 +16,17 @@ declare namespace MasterModel {
to?: number; to?: number;
format?: string; format?: string;
total?: number; total?: number;
messages?: Message[]; messages?: Message<T>[];
} }
interface Message { // Response types cho từng domain
type CameraMessageResponse = MesageReaderResponse<CameraV5>;
type CameraV6MessageResponse = MesageReaderResponse<CameraV6>;
type NodeConfigMessageResponse = MesageReaderResponse<NodeConfig[]>;
type MessageDataType = NodeConfig[] | CameraV5 | CameraV6;
interface Message<T = MessageDataType> {
channel?: string; channel?: string;
subtopic?: string; subtopic?: string;
publisher?: string; publisher?: string;
@@ -27,6 +34,32 @@ declare namespace MasterModel {
name?: string; name?: string;
time?: number; time?: number;
string_value?: string; string_value?: string;
string_value_parsed?: NodeConfig[]; string_value_parsed?: T;
}
// Message types cho từng domain
type CameraMessage = Message<CameraV5>;
type CameraV6Message = Message<CameraV6>;
type NodeConfigMessage = Message<NodeConfig[]>;
interface CameraV5 {
cams?: Camera[];
}
interface CameraV6 extends CameraV5 {
record_type?: string;
record_alarm_list?: string[];
}
interface Camera {
id?: string;
name?: string;
cate_id?: string;
username?: string;
password?: string;
rtsp_port?: number;
http_port?: number;
channel?: number;
ip?: string;
stream?: number;
} }
} }