Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Matthias Kretschmann 2024-02-01 16:28:52 +00:00
commit defd6ac5c0
Signed by: m
GPG Key ID: 606EEEF3C479A91F
287 changed files with 3671 additions and 2709 deletions

View File

@ -4,14 +4,6 @@
"es2020": true, "es2020": true,
"node": true "node": true
}, },
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 11,
"sourceType": "module"
},
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:prettier/recommended", "plugin:prettier/recommended",
@ -19,6 +11,14 @@
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"next" "next"
], ],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 11,
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "prettier"], "plugins": ["@typescript-eslint", "prettier"],
"settings": { "settings": {
"import/resolver": { "import/resolver": {

View File

@ -16,10 +16,6 @@ jobs:
strategy: strategy:
matrix: matrix:
include: include:
- node-version: 16.x
db-type: postgresql
- node-version: 16.x
db-type: mysql
- node-version: 18.x - node-version: 18.x
db-type: postgresql db-type: postgresql
- node-version: 18.x - node-version: 18.x

View File

@ -22,3 +22,4 @@ jobs:
operations-per-run: 200 operations-per-run: 200
ascending: true ascending: true
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
exempt-issue-labels: bug,enhancement

View File

@ -35,7 +35,9 @@ ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs RUN adduser --system --uid 1001 nextjs
RUN yarn add npm-run-all dotenv prisma semver RUN set -x \
&& apk add --no-cache curl \
&& yarn add npm-run-all dotenv prisma semver
# You only need to copy next.config.js if you are NOT using the default configuration # You only need to copy next.config.js if you are NOT using the default configuration
COPY --from=builder /app/next.config.js . COPY --from=builder /app/next.config.js .

View File

@ -1,9 +1,9 @@
-- AlterTable -- AlterTable
ALTER TABLE `event_data` RENAME COLUMN `event_data_type` TO `data_type`; ALTER TABLE `event_data` CHANGE `event_data_type` `data_type` INTEGER UNSIGNED NOT NULL;
ALTER TABLE `event_data` RENAME COLUMN `event_date_value` TO `date_value`; ALTER TABLE `event_data` CHANGE `event_date_value` `date_value` TIMESTAMP(0) NULL;
ALTER TABLE `event_data` RENAME COLUMN `event_id` TO `event_data_id`; ALTER TABLE `event_data` CHANGE `event_id` `event_data_id` VARCHAR(36) NOT NULL;
ALTER TABLE `event_data` RENAME COLUMN `event_numeric_value` TO `number_value`; ALTER TABLE `event_data` CHANGE `event_numeric_value` `number_value` DECIMAL(19,4) NULL;
ALTER TABLE `event_data` RENAME COLUMN `event_string_value` TO `string_value`; ALTER TABLE `event_data` CHANGE `event_string_value` `string_value` VARCHAR(500) NULL;
-- CreateTable -- CreateTable
CREATE TABLE `session_data` ( CREATE TABLE `session_data` (
@ -50,4 +50,4 @@ WHERE data_type = 2;
UPDATE event_data UPDATE event_data
SET string_value = CONCAT(REPLACE(DATE_FORMAT(date_value, '%Y-%m-%d %T'), ' ', 'T'), 'Z') SET string_value = CONCAT(REPLACE(DATE_FORMAT(date_value, '%Y-%m-%d %T'), ' ', 'T'), 'Z')
WHERE data_type = 4; WHERE data_type = 4;

View File

@ -13,6 +13,11 @@ services:
db: db:
condition: service_healthy condition: service_healthy
restart: always restart: always
healthcheck:
test: ["CMD-SHELL", "curl http://localhost:3000/api/heartbeat"]
interval: 5s
timeout: 5s
retries: 5
db: db:
image: postgres:15-alpine image: postgres:15-alpine
environment: environment:

View File

@ -3,27 +3,26 @@ require('dotenv').config();
const path = require('path'); const path = require('path');
const pkg = require('./package.json'); const pkg = require('./package.json');
const contentSecurityPolicy = ` const contentSecurityPolicy = [
default-src 'self'; `default-src 'self'`,
img-src *; `img-src *`,
script-src 'self' 'unsafe-eval' 'unsafe-inline'; `script-src 'self' 'unsafe-eval' 'unsafe-inline'`,
style-src 'self' 'unsafe-inline'; `style-src 'self' 'unsafe-inline'`,
connect-src 'self' api.umami.is; `connect-src 'self' api.umami.is`,
frame-ancestors 'self' ${process.env.ALLOWED_FRAME_URLS}; `frame-ancestors 'self' ${process.env.ALLOWED_FRAME_URLS || ''}`,
`; ];
const headers = [ const headers = [
{ {
key: 'X-DNS-Prefetch-Control', key: 'X-DNS-Prefetch-Control',
value: 'on', value: 'on',
}, },
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN',
},
{ {
key: 'Content-Security-Policy', key: 'Content-Security-Policy',
value: contentSecurityPolicy.replace(/\s{2,}/g, ' ').trim(), value: contentSecurityPolicy
.join(';')
.replace(/\s{2,}/g, ' ')
.trim(),
}, },
]; ];
@ -81,14 +80,14 @@ const config = {
reactStrictMode: false, reactStrictMode: false,
env: { env: {
basePath: basePath || '', basePath: basePath || '',
cloudMode: !!process.env.CLOUD_MODE, cloudMode: process.env.CLOUD_MODE || '',
cloudUrl: process.env.CLOUD_URL, cloudUrl: process.env.CLOUD_URL || '',
configUrl: '/config', configUrl: '/config',
currentVersion: pkg.version, currentVersion: pkg.version,
defaultLocale: process.env.DEFAULT_LOCALE, defaultLocale: process.env.DEFAULT_LOCALE || '',
disableLogin: process.env.DISABLE_LOGIN, disableLogin: process.env.DISABLE_LOGIN || '',
disableUI: process.env.DISABLE_UI, disableUI: process.env.DISABLE_UI || '',
isProduction: process.env.NODE_ENV === 'production', hostUrl: process.env.HOST_URL || '',
}, },
basePath, basePath,
output: 'standalone', output: 'standalone',

View File

@ -1,6 +1,6 @@
{ {
"name": "umami", "name": "umami",
"version": "2.8.0", "version": "2.9.0",
"description": "A simple, fast, privacy-focused alternative to Google Analytics.", "description": "A simple, fast, privacy-focused alternative to Google Analytics.",
"author": "Mike Cao <mike@mikecao.com>", "author": "Mike Cao <mike@mikecao.com>",
"license": "MIT", "license": "MIT",
@ -63,11 +63,12 @@
"dependencies": { "dependencies": {
"@clickhouse/client": "^0.2.2", "@clickhouse/client": "^0.2.2",
"@fontsource/inter": "^4.5.15", "@fontsource/inter": "^4.5.15",
"@prisma/client": "5.3.1", "@prisma/client": "5.6.0",
"@prisma/extension-read-replicas": "^0.3.0",
"@react-spring/web": "^9.7.3", "@react-spring/web": "^9.7.3",
"@tanstack/react-query": "^4.33.0", "@tanstack/react-query": "^5.12.2",
"@umami/prisma-client": "^0.3.0", "@umami/prisma-client": "^0.8.0",
"@umami/redis-client": "^0.16.0", "@umami/redis-client": "^0.18.0",
"chalk": "^4.1.1", "chalk": "^4.1.1",
"chart.js": "^4.2.1", "chart.js": "^4.2.1",
"chartjs-adapter-date-fns": "^3.0.0", "chartjs-adapter-date-fns": "^3.0.0",
@ -92,17 +93,17 @@
"kafkajs": "^2.1.0", "kafkajs": "^2.1.0",
"maxmind": "^4.3.6", "maxmind": "^4.3.6",
"moment-timezone": "^0.5.35", "moment-timezone": "^0.5.35",
"next": "^13.5.3", "next": "14.0.4",
"next-basics": "^0.37.0", "next-basics": "^0.39.0",
"node-fetch": "^3.2.8", "node-fetch": "^3.2.8",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prisma": "5.3.1", "prisma": "5.6.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-basics": "^0.105.0", "react-basics": "^0.114.0",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-error-boundary": "^4.0.4", "react-error-boundary": "^4.0.4",
"react-intl": "^6.4.7", "react-intl": "^6.5.5",
"react-simple-maps": "^2.3.0", "react-simple-maps": "^2.3.0",
"react-use-measure": "^2.0.4", "react-use-measure": "^2.0.4",
"react-window": "^1.8.6", "react-window": "^1.8.6",
@ -125,9 +126,10 @@
"@rollup/plugin-replace": "^5.0.2", "@rollup/plugin-replace": "^5.0.2",
"@svgr/rollup": "^8.1.0", "@svgr/rollup": "^8.1.0",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
"@types/node": "^18.11.9", "@types/node": "^20.9.0",
"@types/react": "^18.0.25", "@types/react": "^18.2.41",
"@types/react-dom": "^18.0.8", "@types/react-dom": "^18.2.17",
"@types/react-window": "^1.8.8",
"@typescript-eslint/eslint-plugin": "^6.7.3", "@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.3", "@typescript-eslint/parser": "^6.7.3",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",

View File

@ -1,14 +1,23 @@
'use client'; 'use client';
import { Loading } from 'react-basics';
import Script from 'next/script'; import Script from 'next/script';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import UpdateNotice from 'components/common/UpdateNotice'; import { useLogin, useConfig } from 'components/hooks';
import { useRequireLogin, useConfig } from 'components/hooks'; import UpdateNotice from './UpdateNotice';
export function Shell({ children }) { export function App({ children }) {
const { user } = useRequireLogin(); const { user, isLoading, error } = useLogin();
const config = useConfig(); const config = useConfig();
const pathname = usePathname(); const pathname = usePathname();
if (isLoading) {
return <Loading />;
}
if (error) {
window.location.href = `${process.env.basePath || ''}/login`;
}
if (!user || !config) { if (!user || !config) {
return null; return null;
} }
@ -24,4 +33,4 @@ export function Shell({ children }) {
); );
} }
export default Shell; export default App;

View File

@ -14,6 +14,7 @@ import styles from './NavBar.module.css';
export function NavBar() { export function NavBar() {
const pathname = usePathname(); const pathname = usePathname();
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const cloudMode = Boolean(process.env.cloudMode);
const links = [ const links = [
{ label: formatMessage(labels.dashboard), url: '/dashboard' }, { label: formatMessage(labels.dashboard), url: '/dashboard' },
@ -22,6 +23,40 @@ export function NavBar() {
{ label: formatMessage(labels.settings), url: '/settings' }, { label: formatMessage(labels.settings), url: '/settings' },
].filter(n => n); ].filter(n => n);
const menuItems = [
{
label: formatMessage(labels.dashboard),
url: '/dashboard',
},
!cloudMode && {
label: formatMessage(labels.settings),
url: '/settings',
children: [
{
label: formatMessage(labels.websites),
url: '/settings/websites',
},
{
label: formatMessage(labels.teams),
url: '/settings/teams',
},
{
label: formatMessage(labels.users),
url: '/settings/users',
},
{
label: formatMessage(labels.profile),
url: '/settings/profile',
},
],
},
cloudMode && {
label: formatMessage(labels.profile),
url: '/settings/profile',
},
!cloudMode && { label: formatMessage(labels.logout), url: '/logout' },
].filter(n => n);
return ( return (
<div className={styles.navbar}> <div className={styles.navbar}>
<div className={styles.logo}> <div className={styles.logo}>
@ -49,7 +84,7 @@ export function NavBar() {
<ProfileButton /> <ProfileButton />
</div> </div>
<div className={styles.mobile}> <div className={styles.mobile}>
<HamburgerButton /> <HamburgerButton menuItems={menuItems} />
</div> </div>
</div> </div>
); );

View File

@ -1,30 +1,33 @@
'use client'; 'use client';
import { Button } from 'react-basics';
import Head from 'next/head';
import Link from 'next/link';
import Script from 'next/script';
import WebsiteSelect from 'components/input/WebsiteSelect'; import WebsiteSelect from 'components/input/WebsiteSelect';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import EventsChart from 'components/metrics/EventsChart'; import EventsChart from 'components/metrics/EventsChart';
import WebsiteChart from '../../(main)/websites/[id]/WebsiteChart'; import WebsiteChart from 'app/(main)/websites/[id]/WebsiteChart';
import useApi from 'components/hooks/useApi'; import useApi from 'components/hooks/useApi';
import Head from 'next/head';
import Link from 'next/link';
import useNavigation from 'components/hooks/useNavigation'; import useNavigation from 'components/hooks/useNavigation';
import Script from 'next/script';
import { Button } from 'react-basics';
import styles from './TestConsole.module.css'; import styles from './TestConsole.module.css';
export function TestConsole({ websiteId }) { export function TestConsole({ websiteId }: { websiteId: string }) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, isLoading, error } = useQuery(['websites:me'], () => get('/me/websites')); const { data, isLoading, error } = useQuery({
queryKey: ['websites:me'],
queryFn: () => get('/me/websites'),
});
const { router } = useNavigation(); const { router } = useNavigation();
function handleChange(value) { function handleChange(value: string) {
router.push(`/console/${value}`); router.push(`/console/${value}`);
} }
function handleClick() { function handleClick() {
window.umami.track({ url: '/page-view', referrer: 'https://www.google.com' }); window['umami'].track({ url: '/page-view', referrer: 'https://www.google.com' });
window.umami.track('track-event-no-data'); window['umami'].track('track-event-no-data');
window.umami.track('track-event-with-data', { window['umami'].track('track-event-with-data', {
test: 'test-data', test: 'test-data',
boolean: true, boolean: true,
booleanError: 'true', booleanError: 'true',
@ -44,7 +47,7 @@ export function TestConsole({ websiteId }) {
} }
function handleIdentifyClick() { function handleIdentifyClick() {
window.umami.identify({ window['umami'].identify({
userId: 123, userId: 123,
name: 'brian', name: 'brian',
number: Math.random() * 100, number: Math.random() * 100,
@ -71,7 +74,7 @@ export function TestConsole({ websiteId }) {
const website = data?.data.find(({ id }) => websiteId === id); const website = data?.data.find(({ id }) => websiteId === id);
return ( return (
<Page loading={isLoading} error={error}> <Page isLoading={isLoading} error={error}>
<Head> <Head>
<title>{website ? `${website.name} | Umami Console` : 'Umami Console'}</title> <title>{website ? `${website.name} | Umami Console` : 'Umami Console'}</title>
</Head> </Head>
@ -113,7 +116,7 @@ export function TestConsole({ websiteId }) {
</div> </div>
<div> <div>
<div className={styles.header}>Click events</div> <div className={styles.header}>Click events</div>
<Button id="send-event-button" data-umami-event="button-click" variant="action"> <Button id="send-event-button" data-umami-event="button-click" variant="primary">
Send event Send event
</Button> </Button>
<p /> <p />
@ -122,18 +125,18 @@ export function TestConsole({ websiteId }) {
data-umami-event="button-click" data-umami-event="button-click"
data-umami-event-name="bob" data-umami-event-name="bob"
data-umami-event-id="123" data-umami-event-id="123"
variant="action" variant="primary"
> >
Send event with data Send event with data
</Button> </Button>
</div> </div>
<div> <div>
<div className={styles.header}>Javascript events</div> <div className={styles.header}>Javascript events</div>
<Button id="manual-button" variant="action" onClick={handleClick}> <Button id="manual-button" variant="primary" onClick={handleClick}>
Run script Run script
</Button> </Button>
<p /> <p />
<Button id="manual-button" variant="action" onClick={handleIdentifyClick}> <Button id="manual-button" variant="primary" onClick={handleIdentifyClick}>
Run identify Run identify
</Button> </Button>
</div> </div>

View File

@ -11,23 +11,34 @@ import useApi from 'components/hooks/useApi';
import useDashboard from 'store/dashboard'; import useDashboard from 'store/dashboard';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
import useLocale from 'components/hooks/useLocale'; import useLocale from 'components/hooks/useLocale';
import useApiFilter from 'components/hooks/useApiFilter'; import useFilterQuery from 'components/hooks/useFilterQuery';
import { useUser } from 'components/hooks';
export function Dashboard() { export function Dashboard() {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { user } = useUser();
const { showCharts, editing } = useDashboard(); const { showCharts, editing } = useDashboard();
const { dir } = useLocale(); const { dir } = useLocale();
const { get, useQuery } = useApi(); const { get } = useApi();
const { page, handlePageChange } = useApiFilter();
const pageSize = 10; const pageSize = 10;
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) { const { query, params, setParams, result } = useFilterQuery({
return <Loading size="lg" />; queryKey: ['dashboard:websites'],
queryFn: (params: any) => {
return get(`/users/${user.id}/websites`, { ...params, includeTeams: true, pageSize });
},
});
const handlePageChange = (page: number) => {
setParams({ ...params, page });
};
const { data, count } = result || {};
const hasData = !!(data as any)?.length;
const { page } = params;
if (query.isLoading) {
return <Loading />;
} }
return ( return (

View File

@ -17,7 +17,10 @@ export function DashboardEdit() {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const [order, setOrder] = useState(websiteOrder || []); const [order, setOrder] = useState(websiteOrder || []);
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data: result } = useQuery(['websites'], () => get('/websites', { includeTeams: 1 })); const { data: result } = useQuery({
queryKey: ['websites'],
queryFn: () => get('/websites', { includeTeams: 1 }),
});
const { data: websites } = result || {}; const { data: websites } = result || {};
const ordered = useMemo(() => { const ordered = useMemo(() => {
@ -57,13 +60,13 @@ export function DashboardEdit() {
return ( return (
<> <>
<div className={styles.buttons}> <div className={styles.buttons}>
<Button onClick={handleSave} variant="action" size="small"> <Button onClick={handleSave} variant="primary" size="sm">
{formatMessage(labels.save)} {formatMessage(labels.save)}
</Button> </Button>
<Button onClick={handleCancel} size="small"> <Button onClick={handleCancel} size="sm">
{formatMessage(labels.cancel)} {formatMessage(labels.cancel)}
</Button> </Button>
<Button onClick={handleReset} size="small"> <Button onClick={handleReset} size="sm">
{formatMessage(labels.reset)} {formatMessage(labels.reset)}
</Button> </Button>
</div> </div>

View File

@ -1,7 +1,7 @@
import Dashboard from 'app/(main)/dashboard/Dashboard'; import Dashboard from 'app/(main)/dashboard/Dashboard';
import { Metadata } from 'next'; import { Metadata } from 'next';
export default function DashboardPage() { export default function () {
return <Dashboard />; return <Dashboard />;
} }

View File

@ -1,11 +1,11 @@
import Shell from './Shell'; import App from './App';
import NavBar from './NavBar'; import NavBar from './NavBar';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import styles from './layout.module.css'; import styles from './layout.module.css';
export default function AppLayout({ children }) { export default function ({ children }) {
return ( return (
<Shell> <App>
<main className={styles.layout}> <main className={styles.layout}>
<nav className={styles.nav}> <nav className={styles.nav}>
<NavBar /> <NavBar />
@ -14,6 +14,6 @@ export default function AppLayout({ children }) {
<Page>{children}</Page> <Page>{children}</Page>
</section> </section>
</main> </main>
</Shell> </App>
); );
} }

View File

@ -3,13 +3,21 @@ import ConfirmDeleteForm from 'components/common/ConfirmDeleteForm';
import { useApi, useMessages } from 'components/hooks'; import { useApi, useMessages } from 'components/hooks';
import { setValue } from 'store/cache'; import { setValue } from 'store/cache';
export function ReportDeleteButton({ reportId, reportName, onDelete }) { export function ReportDeleteButton({
reportId,
reportName,
onDelete,
}: {
reportId: string;
reportName: string;
onDelete?: () => void;
}) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { del, useMutation } = useApi(); const { del, useMutation } = useApi();
const { mutate } = useMutation(reportId => del(`/reports/${reportId}`)); const { mutate } = useMutation({ mutationFn: reportId => del(`/reports/${reportId}`) });
const handleConfirm = close => { const handleConfirm = (close: () => void) => {
mutate(reportId, { mutate(reportId as any, {
onSuccess: () => { onSuccess: () => {
setValue('reports', Date.now()); setValue('reports', Date.now());
onDelete?.(); onDelete?.();

View File

@ -1,20 +0,0 @@
'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>
);
}

View File

@ -0,0 +1,14 @@
'use client';
import { useReports } from 'components/hooks';
import ReportsTable from './ReportsTable';
import DataTable from 'components/common/DataTable';
export default function ReportsDataTable({ websiteId }: { websiteId?: string }) {
const queryResult = useReports(websiteId);
return (
<DataTable queryResult={queryResult}>
{({ data }) => <ReportsTable data={data} showDomain={!websiteId} />}
</DataTable>
);
}

View File

@ -5,7 +5,7 @@ import useUser from 'components/hooks/useUser';
import { REPORT_TYPES } from 'lib/constants'; import { REPORT_TYPES } from 'lib/constants';
import ReportDeleteButton from './ReportDeleteButton'; import ReportDeleteButton from './ReportDeleteButton';
export function ReportsTable({ data = [], showDomain }) { export function ReportsTable({ data = [], showDomain }: { data: any[]; showDomain?: boolean }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { user } = useUser(); const { user } = useUser();
const breakpoint = useBreakpoint(); const breakpoint = useBreakpoint();

View File

@ -6,12 +6,19 @@ import WebsiteSelect from 'components/input/WebsiteSelect';
import { useMessages } from 'components/hooks'; import { useMessages } from 'components/hooks';
import { ReportContext } from './Report'; import { ReportContext } from './Report';
export interface BaseParametersProps {
showWebsiteSelect?: boolean;
allowWebsiteSelect?: boolean;
showDateSelect?: boolean;
allowDateSelect?: boolean;
}
export function BaseParameters({ export function BaseParameters({
showWebsiteSelect = true, showWebsiteSelect = true,
allowWebsiteSelect = true, allowWebsiteSelect = true,
showDateSelect = true, showDateSelect = true,
allowDateSelect = true, allowDateSelect = true,
}) { }: BaseParametersProps) {
const { report, updateReport } = useContext(ReportContext); const { report, updateReport } = useContext(ReportContext);
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
@ -19,11 +26,11 @@ export function BaseParameters({
const { websiteId, dateRange } = parameters || {}; const { websiteId, dateRange } = parameters || {};
const { value, startDate, endDate } = dateRange || {}; const { value, startDate, endDate } = dateRange || {};
const handleWebsiteSelect = websiteId => { const handleWebsiteSelect = (websiteId: string) => {
updateReport({ websiteId, parameters: { websiteId } }); updateReport({ websiteId, parameters: { websiteId } });
}; };
const handleDateChange = value => { const handleDateChange = (value: string) => {
updateReport({ parameters: { dateRange: { ...parseDateRange(value) } } }); updateReport({ parameters: { dateRange: { ...parseDateRange(value) } } });
}; };

View File

@ -7,10 +7,20 @@ import FieldAggregateForm from './FieldAggregateForm';
import FieldFilterForm from './FieldFilterForm'; import FieldFilterForm from './FieldFilterForm';
import styles from './FieldAddForm.module.css'; import styles from './FieldAddForm.module.css';
export function FieldAddForm({ fields = [], group, onAdd, onClose }) { export function FieldAddForm({
const [selected, setSelected] = useState(); fields = [],
group,
onAdd,
onClose,
}: {
fields?: any[];
group: string;
onAdd: (group: string, value: string) => void;
onClose: () => void;
}) {
const [selected, setSelected] = useState<{ name: string; type: string; value: string }>();
const handleSelect = value => { const handleSelect = (value: any) => {
const { type } = value; const { type } = value;
if (group === REPORT_PARAMETERS.groups || type === 'array' || type === 'boolean') { if (group === REPORT_PARAMETERS.groups || type === 'array' || type === 'boolean') {
@ -22,7 +32,7 @@ export function FieldAddForm({ fields = [], group, onAdd, onClose }) {
setSelected(value); setSelected(value);
}; };
const handleSave = value => { const handleSave = (value: any) => {
onAdd(group, value); onAdd(group, value);
onClose(); onClose();
}; };

View File

@ -1,7 +1,15 @@
import { Form, FormRow, Menu, Item } from 'react-basics'; import { Form, FormRow, Menu, Item } from 'react-basics';
import { useMessages } from 'components/hooks'; import { useMessages } from 'components/hooks';
export default function FieldAggregateForm({ name, type, onSelect }) { export default function FieldAggregateForm({
name,
type,
onSelect,
}: {
name: string;
type: string;
onSelect: (key: any) => void;
}) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const options = { const options = {
@ -27,7 +35,7 @@ export default function FieldAggregateForm({ name, type, onSelect }) {
const items = options[type]; const items = options[type];
const handleSelect = value => { const handleSelect = (value: any) => {
onSelect({ name, type, value }); onSelect({ name, type, value });
}; };

View File

@ -1,8 +1,17 @@
import { useState } from 'react'; import { useState, useMemo } from 'react';
import { Form, FormRow, Item, Flexbox, Dropdown, Button } from 'react-basics'; import { Form, FormRow, Item, Flexbox, Dropdown, Button } from 'react-basics';
import { useMessages, useFilters, useFormat } from 'components/hooks'; import { useMessages, useFilters, useFormat, useLocale } from 'components/hooks';
import styles from './FieldFilterForm.module.css'; import styles from './FieldFilterForm.module.css';
export interface FieldFilterFormProps {
name: string;
label?: string;
type: string;
values?: any[];
onSelect?: (key: any) => void;
allowFilterSelect?: boolean;
}
export default function FieldFilterForm({ export default function FieldFilterForm({
name, name,
label, label,
@ -10,20 +19,36 @@ export default function FieldFilterForm({
values, values,
onSelect, onSelect,
allowFilterSelect = true, allowFilterSelect = true,
}) { }: FieldFilterFormProps) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const [filter, setFilter] = useState('eq'); const [filter, setFilter] = useState('eq');
const [value, setValue] = useState(); const [value, setValue] = useState();
const { getFilters } = useFilters(); const { getFilters } = useFilters();
const { formatValue } = useFormat(); const { formatValue } = useFormat();
const { locale } = useLocale();
const filters = getFilters(type); const filters = getFilters(type);
const formattedValues = useMemo(() => {
const formatted = {};
const format = (val: string) => {
formatted[val] = formatValue(val, name);
return formatted[val];
};
if (values.length !== 1) {
const { compare } = new Intl.Collator(locale, { numeric: true });
values.sort((a, b) => compare(formatted[a] ?? format(a), formatted[b] ?? format(b)));
} else {
format(values[0]);
}
return formatted;
}, [values]);
const renderFilterValue = value => { const renderFilterValue = value => {
return filters.find(f => f.value === value)?.label; return filters.find(f => f.value === value)?.label;
}; };
const renderValue = value => { const renderValue = value => {
return formatValue(value, name); return formattedValues[value];
}; };
const handleAdd = () => { const handleAdd = () => {
@ -40,7 +65,7 @@ export default function FieldFilterForm({
items={filters} items={filters}
value={filter} value={filter}
renderValue={renderFilterValue} renderValue={renderFilterValue}
onChange={setFilter} onChange={(key: any) => setFilter(key)}
> >
{({ value, label }) => { {({ value, label }) => {
return <Item key={value}>{label}</Item>; return <Item key={value}>{label}</Item>;
@ -53,13 +78,13 @@ export default function FieldFilterForm({
items={values} items={values}
value={value} value={value}
renderValue={renderValue} renderValue={renderValue}
onChange={setValue} onChange={(key: any) => setValue(key)}
style={{ style={{
minWidth: '250px', minWidth: '250px',
}} }}
> >
{value => { {(value: string) => {
return <Item key={value}>{formatValue(value, name)}</Item>; return <Item key={value}>{formattedValues[value]}</Item>;
}} }}
</Dropdown> </Dropdown>
</Flexbox> </Flexbox>

View File

@ -1,15 +1,26 @@
import { Menu, Item, Form, FormRow } from 'react-basics'; import { Menu, Item, Form, FormRow } from 'react-basics';
import { useMessages } from 'components/hooks'; import { useMessages } from 'components/hooks';
import styles from './FieldSelectForm.module.css'; import styles from './FieldSelectForm.module.css';
import { Key } from 'react';
export default function FieldSelectForm({ items, onSelect, showType = true }) { export interface FieldSelectFormProps {
fields?: any[];
onSelect?: (key: any) => void;
showType?: boolean;
}
export default function FieldSelectForm({
fields = [],
onSelect,
showType = true,
}: FieldSelectFormProps) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
return ( return (
<Form> <Form>
<FormRow label={formatMessage(labels.fields)}> <FormRow label={formatMessage(labels.fields)}>
<Menu className={styles.menu} onSelect={key => onSelect(items[key])}> <Menu className={styles.menu} onSelect={key => onSelect(fields[key as any])}>
{items.map(({ name, label, type }, index) => { {fields.map(({ name, label, type }: any, index: Key) => {
return ( return (
<Item key={index} className={styles.item}> <Item key={index} className={styles.item}>
<div>{label || name}</div> <div>{label || name}</div>

View File

@ -5,29 +5,41 @@ import FieldSelectForm from './FieldSelectForm';
import FieldFilterForm from './FieldFilterForm'; import FieldFilterForm from './FieldFilterForm';
import { useApi } from 'components/hooks'; import { useApi } from 'components/hooks';
function useValues(websiteId, type) { function useValues(websiteId: string, type: string) {
const now = Date.now(); const now = Date.now();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, error, isLoading } = useQuery( const { data, error, isLoading } = useQuery({
['websites:values', websiteId, type], queryKey: ['websites:values', websiteId, type],
() => queryFn: () =>
get(`/websites/${websiteId}/values`, { get(`/websites/${websiteId}/values`, {
type, type,
startAt: +subDays(now, 90), startAt: +subDays(now, 90),
endAt: now, endAt: now,
}), }),
{ enabled: !!(websiteId && type) }, enabled: !!(websiteId && type),
); });
return { data, error, isLoading }; return { data, error, isLoading };
} }
export default function FilterSelectForm({ websiteId, items, onSelect, allowFilterSelect }) { export interface FilterSelectFormProps {
const [field, setField] = useState(); websiteId: string;
items: any[];
onSelect?: (key: any) => void;
allowFilterSelect?: boolean;
}
export default function FilterSelectForm({
websiteId,
items,
onSelect,
allowFilterSelect,
}: FilterSelectFormProps) {
const [field, setField] = useState<{ name: string; label: string; type: string }>();
const { data, isLoading } = useValues(websiteId, field?.name); const { data, isLoading } = useValues(websiteId, field?.name);
if (!field) { if (!field) {
return <FieldSelectForm items={items} onSelect={setField} showType={false} />; return <FieldSelectForm fields={items} onSelect={setField} showType={false} />;
} }
if (isLoading) { if (isLoading) {

View File

@ -1,10 +1,17 @@
import { ReactNode } from 'react';
import { Icon, TooltipPopup } from 'react-basics'; import { Icon, TooltipPopup } from 'react-basics';
import Icons from 'components/icons'; import Icons from 'components/icons';
import Empty from 'components/common/Empty'; import Empty from 'components/common/Empty';
import { useMessages } from 'components/hooks'; import { useMessages } from 'components/hooks';
import styles from './ParameterList.module.css'; import styles from './ParameterList.module.css';
export function ParameterList({ items = [], children, onRemove }) { export interface ParameterListProps {
items: any[];
children?: ReactNode | ((item: any) => ReactNode);
onRemove: (index: number, e: any) => void;
}
export function ParameterList({ items = [], children, onRemove }: ParameterListProps) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
return ( return (

View File

@ -1,7 +1,16 @@
import { CSSProperties, ReactNode } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import styles from './PopupForm.module.css'; import styles from './PopupForm.module.css';
export function PopupForm({ className, style, children }) { export function PopupForm({
className,
style,
children,
}: {
className?: string;
style?: CSSProperties;
children: ReactNode;
}) {
return ( return (
<div <div
className={classNames(styles.form, className)} className={classNames(styles.form, className)}

View File

@ -1,24 +0,0 @@
'use client';
import { createContext } from 'react';
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 }}>
<div {...props} className={styles.container}>
{children}
</div>
</ReportContext.Provider>
);
}
export default Report;

View File

@ -0,0 +1,31 @@
'use client';
import { createContext, ReactNode } from 'react';
import { Loading } from 'react-basics';
import { useReport } from 'components/hooks';
import styles from './Report.module.css';
import classNames from 'classnames';
export const ReportContext = createContext(null);
export interface ReportProps {
reportId: string;
defaultParameters: { [key: string]: any };
children: ReactNode;
className?: string;
}
export function Report({ reportId, defaultParameters, children, className }: ReportProps) {
const report = useReport(reportId, defaultParameters);
if (!report) {
return reportId ? <Loading position="page" /> : null;
}
return (
<ReportContext.Provider value={report}>
<div className={classNames(styles.container, className)}>{children}</div>
</ReportContext.Provider>
);
}
export default Report;

View File

@ -1,6 +1,14 @@
import styles from './ReportBody.module.css'; import styles from './ReportBody.module.css';
import { useContext } from 'react';
import { ReportContext } from './Report';
export function ReportBody({ children }) { export function ReportBody({ children }) {
const { report } = useContext(ReportContext);
if (!report) {
return null;
}
return <div className={styles.body}>{children}</div>; return <div className={styles.body}>{children}</div>;
} }

View File

@ -12,9 +12,12 @@ const reports = {
retention: RetentionReport, retention: RetentionReport,
}; };
export default function ReportDetails({ reportId }) { export default function ReportDetails({ reportId }: { reportId: string }) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data: report } = useQuery(['reports', reportId], () => get(`/reports/${reportId}`)); const { data: report } = useQuery({
queryKey: ['reports', reportId],
queryFn: () => get(`/reports/${reportId}`),
});
if (!report) { if (!report) {
return null; return null;

View File

@ -12,10 +12,12 @@ export function ReportHeader({ icon }) {
const { showToast } = useToasts(); const { showToast } = useToasts();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const router = useRouter(); const router = useRouter();
const { mutate: create, isLoading: isCreating } = useMutation(data => post(`/reports`, data)); const { mutate: create, isPending: isCreating } = useMutation({
const { mutate: update, isLoading: isUpdating } = useMutation(data => mutationFn: (data: any) => post(`/reports`, data),
post(`/reports/${data.id}`, data), });
); const { mutate: update, isPending: isUpdating } = useMutation({
mutationFn: (data: any) => post(`/reports/${data.id}`, data),
});
const { name, description, parameters } = report || {}; const { name, description, parameters } = report || {};
const { websiteId, dateRange } = parameters || {}; const { websiteId, dateRange } = parameters || {};
@ -26,7 +28,7 @@ export function ReportHeader({ icon }) {
create(report, { create(report, {
onSuccess: async ({ id }) => { onSuccess: async ({ id }) => {
showToast({ message: formatMessage(messages.saved), variant: 'success' }); showToast({ message: formatMessage(messages.saved), variant: 'success' });
router.push(`/reports/${id}`, null, { shallow: true }); router.push(`/reports/${id}`);
}, },
}); });
} else { } else {
@ -38,11 +40,11 @@ export function ReportHeader({ icon }) {
} }
}; };
const handleNameChange = name => { const handleNameChange = (name: string) => {
updateReport({ name: name || defaultName }); updateReport({ name: name || defaultName });
}; };
const handleDescriptionChange = description => { const handleDescriptionChange = (description: string) => {
updateReport({ description }); updateReport({ description });
}; };

View File

@ -1,6 +1,14 @@
import styles from './ReportMenu.module.css'; import styles from './ReportMenu.module.css';
import { useContext } from 'react';
import { ReportContext } from './Report';
export function ReportMenu({ children }) { export function ReportMenu({ children }) {
const { report } = useContext(ReportContext);
if (!report) {
return null;
}
return <div className={styles.menu}>{children}</div>; return <div className={styles.menu}>{children}</div>;
} }

View File

@ -1,4 +1,4 @@
import { useContext, useRef } from 'react'; import { useContext } from 'react';
import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics'; import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics';
import Empty from 'components/common/Empty'; import Empty from 'components/common/Empty';
import Icons from 'components/icons'; import Icons from 'components/icons';
@ -12,16 +12,16 @@ import styles from './EventDataParameters.module.css';
function useFields(websiteId, startDate, endDate) { function useFields(websiteId, startDate, endDate) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, error, isLoading } = useQuery( const { data, error, isLoading } = useQuery({
['fields', websiteId, startDate, endDate], queryKey: ['fields', websiteId, startDate, endDate],
() => queryFn: () =>
get('/reports/event-data', { get('/reports/event-data', {
websiteId, websiteId,
startAt: +startDate, startAt: +startDate,
endAt: +endDate, endAt: +endDate,
}), }),
{ enabled: !!(websiteId && startDate && endDate) }, enabled: !!(websiteId && startDate && endDate),
); });
return { data, error, isLoading }; return { data, error, isLoading };
} }
@ -29,7 +29,6 @@ function useFields(websiteId, startDate, endDate) {
export function EventDataParameters() { export function EventDataParameters() {
const { report, runReport, updateReport, isRunning } = useContext(ReportContext); const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const ref = useRef(null);
const { parameters } = report || {}; const { parameters } = report || {};
const { websiteId, dateRange, fields, filters, groups } = parameters || {}; const { websiteId, dateRange, fields, filters, groups } = parameters || {};
const { startDate, endDate } = dateRange || {}; const { startDate, endDate } = dateRange || {};
@ -53,28 +52,28 @@ export function EventDataParameters() {
runReport(values); runReport(values);
}; };
const handleAdd = (group, value) => { const handleAdd = (group: string, value: any) => {
const data = parameterData[group]; const data = parameterData[group];
if (!data.find(({ name }) => name === value.name)) { if (!data.find(({ name }) => name === value?.name)) {
updateReport({ parameters: { [group]: data.concat(value) } }); updateReport({ parameters: { [group]: data.concat(value) } });
} }
}; };
const handleRemove = (group, index) => { const handleRemove = (group: string, index: number) => {
const data = [...parameterData[group]]; const data = [...parameterData[group]];
data.splice(index, 1); data.splice(index, 1);
updateReport({ parameters: { [group]: data } }); updateReport({ parameters: { [group]: data } });
}; };
const AddButton = ({ group }) => { const AddButton = ({ group, onAdd }) => {
return ( return (
<PopupTrigger> <PopupTrigger>
<Icon> <Icon>
<Icons.Plus /> <Icons.Plus />
</Icon> </Icon>
<Popup position="bottom" alignment="start"> <Popup position="bottom" alignment="start">
{close => { {(close: () => void) => {
return ( return (
<FieldAddForm <FieldAddForm
fields={data.map(({ eventKey, eventDataType }) => ({ fields={data.map(({ eventKey, eventDataType }) => ({
@ -82,7 +81,7 @@ export function EventDataParameters() {
type: DATA_TYPES[eventDataType], type: DATA_TYPES[eventDataType],
}))} }))}
group={group} group={group}
onAdd={handleAdd} onAdd={onAdd}
onClose={close} onClose={close}
/> />
); );
@ -93,7 +92,7 @@ export function EventDataParameters() {
}; };
return ( return (
<Form ref={ref} values={parameters} error={error} onSubmit={handleSubmit}> <Form values={parameters} error={error} onSubmit={handleSubmit}>
<BaseParameters /> <BaseParameters />
{!hasData && <Empty message={formatMessage(messages.noEventData)} />} {!hasData && <Empty message={formatMessage(messages.noEventData)} />}
{parametersSelected && {parametersSelected &&

View File

@ -11,7 +11,7 @@ const defaultParameters = {
parameters: { fields: [], filters: [] }, parameters: { fields: [], filters: [] },
}; };
export default function EventDataReport({ reportId }) { export default function EventDataReport({ reportId }: { reportId: string }) {
return ( return (
<Report reportId={reportId} defaultParameters={defaultParameters}> <Report reportId={reportId} defaultParameters={defaultParameters}>
<ReportHeader icon={<Nodes />} /> <ReportHeader icon={<Nodes />} />

View File

@ -1,13 +1,18 @@
import { useCallback, useContext, useMemo } from 'react'; import { JSX, useCallback, useContext, useMemo } from 'react';
import { Loading, StatusLight } from 'react-basics'; import { Loading, StatusLight } from 'react-basics';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
import useTheme from 'components/hooks/useTheme'; import useTheme from 'components/hooks/useTheme';
import BarChart from 'components/metrics/BarChart'; import BarChart from 'components/metrics/BarChart';
import { formatLongNumber } from 'lib/format'; import { formatLongNumber } from 'lib/format';
import styles from './FunnelChart.module.css';
import { ReportContext } from '../[id]/Report'; import { ReportContext } from '../[id]/Report';
import styles from './FunnelChart.module.css';
export function FunnelChart({ className, loading }) { export interface FunnelChartProps {
className?: string;
isLoading?: boolean;
}
export function FunnelChart({ className, isLoading }: FunnelChartProps) {
const { report } = useContext(ReportContext); const { report } = useContext(ReportContext);
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { colors } = useTheme(); const { colors } = useTheme();
@ -15,33 +20,39 @@ export function FunnelChart({ className, loading }) {
const { parameters, data } = report || {}; const { parameters, data } = report || {};
const renderXLabel = useCallback( const renderXLabel = useCallback(
(label, index) => { (label: string, index: number) => {
return parameters.urls[index]; return parameters.urls[index];
}, },
[parameters], [parameters],
); );
const renderTooltipPopup = useCallback((setTooltipPopup, model) => { const renderTooltipPopup = useCallback(
const { opacity, labelColors, dataPoints } = model.tooltip; (
setTooltipPopup: (arg0: JSX.Element) => void,
model: { tooltip: { opacity: any; labelColors: any; dataPoints: any } },
) => {
const { opacity, labelColors, dataPoints } = model.tooltip;
if (!dataPoints?.length || !opacity) { if (!dataPoints?.length || !opacity) {
setTooltipPopup(null); setTooltipPopup(null);
return; return;
} }
setTooltipPopup( setTooltipPopup(
<> <>
<div> <div>
{formatLongNumber(dataPoints[0].raw.y)} {formatMessage(labels.visitors)} {formatLongNumber(dataPoints[0].raw.y)} {formatMessage(labels.visitors)}
</div> </div>
<div> <div>
<StatusLight color={labelColors?.[0]?.backgroundColor}> <StatusLight color={labelColors?.[0]?.backgroundColor}>
{formatLongNumber(dataPoints[0].raw.z)}% {formatMessage(labels.dropoff)} {formatLongNumber(dataPoints[0].raw.z)}% {formatMessage(labels.dropoff)}
</StatusLight> </StatusLight>
</div> </div>
</>, </>,
); );
}, []); },
[],
);
const datasets = useMemo(() => { const datasets = useMemo(() => {
return [ return [
@ -54,7 +65,7 @@ export function FunnelChart({ className, loading }) {
]; ];
}, [data, colors, formatMessage, labels]); }, [data, colors, formatMessage, labels]);
if (loading) { if (isLoading) {
return <Loading icon="dots" className={styles.loading} />; return <Loading icon="dots" className={styles.loading} />;
} }
@ -63,7 +74,7 @@ export function FunnelChart({ className, loading }) {
className={className} className={className}
datasets={datasets} datasets={datasets}
unit="day" unit="day"
loading={loading} isLoading={isLoading}
renderXLabel={renderXLabel} renderXLabel={renderXLabel}
renderTooltipPopup={renderTooltipPopup} renderTooltipPopup={renderTooltipPopup}
XAxisType="category" XAxisType="category"

View File

@ -1,4 +1,4 @@
import { useContext, useRef } from 'react'; import { useContext } from 'react';
import { useMessages } from 'components/hooks'; import { useMessages } from 'components/hooks';
import { import {
Icon, Icon,
@ -21,13 +21,12 @@ import PopupForm from '../[id]/PopupForm';
export function FunnelParameters() { export function FunnelParameters() {
const { report, runReport, updateReport, isRunning } = useContext(ReportContext); const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const ref = useRef(null);
const { parameters } = report || {}; const { parameters } = report || {};
const { websiteId, dateRange, urls } = parameters || {}; const { websiteId, dateRange, urls } = parameters || {};
const queryDisabled = !websiteId || !dateRange || urls?.length < 2; const queryDisabled = !websiteId || !dateRange || urls?.length < 2;
const handleSubmit = (data, e) => { const handleSubmit = (data: any, e: any) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
if (!queryDisabled) { if (!queryDisabled) {
@ -35,11 +34,11 @@ export function FunnelParameters() {
} }
}; };
const handleAddUrl = url => { const handleAddUrl = (url: string) => {
updateReport({ parameters: { urls: parameters.urls.concat(url) } }); updateReport({ parameters: { urls: parameters.urls.concat(url) } });
}; };
const handleRemoveUrl = (index, e) => { const handleRemoveUrl = (index: number, e: any) => {
e.stopPropagation(); e.stopPropagation();
const urls = [...parameters.urls]; const urls = [...parameters.urls];
urls.splice(index, 1); urls.splice(index, 1);
@ -62,7 +61,7 @@ export function FunnelParameters() {
}; };
return ( return (
<Form ref={ref} values={parameters} onSubmit={handleSubmit} preventSubmit={true}> <Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
<BaseParameters /> <BaseParameters />
<FormRow label={formatMessage(labels.window)}> <FormRow label={formatMessage(labels.window)}>
<FormInput <FormInput
@ -73,7 +72,10 @@ export function FunnelParameters() {
</FormInput> </FormInput>
</FormRow> </FormRow>
<FormRow label={formatMessage(labels.urls)} action={<AddUrlButton />}> <FormRow label={formatMessage(labels.urls)} action={<AddUrlButton />}>
<ParameterList items={urls} onRemove={handleRemoveUrl} /> <ParameterList
items={urls}
onRemove={(index: number, e: any) => handleRemoveUrl(index, e)}
/>
</FormRow> </FormRow>
<FormButtons> <FormButtons>
<SubmitButton variant="primary" disabled={queryDisabled} isLoading={isRunning}> <SubmitButton variant="primary" disabled={queryDisabled} isLoading={isRunning}>

View File

@ -3,7 +3,12 @@ import { useMessages } from 'components/hooks';
import { Button, Form, FormRow, TextField, Flexbox } from 'react-basics'; import { Button, Form, FormRow, TextField, Flexbox } from 'react-basics';
import styles from './UrlAddForm.module.css'; import styles from './UrlAddForm.module.css';
export function UrlAddForm({ defaultValue = '', onAdd }) { export interface UrlAddFormProps {
defaultValue?: string;
onAdd?: (url: string) => void;
}
export function UrlAddForm({ defaultValue = '', onAdd }: UrlAddFormProps) {
const [url, setUrl] = useState(defaultValue); const [url, setUrl] = useState(defaultValue);
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();

View File

@ -1,4 +1,4 @@
import { useContext, useRef } from 'react'; import { useContext } from 'react';
import { useFormat, useMessages, useFilters } from 'components/hooks'; import { useFormat, useMessages, useFilters } from 'components/hooks';
import { import {
Form, Form,
@ -24,7 +24,6 @@ export function InsightsParameters() {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { formatValue } = useFormat(); const { formatValue } = useFormat();
const { filterLabels } = useFilters(); const { filterLabels } = useFilters();
const ref = useRef(null);
const { parameters } = report || {}; const { parameters } = report || {};
const { websiteId, dateRange, fields, filters } = parameters || {}; const { websiteId, dateRange, fields, filters } = parameters || {};
const { startDate, endDate } = dateRange || {}; const { startDate, endDate } = dateRange || {};
@ -72,7 +71,7 @@ export function InsightsParameters() {
updateReport({ parameters: { [id]: data } }); updateReport({ parameters: { [id]: data } });
}; };
const AddButton = ({ id }) => { const AddButton = ({ id, onAdd }) => {
return ( return (
<PopupTrigger> <PopupTrigger>
<TooltipPopup label={formatMessage(labels.add)} position="top"> <TooltipPopup label={formatMessage(labels.add)} position="top">
@ -84,8 +83,8 @@ export function InsightsParameters() {
<PopupForm> <PopupForm>
{id === 'fields' && ( {id === 'fields' && (
<FieldSelectForm <FieldSelectForm
items={fieldOptions} fields={fieldOptions}
onSelect={handleAdd.bind(null, id)} onSelect={onAdd.bind(null, id)}
showType={false} showType={false}
/> />
)} )}
@ -93,7 +92,7 @@ export function InsightsParameters() {
<FilterSelectForm <FilterSelectForm
websiteId={websiteId} websiteId={websiteId}
items={fieldOptions} items={fieldOptions}
onSelect={handleAdd.bind(null, id)} onSelect={onAdd.bind(null, id)}
/> />
)} )}
</PopupForm> </PopupForm>
@ -103,7 +102,7 @@ export function InsightsParameters() {
}; };
return ( return (
<Form ref={ref} values={parameters} onSubmit={handleSubmit}> <Form values={parameters} onSubmit={handleSubmit}>
<BaseParameters /> <BaseParameters />
{parametersSelected && {parametersSelected &&
parameterGroups.map(({ id, label }) => { parameterGroups.map(({ id, label }) => {

View File

@ -13,7 +13,7 @@ const defaultParameters = {
parameters: { fields: [], filters: [] }, parameters: { fields: [], filters: [] },
}; };
export default function InsightsReport({ reportId }) { export default function InsightsReport({ reportId }: { reportId: string }) {
return ( return (
<Report reportId={reportId} defaultParameters={defaultParameters}> <Report reportId={reportId} defaultParameters={defaultParameters}>
<ReportHeader icon={<Lightbulb />} /> <ReportHeader icon={<Lightbulb />} />

View File

@ -5,7 +5,7 @@ import { ReportContext } from '../[id]/Report';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
export function InsightsTable() { export function InsightsTable() {
const [fields, setFields] = useState(); const [fields, setFields] = useState([]);
const { report } = useContext(ReportContext); const { report } = useContext(ReportContext);
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { formatValue } = useFormat(); const { formatValue } = useFormat();
@ -37,10 +37,10 @@ export function InsightsTable() {
width="100px" width="100px"
alignment="end" alignment="end"
> >
{row => row.visitors.toLocaleString()} {row => row?.visitors?.toLocaleString()}
</GridColumn> </GridColumn>
<GridColumn name="views" label={formatMessage(labels.views)} width="100px" alignment="end"> <GridColumn name="views" label={formatMessage(labels.views)} width="100px" alignment="end">
{row => row.views.toLocaleString()} {row => row?.views?.toLocaleString()}
</GridColumn> </GridColumn>
</GridTable> </GridTable>
); );

View File

@ -1,7 +1,7 @@
import ReportsHeader from './ReportsHeader'; import ReportsHeader from './ReportsHeader';
import ReportsDataTable from './ReportsDataTable'; import ReportsDataTable from './ReportsDataTable';
export default function ReportsPage() { export default function () {
return ( return (
<> <>
<ReportsHeader /> <ReportsHeader />

View File

@ -1,4 +1,4 @@
import { useContext, useRef } from 'react'; import { useContext } from 'react';
import { useMessages } from 'components/hooks'; import { useMessages } from 'components/hooks';
import { Form, FormButtons, FormRow, SubmitButton } from 'react-basics'; import { Form, FormButtons, FormRow, SubmitButton } from 'react-basics';
import { ReportContext } from '../[id]/Report'; import { ReportContext } from '../[id]/Report';
@ -9,14 +9,13 @@ import { parseDateRange } from 'lib/date';
export function RetentionParameters() { export function RetentionParameters() {
const { report, runReport, isRunning, updateReport } = useContext(ReportContext); const { report, runReport, isRunning, updateReport } = useContext(ReportContext);
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const ref = useRef(null);
const { parameters } = report || {}; const { parameters } = report || {};
const { websiteId, dateRange } = parameters || {}; const { websiteId, dateRange } = parameters || {};
const { startDate } = dateRange || {}; const { startDate } = dateRange || {};
const queryDisabled = !websiteId || !dateRange; const queryDisabled = !websiteId || !dateRange;
const handleSubmit = (data, e) => { const handleSubmit = (data: any, e: any) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -30,7 +29,7 @@ export function RetentionParameters() {
}; };
return ( return (
<Form ref={ref} values={parameters} onSubmit={handleSubmit} preventSubmit={true}> <Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
<BaseParameters showDateSelect={false} /> <BaseParameters showDateSelect={false} />
<FormRow label={formatMessage(labels.date)}> <FormRow label={formatMessage(labels.date)}>
<MonthSelect date={startDate} onChange={handleDateChange} /> <MonthSelect date={startDate} onChange={handleDateChange} />

View File

@ -19,7 +19,7 @@ const defaultParameters = {
}, },
}; };
export default function RetentionReport({ reportId }) { export default function RetentionReport({ reportId }: { reportId: string }) {
return ( return (
<Report reportId={reportId} defaultParameters={defaultParameters}> <Report reportId={reportId} defaultParameters={defaultParameters}>
<ReportHeader icon={<Magnet />} /> <ReportHeader icon={<Magnet />} />

View File

@ -18,7 +18,7 @@ export function RetentionTable({ days = DAYS }) {
return <EmptyPlaceholder />; return <EmptyPlaceholder />;
} }
const rows = data.reduce((arr, row) => { const rows = data.reduce((arr: any[], row: { date: any; visitors: any; day: any }) => {
const { date, visitors, day } = row; const { date, visitors, day } = row;
if (day === 0) { if (day === 0) {
return arr.concat({ return arr.concat({

View File

@ -0,0 +1,5 @@
import { createContext } from 'react';
export const SettingsContext = createContext(null);
export default SettingsContext;

View File

@ -20,7 +20,7 @@ export default function SettingsLayout({ children }) {
const getKey = () => items.find(({ url }) => pathname === url)?.key; const getKey = () => items.find(({ url }) => pathname === url)?.key;
if (cloudMode && pathname != '/settings/profile') { if (cloudMode && pathname !== '/settings/profile') {
return null; return null;
} }

View File

@ -6,10 +6,12 @@ import useMessages from 'components/hooks/useMessages';
export function PasswordEditForm({ onSave, onClose }) { export function PasswordEditForm({ onSave, onClose }) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error, isLoading } = useMutation(data => post('/me/password', data)); const { mutate, error, isPending } = useMutation({
mutationFn: (data: any) => post('/me/password', data),
});
const ref = useRef(null); const ref = useRef(null);
const handleSubmit = async data => { const handleSubmit = async (data: any) => {
mutate(data, { mutate(data, {
onSuccess: async () => { onSuccess: async () => {
onSave(); onSave();
@ -18,7 +20,7 @@ export function PasswordEditForm({ onSave, onClose }) {
}); });
}; };
const samePassword = value => { const samePassword = (value: string) => {
if (value !== ref?.current?.getValues('newPassword')) { if (value !== ref?.current?.getValues('newPassword')) {
return formatMessage(messages.noMatchPassword); return formatMessage(messages.noMatchPassword);
} }
@ -56,7 +58,7 @@ export function PasswordEditForm({ onSave, onClose }) {
</FormInput> </FormInput>
</FormRow> </FormRow>
<FormButtons flex> <FormButtons flex>
<Button type="submit" variant="primary" disabled={isLoading}> <Button type="submit" variant="primary" disabled={isPending}>
{formatMessage(labels.save)} {formatMessage(labels.save)}
</Button> </Button>
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button> <Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>

View File

@ -1,4 +1,3 @@
import { useRef } from 'react';
import { import {
Form, Form,
FormRow, FormRow,
@ -12,11 +11,12 @@ import { setValue } from 'store/cache';
import useApi from 'components/hooks/useApi'; import useApi from 'components/hooks/useApi';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
export function TeamAddForm({ onSave, onClose }) { export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error, isLoading } = useMutation(data => post('/teams', data)); const { mutate, error, isPending } = useMutation({
const ref = useRef(null); mutationFn: (data: any) => post('/teams', data),
});
const handleSubmit = async data => { const handleSubmit = async data => {
mutate(data, { mutate(data, {
@ -29,17 +29,17 @@ export function TeamAddForm({ onSave, onClose }) {
}; };
return ( return (
<Form ref={ref} onSubmit={handleSubmit} error={error}> <Form onSubmit={handleSubmit} error={error}>
<FormRow label={formatMessage(labels.name)}> <FormRow label={formatMessage(labels.name)}>
<FormInput name="name" rules={{ required: formatMessage(labels.required) }}> <FormInput name="name" rules={{ required: formatMessage(labels.required) }}>
<TextField autoComplete="off" /> <TextField autoComplete="off" />
</FormInput> </FormInput>
</FormRow> </FormRow>
<FormButtons flex> <FormButtons flex>
<SubmitButton variant="primary" disabled={isLoading}> <SubmitButton variant="primary" disabled={isPending}>
{formatMessage(labels.save)} {formatMessage(labels.save)}
</SubmitButton> </SubmitButton>
<Button disabled={isLoading} onClick={onClose}> <Button disabled={isPending} onClick={onClose}>
{formatMessage(labels.cancel)} {formatMessage(labels.cancel)}
</Button> </Button>
</FormButtons> </FormButtons>

View File

@ -2,7 +2,15 @@ import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
import TeamDeleteForm from './TeamDeleteForm'; import TeamDeleteForm from './TeamDeleteForm';
export function TeamDeleteButton({ teamId, teamName, onDelete }) { export function TeamDeleteButton({
teamId,
teamName,
onDelete,
}: {
teamId: string;
teamName: string;
onDelete?: () => void;
}) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
return ( return (
@ -14,7 +22,7 @@ export function TeamDeleteButton({ teamId, teamName, onDelete }) {
<Text>{formatMessage(labels.delete)}</Text> <Text>{formatMessage(labels.delete)}</Text>
</Button> </Button>
<Modal title={formatMessage(labels.deleteTeam)}> <Modal title={formatMessage(labels.deleteTeam)}>
{close => ( {(close: any) => (
<TeamDeleteForm teamId={teamId} teamName={teamName} onSave={onDelete} onClose={close} /> <TeamDeleteForm teamId={teamId} teamName={teamName} onSave={onDelete} onClose={close} />
)} )}
</Modal> </Modal>

View File

@ -3,10 +3,22 @@ import useApi from 'components/hooks/useApi';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
import { setValue } from 'store/cache'; import { setValue } from 'store/cache';
export function TeamDeleteForm({ teamId, teamName, onSave, onClose }) { export function TeamDeleteForm({
teamId,
teamName,
onSave,
onClose,
}: {
teamId: string;
teamName: string;
onSave: () => void;
onClose: () => void;
}) {
const { formatMessage, labels, messages, FormattedMessage } = useMessages(); const { formatMessage, labels, messages, FormattedMessage } = useMessages();
const { del, useMutation } = useApi(); const { del, useMutation } = useApi();
const { mutate, error, isLoading } = useMutation(data => del(`/teams/${teamId}`, data)); const { mutate, error, isPending } = useMutation({
mutationFn: (data: any) => del(`/teams/${teamId}`, data),
});
const handleSubmit = async data => { const handleSubmit = async data => {
mutate(data, { mutate(data, {
@ -24,7 +36,7 @@ export function TeamDeleteForm({ teamId, teamName, onSave, onClose }) {
<FormattedMessage {...messages.confirmDelete} values={{ target: <b>{teamName}</b> }} /> <FormattedMessage {...messages.confirmDelete} values={{ target: <b>{teamName}</b> }} />
</p> </p>
<FormButtons flex> <FormButtons flex>
<SubmitButton variant="danger" disabled={isLoading}> <SubmitButton variant="danger" disabled={isPending}>
{formatMessage(labels.delete)} {formatMessage(labels.delete)}
</SubmitButton> </SubmitButton>
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button> <Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>

View File

@ -12,10 +12,10 @@ import useApi from 'components/hooks/useApi';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
import { setValue } from 'store/cache'; import { setValue } from 'store/cache';
export function TeamJoinForm({ onSave, onClose }) { export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
const { formatMessage, labels, getMessage } = useMessages(); const { formatMessage, labels, getMessage } = useMessages();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error } = useMutation(data => post('/teams/join', data)); const { mutate, error } = useMutation({ mutationFn: (data: any) => post('/teams/join', data) });
const ref = useRef(null); const ref = useRef(null);
const handleSubmit = async data => { const handleSubmit = async data => {

View File

@ -4,7 +4,15 @@ import useLocale from 'components/hooks/useLocale';
import useUser from 'components/hooks/useUser'; import useUser from 'components/hooks/useUser';
import TeamDeleteForm from './TeamLeaveForm'; import TeamDeleteForm from './TeamLeaveForm';
export function TeamLeaveButton({ teamId, teamName, onLeave }) { export function TeamLeaveButton({
teamId,
teamName,
onLeave,
}: {
teamId: string;
teamName: string;
onLeave?: () => void;
}) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { dir } = useLocale(); const { dir } = useLocale();
const { user } = useUser(); const { user } = useUser();

View File

@ -3,22 +3,33 @@ import useApi from 'components/hooks/useApi';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
import { setValue } from 'store/cache'; import { setValue } from 'store/cache';
export function TeamLeaveForm({ teamId, userId, teamName, onSave, onClose }) { export function TeamLeaveForm({
teamId,
userId,
teamName,
onSave,
onClose,
}: {
teamId: string;
userId: string;
teamName: string;
onSave: () => void;
onClose: () => void;
}) {
const { formatMessage, labels, messages, FormattedMessage } = useMessages(); const { formatMessage, labels, messages, FormattedMessage } = useMessages();
const { del, useMutation } = useApi(); const { del, useMutation } = useApi();
const { mutate, error, isLoading } = useMutation(() => del(`/teams/${teamId}/users/${userId}`)); const { mutate, error, isPending } = useMutation({
mutationFn: () => del(`/teams/${teamId}/users/${userId}`),
});
const handleSubmit = async () => { const handleSubmit = async () => {
mutate( mutate(null, {
{}, onSuccess: async () => {
{ setValue('team:members', Date.now());
onSuccess: async () => { onSave();
setValue('team:members', Date.now()); onClose();
onSave();
onClose();
},
}, },
); });
}; };
return ( return (
@ -27,7 +38,7 @@ export function TeamLeaveForm({ teamId, userId, teamName, onSave, onClose }) {
<FormattedMessage {...messages.confirmDelete} values={{ target: <b>{teamName}</b> }} /> <FormattedMessage {...messages.confirmDelete} values={{ target: <b>{teamName}</b> }} />
</p> </p>
<FormButtons flex> <FormButtons flex>
<SubmitButton variant="danger" disabled={isLoading}> <SubmitButton variant="danger" disabled={isPending}>
{formatMessage(labels.leave)} {formatMessage(labels.leave)}
</SubmitButton> </SubmitButton>
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button> <Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>

View File

@ -3,7 +3,7 @@ import Icons from 'components/icons';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
import TeamAddForm from './TeamAddForm'; import TeamAddForm from './TeamAddForm';
export function TeamsAddButton({ onAdd }) { export function TeamsAddButton({ onAdd }: { onAdd?: () => void }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
return ( return (
@ -15,7 +15,7 @@ export function TeamsAddButton({ onAdd }) {
<Text>{formatMessage(labels.createTeam)}</Text> <Text>{formatMessage(labels.createTeam)}</Text>
</Button> </Button>
<Modal title={formatMessage(labels.createTeam)}> <Modal title={formatMessage(labels.createTeam)}>
{close => <TeamAddForm onSave={onAdd} onClose={close} />} {(close: () => void) => <TeamAddForm onSave={onAdd} onClose={close} />}
</Modal> </Modal>
</ModalTrigger> </ModalTrigger>
); );

View File

@ -7,11 +7,14 @@ import useCache from 'store/cache';
export function TeamsDataTable() { export function TeamsDataTable() {
const { get } = useApi(); const { get } = useApi();
const modified = useCache(state => state?.teams); const modified = useCache((state: any) => state?.teams);
const queryResult = useFilterQuery(['teams', { modified }], params => { const queryResult = useFilterQuery({
return get(`/teams`, { queryKey: ['teams', { modified }],
...params, queryFn: (params: any) => {
}); return get(`/teams`, {
...params,
});
},
}); });
return ( return (

View File

@ -7,7 +7,7 @@ import { Button, GridColumn, GridTable, Icon, Icons, Text, useBreakpoint } from
import TeamDeleteButton from './TeamDeleteButton'; import TeamDeleteButton from './TeamDeleteButton';
import TeamLeaveButton from './TeamLeaveButton'; import TeamLeaveButton from './TeamLeaveButton';
export function TeamsTable({ data = [] }) { export function TeamsTable({ data = [] }: { data: any[] }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { user } = useUser(); const { user } = useUser();
const breakpoint = useBreakpoint(); const breakpoint = useBreakpoint();

View File

@ -1,7 +1,15 @@
import { Button, Icon, Icons, Text } from 'react-basics'; import { Button, Icon, Icons, Text } from 'react-basics';
import styles from './WebsiteTags.module.css'; import styles from './WebsiteTags.module.css';
export function WebsiteTags({ items = [], websites = [], onClick }) { export function WebsiteTags({
items = [],
websites = [],
onClick,
}: {
items: any[];
websites: any[];
onClick: (e: Event) => void;
}) {
if (websites.length === 0) { if (websites.length === 0) {
return null; return null;
} }

View File

@ -18,15 +18,17 @@ const generateId = () => getRandomChars(16);
export function TeamEditForm({ teamId, data, onSave, readOnly }) { export function TeamEditForm({ teamId, data, onSave, readOnly }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error } = useMutation(data => post(`/teams/${teamId}`, data)); const { mutate, error } = useMutation({
mutationFn: (data: any) => post(`/teams/${teamId}`, data),
});
const ref = useRef(null); const ref = useRef(null);
const [accessCode, setAccessCode] = useState(data.accessCode); const [accessCode, setAccessCode] = useState(data.accessCode);
const handleSubmit = async data => { const handleSubmit = async (data: any) => {
mutate(data, { mutate(data, {
onSuccess: async () => { onSuccess: async () => {
ref.current.reset(data); ref.current.reset(data);
onSave(data); onSave?.(data);
}, },
}); });
}; };

View File

@ -3,28 +3,37 @@ import useMessages from 'components/hooks/useMessages';
import { Icon, Icons, LoadingButton, Text } from 'react-basics'; import { Icon, Icons, LoadingButton, Text } from 'react-basics';
import { setValue } from 'store/cache'; import { setValue } from 'store/cache';
export function TeamMemberRemoveButton({ teamId, userId, disabled, onSave }) { export function TeamMemberRemoveButton({
teamId,
userId,
disabled,
onSave,
}: {
teamId: string;
userId: string;
disabled?: boolean;
onSave?: () => void;
}) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { del, useMutation } = useApi(); const { del, useMutation } = useApi();
const { mutate, isLoading } = useMutation(() => del(`/teams/${teamId}/users/${userId}`)); const { mutate, isPending } = useMutation({
mutationFn: () => del(`/teams/${teamId}/users/${userId}`),
});
const handleRemoveTeamMember = () => { const handleRemoveTeamMember = () => {
mutate( mutate(null, {
{}, onSuccess: () => {
{ setValue('team:members', Date.now());
onSuccess: () => { onSave?.();
setValue('team:members', Date.now());
onSave?.();
},
}, },
); });
}; };
return ( return (
<LoadingButton <LoadingButton
onClick={() => handleRemoveTeamMember()} onClick={() => handleRemoveTeamMember()}
disabled={disabled} disabled={disabled}
isLoading={isLoading} isLoading={isPending}
> >
<Icon> <Icon>
<Icons.Close /> <Icons.Close />

View File

@ -4,18 +4,18 @@ import useFilterQuery from 'components/hooks/useFilterQuery';
import DataTable from 'components/common/DataTable'; import DataTable from 'components/common/DataTable';
import useCache from 'store/cache'; import useCache from 'store/cache';
export function TeamMembers({ teamId, readOnly }) { export function TeamMembers({ teamId, readOnly }: { teamId: string; readOnly: boolean }) {
const { get } = useApi(); const { get } = useApi();
const modified = useCache(state => state?.['team:members']); const modified = useCache(state => state?.['team:members']);
const queryResult = useFilterQuery( const queryResult = useFilterQuery({
['team:members', { teamId, modified }], queryKey: ['team:members', { teamId, modified }],
params => { queryFn: params => {
return get(`/teams/${teamId}/users`, { return get(`/teams/${teamId}/users`, {
...params, ...params,
}); });
}, },
{ enabled: !!teamId }, enabled: !!teamId,
); });
return ( return (
<> <>

View File

@ -4,7 +4,15 @@ import useUser from 'components/hooks/useUser';
import { ROLES } from 'lib/constants'; import { ROLES } from 'lib/constants';
import TeamMemberRemoveButton from './TeamMemberRemoveButton'; import TeamMemberRemoveButton from './TeamMemberRemoveButton';
export function TeamMembersTable({ data = [], teamId, readOnly }) { export function TeamMembersTable({
data = [],
teamId,
readOnly,
}: {
data: any[];
teamId: string;
readOnly: boolean;
}) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { user } = useUser(); const { user } = useUser();
const breakpoint = useBreakpoint(); const breakpoint = useBreakpoint();

View File

@ -10,22 +10,22 @@ import TeamEditForm from './TeamEditForm';
import TeamMembers from './TeamMembers'; import TeamMembers from './TeamMembers';
import TeamWebsites from './TeamWebsites'; import TeamWebsites from './TeamWebsites';
export function TeamSettings({ teamId }) { export function TeamSettings({ teamId }: { teamId: string }) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { user } = useUser(); const { user } = useUser();
const [values, setValues] = useState(null); const [values, setValues] = useState(null);
const [tab, setTab] = useState('details'); const [tab, setTab] = useState('details');
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { showToast } = useToasts(); const { showToast } = useToasts();
const { data, isLoading } = useQuery( const { data, isLoading } = useQuery({
['team', teamId], queryKey: ['team', teamId],
() => { queryFn: () => {
if (teamId) { if (teamId) {
return get(`/teams/${teamId}`); return get(`/teams/${teamId}`);
} }
}, },
{ cacheTime: 0 }, gcTime: 0,
); });
const canEdit = data?.teamUser?.find( const canEdit = data?.teamUser?.find(
({ userId, role }) => role === ROLES.teamOwner && userId === user.id, ({ userId, role }) => role === ROLES.teamOwner && userId === user.id,
); );
@ -48,7 +48,7 @@ export function TeamSettings({ teamId }) {
return ( return (
<Flexbox direction="column"> <Flexbox direction="column">
<PageHeader title={values?.name} /> <PageHeader title={values?.name} />
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30 }}> <Tabs selectedKey={tab} onSelect={(value: any) => setTab(value)} style={{ marginBottom: 30 }}>
<Item key="details">{formatMessage(labels.details)}</Item> <Item key="details">{formatMessage(labels.details)}</Item>
<Item key="members">{formatMessage(labels.members)}</Item> <Item key="members">{formatMessage(labels.members)}</Item>
<Item key="websites">{formatMessage(labels.websites)}</Item> <Item key="websites">{formatMessage(labels.websites)}</Item>

View File

@ -2,15 +2,30 @@ import useApi from 'components/hooks/useApi';
import { useState } from 'react'; import { useState } from 'react';
import { Button, Form, FormButtons, GridColumn, Loading, SubmitButton, Toggle } from 'react-basics'; import { Button, Form, FormButtons, GridColumn, Loading, SubmitButton, Toggle } from 'react-basics';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
import WebsitesDataTable from '../../websites/WebsitesDataTable'; import WebsitesDataTable from 'app/(main)/settings/websites/WebsitesDataTable';
import Empty from 'components/common/Empty'; import Empty from 'components/common/Empty';
import { setValue } from 'store/cache'; import { setValue } from 'store/cache';
import { useUser } from 'components/hooks';
export function TeamWebsiteAddForm({ teamId, onSave, onClose }) { export function TeamWebsiteAddForm({
teamId,
onSave,
onClose,
}: {
teamId: string;
onSave: () => void;
onClose: () => void;
}) {
const { user } = useUser();
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { get, post, useQuery, useMutation } = useApi(); const { get, post, useQuery, useMutation } = useApi();
const { mutate, error } = useMutation(data => post(`/teams/${teamId}/websites`, data)); const { mutate, error } = useMutation({
const { data: websites, isLoading } = useQuery(['websites'], () => get('/websites')); mutationFn: (data: any) => post(`/teams/${teamId}/websites`, data),
});
const { data: websites, isLoading } = useQuery({
queryKey: ['websites'],
queryFn: () => get('/websites'),
});
const [selected, setSelected] = useState([]); const [selected, setSelected] = useState([]);
const hasData = websites && websites.data.length > 0; const hasData = websites && websites.data.length > 0;
@ -37,7 +52,7 @@ export function TeamWebsiteAddForm({ teamId, onSave, onClose }) {
{!isLoading && !hasData && <Empty />} {!isLoading && !hasData && <Empty />}
{hasData && ( {hasData && (
<Form onSubmit={handleSubmit} error={error}> <Form onSubmit={handleSubmit} error={error}>
<WebsitesDataTable showHeader={false} showActions={false}> <WebsitesDataTable userId={user.id} showActions={false}>
<GridColumn name="select" label={formatMessage(labels.selectWebsite)} alignment="end"> <GridColumn name="select" label={formatMessage(labels.selectWebsite)} alignment="end">
{row => ( {row => (
<Toggle <Toggle

View File

@ -5,10 +5,12 @@ import { Icon, Icons, LoadingButton, Text } from 'react-basics';
export function TeamWebsiteRemoveButton({ teamId, websiteId, onSave }) { export function TeamWebsiteRemoveButton({ teamId, websiteId, onSave }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { del, useMutation } = useApi(); const { del, useMutation } = useApi();
const { mutate, isLoading } = useMutation(() => del(`/teams/${teamId}/websites/${websiteId}`)); const { mutate, isPending } = useMutation({
mutationFn: () => del(`/teams/${teamId}/websites/${websiteId}`),
});
const handleRemoveTeamMember = async () => { const handleRemoveTeamMember = async () => {
await mutate(null, { mutate(null, {
onSuccess: () => { onSuccess: () => {
onSave(); onSave();
}, },
@ -16,7 +18,7 @@ export function TeamWebsiteRemoveButton({ teamId, websiteId, onSave }) {
}; };
return ( return (
<LoadingButton variant="quiet" onClick={() => handleRemoveTeamMember()} isLoading={isLoading}> <LoadingButton variant="quiet" onClick={() => handleRemoveTeamMember()} isLoading={isPending}>
<Icon> <Icon>
<Icons.Close /> <Icons.Close />
</Icon> </Icon>

View File

@ -8,23 +8,23 @@ import useFilterQuery from 'components/hooks/useFilterQuery';
import DataTable from 'components/common/DataTable'; import DataTable from 'components/common/DataTable';
import useCache from 'store/cache'; import useCache from 'store/cache';
export function TeamWebsites({ teamId }) { export function TeamWebsites({ teamId, readOnly }: { teamId: string; readOnly: boolean }) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { user } = useUser(); const { user } = useUser();
const { get } = useApi(); const { get } = useApi();
const modified = useCache(state => state?.['team:websites']); const modified = useCache(state => state?.['team:websites']);
const queryResult = useFilterQuery( const queryResult = useFilterQuery({
['team:websites', { teamId, modified }], queryKey: ['team:websites', { teamId, modified }],
params => { queryFn: params => {
return get(`/teams/${teamId}/websites`, { return get(`/teams/${teamId}/websites`, {
...params, ...params,
}); });
}, },
{ enabled: !!user }, enabled: !!user,
); });
const handleChange = () => { const handleChange = () => {
queryResult.refetch(); queryResult.query.refetch();
}; };
return ( return (
@ -43,7 +43,9 @@ export function TeamWebsites({ teamId }) {
</ModalTrigger> </ModalTrigger>
</ActionForm> </ActionForm>
<DataTable queryResult={queryResult}> <DataTable queryResult={queryResult}>
{({ data }) => <TeamWebsitesTable data={data} onRemove={handleChange} />} {({ data }) => (
<TeamWebsitesTable data={data} onRemove={handleChange} readOnly={readOnly} />
)}
</DataTable> </DataTable>
</> </>
); );

View File

@ -4,7 +4,15 @@ import useMessages from 'components/hooks/useMessages';
import useUser from 'components/hooks/useUser'; import useUser from 'components/hooks/useUser';
import TeamWebsiteRemoveButton from './TeamWebsiteRemoveButton'; import TeamWebsiteRemoveButton from './TeamWebsiteRemoveButton';
export function TeamWebsitesTable({ data = [], onRemove }) { export function TeamWebsitesTable({
data = [],
readOnly,
onRemove,
}: {
data: any[];
readOnly: boolean;
onRemove: () => void;
}) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { user } = useUser(); const { user } = useUser();
@ -17,7 +25,7 @@ export function TeamWebsitesTable({ data = [], onRemove }) {
const { id: teamId, teamUser } = row.teamWebsite[0].team; const { id: teamId, teamUser } = row.teamWebsite[0].team;
const { id: websiteId, userId } = row; const { id: websiteId, userId } = row;
const owner = teamUser[0]; const owner = teamUser[0];
const canRemove = user.id === userId || user.id === owner.userId; const canRemove = !readOnly && (user.id === userId || user.id === owner.userId);
return ( return (
<> <>
{canRemove && ( {canRemove && (

View File

@ -1,27 +0,0 @@
import { Button, Icon, Text, Modal, Icons, ModalTrigger } from 'react-basics';
import UserAddForm from './UserAddForm';
import useMessages from 'components/hooks/useMessages';
export function UserAddButton({ onSave }) {
const { formatMessage, labels } = useMessages();
const handleSave = () => {
onSave();
};
return (
<ModalTrigger>
<Button variant="primary">
<Icon>
<Icons.Plus />
</Icon>
<Text>{formatMessage(labels.createUser)}</Text>
</Button>
<Modal title={formatMessage(labels.createUser)}>
{close => <UserAddForm onSave={handleSave} onClose={close} />}
</Modal>
</ModalTrigger>
);
}
export default UserAddButton;

View File

@ -0,0 +1,31 @@
import { Button, Icon, Text, Modal, Icons, ModalTrigger, useToasts } from 'react-basics';
import UserAddForm from './UserAddForm';
import useMessages from 'components/hooks/useMessages';
import { setValue } from 'store/cache';
export function UserAddButton({ onSave }: { onSave?: () => void }) {
const { formatMessage, labels, messages } = useMessages();
const { showToast } = useToasts();
const handleSave = () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' });
setValue('users', Date.now());
onSave?.();
};
return (
<ModalTrigger>
<Button variant="primary">
<Icon>
<Icons.Plus />
</Icon>
<Text>{formatMessage(labels.createUser)}</Text>
</Button>
<Modal title={formatMessage(labels.createUser)}>
{(close: () => void) => <UserAddForm onSave={handleSave} onClose={close} />}
</Modal>
</ModalTrigger>
);
}
export default UserAddButton;

View File

@ -16,10 +16,12 @@ import useMessages from 'components/hooks/useMessages';
export function UserAddForm({ onSave, onClose }) { export function UserAddForm({ onSave, onClose }) {
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error, isLoading } = useMutation(data => post(`/users`, data)); const { mutate, error, isPending } = useMutation({
mutationFn: (data: any) => post(`/users`, data),
});
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const handleSubmit = async data => { const handleSubmit = async (data: any) => {
mutate(data, { mutate(data, {
onSuccess: async () => { onSuccess: async () => {
onSave(data); onSave(data);
@ -65,7 +67,7 @@ export function UserAddForm({ onSave, onClose }) {
<SubmitButton variant="primary" disabled={false}> <SubmitButton variant="primary" disabled={false}>
{formatMessage(labels.save)} {formatMessage(labels.save)}
</SubmitButton> </SubmitButton>
<Button disabled={isLoading} onClick={onClose}> <Button disabled={isPending} onClick={onClose}>
{formatMessage(labels.cancel)} {formatMessage(labels.cancel)}
</Button> </Button>
</FormButtons> </FormButtons>

View File

@ -3,7 +3,15 @@ import useMessages from 'components/hooks/useMessages';
import useUser from 'components/hooks/useUser'; import useUser from 'components/hooks/useUser';
import UserDeleteForm from './UserDeleteForm'; import UserDeleteForm from './UserDeleteForm';
export function UserDeleteButton({ userId, username, onDelete }) { export function UserDeleteButton({
userId,
username,
onDelete,
}: {
userId: string;
username: string;
onDelete?: () => void;
}) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { user } = useUser(); const { user } = useUser();
@ -16,7 +24,7 @@ export function UserDeleteButton({ userId, username, onDelete }) {
<Text>{formatMessage(labels.delete)}</Text> <Text>{formatMessage(labels.delete)}</Text>
</Button> </Button>
<Modal title={formatMessage(labels.deleteUser)}> <Modal title={formatMessage(labels.deleteUser)}>
{close => ( {(close: () => void) => (
<UserDeleteForm userId={userId} username={username} onSave={onDelete} onClose={close} /> <UserDeleteForm userId={userId} username={username} onSave={onDelete} onClose={close} />
)} )}
</Modal> </Modal>

View File

@ -1,14 +1,13 @@
import { useMutation } from '@tanstack/react-query';
import { Button, Form, FormButtons, SubmitButton } from 'react-basics'; import { Button, Form, FormButtons, SubmitButton } from 'react-basics';
import useApi from 'components/hooks/useApi'; import useApi from 'components/hooks/useApi';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
export function UserDeleteForm({ userId, username, onSave, onClose }) { export function UserDeleteForm({ userId, username, onSave, onClose }) {
const { formatMessage, FormattedMessage, labels, messages } = useMessages(); const { formatMessage, FormattedMessage, labels, messages } = useMessages();
const { del } = useApi(); const { del, useMutation } = useApi();
const { mutate, error, isLoading } = useMutation(() => del(`/users/${userId}`)); const { mutate, error, isPending } = useMutation({ mutationFn: () => del(`/users/${userId}`) });
const handleSubmit = async data => { const handleSubmit = async (data: any) => {
mutate(data, { mutate(data, {
onSuccess: async () => { onSuccess: async () => {
onSave(); onSave();
@ -23,10 +22,10 @@ export function UserDeleteForm({ userId, username, onSave, onClose }) {
<FormattedMessage {...messages.confirmDelete} values={{ target: <b>{username}</b> }} /> <FormattedMessage {...messages.confirmDelete} values={{ target: <b>{username}</b> }} />
</p> </p>
<FormButtons flex> <FormButtons flex>
<SubmitButton variant="danger" disabled={isLoading}> <SubmitButton variant="danger" disabled={isPending}>
{formatMessage(labels.delete)} {formatMessage(labels.delete)}
</SubmitButton> </SubmitButton>
<Button disabled={isLoading} onClick={onClose}> <Button disabled={isPending} onClick={onClose}>
{formatMessage(labels.cancel)} {formatMessage(labels.cancel)}
</Button> </Button>
</FormButtons> </FormButtons>

View File

@ -13,14 +13,30 @@ import useApi from 'components/hooks/useApi';
import { ROLES } from 'lib/constants'; import { ROLES } from 'lib/constants';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
export function UserEditForm({ userId, data, onSave }) { export function UserEditForm({
userId,
data,
onSave,
}: {
userId: string;
data: any[];
onSave: (data: any) => void;
}) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error } = useMutation(({ username, password, role }) => const { mutate, error } = useMutation({
post(`/users/${userId}`, { username, password, role }), mutationFn: ({
); username,
password,
role,
}: {
username: string;
password: string;
role: string;
}) => post(`/users/${userId}`, { username, password, role }),
});
const handleSubmit = async data => { const handleSubmit = async (data: any) => {
mutate(data, { mutate(data, {
onSuccess: async () => { onSuccess: async () => {
onSave(data); onSave(data);

View File

@ -1,36 +0,0 @@
import Page from 'components/layout/Page';
import useApi from 'components/hooks/useApi';
import WebsitesTable from 'app/(main)/settings/websites/WebsitesTable';
import useApiFilter from 'components/hooks/useApiFilter';
export function UserWebsites({ userId }) {
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
useApiFilter();
const { get, useQuery } = useApi();
const { data, isLoading, error } = useQuery(
['user:websites', userId, filter, page, pageSize],
() =>
get(`/users/${userId}/websites`, {
filter,
page,
pageSize,
}),
);
const hasData = data && data.length !== 0;
return (
<Page loading={isLoading} error={error}>
{hasData && (
<WebsitesTable
data={data.data}
onFilterChange={handleFilterChange}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
filterValue={filter}
/>
)}
</Page>
);
}
export default UserWebsites;

View File

@ -0,0 +1,26 @@
import Page from 'components/layout/Page';
import useApi from 'components/hooks/useApi';
import WebsitesTable from 'app/(main)/settings/websites/WebsitesTable';
import useFilterQuery from 'components/hooks/useFilterQuery';
import DataTable from 'components/common/DataTable';
export function UserWebsites({ userId }) {
const { get } = useApi();
const queryResult = useFilterQuery({
queryKey: ['user:websites', userId],
queryFn: (params: any) => get(`/users/${userId}/websites`, params),
});
const hasData = queryResult.result && queryResult.result.data.length !== 0;
return (
<Page isLoading={queryResult.query.isLoading} error={queryResult.query.error}>
{hasData && (
<DataTable queryResult={queryResult}>
{({ data }) => <WebsitesTable data={data} />}
</DataTable>
)}
</Page>
);
}
export default UserWebsites;

View File

@ -8,11 +8,10 @@ import useCache from 'store/cache';
export function UsersDataTable() { export function UsersDataTable() {
const { get } = useApi(); const { get } = useApi();
const modified = useCache(state => state?.users); const modified = useCache((state: any) => state?.users);
const queryResult = useFilterQuery(['users', { modified }], params => { const queryResult = useFilterQuery({
return get(`/users`, { queryKey: ['users', { modified }],
...params, queryFn: (params: { [key: string]: any }) => get(`/admin/users`, params),
});
}); });
return ( return (

View File

@ -3,7 +3,7 @@ import PageHeader from 'components/layout/PageHeader';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
import UserAddButton from './UserAddButton'; import UserAddButton from './UserAddButton';
export function UsersHeader({ onAdd }) { export function UsersHeader({ onAdd }: { onAdd?: () => void }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
return ( return (

View File

@ -6,7 +6,7 @@ import useMessages from 'components/hooks/useMessages';
import useLocale from 'components/hooks/useLocale'; import useLocale from 'components/hooks/useLocale';
import UserDeleteButton from './UserDeleteButton'; import UserDeleteButton from './UserDeleteButton';
export function UsersTable({ data = [] }) { export function UsersTable({ data = [] }: { data: any[] }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { dateLocale } = useLocale(); const { dateLocale } = useLocale();
const breakpoint = useBreakpoint(); const breakpoint = useBreakpoint();

View File

@ -1,30 +1,30 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { Key, useEffect, useState } from 'react';
import { Item, Loading, Tabs, useToasts } from 'react-basics'; import { Item, Loading, Tabs, useToasts } from 'react-basics';
import UserEditForm from '../UserEditForm'; import UserEditForm from '../UserEditForm';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import useApi from 'components/hooks/useApi'; import useApi from 'components/hooks/useApi';
import UserWebsites from '../UserWebsites';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
import UserWebsites from '../UserWebsites';
export function UserSettings({ userId }) { export function UserSettings({ userId }) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const [edit, setEdit] = useState(false); const [edit, setEdit] = useState(false);
const [values, setValues] = useState(null); const [values, setValues] = useState(null);
const [tab, setTab] = useState('details'); const [tab, setTab] = useState<Key>('details');
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { showToast } = useToasts(); const { showToast } = useToasts();
const { data, isLoading } = useQuery( const { data, isLoading } = useQuery({
['user', userId], queryKey: ['user', userId],
() => { queryFn: () => {
if (userId) { if (userId) {
return get(`/users/${userId}`); return get(`/users/${userId}`);
} }
}, },
{ cacheTime: 0 }, gcTime: 0,
); });
const handleSave = data => { const handleSave = (data: any) => {
showToast({ message: formatMessage(messages.saved), variant: 'success' }); showToast({ message: formatMessage(messages.saved), variant: 'success' });
if (data) { if (data) {
setValues(state => ({ ...state, ...data })); setValues(state => ({ ...state, ...data }));
@ -42,7 +42,7 @@ export function UserSettings({ userId }) {
}, [data]); }, [data]);
if (isLoading || !values) { if (isLoading || !values) {
return <Loading size="lg" />; return <Loading />;
} }
return ( return (

View File

@ -3,7 +3,7 @@ import WebsiteAddForm from './WebsiteAddForm';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
import { setValue } from 'store/cache'; import { setValue } from 'store/cache';
export function WebsiteAddButton({ onSave }) { export function WebsiteAddButton({ onSave }: { onSave?: () => void }) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { showToast } = useToasts(); const { showToast } = useToasts();
@ -22,7 +22,7 @@ export function WebsiteAddButton({ onSave }) {
<Text>{formatMessage(labels.addWebsite)}</Text> <Text>{formatMessage(labels.addWebsite)}</Text>
</Button> </Button>
<Modal title={formatMessage(labels.addWebsite)}> <Modal title={formatMessage(labels.addWebsite)}>
{close => <WebsiteAddForm onSave={handleSave} onClose={close} />} {(close: () => void) => <WebsiteAddForm onSave={handleSave} onClose={close} />}
</Modal> </Modal>
</ModalTrigger> </ModalTrigger>
); );

View File

@ -10,17 +10,22 @@ import {
import useApi from 'components/hooks/useApi'; import useApi from 'components/hooks/useApi';
import { DOMAIN_REGEX } from 'lib/constants'; import { DOMAIN_REGEX } from 'lib/constants';
import useMessages from 'components/hooks/useMessages'; import useMessages from 'components/hooks/useMessages';
import { useContext } from 'react';
import SettingsContext from '../SettingsContext';
export function WebsiteAddForm({ onSave, onClose }) { export function WebsiteAddForm({ onSave, onClose }: { onSave?: () => void; onClose?: () => void }) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { websitesUrl } = useContext(SettingsContext);
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error, isLoading } = useMutation(data => post('/websites', data)); const { mutate, error, isPending } = useMutation({
mutationFn: (data: any) => post(websitesUrl, data),
});
const handleSubmit = async data => { const handleSubmit = async (data: any) => {
mutate(data, { mutate(data, {
onSuccess: async () => { onSuccess: async () => {
onSave(); onSave?.();
onClose(); onClose?.();
}, },
}); });
}; };
@ -47,9 +52,11 @@ export function WebsiteAddForm({ onSave, onClose }) {
<SubmitButton variant="primary" disabled={false}> <SubmitButton variant="primary" disabled={false}>
{formatMessage(labels.save)} {formatMessage(labels.save)}
</SubmitButton> </SubmitButton>
<Button disabled={isLoading} onClick={onClose}> {onClose && (
{formatMessage(labels.cancel)} <Button disabled={isPending} onClick={onClose}>
</Button> {formatMessage(labels.cancel)}
</Button>
)}
</FormButtons> </FormButtons>
</Form> </Form>
); );

Some files were not shown because too many files have changed in this diff Show More