diff --git a/web/package-lock.json b/web/package-lock.json index d33cf0e54..7df832b3a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -48,6 +48,7 @@ "tailwindcss-animate": "^1.0.7", "video.js": "^8.6.1", "videojs-playlist": "^5.1.0", + "vis-timeline": "^7.7.3", "vite-plugin-monaco-editor": "^1.1.0", "zod": "^3.22.4" }, @@ -215,6 +216,18 @@ "resolved": "https://registry.npmjs.org/@cycjimmy/jsmpeg-player/-/jsmpeg-player-6.0.5.tgz", "integrity": "sha512-bVNHQ7VN9ecKT5AI/6RC7zpW/y4ca68a9txeR5Wiin+jKpUn/7buMe+5NPub89A8NNeNnKPQfrD2+c76ch36mA==" }, + "node_modules/@egjs/hammerjs": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", + "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", + "peer": true, + "dependencies": { + "@types/hammerjs": "^2.0.36" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@esbuild/android-arm": { "version": "0.19.8", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.8.tgz", @@ -2253,6 +2266,12 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", "dev": true }, + "node_modules/@types/hammerjs": { + "version": "2.0.45", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.45.tgz", + "integrity": "sha512-qkcUlZmX6c4J8q45taBKTL3p+LbITgyx7qhlPYOdOHZB7B31K0mXbP5YA7i7SgDeEGuI9MnumiKPEMrxg8j3KQ==", + "peer": true + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -3439,6 +3458,15 @@ "node": ">= 6" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3498,6 +3526,12 @@ "node": ">=4" } }, + "node_modules/cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", + "peer": true + }, "node_modules/cssstyle": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", @@ -5184,6 +5218,12 @@ "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, + "node_modules/keycharm": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz", + "integrity": "sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==", + "peer": true + }, "node_modules/keycode": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.0.tgz", @@ -5500,6 +5540,15 @@ "node": ">= 8" } }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/monaco-editor": { "version": "0.44.0", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.44.0.tgz", @@ -6291,6 +6340,15 @@ "node": ">= 0.6.0" } }, + "node_modules/propagating-hammerjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagating-hammerjs/-/propagating-hammerjs-2.0.1.tgz", + "integrity": "sha512-PH3zG5whbSxMocphXJzVtvKr+vWAgfkqVvtuwjSJ/apmEACUoiw6auBAT5HYXpZOR0eGcTAfYG5Yl8h91O5Elg==", + "peer": true, + "peerDependencies": { + "@egjs/hammerjs": "^2.0.17" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -7673,6 +7731,19 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-to-istanbul": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", @@ -7748,6 +7819,57 @@ "global": "^4.3.1" } }, + "node_modules/vis-data": { + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.9.tgz", + "integrity": "sha512-COQsxlVrmcRIbZMMTYwD+C2bxYCFDNQ2EHESklPiInbD/Pk3JZ6qNL84Bp9wWjYjAzXfSlsNaFtRk+hO9yBPWA==", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/visjs" + }, + "peerDependencies": { + "uuid": "^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "vis-util": "^5.0.1" + } + }, + "node_modules/vis-timeline": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/vis-timeline/-/vis-timeline-7.7.3.tgz", + "integrity": "sha512-hGMzTttdOFWaw1PPlJuCXU2/4UjnsIxT684Thg9fV6YU1JuKZJs3s3BrJgZ4hO3gu5i1hsMe1YIi96o+eNT0jg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/visjs" + }, + "peerDependencies": { + "@egjs/hammerjs": "^2.0.0", + "component-emitter": "^1.3.0", + "keycharm": "^0.2.0 || ^0.3.0 || ^0.4.0", + "moment": "^2.24.0", + "propagating-hammerjs": "^1.4.0 || ^2.0.0", + "uuid": "^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "vis-data": "^6.3.0 || ^7.0.0", + "vis-util": "^5.0.1", + "xss": "^1.0.0" + } + }, + "node_modules/vis-util": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.7.tgz", + "integrity": "sha512-E3L03G3+trvc/X4LXvBfih3YIHcKS2WrP0XTdZefr6W6Qi/2nNCqZfe4JFfJU6DcQLm6Gxqj2Pfl+02859oL5A==", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/visjs" + }, + "peerDependencies": { + "@egjs/hammerjs": "^2.0.0", + "component-emitter": "^1.3.0 || ^2.0.0" + } + }, "node_modules/vite": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.5.tgz", @@ -8093,6 +8215,28 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "node_modules/xss": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.14.tgz", + "integrity": "sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==", + "peer": true, + "dependencies": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "bin": { + "xss": "bin/xss" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/xss/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "peer": true + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/web/package.json b/web/package.json index 753bf7776..b1512b431 100644 --- a/web/package.json +++ b/web/package.json @@ -53,6 +53,7 @@ "tailwindcss-animate": "^1.0.7", "video.js": "^8.6.1", "videojs-playlist": "^5.1.0", + "vis-timeline": "^7.7.3", "vite-plugin-monaco-editor": "^1.1.0", "zod": "^3.22.4" }, diff --git a/web/src/App.tsx b/web/src/App.tsx index 294b4cd75..46fcc5b64 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -14,6 +14,7 @@ import ConfigEditor from "@/pages/ConfigEditor"; import Logs from "@/pages/Logs"; import NoMatch from "@/pages/NoMatch"; import Settings from "@/pages/Settings"; +import UIPlayground from "./pages/UIPlayground"; function App() { const [sheetOpen, setSheetOpen] = useState(false); @@ -29,7 +30,10 @@ function App() {
-
+
} /> } /> @@ -40,6 +44,7 @@ function App() { } /> } /> } /> + } /> } />
diff --git a/web/src/components/Header.tsx b/web/src/components/Header.tsx index 3b7a1e6f0..d607b98e6 100644 --- a/web/src/components/Header.tsx +++ b/web/src/components/Header.tsx @@ -119,7 +119,7 @@ function Header({ onToggleNavbar }: HeaderProps) { - + System diff --git a/web/src/components/Sidebar.tsx b/web/src/components/Sidebar.tsx index 069f6307c..be9edb6c0 100644 --- a/web/src/components/Sidebar.tsx +++ b/web/src/components/Sidebar.tsx @@ -1,8 +1,15 @@ import { IconType } from "react-icons"; -import { LuFileUp, LuFilm, LuLayoutDashboard, LuVideo } from "react-icons/lu"; +import { + LuConstruction, + LuFileUp, + LuFilm, + LuLayoutDashboard, + LuVideo, +} from "react-icons/lu"; import { NavLink } from "react-router-dom"; import { Sheet, SheetContent } from "@/components/ui/sheet"; import Logo from "./Logo"; +import { ENV } from "@/env"; const navbarLinks = [ { @@ -29,6 +36,13 @@ const navbarLinks = [ title: "Export", url: "/export", }, + { + id: 5, + icon: LuConstruction, + title: "UI Playground", + url: "/playground", + dev: true, + }, ]; function Sidebar({ @@ -46,6 +60,7 @@ function Sidebar({ Icon={item.icon} title={item.title} url={item.url} + dev={item.dev} onClick={() => setSheetOpen(false)} /> ))} @@ -78,23 +93,28 @@ type SidebarItemProps = { Icon: IconType; title: string; url: string; + dev?: boolean; onClick?: () => void; }; -function SidebarItem({ Icon, title, url, onClick }: SidebarItemProps) { +function SidebarItem({ Icon, title, url, dev, onClick }: SidebarItemProps) { + const shouldRender = dev ? ENV !== "production" : true; + return ( - - `py-4 px-2 flex flex-col lg:flex-row items-center rounded-lg gap-2 lg:w-full hover:bg-border ${ - isActive ? "font-bold bg-popover text-popover-foreground" : "" - }` - } - > - -
{title}
-
+ shouldRender && ( + + `py-4 px-2 flex flex-col lg:flex-row items-center rounded-lg gap-2 lg:w-full hover:bg-border ${ + isActive ? "font-bold bg-popover text-popover-foreground" : "" + }` + } + > + +
{title}
+
+ ) ); } diff --git a/web/src/components/playground/TimelineScrubber.tsx b/web/src/components/playground/TimelineScrubber.tsx new file mode 100644 index 000000000..44bd17eac --- /dev/null +++ b/web/src/components/playground/TimelineScrubber.tsx @@ -0,0 +1,46 @@ +import useSWR from "swr"; +import ActivityScrubber, { ScrubberItem } from "../scrubber/ActivityScrubber"; + +type TimelineScrubberProps = { + eventID: string; +}; + +function timelineEventsToScrubberItems(events: Timeline[]): ScrubberItem[] { + return events.map((event: Timeline, index: number) => ({ + id: index, + content: event.class_type, + start: event.timestamp * 1000, + type: "box", + })); +} + +function generateScrubberOptions(events: Timeline[]) { + const startTime = events[0].timestamp * 1000 - 10; + const endTime = events[events.length - 1].timestamp * 1000 + 10; + + return { start: startTime, end: endTime }; +} + +function TimelineScrubber({ eventID }: TimelineScrubberProps) { + const { data: eventTimeline } = useSWR([ + "timeline", + { + source_id: eventID, + }, + ]); + + return ( + <> + {eventTimeline && ( + <> + + + )} + + ); +} + +export default TimelineScrubber; diff --git a/web/src/components/scrubber/ActivityScrubber.tsx b/web/src/components/scrubber/ActivityScrubber.tsx new file mode 100644 index 000000000..31c9db6ed --- /dev/null +++ b/web/src/components/scrubber/ActivityScrubber.tsx @@ -0,0 +1,180 @@ +import { useEffect, useRef, useState } from "react"; +import { + Timeline as VisTimeline, + TimelineGroup, + TimelineItem, + TimelineOptions, +} from "vis-timeline"; +import type { DataGroup, DataItem, TimelineEvents } from "vis-timeline/types"; +import "./scrubber.css"; + +export type TimelineEventsWithMissing = + | TimelineEvents + | "dragover" + | "markerchange" + | "markerchanged"; + +export type TimelineEventHandler = + | "currentTimeTickHandler" + | "clickHandler" + | "contextmenuHandler" + | "doubleClickHandler" + | "dragoverHandler" + | "dropHandler" + | "mouseOverHandler" + | "mouseDownHandler" + | "mouseUpHandler" + | "mouseMoveHandler" + | "groupDraggedHandler" + | "changedHandler" + | "rangechangeHandler" + | "rangechangedHandler" + | "selectHandler" + | "itemoverHandler" + | "itemoutHandler" + | "timechangeHandler" + | "timechangedHandler" + | "markerchangeHandler" + | "markerchangedHandler"; + +type EventHandler = { + (properties: any): void; +}; + +export type TimelineEventsHandlers = Partial< + Record +>; + +export type ScrubberItem = TimelineItem; + +const domEvents: TimelineEventsWithMissing[] = [ + "currentTimeTick", + "click", + "contextmenu", + "doubleClick", + "dragover", + "drop", + "mouseOver", + "mouseDown", + "mouseUp", + "mouseMove", + "groupDragged", + "changed", + "rangechange", + "rangechanged", + "select", + "itemover", + "itemout", + "timechange", + "timechanged", + "markerchange", + "markerchanged", +]; + +type ActivityScrubberProps = { + items: TimelineItem[]; + groups?: TimelineGroup[]; + options?: TimelineOptions; +} & TimelineEventsHandlers; + +function ActivityScrubber({ + items, + groups, + options, + ...eventHandlers +}: ActivityScrubberProps) { + const containerRef = useRef(null); + const timelineRef = useRef<{ timeline: VisTimeline | null }>({ + timeline: null, + }); + const [currentTime, setCurrentTime] = useState(Date.now()); + + const defaultOptions: TimelineOptions = { + width: "100%", + maxHeight: "350px", + stack: true, + showMajorLabels: true, + showCurrentTime: false, + zoomMin: 10 * 1000, // 10 seconds + // start: new Date(currentTime - 60 * 1 * 60 * 1000), // 1 hour ago + end: currentTime, + max: currentTime, + format: { + minorLabels: { + minute: "h:mma", + hour: "ha", + }, + }, + }; + + useEffect(() => { + const intervalId = setInterval(() => { + setCurrentTime(Date.now()); + }, 60000); // Update every minute + + return () => { + clearInterval(intervalId); + }; + }, []); + + useEffect(() => { + const divElement = containerRef.current; + if (!divElement) { + return; + } + + const timelineInstance = new VisTimeline( + divElement, + items as DataItem[], + groups as DataGroup[], + options + ); + + domEvents.forEach((event) => { + const eventHandler = eventHandlers[`${event}Handler`]; + if (typeof eventHandler === "function") { + timelineInstance.on(event, eventHandler); + } + }); + + timelineRef.current.timeline = timelineInstance; + + const timelineOptions: TimelineOptions = { + ...defaultOptions, + ...options, + }; + + timelineInstance.setOptions(timelineOptions); + + return () => { + timelineInstance.destroy(); + }; + }, []); + + useEffect(() => { + if (!timelineRef.current.timeline) { + return; + } + + // If the currentTime updates, adjust the scrubber's end date and max + // May not be applicable to all scrubbers, might want to just pass this in + // for any scrubbers that we want to dynamically move based on time + // const updatedTimeOptions: TimelineOptions = { + // end: currentTime, + // max: currentTime, + // }; + + const timelineOptions: TimelineOptions = { + ...defaultOptions, + // ...updatedTimeOptions, + ...options, + }; + + timelineRef.current.timeline.setOptions(timelineOptions); + if (items) timelineRef.current.timeline.setItems(items); + }, [items, groups, options, currentTime, eventHandlers]); + + return
; +} + +export default ActivityScrubber; diff --git a/web/src/components/scrubber/scrubber.css b/web/src/components/scrubber/scrubber.css new file mode 100644 index 000000000..9a97ac8fa --- /dev/null +++ b/web/src/components/scrubber/scrubber.css @@ -0,0 +1,670 @@ +.vis-time-axis { + @apply overflow-hidden relative; +} +.vis-time-axis.vis-foreground { + @apply w-full left-0 top-0; +} +.vis-time-axis.vis-background { + @apply h-full absolute w-full left-0 top-0; +} +.vis-time-axis .vis-text { + @apply box-border text-muted-foreground overflow-hidden absolute whitespace-nowrap p-[3px]; +} +.vis-time-axis .vis-text.vis-measure { + @apply absolute invisible mx-0 px-0; +} +.vis-time-axis .vis-grid.vis-vertical { + @apply absolute border-l border-dashed border-muted-foreground; +} +.vis-time-axis .vis-grid.vis-vertical-rtl { + @apply absolute border-r border-dashed border-muted-foreground; +} +.vis-time-axis .vis-grid.vis-minor { + @apply border-foreground; +} +.vis-time-axis .vis-grid.vis-major { + @apply border-muted-foreground; +} +.vis .overlay { + @apply h-full absolute w-full z-10 left-0 top-0; +} +.vis-active { + @apply shadow-[0_0_10px_#86d5f8]; +} +.vis-custom-time { + @apply bg-[#6e94ff] cursor-move w-0.5 z-[1]; +} +.vis-custom-time > .vis-custom-time-marker { + @apply bg-inherit text-white cursor-auto text-xs whitespace-nowrap z-[inherit] px-[5px] py-[3px] top-0; +} +.vis-current-time { + @apply bg-[#ff7f6e] pointer-events-none w-0.5 z-[1]; +} +.vis-rolling-mode-btn { + @apply text-white cursor-pointer text-[28px] font-bold h-10 opacity-80 absolute text-center w-10 rounded-[50%] right-5 top-[7px] before:content-["\26F6"] hover:opacity-100; + background: #3876c2; +} +.vis-panel { + @apply box-border absolute m-0 p-0; +} +.vis-panel.vis-bottom, +.vis-panel.vis-center, +.vis-panel.vis-left, +.vis-panel.vis-right, +.vis-panel.vis-top { + @apply border; +} +.vis-panel.vis-center, +.vis-panel.vis-left, +.vis-panel.vis-right { + @apply overflow-hidden; + border-bottom-style: solid; + border-top-style: solid; +} +.vis-left.vis-panel.vis-vertical-scroll, +.vis-right.vis-panel.vis-vertical-scroll { + @apply h-full overflow-x-hidden overflow-y-scroll; +} +.vis-left.vis-panel.vis-vertical-scroll { + direction: rtl; +} +.vis-left.vis-panel.vis-vertical-scroll .vis-content, +.vis-right.vis-panel.vis-vertical-scroll { + direction: ltr; +} +.vis-right.vis-panel.vis-vertical-scroll .vis-content { + direction: rtl; +} +.vis-panel.vis-bottom, +.vis-panel.vis-center, +.vis-panel.vis-top { + border-left-style: solid; + border-right-style: solid; +} +.vis-background { + @apply overflow-hidden; +} +.vis-panel > .vis-content { + @apply relative; +} +.vis-panel .vis-shadow { + @apply shadow-[0_0_10px_rgba(0,0,0,0.8)] h-px absolute w-full; +} +.vis-panel .vis-shadow.vis-top { + @apply left-0 -top-px; +} +.vis-panel .vis-shadow.vis-bottom { + @apply left-0 -bottom-px; +} +.vis-graph-group0 { + @apply fill-[#4f81bd] stroke-[2px] stroke-[#4f81bd]; + fill-opacity: 0; +} +.vis-graph-group1 { + @apply fill-[#f79646] stroke-[2px] stroke-[#f79646]; + fill-opacity: 0; +} +.vis-graph-group2 { + @apply fill-[#8c51cf] stroke-[2px] stroke-[#8c51cf]; + fill-opacity: 0; +} +.vis-graph-group3 { + @apply fill-[#75c841] stroke-[2px] stroke-[#75c841]; + fill-opacity: 0; +} +.vis-graph-group4 { + @apply fill-[#ff0100] stroke-[2px] stroke-[#ff0100]; + fill-opacity: 0; +} +.vis-graph-group5 { + @apply fill-[#37d8e6] stroke-[2px] stroke-[#37d8e6]; + fill-opacity: 0; +} +.vis-graph-group6 { + @apply fill-[#042662] stroke-[2px] stroke-[#042662]; + fill-opacity: 0; +} +.vis-graph-group7 { + @apply fill-[#00ff26] stroke-[2px] stroke-[#00ff26]; + fill-opacity: 0; +} +.vis-graph-group8 { + @apply fill-[#f0f] stroke-[2px] stroke-[#f0f]; + fill-opacity: 0; +} +.vis-graph-group9 { + @apply fill-[#8f3938] stroke-[2px] stroke-[#8f3938]; + fill-opacity: 0; +} +.vis-timeline .vis-fill { + @apply stroke-none; + fill-opacity: 0.1; +} +.vis-timeline .vis-bar { + @apply stroke-[1px]; + fill-opacity: 0.5; +} +.vis-timeline .vis-point { + @apply stroke-[2px]; + fill-opacity: 1; +} +.vis-timeline .vis-legend-background { + @apply stroke-[1px] fill-white stroke-[#c2c2c2]; + fill-opacity: 0.9; +} +.vis-timeline .vis-outline { + @apply stroke-[1px] fill-white stroke-neutral-200; + fill-opacity: 1; +} +.vis-timeline .vis-icon-fill { + @apply stroke-none; + fill-opacity: 0.3; +} +.vis-timeline { + @apply border box-border overflow-hidden relative m-0 p-0 border-solid border-[#bfbfbf]; +} +.vis-loading-screen { + @apply h-full absolute w-full left-0 top-0; +} +.vis [class*="span"] { + @apply min-h-0 w-auto; +} +.vis-item { + @apply bg-accent border text-foreground inline-block absolute z-[1] border-border; +} +.vis-item.vis-selected { + @apply bg-muted-foreground z-[2] border-muted text-muted; +} +.vis-editable.vis-selected { + @apply cursor-move; +} +.vis-item.vis-point.vis-selected { + @apply bg-muted-foreground; +} +.vis-item.vis-box { + @apply text-center rounded-sm border-solid; +} +.vis-item.vis-point { + background: none; +} +.vis-item.vis-dot { + @apply rounded absolute p-0 border-solid border-4; +} +.vis-item.vis-range { + @apply box-border rounded-sm border-solid; +} +.vis-item.vis-background { + @apply bg-[rgba(213,221,246,0.4)] box-border m-0 p-0 border-[none]; +} +.vis-item .vis-item-overflow { + @apply h-full overflow-hidden relative w-full m-0 p-0; +} +.vis-item-visible-frame { + @apply whitespace-nowrap; +} +.vis-item.vis-range .vis-item-content { + @apply inline-block relative; +} +.vis-item.vis-background .vis-item-content { + @apply inline-block absolute; +} +.vis-item.vis-line { + @apply absolute w-0 p-0 border-l border-solid text-muted; +} +.vis-item .vis-item-content { + @apply box-border whitespace-nowrap p-[5px]; +} +.vis-item .vis-onUpdateTime-tooltip { + @apply text-white absolute text-center transition-[0.4s] whitespace-nowrap w-[200px] p-[5px] rounded-[1px]; + background: #4f81bd; + -o-transition: 0.4s; + -moz-transition: 0.4s; + -webkit-transition: 0.4s; +} +.vis-item .vis-delete, +.vis-item .vis-delete-rtl { + @apply box-border cursor-pointer h-6 absolute transition-[background] duration-200 ease-linear w-6 px-[5px] py-0 top-0; + -webkit-transition: background 0.2s linear; + -moz-transition: background 0.2s linear; + -ms-transition: background 0.2s linear; + -o-transition: background 0.2s linear; +} +.vis-item .vis-delete { + @apply -right-6; +} +.vis-item .vis-delete-rtl { + @apply -left-6; +} +.vis-item .vis-delete-rtl:after, +.vis-item .vis-delete:after { + @apply text-[red] content-["\00D7"] text-[22px] font-bold transition-[color] duration-200 ease-linear; + font-family: arial, sans-serif; + -webkit-transition: color 0.2s linear; + -moz-transition: color 0.2s linear; + -ms-transition: color 0.2s linear; + -o-transition: color 0.2s linear; +} +.vis-item .vis-delete-rtl:hover, +.vis-item .vis-delete:hover { + background: red; +} +.vis-item .vis-delete-rtl:hover:after, +.vis-item .vis-delete:hover:after { + @apply text-white; +} +.vis-item .vis-drag-center { + @apply cursor-move h-full absolute w-full left-0 top-0; +} +.vis-item.vis-range .vis-drag-left { + @apply cursor-w-resize -left-1; +} +.vis-item.vis-range .vis-drag-left, +.vis-item.vis-range .vis-drag-right { + @apply h-full max-w-[20%] min-w-[2px] absolute w-6 top-0; +} +.vis-item.vis-range .vis-drag-right { + @apply cursor-e-resize -right-1; +} +.vis-range.vis-item.vis-readonly .vis-drag-left, +.vis-range.vis-item.vis-readonly .vis-drag-right { + @apply cursor-auto; +} +.vis-item.vis-cluster { + @apply text-center rounded-sm border-solid; + vertical-align: center; +} +.vis-item.vis-cluster-line { + @apply absolute w-0 p-0 border-l; + border-left-style: solid; +} +.vis-item.vis-cluster-dot { + @apply rounded absolute p-0 border-solid border-4; +} +div.vis-tooltip { + @apply bg-[#f5f4ed] border shadow-[3px_3px_10px_rgba(0,0,0,0.2)] text-black text-sm pointer-events-none absolute invisible whitespace-nowrap z-[5] p-[5px] rounded-[3px] border-solid border-[#808074]; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + font-family: verdana; +} +.vis-itemset { + @apply box-border relative m-0 p-0; +} +.vis-itemset .vis-background, +.vis-itemset .vis-foreground { + @apply h-full overflow-visible absolute w-full; +} +.vis-axis { + @apply h-0 absolute w-full z-[1] left-0; +} +.vis-foreground .vis-group { + @apply box-border relative border-b-[#bfbfbf] border-b border-solid last:border-b-[none]; +} +.vis-nesting-group { + @apply cursor-pointer; +} +.vis-label.vis-nested-group.vis-group-level-unknown-but-gte1 { + background: #f5f5f5; +} +.vis-label.vis-nested-group.vis-group-level-0 { + @apply bg-white; +} +.vis-ltr .vis-label.vis-nested-group.vis-group-level-0 .vis-inner { + @apply pl-0; +} +.vis-rtl .vis-label.vis-nested-group.vis-group-level-0 .vis-inner { + @apply pr-0; +} +.vis-label.vis-nested-group.vis-group-level-1 { + @apply bg-[rgba(0,0,0,0.05)]; +} +.vis-ltr .vis-label.vis-nested-group.vis-group-level-1 .vis-inner { + @apply pl-[15px]; +} +.vis-rtl .vis-label.vis-nested-group.vis-group-level-1 .vis-inner { + @apply pr-[15px]; +} +.vis-label.vis-nested-group.vis-group-level-2 { + @apply bg-[rgba(0,0,0,0.1)]; +} +.vis-ltr .vis-label.vis-nested-group.vis-group-level-2 .vis-inner { + @apply pl-[30px]; +} +.vis-rtl .vis-label.vis-nested-group.vis-group-level-2 .vis-inner { + @apply pr-[30px]; +} +.vis-label.vis-nested-group.vis-group-level-3 { + @apply bg-[rgba(0,0,0,0.15)]; +} +.vis-ltr .vis-label.vis-nested-group.vis-group-level-3 .vis-inner { + @apply pl-[45px]; +} +.vis-rtl .vis-label.vis-nested-group.vis-group-level-3 .vis-inner { + @apply pr-[45px]; +} +.vis-label.vis-nested-group.vis-group-level-4 { + @apply bg-[rgba(0,0,0,0.2)]; +} +.vis-ltr .vis-label.vis-nested-group.vis-group-level-4 .vis-inner { + @apply pl-[60px]; +} +.vis-rtl .vis-label.vis-nested-group.vis-group-level-4 .vis-inner { + @apply pr-[60px]; +} +.vis-label.vis-nested-group.vis-group-level-5 { + @apply bg-[rgba(0,0,0,0.25)]; +} +.vis-ltr .vis-label.vis-nested-group.vis-group-level-5 .vis-inner { + @apply pl-[75px]; +} +.vis-rtl .vis-label.vis-nested-group.vis-group-level-5 .vis-inner { + @apply pr-[75px]; +} +.vis-label.vis-nested-group.vis-group-level-6 { + @apply bg-[rgba(0,0,0,0.3)]; +} +.vis-ltr .vis-label.vis-nested-group.vis-group-level-6 .vis-inner { + @apply pl-[90px]; +} +.vis-rtl .vis-label.vis-nested-group.vis-group-level-6 .vis-inner { + @apply pr-[90px]; +} +.vis-label.vis-nested-group.vis-group-level-7 { + @apply bg-[rgba(0,0,0,0.35)]; +} +.vis-ltr .vis-label.vis-nested-group.vis-group-level-7 .vis-inner { + @apply pl-[105px]; +} +.vis-rtl .vis-label.vis-nested-group.vis-group-level-7 .vis-inner { + @apply pr-[105px]; +} +.vis-label.vis-nested-group.vis-group-level-8 { + @apply bg-[rgba(0,0,0,0.4)]; +} +.vis-ltr .vis-label.vis-nested-group.vis-group-level-8 .vis-inner { + @apply pl-[120px]; +} +.vis-rtl .vis-label.vis-nested-group.vis-group-level-8 .vis-inner { + @apply pr-[120px]; +} +.vis-label.vis-nested-group.vis-group-level-9 { + @apply bg-[rgba(0,0,0,0.45)]; +} +.vis-ltr .vis-label.vis-nested-group.vis-group-level-9 .vis-inner { + @apply pl-[135px]; +} +.vis-rtl .vis-label.vis-nested-group.vis-group-level-9 .vis-inner { + @apply pr-[135px]; +} +.vis-label.vis-nested-group { + @apply bg-[rgba(0,0,0,0.5)]; +} +.vis-ltr .vis-label.vis-nested-group .vis-inner { + @apply pl-[150px]; +} +.vis-rtl .vis-label.vis-nested-group .vis-inner { + @apply pr-[150px]; +} +.vis-group-level-unknown-but-gte1 { + @apply border border-solid border-[red]; +} +.vis-label.vis-nesting-group:before { + @apply inline-block w-[15px]; +} +.vis-label.vis-nesting-group.expanded:before { + @apply content-["\25BC"]; +} +.vis-label.vis-nesting-group.collapsed:before { + @apply content-["\25B6"]; +} +.vis-rtl .vis-label.vis-nesting-group.collapsed:before { + @apply content-["\25C0"]; +} +.vis-ltr .vis-label:not(.vis-nesting-group):not(.vis-group-level-0) { + @apply pl-[15px]; +} +.vis-rtl .vis-label:not(.vis-nesting-group):not(.vis-group-level-0) { + @apply pr-[15px]; +} +.vis-overlay { + @apply h-full absolute w-full z-10 left-0 top-0; +} +.vis-labelset { + @apply overflow-hidden; +} +.vis-labelset, +.vis-labelset .vis-label { + @apply box-border relative; +} +.vis-labelset .vis-label { + @apply text-[#4d4d4d] w-full border-b-[#bfbfbf] border-b border-solid left-0 top-0 last:border-b-[none]; +} +.vis-labelset .vis-label.draggable { + @apply cursor-pointer; +} +.vis-group-is-dragging { + background: rgba(0, 0, 0, 0.1); +} +.vis-labelset .vis-label .vis-inner { + @apply inline-block p-[5px]; +} +.vis-labelset .vis-label .vis-inner.vis-hidden { + @apply p-0; +} +div.vis-configuration { + @apply block float-left text-xs relative; +} +div.vis-configuration-wrapper { + @apply block w-[700px] after:clear-both after:content-[""] after:block; +} +div.vis-configuration.vis-config-option-container { + @apply bg-white rounded block w-[495px] mt-5 pl-[5px] border-2 border-solid border-[#f7f8fa] left-2.5; +} +div.vis-configuration.vis-config-button { + @apply bg-[#f7f8fa] rounded cursor-pointer block h-[25px] leading-[25px] align-middle w-[495px] mt-5 mb-[30px] pl-[5px] border-2 border-solid border-[#ceced0] left-2.5; +} +div.vis-configuration.vis-config-button.hover { + @apply bg-[#4588e6] text-white border-2 border-solid border-[#214373]; +} +div.vis-configuration.vis-config-item { + @apply block float-left h-[25px] leading-[25px] align-middle w-[495px]; +} +div.vis-configuration.vis-config-item.vis-config-s2 { + @apply bg-[#f7f8fa] pl-[5px] rounded-[3px] left-2.5; +} +div.vis-configuration.vis-config-item.vis-config-s3 { + @apply bg-[#e4e9f0] pl-[5px] rounded-[3px] left-5; +} +div.vis-configuration.vis-config-item.vis-config-s4 { + @apply bg-[#cfd8e6] pl-[5px] rounded-[3px] left-[30px]; +} +div.vis-configuration.vis-config-header { + @apply text-lg font-bold; +} +div.vis-configuration.vis-config-label { + @apply h-[25px] leading-[25px] w-[120px]; +} +div.vis-configuration.vis-config-label.vis-config-s3 { + @apply w-[110px]; +} +div.vis-configuration.vis-config-label.vis-config-s4 { + @apply w-[100px]; +} +div.vis-configuration.vis-config-colorBlock { + @apply border cursor-pointer h-[19px] w-[30px] m-0 p-0 rounded-sm border-solid border-[#444] top-px; +} +input.vis-configuration.vis-config-checkbox { + @apply left-[-5px]; +} +input.vis-configuration.vis-config-rangeinput { + @apply pointer-events-none relative top-[-5px] w-[60px] m-0 p-px; +} +input.vis-configuration.vis-config-range { + @apply bg-transparent h-5 w-[300px] border-0 border-solid border-white; + -webkit-appearance: none; +} +input.vis-configuration.vis-config-range::-webkit-slider-runnable-track { + @apply border shadow-[0_0_3px_0_#aaa] h-[5px] w-[300px] rounded-[3px] border-solid border-[#999]; + background: #dedede; + background: -moz-linear-gradient(top, #dedede 0, #c8c8c8 99%); + background: -webkit-gradient( + linear, + left top, + left bottom, + color-stop(0, #dedede), + color-stop(99%, #c8c8c8) + ); + background: -webkit-linear-gradient(top, #dedede, #c8c8c8 99%); + background: -o-linear-gradient(top, #dedede 0, #c8c8c8 99%); + background: -ms-linear-gradient(top, #dedede 0, #c8c8c8 99%); + background: linear-gradient(180deg, #dedede 0, #c8c8c8 99%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#dedede",endColorstr="#c8c8c8",GradientType=0); +} +input.vis-configuration.vis-config-range::-webkit-slider-thumb { + @apply border shadow-[0_0_1px_0_#111927] h-[17px] mt-[-7px] w-[17px] rounded-[50%] border-solid border-[#14334b]; + -webkit-appearance: none; + background: #3876c2; + background: -moz-linear-gradient(top, #3876c2 0, #385380 100%); + background: -webkit-gradient( + linear, + left top, + left bottom, + color-stop(0, #3876c2), + color-stop(100%, #385380) + ); + background: -webkit-linear-gradient(top, #3876c2, #385380); + background: -o-linear-gradient(top, #3876c2 0, #385380 100%); + background: -ms-linear-gradient(top, #3876c2 0, #385380 100%); + background: linear-gradient(180deg, #3876c2 0, #385380); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#3876c2",endColorstr="#385380",GradientType=0); +} +input.vis-configuration.vis-config-range:focus { + outline: none; +} +input.vis-configuration.vis-config-range:focus::-webkit-slider-runnable-track { + background: #9d9d9d; + background: -moz-linear-gradient(top, #9d9d9d 0, #c8c8c8 99%); + background: -webkit-gradient( + linear, + left top, + left bottom, + color-stop(0, #9d9d9d), + color-stop(99%, #c8c8c8) + ); + background: -webkit-linear-gradient(top, #9d9d9d, #c8c8c8 99%); + background: -o-linear-gradient(top, #9d9d9d 0, #c8c8c8 99%); + background: -ms-linear-gradient(top, #9d9d9d 0, #c8c8c8 99%); + background: linear-gradient(180deg, #9d9d9d 0, #c8c8c8 99%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#9d9d9d",endColorstr="#c8c8c8",GradientType=0); +} +input.vis-configuration.vis-config-range::-moz-range-track { + @apply border shadow-[0_0_3px_0_#aaa] h-2.5 w-[300px] rounded-[3px] border-solid border-[#999]; + background: #dedede; + background: -moz-linear-gradient(top, #dedede 0, #c8c8c8 99%); + background: -webkit-gradient( + linear, + left top, + left bottom, + color-stop(0, #dedede), + color-stop(99%, #c8c8c8) + ); + background: -webkit-linear-gradient(top, #dedede, #c8c8c8 99%); + background: -o-linear-gradient(top, #dedede 0, #c8c8c8 99%); + background: -ms-linear-gradient(top, #dedede 0, #c8c8c8 99%); + background: linear-gradient(180deg, #dedede 0, #c8c8c8 99%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#dedede",endColorstr="#c8c8c8",GradientType=0); +} +input.vis-configuration.vis-config-range::-moz-range-thumb { + @apply h-4 w-4 rounded-[50%] border-[none]; + background: #385380; +} +input.vis-configuration.vis-config-range:-moz-focusring { + @apply -outline-offset-1; + outline: 1px solid #fff; +} +input.vis-configuration.vis-config-range::-ms-track { + @apply text-transparent h-[5px] w-[300px] border-[6px_0]; + background: transparent; +} +input.vis-configuration.vis-config-range::-ms-fill-lower { + @apply rounded-[10px]; + background: #777; +} +input.vis-configuration.vis-config-range::-ms-fill-upper { + @apply rounded-[10px]; + background: #ddd; +} +input.vis-configuration.vis-config-range::-ms-thumb { + @apply h-4 w-4 rounded-[50%] border-[none]; + background: #385380; +} +input.vis-configuration.vis-config-range:focus::-ms-fill-lower { + background: #888; +} +input.vis-configuration.vis-config-range:focus::-ms-fill-upper { + background: #ccc; +} +.vis-configuration-popup { + @apply rounded text-white text-sm h-[30px] leading-[30px] absolute text-center transition-opacity duration-300 ease-in-out w-[150px] border-2 border-solid border-[#f2faff] after:-mt-2 after:border-[rgba(136,183,213,0)_rgba(136,183,213,0)_rgba(136,183,213,0)_rgba(57,76,89,0.85)] after:border-8 before:-mt-3 before:border-[rgba(194,225,245,0)_rgba(194,225,245,0)_rgba(194,225,245,0)_#f2faff] before:border-[12px]; + background: rgba(57, 76, 89, 0.85); + -webkit-transition: opacity 0.3s ease-in-out; + -moz-transition: opacity 0.3s ease-in-out; +} +.vis-configuration-popup:after, +.vis-configuration-popup:before { + @apply content-["_"] h-0 pointer-events-none absolute w-0 border-[solid] left-full top-2/4; +} +.vis-panel.vis-background.vis-horizontal .vis-grid.vis-horizontal { + @apply h-0 absolute w-full border-b border-solid; +} +.vis-panel.vis-background.vis-horizontal .vis-grid.vis-minor { + @apply border-neutral-200; +} +.vis-panel.vis-background.vis-horizontal .vis-grid.vis-major { + @apply border-[#bfbfbf]; +} +.vis-data-axis .vis-y-axis.vis-major { + @apply text-[#4d4d4d] absolute whitespace-nowrap w-full; +} +.vis-data-axis .vis-y-axis.vis-major.vis-measure { + @apply invisible w-auto m-0 p-0 border-0; +} +.vis-data-axis .vis-y-axis.vis-minor { + @apply text-[#bebebe] absolute whitespace-nowrap w-full; +} +.vis-data-axis .vis-y-axis.vis-minor.vis-measure { + @apply invisible w-auto m-0 p-0 border-0; +} +.vis-data-axis .vis-y-axis.vis-title { + @apply text-[#4d4d4d] absolute text-center whitespace-nowrap bottom-5; +} +.vis-data-axis .vis-y-axis.vis-title.vis-measure { + @apply invisible w-auto m-0 p-0; +} +.vis-data-axis .vis-y-axis.vis-title.vis-left { + @apply -rotate-90 origin-[left_bottom] bottom-0; + -webkit-transform: rotate(-90deg); + -moz-transform: rotate(-90deg); + -ms-transform: rotate(-90deg); + -o-transform: rotate(-90deg); + -webkit-transform-origin: left top; + -moz-transform-origin: left top; + -ms-transform-origin: left top; + -o-transform-origin: left top; +} +.vis-data-axis .vis-y-axis.vis-title.vis-right { + @apply rotate-90 origin-[right_bottom] bottom-0; + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + -o-transform: rotate(90deg); + -webkit-transform-origin: right bottom; + -moz-transform-origin: right bottom; + -ms-transform-origin: right bottom; + -o-transform-origin: right bottom; +} +.vis-legend { + @apply bg-[rgba(247,252,255,0.65)] border shadow-[2px_2px_10px_hsla(0,0%,60%,0.55)] p-[5px] border-solid border-[#b3b3b3]; +} +.vis-legend-text { + @apply inline-block whitespace-nowrap; +} diff --git a/web/src/env.ts b/web/src/env.ts new file mode 100644 index 000000000..2ea76dc8f --- /dev/null +++ b/web/src/env.ts @@ -0,0 +1 @@ +export const ENV = import.meta.env.MODE; \ No newline at end of file diff --git a/web/src/pages/UIPlayground.tsx b/web/src/pages/UIPlayground.tsx new file mode 100644 index 000000000..1359f77bf --- /dev/null +++ b/web/src/pages/UIPlayground.tsx @@ -0,0 +1,138 @@ +import { useCallback, useMemo, useState } from "react"; +import Heading from "@/components/ui/heading"; +import ActivityScrubber, { + ScrubberItem, +} from "@/components/scrubber/ActivityScrubber"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { Event } from "@/types/event"; +import ActivityIndicator from "@/components/ui/activity-indicator"; +import { useApiHost } from "@/api"; +import TimelineScrubber from "@/components/playground/TimelineScrubber"; + +// Color data +const colors = [ + "background", + "foreground", + "card", + "card-foreground", + "popover", + "popover-foreground", + "primary", + "primary-foreground", + "secondary", + "secondary-foreground", + "muted", + "muted-foreground", + "accent", + "accent-foreground", + "destructive", + "destructive-foreground", + "border", + "input", + "ring", +]; + +function ColorSwatch({ name, value }: { name: string; value: string }) { + return ( +
+
+ {name} +
+ ); +} + +function eventsToScrubberItems(events: Event[]): ScrubberItem[] { + const apiHost = useApiHost(); + + return events.map((event: Event) => ({ + id: event.id, + content: `
${event.label}
`, + start: new Date(event.start_time * 1000), + end: event.end_time ? new Date(event.end_time * 1000) : undefined, + type: "box", + })); +} + +function UIPlayground() { + const { data: config } = useSWR("config"); + const [timeline, setTimeline] = useState(undefined); + + const onSelect = useCallback(({ items }: { items: string[] }) => { + setTimeline(items[0]); + }, []); + + const recentTimestamp = useMemo(() => { + const now = new Date(); + now.setMinutes(now.getMinutes() - 240); + return now.getTime() / 1000; + }, []); + const { data: events } = useSWR([ + "events", + { limit: 10, after: recentTimestamp }, + ]); + + return ( + <> + UI Playground + + + Scrubber + +

+ Shows the 10 most recent events within the last 4 hours +

+ + {!config && } + + {config && ( +
+ {events && events.length > 0 && ( + <> + + + )} +
+ )} + + {config && ( +
+ {timeline && ( + <> + + + )} +
+ )} + + + Color scheme + +

+ Colors as set by the current theme. See the{" "} + + shadcn theming docs + {" "} + for usage. +

+ +
+ {colors.map((color, index) => ( + + ))} +
+ + ); +} + +export default UIPlayground;