mirror of
https://github.com/umami-software/umami.git
synced 2026-02-08 14:47:14 +01:00
Merge branch 'dev' into master
This commit is contained in:
commit
43a278c207
570 changed files with 20083 additions and 8319 deletions
|
|
@ -27,7 +27,7 @@ export function App({ children }) {
|
|||
{children}
|
||||
<UpdateNotice user={user} config={config} />
|
||||
{process.env.NODE_ENV === 'production' && !pathname.includes('/share/') && (
|
||||
<Script src={`telemetry.js`} />
|
||||
<Script src={`${process.env.basePath}/telemetry.js`} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
background: var(--base75);
|
||||
border-bottom: 1px solid var(--base300);
|
||||
padding: 0 20px;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.logo {
|
||||
|
|
@ -26,6 +27,7 @@
|
|||
padding: 0 40px;
|
||||
font-weight: 700;
|
||||
max-height: 60px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.links a,
|
||||
|
|
|
|||
|
|
@ -2,35 +2,37 @@
|
|||
import { Icon, Text } from 'react-basics';
|
||||
import Link from 'next/link';
|
||||
import classNames from 'classnames';
|
||||
import Icons from 'components/icons';
|
||||
import HamburgerButton from 'components/common/HamburgerButton';
|
||||
import ThemeButton from 'components/input/ThemeButton';
|
||||
import LanguageButton from 'components/input/LanguageButton';
|
||||
import ProfileButton from 'components/input/ProfileButton';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import HamburgerButton from 'components/common/HamburgerButton';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import TeamsButton from 'components/input/TeamsButton';
|
||||
import Icons from 'components/icons';
|
||||
import { useMessages, useNavigation, useTeamUrl } from 'components/hooks';
|
||||
import styles from './NavBar.module.css';
|
||||
|
||||
export function NavBar() {
|
||||
const pathname = usePathname();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const cloudMode = Boolean(process.env.cloudMode);
|
||||
const { pathname, router } = useNavigation();
|
||||
const { renderTeamUrl } = useTeamUrl();
|
||||
|
||||
const cloudMode = !!process.env.cloudMode;
|
||||
|
||||
const links = [
|
||||
{ label: formatMessage(labels.dashboard), url: '/dashboard' },
|
||||
{ label: formatMessage(labels.websites), url: '/websites' },
|
||||
{ label: formatMessage(labels.reports), url: '/reports' },
|
||||
{ label: formatMessage(labels.settings), url: '/settings' },
|
||||
{ label: formatMessage(labels.dashboard), url: renderTeamUrl('/dashboard') },
|
||||
{ label: formatMessage(labels.websites), url: renderTeamUrl('/websites') },
|
||||
{ label: formatMessage(labels.reports), url: renderTeamUrl('/reports') },
|
||||
{ label: formatMessage(labels.settings), url: renderTeamUrl('/settings') },
|
||||
].filter(n => n);
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
label: formatMessage(labels.dashboard),
|
||||
url: '/dashboard',
|
||||
url: renderTeamUrl('/dashboard'),
|
||||
},
|
||||
!cloudMode && {
|
||||
label: formatMessage(labels.settings),
|
||||
url: '/settings',
|
||||
url: renderTeamUrl('/settings'),
|
||||
children: [
|
||||
{
|
||||
label: formatMessage(labels.websites),
|
||||
|
|
@ -46,17 +48,23 @@ export function NavBar() {
|
|||
},
|
||||
{
|
||||
label: formatMessage(labels.profile),
|
||||
url: '/settings/profile',
|
||||
url: '/profile',
|
||||
},
|
||||
],
|
||||
},
|
||||
cloudMode && {
|
||||
label: formatMessage(labels.profile),
|
||||
url: '/settings/profile',
|
||||
url: '/profile',
|
||||
},
|
||||
!cloudMode && { label: formatMessage(labels.logout), url: '/logout' },
|
||||
].filter(n => n);
|
||||
|
||||
const handleTeamChange = (teamId: string) => {
|
||||
const url = teamId ? `/teams/${teamId}` : '/';
|
||||
|
||||
router.push(cloudMode ? `${process.env.cloudUrl}${url}` : url);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.navbar}>
|
||||
<div className={styles.logo}>
|
||||
|
|
@ -72,6 +80,7 @@ export function NavBar() {
|
|||
key={url}
|
||||
href={url}
|
||||
className={classNames({ [styles.selected]: pathname.startsWith(url) })}
|
||||
prefetch={url !== '/settings'}
|
||||
>
|
||||
<Text>{label}</Text>
|
||||
</Link>
|
||||
|
|
@ -79,6 +88,7 @@ export function NavBar() {
|
|||
})}
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
<TeamsButton onChange={handleTeamChange} />
|
||||
<ThemeButton />
|
||||
<LanguageButton />
|
||||
<ProfileButton />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
import { useEffect, useCallback, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Button } from 'react-basics';
|
||||
|
|
@ -6,7 +5,7 @@ import { setItem } from 'next-basics';
|
|||
import useStore, { checkVersion } from 'store/version';
|
||||
import { REPO_URL, VERSION_CHECK } from 'lib/constants';
|
||||
import styles from './UpdateNotice.module.css';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
export function UpdateNotice({ user, config }) {
|
||||
|
|
|
|||
6
src/app/(main)/console/ConsolePage.tsx
Normal file
6
src/app/(main)/console/ConsolePage.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
'use client';
|
||||
import TestConsole from './TestConsole';
|
||||
|
||||
export default function ConsolePage({ websiteId }) {
|
||||
return <TestConsole websiteId={websiteId} />;
|
||||
}
|
||||
|
|
@ -1,7 +1,17 @@
|
|||
.test {
|
||||
.container {
|
||||
display: grid;
|
||||
gap: 30px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
border: 1px solid var(--base400);
|
||||
border-radius: 5px;
|
||||
padding: 0 20px 20px 20px;
|
||||
display: grid;
|
||||
gap: 40px;
|
||||
grid-template-columns: repeat(3, minmax(300px, 1fr));
|
||||
box-shadow: 0 0 0 10px var(--base100);
|
||||
}
|
||||
|
||||
.header {
|
||||
|
|
@ -9,3 +19,15 @@
|
|||
font-weight: 700;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.wrapped {
|
||||
border: 1px solid var(--blue900);
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
'use client';
|
||||
import { Button } from 'react-basics';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import Script from 'next/script';
|
||||
import WebsiteSelect from 'components/input/WebsiteSelect';
|
||||
import Page from 'components/layout/Page';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import EventsChart from 'components/metrics/EventsChart';
|
||||
import WebsiteChart from 'app/(main)/websites/[id]/WebsiteChart';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useNavigation from 'components/hooks/useNavigation';
|
||||
import WebsiteChart from '../websites/[websiteId]/WebsiteChart';
|
||||
import { useApi, useNavigation } from 'components/hooks';
|
||||
import styles from './TestConsole.module.css';
|
||||
|
||||
export function TestConsole({ websiteId }: { websiteId: string }) {
|
||||
|
|
@ -75,22 +72,19 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
|
|||
|
||||
return (
|
||||
<Page isLoading={isLoading} error={error}>
|
||||
<Head>
|
||||
<title>{website ? `${website.name} | Umami Console` : 'Umami Console'}</title>
|
||||
</Head>
|
||||
<PageHeader title="Test console">
|
||||
<WebsiteSelect websiteId={website?.id} onSelect={handleChange} />
|
||||
</PageHeader>
|
||||
{website && (
|
||||
<>
|
||||
<div className={styles.container}>
|
||||
<Script
|
||||
async
|
||||
data-website-id={websiteId}
|
||||
src={`${process.env.basePath}/script.js`}
|
||||
data-cache="true"
|
||||
/>
|
||||
<div className={styles.test}>
|
||||
<div>
|
||||
<div className={styles.actions}>
|
||||
<div className={styles.group}>
|
||||
<div className={styles.header}>Page links</div>
|
||||
<div>
|
||||
<Link href={`/console/${websiteId}/page/1/?q=abc`}>page one</Link>
|
||||
|
|
@ -114,12 +108,11 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.group}>
|
||||
<div className={styles.header}>Click events</div>
|
||||
<Button id="send-event-button" data-umami-event="button-click" variant="primary">
|
||||
Send event
|
||||
</Button>
|
||||
<p />
|
||||
<Button
|
||||
id="send-event-data-button"
|
||||
data-umami-event="button-click"
|
||||
|
|
@ -129,23 +122,39 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
|
|||
>
|
||||
Send event with data
|
||||
</Button>
|
||||
<Button
|
||||
id="button-with-div-button"
|
||||
data-umami-event="button-click"
|
||||
data-umami-event-name="bob"
|
||||
data-umami-event-id="123"
|
||||
variant="primary"
|
||||
>
|
||||
<div className={styles.wrapped}>Button with div</div>
|
||||
</Button>
|
||||
<div data-umami-event="div-click" className={styles.wrapped}>
|
||||
DIV with attribute
|
||||
</div>
|
||||
<div data-umami-event="div-click-one" className={styles.wrapped}>
|
||||
<div data-umami-event="div-click-two" className={styles.wrapped}>
|
||||
<div data-umami-event="div-click-three" className={styles.wrapped}>
|
||||
Nested DIV
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.group}>
|
||||
<div className={styles.header}>Javascript events</div>
|
||||
<Button id="manual-button" variant="primary" onClick={handleClick}>
|
||||
Run script
|
||||
</Button>
|
||||
<p />
|
||||
<Button id="manual-button" variant="primary" onClick={handleIdentifyClick}>
|
||||
Run identify
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<WebsiteChart websiteId={website.id} />
|
||||
<EventsChart websiteId={website.id} />
|
||||
</div>
|
||||
</>
|
||||
<WebsiteChart websiteId={website.id} />
|
||||
<EventsChart websiteId={website.id} />
|
||||
</div>
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
import TestConsole from '../TestConsole';
|
||||
import { Metadata } from 'next';
|
||||
import ConsolePage from '../ConsolePage';
|
||||
|
||||
async function getEnabled() {
|
||||
return !!process.env.ENABLE_TEST_CONSOLE;
|
||||
}
|
||||
|
||||
export default async function ({ params: { id } }) {
|
||||
export default async function ({ params: { websiteId } }) {
|
||||
const enabled = await getEnabled();
|
||||
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <TestConsole websiteId={id?.[0]} />;
|
||||
return <ConsolePage websiteId={websiteId?.[0]} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Test Console | umami',
|
||||
title: 'Test Console',
|
||||
};
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
'use client';
|
||||
import { Button, Icon, Icons, Loading, Text } from 'react-basics';
|
||||
import Link from 'next/link';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import Pager from 'components/common/Pager';
|
||||
import WebsiteChartList from '../../(main)/websites/[id]/WebsiteChartList';
|
||||
import DashboardSettingsButton from 'app/(main)/dashboard/DashboardSettingsButton';
|
||||
import DashboardEdit from 'app/(main)/dashboard/DashboardEdit';
|
||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useDashboard from 'store/dashboard';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import useFilterQuery from 'components/hooks/useFilterQuery';
|
||||
import { useUser } from 'components/hooks';
|
||||
|
||||
export function Dashboard() {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { user } = useUser();
|
||||
const { showCharts, editing } = useDashboard();
|
||||
const { dir } = useLocale();
|
||||
const { get } = useApi();
|
||||
const pageSize = 10;
|
||||
|
||||
const { query, params, setParams, result } = useFilterQuery({
|
||||
queryKey: ['dashboard:websites'],
|
||||
queryFn: (params: any) => {
|
||||
return get(`/users/${user.id}/websites`, { ...params, includeTeams: true, pageSize });
|
||||
},
|
||||
});
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setParams({ ...params, page });
|
||||
};
|
||||
|
||||
const { data, count } = result || {};
|
||||
const hasData = !!(data as any)?.length;
|
||||
const { page } = params;
|
||||
|
||||
if (query.isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={formatMessage(labels.dashboard)}>
|
||||
{!editing && hasData && <DashboardSettingsButton />}
|
||||
</PageHeader>
|
||||
{!hasData && (
|
||||
<EmptyPlaceholder message={formatMessage(messages.noWebsitesConfigured)}>
|
||||
<Link href="/settings/websites">
|
||||
<Button>
|
||||
<Icon rotate={dir === 'rtl' ? 180 : 0}>
|
||||
<Icons.ArrowRight />
|
||||
</Icon>
|
||||
<Text>{formatMessage(messages.goToSettings)}</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
</EmptyPlaceholder>
|
||||
)}
|
||||
{hasData && (
|
||||
<>
|
||||
{editing && <DashboardEdit />}
|
||||
{!editing && (
|
||||
<>
|
||||
<WebsiteChartList websites={data} showCharts={showCharts} limit={pageSize} />
|
||||
<Pager
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
count={count}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Dashboard;
|
||||
|
|
@ -1,32 +1,30 @@
|
|||
'use client';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
|
||||
import classNames from 'classnames';
|
||||
import { Button } from 'react-basics';
|
||||
import { Button, Loading } from 'react-basics';
|
||||
import { firstBy } from 'thenby';
|
||||
import useDashboard, { saveDashboard } from 'store/dashboard';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import { useMessages, useWebsites } from 'components/hooks';
|
||||
import styles from './DashboardEdit.module.css';
|
||||
|
||||
const dragId = 'dashboard-website-ordering';
|
||||
const DRAG_ID = 'dashboard-website-ordering';
|
||||
|
||||
export function DashboardEdit() {
|
||||
export function DashboardEdit({ teamId }: { teamId: string }) {
|
||||
const settings = useDashboard();
|
||||
const { websiteOrder } = settings;
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [order, setOrder] = useState(websiteOrder || []);
|
||||
const { get, useQuery } = useApi();
|
||||
const { data: result } = useQuery({
|
||||
queryKey: ['websites'],
|
||||
queryFn: () => get('/websites', { includeTeams: 1 }),
|
||||
});
|
||||
const { data: websites } = result || {};
|
||||
const {
|
||||
result,
|
||||
query: { isLoading },
|
||||
} = useWebsites({ teamId });
|
||||
|
||||
const websites = result?.data;
|
||||
|
||||
const ordered = useMemo(() => {
|
||||
if (websites) {
|
||||
return websites
|
||||
.map(website => ({ ...website, order: order.indexOf(website.id) }))
|
||||
.map((website: { id: any }) => ({ ...website, order: order.indexOf(website.id) }))
|
||||
.sort(firstBy('order'));
|
||||
}
|
||||
return [];
|
||||
|
|
@ -57,6 +55,10 @@ export function DashboardEdit() {
|
|||
setOrder([]);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.buttons}>
|
||||
|
|
@ -72,7 +74,7 @@ export function DashboardEdit() {
|
|||
</div>
|
||||
<div className={styles.dragActive}>
|
||||
<DragDropContext onDragEnd={handleWebsiteDrag}>
|
||||
<Droppable droppableId={dragId}>
|
||||
<Droppable droppableId={DRAG_ID}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
|
|
@ -80,7 +82,7 @@ export function DashboardEdit() {
|
|||
style={{ marginBottom: snapshot.isDraggingOver ? 260 : null }}
|
||||
>
|
||||
{ordered.map(({ id, name, domain }, index) => (
|
||||
<Draggable key={id} draggableId={`${dragId}-${id}`} index={index}>
|
||||
<Draggable key={id} draggableId={`${DRAG_ID}-${id}`} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
|
|
|
|||
71
src/app/(main)/dashboard/DashboardPage.tsx
Normal file
71
src/app/(main)/dashboard/DashboardPage.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
'use client';
|
||||
import { Icon, Icons, Loading, Text } from 'react-basics';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import Pager from 'components/common/Pager';
|
||||
import WebsiteChartList from '../websites/[websiteId]/WebsiteChartList';
|
||||
import DashboardSettingsButton from 'app/(main)/dashboard/DashboardSettingsButton';
|
||||
import DashboardEdit from 'app/(main)/dashboard/DashboardEdit';
|
||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||
import { useMessages, useLocale, useTeamUrl, useWebsites } from 'components/hooks';
|
||||
import useDashboard from 'store/dashboard';
|
||||
import LinkButton from 'components/common/LinkButton';
|
||||
|
||||
export function DashboardPage() {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { teamId, renderTeamUrl } = useTeamUrl();
|
||||
const { showCharts, editing } = useDashboard();
|
||||
const { dir } = useLocale();
|
||||
const pageSize = 10;
|
||||
|
||||
const { result, query, params, setParams } = useWebsites({ teamId }, { pageSize });
|
||||
const { page } = params;
|
||||
const hasData = !!result?.data?.length;
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setParams({ ...params, page });
|
||||
};
|
||||
|
||||
if (query.isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<section style={{ marginBottom: 60 }}>
|
||||
<PageHeader title={formatMessage(labels.dashboard)}>
|
||||
{!editing && hasData && <DashboardSettingsButton />}
|
||||
</PageHeader>
|
||||
{!hasData && (
|
||||
<EmptyPlaceholder message={formatMessage(messages.noWebsitesConfigured)}>
|
||||
<LinkButton href={renderTeamUrl('/settings')}>
|
||||
<Icon rotate={dir === 'rtl' ? 180 : 0}>
|
||||
<Icons.ArrowRight />
|
||||
</Icon>
|
||||
<Text>{formatMessage(messages.goToSettings)}</Text>
|
||||
</LinkButton>
|
||||
</EmptyPlaceholder>
|
||||
)}
|
||||
{hasData && (
|
||||
<>
|
||||
{editing && <DashboardEdit teamId={teamId} />}
|
||||
{!editing && (
|
||||
<>
|
||||
<WebsiteChartList
|
||||
websites={result?.data as any}
|
||||
showCharts={showCharts}
|
||||
limit={pageSize}
|
||||
/>
|
||||
<Pager
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
count={result?.count}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardPage;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { TooltipPopup, Icon, Text, Flexbox, Button } from 'react-basics';
|
||||
import Icons from 'components/icons';
|
||||
import { saveDashboard } from 'store/dashboard';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages } from 'components/hooks';
|
||||
|
||||
export function DashboardSettingsButton() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import Dashboard from 'app/(main)/dashboard/Dashboard';
|
||||
import DashboardPage from './DashboardPage';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function () {
|
||||
return <Dashboard />;
|
||||
return <DashboardPage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Dashboard | umami',
|
||||
title: 'Dashboard',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,5 +18,16 @@
|
|||
min-height: 0;
|
||||
height: calc(100vh - 60px);
|
||||
overflow-y: auto;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 1320px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Metadata } from 'next';
|
||||
import App from './App';
|
||||
import NavBar from './NavBar';
|
||||
import Page from 'components/layout/Page';
|
||||
|
|
@ -17,3 +18,10 @@ export default function ({ children }) {
|
|||
</App>
|
||||
);
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
template: '%s | Umami',
|
||||
default: 'Umami',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import DateFilter from 'components/input/DateFilter';
|
||||
import { Button, Flexbox } from 'react-basics';
|
||||
import useDateRange from 'components/hooks/useDateRange';
|
||||
import { useDateRange, useMessages } from 'components/hooks';
|
||||
import { DEFAULT_DATE_RANGE } from 'lib/constants';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { DateRange } from 'lib/types';
|
||||
|
||||
export function DateRangeSetting() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [dateRange, setDateRange] = useDateRange();
|
||||
const { value } = dateRange;
|
||||
|
||||
const handleChange = value => setDateRange(value);
|
||||
const handleChange = (value: string | DateRange) => setDateRange(value);
|
||||
const handleReset = () => setDateRange(DEFAULT_DATE_RANGE);
|
||||
|
||||
return (
|
||||
4
src/app/(main)/profile/LanguageSetting.module.css
Normal file
4
src/app/(main)/profile/LanguageSetting.module.css
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
div.menu {
|
||||
max-height: 300px;
|
||||
width: 300px;
|
||||
}
|
||||
|
|
@ -1,17 +1,26 @@
|
|||
import { useState } from 'react';
|
||||
import { Button, Dropdown, Item, Flexbox } from 'react-basics';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import { useLocale, useMessages } from 'components/hooks';
|
||||
import { DEFAULT_LOCALE } from 'lib/constants';
|
||||
import { languages } from 'lib/lang';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import styles from './LanguageSetting.module.css';
|
||||
|
||||
export function LanguageSetting() {
|
||||
const [search, setSearch] = useState('');
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { locale, saveLocale } = useLocale();
|
||||
const options = Object.keys(languages);
|
||||
const options = search
|
||||
? Object.keys(languages).filter(n => {
|
||||
return (
|
||||
n.toLowerCase().includes(search.toLowerCase()) ||
|
||||
languages[n].label.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
})
|
||||
: Object.keys(languages);
|
||||
|
||||
const handleReset = () => saveLocale(DEFAULT_LOCALE);
|
||||
|
||||
const renderValue = value => languages[value].label;
|
||||
const renderValue = (value: string | number) => languages[value].label;
|
||||
|
||||
return (
|
||||
<Flexbox gap={10}>
|
||||
|
|
@ -19,8 +28,10 @@ export function LanguageSetting() {
|
|||
items={options}
|
||||
value={locale}
|
||||
renderValue={renderValue}
|
||||
onChange={saveLocale}
|
||||
menuProps={{ style: { height: 300, width: 300 } }}
|
||||
onChange={val => saveLocale(val as string)}
|
||||
allowSearch={true}
|
||||
onSearch={setSearch}
|
||||
menuProps={{ className: styles.menu }}
|
||||
>
|
||||
{item => <Item key={item}>{languages[item].label}</Item>}
|
||||
</Dropdown>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { Button, Icon, Text, useToasts, ModalTrigger, Modal } from 'react-basics';
|
||||
import PasswordEditForm from 'app/(main)/settings/profile/PasswordEditForm';
|
||||
import PasswordEditForm from 'app/(main)/profile/PasswordEditForm';
|
||||
import Icons from 'components/icons';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages } from 'components/hooks';
|
||||
|
||||
export function PasswordChangeButton() {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import { useRef } from 'react';
|
||||
import { Form, FormRow, FormInput, FormButtons, PasswordField, Button } from 'react-basics';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useApi, useMessages } from 'components/hooks';
|
||||
|
||||
export function PasswordEditForm({ onSave, onClose }) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import { useMessages } from 'components/hooks';
|
||||
|
||||
9
src/app/(main)/profile/ProfilePage.module.css
Normal file
9
src/app/(main)/profile/ProfilePage.module.css
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
.container {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.container {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,13 @@
|
|||
'use client';
|
||||
import ProfileHeader from './ProfileHeader';
|
||||
import ProfileSettings from './ProfileSettings';
|
||||
import { Metadata } from 'next';
|
||||
import styles from './ProfilePage.module.css';
|
||||
|
||||
export default function () {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.container}>
|
||||
<ProfileHeader />
|
||||
<ProfileSettings />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Profile Settings | umami',
|
||||
};
|
||||
|
|
@ -1,18 +1,16 @@
|
|||
'use client';
|
||||
import { Form, FormRow } from 'react-basics';
|
||||
import TimezoneSetting from 'app/(main)/settings/profile/TimezoneSetting';
|
||||
import DateRangeSetting from 'app/(main)/settings/profile/DateRangeSetting';
|
||||
import LanguageSetting from 'app/(main)/settings/profile/LanguageSetting';
|
||||
import ThemeSetting from 'app/(main)/settings/profile/ThemeSetting';
|
||||
import TimezoneSetting from 'app/(main)/profile/TimezoneSetting';
|
||||
import DateRangeSetting from 'app/(main)/profile/DateRangeSetting';
|
||||
import LanguageSetting from 'app/(main)/profile/LanguageSetting';
|
||||
import ThemeSetting from 'app/(main)/profile/ThemeSetting';
|
||||
import PasswordChangeButton from './PasswordChangeButton';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useLogin, useMessages } from 'components/hooks';
|
||||
import { ROLES } from 'lib/constants';
|
||||
|
||||
export function ProfileSettings() {
|
||||
const { user } = useUser();
|
||||
const { user } = useLogin();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const cloudMode = Boolean(process.env.cloudMode);
|
||||
const cloudMode = !!process.env.cloudMode;
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
|
|
@ -20,12 +18,12 @@ export function ProfileSettings() {
|
|||
|
||||
const { username, role } = user;
|
||||
|
||||
const renderRole = value => {
|
||||
const renderRole = (value: string) => {
|
||||
if (value === ROLES.user) {
|
||||
return formatMessage(labels.user);
|
||||
}
|
||||
if (value === ROLES.admin) {
|
||||
return formatMessage(labels.admin);
|
||||
return formatMessage(labels.administrator);
|
||||
}
|
||||
if (value === ROLES.viewOnly) {
|
||||
return formatMessage(labels.viewOnly);
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import classNames from 'classnames';
|
||||
import { Button, Icon } from 'react-basics';
|
||||
import useTheme from 'components/hooks/useTheme';
|
||||
import { useTheme } from 'components/hooks';
|
||||
import Sun from 'assets/sun.svg';
|
||||
import Moon from 'assets/moon.svg';
|
||||
import styles from './ThemeSetting.module.css';
|
||||
4
src/app/(main)/profile/TimezoneSetting.module.css
Normal file
4
src/app/(main)/profile/TimezoneSetting.module.css
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
div.menu {
|
||||
max-height: 300px;
|
||||
width: 300px;
|
||||
}
|
||||
|
|
@ -1,13 +1,17 @@
|
|||
import { useState } from 'react';
|
||||
import { Dropdown, Item, Button, Flexbox } from 'react-basics';
|
||||
import { listTimeZones } from 'timezone-support';
|
||||
import useTimezone from 'components/hooks/useTimezone';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useTimezone, useMessages } from 'components/hooks';
|
||||
import { getTimezone } from 'lib/date';
|
||||
import styles from './TimezoneSetting.module.css';
|
||||
|
||||
export function TimezoneSetting() {
|
||||
const [search, setSearch] = useState('');
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [timezone, saveTimezone] = useTimezone();
|
||||
const options = listTimeZones();
|
||||
const options = search
|
||||
? listTimeZones().filter(n => n.toLowerCase().includes(search.toLowerCase()))
|
||||
: listTimeZones();
|
||||
|
||||
const handleReset = () => saveTimezone(getTimezone());
|
||||
|
||||
|
|
@ -17,8 +21,9 @@ export function TimezoneSetting() {
|
|||
items={options}
|
||||
value={timezone}
|
||||
onChange={saveTimezone}
|
||||
style={{ flex: 1 }}
|
||||
menuProps={{ style: { height: 300 } }}
|
||||
menuProps={{ className: styles.menu }}
|
||||
allowSearch={true}
|
||||
onSearch={setSearch}
|
||||
>
|
||||
{item => <Item key={item}>{item}</Item>}
|
||||
</Dropdown>
|
||||
10
src/app/(main)/profile/page.tsx
Normal file
10
src/app/(main)/profile/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Metadata } from 'next';
|
||||
import ProfilePage from './ProfilePage';
|
||||
|
||||
export default function () {
|
||||
return <ProfilePage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Profile',
|
||||
};
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
|
||||
import ConfirmDeleteForm from 'components/common/ConfirmDeleteForm';
|
||||
import { useApi, useMessages } from 'components/hooks';
|
||||
import { setValue } from 'store/cache';
|
||||
import { useApi, useMessages, useModified } from 'components/hooks';
|
||||
import ConfirmationForm from 'components/common/ConfirmationForm';
|
||||
|
||||
export function ReportDeleteButton({
|
||||
reportId,
|
||||
|
|
@ -12,14 +11,17 @@ export function ReportDeleteButton({
|
|||
reportName: string;
|
||||
onDelete?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { formatMessage, labels, messages, FormattedMessage } = useMessages();
|
||||
const { del, useMutation } = useApi();
|
||||
const { mutate } = useMutation({ mutationFn: reportId => del(`/reports/${reportId}`) });
|
||||
const { mutate, isPending, error } = useMutation({
|
||||
mutationFn: reportId => del(`/reports/${reportId}`),
|
||||
});
|
||||
const { touch } = useModified();
|
||||
|
||||
const handleConfirm = (close: () => void) => {
|
||||
mutate(reportId as any, {
|
||||
onSuccess: () => {
|
||||
setValue('reports', Date.now());
|
||||
touch('reports');
|
||||
onDelete?.();
|
||||
close();
|
||||
},
|
||||
|
|
@ -34,12 +36,20 @@ export function ReportDeleteButton({
|
|||
</Icon>
|
||||
<Text>{formatMessage(labels.delete)}</Text>
|
||||
</Button>
|
||||
<Modal>
|
||||
{close => (
|
||||
<ConfirmDeleteForm
|
||||
name={reportName}
|
||||
<Modal title={formatMessage(labels.deleteReport)}>
|
||||
{(close: () => void) => (
|
||||
<ConfirmationForm
|
||||
message={
|
||||
<FormattedMessage
|
||||
{...messages.confirmDelete}
|
||||
values={{ target: <b>{reportName}</b> }}
|
||||
/>
|
||||
}
|
||||
isLoading={isPending}
|
||||
error={error}
|
||||
onConfirm={handleConfirm.bind(null, close)}
|
||||
onClose={close}
|
||||
buttonLabel={formatMessage(labels.delete)}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
'use client';
|
||||
import { useReports } from 'components/hooks';
|
||||
import ReportsTable from './ReportsTable';
|
||||
import DataTable from 'components/common/DataTable';
|
||||
|
||||
export default function ReportsDataTable({ websiteId }: { websiteId?: string }) {
|
||||
const queryResult = useReports(websiteId);
|
||||
export default function ReportsDataTable({
|
||||
websiteId,
|
||||
teamId,
|
||||
}: {
|
||||
websiteId?: string;
|
||||
teamId?: string;
|
||||
}) {
|
||||
const queryResult = useReports({ websiteId, teamId });
|
||||
|
||||
return (
|
||||
<DataTable queryResult={queryResult}>
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
'use client';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import { Button, Icon, Icons, Text } from 'react-basics';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Icon, Icons, Text } from 'react-basics';
|
||||
import { useLogin, useMessages, useTeamUrl } from 'components/hooks';
|
||||
import LinkButton from 'components/common/LinkButton';
|
||||
import { ROLES } from 'lib/constants';
|
||||
|
||||
export function ReportsHeader() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const router = useRouter();
|
||||
|
||||
const handleClick = () => router.push('/reports/create');
|
||||
const { renderTeamUrl } = useTeamUrl();
|
||||
const { user } = useLogin();
|
||||
const canEdit = user.role !== ROLES.viewOnly;
|
||||
|
||||
return (
|
||||
<PageHeader title={formatMessage(labels.reports)}>
|
||||
<Button variant="primary" onClick={handleClick}>
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.createReport)}</Text>
|
||||
</Button>
|
||||
{canEdit && (
|
||||
<LinkButton href={renderTeamUrl('/reports/create')} variant="primary">
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.createReport)}</Text>
|
||||
</LinkButton>
|
||||
)}
|
||||
</PageHeader>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
15
src/app/(main)/reports/ReportsPage.tsx
Normal file
15
src/app/(main)/reports/ReportsPage.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
'use client';
|
||||
import ReportsHeader from './ReportsHeader';
|
||||
import ReportsDataTable from './ReportsDataTable';
|
||||
|
||||
export default function ReportsPage({ teamId }: { teamId: string }) {
|
||||
return (
|
||||
<>
|
||||
<ReportsHeader />
|
||||
<ReportsDataTable teamId={teamId} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
export const metadata = {
|
||||
title: 'Reports',
|
||||
};
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
import { GridColumn, GridTable, Icon, Icons, Text, useBreakpoint } from 'react-basics';
|
||||
import LinkButton from 'components/common/LinkButton';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
import { useMessages, useLogin, useTeamUrl } from 'components/hooks';
|
||||
import { REPORT_TYPES } from 'lib/constants';
|
||||
import ReportDeleteButton from './ReportDeleteButton';
|
||||
|
||||
export function ReportsTable({ data = [], showDomain }: { data: any[]; showDomain?: boolean }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useUser();
|
||||
const { user } = useLogin();
|
||||
const breakpoint = useBreakpoint();
|
||||
const { renderTeamUrl } = useTeamUrl();
|
||||
|
||||
return (
|
||||
<GridTable data={data} cardMode={['xs', 'sm', 'md'].includes(breakpoint)}>
|
||||
|
|
@ -34,7 +34,7 @@ export function ReportsTable({ data = [], showDomain }: { data: any[]; showDomai
|
|||
{(user.id === userId || user.id === website?.userId) && (
|
||||
<ReportDeleteButton reportId={id} reportName={name} />
|
||||
)}
|
||||
<LinkButton href={`/reports/${id}`}>
|
||||
<LinkButton href={renderTeamUrl(`/reports/${id}`)}>
|
||||
<Icon>
|
||||
<Icons.ArrowRight />
|
||||
</Icon>
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
.menu {
|
||||
width: 300px;
|
||||
padding-right: 20px;
|
||||
border-right: 1px solid var(--base300);
|
||||
grid-row: 2 / 3;
|
||||
grid-column: 1 / 2;
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import ReportDetails from './ReportDetails';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function ReportDetailsPage({ params: { id } }) {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <ReportDetails reportId={id} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Reports | umami',
|
||||
};
|
||||
|
|
@ -3,7 +3,7 @@ import { FormRow } from 'react-basics';
|
|||
import { parseDateRange } from 'lib/date';
|
||||
import DateFilter from 'components/input/DateFilter';
|
||||
import WebsiteSelect from 'components/input/WebsiteSelect';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { useMessages, useTeamUrl, useWebsite } from 'components/hooks';
|
||||
import { ReportContext } from './Report';
|
||||
|
||||
export interface BaseParametersProps {
|
||||
|
|
@ -21,10 +21,12 @@ export function BaseParameters({
|
|||
}: BaseParametersProps) {
|
||||
const { report, updateReport } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const { teamId } = useTeamUrl();
|
||||
const { parameters } = report || {};
|
||||
const { websiteId, dateRange } = parameters || {};
|
||||
const { value, startDate, endDate } = dateRange || {};
|
||||
const { data: website } = useWebsite(websiteId);
|
||||
const { name } = website || {};
|
||||
|
||||
const handleWebsiteSelect = (websiteId: string) => {
|
||||
updateReport({ websiteId, parameters: { websiteId } });
|
||||
|
|
@ -38,8 +40,10 @@ export function BaseParameters({
|
|||
<>
|
||||
{showWebsiteSelect && (
|
||||
<FormRow label={formatMessage(labels.website)}>
|
||||
{allowWebsiteSelect && (
|
||||
<WebsiteSelect websiteId={websiteId} onSelect={handleWebsiteSelect} />
|
||||
{allowWebsiteSelect ? (
|
||||
<WebsiteSelect teamId={teamId} websiteId={websiteId} onSelect={handleWebsiteSelect} />
|
||||
) : (
|
||||
name
|
||||
)}
|
||||
</FormRow>
|
||||
)}
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
.selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.popup {
|
||||
display: flex;
|
||||
max-width: 300px;
|
||||
max-height: 400px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.popup > div {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.filter {
|
||||
|
|
@ -17,6 +20,5 @@
|
|||
}
|
||||
|
||||
.menu {
|
||||
min-width: 360px;
|
||||
max-height: 300px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ export default function FieldFilterForm({
|
|||
const { formatValue } = useFormat();
|
||||
const { locale } = useLocale();
|
||||
const filters = getFilters(type);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const formattedValues = useMemo(() => {
|
||||
const formatted = {};
|
||||
|
|
@ -41,7 +42,11 @@ export default function FieldFilterForm({
|
|||
format(values[0]);
|
||||
}
|
||||
return formatted;
|
||||
}, [values]);
|
||||
}, [formatValue, locale, name, values]);
|
||||
|
||||
const filteredValues = useMemo(() => {
|
||||
return search ? values.filter(n => n.includes(search)) : values;
|
||||
}, [search, formattedValues]);
|
||||
|
||||
const renderFilterValue = value => {
|
||||
return filters.find(f => f.value === value)?.label;
|
||||
|
|
@ -74,14 +79,14 @@ export default function FieldFilterForm({
|
|||
)}
|
||||
<Dropdown
|
||||
className={styles.dropdown}
|
||||
popupProps={{ className: styles.popup }}
|
||||
menuProps={{ className: styles.menu }}
|
||||
items={values}
|
||||
items={filteredValues}
|
||||
value={value}
|
||||
renderValue={renderValue}
|
||||
onChange={(key: any) => setValue(key)}
|
||||
style={{
|
||||
minWidth: '250px',
|
||||
}}
|
||||
allowSearch={true}
|
||||
onSearch={setSearch}
|
||||
>
|
||||
{(value: string) => {
|
||||
return <Item key={value}>{formattedValues[value]}</Item>;
|
||||
|
|
@ -1,20 +1,22 @@
|
|||
'use client';
|
||||
import { createContext, ReactNode } from 'react';
|
||||
import { Loading } from 'react-basics';
|
||||
import classNames from 'classnames';
|
||||
import { useReport } from 'components/hooks';
|
||||
import styles from './Report.module.css';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export const ReportContext = createContext(null);
|
||||
|
||||
export interface ReportProps {
|
||||
export function Report({
|
||||
reportId,
|
||||
defaultParameters,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
reportId: string;
|
||||
defaultParameters: { [key: string]: any };
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Report({ reportId, defaultParameters, children, className }: ReportProps) {
|
||||
}) {
|
||||
const report = useReport(reportId, defaultParameters);
|
||||
|
||||
if (!report) {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
.body {
|
||||
padding-left: 20px;
|
||||
padding-inline-start: 20px;
|
||||
grid-row: 2/3;
|
||||
grid-column: 2 / 3;
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
import FunnelReport from '../funnel/FunnelReport';
|
||||
import EventDataReport from '../event-data/EventDataReport';
|
||||
import InsightsReport from '../insights/InsightsReport';
|
||||
|
|
@ -1,17 +1,19 @@
|
|||
import { useContext } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Icon, LoadingButton, InlineEditField, useToasts } from 'react-basics';
|
||||
import { useMessages, useApi } from 'components/hooks';
|
||||
import { useMessages, useApi, useNavigation, useTeamUrl } from 'components/hooks';
|
||||
import { ReportContext } from './Report';
|
||||
import styles from './ReportHeader.module.css';
|
||||
import { REPORT_TYPES } from 'lib/constants';
|
||||
import Breadcrumb from 'components/common/Breadcrumb';
|
||||
|
||||
export function ReportHeader({ icon }) {
|
||||
const { report, updateReport } = useContext(ReportContext);
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { showToast } = useToasts();
|
||||
const { router } = useNavigation();
|
||||
const { renderTeamUrl } = useTeamUrl();
|
||||
|
||||
const { post, useMutation } = useApi();
|
||||
const router = useRouter();
|
||||
const { mutate: create, isPending: isCreating } = useMutation({
|
||||
mutationFn: (data: any) => post(`/reports`, data),
|
||||
});
|
||||
|
|
@ -28,7 +30,7 @@ export function ReportHeader({ icon }) {
|
|||
create(report, {
|
||||
onSuccess: async ({ id }) => {
|
||||
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||
router.push(`/reports/${id}`);
|
||||
router.push(renderTeamUrl(`/reports/${id}`));
|
||||
},
|
||||
});
|
||||
} else {
|
||||
|
|
@ -56,9 +58,16 @@ export function ReportHeader({ icon }) {
|
|||
<div className={styles.header}>
|
||||
<div>
|
||||
<div className={styles.type}>
|
||||
{formatMessage(
|
||||
labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === report?.type)],
|
||||
)}
|
||||
<Breadcrumb
|
||||
data={[
|
||||
{ label: formatMessage(labels.reports), url: '/reports' },
|
||||
{
|
||||
label: formatMessage(
|
||||
labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === report?.type)],
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.title}>
|
||||
<Icon size="lg">{icon}</Icon>
|
||||
7
src/app/(main)/reports/[reportId]/ReportMenu.module.css
Normal file
7
src/app/(main)/reports/[reportId]/ReportMenu.module.css
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
.menu {
|
||||
width: 300px;
|
||||
padding-inline-end: 20px;
|
||||
border-inline-end: 1px solid var(--base300);
|
||||
grid-row: 2 / 3;
|
||||
grid-column: 1 / 2;
|
||||
}
|
||||
6
src/app/(main)/reports/[reportId]/ReportPage.tsx
Normal file
6
src/app/(main)/reports/[reportId]/ReportPage.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
'use client';
|
||||
import ReportDetails from './ReportDetails';
|
||||
|
||||
export default function ReportPage({ reportId }) {
|
||||
return <ReportDetails reportId={reportId} />;
|
||||
}
|
||||
10
src/app/(main)/reports/[reportId]/page.tsx
Normal file
10
src/app/(main)/reports/[reportId]/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Metadata } from 'next';
|
||||
import ReportPage from './ReportPage';
|
||||
|
||||
export default function ({ params: { reportId } }) {
|
||||
return <ReportPage reportId={reportId} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Reports',
|
||||
};
|
||||
6
src/app/(main)/reports/create/ReportCreatePage.tsx
Normal file
6
src/app/(main)/reports/create/ReportCreatePage.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
'use client';
|
||||
import ReportTemplates from './ReportTemplates';
|
||||
|
||||
export default function ReportCreatePage() {
|
||||
return <ReportTemplates />;
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
'use client';
|
||||
import Link from 'next/link';
|
||||
import { Button, Icons, Text, Icon } from 'react-basics';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
|
|
@ -6,7 +5,46 @@ import Funnel from 'assets/funnel.svg';
|
|||
import Lightbulb from 'assets/lightbulb.svg';
|
||||
import Magnet from 'assets/magnet.svg';
|
||||
import styles from './ReportTemplates.module.css';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { useMessages, useTeamUrl } from 'components/hooks';
|
||||
|
||||
export function ReportTemplates({ showHeader = true }: { showHeader?: boolean }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { renderTeamUrl } = useTeamUrl();
|
||||
|
||||
const reports = [
|
||||
{
|
||||
title: formatMessage(labels.insights),
|
||||
description: formatMessage(labels.insightsDescription),
|
||||
url: renderTeamUrl('/reports/insights'),
|
||||
icon: <Lightbulb />,
|
||||
},
|
||||
{
|
||||
title: formatMessage(labels.funnel),
|
||||
description: formatMessage(labels.funnelDescription),
|
||||
url: renderTeamUrl('/reports/funnel'),
|
||||
icon: <Funnel />,
|
||||
},
|
||||
{
|
||||
title: formatMessage(labels.retention),
|
||||
description: formatMessage(labels.retentionDescription),
|
||||
url: renderTeamUrl('/reports/retention'),
|
||||
icon: <Magnet />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{showHeader && <PageHeader title={formatMessage(labels.reports)} />}
|
||||
<div className={styles.reports}>
|
||||
{reports.map(({ title, description, url, icon }) => {
|
||||
return (
|
||||
<ReportItem key={title} icon={icon} title={title} description={description} url={url} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ReportItem({ title, description, url, icon }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
|
@ -32,42 +70,4 @@ function ReportItem({ title, description, url, icon }) {
|
|||
);
|
||||
}
|
||||
|
||||
export function ReportTemplates({ showHeader = true }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const reports = [
|
||||
{
|
||||
title: formatMessage(labels.insights),
|
||||
description: formatMessage(labels.insightsDescription),
|
||||
url: '/reports/insights',
|
||||
icon: <Lightbulb />,
|
||||
},
|
||||
{
|
||||
title: formatMessage(labels.funnel),
|
||||
description: formatMessage(labels.funnelDescription),
|
||||
url: '/reports/funnel',
|
||||
icon: <Funnel />,
|
||||
},
|
||||
{
|
||||
title: formatMessage(labels.retention),
|
||||
description: formatMessage(labels.retentionDescription),
|
||||
url: '/reports/retention',
|
||||
icon: <Magnet />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{showHeader && <PageHeader title={formatMessage(labels.reports)} />}
|
||||
<div className={styles.reports}>
|
||||
{reports.map(({ title, description, url, icon }) => {
|
||||
return (
|
||||
<ReportItem key={title} icon={icon} title={title} description={description} url={url} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReportTemplates;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import ReportTemplates from './ReportTemplates';
|
||||
import ReportCreatePage from './ReportCreatePage';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function ReportsCreatePage() {
|
||||
return <ReportTemplates />;
|
||||
export default function () {
|
||||
return <ReportCreatePage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Create Report | umami',
|
||||
title: 'Create Report',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import Empty from 'components/common/Empty';
|
|||
import Icons from 'components/icons';
|
||||
import { useApi, useMessages } from 'components/hooks';
|
||||
import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import FieldAddForm from '../[id]/FieldAddForm';
|
||||
import ParameterList from '../[id]/ParameterList';
|
||||
import BaseParameters from '../[id]/BaseParameters';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import FieldAddForm from '../[reportId]/FieldAddForm';
|
||||
import ParameterList from '../[reportId]/ParameterList';
|
||||
import BaseParameters from '../[reportId]/BaseParameters';
|
||||
import styles from './EventDataParameters.module.css';
|
||||
|
||||
function useFields(websiteId, startDate, endDate) {
|
||||
|
|
@ -29,7 +29,7 @@ function useFields(websiteId, startDate, endDate) {
|
|||
export function EventDataParameters() {
|
||||
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { parameters } = report || {};
|
||||
const { id, parameters } = report || {};
|
||||
const { websiteId, dateRange, fields, filters, groups } = parameters || {};
|
||||
const { startDate, endDate } = dateRange || {};
|
||||
const queryEnabled = websiteId && dateRange && fields?.length;
|
||||
|
|
@ -93,7 +93,7 @@ export function EventDataParameters() {
|
|||
|
||||
return (
|
||||
<Form values={parameters} error={error} onSubmit={handleSubmit}>
|
||||
<BaseParameters />
|
||||
<BaseParameters allowWebsiteSelect={!id} />
|
||||
{!hasData && <Empty message={formatMessage(messages.noEventData)} />}
|
||||
{parametersSelected &&
|
||||
hasData &&
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import Report from '../[id]/Report';
|
||||
import ReportHeader from '../[id]/ReportHeader';
|
||||
import ReportMenu from '../[id]/ReportMenu';
|
||||
import ReportBody from '../[id]/ReportBody';
|
||||
import Report from '../[reportId]/Report';
|
||||
import ReportHeader from '../[reportId]/ReportHeader';
|
||||
import ReportMenu from '../[reportId]/ReportMenu';
|
||||
import ReportBody from '../[reportId]/ReportBody';
|
||||
import EventDataParameters from './EventDataParameters';
|
||||
import EventDataTable from './EventDataTable';
|
||||
import Nodes from 'assets/nodes.svg';
|
||||
|
|
@ -11,7 +11,7 @@ const defaultParameters = {
|
|||
parameters: { fields: [], filters: [] },
|
||||
};
|
||||
|
||||
export default function EventDataReport({ reportId }: { reportId: string }) {
|
||||
export default function EventDataReport({ reportId }: { reportId?: string }) {
|
||||
return (
|
||||
<Report reportId={reportId} defaultParameters={defaultParameters}>
|
||||
<ReportHeader icon={<Nodes />} />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
'use client';
|
||||
import EventDataReport from './EventDataReport';
|
||||
|
||||
export default function EventDataReportPage() {
|
||||
return <EventDataReport />;
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { useContext } from 'react';
|
||||
import { GridTable, GridColumn } from 'react-basics';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
|
||||
export function EventDataTable() {
|
||||
const { report } = useContext(ReportContext);
|
||||
|
|
|
|||
10
src/app/(main)/reports/event-data/page.tsx
Normal file
10
src/app/(main)/reports/event-data/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Metadata } from 'next';
|
||||
import EventDataReportPage from './EventDataReportPage';
|
||||
|
||||
export default function () {
|
||||
return <EventDataReportPage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Event Data Report',
|
||||
};
|
||||
|
|
@ -1,3 +1,84 @@
|
|||
.loading {
|
||||
height: 300px;
|
||||
.chart {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.num {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 100%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--base800);
|
||||
background: var(--base100);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
column-gap: 30px;
|
||||
position: relative;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.step::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 25px;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background-color: var(--base100);
|
||||
}
|
||||
|
||||
.card {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
background: var(--base900);
|
||||
height: 50px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--base700);
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--base50);
|
||||
margin-inline-end: 20px;
|
||||
}
|
||||
|
||||
.track {
|
||||
background-color: var(--base100);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--base300);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,84 +1,57 @@
|
|||
import { JSX, useCallback, useContext, useMemo } from 'react';
|
||||
import { Loading, StatusLight } from 'react-basics';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useTheme from 'components/hooks/useTheme';
|
||||
import BarChart from 'components/metrics/BarChart';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import { useContext } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import styles from './FunnelChart.module.css';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
|
||||
export interface FunnelChartProps {
|
||||
className?: string;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function FunnelChart({ className, isLoading }: FunnelChartProps) {
|
||||
export function FunnelChart({ className }: FunnelChartProps) {
|
||||
const { report } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { colors } = useTheme();
|
||||
|
||||
const { parameters, data } = report || {};
|
||||
|
||||
const renderXLabel = useCallback(
|
||||
(label: string, index: number) => {
|
||||
return parameters.urls[index];
|
||||
},
|
||||
[parameters],
|
||||
);
|
||||
|
||||
const renderTooltipPopup = useCallback(
|
||||
(
|
||||
setTooltipPopup: (arg0: JSX.Element) => void,
|
||||
model: { tooltip: { opacity: any; labelColors: any; dataPoints: any } },
|
||||
) => {
|
||||
const { opacity, labelColors, dataPoints } = model.tooltip;
|
||||
|
||||
if (!dataPoints?.length || !opacity) {
|
||||
setTooltipPopup(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setTooltipPopup(
|
||||
<>
|
||||
<div>
|
||||
{formatLongNumber(dataPoints[0].raw.y)} {formatMessage(labels.visitors)}
|
||||
</div>
|
||||
<div>
|
||||
<StatusLight color={labelColors?.[0]?.backgroundColor}>
|
||||
{formatLongNumber(dataPoints[0].raw.z)}% {formatMessage(labels.dropoff)}
|
||||
</StatusLight>
|
||||
</div>
|
||||
</>,
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const datasets = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
label: formatMessage(labels.uniqueVisitors),
|
||||
data: data,
|
||||
borderWidth: 1,
|
||||
...colors.chart.visitors,
|
||||
},
|
||||
];
|
||||
}, [data, colors, formatMessage, labels]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading icon="dots" className={styles.loading} />;
|
||||
}
|
||||
const { data } = report || {};
|
||||
|
||||
return (
|
||||
<BarChart
|
||||
className={className}
|
||||
datasets={datasets}
|
||||
unit="day"
|
||||
isLoading={isLoading}
|
||||
renderXLabel={renderXLabel}
|
||||
renderTooltipPopup={renderTooltipPopup}
|
||||
XAxisType="category"
|
||||
/>
|
||||
<div className={classNames(styles.chart, className)}>
|
||||
{data?.map(({ url, visitors, dropped, dropoff, remaining }, index: number) => {
|
||||
return (
|
||||
<div key={url} className={styles.step}>
|
||||
<div className={styles.num}>{index + 1}</div>
|
||||
<div className={styles.card}>
|
||||
<div className={styles.header}>
|
||||
<span className={styles.label}>{formatMessage(labels.viewedPage)}:</span>
|
||||
<span className={styles.item}>{url}</span>
|
||||
</div>
|
||||
<div className={styles.track}>
|
||||
<div className={styles.bar} style={{ width: `${remaining * 100}%` }}>
|
||||
<span className={styles.value}>
|
||||
{remaining > 0.1 && `${(remaining * 100).toFixed(2)}%`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<div>
|
||||
<b>{formatLongNumber(visitors)}</b>
|
||||
<span> {formatMessage(labels.visitors)}</span>
|
||||
<span> ({(remaining * 100).toFixed(2)}%)</span>
|
||||
</div>
|
||||
{dropoff > 0 && (
|
||||
<div>
|
||||
<b>{formatLongNumber(dropped)}</b> {formatMessage(labels.visitorsDroppedOff)} (
|
||||
{(dropoff * 100).toFixed(2)}%)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,16 +13,16 @@ import {
|
|||
} from 'react-basics';
|
||||
import Icons from 'components/icons';
|
||||
import UrlAddForm from './UrlAddForm';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import BaseParameters from '../[id]/BaseParameters';
|
||||
import ParameterList from '../[id]/ParameterList';
|
||||
import PopupForm from '../[id]/PopupForm';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import BaseParameters from '../[reportId]/BaseParameters';
|
||||
import ParameterList from '../[reportId]/ParameterList';
|
||||
import PopupForm from '../[reportId]/PopupForm';
|
||||
|
||||
export function FunnelParameters() {
|
||||
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const { parameters } = report || {};
|
||||
const { id, parameters } = report || {};
|
||||
const { websiteId, dateRange, urls } = parameters || {};
|
||||
const queryDisabled = !websiteId || !dateRange || urls?.length < 2;
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ export function FunnelParameters() {
|
|||
|
||||
return (
|
||||
<Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
|
||||
<BaseParameters />
|
||||
<BaseParameters allowWebsiteSelect={!id} />
|
||||
<FormRow label={formatMessage(labels.window)}>
|
||||
<FormInput
|
||||
name="window"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
'use client';
|
||||
import FunnelChart from './FunnelChart';
|
||||
import FunnelTable from './FunnelTable';
|
||||
import FunnelParameters from './FunnelParameters';
|
||||
import Report from '../[id]/Report';
|
||||
import ReportHeader from '../[id]/ReportHeader';
|
||||
import ReportMenu from '../[id]/ReportMenu';
|
||||
import ReportBody from '../[id]/ReportBody';
|
||||
import Report from '../[reportId]/Report';
|
||||
import ReportHeader from '../[reportId]/ReportHeader';
|
||||
import ReportMenu from '../[reportId]/ReportMenu';
|
||||
import ReportBody from '../[reportId]/ReportBody';
|
||||
import Funnel from 'assets/funnel.svg';
|
||||
import { REPORT_TYPES } from 'lib/constants';
|
||||
|
||||
|
|
@ -14,7 +12,7 @@ const defaultParameters = {
|
|||
parameters: { window: 60, urls: [] },
|
||||
};
|
||||
|
||||
export default function FunnelReport({ reportId }) {
|
||||
export default function FunnelReport({ reportId }: { reportId?: string }) {
|
||||
return (
|
||||
<Report reportId={reportId} defaultParameters={defaultParameters}>
|
||||
<ReportHeader icon={<Funnel />} />
|
||||
|
|
@ -23,7 +21,6 @@ export default function FunnelReport({ reportId }) {
|
|||
</ReportMenu>
|
||||
<ReportBody>
|
||||
<FunnelChart />
|
||||
<FunnelTable />
|
||||
</ReportBody>
|
||||
</Report>
|
||||
);
|
||||
|
|
|
|||
6
src/app/(main)/reports/funnel/FunnelReportPage.tsx
Normal file
6
src/app/(main)/reports/funnel/FunnelReportPage.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
'use client';
|
||||
import FunnelReport from './FunnelReport';
|
||||
|
||||
export default function FunnelReportPage() {
|
||||
return <FunnelReport />;
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { useContext } from 'react';
|
||||
import ListTable from 'components/metrics/ListTable';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
|
||||
export function FunnelTable() {
|
||||
const { report } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
return (
|
||||
<ListTable
|
||||
data={report?.data}
|
||||
title={formatMessage(labels.url)}
|
||||
metric={formatMessage(labels.visitors)}
|
||||
showPercentage={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default FunnelTable;
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import FunnelReport from './FunnelReport';
|
||||
import FunnelReportPage from './FunnelReportPage';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function FunnelReportPage() {
|
||||
return <FunnelReport reportId={null} />;
|
||||
export default function () {
|
||||
return <FunnelReportPage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Funnel Report | umami',
|
||||
title: 'Funnel Report',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@
|
|||
|
||||
.popup {
|
||||
margin-top: -10px;
|
||||
margin-left: 30px;
|
||||
margin-inline-start: 30px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
import { useFilters, useFormat, useMessages } from 'components/hooks';
|
||||
import Icons from 'components/icons';
|
||||
import { useContext } from 'react';
|
||||
import { useFormat, useMessages, useFilters } from 'components/hooks';
|
||||
import {
|
||||
Form,
|
||||
FormRow,
|
||||
FormButtons,
|
||||
SubmitButton,
|
||||
PopupTrigger,
|
||||
FormRow,
|
||||
Icon,
|
||||
Popup,
|
||||
PopupTrigger,
|
||||
SubmitButton,
|
||||
TooltipPopup,
|
||||
} from 'react-basics';
|
||||
import Icons from 'components/icons';
|
||||
import BaseParameters from '../[id]/BaseParameters';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import ParameterList from '../[id]/ParameterList';
|
||||
import FilterSelectForm from '../[id]/FilterSelectForm';
|
||||
import FieldSelectForm from '../[id]/FieldSelectForm';
|
||||
import PopupForm from '../[id]/PopupForm';
|
||||
import BaseParameters from '../[reportId]/BaseParameters';
|
||||
import FieldSelectForm from '../[reportId]/FieldSelectForm';
|
||||
import FilterSelectForm from '../[reportId]/FilterSelectForm';
|
||||
import ParameterList from '../[reportId]/ParameterList';
|
||||
import PopupForm from '../[reportId]/PopupForm';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import styles from './InsightsParameters.module.css';
|
||||
|
||||
export function InsightsParameters() {
|
||||
|
|
@ -24,7 +24,7 @@ export function InsightsParameters() {
|
|||
const { formatMessage, labels } = useMessages();
|
||||
const { formatValue } = useFormat();
|
||||
const { filterLabels } = useFilters();
|
||||
const { parameters } = report || {};
|
||||
const { id, parameters } = report || {};
|
||||
const { websiteId, dateRange, fields, filters } = parameters || {};
|
||||
const { startDate, endDate } = dateRange || {};
|
||||
const parametersSelected = websiteId && startDate && endDate;
|
||||
|
|
@ -103,7 +103,7 @@ export function InsightsParameters() {
|
|||
|
||||
return (
|
||||
<Form values={parameters} onSubmit={handleSubmit}>
|
||||
<BaseParameters />
|
||||
<BaseParameters allowWebsiteSelect={!id} />
|
||||
{parametersSelected &&
|
||||
parameterGroups.map(({ id, label }) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
'use client';
|
||||
import Report from '../[id]/Report';
|
||||
import ReportHeader from '../[id]/ReportHeader';
|
||||
import ReportMenu from '../[id]/ReportMenu';
|
||||
import ReportBody from '../[id]/ReportBody';
|
||||
import Report from '../[reportId]/Report';
|
||||
import ReportHeader from '../[reportId]/ReportHeader';
|
||||
import ReportMenu from '../[reportId]/ReportMenu';
|
||||
import ReportBody from '../[reportId]/ReportBody';
|
||||
import InsightsParameters from './InsightsParameters';
|
||||
import InsightsTable from './InsightsTable';
|
||||
import Lightbulb from 'assets/lightbulb.svg';
|
||||
|
|
@ -13,7 +12,7 @@ const defaultParameters = {
|
|||
parameters: { fields: [], filters: [] },
|
||||
};
|
||||
|
||||
export default function InsightsReport({ reportId }: { reportId: string }) {
|
||||
export default function InsightsReport({ reportId }: { reportId?: string }) {
|
||||
return (
|
||||
<Report reportId={reportId} defaultParameters={defaultParameters}>
|
||||
<ReportHeader icon={<Lightbulb />} />
|
||||
|
|
|
|||
6
src/app/(main)/reports/insights/InsightsReportPage.tsx
Normal file
6
src/app/(main)/reports/insights/InsightsReportPage.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
'use client';
|
||||
import InsightsReport from './InsightsReport';
|
||||
|
||||
export default function InsightsReportPage() {
|
||||
return <InsightsReport />;
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { useContext, useEffect, useState } from 'react';
|
||||
import { GridTable, GridColumn } from 'react-basics';
|
||||
import { useFormat, useMessages } from 'components/hooks';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||
|
||||
export function InsightsTable() {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import InsightsReport from './InsightsReport';
|
||||
import InsightsReportPage from './InsightsReportPage';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function InsightsReportPage() {
|
||||
return <InsightsReport reportId={null} />;
|
||||
export default function () {
|
||||
return <InsightsReportPage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Insights Report | umami',
|
||||
title: 'Insights Report',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,14 +1,10 @@
|
|||
import ReportsHeader from './ReportsHeader';
|
||||
import ReportsDataTable from './ReportsDataTable';
|
||||
import ReportsPage from './ReportsPage';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function () {
|
||||
return (
|
||||
<>
|
||||
<ReportsHeader />
|
||||
<ReportsDataTable />
|
||||
</>
|
||||
);
|
||||
export default function ({ params: { teamId } }: { params: { teamId: string } }) {
|
||||
return <ReportsPage teamId={teamId} />;
|
||||
}
|
||||
export const metadata = {
|
||||
title: 'Reports | umami',
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Reports',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import { useContext } from 'react';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { Form, FormButtons, FormRow, SubmitButton } from 'react-basics';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import { MonthSelect } from 'components/input/MonthSelect';
|
||||
import BaseParameters from '../[id]/BaseParameters';
|
||||
import BaseParameters from '../[reportId]/BaseParameters';
|
||||
import { parseDateRange } from 'lib/date';
|
||||
|
||||
export function RetentionParameters() {
|
||||
const { report, runReport, isRunning, updateReport } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const { parameters } = report || {};
|
||||
const { id, parameters } = report || {};
|
||||
const { websiteId, dateRange } = parameters || {};
|
||||
const { startDate } = dateRange || {};
|
||||
const queryDisabled = !websiteId || !dateRange;
|
||||
|
|
@ -30,7 +30,7 @@ export function RetentionParameters() {
|
|||
|
||||
return (
|
||||
<Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
|
||||
<BaseParameters showDateSelect={false} />
|
||||
<BaseParameters showDateSelect={false} allowWebsiteSelect={!id} />
|
||||
<FormRow label={formatMessage(labels.date)}>
|
||||
<MonthSelect date={startDate} onChange={handleDateChange} />
|
||||
</FormRow>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
'use client';
|
||||
import RetentionTable from './RetentionTable';
|
||||
import RetentionParameters from './RetentionParameters';
|
||||
import Report from '../[id]/Report';
|
||||
import ReportHeader from '../[id]/ReportHeader';
|
||||
import ReportMenu from '../[id]/ReportMenu';
|
||||
import ReportBody from '../[id]/ReportBody';
|
||||
import Report from '../[reportId]/Report';
|
||||
import ReportHeader from '../[reportId]/ReportHeader';
|
||||
import ReportMenu from '../[reportId]/ReportMenu';
|
||||
import ReportBody from '../[reportId]/ReportBody';
|
||||
import Magnet from 'assets/magnet.svg';
|
||||
import { REPORT_TYPES } from 'lib/constants';
|
||||
import { parseDateRange } from 'lib/date';
|
||||
|
|
@ -19,7 +18,7 @@ const defaultParameters = {
|
|||
},
|
||||
};
|
||||
|
||||
export default function RetentionReport({ reportId }: { reportId: string }) {
|
||||
export default function RetentionReport({ reportId }: { reportId?: string }) {
|
||||
return (
|
||||
<Report reportId={reportId} defaultParameters={defaultParameters}>
|
||||
<ReportHeader icon={<Magnet />} />
|
||||
|
|
|
|||
6
src/app/(main)/reports/retention/RetentionReportPage.tsx
Normal file
6
src/app/(main)/reports/retention/RetentionReportPage.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
'use client';
|
||||
import RetentionReport from './RetentionReport';
|
||||
|
||||
export default function RetentionReportPage() {
|
||||
return <RetentionReport />;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { useContext } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||
import { useMessages, useLocale } from 'components/hooks';
|
||||
import { formatDate } from 'lib/date';
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
import RetentionReport from './RetentionReport';
|
||||
|
||||
export default function RetentionReportPage() {
|
||||
return <RetentionReport reportId={null} />;
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: 'Create Report | umami',
|
||||
};
|
||||
10
src/app/(main)/reports/retention/page.tsx
Normal file
10
src/app/(main)/reports/retention/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Metadata } from 'next';
|
||||
import RetentionReportPage from './RetentionReportPage';
|
||||
|
||||
export default function () {
|
||||
return <RetentionReportPage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Retention Report',
|
||||
};
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { createContext } from 'react';
|
||||
|
||||
export const SettingsContext = createContext(null);
|
||||
|
||||
export default SettingsContext;
|
||||
25
src/app/(main)/settings/SettingsLayout.tsx
Normal file
25
src/app/(main)/settings/SettingsLayout.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
'use client';
|
||||
import { ReactNode } from 'react';
|
||||
import { useLogin, useMessages } from 'components/hooks';
|
||||
import MenuLayout from 'components/layout/MenuLayout';
|
||||
|
||||
export default function SettingsLayout({ children }: { children: ReactNode }) {
|
||||
const { user } = useLogin();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: 'websites',
|
||||
label: formatMessage(labels.websites),
|
||||
url: '/settings/websites',
|
||||
},
|
||||
{ key: 'teams', label: formatMessage(labels.teams), url: '/settings/teams' },
|
||||
user.isAdmin && {
|
||||
key: 'users',
|
||||
label: formatMessage(labels.users),
|
||||
url: '/settings/users',
|
||||
},
|
||||
].filter(n => n);
|
||||
|
||||
return <MenuLayout items={items}>{children}</MenuLayout>;
|
||||
}
|
||||
|
|
@ -1,37 +1,17 @@
|
|||
'use client';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import SideNav from 'components/layout/SideNav';
|
||||
import styles from './layout.module.css';
|
||||
import SettingsLayout from './SettingsLayout';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function SettingsLayout({ children }) {
|
||||
const { user } = useUser();
|
||||
const pathname = usePathname();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const cloudMode = !!process.env.cloudMode;
|
||||
|
||||
const items = [
|
||||
{ key: 'websites', label: formatMessage(labels.websites), url: '/settings/websites' },
|
||||
{ key: 'teams', label: formatMessage(labels.teams), url: '/settings/teams' },
|
||||
user.isAdmin && { key: 'users', label: formatMessage(labels.users), url: '/settings/users' },
|
||||
{ key: 'profile', label: formatMessage(labels.profile), url: '/settings/profile' },
|
||||
].filter(n => n);
|
||||
|
||||
const getKey = () => items.find(({ url }) => pathname === url)?.key;
|
||||
|
||||
if (cloudMode && pathname !== '/settings/profile') {
|
||||
export default function ({ children }) {
|
||||
if (process.env.cloudMode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.layout}>
|
||||
{!cloudMode && (
|
||||
<div className={styles.menu}>
|
||||
<SideNav items={items} shallow={true} selectedKey={getKey()} />
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.content}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
return <SettingsLayout>{children}</SettingsLayout>;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
template: '%s | Settings | Umami',
|
||||
default: 'Settings | Umami',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
import { useApi, useMessages } from 'components/hooks';
|
||||
import {
|
||||
Form,
|
||||
FormRow,
|
||||
FormInput,
|
||||
FormButtons,
|
||||
TextField,
|
||||
Button,
|
||||
Form,
|
||||
FormButtons,
|
||||
FormInput,
|
||||
FormRow,
|
||||
SubmitButton,
|
||||
TextField,
|
||||
} from 'react-basics';
|
||||
import { setValue } from 'store/cache';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
|
||||
export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
|
@ -18,10 +16,9 @@ export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose:
|
|||
mutationFn: (data: any) => post('/teams', data),
|
||||
});
|
||||
|
||||
const handleSubmit = async data => {
|
||||
const handleSubmit = async (data: any) => {
|
||||
mutate(data, {
|
||||
onSuccess: async () => {
|
||||
setValue('teams', Date.now());
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import TeamDeleteForm from './TeamDeleteForm';
|
||||
|
||||
export function TeamDeleteButton({
|
||||
teamId,
|
||||
teamName,
|
||||
onDelete,
|
||||
}: {
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
onDelete?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<ModalTrigger>
|
||||
<Button>
|
||||
<Icon>
|
||||
<Icons.Trash />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.delete)}</Text>
|
||||
</Button>
|
||||
<Modal title={formatMessage(labels.deleteTeam)}>
|
||||
{(close: any) => (
|
||||
<TeamDeleteForm teamId={teamId} teamName={teamName} onSave={onDelete} onClose={close} />
|
||||
)}
|
||||
</Modal>
|
||||
</ModalTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamDeleteButton;
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
import { Button, Form, FormButtons, SubmitButton } from 'react-basics';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { setValue } from 'store/cache';
|
||||
|
||||
export function TeamDeleteForm({
|
||||
teamId,
|
||||
teamName,
|
||||
onSave,
|
||||
onClose,
|
||||
}: {
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
onSave: () => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels, messages, FormattedMessage } = useMessages();
|
||||
const { del, useMutation } = useApi();
|
||||
const { mutate, error, isPending } = useMutation({
|
||||
mutationFn: (data: any) => del(`/teams/${teamId}`, data),
|
||||
});
|
||||
|
||||
const handleSubmit = async data => {
|
||||
mutate(data, {
|
||||
onSuccess: async () => {
|
||||
setValue('teams', Date.now());
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} error={error}>
|
||||
<p>
|
||||
<FormattedMessage {...messages.confirmDelete} values={{ target: <b>{teamName}</b> }} />
|
||||
</p>
|
||||
<FormButtons flex>
|
||||
<SubmitButton variant="danger" disabled={isPending}>
|
||||
{formatMessage(labels.delete)}
|
||||
</SubmitButton>
|
||||
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamDeleteForm;
|
||||
|
|
@ -8,20 +8,19 @@ import {
|
|||
Button,
|
||||
SubmitButton,
|
||||
} from 'react-basics';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { setValue } from 'store/cache';
|
||||
import { useApi, useMessages, useModified } from 'components/hooks';
|
||||
|
||||
export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
|
||||
const { formatMessage, labels, getMessage } = useMessages();
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, error } = useMutation({ mutationFn: (data: any) => post('/teams/join', data) });
|
||||
const ref = useRef(null);
|
||||
const { touch } = useModified();
|
||||
|
||||
const handleSubmit = async data => {
|
||||
const handleSubmit = async (data: any) => {
|
||||
mutate(data, {
|
||||
onSuccess: async () => {
|
||||
setValue('teams:members', Date.now());
|
||||
touch('teams:members');
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,37 +1,35 @@
|
|||
import { useLocale, useLogin, useMessages, useModified } from 'components/hooks';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
import TeamDeleteForm from './TeamLeaveForm';
|
||||
|
||||
export function TeamLeaveButton({
|
||||
teamId,
|
||||
teamName,
|
||||
onLeave,
|
||||
}: {
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
onLeave?: () => void;
|
||||
}) {
|
||||
export function TeamLeaveButton({ teamId, teamName }: { teamId: string; teamName: string }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const router = useRouter();
|
||||
const { dir } = useLocale();
|
||||
const { user } = useUser();
|
||||
const { user } = useLogin();
|
||||
const { touch } = useModified();
|
||||
|
||||
const handleLeave = async () => {
|
||||
touch('teams');
|
||||
router.push('/settings/teams');
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalTrigger>
|
||||
<Button>
|
||||
<Button variant="secondary">
|
||||
<Icon rotate={dir === 'rtl' ? 180 : 0}>
|
||||
<Icons.Logout />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.leave)}</Text>
|
||||
</Button>
|
||||
<Modal title={formatMessage(labels.leaveTeam)}>
|
||||
{close => (
|
||||
{(close: () => void) => (
|
||||
<TeamDeleteForm
|
||||
teamId={teamId}
|
||||
userId={user.id}
|
||||
teamName={teamName}
|
||||
onSave={onLeave}
|
||||
onSave={handleLeave}
|
||||
onClose={close}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { Button, Form, FormButtons, SubmitButton } from 'react-basics';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { setValue } from 'store/cache';
|
||||
import { useApi, useMessages, useModified } from 'components/hooks';
|
||||
import ConfirmationForm from 'components/common/ConfirmationForm';
|
||||
|
||||
export function TeamLeaveForm({
|
||||
teamId,
|
||||
|
|
@ -21,11 +19,12 @@ export function TeamLeaveForm({
|
|||
const { mutate, error, isPending } = useMutation({
|
||||
mutationFn: () => del(`/teams/${teamId}/users/${userId}`),
|
||||
});
|
||||
const { touch } = useModified();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const handleConfirm = async () => {
|
||||
mutate(null, {
|
||||
onSuccess: async () => {
|
||||
setValue('team:members', Date.now());
|
||||
touch('teams:members');
|
||||
onSave();
|
||||
onClose();
|
||||
},
|
||||
|
|
@ -33,17 +32,16 @@ export function TeamLeaveForm({
|
|||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} error={error}>
|
||||
<p>
|
||||
<FormattedMessage {...messages.confirmDelete} values={{ target: <b>{teamName}</b> }} />
|
||||
</p>
|
||||
<FormButtons flex>
|
||||
<SubmitButton variant="danger" disabled={isPending}>
|
||||
{formatMessage(labels.leave)}
|
||||
</SubmitButton>
|
||||
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
<ConfirmationForm
|
||||
buttonLabel={formatMessage(labels.leave)}
|
||||
message={
|
||||
<FormattedMessage {...messages.confirmLeave} values={{ target: <b>{teamName}</b> }} />
|
||||
}
|
||||
onConfirm={handleConfirm}
|
||||
onClose={onClose}
|
||||
isLoading={isPending}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,19 @@
|
|||
import { Button, Icon, Modal, ModalTrigger, Text } from 'react-basics';
|
||||
import { Button, Icon, Modal, ModalTrigger, Text, useToasts } from 'react-basics';
|
||||
import Icons from 'components/icons';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages, useModified } from 'components/hooks';
|
||||
import TeamAddForm from './TeamAddForm';
|
||||
import { messages } from 'components/messages';
|
||||
|
||||
export function TeamsAddButton({ onAdd }: { onAdd?: () => void }) {
|
||||
export function TeamsAddButton({ onSave }: { onSave?: () => void }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { showToast } = useToasts();
|
||||
const { touch } = useModified();
|
||||
|
||||
const handleSave = async () => {
|
||||
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||
touch('teams');
|
||||
onSave?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalTrigger>
|
||||
|
|
@ -15,7 +24,7 @@ export function TeamsAddButton({ onAdd }: { onAdd?: () => void }) {
|
|||
<Text>{formatMessage(labels.createTeam)}</Text>
|
||||
</Button>
|
||||
<Modal title={formatMessage(labels.createTeam)}>
|
||||
{(close: () => void) => <TeamAddForm onSave={onAdd} onClose={close} />}
|
||||
{(close: () => void) => <TeamAddForm onSave={handleSave} onClose={close} />}
|
||||
</Modal>
|
||||
</ModalTrigger>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,26 +1,21 @@
|
|||
'use client';
|
||||
import DataTable from 'components/common/DataTable';
|
||||
import TeamsTable from 'app/(main)/settings/teams/TeamsTable';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useFilterQuery from 'components/hooks/useFilterQuery';
|
||||
import useCache from 'store/cache';
|
||||
import { useLogin, useTeams } from 'components/hooks';
|
||||
|
||||
export function TeamsDataTable() {
|
||||
const { get } = useApi();
|
||||
const modified = useCache((state: any) => state?.teams);
|
||||
const queryResult = useFilterQuery({
|
||||
queryKey: ['teams', { modified }],
|
||||
queryFn: (params: any) => {
|
||||
return get(`/teams`, {
|
||||
...params,
|
||||
});
|
||||
},
|
||||
});
|
||||
export function TeamsDataTable({
|
||||
allowEdit,
|
||||
showActions,
|
||||
}: {
|
||||
allowEdit?: boolean;
|
||||
showActions?: boolean;
|
||||
}) {
|
||||
const { user } = useLogin();
|
||||
const queryResult = useTeams(user.id);
|
||||
|
||||
return (
|
||||
<DataTable queryResult={queryResult}>
|
||||
{({ data }) => {
|
||||
return <TeamsTable data={data} />;
|
||||
return <TeamsTable data={data} allowEdit={allowEdit} showActions={showActions} />;
|
||||
}}
|
||||
</DataTable>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,21 +1,20 @@
|
|||
'use client';
|
||||
import { Flexbox } from 'react-basics';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useLogin, useMessages } from 'components/hooks';
|
||||
import TeamsJoinButton from './TeamsJoinButton';
|
||||
import TeamsAddButton from './TeamsAddButton';
|
||||
|
||||
export function TeamsHeader() {
|
||||
export function TeamsHeader({ allowCreate = true }: { allowCreate?: boolean }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useUser();
|
||||
const { user } = useLogin();
|
||||
const cloudMode = !!process.env.cloudMode;
|
||||
|
||||
return (
|
||||
<PageHeader title={formatMessage(labels.teams)}>
|
||||
<Flexbox gap={10}>
|
||||
<TeamsJoinButton />
|
||||
{user.role !== ROLES.viewOnly && <TeamsAddButton />}
|
||||
{!cloudMode && <TeamsJoinButton />}
|
||||
{allowCreate && user.role !== ROLES.viewOnly && <TeamsAddButton />}
|
||||
</Flexbox>
|
||||
</PageHeader>
|
||||
);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue