mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-16 02:05:04 +01:00
Refactored to use app folder.
This commit is contained in:
parent
40cfcd41e9
commit
9a52cdd2e1
@ -6,7 +6,7 @@ const pkg = require('./package.json');
|
||||
const contentSecurityPolicy = `
|
||||
default-src 'self';
|
||||
img-src *;
|
||||
script-src 'self' 'unsafe-eval';
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
connect-src 'self' api.umami.is;
|
||||
frame-ancestors 'self' ${process.env.ALLOWED_FRAME_URLS};
|
||||
@ -74,16 +74,20 @@ if (process.env.CLOUD_MODE && process.env.CLOUD_URL && process.env.DISABLE_LOGIN
|
||||
});
|
||||
}
|
||||
|
||||
const basePath = process.env.BASE_PATH;
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const config = {
|
||||
env: {
|
||||
cloudMode: process.env.CLOUD_MODE,
|
||||
basePath: basePath || '',
|
||||
cloudMode: !!process.env.CLOUD_MODE,
|
||||
cloudUrl: process.env.CLOUD_URL,
|
||||
configUrl: '/config',
|
||||
currentVersion: pkg.version,
|
||||
defaultLocale: process.env.DEFAULT_LOCALE,
|
||||
isProduction: process.env.NODE_ENV === 'production',
|
||||
},
|
||||
basePath: process.env.BASE_PATH,
|
||||
basePath,
|
||||
output: 'standalone',
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
@ -92,11 +96,23 @@ const config = {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
issuer: /\.{js|jsx|ts|tsx}$/,
|
||||
use: ['@svgr/webpack'],
|
||||
});
|
||||
const fileLoaderRule = config.module.rules.find(rule => rule.test?.test?.('.svg'));
|
||||
|
||||
config.module.rules.push(
|
||||
{
|
||||
...fileLoaderRule,
|
||||
test: /\.svg$/i,
|
||||
resourceQuery: /url/,
|
||||
},
|
||||
{
|
||||
test: /\.svg$/i,
|
||||
issuer: fileLoaderRule.issuer,
|
||||
resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] },
|
||||
use: ['@svgr/webpack'],
|
||||
},
|
||||
);
|
||||
|
||||
fileLoaderRule.exclude = /\.svg$/i;
|
||||
|
||||
config.resolve.alias['public'] = path.resolve('./public');
|
||||
|
||||
|
16
package.json
16
package.json
@ -64,7 +64,7 @@
|
||||
"@fontsource/inter": "^4.5.15",
|
||||
"@prisma/client": "5.3.1",
|
||||
"@tanstack/react-query": "^4.33.0",
|
||||
"@umami/prisma-client": "^0.2.0",
|
||||
"@umami/prisma-client": "^0.3.0",
|
||||
"@umami/redis-client": "^0.15.0",
|
||||
"chalk": "^4.1.1",
|
||||
"chart.js": "^4.2.1",
|
||||
@ -91,7 +91,7 @@
|
||||
"kafkajs": "^2.1.0",
|
||||
"maxmind": "^4.3.6",
|
||||
"moment-timezone": "^0.5.35",
|
||||
"next": "13.5.2",
|
||||
"next": "13.5.3",
|
||||
"next-basics": "^0.36.0",
|
||||
"node-fetch": "^3.2.8",
|
||||
"npm-run-all": "^4.1.5",
|
||||
@ -100,7 +100,7 @@
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^4.0.4",
|
||||
"react-intl": "^5.24.7",
|
||||
"react-intl": "^6.4.7",
|
||||
"react-simple-maps": "^2.3.0",
|
||||
"react-spring": "^9.4.4",
|
||||
"react-use-measure": "^2.0.4",
|
||||
@ -123,12 +123,12 @@
|
||||
"@rollup/plugin-node-resolve": "^15.2.0",
|
||||
"@rollup/plugin-replace": "^5.0.2",
|
||||
"@svgr/rollup": "^8.1.0",
|
||||
"@svgr/webpack": "^6.2.1",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/react": "^18.0.25",
|
||||
"@types/react-dom": "^18.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^5.50.0",
|
||||
"@typescript-eslint/parser": "^5.50.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||
"@typescript-eslint/parser": "^6.7.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.17.17",
|
||||
"eslint": "^8.33.0",
|
||||
@ -138,8 +138,8 @@
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"extract-react-intl-messages": "^4.1.1",
|
||||
"husky": "^7.0.0",
|
||||
"lint-staged": "^11.0.0",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^14.0.1",
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-flexbugs-fixes": "^5.0.2",
|
||||
"postcss-import": "^15.1.0",
|
||||
|
58
src/app/(app)/NavBar.js
Normal file
58
src/app/(app)/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/(app)/Shell.tsx
Normal file
27
src/app/(app)/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;
|
@ -2,11 +2,11 @@ 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 '../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 styles from './TestConsole.module.css';
|
15
src/app/(app)/console/[[...id]]/page.tsx
Normal file
15
src/app/(app)/console/[[...id]]/page.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import TestConsole from '../TestConsole';
|
||||
|
||||
async function getEnabled() {
|
||||
return !!process.env.ENABLE_TEST_CONSOLE;
|
||||
}
|
||||
|
||||
export default async function ConsolePage() {
|
||||
const enabled = await getEnabled();
|
||||
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <TestConsole />;
|
||||
}
|
@ -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 '../websites/[id]/WebsiteChartList';
|
||||
import DashboardSettingsButton from 'app/(app)/dashboard/DashboardSettingsButton';
|
||||
import DashboardEdit from 'app/(app)/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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
10
src/app/(app)/dashboard/page.tsx
Normal file
10
src/app/(app)/dashboard/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import Dashboard from 'app/(app)/dashboard/Dashboard';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function () {
|
||||
return <Dashboard />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Dashboard | umami',
|
||||
};
|
20
src/app/(app)/layout.tsx
Normal file
20
src/app/(app)/layout.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
'use client';
|
||||
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>
|
||||
);
|
||||
}
|
@ -1,20 +1,18 @@
|
||||
'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);
|
||||
|
||||
//console.log({ report });
|
||||
|
||||
return (
|
||||
<ReportContext.Provider value={{ ...report }}>
|
||||
<Page {...props} className={styles.container}>
|
||||
<div {...props} className={styles.container}>
|
||||
{children}
|
||||
</Page>
|
||||
</div>
|
||||
</ReportContext.Provider>
|
||||
);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import styles from './reports.module.css';
|
||||
import styles from './Report.module.css';
|
||||
|
||||
export function ReportBody({ children }) {
|
||||
return <div className={styles.body}>{children}</div>;
|
@ -1,11 +1,11 @@
|
||||
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 reportStyles from './Report.module.css';
|
||||
|
||||
export function ReportHeader({ icon }) {
|
||||
const { report, updateReport } = useContext(ReportContext);
|
@ -1,4 +1,4 @@
|
||||
import styles from './reports.module.css';
|
||||
import styles from './Report.module.css';
|
||||
|
||||
export function ReportMenu({ children }) {
|
||||
return <div className={styles.menu}>{children}</div>;
|
24
src/app/(app)/reports/ReportsHeader.js
Normal file
24
src/app/(app)/reports/ReportsHeader.js
Normal file
@ -0,0 +1,24 @@
|
||||
'use client';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import Link from 'next/link';
|
||||
import { Button, Icon, Icons, Text } from 'react-basics';
|
||||
import { useMessages } from 'components/hooks';
|
||||
|
||||
export function ReportsHeader() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<PageHeader title={formatMessage(labels.reports)}>
|
||||
<Link href="/reports/create">
|
||||
<Button variant="primary">
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.createReport)}</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
</PageHeader>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReportsHeader;
|
37
src/app/(app)/reports/ReportsList.js
Normal file
37
src/app/(app)/reports/ReportsList.js
Normal file
@ -0,0 +1,37 @@
|
||||
'use client';
|
||||
import { useApi } from 'components/hooks';
|
||||
import ReportsTable from './ReportsTable';
|
||||
import useFilterQuery from 'components/hooks/useFilterQuery';
|
||||
import DataTable from 'components/common/DataTable';
|
||||
|
||||
function useReports() {
|
||||
const { get, del, useMutation } = useApi();
|
||||
const { mutate } = useMutation(reportId => del(`/reports/${reportId}`));
|
||||
const reports = useFilterQuery(['reports'], params => get(`/reports`, params));
|
||||
|
||||
const deleteReport = id => {
|
||||
mutate(id, {
|
||||
onSuccess: () => {
|
||||
reports.refetch();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return { reports, deleteReport };
|
||||
}
|
||||
|
||||
export default function ReportsList() {
|
||||
const { reports, deleteReport } = useReports();
|
||||
|
||||
const handleDelete = async (id, callback) => {
|
||||
await deleteReport(id);
|
||||
await reports.refetch();
|
||||
callback?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<DataTable {...reports.getProps()}>
|
||||
{({ data }) => <ReportsTable data={data} showDomain={true} onDelete={handleDelete} />}
|
||||
</DataTable>
|
||||
);
|
||||
}
|
26
src/app/(app)/reports/[id]/ReportDetails.js
Normal file
26
src/app/(app)/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} />;
|
||||
}
|
14
src/app/(app)/reports/[id]/page.tsx
Normal file
14
src/app/(app)/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/(app)/reports/create/page.tsx
Normal file
10
src/app/(app)/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,7 +1,7 @@
|
||||
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 { ReportContext } from '../Report';
|
||||
import Empty from 'components/common/Empty';
|
||||
import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants';
|
||||
import Icons from 'components/icons';
|
@ -13,7 +13,7 @@ import {
|
||||
} from 'react-basics';
|
||||
import Icons from 'components/icons';
|
||||
import UrlAddForm from './UrlAddForm';
|
||||
import { ReportContext } from 'components/pages/reports/Report';
|
||||
import { ReportContext } from '../Report';
|
||||
import BaseParameters from '../BaseParameters';
|
||||
import ParameterList from '../ParameterList';
|
||||
import PopupForm from '../PopupForm';
|
@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import FunnelChart from './FunnelChart';
|
||||
import FunnelTable from './FunnelTable';
|
||||
import FunnelParameters from './FunnelParameters';
|
10
src/app/(app)/reports/funnel/page.tsx
Normal file
10
src/app/(app)/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,7 +10,7 @@ import {
|
||||
Popup,
|
||||
TooltipPopup,
|
||||
} from 'react-basics';
|
||||
import { ReportContext } from 'components/pages/reports/Report';
|
||||
import { ReportContext } from '../Report';
|
||||
import Icons from 'components/icons';
|
||||
import BaseParameters from '../BaseParameters';
|
||||
import ParameterList from '../ParameterList';
|
@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import Report from '../Report';
|
||||
import ReportHeader from '../ReportHeader';
|
||||
import ReportMenu from '../ReportMenu';
|
10
src/app/(app)/reports/insights/page.tsx
Normal file
10
src/app/(app)/reports/insights/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import InsightsReport from './InsightsReport';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function () {
|
||||
return <InsightsReport reportId={null} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Insights Report | umami',
|
||||
};
|
14
src/app/(app)/reports/page.tsx
Normal file
14
src/app/(app)/reports/page.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import ReportsHeader from './ReportsHeader';
|
||||
import ReportsList from './ReportsList';
|
||||
|
||||
export default function ReportsPage() {
|
||||
return (
|
||||
<>
|
||||
<ReportsHeader />
|
||||
<ReportsList />
|
||||
</>
|
||||
);
|
||||
}
|
||||
export const metadata = {
|
||||
title: 'Reports | umami',
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
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 '../Report';
|
||||
import { MonthSelect } from 'components/input/MonthSelect';
|
||||
import BaseParameters from '../BaseParameters';
|
||||
import { parseDateRange } from 'lib/date';
|
@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import RetentionTable from './RetentionTable';
|
||||
import RetentionParameters from './RetentionParameters';
|
||||
import Report from '../Report';
|
9
src/app/(app)/reports/retention/page.js
Normal file
9
src/app/(app)/reports/retention/page.js
Normal file
@ -0,0 +1,9 @@
|
||||
import RetentionReport from './RetentionReport';
|
||||
|
||||
export default function () {
|
||||
return <RetentionReport reportId={null} />;
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: 'Create Report | umami',
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import { Menu, Item } from 'react-basics';
|
||||
import { useRouter } from 'next/router';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import styles from './SideNav.module.css';
|
||||
|
||||
@ -11,13 +11,13 @@ export function SideNav({
|
||||
scroll = false,
|
||||
onSelect = () => {},
|
||||
}) {
|
||||
const { asPath } = useRouter();
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<Menu items={items} selectedKey={selectedKey} className={styles.menu} onSelect={onSelect}>
|
||||
{({ key, label, url }) => (
|
||||
<Item
|
||||
key={key}
|
||||
className={classNames(styles.item, { [styles.selected]: asPath.startsWith(url) })}
|
||||
className={classNames(styles.item, { [styles.selected]: pathname.startsWith(url) })}
|
||||
>
|
||||
<Link href={url} shallow={shallow} scroll={scroll}>
|
||||
{label}
|
@ -1,6 +1,11 @@
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 240px;
|
||||
padding-top: 40px;
|
||||
padding-right: 20px;
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
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 './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);
|
||||
|
||||
@ -21,17 +21,13 @@ export function SettingsLayout({ children }) {
|
||||
const getKey = () => items.find(({ url }) => pathname === url)?.key;
|
||||
|
||||
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/(app)/settings/profile/PasswordEditForm';
|
||||
import Icons from 'components/icons';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
|
11
src/app/(app)/settings/profile/ProfileHeader.js
Normal file
11
src/app/(app)/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/(app)/settings/profile/TimezoneSetting';
|
||||
import DateRangeSetting from 'app/(app)/settings/profile/DateRangeSetting';
|
||||
import LanguageSetting from 'app/(app)/settings/profile/LanguageSetting';
|
||||
import ThemeSetting from 'app/(app)/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;
|
11
src/app/(app)/settings/profile/page.js
Normal file
11
src/app/(app)/settings/profile/page.js
Normal file
@ -0,0 +1,11 @@
|
||||
import ProfileHeader from './ProfileHeader';
|
||||
import ProfileSettings from './ProfileSettings';
|
||||
|
||||
export default function () {
|
||||
return (
|
||||
<>
|
||||
<ProfileHeader />
|
||||
<ProfileSettings />
|
||||
</>
|
||||
);
|
||||
}
|
25
src/app/(app)/settings/teams/TeamDeleteButton.js
Normal file
25
src/app/(app)/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;
|
35
src/app/(app)/settings/teams/TeamLeaveButton.js
Normal file
35
src/app/(app)/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.ArrowRight />
|
||||
</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;
|
24
src/app/(app)/settings/teams/TeamsAddButton.js
Normal file
24
src/app/(app)/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;
|
24
src/app/(app)/settings/teams/TeamsHeader.js
Normal file
24
src/app/(app)/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/(app)/settings/teams/TeamsJoinButton.js
Normal file
29
src/app/(app)/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;
|
19
src/app/(app)/settings/teams/TeamsList.js
Normal file
19
src/app/(app)/settings/teams/TeamsList.js
Normal file
@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
import DataTable from 'components/common/DataTable';
|
||||
import TeamsTable from 'app/(app)/settings/teams/TeamsTable';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useFilterQuery from 'components/hooks/useFilterQuery';
|
||||
|
||||
export function TeamsList() {
|
||||
const { get } = useApi();
|
||||
const filterQuery = useFilterQuery(['teams'], params => {
|
||||
return get(`/teams`, {
|
||||
...params,
|
||||
});
|
||||
});
|
||||
const { getProps } = filterQuery;
|
||||
|
||||
return <DataTable {...getProps()}>{({ data }) => <TeamsTable data={data} />}</DataTable>;
|
||||
}
|
||||
|
||||
export default TeamsList;
|
46
src/app/(app)/settings/teams/TeamsTable.js
Normal file
46
src/app/(app)/settings/teams/TeamsTable.js
Normal file
@ -0,0 +1,46 @@
|
||||
'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, Flexbox, GridColumn, GridTable, Icon, Icons, Text } from 'react-basics';
|
||||
import TeamDeleteButton from './TeamDeleteButton';
|
||||
import TeamLeaveButton from './TeamLeaveButton';
|
||||
|
||||
export function TeamsTable({ data = [] }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useUser();
|
||||
|
||||
return (
|
||||
<GridTable data={data}>
|
||||
<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 showDelete = user.id === owner?.userId;
|
||||
|
||||
return (
|
||||
<Flexbox gap={10}>
|
||||
<Link href={`/settings/teams/${id}`}>
|
||||
<Button>
|
||||
<Icon>
|
||||
<Icons.Edit />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.view)}</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
{showDelete && <TeamDeleteButton teamId={id} teamName={name} />}
|
||||
{!showDelete && <TeamLeaveButton teamId={id} teamName={name} />}
|
||||
</Flexbox>
|
||||
);
|
||||
}}
|
||||
</GridColumn>
|
||||
</GridTable>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamsTable;
|
@ -1,7 +1,7 @@
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import { useRef, useState } from 'react';
|
||||
import { Button, Dropdown, Form, FormButtons, FormRow, Item, SubmitButton } from 'react-basics';
|
||||
import WebsiteTags from './WebsiteTags';
|
||||
import WebsiteTags from '../WebsiteTags';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
|
||||
export function TeamAddWebsiteForm({ teamId, onSave, onClose }) {
|
@ -1,8 +1,8 @@
|
||||
import { Loading, useToasts } from 'react-basics';
|
||||
import TeamMembersTable from 'components/pages/settings/teams/TeamMembersTable';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useApiFilter from 'components/hooks/useApiFilter';
|
||||
import TeamMembersTable from './TeamMembersTable';
|
||||
|
||||
export function TeamMembers({ teamId, readOnly }) {
|
||||
const { showToast } = useToasts();
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user