mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-22 09:57:00 +01:00
Merge branch 'master' into dev
This commit is contained in:
commit
f93584092a
@ -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
|
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 .
|
||||||
|
@ -72,13 +72,13 @@ docker compose up -d
|
|||||||
Alternatively, to pull just the Umami Docker image with PostgreSQL support:
|
Alternatively, to pull just the Umami Docker image with PostgreSQL support:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull docker.umami.dev/umami-software/umami:postgresql-latest
|
docker pull ghcr.io/umami-software/umami:postgresql-latest
|
||||||
```
|
```
|
||||||
|
|
||||||
Or with MySQL support:
|
Or with MySQL support:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull docker.umami.dev/umami-software/umami:mysql-latest
|
docker pull ghcr.io/umami-software/umami:mysql-latest
|
||||||
```
|
```
|
||||||
|
|
||||||
## Getting updates
|
## Getting updates
|
||||||
|
@ -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` (
|
||||||
|
@ -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:
|
||||||
|
1
next-env.d.ts
vendored
1
next-env.d.ts
vendored
@ -1,5 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
/// <reference types="next/navigation-types/compat/navigation" />
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||||
|
@ -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';
|
`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(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -74,16 +73,23 @@ if (process.env.CLOUD_MODE && process.env.CLOUD_URL && process.env.DISABLE_LOGIN
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const basePath = process.env.BASE_PATH;
|
||||||
|
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
|
reactStrictMode: false,
|
||||||
env: {
|
env: {
|
||||||
cloudMode: process.env.CLOUD_MODE,
|
basePath: basePath || '',
|
||||||
cloudUrl: process.env.CLOUD_URL,
|
cloudMode: process.env.CLOUD_MODE || '',
|
||||||
|
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 || '',
|
||||||
isProduction: process.env.NODE_ENV === 'production',
|
disableLogin: process.env.DISABLE_LOGIN || '',
|
||||||
|
disableUI: process.env.DISABLE_UI || '',
|
||||||
|
hostUrl: process.env.HOST_URL || '',
|
||||||
},
|
},
|
||||||
basePath: process.env.BASE_PATH,
|
basePath,
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true,
|
ignoreDuringBuilds: true,
|
||||||
@ -92,11 +98,23 @@ const config = {
|
|||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
webpack(config) {
|
webpack(config) {
|
||||||
config.module.rules.push({
|
const fileLoaderRule = config.module.rules.find(rule => rule.test?.test?.('.svg'));
|
||||||
test: /\.svg$/,
|
|
||||||
issuer: /\.{js|jsx|ts|tsx}$/,
|
config.module.rules.push(
|
||||||
use: ['@svgr/webpack'],
|
{
|
||||||
});
|
...fileLoaderRule,
|
||||||
|
test: /\.svg$/i,
|
||||||
|
resourceQuery: /url/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.svg$/i,
|
||||||
|
issuer: fileLoaderRule.issuer,
|
||||||
|
resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] },
|
||||||
|
use: ['@svgr/webpack'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
fileLoaderRule.exclude = /\.svg$/i;
|
||||||
|
|
||||||
config.resolve.alias['public'] = path.resolve('./public');
|
config.resolve.alias['public'] = path.resolve('./public');
|
||||||
|
|
||||||
|
46
package.json
46
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "umami",
|
"name": "umami",
|
||||||
"version": "2.7.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",
|
||||||
@ -61,16 +61,18 @@
|
|||||||
".next/cache"
|
".next/cache"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@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",
|
||||||
"@tanstack/react-query": "^4.33.0",
|
"@prisma/extension-read-replicas": "^0.3.0",
|
||||||
"@umami/prisma-client": "^0.2.0",
|
"@react-spring/web": "^9.7.3",
|
||||||
"@umami/redis-client": "^0.15.0",
|
"@tanstack/react-query": "^5.12.2",
|
||||||
|
"@umami/prisma-client": "^0.8.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",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"clickhouse": "^2.5.0",
|
|
||||||
"colord": "^2.9.2",
|
"colord": "^2.9.2",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.3",
|
||||||
@ -91,22 +93,22 @@
|
|||||||
"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.2",
|
"next": "14.0.4",
|
||||||
"next-basics": "^0.36.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.6.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-basics": "^0.100.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": "^5.24.7",
|
"react-intl": "^6.5.5",
|
||||||
"react-simple-maps": "^2.3.0",
|
"react-simple-maps": "^2.3.0",
|
||||||
"react-spring": "^9.4.4",
|
|
||||||
"react-use-measure": "^2.0.4",
|
"react-use-measure": "^2.0.4",
|
||||||
"react-window": "^1.8.6",
|
"react-window": "^1.8.6",
|
||||||
"request-ip": "^3.3.0",
|
"request-ip": "^3.3.0",
|
||||||
"semver": "^7.5.2",
|
"semver": "^7.5.4",
|
||||||
"thenby": "^1.3.4",
|
"thenby": "^1.3.4",
|
||||||
"timezone-support": "^2.0.2",
|
"timezone-support": "^2.0.2",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
@ -123,12 +125,13 @@
|
|||||||
"@rollup/plugin-node-resolve": "^15.2.0",
|
"@rollup/plugin-node-resolve": "^15.2.0",
|
||||||
"@rollup/plugin-replace": "^5.0.2",
|
"@rollup/plugin-replace": "^5.0.2",
|
||||||
"@svgr/rollup": "^8.1.0",
|
"@svgr/rollup": "^8.1.0",
|
||||||
"@svgr/webpack": "^6.2.1",
|
"@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",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.50.0",
|
"@types/react-window": "^1.8.8",
|
||||||
"@typescript-eslint/parser": "^5.50.0",
|
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||||
|
"@typescript-eslint/parser": "^6.7.3",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"esbuild": "^0.17.17",
|
"esbuild": "^0.17.17",
|
||||||
"eslint": "^8.33.0",
|
"eslint": "^8.33.0",
|
||||||
@ -138,15 +141,14 @@
|
|||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"extract-react-intl-messages": "^4.1.1",
|
"extract-react-intl-messages": "^4.1.1",
|
||||||
"husky": "^7.0.0",
|
"husky": "^8.0.3",
|
||||||
"lint-staged": "^11.0.0",
|
"lint-staged": "^14.0.1",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.31",
|
||||||
"postcss-flexbugs-fixes": "^5.0.2",
|
"postcss-flexbugs-fixes": "^5.0.2",
|
||||||
"postcss-import": "^15.1.0",
|
"postcss-import": "^15.1.0",
|
||||||
"postcss-preset-env": "7.8.3",
|
"postcss-preset-env": "7.8.3",
|
||||||
"postcss-rtlcss": "^4.0.1",
|
"postcss-rtlcss": "^4.0.1",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.6.2",
|
||||||
"prisma": "5.3.1",
|
|
||||||
"prompts": "2.4.2",
|
"prompts": "2.4.2",
|
||||||
"rollup": "^3.28.0",
|
"rollup": "^3.28.0",
|
||||||
"rollup-plugin-copy": "^3.4.0",
|
"rollup-plugin-copy": "^3.4.0",
|
||||||
|
BIN
public/images/os/windows-mobile.png
Normal file
BIN
public/images/os/windows-mobile.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
@ -104,7 +104,7 @@
|
|||||||
"label.browser": [
|
"label.browser": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Browser"
|
"value": "Navegador"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.browsers": [
|
"label.browsers": [
|
||||||
@ -134,7 +134,7 @@
|
|||||||
"label.city": [
|
"label.city": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "City"
|
"value": "Ciudad"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.clear-all": [
|
"label.clear-all": [
|
||||||
@ -176,19 +176,19 @@
|
|||||||
"label.country": [
|
"label.country": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Country"
|
"value": "País"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.create": [
|
"label.create": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Create"
|
"value": "Crear"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.create-report": [
|
"label.create-report": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Crear reporte"
|
"value": "Crear informe"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.create-team": [
|
"label.create-team": [
|
||||||
@ -236,7 +236,7 @@
|
|||||||
"label.date": [
|
"label.date": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Date"
|
"value": "Fecha"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.date-range": [
|
"label.date-range": [
|
||||||
@ -248,7 +248,7 @@
|
|||||||
"label.day": [
|
"label.day": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Day"
|
"value": "Día"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.default-date-range": [
|
"label.default-date-range": [
|
||||||
@ -284,7 +284,7 @@
|
|||||||
"label.description": [
|
"label.description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Descripciones"
|
"value": "Descripción"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.desktop": [
|
"label.desktop": [
|
||||||
@ -302,7 +302,7 @@
|
|||||||
"label.device": [
|
"label.device": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Device"
|
"value": "Dispositivo"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.devices": [
|
"label.devices": [
|
||||||
@ -314,7 +314,7 @@
|
|||||||
"label.dismiss": [
|
"label.dismiss": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Ignorar"
|
"value": "Cerrar"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.does-not-contain": [
|
"label.does-not-contain": [
|
||||||
@ -332,7 +332,7 @@
|
|||||||
"label.dropoff": [
|
"label.dropoff": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Dropoff"
|
"value": "Abandono"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.edit": [
|
"label.edit": [
|
||||||
@ -374,7 +374,7 @@
|
|||||||
"label.false": [
|
"label.false": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "False"
|
"value": "Falso"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.field": [
|
"label.field": [
|
||||||
@ -392,7 +392,7 @@
|
|||||||
"label.filter": [
|
"label.filter": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Filter"
|
"value": "Filtro"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.filter-combined": [
|
"label.filter-combined": [
|
||||||
@ -422,7 +422,7 @@
|
|||||||
"label.funnel-description": [
|
"label.funnel-description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Understand the conversion and drop-off rate of users."
|
"value": "Comprender conversión y abandono de usuarios."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.greater-than": [
|
"label.greater-than": [
|
||||||
@ -470,7 +470,7 @@
|
|||||||
"label.is-set": [
|
"label.is-set": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Is set"
|
"value": "Está establecido"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.join": [
|
"label.join": [
|
||||||
@ -600,7 +600,7 @@
|
|||||||
"label.my-websites": [
|
"label.my-websites": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "My websites"
|
"value": "Mis sitios web"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.name": [
|
"label.name": [
|
||||||
@ -624,7 +624,7 @@
|
|||||||
"label.os": [
|
"label.os": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "OS"
|
"value": "Sistema"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.overview": [
|
"label.overview": [
|
||||||
@ -642,7 +642,7 @@
|
|||||||
"label.page-of": [
|
"label.page-of": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Page "
|
"value": "Página "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -650,7 +650,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": " of "
|
"value": " de "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -666,7 +666,7 @@
|
|||||||
"label.pageTitle": [
|
"label.pageTitle": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Page title"
|
"value": "Título de página"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.pages": [
|
"label.pages": [
|
||||||
@ -684,7 +684,7 @@
|
|||||||
"label.powered-by": [
|
"label.powered-by": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Con la ayuda de "
|
"value": "Analíticas de "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -706,7 +706,7 @@
|
|||||||
"label.query": [
|
"label.query": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Query"
|
"value": "Consulta"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.query-parameters": [
|
"label.query-parameters": [
|
||||||
@ -724,7 +724,7 @@
|
|||||||
"label.referrer": [
|
"label.referrer": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Referrer"
|
"value": "Referido"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.referrers": [
|
"label.referrers": [
|
||||||
@ -766,7 +766,7 @@
|
|||||||
"label.reports": [
|
"label.reports": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Reportes"
|
"value": "Informes"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.required": [
|
"label.required": [
|
||||||
@ -784,19 +784,19 @@
|
|||||||
"label.reset-website": [
|
"label.reset-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Reiniciar estadísticas"
|
"value": "Reiniciar analíticas"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.retention": [
|
"label.retention": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Retention"
|
"value": "Retención"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.retention-description": [
|
"label.retention-description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Measure your website stickiness by tracking how often users return."
|
"value": "Medir la frecuencia con la que los usuarios vuelven a tu sitio web."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.role": [
|
"label.role": [
|
||||||
@ -826,7 +826,7 @@
|
|||||||
"label.search": [
|
"label.search": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Search"
|
"value": "Buscar"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.select-date": [
|
"label.select-date": [
|
||||||
@ -850,7 +850,7 @@
|
|||||||
"label.settings": [
|
"label.settings": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Configuraciones"
|
"value": "Ajustes"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.share-url": [
|
"label.share-url": [
|
||||||
@ -892,7 +892,7 @@
|
|||||||
"label.team-id": [
|
"label.team-id": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "ID de equipo"
|
"value": "ID del equipo"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.team-member": [
|
"label.team-member": [
|
||||||
@ -904,7 +904,7 @@
|
|||||||
"label.team-name": [
|
"label.team-name": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Team name"
|
"value": "Nombre del equipo"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.team-owner": [
|
"label.team-owner": [
|
||||||
@ -916,7 +916,7 @@
|
|||||||
"label.team-websites": [
|
"label.team-websites": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Team websites"
|
"value": "Sitios web del equipo"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.teams": [
|
"label.teams": [
|
||||||
@ -1288,7 +1288,7 @@
|
|||||||
"message.new-version-available": [
|
"message.new-version-available": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "A new version of Umami "
|
"value": "Una nueva versión de Umami "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1296,7 +1296,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": " is available!"
|
"value": " está disponible"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.no-data-available": [
|
"message.no-data-available": [
|
||||||
@ -1376,7 +1376,7 @@
|
|||||||
"message.saved": [
|
"message.saved": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Guardado."
|
"value": "Guardado"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.share-url": [
|
"message.share-url": [
|
||||||
|
@ -20,13 +20,13 @@
|
|||||||
"label.add": [
|
"label.add": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Add"
|
"value": "Нэмэх"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.add-description": [
|
"label.add-description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Add description"
|
"value": "Тайлбар нэмэх"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.add-website": [
|
"label.add-website": [
|
||||||
@ -44,7 +44,7 @@
|
|||||||
"label.after": [
|
"label.after": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "After"
|
"value": "Хойно"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.all": [
|
"label.all": [
|
||||||
@ -68,7 +68,7 @@
|
|||||||
"label.average": [
|
"label.average": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Average"
|
"value": "Дундаж"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.average-visit-time": [
|
"label.average-visit-time": [
|
||||||
@ -86,7 +86,7 @@
|
|||||||
"label.before": [
|
"label.before": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Before"
|
"value": "Өмнө"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.bounce-rate": [
|
"label.bounce-rate": [
|
||||||
@ -98,13 +98,13 @@
|
|||||||
"label.breakdown": [
|
"label.breakdown": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Breakdown"
|
"value": "Задаргаа"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.browser": [
|
"label.browser": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Browser"
|
"value": "Хөтөч"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.browsers": [
|
"label.browsers": [
|
||||||
@ -134,7 +134,7 @@
|
|||||||
"label.city": [
|
"label.city": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "City"
|
"value": "Хот"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.clear-all": [
|
"label.clear-all": [
|
||||||
@ -158,7 +158,7 @@
|
|||||||
"label.contains": [
|
"label.contains": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Contains"
|
"value": "Агуулах"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.continue": [
|
"label.continue": [
|
||||||
@ -176,19 +176,19 @@
|
|||||||
"label.country": [
|
"label.country": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Country"
|
"value": "Улс"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.create": [
|
"label.create": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Create"
|
"value": "Үүсгэх"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.create-report": [
|
"label.create-report": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Create report"
|
"value": "Тайлан үүсгэх"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.create-team": [
|
"label.create-team": [
|
||||||
@ -236,7 +236,7 @@
|
|||||||
"label.date": [
|
"label.date": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Date"
|
"value": "Огноо"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.date-range": [
|
"label.date-range": [
|
||||||
@ -248,7 +248,7 @@
|
|||||||
"label.day": [
|
"label.day": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Day"
|
"value": "Өдөр"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.default-date-range": [
|
"label.default-date-range": [
|
||||||
@ -284,7 +284,7 @@
|
|||||||
"label.description": [
|
"label.description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Description"
|
"value": "Тайлбар"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.desktop": [
|
"label.desktop": [
|
||||||
@ -302,7 +302,7 @@
|
|||||||
"label.device": [
|
"label.device": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Device"
|
"value": "Төхөөрөмж"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.devices": [
|
"label.devices": [
|
||||||
@ -320,7 +320,7 @@
|
|||||||
"label.does-not-contain": [
|
"label.does-not-contain": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Does not contain"
|
"value": "Агуулахгүй"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.domain": [
|
"label.domain": [
|
||||||
@ -332,7 +332,7 @@
|
|||||||
"label.dropoff": [
|
"label.dropoff": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Dropoff"
|
"value": "Уналт"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.edit": [
|
"label.edit": [
|
||||||
@ -356,13 +356,13 @@
|
|||||||
"label.event": [
|
"label.event": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Event"
|
"value": "Үйлдэл"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.event-data": [
|
"label.event-data": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Event data"
|
"value": "Үйлдлийн өгөгдөл"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.events": [
|
"label.events": [
|
||||||
@ -374,25 +374,25 @@
|
|||||||
"label.false": [
|
"label.false": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "False"
|
"value": "Худал"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.field": [
|
"label.field": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Field"
|
"value": "Талбар"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.fields": [
|
"label.fields": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Fields"
|
"value": "Талбар"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.filter": [
|
"label.filter": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Filter"
|
"value": "Шүүлтүүр"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.filter-combined": [
|
"label.filter-combined": [
|
||||||
@ -410,67 +410,67 @@
|
|||||||
"label.filters": [
|
"label.filters": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Filters"
|
"value": "Шүүлтүүр"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.funnel": [
|
"label.funnel": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Funnel"
|
"value": "Цутгал"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.funnel-description": [
|
"label.funnel-description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Understand the conversion and drop-off rate of users."
|
"value": "Хэрэглэгчдийн шилжилт, уналтын хэмжээг шижнлэх."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.greater-than": [
|
"label.greater-than": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Greater than"
|
"value": "Их"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.greater-than-equals": [
|
"label.greater-than-equals": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Greater than or equals"
|
"value": "Их буюу тэнцүү"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.insights": [
|
"label.insights": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Insights"
|
"value": "Шинжлэх"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.insights-description": [
|
"label.insights-description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Dive deeper into your data by using segments and filters."
|
"value": "Өгөгдлөө хэсэгчлэн хуваах, шүүх байдлаар задлах шинжлэх."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.is": [
|
"label.is": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Is"
|
"value": "Бол"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.is-not": [
|
"label.is-not": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Is not"
|
"value": "Биш"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.is-not-set": [
|
"label.is-not-set": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Is not set"
|
"value": "Утга оноогоогүй"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.is-set": [
|
"label.is-set": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Is set"
|
"value": "Утга оноосон"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.join": [
|
"label.join": [
|
||||||
@ -546,13 +546,13 @@
|
|||||||
"label.less-than": [
|
"label.less-than": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Less than"
|
"value": "Бага"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.less-than-equals": [
|
"label.less-than-equals": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Less than or equals"
|
"value": "Бага буюу тэнцүү"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.login": [
|
"label.login": [
|
||||||
@ -600,7 +600,7 @@
|
|||||||
"label.my-websites": [
|
"label.my-websites": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "My websites"
|
"value": "Миний вебүүд"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.name": [
|
"label.name": [
|
||||||
@ -630,7 +630,7 @@
|
|||||||
"label.overview": [
|
"label.overview": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Overview"
|
"value": "Тойм"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.owner": [
|
"label.owner": [
|
||||||
@ -642,19 +642,19 @@
|
|||||||
"label.page-of": [
|
"label.page-of": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Page "
|
"value": "Хуудас "
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": 1,
|
|
||||||
"value": "current"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": 0,
|
|
||||||
"value": " of "
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
"value": "total"
|
"value": "total"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "-с "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "current"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.page-views": [
|
"label.page-views": [
|
||||||
@ -666,7 +666,7 @@
|
|||||||
"label.pageTitle": [
|
"label.pageTitle": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Page title"
|
"value": "Хуудасны гарчиг"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.pages": [
|
"label.pages": [
|
||||||
@ -724,7 +724,7 @@
|
|||||||
"label.referrer": [
|
"label.referrer": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Referrer"
|
"value": "Чиглүүлэгч"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.referrers": [
|
"label.referrers": [
|
||||||
@ -748,7 +748,7 @@
|
|||||||
"label.region": [
|
"label.region": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Region"
|
"value": "Бүс"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.regions": [
|
"label.regions": [
|
||||||
@ -766,7 +766,7 @@
|
|||||||
"label.reports": [
|
"label.reports": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Reports"
|
"value": "Тайлан"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.required": [
|
"label.required": [
|
||||||
@ -790,13 +790,13 @@
|
|||||||
"label.retention": [
|
"label.retention": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Retention"
|
"value": "Барилт"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.retention-description": [
|
"label.retention-description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Measure your website stickiness by tracking how often users return."
|
"value": "Хэрэглэгчид таны веб рүү дахин хандах буюу хэрэглэгчидээ хэр тогтоож буйг хэмжих."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.role": [
|
"label.role": [
|
||||||
@ -808,7 +808,7 @@
|
|||||||
"label.run-query": [
|
"label.run-query": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Run query"
|
"value": "Query ажиллуулах"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.save": [
|
"label.save": [
|
||||||
@ -826,13 +826,13 @@
|
|||||||
"label.search": [
|
"label.search": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Search"
|
"value": "Хайх"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.select-date": [
|
"label.select-date": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Select date"
|
"value": "Огноо сонгох"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.select-website": [
|
"label.select-website": [
|
||||||
@ -868,7 +868,7 @@
|
|||||||
"label.sum": [
|
"label.sum": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Sum"
|
"value": "Нийлбэр"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.tablet": [
|
"label.tablet": [
|
||||||
@ -904,7 +904,7 @@
|
|||||||
"label.team-name": [
|
"label.team-name": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Team name"
|
"value": "Багийн нэр"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.team-owner": [
|
"label.team-owner": [
|
||||||
@ -916,7 +916,7 @@
|
|||||||
"label.team-websites": [
|
"label.team-websites": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Team websites"
|
"value": "Багийн вебүүд"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.teams": [
|
"label.teams": [
|
||||||
@ -976,13 +976,13 @@
|
|||||||
"label.total": [
|
"label.total": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Total"
|
"value": "Нийт"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.total-records": [
|
"label.total-records": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Total records"
|
"value": "Нийт мөриийн тоо"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.tracking-code": [
|
"label.tracking-code": [
|
||||||
@ -994,13 +994,13 @@
|
|||||||
"label.true": [
|
"label.true": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "True"
|
"value": "Үнэн"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.type": [
|
"label.type": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Type"
|
"value": "Төрөл"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.unique": [
|
"label.unique": [
|
||||||
@ -1024,7 +1024,7 @@
|
|||||||
"label.untitled": [
|
"label.untitled": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Untitled"
|
"value": "Гарчиггүй"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.url": [
|
"label.url": [
|
||||||
@ -1060,7 +1060,7 @@
|
|||||||
"label.value": [
|
"label.value": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Value"
|
"value": "Утга"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.view": [
|
"label.view": [
|
||||||
@ -1078,7 +1078,7 @@
|
|||||||
"label.view-only": [
|
"label.view-only": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "View only"
|
"value": "Зөвхөн үзэх"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.views": [
|
"label.views": [
|
||||||
@ -1096,7 +1096,7 @@
|
|||||||
"label.website": [
|
"label.website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Website"
|
"value": "Веб"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.website-id": [
|
"label.website-id": [
|
||||||
@ -1114,7 +1114,7 @@
|
|||||||
"label.window": [
|
"label.window": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Window"
|
"value": "Цонх"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.yesterday": [
|
"label.yesterday": [
|
||||||
@ -1210,7 +1210,7 @@
|
|||||||
"message.delete-account": [
|
"message.delete-account": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "To delete this account, type "
|
"value": "Энэ бүртгэлийг устгахын тулд доорх хэсэгт "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1218,13 +1218,13 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": " in the box below to confirm."
|
"value": " гэж бичиж баталгаажуулна уу."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.delete-website": [
|
"message.delete-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "To delete this website, type "
|
"value": "Энэ вебийг устгахын тулд доорх хэсэгт "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1232,7 +1232,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": " in the box below to confirm."
|
"value": " гэж бичиж баталгаажуулна уу."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.delete-website-warning": [
|
"message.delete-website-warning": [
|
||||||
@ -1296,7 +1296,7 @@
|
|||||||
"message.new-version-available": [
|
"message.new-version-available": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "A new version of Umami "
|
"value": "Umami-н шинэ хувилбар "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1304,7 +1304,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": " is available!"
|
"value": " гарсан байна!"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.no-data-available": [
|
"message.no-data-available": [
|
||||||
@ -1316,7 +1316,7 @@
|
|||||||
"message.no-event-data": [
|
"message.no-event-data": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "No event data is available."
|
"value": "Үйлдлийн өгөгдөл алга."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.no-match-password": [
|
"message.no-match-password": [
|
||||||
@ -1328,7 +1328,7 @@
|
|||||||
"message.no-results-found": [
|
"message.no-results-found": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "No results were found."
|
"value": "Ямар ч үр дүн олдсонгүй."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.no-team-websites": [
|
"message.no-team-websites": [
|
||||||
|
@ -182,7 +182,7 @@
|
|||||||
"label.create": [
|
"label.create": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Create"
|
"value": "创建"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.create-report": [
|
"label.create-report": [
|
||||||
@ -380,19 +380,19 @@
|
|||||||
"label.field": [
|
"label.field": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Field"
|
"value": "字段"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.fields": [
|
"label.fields": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Fields"
|
"value": "字段"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.filter": [
|
"label.filter": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Filter"
|
"value": "筛选器"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.filter-combined": [
|
"label.filter-combined": [
|
||||||
@ -422,19 +422,19 @@
|
|||||||
"label.funnel-description": [
|
"label.funnel-description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Understand the conversion and drop-off rate of users."
|
"value": "了解用户的转换率和退出率。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.greater-than": [
|
"label.greater-than": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Greater than"
|
"value": "大于"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.greater-than-equals": [
|
"label.greater-than-equals": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Greater than or equals"
|
"value": "大于或等于"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.insights": [
|
"label.insights": [
|
||||||
@ -446,7 +446,7 @@
|
|||||||
"label.insights-description": [
|
"label.insights-description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Dive deeper into your data by using segments and filters."
|
"value": "通过使用筛选器和划分时间段来更深入地研究数据。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.is": [
|
"label.is": [
|
||||||
@ -804,7 +804,7 @@
|
|||||||
"label.retention-description": [
|
"label.retention-description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Measure your website stickiness by tracking how often users return."
|
"value": "通过跟踪用户返回的频率来衡量网站的用户粘性。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.role": [
|
"label.role": [
|
||||||
@ -834,7 +834,7 @@
|
|||||||
"label.search": [
|
"label.search": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Search"
|
"value": "搜索"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.select-date": [
|
"label.select-date": [
|
||||||
|
@ -182,7 +182,7 @@
|
|||||||
"label.create": [
|
"label.create": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Create"
|
"value": "建立"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.create-report": [
|
"label.create-report": [
|
||||||
@ -392,7 +392,7 @@
|
|||||||
"label.filter": [
|
"label.filter": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Filter"
|
"value": "篩選器"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.filter-combined": [
|
"label.filter-combined": [
|
||||||
@ -422,7 +422,7 @@
|
|||||||
"label.funnel-description": [
|
"label.funnel-description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Understand the conversion and drop-off rate of users."
|
"value": "瞭解使用者的轉換率和退出率"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.greater-than": [
|
"label.greater-than": [
|
||||||
@ -446,7 +446,7 @@
|
|||||||
"label.insights-description": [
|
"label.insights-description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Dive deeper into your data by using segments and filters."
|
"value": "透過使用區段和篩選器來深入探索你的數據"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.is": [
|
"label.is": [
|
||||||
@ -800,7 +800,7 @@
|
|||||||
"label.retention-description": [
|
"label.retention-description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Measure your website stickiness by tracking how often users return."
|
"value": "透過追蹤使用者回訪的頻率來衡量您的網站黏著度。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.role": [
|
"label.role": [
|
||||||
|
@ -1 +0,0 @@
|
|||||||
{"name":"analytics","short_name":"analytics","display":"standalone","start_url":"/","icons":[{"src":"/manifest/favicon-192.png","type":"image/png","sizes":"192x192"},{"src":"/manifest/favicon-512.png","type":"image/png","sizes":"512x512"}]}
|
|
10
public/manifest/site.webmanifest
Normal file
10
public/manifest/site.webmanifest
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "analytics",
|
||||||
|
"short_name": "analytics",
|
||||||
|
"display": "standalone",
|
||||||
|
"start_url": "/",
|
||||||
|
"icons": [
|
||||||
|
{ "src": "/manifest/favicon-192.png", "type": "image/png", "sizes": "192x192" },
|
||||||
|
{ "src": "/manifest/favicon-512.png", "type": "image/png", "sizes": "512x512" }
|
||||||
|
]
|
||||||
|
}
|
@ -19,6 +19,7 @@ const customResolver = resolve({
|
|||||||
|
|
||||||
const aliasConfig = {
|
const aliasConfig = {
|
||||||
entries: [
|
entries: [
|
||||||
|
{ find: /^app/, replacement: path.resolve('./src/app') },
|
||||||
{ find: /^components/, replacement: path.resolve('./src/components') },
|
{ find: /^components/, replacement: path.resolve('./src/components') },
|
||||||
{ find: /^hooks/, replacement: path.resolve('./src/hooks') },
|
{ find: /^hooks/, replacement: path.resolve('./src/hooks') },
|
||||||
{ find: /^lib/, replacement: path.resolve('./src/lib') },
|
{ find: /^lib/, replacement: path.resolve('./src/lib') },
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
const cli = require('next/dist/cli/next-start');
|
const cli = require('next/dist/cli/next-start');
|
||||||
|
|
||||||
cli.nextStart(['-p', process.env.PORT || 3000, '-H', process.env.HOSTNAME || '0.0.0.0']);
|
cli.nextStart({
|
||||||
|
'--port': process.env.PORT || 3000,
|
||||||
|
'--hostname': process.env.HOSTNAME || '0.0.0.0',
|
||||||
|
_: [],
|
||||||
|
});
|
||||||
|
36
src/app/(main)/App.tsx
Normal file
36
src/app/(main)/App.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
'use client';
|
||||||
|
import { Loading } from 'react-basics';
|
||||||
|
import Script from 'next/script';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { useLogin, useConfig } from 'components/hooks';
|
||||||
|
import UpdateNotice from './UpdateNotice';
|
||||||
|
|
||||||
|
export function App({ children }) {
|
||||||
|
const { user, isLoading, error } = useLogin();
|
||||||
|
const config = useConfig();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
window.location.href = `${process.env.basePath || ''}/login`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user || !config) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<UpdateNotice user={user} config={config} />
|
||||||
|
{process.env.NODE_ENV === 'production' && !pathname.includes('/share/') && (
|
||||||
|
<Script src={`telemetry.js`} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
@ -1,7 +1,7 @@
|
|||||||
.navbar {
|
.navbar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: max-content 1fr 1fr;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
background: var(--base75);
|
background: var(--base75);
|
||||||
@ -9,17 +9,6 @@
|
|||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.left,
|
|
||||||
.right {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -35,29 +24,24 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 30px;
|
gap: 30px;
|
||||||
padding: 0 40px;
|
padding: 0 40px;
|
||||||
flex: 1;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
max-height: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.links a {
|
.links a,
|
||||||
display: flex;
|
.links a:active,
|
||||||
align-items: center;
|
.links a:visited {
|
||||||
gap: 10px;
|
|
||||||
line-height: 60px;
|
|
||||||
color: var(--font-color200);
|
color: var(--font-color200);
|
||||||
|
line-height: 60px;
|
||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.links span {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.links a:hover {
|
.links a:hover {
|
||||||
color: var(--font-color100);
|
color: var(--font-color100);
|
||||||
border-bottom: 2px solid var(--primary400);
|
border-bottom: 2px solid var(--primary400);
|
||||||
}
|
}
|
||||||
|
|
||||||
.links .selected {
|
.links a.selected {
|
||||||
color: var(--font-color100);
|
color: var(--font-color100);
|
||||||
border-bottom: 2px solid var(--primary400);
|
border-bottom: 2px solid var(--primary400);
|
||||||
}
|
}
|
||||||
@ -68,7 +52,6 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
min-width: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile {
|
.mobile {
|
||||||
@ -76,6 +59,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 768px) {
|
@media only screen and (max-width: 768px) {
|
||||||
|
.navbar {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
.links,
|
.links,
|
||||||
.actions {
|
.actions {
|
||||||
display: none;
|
display: none;
|
93
src/app/(main)/NavBar.tsx
Normal file
93
src/app/(main)/NavBar.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
'use client';
|
||||||
|
import { Icon, Text } from 'react-basics';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import Icons from 'components/icons';
|
||||||
|
import ThemeButton from 'components/input/ThemeButton';
|
||||||
|
import LanguageButton from 'components/input/LanguageButton';
|
||||||
|
import ProfileButton from 'components/input/ProfileButton';
|
||||||
|
import useMessages from 'components/hooks/useMessages';
|
||||||
|
import HamburgerButton from 'components/common/HamburgerButton';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import styles from './NavBar.module.css';
|
||||||
|
|
||||||
|
export function NavBar() {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const cloudMode = Boolean(process.env.cloudMode);
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{ label: formatMessage(labels.dashboard), url: '/dashboard' },
|
||||||
|
{ label: formatMessage(labels.websites), url: '/websites' },
|
||||||
|
{ label: formatMessage(labels.reports), url: '/reports' },
|
||||||
|
{ label: formatMessage(labels.settings), url: '/settings' },
|
||||||
|
].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 (
|
||||||
|
<div className={styles.navbar}>
|
||||||
|
<div className={styles.logo}>
|
||||||
|
<Icon size="lg">
|
||||||
|
<Icons.Logo />
|
||||||
|
</Icon>
|
||||||
|
<Text>umami</Text>
|
||||||
|
</div>
|
||||||
|
<div className={styles.links}>
|
||||||
|
{links.map(({ url, label }) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={url}
|
||||||
|
href={url}
|
||||||
|
className={classNames({ [styles.selected]: pathname.startsWith(url) })}
|
||||||
|
>
|
||||||
|
<Text>{label}</Text>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<ThemeButton />
|
||||||
|
<LanguageButton />
|
||||||
|
<ProfileButton />
|
||||||
|
</div>
|
||||||
|
<div className={styles.mobile}>
|
||||||
|
<HamburgerButton menuItems={menuItems} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NavBar;
|
@ -1,17 +1,18 @@
|
|||||||
|
'use client';
|
||||||
import { useEffect, useCallback, useState } from 'react';
|
import { useEffect, useCallback, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { Button, Row, Column } from 'react-basics';
|
import { Button } from 'react-basics';
|
||||||
import { setItem } from 'next-basics';
|
import { setItem } from 'next-basics';
|
||||||
import useStore, { checkVersion } from 'store/version';
|
import useStore, { checkVersion } from 'store/version';
|
||||||
import { REPO_URL, VERSION_CHECK } from 'lib/constants';
|
import { REPO_URL, VERSION_CHECK } from 'lib/constants';
|
||||||
import styles from './UpdateNotice.module.css';
|
import styles from './UpdateNotice.module.css';
|
||||||
import useMessages from 'components/hooks/useMessages';
|
import useMessages from 'components/hooks/useMessages';
|
||||||
import { useRouter } from 'next/router';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
export function UpdateNotice({ user, config }) {
|
export function UpdateNotice({ user, config }) {
|
||||||
const { formatMessage, labels, messages } = useMessages();
|
const { formatMessage, labels, messages } = useMessages();
|
||||||
const { latest, checked, hasUpdate, releaseUrl } = useStore();
|
const { latest, checked, hasUpdate, releaseUrl } = useStore();
|
||||||
const { pathname } = useRouter();
|
const pathname = usePathname();
|
||||||
const [dismissed, setDismissed] = useState(checked);
|
const [dismissed, setDismissed] = useState(checked);
|
||||||
const allowUpdate =
|
const allowUpdate =
|
||||||
user?.isAdmin &&
|
user?.isAdmin &&
|
||||||
@ -46,17 +47,17 @@ export function UpdateNotice({ user, config }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<Row className={styles.notice}>
|
<div className={styles.notice}>
|
||||||
<Column variant="two" className={styles.message}>
|
<div className={styles.message}>
|
||||||
{formatMessage(messages.newVersionAvailable, { version: `v${latest}` })}
|
{formatMessage(messages.newVersionAvailable, { version: `v${latest}` })}
|
||||||
</Column>
|
</div>
|
||||||
<Column className={styles.buttons}>
|
<div className={styles.buttons}>
|
||||||
<Button variant="primary" onClick={handleViewClick}>
|
<Button variant="primary" onClick={handleViewClick}>
|
||||||
{formatMessage(labels.viewDetails)}
|
{formatMessage(labels.viewDetails)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleDismissClick}>{formatMessage(labels.dismiss)}</Button>
|
<Button onClick={handleDismissClick}>{formatMessage(labels.dismiss)}</Button>
|
||||||
</Column>
|
</div>
|
||||||
</Row>,
|
</div>,
|
||||||
document.body,
|
document.body,
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,33 +1,33 @@
|
|||||||
|
'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 'components/pages/websites/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 useNavigation from 'components/hooks/useNavigation';
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import Script from 'next/script';
|
|
||||||
import { Button, Column, Row } from 'react-basics';
|
|
||||||
import styles from './TestConsole.module.css';
|
import styles from './TestConsole.module.css';
|
||||||
|
|
||||||
export function TestConsole() {
|
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({
|
||||||
const router = useRouter();
|
queryKey: ['websites:me'],
|
||||||
const {
|
queryFn: () => get('/me/websites'),
|
||||||
basePath,
|
});
|
||||||
query: { id },
|
const { router } = useNavigation();
|
||||||
} = router;
|
|
||||||
|
|
||||||
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',
|
||||||
@ -47,7 +47,7 @@ export function TestConsole() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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,11 +71,10 @@ export function TestConsole() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [websiteId] = id || [];
|
|
||||||
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>
|
||||||
@ -86,12 +85,12 @@ export function TestConsole() {
|
|||||||
<>
|
<>
|
||||||
<Script
|
<Script
|
||||||
async
|
async
|
||||||
data-website-id={website.id}
|
data-website-id={websiteId}
|
||||||
src={`${basePath}/script.js`}
|
src={`${process.env.basePath}/script.js`}
|
||||||
data-cache="true"
|
data-cache="true"
|
||||||
/>
|
/>
|
||||||
<Row className={styles.test}>
|
<div className={styles.test}>
|
||||||
<Column xs="4">
|
<div>
|
||||||
<div className={styles.header}>Page links</div>
|
<div className={styles.header}>Page links</div>
|
||||||
<div>
|
<div>
|
||||||
<Link href={`/console/${websiteId}/page/1/?q=abc`}>page one</Link>
|
<Link href={`/console/${websiteId}/page/1/?q=abc`}>page one</Link>
|
||||||
@ -114,10 +113,10 @@ export function TestConsole() {
|
|||||||
external link (tab)
|
external link (tab)
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</Column>
|
</div>
|
||||||
<Column xs="4">
|
<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 />
|
||||||
@ -126,28 +125,26 @@ export function TestConsole() {
|
|||||||
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>
|
||||||
</Column>
|
</div>
|
||||||
<Column xs="4">
|
<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>
|
||||||
</Column>
|
</div>
|
||||||
</Row>
|
</div>
|
||||||
<Row>
|
<div>
|
||||||
<Column>
|
<WebsiteChart websiteId={website.id} />
|
||||||
<WebsiteChart websiteId={website.id} />
|
<EventsChart websiteId={website.id} />
|
||||||
<EventsChart websiteId={website.id} />
|
</div>
|
||||||
</Column>
|
|
||||||
</Row>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Page>
|
</Page>
|
20
src/app/(main)/console/[[...id]]/page.tsx
Normal file
20
src/app/(main)/console/[[...id]]/page.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import TestConsole from '../TestConsole';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
async function getEnabled() {
|
||||||
|
return !!process.env.ENABLE_TEST_CONSOLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function ({ params: { id } }) {
|
||||||
|
const enabled = await getEnabled();
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <TestConsole websiteId={id?.[0]} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Test Console | umami',
|
||||||
|
};
|
@ -1,37 +1,48 @@
|
|||||||
import { Button, Icon, Icons, Text } from 'react-basics';
|
'use client';
|
||||||
|
import { Button, Icon, Icons, Loading, Text } from 'react-basics';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Page from 'components/layout/Page';
|
|
||||||
import PageHeader from 'components/layout/PageHeader';
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
import Pager from 'components/common/Pager';
|
import Pager from 'components/common/Pager';
|
||||||
import WebsiteChartList from 'components/pages/websites/WebsiteChartList';
|
import WebsiteChartList from '../../(main)/websites/[id]/WebsiteChartList';
|
||||||
import DashboardSettingsButton from 'components/pages/dashboard/DashboardSettingsButton';
|
import DashboardSettingsButton from 'app/(main)/dashboard/DashboardSettingsButton';
|
||||||
import DashboardEdit from 'components/pages/dashboard/DashboardEdit';
|
import DashboardEdit from 'app/(main)/dashboard/DashboardEdit';
|
||||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||||
import useApi from 'components/hooks/useApi';
|
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,
|
const { query, params, setParams, result } = useFilterQuery({
|
||||||
isLoading,
|
queryKey: ['dashboard:websites'],
|
||||||
error,
|
queryFn: (params: any) => {
|
||||||
} = useQuery(['websites', page, pageSize], () =>
|
return get(`/users/${user.id}/websites`, { ...params, includeTeams: true, pageSize });
|
||||||
get('/websites', { includeTeams: 1, page, pageSize }),
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
setParams({ ...params, page });
|
||||||
|
};
|
||||||
|
|
||||||
const { data, count } = result || {};
|
const { data, count } = result || {};
|
||||||
const hasData = data && data?.length !== 0;
|
const hasData = !!(data as any)?.length;
|
||||||
|
const { page } = params;
|
||||||
|
|
||||||
|
if (query.isLoading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page loading={isLoading} error={error}>
|
<>
|
||||||
<PageHeader title={formatMessage(labels.dashboard)}>
|
<PageHeader title={formatMessage(labels.dashboard)}>
|
||||||
{!editing && hasData && <DashboardSettingsButton />}
|
{!editing && hasData && <DashboardSettingsButton />}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
@ -63,7 +74,7 @@ export function Dashboard() {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Page>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
|||||||
|
'use client';
|
||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
|
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
@ -7,7 +8,6 @@ import useDashboard, { saveDashboard } from 'store/dashboard';
|
|||||||
import useMessages from 'components/hooks/useMessages';
|
import useMessages from 'components/hooks/useMessages';
|
||||||
import useApi from 'components/hooks/useApi';
|
import useApi from 'components/hooks/useApi';
|
||||||
import styles from './DashboardEdit.module.css';
|
import styles from './DashboardEdit.module.css';
|
||||||
import Page from 'components/layout/Page';
|
|
||||||
|
|
||||||
const dragId = 'dashboard-website-ordering';
|
const dragId = 'dashboard-website-ordering';
|
||||||
|
|
||||||
@ -17,11 +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 {
|
const { data: result } = useQuery({
|
||||||
data: result,
|
queryKey: ['websites'],
|
||||||
isLoading,
|
queryFn: () => get('/websites', { includeTeams: 1 }),
|
||||||
error,
|
});
|
||||||
} = useQuery(['websites'], () => get('/websites', { includeTeams: 1 }));
|
|
||||||
const { data: websites } = result || {};
|
const { data: websites } = result || {};
|
||||||
|
|
||||||
const ordered = useMemo(() => {
|
const ordered = useMemo(() => {
|
||||||
@ -59,15 +58,15 @@ export function DashboardEdit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page loading={isLoading} error={error}>
|
<>
|
||||||
<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>
|
||||||
@ -105,7 +104,7 @@ export function DashboardEdit() {
|
|||||||
</Droppable>
|
</Droppable>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
10
src/app/(main)/dashboard/page.tsx
Normal file
10
src/app/(main)/dashboard/page.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import Dashboard from 'app/(main)/dashboard/Dashboard';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
return <Dashboard />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Dashboard | umami',
|
||||||
|
};
|
@ -10,7 +10,6 @@
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 1 / 2;
|
grid-row: 1 / 2;
|
||||||
z-index: var(--z-index-popup);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
19
src/app/(main)/layout.tsx
Normal file
19
src/app/(main)/layout.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import App from './App';
|
||||||
|
import NavBar from './NavBar';
|
||||||
|
import Page from 'components/layout/Page';
|
||||||
|
import styles from './layout.module.css';
|
||||||
|
|
||||||
|
export default function ({ children }) {
|
||||||
|
return (
|
||||||
|
<App>
|
||||||
|
<main className={styles.layout}>
|
||||||
|
<nav className={styles.nav}>
|
||||||
|
<NavBar />
|
||||||
|
</nav>
|
||||||
|
<section className={styles.body}>
|
||||||
|
<Page>{children}</Page>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</App>
|
||||||
|
);
|
||||||
|
}
|
50
src/app/(main)/reports/ReportDeleteButton.tsx
Normal file
50
src/app/(main)/reports/ReportDeleteButton.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
|
||||||
|
import ConfirmDeleteForm from 'components/common/ConfirmDeleteForm';
|
||||||
|
import { useApi, useMessages } from 'components/hooks';
|
||||||
|
import { setValue } from 'store/cache';
|
||||||
|
|
||||||
|
export function ReportDeleteButton({
|
||||||
|
reportId,
|
||||||
|
reportName,
|
||||||
|
onDelete,
|
||||||
|
}: {
|
||||||
|
reportId: string;
|
||||||
|
reportName: string;
|
||||||
|
onDelete?: () => void;
|
||||||
|
}) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { del, useMutation } = useApi();
|
||||||
|
const { mutate } = useMutation({ mutationFn: reportId => del(`/reports/${reportId}`) });
|
||||||
|
|
||||||
|
const handleConfirm = (close: () => void) => {
|
||||||
|
mutate(reportId as any, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setValue('reports', Date.now());
|
||||||
|
onDelete?.();
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalTrigger>
|
||||||
|
<Button>
|
||||||
|
<Icon>
|
||||||
|
<Icons.Trash />
|
||||||
|
</Icon>
|
||||||
|
<Text>{formatMessage(labels.delete)}</Text>
|
||||||
|
</Button>
|
||||||
|
<Modal>
|
||||||
|
{close => (
|
||||||
|
<ConfirmDeleteForm
|
||||||
|
name={reportName}
|
||||||
|
onConfirm={handleConfirm.bind(null, close)}
|
||||||
|
onClose={close}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</ModalTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReportDeleteButton;
|
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>
|
||||||
|
);
|
||||||
|
}
|
25
src/app/(main)/reports/ReportsHeader.tsx
Normal file
25
src/app/(main)/reports/ReportsHeader.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
'use client';
|
||||||
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
|
import { Button, Icon, Icons, Text } from 'react-basics';
|
||||||
|
import { useMessages } from 'components/hooks';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
export function ReportsHeader() {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleClick = () => router.push('/reports/create');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageHeader title={formatMessage(labels.reports)}>
|
||||||
|
<Button variant="primary" onClick={handleClick}>
|
||||||
|
<Icon>
|
||||||
|
<Icons.Plus />
|
||||||
|
</Icon>
|
||||||
|
<Text>{formatMessage(labels.createReport)}</Text>
|
||||||
|
</Button>
|
||||||
|
</PageHeader>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReportsHeader;
|
51
src/app/(main)/reports/ReportsTable.tsx
Normal file
51
src/app/(main)/reports/ReportsTable.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { GridColumn, GridTable, Icon, Icons, Text, useBreakpoint } from 'react-basics';
|
||||||
|
import LinkButton from 'components/common/LinkButton';
|
||||||
|
import { useMessages } from 'components/hooks';
|
||||||
|
import useUser from 'components/hooks/useUser';
|
||||||
|
import { REPORT_TYPES } from 'lib/constants';
|
||||||
|
import ReportDeleteButton from './ReportDeleteButton';
|
||||||
|
|
||||||
|
export function ReportsTable({ data = [], showDomain }: { data: any[]; showDomain?: boolean }) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { user } = useUser();
|
||||||
|
const breakpoint = useBreakpoint();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridTable data={data} cardMode={['xs', 'sm', 'md'].includes(breakpoint)}>
|
||||||
|
<GridColumn name="name" label={formatMessage(labels.name)} />
|
||||||
|
<GridColumn name="description" label={formatMessage(labels.description)} />
|
||||||
|
<GridColumn name="type" label={formatMessage(labels.type)}>
|
||||||
|
{row => {
|
||||||
|
return formatMessage(
|
||||||
|
labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === row.type)],
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</GridColumn>
|
||||||
|
{showDomain && (
|
||||||
|
<GridColumn name="domain" label={formatMessage(labels.domain)}>
|
||||||
|
{row => row?.website?.domain}
|
||||||
|
</GridColumn>
|
||||||
|
)}
|
||||||
|
<GridColumn name="action" label="" alignment="end">
|
||||||
|
{row => {
|
||||||
|
const { id, name, userId, website } = row;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{(user.id === userId || user.id === website?.userId) && (
|
||||||
|
<ReportDeleteButton reportId={id} reportName={name} />
|
||||||
|
)}
|
||||||
|
<LinkButton href={`/reports/${id}`}>
|
||||||
|
<Icon>
|
||||||
|
<Icons.ArrowRight />
|
||||||
|
</Icon>
|
||||||
|
<Text>{formatMessage(labels.view)}</Text>
|
||||||
|
</LinkButton>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</GridColumn>
|
||||||
|
</GridTable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReportsTable;
|
@ -1,17 +1,24 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
import { FormRow } from 'react-basics';
|
import { FormRow } from 'react-basics';
|
||||||
|
import { parseDateRange } from 'lib/date';
|
||||||
import DateFilter from 'components/input/DateFilter';
|
import DateFilter from 'components/input/DateFilter';
|
||||||
import WebsiteSelect from 'components/input/WebsiteSelect';
|
import WebsiteSelect from 'components/input/WebsiteSelect';
|
||||||
import { parseDateRange } from 'lib/date';
|
|
||||||
import { useContext } from 'react';
|
|
||||||
import { ReportContext } from './Report';
|
|
||||||
import { useMessages } from 'components/hooks';
|
import { useMessages } from 'components/hooks';
|
||||||
|
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, element, 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,13 +32,13 @@ export function FieldAddForm({ fields = [], group, element, onAdd, onClose }) {
|
|||||||
setSelected(value);
|
setSelected(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = value => {
|
const handleSave = (value: any) => {
|
||||||
onAdd(group, value);
|
onAdd(group, value);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<PopupForm className={styles.popup} element={element} onClose={onClose}>
|
<PopupForm className={styles.popup}>
|
||||||
{!selected && <FieldSelectForm fields={fields} onSelect={handleSelect} />}
|
{!selected && <FieldSelectForm fields={fields} onSelect={handleSelect} />}
|
||||||
{selected && group === REPORT_PARAMETERS.fields && (
|
{selected && group === REPORT_PARAMETERS.fields && (
|
||||||
<FieldAggregateForm {...selected} onSelect={handleSave} />
|
<FieldAggregateForm {...selected} onSelect={handleSave} />
|
@ -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>
|
59
src/app/(main)/reports/[id]/FilterSelectForm.tsx
Normal file
59
src/app/(main)/reports/[id]/FilterSelectForm.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Loading } from 'react-basics';
|
||||||
|
import { subDays } from 'date-fns';
|
||||||
|
import FieldSelectForm from './FieldSelectForm';
|
||||||
|
import FieldFilterForm from './FieldFilterForm';
|
||||||
|
import { useApi } from 'components/hooks';
|
||||||
|
|
||||||
|
function useValues(websiteId: string, type: string) {
|
||||||
|
const now = Date.now();
|
||||||
|
const { get, useQuery } = useApi();
|
||||||
|
const { data, error, isLoading } = useQuery({
|
||||||
|
queryKey: ['websites:values', websiteId, type],
|
||||||
|
queryFn: () =>
|
||||||
|
get(`/websites/${websiteId}/values`, {
|
||||||
|
type,
|
||||||
|
startAt: +subDays(now, 90),
|
||||||
|
endAt: now,
|
||||||
|
}),
|
||||||
|
enabled: !!(websiteId && type),
|
||||||
|
});
|
||||||
|
|
||||||
|
return { data, error, isLoading };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterSelectFormProps {
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (!field) {
|
||||||
|
return <FieldSelectForm fields={items} onSelect={setField} showType={false} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loading position="center" icon="dots" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldFilterForm
|
||||||
|
name={field?.name}
|
||||||
|
label={field?.label}
|
||||||
|
type={field?.type}
|
||||||
|
values={data}
|
||||||
|
onSelect={onSelect}
|
||||||
|
allowFilterSelect={allowFilterSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -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)}
|
5
src/app/(main)/reports/[id]/Report.module.css
Normal file
5
src/app/(main)/reports/[id]/Report.module.css
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: max-content 1fr;
|
||||||
|
grid-template-columns: max-content 1fr;
|
||||||
|
}
|
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;
|
5
src/app/(main)/reports/[id]/ReportBody.module.css
Normal file
5
src/app/(main)/reports/[id]/ReportBody.module.css
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.body {
|
||||||
|
padding-left: 20px;
|
||||||
|
grid-row: 2/3;
|
||||||
|
grid-column: 2 / 3;
|
||||||
|
}
|
15
src/app/(main)/reports/[id]/ReportBody.tsx
Normal file
15
src/app/(main)/reports/[id]/ReportBody.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import styles from './ReportBody.module.css';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { ReportContext } from './Report';
|
||||||
|
|
||||||
|
export function ReportBody({ children }) {
|
||||||
|
const { report } = useContext(ReportContext);
|
||||||
|
|
||||||
|
if (!report) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={styles.body}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReportBody;
|
29
src/app/(main)/reports/[id]/ReportDetails.tsx
Normal file
29
src/app/(main)/reports/[id]/ReportDetails.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
'use client';
|
||||||
|
import FunnelReport from '../funnel/FunnelReport';
|
||||||
|
import EventDataReport from '../event-data/EventDataReport';
|
||||||
|
import InsightsReport from '../insights/InsightsReport';
|
||||||
|
import RetentionReport from '../retention/RetentionReport';
|
||||||
|
import { useApi } from 'components/hooks';
|
||||||
|
|
||||||
|
const reports = {
|
||||||
|
funnel: FunnelReport,
|
||||||
|
'event-data': EventDataReport,
|
||||||
|
insights: InsightsReport,
|
||||||
|
retention: RetentionReport,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ReportDetails({ reportId }: { reportId: string }) {
|
||||||
|
const { get, useQuery } = useApi();
|
||||||
|
const { data: report } = useQuery({
|
||||||
|
queryKey: ['reports', reportId],
|
||||||
|
queryFn: () => get(`/reports/${reportId}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!report) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReportComponent = reports[report.type];
|
||||||
|
|
||||||
|
return <ReportComponent reportId={reportId} />;
|
||||||
|
}
|
36
src/app/(main)/reports/[id]/ReportHeader.module.css
Normal file
36
src/app/(main)/reports/[id]/ReportHeader.module.css
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
.header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr min-content;
|
||||||
|
align-items: center;
|
||||||
|
grid-row: 1 / 2;
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
margin: 20px 0 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
gap: 20px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--base600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
color: var(--font-color300);
|
||||||
|
max-width: 500px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
@ -1,11 +1,10 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/navigation';
|
||||||
import { Icon, LoadingButton, InlineEditField, useToasts } from 'react-basics';
|
import { Icon, LoadingButton, InlineEditField, useToasts } from 'react-basics';
|
||||||
import PageHeader from 'components/layout/PageHeader';
|
|
||||||
import { useMessages, useApi } from 'components/hooks';
|
import { useMessages, useApi } from 'components/hooks';
|
||||||
import { ReportContext } from './Report';
|
import { ReportContext } from './Report';
|
||||||
import styles from './ReportHeader.module.css';
|
import styles from './ReportHeader.module.css';
|
||||||
import reportStyles from './reports.module.css';
|
import { REPORT_TYPES } from 'lib/constants';
|
||||||
|
|
||||||
export function ReportHeader({ icon }) {
|
export function ReportHeader({ icon }) {
|
||||||
const { report, updateReport } = useContext(ReportContext);
|
const { report, updateReport } = useContext(ReportContext);
|
||||||
@ -13,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 || {};
|
||||||
@ -27,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 {
|
||||||
@ -39,32 +40,47 @@ 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 });
|
||||||
};
|
};
|
||||||
|
|
||||||
const Title = () => {
|
if (!report) {
|
||||||
return (
|
return null;
|
||||||
<>
|
}
|
||||||
<Icon size="lg">{icon}</Icon>
|
|
||||||
<InlineEditField
|
|
||||||
key={name}
|
|
||||||
name="name"
|
|
||||||
value={name}
|
|
||||||
placeholder={defaultName}
|
|
||||||
onCommit={handleNameChange}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={reportStyles.header}>
|
<div className={styles.header}>
|
||||||
<PageHeader title={<Title />}>
|
<div>
|
||||||
|
<div className={styles.type}>
|
||||||
|
{formatMessage(
|
||||||
|
labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === report?.type)],
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.title}>
|
||||||
|
<Icon size="lg">{icon}</Icon>
|
||||||
|
<InlineEditField
|
||||||
|
key={name}
|
||||||
|
name="name"
|
||||||
|
value={name}
|
||||||
|
placeholder={defaultName}
|
||||||
|
onCommit={handleNameChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.description}>
|
||||||
|
<InlineEditField
|
||||||
|
key={description}
|
||||||
|
name="description"
|
||||||
|
value={description}
|
||||||
|
placeholder={`+ ${formatMessage(labels.addDescription)}`}
|
||||||
|
onCommit={handleDescriptionChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.actions}>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
variant="primary"
|
variant="primary"
|
||||||
isLoading={isCreating || isUpdating}
|
isLoading={isCreating || isUpdating}
|
||||||
@ -73,15 +89,6 @@ export function ReportHeader({ icon }) {
|
|||||||
>
|
>
|
||||||
{formatMessage(labels.save)}
|
{formatMessage(labels.save)}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
</PageHeader>
|
|
||||||
<div className={styles.description}>
|
|
||||||
<InlineEditField
|
|
||||||
key={description}
|
|
||||||
name="description"
|
|
||||||
value={description}
|
|
||||||
placeholder={`+ ${formatMessage(labels.addDescription)}`}
|
|
||||||
onCommit={handleDescriptionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
7
src/app/(main)/reports/[id]/ReportMenu.module.css
Normal file
7
src/app/(main)/reports/[id]/ReportMenu.module.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.menu {
|
||||||
|
width: 300px;
|
||||||
|
padding-right: 20px;
|
||||||
|
border-right: 1px solid var(--base300);
|
||||||
|
grid-row: 2 / 3;
|
||||||
|
grid-column: 1 / 2;
|
||||||
|
}
|
15
src/app/(main)/reports/[id]/ReportMenu.tsx
Normal file
15
src/app/(main)/reports/[id]/ReportMenu.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import styles from './ReportMenu.module.css';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { ReportContext } from './Report';
|
||||||
|
|
||||||
|
export function ReportMenu({ children }) {
|
||||||
|
const { report } = useContext(ReportContext);
|
||||||
|
|
||||||
|
if (!report) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={styles.menu}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReportMenu;
|
14
src/app/(main)/reports/[id]/page.tsx
Normal file
14
src/app/(main)/reports/[id]/page.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import ReportDetails from './ReportDetails';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
export default function ReportDetailsPage({ params: { id } }) {
|
||||||
|
if (!id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ReportDetails reportId={id} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Reports | umami',
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
|
'use client';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Button, Icons, Text, Icon } from 'react-basics';
|
import { Button, Icons, Text, Icon } from 'react-basics';
|
||||||
import Page from 'components/layout/Page';
|
|
||||||
import PageHeader from 'components/layout/PageHeader';
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
import Funnel from 'assets/funnel.svg';
|
import Funnel from 'assets/funnel.svg';
|
||||||
import Lightbulb from 'assets/lightbulb.svg';
|
import Lightbulb from 'assets/lightbulb.svg';
|
||||||
@ -57,7 +57,7 @@ export function ReportTemplates({ showHeader = true }) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<>
|
||||||
{showHeader && <PageHeader title={formatMessage(labels.reports)} />}
|
{showHeader && <PageHeader title={formatMessage(labels.reports)} />}
|
||||||
<div className={styles.reports}>
|
<div className={styles.reports}>
|
||||||
{reports.map(({ title, description, url, icon }) => {
|
{reports.map(({ title, description, url, icon }) => {
|
||||||
@ -66,7 +66,7 @@ export function ReportTemplates({ showHeader = true }) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
10
src/app/(main)/reports/create/page.tsx
Normal file
10
src/app/(main)/reports/create/page.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import ReportTemplates from './ReportTemplates';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
export default function ReportsCreatePage() {
|
||||||
|
return <ReportTemplates />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Create Report | umami',
|
||||||
|
};
|
@ -1,27 +1,27 @@
|
|||||||
import { useContext, useRef } from 'react';
|
import { useContext } from 'react';
|
||||||
import { useApi, useMessages } from 'components/hooks';
|
|
||||||
import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics';
|
import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics';
|
||||||
import { ReportContext } from 'components/pages/reports/Report';
|
|
||||||
import Empty from 'components/common/Empty';
|
import Empty from 'components/common/Empty';
|
||||||
import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants';
|
|
||||||
import Icons from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
import FieldAddForm from '../FieldAddForm';
|
import { useApi, useMessages } from 'components/hooks';
|
||||||
import BaseParameters from '../BaseParameters';
|
import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants';
|
||||||
import ParameterList from '../ParameterList';
|
import { ReportContext } from '../[id]/Report';
|
||||||
|
import FieldAddForm from '../[id]/FieldAddForm';
|
||||||
|
import ParameterList from '../[id]/ParameterList';
|
||||||
|
import BaseParameters from '../[id]/BaseParameters';
|
||||||
import styles from './EventDataParameters.module.css';
|
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, element) => {
|
{(close: () => void) => {
|
||||||
return (
|
return (
|
||||||
<FieldAddForm
|
<FieldAddForm
|
||||||
fields={data.map(({ eventKey, eventDataType }) => ({
|
fields={data.map(({ eventKey, eventDataType }) => ({
|
||||||
@ -82,8 +81,7 @@ export function EventDataParameters() {
|
|||||||
type: DATA_TYPES[eventDataType],
|
type: DATA_TYPES[eventDataType],
|
||||||
}))}
|
}))}
|
||||||
group={group}
|
group={group}
|
||||||
element={element}
|
onAdd={onAdd}
|
||||||
onAdd={handleAdd}
|
|
||||||
onClose={close}
|
onClose={close}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -94,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 &&
|
@ -1,7 +1,7 @@
|
|||||||
import Report from '../Report';
|
import Report from '../[id]/Report';
|
||||||
import ReportHeader from '../ReportHeader';
|
import ReportHeader from '../[id]/ReportHeader';
|
||||||
import ReportMenu from '../ReportMenu';
|
import ReportMenu from '../[id]/ReportMenu';
|
||||||
import ReportBody from '../ReportBody';
|
import ReportBody from '../[id]/ReportBody';
|
||||||
import EventDataParameters from './EventDataParameters';
|
import EventDataParameters from './EventDataParameters';
|
||||||
import EventDataTable from './EventDataTable';
|
import EventDataTable from './EventDataTable';
|
||||||
import Nodes from 'assets/nodes.svg';
|
import Nodes from 'assets/nodes.svg';
|
||||||
@ -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,7 +1,7 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { GridTable, GridColumn } from 'react-basics';
|
import { GridTable, GridColumn } from 'react-basics';
|
||||||
import { useMessages } from 'components/hooks';
|
import { useMessages } from 'components/hooks';
|
||||||
import { ReportContext } from '../Report';
|
import { ReportContext } from '../[id]/Report';
|
||||||
|
|
||||||
export function EventDataTable() {
|
export function EventDataTable() {
|
||||||
const { report } = useContext(ReportContext);
|
const { report } = useContext(ReportContext);
|
@ -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 { ReportContext } from '../[id]/Report';
|
||||||
import styles from './FunnelChart.module.css';
|
import styles from './FunnelChart.module.css';
|
||||||
import { ReportContext } from '../Report';
|
|
||||||
|
|
||||||
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,
|
||||||
@ -13,21 +13,20 @@ import {
|
|||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
import Icons from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
import UrlAddForm from './UrlAddForm';
|
import UrlAddForm from './UrlAddForm';
|
||||||
import { ReportContext } from 'components/pages/reports/Report';
|
import { ReportContext } from '../[id]/Report';
|
||||||
import BaseParameters from '../BaseParameters';
|
import BaseParameters from '../[id]/BaseParameters';
|
||||||
import ParameterList from '../ParameterList';
|
import ParameterList from '../[id]/ParameterList';
|
||||||
import PopupForm from '../PopupForm';
|
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);
|
||||||
@ -52,21 +51,17 @@ export function FunnelParameters() {
|
|||||||
<Icon>
|
<Icon>
|
||||||
<Icons.Plus />
|
<Icons.Plus />
|
||||||
</Icon>
|
</Icon>
|
||||||
<Popup position="bottom" alignment="start">
|
<Popup position="right" alignment="start">
|
||||||
{(close, element) => {
|
<PopupForm>
|
||||||
return (
|
<UrlAddForm onAdd={handleAddUrl} />
|
||||||
<PopupForm element={element} onClose={close}>
|
</PopupForm>
|
||||||
<UrlAddForm onAdd={handleAddUrl} />
|
|
||||||
</PopupForm>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Popup>
|
</Popup>
|
||||||
</PopupTrigger>
|
</PopupTrigger>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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
|
||||||
@ -77,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}>
|
@ -1,10 +1,11 @@
|
|||||||
|
'use client';
|
||||||
import FunnelChart from './FunnelChart';
|
import FunnelChart from './FunnelChart';
|
||||||
import FunnelTable from './FunnelTable';
|
import FunnelTable from './FunnelTable';
|
||||||
import FunnelParameters from './FunnelParameters';
|
import FunnelParameters from './FunnelParameters';
|
||||||
import Report from '../Report';
|
import Report from '../[id]/Report';
|
||||||
import ReportHeader from '../ReportHeader';
|
import ReportHeader from '../[id]/ReportHeader';
|
||||||
import ReportMenu from '../ReportMenu';
|
import ReportMenu from '../[id]/ReportMenu';
|
||||||
import ReportBody from '../ReportBody';
|
import ReportBody from '../[id]/ReportBody';
|
||||||
import Funnel from 'assets/funnel.svg';
|
import Funnel from 'assets/funnel.svg';
|
||||||
import { REPORT_TYPES } from 'lib/constants';
|
import { REPORT_TYPES } from 'lib/constants';
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import ListTable from 'components/metrics/ListTable';
|
import ListTable from 'components/metrics/ListTable';
|
||||||
import { useMessages } from 'components/hooks';
|
import { useMessages } from 'components/hooks';
|
||||||
import { ReportContext } from '../Report';
|
import { ReportContext } from '../[id]/Report';
|
||||||
|
|
||||||
export function FunnelTable() {
|
export function FunnelTable() {
|
||||||
const { report } = useContext(ReportContext);
|
const { report } = useContext(ReportContext);
|
@ -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();
|
||||||
|
|
10
src/app/(main)/reports/funnel/page.tsx
Normal file
10
src/app/(main)/reports/funnel/page.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import FunnelReport from './FunnelReport';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
export default function FunnelReportPage() {
|
||||||
|
return <FunnelReport reportId={null} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Funnel Report | umami',
|
||||||
|
};
|
@ -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,
|
||||||
@ -10,21 +10,20 @@ import {
|
|||||||
Popup,
|
Popup,
|
||||||
TooltipPopup,
|
TooltipPopup,
|
||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
import { ReportContext } from 'components/pages/reports/Report';
|
|
||||||
import Icons from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
import BaseParameters from '../BaseParameters';
|
import BaseParameters from '../[id]/BaseParameters';
|
||||||
import ParameterList from '../ParameterList';
|
import { ReportContext } from '../[id]/Report';
|
||||||
|
import ParameterList from '../[id]/ParameterList';
|
||||||
|
import FilterSelectForm from '../[id]/FilterSelectForm';
|
||||||
|
import FieldSelectForm from '../[id]/FieldSelectForm';
|
||||||
|
import PopupForm from '../[id]/PopupForm';
|
||||||
import styles from './InsightsParameters.module.css';
|
import styles from './InsightsParameters.module.css';
|
||||||
import PopupForm from '../PopupForm';
|
|
||||||
import FilterSelectForm from '../FilterSelectForm';
|
|
||||||
import FieldSelectForm from '../FieldSelectForm';
|
|
||||||
|
|
||||||
export function InsightsParameters() {
|
export function InsightsParameters() {
|
||||||
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
|
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
|
||||||
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">
|
||||||
@ -81,33 +80,29 @@ export function InsightsParameters() {
|
|||||||
</Icon>
|
</Icon>
|
||||||
</TooltipPopup>
|
</TooltipPopup>
|
||||||
<Popup position="bottom" alignment="start" className={styles.popup}>
|
<Popup position="bottom" alignment="start" className={styles.popup}>
|
||||||
{close => {
|
<PopupForm>
|
||||||
return (
|
{id === 'fields' && (
|
||||||
<PopupForm onClose={close}>
|
<FieldSelectForm
|
||||||
{id === 'fields' && (
|
fields={fieldOptions}
|
||||||
<FieldSelectForm
|
onSelect={onAdd.bind(null, id)}
|
||||||
items={fieldOptions}
|
showType={false}
|
||||||
onSelect={handleAdd.bind(null, id)}
|
/>
|
||||||
showType={false}
|
)}
|
||||||
/>
|
{id === 'filters' && (
|
||||||
)}
|
<FilterSelectForm
|
||||||
{id === 'filters' && (
|
websiteId={websiteId}
|
||||||
<FilterSelectForm
|
items={fieldOptions}
|
||||||
websiteId={websiteId}
|
onSelect={onAdd.bind(null, id)}
|
||||||
items={fieldOptions}
|
/>
|
||||||
onSelect={handleAdd.bind(null, id)}
|
)}
|
||||||
/>
|
</PopupForm>
|
||||||
)}
|
|
||||||
</PopupForm>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Popup>
|
</Popup>
|
||||||
</PopupTrigger>
|
</PopupTrigger>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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 }) => {
|
@ -1,7 +1,8 @@
|
|||||||
import Report from '../Report';
|
'use client';
|
||||||
import ReportHeader from '../ReportHeader';
|
import Report from '../[id]/Report';
|
||||||
import ReportMenu from '../ReportMenu';
|
import ReportHeader from '../[id]/ReportHeader';
|
||||||
import ReportBody from '../ReportBody';
|
import ReportMenu from '../[id]/ReportMenu';
|
||||||
|
import ReportBody from '../[id]/ReportBody';
|
||||||
import InsightsParameters from './InsightsParameters';
|
import InsightsParameters from './InsightsParameters';
|
||||||
import InsightsTable from './InsightsTable';
|
import InsightsTable from './InsightsTable';
|
||||||
import Lightbulb from 'assets/lightbulb.svg';
|
import Lightbulb from 'assets/lightbulb.svg';
|
||||||
@ -12,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 />} />
|
@ -1,11 +1,11 @@
|
|||||||
import { useContext, useEffect, useState } from 'react';
|
import { useContext, useEffect, useState } from 'react';
|
||||||
import { GridTable, GridColumn } from 'react-basics';
|
import { GridTable, GridColumn } from 'react-basics';
|
||||||
import { useFormat, useMessages } from 'components/hooks';
|
import { useFormat, useMessages } from 'components/hooks';
|
||||||
import { ReportContext } from '../Report';
|
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>
|
||||||
);
|
);
|
10
src/app/(main)/reports/insights/page.tsx
Normal file
10
src/app/(main)/reports/insights/page.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import InsightsReport from './InsightsReport';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
export default function InsightsReportPage() {
|
||||||
|
return <InsightsReport reportId={null} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Insights Report | umami',
|
||||||
|
};
|
14
src/app/(main)/reports/page.tsx
Normal file
14
src/app/(main)/reports/page.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import ReportsHeader from './ReportsHeader';
|
||||||
|
import ReportsDataTable from './ReportsDataTable';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ReportsHeader />
|
||||||
|
<ReportsDataTable />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Reports | umami',
|
||||||
|
};
|
@ -1,24 +1,24 @@
|
|||||||
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 'components/pages/reports/Report';
|
import { ReportContext } from '../[id]/Report';
|
||||||
import { MonthSelect } from 'components/input/MonthSelect';
|
import { MonthSelect } from 'components/input/MonthSelect';
|
||||||
import BaseParameters from '../BaseParameters';
|
import BaseParameters from '../[id]/BaseParameters';
|
||||||
import { parseDateRange } from 'lib/date';
|
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();
|
||||||
|
|
||||||
if (!queryDisabled) {
|
if (!queryDisabled) {
|
||||||
runReport(data);
|
runReport(data);
|
||||||
}
|
}
|
||||||
@ -29,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} />
|
@ -1,9 +1,10 @@
|
|||||||
|
'use client';
|
||||||
import RetentionTable from './RetentionTable';
|
import RetentionTable from './RetentionTable';
|
||||||
import RetentionParameters from './RetentionParameters';
|
import RetentionParameters from './RetentionParameters';
|
||||||
import Report from '../Report';
|
import Report from '../[id]/Report';
|
||||||
import ReportHeader from '../ReportHeader';
|
import ReportHeader from '../[id]/ReportHeader';
|
||||||
import ReportMenu from '../ReportMenu';
|
import ReportMenu from '../[id]/ReportMenu';
|
||||||
import ReportBody from '../ReportBody';
|
import ReportBody from '../[id]/ReportBody';
|
||||||
import Magnet from 'assets/magnet.svg';
|
import Magnet from 'assets/magnet.svg';
|
||||||
import { REPORT_TYPES } from 'lib/constants';
|
import { REPORT_TYPES } from 'lib/constants';
|
||||||
import { parseDateRange } from 'lib/date';
|
import { parseDateRange } from 'lib/date';
|
||||||
@ -18,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 />} />
|
@ -1,13 +1,14 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { ReportContext } from '../Report';
|
import { ReportContext } from '../[id]/Report';
|
||||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||||
import { useMessages } from 'components/hooks';
|
import { useMessages, useLocale } from 'components/hooks';
|
||||||
import { useLocale } from 'components/hooks';
|
|
||||||
import { formatDate } from 'lib/date';
|
import { formatDate } from 'lib/date';
|
||||||
import styles from './RetentionTable.module.css';
|
import styles from './RetentionTable.module.css';
|
||||||
|
|
||||||
export function RetentionTable() {
|
const DAYS = [1, 2, 3, 4, 5, 6, 7, 14, 21, 28];
|
||||||
|
|
||||||
|
export function RetentionTable({ days = DAYS }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
const { report } = useContext(ReportContext);
|
const { report } = useContext(ReportContext);
|
||||||
@ -17,9 +18,7 @@ export function RetentionTable() {
|
|||||||
return <EmptyPlaceholder />;
|
return <EmptyPlaceholder />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const days = [1, 2, 3, 4, 5, 6, 7, 14, 21, 28];
|
const rows = data.reduce((arr: any[], row: { date: any; visitors: any; day: any }) => {
|
||||||
|
|
||||||
const rows = data.reduce((arr, row) => {
|
|
||||||
const { date, visitors, day } = row;
|
const { date, visitors, day } = row;
|
||||||
if (day === 0) {
|
if (day === 0) {
|
||||||
return arr.concat({
|
return arr.concat({
|
9
src/app/(main)/reports/retention/page.js
Normal file
9
src/app/(main)/reports/retention/page.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import RetentionReport from './RetentionReport';
|
||||||
|
|
||||||
|
export default function RetentionReportPage() {
|
||||||
|
return <RetentionReport reportId={null} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Create Report | umami',
|
||||||
|
};
|
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;
|
31
src/app/(main)/settings/layout.module.css
Normal file
31
src/app/(main)/settings/layout.module.css
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
.layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: max-content 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
width: 240px;
|
||||||
|
padding-top: 34px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 50vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 992px) {
|
||||||
|
.layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,15 @@
|
|||||||
import { Row, Column } from 'react-basics';
|
'use client';
|
||||||
import { useRouter } from 'next/router';
|
import { usePathname } from 'next/navigation';
|
||||||
import SideNav from './SideNav';
|
|
||||||
import useUser from 'components/hooks/useUser';
|
import useUser from 'components/hooks/useUser';
|
||||||
import useMessages from 'components/hooks/useMessages';
|
import useMessages from 'components/hooks/useMessages';
|
||||||
import styles from './SettingsLayout.module.css';
|
import SideNav from 'components/layout/SideNav';
|
||||||
|
import styles from './layout.module.css';
|
||||||
|
|
||||||
export function SettingsLayout({ children }) {
|
export default function SettingsLayout({ children }) {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { pathname } = useRouter();
|
const pathname = usePathname();
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const cloudMode = Boolean(process.env.cloudMode);
|
const cloudMode = !!process.env.cloudMode;
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{ key: 'websites', label: formatMessage(labels.websites), url: '/settings/websites' },
|
{ key: 'websites', label: formatMessage(labels.websites), url: '/settings/websites' },
|
||||||
@ -20,18 +20,18 @@ export function SettingsLayout({ children }) {
|
|||||||
|
|
||||||
const getKey = () => items.find(({ url }) => pathname === url)?.key;
|
const getKey = () => items.find(({ url }) => pathname === url)?.key;
|
||||||
|
|
||||||
|
if (cloudMode && pathname !== '/settings/profile') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row>
|
<div className={styles.layout}>
|
||||||
{!cloudMode && (
|
{!cloudMode && (
|
||||||
<Column className={styles.menu} defaultSize={12} md={4} lg={3} xl={2}>
|
<div className={styles.menu}>
|
||||||
<SideNav items={items} shallow={true} selectedKey={getKey()} />
|
<SideNav items={items} shallow={true} selectedKey={getKey()} />
|
||||||
</Column>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Column className={styles.content} defaultSize={12} md={8} lg={9} xl={10}>
|
<div className={styles.content}>{children}</div>
|
||||||
{children}
|
</div>
|
||||||
</Column>
|
|
||||||
</Row>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SettingsLayout;
|
|
@ -1,5 +1,5 @@
|
|||||||
import { Button, Icon, Text, useToasts, ModalTrigger, Modal } from 'react-basics';
|
import { Button, Icon, Text, useToasts, ModalTrigger, Modal } from 'react-basics';
|
||||||
import PasswordEditForm from 'components/pages/settings/profile/PasswordEditForm';
|
import PasswordEditForm from 'app/(main)/settings/profile/PasswordEditForm';
|
||||||
import Icons from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
import useMessages from 'components/hooks/useMessages';
|
import useMessages from 'components/hooks/useMessages';
|
||||||
|
|
@ -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>
|
11
src/app/(main)/settings/profile/ProfileHeader.tsx
Normal file
11
src/app/(main)/settings/profile/ProfileHeader.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
'use client';
|
||||||
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
|
import { useMessages } from 'components/hooks';
|
||||||
|
|
||||||
|
export function ProfileHeader() {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
|
return <PageHeader title={formatMessage(labels.profile)}></PageHeader>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProfileHeader;
|
@ -1,14 +1,15 @@
|
|||||||
|
'use client';
|
||||||
import { Form, FormRow } from 'react-basics';
|
import { Form, FormRow } from 'react-basics';
|
||||||
import TimezoneSetting from 'components/pages/settings/profile/TimezoneSetting';
|
import TimezoneSetting from 'app/(main)/settings/profile/TimezoneSetting';
|
||||||
import DateRangeSetting from 'components/pages/settings/profile/DateRangeSetting';
|
import DateRangeSetting from 'app/(main)/settings/profile/DateRangeSetting';
|
||||||
import LanguageSetting from 'components/pages/settings/profile/LanguageSetting';
|
import LanguageSetting from 'app/(main)/settings/profile/LanguageSetting';
|
||||||
import ThemeSetting from 'components/pages/settings/profile/ThemeSetting';
|
import ThemeSetting from 'app/(main)/settings/profile/ThemeSetting';
|
||||||
import PasswordChangeButton from './PasswordChangeButton';
|
import PasswordChangeButton from './PasswordChangeButton';
|
||||||
import useUser from 'components/hooks/useUser';
|
import useUser from 'components/hooks/useUser';
|
||||||
import useMessages from 'components/hooks/useMessages';
|
import useMessages from 'components/hooks/useMessages';
|
||||||
import { ROLES } from 'lib/constants';
|
import { ROLES } from 'lib/constants';
|
||||||
|
|
||||||
export function ProfileDetails() {
|
export function ProfileSettings() {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const cloudMode = Boolean(process.env.cloudMode);
|
const cloudMode = Boolean(process.env.cloudMode);
|
||||||
@ -58,4 +59,4 @@ export function ProfileDetails() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProfileDetails;
|
export default ProfileSettings;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user