Skip to content

Commit

Permalink
add camera preset configure page
Browse files Browse the repository at this point in the history
  • Loading branch information
rithviknishad committed Sep 28, 2024
1 parent a958669 commit 175ee66
Show file tree
Hide file tree
Showing 5 changed files with 361 additions and 43 deletions.
298 changes: 298 additions & 0 deletions src/Components/CameraFeed/CameraPresetsConfigure.tsx
Original file line number Diff line number Diff line change
@@ -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<AssetBedModel>();

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 <Loading />;
}

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 (
<Page title="Configure Cameras linked to Bed">
<div className="flex flex-col gap-8 p-4 md:flex-row">
<div className="min-w-72 whitespace-nowrap rounded-lg bg-white p-4 shadow">
<div>
<h5 className="pt-2">Cameras</h5>
<ul className="space-y-2 py-4">
{assetBeds.map((assetBed) => {
const isSelected = current?.id === assetBed.id;
return (
<li
key={`assetbed-${assetBed.id}`}
className={classNames(
"flex items-center justify-between gap-12 rounded border-2 p-2 transition-all duration-200 ease-in-out",
isSelected
? "border-primary-500"
: "border-secondary-300",
)}
>
<span className="text-sm font-semibold capitalize text-secondary-800">
{assetBed.asset_object.name}
</span>
<div className="flex items-center gap-1">
<ButtonV2
size="small"
variant="secondary"
className="!gap-0.5"
shadow={false}
border
tooltip="Configure"
tooltipClassName="tooltip-bottom translate-y-2 -translate-x-1/2 text-xs"
onClick={() => setCurrent(assetBed)}
>
<CareIcon icon="l-cog" className="text-base" />
</ButtonV2>
<ButtonV2
size="small"
variant="danger"
ghost
className="!gap-0.5"
shadow={false}
border
tooltip="Unlink"
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();
}
}}
>
<CareIcon icon="l-ban" className="text-sm" />
</ButtonV2>
</div>
</li>
);
})}
{camerasNotLinked.map((camera) => (
<li
key={`camera-${camera.id}`}
className="flex items-center justify-between gap-12 rounded border-2 border-secondary-300 p-2 transition-all duration-200 ease-in-out"
>
<span className="text-sm font-semibold capitalize text-secondary-600">
{camera.name}
</span>
<ButtonV2
size="small"
variant="secondary"
className="!gap-0.5"
shadow={false}
border
tooltip="Link"
tooltipClassName="tooltip-bottom translate-y-2 -translate-x-1/2 text-xs"
onClick={async () => {
const { res } = await request(routes.createAssetBed, {
body: { asset: camera.id, bed: props.bedId },
});

if (res?.ok) {
camerasQuery.refetch();
assetBedsQuery.refetch();
}
}}
>
<CareIcon icon="l-link-alt" className="text-sm" />
</ButtonV2>
</li>
))}
</ul>
</div>

{/* Camera Presets */}
<div>
{!!current && (
<>
<h5 className="pt-2">Position Presets</h5>
<ul className="space-y-2.5 py-4">
{cameraPresetsQuery.data?.results.map((preset) => (
<li
key={`preset-${preset.id}`}
className="flex items-center justify-between gap-12"
>
<span className="text-sm font-semibold text-secondary-800">
{preset.name}
</span>
<div className="flex items-center gap-1">
<ButtonV2
size="small"
variant="secondary"
className="!gap-0.5"
shadow={false}
border
tooltip="Use current position as preset"
tooltipClassName="tooltip-bottom translate-y-2 -translate-x-1/2 text-xs"
// onClick={() => setCurrent(assetBed)}
>
<CareIcon icon="l-cog" className="text-base" />
</ButtonV2>
<ButtonV2
size="small"
variant="danger"
ghost
className="!gap-0.5"
shadow={false}
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();
// }
// }}
>
<CareIcon icon="l-trash-alt" className="text-sm" />
</ButtonV2>
</div>
</li>
))}

{!cameraPresetsQuery.data?.results.length && (
<span>No position presets</span>
)}
</ul>
</>
)}
</div>
</div>
<div className="w-full">
{!current ? (
<div className="mx-4 flex aspect-video items-center justify-center rounded-lg border-4 border-dashed border-secondary-400 p-10 shadow">
<span className="text-center text-lg font-semibold text-secondary-500">
<p>No camera selected</p>
<p>Select a linked camera to preview its feed here</p>
</span>
</div>
) : (
<LinkedCameraFeed
key={current.asset_object.id}
asset={current.asset_object}
/>
)}
</div>
</div>
</Page>
);
}

const LinkedCameraFeed = (props: { asset: AssetData }) => {
const { operate, key } = useOperateCamera(props.asset.id, true);
const [cameraPresets, setCameraPresets] = useState<Record<string, number>>();
// const [search, setSearch] = useState("");

return (
<div className="flex overflow-hidden rounded-lg shadow">
<div className="w-full">
<CameraFeed
asset={props.asset}
key={key}
// preset={preset?.meta.position}
shortcutsDisabled
operate={operate}
onCameraPresetsObtained={(presets) => {
if (!cameraPresets) {
setCameraPresets(presets);
}
}}
/>
</div>
{/* <div className="whitespace-nowrap rounded-r-lg bg-white p-4">
<h5 className="py-2">ONVIF Presets</h5>
<TextFormField
name="onvif-presets-search"
value={search}
placeholder="Search"
onChange={(e) => setSearch(e.value)}
errorClassName="hidden"
/>
<ul className="space-y-2.5 py-4 overflow-y-auto h-min">
{!!cameraPresets &&
Object.keys(cameraPresets).map((key) => (
<li
key={key}
className="flex items-center justify-between gap-12"
>
<span className="text-sm font-semibold text-secondary-800">
{key}
</span>
<div className="flex items-center gap-1">
<ButtonV2
size="small"
variant="secondary"
className="!gap-0.5"
shadow={false}
border
tooltip="Configure"
tooltipClassName="tooltip-bottom translate-y-2 -translate-x-1/2 text-xs"
// onClick={() => setCurrent(assetBed)}
>
<CareIcon icon="l-cog" className="text-base" />
</ButtonV2>
</div>
</li>
))}
</ul>
</div> */}
</div>
);
};
40 changes: 40 additions & 0 deletions src/Components/CameraFeed/routes.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -23,6 +27,19 @@ export type GetPresetsResponse = {
result: Record<string, number>;
};

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/",
Expand All @@ -32,4 +49,27 @@ export const FeedRoutes = {
>(),
TBody: Type<{ action: OperationAction }>(),
},

listPresets: {
path: "/api/v1/assetbed/{assetbed_id}/camera_presets/",
method: "GET",
TRes: Type<PaginatedResponse<CameraPreset>>(),
},
createPreset: {
path: "/api/v1/assetbed/{assetbed_id}/camera_presets/",
method: "POST",
TRes: Type<CameraPreset>(),
TBody: Type<WritableOnly<CameraPreset>>(),
},
updatePreset: {
path: "/api/v1/assetbed/{assetbed_id}/camera_presets/{id}/",
method: "PATCH",
TRes: Type<CameraPreset>(),
TBody: Type<Partial<WritableOnly<CameraPreset>>>(),
},
deletePreset: {
path: "/api/v1/assetbed/{assetbed_id}/camera_presets/{id}/",
method: "DELETE",
TRes: Type<CameraPreset>(),
},
} as const;
Loading

0 comments on commit 175ee66

Please sign in to comment.