Merge branch 'dev' into os-names
This commit is contained in:
472 files changed
+5460
-5026
No files matched your search
@@ -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={`/telemetry.js`} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
padding: 0 40px;
|
||||
font-weight: 700;
|
||||
max-height: 60px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.links a,
|
||||
|
||||
+19
-14
@@ -2,35 +2,38 @@
|
||||
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 { useLogin, useMessages, useNavigation, useTeamUrl } from 'components/hooks';
|
||||
import styles from './NavBar.module.css';
|
||||
|
||||
export function NavBar() {
|
||||
const pathname = usePathname();
|
||||
const { user } = useLogin();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const cloudMode = Boolean(process.env.cloudMode);
|
||||
const { pathname } = useNavigation();
|
||||
const { teamId, 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,13 +49,13 @@ 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);
|
||||
@@ -72,6 +75,7 @@ export function NavBar() {
|
||||
key={url}
|
||||
href={url}
|
||||
className={classNames({ [styles.selected]: pathname.startsWith(url) })}
|
||||
prefetch={url !== '/settings'}
|
||||
>
|
||||
<Text>{label}</Text>
|
||||
</Link>
|
||||
@@ -79,6 +83,7 @@ export function NavBar() {
|
||||
})}
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
{user?.teams?.length && <TeamsButton teamId={teamId} />}
|
||||
<ThemeButton />
|
||||
<LanguageButton />
|
||||
<ProfileButton />
|
||||
|
||||
@@ -6,7 +6,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 }) {
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
'use client';
|
||||
import { Button } from 'react-basics';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
@@ -7,9 +6,8 @@ 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 }) {
|
||||
@@ -82,15 +80,15 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
|
||||
<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 +112,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 +126,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>
|
||||
);
|
||||
|
||||
+4
-4
@@ -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 | Umami',
|
||||
};
|
||||
@@ -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}
|
||||
|
||||
@@ -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 | Umami',
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
+3
-3
@@ -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 (
|
||||
@@ -0,0 +1,4 @@
|
||||
div.menu {
|
||||
max-height: 300px;
|
||||
width: 300px;
|
||||
}
|
||||
+17
-6
@@ -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>
|
||||
+2
-2
@@ -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
-2
@@ -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
@@ -1,4 +1,3 @@
|
||||
'use client';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import { useMessages } from 'components/hooks';
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
+9
-11
@@ -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);
|
||||
File renamed without changes.
+1
-1
@@ -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';
|
||||
@@ -0,0 +1,4 @@
|
||||
div.menu {
|
||||
max-height: 300px;
|
||||
width: 300px;
|
||||
}
|
||||
+10
-5
@@ -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>
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Metadata } from 'next';
|
||||
import ProfilePage from './ProfilePage';
|
||||
|
||||
export default function () {
|
||||
return <ProfilePage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Profile | Umami',
|
||||
};
|
||||
@@ -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,20 @@
|
||||
'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 { useMessages, useTeamUrl } from 'components/hooks';
|
||||
import LinkButton from 'components/common/LinkButton';
|
||||
|
||||
export function ReportsHeader() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const router = useRouter();
|
||||
|
||||
const handleClick = () => router.push('/reports/create');
|
||||
const { renderTeamUrl } = useTeamUrl();
|
||||
|
||||
return (
|
||||
<PageHeader title={formatMessage(labels.reports)}>
|
||||
<Button variant="primary" onClick={handleClick}>
|
||||
<LinkButton href={renderTeamUrl('/reports/create')} variant="primary">
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.createReport)}</Text>
|
||||
</Button>
|
||||
</LinkButton>
|
||||
</PageHeader>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 | Umami',
|
||||
};
|
||||
@@ -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,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',
|
||||
};
|
||||
+8
-4
@@ -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>
|
||||
)}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -41,7 +41,7 @@ export default function FieldFilterForm({
|
||||
format(values[0]);
|
||||
}
|
||||
return formatted;
|
||||
}, [values]);
|
||||
}, [formatValue, locale, name, values]);
|
||||
|
||||
const renderFilterValue = value => {
|
||||
return filters.find(f => f.value === value)?.label;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+8
-6
@@ -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) {
|
||||
File renamed without changes.
File renamed without changes.
-1
@@ -1,4 +1,3 @@
|
||||
'use client';
|
||||
import FunnelReport from '../funnel/FunnelReport';
|
||||
import EventDataReport from '../event-data/EventDataReport';
|
||||
import InsightsReport from '../insights/InsightsReport';
|
||||
File renamed without changes.
+5
-4
@@ -1,7 +1,6 @@
|
||||
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';
|
||||
@@ -10,8 +9,10 @@ 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 +29,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 {
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,6 @@
|
||||
'use client';
|
||||
import ReportDetails from './ReportDetails';
|
||||
|
||||
export default function ReportPage({ reportId }) {
|
||||
return <ReportDetails reportId={reportId} />;
|
||||
}
|
||||
@@ -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 | Umami',
|
||||
};
|
||||
@@ -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 | Umami',
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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 | Umami',
|
||||
};
|
||||
@@ -1,10 +1,9 @@
|
||||
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 { useMessages, useTheme } from 'components/hooks';
|
||||
import BarChart from 'components/metrics/BarChart';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import styles from './FunnelChart.module.css';
|
||||
|
||||
export interface FunnelChartProps {
|
||||
|
||||
@@ -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,10 @@
|
||||
'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 +13,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 />} />
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
'use client';
|
||||
import FunnelReport from './FunnelReport';
|
||||
|
||||
export default function FunnelReportPage() {
|
||||
return <FunnelReport />;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useContext } from 'react';
|
||||
import ListTable from 'components/metrics/ListTable';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
|
||||
export function FunnelTable() {
|
||||
const { report } = useContext(ReportContext);
|
||||
|
||||
@@ -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 | Umami',
|
||||
};
|
||||
@@ -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 />} />
|
||||
|
||||
@@ -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 | Umami',
|
||||
};
|
||||
@@ -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 | Umami',
|
||||
};
|
||||
@@ -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 />} />
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Metadata } from 'next';
|
||||
import RetentionReportPage from './RetentionReportPage';
|
||||
|
||||
export default function () {
|
||||
return <RetentionReportPage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Create Report | Umami',
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const SettingsContext = createContext(null);
|
||||
|
||||
export default SettingsContext;
|
||||
@@ -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,9 @@
|
||||
'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';
|
||||
|
||||
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>;
|
||||
}
|
||||
@@ -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,20 @@
|
||||
'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 { 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 queryResult = useTeams();
|
||||
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
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 TeamJoinForm from './TeamJoinForm';
|
||||
|
||||
export function TeamsJoinButton() {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { showToast } = useToasts();
|
||||
const { touch } = useModified();
|
||||
|
||||
const handleJoin = () => {
|
||||
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||
touch('teams');
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
'use client';
|
||||
import TeamsDataTable from './TeamsDataTable';
|
||||
import TeamsHeader from './TeamsHeader';
|
||||
|
||||
export default function TeamsPage() {
|
||||
return (
|
||||
<>
|
||||
<TeamsHeader />
|
||||
<TeamsDataTable />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
'use client';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
import { GridColumn, GridTable, Icon, Text, useBreakpoint } from 'react-basics';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import Icons from 'components/icons';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import Link from 'next/link';
|
||||
import { Button, GridColumn, GridTable, Icon, Icons, Text, useBreakpoint } from 'react-basics';
|
||||
import TeamDeleteButton from './TeamDeleteButton';
|
||||
import TeamLeaveButton from './TeamLeaveButton';
|
||||
import LinkButton from 'components/common/LinkButton';
|
||||
|
||||
export function TeamsTable({ data = [] }: { data: any[] }) {
|
||||
export function TeamsTable({
|
||||
data = [],
|
||||
showActions = true,
|
||||
}: {
|
||||
data: any[];
|
||||
allowEdit?: boolean;
|
||||
showActions?: boolean;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useUser();
|
||||
const breakpoint = useBreakpoint();
|
||||
|
||||
return (
|
||||
@@ -18,28 +21,28 @@ export function TeamsTable({ data = [] }: { data: any[] }) {
|
||||
<GridColumn name="owner" label={formatMessage(labels.owner)}>
|
||||
{row => row.teamUser.find(({ role }) => role === ROLES.teamOwner)?.user?.username}
|
||||
</GridColumn>
|
||||
<GridColumn name="action" label=" " alignment="end">
|
||||
{row => {
|
||||
const { id, name, teamUser } = row;
|
||||
const owner = teamUser.find(({ role }) => role === ROLES.teamOwner);
|
||||
const isOwner = user.id === owner?.userId;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOwner && <TeamDeleteButton teamId={id} teamName={name} />}
|
||||
{!isOwner && <TeamLeaveButton teamId={id} teamName={name} />}
|
||||
<Link href={`/settings/teams/${id}`}>
|
||||
<Button>
|
||||
<Icon>
|
||||
<Icons.Edit />
|
||||
</Icon>
|
||||
<Text>{formatMessage(isOwner ? labels.edit : labels.view)}</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
<GridColumn name="websites" label={formatMessage(labels.websites)}>
|
||||
{row => row._count.website}
|
||||
</GridColumn>
|
||||
<GridColumn name="members" label={formatMessage(labels.members)}>
|
||||
{row => row._count.teamUser}
|
||||
</GridColumn>
|
||||
{showActions && (
|
||||
<GridColumn name="action" label=" " alignment="end">
|
||||
{row => {
|
||||
const { id } = row;
|
||||
|
||||
return (
|
||||
<LinkButton href={`/teams/${id}/settings`}>
|
||||
<Icon>
|
||||
<Icons.ArrowRight />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.view)}</Text>
|
||||
</LinkButton>
|
||||
);
|
||||
}}
|
||||
</GridColumn>
|
||||
)}
|
||||
</GridTable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { Icon, Icons, LoadingButton, Text } from 'react-basics';
|
||||
import { setValue } from 'store/cache';
|
||||
|
||||
export function TeamMemberRemoveButton({
|
||||
teamId,
|
||||
userId,
|
||||
disabled,
|
||||
onSave,
|
||||
}: {
|
||||
teamId: string;
|
||||
userId: string;
|
||||
disabled?: boolean;
|
||||
onSave?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { del, useMutation } = useApi();
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: () => del(`/teams/${teamId}/users/${userId}`),
|
||||
});
|
||||
|
||||
const handleRemoveTeamMember = () => {
|
||||
mutate(null, {
|
||||
onSuccess: () => {
|
||||
setValue('team:members', Date.now());
|
||||
onSave?.();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<LoadingButton
|
||||
onClick={() => handleRemoveTeamMember()}
|
||||
disabled={disabled}
|
||||
isLoading={isPending}
|
||||
>
|
||||
<Icon>
|
||||
<Icons.Close />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.remove)}</Text>
|
||||
</LoadingButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamMemberRemoveButton;
|
||||
Loaded 100 of 472 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user