add attributes to tracked object details pane

This commit is contained in:
Josh Hawkins 2025-12-17 20:14:28 -06:00
parent 3820c511d9
commit 0a28bd7425
4 changed files with 67 additions and 13 deletions

View File

@ -136,6 +136,7 @@
"label": "Score" "label": "Score"
}, },
"recognizedLicensePlate": "Recognized License Plate", "recognizedLicensePlate": "Recognized License Plate",
"attributes": "Classification Attributes",
"estimatedSpeed": "Estimated Speed", "estimatedSpeed": "Estimated Speed",
"objects": "Objects", "objects": "Objects",
"camera": "Camera", "camera": "Camera",

View File

@ -678,6 +678,15 @@ function ObjectDetailsTab({
]); ]);
const apiHost = useApiHost(); const apiHost = useApiHost();
const hasCustomClassificationModels = useMemo(
() => Object.keys(config?.classification?.custom ?? {}).length > 0,
[config],
);
const { data: allowedAttributes } = useSWR<string[]>(
hasCustomClassificationModels && search
? `classification/attributes?object_type=${encodeURIComponent(search.label)}`
: null,
);
// mutation / revalidation // mutation / revalidation
@ -807,6 +816,24 @@ function ObjectDetailsTab({
} }
}, [search]); }, [search]);
const eventAttributes = useMemo(() => {
if (!search || !allowedAttributes || allowedAttributes.length === 0) {
return [];
}
const collected = new Set<string>();
const dataAny = search.data as Record<string, unknown>;
// Check top-level keys in data that match allowed attributes
allowedAttributes.forEach((attr) => {
if (dataAny[attr] !== undefined && dataAny[attr] !== null) {
collected.add(attr);
}
});
return Array.from(collected).sort((a, b) => a.localeCompare(b));
}, [search, allowedAttributes]);
const isEventsKey = useCallback((key: unknown): boolean => { const isEventsKey = useCallback((key: unknown): boolean => {
const candidate = Array.isArray(key) ? key[0] : key; const candidate = Array.isArray(key) ? key[0] : key;
const EVENTS_KEY_PATTERNS = ["events", "events/search", "events/explore"]; const EVENTS_KEY_PATTERNS = ["events", "events/search", "events/explore"];
@ -1295,6 +1322,15 @@ function ObjectDetailsTab({
</div> </div>
</div> </div>
)} )}
{eventAttributes.length > 0 && (
<div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">
{t("details.attributes")}
</div>
<div className="text-sm">{eventAttributes.join(", ")}</div>
</div>
)}
</div> </div>
</div> </div>

View File

@ -65,7 +65,13 @@ export default function SearchFilterDialog({
const { t } = useTranslation(["components/filter"]); const { t } = useTranslation(["components/filter"]);
const [currentFilter, setCurrentFilter] = useState(filter ?? {}); const [currentFilter, setCurrentFilter] = useState(filter ?? {});
const { data: allSubLabels } = useSWR(["sub_labels", { split_joined: 1 }]); const { data: allSubLabels } = useSWR(["sub_labels", { split_joined: 1 }]);
const { data: allAttributes } = useSWR("classification/attributes"); const hasCustomClassificationModels = useMemo(
() => Object.keys(config?.classification?.custom ?? {}).length > 0,
[config],
);
const { data: allAttributes } = useSWR(
hasCustomClassificationModels ? "classification/attributes" : null,
);
const { data: allRecognizedLicensePlates } = useSWR<string[]>( const { data: allRecognizedLicensePlates } = useSWR<string[]>(
"recognized_license_plates", "recognized_license_plates",
); );
@ -92,9 +98,10 @@ export default function SearchFilterDialog({
(currentFilter.max_speed ?? 150) < 150 || (currentFilter.max_speed ?? 150) < 150 ||
(currentFilter.zones?.length ?? 0) > 0 || (currentFilter.zones?.length ?? 0) > 0 ||
(currentFilter.sub_labels?.length ?? 0) > 0 || (currentFilter.sub_labels?.length ?? 0) > 0 ||
(currentFilter.attributes?.length ?? 0) > 0 || (hasCustomClassificationModels &&
(currentFilter.attributes?.length ?? 0) > 0) ||
(currentFilter.recognized_license_plate?.length ?? 0) > 0), (currentFilter.recognized_license_plate?.length ?? 0) > 0),
[currentFilter], [currentFilter, hasCustomClassificationModels],
); );
const trigger = ( const trigger = (
@ -135,13 +142,15 @@ export default function SearchFilterDialog({
setCurrentFilter({ ...currentFilter, sub_labels: newSubLabels }) setCurrentFilter({ ...currentFilter, sub_labels: newSubLabels })
} }
/> />
<AttributeFilterContent {hasCustomClassificationModels && (
allAttributes={allAttributes} <AttributeFilterContent
attributes={currentFilter.attributes} allAttributes={allAttributes}
setAttributes={(newAttributes) => attributes={currentFilter.attributes}
setCurrentFilter({ ...currentFilter, attributes: newAttributes }) setAttributes={(newAttributes) =>
} setCurrentFilter({ ...currentFilter, attributes: newAttributes })
/> }
/>
)}
<RecognizedLicensePlatesFilterContent <RecognizedLicensePlatesFilterContent
allRecognizedLicensePlates={allRecognizedLicensePlates} allRecognizedLicensePlates={allRecognizedLicensePlates}
recognizedLicensePlates={currentFilter.recognized_license_plate} recognizedLicensePlates={currentFilter.recognized_license_plate}
@ -225,6 +234,7 @@ export default function SearchFilterDialog({
max_speed: undefined, max_speed: undefined,
has_snapshot: undefined, has_snapshot: undefined,
has_clip: undefined, has_clip: undefined,
...(hasCustomClassificationModels && { attributes: undefined }),
recognized_license_plate: undefined, recognized_license_plate: undefined,
})); }));
}} }}
@ -1098,7 +1108,7 @@ export function RecognizedLicensePlatesFilterContent({
} }
type AttributeFilterContentProps = { type AttributeFilterContentProps = {
allAttributes: string[]; allAttributes?: string[];
attributes: string[] | undefined; attributes: string[] | undefined;
setAttributes: (labels: string[] | undefined) => void; setAttributes: (labels: string[] | undefined) => void;
}; };

