mirror of
https://github.com/umami-software/umami.git
synced 2025-12-06 01:18:00 +01:00
Updated grid layouts. Fixed chart tooltip.
This commit is contained in:
parent
96c2c32d14
commit
1d24e23a34
9 changed files with 64 additions and 79 deletions
|
|
@ -78,7 +78,7 @@
|
||||||
"@react-spring/web": "^9.7.5",
|
"@react-spring/web": "^9.7.5",
|
||||||
"@tanstack/react-query": "^5.68.0",
|
"@tanstack/react-query": "^5.68.0",
|
||||||
"@umami/prisma-client": "^0.16.0",
|
"@umami/prisma-client": "^0.16.0",
|
||||||
"@umami/react-zen": "^0.77.0",
|
"@umami/react-zen": "^0.79.0",
|
||||||
"@umami/redis-client": "^0.27.0",
|
"@umami/redis-client": "^0.27.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
|
|
|
||||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
|
|
@ -45,8 +45,8 @@ importers:
|
||||||
specifier: ^0.16.0
|
specifier: ^0.16.0
|
||||||
version: 0.16.0(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2))(@prisma/extension-read-replicas@0.4.0(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2)))
|
version: 0.16.0(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2))(@prisma/extension-read-replicas@0.4.0(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2)))
|
||||||
'@umami/react-zen':
|
'@umami/react-zen':
|
||||||
specifier: ^0.77.0
|
specifier: ^0.79.0
|
||||||
version: 0.77.0(@babel/core@7.26.9)(@types/react@19.0.10)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.0.0))
|
version: 0.79.0(@babel/core@7.26.9)(@types/react@19.0.10)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.0.0))
|
||||||
'@umami/redis-client':
|
'@umami/redis-client':
|
||||||
specifier: ^0.27.0
|
specifier: ^0.27.0
|
||||||
version: 0.27.0
|
version: 0.27.0
|
||||||
|
|
@ -2961,8 +2961,8 @@ packages:
|
||||||
'@prisma/client': ^4.8.0
|
'@prisma/client': ^4.8.0
|
||||||
'@prisma/extension-read-replicas': ^0.3.0
|
'@prisma/extension-read-replicas': ^0.3.0
|
||||||
|
|
||||||
'@umami/react-zen@0.77.0':
|
'@umami/react-zen@0.79.0':
|
||||||
resolution: {integrity: sha512-tdoPdCMfPOhBMEiGXZ62zfTG8qjto7Ii+mWXNX2qdPjZ+SP5AB8wnQpqIGiXoUZUnP41iWw0GS97sTZq3xeEBQ==}
|
resolution: {integrity: sha512-qumZSV/dWvtq7iR7QwxEO5emE/jzgI5uPP5Y1E9S+MMGnJRhD5gQ1TvURf+jYAlTuiPubLysu6U3nKYmPNJxUg==}
|
||||||
|
|
||||||
'@umami/redis-client@0.27.0':
|
'@umami/redis-client@0.27.0':
|
||||||
resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==}
|
resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==}
|
||||||
|
|
@ -3329,8 +3329,8 @@ packages:
|
||||||
caniuse-lite@1.0.30001702:
|
caniuse-lite@1.0.30001702:
|
||||||
resolution: {integrity: sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==}
|
resolution: {integrity: sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==}
|
||||||
|
|
||||||
caniuse-lite@1.0.30001709:
|
caniuse-lite@1.0.30001712:
|
||||||
resolution: {integrity: sha512-NgL3vUTnDrPCZ3zTahp4fsugQ4dc7EKTSzwQDPEel6DMoMnfH2jhry9n2Zm8onbSR+f/QtKHFOA+iAQu4kbtWA==}
|
resolution: {integrity: sha512-MBqPpGYYdQ7/hfKiet9SCI+nmN5/hp4ZzveOJubl5DTAMa5oggjAuoi0Z4onBpKPFI2ePGnQuQIzF3VxDjDJig==}
|
||||||
|
|
||||||
caseless@0.12.0:
|
caseless@0.12.0:
|
||||||
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
|
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
|
||||||
|
|
@ -10646,7 +10646,7 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@umami/react-zen@0.77.0(@babel/core@7.26.9)(@types/react@19.0.10)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.0.0))':
|
'@umami/react-zen@0.79.0(@babel/core@7.26.9)(@types/react@19.0.10)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.0.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@fontsource/jetbrains-mono': 5.2.5
|
'@fontsource/jetbrains-mono': 5.2.5
|
||||||
'@react-aria/focus': 3.20.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@react-aria/focus': 3.20.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
|
@ -11120,13 +11120,13 @@ snapshots:
|
||||||
caniuse-api@3.0.0:
|
caniuse-api@3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.24.4
|
browserslist: 4.24.4
|
||||||
caniuse-lite: 1.0.30001709
|
caniuse-lite: 1.0.30001712
|
||||||
lodash.memoize: 4.1.2
|
lodash.memoize: 4.1.2
|
||||||
lodash.uniq: 4.5.0
|
lodash.uniq: 4.5.0
|
||||||
|
|
||||||
caniuse-lite@1.0.30001702: {}
|
caniuse-lite@1.0.30001702: {}
|
||||||
|
|
||||||
caniuse-lite@1.0.30001709: {}
|
caniuse-lite@1.0.30001712: {}
|
||||||
|
|
||||||
caseless@0.12.0: {}
|
caseless@0.12.0: {}
|
||||||
|
|
||||||
|
|
@ -13667,7 +13667,7 @@ snapshots:
|
||||||
'@swc/counter': 0.1.3
|
'@swc/counter': 0.1.3
|
||||||
'@swc/helpers': 0.5.15
|
'@swc/helpers': 0.5.15
|
||||||
busboy: 1.6.0
|
busboy: 1.6.0
|
||||||
caniuse-lite: 1.0.30001709
|
caniuse-lite: 1.0.30001712
|
||||||
postcss: 8.4.31
|
postcss: 8.4.31
|
||||||
react: 19.0.0
|
react: 19.0.0
|
||||||
react-dom: 19.0.0(react@19.0.0)
|
react-dom: 19.0.0(react@19.0.0)
|
||||||
|
|
@ -13692,7 +13692,7 @@ snapshots:
|
||||||
'@swc/counter': 0.1.3
|
'@swc/counter': 0.1.3
|
||||||
'@swc/helpers': 0.5.15
|
'@swc/helpers': 0.5.15
|
||||||
busboy: 1.6.0
|
busboy: 1.6.0
|
||||||
caniuse-lite: 1.0.30001709
|
caniuse-lite: 1.0.30001712
|
||||||
postcss: 8.4.31
|
postcss: 8.4.31
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,17 @@ import { WebsiteMenu } from '@/app/(main)/websites/[websiteId]/WebsiteMenu';
|
||||||
import { WebsiteCompareBar } from '@/app/(main)/websites/[websiteId]/WebsiteCompareBar';
|
import { WebsiteCompareBar } from '@/app/(main)/websites/[websiteId]/WebsiteCompareBar';
|
||||||
|
|
||||||
export function WebsiteHeader({
|
export function WebsiteHeader({
|
||||||
|
websiteId,
|
||||||
showFilter = true,
|
showFilter = true,
|
||||||
allowEdit = true,
|
allowEdit = true,
|
||||||
}: {
|
}: {
|
||||||
|
websiteId: string;
|
||||||
showFilter?: boolean;
|
showFilter?: boolean;
|
||||||
allowEdit?: boolean;
|
allowEdit?: boolean;
|
||||||
compareMode?: boolean;
|
compareMode?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const website = useWebsite();
|
const website = useWebsite();
|
||||||
const { id: websiteId, name, domain } = website || {};
|
const { name, domain } = website || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column marginY="6" gap="6">
|
<Column marginY="6" gap="6">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Grid } from '@umami/react-zen';
|
import { Grid } from '@umami/react-zen';
|
||||||
|
import { GridRow } from '@/components/common/GridRow';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { PagesTable } from '@/components/metrics/PagesTable';
|
import { PagesTable } from '@/components/metrics/PagesTable';
|
||||||
import { ReferrersTable } from '@/components/metrics/ReferrersTable';
|
import { ReferrersTable } from '@/components/metrics/ReferrersTable';
|
||||||
|
|
@ -7,57 +8,37 @@ import { OSTable } from '@/components/metrics/OSTable';
|
||||||
import { DevicesTable } from '@/components/metrics/DevicesTable';
|
import { DevicesTable } from '@/components/metrics/DevicesTable';
|
||||||
import { WorldMap } from '@/components/metrics/WorldMap';
|
import { WorldMap } from '@/components/metrics/WorldMap';
|
||||||
import { CountriesTable } from '@/components/metrics/CountriesTable';
|
import { CountriesTable } from '@/components/metrics/CountriesTable';
|
||||||
import { EventsTable } from '@/components/metrics/EventsTable';
|
|
||||||
import { EventsChart } from '@/components/metrics/EventsChart';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
|
|
||||||
export function WebsiteTableView({ websiteId }: { websiteId: string }) {
|
export function WebsiteTableView({ websiteId }: { websiteId: string }) {
|
||||||
const pathname = usePathname();
|
|
||||||
const tableProps = {
|
|
||||||
websiteId,
|
|
||||||
limit: 10,
|
|
||||||
};
|
|
||||||
const isSharePage = pathname.includes('/share/');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid gap="3">
|
<Grid gap="3">
|
||||||
<Grid gap="3" columns="repeat(auto-fill, minmax(500px, 1fr))">
|
<GridRow layout="two">
|
||||||
<Panel>
|
<Panel>
|
||||||
<PagesTable {...tableProps} />
|
<PagesTable websiteId={websiteId} limit={10} />
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel>
|
<Panel>
|
||||||
<ReferrersTable {...tableProps} />
|
<ReferrersTable websiteId={websiteId} limit={10} />
|
||||||
</Panel>
|
</Panel>
|
||||||
</Grid>
|
</GridRow>
|
||||||
<Grid gap="3" columns="repeat(auto-fill, minmax(400px, 1fr))">
|
<GridRow layout="three">
|
||||||
<Panel>
|
<Panel>
|
||||||
<BrowsersTable {...tableProps} />
|
<BrowsersTable websiteId={websiteId} limit={10} />
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel>
|
<Panel>
|
||||||
<OSTable {...tableProps} />
|
<OSTable websiteId={websiteId} limit={10} />
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel>
|
<Panel>
|
||||||
<DevicesTable {...tableProps} />
|
<DevicesTable websiteId={websiteId} limit={10} />
|
||||||
</Panel>
|
</Panel>
|
||||||
</Grid>
|
</GridRow>
|
||||||
<Grid gap="3" columns="2fr 1fr">
|
<GridRow layout="two-one">
|
||||||
<Panel padding="0">
|
<Panel padding="0" gridColumn="span 2">
|
||||||
<WorldMap websiteId={websiteId} />
|
<WorldMap websiteId={websiteId} />
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel>
|
<Panel>
|
||||||
<CountriesTable {...tableProps} />
|
<CountriesTable websiteId={websiteId} limit={10} />
|
||||||
</Panel>
|
</Panel>
|
||||||
</Grid>
|
</GridRow>
|
||||||
{isSharePage && (
|
|
||||||
<Grid gap="3">
|
|
||||||
<Panel>
|
|
||||||
<EventsTable {...tableProps} />
|
|
||||||
</Panel>
|
|
||||||
<Panel>
|
|
||||||
<EventsChart websiteId={websiteId} />
|
|
||||||
</Panel>
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,19 +82,19 @@ export function BarChart(props: BarChartProps) {
|
||||||
const handleTooltip = ({ tooltip }: { tooltip: any }) => {
|
const handleTooltip = ({ tooltip }: { tooltip: any }) => {
|
||||||
const { opacity } = tooltip;
|
const { opacity } = tooltip;
|
||||||
|
|
||||||
setTooltip(
|
setTooltip(opacity ? tooltip : null);
|
||||||
opacity ? <BarChartTooltip tooltip={tooltip} unit={unit} currency={currency} /> : null,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Chart
|
<>
|
||||||
{...props}
|
<Chart
|
||||||
type="bar"
|
{...props}
|
||||||
chartOptions={options}
|
type="bar"
|
||||||
tooltip={tooltip}
|
chartOptions={options}
|
||||||
onTooltip={handleTooltip}
|
onTooltip={handleTooltip}
|
||||||
style={{ height: 400 }}
|
style={{ height: 400 }}
|
||||||
/>
|
/>
|
||||||
|
{tooltip && <BarChartTooltip tooltip={tooltip} unit={unit} currency={currency} />}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useRef, useEffect, useMemo, ReactNode, HTMLAttributes } from 'react';
|
import { useState, useRef, useEffect, useMemo, HTMLAttributes } from 'react';
|
||||||
import { Loading } from '@umami/react-zen';
|
import { Loading } from '@umami/react-zen';
|
||||||
import ChartJS, { LegendItem, ChartOptions } from 'chart.js/auto';
|
import ChartJS, { LegendItem, ChartOptions } from 'chart.js/auto';
|
||||||
import { Legend } from '@/components/metrics/Legend';
|
import { Legend } from '@/components/metrics/Legend';
|
||||||
|
|
@ -14,7 +14,6 @@ export interface ChartProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
onUpdate?: (chart: any) => void;
|
onUpdate?: (chart: any) => void;
|
||||||
onTooltip?: (model: any) => void;
|
onTooltip?: (model: any) => void;
|
||||||
chartOptions?: ChartOptions;
|
chartOptions?: ChartOptions;
|
||||||
tooltip?: ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Chart({
|
export function Chart({
|
||||||
|
|
@ -27,7 +26,6 @@ export function Chart({
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onTooltip,
|
onTooltip,
|
||||||
chartOptions,
|
chartOptions,
|
||||||
tooltip,
|
|
||||||
...props
|
...props
|
||||||
}: ChartProps) {
|
}: ChartProps) {
|
||||||
const canvas = useRef(null);
|
const canvas = useRef(null);
|
||||||
|
|
@ -53,6 +51,7 @@ export function Chart({
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
intersect: true,
|
||||||
external: onTooltip,
|
external: onTooltip,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -139,7 +138,6 @@ export function Chart({
|
||||||
<canvas ref={canvas} />
|
<canvas ref={canvas} />
|
||||||
</div>
|
</div>
|
||||||
<Legend items={legendItems} onClick={handleLegendClick} />
|
<Legend items={legendItems} onClick={handleLegendClick} />
|
||||||
{tooltip}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,20 @@ import { Grid } from '@umami/react-zen';
|
||||||
|
|
||||||
const LAYOUTS = {
|
const LAYOUTS = {
|
||||||
one: { columns: '1fr' },
|
one: { columns: '1fr' },
|
||||||
two: { columns: { xs: '1fr', sm: '1fr', md: '1fr 1fr', lg: '1fr 1fr' } },
|
two: {
|
||||||
three: { columns: { xs: '1fr', sm: '1fr', md: '1fr 1fr 1fr', lg: '1fr 2fr' } },
|
columns: {
|
||||||
'one-two': { columns: { xs: '1fr', sm: '1fr', md: '1fr 2fr', lg: '1fr 2fr' } },
|
xs: '1fr',
|
||||||
'two-one': { columns: { xs: '1fr', sm: '1fr', md: '2fr 1fr', lg: '2fr 1fr', xl: '2fr 1fr' } },
|
md: 'repeat(auto-fill, minmax(600px, 1fr))',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
three: {
|
||||||
|
columns: {
|
||||||
|
xs: '1fr',
|
||||||
|
md: 'repeat(auto-fill, minmax(400px, 1fr))',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'one-two': { columns: { xs: '1fr', lg: 'repeat(3, 1fr)' } },
|
||||||
|
'two-one': { columns: { xs: '1fr', lg: 'repeat(3, 1fr)' } },
|
||||||
};
|
};
|
||||||
|
|
||||||
export function GridRow(props: {
|
export function GridRow(props: {
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,9 @@ export function TypeIcon({
|
||||||
return (
|
return (
|
||||||
<Row gap="3" alignItems="center">
|
<Row gap="3" alignItems="center">
|
||||||
<img
|
<img
|
||||||
src={`${process.env.basePath || ''}/images/${type}/${value
|
src={`${process.env.basePath || ''}/images/${type}/${
|
||||||
?.replaceAll(' ', '-')
|
value?.replaceAll(' ', '-').toLowerCase() || 'xx'
|
||||||
.toLowerCase()}.png`}
|
}.png`}
|
||||||
onError={e => {
|
onError={e => {
|
||||||
e.currentTarget.src = `${process.env.basePath || ''}/images/${type}/unknown.png`;
|
e.currentTarget.src = `${process.env.basePath || ''}/images/${type}/unknown.png`;
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState } from 'react';
|
import { Key } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import {
|
import {
|
||||||
Text,
|
Text,
|
||||||
|
|
@ -30,16 +30,10 @@ export function TeamsButton({
|
||||||
const { teamId } = useNavigation();
|
const { teamId } = useNavigation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const team = result?.data?.find(({ id }) => id === teamId);
|
const team = result?.data?.find(({ id }) => id === teamId);
|
||||||
const [selectedKeys, setSelectedKeys] = useState<any>(new Set([teamId || user.id]));
|
const selectedKeys = new Set([teamId || user.id]);
|
||||||
|
|
||||||
const handleSelect = (keys: Set<string>) => {
|
const handleSelect = (id: Key) => {
|
||||||
if (keys.size > 0) {
|
router.push(id === user.id ? '/dashboard' : `/teams/${id}/dashboard`);
|
||||||
const [id] = [...keys];
|
|
||||||
|
|
||||||
router.push(id === user.id ? '/dashboard' : `/teams/${id}/dashboard`);
|
|
||||||
|
|
||||||
setSelectedKeys(keys);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!result?.count) {
|
if (!result?.count) {
|
||||||
|
|
@ -63,7 +57,7 @@ export function TeamsButton({
|
||||||
selectionMode="single"
|
selectionMode="single"
|
||||||
selectedKeys={selectedKeys}
|
selectedKeys={selectedKeys}
|
||||||
autoFocus="last"
|
autoFocus="last"
|
||||||
onSelectionChange={keys => handleSelect(keys as Set<string>)}
|
onAction={handleSelect}
|
||||||
>
|
>
|
||||||
<MenuSection title={formatMessage(labels.myAccount)}>
|
<MenuSection title={formatMessage(labels.myAccount)}>
|
||||||
<MenuItem id={user.id}>
|
<MenuItem id={user.id}>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue