Merge branch 'dev' into analytics
# Conflicts: # src/app/(main)/reports/[id]/Report.js # src/queries/analytics/eventData/getEventDataEvents.ts # src/queries/analytics/getActiveVisitors.ts # src/queries/analytics/sessions/getSessionMetrics.ts # yarn.lock
This commit is contained in:
58
src/app/(main)/NavBar.js
Normal file
58
src/app/(main)/NavBar.js
Normal file
@@ -0,0 +1,58 @@
|
||||
'use client';
|
||||
import { Icon, Text } from 'react-basics';
|
||||
import Link from 'next/link';
|
||||
import classNames from 'classnames';
|
||||
import Icons from 'components/icons';
|
||||
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 styles from './NavBar.module.css';
|
||||
|
||||
export function NavBar() {
|
||||
const pathname = usePathname();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
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' },
|
||||
].filter(n => n);
|
||||
|
||||
return (
|
||||
<div className={styles.navbar}>
|
||||
<div className={styles.logo}>
|
||||
<Icon size="lg">
|
||||
<Icons.Logo />
|
||||
</Icon>
|
||||
<Text>umami</Text>
|
||||
</div>
|
||||
<div className={styles.links}>
|
||||
{links.map(({ url, label }) => {
|
||||
return (
|
||||
<Link
|
||||
key={url}
|
||||
href={url}
|
||||
className={classNames({ [styles.selected]: pathname.startsWith(url) })}
|
||||
>
|
||||
<Text>{label}</Text>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
<ThemeButton />
|
||||
<LanguageButton />
|
||||
<ProfileButton />
|
||||
</div>
|
||||
<div className={styles.mobile}>
|
||||
<HamburgerButton />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NavBar;
|
||||
@@ -1,7 +1,7 @@
|
||||
.navbar {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr 1fr;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
background: var(--base75);
|
||||
@@ -9,17 +9,6 @@
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -35,29 +24,24 @@
|
||||
flex-direction: row;
|
||||
gap: 30px;
|
||||
padding: 0 40px;
|
||||
flex: 1;
|
||||
font-weight: 700;
|
||||
max-height: 60px;
|
||||
}
|
||||
|
||||
.links a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
line-height: 60px;
|
||||
.links a,
|
||||
.links a:active,
|
||||
.links a:visited {
|
||||
color: var(--font-color200);
|
||||
line-height: 60px;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.links span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.links a:hover {
|
||||
color: var(--font-color100);
|
||||
border-bottom: 2px solid var(--primary400);
|
||||
}
|
||||
|
||||
.links .selected {
|
||||
.links a.selected {
|
||||
color: var(--font-color100);
|
||||
border-bottom: 2px solid var(--primary400);
|
||||
}
|
||||
@@ -68,7 +52,6 @@
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
@@ -76,6 +59,10 @@
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.navbar {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.links,
|
||||
.actions {
|
||||
display: none;
|
||||
27
src/app/(main)/Shell.tsx
Normal file
27
src/app/(main)/Shell.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
import Script from 'next/script';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import UpdateNotice from 'components/common/UpdateNotice';
|
||||
import { useRequireLogin, useConfig } from 'components/hooks';
|
||||
|
||||
export function Shell({ children }) {
|
||||
const { user } = useRequireLogin();
|
||||
const config = useConfig();
|
||||
const pathname = usePathname();
|
||||
|
||||
if (!user || !config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<UpdateNotice user={user} config={config} />
|
||||
{process.env.NODE_ENV === 'production' && !pathname.includes('/share/') && (
|
||||
<Script src={`telemetry.js`} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Shell;
|
||||
@@ -1,14 +1,15 @@
|
||||
'use client';
|
||||
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 'components/pages/websites/WebsiteChart';
|
||||
import WebsiteChart from '../../(main)/websites/[id]/WebsiteChart';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Script from 'next/script';
|
||||
import { Button, Column, Row } from 'react-basics';
|
||||
import { Button } from 'react-basics';
|
||||
import styles from './TestConsole.module.css';
|
||||
|
||||
export function TestConsole() {
|
||||
@@ -90,8 +91,8 @@ export function TestConsole() {
|
||||
src={`${basePath}/script.js`}
|
||||
data-cache="true"
|
||||
/>
|
||||
<Row className={styles.test}>
|
||||
<Column xs="4">
|
||||
<div className={styles.test}>
|
||||
<div>
|
||||
<div className={styles.header}>Page links</div>
|
||||
<div>
|
||||
<Link href={`/console/${websiteId}/page/1/?q=abc`}>page one</Link>
|
||||
@@ -114,8 +115,8 @@ export function TestConsole() {
|
||||
external link (tab)
|
||||
</a>
|
||||
</div>
|
||||
</Column>
|
||||
<Column xs="4">
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.header}>Click events</div>
|
||||
<Button id="send-event-button" data-umami-event="button-click" variant="action">
|
||||
Send event
|
||||
@@ -130,8 +131,8 @@ export function TestConsole() {
|
||||
>
|
||||
Send event with data
|
||||
</Button>
|
||||
</Column>
|
||||
<Column xs="4">
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.header}>Javascript events</div>
|
||||
<Button id="manual-button" variant="action" onClick={handleClick}>
|
||||
Run script
|
||||
@@ -140,14 +141,12 @@ export function TestConsole() {
|
||||
<Button id="manual-button" variant="action" onClick={handleIdentifyClick}>
|
||||
Run identify
|
||||
</Button>
|
||||
</Column>
|
||||
</Row>
|
||||
<Row>
|
||||
<Column>
|
||||
<WebsiteChart websiteId={website.id} />
|
||||
<EventsChart websiteId={website.id} />
|
||||
</Column>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<WebsiteChart websiteId={website.id} />
|
||||
<EventsChart websiteId={website.id} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Page>
|
||||
20
src/app/(main)/console/[[...id]]/page.tsx
Normal file
20
src/app/(main)/console/[[...id]]/page.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import TestConsole from '../TestConsole';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
async function getEnabled() {
|
||||
return !!process.env.ENABLE_TEST_CONSOLE;
|
||||
}
|
||||
|
||||
export default async function ConsolePage() {
|
||||
const enabled = await getEnabled();
|
||||
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <TestConsole />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Test Console | umami',
|
||||
};
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Button, Icon, Icons, Text } from 'react-basics';
|
||||
'use client';
|
||||
import { Button, Icon, Icons, Loading, Text } from 'react-basics';
|
||||
import Link from 'next/link';
|
||||
import Page from 'components/layout/Page';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import Pager from 'components/common/Pager';
|
||||
import WebsiteChartList from 'components/pages/websites/WebsiteChartList';
|
||||
import DashboardSettingsButton from 'components/pages/dashboard/DashboardSettingsButton';
|
||||
import DashboardEdit from 'components/pages/dashboard/DashboardEdit';
|
||||
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';
|
||||
@@ -20,18 +20,18 @@ export function Dashboard() {
|
||||
const { get, useQuery } = useApi();
|
||||
const { page, handlePageChange } = useApiFilter();
|
||||
const pageSize = 10;
|
||||
const {
|
||||
data: result,
|
||||
isLoading,
|
||||
error,
|
||||
} = useQuery(['websites', page, pageSize], () =>
|
||||
const { data: result, isLoading } = useQuery(['websites', page, pageSize], () =>
|
||||
get('/websites', { includeTeams: 1, page, pageSize }),
|
||||
);
|
||||
const { data, count } = result || {};
|
||||
const hasData = data && data?.length !== 0;
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading size="lg" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page loading={isLoading} error={error}>
|
||||
<>
|
||||
<PageHeader title={formatMessage(labels.dashboard)}>
|
||||
{!editing && hasData && <DashboardSettingsButton />}
|
||||
</PageHeader>
|
||||
@@ -63,7 +63,7 @@ export function Dashboard() {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
|
||||
import classNames from 'classnames';
|
||||
@@ -7,7 +8,6 @@ import useDashboard, { saveDashboard } from 'store/dashboard';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import styles from './DashboardEdit.module.css';
|
||||
import Page from 'components/layout/Page';
|
||||
|
||||
const dragId = 'dashboard-website-ordering';
|
||||
|
||||
@@ -17,11 +17,7 @@ export function DashboardEdit() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [order, setOrder] = useState(websiteOrder || []);
|
||||
const { get, useQuery } = useApi();
|
||||
const {
|
||||
data: result,
|
||||
isLoading,
|
||||
error,
|
||||
} = useQuery(['websites'], () => get('/websites', { includeTeams: 1 }));
|
||||
const { data: result } = useQuery(['websites'], () => get('/websites', { includeTeams: 1 }));
|
||||
const { data: websites } = result || {};
|
||||
|
||||
const ordered = useMemo(() => {
|
||||
@@ -59,7 +55,7 @@ export function DashboardEdit() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Page loading={isLoading} error={error}>
|
||||
<>
|
||||
<div className={styles.buttons}>
|
||||
<Button onClick={handleSave} variant="action" size="small">
|
||||
{formatMessage(labels.save)}
|
||||
@@ -105,7 +101,7 @@ export function DashboardEdit() {
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
10
src/app/(main)/dashboard/page.tsx
Normal file
10
src/app/(main)/dashboard/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import Dashboard from 'app/(main)/dashboard/Dashboard';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function DashboardPage() {
|
||||
return <Dashboard />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Dashboard | umami',
|
||||
};
|
||||
@@ -10,7 +10,6 @@
|
||||
width: 100vw;
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 2;
|
||||
z-index: var(--z-index-popup);
|
||||
}
|
||||
|
||||
.body {
|
||||
19
src/app/(main)/layout.tsx
Normal file
19
src/app/(main)/layout.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import Shell from './Shell';
|
||||
import NavBar from './NavBar';
|
||||
import Page from 'components/layout/Page';
|
||||
import styles from './layout.module.css';
|
||||
|
||||
export default function AppLayout({ children }) {
|
||||
return (
|
||||
<Shell>
|
||||
<main className={styles.layout}>
|
||||
<nav className={styles.nav}>
|
||||
<NavBar />
|
||||
</nav>
|
||||
<section className={styles.body}>
|
||||
<Page>{children}</Page>
|
||||
</section>
|
||||
</main>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
42
src/app/(main)/reports/ReportDeleteButton.js
Normal file
42
src/app/(main)/reports/ReportDeleteButton.js
Normal file
@@ -0,0 +1,42 @@
|
||||
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';
|
||||
|
||||
export function ReportDeleteButton({ reportId, reportName, onDelete }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { del, useMutation } = useApi();
|
||||
const { mutate } = useMutation(reportId => del(`/reports/${reportId}`));
|
||||
|
||||
const handleConfirm = close => {
|
||||
mutate(reportId, {
|
||||
onSuccess: () => {
|
||||
setValue('reports', Date.now());
|
||||
onDelete?.();
|
||||
close();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalTrigger>
|
||||
<Button>
|
||||
<Icon>
|
||||
<Icons.Trash />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.delete)}</Text>
|
||||
</Button>
|
||||
<Modal>
|
||||
{close => (
|
||||
<ConfirmDeleteForm
|
||||
name={reportName}
|
||||
onConfirm={handleConfirm.bind(null, close)}
|
||||
onClose={close}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</ModalTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReportDeleteButton;
|
||||
20
src/app/(main)/reports/ReportsDataTable.js
Normal file
20
src/app/(main)/reports/ReportsDataTable.js
Normal file
@@ -0,0 +1,20 @@
|
||||
'use client';
|
||||
import { useApi } from 'components/hooks';
|
||||
import ReportsTable from './ReportsTable';
|
||||
import useFilterQuery from 'components/hooks/useFilterQuery';
|
||||
import DataTable from 'components/common/DataTable';
|
||||
import useCache from 'store/cache';
|
||||
|
||||
export default function ReportsDataTable({ websiteId }) {
|
||||
const { get } = useApi();
|
||||
const modified = useCache(state => state?.reports);
|
||||
const queryResult = useFilterQuery(['reports', { websiteId, modified }], params =>
|
||||
get(websiteId ? `/websites/${websiteId}/reports` : `/reports`, params),
|
||||
);
|
||||
|
||||
return (
|
||||
<DataTable queryResult={queryResult}>
|
||||
{({ data }) => <ReportsTable data={data} showDomain={!websiteId} />}
|
||||
</DataTable>
|
||||
);
|
||||
}
|
||||
25
src/app/(main)/reports/ReportsHeader.js
Normal file
25
src/app/(main)/reports/ReportsHeader.js
Normal file
@@ -0,0 +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';
|
||||
|
||||
export function ReportsHeader() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const router = useRouter();
|
||||
|
||||
const handleClick = () => router.push('/reports/create');
|
||||
|
||||
return (
|
||||
<PageHeader title={formatMessage(labels.reports)}>
|
||||
<Button variant="primary" onClick={handleClick}>
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.createReport)}</Text>
|
||||
</Button>
|
||||
</PageHeader>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReportsHeader;
|
||||
51
src/app/(main)/reports/ReportsTable.js
Normal file
51
src/app/(main)/reports/ReportsTable.js
Normal file
@@ -0,0 +1,51 @@
|
||||
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 { REPORT_TYPES } from 'lib/constants';
|
||||
import ReportDeleteButton from './ReportDeleteButton';
|
||||
|
||||
export function ReportsTable({ data = [], showDomain }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useUser();
|
||||
const breakpoint = useBreakpoint();
|
||||
|
||||
return (
|
||||
<GridTable data={data} cardMode={['xs', 'sm', 'md'].includes(breakpoint)}>
|
||||
<GridColumn name="name" label={formatMessage(labels.name)} />
|
||||
<GridColumn name="description" label={formatMessage(labels.description)} />
|
||||
<GridColumn name="type" label={formatMessage(labels.type)}>
|
||||
{row => {
|
||||
return formatMessage(
|
||||
labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === row.type)],
|
||||
);
|
||||
}}
|
||||
</GridColumn>
|
||||
{showDomain && (
|
||||
<GridColumn name="domain" label={formatMessage(labels.domain)}>
|
||||
{row => row?.website?.domain}
|
||||
</GridColumn>
|
||||
)}
|
||||
<GridColumn name="action" label="" alignment="end">
|
||||
{row => {
|
||||
const { id, name, userId, website } = row;
|
||||
return (
|
||||
<>
|
||||
{(user.id === userId || user.id === website?.userId) && (
|
||||
<ReportDeleteButton reportId={id} reportName={name} />
|
||||
)}
|
||||
<LinkButton href={`/reports/${id}`}>
|
||||
<Icon>
|
||||
<Icons.ArrowRight />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.view)}</Text>
|
||||
</LinkButton>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</GridColumn>
|
||||
</GridTable>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReportsTable;
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useContext } from 'react';
|
||||
import { FormRow } from 'react-basics';
|
||||
import { parseDateRange } from 'lib/date';
|
||||
import DateFilter from 'components/input/DateFilter';
|
||||
import WebsiteSelect from 'components/input/WebsiteSelect';
|
||||
import { parseDateRange } from 'lib/date';
|
||||
import { useContext } from 'react';
|
||||
import { ReportContext } from './Report';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { ReportContext } from './Report';
|
||||
|
||||
export function BaseParameters({
|
||||
showWebsiteSelect = true,
|
||||
@@ -7,7 +7,7 @@ import FieldAggregateForm from './FieldAggregateForm';
|
||||
import FieldFilterForm from './FieldFilterForm';
|
||||
import styles from './FieldAddForm.module.css';
|
||||
|
||||
export function FieldAddForm({ fields = [], group, element, onAdd, onClose }) {
|
||||
export function FieldAddForm({ fields = [], group, onAdd, onClose }) {
|
||||
const [selected, setSelected] = useState();
|
||||
|
||||
const handleSelect = value => {
|
||||
@@ -28,7 +28,7 @@ export function FieldAddForm({ fields = [], group, element, onAdd, onClose }) {
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<PopupForm className={styles.popup} element={element} onClose={onClose}>
|
||||
<PopupForm className={styles.popup}>
|
||||
{!selected && <FieldSelectForm fields={fields} onSelect={handleSelect} />}
|
||||
{selected && group === REPORT_PARAMETERS.fields && (
|
||||
<FieldAggregateForm {...selected} onSelect={handleSave} />
|
||||
@@ -1,16 +1,20 @@
|
||||
import { useState } from 'react';
|
||||
import { Loading } from 'react-basics';
|
||||
import { subDays } from 'date-fns';
|
||||
import FieldSelectForm from './FieldSelectForm';
|
||||
import FieldFilterForm from './FieldFilterForm';
|
||||
import { useApi } from 'components/hooks';
|
||||
import { Loading } from 'react-basics';
|
||||
|
||||
function useValues(websiteId, type) {
|
||||
const now = Date.now();
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, error, isLoading } = useQuery(
|
||||
['websites:values', websiteId, type],
|
||||
() =>
|
||||
get(`/websites/${websiteId}/values`, {
|
||||
type,
|
||||
startAt: +subDays(now, 90),
|
||||
endAt: now,
|
||||
}),
|
||||
{ enabled: !!(websiteId && type) },
|
||||
);
|
||||
@@ -1,18 +1,22 @@
|
||||
'use client';
|
||||
import { createContext } from 'react';
|
||||
import Page from 'components/layout/Page';
|
||||
import styles from './reports.module.css';
|
||||
import { useReport } from 'components/hooks';
|
||||
import styles from './Report.module.css';
|
||||
|
||||
export const ReportContext = createContext(null);
|
||||
|
||||
export function Report({ reportId, defaultParameters, children, ...props }) {
|
||||
const report = useReport(reportId, defaultParameters);
|
||||
|
||||
if (!report) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ReportContext.Provider value={{ ...report }}>
|
||||
<Page {...props} className={styles.container}>
|
||||
<div {...props} className={styles.container}>
|
||||
{children}
|
||||
</Page>
|
||||
</div>
|
||||
</ReportContext.Provider>
|
||||
);
|
||||
}
|
||||
5
src/app/(main)/reports/[id]/Report.module.css
Normal file
5
src/app/(main)/reports/[id]/Report.module.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-rows: max-content 1fr;
|
||||
grid-template-columns: max-content 1fr;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import styles from './reports.module.css';
|
||||
import styles from './ReportBody.module.css';
|
||||
|
||||
export function ReportBody({ children }) {
|
||||
return <div className={styles.body}>{children}</div>;
|
||||
5
src/app/(main)/reports/[id]/ReportBody.module.css
Normal file
5
src/app/(main)/reports/[id]/ReportBody.module.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.body {
|
||||
padding-left: 20px;
|
||||
grid-row: 2/3;
|
||||
grid-column: 2 / 3;
|
||||
}
|
||||
26
src/app/(main)/reports/[id]/ReportDetails.js
Normal file
26
src/app/(main)/reports/[id]/ReportDetails.js
Normal file
@@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
import FunnelReport from '../funnel/FunnelReport';
|
||||
import EventDataReport from '../event-data/EventDataReport';
|
||||
import InsightsReport from '../insights/InsightsReport';
|
||||
import RetentionReport from '../retention/RetentionReport';
|
||||
import { useApi } from 'components/hooks';
|
||||
|
||||
const reports = {
|
||||
funnel: FunnelReport,
|
||||
'event-data': EventDataReport,
|
||||
insights: InsightsReport,
|
||||
retention: RetentionReport,
|
||||
};
|
||||
|
||||
export default function ReportDetails({ reportId }) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { data: report } = useQuery(['reports', reportId], () => get(`/reports/${reportId}`));
|
||||
|
||||
if (!report) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ReportComponent = reports[report.type];
|
||||
|
||||
return <ReportComponent reportId={reportId} />;
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Icon, LoadingButton, InlineEditField, useToasts } from 'react-basics';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import { useMessages, useApi } from 'components/hooks';
|
||||
import { ReportContext } from './Report';
|
||||
import styles from './ReportHeader.module.css';
|
||||
import reportStyles from './reports.module.css';
|
||||
import { REPORT_TYPES } from 'lib/constants';
|
||||
|
||||
export function ReportHeader({ icon }) {
|
||||
const { report, updateReport } = useContext(ReportContext);
|
||||
@@ -47,24 +46,39 @@ export function ReportHeader({ icon }) {
|
||||
updateReport({ description });
|
||||
};
|
||||
|
||||
const Title = () => {
|
||||
return (
|
||||
<>
|
||||
<Icon size="lg">{icon}</Icon>
|
||||
<InlineEditField
|
||||
key={name}
|
||||
name="name"
|
||||
value={name}
|
||||
placeholder={defaultName}
|
||||
onCommit={handleNameChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
if (!report) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={reportStyles.header}>
|
||||
<PageHeader title={<Title />}>
|
||||
<div className={styles.header}>
|
||||
<div>
|
||||
<div className={styles.type}>
|
||||
{formatMessage(
|
||||
labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === report?.type)],
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.title}>
|
||||
<Icon size="lg">{icon}</Icon>
|
||||
<InlineEditField
|
||||
key={name}
|
||||
name="name"
|
||||
value={name}
|
||||
placeholder={defaultName}
|
||||
onCommit={handleNameChange}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.description}>
|
||||
<InlineEditField
|
||||
key={description}
|
||||
name="description"
|
||||
value={description}
|
||||
placeholder={`+ ${formatMessage(labels.addDescription)}`}
|
||||
onCommit={handleDescriptionChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
<LoadingButton
|
||||
variant="primary"
|
||||
isLoading={isCreating || isUpdating}
|
||||
@@ -73,15 +87,6 @@ export function ReportHeader({ icon }) {
|
||||
>
|
||||
{formatMessage(labels.save)}
|
||||
</LoadingButton>
|
||||
</PageHeader>
|
||||
<div className={styles.description}>
|
||||
<InlineEditField
|
||||
key={description}
|
||||
name="description"
|
||||
value={description}
|
||||
placeholder={`+ ${formatMessage(labels.addDescription)}`}
|
||||
onCommit={handleDescriptionChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
36
src/app/(main)/reports/[id]/ReportHeader.module.css
Normal file
36
src/app/(main)/reports/[id]/ReportHeader.module.css
Normal file
@@ -0,0 +1,36 @@
|
||||
.header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr min-content;
|
||||
align-items: center;
|
||||
grid-row: 1 / 2;
|
||||
grid-column: 1 / 3;
|
||||
margin: 20px 0 40px 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
gap: 20px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.type {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
color: var(--base600);
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--font-color300);
|
||||
max-width: 500px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import styles from './reports.module.css';
|
||||
import styles from './ReportMenu.module.css';
|
||||
|
||||
export function ReportMenu({ children }) {
|
||||
return <div className={styles.menu}>{children}</div>;
|
||||
7
src/app/(main)/reports/[id]/ReportMenu.module.css
Normal file
7
src/app/(main)/reports/[id]/ReportMenu.module.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.menu {
|
||||
width: 300px;
|
||||
padding-right: 20px;
|
||||
border-right: 1px solid var(--base300);
|
||||
grid-row: 2 / 3;
|
||||
grid-column: 1 / 2;
|
||||
}
|
||||
14
src/app/(main)/reports/[id]/page.tsx
Normal file
14
src/app/(main)/reports/[id]/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
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',
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
import Link from 'next/link';
|
||||
import { Button, Icons, Text, Icon } from 'react-basics';
|
||||
import Page from 'components/layout/Page';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import Funnel from 'assets/funnel.svg';
|
||||
import Lightbulb from 'assets/lightbulb.svg';
|
||||
@@ -57,7 +57,7 @@ export function ReportTemplates({ showHeader = true }) {
|
||||
];
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<>
|
||||
{showHeader && <PageHeader title={formatMessage(labels.reports)} />}
|
||||
<div className={styles.reports}>
|
||||
{reports.map(({ title, description, url, icon }) => {
|
||||
@@ -66,7 +66,7 @@ export function ReportTemplates({ showHeader = true }) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
10
src/app/(main)/reports/create/page.tsx
Normal file
10
src/app/(main)/reports/create/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import ReportTemplates from './ReportTemplates';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function ReportsCreatePage() {
|
||||
return <ReportTemplates />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Create Report | umami',
|
||||
};
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useContext, useRef } from 'react';
|
||||
import { useApi, useMessages } from 'components/hooks';
|
||||
import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics';
|
||||
import { ReportContext } from 'components/pages/reports/Report';
|
||||
import Empty from 'components/common/Empty';
|
||||
import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants';
|
||||
import Icons from 'components/icons';
|
||||
import FieldAddForm from '../FieldAddForm';
|
||||
import BaseParameters from '../BaseParameters';
|
||||
import ParameterList from '../ParameterList';
|
||||
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 styles from './EventDataParameters.module.css';
|
||||
|
||||
function useFields(websiteId, startDate, endDate) {
|
||||
@@ -74,7 +74,7 @@ export function EventDataParameters() {
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
<Popup position="bottom" alignment="start">
|
||||
{(close, element) => {
|
||||
{close => {
|
||||
return (
|
||||
<FieldAddForm
|
||||
fields={data.map(({ eventKey, eventDataType }) => ({
|
||||
@@ -82,7 +82,6 @@ export function EventDataParameters() {
|
||||
type: DATA_TYPES[eventDataType],
|
||||
}))}
|
||||
group={group}
|
||||
element={element}
|
||||
onAdd={handleAdd}
|
||||
onClose={close}
|
||||
/>
|
||||
@@ -1,7 +1,7 @@
|
||||
import Report from '../Report';
|
||||
import ReportHeader from '../ReportHeader';
|
||||
import ReportMenu from '../ReportMenu';
|
||||
import ReportBody from '../ReportBody';
|
||||
import Report from '../[id]/Report';
|
||||
import ReportHeader from '../[id]/ReportHeader';
|
||||
import ReportMenu from '../[id]/ReportMenu';
|
||||
import ReportBody from '../[id]/ReportBody';
|
||||
import EventDataParameters from './EventDataParameters';
|
||||
import EventDataTable from './EventDataTable';
|
||||
import Nodes from 'assets/nodes.svg';
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useContext } from 'react';
|
||||
import { GridTable, GridColumn } from 'react-basics';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { ReportContext } from '../Report';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
|
||||
export function EventDataTable() {
|
||||
const { report } = useContext(ReportContext);
|
||||
@@ -5,7 +5,7 @@ import useTheme from 'components/hooks/useTheme';
|
||||
import BarChart from 'components/metrics/BarChart';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
import styles from './FunnelChart.module.css';
|
||||
import { ReportContext } from '../Report';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
|
||||
export function FunnelChart({ className, loading }) {
|
||||
const { report } = useContext(ReportContext);
|
||||
@@ -13,10 +13,10 @@ import {
|
||||
} from 'react-basics';
|
||||
import Icons from 'components/icons';
|
||||
import UrlAddForm from './UrlAddForm';
|
||||
import { ReportContext } from 'components/pages/reports/Report';
|
||||
import BaseParameters from '../BaseParameters';
|
||||
import ParameterList from '../ParameterList';
|
||||
import PopupForm from '../PopupForm';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import BaseParameters from '../[id]/BaseParameters';
|
||||
import ParameterList from '../[id]/ParameterList';
|
||||
import PopupForm from '../[id]/PopupForm';
|
||||
|
||||
export function FunnelParameters() {
|
||||
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
|
||||
@@ -52,14 +52,10 @@ export function FunnelParameters() {
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
<Popup position="bottom" alignment="start">
|
||||
{(close, element) => {
|
||||
return (
|
||||
<PopupForm element={element} onClose={close}>
|
||||
<UrlAddForm onAdd={handleAddUrl} />
|
||||
</PopupForm>
|
||||
);
|
||||
}}
|
||||
<Popup position="right" alignment="start">
|
||||
<PopupForm>
|
||||
<UrlAddForm onAdd={handleAddUrl} />
|
||||
</PopupForm>
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
);
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
import FunnelChart from './FunnelChart';
|
||||
import FunnelTable from './FunnelTable';
|
||||
import FunnelParameters from './FunnelParameters';
|
||||
import Report from '../Report';
|
||||
import ReportHeader from '../ReportHeader';
|
||||
import ReportMenu from '../ReportMenu';
|
||||
import ReportBody from '../ReportBody';
|
||||
import Report from '../[id]/Report';
|
||||
import ReportHeader from '../[id]/ReportHeader';
|
||||
import ReportMenu from '../[id]/ReportMenu';
|
||||
import ReportBody from '../[id]/ReportBody';
|
||||
import Funnel from 'assets/funnel.svg';
|
||||
import { REPORT_TYPES } from 'lib/constants';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useContext } from 'react';
|
||||
import ListTable from 'components/metrics/ListTable';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { ReportContext } from '../Report';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
|
||||
export function FunnelTable() {
|
||||
const { report } = useContext(ReportContext);
|
||||
10
src/app/(main)/reports/funnel/page.tsx
Normal file
10
src/app/(main)/reports/funnel/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import FunnelReport from './FunnelReport';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function FunnelReportPage() {
|
||||
return <FunnelReport reportId={null} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Funnel Report | umami',
|
||||
};
|
||||
@@ -10,14 +10,14 @@ import {
|
||||
Popup,
|
||||
TooltipPopup,
|
||||
} from 'react-basics';
|
||||
import { ReportContext } from 'components/pages/reports/Report';
|
||||
import Icons from 'components/icons';
|
||||
import BaseParameters from '../BaseParameters';
|
||||
import ParameterList from '../ParameterList';
|
||||
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 styles from './InsightsParameters.module.css';
|
||||
import PopupForm from '../PopupForm';
|
||||
import FilterSelectForm from '../FilterSelectForm';
|
||||
import FieldSelectForm from '../FieldSelectForm';
|
||||
|
||||
export function InsightsParameters() {
|
||||
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
|
||||
@@ -81,26 +81,22 @@ export function InsightsParameters() {
|
||||
</Icon>
|
||||
</TooltipPopup>
|
||||
<Popup position="bottom" alignment="start" className={styles.popup}>
|
||||
{close => {
|
||||
return (
|
||||
<PopupForm onClose={close}>
|
||||
{id === 'fields' && (
|
||||
<FieldSelectForm
|
||||
items={fieldOptions}
|
||||
onSelect={handleAdd.bind(null, id)}
|
||||
showType={false}
|
||||
/>
|
||||
)}
|
||||
{id === 'filters' && (
|
||||
<FilterSelectForm
|
||||
websiteId={websiteId}
|
||||
items={fieldOptions}
|
||||
onSelect={handleAdd.bind(null, id)}
|
||||
/>
|
||||
)}
|
||||
</PopupForm>
|
||||
);
|
||||
}}
|
||||
<PopupForm>
|
||||
{id === 'fields' && (
|
||||
<FieldSelectForm
|
||||
items={fieldOptions}
|
||||
onSelect={handleAdd.bind(null, id)}
|
||||
showType={false}
|
||||
/>
|
||||
)}
|
||||
{id === 'filters' && (
|
||||
<FilterSelectForm
|
||||
websiteId={websiteId}
|
||||
items={fieldOptions}
|
||||
onSelect={handleAdd.bind(null, id)}
|
||||
/>
|
||||
)}
|
||||
</PopupForm>
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
);
|
||||
@@ -1,7 +1,8 @@
|
||||
import Report from '../Report';
|
||||
import ReportHeader from '../ReportHeader';
|
||||
import ReportMenu from '../ReportMenu';
|
||||
import ReportBody from '../ReportBody';
|
||||
'use client';
|
||||
import Report from '../[id]/Report';
|
||||
import ReportHeader from '../[id]/ReportHeader';
|
||||
import ReportMenu from '../[id]/ReportMenu';
|
||||
import ReportBody from '../[id]/ReportBody';
|
||||
import InsightsParameters from './InsightsParameters';
|
||||
import InsightsTable from './InsightsTable';
|
||||
import Lightbulb from 'assets/lightbulb.svg';
|
||||
@@ -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 '../Report';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||
|
||||
export function InsightsTable() {
|
||||
10
src/app/(main)/reports/insights/page.tsx
Normal file
10
src/app/(main)/reports/insights/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import InsightsReport from './InsightsReport';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function InsightsReportPage() {
|
||||
return <InsightsReport reportId={null} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Insights Report | umami',
|
||||
};
|
||||
14
src/app/(main)/reports/page.tsx
Normal file
14
src/app/(main)/reports/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import ReportsHeader from './ReportsHeader';
|
||||
import ReportsDataTable from './ReportsDataTable';
|
||||
|
||||
export default function ReportsPage() {
|
||||
return (
|
||||
<>
|
||||
<ReportsHeader />
|
||||
<ReportsDataTable />
|
||||
</>
|
||||
);
|
||||
}
|
||||
export const metadata = {
|
||||
title: 'Reports | umami',
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useContext, useRef } from 'react';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { Form, FormButtons, FormRow, SubmitButton } from 'react-basics';
|
||||
import { ReportContext } from 'components/pages/reports/Report';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import { MonthSelect } from 'components/input/MonthSelect';
|
||||
import BaseParameters from '../BaseParameters';
|
||||
import BaseParameters from '../[id]/BaseParameters';
|
||||
import { parseDateRange } from 'lib/date';
|
||||
|
||||
export function RetentionParameters() {
|
||||
@@ -19,6 +19,7 @@ export function RetentionParameters() {
|
||||
const handleSubmit = (data, e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!queryDisabled) {
|
||||
runReport(data);
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
import RetentionTable from './RetentionTable';
|
||||
import RetentionParameters from './RetentionParameters';
|
||||
import Report from '../Report';
|
||||
import ReportHeader from '../ReportHeader';
|
||||
import ReportMenu from '../ReportMenu';
|
||||
import ReportBody from '../ReportBody';
|
||||
import Report from '../[id]/Report';
|
||||
import ReportHeader from '../[id]/ReportHeader';
|
||||
import ReportMenu from '../[id]/ReportMenu';
|
||||
import ReportBody from '../[id]/ReportBody';
|
||||
import Magnet from 'assets/magnet.svg';
|
||||
import { REPORT_TYPES } from 'lib/constants';
|
||||
import { parseDateRange } from 'lib/date';
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useContext } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ReportContext } from '../Report';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { useLocale } from 'components/hooks';
|
||||
import { useMessages, useLocale } from 'components/hooks';
|
||||
import { formatDate } from 'lib/date';
|
||||
import styles from './RetentionTable.module.css';
|
||||
|
||||
export function RetentionTable() {
|
||||
const DAYS = [1, 2, 3, 4, 5, 6, 7, 14, 21, 28];
|
||||
|
||||
export function RetentionTable({ days = DAYS }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { locale } = useLocale();
|
||||
const { report } = useContext(ReportContext);
|
||||
@@ -17,8 +18,6 @@ export function RetentionTable() {
|
||||
return <EmptyPlaceholder />;
|
||||
}
|
||||
|
||||
const days = [1, 2, 3, 4, 5, 6, 7, 14, 21, 28];
|
||||
|
||||
const rows = data.reduce((arr, row) => {
|
||||
const { date, visitors, day } = row;
|
||||
if (day === 0) {
|
||||
9
src/app/(main)/reports/retention/page.js
Normal file
9
src/app/(main)/reports/retention/page.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import RetentionReport from './RetentionReport';
|
||||
|
||||
export default function RetentionReportPage() {
|
||||
return <RetentionReport reportId={null} />;
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: 'Create Report | umami',
|
||||
};
|
||||
31
src/app/(main)/settings/layout.module.css
Normal file
31
src/app/(main)/settings/layout.module.css
Normal file
@@ -0,0 +1,31 @@
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
width: 240px;
|
||||
padding-top: 34px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 50vh;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Row, Column } from 'react-basics';
|
||||
import { useRouter } from 'next/router';
|
||||
import SideNav from './SideNav';
|
||||
'use client';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import styles from './SettingsLayout.module.css';
|
||||
import SideNav from 'components/layout/SideNav';
|
||||
import styles from './layout.module.css';
|
||||
|
||||
export function SettingsLayout({ children }) {
|
||||
export default function SettingsLayout({ children }) {
|
||||
const { user } = useUser();
|
||||
const { pathname } = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const cloudMode = Boolean(process.env.cloudMode);
|
||||
const cloudMode = !!process.env.cloudMode;
|
||||
|
||||
const items = [
|
||||
{ key: 'websites', label: formatMessage(labels.websites), url: '/settings/websites' },
|
||||
@@ -20,18 +20,18 @@ export function SettingsLayout({ children }) {
|
||||
|
||||
const getKey = () => items.find(({ url }) => pathname === url)?.key;
|
||||
|
||||
if (cloudMode && pathname != '/settings/profile') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<div className={styles.layout}>
|
||||
{!cloudMode && (
|
||||
<Column className={styles.menu} defaultSize={12} md={4} lg={3} xl={2}>
|
||||
<div className={styles.menu}>
|
||||
<SideNav items={items} shallow={true} selectedKey={getKey()} />
|
||||
</Column>
|
||||
</div>
|
||||
)}
|
||||
<Column className={styles.content} defaultSize={12} md={8} lg={9} xl={10}>
|
||||
{children}
|
||||
</Column>
|
||||
</Row>
|
||||
<div className={styles.content}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsLayout;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button, Icon, Text, useToasts, ModalTrigger, Modal } from 'react-basics';
|
||||
import PasswordEditForm from 'components/pages/settings/profile/PasswordEditForm';
|
||||
import PasswordEditForm from 'app/(main)/settings/profile/PasswordEditForm';
|
||||
import Icons from 'components/icons';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
|
||||
11
src/app/(main)/settings/profile/ProfileHeader.js
Normal file
11
src/app/(main)/settings/profile/ProfileHeader.js
Normal file
@@ -0,0 +1,11 @@
|
||||
'use client';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import { useMessages } from 'components/hooks';
|
||||
|
||||
export function ProfileHeader() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return <PageHeader title={formatMessage(labels.profile)}></PageHeader>;
|
||||
}
|
||||
|
||||
export default ProfileHeader;
|
||||
@@ -1,14 +1,15 @@
|
||||
'use client';
|
||||
import { Form, FormRow } from 'react-basics';
|
||||
import TimezoneSetting from 'components/pages/settings/profile/TimezoneSetting';
|
||||
import DateRangeSetting from 'components/pages/settings/profile/DateRangeSetting';
|
||||
import LanguageSetting from 'components/pages/settings/profile/LanguageSetting';
|
||||
import ThemeSetting from 'components/pages/settings/profile/ThemeSetting';
|
||||
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 PasswordChangeButton from './PasswordChangeButton';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { ROLES } from 'lib/constants';
|
||||
|
||||
export function ProfileDetails() {
|
||||
export function ProfileSettings() {
|
||||
const { user } = useUser();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const cloudMode = Boolean(process.env.cloudMode);
|
||||
@@ -58,4 +59,4 @@ export function ProfileDetails() {
|
||||
);
|
||||
}
|
||||
|
||||
export default ProfileDetails;
|
||||
export default ProfileSettings;
|
||||
16
src/app/(main)/settings/profile/page.tsx
Normal file
16
src/app/(main)/settings/profile/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import ProfileHeader from './ProfileHeader';
|
||||
import ProfileSettings from './ProfileSettings';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function () {
|
||||
return (
|
||||
<>
|
||||
<ProfileHeader />
|
||||
<ProfileSettings />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Profile Settings | umami',
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Button,
|
||||
SubmitButton,
|
||||
} from 'react-basics';
|
||||
import { setValue } from 'store/cache';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
|
||||
@@ -20,8 +21,9 @@ export function TeamAddForm({ onSave, onClose }) {
|
||||
const handleSubmit = async data => {
|
||||
mutate(data, {
|
||||
onSuccess: async () => {
|
||||
onSave();
|
||||
onClose();
|
||||
setValue('teams', Date.now());
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
});
|
||||
};
|
||||
25
src/app/(main)/settings/teams/TeamDeleteButton.js
Normal file
25
src/app/(main)/settings/teams/TeamDeleteButton.js
Normal file
@@ -0,0 +1,25 @@
|
||||
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 }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<ModalTrigger>
|
||||
<Button>
|
||||
<Icon>
|
||||
<Icons.Trash />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.delete)}</Text>
|
||||
</Button>
|
||||
<Modal title={formatMessage(labels.deleteTeam)}>
|
||||
{close => (
|
||||
<TeamDeleteForm teamId={teamId} teamName={teamName} onSave={onDelete} onClose={close} />
|
||||
)}
|
||||
</Modal>
|
||||
</ModalTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamDeleteButton;
|
||||
@@ -1,6 +1,7 @@
|
||||
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 }) {
|
||||
const { formatMessage, labels, messages, FormattedMessage } = useMessages();
|
||||
@@ -10,8 +11,9 @@ export function TeamDeleteForm({ teamId, teamName, onSave, onClose }) {
|
||||
const handleSubmit = async data => {
|
||||
mutate(data, {
|
||||
onSuccess: async () => {
|
||||
onSave();
|
||||
onClose();
|
||||
setValue('teams', Date.now());
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from 'react-basics';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { setValue } from 'store/cache';
|
||||
|
||||
export function TeamJoinForm({ onSave, onClose }) {
|
||||
const { formatMessage, labels, getMessage } = useMessages();
|
||||
@@ -20,8 +21,9 @@ export function TeamJoinForm({ onSave, onClose }) {
|
||||
const handleSubmit = async data => {
|
||||
mutate(data, {
|
||||
onSuccess: async () => {
|
||||
onSave();
|
||||
onClose();
|
||||
setValue('teams:members', Date.now());
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
});
|
||||
};
|
||||
35
src/app/(main)/settings/teams/TeamLeaveButton.js
Normal file
35
src/app/(main)/settings/teams/TeamLeaveButton.js
Normal file
@@ -0,0 +1,35 @@
|
||||
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 }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { dir } = useLocale();
|
||||
const { user } = useUser();
|
||||
|
||||
return (
|
||||
<ModalTrigger>
|
||||
<Button>
|
||||
<Icon rotate={dir === 'rtl' ? 180 : 0}>
|
||||
<Icons.Logout />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.leave)}</Text>
|
||||
</Button>
|
||||
<Modal title={formatMessage(labels.leaveTeam)}>
|
||||
{close => (
|
||||
<TeamDeleteForm
|
||||
teamId={teamId}
|
||||
userId={user.id}
|
||||
teamName={teamName}
|
||||
onSave={onLeave}
|
||||
onClose={close}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</ModalTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamLeaveButton;
|
||||
@@ -1,6 +1,7 @@
|
||||
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 TeamLeaveForm({ teamId, userId, teamName, onSave, onClose }) {
|
||||
const { formatMessage, labels, messages, FormattedMessage } = useMessages();
|
||||
@@ -12,6 +13,7 @@ export function TeamLeaveForm({ teamId, userId, teamName, onSave, onClose }) {
|
||||
{},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
setValue('team:members', Date.now());
|
||||
onSave();
|
||||
onClose();
|
||||
},
|
||||
24
src/app/(main)/settings/teams/TeamsAddButton.js
Normal file
24
src/app/(main)/settings/teams/TeamsAddButton.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Button, Icon, Modal, ModalTrigger, Text } from 'react-basics';
|
||||
import Icons from 'components/icons';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import TeamAddForm from './TeamAddForm';
|
||||
|
||||
export function TeamsAddButton({ onAdd }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<ModalTrigger>
|
||||
<Button variant="primary">
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.createTeam)}</Text>
|
||||
</Button>
|
||||
<Modal title={formatMessage(labels.createTeam)}>
|
||||
{close => <TeamAddForm onSave={onAdd} onClose={close} />}
|
||||
</Modal>
|
||||
</ModalTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamsAddButton;
|
||||
26
src/app/(main)/settings/teams/TeamsDataTable.js
Normal file
26
src/app/(main)/settings/teams/TeamsDataTable.js
Normal file
@@ -0,0 +1,26 @@
|
||||
'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';
|
||||
|
||||
export function TeamsDataTable() {
|
||||
const { get } = useApi();
|
||||
const modified = useCache(state => state?.teams);
|
||||
const queryResult = useFilterQuery(['teams', { modified }], params => {
|
||||
return get(`/teams`, {
|
||||
...params,
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<DataTable queryResult={queryResult}>
|
||||
{({ data }) => {
|
||||
return <TeamsTable data={data} />;
|
||||
}}
|
||||
</DataTable>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamsDataTable;
|
||||
24
src/app/(main)/settings/teams/TeamsHeader.js
Normal file
24
src/app/(main)/settings/teams/TeamsHeader.js
Normal file
@@ -0,0 +1,24 @@
|
||||
'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 TeamsJoinButton from './TeamsJoinButton';
|
||||
import TeamsAddButton from './TeamsAddButton';
|
||||
|
||||
export function TeamsHeader() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useUser();
|
||||
|
||||
return (
|
||||
<PageHeader title={formatMessage(labels.teams)}>
|
||||
<Flexbox gap={10}>
|
||||
<TeamsJoinButton />
|
||||
{user.role !== ROLES.viewOnly && <TeamsAddButton />}
|
||||
</Flexbox>
|
||||
</PageHeader>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamsHeader;
|
||||
29
src/app/(main)/settings/teams/TeamsJoinButton.js
Normal file
29
src/app/(main)/settings/teams/TeamsJoinButton.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Button, Icon, Modal, ModalTrigger, Text, useToasts } from 'react-basics';
|
||||
import Icons from 'components/icons';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import TeamJoinForm from './TeamJoinForm';
|
||||
|
||||
export function TeamsJoinButton() {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { showToast } = useToasts();
|
||||
|
||||
const handleJoin = () => {
|
||||
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalTrigger>
|
||||
<Button variant="secondary">
|
||||
<Icon>
|
||||
<Icons.AddUser />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.joinTeam)}</Text>
|
||||
</Button>
|
||||
<Modal title={formatMessage(labels.joinTeam)}>
|
||||
{close => <TeamJoinForm onSave={handleJoin} onClose={close} />}
|
||||
</Modal>
|
||||
</ModalTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamsJoinButton;
|
||||
47
src/app/(main)/settings/teams/TeamsTable.js
Normal file
47
src/app/(main)/settings/teams/TeamsTable.js
Normal file
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
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';
|
||||
|
||||
export function TeamsTable({ data = [] }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useUser();
|
||||
const breakpoint = useBreakpoint();
|
||||
|
||||
return (
|
||||
<GridTable data={data} cardMode={['xs', 'sm', 'md'].includes(breakpoint)}>
|
||||
<GridColumn name="name" label={formatMessage(labels.name)} />
|
||||
<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>
|
||||
</GridTable>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamsTable;
|
||||
@@ -1,6 +1,7 @@
|
||||
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 }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
@@ -12,7 +13,8 @@ export function TeamMemberRemoveButton({ teamId, userId, disabled, onSave }) {
|
||||
{},
|
||||
{
|
||||
onSuccess: () => {
|
||||
onSave();
|
||||
setValue('team:members', Date.now());
|
||||
onSave?.();
|
||||
},
|
||||
},
|
||||
);
|
||||
29
src/app/(main)/settings/teams/[id]/TeamMembers.js
Normal file
29
src/app/(main)/settings/teams/[id]/TeamMembers.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import TeamMembersTable from './TeamMembersTable';
|
||||
import useFilterQuery from 'components/hooks/useFilterQuery';
|
||||
import DataTable from 'components/common/DataTable';
|
||||
import useCache from 'store/cache';
|
||||
|
||||
export function TeamMembers({ teamId, readOnly }) {
|
||||
const { get } = useApi();
|
||||
const modified = useCache(state => state?.['team:members']);
|
||||
const queryResult = useFilterQuery(
|
||||
['team:members', { teamId, modified }],
|
||||
params => {
|
||||
return get(`/teams/${teamId}/users`, {
|
||||
...params,
|
||||
});
|
||||
},
|
||||
{ enabled: !!teamId },
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable queryResult={queryResult}>
|
||||
{({ data }) => <TeamMembersTable data={data} teamId={teamId} readOnly={readOnly} />}
|
||||
</DataTable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamMembers;
|
||||
36
src/app/(main)/settings/teams/[id]/TeamMembersTable.js
Normal file
36
src/app/(main)/settings/teams/[id]/TeamMembersTable.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { GridColumn, GridTable, useBreakpoint } from 'react-basics';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import TeamMemberRemoveButton from './TeamMemberRemoveButton';
|
||||
|
||||
export function TeamMembersTable({ data = [], teamId, readOnly }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useUser();
|
||||
const breakpoint = useBreakpoint();
|
||||
|
||||
const roles = {
|
||||
[ROLES.teamOwner]: formatMessage(labels.teamOwner),
|
||||
[ROLES.teamMember]: formatMessage(labels.teamMember),
|
||||
};
|
||||
|
||||
return (
|
||||
<GridTable data={data} cardMode={['xs', 'sm', 'md'].includes(breakpoint)}>
|
||||
<GridColumn name="username" label={formatMessage(labels.username)} />
|
||||
<GridColumn name="role" label={formatMessage(labels.role)}>
|
||||
{row => roles[row?.teamUser?.[0]?.role]}
|
||||
</GridColumn>
|
||||
<GridColumn name="action" label=" " alignment="end">
|
||||
{row => {
|
||||
return (
|
||||
!readOnly &&
|
||||
row?.teamUser?.[0]?.role !== ROLES.teamOwner &&
|
||||
user?.id !== row?.id && <TeamMemberRemoveButton teamId={teamId} userId={row.id} />
|
||||
);
|
||||
}}
|
||||
</GridColumn>
|
||||
</GridTable>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamMembersTable;
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Item, Tabs, useToasts } from 'react-basics';
|
||||
import Page from 'components/layout/Page';
|
||||
import { Item, Loading, Tabs, useToasts, Flexbox } from 'react-basics';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
@@ -41,8 +41,12 @@ export function TeamSettings({ teamId }) {
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
if (isLoading || !values) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page loading={isLoading || !values}>
|
||||
<Flexbox direction="column">
|
||||
<PageHeader title={values?.name} />
|
||||
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30 }}>
|
||||
<Item key="details">{formatMessage(labels.details)}</Item>
|
||||
@@ -54,7 +58,7 @@ export function TeamSettings({ teamId }) {
|
||||
)}
|
||||
{tab === 'members' && <TeamMembers teamId={teamId} readOnly={!canEdit} />}
|
||||
{tab === 'websites' && <TeamWebsites teamId={teamId} readOnly={!canEdit} />}
|
||||
</Page>
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
||||
64
src/app/(main)/settings/teams/[id]/TeamWebsiteAddForm.js
Normal file
64
src/app/(main)/settings/teams/[id]/TeamWebsiteAddForm.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import { useState } from 'react';
|
||||
import { Button, Form, FormButtons, GridColumn, Loading, SubmitButton, Toggle } from 'react-basics';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import WebsitesDataTable from '../../websites/WebsitesDataTable';
|
||||
import Empty from 'components/common/Empty';
|
||||
import { setValue } from 'store/cache';
|
||||
|
||||
export function TeamWebsiteAddForm({ teamId, onSave, onClose }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { get, post, useQuery, useMutation } = useApi();
|
||||
const { mutate, error } = useMutation(data => post(`/teams/${teamId}/websites`, data));
|
||||
const { data: websites, isLoading } = useQuery(['websites'], () => get('/websites'));
|
||||
const [selected, setSelected] = useState([]);
|
||||
const hasData = websites && websites.data.length > 0;
|
||||
|
||||
const handleSubmit = () => {
|
||||
mutate(
|
||||
{ websiteIds: selected },
|
||||
{
|
||||
onSuccess: async () => {
|
||||
setValue('team:websites', Date.now());
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handleSelect = id => {
|
||||
setSelected(state => (state.includes(id) ? state.filter(n => n !== id) : state.concat(id)));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && !hasData && <Loading icon="dots" position="center" />}
|
||||
{!isLoading && !hasData && <Empty />}
|
||||
{hasData && (
|
||||
<Form onSubmit={handleSubmit} error={error}>
|
||||
<WebsitesDataTable showHeader={false} showActions={false}>
|
||||
<GridColumn name="select" label={formatMessage(labels.selectWebsite)} alignment="end">
|
||||
{row => (
|
||||
<Toggle
|
||||
key={row.id}
|
||||
value={row.id}
|
||||
checked={selected?.includes(row.id)}
|
||||
onChange={handleSelect.bind(null, row.id)}
|
||||
/>
|
||||
)}
|
||||
</GridColumn>
|
||||
</WebsitesDataTable>
|
||||
<FormButtons flex>
|
||||
<SubmitButton disabled={selected?.length === 0}>
|
||||
{formatMessage(labels.addWebsite)}
|
||||
</SubmitButton>
|
||||
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamWebsiteAddForm;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user