= {
+ title: {
+ dataIndex: 'name',
+ render(_, item) {
+ return (
+
+ {item.name}
+
+ );
+ },
+ },
+ avatar: {
+ render: (_, item) => getBadgeStatus(item.level || 0),
+ },
+ description: {
+ dataIndex: 'time',
+ render: (_, item) => {
+ return (
+ <>
+ {item.confirmed ? (
+
+ ) : (
+ {moment.unix(item?.time || 0).format(DATE_TIME_FORMAT)}
+ )}
+ >
+ );
+ },
+ },
+ actions: {
+ render: (_, entity) => [
+ entity.confirmed ? (
+ {
+ if (isReload) actionRef.current?.reload();
+ }}
+ />
+ ) : (
+ }
+ type="dashed"
+ className="green-btn"
+ style={{ color: 'green', borderColor: 'green' }}
+ size="small"
+ onClick={() => setAlarmConfirmed(entity)}
+ >
+ ),
+ ],
+ },
+ };
+ return (
+ <>
+ {contextHolder}
+ {
+ if (!v) setAlarmConfirmed(undefined);
+ }}
+ alarm={alarmConfirmed || ({} as MasterModel.Alarm)}
+ trigger={<>>}
+ message={messageApi}
+ onFinish={(isReload) => {
+ if (isReload) actionRef.current?.reload();
+ }}
+ />
+
+ 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;
diff --git a/src/components/shared/ThingShared.tsx b/src/components/shared/ThingShared.tsx
index 3f03470..6fee864 100644
--- a/src/components/shared/ThingShared.tsx
+++ b/src/components/shared/ThingShared.tsx
@@ -4,7 +4,7 @@ import {
STATUS_SOS,
STATUS_WARNING,
} from '@/constants';
-import { Badge } from 'antd';
+import { Badge, GlobalToken } from 'antd';
import IconFont from '../IconFont';
export const getBadgeStatus = (status: number) => {
@@ -30,3 +30,21 @@ export const getBadgeConnection = (online: boolean) => {
return ;
}
};
+
+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;
+ }
+};
diff --git a/src/locales/en-US/master/master-en.ts b/src/locales/en-US/master/master-en.ts
index 379ef04..36e019d 100644
--- a/src/locales/en-US/master/master-en.ts
+++ b/src/locales/en-US/master/master-en.ts
@@ -4,6 +4,7 @@ import masterGroupEn from './master-group-en';
import masterSysLogEn from './master-log-en';
import masterMenuEn from './master-menu-en';
import masterMenuProfileEn from './master-profile-en';
+import masterThingDetailEn from './master-thing-detail-en';
import masterThingEn from './master-thing-en';
import masterUserEn from './master-user-en';
export default {
@@ -16,4 +17,5 @@ export default {
...masterSysLogEn,
...masterUserEn,
...masterGroupEn,
+ ...masterThingDetailEn,
};
diff --git a/src/locales/en-US/master/master-thing-detail-en.ts b/src/locales/en-US/master/master-thing-detail-en.ts
new file mode 100644
index 0000000..3b552db
--- /dev/null
+++ b/src/locales/en-US/master/master-thing-detail-en.ts
@@ -0,0 +1 @@
+export default { 'master.thing.detail.alarmList.title': 'Alarm List' };
diff --git a/src/locales/vi-VN/master/master-thing-detail-vi.ts b/src/locales/vi-VN/master/master-thing-detail-vi.ts
new file mode 100644
index 0000000..78e5782
--- /dev/null
+++ b/src/locales/vi-VN/master/master-thing-detail-vi.ts
@@ -0,0 +1,3 @@
+export default {
+ 'master.thing.detail.alarmList.title': 'Danh sách cảnh báo',
+};
diff --git a/src/locales/vi-VN/master/master-vi.ts b/src/locales/vi-VN/master/master-vi.ts
index fc9a44c..26da0c8 100644
--- a/src/locales/vi-VN/master/master-vi.ts
+++ b/src/locales/vi-VN/master/master-vi.ts
@@ -4,6 +4,7 @@ import masterGroupVi from './master-group-vi';
import masterSysLogVi from './master-log-vi';
import masterMenuVi from './master-menu-vi';
import masterProfileVi from './master-profile-vi';
+import masterThingDetailVi from './master-thing-detail-vi';
import masterThingVi from './master-thing-vi';
import masterUserVi from './master-user-vi';
export default {
@@ -16,4 +17,5 @@ export default {
...masterSysLogVi,
...masterUserVi,
...masterGroupVi,
+ ...masterThingDetailVi,
};
diff --git a/src/pages/Alarm/components/AlarmDescription.tsx b/src/pages/Alarm/components/AlarmDescription.tsx
index 12e55c0..12a4ed9 100644
--- a/src/pages/Alarm/components/AlarmDescription.tsx
+++ b/src/pages/Alarm/components/AlarmDescription.tsx
@@ -4,21 +4,23 @@ import {
UserOutlined,
} from '@ant-design/icons';
import { Space, Typography } from 'antd';
+import { SpaceSize } from 'antd/es/space';
import moment from 'moment';
const { Text } = Typography;
interface AlarmDescriptionProps {
alarm: MasterModel.Alarm;
+ size?: SpaceSize;
}
-const AlarmDescription = ({ alarm }: AlarmDescriptionProps) => {
+const AlarmDescription = ({ alarm, size = 'large' }: AlarmDescriptionProps) => {
if (!alarm?.confirmed) {
return null;
}
return (
-
+
diff --git a/src/pages/Alarm/components/AlarmFormConfirm.tsx b/src/pages/Alarm/components/AlarmFormConfirm.tsx
index b00aee0..ee5948a 100644
--- a/src/pages/Alarm/components/AlarmFormConfirm.tsx
+++ b/src/pages/Alarm/components/AlarmFormConfirm.tsx
@@ -11,7 +11,7 @@ import { FormattedMessage, useIntl } from '@umijs/max';
import { Button, Flex } from 'antd';
import { MessageInstance } from 'antd/es/message/interface';
import moment from 'moment';
-import { useRef, useState } from 'react';
+import React, { useRef } from 'react';
type AlarmForm = {
name: string;
@@ -19,22 +19,26 @@ type AlarmForm = {
description: string;
};
type AlarmFormConfirmProps = {
+ isOpen: boolean;
+ setIsOpen: React.Dispatch>;
+ trigger?: React.ReactNode;
alarm: MasterModel.Alarm;
message: MessageInstance;
onFinish?: (reload: boolean) => void;
};
const AlarmFormConfirm = ({
+ isOpen,
+ setIsOpen,
+ trigger,
alarm,
message,
onFinish,
}: AlarmFormConfirmProps) => {
- const [modalVisit, setModalVisit] = useState(false);
const formRef = useRef>();
const intl = useIntl();
-
return (
- open={modalVisit}
+ open={isOpen}
formRef={formRef}
title={
@@ -45,8 +49,9 @@ const AlarmFormConfirm = ({
layout="horizontal"
modalProps={{
destroyOnHidden: true,
+ // maskStyle: { backgroundColor: 'rgba(0,0,0,0.1)' },
}}
- onOpenChange={setModalVisit}
+ onOpenChange={setIsOpen}
request={async () => {
return {
name: alarm.name ?? '',
@@ -95,17 +100,20 @@ const AlarmFormConfirm = ({
return true;
}}
trigger={
- }
- onClick={() => setModalVisit(true)}
- >
- {intl.formatMessage({
- id: 'common.confirm',
- })}
-
+ React.isValidElement(trigger) ? (
+ trigger
+ ) : (
+ }
+ onClick={() => setIsOpen(true)}
+ >
+ {intl.formatMessage({
+ id: 'common.confirm',
+ })}
+
+ )
}
>
diff --git a/src/pages/Alarm/index.tsx b/src/pages/Alarm/index.tsx
index b0bd733..bc1d0ae 100644
--- a/src/pages/Alarm/index.tsx
+++ b/src/pages/Alarm/index.tsx
@@ -1,9 +1,7 @@
+import AlarmUnConfirmButton from '@/components/shared/Alarm/AlarmUnConfirmButton';
import ThingsFilter from '@/components/shared/ThingFilterModal';
-import { DATE_TIME_FORMAT, DEFAULT_PAGE_SIZE, HTTPSTATUS } from '@/constants';
-import {
- apiGetAlarms,
- apiUnconfirmAlarm,
-} from '@/services/master/AlarmController';
+import { DATE_TIME_FORMAT, DEFAULT_PAGE_SIZE } from '@/constants';
+import { apiGetAlarms } from '@/services/master/AlarmController';
import {
CloseOutlined,
DeleteOutlined,
@@ -11,7 +9,7 @@ import {
} from '@ant-design/icons';
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
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 { useRef, useState } from 'react';
import AlarmDescription from './components/AlarmDescription';
@@ -26,7 +24,7 @@ const AlarmPage = () => {
const [messageApi, contextHolder] = message.useMessage();
const { initialState } = useModel('@@initialState');
const { currentUserProfile } = initialState || {};
-
+ const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false);
const columns: ProColumns[] = [
{
title: intl.formatMessage({
@@ -202,65 +200,22 @@ const AlarmPage = () => {
return (
{alarm?.confirmed || false ? (
- {
- 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',
- }),
- });
- }
+ } size="small">
+
+
+ }
+ onFinish={(isReload) => {
+ if (isReload) tableRef.current?.reload();
}}
- >
- } size="small">
-
-
-
+ />
) : (
{
diff --git a/src/pages/Manager/Device/Detail/components/BinarySensors.tsx b/src/pages/Manager/Device/Detail/components/BinarySensors.tsx
new file mode 100644
index 0000000..a01e507
--- /dev/null
+++ b/src/pages/Manager/Device/Detail/components/BinarySensors.tsx
@@ -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 (
+
+ ),
+ 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 (
+
+
+ {binarySensors.map((entity) => StatisticCardItem(entity, token))}
+
+
+ );
+};
+
+export default BinarySensors;
diff --git a/src/pages/Manager/Device/Detail/index.tsx b/src/pages/Manager/Device/Detail/index.tsx
index 94228ff..7183780 100644
--- a/src/pages/Manager/Device/Detail/index.tsx
+++ b/src/pages/Manager/Device/Detail/index.tsx
@@ -1,10 +1,13 @@
+import DeviceAlarmList from '@/components/shared/DeviceAlarmList';
import TooltipIconFontButton from '@/components/shared/TooltipIconFontButton';
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 { PageContainer } from '@ant-design/pro-components';
-import { history, useModel, useParams } from '@umijs/max';
+import { PageContainer, ProCard } from '@ant-design/pro-components';
+import { history, useIntl, useModel, useParams } from '@umijs/max';
+import { Grid } from 'antd';
import { useEffect, useState } from 'react';
+import BinarySensors from './components/BinarySensors';
import ThingTitle from './components/ThingTitle';
const DetailDevicePage = () => {
@@ -12,6 +15,10 @@ const DetailDevicePage = () => {
const [thing, setThing] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const { initialState } = useModel('@@initialState');
+ const { useBreakpoint } = Grid;
+ const screens = useBreakpoint();
+ const intl = useIntl();
+ const [nodeConfigs, setNodeConfigs] = useState([]);
const getThingDetail = async () => {
setIsLoading(true);
try {
@@ -25,7 +32,7 @@ const DetailDevicePage = () => {
};
const getDeviceConfig = async () => {
try {
- const resp = await apiQueryMessage(
+ const resp = await apiQueryNodeConfigMessage(
thing?.metadata?.data_channel_id || '',
initialState?.currentUserProfile?.metadata?.frontend_thing_key || '',
{
@@ -34,7 +41,11 @@ const DetailDevicePage = () => {
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) {}
};
useEffect(() => {
@@ -82,7 +93,21 @@ const DetailDevicePage = () => {
/>,
]}
>
- Thing ID: {thingId}
+
+
+
+
+
+
+
+
);
};
diff --git a/src/services/master/MessageController.ts b/src/services/master/MessageController.ts
index ed2defd..73c7643 100644
--- a/src/services/master/MessageController.ts
+++ b/src/services/master/MessageController.ts
@@ -111,21 +111,20 @@ export function transformRawNodeConfig(
};
}
-export async function apiQueryMessage(
+export async function apiQueryNodeConfigMessage(
dataChanelId: string,
authorization: string,
params: MasterModel.SearchMessagePaginationBody,
) {
- const resp = await request(
- `${API_READER}/${dataChanelId}/messages`,
- {
- method: 'GET',
- headers: {
- Authorization: authorization,
- },
- params: params,
+ const resp = await request<
+ MasterModel.MesageReaderResponse
+ >(`${API_READER}/${dataChanelId}/messages`, {
+ method: 'GET',
+ headers: {
+ Authorization: authorization,
},
- );
+ params: params,
+ });
// Process messages to add string_value_parsed
if (resp.messages) {
diff --git a/src/services/master/typings/log.d.ts b/src/services/master/typings/log.d.ts
index eae5504..64569bd 100644
--- a/src/services/master/typings/log.d.ts
+++ b/src/services/master/typings/log.d.ts
@@ -8,7 +8,7 @@ declare namespace MasterModel {
type LogTypeRequest = 'user_logs' | undefined;
- interface MesageReaderResponse {
+ interface MesageReaderResponse {
offset?: number;
limit?: number;
publisher?: string;
@@ -16,10 +16,17 @@ declare namespace MasterModel {
to?: number;
format?: string;
total?: number;
- messages?: Message[];
+ messages?: Message[];
}
- interface Message {
+ // Response types cho từng domain
+ type CameraMessageResponse = MesageReaderResponse;
+ type CameraV6MessageResponse = MesageReaderResponse;
+ type NodeConfigMessageResponse = MesageReaderResponse;
+
+ type MessageDataType = NodeConfig[] | CameraV5 | CameraV6;
+
+ interface Message {
channel?: string;
subtopic?: string;
publisher?: string;
@@ -27,6 +34,32 @@ declare namespace MasterModel {
name?: string;
time?: number;
string_value?: string;
- string_value_parsed?: NodeConfig[];
+ string_value_parsed?: T;
+ }
+
+ // Message types cho từng domain
+ type CameraMessage = Message;
+ type CameraV6Message = Message;
+ type NodeConfigMessage = Message;
+
+ 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;
}
}