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"
},
"recognizedLicensePlate": "Recognized License Plate",
"attributes": "Classification Attributes",
"estimatedSpeed": "Estimated Speed",
"objects": "Objects",
"camera": "Camera",

View File

@ -678,6 +678,15 @@ function ObjectDetailsTab({
]);
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
@ -807,6 +816,24 @@ function ObjectDetailsTab({
}
}, [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 candidate = Array.isArray(key) ? key[0] : key;
const EVENTS_KEY_PATTERNS = ["events", "events/search", "events/explore"];
@ -1295,6 +1322,15 @@ function ObjectDetailsTab({
</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>

View File

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

View File

@ -143,7 +143,13 @@ export default function SearchView({
}, [config, searchFilter, allowedCameras]);
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(
"recognized_license_plates",
);
@ -183,7 +189,7 @@ export default function SearchView({
labels: Object.values(allLabels || {}),
zones: Object.values(allZones || {}),
sub_labels: allSubLabels,
attributes: allAttributes,
...(hasCustomClassificationModels && { attributes: allAttributes }),
search_type: ["thumbnail", "description"] as SearchSource[],
time_range:
config?.ui.time_format == "24hour"
@ -210,6 +216,7 @@ export default function SearchView({
allRecognizedLicensePlates,
searchFilter,
allowedCameras,
hasCustomClassificationModels,
],
);