Update annotation date picker and place annotation actions beside chart legend

This commit is contained in:
Mike Cao committed 2026-09-13 22:30:30 -07:00
1 parent fed1ff76f6
commit 5f92ec5c49
7 files changed
+44 -71

No files matched your search

+1 -1
View File
@@ -83,7 +83,7 @@
"@tanstack/react-query": "^5.102.8", "@tanstack/react-query": "^5.102.8",
"@umami/api-client": "workspace:*", "@umami/api-client": "workspace:*",
"@umami/mcp": "workspace:*", "@umami/mcp": "workspace:*",
"@umami/react-zen": "^0.253.0", "@umami/react-zen": "^0.254.0",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"chalk": "^5.6.2", "chalk": "^5.6.2",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
+5 -5
View File
@@ -62,8 +62,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:packages/mcp version: link:packages/mcp
'@umami/react-zen': '@umami/react-zen':
specifier: ^0.253.0 specifier: ^0.254.0
version: 0.253.0(@date-fns/tz@1.5.0)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(date-fns@4.4.0)(immer@11.1.18)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(tailwindcss@4.1.18)(use-sync-external-store@1.6.0(react@19.3.0)) version: 0.254.0(@date-fns/tz@1.5.0)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(date-fns@4.4.0)(immer@11.1.18)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(tailwindcss@4.1.18)(use-sync-external-store@1.6.0(react@19.3.0))
bcryptjs: bcryptjs:
specifier: ^3.0.2 specifier: ^3.0.2
version: 3.0.3 version: 3.0.3
@@ -2774,8 +2774,8 @@ packages:
'@umami/api-client@0.81.0': '@umami/api-client@0.81.0':
resolution: {integrity: sha512-cwkQbe8U/m+zEgfe+nNExU0AoY8BSd+x4Qs7i5nfR2gGrktfoH6Y375e237OFYpmvhJr5BJdML1Vzuu6v7+kmw==} resolution: {integrity: sha512-cwkQbe8U/m+zEgfe+nNExU0AoY8BSd+x4Qs7i5nfR2gGrktfoH6Y375e237OFYpmvhJr5BJdML1Vzuu6v7+kmw==}
'@umami/react-zen@0.253.0': '@umami/react-zen@0.254.0':
resolution: {integrity: sha512-ilprZZ+CM+ryHYcNFJKh2+DJFdnyby2y8UPa458e9hG5dXTtv5qpnhVcr6xSTxBfZqoMSC992vcXbWsn+PhjRg==} resolution: {integrity: sha512-1Ip3C3UuL+ltBTjGmLLB5y1IgxwNBtxYY8z/jkYyfVIpX3mNMP6NKdQIqZGHgrbgcq7hj88dslfD9Gc2SzJk1w==}
peerDependencies: peerDependencies:
react: ^18.0.0 || ^19.0.0 react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0
@@ -7955,7 +7955,7 @@ snapshots:
'@umami/api-client@0.81.0': {} '@umami/api-client@0.81.0': {}
'@umami/react-zen@0.253.0(@date-fns/tz@1.5.0)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(date-fns@4.4.0)(immer@11.1.18)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(tailwindcss@4.1.18)(use-sync-external-store@1.6.0(react@19.3.0))': '@umami/react-zen@0.254.0(@date-fns/tz@1.5.0)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(date-fns@4.4.0)(immer@11.1.18)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(tailwindcss@4.1.18)(use-sync-external-store@1.6.0(react@19.3.0))':
dependencies: dependencies:
'@base-ui/react': 1.8.0(@date-fns/tz@1.5.0)(@types/react@19.3.0)(date-fns@4.4.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@base-ui/react': 1.8.0(@date-fns/tz@1.5.0)(@types/react@19.3.0)(date-fns@4.4.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@react-spring/web': 9.7.5(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@react-spring/web': 9.7.5(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
@@ -1,5 +1,5 @@
import { isSameDay } from 'date-fns'; import { isSameDay } from 'date-fns';
import { useCallback, useMemo } from 'react'; import { type ReactNode, useCallback, useMemo } from 'react';
import type { ChartAnnotation } from '@/components/charts/ChartAnnotationMarkers'; import type { ChartAnnotation } from '@/components/charts/ChartAnnotationMarkers';
import { LoadingPanel } from '@/components/common/LoadingPanel'; import { LoadingPanel } from '@/components/common/LoadingPanel';
import { import {
@@ -19,11 +19,13 @@ export function WebsiteChart({
compareMode, compareMode,
showAnnotations, showAnnotations,
onAnnotationMoreClick, onAnnotationMoreClick,
legendActions,
}: { }: {
websiteId: string; websiteId: string;
compareMode?: boolean; compareMode?: boolean;
showAnnotations?: boolean; showAnnotations?: boolean;
onAnnotationMoreClick?: (range: AnnotationRange) => void; onAnnotationMoreClick?: (range: AnnotationRange) => void;
legendActions?: ReactNode;
}) { }) {
const { timezone, localFromUtc, localToUtc } = useTimezone(); const { timezone, localFromUtc, localToUtc } = useTimezone();
const { dateRange, dateCompare } = useDateRange({ timezone: timezone }); const { dateRange, dateCompare } = useDateRange({ timezone: timezone });
@@ -135,6 +137,7 @@ export function WebsiteChart({
<PageviewsChart <PageviewsChart
key={value} key={value}
data={chartData} data={chartData}
legendActions={legendActions}
minDate={startDate} minDate={startDate}
maxDate={endDate} maxDate={endDate}
unit={unit} unit={unit}
@@ -4,8 +4,8 @@ import { useState } from 'react';
import { ExpandedViewModal } from '@/app/(main)/websites/[websiteId]/ExpandedViewModal'; import { ExpandedViewModal } from '@/app/(main)/websites/[websiteId]/ExpandedViewModal';
import { Panel } from '@/components/common/Panel'; import { Panel } from '@/components/common/Panel';
import { DialogButton } from '@/components/input/DialogButton'; import { DialogButton } from '@/components/input/DialogButton';
import type { AnnotationRange } from '@/lib/annotations';
import { UnitFilter } from '@/components/input/UnitFilter'; import { UnitFilter } from '@/components/input/UnitFilter';
import type { AnnotationRange } from '@/lib/annotations';
import { AnnotationsButton } from './annotations/AnnotationsButton'; import { AnnotationsButton } from './annotations/AnnotationsButton';
import { AnnotationsModal } from './annotations/AnnotationsModal'; import { AnnotationsModal } from './annotations/AnnotationsModal';
import { WebsiteChart } from './WebsiteChart'; import { WebsiteChart } from './WebsiteChart';
@@ -28,10 +28,8 @@ export function WebsitePage({ websiteId }: { websiteId: string }) {
websiteId={websiteId} websiteId={websiteId}
showAnnotations showAnnotations
onAnnotationMoreClick={setAnnotationRange} onAnnotationMoreClick={setAnnotationRange}
legendActions={<AnnotationsButton websiteId={websiteId} />}
/> />
<Row justifyContent="end">
<AnnotationsButton websiteId={websiteId} />
</Row>
</Panel> </Panel>
<WebsitePanels websiteId={websiteId} /> <WebsitePanels websiteId={websiteId} />
<ExpandedViewModal websiteId={websiteId} /> <ExpandedViewModal websiteId={websiteId} />
@@ -1,23 +1,17 @@
import { import {
Button, Button,
Calendar as ZenCalendar,
Column, Column,
DialogTrigger, DatePicker,
Form, Form,
FormButtons, FormButtons,
FormField, FormField,
FormSubmitButton, FormSubmitButton,
Icon,
Label,
Popover,
Text, Text,
TextField, TextField,
} from '@umami/react-zen'; } from '@umami/react-zen';
import { startOfDay } from 'date-fns'; import { startOfDay } from 'date-fns';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useLocale, useMessages, useTimezone, useUpdateQuery } from '@/components/hooks'; import { useLocale, useMessages, useTimezone, useUpdateQuery } from '@/components/hooks';
import { Calendar } from '@/components/icons';
import { formatDate } from '@/lib/date';
export interface Annotation { export interface Annotation {
id: string; id: string;
@@ -27,51 +21,6 @@ export interface Annotation {
note: string; note: string;
} }
function AnnotationDatePicker({
date,
label,
locale,
onChange,
}: {
date: Date;
label: string;
locale: string;
onChange: (date: Date) => void;
}) {
const [isOpen, setIsOpen] = useState(false);
const handleChange = (value: Date) => {
onChange(value);
setIsOpen(false);
};
return (
<Column gap="1">
<Label>{label}</Label>
<DialogTrigger overlayType="popover" isOpen={isOpen} onOpenChange={setIsOpen}>
<Button
variant="outline"
aria-label={label}
style={{ width: 'fit-content', cursor: 'pointer', flexDirection: 'row-reverse' }}
>
<Icon size="sm">
<Calendar />
</Icon>
{formatDate(date, 'PP', locale)}
</Button>
<Popover
side="bottom"
align="start"
sideOffset={8}
className="min-w-[340px] bg-surface-overlay border border-edge-muted rounded-lg shadow-lg p-4"
>
<ZenCalendar value={date} onChange={handleChange} />
</Popover>
</DialogTrigger>
</Column>
);
}
export function AnnotationEditForm({ export function AnnotationEditForm({
annotation, annotation,
websiteId, websiteId,
@@ -127,11 +76,16 @@ export function AnnotationEditForm({
return ( return (
<Column gap> <Column gap>
<AnnotationDatePicker <DatePicker
triggerProps={{ style: { width: '100%', maxWidth: 280 } }}
label={t(labels.date)} label={t(labels.date)}
date={date} value={date}
locale={locale} locale={locale}
onChange={setDate} onChange={value => {
if (value) {
setDate(value);
}
}}
/> />
<Column gap="1"> <Column gap="1">
@@ -13,7 +13,6 @@ import { formatAnnotationDate } from '@/components/charts/ChartAnnotationMarkers
import { Empty } from '@/components/common/Empty'; import { Empty } from '@/components/common/Empty';
import { IconLabel } from '@/components/common/IconLabel'; import { IconLabel } from '@/components/common/IconLabel';
import { Pager } from '@/components/common/Pager'; import { Pager } from '@/components/common/Pager';
import { FilterButtons } from '@/components/input/FilterButtons';
import { import {
useLocale, useLocale,
useMessages, useMessages,
@@ -24,6 +23,7 @@ import {
useWebsiteAnnotationsQuery, useWebsiteAnnotationsQuery,
} from '@/components/hooks'; } from '@/components/hooks';
import { Edit, Plus } from '@/components/icons'; import { Edit, Plus } from '@/components/icons';
import { FilterButtons } from '@/components/input/FilterButtons';
import { type AnnotationRange, getAnnotationDateRangeValue } from '@/lib/annotations'; import { type AnnotationRange, getAnnotationDateRangeValue } from '@/lib/annotations';
import { AnnotationDeleteButton } from './AnnotationDeleteButton'; import { AnnotationDeleteButton } from './AnnotationDeleteButton';
import { type Annotation, AnnotationEditForm } from './AnnotationEditForm'; import { type Annotation, AnnotationEditForm } from './AnnotationEditForm';
@@ -99,7 +99,7 @@ export function AnnotationsModal({
const rows: Annotation[] = data?.data || []; const rows: Annotation[] = data?.data || [];
return ( return (
<Column gap> <Column gap flexGrow={1}>
<Row alignItems="center" justifyContent="space-between"> <Row alignItems="center" justifyContent="space-between">
<Heading size="xl">{t(labels.notes)}</Heading> <Heading size="xl">{t(labels.notes)}</Heading>
{!share && ( {!share && (
@@ -155,7 +155,7 @@ export function AnnotationsModal({
<Pager page={page} pageSize={PAGE_SIZE} count={data.count} onPageChange={setPage} /> <Pager page={page} pageSize={PAGE_SIZE} count={data.count} onPageChange={setPage} />
)} )}
{onClose && ( {onClose && (
<Row justifyContent="flex-end"> <Row justifyContent="flex-end" style={{ marginTop: 'auto' }}>
<Button onPress={onClose}>{t(labels.close)}</Button> <Button onPress={onClose}>{t(labels.close)}</Button>
</Row> </Row>
)} )}
+20 -2
View File
@@ -5,7 +5,7 @@ import ChartJS, {
type LegendItem, type LegendItem,
type UpdateMode, type UpdateMode,
} from 'chart.js/auto'; } from 'chart.js/auto';
import { useEffect, useMemo, useRef, useState } from 'react'; import { type ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { import {
type AnnotationMarker, type AnnotationMarker,
type ChartAnnotation, type ChartAnnotation,
@@ -26,6 +26,7 @@ export interface ChartProps extends BoxProps {
onTooltip?: (model: any) => void; onTooltip?: (model: any) => void;
hiddenLabels?: Set<string>; hiddenLabels?: Set<string>;
onLegendClick?: (label: string, willBeHidden: boolean) => void; onLegendClick?: (label: string, willBeHidden: boolean) => void;
legendActions?: ReactNode;
annotations?: ChartAnnotation[]; annotations?: ChartAnnotation[];
onAnnotationClick?: (annotations: ChartAnnotation[]) => void; onAnnotationClick?: (annotations: ChartAnnotation[]) => void;
onAnnotationMoreClick?: (annotations: ChartAnnotation[]) => void; onAnnotationMoreClick?: (annotations: ChartAnnotation[]) => void;
@@ -53,6 +54,7 @@ export function Chart({
chartOptions, chartOptions,
hiddenLabels, hiddenLabels,
onLegendClick, onLegendClick,
legendActions,
annotations, annotations,
onAnnotationClick, onAnnotationClick,
onAnnotationMoreClick, onAnnotationMoreClick,
@@ -234,7 +236,23 @@ export function Chart({
)} )}
</div> </div>
</Box> </Box>
<Legend items={legendItems} onClick={handleLegendClick} /> {legendActions ? (
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr auto 1fr',
alignItems: 'center',
gap: 8,
}}
>
<div style={{ gridColumn: 2 }}>
<Legend items={legendItems} onClick={handleLegendClick} />
</div>
<div style={{ justifySelf: 'end' }}>{legendActions}</div>
</div>
) : (
<Legend items={legendItems} onClick={handleLegendClick} />
)}
</Column> </Column>
); );
} }