mirror of
https://github.com/umami-software/umami.git
synced 2026-02-13 09:05:36 +01:00
New menu layout.
This commit is contained in:
parent
0cfee43814
commit
1c22c18de5
17 changed files with 103 additions and 47 deletions
|
|
@ -19,6 +19,7 @@ export function App({ children }) {
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
window.location.href = `${process.env.basePath || ''}/login`;
|
window.location.href = `${process.env.basePath || ''}/login`;
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user || !config) {
|
if (!user || !config) {
|
||||||
|
|
@ -30,7 +31,7 @@ export function App({ children }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid height="100vh" width="100%" columns="auto 1fr" rows="auto 1fr" overflow="hidden">
|
<Grid height="100vh" width="100%" columns="auto 1fr" rows="auto 1fr">
|
||||||
<Nav gridColumn="1 / 2" gridRow="1 / 3" />
|
<Nav gridColumn="1 / 2" gridRow="1 / 3" />
|
||||||
<MenuBar gridColumn="2 / 3" gridRow="1 / 2" />
|
<MenuBar gridColumn="2 / 3" gridRow="1 / 2" />
|
||||||
<Column
|
<Column
|
||||||
|
|
@ -39,6 +40,7 @@ export function App({ children }) {
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
overflow="auto"
|
overflow="auto"
|
||||||
backgroundColor="2"
|
backgroundColor="2"
|
||||||
|
position="relative"
|
||||||
>
|
>
|
||||||
<Page>
|
<Page>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export function Nav(props: any) {
|
||||||
].filter(n => n);
|
].filter(n => n);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SideNav {...props} isCollapsed={isCollapsed} variant="0" showBorder={true}>
|
<SideNav {...props} isCollapsed={isCollapsed} variant="2" showBorder={true}>
|
||||||
<SideNavSection>
|
<SideNavSection>
|
||||||
<SideNavHeader label="umami" icon={<Icons.Logo />} />
|
<SideNavHeader label="umami" icon={<Icons.Logo />} />
|
||||||
</SideNavSection>
|
</SideNavSection>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { Panel } from '@/components/common/Panel';
|
||||||
import { useNavigation } from '@/components/hooks';
|
import { useNavigation } from '@/components/hooks';
|
||||||
import { WebsiteChart } from './WebsiteChart';
|
import { WebsiteChart } from './WebsiteChart';
|
||||||
import { WebsiteExpandedView } from './WebsiteExpandedView';
|
import { WebsiteExpandedView } from './WebsiteExpandedView';
|
||||||
import { WebsiteHeader } from './WebsiteHeader';
|
|
||||||
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
|
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
|
||||||
import { WebsiteTableView } from './WebsiteTableView';
|
import { WebsiteTableView } from './WebsiteTableView';
|
||||||
import { WebsiteCompareTables } from './WebsiteCompareTables';
|
import { WebsiteCompareTables } from './WebsiteCompareTables';
|
||||||
|
|
@ -15,8 +14,7 @@ export function WebsiteDetailsPage({ websiteId }: { websiteId: string }) {
|
||||||
} = useNavigation();
|
} = useNavigation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap="3">
|
<Column gap>
|
||||||
<WebsiteHeader websiteId={websiteId} />
|
|
||||||
<Panel>
|
<Panel>
|
||||||
<WebsiteMetricsBar websiteId={websiteId} showFilter={true} showChange={true} />
|
<WebsiteMetricsBar websiteId={websiteId} showFilter={true} showChange={true} />
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
|
||||||
13
src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx
Normal file
13
src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
'use client';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { WebsiteProvider } from './WebsiteProvider';
|
||||||
|
import { WebsiteHeader } from '@/app/(main)/websites/[websiteId]/WebsiteHeader';
|
||||||
|
|
||||||
|
export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<WebsiteProvider websiteId={websiteId}>
|
||||||
|
<WebsiteHeader websiteId={websiteId} />
|
||||||
|
{children}
|
||||||
|
</WebsiteProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Tabs, TabList, Tab, Icon, Text, Row } from '@umami/react-zen';
|
import { Column, Icon, Text, Row } from '@umami/react-zen';
|
||||||
import { Icons } from '@/components/icons';
|
import { Icons } from '@/components/icons';
|
||||||
import { useMessages, useNavigation } from '@/components/hooks';
|
import { useMessages, useNavigation } from '@/components/hooks';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
export function WebsiteTabs({ websiteId }: { websiteId: string }) {
|
export function WebsiteTabs({ websiteId }: { websiteId: string }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
@ -31,6 +32,54 @@ export function WebsiteTabs({ websiteId }: { websiteId: string }) {
|
||||||
icon: <Icons.Clock />,
|
icon: <Icons.Clock />,
|
||||||
path: '/realtime',
|
path: '/realtime',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'insights',
|
||||||
|
label: formatMessage(labels.insights),
|
||||||
|
icon: <Icons.Lightbulb />,
|
||||||
|
path: '/insights',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'goals',
|
||||||
|
label: formatMessage(labels.goals),
|
||||||
|
icon: <Icons.Target />,
|
||||||
|
path: '/goals',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'funnel',
|
||||||
|
label: formatMessage(labels.funnel),
|
||||||
|
icon: <Icons.Funnel />,
|
||||||
|
path: '/funnels',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'journeys',
|
||||||
|
label: formatMessage(labels.journey),
|
||||||
|
icon: <Icons.Path />,
|
||||||
|
path: '/goals',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'retention',
|
||||||
|
label: formatMessage(labels.retention),
|
||||||
|
icon: <Icons.Magnet />,
|
||||||
|
path: '/funnels',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'utm',
|
||||||
|
label: formatMessage(labels.utm),
|
||||||
|
icon: <Icons.Tag />,
|
||||||
|
path: '/utm',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'revenue',
|
||||||
|
label: formatMessage(labels.revenue),
|
||||||
|
icon: <Icons.Money />,
|
||||||
|
path: '/revenue',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'attribution',
|
||||||
|
label: formatMessage(labels.attribution),
|
||||||
|
icon: <Icons.Network />,
|
||||||
|
path: '/attribution',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'reports',
|
id: 'reports',
|
||||||
label: formatMessage(labels.reports),
|
label: formatMessage(labels.reports),
|
||||||
|
|
@ -39,24 +88,29 @@ export function WebsiteTabs({ websiteId }: { websiteId: string }) {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const selectedKey = links
|
const selected = links
|
||||||
? links.find(({ path }) => path && pathname.endsWith(path))?.id
|
? links.find(({ path }) => path && pathname.endsWith(path))?.id
|
||||||
: 'overview';
|
: 'overview';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs selectedKey={selectedKey}>
|
<Column gap="2" position="absolute" padding="4" style={{ top: 0, left: 0, bottom: 0 }}>
|
||||||
<TabList items={links}>
|
{links.map(({ id, label, icon, path }) => {
|
||||||
{({ id, label, icon, path }) => {
|
return (
|
||||||
return (
|
<Link key={id} href={renderTeamUrl(`/websites/${websiteId}/${path}`)}>
|
||||||
<Tab id={id} href={renderTeamUrl(`/websites/${websiteId}/${path}`)}>
|
<Row
|
||||||
<Row gap="3" alignItems="center">
|
alignItems="center"
|
||||||
<Icon fillColor="currentColor">{icon}</Icon>
|
padding
|
||||||
<Text>{label}</Text>
|
gap
|
||||||
</Row>
|
hoverBackgroundColor="3"
|
||||||
</Tab>
|
borderRadius
|
||||||
);
|
width="160px"
|
||||||
}}
|
>
|
||||||
</TabList>
|
<Icon fillColor="currentColor">{icon}</Icon>
|
||||||
</Tabs>
|
<Text weight={selected === id ? 'bold' : undefined}>{label}</Text>
|
||||||
|
</Row>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
import { TabList, Tab, Tabs, TabPanel, Column } from '@umami/react-zen';
|
import { TabList, Tab, Tabs, TabPanel, Column } from '@umami/react-zen';
|
||||||
import { EventsTable } from '@/components/metrics/EventsTable';
|
import { EventsTable } from '@/components/metrics/EventsTable';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { WebsiteHeader } from '../WebsiteHeader';
|
|
||||||
import { EventsDataTable } from './EventsDataTable';
|
import { EventsDataTable } from './EventsDataTable';
|
||||||
import { EventsMetricsBar } from './EventsMetricsBar';
|
import { EventsMetricsBar } from './EventsMetricsBar';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
|
|
@ -22,7 +21,6 @@ export function EventsPage({ websiteId }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap="3">
|
<Column gap="3">
|
||||||
<WebsiteHeader websiteId={websiteId} />
|
|
||||||
<Panel>
|
<Panel>
|
||||||
<EventsMetricsBar websiteId={websiteId} />
|
<EventsMetricsBar websiteId={websiteId} />
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { WebsiteProvider } from './WebsiteProvider';
|
import { WebsiteLayout } from '@/app/(main)/websites/[websiteId]/WebsiteLayout';
|
||||||
|
|
||||||
export default async function ({
|
export default async function ({
|
||||||
children,
|
children,
|
||||||
|
|
@ -10,7 +10,7 @@ export default async function ({
|
||||||
}) {
|
}) {
|
||||||
const { websiteId } = await params;
|
const { websiteId } = await params;
|
||||||
|
|
||||||
return <WebsiteProvider websiteId={websiteId}>{children}</WebsiteProvider>;
|
return <WebsiteLayout websiteId={websiteId}>{children}</WebsiteLayout>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import { RealtimeLog } from './RealtimeLog';
|
||||||
import { RealtimeHeader } from './RealtimeHeader';
|
import { RealtimeHeader } from './RealtimeHeader';
|
||||||
import { RealtimeUrls } from './RealtimeUrls';
|
import { RealtimeUrls } from './RealtimeUrls';
|
||||||
import { RealtimeCountries } from './RealtimeCountries';
|
import { RealtimeCountries } from './RealtimeCountries';
|
||||||
import { WebsiteHeader } from '../WebsiteHeader';
|
|
||||||
import { percentFilter } from '@/lib/filters';
|
import { percentFilter } from '@/lib/filters';
|
||||||
|
|
||||||
export function WebsiteRealtimePage({ websiteId }: { websiteId: string }) {
|
export function WebsiteRealtimePage({ websiteId }: { websiteId: string }) {
|
||||||
|
|
@ -29,7 +28,6 @@ export function WebsiteRealtimePage({ websiteId }: { websiteId: string }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid gap="3">
|
<Grid gap="3">
|
||||||
<WebsiteHeader websiteId={websiteId} />
|
|
||||||
<Panel>
|
<Panel>
|
||||||
<RealtimeHeader data={data} />
|
<RealtimeHeader data={data} />
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Button, Flexbox, Icon, Icons, Text } from '@umami/react-zen';
|
import { Button, Flexbox, Icon, Icons, Text } from '@umami/react-zen';
|
||||||
import { useMessages, useNavigation } from '@/components/hooks';
|
import { useMessages, useNavigation } from '@/components/hooks';
|
||||||
import { WebsiteHeader } from '../WebsiteHeader';
|
|
||||||
import { ReportsDataTable } from '@/app/(main)/reports/ReportsDataTable';
|
import { ReportsDataTable } from '@/app/(main)/reports/ReportsDataTable';
|
||||||
|
|
||||||
export function WebsiteReportsPage({ websiteId }) {
|
export function WebsiteReportsPage({ websiteId }) {
|
||||||
|
|
@ -11,7 +10,6 @@ export function WebsiteReportsPage({ websiteId }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<WebsiteHeader websiteId={websiteId} />
|
|
||||||
<Flexbox alignItems="center" justifyContent="end">
|
<Flexbox alignItems="center" justifyContent="end">
|
||||||
<Link href={renderTeamUrl('/reports/create')}>
|
<Link href={renderTeamUrl('/reports/create')}>
|
||||||
<Button variant="primary">
|
<Button variant="primary">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { TabList, Tab, Tabs, TabPanel, Column } from '@umami/react-zen';
|
import { TabList, Tab, Tabs, TabPanel, Column } from '@umami/react-zen';
|
||||||
import { WebsiteHeader } from '../WebsiteHeader';
|
|
||||||
import { SessionsDataTable } from './SessionsDataTable';
|
import { SessionsDataTable } from './SessionsDataTable';
|
||||||
import { SessionsMetricsBar } from './SessionsMetricsBar';
|
import { SessionsMetricsBar } from './SessionsMetricsBar';
|
||||||
import { SessionProperties } from './SessionProperties';
|
import { SessionProperties } from './SessionProperties';
|
||||||
|
|
@ -17,7 +16,6 @@ export function SessionsPage({ websiteId }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap="3">
|
<Column gap="3">
|
||||||
<WebsiteHeader websiteId={websiteId} />
|
|
||||||
<Panel>
|
<Panel>
|
||||||
<SessionsMetricsBar websiteId={websiteId} />
|
<SessionsMetricsBar websiteId={websiteId} />
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { Grid, Row, Column } from '@umami/react-zen';
|
||||||
import { Avatar } from '@/components/common/Avatar';
|
import { Avatar } from '@/components/common/Avatar';
|
||||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
import { useWebsiteSessionQuery } from '@/components/hooks';
|
import { useWebsiteSessionQuery } from '@/components/hooks';
|
||||||
import { WebsiteHeader } from '@/app/(main)/websites/[websiteId]/WebsiteHeader';
|
|
||||||
import { SessionActivity } from './SessionActivity';
|
import { SessionActivity } from './SessionActivity';
|
||||||
import { SessionData } from './SessionData';
|
import { SessionData } from './SessionData';
|
||||||
import { SessionInfo } from './SessionInfo';
|
import { SessionInfo } from './SessionInfo';
|
||||||
|
|
@ -21,7 +20,6 @@ export function SessionDetailsPage({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoadingPanel {...query} loadingIcon="spinner" data={data}>
|
<LoadingPanel {...query} loadingIcon="spinner" data={data}>
|
||||||
<WebsiteHeader websiteId={websiteId} />
|
|
||||||
<Grid
|
<Grid
|
||||||
gap
|
gap
|
||||||
columns={{ xs: '1fr', sm: '1fr', md: '1fr 1fr', lg: '1fr 2fr 1fr', xl: '1fr 2fr 1fr' }}
|
columns={{ xs: '1fr', sm: '1fr', md: '1fr 1fr', lg: '1fr 2fr 1fr', xl: '1fr 2fr 1fr' }}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Icon, Row, Text } from '@umami/react-zen';
|
import { Icon, Row, Text } from '@umami/react-zen';
|
||||||
import { differenceInDays, isSameDay } from 'date-fns';
|
import { differenceInDays, isSameDay } from 'date-fns';
|
||||||
import { useLocale } from '@/components/hooks';
|
import { useLocale } from '@/components/hooks';
|
||||||
import { Icons } from '@/components/icons';
|
import { Lucide } from '@/components/icons';
|
||||||
import { formatDate } from '@/lib/date';
|
import { formatDate } from '@/lib/date';
|
||||||
|
|
||||||
export function DateDisplay({ startDate, endDate }) {
|
export function DateDisplay({ startDate, endDate }) {
|
||||||
|
|
@ -11,7 +11,7 @@ export function DateDisplay({ startDate, endDate }) {
|
||||||
return (
|
return (
|
||||||
<Row gap="3" alignItems="center" wrap="nowrap">
|
<Row gap="3" alignItems="center" wrap="nowrap">
|
||||||
<Icon>
|
<Icon>
|
||||||
<Icons.Calendar />
|
<Lucide.Calendar />
|
||||||
</Icon>
|
</Icon>
|
||||||
<Text wrap="nowrap">
|
<Text wrap="nowrap">
|
||||||
{isSingleDate ? (
|
{isSingleDate ? (
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ export function DateFilter({
|
||||||
placeholder={formatMessage(labels.selectDate)}
|
placeholder={formatMessage(labels.selectDate)}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
renderValue={renderValue}
|
renderValue={renderValue}
|
||||||
|
popoverProps={{ style: { width: 200 } }}
|
||||||
>
|
>
|
||||||
{options.map(({ label, value, divider }: any) => {
|
{options.map(({ label, value, divider }: any) => {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -85,8 +85,8 @@ export function WebsiteDateFilter({
|
||||||
/>
|
/>
|
||||||
{!isAllTime && compare && (
|
{!isAllTime && compare && (
|
||||||
<Row alignItems="center" gap>
|
<Row alignItems="center" gap>
|
||||||
<Text>VS</Text>
|
<Text weight="bold">VS</Text>
|
||||||
<Select selectedKey={compare} onSelectionChange={handleSelect} style={{ width: '200px' }}>
|
<Select value={compare} onChange={handleSelect} popoverProps={{ style: { width: 200 } }}>
|
||||||
<ListItem id="prev">{formatMessage(labels.previousPeriod)}</ListItem>
|
<ListItem id="prev">{formatMessage(labels.previousPeriod)}</ListItem>
|
||||||
<ListItem id="yoy">{formatMessage(labels.previousYear)}</ListItem>
|
<ListItem id="yoy">{formatMessage(labels.previousYear)}</ListItem>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
|
||||||
|
|
@ -26,16 +26,16 @@ export function FilterBar({ websiteId }: { websiteId: string }) {
|
||||||
return (
|
return (
|
||||||
<Row
|
<Row
|
||||||
gap
|
gap
|
||||||
backgroundColor="1"
|
backgroundColor="3"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
paddingY="3"
|
paddingY="2"
|
||||||
paddingLeft="5"
|
paddingLeft="3"
|
||||||
paddingRight="2"
|
paddingRight="2"
|
||||||
border
|
border
|
||||||
borderRadius="2"
|
borderRadius="2"
|
||||||
>
|
>
|
||||||
<Row alignItems="center" gap="3" wrap="wrap">
|
<Row alignItems="center" gap="3" wrap="wrap" paddingX="2">
|
||||||
<Text color="11" weight="bold">
|
<Text color="11" weight="bold">
|
||||||
{formatMessage(labels.filters)}
|
{formatMessage(labels.filters)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -61,11 +61,9 @@ export function FilterBar({ websiteId }: { websiteId: string }) {
|
||||||
<Text color="11">{operatorLabels[operator]}</Text>
|
<Text color="11">{operatorLabels[operator]}</Text>
|
||||||
<Text weight="bold">{paramValue}</Text>
|
<Text weight="bold">{paramValue}</Text>
|
||||||
</Row>
|
</Row>
|
||||||
<Button variant="quiet" size="xs" style={{ left: '5px' }}>
|
<Icon onClick={e => handleCloseFilter(name, e)} size="xs">
|
||||||
<Icon onClick={e => handleCloseFilter(name, e)} size="xs">
|
<Icons.Close />
|
||||||
<Icons.Close />
|
</Icon>
|
||||||
</Icon>
|
|
||||||
</Button>
|
|
||||||
</Row>
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
1
src/declaration.d.ts
vendored
1
src/declaration.d.ts
vendored
|
|
@ -6,3 +6,4 @@ declare module 'fs-extra';
|
||||||
declare module 'jsonwebtoken';
|
declare module 'jsonwebtoken';
|
||||||
declare module 'md5';
|
declare module 'md5';
|
||||||
declare module 'prettier';
|
declare module 'prettier';
|
||||||
|
declare module 'semver';
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ body {
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a,
|
a,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue