diff --git a/frontend/src/lib/api/api.ts b/frontend/src/lib/api/api.ts index 0df2e344cd..83c23d9894 100644 --- a/frontend/src/lib/api/api.ts +++ b/frontend/src/lib/api/api.ts @@ -1,4 +1,8 @@ -import type { ModelsResponse, TimeSeriesResponse } from '$lib/types/Model/Model'; +import type { + JoinTimeSeriesResponse, + ModelsResponse, + TimeSeriesResponse +} from '$lib/types/Model/Model'; import { error } from '@sveltejs/kit'; import { browser } from '$app/environment'; @@ -52,3 +56,24 @@ export async function search(term: string, limit: number = 20): Promise { + const params = new URLSearchParams({ + startTs: startTs.toString(), + endTs: endTs.toString(), + metricType, + metrics, + offset, + algorithm + }); + + return get(`join/${joinId}/timeseries?${params.toString()}`); +} diff --git a/frontend/src/lib/components/CollapsibleSection/CollapsibleSection.svelte b/frontend/src/lib/components/CollapsibleSection/CollapsibleSection.svelte new file mode 100644 index 0000000000..f0b559105b --- /dev/null +++ b/frontend/src/lib/components/CollapsibleSection/CollapsibleSection.svelte @@ -0,0 +1,27 @@ + + + + + +

{title}

