diff --git a/src/app.tsx b/src/app.tsx index 756869b..df3b495 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -86,7 +86,7 @@ export const layout: RunTimeLayoutConfig = ({ initialState }) => { contentWidth: 'Fluid', navTheme: isDark ? 'realDark' : 'light', splitMenus: true, - iconfontUrl: '//at.alicdn.com/t/c/font_5096559_gjd5149497o.js', + iconfontUrl: '//at.alicdn.com/t/c/font_5096559_qeg2471go4g.js', contentStyle: { padding: 0, margin: 0, diff --git a/src/components/IconFont/index.tsx b/src/components/IconFont/index.tsx index 5b854c0..1176697 100644 --- a/src/components/IconFont/index.tsx +++ b/src/components/IconFont/index.tsx @@ -1,7 +1,7 @@ import { createFromIconfontCN } from '@ant-design/icons'; 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; diff --git a/src/components/shared/Alarm/AlarmUnConfirmButton.tsx b/src/components/shared/Alarm/AlarmUnConfirmButton.tsx new file mode 100644 index 0000000..18b72e4 --- /dev/null +++ b/src/components/shared/Alarm/AlarmUnConfirmButton.tsx @@ -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 ( + { + 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 + ) : ( + + ), + ], + }, + }; + 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={ - + React.isValidElement(trigger) ? ( + trigger + ) : ( + + ) } > 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(); }} - > - - + /> ) : ( { 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; } }