From e5f923a6e7e467a56f884f98126ee254c6bacb38 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Mon, 5 Aug 2024 17:45:51 +0530 Subject: [PATCH 01/16] Switch to using newer camera feed component and remove old components and utilities --- src/Common/hooks/useFeedPTZ.ts | 211 ------ src/Common/hooks/useMSEplayer.ts | 14 - .../Assets/configure/CameraConfigure.tsx | 35 +- .../Facility/Consultations/Feed.tsx | 704 ------------------ .../Facility/Consultations/LiveFeed.tsx | 674 ----------------- 5 files changed, 13 insertions(+), 1625 deletions(-) delete mode 100644 src/Common/hooks/useFeedPTZ.ts delete mode 100644 src/Components/Facility/Consultations/Feed.tsx delete mode 100644 src/Components/Facility/Consultations/LiveFeed.tsx diff --git a/src/Common/hooks/useFeedPTZ.ts b/src/Common/hooks/useFeedPTZ.ts deleted file mode 100644 index a393edc5922..00000000000 --- a/src/Common/hooks/useFeedPTZ.ts +++ /dev/null @@ -1,211 +0,0 @@ -/** - * Deprecated. Use `useOperateAsset` instead. - * - * Preserving for backwards compatibility and preventing merge conflict with a - * co-related PR. Will be removed in the future. - */ - -import { operateAsset } from "../../Redux/actions"; - -export interface IAsset { - id: string; -} - -interface PTZPayload { - x: number; - y: number; - zoom: number; -} - -export interface PTZState { - x: number; - y: number; - zoom: number; - precision: number; -} - -interface UseMSEMediaPlayerOption { - config: IAsset; - dispatch: any; -} - -export interface ICameraAssetState { - id: string; - username: string; - password: string; - hostname: string; - port: number; -} - -export enum StreamStatus { - Playing, - Stop, - Loading, - Offline, -} - -interface UseMSEMediaPlayerReturnType { - absoluteMove: (payload: PTZPayload, options: IOptions) => void; - relativeMove: (payload: PTZPayload, options: IOptions) => void; - getPTZPayload: ( - action: PTZ, - precision?: number, - value?: number, - ) => PTZPayload; - getCameraStatus: (options: IOptions) => void; - getPresets: (options: IOptions) => void; - gotoPreset: (payload: IGotoPresetPayload, options: IOptions) => void; -} - -interface IOptions { - onSuccess?: (resp: Record) => void; - onError?: (resp: Record) => void; -} - -export enum PTZ { - Up = "up", - Down = "down", - Left = "left", - Right = "right", - ZoomIn = "zoomIn", - ZoomOut = "zoomOut", -} - -const getCameraStatus = - (config: IAsset, dispatch: any) => - async (options: IOptions = {}) => { - if (!config.id) return; - const resp = await dispatch( - operateAsset(config.id, { - action: { - type: "get_status", - }, - }), - ); - resp && - (resp.status === 200 - ? options?.onSuccess && options.onSuccess(resp.data.result) - : options?.onError && options.onError(resp)); - }; - -const getPresets = - (config: IAsset, dispatch: any) => - async (options: IOptions = {}) => { - if (!config.id) return; - const resp = await dispatch( - operateAsset(config.id, { - action: { - type: "get_presets", - }, - }), - ); - resp && - (resp.status === 200 - ? options?.onSuccess && options.onSuccess(resp.data.result) - : options?.onError && options.onError(resp)); - }; - -interface IGotoPresetPayload { - preset: string; -} - -const gotoPreset = - (config: IAsset, dispatch: any) => - async (payload: IGotoPresetPayload, options: IOptions = {}) => { - if (!config.id) return; - const resp = await dispatch( - operateAsset(config.id, { - action: { - type: "goto_preset", - data: payload, - }, - }), - ); - resp && - (resp.status === 200 - ? options?.onSuccess && options.onSuccess(resp.data.result) - : options?.onError && options.onError(resp)); - }; - -const absoluteMove = - (config: IAsset, dispatch: any) => - async (payload: PTZPayload, options: IOptions = {}) => { - if (!config.id) return; - const resp = await dispatch( - operateAsset(config.id, { - action: { - type: "absolute_move", - data: payload, - }, - }), - ); - resp && - (resp.status === 200 - ? options?.onSuccess && options.onSuccess(resp.data.result) - : options?.onError && options.onError(resp)); - }; - -const relativeMove = - (config: IAsset, dispatch: any) => - async (payload: PTZPayload, options: IOptions = {}) => { - if (!config.id) return; - const resp = await dispatch( - operateAsset(config.id, { - action: { - type: "relative_move", - data: payload, - }, - }), - ); - resp && - (resp.status === 200 - ? options?.onSuccess && options.onSuccess(resp.data.result) - : options?.onError && options.onError(resp)); - }; - -export const getPTZPayload = ( - action: PTZ, - precision = 1, - value?: number, -): PTZPayload => { - let x = 0; - let y = 0; - let zoom = 0; - const delta = !value ? 0.1 / Math.max(1, precision) : value; - switch (action) { - case PTZ.Up: - y = delta; - break; - case PTZ.Down: - y = -delta; - break; - case PTZ.Left: - x = -delta; - break; - case PTZ.Right: - x = delta; - break; - case PTZ.ZoomIn: - zoom = delta; - break; - case PTZ.ZoomOut: - zoom = -delta; - break; - } - - return { x, y, zoom }; -}; - -export const useFeedPTZ = ({ - config, - dispatch, -}: UseMSEMediaPlayerOption): UseMSEMediaPlayerReturnType => { - return { - absoluteMove: absoluteMove(config, dispatch), - relativeMove: relativeMove(config, dispatch), - getPTZPayload, - getCameraStatus: getCameraStatus(config, dispatch), - getPresets: getPresets(config, dispatch), - gotoPreset: gotoPreset(config, dispatch), - }; -}; diff --git a/src/Common/hooks/useMSEplayer.ts b/src/Common/hooks/useMSEplayer.ts index 5271c08fd56..6074a7fdc48 100644 --- a/src/Common/hooks/useMSEplayer.ts +++ b/src/Common/hooks/useMSEplayer.ts @@ -10,20 +10,6 @@ interface UseMSEMediaPlayerOption { videoEl: HTMLVideoElement | null; } -export interface ICameraAssetState { - id: string; - accessKey: string; - middleware_address: string; - location_middleware: string; -} - -export enum StreamStatus { - Playing, - Stop, - Loading, - Offline, -} - interface UseMSEMediaPlayerReturnType { stopStream: (config: { id: string }, options: IOptions) => void; startStream: (options?: IOptions) => void; diff --git a/src/Components/Assets/configure/CameraConfigure.tsx b/src/Components/Assets/configure/CameraConfigure.tsx index 5a8ccd5c184..f92e2d81b7d 100644 --- a/src/Components/Assets/configure/CameraConfigure.tsx +++ b/src/Components/Assets/configure/CameraConfigure.tsx @@ -1,13 +1,13 @@ import { SyntheticEvent } from "react"; import { AssetData } from "../AssetTypes"; -import LiveFeed from "../../Facility/Consultations/LiveFeed"; import { BedSelect } from "../../Common/BedSelect"; import { BedModel } from "../../Facility/models"; -import { getCameraConfig } from "../../../Utils/transformUtils"; import { Submit } from "../../Common/components/ButtonV2"; import TextFormField from "../../Form/FormFields/TextFormField"; import Card from "../../../CAREUI/display/Card"; import { FieldErrorText } from "../../Form/FormFields/FormField"; +import CameraFeed from "../../CameraFeed/CameraFeed"; +import useOperateCamera from "../../CameraFeed/useOperateCamera"; interface CameraConfigureProps { asset: AssetData; @@ -16,22 +16,18 @@ interface CameraConfigureProps { bed: BedModel; newPreset: string; setNewPreset(preset: string): void; - refreshPresetsHash: number; - facilityMiddlewareHostname: string; isLoading: boolean; } -export default function CameraConfigure(props: CameraConfigureProps) { - const { - asset, - addPreset, - setBed, - bed, - isLoading, - newPreset, - setNewPreset, - refreshPresetsHash, - facilityMiddlewareHostname, - } = props; +export default function CameraConfigure({ + asset, + addPreset, + setBed, + bed, + isLoading, + newPreset, + setNewPreset, +}: CameraConfigureProps) { + const { operate, key } = useOperateCamera(asset.id); return (
@@ -76,12 +72,7 @@ export default function CameraConfigure(props: CameraConfigureProps) { - +
); diff --git a/src/Components/Facility/Consultations/Feed.tsx b/src/Components/Facility/Consultations/Feed.tsx deleted file mode 100644 index 317e9d768c7..00000000000 --- a/src/Components/Facility/Consultations/Feed.tsx +++ /dev/null @@ -1,704 +0,0 @@ -import * as Notification from "../../../Utils/Notifications.js"; -import routes from "../../../Redux/api"; -import request from "../../../Utils/request/request"; -import { - CAMERA_STATES, - CameraPTZ, - getCameraPTZ, -} from "../../../Common/constants"; -import { - ICameraAssetState, - StreamStatus, - useMSEMediaPlayer, -} from "../../../Common/hooks/useMSEplayer"; -import { PTZState, useFeedPTZ } from "../../../Common/hooks/useFeedPTZ"; -import { useEffect, useRef, useState } from "react"; - -import CareIcon, { IconName } from "../../../CAREUI/icons/CareIcon.js"; -import FeedButton from "./FeedButton"; -import Loading from "../../Common/Loading"; -import ReactPlayer from "react-player"; -import { classNames } from "../../../Utils/utils"; -import { useDispatch } from "react-redux"; -import { useHLSPLayer } from "../../../Common/hooks/useHLSPlayer"; -import useKeyboardShortcut from "use-keyboard-shortcut"; -import useFullscreen from "../../../Common/hooks/useFullscreen.js"; -import { triggerGoal } from "../../../Integrations/Plausible.js"; -import useAuthUser from "../../../Common/hooks/useAuthUser.js"; -import Spinner from "../../Common/Spinner.js"; -import useQuery from "../../../Utils/request/useQuery.js"; -import { ResolvedMiddleware } from "../../Assets/AssetTypes.js"; - -interface IFeedProps { - facilityId: string; - consultationId: any; -} - -const PATIENT_DEFAULT_PRESET = "Patient View".trim().toLowerCase(); - -export const Feed: React.FC = ({ consultationId }) => { - const dispatch: any = useDispatch(); - - const videoWrapper = useRef(null); - - const [cameraAsset, setCameraAsset] = useState({ - id: "", - accessKey: "", - middleware_address: "", - location_middleware: "", - }); - - const [cameraConfig, setCameraConfig] = useState({}); - const [bedPresets, setBedPresets] = useState([]); - const [bed, setBed] = useState(); - const [precision, setPrecision] = useState(1); - const [cameraState, setCameraState] = useState(null); - const [isFullscreen, setFullscreen] = useFullscreen(); - const [videoStartTime, setVideoStartTime] = useState(null); - const [statusReported, setStatusReported] = useState(false); - const [resolvedMiddleware, setResolvedMiddleware] = - useState(); - const authUser = useAuthUser(); - - useEffect(() => { - if (cameraState) { - setCameraState({ - ...cameraState, - precision: precision, - }); - } - }, [precision]); - - useEffect(() => { - const timeout = setTimeout(() => { - setCameraState({ - ...cameraConfig.position, - precision: cameraState?.precision, - }); - setCamTimeout(0); - }, 5000); - return () => clearTimeout(timeout); - }, [cameraState]); - - const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); - - const liveFeedPlayerRef = useRef(null); - - const { loading: getConsultationLoading } = useQuery(routes.getConsultation, { - pathParams: { id: consultationId }, - onResponse: ({ res, data }) => { - if (res && res.status === 200 && data) { - const consultationBedId = data.current_bed?.bed_object?.id; - if (consultationBedId) { - (async () => { - const { res: listAssetBedsRes, data: listAssetBedsData } = - await request(routes.listAssetBeds, { - query: { - bed: consultationBedId, - }, - }); - setBed(consultationBedId); - const bedAssets: any = { - ...listAssetBedsRes, - data: { - ...listAssetBedsData, - results: listAssetBedsData?.results.filter((asset) => { - return asset?.asset_object?.meta?.asset_type === "CAMERA"; - }), - }, - }; - - if (bedAssets?.data?.results?.length) { - const { camera_access_key } = - bedAssets.data.results[0].asset_object.meta; - const config = camera_access_key.split(":"); - setCameraAsset({ - id: bedAssets.data.results[0].asset_object.id, - accessKey: config[2] || "", - middleware_address: - bedAssets.data.results[0].asset_object?.meta - ?.middleware_hostname, - location_middleware: - bedAssets.data.results[0].asset_object.location_object - ?.middleware_address, - }); - setResolvedMiddleware( - bedAssets.data.results[0].asset_object.resolved_middleware, - ); - setCameraConfig(bedAssets.data.results[0].meta); - setCameraState({ - ...bedAssets.data.results[0].meta.position, - precision: 1, - }); - } - })(); - } - } - }, - }); - - // const [position, setPosition] = useState(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [presets, setPresets] = useState([]); - const [currentPreset, setCurrentPreset] = useState(); - // const [showDefaultPresets, setShowDefaultPresets] = useState(false); - - const [loading, setLoading] = useState(CAMERA_STATES.IDLE); - const [camTimeout, setCamTimeout] = useState(0); - useEffect(() => { - const timeout = setTimeout(() => { - if (cameraState) { - cameraPTZ[5].callback(camTimeout - cameraState.zoom); - setCameraState({ - ...cameraState, - zoom: camTimeout, - }); - } - }, 1000); - return () => clearTimeout(timeout); - }, [camTimeout]); - const [streamStatus, setStreamStatus] = useState( - StreamStatus.Offline, - ); - - const url = !isIOS - ? `wss://${resolvedMiddleware?.hostname}/stream/${cameraAsset?.accessKey}/channel/0/mse?uuid=${cameraAsset?.accessKey}&channel=0` - : `https://${resolvedMiddleware?.hostname}/stream/${cameraAsset?.accessKey}/channel/0/hls/live/index.m3u8?uuid=${cameraAsset?.accessKey}&channel=0`; - - const { - startStream, - // setVideoEl, - } = isIOS - ? // eslint-disable-next-line react-hooks/rules-of-hooks - useHLSPLayer(liveFeedPlayerRef.current as ReactPlayer) - : // eslint-disable-next-line react-hooks/rules-of-hooks - useMSEMediaPlayer({ - config: { - middlewareHostname: resolvedMiddleware?.hostname ?? "", - ...cameraAsset, - }, - url, - videoEl: liveFeedPlayerRef.current as HTMLVideoElement, - }); - - const { - absoluteMove, - getCameraStatus, - getPTZPayload, - getPresets, - relativeMove, - } = useFeedPTZ({ - config: cameraAsset, - dispatch, - }); - - const calculateVideoLiveDelay = () => { - const video = liveFeedPlayerRef.current as HTMLVideoElement; - if (!video || !videoStartTime) return 0; - - const timeDifference = - (new Date().getTime() - videoStartTime.getTime()) / 1000; - - return timeDifference - video.currentTime; - }; - - const getBedPresets = async (asset: any) => { - if (asset.id && bed) { - const { data: bedAssets } = await request(routes.listAssetBeds, { - query: { asset: asset.id, bed }, - }); - setBedPresets(bedAssets?.results); - } - }; - - const startStreamFeed = () => { - startStream({ - onSuccess: () => setStreamStatus(StreamStatus.Playing), - onError: () => { - setStreamStatus(StreamStatus.Offline); - if (!statusReported) { - triggerGoal("Camera Feed Viewed", { - consultationId, - userId: authUser.id, - result: "error", - }); - setStatusReported(true); - } - }, - }); - }; - - useEffect(() => { - if (cameraAsset.id) { - setTimeout(() => { - startStreamFeed(); - }, 1000); - getPresets({ - onSuccess: (resp) => setPresets(resp), - onError: (_) => { - Notification.Error({ - msg: "Fetching presets failed", - }); - }, - }); - getBedPresets(cameraAsset); - } - }, [cameraAsset, resolvedMiddleware?.hostname]); - - useEffect(() => { - let tId: any; - if (streamStatus !== StreamStatus.Playing) { - if (streamStatus !== StreamStatus.Offline) { - setStreamStatus(StreamStatus.Loading); - } - tId = setTimeout(() => { - startStreamFeed(); - }, 5000); - } else if (!statusReported) { - triggerGoal("Camera Feed Viewed", { - consultationId, - userId: authUser.id, - result: "success", - }); - setStatusReported(true); - } - - return () => { - clearTimeout(tId); - }; - }, [startStream, streamStatus]); - - useEffect(() => { - if (!currentPreset && streamStatus === StreamStatus.Playing) { - setLoading(CAMERA_STATES.MOVING.GENERIC); - const preset = - bedPresets?.find( - (preset: any) => - String(preset?.meta?.preset_name).trim().toLowerCase() === - PATIENT_DEFAULT_PRESET, - ) || bedPresets?.[0]; - - if (preset) { - absoluteMove(preset?.meta?.position, { - onSuccess: () => { - setLoading(CAMERA_STATES.IDLE); - setCurrentPreset(preset); - }, - onError: (err: Record) => { - setLoading(CAMERA_STATES.IDLE); - const responseData = err.data.result; - if (responseData.status) { - switch (responseData.status) { - case "error": - if (responseData.error.code === "EHOSTUNREACH") { - Notification.Error({ msg: "Camera is Offline!" }); - } else if (responseData.message) { - Notification.Error({ msg: responseData.message }); - } - break; - case "fail": - responseData.errors && - responseData.errors.map((error: any) => { - Notification.Error({ msg: error.message }); - }); - break; - } - } else { - Notification.Error({ msg: "Unable to connect server!" }); - } - setCurrentPreset(preset); - }, - }); - } else { - setLoading(CAMERA_STATES.IDLE); - } - } - }, [bedPresets, streamStatus]); - - const cameraPTZActionCBs: { - [key: string]: (option: any, value?: any) => void; - } = { - precision: () => { - setPrecision((precision: number) => - precision === 16 ? 1 : precision * 2, - ); - }, - reset: () => { - setStreamStatus(StreamStatus.Loading); - setVideoStartTime(null); - startStream({ - onSuccess: () => setStreamStatus(StreamStatus.Playing), - onError: () => setStreamStatus(StreamStatus.Offline), - }); - }, - fullScreen: () => { - if (isIOS) { - const element = document.querySelector("video"); - if (!element) return; - setFullscreen(true, element as HTMLElement); - return; - } - if (!liveFeedPlayerRef.current) return; - setFullscreen( - !isFullscreen, - videoWrapper.current - ? videoWrapper.current - : (liveFeedPlayerRef.current as HTMLElement), - ); - }, - updatePreset: (option) => { - getCameraStatus({ - onSuccess: async (data) => { - if (currentPreset?.asset_object?.id && data?.position) { - setLoading(option.loadingLabel); - const { res, data: assetBedData } = await request( - routes.partialUpdateAssetBed, - { - body: { - asset: currentPreset.asset_object.id, - bed: currentPreset.bed_object.id, - meta: { - ...currentPreset.meta, - position: data?.position, - }, - }, - pathParams: { external_id: currentPreset?.id }, - }, - ); - if (res && assetBedData && res.status === 200) { - Notification.Success({ msg: "Preset Updated" }); - await getBedPresets(cameraAsset?.id); - getPresets({}); - } - setLoading(CAMERA_STATES.IDLE); - } - }, - }); - }, - other: (option, value) => { - setLoading(option.loadingLabel); - relativeMove(getPTZPayload(option.action, precision, value), { - onSuccess: () => setLoading(CAMERA_STATES.IDLE), - }); - }, - }; - - const cameraPTZ = getCameraPTZ(precision).map((option) => { - const cb = - cameraPTZActionCBs[ - cameraPTZActionCBs[option.action] ? option.action : "other" - ]; - return { ...option, callback: (value?: any) => cb(option, value) }; - }); - - // Voluntarily disabling eslint, since length of `cameraPTZ` is constant and - // hence shall not cause issues. (https://news.ycombinator.com/item?id=24363703) - for (const option of cameraPTZ) { - if (!option.shortcutKey) continue; - // eslint-disable-next-line react-hooks/rules-of-hooks - useKeyboardShortcut(option.shortcutKey, option.callback); - } - - if (getConsultationLoading) return ; - - return ( -
-
-
-

Camera Presets :

-
- {bedPresets?.map((preset: any, index: number) => ( - - ))} -
-
-
-
- {isIOS ? ( - { - setVideoStartTime(() => new Date()); - }} - width="100%" - height="100%" - onBuffer={() => { - const delay = calculateVideoLiveDelay(); - if (delay > 5) { - setStreamStatus(StreamStatus.Loading); - } - }} - onError={(e: any, _: any, hlsInstance: any) => { - if (e === "hlsError") { - const recovered = hlsInstance.recoverMediaError(); - console.log(recovered); - } - }} - onEnded={() => { - setStreamStatus(StreamStatus.Stop); - }} - /> - ) : ( -
- ); -}; - -export const FeedCameraPTZHelpButton = (props: { cameraPTZ: CameraPTZ[] }) => { - const { cameraPTZ } = props; - return ( - - ); -}; diff --git a/src/Components/Facility/Consultations/LiveFeed.tsx b/src/Components/Facility/Consultations/LiveFeed.tsx deleted file mode 100644 index a3647517921..00000000000 --- a/src/Components/Facility/Consultations/LiveFeed.tsx +++ /dev/null @@ -1,674 +0,0 @@ -import { useEffect, useState, useRef, LegacyRef } from "react"; -import { useDispatch } from "react-redux"; -import useKeyboardShortcut from "use-keyboard-shortcut"; -import { - listAssetBeds, - partialUpdateAssetBed, - deleteAssetBed, -} from "../../../Redux/actions"; -import { getCameraPTZ } from "../../../Common/constants"; -import { - StreamStatus, - useMSEMediaPlayer, -} from "../../../Common/hooks/useMSEplayer"; -import { useFeedPTZ } from "../../../Common/hooks/useFeedPTZ"; -import * as Notification from "../../../Utils/Notifications.js"; -import { FeedCameraPTZHelpButton } from "./Feed"; -import { AxiosError } from "axios"; -import { BedSelect } from "../../Common/BedSelect"; -import { BedModel } from "../models"; -import useWindowDimensions from "../../../Common/hooks/useWindowDimensions"; -import CareIcon from "../../../CAREUI/icons/CareIcon"; -import Page from "../../Common/components/Page"; -import ConfirmDialog from "../../Common/ConfirmDialog"; -import { FieldLabel } from "../../Form/FormFields/FormField"; -import useFullscreen from "../../../Common/hooks/useFullscreen"; -import ReactPlayer from "react-player"; -import { isIOS } from "../../../Utils/utils"; -import TextFormField from "../../Form/FormFields/TextFormField"; - -const LiveFeed = (props: any) => { - const middlewareHostname = props.middlewareHostname; - const [presetsPage, setPresetsPage] = useState(0); - const cameraAsset = props.asset; - const [presets, setPresets] = useState([]); - const [bedPresets, setBedPresets] = useState([]); - const [showDefaultPresets, setShowDefaultPresets] = useState(false); - const [precision, setPrecision] = useState(1); - const [streamStatus, setStreamStatus] = useState( - StreamStatus.Offline, - ); - const [videoStartTime, setVideoStartTime] = useState(null); - const [bed, setBed] = useState({}); - const [presetName, setPresetName] = useState(""); - const [loading, setLoading] = useState(); - const dispatch: any = useDispatch(); - const [page, setPage] = useState({ - count: 0, - limit: 8, - offset: 0, - }); - const [toDelete, setToDelete] = useState(null); - const [toUpdate, setToUpdate] = useState(null); - const [_isFullscreen, setFullscreen] = useFullscreen(); - - const { width } = useWindowDimensions(); - const extremeSmallScreenBreakpoint = 320; - const isExtremeSmallScreen = - width <= extremeSmallScreenBreakpoint ? true : false; - const liveFeedPlayerRef = useRef(null); - - const videoEl = liveFeedPlayerRef.current as HTMLVideoElement; - - const streamUrl = isIOS - ? `https://${middlewareHostname}/stream/${cameraAsset?.accessKey}/channel/0/hls/live/index.m3u8?uuid=${cameraAsset?.accessKey}&channel=0` - : `wss://${middlewareHostname}/stream/${cameraAsset?.accessKey}/channel/0/mse?uuid=${cameraAsset?.accessKey}&channel=0`; - - const { startStream } = useMSEMediaPlayer({ - config: { - middlewareHostname, - ...cameraAsset, - }, - url: streamUrl, - videoEl, - }); - - const refreshPresetsHash = props.refreshPresetsHash; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [currentPreset, setCurrentPreset] = useState(); - const { - absoluteMove, - getCameraStatus, - getPTZPayload, - getPresets, - gotoPreset, - relativeMove, - } = useFeedPTZ({ - config: { - middlewareHostname, - ...cameraAsset, - }, - dispatch, - }); - - const fetchCameraPresets = () => - getPresets({ - onSuccess: (resp) => { - setPresets(resp); - }, - onError: (resp) => { - resp instanceof AxiosError && - Notification.Error({ - msg: "Camera is offline", - }); - }, - }); - - const calculateVideoLiveDelay = () => { - const video = liveFeedPlayerRef.current as HTMLVideoElement; - if (!video || !videoStartTime) return 0; - - const timeDifference = - (new Date().getTime() - videoStartTime.getTime()) / 1000; - - return timeDifference - video.currentTime; - }; - - const getBedPresets = async (id: any) => { - const bedAssets = await dispatch( - listAssetBeds({ - asset: id, - limit: page.limit, - offset: page.offset, - }), - ); - setBedPresets(bedAssets?.data?.results); - setPage({ - ...page, - count: bedAssets?.data?.count, - }); - }; - - const deletePreset = async (id: any) => { - const res = await dispatch(deleteAssetBed(id)); - if (res?.status === 204) { - Notification.Success({ msg: "Preset deleted successfully" }); - getBedPresets(cameraAsset.id); - } else { - Notification.Error({ - msg: "Error while deleting Preset: " + (res?.data?.detail || ""), - }); - } - setToDelete(null); - }; - - const updatePreset = async (currentPreset: any) => { - const data = { - bed_id: bed.id, - preset_name: presetName, - }; - const response = await dispatch( - partialUpdateAssetBed( - { - asset: currentPreset.asset_object.id, - bed: bed.id, - meta: { - ...currentPreset.meta, - ...data, - }, - }, - currentPreset?.id, - ), - ); - if (response && response.status === 200) { - Notification.Success({ msg: "Preset Updated" }); - } else { - Notification.Error({ msg: "Something Went Wrong" }); - } - getBedPresets(cameraAsset?.id); - fetchCameraPresets(); - setToUpdate(null); - }; - - const gotoBedPreset = (preset: any) => { - setLoading("Moving"); - absoluteMove(preset.meta.position, { - onSuccess: () => setLoading(undefined), - }); - }; - - useEffect(() => { - if (cameraAsset?.hostname) { - fetchCameraPresets(); - setTimeout(() => { - startStreamFeed(); - }, 1000); - } - }, []); - - useEffect(() => { - setPresetName(toUpdate?.meta?.preset_name); - setBed(toUpdate?.bed_object); - }, [toUpdate]); - - useEffect(() => { - getBedPresets(cameraAsset.id); - if (bedPresets?.[0]?.position) { - absoluteMove(bedPresets[0]?.position, {}); - } - }, [page.offset, cameraAsset.id, refreshPresetsHash]); - - const startStreamFeed = () => { - startStream({ - onSuccess: () => setStreamStatus(StreamStatus.Playing), - onError: () => setStreamStatus(StreamStatus.Offline), - }); - }; - - const viewOptions = (page: number) => { - return presets - ? Object.entries(presets) - .map(([key, value]) => ({ label: key, value })) - .slice(page, page + 10) - : Array.from(Array(10), (_, i) => ({ - label: "Monitor " + (i + 1), - value: i + 1, - })); - }; - useEffect(() => { - let tId: any; - if (streamStatus !== StreamStatus.Playing) { - setStreamStatus(StreamStatus.Loading); - tId = setTimeout(() => { - startStreamFeed(); - }, 5000); - } - - return () => { - clearTimeout(tId); - }; - }, [startStream, streamStatus]); - - const handlePagination = (cOffset: number) => { - setPage({ - ...page, - offset: cOffset, - }); - }; - - const cameraPTZActionCBs: { [key: string]: (option: any) => void } = { - precision: () => { - setPrecision((precision: number) => - precision === 16 ? 1 : precision * 2, - ); - }, - reset: () => { - setStreamStatus(StreamStatus.Loading); - setVideoStartTime(null); - startStream({ - onSuccess: () => setStreamStatus(StreamStatus.Playing), - onError: () => setStreamStatus(StreamStatus.Offline), - }); - }, - fullScreen: () => { - if (!liveFeedPlayerRef.current) return; - setFullscreen(true, liveFeedPlayerRef.current); - }, - updatePreset: (option) => { - getCameraStatus({ - onSuccess: async (data) => { - console.log({ currentPreset, data }); - if (currentPreset?.asset_object?.id && data?.position) { - setLoading(option.loadingLabel); - console.log("Updating Preset"); - const response = await dispatch( - partialUpdateAssetBed( - { - asset: currentPreset.asset_object.id, - bed: currentPreset.bed_object.id, - meta: { - ...currentPreset.meta, - position: data?.position, - }, - }, - currentPreset?.id, - ), - ); - if (response && response.status === 200) { - Notification.Success({ msg: "Preset Updated" }); - getBedPresets(cameraAsset?.id); - fetchCameraPresets(); - } - setLoading(undefined); - } - }, - }); - }, - other: (option) => { - setLoading(option.loadingLabel); - relativeMove(getPTZPayload(option.action, precision), { - onSuccess: () => setLoading(undefined), - }); - }, - }; - - const cameraPTZ = getCameraPTZ(precision).map((option) => { - const cb = - cameraPTZActionCBs[ - cameraPTZActionCBs[option.action] ? option.action : "other" - ]; - return { ...option, callback: () => cb(option) }; - }); - - // Voluntarily disabling eslint, since length of `cameraPTZ` is constant and - // hence shall not cause issues. (https://news.ycombinator.com/item?id=24363703) - for (const option of cameraPTZ) { - if (!option.shortcutKey) continue; - // eslint-disable-next-line react-hooks/rules-of-hooks - useKeyboardShortcut(option.shortcutKey, option.callback); - } - - return ( - - {toDelete && ( - -

- Preset: {toDelete.meta.preset_name} -

-

- Bed: {toDelete.bed_object.name} -

- - } - action="Delete" - variant="danger" - onClose={() => setToDelete(null)} - onConfirm={() => deletePreset(toDelete.id)} - /> - )} - {toUpdate && ( - setToUpdate(null)} - onConfirm={() => updatePreset(toUpdate)} - > -
- setPresetName(value)} - /> -
- Bed - setBed(selected as BedModel)} - selected={bed} - error="" - multiple={false} - location={cameraAsset.location_id} - facility={cameraAsset.facility_id} - /> -
-
-
- )} -
-
-
- {/* ADD VIDEO PLAYER HERE */} -
- {isIOS ? ( -
- } - controls={false} - playsinline - playing - muted - width="100%" - height="100%" - onPlay={() => { - setVideoStartTime(() => new Date()); - setStreamStatus(StreamStatus.Playing); - }} - onWaiting={() => { - const delay = calculateVideoLiveDelay(); - if (delay > 5) { - setStreamStatus(StreamStatus.Loading); - } - }} - onError={(e, _, hlsInstance) => { - if (e === "hlsError") { - const recovered = hlsInstance.recoverMediaError(); - console.info(recovered); - } - }} - /> -
- ) : ( - - )} - - {streamStatus === StreamStatus.Playing && - calculateVideoLiveDelay() > 3 && ( -
- - Slow Network Detected -
- )} - - {loading && ( -
-
-
-

{loading}

-
-
- )} - {/* { streamStatus > 0 && */} -
- {streamStatus === StreamStatus.Offline && ( -
-

- STATUS: OFFLINE -

-

- Feed is currently not live. -

-

- Click refresh button to try again. -

-
- )} - {streamStatus === StreamStatus.Stop && ( -
-

- STATUS: STOPPED -

-

Feed is Stooped.

-

- Click refresh button to start feed. -

-
- )} - {streamStatus === StreamStatus.Loading && ( -
-

- STATUS: LOADING -

-

- Fetching latest feed. -

-
- )} -
-
-
- {cameraPTZ.map((option) => { - const shortcutKeyDescription = - option.shortcutKey && - option.shortcutKey - .join(" + ") - .replace("Control", "Ctrl") - .replace("ArrowUp", "↑") - .replace("ArrowDown", "↓") - .replace("ArrowLeft", "←") - .replace("ArrowRight", "→"); - - return ( - - ); - })} -
- -
-
-
- -
- -
-
- {showDefaultPresets ? ( - <> - {viewOptions(presetsPage)?.map((option: any, i) => ( - - ))} - - ) : ( - <> - {bedPresets?.map((preset: any, index: number) => ( -
- -
- - -
-
- ))} - - )} -
- {/* Page Number Next and Prev buttons */} - {showDefaultPresets ? ( -
- - -
- ) : ( -
- - -
- )} - {props?.showRefreshButton && ( - - )} -
-
-
-
- - ); -}; - -export default LiveFeed; From 24b41d109ee0ec70685a5bd04eb5784a247979da Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Mon, 5 Aug 2024 17:47:27 +0530 Subject: [PATCH 02/16] remove unused code --- src/Components/Assets/AssetType/ONVIFCamera.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Components/Assets/AssetType/ONVIFCamera.tsx b/src/Components/Assets/AssetType/ONVIFCamera.tsx index 21beb5f7bf8..58946263c92 100644 --- a/src/Components/Assets/AssetType/ONVIFCamera.tsx +++ b/src/Components/Assets/AssetType/ONVIFCamera.tsx @@ -39,9 +39,6 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { const [newPreset, setNewPreset] = useState(""); const [loadingAddPreset, setLoadingAddPreset] = useState(false); const [loadingSetConfiguration, setLoadingSetConfiguration] = useState(false); - const [refreshPresetsHash, setRefreshPresetsHash] = useState( - Number(new Date()), - ); const { data: facility, loading } = useQuery(routes.getPermittedFacility, { pathParams: { id: facilityId }, }); @@ -121,7 +118,6 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { }); setBed({}); setNewPreset(""); - setRefreshPresetsHash(Number(new Date())); } else { Notification.Error({ msg: "Something went wrong..!", @@ -217,8 +213,6 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { setNewPreset={setNewPreset} addPreset={addPreset} isLoading={loadingAddPreset} - refreshPresetsHash={refreshPresetsHash} - facilityMiddlewareHostname={resolvedMiddleware?.hostname || ""} /> ) : null}
From 175ee668d724fe2af224d6276ad247c1c652b89d Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Sat, 28 Sep 2024 14:36:10 +0530 Subject: [PATCH 03/16] add camera preset configure page --- .../CameraFeed/CameraPresetsConfigure.tsx | 298 ++++++++++++++++++ src/Components/CameraFeed/routes.ts | 40 +++ .../Facility/ConsultationDetails/index.tsx | 34 +- src/Redux/actions.tsx | 27 -- src/Routers/routes/FacilityLocationRoutes.tsx | 5 + 5 files changed, 361 insertions(+), 43 deletions(-) create mode 100644 src/Components/CameraFeed/CameraPresetsConfigure.tsx diff --git a/src/Components/CameraFeed/CameraPresetsConfigure.tsx b/src/Components/CameraFeed/CameraPresetsConfigure.tsx new file mode 100644 index 00000000000..74fb089a214 --- /dev/null +++ b/src/Components/CameraFeed/CameraPresetsConfigure.tsx @@ -0,0 +1,298 @@ +import { useState } from "react"; +import routes from "../../Redux/api"; +import useQuery from "../../Utils/request/useQuery"; +import { AssetBedModel, AssetData } from "../Assets/AssetTypes"; +import Page from "../Common/components/Page"; +import Loading from "../Common/Loading"; +import CameraFeed from "./CameraFeed"; +import useOperateCamera from "./useOperateCamera"; +import ButtonV2 from "../Common/components/ButtonV2"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import request from "../../Utils/request/request"; +import { FeedRoutes } from "./routes"; +import { classNames } from "../../Utils/utils"; + +type Props = { + locationId: string; +} & ( + | { assetId: string; bedId?: undefined } + | { assetId?: undefined; bedId?: string } + | { assetId: string; bedId: string } +); + +export default function CameraPresetsConfigure(props: Props) { + const [current, setCurrent] = useState(); + + const camerasQuery = useQuery(routes.listAssets, { + query: { location: props.locationId, asset_class: "ONVIF" }, + prefetch: !props.assetId, + }); + + // const bedsQuery = useQuery(routes.listFacilityBeds, { + // query: { location: props.locationId }, + // prefetch: !props.bedId, + // }); + + const assetBedsQuery = useQuery(routes.listAssetBeds, { + query: { + asset: props.assetId, + bed: props.bedId, + }, + onResponse: ({ data }) => setCurrent(data?.results[0]), + }); + + const cameraPresetsQuery = useQuery(FeedRoutes.listPresets, { + pathParams: { assetbed_id: current?.id ?? "" }, + prefetch: !!current?.id, + }); + + if (!assetBedsQuery.data || !camerasQuery.data) { + return ; + } + + const assetBeds = assetBedsQuery.data.results; + + const linkedCameraIds = assetBeds.map((obj) => obj.asset_object.id); + + // TODO: filter using Backend + const camerasNotLinked = camerasQuery.data.results.filter( + ({ id }) => !linkedCameraIds.includes(id), + ); + + return ( + +
+
+
+
Cameras
+
    + {assetBeds.map((assetBed) => { + const isSelected = current?.id === assetBed.id; + return ( +
  • + + {assetBed.asset_object.name} + +
    + setCurrent(assetBed)} + > + + + { + const { res } = await request(routes.deleteAssetBed, { + pathParams: { external_id: assetBed.id }, + }); + + if (res?.ok) { + camerasQuery.refetch(); + assetBedsQuery.refetch(); + } + }} + > + + +
    +
  • + ); + })} + {camerasNotLinked.map((camera) => ( +
  • + + {camera.name} + + { + const { res } = await request(routes.createAssetBed, { + body: { asset: camera.id, bed: props.bedId }, + }); + + if (res?.ok) { + camerasQuery.refetch(); + assetBedsQuery.refetch(); + } + }} + > + + +
  • + ))} +