+
+ + {@render children()} + +
diff --git a/frontend/src/lib/components/EChart/EChart.svelte b/frontend/src/lib/components/EChart/EChart.svelte index 8e492de762..534d951ade 100644 --- a/frontend/src/lib/components/EChart/EChart.svelte +++ b/frontend/src/lib/components/EChart/EChart.svelte @@ -2,21 +2,48 @@ import { onMount, onDestroy, createEventDispatcher } from 'svelte'; import * as echarts from 'echarts'; import type { ECElementEvent, EChartOption } from 'echarts'; + import merge from 'lodash/merge'; - const { option }: { option: EChartOption } = $props(); + let { + option, + chartInstance = $bindable(), + enableMousemove = false + }: { + option: EChartOption; + chartInstance: echarts.ECharts | null; + enableMousemove?: boolean; + } = $props(); const dispatch = createEventDispatcher(); let chartDiv: HTMLDivElement; - let chartInstance: echarts.ECharts; let resizeObserver: ResizeObserver; + // todo this needs to change dynamically when we support light mode + const theme = $derived('dark'); + const defaultOption: EChartOption = { + backgroundColor: 'transparent' + }; + + const mergedOption: EChartOption = $derived.by(() => { + return merge({}, defaultOption, option); + }); + function initChart() { if (!chartDiv) return; chartInstance?.dispose(); - chartInstance = echarts.init(chartDiv); - chartInstance.setOption(option); + chartInstance = echarts.init(chartDiv, theme); + chartInstance.setOption(mergedOption); chartInstance.on('click', (params: ECElementEvent) => dispatch('click', params)); + chartInstance.on('datazoom', (params: EChartOption.DataZoom) => dispatch('datazoom', params)); + + if (enableMousemove) { + chartInstance.on('mousemove', (params: ECElementEvent) => { + if (params.componentType === 'series') { + chartInstance?.getZr().setCursorStyle('pointer'); + } + }); + } } function handleResize() { @@ -35,7 +62,7 @@ }); $effect(() => { - chartInstance?.setOption(option, true); + chartInstance?.setOption(mergedOption, true); }); diff --git a/frontend/src/lib/components/NavigationBar/NavigationBar.svelte b/frontend/src/lib/components/NavigationBar/NavigationBar.svelte index 339bb63d15..cd57c1841f 100644 --- a/frontend/src/lib/components/NavigationBar/NavigationBar.svelte +++ b/frontend/src/lib/components/NavigationBar/NavigationBar.svelte @@ -15,6 +15,13 @@ import type { Model } from '$lib/types/Model/Model'; import debounce from 'lodash/debounce'; import { onDestroy } from 'svelte'; + import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem + } from '$lib/components/ui/dropdown-menu/'; + import { ChevronDown } from 'svelte-radix'; type Props = { navItems: { label: string; href: string }[]; @@ -73,13 +80,24 @@ {/each}
- {user.name} - - - - - - + + + + + + alert('Settings clicked')}>Settings + alert('Sign out clicked')}>Sign out + +
diff --git a/frontend/src/lib/components/ui/collapsible/collapsible-content.svelte b/frontend/src/lib/components/ui/collapsible/collapsible-content.svelte new file mode 100644 index 0000000000..812ea5a423 --- /dev/null +++ b/frontend/src/lib/components/ui/collapsible/collapsible-content.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/frontend/src/lib/components/ui/collapsible/index.ts b/frontend/src/lib/components/ui/collapsible/index.ts new file mode 100644 index 0000000000..ac7413ce59 --- /dev/null +++ b/frontend/src/lib/components/ui/collapsible/index.ts @@ -0,0 +1,15 @@ +import { Collapsible as CollapsiblePrimitive } from 'bits-ui'; +import Content from './collapsible-content.svelte'; + +const Root = CollapsiblePrimitive.Root; +const Trigger = CollapsiblePrimitive.Trigger; + +export { + Root, + Content, + Trigger, + // + Root as Collapsible, + Content as CollapsibleContent, + Trigger as CollapsibleTrigger +}; diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte new file mode 100644 index 0000000000..0f408c6f36 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte new file mode 100644 index 0000000000..03a586654f --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -0,0 +1,26 @@ + + + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte new file mode 100644 index 0000000000..b89f5fb830 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte new file mode 100644 index 0000000000..43f1527770 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte new file mode 100644 index 0000000000..1c74ae1a0e --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte new file mode 100644 index 0000000000..7cdfdcab4c --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte new file mode 100644 index 0000000000..8b16e03db9 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -0,0 +1,14 @@ + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte new file mode 100644 index 0000000000..d8c7378c5a --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte new file mode 100644 index 0000000000..042398d725 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte new file mode 100644 index 0000000000..f207f300fc --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -0,0 +1,32 @@ + + + + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/index.ts b/frontend/src/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000000..c1749e923e --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/index.ts @@ -0,0 +1,48 @@ +import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; +import Item from "./dropdown-menu-item.svelte"; +import Label from "./dropdown-menu-label.svelte"; +import Content from "./dropdown-menu-content.svelte"; +import Shortcut from "./dropdown-menu-shortcut.svelte"; +import RadioItem from "./dropdown-menu-radio-item.svelte"; +import Separator from "./dropdown-menu-separator.svelte"; +import RadioGroup from "./dropdown-menu-radio-group.svelte"; +import SubContent from "./dropdown-menu-sub-content.svelte"; +import SubTrigger from "./dropdown-menu-sub-trigger.svelte"; +import CheckboxItem from "./dropdown-menu-checkbox-item.svelte"; + +const Sub = DropdownMenuPrimitive.Sub; +const Root = DropdownMenuPrimitive.Root; +const Trigger = DropdownMenuPrimitive.Trigger; +const Group = DropdownMenuPrimitive.Group; + +export { + Sub, + Root, + Item, + Label, + Group, + Trigger, + Content, + Shortcut, + Separator, + RadioItem, + SubContent, + SubTrigger, + RadioGroup, + CheckboxItem, + // + Root as DropdownMenu, + Sub as DropdownMenuSub, + Item as DropdownMenuItem, + Label as DropdownMenuLabel, + Group as DropdownMenuGroup, + Content as DropdownMenuContent, + Trigger as DropdownMenuTrigger, + Shortcut as DropdownMenuShortcut, + RadioItem as DropdownMenuRadioItem, + Separator as DropdownMenuSeparator, + RadioGroup as DropdownMenuRadioGroup, + SubContent as DropdownMenuSubContent, + SubTrigger as DropdownMenuSubTrigger, + CheckboxItem as DropdownMenuCheckboxItem, +}; diff --git a/frontend/src/lib/components/ui/scroll-area/index.ts b/frontend/src/lib/components/ui/scroll-area/index.ts new file mode 100644 index 0000000000..e86a25b213 --- /dev/null +++ b/frontend/src/lib/components/ui/scroll-area/index.ts @@ -0,0 +1,10 @@ +import Scrollbar from "./scroll-area-scrollbar.svelte"; +import Root from "./scroll-area.svelte"; + +export { + Root, + Scrollbar, + //, + Root as ScrollArea, + Scrollbar as ScrollAreaScrollbar, +}; diff --git a/frontend/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte b/frontend/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte new file mode 100644 index 0000000000..71b3328001 --- /dev/null +++ b/frontend/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte @@ -0,0 +1,27 @@ + + + + + + diff --git a/frontend/src/lib/components/ui/scroll-area/scroll-area.svelte b/frontend/src/lib/components/ui/scroll-area/scroll-area.svelte new file mode 100644 index 0000000000..e8e368c97c --- /dev/null +++ b/frontend/src/lib/components/ui/scroll-area/scroll-area.svelte @@ -0,0 +1,32 @@ + + + + + + + + + {#if orientation === "vertical" || orientation === "both"} + + {/if} + {#if orientation === "horizontal" || orientation === "both"} + + {/if} + + diff --git a/frontend/src/lib/components/ui/select/index.ts b/frontend/src/lib/components/ui/select/index.ts new file mode 100644 index 0000000000..5a9dd8784b --- /dev/null +++ b/frontend/src/lib/components/ui/select/index.ts @@ -0,0 +1,34 @@ +import { Select as SelectPrimitive } from 'bits-ui'; + +import Label from './select-label.svelte'; +import Item from './select-item.svelte'; +import Content from './select-content.svelte'; +import Trigger from './select-trigger.svelte'; +import Separator from './select-separator.svelte'; + +const Root = SelectPrimitive.Root; +const Group = SelectPrimitive.Group; +const Input = SelectPrimitive.Input; +const Value = SelectPrimitive.Value; + +export { + Root, + Item, + Group, + Input, + Label, + Value, + Content, + Trigger, + Separator, + // + Root as Select, + Item as SelectItem, + Group as SelectGroup, + Input as SelectInput, + Label as SelectLabel, + Value as SelectValue, + Content as SelectContent, + Trigger as SelectTrigger, + Separator as SelectSeparator +}; diff --git a/frontend/src/lib/components/ui/select/select-content.svelte b/frontend/src/lib/components/ui/select/select-content.svelte new file mode 100644 index 0000000000..3c624e347e --- /dev/null +++ b/frontend/src/lib/components/ui/select/select-content.svelte @@ -0,0 +1,36 @@ + + + +
+ +
+
diff --git a/frontend/src/lib/components/ui/select/select-item.svelte b/frontend/src/lib/components/ui/select/select-item.svelte new file mode 100644 index 0000000000..98ddc948d2 --- /dev/null +++ b/frontend/src/lib/components/ui/select/select-item.svelte @@ -0,0 +1,37 @@ + + + + + + + + + + {label || value} + + diff --git a/frontend/src/lib/components/ui/select/select-label.svelte b/frontend/src/lib/components/ui/select/select-label.svelte new file mode 100644 index 0000000000..3c356073bf --- /dev/null +++ b/frontend/src/lib/components/ui/select/select-label.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/frontend/src/lib/components/ui/select/select-separator.svelte b/frontend/src/lib/components/ui/select/select-separator.svelte new file mode 100644 index 0000000000..0b08542593 --- /dev/null +++ b/frontend/src/lib/components/ui/select/select-separator.svelte @@ -0,0 +1,11 @@ + + + diff --git a/frontend/src/lib/components/ui/select/select-trigger.svelte b/frontend/src/lib/components/ui/select/select-trigger.svelte new file mode 100644 index 0000000000..c13e6ddf89 --- /dev/null +++ b/frontend/src/lib/components/ui/select/select-trigger.svelte @@ -0,0 +1,24 @@ + + +span]:text-muted-foreground flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1', + className + )} + {...$$restProps} +> + +
+ +
+
diff --git a/frontend/src/lib/components/ui/sheet/sheet-content.svelte b/frontend/src/lib/components/ui/sheet/sheet-content.svelte index 31504abbe1..0d71566497 100644 --- a/frontend/src/lib/components/ui/sheet/sheet-content.svelte +++ b/frontend/src/lib/components/ui/sheet/sheet-content.svelte @@ -13,10 +13,12 @@ type $$Props = SheetPrimitive.ContentProps & { side?: Side; + noAnimation?: boolean; }; let className: $$Props['class'] = undefined; export let side: $$Props['side'] = 'right'; + export let noAnimation: $$Props['noAnimation'] = false; export { className as class }; export let inTransition: $$Props['inTransition'] = fly; export let inTransitionConfig: $$Props['inTransitionConfig'] = @@ -24,6 +26,11 @@ export let outTransition: $$Props['outTransition'] = fly; export let outTransitionConfig: $$Props['outTransitionConfig'] = sheetTransitions[side ?? 'right'].out; + + $: if (noAnimation) { + inTransition = undefined; + outTransition = undefined; + } diff --git a/frontend/src/lib/components/ui/tabs/index.ts b/frontend/src/lib/components/ui/tabs/index.ts new file mode 100644 index 0000000000..00ecd7702b --- /dev/null +++ b/frontend/src/lib/components/ui/tabs/index.ts @@ -0,0 +1,18 @@ +import { Tabs as TabsPrimitive } from 'bits-ui'; +import Content from './tabs-content.svelte'; +import List from './tabs-list.svelte'; +import Trigger from './tabs-trigger.svelte'; + +const Root = TabsPrimitive.Root; + +export { + Root, + Content, + List, + Trigger, + // + Root as Tabs, + Content as TabsContent, + List as TabsList, + Trigger as TabsTrigger +}; diff --git a/frontend/src/lib/components/ui/tabs/tabs-content.svelte b/frontend/src/lib/components/ui/tabs/tabs-content.svelte new file mode 100644 index 0000000000..d213d5d4d0 --- /dev/null +++ b/frontend/src/lib/components/ui/tabs/tabs-content.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/frontend/src/lib/components/ui/tabs/tabs-list.svelte b/frontend/src/lib/components/ui/tabs/tabs-list.svelte new file mode 100644 index 0000000000..769ca0c0f7 --- /dev/null +++ b/frontend/src/lib/components/ui/tabs/tabs-list.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte b/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte new file mode 100644 index 0000000000..bb491d3bfe --- /dev/null +++ b/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte @@ -0,0 +1,25 @@ + + + + + diff --git a/frontend/src/lib/constants/date-ranges.ts b/frontend/src/lib/constants/date-ranges.ts new file mode 100644 index 0000000000..b0c89082dc --- /dev/null +++ b/frontend/src/lib/constants/date-ranges.ts @@ -0,0 +1,13 @@ +export const DATE_RANGE_PARAM = 'date-range'; + +export const LAST_DAY = 'last-day'; +export const LAST_7_DAYS = 'last-7-days'; +export const LAST_MONTH = 'last-month'; + +export const DATE_RANGES = [ + { value: LAST_DAY, label: 'Last Day' }, + { value: LAST_7_DAYS, label: 'Last 7 Days' }, + { value: LAST_MONTH, label: 'Last Month' } +] as const; + +export type DateRangeOption = (typeof DATE_RANGES)[number]; diff --git a/frontend/src/lib/types/Model/Model.test.ts b/frontend/src/lib/types/Model/Model.test.ts index 8df29fcdf0..9a914eca60 100644 --- a/frontend/src/lib/types/Model/Model.test.ts +++ b/frontend/src/lib/types/Model/Model.test.ts @@ -1,6 +1,11 @@ import { describe, it, expect } from 'vitest'; import * as api from '$lib/api/api'; -import type { ModelsResponse, TimeSeriesResponse, Model } from '$lib/types/Model/Model'; +import type { + ModelsResponse, + TimeSeriesResponse, + Model, + JoinTimeSeriesResponse +} from '$lib/types/Model/Model'; describe('Model types', () => { it('should match ModelsResponse type', async () => { @@ -120,4 +125,74 @@ describe('Model types', () => { expect(model.name.toLowerCase()).toContain(searchTerm.toLowerCase()); } }); + + it('should match JoinTimeSeriesResponse type', async () => { + const modelId = '0'; + const result = (await api.getJoinTimeseries( + modelId, + 1725926400000, + 1726106400000 + )) as JoinTimeSeriesResponse; + + const expectedKeys = ['name', 'items']; + expect(Object.keys(result)).toEqual(expect.arrayContaining(expectedKeys)); + + // Log a warning if there are additional fields + const additionalKeys = Object.keys(result).filter((key) => !expectedKeys.includes(key)); + if (additionalKeys.length > 0) { + console.warn( + `Additional fields found in JoinTimeSeriesResponse: ${additionalKeys.join(', ')}` + ); + } + + expect(Array.isArray(result.items)).toBe(true); + + if (result.items.length > 0) { + const item = result.items[0]; + const expectedItemKeys = ['name', 'items']; + expect(Object.keys(item)).toEqual(expect.arrayContaining(expectedItemKeys)); + + // Log a warning if there are additional fields + const additionalItemKeys = Object.keys(item).filter((key) => !expectedItemKeys.includes(key)); + if (additionalItemKeys.length > 0) { + console.warn( + `Additional fields found in JoinTimeSeriesResponse item: ${additionalItemKeys.join(', ')}` + ); + } + + if (item.items.length > 0) { + const subItem = item.items[0]; + const expectedSubItemKeys = ['feature', 'points']; + expect(Object.keys(subItem)).toEqual(expect.arrayContaining(expectedSubItemKeys)); + + // Log a warning if there are additional fields + const additionalSubItemKeys = Object.keys(subItem).filter( + (key) => !expectedSubItemKeys.includes(key) + ); + if (additionalSubItemKeys.length > 0) { + console.warn( + `Additional fields found in JoinTimeSeriesResponse sub-item: ${additionalSubItemKeys.join(', ')}` + ); + } + + expect(Array.isArray(subItem.points)).toBe(true); + + if (subItem.points.length > 0) { + const point = subItem.points[0]; + const expectedPointKeys = ['value', 'ts', 'label']; + expect(Object.keys(point)).toEqual(expect.arrayContaining(expectedPointKeys)); + + // Log a warning if there are additional fields + const additionalPointKeys = Object.keys(point).filter( + (key) => !expectedPointKeys.includes(key) + ); + if (additionalPointKeys.length > 0) { + console.warn( + `Additional fields found in JoinTimeSeriesResponse point: ${additionalPointKeys.join(', ')}` + ); + } + } + } + } + }); }); diff --git a/frontend/src/lib/types/Model/Model.ts b/frontend/src/lib/types/Model/Model.ts index b6346ae8dc..3bd4466ff8 100644 --- a/frontend/src/lib/types/Model/Model.ts +++ b/frontend/src/lib/types/Model/Model.ts @@ -24,3 +24,14 @@ export type TimeSeriesResponse = { id: string; items: TimeSeriesItem[]; }; + +export type JoinTimeSeriesResponse = { + name: string; // todo: rename to joinName + items: { + name: string; // todo: rename to groupByName + items: { + feature: string; + points: TimeSeriesItem[]; + }[]; + }[]; +}; diff --git a/frontend/src/lib/util/date-ranges.ts b/frontend/src/lib/util/date-ranges.ts new file mode 100644 index 0000000000..7dd48d2e7f --- /dev/null +++ b/frontend/src/lib/util/date-ranges.ts @@ -0,0 +1,23 @@ +import { LAST_DAY, LAST_7_DAYS, LAST_MONTH } from '$lib/constants/date-ranges'; + +export function getDateRange(range: string): [number, number] { + const now = new Date(); + const end = now.getTime(); + let start: number; + + switch (range) { + case LAST_DAY: + start = end - 24 * 60 * 60 * 1000; + break; + case LAST_7_DAYS: + start = end - 7 * 24 * 60 * 60 * 1000; + break; + case LAST_MONTH: + start = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()).getTime(); + break; + default: + start = end - 7 * 24 * 60 * 60 * 1000; // Default to last 7 days + } + + return [start, end]; +} diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 5f008a3470..a6b6634f48 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -5,6 +5,7 @@ import NavigationSlider from '$lib/components/NavigationSlider/NavigationSlider.svelte'; import NavigationBar from '$lib/components/NavigationBar/NavigationBar.svelte'; import BreadcrumbNav from '$lib/components/BreadcrumbNav/BreadcrumbNav.svelte'; + import { ScrollArea } from '$lib/components/ui/scroll-area'; let { children }: { children: Snippet } = $props(); @@ -32,10 +33,16 @@
- - {@render children()} +
+ +
+ +
+ {@render children()} +
+
diff --git a/frontend/src/routes/models/[slug]/observability/+page.server.ts b/frontend/src/routes/models/[slug]/observability/+page.server.ts index 80fc1590fe..0998b1014e 100644 --- a/frontend/src/routes/models/[slug]/observability/+page.server.ts +++ b/frontend/src/routes/models/[slug]/observability/+page.server.ts @@ -1,11 +1,34 @@ import type { PageServerLoad } from './$types'; import * as api from '$lib/api/api'; -import type { TimeSeriesResponse } from '$lib/types/Model/Model'; +import type { TimeSeriesResponse, JoinTimeSeriesResponse } from '$lib/types/Model/Model'; +import { DATE_RANGE_PARAM, DATE_RANGES, type DateRangeOption } from '$lib/constants/date-ranges'; +import { getDateRange } from '$lib/util/date-ranges'; +import { LAST_7_DAYS } from '$lib/constants/date-ranges'; export const load: PageServerLoad = async ({ - params -}): Promise<{ timeseries: TimeSeriesResponse }> => { + params, + url +}): Promise<{ + timeseries: TimeSeriesResponse; + joinTimeseries: JoinTimeSeriesResponse; + dateRange: DateRangeOption; +}> => { + const dateRangeValue = url.searchParams.get(DATE_RANGE_PARAM) || LAST_7_DAYS; + const [startTimestamp, endTimestamp] = getDateRange(dateRangeValue); + + const dateRange = + DATE_RANGES.find((range) => range.value === dateRangeValue) || + DATE_RANGES.find((range) => range.value === LAST_7_DAYS) || + DATE_RANGES[1]; + + const [timeseries, joinTimeseries] = await Promise.all([ + api.getModelTimeseries(params.slug, startTimestamp, endTimestamp), + api.getJoinTimeseries(params.slug, startTimestamp, endTimestamp) + ]); + return { - timeseries: await api.getModelTimeseries(params.slug, 1725926400000, 1726106400000) + timeseries, + joinTimeseries, + dateRange }; }; diff --git a/frontend/src/routes/models/[slug]/observability/+page.svelte b/frontend/src/routes/models/[slug]/observability/+page.svelte index ca13c05aeb..49d86f1e5e 100644 --- a/frontend/src/routes/models/[slug]/observability/+page.svelte +++ b/frontend/src/routes/models/[slug]/observability/+page.svelte @@ -1,9 +1,342 @@ -
Observability for Model {timeseries.id}
-
Timeseries below (to be replaced with a line chart)
+