View File

@ -143,7 +143,13 @@ export default function SearchView({
}, [config, searchFilter, allowedCameras]); }, [config, searchFilter, allowedCameras]);
const { data: allSubLabels } = useSWR("sub_labels"); const { data: allSubLabels } = useSWR("sub_labels");
const { data: allAttributes } = useSWR("classification/attributes"); const hasCustomClassificationModels = useMemo(
() => Object.keys(config?.classification?.custom ?? {}).length > 0,
[config],
);
const { data: allAttributes } = useSWR(
hasCustomClassificationModels ? "classification/attributes" : null,
);
const { data: allRecognizedLicensePlates } = useSWR( const { data: allRecognizedLicensePlates } = useSWR(
"recognized_license_plates", "recognized_license_plates",
); );
@ -183,7 +189,7 @@ export default function SearchView({
labels: Object.values(allLabels || {}), labels: Object.values(allLabels || {}),
zones: Object.values(allZones || {}), zones: Object.values(allZones || {}),
sub_labels: allSubLabels, sub_labels: allSubLabels,
attributes: allAttributes, ...(hasCustomClassificationModels && { attributes: allAttributes }),
search_type: ["thumbnail", "description"] as SearchSource[], search_type: ["thumbnail", "description"] as SearchSource[],
time_range: time_range:
config?.ui.time_format == "24hour" config?.ui.time_format == "24hour"
@ -210,6 +216,7 @@ export default function SearchView({
allRecognizedLicensePlates, allRecognizedLicensePlates,
searchFilter, searchFilter,
allowedCameras, allowedCameras,
hasCustomClassificationModels,
], ],
); );