+
+ + {/* Camera Presets */} +
+ {!!current && ( + <> +
Position Presets
+
    + {cameraPresetsQuery.data?.results.map((preset) => ( +
  • + + {preset.name} + +
    + setCurrent(assetBed)} + > + + + { + // const { res } = await request(routes.deleteAssetBed, { + // pathParams: { external_id: assetBed.id }, + // }); + + // if (res?.ok) { + // camerasQuery.refetch(); + // assetBedsQuery.refetch(); + // } + // }} + > + + +
    +
  • + ))} + + {!cameraPresetsQuery.data?.results.length && ( + No position presets + )} +
+ + )} +
+
+
+ {!current ? ( +
+ +

No camera selected

+

Select a linked camera to preview its feed here

+
+
+ ) : ( + + )} +
+
+
+ ); +} + +const LinkedCameraFeed = (props: { asset: AssetData }) => { + const { operate, key } = useOperateCamera(props.asset.id, true); + const [cameraPresets, setCameraPresets] = useState>(); + // const [search, setSearch] = useState(""); + + return ( +
+
+ { + if (!cameraPresets) { + setCameraPresets(presets); + } + }} + /> +
+ {/*
+
ONVIF Presets
+ setSearch(e.value)} + errorClassName="hidden" + /> +
    + {!!cameraPresets && + Object.keys(cameraPresets).map((key) => ( +
  • + + {key} + +
    + setCurrent(assetBed)} + > + + +
    +
  • + ))} +
+
*/} +
+ ); +}; diff --git a/src/Components/CameraFeed/routes.ts b/src/Components/CameraFeed/routes.ts index aecbdc655fa..bc4b1c0cd98 100644 --- a/src/Components/CameraFeed/routes.ts +++ b/src/Components/CameraFeed/routes.ts @@ -1,4 +1,8 @@ import { Type } from "../../Redux/api"; +import { PaginatedResponse } from "../../Utils/request/types"; +import { WritableOnly } from "../../Utils/types"; +import { AssetBedModel } from "../Assets/AssetTypes"; +import { PerformedByModel } from "../HCX/misc"; import { OperationAction, PTZPayload } from "./useOperateCamera"; export type GetStatusResponse = { @@ -23,6 +27,19 @@ export type GetPresetsResponse = { result: Record; }; +export type CameraPreset = { + readonly id: string; + name: string; + readonly asset_bed_object: AssetBedModel; + position?: { x: number; y: number; z: number }; + boundary?: { x0: number; y0: number; x1: number; y1: number }; + readonly created_by: PerformedByModel; + readonly updated_by: PerformedByModel; + readonly created_date: string; + readonly modified_date: string; + readonly is_migrated: boolean; +}; + export const FeedRoutes = { operateAsset: { path: "/api/v1/asset/{id}/operate_assets/", @@ -32,4 +49,27 @@ export const FeedRoutes = { >(), TBody: Type<{ action: OperationAction }>(), }, + + listPresets: { + path: "/api/v1/assetbed/{assetbed_id}/camera_presets/", + method: "GET", + TRes: Type>(), + }, + createPreset: { + path: "/api/v1/assetbed/{assetbed_id}/camera_presets/", + method: "POST", + TRes: Type(), + TBody: Type>(), + }, + updatePreset: { + path: "/api/v1/assetbed/{assetbed_id}/camera_presets/{id}/", + method: "PATCH", + TRes: Type(), + TBody: Type>>(), + }, + deletePreset: { + path: "/api/v1/assetbed/{assetbed_id}/camera_presets/{id}/", + method: "DELETE", + TRes: Type(), + }, } as const; diff --git a/src/Components/Facility/ConsultationDetails/index.tsx b/src/Components/Facility/ConsultationDetails/index.tsx index 911e62d5627..67e9afa836a 100644 --- a/src/Components/Facility/ConsultationDetails/index.tsx +++ b/src/Components/Facility/ConsultationDetails/index.tsx @@ -3,7 +3,6 @@ import { ConsultationModel } from "../models"; import { getConsultation, getPatient, - listAssetBeds, listShiftRequests, } from "../../../Redux/actions"; import { statusType, useAbortableEffect } from "../../../Common/utils"; @@ -36,7 +35,6 @@ import { ConsultationNeurologicalMonitoringTab } from "./ConsultationNeurologica import ABDMRecordsTab from "../../ABDM/ABDMRecordsTab"; import { ConsultationNutritionTab } from "./ConsultationNutritionTab"; import PatientNotesSlideover from "../PatientNotesSlideover"; -import { AssetBedModel } from "../../Assets/AssetTypes"; import PatientInfoCard from "../../Patient/PatientInfoCard"; import RelativeDateUserMention from "../../Common/RelativeDateUserMention"; import DiagnosesListAccordion from "../../Diagnosis/DiagnosesListAccordion"; @@ -130,20 +128,24 @@ export const ConsultationDetails = (props: any) => { ); } setConsultationData(data); - const assetRes = data?.current_bed?.bed_object?.id - ? await dispatch( - listAssetBeds({ - bed: data?.current_bed?.bed_object?.id, - }), - ) - : null; - const isCameraAttachedRes = - assetRes != null - ? assetRes.data.results.some((asset: AssetBedModel) => { - return asset?.asset_object?.asset_class === "ONVIF"; - }) - : false; - setIsCameraAttached(isCameraAttachedRes); + + setIsCameraAttached( + await (async () => { + const bedId = data?.current_bed?.bed_object?.id; + if (!bedId) { + return false; + } + const { data: assetBeds } = await request(routes.listAssetBeds, { + query: { bed: bedId }, + }); + if (!assetBeds) { + return false; + } + return assetBeds.results.some( + (a) => a.asset_object.asset_class === "ONVIF", + ); + })(), + ); // Get patient data const id = res.data.patient; diff --git a/src/Redux/actions.tsx b/src/Redux/actions.tsx index 00e96e48eef..e267895b810 100644 --- a/src/Redux/actions.tsx +++ b/src/Redux/actions.tsx @@ -1,29 +1,4 @@ import { fireRequest } from "./fireRequest"; - -// asset bed -export const listAssetBeds = (params: object, altKey?: string) => - fireRequest("listAssetBeds", [], params, {}, altKey); - -export const partialUpdateAssetBed = (params: object, asset_id: string) => - fireRequest( - "partialUpdateAssetBed", - [], - { ...params }, - { - external_id: asset_id, - }, - ); - -export const deleteAssetBed = (asset_id: string) => - fireRequest( - "deleteAssetBed", - [], - {}, - { - external_id: asset_id, - }, - ); - // Download Actions export const downloadFacility = () => { return fireRequest("downloadFacility"); @@ -89,5 +64,3 @@ export const downloadResourceRequests = (params: object) => { export const listAssets = (params: object) => fireRequest("listAssets", [], params); -export const operateAsset = (id: string, params: object) => - fireRequest("operateAsset", [], params, { external_id: id }); diff --git a/src/Routers/routes/FacilityLocationRoutes.tsx b/src/Routers/routes/FacilityLocationRoutes.tsx index 5d547ebd466..da455b5fe56 100644 --- a/src/Routers/routes/FacilityLocationRoutes.tsx +++ b/src/Routers/routes/FacilityLocationRoutes.tsx @@ -6,6 +6,7 @@ import LocationManagement from "../../Components/Facility/LocationManagement"; import CentralLiveMonitoring from "../../Components/CameraFeed/CentralLiveMonitoring"; import { AuthorizeUserRoute } from "../../Utils/AuthorizeFor"; import { CameraFeedPermittedUserTypes } from "../../Utils/permissions"; +import CameraPresetsConfigure from "../../Components/CameraFeed/CameraPresetsConfigure"; export default { "/facility/:facilityId/location": ({ facilityId }: any) => ( @@ -38,6 +39,10 @@ export default { }: any) => ( ), + "/facility/:facilityId/location/:locationId/beds/:bedId/cameras": ({ + locationId, + bedId, + }: any) => , "/facility/:facilityId/live-monitoring": (props: any) => ( From f6490390be55d5c25bcf9579c93b285f9fa05855 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Sun, 29 Sep 2024 13:30:43 +0530 Subject: [PATCH 04/16] update asset configure, add camera preset configure for beds --- .../Assets/AssetType/HL7Monitor.tsx | 83 +++++- .../Assets/AssetType/ONVIFCamera.tsx | 68 +---- .../Assets/configure/CameraConfigure.tsx | 79 ----- .../Assets/configure/MonitorConfigure.tsx | 87 ------ .../CameraFeed/CameraFeedWithBedPresets.tsx | 20 +- ...etBedSelect.tsx => CameraPresetSelect.tsx} | 24 +- .../CameraFeed/CameraPresetsConfigure.tsx | 269 ++++++++++++------ src/Components/CameraFeed/routes.ts | 11 +- src/Components/CameraFeed/useOperateCamera.ts | 6 +- .../ConsultationFeedTab.tsx | 88 +++--- 10 files changed, 354 insertions(+), 381 deletions(-) delete mode 100644 src/Components/Assets/configure/CameraConfigure.tsx delete mode 100644 src/Components/Assets/configure/MonitorConfigure.tsx rename src/Components/CameraFeed/{AssetBedSelect.tsx => CameraPresetSelect.tsx} (88%) diff --git a/src/Components/Assets/AssetType/HL7Monitor.tsx b/src/Components/Assets/AssetType/HL7Monitor.tsx index 6583157018d..383af38fa28 100644 --- a/src/Components/Assets/AssetType/HL7Monitor.tsx +++ b/src/Components/Assets/AssetType/HL7Monitor.tsx @@ -1,7 +1,6 @@ import { SyntheticEvent, useEffect, useState } from "react"; -import { AssetData, ResolvedMiddleware } from "../AssetTypes"; +import { AssetClass, AssetData, ResolvedMiddleware } from "../AssetTypes"; import * as Notification from "../../../Utils/Notifications.js"; -import MonitorConfigure from "../configure/MonitorConfigure"; import Loading from "../../Common/Loading"; import { checkIfValidIP } from "../../../Common/validation"; import Card from "../../../CAREUI/display/Card"; @@ -13,6 +12,10 @@ import VentilatorPatientVitalsMonitor from "../../VitalsMonitor/VentilatorPatien import useAuthUser from "../../../Common/hooks/useAuthUser"; import request from "../../../Utils/request/request"; import routes from "../../../Redux/api"; +import { BedModel } from "../../Facility/models"; +import useQuery from "../../../Utils/request/useQuery"; +import { FieldLabel } from "../../Form/FormFields/FormField"; +import { BedSelect } from "../../Common/BedSelect"; interface HL7MonitorProps { assetId: string; @@ -151,3 +154,79 @@ const HL7Monitor = (props: HL7MonitorProps) => { ); }; export default HL7Monitor; + +const saveLink = async (assetId: string, bedId: string) => { + await request(routes.createAssetBed, { + body: { + asset: assetId, + bed: bedId, + }, + }); + Notification.Success({ msg: "AssetBed Link created successfully" }); +}; +const updateLink = async ( + assetbedId: string, + assetId: string, + bed: BedModel, +) => { + await request(routes.partialUpdateAssetBed, { + pathParams: { external_id: assetbedId }, + body: { + asset: assetId, + bed: bed.id ?? "", + }, + }); + Notification.Success({ msg: "AssetBed Link updated successfully" }); +}; + +function MonitorConfigure({ asset }: { asset: AssetData }) { + const [bed, setBed] = useState({}); + const [shouldUpdateLink, setShouldUpdateLink] = useState(false); + const { data: assetBed } = useQuery(routes.listAssetBeds, { + query: { asset: asset.id }, + onResponse: ({ res, data }) => { + if (res?.status === 200 && data && data.results.length > 0) { + setBed(data.results[0].bed_object); + setShouldUpdateLink(true); + } + }, + }); + + return ( +
{ + e.preventDefault(); + if (shouldUpdateLink) { + updateLink( + assetBed?.results[0].id as string, + asset.id as string, + bed as BedModel, + ); + } else { + saveLink(asset.id as string, bed?.id as string); + } + }} + > +
+
+ Bed + setBed(selected as BedModel)} + selected={bed} + error="" + multiple={false} + location={asset?.location_object?.id} + facility={asset?.location_object?.facility?.id} + not_occupied_by_asset_type={AssetClass.HL7MONITOR} + className="w-full" + /> +
+ + + {shouldUpdateLink ? "Update Bed" : "Save Bed"} + +
+
+ ); +} diff --git a/src/Components/Assets/AssetType/ONVIFCamera.tsx b/src/Components/Assets/AssetType/ONVIFCamera.tsx index 6f20f65836c..cad38270cef 100644 --- a/src/Components/Assets/AssetType/ONVIFCamera.tsx +++ b/src/Components/Assets/AssetType/ONVIFCamera.tsx @@ -1,9 +1,7 @@ import { useEffect, useState } from "react"; -import { AssetData, ResolvedMiddleware } from "../AssetTypes"; +import { ResolvedMiddleware } from "../AssetTypes"; import * as Notification from "../../../Utils/Notifications.js"; -import { BedModel } from "../../Facility/models"; import { getCameraConfig } from "../../../Utils/transformUtils"; -import CameraConfigure from "../configure/CameraConfigure"; import Loading from "../../Common/Loading"; import { checkIfValidIP } from "../../../Common/validation"; import TextFormField from "../../Form/FormFields/TextFormField"; @@ -14,9 +12,9 @@ import useAuthUser from "../../../Common/hooks/useAuthUser"; import request from "../../../Utils/request/request"; import routes from "../../../Redux/api"; import useQuery from "../../../Utils/request/useQuery"; - import CareIcon from "../../../CAREUI/icons/CareIcon"; import useOperateCamera from "../../CameraFeed/useOperateCamera"; +import CameraFeed from "../../CameraFeed/CameraFeed"; interface Props { assetId: string; @@ -36,17 +34,12 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [streamUuid, setStreamUuid] = useState(""); - const [bed, setBed] = useState({}); - const [newPreset, setNewPreset] = useState(""); - const [loadingAddPreset, setLoadingAddPreset] = useState(false); const [loadingSetConfiguration, setLoadingSetConfiguration] = useState(false); const { data: facility, loading } = useQuery(routes.getPermittedFacility, { pathParams: { id: facilityId }, }); const authUser = useAuthUser(); - const { operate } = useOperateCamera(assetId ?? "", true); - useEffect(() => { if (asset) { setAssetType(asset?.asset_class); @@ -90,49 +83,18 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { } }; - const addPreset = async (e: SyntheticEvent) => { - e.preventDefault(); - const data = { - bed_id: bed.id, - preset_name: newPreset, - }; - try { - setLoadingAddPreset(true); - - const { data: presetData } = await operate({ type: "get_status" }); + const { operate, key } = useOperateCamera(asset.id); - const { res } = await request(routes.createAssetBed, { - body: { - meta: { ...data, ...presetData }, - asset: assetId, - bed: bed?.id as string, - }, - }); - if (res?.status === 201) { - Notification.Success({ - msg: "Preset Added Successfully", - }); - setBed({}); - setNewPreset(""); - } else { - Notification.Error({ - msg: "Something went wrong..!", - }); - } - } catch (e) { - Notification.Error({ - msg: "Something went wrong..!", - }); - } - setLoadingAddPreset(false); - }; if (isLoading || loading || !facility) return ; return ( -
+
{["DistrictAdmin", "StateAdmin"].includes(authUser.user_type) && ( -
-
+ +
{ )} {assetType === "ONVIF" ? ( - +
+ +
) : null}
); diff --git a/src/Components/Assets/configure/CameraConfigure.tsx b/src/Components/Assets/configure/CameraConfigure.tsx deleted file mode 100644 index f92e2d81b7d..00000000000 --- a/src/Components/Assets/configure/CameraConfigure.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { SyntheticEvent } from "react"; -import { AssetData } from "../AssetTypes"; -import { BedSelect } from "../../Common/BedSelect"; -import { BedModel } from "../../Facility/models"; -import { Submit } from "../../Common/components/ButtonV2"; -import TextFormField from "../../Form/FormFields/TextFormField"; -import Card from "../../../CAREUI/display/Card"; -import { FieldErrorText } from "../../Form/FormFields/FormField"; -import CameraFeed from "../../CameraFeed/CameraFeed"; -import useOperateCamera from "../../CameraFeed/useOperateCamera"; - -interface CameraConfigureProps { - asset: AssetData; - addPreset(e: SyntheticEvent): void; - setBed(bed: BedModel): void; - bed: BedModel; - newPreset: string; - setNewPreset(preset: string): void; - isLoading: boolean; -} -export default function CameraConfigure({ - asset, - addPreset, - setBed, - bed, - isLoading, - newPreset, - setNewPreset, -}: CameraConfigureProps) { - const { operate, key } = useOperateCamera(asset.id); - - return ( -
- - -
-
- - setBed(selected as BedModel)} - selected={bed} - error="" - multiple={false} - location={asset?.location_object?.id} - facility={asset?.location_object?.facility?.id} - /> -
-
- - setNewPreset(e.value)} - errorClassName="hidden" - /> - {newPreset.length > 12 && ( - - )} -
-
-
- -
- -
- - - -
- ); -} diff --git a/src/Components/Assets/configure/MonitorConfigure.tsx b/src/Components/Assets/configure/MonitorConfigure.tsx deleted file mode 100644 index 785b82873de..00000000000 --- a/src/Components/Assets/configure/MonitorConfigure.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { useState } from "react"; -import { BedSelect } from "../../Common/BedSelect"; -import { BedModel } from "../../Facility/models"; -import { AssetClass, AssetData } from "../AssetTypes"; -import * as Notification from "../../../Utils/Notifications.js"; -import { Submit } from "../../Common/components/ButtonV2"; -import { FieldLabel } from "../../Form/FormFields/FormField"; -import request from "../../../Utils/request/request"; -import routes from "../../../Redux/api"; -import useQuery from "../../../Utils/request/useQuery"; -import CareIcon from "../../../CAREUI/icons/CareIcon"; - -const saveLink = async (assetId: string, bedId: string) => { - await request(routes.createAssetBed, { - body: { - asset: assetId, - bed: bedId, - }, - }); - Notification.Success({ msg: "AssetBed Link created successfully" }); -}; -const update_Link = async ( - assetbedId: string, - assetId: string, - bed: BedModel, -) => { - await request(routes.partialUpdateAssetBed, { - pathParams: { external_id: assetbedId }, - body: { - asset: assetId, - bed: bed.id ?? "", - }, - }); - Notification.Success({ msg: "AssetBed Link updated successfully" }); -}; - -export default function MonitorConfigure({ asset }: { asset: AssetData }) { - const [bed, setBed] = useState({}); - const [updateLink, setUpdateLink] = useState(false); - const { data: assetBed } = useQuery(routes.listAssetBeds, { - query: { asset: asset.id }, - onResponse: ({ res, data }) => { - if (res?.status === 200 && data && data.results.length > 0) { - setBed(data.results[0].bed_object); - setUpdateLink(true); - } - }, - }); - - return ( -
{ - e.preventDefault(); - if (updateLink) { - update_Link( - assetBed?.results[0].id as string, - asset.id as string, - bed as BedModel, - ); - } else { - saveLink(asset.id as string, bed?.id as string); - } - }} - > -
-
- Bed - setBed(selected as BedModel)} - selected={bed} - error="" - multiple={false} - location={asset?.location_object?.id} - facility={asset?.location_object?.facility?.id} - not_occupied_by_asset_type={AssetClass.HL7MONITOR} - className="w-full" - /> -
- - - {updateLink ? "Update Bed" : "Save Bed"} - -
-
- ); -} diff --git a/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx b/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx index 7268397b81a..11e5fc3f23e 100644 --- a/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx +++ b/src/Components/CameraFeed/CameraFeedWithBedPresets.tsx @@ -1,32 +1,28 @@ import { useState } from "react"; -import { AssetBedModel, AssetData } from "../Assets/AssetTypes"; +import { AssetData } from "../Assets/AssetTypes"; import CameraFeed from "./CameraFeed"; import useQuery from "../../Utils/request/useQuery"; -import routes from "../../Redux/api"; -import useSlug from "../../Common/hooks/useSlug"; -import { CameraPresetDropdown } from "./AssetBedSelect"; +import { CameraPresetDropdown } from "./CameraPresetSelect"; import useOperateCamera from "./useOperateCamera"; import { classNames } from "../../Utils/utils"; +import { CameraPreset, FeedRoutes } from "./routes"; interface Props { asset: AssetData; } export default function LocationFeedTile(props: Props) { - const facility = useSlug("facility"); - const [preset, setPreset] = useState(); - - const { data, loading } = useQuery(routes.listAssetBeds, { - query: { limit: 100, facility, asset: props.asset?.id }, + const [preset, setPreset] = useState(); + const { operate, key } = useOperateCamera(props.asset.id); + const { data, loading } = useQuery(FeedRoutes.listPresets, { + query: { limit: 100, asset: props.asset.id, position: true }, }); - const { operate, key } = useOperateCamera(props.asset.id, true); - return ( string; - onChange?: (value: AssetBedModel) => void; + options: CameraPreset[]; + value?: CameraPreset; + label?: (value: CameraPreset) => string; + onChange?: (value: CameraPreset) => void; } export default function CameraPresetSelect(props: Props) { @@ -71,16 +70,13 @@ export const CameraPresetDropdown = ( props: Props & { placeholder: string }, ) => { const selected = props.value; - - const options = props.options.filter(({ meta }) => meta.type !== "boundary"); - const label = props.label ?? defaultLabel; return (
- {options.length === 0 + {props.options.length === 0 ? "No presets" : selected ? label(selected) @@ -113,7 +109,7 @@ export const CameraPresetDropdown = ( as="ul" className="absolute z-20 max-h-48 w-full overflow-auto rounded-b-lg bg-white py-1 text-base shadow-lg ring-1 ring-secondary-500 focus:outline-none md:max-h-60" > - {options?.map((obj) => ( + {props.options.map((obj) => ( { - return `${bed_object.name}: ${meta.preset_name}`; +const defaultLabel = (preset: CameraPreset) => { + return `${preset.asset_bed.bed_object.name}: ${preset.name}`; }; diff --git a/src/Components/CameraFeed/CameraPresetsConfigure.tsx b/src/Components/CameraFeed/CameraPresetsConfigure.tsx index 74fb089a214..851f144fa13 100644 --- a/src/Components/CameraFeed/CameraPresetsConfigure.tsx +++ b/src/Components/CameraFeed/CameraPresetsConfigure.tsx @@ -1,48 +1,54 @@ import { useState } from "react"; import routes from "../../Redux/api"; import useQuery from "../../Utils/request/useQuery"; -import { AssetBedModel, AssetData } from "../Assets/AssetTypes"; +import { AssetBedModel } from "../Assets/AssetTypes"; import Page from "../Common/components/Page"; import Loading from "../Common/Loading"; import CameraFeed from "./CameraFeed"; import useOperateCamera from "./useOperateCamera"; -import ButtonV2 from "../Common/components/ButtonV2"; +import ButtonV2, { Submit } from "../Common/components/ButtonV2"; import CareIcon from "../../CAREUI/icons/CareIcon"; import request from "../../Utils/request/request"; -import { FeedRoutes } from "./routes"; +import { CameraPreset, FeedRoutes, GetStatusResponse } from "./routes"; import { classNames } from "../../Utils/utils"; +import { + Listbox, + ListboxButton, + ListboxOption, + ListboxOptions, +} from "@headlessui/react"; +import { dropdownOptionClassNames } from "../Form/MultiSelectMenuV2"; +import DialogModal from "../Common/Dialog"; +import TextFormField from "../Form/FormFields/TextFormField"; +import { Success } from "../../Utils/Notifications"; type Props = { locationId: string; -} & ( - | { assetId: string; bedId?: undefined } - | { assetId?: undefined; bedId?: string } - | { assetId: string; bedId: string } -); + bedId: string; +}; + +type OnvifPreset = { name: string; value: number }; export default function CameraPresetsConfigure(props: Props) { const [current, setCurrent] = useState(); const camerasQuery = useQuery(routes.listAssets, { - query: { location: props.locationId, asset_class: "ONVIF" }, - prefetch: !props.assetId, + query: { + location: props.locationId, + asset_class: "ONVIF", + }, }); - // const bedsQuery = useQuery(routes.listFacilityBeds, { - // query: { location: props.locationId }, - // prefetch: !props.bedId, - // }); - const assetBedsQuery = useQuery(routes.listAssetBeds, { query: { - asset: props.assetId, bed: props.bedId, }, onResponse: ({ data }) => setCurrent(data?.results[0]), }); - const cameraPresetsQuery = useQuery(FeedRoutes.listPresets, { + const cameraPresetsQuery = useQuery(FeedRoutes.listAssetBedPresets, { pathParams: { assetbed_id: current?.id ?? "" }, + query: { position: true }, prefetch: !!current?.id, }); @@ -62,7 +68,7 @@ export default function CameraPresetsConfigure(props: Props) { return (
-
+
Cameras
    @@ -72,13 +78,13 @@ export default function CameraPresetsConfigure(props: Props) {
  • - + {assetBed.asset_object.name}
    @@ -96,8 +102,7 @@ export default function CameraPresetsConfigure(props: Props) { - +
  • @@ -123,7 +128,7 @@ export default function CameraPresetsConfigure(props: Props) { {camerasNotLinked.map((camera) => (
  • {camera.name} @@ -140,18 +145,22 @@ export default function CameraPresetsConfigure(props: Props) { const { res } = await request(routes.createAssetBed, { body: { asset: camera.id, bed: props.bedId }, }); - if (res?.ok) { camerasQuery.refetch(); assetBedsQuery.refetch(); } }} > - +
  • ))}
+ {camerasNotLinked.length === 0 && assetBeds.length === 0 && ( + + No cameras available in this location + + )}
{/* Camera Presets */} @@ -190,16 +199,21 @@ export default function CameraPresetsConfigure(props: Props) { border tooltip="Delete preset" tooltipClassName="tooltip-bottom translate-y-2 -translate-x-1/2 text-xs" - // onClick={async () => { - // const { res } = await request(routes.deleteAssetBed, { - // pathParams: { external_id: assetBed.id }, - // }); - - // if (res?.ok) { - // camerasQuery.refetch(); - // assetBedsQuery.refetch(); - // } - // }} + onClick={async () => { + const { res } = await request( + FeedRoutes.deletePreset, + { + pathParams: { + assetbed_id: current.id, + id: preset.id, + }, + }, + ); + if (res?.ok) { + Success({ msg: "Preset deleted" }); + cameraPresetsQuery.refetch(); + } + }} > @@ -225,8 +239,9 @@ export default function CameraPresetsConfigure(props: Props) {
) : ( cameraPresetsQuery.refetch()} /> )}
@@ -235,64 +250,150 @@ export default function CameraPresetsConfigure(props: Props) { ); } -const LinkedCameraFeed = (props: { asset: AssetData }) => { - const { operate, key } = useOperateCamera(props.asset.id, true); - const [cameraPresets, setCameraPresets] = useState>(); - // const [search, setSearch] = useState(""); +const LinkedCameraFeed = (props: { + assetBed: AssetBedModel; + onPresetCreated: () => void; +}) => { + const { operate, key } = useOperateCamera(props.assetBed.asset_object.id); + const [onvifPresets, setOnvifPresets] = useState(); + const [currentOnvifPreset, setCurrentOnvifPreset] = useState(); + const [createPresetPosition, setCreatePresetPosition] = + useState(); + const [presetName, setPresetName] = useState(""); return (
+ { + setCreatePresetPosition(undefined); + setPresetName(""); + }} + > + setPresetName(value)} + errorClassName="hidden" + placeholder="Specify an identifiable name for the new preset" + /> + {/*
+ {JSON.stringify(createPresetPosition, undefined, " ")} +
*/} +
+ { + const { res } = await request(FeedRoutes.createPreset, { + pathParams: { assetbed_id: props.assetBed.id }, + body: { + name: presetName, + position: createPresetPosition, + }, + }); + if (!res?.ok) { + return; + } + setCreatePresetPosition(undefined); + setPresetName(""); + Success({ msg: "Preset created" }); + props.onPresetCreated(); + }} + disabled={!presetName} + /> +
+
{ - if (!cameraPresets) { - setCameraPresets(presets); + if (!onvifPresets) { + setOnvifPresets( + Object.entries(presets).map(([name, value]) => ({ + name, + value, + })), + ); } }} - /> + > +
+ { + setCurrentOnvifPreset(preset); + operate({ + type: "goto_preset", + data: { + preset: preset.value, + }, + }); + }} + disabled={!onvifPresets?.length} + > +
+ + + {!onvifPresets?.length + ? "No presets" + : (currentOnvifPreset?.name ?? + "Select to move to an ONVIF Preset")} + + + + + + + {onvifPresets?.map((obj) => ( + + classNames( + dropdownOptionClassNames(args), + "px-2 py-1.5", + ) + } + value={obj} + > + {obj.name} + + ))} + +
+
+ { + const { data } = await operate({ type: "get_status" }); + if (data) { + setCreatePresetPosition( + (data as GetStatusResponse).result.position, + ); + } + }} + > + + Create new preset + +
+
- {/*
-
ONVIF Presets
- setSearch(e.value)} - errorClassName="hidden" - /> -
    - {!!cameraPresets && - Object.keys(cameraPresets).map((key) => ( -
  • - - {key} - -
    - setCurrent(assetBed)} - > - - -
    -
  • - ))} -
-
*/}
); }; diff --git a/src/Components/CameraFeed/routes.ts b/src/Components/CameraFeed/routes.ts index bc4b1c0cd98..025bdd52b3b 100644 --- a/src/Components/CameraFeed/routes.ts +++ b/src/Components/CameraFeed/routes.ts @@ -30,8 +30,8 @@ export type GetPresetsResponse = { export type CameraPreset = { readonly id: string; name: string; - readonly asset_bed_object: AssetBedModel; - position?: { x: number; y: number; z: number }; + readonly asset_bed: AssetBedModel; + position?: PTZPayload; boundary?: { x0: number; y0: number; x1: number; y1: number }; readonly created_by: PerformedByModel; readonly updated_by: PerformedByModel; @@ -50,11 +50,16 @@ export const FeedRoutes = { TBody: Type<{ action: OperationAction }>(), }, - listPresets: { + listAssetBedPresets: { path: "/api/v1/assetbed/{assetbed_id}/camera_presets/", method: "GET", TRes: Type>(), }, + listPresets: { + path: "/api/v1/camera_presets/", + method: "GET", + TRes: Type>(), + }, createPreset: { path: "/api/v1/assetbed/{assetbed_id}/camera_presets/", method: "POST", diff --git a/src/Components/CameraFeed/useOperateCamera.ts b/src/Components/CameraFeed/useOperateCamera.ts index bfddbf5b887..0e65fb0130c 100644 --- a/src/Components/CameraFeed/useOperateCamera.ts +++ b/src/Components/CameraFeed/useOperateCamera.ts @@ -54,7 +54,7 @@ export type OperationAction = * This hook is used to control the PTZ of a camera asset and retrieve other related information. * @param id The external id of the camera asset */ -export default function useOperateCamera(id: string, silent = false) { +export default function useOperateCamera(id: string) { const [key, setKey] = useState(0); return { @@ -70,14 +70,14 @@ export default function useOperateCamera(id: string, silent = false) { type: "get_status", }, }, - silent, + silent: true, }); } return request(FeedRoutes.operateAsset, { pathParams: { id }, body: { action }, - silent, + silent: true, }); }, }; diff --git a/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx index 2e999d1956e..6a8784ba6fb 100644 --- a/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx +++ b/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx @@ -1,14 +1,12 @@ import { useEffect, useRef, useState } from "react"; import { ConsultationTabProps } from "./index"; -import { AssetBedModel, AssetData } from "../../Assets/AssetTypes"; -import routes from "../../../Redux/api"; +import { AssetData } from "../../Assets/AssetTypes"; import useQuery from "../../../Utils/request/useQuery"; import CameraFeed from "../../CameraFeed/CameraFeed"; import Loading from "../../Common/Loading"; -import AssetBedSelect from "../../CameraFeed/AssetBedSelect"; +import CameraPresetSelect from "../../CameraFeed/CameraPresetSelect"; import { triggerGoal } from "../../../Integrations/Plausible"; import useAuthUser from "../../../Common/hooks/useAuthUser"; -import useSlug from "../../../Common/hooks/useSlug"; import CareIcon from "../../../CAREUI/icons/CareIcon"; import ButtonV2 from "../../Common/components/ButtonV2"; import useOperateCamera, { @@ -20,18 +18,21 @@ import ConfirmDialog from "../../Common/ConfirmDialog"; import useBreakpoints from "../../../Common/hooks/useBreakpoints"; import { Warn } from "../../../Utils/Notifications"; import { useTranslation } from "react-i18next"; -import { GetStatusResponse } from "../../CameraFeed/routes"; +import { + CameraPreset, + FeedRoutes, + GetStatusResponse, +} from "../../CameraFeed/routes"; import StillWatching from "../../CameraFeed/StillWatching"; export const ConsultationFeedTab = (props: ConsultationTabProps) => { const { t } = useTranslation(); const authUser = useAuthUser(); - const facility = useSlug("facility"); const bed = props.consultationData.current_bed?.bed_object; const feedStateSessionKey = `encounterFeedState[${props.consultationId}]`; const [asset, setAsset] = useState(); - const [preset, setPreset] = useState(); + const [preset, setPreset] = useState(); const [showPresetSaveConfirmation, setShowPresetSaveConfirmation] = useState(false); const [isUpdatingPreset, setIsUpdatingPreset] = useState(false); @@ -52,23 +53,19 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { } }, []); - const { key, operate } = useOperateCamera(asset?.id ?? "", true); + const { key, operate } = useOperateCamera(asset?.id ?? ""); - const { data, loading, refetch } = useQuery(routes.listAssetBeds, { - query: { limit: 100, facility, bed: bed?.id, asset: asset?.id }, + const presetsQuery = useQuery(FeedRoutes.listPresets, { + query: { bed: bed?.id, position: true, limit: 100 }, prefetch: !!bed, onResponse: ({ data }) => { if (!data) { return; } - const presets = data.results.filter( - (obj) => - obj.asset_object.meta?.asset_type === "CAMERA" && - obj.meta.type !== "boundary", - ); - + const presets = data.results; const lastStateJSON = sessionStorage.getItem(feedStateSessionKey); + const preset = (() => { if (lastStateJSON) { @@ -77,23 +74,34 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { return presets.find((obj) => obj.id === lastState.value); } if (lastState.type === "position") { + const assetBedObj = presets.find( + (p) => p.asset_bed.id === lastState.assetBed, + )?.asset_bed; + + if (!assetBedObj) { + return; + } + return { ...presets[0], id: "", - meta: { ...presets[0].meta, position: lastState.value }, - }; + asset_bed: assetBedObj, + position: lastState.value, + } satisfies CameraPreset; } } })() ?? presets[0]; + console.log({ preset, presets }); + if (preset) { setPreset(preset); - setAsset(preset.asset_object); + setAsset(preset.asset_bed.asset_object); } }, }); - const presets = data?.results; + const presets = presetsQuery.data?.results; const handleUpdatePreset = async () => { if (!preset) return; @@ -102,17 +110,17 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { const { data } = await operate({ type: "get_status" }); const { position } = (data as { result: { position: PTZPayload } }).result; - - const { data: updated } = await request(routes.partialUpdateAssetBed, { - pathParams: { external_id: preset.id }, + const { data: updated } = await request(FeedRoutes.updatePreset, { + pathParams: { + assetbed_id: preset.asset_bed.id, + external_id: preset.id, + }, body: { - asset: preset.asset_object.id, - bed: preset.bed_object.id, - meta: { ...preset.meta, position }, + position, }, }); - await refetch(); + await presetsQuery.refetch(); setPreset(updated); setHasMoved(false); @@ -124,7 +132,7 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { if (divRef.current) { divRef.current.scrollIntoView({ behavior: "smooth" }); } - }, [!!bed, loading, !!asset, divRef.current]); + }, [!!bed, presetsQuery.loading, !!asset, divRef.current]); useEffect(() => { if (preset?.id) { @@ -138,7 +146,7 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { } }, [feedStateSessionKey, preset]); - if (loading) { + if (presetsQuery.loading) { return ; } @@ -169,8 +177,11 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { { + if (!preset) { + return; + } setHasMoved(true); setTimeout(async () => { const { data } = await operate({ type: "get_status" }); @@ -179,6 +190,7 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { feedStateSessionKey, JSON.stringify({ type: "position", + assetBed: preset.asset_bed.id, value: (data as GetStatusResponse).result.position, } satisfies LastAccessedPosition), ); @@ -204,26 +216,19 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => {
{presets ? ( <> - obj.meta.preset_name} + label={(obj) => obj.name} value={preset} onChange={(value) => { triggerGoal("Camera Preset Clicked", { - presetName: preset?.meta?.preset_name, + presetName: preset?.name, consultationId: props.consultationId, userId: authUser.id, result: "success", }); setHasMoved(false); - // Voluntarily copying to trigger change of reference of the position attribute, so that the useEffect of CameraFeed that handles the moves gets triggered. - setPreset({ - ...value, - meta: { - ...value.meta, - position: { ...value.meta.position }, - }, - }); + setPreset(value); }} /> {isUpdatingPreset ? ( @@ -269,6 +274,7 @@ type LastAccessedPreset = { type LastAccessedPosition = { type: "position"; + assetBed: string; value: PTZPayload; }; From e744ac420f23db9d71ccad7891a16a4442760c16 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Sun, 29 Sep 2024 13:33:34 +0530 Subject: [PATCH 05/16] add link to camera preset configure from bed management --- src/Components/Assets/AssetType/ONVIFCamera.tsx | 2 +- src/Components/Facility/BedManagement.tsx | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Components/Assets/AssetType/ONVIFCamera.tsx b/src/Components/Assets/AssetType/ONVIFCamera.tsx index cad38270cef..fb0aa0c9a37 100644 --- a/src/Components/Assets/AssetType/ONVIFCamera.tsx +++ b/src/Components/Assets/AssetType/ONVIFCamera.tsx @@ -88,7 +88,7 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { if (isLoading || loading || !facility) return ; return ( -
+
{["DistrictAdmin", "StateAdmin"].includes(authUser.user_type) && (
{

{description}

+ + + Manage linked cameras + Date: Mon, 30 Sep 2024 10:42:07 +0530 Subject: [PATCH 06/16] remove unused type --- src/Components/CameraFeed/FeedControls.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Components/CameraFeed/FeedControls.tsx b/src/Components/CameraFeed/FeedControls.tsx index 1bacd6c1cfc..36d3aa96cd2 100644 --- a/src/Components/CameraFeed/FeedControls.tsx +++ b/src/Components/CameraFeed/FeedControls.tsx @@ -15,8 +15,6 @@ const Actions = { const metaKey = isAppleDevice ? "Meta" : "Control"; -export type PTZAction = keyof typeof Actions; - /** * Returns the PTZ payload for the given action * From 7088f8d1eef203e9d9d5fa87ffd95703ed84f846 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Mon, 30 Sep 2024 10:54:51 +0530 Subject: [PATCH 07/16] use host unreachable instead of authn. error --- src/Components/CameraFeed/CameraFeed.tsx | 13 +------------ src/Components/CameraFeed/FeedAlert.tsx | 4 +--- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/Components/CameraFeed/CameraFeed.tsx b/src/Components/CameraFeed/CameraFeed.tsx index 863a6bbac8a..70634ba863a 100644 --- a/src/Components/CameraFeed/CameraFeed.tsx +++ b/src/Components/CameraFeed/CameraFeed.tsx @@ -77,7 +77,7 @@ export default function CameraFeed(props: Props) { .operate({ type: "get_stream_token" }) .then(({ res, data }) => { if (res?.status != 200) { - setState("authentication_error"); + setState("host_unreachable"); return props.onStreamError?.(); } const result = data?.result as { token: string }; @@ -221,17 +221,6 @@ export default function CameraFeed(props: Props) { onResetClick={resetStream} /> ); - case "authentication_error": - return ( - - ); case "offline": return ( > = { zooming: "l-search", saving_preset: "l-save", host_unreachable: "l-exclamation-triangle", - authentication_error: "l-exclamation-triangle", }; export default function FeedAlert({ state }: Props) { From 7ffe232dc2f1b075e69c836a9914739ba63b7c5c Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Thu, 3 Oct 2024 13:47:37 +0530 Subject: [PATCH 08/16] Support for configuring presets of beds from asset configure page --- src/Common/constants.tsx | 14 - src/Components/Assets/AssetConfigure.tsx | 19 +- .../Assets/AssetType/ONVIFCamera.tsx | 173 ----- src/Components/Assets/AssetTypes.tsx | 3 + .../Assets/ConfigureONVIFCamera.tsx | 671 ++++++++++++++++++ src/Components/CameraFeed/CameraFeed.tsx | 16 +- src/Components/CameraFeed/utils.ts | 2 +- src/Components/Facility/FacilityUsers.tsx | 6 +- src/Utils/transformUtils.ts | 17 +- 9 files changed, 705 insertions(+), 216 deletions(-) delete mode 100644 src/Components/Assets/AssetType/ONVIFCamera.tsx create mode 100644 src/Components/Assets/ConfigureONVIFCamera.tsx diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index 70b5bc78e23..dcdf3728497 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -839,20 +839,6 @@ export const LOCATION_BED_TYPES = [ { id: "REGULAR", name: "Regular" }, ] as const; -export const ASSET_META_TYPE = [ - { id: "CAMERA", text: "Camera(ONVIF)" }, - { id: "HL7MONITOR", text: "Vitals Monitor(HL7)" }, -]; - -export const CAMERA_TYPE = [ - { id: "HIKVISION", text: "ONVIF Camera (HIKVISION)" }, -]; - -export const GENDER: { [key: number]: string } = GENDER_TYPES.reduce( - (acc, curr) => ({ ...acc, [curr.id]: curr.text }), - {}, -); - export type CameraPTZ = { icon?: IconName; label: string; diff --git a/src/Components/Assets/AssetConfigure.tsx b/src/Components/Assets/AssetConfigure.tsx index aa6b7c9221e..ab80b4c5062 100644 --- a/src/Components/Assets/AssetConfigure.tsx +++ b/src/Components/Assets/AssetConfigure.tsx @@ -1,6 +1,6 @@ import Loading from "../Common/Loading"; import HL7Monitor from "./AssetType/HL7Monitor"; -import ONVIFCamera from "./AssetType/ONVIFCamera"; +import ConfigureONVIFCamera from "./ConfigureONVIFCamera"; import Page from "../Common/components/Page"; import useQuery from "../../Utils/request/useQuery"; import routes from "../../Redux/api"; @@ -11,13 +11,11 @@ interface AssetConfigureProps { } const AssetConfigure = ({ assetId, facilityId }: AssetConfigureProps) => { - const { - data: asset, - loading, - refetch, - } = useQuery(routes.getAsset, { pathParams: { external_id: assetId } }); + const { data: asset, refetch } = useQuery(routes.getAsset, { + pathParams: { external_id: assetId }, + }); - if (loading || !asset) { + if (!asset) { return ; } @@ -63,12 +61,7 @@ const AssetConfigure = ({ assetId, facilityId }: AssetConfigureProps) => { }} backUrl={`/facility/${facilityId}/assets/${assetId}`} > - refetch()} - /> + refetch()} /> ); }; diff --git a/src/Components/Assets/AssetType/ONVIFCamera.tsx b/src/Components/Assets/AssetType/ONVIFCamera.tsx deleted file mode 100644 index fb0aa0c9a37..00000000000 --- a/src/Components/Assets/AssetType/ONVIFCamera.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { useEffect, useState } from "react"; -import { ResolvedMiddleware } from "../AssetTypes"; -import * as Notification from "../../../Utils/Notifications.js"; -import { getCameraConfig } from "../../../Utils/transformUtils"; -import Loading from "../../Common/Loading"; -import { checkIfValidIP } from "../../../Common/validation"; -import TextFormField from "../../Form/FormFields/TextFormField"; -import { Submit } from "../../Common/components/ButtonV2"; -import { SyntheticEvent } from "react"; -import useAuthUser from "../../../Common/hooks/useAuthUser"; - -import request from "../../../Utils/request/request"; -import routes from "../../../Redux/api"; -import useQuery from "../../../Utils/request/useQuery"; -import CareIcon from "../../../CAREUI/icons/CareIcon"; -import useOperateCamera from "../../CameraFeed/useOperateCamera"; -import CameraFeed from "../../CameraFeed/CameraFeed"; - -interface Props { - assetId: string; - facilityId: string; - asset: any; - onUpdated?: () => void; -} - -const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { - const [isLoading, setIsLoading] = useState(true); - const [assetType, setAssetType] = useState(""); - const [middlewareHostname, setMiddlewareHostname] = useState(""); - const [resolvedMiddleware, setResolvedMiddleware] = - useState(); - const [cameraAddress, setCameraAddress] = useState(""); - const [ipadrdress_error, setIpAddress_error] = useState(""); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [streamUuid, setStreamUuid] = useState(""); - const [loadingSetConfiguration, setLoadingSetConfiguration] = useState(false); - const { data: facility, loading } = useQuery(routes.getPermittedFacility, { - pathParams: { id: facilityId }, - }); - const authUser = useAuthUser(); - - useEffect(() => { - if (asset) { - setAssetType(asset?.asset_class); - setResolvedMiddleware(asset?.resolved_middleware); - const cameraConfig = getCameraConfig(asset); - setMiddlewareHostname(cameraConfig.middleware_hostname); - setCameraAddress(cameraConfig.hostname); - setUsername(cameraConfig.username); - setPassword(cameraConfig.password); - setStreamUuid(cameraConfig.accessKey); - } - setIsLoading(false); - }, [asset]); - - const handleSubmit = async (e: SyntheticEvent) => { - e.preventDefault(); - if (checkIfValidIP(cameraAddress)) { - setLoadingSetConfiguration(true); - setIpAddress_error(""); - const data = { - meta: { - asset_type: "CAMERA", - middleware_hostname: middlewareHostname, - local_ip_address: cameraAddress, - camera_access_key: `${username}:${password}:${streamUuid}`, - }, - }; - const { res } = await request(routes.partialUpdateAsset, { - pathParams: { external_id: assetId }, - body: data, - }); - if (res?.status === 200) { - Notification.Success({ msg: "Asset Configured Successfully" }); - onUpdated?.(); - } else { - Notification.Error({ msg: "Something went wrong!" }); - } - setLoadingSetConfiguration(false); - } else { - setIpAddress_error("IP address is invalid"); - } - }; - - const { operate, key } = useOperateCamera(asset.id); - - if (isLoading || loading || !facility) return ; - - return ( -
- {["DistrictAdmin", "StateAdmin"].includes(authUser.user_type) && ( - -
- -

Middleware Hostname

- {resolvedMiddleware?.source != "asset" && ( -
- - - Middleware hostname sourced from asset{" "} - {resolvedMiddleware?.source} - -
- )} -
- } - placeholder={resolvedMiddleware?.hostname} - value={middlewareHostname} - onChange={({ value }) => setMiddlewareHostname(value)} - /> - setCameraAddress(value)} - error={ipadrdress_error} - /> - setUsername(value)} - /> - setPassword(value)} - /> - setStreamUuid(value)} - /> -
-
- -
- - )} - - {assetType === "ONVIF" ? ( -
- -
- ) : null} -
- ); -}; -export default ONVIFCamera; diff --git a/src/Components/Assets/AssetTypes.tsx b/src/Components/Assets/AssetTypes.tsx index e3dee31bb7f..2f8e086a813 100644 --- a/src/Components/Assets/AssetTypes.tsx +++ b/src/Components/Assets/AssetTypes.tsx @@ -109,6 +109,9 @@ export interface AssetData { latest_status: string; last_service: AssetService; meta?: { + middleware_hostname?: string; + local_ip_address?: string; + camera_access_key?: string; [key: string]: any; }; } diff --git a/src/Components/Assets/ConfigureONVIFCamera.tsx b/src/Components/Assets/ConfigureONVIFCamera.tsx new file mode 100644 index 00000000000..c29488076f3 --- /dev/null +++ b/src/Components/Assets/ConfigureONVIFCamera.tsx @@ -0,0 +1,671 @@ +import { useEffect, useState } from "react"; +import { AssetData } from "./AssetTypes"; +import { getCameraConfig, makeAccessKey } from "../../Utils/transformUtils"; +import TextFormField from "../Form/FormFields/TextFormField"; +import ButtonV2, { Cancel, Submit } from "../Common/components/ButtonV2"; +import useAuthUser from "../../Common/hooks/useAuthUser"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import useOperateCamera from "../CameraFeed/useOperateCamera"; +import CameraFeed from "../CameraFeed/CameraFeed"; +import { useTranslation } from "react-i18next"; +import request from "../../Utils/request/request"; +import routes from "../../Redux/api"; +import { Error, Success } from "../../Utils/Notifications"; +import { useQueryParams } from "raviger"; +import useQuery from "../../Utils/request/useQuery"; +import { classNames, compareBy } from "../../Utils/utils"; +import RecordMeta from "../../CAREUI/display/RecordMeta"; +import { + CameraPreset, + FeedRoutes, + GetStatusResponse, +} from "../CameraFeed/routes"; +import DialogModal from "../Common/Dialog"; +import { + Listbox, + ListboxButton, + ListboxOption, + ListboxOptions, +} from "@headlessui/react"; +import { dropdownOptionClassNames } from "../Form/MultiSelectMenuV2"; +import Loading from "../Common/Loading"; +import ConfirmDialog from "../Common/ConfirmDialog"; +import { FieldLabel } from "../Form/FormFields/FormField"; + +interface Props { + asset: AssetData; + onUpdated: () => void; +} + +type OnvifPreset = { name: string; value: number }; + +export default function ConfigureONVIFCamera(props: Props) { + const { t } = useTranslation(); + const authUser = useAuthUser(); + + const [query, setQuery] = useQueryParams<{ bed?: string }>(); + const [meta, setMeta] = useState(props.asset.meta); + const [onvifPresets, setOnvifPresets] = useState(); + const [currentOnvifPreset, setCurrentOnvifPreset] = useState(); + const [createPreset, setCreatePreset] = useState(); + const [editPreset, setEditPreset] = useState<{ + preset: CameraPreset["id"]; + position?: CameraPreset["position"]; + }>(); + const [presetName, setPresetName] = useState(""); + const [showUnlinkConfirmation, setShowUnlinkConfirmation] = useState(false); + + const assetBedsQuery = useQuery(routes.listAssetBeds, { + query: { asset: props.asset.id, limit: 50 }, + }); + + const bedsQuery = useQuery(routes.listFacilityBeds, { + query: { location: props.asset.location_object.id, limit: 50 }, + }); + + const linkedAssetBeds = assetBedsQuery.data?.results.sort( + compareBy("created_date"), + ); + + const firstAssetBedId = linkedAssetBeds?.[0]?.bed_object.id; + useEffect(() => { + if (!query.bed && firstAssetBedId) { + setQuery({ bed: firstAssetBedId }); + } + }, [query.bed, firstAssetBedId]); + + const linkedBedIDs = linkedAssetBeds?.map((a) => a.bed_object.id!); + const unlinkedBeds = + linkedBedIDs && + bedsQuery.data?.results.filter((bed) => !linkedBedIDs.includes(bed.id!)); + const selectedAssetBed = linkedAssetBeds?.find( + (a) => a.bed_object.id === query.bed, + ); + const selectedUnlinkedBed = unlinkedBeds?.find((bed) => bed.id === query.bed); + + const cameraPresetsQuery = useQuery(FeedRoutes.listAssetBedPresets, { + pathParams: { assetbed_id: selectedAssetBed?.id ?? "" }, + query: { position: true, limit: 50 }, + prefetch: !!selectedAssetBed?.id, + }); + + useEffect(() => setMeta(props.asset.meta), [props.asset]); + + const accessKeyAttributes = getCameraConfig(meta); + + const { operate, key } = useOperateCamera(props.asset.id); + + if (!["DistrictAdmin", "StateAdmin"].includes(authUser.user_type)) { + return ( +
+ +
+ ); + } + + return ( +
+
+
{ + e.preventDefault(); + + // TODO: perform validations + + const { res } = await request(routes.partialUpdateAsset, { + pathParams: { external_id: props.asset.id }, + body: { meta }, + }); + + if (res?.ok) { + Success({ msg: "Asset Configured Successfully" }); + props.onUpdated(); + } + }} + > +

Feed Configurations

+
+ +

Middleware Hostname

+ {props.asset.resolved_middleware?.source != "asset" && ( +
+ + + Middleware hostname sourced from asset{" "} + {props.asset.resolved_middleware?.source} + +
+ )} +
+ } + placeholder={props.asset.resolved_middleware?.hostname} + value={meta?.middleware_hostname} + onChange={({ value }) => + setMeta({ ...meta, middleware_hostname: value }) + } + /> + + setMeta({ ...meta, local_ip_address: value }) + } + // error={ipadrdress_error} + /> + + setMeta({ + ...meta, + camera_access_key: makeAccessKey({ + ...accessKeyAttributes, + username: value, + }), + }) + } + /> + + setMeta({ + ...meta, + camera_access_key: makeAccessKey({ + ...accessKeyAttributes, + password: value, + }), + }) + } + /> + + setMeta({ + ...meta, + camera_access_key: makeAccessKey({ + ...accessKeyAttributes, + accessKey: value, + }), + }) + } + /> +
+
+ +
+ + +
+ { + if (!onvifPresets) { + setOnvifPresets( + Object.entries(presets).map(([name, value]) => ({ + name, + value, + })), + ); + } + }} + > +
+ { + setCurrentOnvifPreset(preset); + operate({ + type: "goto_preset", + data: { + preset: preset.value, + }, + }); + }} + disabled={!onvifPresets?.length} + > +
+ + + {!onvifPresets?.length + ? "No presets" + : (currentOnvifPreset?.name ?? + "Move to an ONVIF Preset")} + + + + + + + {onvifPresets?.map((obj) => ( + + classNames( + dropdownOptionClassNames(args), + "px-2 py-1.5", + ) + } + value={obj} + > + {obj.name} + + ))} + +
+
+
+
+
+
+ + {linkedAssetBeds && ( +
+

Manage Presets of Bed

+
+ + {cameraPresetsQuery.loading && } + {selectedAssetBed && ( + <> + setShowUnlinkConfirmation(false)} + onConfirm={async () => { + const { res } = await request(routes.deleteAssetBed, { + pathParams: { external_id: selectedAssetBed.id }, + }); + + if (res?.ok) { + Success({ + msg: `${selectedAssetBed.bed_object.name} was unlinked from ${selectedAssetBed.asset_object.name}.`, + }); + setShowUnlinkConfirmation(false); + assetBedsQuery.refetch(); + } + }} + /> + { + setCreatePreset(undefined); + setPresetName(""); + }} + > + setPresetName(value)} + errorClassName="hidden" + placeholder="Specify an identifiable name for the new preset" + /> +
+ { + const { res } = await request(FeedRoutes.createPreset, { + pathParams: { assetbed_id: selectedAssetBed.id }, + body: { + name: presetName, + position: createPreset, + }, + }); + if (!res?.ok) { + return; + } + setCreatePreset(undefined); + setPresetName(""); + Success({ msg: "Preset created" }); + cameraPresetsQuery.refetch(); + }} + disabled={!presetName} + /> +
+
+
+
    +
  • { + const { data } = await operate({ type: "get_status" }); + if (!data) { + Error({ msg: "Unable to get current position." }); + return; + } + setCreatePreset( + (data as GetStatusResponse).result.position, + ); + }} + > + + Add a preset +
  • + {cameraPresetsQuery.data?.results.map((preset) => ( +
  • + { + setEditPreset(undefined); + setPresetName(""); + }} + > +
    + setPresetName(value)} + placeholder="Specify an identifiable name for the new preset" + /> + Position + {editPreset?.position ? ( + <> +
    + X: {preset.position?.x} + Y: {preset.position?.y} + Zoom: {preset.position?.zoom} +
    +
    + X: {editPreset.position?.x} + Y: {editPreset.position?.y} + Zoom: {editPreset.position?.zoom} +
    + + ) : ( + <> +
    + X: {preset.position?.x} + Y: {preset.position?.y} + Zoom: {preset.position?.zoom} +
    +
    + + Unchanged + + { + const { data } = await operate({ + type: "get_status", + }); + if (!data) { + Error({ + msg: "Unable to get current position.", + }); + return; + } + setEditPreset({ + ...editPreset!, + position: (data as GetStatusResponse) + .result.position!, + }); + }} + shadow + > + Change to camera's current position + +
    + + )} +
    + { + setEditPreset(undefined); + setPresetName(""); + }} + /> + { + const { res } = await request( + FeedRoutes.deletePreset, + { + pathParams: { + assetbed_id: selectedAssetBed.id, + id: preset.id, + }, + }, + ); + if (!res?.ok) { + return; + } + Success({ msg: "Preset deleted" }); + cameraPresetsQuery.refetch(); + setEditPreset(undefined); + setPresetName(""); + }} + variant="danger" + > + + Delete + + { + const { res } = await request( + FeedRoutes.updatePreset, + { + pathParams: { + assetbed_id: selectedAssetBed.id, + id: preset.id, + }, + body: { + name: presetName || undefined, + position: editPreset?.position, + }, + }, + ); + if (!res?.ok) { + return; + } + Success({ msg: "Preset updated" }); + setEditPreset(undefined); + setPresetName(""); + cameraPresetsQuery.refetch(); + }} + /> +
    + +
    +
    + {preset.name} +
    + + operate({ + type: "absolute_move", + data: preset.position!, + }) + } + > + + View + + { + setEditPreset({ preset: preset.id }); + }} + > + + Update + +
    +
    + {preset.position && ( +
    + + Position + +
    + X: {preset.position?.x} + Y: {preset.position?.y} + Zoom: {preset.position?.zoom} +
    +
    + )} + {preset.boundary && ( +
    + + Boundary + +
    + X-: {preset.boundary.x0} + Y0: {preset.boundary.y0} + X1: {preset.boundary.x1} + Y1: {preset.boundary.y1} +
    +
    + )} + + + +
    +
  • + ))} +
+
+ + setShowUnlinkConfirmation(true)} + > + Unlink this bed from this camera + +
+
+ + )} + {selectedUnlinkedBed && ( +
+ +

This bed has not been linked to this camera.

+

+ To create presets for this bed, you'll need to link the + camera to the bed first. +

+ { + const { res } = await request(routes.createAssetBed, { + body: { + asset: props.asset.id, + bed: selectedUnlinkedBed.id, + }, + }); + if (res?.ok) { + Success({ msg: "Camera linked to bed successfully." }); + assetBedsQuery.refetch(); + } + }} + className="mt-6" + > + Link bed to Camera + +
+
+ )} +
+
+ )} +
+ ); +} diff --git a/src/Components/CameraFeed/CameraFeed.tsx b/src/Components/CameraFeed/CameraFeed.tsx index 40e82d43763..3c9b25b6fc9 100644 --- a/src/Components/CameraFeed/CameraFeed.tsx +++ b/src/Components/CameraFeed/CameraFeed.tsx @@ -183,13 +183,15 @@ export default function CameraFeed(props: Props) { {props.children}
- - {props.asset.name} - - +
+ + {props.asset.name} + + +
{!isIOS && (
{ throw "getStreamUrl can be invoked only for ONVIF Assets"; } - const config = getCameraConfig(asset); + const config = getCameraConfig(asset.meta); const host = asset.resolved_middleware?.hostname; const uuid = config.accessKey; diff --git a/src/Components/Facility/FacilityUsers.tsx b/src/Components/Facility/FacilityUsers.tsx index 2572731f720..3c6cfa9f003 100644 --- a/src/Components/Facility/FacilityUsers.tsx +++ b/src/Components/Facility/FacilityUsers.tsx @@ -4,7 +4,7 @@ import CareIcon from "../../CAREUI/icons/CareIcon"; import { RESULTS_PER_PAGE_LIMIT } from "../../Common/constants"; import * as Notification from "../../Utils/Notifications.js"; import { formatName, isUserOnline, relativeTime } from "../../Utils/utils"; -import SlideOverCustom from "../../CAREUI/interactive/SlideOver"; +import SlideOver from "../../CAREUI/interactive/SlideOver"; import Pagination from "../Common/Pagination"; import UserDetails from "../Common/UserDetails"; import ButtonV2 from "../Common/components/ButtonV2"; @@ -280,7 +280,7 @@ export default function FacilityUsers(props: any) { /> )}
- - +
{manageUsers}
diff --git a/src/Utils/transformUtils.ts b/src/Utils/transformUtils.ts index 0050a0bcbb6..4aa63da734c 100644 --- a/src/Utils/transformUtils.ts +++ b/src/Utils/transformUtils.ts @@ -1,16 +1,23 @@ import { AssetData } from "../Components/Assets/AssetTypes"; -export const getCameraConfig = (asset: AssetData) => { - const { meta } = asset; +export const getCameraConfig = (meta: AssetData["meta"]) => { return { middleware_hostname: meta?.middleware_hostname, - id: asset?.id, hostname: meta?.local_ip_address, username: meta?.camera_access_key?.split(":")[0], password: meta?.camera_access_key?.split(":")[1], accessKey: meta?.camera_access_key?.split(":")[2], port: 80, - location_id: asset?.location_object?.id, - facility_id: asset?.location_object?.facility?.id, }; }; + +export const makeAccessKey = ( + attrs: Pick< + ReturnType, + "username" | "password" | "accessKey" + >, +) => { + return [attrs.username, attrs.password, attrs.accessKey] + .map((a) => a ?? "") + .join(":"); +}; From 0a7b27947f0e24372d0f208363a9134d956ee82f Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Thu, 3 Oct 2024 15:42:11 +0530 Subject: [PATCH 09/16] cleanup --- src/Components/Assets/AssetConfigure.tsx | 4 +- .../CameraFeed/CameraPresetsConfigure.tsx | 399 ------------------ .../ConfigureCamera.tsx} | 14 +- src/Components/Facility/BedManagement.tsx | 12 - src/Routers/routes/FacilityLocationRoutes.tsx | 5 - 5 files changed, 7 insertions(+), 427 deletions(-) delete mode 100644 src/Components/CameraFeed/CameraPresetsConfigure.tsx rename src/Components/{Assets/ConfigureONVIFCamera.tsx => CameraFeed/ConfigureCamera.tsx} (98%) diff --git a/src/Components/Assets/AssetConfigure.tsx b/src/Components/Assets/AssetConfigure.tsx index ab80b4c5062..9f5f73f296d 100644 --- a/src/Components/Assets/AssetConfigure.tsx +++ b/src/Components/Assets/AssetConfigure.tsx @@ -1,6 +1,6 @@ import Loading from "../Common/Loading"; import HL7Monitor from "./AssetType/HL7Monitor"; -import ConfigureONVIFCamera from "./ConfigureONVIFCamera"; +import ConfigureCamera from "../CameraFeed/ConfigureCamera"; import Page from "../Common/components/Page"; import useQuery from "../../Utils/request/useQuery"; import routes from "../../Redux/api"; @@ -61,7 +61,7 @@ const AssetConfigure = ({ assetId, facilityId }: AssetConfigureProps) => { }} backUrl={`/facility/${facilityId}/assets/${assetId}`} > - refetch()} /> + refetch()} /> ); }; diff --git a/src/Components/CameraFeed/CameraPresetsConfigure.tsx b/src/Components/CameraFeed/CameraPresetsConfigure.tsx deleted file mode 100644 index 851f144fa13..00000000000 --- a/src/Components/CameraFeed/CameraPresetsConfigure.tsx +++ /dev/null @@ -1,399 +0,0 @@ -import { useState } from "react"; -import routes from "../../Redux/api"; -import useQuery from "../../Utils/request/useQuery"; -import { AssetBedModel } from "../Assets/AssetTypes"; -import Page from "../Common/components/Page"; -import Loading from "../Common/Loading"; -import CameraFeed from "./CameraFeed"; -import useOperateCamera from "./useOperateCamera"; -import ButtonV2, { Submit } from "../Common/components/ButtonV2"; -import CareIcon from "../../CAREUI/icons/CareIcon"; -import request from "../../Utils/request/request"; -import { CameraPreset, FeedRoutes, GetStatusResponse } from "./routes"; -import { classNames } from "../../Utils/utils"; -import { - Listbox, - ListboxButton, - ListboxOption, - ListboxOptions, -} from "@headlessui/react"; -import { dropdownOptionClassNames } from "../Form/MultiSelectMenuV2"; -import DialogModal from "../Common/Dialog"; -import TextFormField from "../Form/FormFields/TextFormField"; -import { Success } from "../../Utils/Notifications"; - -type Props = { - locationId: string; - bedId: string; -}; - -type OnvifPreset = { name: string; value: number }; - -export default function CameraPresetsConfigure(props: Props) { - const [current, setCurrent] = useState(); - - const camerasQuery = useQuery(routes.listAssets, { - query: { - location: props.locationId, - asset_class: "ONVIF", - }, - }); - - const assetBedsQuery = useQuery(routes.listAssetBeds, { - query: { - bed: props.bedId, - }, - onResponse: ({ data }) => setCurrent(data?.results[0]), - }); - - const cameraPresetsQuery = useQuery(FeedRoutes.listAssetBedPresets, { - pathParams: { assetbed_id: current?.id ?? "" }, - query: { position: true }, - prefetch: !!current?.id, - }); - - if (!assetBedsQuery.data || !camerasQuery.data) { - return ; - } - - const assetBeds = assetBedsQuery.data.results; - - const linkedCameraIds = assetBeds.map((obj) => obj.asset_object.id); - - // TODO: filter using Backend - const camerasNotLinked = camerasQuery.data.results.filter( - ({ id }) => !linkedCameraIds.includes(id), - ); - - return ( - -
-
-
-
Cameras
-
    - {assetBeds.map((assetBed) => { - const isSelected = current?.id === assetBed.id; - return ( -
  • - - {assetBed.asset_object.name} - -
    - setCurrent(assetBed)} - > - - - { - const { res } = await request(routes.deleteAssetBed, { - pathParams: { external_id: assetBed.id }, - }); - - if (res?.ok) { - camerasQuery.refetch(); - assetBedsQuery.refetch(); - } - }} - > - - -
    -
  • - ); - })} - {camerasNotLinked.map((camera) => ( -
  • - - {camera.name} - - { - const { res } = await request(routes.createAssetBed, { - body: { asset: camera.id, bed: props.bedId }, - }); - if (res?.ok) { - camerasQuery.refetch(); - assetBedsQuery.refetch(); - } - }} - > - - -
  • - ))} -
- {camerasNotLinked.length === 0 && assetBeds.length === 0 && ( - - No cameras available in this location - - )} -
- - {/* Camera Presets */} -
- {!!current && ( - <> -
Position Presets
-
    - {cameraPresetsQuery.data?.results.map((preset) => ( -
  • - - {preset.name} - -
    - setCurrent(assetBed)} - > - - - { - const { res } = await request( - FeedRoutes.deletePreset, - { - pathParams: { - assetbed_id: current.id, - id: preset.id, - }, - }, - ); - if (res?.ok) { - Success({ msg: "Preset deleted" }); - cameraPresetsQuery.refetch(); - } - }} - > - - -
    -
  • - ))} - - {!cameraPresetsQuery.data?.results.length && ( - No position presets - )} -
- - )} -
-
-
- {!current ? ( -
- -

No camera selected

-

Select a linked camera to preview its feed here

-
-
- ) : ( - cameraPresetsQuery.refetch()} - /> - )} -
-
-
- ); -} - -const LinkedCameraFeed = (props: { - assetBed: AssetBedModel; - onPresetCreated: () => void; -}) => { - const { operate, key } = useOperateCamera(props.assetBed.asset_object.id); - const [onvifPresets, setOnvifPresets] = useState(); - const [currentOnvifPreset, setCurrentOnvifPreset] = useState(); - const [createPresetPosition, setCreatePresetPosition] = - useState(); - const [presetName, setPresetName] = useState(""); - - return ( -
- { - setCreatePresetPosition(undefined); - setPresetName(""); - }} - > - setPresetName(value)} - errorClassName="hidden" - placeholder="Specify an identifiable name for the new preset" - /> - {/*
- {JSON.stringify(createPresetPosition, undefined, " ")} -
*/} -
- { - const { res } = await request(FeedRoutes.createPreset, { - pathParams: { assetbed_id: props.assetBed.id }, - body: { - name: presetName, - position: createPresetPosition, - }, - }); - if (!res?.ok) { - return; - } - setCreatePresetPosition(undefined); - setPresetName(""); - Success({ msg: "Preset created" }); - props.onPresetCreated(); - }} - disabled={!presetName} - /> -
-
-
- { - if (!onvifPresets) { - setOnvifPresets( - Object.entries(presets).map(([name, value]) => ({ - name, - value, - })), - ); - } - }} - > -
- { - setCurrentOnvifPreset(preset); - operate({ - type: "goto_preset", - data: { - preset: preset.value, - }, - }); - }} - disabled={!onvifPresets?.length} - > -
- - - {!onvifPresets?.length - ? "No presets" - : (currentOnvifPreset?.name ?? - "Select to move to an ONVIF Preset")} - - - - - - - {onvifPresets?.map((obj) => ( - - classNames( - dropdownOptionClassNames(args), - "px-2 py-1.5", - ) - } - value={obj} - > - {obj.name} - - ))} - -
-
- { - const { data } = await operate({ type: "get_status" }); - if (data) { - setCreatePresetPosition( - (data as GetStatusResponse).result.position, - ); - } - }} - > - - Create new preset - -
-
-
-
- ); -}; diff --git a/src/Components/Assets/ConfigureONVIFCamera.tsx b/src/Components/CameraFeed/ConfigureCamera.tsx similarity index 98% rename from src/Components/Assets/ConfigureONVIFCamera.tsx rename to src/Components/CameraFeed/ConfigureCamera.tsx index c29488076f3..c582b95bc20 100644 --- a/src/Components/Assets/ConfigureONVIFCamera.tsx +++ b/src/Components/CameraFeed/ConfigureCamera.tsx @@ -1,12 +1,12 @@ import { useEffect, useState } from "react"; -import { AssetData } from "./AssetTypes"; +import { AssetData } from "../Assets/AssetTypes"; import { getCameraConfig, makeAccessKey } from "../../Utils/transformUtils"; import TextFormField from "../Form/FormFields/TextFormField"; import ButtonV2, { Cancel, Submit } from "../Common/components/ButtonV2"; import useAuthUser from "../../Common/hooks/useAuthUser"; import CareIcon from "../../CAREUI/icons/CareIcon"; -import useOperateCamera from "../CameraFeed/useOperateCamera"; -import CameraFeed from "../CameraFeed/CameraFeed"; +import useOperateCamera from "./useOperateCamera"; +import CameraFeed from "./CameraFeed"; import { useTranslation } from "react-i18next"; import request from "../../Utils/request/request"; import routes from "../../Redux/api"; @@ -15,11 +15,7 @@ import { useQueryParams } from "raviger"; import useQuery from "../../Utils/request/useQuery"; import { classNames, compareBy } from "../../Utils/utils"; import RecordMeta from "../../CAREUI/display/RecordMeta"; -import { - CameraPreset, - FeedRoutes, - GetStatusResponse, -} from "../CameraFeed/routes"; +import { CameraPreset, FeedRoutes, GetStatusResponse } from "./routes"; import DialogModal from "../Common/Dialog"; import { Listbox, @@ -39,7 +35,7 @@ interface Props { type OnvifPreset = { name: string; value: number }; -export default function ConfigureONVIFCamera(props: Props) { +export default function ConfigureCamera(props: Props) { const { t } = useTranslation(); const authUser = useAuthUser(); diff --git a/src/Components/Facility/BedManagement.tsx b/src/Components/Facility/BedManagement.tsx index 0cbbc3d834a..b46323b74bb 100644 --- a/src/Components/Facility/BedManagement.tsx +++ b/src/Components/Facility/BedManagement.tsx @@ -116,18 +116,6 @@ const BedRow = (props: BedRowProps) => {

{description}

- - - Manage linked cameras - ( @@ -39,10 +38,6 @@ export default { }: any) => ( ), - "/facility/:facilityId/location/:locationId/beds/:bedId/cameras": ({ - locationId, - bedId, - }: any) => , "/facility/:facilityId/live-monitoring": (props: any) => ( From a8be173bcfb5ec6f15e36187d4be856ff002326a Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Thu, 3 Oct 2024 17:04:19 +0530 Subject: [PATCH 10/16] adds i18n --- src/Components/CameraFeed/ConfigureCamera.tsx | 152 ++++++++++-------- src/Components/Common/MonitorAssetPopover.tsx | 2 +- src/Locale/en/Asset.json | 26 ++- src/Locale/en/Bed.json | 7 +- src/Locale/en/Common.json | 7 +- src/Locale/hi/Common.json | 2 +- src/Locale/kn/Common.json | 2 +- src/Locale/ml/Common.json | 2 +- src/Locale/ta/Common.json | 2 +- 9 files changed, 125 insertions(+), 77 deletions(-) diff --git a/src/Components/CameraFeed/ConfigureCamera.tsx b/src/Components/CameraFeed/ConfigureCamera.tsx index c582b95bc20..3c8219c7055 100644 --- a/src/Components/CameraFeed/ConfigureCamera.tsx +++ b/src/Components/CameraFeed/ConfigureCamera.tsx @@ -27,6 +27,7 @@ import { dropdownOptionClassNames } from "../Form/MultiSelectMenuV2"; import Loading from "../Common/Loading"; import ConfirmDialog from "../Common/ConfirmDialog"; import { FieldLabel } from "../Form/FormFields/FormField"; +import { checkIfValidIP } from "../../Common/validation"; interface Props { asset: AssetData; @@ -63,17 +64,21 @@ export default function ConfigureCamera(props: Props) { compareBy("created_date"), ); - const firstAssetBedId = linkedAssetBeds?.[0]?.bed_object.id; - useEffect(() => { - if (!query.bed && firstAssetBedId) { - setQuery({ bed: firstAssetBedId }); - } - }, [query.bed, firstAssetBedId]); - const linkedBedIDs = linkedAssetBeds?.map((a) => a.bed_object.id!); const unlinkedBeds = linkedBedIDs && - bedsQuery.data?.results.filter((bed) => !linkedBedIDs.includes(bed.id!)); + bedsQuery.data?.results + .filter((bed) => !linkedBedIDs.includes(bed.id!)) + .sort(compareBy("created_date")); + + const firstBedId = + linkedAssetBeds?.[0]?.bed_object.id ?? unlinkedBeds?.[0]?.id; + useEffect(() => { + if (!query.bed && firstBedId) { + setQuery({ bed: firstBedId }); + } + }, [query.bed, firstBedId]); + const selectedAssetBed = linkedAssetBeds?.find( (a) => a.bed_object.id === query.bed, ); @@ -106,9 +111,6 @@ export default function ConfigureCamera(props: Props) { className="rounded-lg bg-white p-4 shadow md:w-full" onSubmit={async (e) => { e.preventDefault(); - - // TODO: perform validations - const { res } = await request(routes.partialUpdateAsset, { pathParams: { external_id: props.asset.id }, body: { meta }, @@ -120,28 +122,33 @@ export default function ConfigureCamera(props: Props) { } }} > -

Feed Configurations

+

{t("feed_configurations")}

-

Middleware Hostname

- {props.asset.resolved_middleware?.source != "asset" && ( -
- - - Middleware hostname sourced from asset{" "} - {props.asset.resolved_middleware?.source} - -
- )} +

{t("middleware_hostname")}

+ {!!props.asset.resolved_middleware && + props.asset.resolved_middleware.source != "asset" && ( +
+ + + {t("middleware_hostname_sourced_from", { + source: props.asset.resolved_middleware?.source, + })} + +
+ )}
} - placeholder={props.asset.resolved_middleware?.hostname} + placeholder={ + props.asset.resolved_middleware?.hostname ?? + t("middleware_hostname_example") + } value={meta?.middleware_hostname} onChange={({ value }) => setMeta({ ...meta, middleware_hostname: value }) @@ -149,17 +156,22 @@ export default function ConfigureCamera(props: Props) { /> setMeta({ ...meta, local_ip_address: value }) } - // error={ipadrdress_error} + error={ + meta?.local_ip_address && !checkIfValidIP(meta.local_ip_address) + ? t("invalid_ip_address") + : undefined + } /> @@ -174,7 +186,7 @@ export default function ConfigureCamera(props: Props) { /> {!onvifPresets?.length - ? "No presets" + ? t("no_presets") : (currentOnvifPreset?.name ?? - "Move to an ONVIF Preset")} + t("move_to_onvif_preset"))}
- {linkedAssetBeds && ( + {!linkedAssetBeds?.length && !unlinkedBeds?.length ? ( +
+ +

{t("location_beds_empty")}

+

{t("add_beds_to_configure_presets")}

+
+
+ ) : (
-

Manage Presets of Bed

+

{t("manage_bed_presets")}