mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-22 09:57:00 +01:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
defd6ac5c0
@ -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": {
|
||||||
|
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -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
|
||||||
|
1
.github/workflows/stale-issues.yml
vendored
1
.github/workflows/stale-issues.yml
vendored
@ -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
|
||||||
|
@ -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 .
|
||||||
|
@ -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;
|
||||||
|
@ -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:
|
||||||
|
@ -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',
|
||||||
|
28
package.json
28
package.json
@ -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",
|
||||||
|
@ -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;
|
@ -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>
|
||||||
);
|
);
|
@ -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>
|
@ -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 (
|
@ -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>
|
@ -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 />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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?.();
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
14
src/app/(main)/reports/ReportsDataTable.tsx
Normal file
14
src/app/(main)/reports/ReportsDataTable.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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();
|
@ -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) } } });
|
||||||
};
|
};
|
||||||
|
|
@ -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();
|
||||||
};
|
};
|
@ -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 });
|
||||||
};
|
};
|
||||||
|
|
@ -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>
|
@ -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>
|
@ -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) {
|
@ -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 (
|
@ -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)}
|
@ -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;
|
|
31
src/app/(main)/reports/[id]/Report.tsx
Normal file
31
src/app/(main)/reports/[id]/Report.tsx
Normal 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;
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
@ -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 });
|
||||||
};
|
};
|
||||||
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
@ -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 &&
|
@ -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 />} />
|
@ -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"
|
@ -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}>
|
@ -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();
|
||||||
|
|
@ -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 }) => {
|
@ -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 />} />
|
@ -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>
|
||||||
);
|
);
|
@ -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 />
|
||||||
|
@ -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} />
|
@ -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 />} />
|
@ -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({
|
5
src/app/(main)/settings/SettingsContext.tsx
Normal file
5
src/app/(main)/settings/SettingsContext.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export const SettingsContext = createContext(null);
|
||||||
|
|
||||||
|
export default SettingsContext;
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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 => {
|
@ -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();
|
@ -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>
|
@ -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>
|
||||||
);
|
);
|
@ -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 (
|
@ -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();
|
@ -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;
|
||||||
}
|
}
|
@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
@ -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 />
|
@ -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 (
|
||||||
<>
|
<>
|
@ -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();
|
@ -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>
|
@ -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
|
@ -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>
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
@ -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 && (
|
@ -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;
|
|
31
src/app/(main)/settings/users/UserAddButton.tsx
Normal file
31
src/app/(main)/settings/users/UserAddButton.tsx
Normal 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;
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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);
|
@ -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;
|
|
26
src/app/(main)/settings/users/UserWebsites.tsx
Normal file
26
src/app/(main)/settings/users/UserWebsites.tsx
Normal 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;
|
@ -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 (
|
@ -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 (
|
@ -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();
|
@ -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 (
|
@ -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>
|
||||||
);
|
);
|
@ -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
Loading…
Reference in New Issue
Block a user