Use new UI (#8983)

* fixup build

* swap frontends
This commit is contained in:
Blake Blackshear
2023-12-16 16:20:59 +00:00
parent a2c6f45454
commit bdebb99b5a
286 changed files with 20010 additions and 20007 deletions

View File

@@ -1,74 +0,0 @@
import type { TimelineEvent } from '../../components/Timeline/TimelineEvent';
import type { TimelineEventBlock } from '../../components/Timeline/TimelineEventBlock';
import { epochToLong, longToDate } from '../dateUtil';
export const checkEventForOverlap = (firstEvent: TimelineEvent, secondEvent: TimelineEvent) => {
if (secondEvent.startTime < firstEvent.endTime && secondEvent.startTime > firstEvent.startTime) {
return true;
}
return false;
};
export const getTimelineEventBlocksFromTimelineEvents = (events: TimelineEvent[], xOffset: number): TimelineEventBlock[] => {
const firstEvent = events[0];
const firstEventTime = longToDate(firstEvent.start_time);
return events
.map((e, index) => {
const startTime = longToDate(e.start_time);
const endTime = e.end_time ? longToDate(e.end_time) : new Date();
const seconds = Math.round(Math.abs(endTime.getTime() - startTime.getTime()) / 1000);
const positionX = Math.round(Math.abs(startTime.getTime() - firstEventTime.getTime()) / 1000 + xOffset);
return {
...e,
startTime,
endTime,
width: seconds,
positionX,
index,
} as TimelineEventBlock;
})
.reduce((rowMap, current) => {
for (let i = 0; i < rowMap.length; i++) {
const row = rowMap[i] ?? [];
const lastItem = row[row.length - 1];
if (lastItem) {
const isOverlap = checkEventForOverlap(lastItem, current);
if (isOverlap) {
continue;
}
}
rowMap[i] = [...row, current];
return rowMap;
}
rowMap.push([current]);
return rowMap;
}, [] as TimelineEventBlock[][])
.flatMap((rows, rowPosition) => {
rows.forEach((eventBlock) => {
const OFFSET_DISTANCE_IN_PIXELS = 10;
eventBlock.yOffset = OFFSET_DISTANCE_IN_PIXELS * rowPosition;
});
return rows;
})
.sort((a, b) => a.startTime.getTime() - b.startTime.getTime());
};
export const findLargestYOffsetInBlocks = (blocks: TimelineEventBlock[]): number => {
return blocks.reduce((largestYOffset, current) => {
if (current.yOffset > largestYOffset) {
return current.yOffset;
}
return largestYOffset;
}, 0);
};
export const getTimelineWidthFromBlocks = (blocks: TimelineEventBlock[], offset: number): number => {
const firstBlock = blocks[0];
if (firstBlock) {
const startTimeEpoch = firstBlock.startTime.getTime();
const endTimeEpoch = Date.now();
const timelineDurationLong = epochToLong(endTimeEpoch - startTimeEpoch);
return timelineDurationLong + offset * 2;
}
return 0;
};

View File

@@ -35,34 +35,49 @@ export const getNowYesterdayInLong = (): number => {
* @param config An object containing the configuration options for date/time display
* @returns The formatted date/time string, or "Invalid time" if the Unix timestamp is not provided or invalid.
*/
interface DateTimeStyle {
timezone: string;
time_format: 'browser' | '12hour' | '24hour';
date_style: 'full' | 'long' | 'medium' | 'short';
time_style: 'full' | 'long' | 'medium' | 'short';
strftime_fmt: string;
}
// only used as a fallback if the browser does not support dateStyle/timeStyle in Intl.DateTimeFormat
const formatMap: {
[k: string]: {
date: { year: 'numeric' | '2-digit'; month: 'long' | 'short' | '2-digit'; day: 'numeric' | '2-digit' };
time: { hour: 'numeric'; minute: 'numeric'; second?: 'numeric'; timeZoneName?: 'short' | 'long' };
date: {
year: "numeric" | "2-digit";
month: "long" | "short" | "2-digit";
day: "numeric" | "2-digit";
};
time: {
hour: "numeric";
minute: "numeric";
second?: "numeric";
timeZoneName?: "short" | "long";
};
};
} = {
full: {
date: { year: 'numeric', month: 'long', day: 'numeric' },
time: { hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'long' },
date: { year: "numeric", month: "long", day: "numeric" },
time: {
hour: "numeric",
minute: "numeric",
second: "numeric",
timeZoneName: "long",
},
},
long: {
date: { year: 'numeric', month: 'long', day: 'numeric' },
time: { hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'long' },
date: { year: "numeric", month: "long", day: "numeric" },
time: {
hour: "numeric",
minute: "numeric",
second: "numeric",
timeZoneName: "long",
},
},
medium: {
date: { year: 'numeric', month: 'short', day: 'numeric' },
time: { hour: 'numeric', minute: 'numeric', second: 'numeric' },
date: { year: "numeric", month: "short", day: "numeric" },
time: { hour: "numeric", minute: "numeric", second: "numeric" },
},
short: {
date: { year: "2-digit", month: "2-digit", day: "2-digit" },
time: { hour: "numeric", minute: "numeric" },
},
short: { date: { year: '2-digit', month: '2-digit', day: '2-digit' }, time: { hour: 'numeric', minute: 'numeric' } },
};
/**
@@ -85,11 +100,11 @@ const getResolvedTimeZone = () => {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
} catch (error) {
const offsetMinutes = new Date().getTimezoneOffset();
return `UTC${offsetMinutes < 0 ? '+' : '-'}${Math.abs(offsetMinutes / 60)
return `UTC${offsetMinutes < 0 ? "+" : "-"}${Math.abs(offsetMinutes / 60)
.toString()
.padStart(2, '0')}:${Math.abs(offsetMinutes % 60)
.padStart(2, "0")}:${Math.abs(offsetMinutes % 60)
.toString()
.padStart(2, '0')}`;
.padStart(2, "0")}`;
}
};
@@ -109,11 +124,21 @@ const getResolvedTimeZone = () => {
*
* @throws {Error} If the given unixTimestamp is not a valid number, the function will return 'Invalid time'.
*/
export const formatUnixTimestampToDateTime = (unixTimestamp: number, config: DateTimeStyle): string => {
const { timezone, time_format, date_style, time_style, strftime_fmt } = config;
const locale = window.navigator?.language || 'en-us';
export const formatUnixTimestampToDateTime = (
unixTimestamp: number,
config: {
timezone?: string;
time_format?: "browser" | "12hour" | "24hour";
date_style?: "full" | "long" | "medium" | "short";
time_style?: "full" | "long" | "medium" | "short";
strftime_fmt?: string;
}
): string => {
const { timezone, time_format, date_style, time_style, strftime_fmt } =
config;
const locale = window.navigator?.language || "en-US";
if (isNaN(unixTimestamp)) {
return 'Invalid time';
return "Invalid time";
}
try {
@@ -123,7 +148,7 @@ export const formatUnixTimestampToDateTime = (unixTimestamp: number, config: Dat
// use strftime_fmt if defined in config
if (strftime_fmt) {
const offset = getUTCOffset(date, timezone || resolvedTimeZone);
const strftime_locale = strftime.timezone(offset).localizeByIdentifier(locale);
const strftime_locale = strftime.timezone(offset);
return strftime_locale(strftime_fmt, date);
}
@@ -131,7 +156,7 @@ export const formatUnixTimestampToDateTime = (unixTimestamp: number, config: Dat
const options: Intl.DateTimeFormatOptions = {
dateStyle: date_style,
timeStyle: time_style,
hour12: time_format !== 'browser' ? time_format == '12hour' : undefined,
hour12: time_format !== "browser" ? time_format == "12hour" : undefined,
};
// Only set timeZone option when resolvedTimeZone does not match UTC±HH:MM format, or when timezone is set in config
@@ -149,15 +174,26 @@ export const formatUnixTimestampToDateTime = (unixTimestamp: number, config: Dat
// fallback if the browser does not support dateStyle/timeStyle in Intl.DateTimeFormat
// This works even tough the timezone is undefined, it will use the runtime's default time zone
if (!containsTime) {
const dateOptions = { ...formatMap[date_style]?.date, timeZone: options.timeZone, hour12: options.hour12 };
const timeOptions = { ...formatMap[time_style]?.time, timeZone: options.timeZone, hour12: options.hour12 };
const dateOptions = {
...formatMap[date_style ?? ""]?.date,
timeZone: options.timeZone,
hour12: options.hour12,
};
const timeOptions = {
...formatMap[time_style ?? ""]?.time,
timeZone: options.timeZone,
hour12: options.hour12,
};
return `${date.toLocaleDateString(locale, dateOptions)} ${date.toLocaleTimeString(locale, timeOptions)}`;
return `${date.toLocaleDateString(
locale,
dateOptions
)} ${date.toLocaleTimeString(locale, timeOptions)}`;
}
return formattedDateTime;
} catch (error) {
return 'Invalid time';
return "Invalid time";
}
};
@@ -175,28 +211,31 @@ interface DurationToken {
* @param end_time: number|null - Unix timestamp for end time
* @returns string - duration or 'In Progress' if end time is not provided
*/
export const getDurationFromTimestamps = (start_time: number, end_time: number | null): string => {
export const getDurationFromTimestamps = (
start_time: number,
end_time: number | null
): string => {
if (isNaN(start_time)) {
return 'Invalid start time';
return "Invalid start time";
}
let duration = 'In Progress';
let duration = "In Progress";
if (end_time !== null) {
if (isNaN(end_time)) {
return 'Invalid end time';
return "Invalid end time";
}
const start = fromUnixTime(start_time);
const end = fromUnixTime(end_time);
const formatDistanceLocale: DurationToken = {
xSeconds: '{{count}}s',
xMinutes: '{{count}}m',
xHours: '{{count}}h',
xSeconds: "{{count}}s",
xMinutes: "{{count}}m",
xHours: "{{count}}h",
};
const shortEnLocale = {
formatDistance: (token: keyof DurationToken, count: number) =>
formatDistanceLocale[token].replace('{{count}}', count.toString()),
formatDistanceLocale[token].replace("{{count}}", count.toString()),
};
duration = formatDuration(intervalToDuration({ start, end }), {
format: ['hours', 'minutes', 'seconds'],
format: ["hours", "minutes", "seconds"],
locale: shortEnLocale,
});
}
@@ -215,14 +254,18 @@ const getUTCOffset = (date: Date, timezone: string): number => {
if (utcOffsetMatch) {
const hours = parseInt(utcOffsetMatch[2], 10);
const minutes = parseInt(utcOffsetMatch[3], 10);
return (utcOffsetMatch[1] === '+' ? 1 : -1) * (hours * 60 + minutes);
return (utcOffsetMatch[1] === "+" ? 1 : -1) * (hours * 60 + minutes);
}
// Otherwise, calculate offset using provided timezone
const utcDate = new Date(date.getTime() - date.getTimezoneOffset() * 60 * 1000);
const utcDate = new Date(
date.getTime() - date.getTimezoneOffset() * 60 * 1000
);
// locale of en-CA is required for proper locale format
let iso = utcDate.toLocaleString('en-CA', { timeZone: timezone, hour12: false }).replace(', ', 'T');
iso += `.${utcDate.getMilliseconds().toString().padStart(3, '0')}`;
let iso = utcDate
.toLocaleString("en-CA", { timeZone: timezone, hour12: false })
.replace(", ", "T");
iso += `.${utcDate.getMilliseconds().toString().padStart(3, "0")}`;
let target = new Date(`${iso}Z`);
// safari doesn't like the default format

View File

@@ -1 +0,0 @@
export const isNullOrUndefined = (object?: unknown): boolean => object === null || object === undefined;

View File

@@ -1,15 +0,0 @@
import type { TimelineEvent } from '../../components/Timeline/TimelineEvent';
export const getColorFromTimelineEvent = (event: TimelineEvent) => {
const { label } = event;
if (label === 'car') {
return 'bg-red-400';
} else if (label === 'person') {
return 'bg-blue-400';
} else if (label === 'dog') {
return 'bg-green-400';
}
// unknown label
return 'bg-gray-400';
};

View File

@@ -0,0 +1,80 @@
import { LuCircle, LuPlay, LuPlayCircle, LuTruck } from "react-icons/lu";
import { IoMdExit } from "react-icons/io";
import {
MdFaceUnlock,
MdOutlineLocationOn,
MdOutlinePictureInPictureAlt,
} from "react-icons/md";
export function getTimelineIcon(timelineItem: Timeline) {
switch (timelineItem.class_type) {
case "visible":
return <LuPlay className="w-4 mr-1" />;
case "gone":
return <IoMdExit className="w-4 mr-1" />;
case "active":
return <LuPlayCircle className="w-4 mr-1" />;
case "stationary":
return <LuCircle className="w-4 mr-1" />;
case "entered_zone":
return <MdOutlineLocationOn className="w-4 mr-1" />;
case "attribute":
switch (timelineItem.data.attribute) {
case "face":
return <MdFaceUnlock className="w-4 mr-1" />;
case "license_plate":
return <MdOutlinePictureInPictureAlt className="w-4 mr-1" />;
default:
return <LuTruck className="w-4 mr-1" />;
}
case "sub_label":
switch (timelineItem.data.label) {
case "person":
return <MdFaceUnlock className="w-4 mr-1" />;
case "car":
return <MdOutlinePictureInPictureAlt className="w-4 mr-1" />;
}
}
}
export function getTimelineItemDescription(timelineItem: Timeline) {
const label = (
(Array.isArray(timelineItem.data.sub_label)
? timelineItem.data.sub_label[0]
: timelineItem.data.sub_label) || timelineItem.data.label
).replaceAll("_", " ");
switch (timelineItem.class_type) {
case "visible":
return `${label} detected`;
case "entered_zone":
return `${label} entered ${timelineItem.data.zones
.join(" and ")
.replaceAll("_", " ")}`;
case "active":
return `${label} became active`;
case "stationary":
return `${label} became stationary`;
case "attribute": {
let title = "";
if (
timelineItem.data.attribute == "face" ||
timelineItem.data.attribute == "license_plate"
) {
title = `${timelineItem.data.attribute.replaceAll(
"_",
" "
)} detected for ${label}`;
} else {
title = `${
timelineItem.data.sub_label
} recognized as ${timelineItem.data.attribute.replaceAll("_", " ")}`;
}
return title;
}
case "sub_label":
return `${timelineItem.data.label} recognized as ${timelineItem.data.sub_label}`;
case "gone":
return `${label} left`;
}
}

View File

@@ -1,3 +0,0 @@
export const convertRemToPixels = (rem: number): number => {
return rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
};