Update annotation date picker and place annotation actions beside chart legend
This commit is contained in:
1 parent
fed1ff76f6
commit
5f92ec5c49
7 files changed
+44
-71
No files matched your search
+1
-1
@@ -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",
|
||||||
|
|||||||
Generated
+5
-5
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user