mirror of
https://github.com/umami-software/umami.git
synced 2026-02-07 06:07:17 +01:00
Updated session profile page. Added TypeIcon component. Added useRegionNames hook.
This commit is contained in:
parent
ac60d08ee5
commit
c3c3b46ef6
313 changed files with 197 additions and 94 deletions
|
|
@ -7,7 +7,6 @@ import WebsiteExpandedView from './WebsiteExpandedView';
|
|||
import WebsiteHeader from './WebsiteHeader';
|
||||
import WebsiteMetricsBar from './WebsiteMetricsBar';
|
||||
import WebsiteTableView from './WebsiteTableView';
|
||||
import WebsiteProvider from './WebsiteProvider';
|
||||
import { FILTER_COLUMNS } from 'lib/constants';
|
||||
|
||||
export default function WebsiteDetailsPage({ websiteId }: { websiteId: string }) {
|
||||
|
|
@ -25,13 +24,13 @@ export default function WebsiteDetailsPage({ websiteId }: { websiteId: string })
|
|||
}, {});
|
||||
|
||||
return (
|
||||
<WebsiteProvider websiteId={websiteId}>
|
||||
<>
|
||||
<WebsiteHeader websiteId={websiteId} showLinks={showLinks} />
|
||||
<FilterTags websiteId={websiteId} params={params} />
|
||||
<WebsiteMetricsBar websiteId={websiteId} showFilter={true} showChange={true} sticky={true} />
|
||||
<WebsiteChart websiteId={websiteId} />
|
||||
{!view && <WebsiteTableView websiteId={websiteId} />}
|
||||
{view && <WebsiteExpandedView websiteId={websiteId} />}
|
||||
</WebsiteProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
'use client';
|
||||
import { createContext, ReactNode, useEffect } from 'react';
|
||||
import { useModified, useWebsite } from 'components/hooks';
|
||||
import { Loading } from 'react-basics';
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { useNavigation } from 'components/hooks';
|
|||
import { FILTER_COLUMNS } from 'lib/constants';
|
||||
import WebsiteChart from '../WebsiteChart';
|
||||
import WebsiteCompareTables from './WebsiteCompareTables';
|
||||
import WebsiteProvider from '../WebsiteProvider';
|
||||
|
||||
export function WebsiteComparePage({ websiteId }) {
|
||||
const { query } = useNavigation();
|
||||
|
|
@ -19,13 +18,13 @@ export function WebsiteComparePage({ websiteId }) {
|
|||
}, {});
|
||||
|
||||
return (
|
||||
<WebsiteProvider websiteId={websiteId}>
|
||||
<>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<FilterTags websiteId={websiteId} params={params} />
|
||||
<WebsiteMetricsBar websiteId={websiteId} compareMode={true} showFilter={true} />
|
||||
<WebsiteChart websiteId={websiteId} compareMode={true} />
|
||||
<WebsiteCompareTables websiteId={websiteId} />
|
||||
</WebsiteProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
17
src/app/(main)/websites/[websiteId]/layout.tsx
Normal file
17
src/app/(main)/websites/[websiteId]/layout.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { Metadata } from 'next';
|
||||
import WebsiteProvider from './WebsiteProvider';
|
||||
|
||||
export default function ({ children, params: { websiteId } }) {
|
||||
if (process.env.cloudMode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <WebsiteProvider websiteId={websiteId}>{children}</WebsiteProvider>;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
template: '%s | Umami',
|
||||
default: 'Websites | Umami',
|
||||
},
|
||||
};
|
||||
|
|
@ -3,19 +3,17 @@ import ListTable from 'components/metrics/ListTable';
|
|||
import { useLocale, useCountryNames, useMessages } from 'components/hooks';
|
||||
import classNames from 'classnames';
|
||||
import styles from './RealtimeCountries.module.css';
|
||||
import TypeIcon from 'components/common/TypeIcon';
|
||||
|
||||
export function RealtimeCountries({ data }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { locale } = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const { countryNames } = useCountryNames(locale);
|
||||
|
||||
const renderCountryName = useCallback(
|
||||
({ x: code }) => (
|
||||
<span className={classNames(locale, styles.row)}>
|
||||
<img
|
||||
src={`${process.env.basePath || ''}/images/flags/${code?.toLowerCase() || 'xx'}.png`}
|
||||
alt={code}
|
||||
/>
|
||||
<TypeIcon type="country" value={code?.toLowerCase()} />
|
||||
{countryNames[code]}
|
||||
</span>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
|
|||
const { formatMessage, labels, messages, FormattedMessage } = useMessages();
|
||||
const { formatValue } = useFormat();
|
||||
const { locale } = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const { countryNames } = useCountryNames(locale);
|
||||
const [filter, setFilter] = useState(TYPE_ALL);
|
||||
|
||||
const buttons = [
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import RealtimeHeader from './RealtimeHeader';
|
|||
import RealtimeUrls from './RealtimeUrls';
|
||||
import RealtimeCountries from './RealtimeCountries';
|
||||
import WebsiteHeader from '../WebsiteHeader';
|
||||
import WebsiteProvider from '../WebsiteProvider';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
|
||||
export function WebsiteRealtimePage({ websiteId }) {
|
||||
|
|
@ -27,7 +26,7 @@ export function WebsiteRealtimePage({ websiteId }) {
|
|||
);
|
||||
|
||||
return (
|
||||
<WebsiteProvider websiteId={websiteId}>
|
||||
<>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<RealtimeHeader data={data} />
|
||||
<RealtimeChart data={data} unit="minute" />
|
||||
|
|
@ -41,7 +40,7 @@ export function WebsiteRealtimePage({ websiteId }) {
|
|||
<WorldMap data={countries} />
|
||||
</GridRow>
|
||||
</Grid>
|
||||
</WebsiteProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import Link from 'next/link';
|
||||
import { GridColumn, GridTable, useBreakpoint } from 'react-basics';
|
||||
import { useFormat, useMessages } from 'components/hooks';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import Profile from 'components/common/Profile';
|
||||
|
||||
export function SessionsTable({ data = [] }: { data: any[]; showDomain?: boolean }) {
|
||||
|
|
@ -12,14 +11,10 @@ export function SessionsTable({ data = [] }: { data: any[]; showDomain?: boolean
|
|||
return (
|
||||
<GridTable data={data} cardMode={['xs', 'sm', 'md'].includes(breakpoint)}>
|
||||
<GridColumn name="pic" label="" width="90px">
|
||||
{row => <Profile seed={row.id} size={64} />}
|
||||
{row => <Profile key={row.id} seed={row.id} size={64} />}
|
||||
</GridColumn>
|
||||
<GridColumn name="id" label="ID">
|
||||
{row => (
|
||||
<Link href={`sessions/${row.id}`}>
|
||||
{row.id} ({row.firstAt !== row.lastAt ? 'YES' : 'NO'})
|
||||
</Link>
|
||||
)}
|
||||
{row => <Link href={`sessions/${row.id}`}>{row.id}</Link>}
|
||||
</GridColumn>
|
||||
<GridColumn name="country" label={formatMessage(labels.country)}>
|
||||
{row => formatValue(row.country, 'country')}
|
||||
|
|
@ -32,8 +27,8 @@ export function SessionsTable({ data = [] }: { data: any[]; showDomain?: boolean
|
|||
<GridColumn name="device" label={formatMessage(labels.device)}>
|
||||
{row => formatValue(row.device, 'device')}
|
||||
</GridColumn>
|
||||
<GridColumn name="createdAt" label={formatMessage(labels.created)}>
|
||||
{row => formatDistanceToNow(new Date(row.createdAt))}
|
||||
<GridColumn name="lastAt" label={formatMessage(labels.lastSeen)}>
|
||||
{row => row.lastAt}
|
||||
</GridColumn>
|
||||
</GridTable>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,18 @@
|
|||
.page {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
padding-right: 20px;
|
||||
border-right: 1px solid var(--base300);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import WebsiteHeader from '../../WebsiteHeader';
|
|||
import SessionInfo from './SessionInfo';
|
||||
import { useSession } from 'components/hooks';
|
||||
import { Loading } from 'react-basics';
|
||||
import Profile from 'components/common/Profile';
|
||||
import styles from './SessionDetailsPage.module.css';
|
||||
|
||||
export default function SessionDetailsPage({
|
||||
|
|
@ -19,9 +20,15 @@ export default function SessionDetailsPage({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<SessionInfo data={data} />
|
||||
</div>
|
||||
<div className={styles.page}>
|
||||
<div className={styles.sidebar}>
|
||||
<Profile seed={data?.id} />
|
||||
<SessionInfo data={data} />
|
||||
</div>
|
||||
<div className={styles.content}>oh hi.</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
.info {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.info dt {
|
||||
color: var(--font-color200);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.info dd {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin: 5px 0 28px;
|
||||
text-align: left;
|
||||
}
|
||||
|
|
@ -1,17 +1,62 @@
|
|||
import Profile from 'components/common/Profile';
|
||||
import { format } from 'date-fns';
|
||||
import { useFormat, useLocale, useMessages, useRegionNames } from 'components/hooks';
|
||||
import TypeIcon from 'components/common/TypeIcon';
|
||||
import { Icon, CopyIcon } from 'react-basics';
|
||||
import Icons from 'components/icons';
|
||||
import styles from './SessionInfo.module.css';
|
||||
|
||||
export default function SessionInfo({ data }) {
|
||||
const { locale } = useLocale();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { formatValue } = useFormat();
|
||||
const { getRegionName } = useRegionNames(locale);
|
||||
|
||||
return (
|
||||
<h1>
|
||||
<Profile seed={data?.id} />
|
||||
<div className={styles.info}>
|
||||
<dl>
|
||||
<dt>ID</dt>
|
||||
<dd>{data?.id}</dd>
|
||||
<dt>Country</dt>
|
||||
<dd>{data?.country}</dd>
|
||||
<dt>City</dt>
|
||||
<dd>{data?.city}</dd>
|
||||
<dd>
|
||||
{data?.id} <CopyIcon value={data?.id} />
|
||||
</dd>
|
||||
<dt>{formatMessage(labels.firstSeen)}</dt>
|
||||
<dd>{format(new Date(data?.firstAt), 'PPPpp')}</dd>
|
||||
<dt>{formatMessage(labels.lastSeen)}</dt>
|
||||
<dd>{format(new Date(data?.lastAt), 'PPPpp')}</dd>
|
||||
<dt>{formatMessage(labels.country)}</dt>
|
||||
<dd>
|
||||
<TypeIcon type="country" value={data?.country} />
|
||||
{formatValue(data?.country, 'country')}
|
||||
</dd>
|
||||
<dt>{formatMessage(labels.region)}</dt>
|
||||
<dd>
|
||||
<Icon>
|
||||
<Icons.Location />
|
||||
</Icon>
|
||||
{getRegionName(data?.subdivision1)}
|
||||
</dd>
|
||||
<dt>{formatMessage(labels.city)}</dt>
|
||||
<dd>
|
||||
<Icon>
|
||||
<Icons.Location />
|
||||
</Icon>
|
||||
{data?.city}
|
||||
</dd>
|
||||
<dt>{formatMessage(labels.os)}</dt>
|
||||
<dd>
|
||||
<TypeIcon type="os" value={data?.os?.toLowerCase()?.replaceAll(/\W/g, '-')} />
|
||||
{formatValue(data?.os, 'os')}
|
||||
</dd>
|
||||
<dt>{formatMessage(labels.device)}</dt>
|
||||
<dd>
|
||||
<TypeIcon type="device" value={data?.device} />
|
||||
{formatValue(data?.device, 'device')}
|
||||
</dd>
|
||||
<dt>{formatMessage(labels.browser)}</dt>
|
||||
<dd>
|
||||
<TypeIcon type="browser" value={data?.browser} />
|
||||
{formatValue(data?.browser, 'browser')}
|
||||
</dd>
|
||||
</dl>
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
18
src/components/common/TypeIcon.tsx
Normal file
18
src/components/common/TypeIcon.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
export function TypeIcon({
|
||||
type,
|
||||
value,
|
||||
}: {
|
||||
type: 'browser' | 'country' | 'device' | 'os';
|
||||
value: string;
|
||||
}) {
|
||||
return (
|
||||
<img
|
||||
src={`${process.env.basePath || ''}/images/${type}/${value || 'unknown'}.png`}
|
||||
alt={value}
|
||||
width={type === 'country' ? undefined : 16}
|
||||
height={type === 'country' ? undefined : 16}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default TypeIcon;
|
||||
|
|
@ -32,6 +32,7 @@ export * from './useLocale';
|
|||
export * from './useMessages';
|
||||
export * from './useModified';
|
||||
export * from './useNavigation';
|
||||
export * from './useRegionNames';
|
||||
export * from './useSticky';
|
||||
export * from './useTeamUrl';
|
||||
export * from './useTheme';
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export function useCountryNames(locale: string) {
|
|||
}
|
||||
}, [locale]);
|
||||
|
||||
return list;
|
||||
return { countryNames: list };
|
||||
}
|
||||
|
||||
export default useCountryNames;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import regions from '../../../public/iso-3166-2.json';
|
|||
export function useFormat() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { locale } = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const { countryNames } = useCountryNames(locale);
|
||||
|
||||
const formatOS = (value: string): string => {
|
||||
return OS_NAMES[value] || value;
|
||||
|
|
|
|||
19
src/components/hooks/useRegionNames.ts
Normal file
19
src/components/hooks/useRegionNames.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import useCountryNames from './useCountryNames';
|
||||
import regions from '../../../public/iso-3166-2.json';
|
||||
|
||||
export function useRegionNames(locale: string) {
|
||||
const { countryNames } = useCountryNames(locale);
|
||||
|
||||
const getRegionName = (regionCode: string, countryCode?: string) => {
|
||||
if (!countryCode) {
|
||||
return regions[regionCode];
|
||||
}
|
||||
|
||||
const region = regionCode.includes('-') ? regionCode : `${countryCode}-${regionCode}`;
|
||||
return regions[region] ? `${regions[region]}, ${countryNames[countryCode]}` : region;
|
||||
};
|
||||
|
||||
return { regionNames: regions, getRegionName };
|
||||
}
|
||||
|
||||
export default useRegionNames;
|
||||
|
|
@ -11,6 +11,7 @@ import Dashboard from 'assets/dashboard.svg';
|
|||
import Eye from 'assets/eye.svg';
|
||||
import Gear from 'assets/gear.svg';
|
||||
import Globe from 'assets/globe.svg';
|
||||
import Location from 'assets/location.svg';
|
||||
import Lock from 'assets/lock.svg';
|
||||
import Logo from 'assets/logo.svg';
|
||||
import Magnet from 'assets/magnet.svg';
|
||||
|
|
@ -38,6 +39,7 @@ const icons = {
|
|||
Eye,
|
||||
Gear,
|
||||
Globe,
|
||||
Location,
|
||||
Lock,
|
||||
Logo,
|
||||
Magnet,
|
||||
|
|
|
|||
|
|
@ -272,6 +272,8 @@ export const labels = defineMessages({
|
|||
previous: { id: 'label.previous', defaultMessage: 'Previous' },
|
||||
previousPeriod: { id: 'label.previous-period', defaultMessage: 'Previous period' },
|
||||
previousYear: { id: 'label.previous-year', defaultMessage: 'Previous year' },
|
||||
lastSeen: { id: 'label.last-seen', defaultMessage: 'Last seen' },
|
||||
firstSeen: { id: 'label.first-seen', defaultMessage: 'First seen' },
|
||||
});
|
||||
|
||||
export const messages = defineMessages({
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import FilterLink from 'components/common/FilterLink';
|
|||
import MetricsTable, { MetricsTableProps } from 'components/metrics/MetricsTable';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { useFormat } from 'components/hooks';
|
||||
import TypeIcon from 'components/common/TypeIcon';
|
||||
|
||||
export function BrowsersTable(props: MetricsTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
|
@ -10,12 +11,7 @@ export function BrowsersTable(props: MetricsTableProps) {
|
|||
function renderLink({ x: browser }) {
|
||||
return (
|
||||
<FilterLink id="browser" value={browser} label={formatBrowser(browser)}>
|
||||
<img
|
||||
src={`${process.env.basePath || ''}/images/browsers/${browser || 'unknown'}.png`}
|
||||
alt={browser}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
<TypeIcon type="browser" value={browser} />
|
||||
</FilterLink>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import MetricsTable, { MetricsTableProps } from './MetricsTable';
|
||||
import { emptyFilter } from 'lib/filters';
|
||||
import FilterLink from 'components/common/FilterLink';
|
||||
import TypeIcon from 'components/common/TypeIcon';
|
||||
import { useLocale } from 'components/hooks';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { useCountryNames } from 'components/hooks';
|
||||
|
|
@ -8,7 +9,7 @@ import { useCountryNames } from 'components/hooks';
|
|||
export function CitiesTable(props: MetricsTableProps) {
|
||||
const { locale } = useLocale();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const { countryNames } = useCountryNames(locale);
|
||||
|
||||
const renderLabel = (city: string, country: string) => {
|
||||
const countryName = countryNames[country];
|
||||
|
|
@ -18,12 +19,7 @@ export function CitiesTable(props: MetricsTableProps) {
|
|||
const renderLink = ({ x: city, country }) => {
|
||||
return (
|
||||
<FilterLink id="city" value={city} label={renderLabel(city, country)}>
|
||||
{country && (
|
||||
<img
|
||||
src={`${process.env.basePath || ''}/images/flags/${country?.toLowerCase() || 'xx'}.png`}
|
||||
alt={country}
|
||||
/>
|
||||
)}
|
||||
{country && <TypeIcon type="country" value={country} />}
|
||||
</FilterLink>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import FilterLink from 'components/common/FilterLink';
|
|||
import { useCountryNames } from 'components/hooks';
|
||||
import { useLocale, useMessages, useFormat } from 'components/hooks';
|
||||
import MetricsTable, { MetricsTableProps } from './MetricsTable';
|
||||
import TypeIcon from 'components/common/TypeIcon';
|
||||
|
||||
export function CountriesTable({
|
||||
onDataLoad,
|
||||
|
|
@ -10,7 +11,7 @@ export function CountriesTable({
|
|||
onDataLoad: (data: any) => void;
|
||||
} & MetricsTableProps) {
|
||||
const { locale } = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const { countryNames } = useCountryNames(locale);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { formatCountry } = useFormat();
|
||||
|
||||
|
|
@ -26,10 +27,7 @@ export function CountriesTable({
|
|||
value={countryNames[code] && code}
|
||||
label={formatCountry(code)}
|
||||
>
|
||||
<img
|
||||
src={`${process.env.basePath || ''}/images/flags/${code?.toLowerCase() || 'xx'}.png`}
|
||||
alt={code}
|
||||
/>
|
||||
<TypeIcon type="country" value={code?.toLowerCase()} />
|
||||
</FilterLink>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import MetricsTable, { MetricsTableProps } from './MetricsTable';
|
|||
import FilterLink from 'components/common/FilterLink';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { useFormat } from 'components/hooks';
|
||||
import TypeIcon from 'components/common/TypeIcon';
|
||||
|
||||
export function DevicesTable(props: MetricsTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
|
@ -10,14 +11,7 @@ export function DevicesTable(props: MetricsTableProps) {
|
|||
function renderLink({ x: device }) {
|
||||
return (
|
||||
<FilterLink id="device" value={labels[device] && device} label={formatDevice(device)}>
|
||||
<img
|
||||
src={`${process.env.basePath || ''}/images/device/${
|
||||
device?.toLowerCase() || 'unknown'
|
||||
}.png`}
|
||||
alt={device}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
<TypeIcon type="device" value={device?.toLowerCase()} />
|
||||
</FilterLink>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import MetricsTable, { MetricsTableProps } from './MetricsTable';
|
||||
import FilterLink from 'components/common/FilterLink';
|
||||
import { useMessages, useFormat } from 'components/hooks';
|
||||
import TypeIcon from 'components/common/TypeIcon';
|
||||
|
||||
export function OSTable(props: MetricsTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
|
@ -9,14 +10,7 @@ export function OSTable(props: MetricsTableProps) {
|
|||
function renderLink({ x: os }) {
|
||||
return (
|
||||
<FilterLink id="os" value={os} label={formatOS(os)}>
|
||||
<img
|
||||
src={`${process.env.basePath || ''}/images/os/${
|
||||
os?.toLowerCase()?.replaceAll(/\W/g, '-') || 'unknown'
|
||||
}.png`}
|
||||
alt={os}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
<TypeIcon type="os" value={os?.toLowerCase()?.replaceAll(/\W/g, '-')} />
|
||||
</FilterLink>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,18 @@
|
|||
import FilterLink from 'components/common/FilterLink';
|
||||
import { emptyFilter } from 'lib/filters';
|
||||
import { useLocale } from 'components/hooks';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { useCountryNames } from 'components/hooks';
|
||||
import { useMessages, useLocale, useRegionNames } from 'components/hooks';
|
||||
import MetricsTable, { MetricsTableProps } from './MetricsTable';
|
||||
import regions from '../../../public/iso-3166-2.json';
|
||||
import TypeIcon from 'components/common/TypeIcon';
|
||||
|
||||
export function RegionsTable(props: MetricsTableProps) {
|
||||
const { locale } = useLocale();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const countryNames = useCountryNames(locale);
|
||||
|
||||
const renderLabel = (code: string, country: string) => {
|
||||
const region = code.includes('-') ? code : `${country}-${code}`;
|
||||
return regions[region] ? `${regions[region]}, ${countryNames[country]}` : region;
|
||||
};
|
||||
const { getRegionName } = useRegionNames(locale);
|
||||
|
||||
const renderLink = ({ x: code, country }) => {
|
||||
return (
|
||||
<FilterLink id="region" className={locale} value={code} label={renderLabel(code, country)}>
|
||||
<img
|
||||
src={`${process.env.basePath || ''}/images/flags/${country?.toLowerCase() || 'xx'}.png`}
|
||||
alt={code}
|
||||
/>
|
||||
<FilterLink id="region" className={locale} value={code} label={getRegionName(code, country)}>
|
||||
<TypeIcon type="country" value={country?.toLowerCase()} />
|
||||
</FilterLink>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export function WorldMap({ data = [], className }: { data?: any[]; className?: s
|
|||
const { theme, colors } = useTheme();
|
||||
const { locale } = useLocale();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const { countryNames } = useCountryNames(locale);
|
||||
const visitorsLabel = formatMessage(labels.visitors).toLocaleLowerCase(locale);
|
||||
const metrics = useMemo(() => (data ? percentFilter(data) : []), [data]);
|
||||
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@ async function clickhouseQuery(websiteId: string, sessionId: string) {
|
|||
select
|
||||
session_id as id,
|
||||
website_id as websiteId,
|
||||
min(created_at) as firstAt,
|
||||
max(created_at) as lastAt,
|
||||
hostname,
|
||||
browser,
|
||||
os,
|
||||
|
|
@ -35,7 +33,9 @@ async function clickhouseQuery(websiteId: string, sessionId: string) {
|
|||
language,
|
||||
country,
|
||||
subdivision1,
|
||||
city
|
||||
city,
|
||||
min(created_at) as firstAt,
|
||||
max(created_at) as lastAt
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and session_id = {sessionId:UUID}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, pagePar
|
|||
select
|
||||
session_id as id,
|
||||
website_id as websiteId,
|
||||
min(created_at) as createdAt,
|
||||
hostname,
|
||||
browser,
|
||||
os,
|
||||
|
|
@ -41,13 +40,15 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, pagePar
|
|||
language,
|
||||
country,
|
||||
subdivision1,
|
||||
city
|
||||
city,
|
||||
min(created_at) as firstAt,
|
||||
max(created_at) as lastAt
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
${dateQuery}
|
||||
${filterQuery}
|
||||
group by session_id, website_id, hostname, browser, os, device, screen, language, country, subdivision1, city
|
||||
order by createdAt desc
|
||||
order by lastAt desc
|
||||
`,
|
||||
params,
|
||||
pageParams,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue