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:
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
82
src/components/shared/Alarm/AlarmUnConfirmButton.tsx
Normal file
82
src/components/shared/Alarm/AlarmUnConfirmButton.tsx
Normal 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;
|
||||||
158
src/components/shared/DeviceAlarmList.tsx
Normal file
158
src/components/shared/DeviceAlarmList.tsx
Normal 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;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
1
src/locales/en-US/master/master-thing-detail-en.ts
Normal file
1
src/locales/en-US/master/master-thing-detail-en.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export default { 'master.thing.detail.alarmList.title': 'Alarm List' };
|
||||||
3
src/locales/vi-VN/master/master-thing-detail-vi.ts
Normal file
3
src/locales/vi-VN/master/master-thing-detail-vi.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
'master.thing.detail.alarmList.title': 'Danh sách cảnh báo',
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
@@ -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={
|
||||||
<Button
|
React.isValidElement(trigger) ? (
|
||||||
size="small"
|
trigger
|
||||||
variant="solid"
|
) : (
|
||||||
color="green"
|
<Button
|
||||||
icon={<CheckOutlined />}
|
size="small"
|
||||||
onClick={() => setModalVisit(true)}
|
type="primary"
|
||||||
>
|
icon={<CheckOutlined />}
|
||||||
{intl.formatMessage({
|
onClick={() => setIsOpen(true)}
|
||||||
id: 'common.confirm',
|
>
|
||||||
})}
|
{intl.formatMessage({
|
||||||
</Button>
|
id: 'common.confirm',
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ProForm.Group>
|
<ProForm.Group>
|
||||||
|
|||||||
@@ -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?',
|
<Button danger icon={<DeleteOutlined />} size="small">
|
||||||
})}
|
<FormattedMessage id="master.alarms.unconfirm.title" />
|
||||||
okText={intl.formatMessage({
|
</Button>
|
||||||
id: 'common.yes',
|
}
|
||||||
defaultMessage: 'Yes',
|
onFinish={(isReload) => {
|
||||||
})}
|
if (isReload) tableRef.current?.reload();
|
||||||
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">
|
|
||||||
<FormattedMessage id="master.alarms.unconfirm.title" />
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
) : (
|
) : (
|
||||||
<AlarmFormConfirm
|
<AlarmFormConfirm
|
||||||
|
isOpen={isConfirmModalOpen}
|
||||||
|
setIsOpen={setIsConfirmModalOpen}
|
||||||
alarm={alarm}
|
alarm={alarm}
|
||||||
message={messageApi}
|
message={messageApi}
|
||||||
onFinish={(isReload) => {
|
onFinish={(isReload) => {
|
||||||
|
|||||||
105
src/pages/Manager/Device/Detail/components/BinarySensors.tsx
Normal file
105
src/pages/Manager/Device/Detail/components/BinarySensors.tsx
Normal 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;
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
41
src/services/master/typings/log.d.ts
vendored
41
src/services/master/typings/log.d.ts
vendored
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user