Model {timeseries.id}

+
+ + {#if isZoomed} + + {/if} +
+ + +
+ +
+
+ + +
+ +
+
+ + + + + + + Features + + + + Monitoring + + + +
+

Features Content

+
+
+ +
+ {#each joinTimeseries.items as group} + +
+ + handleChartClick(event.detail, group.name, groupByCharts[group.name])} + on:datazoom={handleZoom} + enableMousemove={true} + /> +
+
+ {/each} +
+
+ + -
{JSON.stringify(timeseries, null, 2)}
+ + + + Point Details + Information about the selected point(s) + + {#if selectedPoints.length > 0} + +
+ {#each selectedPoints as point} +
+

Chart: {point.title}

+

Line: {point.line}

+

Date: {point.date}

+

Value: {point.value}

+
+ {/each} +
+
+ {/if} +
+
diff --git a/frontend/src/routes/observability/+page.svelte b/frontend/src/routes/observability/+page.svelte index c31e97ce7b..d2cae5cc94 100644 --- a/frontend/src/routes/observability/+page.svelte +++ b/frontend/src/routes/observability/+page.svelte @@ -3,9 +3,11 @@ import SplitView from '$lib/components/SplitView/SplitView.svelte'; import Button from '$lib/components/ui/button/button.svelte'; import { generateXAxis, generateYAxis, generateHeatmapData } from '$lib/util/heatmap-data-gen'; - import type { ECElementEvent, EChartOption } from 'echarts'; + import type { ECElementEvent, EChartOption, EChartsType } from 'echarts'; import { onMount } from 'svelte'; + let chart: EChartsType | null = $state(null); + const xAxis = generateXAxis(100); const yAxis = generateYAxis(30); let data: number[][] = $state([]); @@ -104,7 +106,7 @@ {#snippet main()}
- +
{/snippet} diff --git a/frontend/static/favicon.png b/frontend/static/favicon.png index 825b9e65af..3f0c7eee39 100644 Binary files a/frontend/static/favicon.png and b/frontend/static/favicon.png differ