From b4ea70a67c64b54f70456fa979e8e4cae55dd32c Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 7 Oct 2020 18:24:53 -0700 Subject: [PATCH 01/27] Fix event colors. Updated packages. --- components/metrics/EventsChart.js | 4 +- lib/constants.js | 10 +- package.json | 16 +- yarn.lock | 268 +++++++++++++++--------------- 4 files changed, 148 insertions(+), 150 deletions(-) diff --git a/components/metrics/EventsChart.js b/components/metrics/EventsChart.js index 113c6f56..0a348a29 100644 --- a/components/metrics/EventsChart.js +++ b/components/metrics/EventsChart.js @@ -5,8 +5,8 @@ import { getDateArray, getDateLength } from 'lib/date'; import useFetch from 'hooks/useFetch'; import useDateRange from 'hooks/useDateRange'; import useTimezone from 'hooks/useTimezone'; +import usePageQuery from 'hooks/usePageQuery'; import { EVENT_COLORS } from 'lib/constants'; -import usePageQuery from '../../hooks/usePageQuery'; export default function EventsChart({ websiteId, token }) { const [dateRange] = useDateRange(websiteId); @@ -44,7 +44,7 @@ export default function EventsChart({ websiteId, token }) { }); return Object.keys(map).map((key, index) => { - const color = tinycolor(EVENT_COLORS[index]); + const color = tinycolor(EVENT_COLORS[index % EVENT_COLORS.length]); return { label: key, data: map[key], diff --git a/lib/constants.js b/lib/constants.js index 87e0cf1d..85515481 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -42,9 +42,13 @@ export const EVENT_COLORS = [ '#44b556', '#e68619', '#e34850', - '#1b959a', - '#d83790', - '#85d044', + '#f7bd12', + '#01bad7', + '#6734bc', + '#89c541', + '#ffc301', + '#ec1562', + '#ffec16', ]; export const DEFAULT_DATE_RANGE = '24hour'; diff --git a/package.json b/package.json index aca8f8c0..8f9edabb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "0.80.0", + "version": "0.81.0", "description": "A simple, fast, website analytics alternative to Google Analytics. ", "author": "Mike Cao ", "license": "MIT", @@ -56,7 +56,7 @@ } }, "dependencies": { - "@prisma/client": "2.8.0", + "@prisma/client": "2.8.1", "@reduxjs/toolkit": "^1.4.0", "bcrypt": "^5.0.0", "chalk": "^4.1.0", @@ -65,7 +65,7 @@ "cookie": "^0.4.1", "cors": "^2.8.5", "date-fns": "^2.16.1", - "date-fns-tz": "^1.0.10", + "date-fns-tz": "^1.0.11", "detect-browser": "^5.1.1", "dotenv": "^8.2.0", "formik": "^2.1.7", @@ -73,9 +73,9 @@ "is-localhost-ip": "^1.4.0", "isbot-fast": "^1.2.0", "jose": "^2.0.2", - "maxmind": "^4.2.0", + "maxmind": "^4.3.0", "moment-timezone": "^0.5.31", - "next": "^9.5.3", + "next": "^9.5.4", "react": "^16.13.1", "react-dom": "^16.13.1", "react-intl": "^5.8.4", @@ -91,11 +91,11 @@ "thenby": "^1.3.4", "timezone-support": "^2.0.2", "tinycolor2": "^1.4.2", - "uuid": "^8.3.0" + "uuid": "^8.3.1" }, "devDependencies": { "@formatjs/cli": "^2.13.0", - "@prisma/cli": "2.8.0", + "@prisma/cli": "2.8.1", "@rollup/plugin-buble": "^0.21.3", "@rollup/plugin-node-resolve": "^9.0.0", "@rollup/plugin-replace": "^2.3.3", @@ -106,7 +106,7 @@ "eslint": "^7.10.0", "eslint-config-prettier": "^6.12.0", "eslint-plugin-prettier": "^3.1.3", - "eslint-plugin-react": "^7.21.2", + "eslint-plugin-react": "^7.21.3", "eslint-plugin-react-hooks": "^4.1.2", "extract-react-intl-messages": "^4.1.1", "husky": "^4.3.0", diff --git a/yarn.lock b/yarn.lock index c38489ea..df03a2ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -398,7 +398,7 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" -"@babel/plugin-proposal-nullish-coalescing-operator@7.10.4", "@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4": +"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz#02a7e961fc32e6d5b2db0649e01bf80ddee7e04a" integrity sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw== @@ -431,7 +431,7 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@7.11.0", "@babel/plugin-proposal-optional-chaining@^7.11.0": +"@babel/plugin-proposal-optional-chaining@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz#de5866d0646f6afdaab8a566382fe3a221755076" integrity sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA== @@ -498,7 +498,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@^7.10.4": +"@babel/plugin-syntax-jsx@7.10.4", "@babel/plugin-syntax-jsx@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.10.4.tgz#39abaae3cbf710c4373d8429484e6ba21340166c" integrity sha512-KCg9mio9jwiARCB7WAcQ7Y1q+qicILjoK8LP/VkPkEKaf5dkaZZK1EcTe91a3JJlZ3qy6L5s9X52boEYi8DM9g== @@ -1159,10 +1159,20 @@ intl-messageformat-parser "^6.0.7" typescript "^4.0" -"@next/react-dev-overlay@9.5.3": - version "9.5.3" - resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-9.5.3.tgz#3275301f08045ecc709e3273031973a1f5e81427" - integrity sha512-R2ZAyFjHHaMTBVi19ZZNRJNXiwn46paRi7EZvKNvMxbrzBcUYtSFj/edU3jQoF1UOcC6vGeMhtPqH55ONrIjCQ== +"@next/env@9.5.4": + version "9.5.4" + resolved "https://registry.yarnpkg.com/@next/env/-/env-9.5.4.tgz#950f3370151a940ecac6e7e19cf125e6113e101e" + integrity sha512-uGnUO68/u9C8bqHj5obIvyGRDqe/jh1dFSLx03mJmlESjcCmV4umXYJOnt3XzU1VhVntSE+jUZtnS5bjYmmLfQ== + +"@next/polyfill-module@9.5.4": + version "9.5.4" + resolved "https://registry.yarnpkg.com/@next/polyfill-module/-/polyfill-module-9.5.4.tgz#35ea31ce5f6bbf0ac31aac483b60d4ba17a79861" + integrity sha512-GA2sW7gs33s7RGPFqkMiT9asYpaV/Hhw9+XM9/UlPrkNdTaxZWaPa2iHgmqJ7k6OHiOmy+CBLFrUBgzqKNhs3Q== + +"@next/react-dev-overlay@9.5.4": + version "9.5.4" + resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-9.5.4.tgz#7d88a710d23021020cca213bc77106df18950b2b" + integrity sha512-tYvNmOQ0inykSvcimkTiONMv4ZyFB2G2clsy9FKLLRZ2OA+Jiov6T7Pq6YpKbBwTLu/BQGVc7Qn4BZ5CDHR8ig== dependencies: "@babel/code-frame" "7.10.4" ally.js "1.4.1" @@ -1175,10 +1185,10 @@ stacktrace-parser "0.1.10" strip-ansi "6.0.0" -"@next/react-refresh-utils@9.5.3": - version "9.5.3" - resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-9.5.3.tgz#a14fb6489d412b201b98aa44716fb8727ca4c6ae" - integrity sha512-W3VKOqbg+4Kw+k6M/SODf+WIzwcx60nAemGV1nNPa/yrDtAS2YcJfqiswrJ3+2nJHzqefAFWn4XOfM0fy8ww2Q== +"@next/react-refresh-utils@9.5.4": + version "9.5.4" + resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-9.5.4.tgz#3bfe067f0cfc717f079482d956211708c9e81126" + integrity sha512-TPhEiYxK5YlEuzVuTzgZiDN7SDh4drvUAqsO9Yccd8WLcfYqOLRN2fCALremW5mNLAZQZW3iFgW8PW8Gckq4EQ== "@nodelib/fs.scandir@2.1.3": version "2.1.3" @@ -1201,20 +1211,27 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" +"@npmcli/move-file@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.0.1.tgz#de103070dac0f48ce49cf6693c23af59c0f70464" + integrity sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw== + dependencies: + mkdirp "^1.0.4" + "@panva/asn1.js@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@panva/asn1.js/-/asn1.js-1.0.0.tgz#dd55ae7b8129e02049f009408b97c61ccf9032f6" integrity sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw== -"@prisma/cli@2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@prisma/cli/-/cli-2.8.0.tgz#919d7f66023affa76d14823212b62a8512cfd37d" - integrity sha512-Kg1C47d75jdEIMmJif8TMlv/2Ihx08E1qWp0euwoZhjd807HGnjgC9tJYjTfkdf+NMJSAUbvoPXKInEX0HoOMw== +"@prisma/cli@2.8.1": + version "2.8.1" + resolved "https://registry.yarnpkg.com/@prisma/cli/-/cli-2.8.1.tgz#fdfeb56857f2fb08ec9769537279745749effcf7" + integrity sha512-mEGJplClGnXI5ki4R0Xq8nq62zcyu7Wzdhybege+cFPZFUbFj0petaPZl/tqta9o5neDyvpU/lDgOwef6mwySg== -"@prisma/client@2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.8.0.tgz#a0f7247786c9b6ee804437acf8215854c5eb3946" - integrity sha512-5+GzRTkPnmv4OEV2tB8kwQt/xLLxBR/daJBcMt6pnnonJvrREsu0tSTdz2LJNPaj3kTT0fSS/OaeGMMdfVYSpw== +"@prisma/client@2.8.1": + version "2.8.1" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.8.1.tgz#6fe87968eed42901cf76c623985222ebc318c292" + integrity sha512-apt6ioi2euOZA1O9mPA8AMRjPoPECsva76gMCcHCVgHvhkMNpFkcbn+UTkErJYrTgcRR7CPQt4D+fw8pkAHfjA== dependencies: pkg-up "^3.1.0" @@ -1816,7 +1833,7 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.0, ajv@^6.12.4: +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.4: version "6.12.5" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag== @@ -2351,28 +2368,27 @@ builtin-status-codes@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= -cacache@13.0.1: - version "13.0.1" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-13.0.1.tgz#a8000c21697089082f85287a1aec6e382024a71c" - integrity sha512-5ZvAxd05HDDU+y9BVvcqYu2LLXmPnQ0hW62h32g4xBTgL/MppR4/04NHfj/ycM2y6lmTnbw6HVi+1eN0Psba6w== +cacache@15.0.5: + version "15.0.5" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.5.tgz#69162833da29170d6732334643c60e005f5f17d0" + integrity sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A== dependencies: - chownr "^1.1.2" - figgy-pudding "^3.5.1" + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" fs-minipass "^2.0.0" glob "^7.1.4" - graceful-fs "^4.2.2" infer-owner "^1.0.4" - lru-cache "^5.1.1" - minipass "^3.0.0" + lru-cache "^6.0.0" + minipass "^3.1.1" minipass-collect "^1.0.2" minipass-flush "^1.0.5" minipass-pipeline "^1.2.2" - mkdirp "^0.5.1" - move-concurrently "^1.0.1" - p-map "^3.0.0" + mkdirp "^1.0.3" + p-map "^4.0.0" promise-inflight "^1.0.1" - rimraf "^2.7.1" - ssri "^7.0.0" + rimraf "^3.0.2" + ssri "^8.0.0" + tar "^6.0.2" unique-filename "^1.1.1" cacache@^12.0.2: @@ -2568,7 +2584,7 @@ chokidar@^3.4.1: optionalDependencies: fsevents "~2.1.2" -chownr@^1.1.1, chownr@^1.1.2: +chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== @@ -2649,15 +2665,6 @@ clone-deep@^0.2.4: lazy-cache "^1.0.3" shallow-clone "^0.1.2" -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - clone-regexp@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-2.2.0.tgz#7d65e00885cd8796405c35a737e7a86b7429e36f" @@ -2975,24 +2982,23 @@ css-has-pseudo@^0.10.0: postcss "^7.0.6" postcss-selector-parser "^5.0.0-rc.4" -css-loader@3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.5.3.tgz#95ac16468e1adcd95c844729e0bb167639eb0bcf" - integrity sha512-UEr9NH5Lmi7+dguAm+/JSPovNjYbm2k3TK58EiwQHzOHH5Jfq1Y+XoP2bQO6TMn7PptMd0opxxedAWcaSTRKHw== +css-loader@4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.3.0.tgz#c888af64b2a5b2e85462c72c0f4a85c7e2e0821e" + integrity sha512-rdezjCjScIrsL8BSYszgT4s476IcNKt6yX69t0pHjJVnPUTDpn4WfIpDQTN3wCJvUvfsz/mFjuGOekf3PY3NUg== dependencies: - camelcase "^5.3.1" + camelcase "^6.0.0" cssesc "^3.0.0" icss-utils "^4.1.1" - loader-utils "^1.2.3" - normalize-path "^3.0.0" - postcss "^7.0.27" + loader-utils "^2.0.0" + postcss "^7.0.32" postcss-modules-extract-imports "^2.0.0" - postcss-modules-local-by-default "^3.0.2" + postcss-modules-local-by-default "^3.0.3" postcss-modules-scope "^2.2.0" postcss-modules-values "^3.0.0" - postcss-value-parser "^4.0.3" - schema-utils "^2.6.6" - semver "^6.3.0" + postcss-value-parser "^4.1.0" + schema-utils "^2.7.1" + semver "^7.3.2" css-prefers-color-scheme@^3.1.1: version "3.1.1" @@ -3212,10 +3218,10 @@ data-uri-to-buffer@3.0.0: dependencies: buffer-from "^1.1.1" -date-fns-tz@^1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.0.10.tgz#30fef0038f80534fddd8e133a6b8ca55ba313748" - integrity sha512-cHQAz0/9uDABaUNDM80Mj1FL4ODlxs1xEY4b0DQuAooO2UdNKvDkNbV8ogLnxLbv02Ru1HXFcot0pVvDRBgptg== +date-fns-tz@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.0.11.tgz#e7d9ceea5ff29280e0ed11a436ac01e10bbc5754" + integrity sha512-nrgEo+rCw1g5KFsIfcnaAIMPg5l6rV5fwlfoPeiGpgctUPiEVI1KNyDY0HXl/LWXpPhTPK4e6SInuPA+bZKBDg== date-fns@^2.16.1: version "2.16.1" @@ -3699,10 +3705,10 @@ eslint-plugin-react-hooks@^4.1.2: resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.1.2.tgz#2eb53731d11c95826ef7a7272303eabb5c9a271e" integrity sha512-ykUeqkGyUGgwTtk78C0o8UG2fzwmgJ0qxBGPp2WqRKsTwcLuVf01kTDRAtOsd4u6whX2XOC8749n2vPydP82fg== -eslint-plugin-react@^7.21.2: - version "7.21.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.21.2.tgz#3bd5d2c4c36d5a0428d0d6dda301ac9a84d681b2" - integrity sha512-j3XKvrK3rpBzveKFbgAeGsWb9uz6iUOrR0jixRfjwdFeGSRsXvVTFtHDQYCjsd1/6Z/xvb8Vy3LiI5Reo7fDrg== +eslint-plugin-react@^7.21.3: + version "7.21.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.21.3.tgz#71655d2af5155b19285ec929dd2cdc67a4470b52" + integrity sha512-OI4GwTCqyIb4ipaOEGLWdaOHCXZZydStAsBEPB2e1ZfNM37bojpgO1BoOQbFb0eLVz3QLDx7b+6kYcrxCuJfhw== dependencies: array-includes "^3.1.1" array.prototype.flatmap "^1.2.3" @@ -4418,7 +4424,7 @@ gonzales-pe@^4.3.0: dependencies: minimist "^1.2.5" -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4: +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== @@ -5230,6 +5236,11 @@ kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +klona@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0" + integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA== + known-css-properties@^0.19.0: version "0.19.0" resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.19.0.tgz#5d92b7fa16c72d971bda9b7fe295bdf61836ee5b" @@ -5498,7 +5509,7 @@ loud-rejection@*, loud-rejection@^2.2.0: currently-unhandled "^0.4.1" signal-exit "^3.0.2" -lru-cache@6.0.0: +lru-cache@6.0.0, lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== @@ -5580,11 +5591,12 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -maxmind@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/maxmind/-/maxmind-4.2.0.tgz#912e5ec4a961807d20d7fb541160aeb5ea802c1c" - integrity sha512-TADiE11Q10IjvLtlo05tTD52xLqfCJMhE3eYJHmpYIKg668STi/fQZGH9X3FpqpIP/2WPgKFxf899awFvfMtQA== +maxmind@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/maxmind/-/maxmind-4.3.0.tgz#0e04f00503e3693615efe848e94ec959500d7573" + integrity sha512-qYdCsoivX7xGW8L5UhjZNxx57odwzmzFPtSt3YvGwW8moSKoIwnCq2NbmfHdmqYzj5ffzSzxKM3lqURN8INyzA== dependencies: + mmdb-lib "1.2.0" tiny-lru "7.0.6" md5.js@^1.3.4: @@ -5865,11 +5877,16 @@ mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1: dependencies: minimist "^1.2.5" -mkdirp@^1.0.3: +mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mmdb-lib@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mmdb-lib/-/mmdb-lib-1.2.0.tgz#0ecd93f4942f65a2d09be0502fa9126939606727" + integrity sha512-3XYebkStxqCgWJjsmT9FCaE19Yi4Tvs2SBPKhUks3rJJh52oF1AKAd9kei+LTutud3a6RCZ0o2Um96Fn7o3zVA== + moment-timezone@^0.5.31: version "0.5.31" resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05" @@ -5957,7 +5974,7 @@ neo-async@2.6.1: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== -neo-async@^2.5.0, neo-async@^2.6.1: +neo-async@^2.5.0, neo-async@^2.6.1, neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== @@ -5967,22 +5984,21 @@ next-tick@~1.0.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= -next@^9.5.3: - version "9.5.3" - resolved "https://registry.yarnpkg.com/next/-/next-9.5.3.tgz#7af5270631f98d330a7f75a6e8e1ac202aa155e2" - integrity sha512-DGrpTNGV2RNMwLaSzpgbkbaUuVk30X71/roXHS10isSXo2Gm+qWcjonDyOxf1KmOvHZRHA/Fa+LaAR7ysdYS3A== +next@^9.5.4: + version "9.5.4" + resolved "https://registry.yarnpkg.com/next/-/next-9.5.4.tgz#3c6aa3fd38ff1711e956ea2b6833475e0262ec35" + integrity sha512-dicsJSxiUFcRjeZ/rNMAO3HS5ttFFuRHhdAn5g7lHnWUZ3MnEX4ggBIihaoUr6qu2So9KoqUPXpS91MuSXUmBw== dependencies: "@ampproject/toolbox-optimizer" "2.6.0" "@babel/code-frame" "7.10.4" "@babel/core" "7.7.7" "@babel/plugin-proposal-class-properties" "7.10.4" "@babel/plugin-proposal-export-namespace-from" "7.10.4" - "@babel/plugin-proposal-nullish-coalescing-operator" "7.10.4" "@babel/plugin-proposal-numeric-separator" "7.10.4" "@babel/plugin-proposal-object-rest-spread" "7.11.0" - "@babel/plugin-proposal-optional-chaining" "7.11.0" "@babel/plugin-syntax-bigint" "7.8.3" "@babel/plugin-syntax-dynamic-import" "7.8.3" + "@babel/plugin-syntax-jsx" "7.10.4" "@babel/plugin-transform-modules-commonjs" "7.10.4" "@babel/plugin-transform-runtime" "7.11.5" "@babel/preset-env" "7.11.5" @@ -5991,19 +6007,20 @@ next@^9.5.3: "@babel/preset-typescript" "7.10.4" "@babel/runtime" "7.11.2" "@babel/types" "7.11.5" - "@next/react-dev-overlay" "9.5.3" - "@next/react-refresh-utils" "9.5.3" + "@next/env" "9.5.4" + "@next/polyfill-module" "9.5.4" + "@next/react-dev-overlay" "9.5.4" + "@next/react-refresh-utils" "9.5.4" ast-types "0.13.2" - babel-plugin-syntax-jsx "6.18.0" babel-plugin-transform-define "2.0.0" babel-plugin-transform-react-remove-prop-types "0.4.24" browserslist "4.13.0" buffer "5.6.0" - cacache "13.0.1" + cacache "15.0.5" caniuse-lite "^1.0.30001113" chokidar "2.1.8" crypto-browserify "3.12.0" - css-loader "3.5.3" + css-loader "4.3.0" cssnano-simple "1.2.0" find-cache-dir "3.3.1" jest-worker "24.9.0" @@ -6020,15 +6037,15 @@ next@^9.5.3: react-is "16.13.1" react-refresh "0.8.3" resolve-url-loader "3.1.1" - sass-loader "8.0.2" - schema-utils "2.6.6" + sass-loader "10.0.2" + schema-utils "2.7.1" stream-browserify "3.0.0" style-loader "1.2.1" styled-jsx "3.3.0" use-subscription "1.4.1" vm-browserify "1.1.2" watchpack "2.0.0-beta.13" - web-vitals "0.2.1" + web-vitals "0.2.4" webpack "4.44.1" webpack-sources "1.4.3" @@ -6401,13 +6418,6 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" -p-map@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-3.0.0.tgz#d704d9af8a2ba684e2600d9a215983d4141a979d" - integrity sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ== - dependencies: - aggregate-error "^3.0.0" - p-map@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" @@ -6837,7 +6847,7 @@ postcss-modules-extract-imports@^2.0.0: dependencies: postcss "^7.0.5" -postcss-modules-local-by-default@^3.0.2: +postcss-modules-local-by-default@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0" integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw== @@ -7022,7 +7032,7 @@ postcss-value-parser@^3.2.3: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== -postcss-value-parser@^4.0.3, postcss-value-parser@^4.1.0: +postcss-value-parser@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== @@ -7054,7 +7064,7 @@ postcss@7.0.32: source-map "^0.6.1" supports-color "^6.1.0" -postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: +postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: version "7.0.34" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.34.tgz#f2baf57c36010df7de4009940f21532c16d65c20" integrity sha512-H/7V2VeNScX9KE83GDrDZNiGT1m2H+UTnlinIzhjlLX9hfMUn1mHNnGeX81a1c8JSBdBvqk7c2ZOG6ZPn5itGw== @@ -7701,7 +7711,7 @@ rimraf@2.6.3: dependencies: glob "^7.1.3" -rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3, rimraf@^2.7.1: +rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -7793,16 +7803,16 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sass-loader@8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-8.0.2.tgz#debecd8c3ce243c76454f2e8290482150380090d" - integrity sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ== +sass-loader@10.0.2: + version "10.0.2" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.0.2.tgz#c7b73010848b264792dd45372eea0b87cba4401e" + integrity sha512-wV6NDUVB8/iEYMalV/+139+vl2LaRFlZGEd5/xmdcdzQcgmis+npyco6NsDTVOlNA3y2NV9Gcz+vHyFMIT+ffg== dependencies: - clone-deep "^4.0.1" - loader-utils "^1.2.3" - neo-async "^2.6.1" - schema-utils "^2.6.1" - semver "^6.3.0" + klona "^2.0.3" + loader-utils "^2.0.0" + neo-async "^2.6.2" + schema-utils "^2.7.1" + semver "^7.3.2" sax@^1.2.4, sax@~1.2.4: version "1.2.4" @@ -7825,7 +7835,7 @@ scheduler@^0.19.1: loose-envify "^1.1.0" object-assign "^4.1.1" -schema-utils@*, schema-utils@^2.6.1, schema-utils@^2.6.6: +schema-utils@*, schema-utils@2.7.1, schema-utils@^2.6.6, schema-utils@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== @@ -7834,14 +7844,6 @@ schema-utils@*, schema-utils@^2.6.1, schema-utils@^2.6.6: ajv "^6.12.4" ajv-keywords "^3.5.2" -schema-utils@2.6.6: - version "2.6.6" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.6.tgz#299fe6bd4a3365dc23d99fd446caff8f1d6c330c" - integrity sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA== - dependencies: - ajv "^6.12.0" - ajv-keywords "^3.4.1" - schema-utils@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" @@ -7871,7 +7873,7 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@^6.0.0, semver@^6.1.2, semver@^6.3.0: +semver@^6.0.0, semver@^6.1.2: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -7926,13 +7928,6 @@ shallow-clone@^0.1.2: lazy-cache "^0.2.3" mixin-object "^2.0.1" -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - shallow-equal@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da" @@ -8155,12 +8150,11 @@ ssri@^6.0.1: dependencies: figgy-pudding "^3.5.1" -ssri@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-7.1.0.tgz#92c241bf6de82365b5c7fb4bd76e975522e1294d" - integrity sha512-77/WrDZUWocK0mvA5NTRQyveUf+wsrIc6vyrxpS8tVvYBcX215QbafrJR3KtkpskIzoFLqqNuuYQvxaMjXJ/0g== +ssri@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.0.tgz#79ca74e21f8ceaeddfcb4b90143c458b8d988808" + integrity sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA== dependencies: - figgy-pudding "^3.5.1" minipass "^3.1.1" stable@^0.1.8: @@ -8619,7 +8613,7 @@ tar@^4.4.2: safe-buffer "^5.1.2" yallist "^3.0.3" -tar@^6.0.5: +tar@^6.0.2, tar@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f" integrity sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg== @@ -9120,10 +9114,10 @@ uuid@^7.0.3: resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== -uuid@^8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" - integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== +uuid@^8.3.1: + version "8.3.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31" + integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg== v8-compile-cache@^2.0.3, v8-compile-cache@^2.1.1: version "2.1.1" @@ -9215,10 +9209,10 @@ watchpack@^1.7.4: chokidar "^3.4.1" watchpack-chokidar2 "^2.0.0" -web-vitals@0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-0.2.1.tgz#60782fa690243fe35613759a0c26431f57ba7b2d" - integrity sha512-2pdRlp6gJpOCg0oMMqwFF0axjk5D9WInc09RSYtqFgPXQ15+YKNQ7YnBBEqAL5jvmfH9WvoXDMb8DHwux7pIew== +web-vitals@0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-0.2.4.tgz#ec3df43c834a207fd7cdefd732b2987896e08511" + integrity sha512-6BjspCO9VriYy12z356nL6JBS0GYeEcA457YyRzD+dD6XYCQ75NKhcOHUMHentOE7OcVCIXXDvOm0jKFfQG2Gg== webidl-conversions@^4.0.2: version "4.0.2" From 5f5335ed1e8e5077638519d4dae4e49a49294327 Mon Sep 17 00:00:00 2001 From: Florian Kapfenberger Date: Thu, 8 Oct 2020 10:27:53 +0200 Subject: [PATCH 02/27] feat: add link to changelog when clicking version --- components/layout/Footer.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/components/layout/Footer.js b/components/layout/Footer.js index 6fd0a46c..62472f4b 100644 --- a/components/layout/Footer.js +++ b/components/layout/Footer.js @@ -24,7 +24,11 @@ export default function Footer() { }} /> -
{`v${current}`}
+
+ + {`v${current}`} + +
); From e64a555652bc88361f8549a9e7be3bae44061298 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 8 Oct 2020 15:02:48 -0700 Subject: [PATCH 03/27] Renamed methods. Initial work on realtime dashboard. --- components/common/RefreshButton.js | 2 +- components/layout/Header.js | 3 + components/metrics/MetricsBar.js | 2 +- components/metrics/MetricsTable.js | 2 +- components/metrics/RealtimeChart.js | 80 +++++++++++++++++++ components/pages/RealtimeDashboard.js | 36 +++++++++ components/pages/RealtimeDashboard.module.css | 3 + components/pages/TestConsole.js | 2 +- ...Test.module.css => TestConsole.module.css} | 0 lib/queries.js | 4 +- pages/api/realtime.js | 20 +++++ pages/api/website/[id]/metrics.js | 58 +++++++++++--- pages/api/website/[id]/pageviews.js | 6 +- pages/api/website/[id]/rankings.js | 68 ---------------- pages/api/website/[id]/stats.js | 28 +++++++ pages/realtime.js | 18 +++++ 16 files changed, 246 insertions(+), 86 deletions(-) create mode 100644 components/metrics/RealtimeChart.js create mode 100644 components/pages/RealtimeDashboard.js create mode 100644 components/pages/RealtimeDashboard.module.css rename components/pages/{Test.module.css => TestConsole.module.css} (100%) create mode 100644 pages/api/realtime.js delete mode 100644 pages/api/website/[id]/rankings.js create mode 100644 pages/api/website/[id]/stats.js create mode 100644 pages/realtime.js diff --git a/components/common/RefreshButton.js b/components/common/RefreshButton.js index af754a9c..013cbf54 100644 --- a/components/common/RefreshButton.js +++ b/components/common/RefreshButton.js @@ -12,7 +12,7 @@ export default function RefreshButton({ websiteId }) { const dispatch = useDispatch(); const [dateRange] = useDateRange(websiteId); const [loading, setLoading] = useState(false); - const completed = useSelector(state => state.queries[`/api/website/${websiteId}/metrics`]); + const completed = useSelector(state => state.queries[`/api/website/${websiteId}/stats`]); function handleClick() { if (dateRange) { diff --git a/components/layout/Header.js b/components/layout/Header.js index c48fdd11..cc8baae3 100644 --- a/components/layout/Header.js +++ b/components/layout/Header.js @@ -30,6 +30,9 @@ export default function Header() { + + + diff --git a/components/metrics/MetricsBar.js b/components/metrics/MetricsBar.js index b7a47a10..ed2c7b12 100644 --- a/components/metrics/MetricsBar.js +++ b/components/metrics/MetricsBar.js @@ -19,7 +19,7 @@ export default function MetricsBar({ websiteId, token, className }) { } = usePageQuery(); const { data, error, loading } = useFetch( - `/api/website/${websiteId}/metrics`, + `/api/website/${websiteId}/stats`, { start_at: +startDate, end_at: +endDate, diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js index 6850a3bf..162dce0f 100644 --- a/components/metrics/MetricsTable.js +++ b/components/metrics/MetricsTable.js @@ -38,7 +38,7 @@ export default function MetricsTable({ } = usePageQuery(); const { data, loading, error } = useFetch( - `/api/website/${websiteId}/rankings`, + `/api/website/${websiteId}/metrics`, { type, start_at: +startDate, diff --git a/components/metrics/RealtimeChart.js b/components/metrics/RealtimeChart.js new file mode 100644 index 00000000..0eb3c528 --- /dev/null +++ b/components/metrics/RealtimeChart.js @@ -0,0 +1,80 @@ +import React from 'react'; +import { useIntl } from 'react-intl'; +import tinycolor from 'tinycolor2'; +import BarChart from './BarChart'; +import useTheme from 'hooks/useTheme'; +import { THEME_COLORS } from 'lib/constants'; + +export default function RealtimeChart({ websiteId, data, unit, records, className, loading }) { + const intl = useIntl(); + const [theme] = useTheme(); + const primaryColor = tinycolor(THEME_COLORS[theme].primary); + const colors = { + views: { + background: primaryColor.setAlpha(0.4).toRgbString(), + border: primaryColor.setAlpha(0.5).toRgbString(), + }, + visitors: { + background: primaryColor.setAlpha(0.6).toRgbString(), + border: primaryColor.setAlpha(0.7).toRgbString(), + }, + }; + + const handleUpdate = chart => { + const { + data: { datasets }, + } = chart; + + datasets[0].data = data.uniques; + datasets[0].label = intl.formatMessage({ + id: 'metrics.unique-visitors', + defaultMessage: 'Unique visitors', + }); + datasets[1].data = data.pageviews; + datasets[1].label = intl.formatMessage({ + id: 'metrics.page-views', + defaultMessage: 'Page views', + }); + + chart.update(); + }; + + if (!data) { + return null; + } + + return ( + + ); +} diff --git a/components/pages/RealtimeDashboard.js b/components/pages/RealtimeDashboard.js new file mode 100644 index 00000000..7fb56596 --- /dev/null +++ b/components/pages/RealtimeDashboard.js @@ -0,0 +1,36 @@ +import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; +import Page from '../layout/Page'; +import PageHeader from '../layout/PageHeader'; +import useFetch from '../../hooks/useFetch'; +import DropDown from '../common/DropDown'; +import RealtimeChart from '../metrics/RealtimeChart'; + +export default function TestConsole() { + const user = useSelector(state => state.user); + const [website, setWebsite] = useState(); + const { data } = useFetch('/api/websites'); + + if (!data || !user?.is_admin) { + return null; + } + + const options = [{ label: 'All websites', value: 0 }].concat( + data.map(({ name, website_id }) => ({ label: name, value: website_id })), + ); + const selectedValue = options.find(({ value }) => value === website?.website_id)?.value || 0; + + function handleSelect(value) { + setWebsite(data.find(({ website_id }) => website_id === value)); + } + + return ( + + +
Real time
+ +
+ +
+ ); +} diff --git a/components/pages/RealtimeDashboard.module.css b/components/pages/RealtimeDashboard.module.css new file mode 100644 index 00000000..abde53a3 --- /dev/null +++ b/components/pages/RealtimeDashboard.module.css @@ -0,0 +1,3 @@ +.container { + display: flex; +} diff --git a/components/pages/TestConsole.js b/components/pages/TestConsole.js index f6fa8a23..fef6c620 100644 --- a/components/pages/TestConsole.js +++ b/components/pages/TestConsole.js @@ -7,7 +7,7 @@ import Page from '../layout/Page'; import PageHeader from '../layout/PageHeader'; import useFetch from '../../hooks/useFetch'; import DropDown from '../common/DropDown'; -import styles from './Test.module.css'; +import styles from './TestConsole.module.css'; import WebsiteChart from '../metrics/WebsiteChart'; import EventsChart from '../metrics/EventsChart'; import Button from '../common/Button'; diff --git a/components/pages/Test.module.css b/components/pages/TestConsole.module.css similarity index 100% rename from components/pages/Test.module.css rename to components/pages/TestConsole.module.css diff --git a/lib/queries.js b/lib/queries.js index 3b138faa..6312057f 100644 --- a/lib/queries.js +++ b/lib/queries.js @@ -285,7 +285,7 @@ export async function createAccount(data) { ); } -export function getMetrics(website_id, start_at, end_at, filters = {}) { +export function getWebsiteStats(website_id, start_at, end_at, filters = {}) { const params = [website_id, start_at, end_at]; const { url } = filters; let urlFilter = ''; @@ -317,7 +317,7 @@ export function getMetrics(website_id, start_at, end_at, filters = {}) { ); } -export function getPageviews( +export function getPageviewStats( website_id, start_at, end_at, diff --git a/pages/api/realtime.js b/pages/api/realtime.js new file mode 100644 index 00000000..7717c9b9 --- /dev/null +++ b/pages/api/realtime.js @@ -0,0 +1,20 @@ +import { useAuth } from 'lib/middleware'; +import { ok, unauthorized, methodNotAllowed } from 'lib/response'; + +export default async (req, res) => { + await useAuth(req, res); + + const { is_admin } = req.auth; + + if (!is_admin) { + return unauthorized(res); + } + + if (req.method === 'GET') { + const [pageviews, sessions, events] = await Promise.all([[], [], []]); + + return ok(res, { pageviews, sessions, events }); + } + + return methodNotAllowed(res); +}; diff --git a/pages/api/website/[id]/metrics.js b/pages/api/website/[id]/metrics.js index 91a0f4cf..17aa6daa 100644 --- a/pages/api/website/[id]/metrics.js +++ b/pages/api/website/[id]/metrics.js @@ -1,27 +1,67 @@ -import { getMetrics } from 'lib/queries'; -import { methodNotAllowed, ok, unauthorized } from 'lib/response'; +import { getPageviewMetrics, getSessionMetrics } from 'lib/queries'; +import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response'; +import { DOMAIN_REGEX } from 'lib/constants'; import { allowQuery } from 'lib/auth'; +const sessionColumns = ['browser', 'os', 'device', 'country']; +const pageviewColumns = ['url', 'referrer']; + +function getTable(type) { + if (type === 'event') { + return 'event'; + } + + if (sessionColumns.includes(type)) { + return 'session'; + } + + return 'pageview'; +} + +function getColumn(type) { + if (type === 'event') { + return `concat(event_type, ':', event_value)`; + } + return type; +} + export default async (req, res) => { if (req.method === 'GET') { if (!(await allowQuery(req))) { return unauthorized(res); } - const { id, start_at, end_at, url } = req.query; + const { id, type, start_at, end_at, domain, url } = req.query; + + if (domain && !DOMAIN_REGEX.test(domain)) { + return badRequest(res); + } const websiteId = +id; const startDate = new Date(+start_at); const endDate = new Date(+end_at); - const metrics = await getMetrics(websiteId, startDate, endDate, { url }); + if (sessionColumns.includes(type)) { + const data = await getSessionMetrics(websiteId, startDate, endDate, type, { url }); - const stats = Object.keys(metrics[0]).reduce((obj, key) => { - obj[key] = Number(metrics[0][key]) || 0; - return obj; - }, {}); + return ok(res, data); + } - return ok(res, stats); + if (type === 'event' || pageviewColumns.includes(type)) { + const data = await getPageviewMetrics( + websiteId, + startDate, + endDate, + getColumn(type), + getTable(type), + { + domain, + url: type !== 'url' && url, + }, + ); + + return ok(res, data); + } } return methodNotAllowed(res); diff --git a/pages/api/website/[id]/pageviews.js b/pages/api/website/[id]/pageviews.js index 2191a4c4..07362359 100644 --- a/pages/api/website/[id]/pageviews.js +++ b/pages/api/website/[id]/pageviews.js @@ -1,5 +1,5 @@ import moment from 'moment-timezone'; -import { getPageviews } from 'lib/queries'; +import { getPageviewStats } from 'lib/queries'; import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response'; import { allowQuery } from 'lib/auth'; @@ -22,8 +22,8 @@ export default async (req, res) => { } const [pageviews, uniques] = await Promise.all([ - getPageviews(websiteId, startDate, endDate, tz, unit, '*', url), - getPageviews(websiteId, startDate, endDate, tz, unit, 'distinct session_id', url), + getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', url), + getPageviewStats(websiteId, startDate, endDate, tz, unit, 'distinct session_id', url), ]); return ok(res, { pageviews, uniques }); diff --git a/pages/api/website/[id]/rankings.js b/pages/api/website/[id]/rankings.js deleted file mode 100644 index 17aa6daa..00000000 --- a/pages/api/website/[id]/rankings.js +++ /dev/null @@ -1,68 +0,0 @@ -import { getPageviewMetrics, getSessionMetrics } from 'lib/queries'; -import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response'; -import { DOMAIN_REGEX } from 'lib/constants'; -import { allowQuery } from 'lib/auth'; - -const sessionColumns = ['browser', 'os', 'device', 'country']; -const pageviewColumns = ['url', 'referrer']; - -function getTable(type) { - if (type === 'event') { - return 'event'; - } - - if (sessionColumns.includes(type)) { - return 'session'; - } - - return 'pageview'; -} - -function getColumn(type) { - if (type === 'event') { - return `concat(event_type, ':', event_value)`; - } - return type; -} - -export default async (req, res) => { - if (req.method === 'GET') { - if (!(await allowQuery(req))) { - return unauthorized(res); - } - - const { id, type, start_at, end_at, domain, url } = req.query; - - if (domain && !DOMAIN_REGEX.test(domain)) { - return badRequest(res); - } - - const websiteId = +id; - const startDate = new Date(+start_at); - const endDate = new Date(+end_at); - - if (sessionColumns.includes(type)) { - const data = await getSessionMetrics(websiteId, startDate, endDate, type, { url }); - - return ok(res, data); - } - - if (type === 'event' || pageviewColumns.includes(type)) { - const data = await getPageviewMetrics( - websiteId, - startDate, - endDate, - getColumn(type), - getTable(type), - { - domain, - url: type !== 'url' && url, - }, - ); - - return ok(res, data); - } - } - - return methodNotAllowed(res); -}; diff --git a/pages/api/website/[id]/stats.js b/pages/api/website/[id]/stats.js new file mode 100644 index 00000000..8b80a363 --- /dev/null +++ b/pages/api/website/[id]/stats.js @@ -0,0 +1,28 @@ +import { getWebsiteStats } from 'lib/queries'; +import { methodNotAllowed, ok, unauthorized } from 'lib/response'; +import { allowQuery } from 'lib/auth'; + +export default async (req, res) => { + if (req.method === 'GET') { + if (!(await allowQuery(req))) { + return unauthorized(res); + } + + const { id, start_at, end_at, url } = req.query; + + const websiteId = +id; + const startDate = new Date(+start_at); + const endDate = new Date(+end_at); + + const metrics = await getWebsiteStats(websiteId, startDate, endDate, { url }); + + const stats = Object.keys(metrics[0]).reduce((obj, key) => { + obj[key] = Number(metrics[0][key]) || 0; + return obj; + }, {}); + + return ok(res, stats); + } + + return methodNotAllowed(res); +}; diff --git a/pages/realtime.js b/pages/realtime.js new file mode 100644 index 00000000..9f1ebffa --- /dev/null +++ b/pages/realtime.js @@ -0,0 +1,18 @@ +import React from 'react'; +import Layout from 'components/layout/Layout'; +import RealtimeDashboard from 'components/pages/RealtimeDashboard'; +import useRequireLogin from 'hooks/useRequireLogin'; + +export default function RealtimePage() { + const { loading } = useRequireLogin(); + + if (loading) { + return null; + } + + return ( + + + + ); +} From fdc92d087b38f4e8f118dc4d41aa69947181cb6e Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 8 Oct 2020 23:26:05 -0700 Subject: [PATCH 04/27] Update realtime chart. --- components/metrics/ActiveUsers.js | 2 +- components/metrics/BarChart.js | 2 + components/metrics/PageviewsChart.js | 4 +- components/metrics/WebsiteChart.js | 14 ++-- components/pages/RealtimeDashboard.js | 99 ++++++++++++++++++++++----- hooks/useFetch.js | 8 +-- lang/da-DK.json | 2 + lang/de-DE.json | 2 + lang/el-GR.json | 2 + lang/en-US.json | 2 + lang/es-MX.json | 2 + lang/fo-FO.json | 2 + lang/fr-FR.json | 2 + lang/id-ID.json | 2 + lang/ja-JP.json | 2 + lang/mn-MN.json | 2 + lang/nb-NO.json | 2 + lang/nl-NL.json | 2 + lang/pt-PT.json | 2 + lang/ro-RO.json | 2 + lang/ru-RU.json | 2 + lang/sv-SE.json | 2 + lang/tr-TR.json | 2 + lang/uk-UA.json | 2 + lang/zh-CN.json | 2 + lib/date.js | 3 + lib/queries.js | 63 ++++++++++++++--- lib/web.js | 12 ++-- package.json | 2 +- pages/api/realtime.js | 43 ++++++++++-- pages/api/website/[id]/events.js | 4 +- pages/api/website/[id]/pageviews.js | 4 +- 32 files changed, 240 insertions(+), 58 deletions(-) diff --git a/components/metrics/ActiveUsers.js b/components/metrics/ActiveUsers.js index 3d7b7001..ca93f391 100644 --- a/components/metrics/ActiveUsers.js +++ b/components/metrics/ActiveUsers.js @@ -1,8 +1,8 @@ import React, { useMemo } from 'react'; +import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import useFetch from 'hooks/useFetch'; import styles from './ActiveUsers.module.css'; -import { FormattedMessage } from 'react-intl'; export default function ActiveUsers({ websiteId, token, className }) { const { data } = useFetch(`/api/website/${websiteId}/active`, { token }, { interval: 60000 }); diff --git a/components/metrics/BarChart.js b/components/metrics/BarChart.js index 4f10e83e..24f642c1 100644 --- a/components/metrics/BarChart.js +++ b/components/metrics/BarChart.js @@ -39,6 +39,8 @@ export default function BarChart({ const w = canvas.current.width; switch (unit) { + case 'minute': + return dateFormat(d, 'h:mm', locale); case 'hour': return dateFormat(d, 'ha', locale); case 'day': diff --git a/components/metrics/PageviewsChart.js b/components/metrics/PageviewsChart.js index 9120a6c3..d20db5eb 100644 --- a/components/metrics/PageviewsChart.js +++ b/components/metrics/PageviewsChart.js @@ -26,7 +26,7 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa data: { datasets }, } = chart; - datasets[0].data = data.uniques; + datasets[0].data = data.sessions; datasets[0].label = intl.formatMessage({ id: 'metrics.unique-visitors', defaultMessage: 'Unique visitors', @@ -56,7 +56,7 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa id: 'metrics.unique-visitors', defaultMessage: 'Unique visitors', }), - data: data.uniques, + data: data.sessions, lineTension: 0, backgroundColor: colors.visitors.background, borderColor: colors.visitors.border, diff --git a/components/metrics/WebsiteChart.js b/components/metrics/WebsiteChart.js index 6a07afe5..13d369c0 100644 --- a/components/metrics/WebsiteChart.js +++ b/components/metrics/WebsiteChart.js @@ -45,14 +45,14 @@ export default function WebsiteChart({ { onDataLoad, update: [modified] }, ); - const [pageviews, uniques] = useMemo(() => { + const chartData = useMemo(() => { if (data) { - return [ - getDateArray(data.pageviews, startDate, endDate, unit), - getDateArray(data.uniques, startDate, endDate, unit), - ]; + return { + pageviews: getDateArray(data.pageviews, startDate, endDate, unit), + sessions: getDateArray(data.sessions, startDate, endDate, unit), + }; } - return [[], []]; + return { pageviews: [], sessions: [] }; }, [data]); function handleCloseFilter() { @@ -87,7 +87,7 @@ export default function WebsiteChart({ {error && } state.user); +function filterTime(data, time) { + return data.filter(({ created_at }) => new Date(created_at).getTime() > time); +} + +function mapData(data) { + let last = 0; + const arr = []; + + data.reduce((obj, val) => { + const { created_at } = val; + const t = startOfMinute(parseISO(created_at)); + if (t.getTime() > last) { + obj = { t: format(t, 'yyyy-LL-dd HH:mm:00'), y: 1 }; + arr.push(obj); + last = t; + } else { + obj.y += 1; + } + return obj; + }, {}); + + return arr; +} + +export default function RealtimeDashboard() { + const [data, setData] = useState(); const [website, setWebsite] = useState(); - const { data } = useFetch('/api/websites'); + const { data: init, loading } = useFetch('/api/realtime', { type: 'init' }); + const { data: updates } = useFetch( + '/api/realtime', + { type: 'update' }, + { disabled: !init?.token, interval: 5000, headers: { 'x-umami-token': init?.token } }, + ); - if (!data || !user?.is_admin) { + const chartData = useMemo(() => { + if (data) { + const endDate = startOfMinute(new Date()); + const startDate = subMinutes(endDate, 30); + const unit = 'minute'; + + console.log({ data }); + + return { + pageviews: getDateArray(mapData(data.pageviews), startDate, endDate, unit), + sessions: getDateArray(mapData(data.sessions), startDate, endDate, unit), + }; + } + return { pageviews: [], sessions: [] }; + }, [data]); + + useEffect(() => { + if (init && !data) { + setData(init.data); + } else if (updates) { + const { pageviews, sessions, events } = updates; + const time = subMinutes(startOfMinute(new Date()), 30).getTime(); + setData(state => ({ + pageviews: filterTime(state.pageviews, time).concat(pageviews), + sessions: filterTime(state.sessions, time).concat(sessions), + events: filterTime(state.events, time).concat(events), + })); + } + }, [updates, init]); + + if (!init || loading || !data) { return null; } - const options = [{ label: 'All websites', value: 0 }].concat( - data.map(({ name, website_id }) => ({ label: name, value: website_id })), - ); + const { websites } = init; + + const options = [ + { label: , value: 0 }, + ].concat(websites.map(({ name, website_id }) => ({ label: name, value: website_id }))); const selectedValue = options.find(({ value }) => value === website?.website_id)?.value || 0; function handleSelect(value) { - setWebsite(data.find(({ website_id }) => website_id === value)); + setWebsite(websites.find(({ website_id }) => website_id === value)); } return ( -
Real time
+
+ +
- +
); } diff --git a/hooks/useFetch.js b/hooks/useFetch.js index 0eb82b13..b74a8fe7 100644 --- a/hooks/useFetch.js +++ b/hooks/useFetch.js @@ -14,14 +14,14 @@ export default function useFetch(url, params = {}, options = {}) { const keys = Object.keys(params) .sort() .map(key => params[key]); - const { update = [], onDataLoad = () => {} } = options; + const { update = [], onDataLoad = () => {}, disabled, headers } = options; async function loadData() { try { setLoadiing(true); setError(null); const time = performance.now(); - const { data, status } = await get(`${basePath}${url}`, params); + const { data, status } = await get(`${basePath}${url}`, params, headers); dispatch(updateQuery({ url, time: performance.now() - time, completed: Date.now() })); @@ -43,7 +43,7 @@ export default function useFetch(url, params = {}, options = {}) { } useEffect(() => { - if (url) { + if (url && !disabled) { const { interval, delay = 0 } = options; setTimeout(() => loadData(), delay); @@ -54,7 +54,7 @@ export default function useFetch(url, params = {}, options = {}) { clearInterval(id); }; } - }, [url, ...keys, ...update]); + }, [url, disabled, ...keys, ...update]); return { data, status, error, loading }; } diff --git a/lang/da-DK.json b/lang/da-DK.json index cd96cb3f..ae8c4321 100644 --- a/lang/da-DK.json +++ b/lang/da-DK.json @@ -18,6 +18,7 @@ "button.view-details": "Vis detajler", "label.accounts": "Kontoer", "label.administrator": "Administrator", + "label.all-websites": "All websites", "label.confirm-password": "Godkendt adgangskode", "label.current-password": "Nuværende adgangskode", "label.custom-range": "Tilpasset interval", @@ -36,6 +37,7 @@ "label.password": "Adgangskode", "label.passwords-dont-match": "Adgangskoder matcher ikke", "label.profile": "Profil", + "label.realtime": "Realtime", "label.required": "Påkrævet", "label.settings": "Indstillinger", "label.this-month": "Denne måned", diff --git a/lang/de-DE.json b/lang/de-DE.json index 8d507693..fdc7c3a1 100644 --- a/lang/de-DE.json +++ b/lang/de-DE.json @@ -18,6 +18,7 @@ "button.view-details": "Details anzeigen", "label.accounts": "Konten", "label.administrator": "Administrator", + "label.all-websites": "All websites", "label.confirm-password": "Passwort wiederholen", "label.current-password": "Derzeitiges Passwort", "label.custom-range": "Benutzerdefinierter Bereich", @@ -36,6 +37,7 @@ "label.password": "Passwort", "label.passwords-dont-match": "Passwörter stimmen nicht überein", "label.profile": "Profil", + "label.realtime": "Realtime", "label.required": "Erforderlich", "label.settings": "Einstellungen", "label.this-month": "Diesen Monat", diff --git a/lang/el-GR.json b/lang/el-GR.json index 14cd69ca..ec4b3217 100644 --- a/lang/el-GR.json +++ b/lang/el-GR.json @@ -18,6 +18,7 @@ "button.view-details": "Λεπτομέρειες", "label.accounts": "Λογαριασμοί", "label.administrator": "Διαχειριστής", + "label.all-websites": "All websites", "label.confirm-password": "Επιβεβαίωση κωδικού", "label.current-password": "Τωρινός κωδικός πρόσβασης", "label.custom-range": "Προσαρμοσμένο εύρος", @@ -36,6 +37,7 @@ "label.password": "Κωδικός", "label.passwords-dont-match": "Οι κωδικοί πρόσβασης δεν ταιριάζουν", "label.profile": "Προφίλ", + "label.realtime": "Realtime", "label.required": "Απαιτείται", "label.settings": "Ρυθμίσεις", "label.this-month": "Αυτο το μήνα", diff --git a/lang/en-US.json b/lang/en-US.json index 7c513b95..7251c123 100644 --- a/lang/en-US.json +++ b/lang/en-US.json @@ -18,6 +18,7 @@ "button.view-details": "View details", "label.accounts": "Accounts", "label.administrator": "Administrator", + "label.all-websites": "All websites", "label.confirm-password": "Confirm password", "label.current-password": "Current password", "label.custom-range": "Custom range", @@ -36,6 +37,7 @@ "label.password": "Password", "label.passwords-dont-match": "Passwords don't match", "label.profile": "Profile", + "label.realtime": "Realtime", "label.required": "Required", "label.settings": "Settings", "label.this-month": "This month", diff --git a/lang/es-MX.json b/lang/es-MX.json index 16d911ea..9e6a6206 100644 --- a/lang/es-MX.json +++ b/lang/es-MX.json @@ -18,6 +18,7 @@ "button.view-details": "Ver detalles", "label.accounts": "Usuarios", "label.administrator": "Administrador", + "label.all-websites": "All websites", "label.confirm-password": "Confirmar contraseña", "label.current-password": "Contraseña actual", "label.custom-range": "Custom range", @@ -36,6 +37,7 @@ "label.password": "Contraseña", "label.passwords-dont-match": "Las contraseñas no coinciden", "label.profile": "Perfil", + "label.realtime": "Realtime", "label.required": "Requerido", "label.settings": "Configuraciones", "label.this-month": "Este mes", diff --git a/lang/fo-FO.json b/lang/fo-FO.json index db9e972b..2c379960 100644 --- a/lang/fo-FO.json +++ b/lang/fo-FO.json @@ -18,6 +18,7 @@ "button.view-details": "Vís upplýsingar", "label.accounts": "Brúkarar", "label.administrator": "Administrator", + "label.all-websites": "All websites", "label.confirm-password": "Vátta loyniorð", "label.current-password": "Núverandi loyniorð", "label.custom-range": "Tillaga spenni", @@ -36,6 +37,7 @@ "label.password": "Loyniorð", "label.passwords-dont-match": "Loyniorðini eru ikki eins", "label.profile": "Brúkari", + "label.realtime": "Realtime", "label.required": "Krav", "label.settings": "Stillingar", "label.this-month": "Hendan mánan", diff --git a/lang/fr-FR.json b/lang/fr-FR.json index 762cae34..013f89e5 100644 --- a/lang/fr-FR.json +++ b/lang/fr-FR.json @@ -18,6 +18,7 @@ "button.view-details": "Voir les details", "label.accounts": "Comptes", "label.administrator": "Administrateur", + "label.all-websites": "All websites", "label.confirm-password": "Confirmation du mot de passe", "label.current-password": "Mot de passe actuel", "label.custom-range": "Plage personnalisée", @@ -36,6 +37,7 @@ "label.password": "Mot de passe", "label.passwords-dont-match": "Les mots de passe ne correspondent pas", "label.profile": "Profile", + "label.realtime": "Realtime", "label.required": "Requis", "label.settings": "Paramètres", "label.this-month": "Ce mois ci", diff --git a/lang/id-ID.json b/lang/id-ID.json index 8099cd75..8b09c0a6 100644 --- a/lang/id-ID.json +++ b/lang/id-ID.json @@ -18,6 +18,7 @@ "button.view-details": "Lihat Detil", "label.accounts": "Akun", "label.administrator": "Pengelola", + "label.all-websites": "All websites", "label.confirm-password": "Konfirmasi kata sandi", "label.current-password": "Kata sandi sekarang", "label.custom-range": "Rentang khusus", @@ -36,6 +37,7 @@ "label.password": "Kata sandi", "label.passwords-dont-match": "Kata sandi tidak cocok", "label.profile": "Profil", + "label.realtime": "Realtime", "label.required": "Wajib", "label.settings": "Pengaturan", "label.this-month": "Bulan ini", diff --git a/lang/ja-JP.json b/lang/ja-JP.json index 51cdaec1..bbf73ed5 100644 --- a/lang/ja-JP.json +++ b/lang/ja-JP.json @@ -18,6 +18,7 @@ "button.view-details": "詳細を見る", "label.accounts": "アカウント", "label.administrator": "管理者", + "label.all-websites": "All websites", "label.confirm-password": "パスワード(確認)", "label.current-password": "現在のパスワード", "label.custom-range": "期間を指定する", @@ -36,6 +37,7 @@ "label.password": "パスワード", "label.passwords-dont-match": "パスワードが一致しません", "label.profile": "プロファイル", + "label.realtime": "Realtime", "label.required": "必須", "label.settings": "設定", "label.this-month": "今月", diff --git a/lang/mn-MN.json b/lang/mn-MN.json index 694e068e..70af8f29 100644 --- a/lang/mn-MN.json +++ b/lang/mn-MN.json @@ -18,6 +18,7 @@ "button.view-details": "Дэлгэрүүлж харах", "label.accounts": "Хэрэглэгчид", "label.administrator": "Админ", + "label.all-websites": "All websites", "label.confirm-password": "Шинэ нууц үгээ давтах", "label.current-password": "Ашиглаж буй нууц үг", "label.custom-range": "Дурын хугацаа", @@ -36,6 +37,7 @@ "label.password": "Нууц үг", "label.passwords-dont-match": "Нууц үг тохирохгүй байна", "label.profile": "Бүртгэл", + "label.realtime": "Realtime", "label.required": "Шаардлагатай", "label.settings": "Тохиргоо", "label.this-month": "Энэ сар", diff --git a/lang/nb-NO.json b/lang/nb-NO.json index dcd97d47..8e93b05c 100644 --- a/lang/nb-NO.json +++ b/lang/nb-NO.json @@ -18,6 +18,7 @@ "button.view-details": "Vis detaljer", "label.accounts": "Kontoer", "label.administrator": "Administrator", + "label.all-websites": "All websites", "label.confirm-password": "Godkjenn passord", "label.current-password": "Nåværende passord", "label.custom-range": "Egendefinert utvalg", @@ -36,6 +37,7 @@ "label.password": "Passord", "label.passwords-dont-match": "Passordene er ikke like", "label.profile": "Profil", + "label.realtime": "Realtime", "label.required": "Påkrevd", "label.settings": "Innstillinger", "label.this-month": "Denne måneden", diff --git a/lang/nl-NL.json b/lang/nl-NL.json index 54f7894f..8978d7ee 100644 --- a/lang/nl-NL.json +++ b/lang/nl-NL.json @@ -18,6 +18,7 @@ "button.view-details": "Meer details", "label.accounts": "Gebruikers", "label.administrator": "Administrator", + "label.all-websites": "All websites", "label.confirm-password": "Wachtwoord bevestigen", "label.current-password": "Huidig wachtwoord", "label.custom-range": "Aangepast bereik", @@ -36,6 +37,7 @@ "label.password": "Wachtwoord", "label.passwords-dont-match": "Wachtwoorden komen niet overeen", "label.profile": "Profiel", + "label.realtime": "Realtime", "label.required": "Verplicht", "label.settings": "Instellingen", "label.this-month": "Deze maand", diff --git a/lang/pt-PT.json b/lang/pt-PT.json index db9a8a7f..971c1aa7 100644 --- a/lang/pt-PT.json +++ b/lang/pt-PT.json @@ -18,6 +18,7 @@ "button.view-details": "Ver detalhes", "label.accounts": "Contas", "label.administrator": "Administrador", + "label.all-websites": "All websites", "label.confirm-password": "Confirmar palavra-passe", "label.current-password": "Palavra-passe atual", "label.custom-range": "Intervalo personalizado", @@ -36,6 +37,7 @@ "label.password": "Palavra-passe", "label.passwords-dont-match": "Palavra-passes não correspondem", "label.profile": "Perfil", + "label.realtime": "Realtime", "label.required": "Obrigatório", "label.settings": "Definições", "label.this-month": "Este mês", diff --git a/lang/ro-RO.json b/lang/ro-RO.json index edaf27d6..8e28f887 100644 --- a/lang/ro-RO.json +++ b/lang/ro-RO.json @@ -18,6 +18,7 @@ "button.view-details": "Vizualizare detalii", "label.accounts": "Conturi", "label.administrator": "Administrator", + "label.all-websites": "All websites", "label.confirm-password": "Confirmare parolă", "label.current-password": "Parola curentă", "label.custom-range": "Interval personalizat", @@ -36,6 +37,7 @@ "label.password": "Parolă", "label.passwords-dont-match": "Parolele nu se potrivesc", "label.profile": "Profil", + "label.realtime": "Realtime", "label.required": "Obligatoriu", "label.settings": "Setări", "label.this-month": "Această lună", diff --git a/lang/ru-RU.json b/lang/ru-RU.json index 74985574..05c9e0da 100644 --- a/lang/ru-RU.json +++ b/lang/ru-RU.json @@ -18,6 +18,7 @@ "button.view-details": "Посмотреть детали", "label.accounts": "Аккаунты", "label.administrator": "Администратор", + "label.all-websites": "All websites", "label.confirm-password": "Подтвердить пароль", "label.current-password": "Текущий пароль", "label.custom-range": "Другой период", @@ -36,6 +37,7 @@ "label.password": "Пароль", "label.passwords-dont-match": "Пароли не совпадают", "label.profile": "Профиль", + "label.realtime": "Realtime", "label.required": "Обязательное", "label.settings": "Настройки", "label.this-month": "Этот месяц", diff --git a/lang/sv-SE.json b/lang/sv-SE.json index 9e5e5958..5a1662a5 100644 --- a/lang/sv-SE.json +++ b/lang/sv-SE.json @@ -18,6 +18,7 @@ "button.view-details": "Visa detaljer", "label.accounts": "Konton", "label.administrator": "Administratör", + "label.all-websites": "All websites", "label.confirm-password": "Bekräfta lösenord", "label.current-password": "Nuvarande lösenord", "label.custom-range": "Anpassat urval", @@ -36,6 +37,7 @@ "label.password": "Lösenord", "label.passwords-dont-match": "Lösenorden är inte samma", "label.profile": "Profil", + "label.realtime": "Realtime", "label.required": "Krävs", "label.settings": "Inställningar", "label.this-month": "Denna månad", diff --git a/lang/tr-TR.json b/lang/tr-TR.json index 2e84051d..b1b206f3 100644 --- a/lang/tr-TR.json +++ b/lang/tr-TR.json @@ -18,6 +18,7 @@ "button.view-details": "Detayı incele", "label.accounts": "Hesaplar", "label.administrator": "Yönetici", + "label.all-websites": "All websites", "label.confirm-password": "Parolayı onayla", "label.current-password": "Mevcut parola", "label.custom-range": "Özelleştirilmiş aralık", @@ -36,6 +37,7 @@ "label.password": "Parola", "label.passwords-dont-match": "Parolalar uyuşmuyor", "label.profile": "Profil", + "label.realtime": "Realtime", "label.required": "Zorunlu alan", "label.settings": "Ayarlar", "label.this-month": "Bu ay", diff --git a/lang/uk-UA.json b/lang/uk-UA.json index 9c5065bf..4d077847 100644 --- a/lang/uk-UA.json +++ b/lang/uk-UA.json @@ -18,6 +18,7 @@ "button.view-details": "Переглянути деталі", "label.accounts": "Облікові записи", "label.administrator": "Адміністратор", + "label.all-websites": "All websites", "label.confirm-password": "Підтвердити пароль", "label.current-password": "Поточний пароль", "label.custom-range": "Довільний період", @@ -36,6 +37,7 @@ "label.password": "Пароль", "label.passwords-dont-match": "Паролі не співпадають", "label.profile": "Профіль", + "label.realtime": "Realtime", "label.required": "Обов'язкове", "label.settings": "Налаштування", "label.this-month": "Поточний місяць", diff --git a/lang/zh-CN.json b/lang/zh-CN.json index d5bf54cd..761ed17a 100644 --- a/lang/zh-CN.json +++ b/lang/zh-CN.json @@ -18,6 +18,7 @@ "button.view-details": "查看更多", "label.accounts": "账户", "label.administrator": "管理员", + "label.all-websites": "All websites", "label.confirm-password": "确认密码", "label.current-password": "目前密码", "label.custom-range": "自定义时间段", @@ -36,6 +37,7 @@ "label.password": "密码", "label.passwords-dont-match": "密码不一致", "label.profile": "个人资料", + "label.realtime": "Realtime", "label.required": "必填", "label.settings": "设置", "label.this-month": "本月", diff --git a/lib/date.js b/lib/date.js index cdfe322c..50d623bb 100644 --- a/lib/date.js +++ b/lib/date.js @@ -7,6 +7,7 @@ import { addYears, subHours, subDays, + startOfMinute, startOfHour, startOfDay, startOfWeek, @@ -17,6 +18,7 @@ import { endOfWeek, endOfMonth, endOfYear, + differenceInMinutes, differenceInHours, differenceInCalendarDays, differenceInCalendarMonths, @@ -114,6 +116,7 @@ export function getDateFromString(str) { } const dateFuncs = { + minute: [differenceInMinutes, addMinutes, startOfMinute], hour: [differenceInHours, addHours, startOfHour], day: [differenceInCalendarDays, addDays, startOfDay], month: [differenceInCalendarMonths, addMonths, startOfMonth], diff --git a/lib/queries.js b/lib/queries.js index 6312057f..ecf351b1 100644 --- a/lib/queries.js +++ b/lib/queries.js @@ -166,16 +166,6 @@ export async function createSession(website_id, data) { ); } -export async function getSessionById(session_id) { - return runQuery( - prisma.session.findOne({ - where: { - session_id, - }, - }), - ); -} - export async function getSessionByUuid(session_uuid) { return runQuery( prisma.session.findOne({ @@ -285,6 +275,57 @@ export async function createAccount(data) { ); } +export async function getSessions(websites, start_at) { + return runQuery( + prisma.session.findMany({ + where: { + website: { + website_id: { + in: websites, + }, + }, + created_at: { + gte: start_at, + }, + }, + }), + ); +} + +export async function getPageviews(websites, start_at) { + return runQuery( + prisma.pageview.findMany({ + where: { + website: { + website_id: { + in: websites, + }, + }, + created_at: { + gte: start_at, + }, + }, + }), + ); +} + +export async function getEvents(websites, start_at) { + return runQuery( + prisma.event.findMany({ + where: { + website: { + website_id: { + in: websites, + }, + }, + created_at: { + gte: start_at, + }, + }, + }), + ); +} + export function getWebsiteStats(website_id, start_at, end_at, filters = {}) { const params = [website_id, start_at, end_at]; const { url } = filters; @@ -425,7 +466,7 @@ export function getActiveVisitors(website_id) { ); } -export function getEvents( +export function getEventMetrics( website_id, start_at, end_at, diff --git a/lib/web.js b/lib/web.js index aceb6498..24d1d982 100644 --- a/lib/web.js +++ b/lib/web.js @@ -19,13 +19,17 @@ export const apiRequest = (method, url, body, headers) => return res.text().then(data => ({ ok: res.ok, status: res.status, res: res, data })); }); -export const get = (url, params) => apiRequest('get', `${url}${getQueryString(params)}`); +export const get = (url, params, headers) => + apiRequest('get', `${url}${getQueryString(params)}`, undefined, headers); -export const del = (url, params) => apiRequest('delete', `${url}${getQueryString(params)}`); +export const del = (url, params, headers) => + apiRequest('delete', `${url}${getQueryString(params)}`, undefined, headers); -export const post = (url, params) => apiRequest('post', url, JSON.stringify(params)); +export const post = (url, params, headers) => + apiRequest('post', url, JSON.stringify(params), headers); -export const put = (url, params) => apiRequest('put', url, JSON.stringify(params)); +export const put = (url, params, headers) => + apiRequest('put', url, JSON.stringify(params), headers); export const hook = (_this, method, callback) => { const orig = _this[method]; diff --git a/package.json b/package.json index 8f9edabb..22dd957b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "0.81.0", + "version": "0.82.0", "description": "A simple, fast, website analytics alternative to Google Analytics. ", "author": "Mike Cao ", "license": "MIT", diff --git a/pages/api/realtime.js b/pages/api/realtime.js index 7717c9b9..2130521b 100644 --- a/pages/api/realtime.js +++ b/pages/api/realtime.js @@ -1,19 +1,48 @@ +import { subMinutes } from 'date-fns'; import { useAuth } from 'lib/middleware'; -import { ok, unauthorized, methodNotAllowed } from 'lib/response'; +import { ok, methodNotAllowed, badRequest } from 'lib/response'; +import { getEvents, getPageviews, getSessions, getUserWebsites } from 'lib/queries'; +import { createToken, parseToken } from 'lib/crypto'; export default async (req, res) => { await useAuth(req, res); - const { is_admin } = req.auth; - - if (!is_admin) { - return unauthorized(res); + async function getData(websites, time) { + return Promise.all([ + getPageviews(websites, time), + getSessions(websites, time), + getEvents(websites, time), + ]); } if (req.method === 'GET') { - const [pageviews, sessions, events] = await Promise.all([[], [], []]); + const { type } = req.query; + const { user_id } = req.auth; - return ok(res, { pageviews, sessions, events }); + if (type === 'init') { + const websites = await getUserWebsites(user_id); + const ids = websites.map(({ website_id }) => website_id); + const [pageviews, sessions, events] = await getData(ids, subMinutes(new Date(), 30)); + const token = await createToken({ websites: ids }); + + return ok(res, { websites, token, data: { pageviews, sessions, events } }); + } + + if (type === 'update') { + const token = req.headers['x-umami-token']; + + if (!token) { + return badRequest(res); + } + + const { websites } = await parseToken(token); + + const [pageviews, sessions, events] = await getData(websites, new Date()); + + return ok(res, { pageviews, sessions, events }); + } + + return badRequest(res); } return methodNotAllowed(res); diff --git a/pages/api/website/[id]/events.js b/pages/api/website/[id]/events.js index 0498052f..7d98717a 100644 --- a/pages/api/website/[id]/events.js +++ b/pages/api/website/[id]/events.js @@ -1,5 +1,5 @@ import moment from 'moment-timezone'; -import { getEvents } from 'lib/queries'; +import { getEventMetrics } from 'lib/queries'; import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response'; import { allowQuery } from 'lib/auth'; @@ -21,7 +21,7 @@ export default async (req, res) => { const startDate = new Date(+start_at); const endDate = new Date(+end_at); - const events = await getEvents(websiteId, startDate, endDate, tz, unit, { url }); + const events = await getEventMetrics(websiteId, startDate, endDate, tz, unit, { url }); return ok(res, events); } diff --git a/pages/api/website/[id]/pageviews.js b/pages/api/website/[id]/pageviews.js index 07362359..965f28ae 100644 --- a/pages/api/website/[id]/pageviews.js +++ b/pages/api/website/[id]/pageviews.js @@ -21,12 +21,12 @@ export default async (req, res) => { return badRequest(res); } - const [pageviews, uniques] = await Promise.all([ + const [pageviews, sessions] = await Promise.all([ getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', url), getPageviewStats(websiteId, startDate, endDate, tz, unit, 'distinct session_id', url), ]); - return ok(res, { pageviews, uniques }); + return ok(res, { pageviews, sessions }); } return methodNotAllowed(res); From 9737127bb12cae34f39e926e7044f82d65918a88 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 9 Oct 2020 01:04:06 -0700 Subject: [PATCH 05/27] Updated polling logic. --- components/metrics/PageviewsChart.js | 12 ++++++++++-- components/pages/RealtimeDashboard.js | 12 ++++++++---- hooks/useFetch.js | 14 ++++++-------- lib/url.js | 4 ++++ lib/web.js | 6 +++--- pages/api/realtime.js | 4 ++-- 6 files changed, 33 insertions(+), 19 deletions(-) diff --git a/components/metrics/PageviewsChart.js b/components/metrics/PageviewsChart.js index d20db5eb..b937abe6 100644 --- a/components/metrics/PageviewsChart.js +++ b/components/metrics/PageviewsChart.js @@ -6,7 +6,15 @@ import BarChart from './BarChart'; import useTheme from 'hooks/useTheme'; import { THEME_COLORS } from 'lib/constants'; -export default function PageviewsChart({ websiteId, data, unit, records, className, loading }) { +export default function PageviewsChart({ + websiteId, + data, + unit, + records, + className, + loading, + animationDuration = 300, +}) { const intl = useIntl(); const [theme] = useTheme(); const primaryColor = tinycolor(THEME_COLORS[theme].primary); @@ -76,7 +84,7 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa ]} unit={unit} records={records} - animationDuration={visible ? 300 : 0} + animationDuration={visible ? animationDuration : 0} onUpdate={handleUpdate} loading={loading} /> diff --git a/components/pages/RealtimeDashboard.js b/components/pages/RealtimeDashboard.js index 35884353..ebc2db6e 100644 --- a/components/pages/RealtimeDashboard.js +++ b/components/pages/RealtimeDashboard.js @@ -35,11 +35,16 @@ function mapData(data) { export default function RealtimeDashboard() { const [data, setData] = useState(); const [website, setWebsite] = useState(); + const [lastTime, setLastTime] = useState(); const { data: init, loading } = useFetch('/api/realtime', { type: 'init' }); const { data: updates } = useFetch( '/api/realtime', - { type: 'update' }, - { disabled: !init?.token, interval: 5000, headers: { 'x-umami-token': init?.token } }, + { type: 'update', start_at: lastTime }, + { + disabled: !init?.token, + interval: 5000, + headers: { 'x-umami-token': init?.token }, + }, ); const chartData = useMemo(() => { @@ -48,8 +53,6 @@ export default function RealtimeDashboard() { const startDate = subMinutes(endDate, 30); const unit = 'minute'; - console.log({ data }); - return { pageviews: getDateArray(mapData(data.pageviews), startDate, endDate, unit), sessions: getDateArray(mapData(data.sessions), startDate, endDate, unit), @@ -70,6 +73,7 @@ export default function RealtimeDashboard() { events: filterTime(state.events, time).concat(events), })); } + setLastTime(Date.now()); }, [updates, init]); if (!init || loading || !data) { diff --git a/hooks/useFetch.js b/hooks/useFetch.js index b74a8fe7..e41b4dbc 100644 --- a/hooks/useFetch.js +++ b/hooks/useFetch.js @@ -11,10 +11,7 @@ export default function useFetch(url, params = {}, options = {}) { const [error, setError] = useState(); const [loading, setLoadiing] = useState(false); const { basePath } = useRouter(); - const keys = Object.keys(params) - .sort() - .map(key => params[key]); - const { update = [], onDataLoad = () => {}, disabled, headers } = options; + const { update = [], onDataLoad = () => {}, disabled, headers, interval, delay = 0 } = options; async function loadData() { try { @@ -43,10 +40,11 @@ export default function useFetch(url, params = {}, options = {}) { } useEffect(() => { + console.log('effect', params); if (url && !disabled) { - const { interval, delay = 0 } = options; - - setTimeout(() => loadData(), delay); + if (!data) { + setTimeout(() => loadData(), delay); + } const id = interval ? setInterval(() => loadData(), interval) : null; @@ -54,7 +52,7 @@ export default function useFetch(url, params = {}, options = {}) { clearInterval(id); }; } - }, [url, disabled, ...keys, ...update]); + }, [data, url, disabled, ...update]); return { data, status, error, loading }; } diff --git a/lib/url.js b/lib/url.js index 500736f9..deec7fe1 100644 --- a/lib/url.js +++ b/lib/url.js @@ -28,3 +28,7 @@ export function getQueryString(params = {}) { return ''; } + +export function makeUrl(url, params) { + return `${url}${getQueryString(params)}`; +} diff --git a/lib/web.js b/lib/web.js index 24d1d982..13889cbe 100644 --- a/lib/web.js +++ b/lib/web.js @@ -1,4 +1,4 @@ -import { getQueryString } from './url'; +import { makeUrl } from './url'; export const apiRequest = (method, url, body, headers) => fetch(url, { @@ -20,10 +20,10 @@ export const apiRequest = (method, url, body, headers) => }); export const get = (url, params, headers) => - apiRequest('get', `${url}${getQueryString(params)}`, undefined, headers); + apiRequest('get', makeUrl(url, params), undefined, headers); export const del = (url, params, headers) => - apiRequest('delete', `${url}${getQueryString(params)}`, undefined, headers); + apiRequest('delete', makeUrl(url, params), undefined, headers); export const post = (url, params, headers) => apiRequest('post', url, JSON.stringify(params), headers); diff --git a/pages/api/realtime.js b/pages/api/realtime.js index 2130521b..64cbea5e 100644 --- a/pages/api/realtime.js +++ b/pages/api/realtime.js @@ -16,7 +16,7 @@ export default async (req, res) => { } if (req.method === 'GET') { - const { type } = req.query; + const { type, start_at } = req.query; const { user_id } = req.auth; if (type === 'init') { @@ -37,7 +37,7 @@ export default async (req, res) => { const { websites } = await parseToken(token); - const [pageviews, sessions, events] = await getData(websites, new Date()); + const [pageviews, sessions, events] = await getData(websites, new Date(+start_at)); return ok(res, { pageviews, sessions, events }); } From db9b2385852f26bc543dad8a25d079d65e368668 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 9 Oct 2020 01:20:02 -0700 Subject: [PATCH 06/27] Updated merge logic. --- components/pages/RealtimeDashboard.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/components/pages/RealtimeDashboard.js b/components/pages/RealtimeDashboard.js index ebc2db6e..341b9e13 100644 --- a/components/pages/RealtimeDashboard.js +++ b/components/pages/RealtimeDashboard.js @@ -9,7 +9,7 @@ import PageviewsChart from '../metrics/PageviewsChart'; import { getDateArray } from '../../lib/date'; function filterTime(data, time) { - return data.filter(({ created_at }) => new Date(created_at).getTime() > time); + return data.filter(({ created_at }) => new Date(created_at).getTime() >= time); } function mapData(data) { @@ -66,11 +66,11 @@ export default function RealtimeDashboard() { setData(init.data); } else if (updates) { const { pageviews, sessions, events } = updates; - const time = subMinutes(startOfMinute(new Date()), 30).getTime(); + const minTime = subMinutes(startOfMinute(new Date()), 30).getTime(); setData(state => ({ - pageviews: filterTime(state.pageviews, time).concat(pageviews), - sessions: filterTime(state.sessions, time).concat(sessions), - events: filterTime(state.events, time).concat(events), + pageviews: filterTime(state.pageviews, minTime).concat(filterTime(pageviews, lastTime)), + sessions: filterTime(state.sessions, minTime).concat(filterTime(sessions, lastTime)), + events: filterTime(state.events, minTime).concat(filterTime(events, lastTime)), })); } setLastTime(Date.now()); From b682e41afff4cfeb70d3d121c900790ef77a8495 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 9 Oct 2020 02:56:15 -0700 Subject: [PATCH 07/27] RealtimeLog component. --- components/common/Table.js | 4 +- components/common/Table.module.css | 1 + components/metrics/RealtimeChart.js | 114 ++++++++-------------- components/metrics/RealtimeLog.js | 95 ++++++++++++++++++ components/metrics/RealtimeLog.module.css | 8 ++ components/pages/RealtimeDashboard.js | 64 ++++-------- 6 files changed, 163 insertions(+), 123 deletions(-) create mode 100644 components/metrics/RealtimeLog.js create mode 100644 components/metrics/RealtimeLog.module.css diff --git a/components/common/Table.js b/components/common/Table.js index a8a42b78..61eb4f6e 100644 --- a/components/common/Table.js +++ b/components/common/Table.js @@ -3,13 +3,13 @@ import classNames from 'classnames'; import NoData from 'components/common/NoData'; import styles from './Table.module.css'; -export default function Table({ columns, rows, empty }) { +export default function Table({ className, columns, rows, empty }) { if (empty && rows.length === 0) { return empty; } return ( -
+
{columns.map(({ key, label, className, style, header }) => (
{ - const { - data: { datasets }, - } = chart; + data.reduce((obj, val) => { + const { created_at } = val; + const t = startOfMinute(parseISO(created_at)); + if (t.getTime() > last) { + obj = { t: format(t, 'yyyy-LL-dd HH:mm:00'), y: 1 }; + arr.push(obj); + last = t; + } else { + obj.y += 1; + } + return obj; + }, {}); - datasets[0].data = data.uniques; - datasets[0].label = intl.formatMessage({ - id: 'metrics.unique-visitors', - defaultMessage: 'Unique visitors', - }); - datasets[1].data = data.pageviews; - datasets[1].label = intl.formatMessage({ - id: 'metrics.page-views', - defaultMessage: 'Page views', - }); - - chart.update(); - }; - - if (!data) { - return null; - } - - return ( - - ); + return arr; +} + +export default function RealtimeChart({ data, ...props }) { + const chartData = useMemo(() => { + if (data) { + const endDate = startOfMinute(new Date()); + const startDate = subMinutes(endDate, 30); + const unit = 'minute'; + + return { + pageviews: getDateArray(mapData(data.pageviews), startDate, endDate, unit), + sessions: getDateArray(mapData(data.sessions), startDate, endDate, unit), + }; + } + return { pageviews: [], sessions: [] }; + }, [data]); + + return ; } diff --git a/components/metrics/RealtimeLog.js b/components/metrics/RealtimeLog.js new file mode 100644 index 00000000..b3a82050 --- /dev/null +++ b/components/metrics/RealtimeLog.js @@ -0,0 +1,95 @@ +import React, { useMemo } from 'react'; +import { FormattedMessage } from 'react-intl'; +import firstBy from 'thenby'; +import { format } from 'date-fns'; +import Table from 'components/common/Table'; +import styles from './RealtimeLog.module.css'; +import useLocale from '../../hooks/useLocale'; +import useCountryNames from '../../hooks/useCountryNames'; +import { BROWSERS } from '../../lib/constants'; + +export default function RealtimeLog({ data, websites }) { + const [locale] = useLocale(); + const countryNames = useCountryNames(locale); + const logs = useMemo(() => { + const { pageviews, sessions, events } = data; + return [...pageviews, ...sessions, ...events].sort(firstBy('created_at', -1)); + }, [data]); + + const columns = [ + { + key: 'time', + label: , + className: 'col', + render: ({ created_at }) => format(new Date(created_at), 'H:mm:ss'), + }, + { + key: 'website', + label: , + className: 'col', + render: getWebsite, + }, + { + key: 'type', + label: , + className: 'col', + render: getType, + }, + { + key: 'type', + className: 'col', + render: getDescription, + }, + ]; + + function getType({ view_id, session_id, event_id }) { + if (event_id) { + return ; + } + if (view_id) { + return ; + } + if (session_id) { + return ; + } + return null; + } + + function getWebsite({ website_id }) { + return websites.find(n => n.website_id === website_id)?.name; + } + + function getDescription({ + event_type, + event_value, + view_id, + session_id, + url, + browser, + os, + country, + device, + }) { + if (event_type) { + return `${event_type}:${event_value}`; + } + if (view_id) { + return url; + } + if (session_id) { + return ( + + ); + } + } + + return ( +
+ + + ); +} diff --git a/components/metrics/RealtimeLog.module.css b/components/metrics/RealtimeLog.module.css new file mode 100644 index 00000000..66a2efd3 --- /dev/null +++ b/components/metrics/RealtimeLog.module.css @@ -0,0 +1,8 @@ +.table { + font-size: var(--font-size-small); +} + +.row { + display: flex; + border-bottom: 1px solid var(--gray300); +} diff --git a/components/pages/RealtimeDashboard.js b/components/pages/RealtimeDashboard.js index 341b9e13..348b6642 100644 --- a/components/pages/RealtimeDashboard.js +++ b/components/pages/RealtimeDashboard.js @@ -1,37 +1,20 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; -import { subMinutes, startOfMinute, parseISO, format } from 'date-fns'; +import { subMinutes, startOfMinute } from 'date-fns'; import Page from 'components/layout/Page'; import PageHeader from 'components/layout/PageHeader'; import DropDown from 'components/common/DropDown'; import useFetch from 'hooks/useFetch'; -import PageviewsChart from '../metrics/PageviewsChart'; -import { getDateArray } from '../../lib/date'; +import RealtimeChart from '../metrics/RealtimeChart'; +import RealtimeLog from '../metrics/RealtimeLog'; + +const REALTIME_RANGE = 30; +const REALTIME_INTERVAL = 5000; function filterTime(data, time) { return data.filter(({ created_at }) => new Date(created_at).getTime() >= time); } -function mapData(data) { - let last = 0; - const arr = []; - - data.reduce((obj, val) => { - const { created_at } = val; - const t = startOfMinute(parseISO(created_at)); - if (t.getTime() > last) { - obj = { t: format(t, 'yyyy-LL-dd HH:mm:00'), y: 1 }; - arr.push(obj); - last = t; - } else { - obj.y += 1; - } - return obj; - }, {}); - - return arr; -} - export default function RealtimeDashboard() { const [data, setData] = useState(); const [website, setWebsite] = useState(); @@ -42,39 +25,25 @@ export default function RealtimeDashboard() { { type: 'update', start_at: lastTime }, { disabled: !init?.token, - interval: 5000, + interval: REALTIME_INTERVAL, headers: { 'x-umami-token': init?.token }, }, ); - const chartData = useMemo(() => { - if (data) { - const endDate = startOfMinute(new Date()); - const startDate = subMinutes(endDate, 30); - const unit = 'minute'; - - return { - pageviews: getDateArray(mapData(data.pageviews), startDate, endDate, unit), - sessions: getDateArray(mapData(data.sessions), startDate, endDate, unit), - }; - } - return { pageviews: [], sessions: [] }; - }, [data]); - useEffect(() => { if (init && !data) { setData(init.data); } else if (updates) { const { pageviews, sessions, events } = updates; - const minTime = subMinutes(startOfMinute(new Date()), 30).getTime(); + const minTime = subMinutes(startOfMinute(new Date()), REALTIME_RANGE).getTime(); setData(state => ({ - pageviews: filterTime(state.pageviews, minTime).concat(filterTime(pageviews, lastTime)), - sessions: filterTime(state.sessions, minTime).concat(filterTime(sessions, lastTime)), - events: filterTime(state.events, minTime).concat(filterTime(events, lastTime)), + pageviews: filterTime(state.pageviews.concat(pageviews), minTime), + sessions: filterTime(state.sessions.concat(sessions), minTime), + events: filterTime(state.events.concat(events), minTime), })); } setLastTime(Date.now()); - }, [updates, init]); + }, [init, updates]); if (!init || loading || !data) { return null; @@ -99,7 +68,12 @@ export default function RealtimeDashboard() { - + +
+
+ +
+
); } From 8e0ea48c871c02900119bd8fce0744c6404bceb9 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 9 Oct 2020 04:21:59 -0700 Subject: [PATCH 08/27] Updated log table rendering. --- assets/bolt.svg | 1 + assets/eye.svg | 1 + assets/visitor.svg | 1 + components/common/Table.js | 45 ++++++++----- components/metrics/RealtimeLog.js | 77 ++++++++++++++++------- components/metrics/RealtimeLog.module.css | 18 ++++++ hooks/useFetch.js | 1 - lang/da-DK.json | 6 ++ lang/de-DE.json | 6 ++ lang/el-GR.json | 6 ++ lang/en-US.json | 6 ++ lang/es-MX.json | 6 ++ lang/fo-FO.json | 6 ++ lang/fr-FR.json | 6 ++ lang/id-ID.json | 6 ++ lang/ja-JP.json | 6 ++ lang/mn-MN.json | 6 ++ lang/nb-NO.json | 6 ++ lang/nl-NL.json | 6 ++ lang/pt-PT.json | 6 ++ lang/ro-RO.json | 6 ++ lang/ru-RU.json | 6 ++ lang/sv-SE.json | 6 ++ lang/tr-TR.json | 6 ++ lang/uk-UA.json | 6 ++ lang/zh-CN.json | 6 ++ 26 files changed, 221 insertions(+), 37 deletions(-) create mode 100644 assets/bolt.svg create mode 100644 assets/eye.svg create mode 100644 assets/visitor.svg diff --git a/assets/bolt.svg b/assets/bolt.svg new file mode 100644 index 00000000..4654a1eb --- /dev/null +++ b/assets/bolt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/eye.svg b/assets/eye.svg new file mode 100644 index 00000000..09c93453 --- /dev/null +++ b/assets/eye.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/visitor.svg b/assets/visitor.svg new file mode 100644 index 00000000..591873a5 --- /dev/null +++ b/assets/visitor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/common/Table.js b/components/common/Table.js index 61eb4f6e..d0a1e0c7 100644 --- a/components/common/Table.js +++ b/components/common/Table.js @@ -3,7 +3,15 @@ import classNames from 'classnames'; import NoData from 'components/common/NoData'; import styles from './Table.module.css'; -export default function Table({ className, columns, rows, empty }) { +export default function Table({ + columns, + rows, + empty, + className, + bodyClassName, + rowKey, + children, +}) { if (empty && rows.length === 0) { return empty; } @@ -21,22 +29,29 @@ export default function Table({ className, columns, rows, empty }) { ))} -
+
{rows.length === 0 && } - {rows.map((row, rowIndex) => ( -
- {columns.map(({ key, render, className, style, cell }) => ( -
- {render ? render(row) : row[key]} -
- ))} -
- ))} + {!children && + rows.map((row, index) => { + const id = rowKey ? rowKey(row) : index; + return ; + })} + {children}
); } + +export const TableRow = ({ columns, row }) => ( +
+ {columns.map(({ key, render, className, style, cell }, index) => ( +
+ {render ? render(row) : row[key]} +
+ ))} +
+); diff --git a/components/metrics/RealtimeLog.js b/components/metrics/RealtimeLog.js index b3a82050..fce9f7e9 100644 --- a/components/metrics/RealtimeLog.js +++ b/components/metrics/RealtimeLog.js @@ -1,14 +1,20 @@ import React, { useMemo } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { FixedSizeList } from 'react-window'; import firstBy from 'thenby'; import { format } from 'date-fns'; -import Table from 'components/common/Table'; +import Icon from 'components/common/Icon'; +import Table, { TableRow } from 'components/common/Table'; +import useLocale from 'hooks/useLocale'; +import useCountryNames from 'hooks/useCountryNames'; +import { BROWSERS } from 'lib/constants'; +import Bolt from 'assets/bolt.svg'; +import Visitor from 'assets/visitor.svg'; +import Eye from 'assets/eye.svg'; import styles from './RealtimeLog.module.css'; -import useLocale from '../../hooks/useLocale'; -import useCountryNames from '../../hooks/useCountryNames'; -import { BROWSERS } from '../../lib/constants'; export default function RealtimeLog({ data, websites }) { + const intl = useIntl(); const [locale] = useLocale(); const countryNames = useCountryNames(locale); const logs = useMemo(() => { @@ -19,38 +25,51 @@ export default function RealtimeLog({ data, websites }) { const columns = [ { key: 'time', - label: , - className: 'col', + label: , + className: 'col-1', render: ({ created_at }) => format(new Date(created_at), 'H:mm:ss'), }, { key: 'website', label: , - className: 'col', + className: 'col-2', render: getWebsite, }, { key: 'type', - label: , - className: 'col', - render: getType, - }, - { - key: 'type', - className: 'col', - render: getDescription, + label: , + className: 'col-9', + render: row => ( + <> + + {getDescription(row)} + + ), }, ]; function getType({ view_id, session_id, event_id }) { if (event_id) { - return ; + return intl.formatMessage({ id: 'label.event', defaultMessage: 'Event' }); } if (view_id) { - return ; + return intl.formatMessage({ id: 'label.pageview', defaultMessage: 'Pageview' }); } if (session_id) { - return ; + return intl.formatMessage({ id: 'label.visitor', defaultMessage: 'Visitor' }); + } + return null; + } + + function getIcon({ view_id, session_id, event_id }) { + if (event_id) { + return ; + } + if (view_id) { + return ; + } + if (session_id) { + return ; } return null; } @@ -71,7 +90,11 @@ export default function RealtimeLog({ data, websites }) { device, }) { if (event_type) { - return `${event_type}:${event_value}`; + return ( +
+ {event_type} {event_value} +
+ ); } if (view_id) { return url; @@ -87,9 +110,21 @@ export default function RealtimeLog({ data, websites }) { } } + const Row = ({ index, style }) => { + return ( +
+ +
+ ); + }; + return (
-
+
+ + {Row} + +
); } diff --git a/components/metrics/RealtimeLog.module.css b/components/metrics/RealtimeLog.module.css index 66a2efd3..a2ffa893 100644 --- a/components/metrics/RealtimeLog.module.css +++ b/components/metrics/RealtimeLog.module.css @@ -6,3 +6,21 @@ display: flex; border-bottom: 1px solid var(--gray300); } + +.body { + height: 600px; + overflow: auto; +} + +.event { + font-size: var(--font-size-small); + padding: 2px 4px; + border: 1px solid var(--gray300); + border-radius: 4px; + margin-right: 10px; +} + +.icon { + align-self: center; + margin-right: 20px; +} diff --git a/hooks/useFetch.js b/hooks/useFetch.js index e41b4dbc..5301e2a1 100644 --- a/hooks/useFetch.js +++ b/hooks/useFetch.js @@ -40,7 +40,6 @@ export default function useFetch(url, params = {}, options = {}) { } useEffect(() => { - console.log('effect', params); if (url && !disabled) { if (!data) { setTimeout(() => loadData(), delay); diff --git a/lang/da-DK.json b/lang/da-DK.json index ae8c4321..9d33fd7b 100644 --- a/lang/da-DK.json +++ b/lang/da-DK.json @@ -26,6 +26,7 @@ "label.default-date-range": "Default date range", "label.domain": "Domæne", "label.enable-share-url": "Aktivér delings-URL", + "label.event": "Event", "label.invalid": "Ugyldig", "label.invalid-domain": "Ugyldigt domæne", "label.last-days": "Sidste {x} dage", @@ -34,6 +35,7 @@ "label.logout": "Log ud", "label.name": "Navn", "label.new-password": "Ny adgangskode", + "label.pageview": "Pageview", "label.password": "Adgangskode", "label.passwords-dont-match": "Adgangskoder matcher ikke", "label.profile": "Profil", @@ -45,8 +47,11 @@ "label.this-year": "Dette år", "label.timezone": "Timezone", "label.today": "Idag", + "label.type": "Type", "label.unknown": "Ukendt", "label.username": "Brugernavn", + "label.visitor": "Visitor", + "label.website": "Website", "label.websites": "Hjemmesider", "message.active-users": "{x} nuværende {x, plural, one {bruger} other {brugere}}", "message.confirm-delete": "Er du sikker på at du vil slette {target}?", @@ -57,6 +62,7 @@ "message.get-tracking-code": "Få sporingskode", "message.go-to-settings": "Gå til betjeningspanel", "message.incorrect-username-password": "Ugyldigt brugernavn/adgangskode.", + "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "A new version of umami {version} is available!", "message.no-data-available": "Ingen data tilgængelig.", "message.no-websites-configured": "Du har ikke konfigureret nogen websteder.", diff --git a/lang/de-DE.json b/lang/de-DE.json index fdc7c3a1..7821f826 100644 --- a/lang/de-DE.json +++ b/lang/de-DE.json @@ -26,6 +26,7 @@ "label.default-date-range": "Voreingestellter Datumsbereich", "label.domain": "Domain", "label.enable-share-url": "Freigabe-URL aktivieren", + "label.event": "Event", "label.invalid": "Ungültig", "label.invalid-domain": "Ungültige Domain", "label.last-days": "Letzten {x} Tage", @@ -34,6 +35,7 @@ "label.logout": "Abmelden", "label.name": "Name", "label.new-password": "Neues Passwort", + "label.pageview": "Pageview", "label.password": "Passwort", "label.passwords-dont-match": "Passwörter stimmen nicht überein", "label.profile": "Profil", @@ -45,8 +47,11 @@ "label.this-year": "Dieses Jahr", "label.timezone": "Zeitzone", "label.today": "Heute", + "label.type": "Type", "label.unknown": "Unbekannt", "label.username": "Benutzername", + "label.visitor": "Visitor", + "label.website": "Website", "label.websites": "Webseiten", "message.active-users": "{x} {x, plural, one {aktiver Besucher} other {aktive Besucher}}", "message.confirm-delete": "Sind sie sich sicher {target} zu löschen?", @@ -57,6 +62,7 @@ "message.get-tracking-code": "Erstelle Tracking Kennung", "message.go-to-settings": "Zu den Einstellungen", "message.incorrect-username-password": "Falsches Passwort oder Benutzername.", + "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "Eine neue Version umami {version} ist verfügbar!", "message.no-data-available": "Keine Daten vorhanden.", "message.no-websites-configured": "Es ist keine Webseite vorhanden.", diff --git a/lang/el-GR.json b/lang/el-GR.json index ec4b3217..88d4a0e1 100644 --- a/lang/el-GR.json +++ b/lang/el-GR.json @@ -26,6 +26,7 @@ "label.default-date-range": "Προεπιλεγμένο εύρος ημερομηνιών", "label.domain": "Τομέας", "label.enable-share-url": "Ενεργοποίηση κοινής χρήσης URL", + "label.event": "Event", "label.invalid": "Μη έγκυρο", "label.invalid-domain": "Μη έγκυρος τομέας", "label.last-days": "Τελευταίες {x} ημέρες", @@ -34,6 +35,7 @@ "label.logout": "Αποσύνδεση", "label.name": "Όνομα", "label.new-password": "Νέος κωδικός", + "label.pageview": "Pageview", "label.password": "Κωδικός", "label.passwords-dont-match": "Οι κωδικοί πρόσβασης δεν ταιριάζουν", "label.profile": "Προφίλ", @@ -45,8 +47,11 @@ "label.this-year": "Αυτή την χρονιά", "label.timezone": "Ζώνη ώρας", "label.today": "Σήμερα", + "label.type": "Type", "label.unknown": "Άγνωστο", "label.username": "Όνομα χρήστη", + "label.visitor": "Visitor", + "label.website": "Website", "label.websites": "Ιστότοποι", "message.active-users": "{x} ενεργοί {x, plural, one {επισκέπτης} other {επισκέπτες}}", "message.confirm-delete": "Είστε βέβαιοι ότι θέλετε να διαγράψετε το {target};", @@ -57,6 +62,7 @@ "message.get-tracking-code": "Λήψη κώδικα παρακολούθησης", "message.go-to-settings": "Μεταβείτε στις ρυθμίσεις", "message.incorrect-username-password": "Εσφαλμένο όνομα χρήστη / κωδικός πρόσβασης.", + "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "A new version of umami {version} is available!", "message.no-data-available": "Δεν υπάρχουν διαθέσιμα δεδομένα.", "message.no-websites-configured": "Δεν έχετε ρυθμίσει κανένα ιστότοπο.", diff --git a/lang/en-US.json b/lang/en-US.json index 7251c123..cadd0512 100644 --- a/lang/en-US.json +++ b/lang/en-US.json @@ -26,6 +26,7 @@ "label.default-date-range": "Default date range", "label.domain": "Domain", "label.enable-share-url": "Enable share URL", + "label.event": "Event", "label.invalid": "Invalid", "label.invalid-domain": "Invalid domain", "label.last-days": "Last {x} days", @@ -34,6 +35,7 @@ "label.logout": "Logout", "label.name": "Name", "label.new-password": "New password", + "label.pageview": "Pageview", "label.password": "Password", "label.passwords-dont-match": "Passwords don't match", "label.profile": "Profile", @@ -45,8 +47,11 @@ "label.this-year": "This year", "label.timezone": "Timezone", "label.today": "Today", + "label.type": "Type", "label.unknown": "Unknown", "label.username": "Username", + "label.visitor": "Visitor", + "label.website": "Website", "label.websites": "Websites", "message.active-users": "{x} current {x, plural, one {visitor} other {visitors}}", "message.confirm-delete": "Are your sure you want to delete {target}?", @@ -57,6 +62,7 @@ "message.get-tracking-code": "Get tracking code", "message.go-to-settings": "Go to settings", "message.incorrect-username-password": "Incorrect username/password.", + "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "A new version of umami {version} is available!", "message.no-data-available": "No data available.", "message.no-websites-configured": "You don't have any websites configured.", diff --git a/lang/es-MX.json b/lang/es-MX.json index 9e6a6206..48efe589 100644 --- a/lang/es-MX.json +++ b/lang/es-MX.json @@ -26,6 +26,7 @@ "label.default-date-range": "Default date range", "label.domain": "Dominio", "label.enable-share-url": "Habilitar compartir URL", + "label.event": "Event", "label.invalid": "Inválido", "label.invalid-domain": "Dominio inválido", "label.last-days": "Últimos {x} días", @@ -34,6 +35,7 @@ "label.logout": "Cerrar sesión", "label.name": "Nombre", "label.new-password": "Nueva contraseña", + "label.pageview": "Pageview", "label.password": "Contraseña", "label.passwords-dont-match": "Las contraseñas no coinciden", "label.profile": "Perfil", @@ -45,8 +47,11 @@ "label.this-year": "Este año", "label.timezone": "Timezone", "label.today": "Hoy", + "label.type": "Type", "label.unknown": "Unknown", "label.username": "Nombre de usuario", + "label.visitor": "Visitor", + "label.website": "Website", "label.websites": "Sitios", "message.active-users": "{x} {x, plural, one {activo} other {activos}}", "message.confirm-delete": "¿Estás seguro(a) de querer eliminar {target}?", @@ -57,6 +62,7 @@ "message.get-tracking-code": "Obtener código de rastreo", "message.go-to-settings": "Ir a la configuración", "message.incorrect-username-password": "Nombre de usuario o contraseña incorrectos.", + "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "A new version of umami {version} is available!", "message.no-data-available": "Sin información disponible.", "message.no-websites-configured": "No tienes ningún sitio configurado.", diff --git a/lang/fo-FO.json b/lang/fo-FO.json index 2c379960..2969b83e 100644 --- a/lang/fo-FO.json +++ b/lang/fo-FO.json @@ -26,6 +26,7 @@ "label.default-date-range": "Standard dato", "label.domain": "Økisnavn", "label.enable-share-url": "Virkja deili leinki", + "label.event": "Event", "label.invalid": "Ógilda", "label.invalid-domain": "Ógilt økisnavn", "label.last-days": "Seinastu {x} dagarnar", @@ -34,6 +35,7 @@ "label.logout": "Rita út", "label.name": "Navn", "label.new-password": "Nýtt loyniorð", + "label.pageview": "Pageview", "label.password": "Loyniorð", "label.passwords-dont-match": "Loyniorðini eru ikki eins", "label.profile": "Brúkari", @@ -45,8 +47,11 @@ "label.this-year": "Hetta árið", "label.timezone": "Tíðarsona", "label.today": "Í dag", + "label.type": "Type", "label.unknown": "Ókent", "label.username": "Brúkaranavn", + "label.visitor": "Visitor", + "label.website": "Website", "label.websites": "Heimasíður", "message.active-users": "{x} í løtuni {x, plural, one {vitjandi} other { vitjandi }}", "message.confirm-delete": "Ert tú sikkur at tú ynskir at sletta {target}?", @@ -57,6 +62,7 @@ "message.get-tracking-code": "Fá sporings kotu", "message.go-to-settings": "Far til stillingar", "message.incorrect-username-password": "Skeivt brúkaranavn/loyniorð.", + "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "A new version of umami {version} is available!", "message.no-data-available": "Einki data tøk.", "message.no-websites-configured": "Tú hevur ongar heimasíður stillaða til.", diff --git a/lang/fr-FR.json b/lang/fr-FR.json index 013f89e5..b62ce587 100644 --- a/lang/fr-FR.json +++ b/lang/fr-FR.json @@ -26,6 +26,7 @@ "label.default-date-range": "Default date range", "label.domain": "Domaine", "label.enable-share-url": "Activer le partage d'URL", + "label.event": "Event", "label.invalid": "Invalide", "label.invalid-domain": "Domaine invalide", "label.last-days": "{x} derniers jours", @@ -34,6 +35,7 @@ "label.logout": "Déconnexion", "label.name": "Nom", "label.new-password": "Nouveau mot de passe", + "label.pageview": "Pageview", "label.password": "Mot de passe", "label.passwords-dont-match": "Les mots de passe ne correspondent pas", "label.profile": "Profile", @@ -45,8 +47,11 @@ "label.this-year": "Cette année", "label.timezone": "Timezone", "label.today": "Aujourd'hui", + "label.type": "Type", "label.unknown": "Unknown", "label.username": "Nom d'utilisateur", + "label.visitor": "Visitor", + "label.website": "Website", "label.websites": "Sites", "message.active-users": "{x} {x, plural, one {visiteur} other {visiteurs}} actuellement", "message.confirm-delete": "Êtes-vous sur de vouloir supprimer {target}?", @@ -57,6 +62,7 @@ "message.get-tracking-code": "Obtenez le code de suivi", "message.go-to-settings": "Aller aux paramètres", "message.incorrect-username-password": "nom d'utilisateurs/mot de passe incorrect.", + "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "A new version of umami {version} is available!", "message.no-data-available": "Pas de données disponibles.", "message.no-websites-configured": "Vous n'avez configuré aucun site Web.", diff --git a/lang/id-ID.json b/lang/id-ID.json index 8b09c0a6..8f12d762 100644 --- a/lang/id-ID.json +++ b/lang/id-ID.json @@ -26,6 +26,7 @@ "label.default-date-range": "Rentang tanggal default", "label.domain": "Domain", "label.enable-share-url": "Aktifkan URL berbagi", + "label.event": "Event", "label.invalid": "Tidak valid", "label.invalid-domain": "Domain tidak valid", "label.last-days": "{x} hari terakhir", @@ -34,6 +35,7 @@ "label.logout": "Keluar", "label.name": "Nama", "label.new-password": "Kata sandi baru", + "label.pageview": "Pageview", "label.password": "Kata sandi", "label.passwords-dont-match": "Kata sandi tidak cocok", "label.profile": "Profil", @@ -45,8 +47,11 @@ "label.this-year": "Tahun ini", "label.timezone": "Zona waktu", "label.today": "Hari ini", + "label.type": "Type", "label.unknown": "Tidak diketahui", "label.username": "Nama pengguna", + "label.visitor": "Visitor", + "label.website": "Website", "label.websites": "Situs web", "message.active-users": "{x} pengunjung saat ini", "message.confirm-delete": "Apakah kamu yakin ingin menghapus {target}?", @@ -57,6 +62,7 @@ "message.get-tracking-code": "Dapatkan kode pelacakan", "message.go-to-settings": "Pergi ke pengaturan", "message.incorrect-username-password": "Nama pengguna/kata sandi salah.", + "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "Versi terbaru umami {version} telah tersedia!", "message.no-data-available": "Tidak ada data.", "message.no-websites-configured": "Anda tidak memiliki situs web yang dikonfigurasi.", diff --git a/lang/ja-JP.json b/lang/ja-JP.json index bbf73ed5..fd276b26 100644 --- a/lang/ja-JP.json +++ b/lang/ja-JP.json @@ -26,6 +26,7 @@ "label.default-date-range": "最初に表示する期間", "label.domain": "ドメイン", "label.enable-share-url": "共有リンクを有効にする", + "label.event": "Event", "label.invalid": "無効", "label.invalid-domain": "無効なドメイン", "label.last-days": "過去{x}日間", @@ -34,6 +35,7 @@ "label.logout": "ログアウト", "label.name": "名前", "label.new-password": "新しいパスワード", + "label.pageview": "Pageview", "label.password": "パスワード", "label.passwords-dont-match": "パスワードが一致しません", "label.profile": "プロファイル", @@ -45,8 +47,11 @@ "label.this-year": "今年", "label.timezone": "タイムゾーン", "label.today": "今日", + "label.type": "Type", "label.unknown": "不明", "label.username": "ユーザー名", + "label.visitor": "Visitor", + "label.website": "Website", "label.websites": "Webサイト", "message.active-users": "{x}人が閲覧中です。", "message.confirm-delete": "{target}を削除してもよろしいですか?", @@ -57,6 +62,7 @@ "message.get-tracking-code": "トラッキングコードを取得", "message.go-to-settings": "設定する", "message.incorrect-username-password": "ユーザー名/パスワードが正しくありません。", + "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "新しいバージョン({version})が利用可能です!", "message.no-data-available": "データがありません。", "message.no-websites-configured": "Webサイトが設定されていません。", diff --git a/lang/mn-MN.json b/lang/mn-MN.json index 70af8f29..a6e632db 100644 --- a/lang/mn-MN.json +++ b/lang/mn-MN.json @@ -26,6 +26,7 @@ "label.default-date-range": "Өгөгдмөл хугацааны муж", "label.domain": "Домэйн", "label.enable-share-url": "Хуваалцах холбоос идэвхжүүлэх", + "label.event": "Event", "label.invalid": "Буруу", "label.invalid-domain": "Буруу домэйн", "label.last-days": "Сүүлийн {x} хоног", @@ -34,6 +35,7 @@ "label.logout": "Гарах", "label.name": "Нэр", "label.new-password": "Шинэ нууц үг", + "label.pageview": "Pageview", "label.password": "Нууц үг", "label.passwords-dont-match": "Нууц үг тохирохгүй байна", "label.profile": "Бүртгэл", @@ -45,8 +47,11 @@ "label.this-year": "Энэ жил", "label.timezone": "Цагийн бүс", "label.today": "Өнөөдөр", + "label.type": "Type", "label.unknown": "Тодорхойгүй", "label.username": "Хэрэглэгчийн нэр", + "label.visitor": "Visitor", + "label.website": "Website", "label.websites": "Вебүүд", "message.active-users": "одоо {x} {x, plural, one {зочин} other {зочин}} байна", "message.confirm-delete": "Та {target}-г устгахдаа итгэлтэй байна уу?", @@ -57,6 +62,7 @@ "message.get-tracking-code": "Мөрдөх код авах", "message.go-to-settings": "Тохиргоо руу очих", "message.incorrect-username-password": "Буруу хэрэглэгчийн нэр/нууц үг.", + "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "Umami-гийн шинэ хувилбар {version} гарсан байна!", "message.no-data-available": "Өгөгдөл алга.", "message.no-websites-configured": "Та ямар нэгэн веб тохируулаагүй байна.", diff --git a/lang/nb-NO.json b/lang/nb-NO.json index 8e93b05c..fc2664f9 100644 --- a/lang/nb-NO.json +++ b/lang/nb-NO.json @@ -26,6 +26,7 @@ "label.default-date-range": "Standard datoperiode", "label.domain": "Domene", "label.enable-share-url": "Aktiver delings-URL", + "label.event": "Event", "label.invalid": "Ugyldig", "label.invalid-domain": "Ugyldig domene", "label.last-days": "Siste {x} dager", @@ -34,6 +35,7 @@ "label.logout": "Logg ut", "label.name": "Navn", "label.new-password": "Nytt passord", + "label.pageview": "Pageview", "label.password": "Passord", "label.passwords-dont-match": "Passordene er ikke like", "label.profile": "Profil", @@ -45,8 +47,11 @@ "label.this-year": "I år", "label.timezone": "Tidssone", "label.today": "I dag", + "label.type": "Type", "label.unknown": "Ukjent", "label.username": "Brukernavn", + "label.visitor": "Visitor", + "label.website": "Website", "label.websites": "Nettsteder", "message.active-users": "{x} {x, plural, one {besøkende} other {besøkende}} nå", "message.confirm-delete": "Er du sikker på at du vil slette {target}?", @@ -57,6 +62,7 @@ "message.get-tracking-code": "Få sporingskode", "message.go-to-settings": "Gå til innstillinger", "message.incorrect-username-password": "Ugyldig brukernavn/passord.", + "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "En ny versjon av umami {version} er tilgjengelig!", "message.no-data-available": "Ingen data tilgjengelig.", "message.no-websites-configured": "Du har ikke satt opp noen nettsteder.", diff --git a/lang/nl-NL.json b/lang/nl-NL.json index 8978d7ee..cbedfc4c 100644 --- a/lang/nl-NL.json +++ b/lang/nl-NL.json @@ -26,6 +26,7 @@ "label.default-date-range": "Standaard bereik", "label.domain": "Domein", "label.enable-share-url": "Sta delen via openbare URL toe", + "label.event": "Event", "label.invalid": "Ongeldig", "label.invalid-domain": "Ongeldig domein", "label.last-days": "Laatste {x} dagen", @@ -34,6 +35,7 @@ "label.logout": "Uitloggen", "label.name": "Naam", "label.new-password": "Nieuw wachtwoord", + "label.pageview": "Pageview", "label.password": "Wachtwoord", "label.passwords-dont-match": "Wachtwoorden komen niet overeen", "label.profile": "Profiel", @@ -45,8 +47,11 @@ "label.this-year": "Dit jaar", "label.timezone": "Tijdzone", "label.today": "Vandaag", + "label.type": "Type", "label.unknown": "Onbekend", "label.username": "Gebruikersnaam", + "label.visitor": "Visitor", + "label.website": "Website", "label.websites": "Websites", "message.active-users": "{x} actieve {x, plural, one {bezoeker} other {bezoekers}}", "message.confirm-delete": "Weet je zeker dat je {target} wilt verwijderen?", @@ -57,6 +62,7 @@ "message.get-tracking-code": "Tracking code", "message.go-to-settings": "Naar instellingen", "message.incorrect-username-password": "Incorrecte gebruikersnaam/wachtwoord.", + "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "Een nieuwe versie van umami {version} is beschikbaar!", "message.no-data-available": "Geen gegevens beschikbaar.", "message.no-websites-configured": "Je hebt geen websites ingesteld.", diff --git a/lang/pt-PT.json b/lang/pt-PT.json index 971c1aa7..ca31437b 100644 --- a/lang/pt-PT.json +++ b/lang/pt-PT.json @@ -26,6 +26,7 @@ "label.default-date-range": "Intervalo de datas predefinido", "label.domain": "Domínio", "label.enable-share-url": "Ativar link de partilha", + "label.event": "Event", "label.invalid": "Inválido", "label.invalid-domain": "Domínio inválido", "label.last-days": "Últimos {x} dias", @@ -34,6 +35,7 @@ "label.logout": "Sair", "label.name": "Nome", "label.new-password": "Nova palavra-passe", + "label.pageview": "Pageview", "label.password": "Palavra-passe", "label.passwords-dont-match": "Palavra-passes não correspondem", "label.profile": "Perfil", @@ -45,8 +47,11 @@ "label.this-year": "Este ano", "label.timezone": "Fuso horário", "label.today": "Hoje", + "label.type": "Type", "label.unknown": "Desconhecido", "label.username": "Nome de utilizador", + "label.visitor": "Visitor", + "label.website": "Website", "label.websites": "Websites", "message.active-users": "{x} {x, plural, one {visitante} other {visitantes}} neste momento", "message.confirm-delete": "Tens a certeza que queres eliminar {target}?", @@ -57,6 +62,7 @@ "message.get-tracking-code": "Obter código de tracking", "message.go-to-settings": "Ir para as definições", "message.incorrect-username-password": "Nome de utilizador/palavra-passe incorretos.", + "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "Uma nova versão de umami {version} está disponível!", "message.no-data-available": "Sem dados disponíveis.", "message.no-websites-configured": "Não tens nenhum website configurado.", diff --git a/lang/ro-RO.json b/lang/ro-RO.json index 8e28f887..7103759a 100644 --- a/lang/ro-RO.json +++ b/lang/ro-RO.json @@ -26,6 +26,7 @@ "label.default-date-range": "Interval de date implicit", "label.domain": "Domeniu", "label.enable-share-url": "Activare adresa URL de distribuire", + "label.event": "Event", "label.invalid": "Invalid", "label.invalid-domain": "Invalid domain", "label.last-days": "Ultimele {x} zile", @@ -34,6 +35,7 @@ "label.logout": "Dezautentificare", "label.name": "Nume", "label.new-password": "Parola nouă", + "label.pageview": "Pageview", "label.password": "Parolă", "label.passwords-dont-match": "Parolele nu se potrivesc", "label.profile": "Profil", @@ -45,8 +47,11 @@ "label.this-year": "Acest an", "label.timezone": "Fus orar", "label.today": "Astăzi", + "label.type": "Type", "label.unknown": "Necunoscut", "label.username": "Username", + "label.visitor": "Visitor", + "label.website": "Website", "label.websites": "Site-uri web", "message.active-users": "{x} {x, plural, one {vizitator activ} other {vizitatori activi}}", "message.confirm-delete": "Sunteți sigur că doriți să ștergeți {target}?", @@ -57,6 +62,7 @@ "message.get-tracking-code": "Obține codul de urmărire", "message.go-to-settings": "Mergi la Setări", "message.incorrect-username-password": "Username/parolă incorecte.", + "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "Este disponibilă o nouă versiune {version} de umami!", "message.no-data-available": "Nicio informație disponibilă.", "message.no-websites-configured": "Nu aveți niciun site web configurat.", diff --git a/lang/ru-RU.json b/lang/ru-RU.json index 05c9e0da..fb2c4088 100644 --- a/lang/ru-RU.json +++ b/lang/ru-RU.json @@ -26,6 +26,7 @@ "label.default-date-range": "Диапазон дат по-умолчанию", "label.domain": "Домен", "label.enable-share-url": "Разрешить делиться ссылкой", + "label.event": "Event", "label.invalid": "Некорректный", "label.invalid-domain": "Некорректный домен", "label.last-days": "Последние {x} дней", @@ -34,6 +35,7 @@ "label.logout": "Выйти", "label.name": "Имя", "label.new-password": "Новый пароль", + "label.pageview": "Pageview", "label.password": "Пароль", "label.passwords-dont-match": "Пароли не совпадают", "label.profile": "Профиль", @@ -45,8 +47,11 @@ "label.this-year": "Этот год", "label.timezone": "Часовой пояс", "label.today": "Сегодня", + "label.type": "Type", "label.unknown": "Неизвестно", "label.username": "Имя пользователя", + "label.visitor": "Visitor", + "label.website": "Website", "label.websites": "Сайты", "message.active-users": "{x} текущих посетителей", "message.confirm-delete": "Вы уверены, что хотите удалить {target}?", @@ -57,6 +62,7 @@ "message.get-tracking-code": "Получить код отслеживания", "message.go-to-settings": "Перейти к настройкам", "message.incorrect-username-password": "Неверное имя пользователя/пароль.", + "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "Доступна новая версия umami {version}", "message.no-data-available": "Нет данных.", "message.no-websites-configured": "У вас нет настроенных сайтов.", diff --git a/lang/sv-SE.json b/lang/sv-SE.json index 5a1662a5..35e61fc7 100644 --- a/lang/sv-SE.json +++ b/lang/sv-SE.json @@ -26,6 +26,7 @@ "label.default-date-range": "Standard datum-urval", "label.domain": "Domän", "label.enable-share-url": "Aktivera delnings-URL", + "label.event": "Event", "label.invalid": "Ogiltig", "label.invalid-domain": "Ogiltig domän", "label.last-days": "Senaste {x} dagarna", @@ -34,6 +35,7 @@ "label.logout": "Logga ut", "label.name": "Namn", "label.new-password": "Nytt lösenord", + "label.pageview": "Pageview", "label.password": "Lösenord", "label.passwords-dont-match": "Lösenorden är inte samma", "label.profile": "Profil", @@ -45,8 +47,11 @@ "label.this-year": "Detta år", "label.timezone": "Tidszon", "label.today": "Idag", + "label.type": "Type", "label.unknown": "Okänd", "label.username": "Användarnamn", + "label.visitor": "Visitor", + "label.website": "Website", "label.websites": "Webbsajt", "message.active-users": "{x} {x, plural, one {besökare} other {besökare}} just nu", "message.confirm-delete": "Är du säker på att du vill radera {target}?", @@ -57,6 +62,7 @@ "message.get-tracking-code": "Visa spårningskod", "message.go-to-settings": "Gå till inställningar", "message.incorrect-username-password": "Felaktikt användarnamn/lösenord.", + "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "A new version of umami {version} is available!", "message.no-data-available": "Ingen data tillgänglig.", "message.no-websites-configured": "Du har inga webbsajter.", diff --git a/lang/tr-TR.json b/lang/tr-TR.json index b1b206f3..c16ba8e5 100644 --- a/lang/tr-TR.json +++ b/lang/tr-TR.json @@ -26,6 +26,7 @@ "label.default-date-range": "Varsayılan tarih aralığı", "label.domain": "Alan adı", "label.enable-share-url": "Anonim paylaşım URL'i aktif", + "label.event": "Event", "label.invalid": "Geçeriz", "label.invalid-domain": "Geçersiz alan adı", "label.last-days": "Son {x} gün", @@ -34,6 +35,7 @@ "label.logout": "Çıkış Yap", "label.name": "İsim", "label.new-password": "Yeni parola", + "label.pageview": "Pageview", "label.password": "Parola", "label.passwords-dont-match": "Parolalar uyuşmuyor", "label.profile": "Profil", @@ -45,8 +47,11 @@ "label.this-year": "Bu yıl", "label.timezone": "Zaman dilimi", "label.today": "Bugün", + "label.type": "Type", "label.unknown": "Bilinmeyen", "label.username": "Kullanıcı adı", + "label.visitor": "Visitor", + "label.website": "Website", "label.websites": "Web siteleri", "message.active-users": "{x} aktif ziyaretçi", "message.confirm-delete": "{target} kaydını silmek istediğinizden emin misiniz?", @@ -57,6 +62,7 @@ "message.get-tracking-code": "İzleme kodunu al", "message.go-to-settings": "Ayarlara git", "message.incorrect-username-password": "Hatalı kullanıcı adı ya da parola.", + "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "A new version of umami {version} is available!", "message.no-data-available": "Henüz hiç veri yok.", "message.no-websites-configured": "Henüz hiç web sitesi tanımlamadınız", diff --git a/lang/uk-UA.json b/lang/uk-UA.json index 4d077847..7f92162e 100644 --- a/lang/uk-UA.json +++ b/lang/uk-UA.json @@ -26,6 +26,7 @@ "label.default-date-range": "Діапазон дат за умовчанням", "label.domain": "Домен", "label.enable-share-url": "Дозволити ділитися посиланням", + "label.event": "Event", "label.invalid": "Некоректний", "label.invalid-domain": "Некоректний домен", "label.last-days": "Останні {x} днів", @@ -34,6 +35,7 @@ "label.logout": "Вийти", "label.name": "Ім'я", "label.new-password": "Новий пароль", + "label.pageview": "Pageview", "label.password": "Пароль", "label.passwords-dont-match": "Паролі не співпадають", "label.profile": "Профіль", @@ -45,8 +47,11 @@ "label.this-year": "Поточний рік", "label.timezone": "Часовий пояс", "label.today": "Сьогодні", + "label.type": "Type", "label.unknown": "Невідомо", "label.username": "Ім'я користувача", + "label.visitor": "Visitor", + "label.website": "Website", "label.websites": "Веб-сайти", "message.active-users": "{x} поточних відвідувачів", "message.confirm-delete": "Ви впевнені, що бажаєте видалити {target}?", @@ -57,6 +62,7 @@ "message.get-tracking-code": "Отримати код для відслідковування", "message.go-to-settings": "Перейти до налаштувань", "message.incorrect-username-password": "Невірне ім'я користувача або пароль.", + "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "Нова версія umami {version} доступна!", "message.no-data-available": "Немає даних.", "message.no-websites-configured": "У вас немає налаштованих веб-сайтів.", diff --git a/lang/zh-CN.json b/lang/zh-CN.json index 761ed17a..6f7150d9 100644 --- a/lang/zh-CN.json +++ b/lang/zh-CN.json @@ -26,6 +26,7 @@ "label.default-date-range": "默认日期范围", "label.domain": "域名", "label.enable-share-url": "激活共享链接", + "label.event": "Event", "label.invalid": "输入无效", "label.invalid-domain": "无效域名", "label.last-days": "最近 {x} 天", @@ -34,6 +35,7 @@ "label.logout": "退出", "label.name": "名字", "label.new-password": "新密码", + "label.pageview": "Pageview", "label.password": "密码", "label.passwords-dont-match": "密码不一致", "label.profile": "个人资料", @@ -45,8 +47,11 @@ "label.this-year": "今年", "label.timezone": "时区", "label.today": "今天", + "label.type": "Type", "label.unknown": "未知", "label.username": "用户名", + "label.visitor": "Visitor", + "label.website": "Website", "label.websites": "网站", "message.active-users": "当前在线 {x} 人", "message.confirm-delete": "你确定要删除{target}吗?", @@ -57,6 +62,7 @@ "message.get-tracking-code": "获得跟踪代码", "message.go-to-settings": "去设置", "message.incorrect-username-password": "用户名密码不正确.", + "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "A new version of umami {version} is available!", "message.no-data-available": "无可用数据.", "message.no-websites-configured": "你还没有设置任何网站.", From 5b8bfc9a32b380ebabc9f56367c2cc9b48b45edb Mon Sep 17 00:00:00 2001 From: endormi Date: Fri, 9 Oct 2020 16:57:14 +0300 Subject: [PATCH 09/27] Add finnish translation --- lang/fi-FI.json | 97 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 lang/fi-FI.json diff --git a/lang/fi-FI.json b/lang/fi-FI.json new file mode 100644 index 00000000..23f046d0 --- /dev/null +++ b/lang/fi-FI.json @@ -0,0 +1,97 @@ +{ + "button.add-account": "Lisää tili", + "button.add-website": "Lisää verkkosivu", + "button.back": "Takaisin", + "button.cancel": "Peruuta", + "button.change-password": "Vaihda salasana", + "button.copy-to-clipboard": "Kopioi leikepöydälle", + "button.date-range": "Ajanjakso", + "button.delete": "Poista", + "button.dismiss": "Hylkää", + "button.edit": "Muokkaa", + "button.login": "Kirjaudu sisään", + "button.more": "Lisää", + "button.refresh": "Päivitä", + "button.reset": "Nollaa", + "button.save": "Tallenna", + "button.single-day": "Yksi päivä", + "button.view-details": "Katso tiedot", + "label.accounts": "Tilit", + "label.administrator": "Järjestelmänvalvoja", + "label.confirm-password": "Vahvista salasana", + "label.current-password": "Nykyinen salasana", + "label.custom-range": "Mukautettu jakso", + "label.dashboard": "Dashboard", + "label.default-date-range": "Oletusajanjakso", + "label.domain": "Verkkotunnus", + "label.enable-share-url": "Ota jakamisen URL-osoite käyttöön", + "label.invalid": "Virheellinen", + "label.invalid-domain": "Virheellinen verkkotunnus", + "label.last-days": "Viimeisimmät {x} päivät", + "label.last-hours": "Viimeisimmät {x} tunnit", + "label.logged-in-as": "Kirjautuneena sisään nimellä {username}", + "label.logout": "Kirjaudu ulos", + "label.name": "Nimi", + "label.new-password": "Uusi salasana", + "label.password": "Salasana", + "label.passwords-dont-match": "Salasanat eivät täsmää", + "label.profile": "Profiili", + "label.required": "Vaaditaan", + "label.settings": "Asetukset", + "label.this-month": "Tämä kuukausi", + "label.this-week": "Tämä viikko", + "label.this-year": "Tämä vuosi", + "label.timezone": "Aikavyöhyke", + "label.today": "Tänään", + "label.unknown": "Tuntematon", + "label.username": "Käyttäjänimi", + "label.websites": "Verkkosivut", + "message.active-users": "{x} nykyinen {x, plural, yksi {visitor} muut {visitors}}", + "message.confirm-delete": "Haluatko varmasti poistaa {target}?", + "message.copied": "Kopioitu!", + "message.delete-warning": "Kaikki siihen liittyvät tiedot poistetaan.", + "message.failure": "Jotain meni väärin.", + "message.get-share-url": "Hanki jakamisen URL-osoite", + "message.get-tracking-code": "Hanki seurantakoodi", + "message.go-to-settings": "Mene asetuksiin", + "message.incorrect-username-password": "Väärä käyttäjänimi/salasana.", + "message.new-version-available": "Uusi versio umamista {version} on käytettävissä!", + "message.no-data-available": "Tietoja ei ole käytettävissä.", + "message.no-websites-configured": "Sinulla ei ole määritettyjä verkkosivustoja.", + "message.page-not-found": "Sivua ei löydetty.", + "message.powered-by": "Voimanlähteenä {name}", + "message.save-success": "Tallennettu onnistuneesti.", + "message.share-url": "Tämä on julkisesti jaettu URL-osoitteelle {target}.", + "message.track-stats": "Jos haluat seurata kohteen {target} tilastoja, aseta seuraava koodi verkkosivustosi {head} osioon.", + "message.type-delete": "Kirjoita {delete} alla olevaan ruutuun vahvistaaksesi.", + "metrics.actions": "Toiminnat", + "metrics.average-visit-time": "Keskimääräinen vierailuaika", + "metrics.bounce-rate": "Välitön poistuminen", + "metrics.browsers": "Selaimet", + "metrics.countries": "Maat", + "metrics.device.desktop": "Pöytäkone", + "metrics.device.laptop": "Kannettava tietokone", + "metrics.device.mobile": "Mobiili", + "metrics.device.tablet": "Tabletti", + "metrics.devices": "Laitteet", + "metrics.events": "Tapahtumat", + "metrics.filter.combined": "Yhdistetty", + "metrics.filter.domain-only": "Vain verkkotunnus", + "metrics.filter.raw": "Käsittelemätön", + "metrics.operating-systems": "Käyttöjärjestelmät", + "metrics.page-views": "Sivun näyttökertoja", + "metrics.pages": "Sivut", + "metrics.referrers": "Viittaajat", + "metrics.unique-visitors": "Uniikit vierailijat", + "metrics.views": "Näyttökertoja", + "metrics.visitors": "Vierailijat", + "title.add-account": "Lisää tili", + "title.add-website": "Lisää verkkosivu", + "title.change-password": "Vaihda salasana", + "title.delete-account": "Poista tili", + "title.delete-website": "Poista verkkosivu", + "title.edit-account": "Muokkaa tiliä", + "title.edit-website": "Muokkaa verkkosivua", + "title.share-url": "Jaa URL", + "title.tracking-code": "Seurantakoodi" +} From 5dd3110fbf489ace3dbe4f8edf324fa3d5465c84 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 9 Oct 2020 10:48:47 -0700 Subject: [PATCH 10/27] Updated log merge logic to prevent duplicates. --- components/common/Table.module.css | 1 + components/metrics/RealtimeLog.js | 2 +- components/pages/RealtimeDashboard.js | 22 ++++++++++-------- pages/api/collect.js | 3 ++- pages/api/realtime.js | 33 +++++++++++++++++++++++---- 5 files changed, 44 insertions(+), 17 deletions(-) diff --git a/components/common/Table.module.css b/components/common/Table.module.css index bde2588a..5db753ec 100644 --- a/components/common/Table.module.css +++ b/components/common/Table.module.css @@ -21,6 +21,7 @@ } .row { + display: flex; border-bottom: 1px solid var(--gray300); padding: 10px 0; } diff --git a/components/metrics/RealtimeLog.js b/components/metrics/RealtimeLog.js index fce9f7e9..9164dcdb 100644 --- a/components/metrics/RealtimeLog.js +++ b/components/metrics/RealtimeLog.js @@ -103,7 +103,7 @@ export default function RealtimeLog({ data, websites }) { return ( ); diff --git a/components/pages/RealtimeDashboard.js b/components/pages/RealtimeDashboard.js index 348b6642..af3fdc58 100644 --- a/components/pages/RealtimeDashboard.js +++ b/components/pages/RealtimeDashboard.js @@ -11,20 +11,22 @@ import RealtimeLog from '../metrics/RealtimeLog'; const REALTIME_RANGE = 30; const REALTIME_INTERVAL = 5000; -function filterTime(data, time) { - return data.filter(({ created_at }) => new Date(created_at).getTime() >= time); +function mergeData(state, data, time) { + const ids = state.map(({ __id }) => __id); + return state + .concat(data.filter(({ __id }) => !ids.includes(__id))) + .filter(({ created_at }) => new Date(created_at).getTime() >= time); } export default function RealtimeDashboard() { const [data, setData] = useState(); const [website, setWebsite] = useState(); - const [lastTime, setLastTime] = useState(); const { data: init, loading } = useFetch('/api/realtime', { type: 'init' }); const { data: updates } = useFetch( '/api/realtime', - { type: 'update', start_at: lastTime }, + { type: 'update', start_at: data?.timestamp }, { - disabled: !init?.token, + disabled: !init?.token || !data, interval: REALTIME_INTERVAL, headers: { 'x-umami-token': init?.token }, }, @@ -34,15 +36,15 @@ export default function RealtimeDashboard() { if (init && !data) { setData(init.data); } else if (updates) { - const { pageviews, sessions, events } = updates; + const { pageviews, sessions, events, timestamp } = updates; const minTime = subMinutes(startOfMinute(new Date()), REALTIME_RANGE).getTime(); setData(state => ({ - pageviews: filterTime(state.pageviews.concat(pageviews), minTime), - sessions: filterTime(state.sessions.concat(sessions), minTime), - events: filterTime(state.events.concat(events), minTime), + pageviews: mergeData(state.pageviews, pageviews, minTime), + sessions: mergeData(state.sessions, sessions, minTime), + events: mergeData(state.events, events, minTime), + timestamp, })); } - setLastTime(Date.now()); }, [init, updates]); if (!init || loading || !data) { diff --git a/pages/api/collect.js b/pages/api/collect.js index 4f01b225..a6140169 100644 --- a/pages/api/collect.js +++ b/pages/api/collect.js @@ -6,6 +6,8 @@ import { createToken } from 'lib/crypto'; import { getIpAddress } from '../../lib/request'; export default async (req, res) => { + await useCors(req, res); + if (isBot(req.headers['user-agent'])) { return ok(res); } @@ -19,7 +21,6 @@ export default async (req, res) => { } } - await useCors(req, res); await useSession(req, res); const { type, payload } = req.body; diff --git a/pages/api/realtime.js b/pages/api/realtime.js index 64cbea5e..538f611a 100644 --- a/pages/api/realtime.js +++ b/pages/api/realtime.js @@ -8,11 +8,30 @@ export default async (req, res) => { await useAuth(req, res); async function getData(websites, time) { - return Promise.all([ + const [pageviews, sessions, events] = await Promise.all([ getPageviews(websites, time), getSessions(websites, time), getEvents(websites, time), ]); + + return { + pageviews: pageviews.map(({ view_id, ...props }) => ({ + __id: `p${view_id}`, + view_id, + ...props, + })), + sessions: sessions.map(({ session_id, ...props }) => ({ + __id: `s${session_id}`, + session_id, + ...props, + })), + events: events.map(({ event_id, ...props }) => ({ + __id: `e${event_id}`, + event_id, + ...props, + })), + timestamp: Date.now(), + }; } if (req.method === 'GET') { @@ -22,10 +41,14 @@ export default async (req, res) => { if (type === 'init') { const websites = await getUserWebsites(user_id); const ids = websites.map(({ website_id }) => website_id); - const [pageviews, sessions, events] = await getData(ids, subMinutes(new Date(), 30)); const token = await createToken({ websites: ids }); + const data = await getData(ids, subMinutes(new Date(), 30)); - return ok(res, { websites, token, data: { pageviews, sessions, events } }); + return ok(res, { + websites, + token, + data, + }); } if (type === 'update') { @@ -37,9 +60,9 @@ export default async (req, res) => { const { websites } = await parseToken(token); - const [pageviews, sessions, events] = await getData(websites, new Date(+start_at)); + const data = await getData(websites, new Date(+start_at)); - return ok(res, { pageviews, sessions, events }); + return ok(res, data); } return badRequest(res); From 0ccae7483ca842804ac6a867c6ba99cc3b3ff4e3 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 9 Oct 2020 11:14:42 -0700 Subject: [PATCH 11/27] Update realtime chart animation. --- components/metrics/BarChart.js | 2 +- components/metrics/RealtimeChart.js | 27 +++++++++++++++++++-------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/components/metrics/BarChart.js b/components/metrics/BarChart.js index 24f642c1..f32691fa 100644 --- a/components/metrics/BarChart.js +++ b/components/metrics/BarChart.js @@ -40,7 +40,7 @@ export default function BarChart({ switch (unit) { case 'minute': - return dateFormat(d, 'h:mm', locale); + return index % 2 === 0 ? dateFormat(d, 'h:mm', locale) : ''; case 'hour': return dateFormat(d, 'ha', locale); case 'day': diff --git a/components/metrics/RealtimeChart.js b/components/metrics/RealtimeChart.js index b27322ca..b4703036 100644 --- a/components/metrics/RealtimeChart.js +++ b/components/metrics/RealtimeChart.js @@ -1,5 +1,5 @@ -import React, { useMemo } from 'react'; -import { format, parseISO, startOfMinute, subMinutes } from 'date-fns'; +import React, { useMemo, useRef } from 'react'; +import { format, parseISO, startOfMinute, subMinutes, isBefore } from 'date-fns'; import PageviewsChart from './PageviewsChart'; import { getDateArray } from 'lib/date'; @@ -23,13 +23,13 @@ function mapData(data) { return arr; } -export default function RealtimeChart({ data, ...props }) { +export default function RealtimeChart({ data, unit, ...props }) { + const endDate = startOfMinute(new Date()); + const startDate = subMinutes(endDate, 30); + const prevEndDate = useRef(endDate); + const chartData = useMemo(() => { if (data) { - const endDate = startOfMinute(new Date()); - const startDate = subMinutes(endDate, 30); - const unit = 'minute'; - return { pageviews: getDateArray(mapData(data.pageviews), startDate, endDate, unit), sessions: getDateArray(mapData(data.sessions), startDate, endDate, unit), @@ -38,5 +38,16 @@ export default function RealtimeChart({ data, ...props }) { return { pageviews: [], sessions: [] }; }, [data]); - return ; + // Don't animate the bars shifting over because it looks weird + const animationDuration = useMemo(() => { + if (isBefore(prevEndDate.current, endDate)) { + prevEndDate.current = endDate; + return 0; + } + return 300; + }, [data]); + + return ( + + ); } From e30f2dfb44ec334b63da87cd0ac306038a526824 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 9 Oct 2020 17:58:27 -0700 Subject: [PATCH 12/27] Realtime header component. --- components/common/Table.js | 25 ++++---- components/common/Table.module.css | 2 - components/common/Tag.module.css | 1 - components/metrics/BarChart.js | 8 +-- components/metrics/PageviewsChart.js | 2 + components/metrics/RealtimeChart.js | 8 ++- components/metrics/RealtimeHeader.js | 53 ++++++++++++++++ components/metrics/RealtimeHeader.module.css | 3 + components/metrics/RealtimeLog.js | 62 ++++++++++--------- components/metrics/RealtimeLog.module.css | 18 +++++- components/pages/RealtimeDashboard.js | 57 ++++++++--------- components/pages/RealtimeDashboard.module.css | 4 ++ lang/da-DK.json | 5 -- lang/de-DE.json | 5 -- lang/el-GR.json | 5 -- lang/en-US.json | 5 -- lang/es-MX.json | 5 -- lang/fi-FI.json | 3 + lang/fo-FO.json | 5 -- lang/fr-FR.json | 5 -- lang/id-ID.json | 5 -- lang/ja-JP.json | 5 -- lang/mn-MN.json | 5 -- lang/nb-NO.json | 5 -- lang/nl-NL.json | 5 -- lang/pt-PT.json | 5 -- lang/ro-RO.json | 5 -- lang/ru-RU.json | 5 -- lang/sv-SE.json | 5 -- lang/tr-TR.json | 5 -- lang/uk-UA.json | 5 -- lang/zh-CN.json | 5 -- lib/constants.js | 1 + package.json | 2 +- 34 files changed, 167 insertions(+), 177 deletions(-) create mode 100644 components/metrics/RealtimeHeader.js create mode 100644 components/metrics/RealtimeHeader.module.css diff --git a/components/common/Table.js b/components/common/Table.js index d0a1e0c7..f0989903 100644 --- a/components/common/Table.js +++ b/components/common/Table.js @@ -10,6 +10,7 @@ export default function Table({ className, bodyClassName, rowKey, + showHeader = true, children, }) { if (empty && rows.length === 0) { @@ -18,17 +19,19 @@ export default function Table({ return (
-
- {columns.map(({ key, label, className, style, header }) => ( -
- {label} -
- ))} -
+ {showHeader && ( +
+ {columns.map(({ key, label, className, style, header }) => ( +
+ {label} +
+ ))} +
+ )}
{rows.length === 0 && } {!children && diff --git a/components/common/Table.module.css b/components/common/Table.module.css index 5db753ec..4d7cd24b 100644 --- a/components/common/Table.module.css +++ b/components/common/Table.module.css @@ -4,7 +4,6 @@ } .header { - display: flex; border-bottom: 1px solid var(--gray300); } @@ -21,7 +20,6 @@ } .row { - display: flex; border-bottom: 1px solid var(--gray300); padding: 10px 0; } diff --git a/components/common/Tag.module.css b/components/common/Tag.module.css index 38d66692..5e145ea1 100644 --- a/components/common/Tag.module.css +++ b/components/common/Tag.module.css @@ -1,5 +1,4 @@ .tag { - font-size: var(--font-size-small); padding: 2px 4px; border: 1px solid var(--gray300); border-radius: 4px; diff --git a/components/metrics/BarChart.js b/components/metrics/BarChart.js index f32691fa..05183e1b 100644 --- a/components/metrics/BarChart.js +++ b/components/metrics/BarChart.js @@ -5,17 +5,17 @@ import ChartJS from 'chart.js'; import { formatLongNumber } from 'lib/format'; import { dateFormat } from 'lib/lang'; import useLocale from 'hooks/useLocale'; -import styles from './BarChart.module.css'; import useTheme from 'hooks/useTheme'; -import { THEME_COLORS } from 'lib/constants'; +import { DEFAUL_CHART_HEIGHT, DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants'; +import styles from './BarChart.module.css'; export default function BarChart({ chartId, datasets, unit, records, - height = 400, - animationDuration = 300, + height = DEFAUL_CHART_HEIGHT, + animationDuration = DEFAULT_ANIMATION_DURATION, className, stacked = false, loading = false, diff --git a/components/metrics/PageviewsChart.js b/components/metrics/PageviewsChart.js index 30e85e80..79fe4917 100644 --- a/components/metrics/PageviewsChart.js +++ b/components/metrics/PageviewsChart.js @@ -14,6 +14,7 @@ export default function PageviewsChart({ className, loading, animationDuration = DEFAULT_ANIMATION_DURATION, + ...props }) { const intl = useIntl(); const [theme] = useTheme(); @@ -56,6 +57,7 @@ export default function PageviewsChart({ {visible => ( + ); } diff --git a/components/metrics/RealtimeHeader.js b/components/metrics/RealtimeHeader.js new file mode 100644 index 00000000..09f83ff6 --- /dev/null +++ b/components/metrics/RealtimeHeader.js @@ -0,0 +1,53 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import PageHeader from '../layout/PageHeader'; +import DropDown from '../common/DropDown'; +import MetricCard from './MetricCard'; +import styles from './RealtimeHeader.module.css'; + +export default function RealtimeHeader({ websites, data, websiteId, onSelect }) { + const options = [ + { label: , value: 0 }, + ].concat(websites.map(({ name, website_id }) => ({ label: name, value: website_id }))); + + const { pageviews, sessions, events } = data; + const countries = sessions.reduce((obj, { country }) => { + if (country) { + if (!obj[country]) { + obj[country] = 1; + } else { + obj[country] += 1; + } + } + return obj; + }, {}); + + return ( + <> + +
+ +
+ +
+
+ } + value={pageviews.length} + /> + } + value={sessions.length} + /> + } + value={events.length} + /> + } + value={Object.keys(countries).length} + /> +
+ + ); +} diff --git a/components/metrics/RealtimeHeader.module.css b/components/metrics/RealtimeHeader.module.css new file mode 100644 index 00000000..8f948eb1 --- /dev/null +++ b/components/metrics/RealtimeHeader.module.css @@ -0,0 +1,3 @@ +.metrics { + display: flex; +} diff --git a/components/metrics/RealtimeLog.js b/components/metrics/RealtimeLog.js index 1ea0080e..6fddf71b 100644 --- a/components/metrics/RealtimeLog.js +++ b/components/metrics/RealtimeLog.js @@ -1,6 +1,7 @@ import React, { useMemo } from 'react'; -import { FormattedMessage, useIntl } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import { FixedSizeList } from 'react-window'; +import classNames from 'classnames'; import firstBy from 'thenby'; import { format } from 'date-fns'; import Icon from 'components/common/Icon'; @@ -14,8 +15,17 @@ import Visitor from 'assets/visitor.svg'; import Eye from 'assets/eye.svg'; import styles from './RealtimeLog.module.css'; +const TYPE_PAGEVIEW = 0; +const TYPE_SESSION = 1; +const TYPE_EVENT = 2; + +const TYPE_ICONS = { + [TYPE_PAGEVIEW]: , + [TYPE_SESSION]: , + [TYPE_EVENT]: , +}; + export default function RealtimeLog({ data, websites }) { - const intl = useIntl(); const [locale] = useLocale(); const countryNames = useCountryNames(locale); const logs = useMemo(() => { @@ -26,24 +36,21 @@ export default function RealtimeLog({ data, websites }) { const columns = [ { key: 'time', - label: , - className: 'col-1', - render: ({ created_at }) => format(new Date(created_at), 'H:mm:ss'), + className: classNames(styles.time, 'col-3 col-lg-1'), + render: ({ created_at }) => format(new Date(created_at), 'h:mm:ss'), }, { key: 'website', - label: , - className: 'col-2', + className: classNames(styles.website, 'col-9 col-lg-2'), render: getWebsite, }, { - key: 'type', - label: , - className: 'col-9', + key: 'detail', + className: classNames(styles.detail, 'col-12 col-lg-9'), render: row => ( <> - - {getDescription(row)} + + {getDetail(row)} ), }, @@ -51,35 +58,26 @@ export default function RealtimeLog({ data, websites }) { function getType({ view_id, session_id, event_id }) { if (event_id) { - return intl.formatMessage({ id: 'label.event', defaultMessage: 'Event' }); + return TYPE_EVENT; } if (view_id) { - return intl.formatMessage({ id: 'label.pageview', defaultMessage: 'Pageview' }); + return TYPE_PAGEVIEW; } if (session_id) { - return intl.formatMessage({ id: 'label.visitor', defaultMessage: 'Visitor' }); + return TYPE_SESSION; } return null; } - function getIcon({ view_id, session_id, event_id }) { - if (event_id) { - return ; - } - if (view_id) { - return ; - } - if (session_id) { - return ; - } - return null; + function getIcon(row) { + return TYPE_ICONS[getType(row)]; } function getWebsite({ website_id }) { return websites.find(n => n.website_id === website_id)?.name; } - function getDescription({ + function getDetail({ event_type, event_value, view_id, @@ -121,8 +119,14 @@ export default function RealtimeLog({ data, websites }) { return (
- - +
+ {Row}
diff --git a/components/metrics/RealtimeLog.module.css b/components/metrics/RealtimeLog.module.css index 5227be98..af100708 100644 --- a/components/metrics/RealtimeLog.module.css +++ b/components/metrics/RealtimeLog.module.css @@ -1,5 +1,5 @@ .table { - font-size: var(--font-size-small); + font-size: var(--font-size-xsmall); } .row { @@ -16,3 +16,19 @@ align-self: center; margin-right: 20px; } + +.website { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.detail { + flex: 1; +} + +.detail span { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} diff --git a/components/pages/RealtimeDashboard.js b/components/pages/RealtimeDashboard.js index bb2948bc..fd123f8b 100644 --- a/components/pages/RealtimeDashboard.js +++ b/components/pages/RealtimeDashboard.js @@ -1,12 +1,12 @@ import React, { useState, useEffect, useMemo } from 'react'; -import { FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; import { subMinutes, startOfMinute } from 'date-fns'; import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import DropDown from 'components/common/DropDown'; import useFetch from 'hooks/useFetch'; import RealtimeChart from '../metrics/RealtimeChart'; import RealtimeLog from '../metrics/RealtimeLog'; +import styles from './RealtimeDashboard.module.css'; +import RealtimeHeader from '../metrics/RealtimeHeader'; const REALTIME_RANGE = 30; const REALTIME_INTERVAL = 5000; @@ -24,28 +24,27 @@ function filterWebsite(data, id) { export default function RealtimeDashboard() { const [data, setData] = useState(); - const [website, setWebsite] = useState(); + const [websiteId, setWebsiteId] = useState(0); const { data: init, loading } = useFetch('/api/realtime', { params: { type: 'init' } }); const { data: updates } = useFetch('/api/realtime', { params: { type: 'update', start_at: data?.timestamp }, - disabled: !init?.token || !data, + disabled: !init?.websites?.length || !data, interval: REALTIME_INTERVAL, headers: { 'x-umami-token': init?.token }, }); const realtimeData = useMemo(() => { - if (website) { - const { website_id } = website; + if (websiteId) { const { pageviews, sessions, events, ...props } = data; return { - pageviews: filterWebsite(pageviews, website_id), - sessions: filterWebsite(sessions, website_id), - events: filterWebsite(events, website_id), + pageviews: filterWebsite(pageviews, websiteId), + sessions: filterWebsite(sessions, websiteId), + events: filterWebsite(events, websiteId), ...props, }; } return data; - }, [data, website]); + }, [data, websiteId]); useEffect(() => { if (init && !data) { @@ -68,33 +67,27 @@ export default function RealtimeDashboard() { const { websites } = init; - const options = [ - { label: , value: 0 }, - ].concat(websites.map(({ name, website_id }) => ({ label: name, value: website_id }))); - const selectedValue = options.find(({ value }) => value === website?.website_id)?.value || 0; - - function handleSelect(value) { - setWebsite(websites.find(({ website_id }) => website_id === value)); - } - return ( - -
- -
- -
- -
-
+
+ +
+
+
+
hi.
); diff --git a/components/pages/RealtimeDashboard.module.css b/components/pages/RealtimeDashboard.module.css index abde53a3..b9b5a632 100644 --- a/components/pages/RealtimeDashboard.module.css +++ b/components/pages/RealtimeDashboard.module.css @@ -1,3 +1,7 @@ .container { display: flex; } + +.chart { + margin-bottom: 30px; +} diff --git a/lang/da-DK.json b/lang/da-DK.json index 9d33fd7b..5532bf69 100644 --- a/lang/da-DK.json +++ b/lang/da-DK.json @@ -26,7 +26,6 @@ "label.default-date-range": "Default date range", "label.domain": "Domæne", "label.enable-share-url": "Aktivér delings-URL", - "label.event": "Event", "label.invalid": "Ugyldig", "label.invalid-domain": "Ugyldigt domæne", "label.last-days": "Sidste {x} dage", @@ -35,7 +34,6 @@ "label.logout": "Log ud", "label.name": "Navn", "label.new-password": "Ny adgangskode", - "label.pageview": "Pageview", "label.password": "Adgangskode", "label.passwords-dont-match": "Adgangskoder matcher ikke", "label.profile": "Profil", @@ -47,11 +45,8 @@ "label.this-year": "Dette år", "label.timezone": "Timezone", "label.today": "Idag", - "label.type": "Type", "label.unknown": "Ukendt", "label.username": "Brugernavn", - "label.visitor": "Visitor", - "label.website": "Website", "label.websites": "Hjemmesider", "message.active-users": "{x} nuværende {x, plural, one {bruger} other {brugere}}", "message.confirm-delete": "Er du sikker på at du vil slette {target}?", diff --git a/lang/de-DE.json b/lang/de-DE.json index 7821f826..df5e6dd1 100644 --- a/lang/de-DE.json +++ b/lang/de-DE.json @@ -26,7 +26,6 @@ "label.default-date-range": "Voreingestellter Datumsbereich", "label.domain": "Domain", "label.enable-share-url": "Freigabe-URL aktivieren", - "label.event": "Event", "label.invalid": "Ungültig", "label.invalid-domain": "Ungültige Domain", "label.last-days": "Letzten {x} Tage", @@ -35,7 +34,6 @@ "label.logout": "Abmelden", "label.name": "Name", "label.new-password": "Neues Passwort", - "label.pageview": "Pageview", "label.password": "Passwort", "label.passwords-dont-match": "Passwörter stimmen nicht überein", "label.profile": "Profil", @@ -47,11 +45,8 @@ "label.this-year": "Dieses Jahr", "label.timezone": "Zeitzone", "label.today": "Heute", - "label.type": "Type", "label.unknown": "Unbekannt", "label.username": "Benutzername", - "label.visitor": "Visitor", - "label.website": "Website", "label.websites": "Webseiten", "message.active-users": "{x} {x, plural, one {aktiver Besucher} other {aktive Besucher}}", "message.confirm-delete": "Sind sie sich sicher {target} zu löschen?", diff --git a/lang/el-GR.json b/lang/el-GR.json index 88d4a0e1..7fbdbfbf 100644 --- a/lang/el-GR.json +++ b/lang/el-GR.json @@ -26,7 +26,6 @@ "label.default-date-range": "Προεπιλεγμένο εύρος ημερομηνιών", "label.domain": "Τομέας", "label.enable-share-url": "Ενεργοποίηση κοινής χρήσης URL", - "label.event": "Event", "label.invalid": "Μη έγκυρο", "label.invalid-domain": "Μη έγκυρος τομέας", "label.last-days": "Τελευταίες {x} ημέρες", @@ -35,7 +34,6 @@ "label.logout": "Αποσύνδεση", "label.name": "Όνομα", "label.new-password": "Νέος κωδικός", - "label.pageview": "Pageview", "label.password": "Κωδικός", "label.passwords-dont-match": "Οι κωδικοί πρόσβασης δεν ταιριάζουν", "label.profile": "Προφίλ", @@ -47,11 +45,8 @@ "label.this-year": "Αυτή την χρονιά", "label.timezone": "Ζώνη ώρας", "label.today": "Σήμερα", - "label.type": "Type", "label.unknown": "Άγνωστο", "label.username": "Όνομα χρήστη", - "label.visitor": "Visitor", - "label.website": "Website", "label.websites": "Ιστότοποι", "message.active-users": "{x} ενεργοί {x, plural, one {επισκέπτης} other {επισκέπτες}}", "message.confirm-delete": "Είστε βέβαιοι ότι θέλετε να διαγράψετε το {target};", diff --git a/lang/en-US.json b/lang/en-US.json index cadd0512..07670232 100644 --- a/lang/en-US.json +++ b/lang/en-US.json @@ -26,7 +26,6 @@ "label.default-date-range": "Default date range", "label.domain": "Domain", "label.enable-share-url": "Enable share URL", - "label.event": "Event", "label.invalid": "Invalid", "label.invalid-domain": "Invalid domain", "label.last-days": "Last {x} days", @@ -35,7 +34,6 @@ "label.logout": "Logout", "label.name": "Name", "label.new-password": "New password", - "label.pageview": "Pageview", "label.password": "Password", "label.passwords-dont-match": "Passwords don't match", "label.profile": "Profile", @@ -47,11 +45,8 @@ "label.this-year": "This year", "label.timezone": "Timezone", "label.today": "Today", - "label.type": "Type", "label.unknown": "Unknown", "label.username": "Username", - "label.visitor": "Visitor", - "label.website": "Website", "label.websites": "Websites", "message.active-users": "{x} current {x, plural, one {visitor} other {visitors}}", "message.confirm-delete": "Are your sure you want to delete {target}?", diff --git a/lang/es-MX.json b/lang/es-MX.json index 48efe589..61395fcf 100644 --- a/lang/es-MX.json +++ b/lang/es-MX.json @@ -26,7 +26,6 @@ "label.default-date-range": "Default date range", "label.domain": "Dominio", "label.enable-share-url": "Habilitar compartir URL", - "label.event": "Event", "label.invalid": "Inválido", "label.invalid-domain": "Dominio inválido", "label.last-days": "Últimos {x} días", @@ -35,7 +34,6 @@ "label.logout": "Cerrar sesión", "label.name": "Nombre", "label.new-password": "Nueva contraseña", - "label.pageview": "Pageview", "label.password": "Contraseña", "label.passwords-dont-match": "Las contraseñas no coinciden", "label.profile": "Perfil", @@ -47,11 +45,8 @@ "label.this-year": "Este año", "label.timezone": "Timezone", "label.today": "Hoy", - "label.type": "Type", "label.unknown": "Unknown", "label.username": "Nombre de usuario", - "label.visitor": "Visitor", - "label.website": "Website", "label.websites": "Sitios", "message.active-users": "{x} {x, plural, one {activo} other {activos}}", "message.confirm-delete": "¿Estás seguro(a) de querer eliminar {target}?", diff --git a/lang/fi-FI.json b/lang/fi-FI.json index 23f046d0..9f0c930b 100644 --- a/lang/fi-FI.json +++ b/lang/fi-FI.json @@ -18,6 +18,7 @@ "button.view-details": "Katso tiedot", "label.accounts": "Tilit", "label.administrator": "Järjestelmänvalvoja", + "label.all-websites": "All websites", "label.confirm-password": "Vahvista salasana", "label.current-password": "Nykyinen salasana", "label.custom-range": "Mukautettu jakso", @@ -36,6 +37,7 @@ "label.password": "Salasana", "label.passwords-dont-match": "Salasanat eivät täsmää", "label.profile": "Profiili", + "label.realtime": "Realtime", "label.required": "Vaaditaan", "label.settings": "Asetukset", "label.this-month": "Tämä kuukausi", @@ -55,6 +57,7 @@ "message.get-tracking-code": "Hanki seurantakoodi", "message.go-to-settings": "Mene asetuksiin", "message.incorrect-username-password": "Väärä käyttäjänimi/salasana.", + "message.log.visitor": "Visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "Uusi versio umamista {version} on käytettävissä!", "message.no-data-available": "Tietoja ei ole käytettävissä.", "message.no-websites-configured": "Sinulla ei ole määritettyjä verkkosivustoja.", diff --git a/lang/fo-FO.json b/lang/fo-FO.json index 2969b83e..ee5ab8cb 100644 --- a/lang/fo-FO.json +++ b/lang/fo-FO.json @@ -26,7 +26,6 @@ "label.default-date-range": "Standard dato", "label.domain": "Økisnavn", "label.enable-share-url": "Virkja deili leinki", - "label.event": "Event", "label.invalid": "Ógilda", "label.invalid-domain": "Ógilt økisnavn", "label.last-days": "Seinastu {x} dagarnar", @@ -35,7 +34,6 @@ "label.logout": "Rita út", "label.name": "Navn", "label.new-password": "Nýtt loyniorð", - "label.pageview": "Pageview", "label.password": "Loyniorð", "label.passwords-dont-match": "Loyniorðini eru ikki eins", "label.profile": "Brúkari", @@ -47,11 +45,8 @@ "label.this-year": "Hetta árið", "label.timezone": "Tíðarsona", "label.today": "Í dag", - "label.type": "Type", "label.unknown": "Ókent", "label.username": "Brúkaranavn", - "label.visitor": "Visitor", - "label.website": "Website", "label.websites": "Heimasíður", "message.active-users": "{x} í løtuni {x, plural, one {vitjandi} other { vitjandi }}", "message.confirm-delete": "Ert tú sikkur at tú ynskir at sletta {target}?", diff --git a/lang/fr-FR.json b/lang/fr-FR.json index b62ce587..cb2cc103 100644 --- a/lang/fr-FR.json +++ b/lang/fr-FR.json @@ -26,7 +26,6 @@ "label.default-date-range": "Default date range", "label.domain": "Domaine", "label.enable-share-url": "Activer le partage d'URL", - "label.event": "Event", "label.invalid": "Invalide", "label.invalid-domain": "Domaine invalide", "label.last-days": "{x} derniers jours", @@ -35,7 +34,6 @@ "label.logout": "Déconnexion", "label.name": "Nom", "label.new-password": "Nouveau mot de passe", - "label.pageview": "Pageview", "label.password": "Mot de passe", "label.passwords-dont-match": "Les mots de passe ne correspondent pas", "label.profile": "Profile", @@ -47,11 +45,8 @@ "label.this-year": "Cette année", "label.timezone": "Timezone", "label.today": "Aujourd'hui", - "label.type": "Type", "label.unknown": "Unknown", "label.username": "Nom d'utilisateur", - "label.visitor": "Visitor", - "label.website": "Website", "label.websites": "Sites", "message.active-users": "{x} {x, plural, one {visiteur} other {visiteurs}} actuellement", "message.confirm-delete": "Êtes-vous sur de vouloir supprimer {target}?", diff --git a/lang/id-ID.json b/lang/id-ID.json index 8f12d762..7c7db298 100644 --- a/lang/id-ID.json +++ b/lang/id-ID.json @@ -26,7 +26,6 @@ "label.default-date-range": "Rentang tanggal default", "label.domain": "Domain", "label.enable-share-url": "Aktifkan URL berbagi", - "label.event": "Event", "label.invalid": "Tidak valid", "label.invalid-domain": "Domain tidak valid", "label.last-days": "{x} hari terakhir", @@ -35,7 +34,6 @@ "label.logout": "Keluar", "label.name": "Nama", "label.new-password": "Kata sandi baru", - "label.pageview": "Pageview", "label.password": "Kata sandi", "label.passwords-dont-match": "Kata sandi tidak cocok", "label.profile": "Profil", @@ -47,11 +45,8 @@ "label.this-year": "Tahun ini", "label.timezone": "Zona waktu", "label.today": "Hari ini", - "label.type": "Type", "label.unknown": "Tidak diketahui", "label.username": "Nama pengguna", - "label.visitor": "Visitor", - "label.website": "Website", "label.websites": "Situs web", "message.active-users": "{x} pengunjung saat ini", "message.confirm-delete": "Apakah kamu yakin ingin menghapus {target}?", diff --git a/lang/ja-JP.json b/lang/ja-JP.json index fd276b26..09458224 100644 --- a/lang/ja-JP.json +++ b/lang/ja-JP.json @@ -26,7 +26,6 @@ "label.default-date-range": "最初に表示する期間", "label.domain": "ドメイン", "label.enable-share-url": "共有リンクを有効にする", - "label.event": "Event", "label.invalid": "無効", "label.invalid-domain": "無効なドメイン", "label.last-days": "過去{x}日間", @@ -35,7 +34,6 @@ "label.logout": "ログアウト", "label.name": "名前", "label.new-password": "新しいパスワード", - "label.pageview": "Pageview", "label.password": "パスワード", "label.passwords-dont-match": "パスワードが一致しません", "label.profile": "プロファイル", @@ -47,11 +45,8 @@ "label.this-year": "今年", "label.timezone": "タイムゾーン", "label.today": "今日", - "label.type": "Type", "label.unknown": "不明", "label.username": "ユーザー名", - "label.visitor": "Visitor", - "label.website": "Website", "label.websites": "Webサイト", "message.active-users": "{x}人が閲覧中です。", "message.confirm-delete": "{target}を削除してもよろしいですか?", diff --git a/lang/mn-MN.json b/lang/mn-MN.json index a6e632db..5e92c9c7 100644 --- a/lang/mn-MN.json +++ b/lang/mn-MN.json @@ -26,7 +26,6 @@ "label.default-date-range": "Өгөгдмөл хугацааны муж", "label.domain": "Домэйн", "label.enable-share-url": "Хуваалцах холбоос идэвхжүүлэх", - "label.event": "Event", "label.invalid": "Буруу", "label.invalid-domain": "Буруу домэйн", "label.last-days": "Сүүлийн {x} хоног", @@ -35,7 +34,6 @@ "label.logout": "Гарах", "label.name": "Нэр", "label.new-password": "Шинэ нууц үг", - "label.pageview": "Pageview", "label.password": "Нууц үг", "label.passwords-dont-match": "Нууц үг тохирохгүй байна", "label.profile": "Бүртгэл", @@ -47,11 +45,8 @@ "label.this-year": "Энэ жил", "label.timezone": "Цагийн бүс", "label.today": "Өнөөдөр", - "label.type": "Type", "label.unknown": "Тодорхойгүй", "label.username": "Хэрэглэгчийн нэр", - "label.visitor": "Visitor", - "label.website": "Website", "label.websites": "Вебүүд", "message.active-users": "одоо {x} {x, plural, one {зочин} other {зочин}} байна", "message.confirm-delete": "Та {target}-г устгахдаа итгэлтэй байна уу?", diff --git a/lang/nb-NO.json b/lang/nb-NO.json index fc2664f9..0af11471 100644 --- a/lang/nb-NO.json +++ b/lang/nb-NO.json @@ -26,7 +26,6 @@ "label.default-date-range": "Standard datoperiode", "label.domain": "Domene", "label.enable-share-url": "Aktiver delings-URL", - "label.event": "Event", "label.invalid": "Ugyldig", "label.invalid-domain": "Ugyldig domene", "label.last-days": "Siste {x} dager", @@ -35,7 +34,6 @@ "label.logout": "Logg ut", "label.name": "Navn", "label.new-password": "Nytt passord", - "label.pageview": "Pageview", "label.password": "Passord", "label.passwords-dont-match": "Passordene er ikke like", "label.profile": "Profil", @@ -47,11 +45,8 @@ "label.this-year": "I år", "label.timezone": "Tidssone", "label.today": "I dag", - "label.type": "Type", "label.unknown": "Ukjent", "label.username": "Brukernavn", - "label.visitor": "Visitor", - "label.website": "Website", "label.websites": "Nettsteder", "message.active-users": "{x} {x, plural, one {besøkende} other {besøkende}} nå", "message.confirm-delete": "Er du sikker på at du vil slette {target}?", diff --git a/lang/nl-NL.json b/lang/nl-NL.json index cbedfc4c..4613ed33 100644 --- a/lang/nl-NL.json +++ b/lang/nl-NL.json @@ -26,7 +26,6 @@ "label.default-date-range": "Standaard bereik", "label.domain": "Domein", "label.enable-share-url": "Sta delen via openbare URL toe", - "label.event": "Event", "label.invalid": "Ongeldig", "label.invalid-domain": "Ongeldig domein", "label.last-days": "Laatste {x} dagen", @@ -35,7 +34,6 @@ "label.logout": "Uitloggen", "label.name": "Naam", "label.new-password": "Nieuw wachtwoord", - "label.pageview": "Pageview", "label.password": "Wachtwoord", "label.passwords-dont-match": "Wachtwoorden komen niet overeen", "label.profile": "Profiel", @@ -47,11 +45,8 @@ "label.this-year": "Dit jaar", "label.timezone": "Tijdzone", "label.today": "Vandaag", - "label.type": "Type", "label.unknown": "Onbekend", "label.username": "Gebruikersnaam", - "label.visitor": "Visitor", - "label.website": "Website", "label.websites": "Websites", "message.active-users": "{x} actieve {x, plural, one {bezoeker} other {bezoekers}}", "message.confirm-delete": "Weet je zeker dat je {target} wilt verwijderen?", diff --git a/lang/pt-PT.json b/lang/pt-PT.json index ca31437b..81e3a334 100644 --- a/lang/pt-PT.json +++ b/lang/pt-PT.json @@ -26,7 +26,6 @@ "label.default-date-range": "Intervalo de datas predefinido", "label.domain": "Domínio", "label.enable-share-url": "Ativar link de partilha", - "label.event": "Event", "label.invalid": "Inválido", "label.invalid-domain": "Domínio inválido", "label.last-days": "Últimos {x} dias", @@ -35,7 +34,6 @@ "label.logout": "Sair", "label.name": "Nome", "label.new-password": "Nova palavra-passe", - "label.pageview": "Pageview", "label.password": "Palavra-passe", "label.passwords-dont-match": "Palavra-passes não correspondem", "label.profile": "Perfil", @@ -47,11 +45,8 @@ "label.this-year": "Este ano", "label.timezone": "Fuso horário", "label.today": "Hoje", - "label.type": "Type", "label.unknown": "Desconhecido", "label.username": "Nome de utilizador", - "label.visitor": "Visitor", - "label.website": "Website", "label.websites": "Websites", "message.active-users": "{x} {x, plural, one {visitante} other {visitantes}} neste momento", "message.confirm-delete": "Tens a certeza que queres eliminar {target}?", diff --git a/lang/ro-RO.json b/lang/ro-RO.json index 7103759a..7e2f4800 100644 --- a/lang/ro-RO.json +++ b/lang/ro-RO.json @@ -26,7 +26,6 @@ "label.default-date-range": "Interval de date implicit", "label.domain": "Domeniu", "label.enable-share-url": "Activare adresa URL de distribuire", - "label.event": "Event", "label.invalid": "Invalid", "label.invalid-domain": "Invalid domain", "label.last-days": "Ultimele {x} zile", @@ -35,7 +34,6 @@ "label.logout": "Dezautentificare", "label.name": "Nume", "label.new-password": "Parola nouă", - "label.pageview": "Pageview", "label.password": "Parolă", "label.passwords-dont-match": "Parolele nu se potrivesc", "label.profile": "Profil", @@ -47,11 +45,8 @@ "label.this-year": "Acest an", "label.timezone": "Fus orar", "label.today": "Astăzi", - "label.type": "Type", "label.unknown": "Necunoscut", "label.username": "Username", - "label.visitor": "Visitor", - "label.website": "Website", "label.websites": "Site-uri web", "message.active-users": "{x} {x, plural, one {vizitator activ} other {vizitatori activi}}", "message.confirm-delete": "Sunteți sigur că doriți să ștergeți {target}?", diff --git a/lang/ru-RU.json b/lang/ru-RU.json index fb2c4088..8a1ffb1d 100644 --- a/lang/ru-RU.json +++ b/lang/ru-RU.json @@ -26,7 +26,6 @@ "label.default-date-range": "Диапазон дат по-умолчанию", "label.domain": "Домен", "label.enable-share-url": "Разрешить делиться ссылкой", - "label.event": "Event", "label.invalid": "Некорректный", "label.invalid-domain": "Некорректный домен", "label.last-days": "Последние {x} дней", @@ -35,7 +34,6 @@ "label.logout": "Выйти", "label.name": "Имя", "label.new-password": "Новый пароль", - "label.pageview": "Pageview", "label.password": "Пароль", "label.passwords-dont-match": "Пароли не совпадают", "label.profile": "Профиль", @@ -47,11 +45,8 @@ "label.this-year": "Этот год", "label.timezone": "Часовой пояс", "label.today": "Сегодня", - "label.type": "Type", "label.unknown": "Неизвестно", "label.username": "Имя пользователя", - "label.visitor": "Visitor", - "label.website": "Website", "label.websites": "Сайты", "message.active-users": "{x} текущих посетителей", "message.confirm-delete": "Вы уверены, что хотите удалить {target}?", diff --git a/lang/sv-SE.json b/lang/sv-SE.json index 35e61fc7..7153a449 100644 --- a/lang/sv-SE.json +++ b/lang/sv-SE.json @@ -26,7 +26,6 @@ "label.default-date-range": "Standard datum-urval", "label.domain": "Domän", "label.enable-share-url": "Aktivera delnings-URL", - "label.event": "Event", "label.invalid": "Ogiltig", "label.invalid-domain": "Ogiltig domän", "label.last-days": "Senaste {x} dagarna", @@ -35,7 +34,6 @@ "label.logout": "Logga ut", "label.name": "Namn", "label.new-password": "Nytt lösenord", - "label.pageview": "Pageview", "label.password": "Lösenord", "label.passwords-dont-match": "Lösenorden är inte samma", "label.profile": "Profil", @@ -47,11 +45,8 @@ "label.this-year": "Detta år", "label.timezone": "Tidszon", "label.today": "Idag", - "label.type": "Type", "label.unknown": "Okänd", "label.username": "Användarnamn", - "label.visitor": "Visitor", - "label.website": "Website", "label.websites": "Webbsajt", "message.active-users": "{x} {x, plural, one {besökare} other {besökare}} just nu", "message.confirm-delete": "Är du säker på att du vill radera {target}?", diff --git a/lang/tr-TR.json b/lang/tr-TR.json index c16ba8e5..8448303a 100644 --- a/lang/tr-TR.json +++ b/lang/tr-TR.json @@ -26,7 +26,6 @@ "label.default-date-range": "Varsayılan tarih aralığı", "label.domain": "Alan adı", "label.enable-share-url": "Anonim paylaşım URL'i aktif", - "label.event": "Event", "label.invalid": "Geçeriz", "label.invalid-domain": "Geçersiz alan adı", "label.last-days": "Son {x} gün", @@ -35,7 +34,6 @@ "label.logout": "Çıkış Yap", "label.name": "İsim", "label.new-password": "Yeni parola", - "label.pageview": "Pageview", "label.password": "Parola", "label.passwords-dont-match": "Parolalar uyuşmuyor", "label.profile": "Profil", @@ -47,11 +45,8 @@ "label.this-year": "Bu yıl", "label.timezone": "Zaman dilimi", "label.today": "Bugün", - "label.type": "Type", "label.unknown": "Bilinmeyen", "label.username": "Kullanıcı adı", - "label.visitor": "Visitor", - "label.website": "Website", "label.websites": "Web siteleri", "message.active-users": "{x} aktif ziyaretçi", "message.confirm-delete": "{target} kaydını silmek istediğinizden emin misiniz?", diff --git a/lang/uk-UA.json b/lang/uk-UA.json index 7f92162e..ce87cf68 100644 --- a/lang/uk-UA.json +++ b/lang/uk-UA.json @@ -26,7 +26,6 @@ "label.default-date-range": "Діапазон дат за умовчанням", "label.domain": "Домен", "label.enable-share-url": "Дозволити ділитися посиланням", - "label.event": "Event", "label.invalid": "Некоректний", "label.invalid-domain": "Некоректний домен", "label.last-days": "Останні {x} днів", @@ -35,7 +34,6 @@ "label.logout": "Вийти", "label.name": "Ім'я", "label.new-password": "Новий пароль", - "label.pageview": "Pageview", "label.password": "Пароль", "label.passwords-dont-match": "Паролі не співпадають", "label.profile": "Профіль", @@ -47,11 +45,8 @@ "label.this-year": "Поточний рік", "label.timezone": "Часовий пояс", "label.today": "Сьогодні", - "label.type": "Type", "label.unknown": "Невідомо", "label.username": "Ім'я користувача", - "label.visitor": "Visitor", - "label.website": "Website", "label.websites": "Веб-сайти", "message.active-users": "{x} поточних відвідувачів", "message.confirm-delete": "Ви впевнені, що бажаєте видалити {target}?", diff --git a/lang/zh-CN.json b/lang/zh-CN.json index 6f7150d9..5b5653c5 100644 --- a/lang/zh-CN.json +++ b/lang/zh-CN.json @@ -26,7 +26,6 @@ "label.default-date-range": "默认日期范围", "label.domain": "域名", "label.enable-share-url": "激活共享链接", - "label.event": "Event", "label.invalid": "输入无效", "label.invalid-domain": "无效域名", "label.last-days": "最近 {x} 天", @@ -35,7 +34,6 @@ "label.logout": "退出", "label.name": "名字", "label.new-password": "新密码", - "label.pageview": "Pageview", "label.password": "密码", "label.passwords-dont-match": "密码不一致", "label.profile": "个人资料", @@ -47,11 +45,8 @@ "label.this-year": "今年", "label.timezone": "时区", "label.today": "今天", - "label.type": "Type", "label.unknown": "未知", "label.username": "用户名", - "label.visitor": "Visitor", - "label.website": "Website", "label.websites": "网站", "message.active-users": "当前在线 {x} 人", "message.confirm-delete": "你确定要删除{target}吗?", diff --git a/lib/constants.js b/lib/constants.js index 95d78f58..721f5e1c 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -51,6 +51,7 @@ export const EVENT_COLORS = [ '#ffec16', ]; +export const DEFAUL_CHART_HEIGHT = 400; export const DEFAULT_ANIMATION_DURATION = 300; export const DEFAULT_DATE_RANGE = '24hour'; diff --git a/package.json b/package.json index 5d4d077c..6ff5b687 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "0.88.0", + "version": "0.89.0", "description": "A simple, fast, website analytics alternative to Google Analytics. ", "author": "Mike Cao ", "license": "MIT", From 910481e6296c6c536cf878cc36fa4ef64fc8ff11 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 9 Oct 2020 20:37:24 -0700 Subject: [PATCH 13/27] Added grid layout components. --- components/layout/GridLayout.js | 31 +++++++++++ components/layout/GridLayout.module.css | 40 ++++++++++++++ components/metrics/EventsChart.js | 3 +- components/metrics/RealtimeLog.js | 5 +- components/metrics/RealtimeLog.module.css | 9 ++- components/pages/RealtimeDashboard.js | 40 +++++++++++--- components/pages/WebsiteDetails.js | 64 +++++++++++----------- components/pages/WebsiteDetails.module.css | 31 +---------- styles/index.css | 3 + 9 files changed, 154 insertions(+), 72 deletions(-) create mode 100644 components/layout/GridLayout.js create mode 100644 components/layout/GridLayout.module.css diff --git a/components/layout/GridLayout.js b/components/layout/GridLayout.js new file mode 100644 index 00000000..01ec1e8f --- /dev/null +++ b/components/layout/GridLayout.js @@ -0,0 +1,31 @@ +import React from 'react'; +import classNames from 'classnames'; +import styles from './GridLayout.module.css'; + +export default function GridLayout({ className, children }) { + return
{children}
; +} + +export const GridRow = ({ className, children }) => { + return
{children}
; +}; + +export const GridColumn = ({ xs, sm, md, lg, xl, className, children }) => { + const classes = []; + + classes.push(xs ? `col-${xs}` : 'col'); + + if (sm) { + classes.push(`col-sm-${sm}`); + } + if (md) { + classes.push(`col-md-${md}`); + } + if (lg) { + classes.push(`col-lg-${lg}`); + } + if (xl) { + classes.push(`col-lg-${xl}`); + } + return
{children}
; +}; diff --git a/components/layout/GridLayout.module.css b/components/layout/GridLayout.module.css new file mode 100644 index 00000000..f17c195e --- /dev/null +++ b/components/layout/GridLayout.module.css @@ -0,0 +1,40 @@ +.grid { + display: flex; + flex-direction: column; +} + +.col { + display: flex; + flex-direction: column; +} + +.row { + border-top: 1px solid var(--gray300); + min-height: 430px; +} + +.row > .col { + border-left: 1px solid var(--gray300); + padding: 20px; +} + +.row > .col:first-child { + border-left: 0; + padding-left: 0; +} + +.row > .col:last-child { + padding-right: 0; +} + +@media only screen and (max-width: 992px) { + .row { + border: 0; + } + + .row > .col { + border-top: 1px solid var(--gray300); + border-left: 0; + padding: 0; + } +} diff --git a/components/metrics/EventsChart.js b/components/metrics/EventsChart.js index c2804f9e..e401402d 100644 --- a/components/metrics/EventsChart.js +++ b/components/metrics/EventsChart.js @@ -8,7 +8,7 @@ import useTimezone from 'hooks/useTimezone'; import usePageQuery from 'hooks/usePageQuery'; import { EVENT_COLORS } from 'lib/constants'; -export default function EventsChart({ websiteId, token }) { +export default function EventsChart({ websiteId, className, token }) { const [dateRange] = useDateRange(websiteId); const { startDate, endDate, unit, modified } = dateRange; const [timezone] = useTimezone(); @@ -79,6 +79,7 @@ export default function EventsChart({ websiteId, token }) { return ( +
+ +
- + {Row}
diff --git a/components/metrics/RealtimeLog.module.css b/components/metrics/RealtimeLog.module.css index af100708..5fa3d7ae 100644 --- a/components/metrics/RealtimeLog.module.css +++ b/components/metrics/RealtimeLog.module.css @@ -2,13 +2,20 @@ font-size: var(--font-size-xsmall); } +.header { + display: flex; + align-items: center; + justify-content: space-between; + line-height: 40px; + font-weight: 600; +} + .row { display: flex; border-bottom: 1px solid var(--gray300); } .body { - height: 600px; overflow: auto; } diff --git a/components/pages/RealtimeDashboard.js b/components/pages/RealtimeDashboard.js index fd123f8b..2b878f1a 100644 --- a/components/pages/RealtimeDashboard.js +++ b/components/pages/RealtimeDashboard.js @@ -1,12 +1,12 @@ import React, { useState, useEffect, useMemo } from 'react'; -import classNames from 'classnames'; import { subMinutes, startOfMinute } from 'date-fns'; import Page from 'components/layout/Page'; -import useFetch from 'hooks/useFetch'; +import GridLayout, { GridRow, GridColumn } from 'components/layout/GridLayout'; import RealtimeChart from '../metrics/RealtimeChart'; import RealtimeLog from '../metrics/RealtimeLog'; import styles from './RealtimeDashboard.module.css'; import RealtimeHeader from '../metrics/RealtimeHeader'; +import useFetch from 'hooks/useFetch'; const REALTIME_RANGE = 30; const REALTIME_INTERVAL = 5000; @@ -36,10 +36,22 @@ export default function RealtimeDashboard() { const realtimeData = useMemo(() => { if (websiteId) { const { pageviews, sessions, events, ...props } = data; + const countries = sessions.reduce((obj, { country }) => { + if (country) { + if (!obj[country]) { + obj[country] = 1; + } else { + obj[country] += 1; + } + } + return obj; + }, {}); + return { pageviews: filterWebsite(pageviews, websiteId), sessions: filterWebsite(sessions, websiteId), events: filterWebsite(events, websiteId), + countries, ...props, }; } @@ -83,12 +95,24 @@ export default function RealtimeDashboard() { records={REALTIME_RANGE} />
-
-
- -
-
hi.
-
+ + + + + + + x + + + + + x + + + x + + + ); } diff --git a/components/pages/WebsiteDetails.js b/components/pages/WebsiteDetails.js index 23b19f19..7f64b6f6 100644 --- a/components/pages/WebsiteDetails.js +++ b/components/pages/WebsiteDetails.js @@ -4,6 +4,7 @@ import classNames from 'classnames'; import WebsiteChart from 'components/metrics/WebsiteChart'; import WorldMap from 'components/common/WorldMap'; import Page from 'components/layout/Page'; +import GridLayout, { GridRow, GridColumn } from 'components/layout/GridLayout'; import MenuLayout from 'components/layout/MenuLayout'; import Link from 'components/common/Link'; import Loading from 'components/common/Loading'; @@ -19,6 +20,7 @@ import EventsTable from '../metrics/EventsTable'; import EventsChart from '../metrics/EventsChart'; import useFetch from 'hooks/useFetch'; import usePageQuery from 'hooks/usePageQuery'; +import { DEFAULT_ANIMATION_DURATION } from 'lib/constants'; const views = { url: PagesTable, @@ -100,7 +102,7 @@ export default function WebsiteDetails({ websiteId, token }) { function handleDataLoad() { if (!chartLoaded) { - setTimeout(() => setChartLoaded(true), 300); + setTimeout(() => setChartLoaded(true), DEFAULT_ANIMATION_DURATION); } } @@ -124,45 +126,43 @@ export default function WebsiteDetails({ websiteId, token }) {
{!chartLoaded && } {chartLoaded && !view && ( - <> -
-
+ + + -
-
+ + -
-
-
-
+ + + + -
-
+ + -
-
+ + -
-
-
-
+ + + + -
-
+ + -
-
-
0 })} - > -
+ + + 0 })}> + -
-
- -
-
- + + + + + + )} {view && ( [class*='col-'] { - border-left: 1px solid var(--gray300); - padding: 20px; -} - -.row > [class*='col-']:first-child { - border-left: 0; - padding-left: 0; -} - -.row > [class*='col-']:last-child { - padding-right: 0; -} - .hidden { display: none; } -@media only screen and (max-width: 992px) { - .row { - border: 0; - } - - .row > [class*='col-'] { - border-top: 1px solid var(--gray300); - border-left: 0; - padding: 0; - } +.eventschart { + padding: 30px 0; } diff --git a/styles/index.css b/styles/index.css index c0e2ae6d..c695a2cf 100644 --- a/styles/index.css +++ b/styles/index.css @@ -37,6 +37,9 @@ h4, h5, h6 { font-weight: 400; + line-height: 30px; + padding: 0; + margin: 0; } button, From 51955c69ec4b143fc365a9cb740356fa8bf8ec80 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 9 Oct 2020 21:08:30 -0700 Subject: [PATCH 14/27] Added Finnish language. --- lib/lang.js | 5 +++++ package.json | 2 +- public/country/fi-FI.json | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 public/country/fi-FI.json diff --git a/lib/lang.js b/lib/lang.js index 200ac893..4b1841f5 100644 --- a/lib/lang.js +++ b/lib/lang.js @@ -17,6 +17,7 @@ import { nb, id, uk, + fi, } from 'date-fns/locale'; import enMessages from 'lang-compiled/en-US.json'; import nlMessages from 'lang-compiled/nl-NL.json'; @@ -37,6 +38,7 @@ import roMessages from 'lang-compiled/ro-RO.json'; import nbNOMessages from 'lang-compiled/nb-NO.json'; import idMessages from 'lang-compiled/id-ID.json'; import ukMessages from 'lang-compiled/uk-UA.json'; +import fiMessages from 'lang-compiled/fi-FI.json'; export const messages = { 'en-US': enMessages, @@ -58,6 +60,7 @@ export const messages = { 'nb-NO': nbNOMessages, 'id-ID': idMessages, 'uk-UA': ukMessages, + 'fi-FI': fiMessages, }; export const dateLocales = { @@ -80,6 +83,7 @@ export const dateLocales = { 'nb-NO': nb, 'id-ID': id, 'uk-UA': uk, + 'fi-FI': fi, }; export const menuOptions = [ @@ -99,6 +103,7 @@ export const menuOptions = [ { label: 'Português', value: 'pt-PT', display: 'pt' }, { label: 'Русский', value: 'ru-RU', display: 'ru' }, { label: 'Română', value: 'ro-RO', display: 'ro' }, + { label: 'Suomi', value: 'fi-FI', display: 'fi' }, { label: 'Svenska', value: 'sv-SE', display: 'sv' }, { label: 'Türkçe', value: 'tr-TR', display: 'tr' }, { label: 'українська', value: 'uk-UA', display: 'uk' }, diff --git a/package.json b/package.json index 6ff5b687..3e2c5c15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "0.89.0", + "version": "0.90.0", "description": "A simple, fast, website analytics alternative to Google Analytics. ", "author": "Mike Cao ", "license": "MIT", diff --git a/public/country/fi-FI.json b/public/country/fi-FI.json new file mode 100644 index 00000000..6410ee12 --- /dev/null +++ b/public/country/fi-FI.json @@ -0,0 +1 @@ +{"AF":"Afganistan","AX":"Ahvenanmaa","NL":"Alankomaat","AL":"Albania","DZ":"Algeria","AS":"Amerikan Samoa","AD":"Andorra","AO":"Angola","AI":"Anguilla","AQ":"Antarktis","AG":"Antigua ja Barbuda","AE":"Arabiemiirikunnat","AR":"Argentiina","AM":"Armenia","AW":"Aruba","AU":"Australia","AZ":"Azerbaid\u017ean","BS":"Bahama","BH":"Bahrain","BD":"Bangladesh","BB":"Barbados","BE":"Belgia","BZ":"Belize","BJ":"Benin","BM":"Bermuda","BT":"Bhutan","BO":"Bolivia","BA":"Bosnia ja Hertsegovina","BW":"Botswana","BV":"Bouvet\u2019nsaari","BR":"Brasilia","IO":"Brittil\u00e4inen Intian valtameren alue","VG":"Brittil\u00e4iset Neitsytsaaret","BN":"Brunei","BG":"Bulgaria","BF":"Burkina Faso","BI":"Burundi","KY":"Caymansaaret","CL":"Chile","CK":"Cookinsaaret","CR":"Costa Rica","CW":"Cura\u00e7ao","DJ":"Djibouti","DM":"Dominica","DO":"Dominikaaninen tasavalta","EC":"Ecuador","EG":"Egypti","SV":"El Salvador","ER":"Eritrea","ES":"Espanja","ZA":"Etel\u00e4-Afrikka","GS":"Etel\u00e4-Georgia ja Etel\u00e4iset Sandwichsaaret","KR":"Etel\u00e4-Korea","SS":"Etel\u00e4-Sudan","ET":"Etiopia","FK":"Falklandinsaaret","FJ":"Fid\u017ei","PH":"Filippiinit","FO":"F\u00e4rsaaret","GA":"Gabon","GM":"Gambia","GE":"Georgia","GH":"Ghana","GI":"Gibraltar","GD":"Grenada","GL":"Gr\u00f6nlanti","GP":"Guadeloupe","GU":"Guam","GT":"Guatemala","GG":"Guernsey","GN":"Guinea","GW":"Guinea-Bissau","GY":"Guyana","HT":"Haiti","HM":"Heard ja McDonaldinsaaret","HN":"Honduras","HK":"Hongkong \u2013 Kiinan e.h.a.","SJ":"Huippuvuoret ja Jan Mayen","ID":"Indonesia","IN":"Intia","IQ":"Irak","IR":"Iran","IE":"Irlanti","IS":"Islanti","GB":"Iso-Britannia","IL":"Israel","IT":"Italia","TL":"It\u00e4-Timor","AT":"It\u00e4valta","JM":"Jamaika","JP":"Japani","YE":"Jemen","JE":"Jersey","JO":"Jordania","CX":"Joulusaari","KH":"Kambod\u017ea","CM":"Kamerun","CA":"Kanada","CV":"Kap Verde","BQ":"Karibian Alankomaat","KZ":"Kazakstan","KE":"Kenia","CF":"Keski-Afrikan tasavalta","CN":"Kiina","KG":"Kirgisia","KI":"Kiribati","CO":"Kolumbia","KM":"Komorit","CD":"Kongon demokraattinen tasavalta","CG":"Kongon tasavalta","CC":"Kookossaaret (Keelingsaaret)","GR":"Kreikka","HR":"Kroatia","CU":"Kuuba","KW":"Kuwait","CY":"Kypros","LA":"Laos","LV":"Latvia","LS":"Lesotho","LB":"Libanon","LR":"Liberia","LY":"Libya","LI":"Liechtenstein","LT":"Liettua","LU":"Luxemburg","EH":"L\u00e4nsi-Sahara","MO":"Macao \u2013 Kiinan e.h.a.","MG":"Madagaskar","MW":"Malawi","MV":"Malediivit","MY":"Malesia","ML":"Mali","MT":"Malta","IM":"Mansaari","MA":"Marokko","MH":"Marshallinsaaret","MQ":"Martinique","MR":"Mauritania","MU":"Mauritius","YT":"Mayotte","MX":"Meksiko","FM":"Mikronesian liittovaltio","MD":"Moldova","MC":"Monaco","MN":"Mongolia","ME":"Montenegro","MS":"Montserrat","MZ":"Mosambik","MM":"Myanmar (Burma)","NA":"Namibia","NR":"Nauru","NP":"Nepal","NI":"Nicaragua","NE":"Niger","NG":"Nigeria","NU":"Niue","NF":"Norfolkinsaari","NO":"Norja","CI":"Norsunluurannikko","OM":"Oman","PK":"Pakistan","PW":"Palau","PS":"Palestiinalaisalueet","PA":"Panama","PG":"Papua-Uusi-Guinea","PY":"Paraguay","PE":"Peru","PN":"Pitcairn","KP":"Pohjois-Korea","MK":"Pohjois-Makedonia","MP":"Pohjois-Mariaanit","PT":"Portugali","PR":"Puerto Rico","PL":"Puola","GQ":"P\u00e4iv\u00e4ntasaajan Guinea","QA":"Qatar","FR":"Ranska","TF":"Ranskan etel\u00e4iset alueet","GF":"Ranskan Guayana","PF":"Ranskan Polynesia","RE":"R\u00e9union","RO":"Romania","RW":"Ruanda","SE":"Ruotsi","SH":"Saint Helena","KN":"Saint Kitts ja Nevis","LC":"Saint Lucia","VC":"Saint Vincent ja Grenadiinit","BL":"Saint-Barth\u00e9lemy","MF":"Saint-Martin","PM":"Saint-Pierre ja Miquelon","DE":"Saksa","SB":"Salomonsaaret","ZM":"Sambia","WS":"Samoa","SM":"San Marino","ST":"S\u00e3o Tom\u00e9 ja Pr\u00edncipe","SA":"Saudi-Arabia","SN":"Senegal","RS":"Serbia","SC":"Seychellit","SL":"Sierra Leone","SG":"Singapore","SX":"Sint Maarten","SK":"Slovakia","SI":"Slovenia","SO":"Somalia","LK":"Sri Lanka","SD":"Sudan","FI":"Suomi","SR":"Suriname","CH":"Sveitsi","SZ":"Swazimaa","SY":"Syyria","TJ":"Tad\u017eikistan","TW":"Taiwan","TZ":"Tansania","DK":"Tanska","TH":"Thaimaa","TG":"Togo","TK":"Tokelau","TO":"Tonga","TT":"Trinidad ja Tobago","TD":"T\u0161ad","CZ":"T\u0161ekki","TN":"Tunisia","TR":"Turkki","TM":"Turkmenistan","TC":"Turks- ja Caicossaaret","TV":"Tuvalu","UG":"Uganda","UA":"Ukraina","HU":"Unkari","UY":"Uruguay","NC":"Uusi-Kaledonia","NZ":"Uusi-Seelanti","UZ":"Uzbekistan","BY":"Valko-Ven\u00e4j\u00e4","VU":"Vanuatu","VA":"Vatikaani","VE":"Venezuela","RU":"Ven\u00e4j\u00e4","VN":"Vietnam","EE":"Viro","WF":"Wallis ja Futuna","US":"Yhdysvallat","UM":"Yhdysvaltain erillissaaret","VI":"Yhdysvaltain Neitsytsaaret","ZW":"Zimbabwe"} \ No newline at end of file From 8912daa2fad940ec531d19ddbd13d1cba046ee32 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 10 Oct 2020 00:07:08 -0700 Subject: [PATCH 15/27] Refactored data tables. Added realtime tables. --- components/layout/GridLayout.js | 2 +- components/metrics/DataTable.js | 91 +++++++++++++++ components/metrics/DataTable.module.css | 96 ++++++++++++++++ components/metrics/MetricsBar.js | 2 - components/metrics/MetricsTable.js | 118 +++++-------------- components/metrics/MetricsTable.module.css | 89 --------------- components/metrics/RealtimeHeader.js | 14 +-- components/pages/RealtimeDashboard.js | 127 ++++++++++++++++----- lang/da-DK.json | 1 + lang/de-DE.json | 1 + lang/el-GR.json | 1 + lang/en-US.json | 1 + lang/es-MX.json | 1 + lang/fi-FI.json | 1 + lang/fo-FO.json | 1 + lang/fr-FR.json | 1 + lang/id-ID.json | 1 + lang/ja-JP.json | 1 + lang/mn-MN.json | 1 + lang/nb-NO.json | 1 + lang/nl-NL.json | 1 + lang/pt-PT.json | 1 + lang/ro-RO.json | 1 + lang/ru-RU.json | 1 + lang/sv-SE.json | 1 + lang/tr-TR.json | 1 + lang/uk-UA.json | 1 + lang/zh-CN.json | 1 + package.json | 2 +- 29 files changed, 335 insertions(+), 226 deletions(-) create mode 100644 components/metrics/DataTable.js create mode 100644 components/metrics/DataTable.module.css diff --git a/components/layout/GridLayout.js b/components/layout/GridLayout.js index 01ec1e8f..1d3170d6 100644 --- a/components/layout/GridLayout.js +++ b/components/layout/GridLayout.js @@ -13,7 +13,7 @@ export const GridRow = ({ className, children }) => { export const GridColumn = ({ xs, sm, md, lg, xl, className, children }) => { const classes = []; - classes.push(xs ? `col-${xs}` : 'col'); + classes.push(xs ? `col-${xs}` : 'col-12'); if (sm) { classes.push(`col-sm-${sm}`); diff --git a/components/metrics/DataTable.js b/components/metrics/DataTable.js new file mode 100644 index 00000000..9e12d887 --- /dev/null +++ b/components/metrics/DataTable.js @@ -0,0 +1,91 @@ +import React, { useState } from 'react'; +import { FixedSizeList } from 'react-window'; +import { useSpring, animated, config } from 'react-spring'; +import classNames from 'classnames'; +import NoData from 'components/common/NoData'; +import { formatNumber, formatLongNumber } from 'lib/format'; +import styles from './DataTable.module.css'; + +export default function DataTable({ + data, + title, + metric, + className, + limit, + renderLabel, + height = 400, + animate = true, +}) { + const [format, setFormat] = useState(true); + const formatFunc = format ? formatLongNumber : formatNumber; + + const handleSetFormat = () => setFormat(state => !state); + + const getRow = row => { + const { x: label, y: value, z: percent } = row; + + return ( + + ); + }; + + const Row = ({ index, style }) => { + return
{getRow(data[index])}
; + }; + + return ( +
+
+
{title}
+
+ {metric} +
+
+
+ {data?.length === 0 && } + {limit + ? data.map(row => getRow(row)) + : data.length > 0 && ( + + {Row} + + )} +
+
+ ); +} + +const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) => { + const props = useSpring({ + width: percent, + y: value, + from: { width: 0, y: 0 }, + config: animate ? config.default : { duration: 0 }, + }); + + return ( +
+
{label}
+
+ {props.y?.interpolate(format)} +
+
+ `${n}%`) }} + /> + + {props.width.interpolate(n => `${n.toFixed(0)}%`)} + +
+
+ ); +}; diff --git a/components/metrics/DataTable.module.css b/components/metrics/DataTable.module.css new file mode 100644 index 00000000..b276e361 --- /dev/null +++ b/components/metrics/DataTable.module.css @@ -0,0 +1,96 @@ +.table { + position: relative; + font-size: var(--font-size-small); + display: flex; + flex-direction: column; + flex: 1; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + line-height: 40px; +} + +.title { + display: flex; + font-weight: 600; + font-size: var(--font-size-normal); +} + +.metric { + font-size: var(--font-size-small); + font-weight: 600; + text-align: center; + width: 100px; + cursor: pointer; +} + +.row { + position: relative; + height: 30px; + line-height: 30px; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 5px; + overflow: hidden; +} + +.label { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + flex: 2; +} + +.label a { + color: inherit; + text-decoration: none; +} + +.label a:hover { + color: var(--primary400); +} + +.label:empty { + color: #b3b3b3; +} + +.label:empty:before { + content: 'Unknown'; +} + +.value { + width: 50px; + text-align: right; + margin-right: 10px; + font-weight: 600; + cursor: pointer; +} + +.percent { + position: relative; + width: 50px; + color: var(--gray600); + border-left: 1px solid var(--gray600); + padding-left: 10px; + z-index: 1; +} + +.bar { + position: absolute; + top: 0; + left: 0; + height: 30px; + opacity: 0.1; + background: var(--primary400); + z-index: -1; +} + +.body { + position: relative; + flex: 1; + overflow: hidden; +} diff --git a/components/metrics/MetricsBar.js b/components/metrics/MetricsBar.js index 9fbc3054..3b159327 100644 --- a/components/metrics/MetricsBar.js +++ b/components/metrics/MetricsBar.js @@ -18,8 +18,6 @@ export default function MetricsBar({ websiteId, token, className }) { query: { url }, } = usePageQuery(); - console.log({ modified }); - const { data, error, loading } = useFetch( `/api/website/${websiteId}/stats`, { diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js index 2f508295..d4b246b3 100644 --- a/components/metrics/MetricsTable.js +++ b/components/metrics/MetricsTable.js @@ -1,19 +1,17 @@ -import React, { useState, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { FormattedMessage } from 'react-intl'; -import { FixedSizeList } from 'react-window'; -import { useSpring, animated, config } from 'react-spring'; import classNames from 'classnames'; import Link from 'components/common/Link'; import Loading from 'components/common/Loading'; -import NoData from 'components/common/NoData'; import useFetch from 'hooks/useFetch'; import Arrow from 'assets/arrow-right.svg'; import { percentFilter } from 'lib/filters'; -import { formatNumber, formatLongNumber } from 'lib/format'; import useDateRange from 'hooks/useDateRange'; import usePageQuery from 'hooks/usePageQuery'; +import ErrorMessage from 'components/common/ErrorMessage'; +import DataTable from './DataTable'; +import { DEFAULT_ANIMATION_DURATION } from 'lib/constants'; import styles from './MetricsTable.module.css'; -import ErrorMessage from '../common/ErrorMessage'; export default function MetricsTable({ websiteId, @@ -49,15 +47,12 @@ export default function MetricsTable({ token, }, onDataLoad, - delay: 300, + delay: DEFAULT_ANIMATION_DURATION, }, [modified], ); - const [format, setFormat] = useState(true); - const formatFunc = format ? formatLongNumber : formatNumber; - const shouldAnimate = limit > 0; - const rankings = useMemo(() => { + const filteredData = useMemo(() => { if (data) { const items = percentFilter(dataFilter ? dataFilter(data, filterOptions) : data); if (limit) { @@ -68,91 +63,34 @@ export default function MetricsTable({ return []; }, [data, error, dataFilter, filterOptions]); - const handleSetFormat = () => setFormat(state => !state); - - const getRow = row => { - const { x: label, y: value, z: percent } = row; - return ( - - ); - }; - - const Row = ({ index, style }) => { - return
{getRow(rankings[index])}
; - }; - return (
{!data && loading && } {error && } {data && !error && ( - <> -
-
{title}
-
- {metric} -
-
-
- {rankings?.length === 0 && } - {limit - ? rankings.map(row => getRow(row)) - : rankings.length > 0 && ( - - {Row} - - )} -
-
- {limit && ( - } - href={router.pathname} - as={resolve({ view: type })} - size="small" - iconRight - > - - - )} -
- + 0} + /> )} +
+ {limit && ( + } + href={router.pathname} + as={resolve({ view: type })} + size="small" + iconRight + > + + + )} +
); } - -const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) => { - const props = useSpring({ - width: percent, - y: value, - from: { width: 0, y: 0 }, - config: animate ? config.default : { duration: 0 }, - }); - - return ( -
-
{label}
-
- {props.y?.interpolate(format)} -
-
- `${n}%`) }} - /> - - {props.width.interpolate(n => `${n.toFixed(0)}%`)} - -
-
- ); -}; diff --git a/components/metrics/MetricsTable.module.css b/components/metrics/MetricsTable.module.css index bbba0009..e93f536e 100644 --- a/components/metrics/MetricsTable.module.css +++ b/components/metrics/MetricsTable.module.css @@ -6,95 +6,6 @@ flex-direction: column; } -.header { - display: flex; - align-items: center; - justify-content: space-between; - line-height: 40px; -} - -.title { - display: flex; - font-weight: 600; - font-size: var(--font-size-normal); -} - -.metric { - font-size: var(--font-size-small); - font-weight: 600; - text-align: center; - width: 100px; - cursor: pointer; -} - -.row { - position: relative; - height: 30px; - line-height: 30px; - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 5px; - overflow: hidden; -} - -.label { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - flex: 2; -} - -.label a { - color: inherit; - text-decoration: none; -} - -.label a:hover { - color: var(--primary400); -} - -.label:empty { - color: #b3b3b3; -} - -.label:empty:before { - content: 'Unknown'; -} - -.value { - width: 50px; - text-align: right; - margin-right: 10px; - font-weight: 600; - cursor: pointer; -} - -.percent { - position: relative; - width: 50px; - color: var(--gray600); - border-left: 1px solid var(--gray600); - padding-left: 10px; - z-index: 1; -} - -.bar { - position: absolute; - top: 0; - left: 0; - height: 30px; - opacity: 0.1; - background: var(--primary400); - z-index: -1; -} - -.body { - position: relative; - flex: 1; - overflow: hidden; -} - .footer { display: flex; justify-content: center; diff --git a/components/metrics/RealtimeHeader.js b/components/metrics/RealtimeHeader.js index 09f83ff6..deea9387 100644 --- a/components/metrics/RealtimeHeader.js +++ b/components/metrics/RealtimeHeader.js @@ -10,17 +10,7 @@ export default function RealtimeHeader({ websites, data, websiteId, onSelect }) { label: , value: 0 }, ].concat(websites.map(({ name, website_id }) => ({ label: name, value: website_id }))); - const { pageviews, sessions, events } = data; - const countries = sessions.reduce((obj, { country }) => { - if (country) { - if (!obj[country]) { - obj[country] = 1; - } else { - obj[country] += 1; - } - } - return obj; - }, {}); + const { pageviews, sessions, events, countries } = data; return ( <> @@ -45,7 +35,7 @@ export default function RealtimeHeader({ websites, data, websiteId, onSelect }) /> } - value={Object.keys(countries).length} + value={countries.length} />
diff --git a/components/pages/RealtimeDashboard.js b/components/pages/RealtimeDashboard.js index 2b878f1a..5c8f052a 100644 --- a/components/pages/RealtimeDashboard.js +++ b/components/pages/RealtimeDashboard.js @@ -1,5 +1,7 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { subMinutes, startOfMinute } from 'date-fns'; +import firstBy from 'thenby'; +import { percentFilter } from 'lib/filters'; import Page from 'components/layout/Page'; import GridLayout, { GridRow, GridColumn } from 'components/layout/GridLayout'; import RealtimeChart from '../metrics/RealtimeChart'; @@ -7,6 +9,11 @@ import RealtimeLog from '../metrics/RealtimeLog'; import styles from './RealtimeDashboard.module.css'; import RealtimeHeader from '../metrics/RealtimeHeader'; import useFetch from 'hooks/useFetch'; +import WorldMap from '../common/WorldMap'; +import DataTable from '../metrics/DataTable'; +import useLocale from 'hooks/useLocale'; +import useCountryNames from 'hooks/useCountryNames'; +import { FormattedMessage } from 'react-intl'; const REALTIME_RANGE = 30; const REALTIME_INTERVAL = 5000; @@ -23,6 +30,8 @@ function filterWebsite(data, id) { } export default function RealtimeDashboard() { + const [locale] = useLocale(); + const countryNames = useCountryNames(locale); const [data, setData] = useState(); const [websiteId, setWebsiteId] = useState(0); const { data: init, loading } = useFetch('/api/realtime', { params: { type: 'init' } }); @@ -33,41 +42,84 @@ export default function RealtimeDashboard() { headers: { 'x-umami-token': init?.token }, }); - const realtimeData = useMemo(() => { - if (websiteId) { - const { pageviews, sessions, events, ...props } = data; - const countries = sessions.reduce((obj, { country }) => { - if (country) { - if (!obj[country]) { - obj[country] = 1; - } else { - obj[country] += 1; - } - } - return obj; - }, {}); + const renderCountryName = useCallback(({ x }) => countryNames[x], []); - return { - pageviews: filterWebsite(pageviews, websiteId), - sessions: filterWebsite(sessions, websiteId), - events: filterWebsite(events, websiteId), - countries, - ...props, - }; + const realtimeData = useMemo(() => { + if (data) { + const { pageviews, sessions, events } = data; + + if (websiteId) { + return { + pageviews: filterWebsite(pageviews, websiteId), + sessions: filterWebsite(sessions, websiteId), + events: filterWebsite(events, websiteId), + }; + } } + return data; }, [data, websiteId]); + const countries = useMemo(() => { + if (realtimeData?.sessions) { + return percentFilter( + realtimeData.sessions + .reduce((arr, { country }) => { + if (country) { + const row = arr.find(({ x }) => x === country); + + if (!row) { + arr.push({ x: country, y: 1 }); + } else { + row.y += 1; + } + } + return arr; + }, []) + .sort(firstBy('y', -1)), + ); + } + return []; + }, [realtimeData]); + + const referrers = useMemo(() => { + if (realtimeData?.pageviews) { + return percentFilter( + realtimeData.pageviews + .reduce((arr, { referrer }) => { + if (referrer?.startsWith('http')) { + const { hostname } = new URL(referrer); + const row = arr.find(({ x }) => x === hostname); + + if (!row) { + arr.push({ x: hostname, y: 1 }); + } else { + row.y += 1; + } + } + return arr; + }, []) + .sort(firstBy('y', -1)), + ); + } + return []; + }, [realtimeData]); + useEffect(() => { if (init && !data) { - setData(init.data); + const { websites, data } = init; + const domains = init.websites.map(({ domain }) => domain); + + setData({ websites, domains, ...data }); } else if (updates) { const { pageviews, sessions, events, timestamp } = updates; - const minTime = subMinutes(startOfMinute(new Date()), REALTIME_RANGE).getTime(); + const time = subMinutes(startOfMinute(new Date()), REALTIME_RANGE).getTime(); + setData(state => ({ - pageviews: mergeData(state.pageviews, pageviews, minTime), - sessions: mergeData(state.sessions, sessions, minTime), - events: mergeData(state.events, events, minTime), + ...state, + pageviews: mergeData(state.pageviews, pageviews, time), + sessions: mergeData(state.sessions, sessions, time), + events: mergeData(state.events, events, time), timestamp, })); } @@ -77,14 +129,16 @@ export default function RealtimeDashboard() { return null; } - const { websites } = init; + const { websites } = data; + + //console.log({realtimeData, countries}); return (
@@ -101,15 +155,26 @@ export default function RealtimeDashboard() { - x + } + metric={} + data={referrers} + animate={false} + /> - x + } + metric={} + data={countries} + renderLabel={renderCountryName} + animate={false} + /> - x + diff --git a/lang/da-DK.json b/lang/da-DK.json index 5532bf69..b66b513d 100644 --- a/lang/da-DK.json +++ b/lang/da-DK.json @@ -38,6 +38,7 @@ "label.passwords-dont-match": "Adgangskoder matcher ikke", "label.profile": "Profil", "label.realtime": "Realtime", + "label.realtime-logs": "Realtime logs", "label.required": "Påkrævet", "label.settings": "Indstillinger", "label.this-month": "Denne måned", diff --git a/lang/de-DE.json b/lang/de-DE.json index df5e6dd1..4447f98b 100644 --- a/lang/de-DE.json +++ b/lang/de-DE.json @@ -38,6 +38,7 @@ "label.passwords-dont-match": "Passwörter stimmen nicht überein", "label.profile": "Profil", "label.realtime": "Realtime", + "label.realtime-logs": "Realtime logs", "label.required": "Erforderlich", "label.settings": "Einstellungen", "label.this-month": "Diesen Monat", diff --git a/lang/el-GR.json b/lang/el-GR.json index 7fbdbfbf..978de681 100644 --- a/lang/el-GR.json +++ b/lang/el-GR.json @@ -38,6 +38,7 @@ "label.passwords-dont-match": "Οι κωδικοί πρόσβασης δεν ταιριάζουν", "label.profile": "Προφίλ", "label.realtime": "Realtime", + "label.realtime-logs": "Realtime logs", "label.required": "Απαιτείται", "label.settings": "Ρυθμίσεις", "label.this-month": "Αυτο το μήνα", diff --git a/lang/en-US.json b/lang/en-US.json index 07670232..12231157 100644 --- a/lang/en-US.json +++ b/lang/en-US.json @@ -38,6 +38,7 @@ "label.passwords-dont-match": "Passwords don't match", "label.profile": "Profile", "label.realtime": "Realtime", + "label.realtime-logs": "Realtime logs", "label.required": "Required", "label.settings": "Settings", "label.this-month": "This month", diff --git a/lang/es-MX.json b/lang/es-MX.json index 61395fcf..eaf92db6 100644 --- a/lang/es-MX.json +++ b/lang/es-MX.json @@ -38,6 +38,7 @@ "label.passwords-dont-match": "Las contraseñas no coinciden", "label.profile": "Perfil", "label.realtime": "Realtime", + "label.realtime-logs": "Realtime logs", "label.required": "Requerido", "label.settings": "Configuraciones", "label.this-month": "Este mes", diff --git a/lang/fi-FI.json b/lang/fi-FI.json index 9f0c930b..fa808980 100644 --- a/lang/fi-FI.json +++ b/lang/fi-FI.json @@ -38,6 +38,7 @@ "label.passwords-dont-match": "Salasanat eivät täsmää", "label.profile": "Profiili", "label.realtime": "Realtime", + "label.realtime-logs": "Realtime logs", "label.required": "Vaaditaan", "label.settings": "Asetukset", "label.this-month": "Tämä kuukausi", diff --git a/lang/fo-FO.json b/lang/fo-FO.json index ee5ab8cb..8cedbcdb 100644 --- a/lang/fo-FO.json +++ b/lang/fo-FO.json @@ -38,6 +38,7 @@ "label.passwords-dont-match": "Loyniorðini eru ikki eins", "label.profile": "Brúkari", "label.realtime": "Realtime", + "label.realtime-logs": "Realtime logs", "label.required": "Krav", "label.settings": "Stillingar", "label.this-month": "Hendan mánan", diff --git a/lang/fr-FR.json b/lang/fr-FR.json index cb2cc103..ff00653d 100644 --- a/lang/fr-FR.json +++ b/lang/fr-FR.json @@ -38,6 +38,7 @@ "label.passwords-dont-match": "Les mots de passe ne correspondent pas", "label.profile": "Profile", "label.realtime": "Realtime", + "label.realtime-logs": "Realtime logs", "label.required": "Requis", "label.settings": "Paramètres", "label.this-month": "Ce mois ci", diff --git a/lang/id-ID.json b/lang/id-ID.json index 7c7db298..9c05cf80 100644 --- a/lang/id-ID.json +++ b/lang/id-ID.json @@ -38,6 +38,7 @@ "label.passwords-dont-match": "Kata sandi tidak cocok", "label.profile": "Profil", "label.realtime": "Realtime", + "label.realtime-logs": "Realtime logs", "label.required": "Wajib", "label.settings": "Pengaturan", "label.this-month": "Bulan ini", diff --git a/lang/ja-JP.json b/lang/ja-JP.json index 09458224..3a019f27 100644 --- a/lang/ja-JP.json +++ b/lang/ja-JP.json @@ -38,6 +38,7 @@ "label.passwords-dont-match": "パスワードが一致しません", "label.profile": "プロファイル", "label.realtime": "Realtime", + "label.realtime-logs": "Realtime logs", "label.required": "必須", "label.settings": "設定", "label.this-month": "今月", diff --git a/lang/mn-MN.json b/lang/mn-MN.json index 5e92c9c7..6a9a0c0b 100644 --- a/lang/mn-MN.json +++ b/lang/mn-MN.json @@ -38,6 +38,7 @@ "label.passwords-dont-match": "Нууц үг тохирохгүй байна", "label.profile": "Бүртгэл", "label.realtime": "Realtime", + "label.realtime-logs": "Realtime logs", "label.required": "Шаардлагатай", "label.settings": "Тохиргоо", "label.this-month": "Энэ сар", diff --git a/lang/nb-NO.json b/lang/nb-NO.json index 0af11471..0988f361 100644 --- a/lang/nb-NO.json +++ b/lang/nb-NO.json @@ -38,6 +38,7 @@ "label.passwords-dont-match": "Passordene er ikke like", "label.profile": "Profil", "label.realtime": "Realtime", + "label.realtime-logs": "Realtime logs", "label.required": "Påkrevd", "label.settings": "Innstillinger", "label.this-month": "Denne måneden", diff --git a/lang/nl-NL.json b/lang/nl-NL.json index 4613ed33..f36289cc 100644 --- a/lang/nl-NL.json +++ b/lang/nl-NL.json @@ -38,6 +38,7 @@ "label.passwords-dont-match": "Wachtwoorden komen niet overeen", "label.profile": "Profiel", "label.realtime": "Realtime", + "label.realtime-logs": "Realtime logs", "label.required": "Verplicht", "label.settings": "Instellingen", "label.this-month": "Deze maand", diff --git a/lang/pt-PT.json b/lang/pt-PT.json index 81e3a334..d1f7ee3c 100644 --- a/lang/pt-PT.json +++ b/lang/pt-PT.json @@ -38,6 +38,7 @@ "label.passwords-dont-match": "Palavra-passes não correspondem", "label.profile": "Perfil", "label.realtime": "Realtime", + "label.realtime-logs": "Realtime logs", "label.required": "Obrigatório", "label.settings": "Definições", "label.this-month": "Este mês", diff --git a/lang/ro-RO.json b/lang/ro-RO.json index 7e2f4800..3fe2fb92 100644 --- a/lang/ro-RO.json +++ b/lang/ro-RO.json @@ -38,6 +38,7 @@ "label.passwords-dont-match": "Parolele nu se potrivesc", "label.profile": "Profil", "label.realtime": "Realtime", + "label.realtime-logs": "Realtime logs", "label.required": "Obligatoriu", "label.settings": "Setări", "label.this-month": "Această lună", diff --git a/lang/ru-RU.json b/lang/ru-RU.json index 8a1ffb1d..4b0ff88b 100644 --- a/lang/ru-RU.json +++ b/lang/ru-RU.json @@ -38,6 +38,7 @@ "label.passwords-dont-match": "Пароли не совпадают", "label.profile": "Профиль", "label.realtime": "Realtime", + "label.realtime-logs": "Realtime logs", "label.required": "Обязательное", "label.settings": "Настройки", "label.this-month": "Этот месяц", diff --git a/lang/sv-SE.json b/lang/sv-SE.json index 7153a449..6133383b 100644 --- a/lang/sv-SE.json +++ b/lang/sv-SE.json @@ -38,6 +38,7 @@ "label.passwords-dont-match": "Lösenorden är inte samma", "label.profile": "Profil", "label.realtime": "Realtime", + "label.realtime-logs": "Realtime logs", "label.required": "Krävs", "label.settings": "Inställningar", "label.this-month": "Denna månad", diff --git a/lang/tr-TR.json b/lang/tr-TR.json index 8448303a..1aff9d2e 100644 --- a/lang/tr-TR.json +++ b/lang/tr-TR.json @@ -38,6 +38,7 @@ "label.passwords-dont-match": "Parolalar uyuşmuyor", "label.profile": "Profil", "label.realtime": "Realtime", + "label.realtime-logs": "Realtime logs", "label.required": "Zorunlu alan", "label.settings": "Ayarlar", "label.this-month": "Bu ay", diff --git a/lang/uk-UA.json b/lang/uk-UA.json index ce87cf68..27d04279 100644 --- a/lang/uk-UA.json +++ b/lang/uk-UA.json @@ -38,6 +38,7 @@ "label.passwords-dont-match": "Паролі не співпадають", "label.profile": "Профіль", "label.realtime": "Realtime", + "label.realtime-logs": "Realtime logs", "label.required": "Обов'язкове", "label.settings": "Налаштування", "label.this-month": "Поточний місяць", diff --git a/lang/zh-CN.json b/lang/zh-CN.json index 5b5653c5..18a82ac4 100644 --- a/lang/zh-CN.json +++ b/lang/zh-CN.json @@ -38,6 +38,7 @@ "label.passwords-dont-match": "密码不一致", "label.profile": "个人资料", "label.realtime": "Realtime", + "label.realtime-logs": "Realtime logs", "label.required": "必填", "label.settings": "设置", "label.this-month": "本月", diff --git a/package.json b/package.json index 3e2c5c15..1398c724 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "0.90.0", + "version": "0.91.0", "description": "A simple, fast, website analytics alternative to Google Analytics. ", "author": "Mike Cao ", "license": "MIT", From a039f405b6f8de2be10c714fbbed7f4d67076ec6 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 10 Oct 2020 01:16:28 -0700 Subject: [PATCH 16/27] Refactored realtime components. --- components/metrics/DataTable.module.css | 11 +++-- components/metrics/MetricsTable.js | 2 + components/metrics/RealtimeLog.js | 49 ++++++----------------- components/metrics/RealtimeLog.module.css | 13 +++++- components/pages/RealtimeDashboard.js | 10 +++-- components/pages/WebsiteDetails.js | 2 +- package.json | 3 +- yarn.lock | 12 ++++++ 8 files changed, 51 insertions(+), 51 deletions(-) diff --git a/components/metrics/DataTable.module.css b/components/metrics/DataTable.module.css index b276e361..a1dc61fc 100644 --- a/components/metrics/DataTable.module.css +++ b/components/metrics/DataTable.module.css @@ -6,6 +6,11 @@ flex: 1; } +.body { + position: relative; + flex: 1; +} + .header { display: flex; align-items: center; @@ -88,9 +93,3 @@ background: var(--primary400); z-index: -1; } - -.body { - position: relative; - flex: 1; - overflow: hidden; -} diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js index d4b246b3..19cd5b95 100644 --- a/components/metrics/MetricsTable.js +++ b/components/metrics/MetricsTable.js @@ -25,6 +25,7 @@ export default function MetricsTable({ filterOptions, limit, renderLabel, + height, onDataLoad = () => {}, }) { const [dateRange] = useDateRange(websiteId); @@ -75,6 +76,7 @@ export default function MetricsTable({ className={className} renderLabel={renderLabel} limit={limit} + height={height} animate={limit > 0} /> )} diff --git a/components/metrics/RealtimeLog.js b/components/metrics/RealtimeLog.js index 9bd36d91..426bd3ce 100644 --- a/components/metrics/RealtimeLog.js +++ b/components/metrics/RealtimeLog.js @@ -1,11 +1,9 @@ import React, { useMemo } from 'react'; import { FormattedMessage } from 'react-intl'; import { FixedSizeList } from 'react-window'; -import classNames from 'classnames'; import firstBy from 'thenby'; import { format } from 'date-fns'; import Icon from 'components/common/Icon'; -import Table, { TableRow } from 'components/common/Table'; import Tag from 'components/common/Tag'; import useLocale from 'hooks/useLocale'; import useCountryNames from 'hooks/useCountryNames'; @@ -33,29 +31,6 @@ export default function RealtimeLog({ data, websites }) { return [...pageviews, ...sessions, ...events].sort(firstBy('created_at', -1)); }, [data]); - const columns = [ - { - key: 'time', - className: classNames(styles.time, 'col-3 col-lg-1'), - render: ({ created_at }) => format(new Date(created_at), 'h:mm:ss'), - }, - { - key: 'website', - className: classNames(styles.website, 'col-9 col-lg-2'), - render: getWebsite, - }, - { - key: 'detail', - className: classNames(styles.detail, 'col-12 col-lg-9'), - render: row => ( - <> - - {getDetail(row)} - - ), - }, - ]; - function getType({ view_id, session_id, event_id }) { if (event_id) { return TYPE_EVENT; @@ -110,29 +85,29 @@ export default function RealtimeLog({ data, websites }) { } const Row = ({ index, style }) => { + const row = logs[index]; return ( -
- +
+
{format(new Date(row.created_at), 'h:mm:ss')}
+
{getWebsite(row)}
+
+ + {getDetail(row)} +
); }; return ( -
+
- - +
+ {Row} -
+
); } diff --git a/components/metrics/RealtimeLog.module.css b/components/metrics/RealtimeLog.module.css index 5fa3d7ae..803cfe0d 100644 --- a/components/metrics/RealtimeLog.module.css +++ b/components/metrics/RealtimeLog.module.css @@ -6,12 +6,15 @@ display: flex; align-items: center; justify-content: space-between; + font-size: 16px; line-height: 40px; font-weight: 600; } .row { display: flex; + align-items: center; + height: 40px; border-bottom: 1px solid var(--gray300); } @@ -20,17 +23,23 @@ } .icon { - align-self: center; - margin-right: 20px; + margin-right: 10px; +} + +.time { + min-width: 60px; + overflow: hidden; } .website { + min-width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } .detail { + display: flex; flex: 1; } diff --git a/components/pages/RealtimeDashboard.js b/components/pages/RealtimeDashboard.js index 5c8f052a..30bf1ee4 100644 --- a/components/pages/RealtimeDashboard.js +++ b/components/pages/RealtimeDashboard.js @@ -16,7 +16,7 @@ import useCountryNames from 'hooks/useCountryNames'; import { FormattedMessage } from 'react-intl'; const REALTIME_RANGE = 30; -const REALTIME_INTERVAL = 5000; +const REALTIME_INTERVAL = 55000; function mergeData(state, data, time) { const ids = state.map(({ __id }) => __id); @@ -151,17 +151,18 @@ export default function RealtimeDashboard() {
- - - } metric={} data={referrers} + height={400} animate={false} /> + + + @@ -170,6 +171,7 @@ export default function RealtimeDashboard() { metric={} data={countries} renderLabel={renderCountryName} + height={500} animate={false} /> diff --git a/components/pages/WebsiteDetails.js b/components/pages/WebsiteDetails.js index 7f64b6f6..39b634e5 100644 --- a/components/pages/WebsiteDetails.js +++ b/components/pages/WebsiteDetails.js @@ -171,7 +171,7 @@ export default function WebsiteDetails({ websiteId, token }) { contentClassName={styles.content} menu={menuOptions} > - + )} diff --git a/package.json b/package.json index 1398c724..4347eaa4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "0.91.0", + "version": "0.92.0", "description": "A simple, fast, website analytics alternative to Google Analytics. ", "author": "Mike Cao ", "license": "MIT", @@ -83,6 +83,7 @@ "react-simple-maps": "^2.1.2", "react-spring": "^8.0.27", "react-tooltip": "^4.2.10", + "react-use-measure": "^2.0.2", "react-window": "^1.8.5", "redux": "^4.0.5", "redux-thunk": "^2.3.0", diff --git a/yarn.lock b/yarn.lock index df03a2ae..b8d1e81c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3228,6 +3228,11 @@ date-fns@^2.16.1: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.16.1.tgz#05775792c3f3331da812af253e1a935851d3834b" integrity sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ== +debounce@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131" + integrity sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg== + debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: version "4.2.0" resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" @@ -7337,6 +7342,13 @@ react-tooltip@^4.2.10: prop-types "^15.7.2" uuid "^7.0.3" +react-use-measure@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/react-use-measure/-/react-use-measure-2.0.2.tgz#06b4f99b887d5dfcd7b7167a2da063d97ec8f62f" + integrity sha512-/+eSmQiU2ePNTwFCXX4JPrQNMvyu3sWrSDi/n5F6IMXwboB46IvtU8VHvG7Nc+egvtM7sBJKwmUx/vx6KIRDog== + dependencies: + debounce "^1.2.0" + react-window@^1.8.5: version "1.8.5" resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.5.tgz#a56b39307e79979721021f5d06a67742ecca52d1" From f2cfab5078d857e12b2d625c8eebf94a1732a4c2 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 10 Oct 2020 01:28:26 -0700 Subject: [PATCH 17/27] Updated realtime logs component. --- components/metrics/RealtimeLog.js | 2 +- components/metrics/RealtimeLog.module.css | 7 +++---- lang/da-DK.json | 2 +- lang/de-DE.json | 2 +- lang/el-GR.json | 2 +- lang/en-US.json | 2 +- lang/es-MX.json | 2 +- lang/fo-FO.json | 2 +- lang/fr-FR.json | 2 +- lang/id-ID.json | 2 +- lang/ja-JP.json | 2 +- lang/mn-MN.json | 2 +- lang/nb-NO.json | 2 +- lang/nl-NL.json | 2 +- lang/pt-PT.json | 2 +- lang/ro-RO.json | 2 +- lang/ru-RU.json | 2 +- lang/sv-SE.json | 2 +- lang/tr-TR.json | 2 +- lang/uk-UA.json | 2 +- lang/zh-CN.json | 2 +- 21 files changed, 23 insertions(+), 24 deletions(-) diff --git a/components/metrics/RealtimeLog.js b/components/metrics/RealtimeLog.js index 426bd3ce..80c14e37 100644 --- a/components/metrics/RealtimeLog.js +++ b/components/metrics/RealtimeLog.js @@ -89,11 +89,11 @@ export default function RealtimeLog({ data, websites }) { return (
{format(new Date(row.created_at), 'h:mm:ss')}
-
{getWebsite(row)}
{getDetail(row)}
+
{getWebsite(row)}
); }; diff --git a/components/metrics/RealtimeLog.module.css b/components/metrics/RealtimeLog.module.css index 803cfe0d..5924b331 100644 --- a/components/metrics/RealtimeLog.module.css +++ b/components/metrics/RealtimeLog.module.css @@ -1,5 +1,6 @@ .table { font-size: var(--font-size-xsmall); + overflow: hidden; } .header { @@ -32,10 +33,8 @@ } .website { - min-width: 120px; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; + text-align: right; + padding-right: 20px; } .detail { diff --git a/lang/da-DK.json b/lang/da-DK.json index b66b513d..7f476ef7 100644 --- a/lang/da-DK.json +++ b/lang/da-DK.json @@ -58,7 +58,7 @@ "message.get-tracking-code": "Få sporingskode", "message.go-to-settings": "Gå til betjeningspanel", "message.incorrect-username-password": "Ugyldigt brugernavn/adgangskode.", - "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", + "message.log.visitor": "Visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "A new version of umami {version} is available!", "message.no-data-available": "Ingen data tilgængelig.", "message.no-websites-configured": "Du har ikke konfigureret nogen websteder.", diff --git a/lang/de-DE.json b/lang/de-DE.json index 4447f98b..ccba1300 100644 --- a/lang/de-DE.json +++ b/lang/de-DE.json @@ -58,7 +58,7 @@ "message.get-tracking-code": "Erstelle Tracking Kennung", "message.go-to-settings": "Zu den Einstellungen", "message.incorrect-username-password": "Falsches Passwort oder Benutzername.", - "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", + "message.log.visitor": "Visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "Eine neue Version umami {version} ist verfügbar!", "message.no-data-available": "Keine Daten vorhanden.", "message.no-websites-configured": "Es ist keine Webseite vorhanden.", diff --git a/lang/el-GR.json b/lang/el-GR.json index 978de681..fd091a94 100644 --- a/lang/el-GR.json +++ b/lang/el-GR.json @@ -58,7 +58,7 @@ "message.get-tracking-code": "Λήψη κώδικα παρακολούθησης", "message.go-to-settings": "Μεταβείτε στις ρυθμίσεις", "message.incorrect-username-password": "Εσφαλμένο όνομα χρήστη / κωδικός πρόσβασης.", - "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", + "message.log.visitor": "Visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "A new version of umami {version} is available!", "message.no-data-available": "Δεν υπάρχουν διαθέσιμα δεδομένα.", "message.no-websites-configured": "Δεν έχετε ρυθμίσει κανένα ιστότοπο.", diff --git a/lang/en-US.json b/lang/en-US.json index 12231157..7bf0ee99 100644 --- a/lang/en-US.json +++ b/lang/en-US.json @@ -58,7 +58,7 @@ "message.get-tracking-code": "Get tracking code", "message.go-to-settings": "Go to settings", "message.incorrect-username-password": "Incorrect username/password.", - "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", + "message.log.visitor": "Visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "A new version of umami {version} is available!", "message.no-data-available": "No data available.", "message.no-websites-configured": "You don't have any websites configured.", diff --git a/lang/es-MX.json b/lang/es-MX.json index eaf92db6..230d3e09 100644 --- a/lang/es-MX.json +++ b/lang/es-MX.json @@ -58,7 +58,7 @@ "message.get-tracking-code": "Obtener código de rastreo", "message.go-to-settings": "Ir a la configuración", "message.incorrect-username-password": "Nombre de usuario o contraseña incorrectos.", - "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", + "message.log.visitor": "Visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "A new version of umami {version} is available!", "message.no-data-available": "Sin información disponible.", "message.no-websites-configured": "No tienes ningún sitio configurado.", diff --git a/lang/fo-FO.json b/lang/fo-FO.json index 8cedbcdb..256382b0 100644 --- a/lang/fo-FO.json +++ b/lang/fo-FO.json @@ -58,7 +58,7 @@ "message.get-tracking-code": "Fá sporings kotu", "message.go-to-settings": "Far til stillingar", "message.incorrect-username-password": "Skeivt brúkaranavn/loyniorð.", - "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", + "message.log.visitor": "Visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "A new version of umami {version} is available!", "message.no-data-available": "Einki data tøk.", "message.no-websites-configured": "Tú hevur ongar heimasíður stillaða til.", diff --git a/lang/fr-FR.json b/lang/fr-FR.json index ff00653d..49cdacce 100644 --- a/lang/fr-FR.json +++ b/lang/fr-FR.json @@ -58,7 +58,7 @@ "message.get-tracking-code": "Obtenez le code de suivi", "message.go-to-settings": "Aller aux paramètres", "message.incorrect-username-password": "nom d'utilisateurs/mot de passe incorrect.", - "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", + "message.log.visitor": "Visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "A new version of umami {version} is available!", "message.no-data-available": "Pas de données disponibles.", "message.no-websites-configured": "Vous n'avez configuré aucun site Web.", diff --git a/lang/id-ID.json b/lang/id-ID.json index 9c05cf80..1cf3a312 100644 --- a/lang/id-ID.json +++ b/lang/id-ID.json @@ -58,7 +58,7 @@ "message.get-tracking-code": "Dapatkan kode pelacakan", "message.go-to-settings": "Pergi ke pengaturan", "message.incorrect-username-password": "Nama pengguna/kata sandi salah.", - "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", + "message.log.visitor": "Visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "Versi terbaru umami {version} telah tersedia!", "message.no-data-available": "Tidak ada data.", "message.no-websites-configured": "Anda tidak memiliki situs web yang dikonfigurasi.", diff --git a/lang/ja-JP.json b/lang/ja-JP.json index 3a019f27..60fa91f1 100644 --- a/lang/ja-JP.json +++ b/lang/ja-JP.json @@ -58,7 +58,7 @@ "message.get-tracking-code": "トラッキングコードを取得", "message.go-to-settings": "設定する", "message.incorrect-username-password": "ユーザー名/パスワードが正しくありません。", - "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", + "message.log.visitor": "Visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "新しいバージョン({version})が利用可能です!", "message.no-data-available": "データがありません。", "message.no-websites-configured": "Webサイトが設定されていません。", diff --git a/lang/mn-MN.json b/lang/mn-MN.json index 6a9a0c0b..0e494938 100644 --- a/lang/mn-MN.json +++ b/lang/mn-MN.json @@ -58,7 +58,7 @@ "message.get-tracking-code": "Мөрдөх код авах", "message.go-to-settings": "Тохиргоо руу очих", "message.incorrect-username-password": "Буруу хэрэглэгчийн нэр/нууц үг.", - "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", + "message.log.visitor": "Visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "Umami-гийн шинэ хувилбар {version} гарсан байна!", "message.no-data-available": "Өгөгдөл алга.", "message.no-websites-configured": "Та ямар нэгэн веб тохируулаагүй байна.", diff --git a/lang/nb-NO.json b/lang/nb-NO.json index 0988f361..c04f6939 100644 --- a/lang/nb-NO.json +++ b/lang/nb-NO.json @@ -58,7 +58,7 @@ "message.get-tracking-code": "Få sporingskode", "message.go-to-settings": "Gå til innstillinger", "message.incorrect-username-password": "Ugyldig brukernavn/passord.", - "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", + "message.log.visitor": "Visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "En ny versjon av umami {version} er tilgjengelig!", "message.no-data-available": "Ingen data tilgjengelig.", "message.no-websites-configured": "Du har ikke satt opp noen nettsteder.", diff --git a/lang/nl-NL.json b/lang/nl-NL.json index f36289cc..44bb697c 100644 --- a/lang/nl-NL.json +++ b/lang/nl-NL.json @@ -58,7 +58,7 @@ "message.get-tracking-code": "Tracking code", "message.go-to-settings": "Naar instellingen", "message.incorrect-username-password": "Incorrecte gebruikersnaam/wachtwoord.", - "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", + "message.log.visitor": "Visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "Een nieuwe versie van umami {version} is beschikbaar!", "message.no-data-available": "Geen gegevens beschikbaar.", "message.no-websites-configured": "Je hebt geen websites ingesteld.", diff --git a/lang/pt-PT.json b/lang/pt-PT.json index d1f7ee3c..1e291452 100644 --- a/lang/pt-PT.json +++ b/lang/pt-PT.json @@ -58,7 +58,7 @@ "message.get-tracking-code": "Obter código de tracking", "message.go-to-settings": "Ir para as definições", "message.incorrect-username-password": "Nome de utilizador/palavra-passe incorretos.", - "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", + "message.log.visitor": "Visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "Uma nova versão de umami {version} está disponível!", "message.no-data-available": "Sem dados disponíveis.", "message.no-websites-configured": "Não tens nenhum website configurado.", diff --git a/lang/ro-RO.json b/lang/ro-RO.json index 3fe2fb92..c6f738ec 100644 --- a/lang/ro-RO.json +++ b/lang/ro-RO.json @@ -58,7 +58,7 @@ "message.get-tracking-code": "Obține codul de urmărire", "message.go-to-settings": "Mergi la Setări", "message.incorrect-username-password": "Username/parolă incorecte.", - "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", + "message.log.visitor": "Visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "Este disponibilă o nouă versiune {version} de umami!", "message.no-data-available": "Nicio informație disponibilă.", "message.no-websites-configured": "Nu aveți niciun site web configurat.", diff --git a/lang/ru-RU.json b/lang/ru-RU.json index 4b0ff88b..afa1bc59 100644 --- a/lang/ru-RU.json +++ b/lang/ru-RU.json @@ -58,7 +58,7 @@ "message.get-tracking-code": "Получить код отслеживания", "message.go-to-settings": "Перейти к настройкам", "message.incorrect-username-password": "Неверное имя пользователя/пароль.", - "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", + "message.log.visitor": "Visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "Доступна новая версия umami {version}", "message.no-data-available": "Нет данных.", "message.no-websites-configured": "У вас нет настроенных сайтов.", diff --git a/lang/sv-SE.json b/lang/sv-SE.json index 6133383b..d1245348 100644 --- a/lang/sv-SE.json +++ b/lang/sv-SE.json @@ -58,7 +58,7 @@ "message.get-tracking-code": "Visa spårningskod", "message.go-to-settings": "Gå till inställningar", "message.incorrect-username-password": "Felaktikt användarnamn/lösenord.", - "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", + "message.log.visitor": "Visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "A new version of umami {version} is available!", "message.no-data-available": "Ingen data tillgänglig.", "message.no-websites-configured": "Du har inga webbsajter.", diff --git a/lang/tr-TR.json b/lang/tr-TR.json index 1aff9d2e..b2ea3127 100644 --- a/lang/tr-TR.json +++ b/lang/tr-TR.json @@ -58,7 +58,7 @@ "message.get-tracking-code": "İzleme kodunu al", "message.go-to-settings": "Ayarlara git", "message.incorrect-username-password": "Hatalı kullanıcı adı ya da parola.", - "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", + "message.log.visitor": "Visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "A new version of umami {version} is available!", "message.no-data-available": "Henüz hiç veri yok.", "message.no-websites-configured": "Henüz hiç web sitesi tanımlamadınız", diff --git a/lang/uk-UA.json b/lang/uk-UA.json index 27d04279..26f2f548 100644 --- a/lang/uk-UA.json +++ b/lang/uk-UA.json @@ -58,7 +58,7 @@ "message.get-tracking-code": "Отримати код для відслідковування", "message.go-to-settings": "Перейти до налаштувань", "message.incorrect-username-password": "Невірне ім'я користувача або пароль.", - "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", + "message.log.visitor": "Visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "Нова версія umami {version} доступна!", "message.no-data-available": "Немає даних.", "message.no-websites-configured": "У вас немає налаштованих веб-сайтів.", diff --git a/lang/zh-CN.json b/lang/zh-CN.json index 18a82ac4..55b342e4 100644 --- a/lang/zh-CN.json +++ b/lang/zh-CN.json @@ -58,7 +58,7 @@ "message.get-tracking-code": "获得跟踪代码", "message.go-to-settings": "去设置", "message.incorrect-username-password": "用户名密码不正确.", - "message.log.visitor": "A visitor from {country} using {browser} on {os} {device}", + "message.log.visitor": "Visitor from {country} using {browser} on {os} {device}", "message.new-version-available": "A new version of umami {version} is available!", "message.no-data-available": "无可用数据.", "message.no-websites-configured": "你还没有设置任何网站.", From 689184bf2f5ac17925c263c8606cbe5e17556a08 Mon Sep 17 00:00:00 2001 From: Florian Kapfenberger Date: Sat, 10 Oct 2020 11:36:44 +0200 Subject: [PATCH 18/27] fix: link to github releases without tag --- components/layout/Footer.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/components/layout/Footer.js b/components/layout/Footer.js index 62472f4b..73e010bc 100644 --- a/components/layout/Footer.js +++ b/components/layout/Footer.js @@ -25,9 +25,7 @@ export default function Footer() { />
- - {`v${current}`} - + {`v${current}`}
From b72a4c001c86475df3988711f33e9c30371400e2 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 10 Oct 2020 11:04:07 -0700 Subject: [PATCH 19/27] Refactored realtime API. Add dot component and colored dots in log. --- components/common/Dot.js | 15 +++++ components/common/Dot.module.css | 17 ++++++ components/metrics/ActiveUsers.js | 3 +- components/metrics/ActiveUsers.module.css | 8 --- components/metrics/RealtimeHeader.js | 8 ++- components/metrics/RealtimeLog.js | 13 ++++ components/pages/RealtimeDashboard.js | 8 +-- lib/format.js | 16 +++++ lib/queries.js | 27 +++++++++ pages/api/realtime.js | 72 ----------------------- pages/api/realtime/init.js | 26 ++++++++ pages/api/realtime/update.js | 26 ++++++++ 12 files changed, 153 insertions(+), 86 deletions(-) create mode 100644 components/common/Dot.js create mode 100644 components/common/Dot.module.css delete mode 100644 pages/api/realtime.js create mode 100644 pages/api/realtime/init.js create mode 100644 pages/api/realtime/update.js diff --git a/components/common/Dot.js b/components/common/Dot.js new file mode 100644 index 00000000..3f424820 --- /dev/null +++ b/components/common/Dot.js @@ -0,0 +1,15 @@ +import React from 'react'; +import classNames from 'classnames'; +import styles from './Dot.module.css'; + +export default function Dot({ color, size, className }) { + return ( +
+ ); +} diff --git a/components/common/Dot.module.css b/components/common/Dot.module.css new file mode 100644 index 00000000..9081dc5c --- /dev/null +++ b/components/common/Dot.module.css @@ -0,0 +1,17 @@ +.dot { + background: var(--green400); + width: 10px; + height: 10px; + border-radius: 100%; + margin-right: 10px; +} + +.dot.small { + width: 8px; + height: 8px; +} + +.dot.large { + width: 16px; + height: 16px; +} diff --git a/components/metrics/ActiveUsers.js b/components/metrics/ActiveUsers.js index c099d8f7..3b691c10 100644 --- a/components/metrics/ActiveUsers.js +++ b/components/metrics/ActiveUsers.js @@ -2,6 +2,7 @@ import React, { useMemo } from 'react'; import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import useFetch from 'hooks/useFetch'; +import Dot from 'components/common/Dot'; import styles from './ActiveUsers.module.css'; export default function ActiveUsers({ websiteId, token, className }) { @@ -19,7 +20,7 @@ export default function ActiveUsers({ websiteId, token, className }) { return (
-
+
, value: 0 }, - ].concat(websites.map(({ name, website_id }) => ({ label: name, value: website_id }))); + ].concat( + websites.map(({ name, website_id }, index) => ({ + label: name, + value: website_id, + divider: index === 0, + })), + ); const { pageviews, sessions, events, countries } = data; diff --git a/components/metrics/RealtimeLog.js b/components/metrics/RealtimeLog.js index 80c14e37..7bb37e08 100644 --- a/components/metrics/RealtimeLog.js +++ b/components/metrics/RealtimeLog.js @@ -11,7 +11,9 @@ import { BROWSERS } from 'lib/constants'; import Bolt from 'assets/bolt.svg'; import Visitor from 'assets/visitor.svg'; import Eye from 'assets/eye.svg'; +import { stringToColor } from 'lib/format'; import styles from './RealtimeLog.module.css'; +import Dot from '../common/Dot'; const TYPE_PAGEVIEW = 0; const TYPE_SESSION = 1; @@ -26,11 +28,19 @@ const TYPE_ICONS = { export default function RealtimeLog({ data, websites }) { const [locale] = useLocale(); const countryNames = useCountryNames(locale); + const logs = useMemo(() => { const { pageviews, sessions, events } = data; return [...pageviews, ...sessions, ...events].sort(firstBy('created_at', -1)); }, [data]); + const uuids = useMemo(() => { + return data.sessions.reduce((obj, { session_id, session_uuid }) => { + obj[session_id] = session_uuid; + return obj; + }, {}); + }, [data]); + function getType({ view_id, session_id, event_id }) { if (event_id) { return TYPE_EVENT; @@ -88,6 +98,9 @@ export default function RealtimeLog({ data, websites }) { const row = logs[index]; return (
+
+ +
{format(new Date(row.created_at), 'h:mm:ss')}
diff --git a/components/pages/RealtimeDashboard.js b/components/pages/RealtimeDashboard.js index 30bf1ee4..d514e348 100644 --- a/components/pages/RealtimeDashboard.js +++ b/components/pages/RealtimeDashboard.js @@ -16,7 +16,7 @@ import useCountryNames from 'hooks/useCountryNames'; import { FormattedMessage } from 'react-intl'; const REALTIME_RANGE = 30; -const REALTIME_INTERVAL = 55000; +const REALTIME_INTERVAL = 3000; function mergeData(state, data, time) { const ids = state.map(({ __id }) => __id); @@ -34,9 +34,9 @@ export default function RealtimeDashboard() { const countryNames = useCountryNames(locale); const [data, setData] = useState(); const [websiteId, setWebsiteId] = useState(0); - const { data: init, loading } = useFetch('/api/realtime', { params: { type: 'init' } }); - const { data: updates } = useFetch('/api/realtime', { - params: { type: 'update', start_at: data?.timestamp }, + const { data: init, loading } = useFetch('/api/realtime/init'); + const { data: updates } = useFetch('/api/realtime/update', { + params: { start_at: data?.timestamp }, disabled: !init?.websites?.length || !data, interval: REALTIME_INTERVAL, headers: { 'x-umami-token': init?.token }, diff --git a/lib/format.js b/lib/format.js index b031509b..a336c1c4 100644 --- a/lib/format.js +++ b/lib/format.js @@ -62,3 +62,19 @@ export function formatLongNumber(value) { return formatNumber(n); } + +export function stringToColor(str) { + if (!str) { + return '#ffffff'; + } + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + let color = '#'; + for (let i = 0; i < 3; i++) { + let value = (hash >> (i * 8)) & 0xff; + color += ('00' + value.toString(16)).substr(-2); + } + return color; +} diff --git a/lib/queries.js b/lib/queries.js index ecf351b1..ed97eb93 100644 --- a/lib/queries.js +++ b/lib/queries.js @@ -500,3 +500,30 @@ export function getEventMetrics( params, ); } + +export async function getRealtimeData(websites, time) { + const [pageviews, sessions, events] = await Promise.all([ + getPageviews(websites, time), + getSessions(websites, time), + getEvents(websites, time), + ]); + + return { + pageviews: pageviews.map(({ view_id, ...props }) => ({ + __id: `p${view_id}`, + view_id, + ...props, + })), + sessions: sessions.map(({ session_id, ...props }) => ({ + __id: `s${session_id}`, + session_id, + ...props, + })), + events: events.map(({ event_id, ...props }) => ({ + __id: `e${event_id}`, + event_id, + ...props, + })), + timestamp: Date.now(), + }; +} diff --git a/pages/api/realtime.js b/pages/api/realtime.js deleted file mode 100644 index 538f611a..00000000 --- a/pages/api/realtime.js +++ /dev/null @@ -1,72 +0,0 @@ -import { subMinutes } from 'date-fns'; -import { useAuth } from 'lib/middleware'; -import { ok, methodNotAllowed, badRequest } from 'lib/response'; -import { getEvents, getPageviews, getSessions, getUserWebsites } from 'lib/queries'; -import { createToken, parseToken } from 'lib/crypto'; - -export default async (req, res) => { - await useAuth(req, res); - - async function getData(websites, time) { - const [pageviews, sessions, events] = await Promise.all([ - getPageviews(websites, time), - getSessions(websites, time), - getEvents(websites, time), - ]); - - return { - pageviews: pageviews.map(({ view_id, ...props }) => ({ - __id: `p${view_id}`, - view_id, - ...props, - })), - sessions: sessions.map(({ session_id, ...props }) => ({ - __id: `s${session_id}`, - session_id, - ...props, - })), - events: events.map(({ event_id, ...props }) => ({ - __id: `e${event_id}`, - event_id, - ...props, - })), - timestamp: Date.now(), - }; - } - - if (req.method === 'GET') { - const { type, start_at } = req.query; - const { user_id } = req.auth; - - if (type === 'init') { - const websites = await getUserWebsites(user_id); - const ids = websites.map(({ website_id }) => website_id); - const token = await createToken({ websites: ids }); - const data = await getData(ids, subMinutes(new Date(), 30)); - - return ok(res, { - websites, - token, - data, - }); - } - - if (type === 'update') { - const token = req.headers['x-umami-token']; - - if (!token) { - return badRequest(res); - } - - const { websites } = await parseToken(token); - - const data = await getData(websites, new Date(+start_at)); - - return ok(res, data); - } - - return badRequest(res); - } - - return methodNotAllowed(res); -}; diff --git a/pages/api/realtime/init.js b/pages/api/realtime/init.js new file mode 100644 index 00000000..52e66600 --- /dev/null +++ b/pages/api/realtime/init.js @@ -0,0 +1,26 @@ +import { subMinutes } from 'date-fns'; +import { useAuth } from 'lib/middleware'; +import { ok, methodNotAllowed } from 'lib/response'; +import { getUserWebsites, getRealtimeData } from 'lib/queries'; +import { createToken } from 'lib/crypto'; + +export default async (req, res) => { + await useAuth(req, res); + + if (req.method === 'GET') { + const { user_id } = req.auth; + + const websites = await getUserWebsites(user_id); + const ids = websites.map(({ website_id }) => website_id); + const token = await createToken({ websites: ids }); + const data = await getRealtimeData(ids, subMinutes(new Date(), 30)); + + return ok(res, { + websites, + token, + data, + }); + } + + return methodNotAllowed(res); +}; diff --git a/pages/api/realtime/update.js b/pages/api/realtime/update.js new file mode 100644 index 00000000..15582f81 --- /dev/null +++ b/pages/api/realtime/update.js @@ -0,0 +1,26 @@ +import { useAuth } from 'lib/middleware'; +import { ok, methodNotAllowed, badRequest } from 'lib/response'; +import { getRealtimeData } from 'lib/queries'; +import { parseToken } from 'lib/crypto'; + +export default async (req, res) => { + await useAuth(req, res); + + if (req.method === 'GET') { + const { start_at } = req.query; + + const token = req.headers['x-umami-token']; + + if (!token) { + return badRequest(res); + } + + const { websites } = await parseToken(token); + + const data = await getRealtimeData(websites, new Date(+start_at)); + + return ok(res, data); + } + + return methodNotAllowed(res); +}; From 1fcb610bdda9c96ea27c8a21a5d1636930a0971e Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 10 Oct 2020 22:36:55 -0700 Subject: [PATCH 20/27] Updated fetch hook. --- components/metrics/RealtimeLog.js | 14 +++++++++-- components/metrics/RealtimeLog.module.css | 5 +--- components/pages/RealtimeDashboard.js | 29 +++++++++++++---------- hooks/useFetch.js | 25 ++++++++++++------- lib/constants.js | 2 ++ redux/actions/app.js | 12 +++++++--- 6 files changed, 57 insertions(+), 30 deletions(-) diff --git a/components/metrics/RealtimeLog.js b/components/metrics/RealtimeLog.js index 7bb37e08..6e205e16 100644 --- a/components/metrics/RealtimeLog.js +++ b/components/metrics/RealtimeLog.js @@ -94,14 +94,24 @@ export default function RealtimeLog({ data, websites }) { } } + function getTime({ created_at }) { + return format(new Date(created_at), 'h:mm:ss'); + } + + function getColor(row) { + const { session_id } = row; + + return stringToColor(uuids[session_id] || `${session_id}${getWebsite(row)}`); + } + const Row = ({ index, style }) => { const row = logs[index]; return (
- +
-
{format(new Date(row.created_at), 'h:mm:ss')}
+
{getTime(row)}
{getDetail(row)} diff --git a/components/metrics/RealtimeLog.module.css b/components/metrics/RealtimeLog.module.css index 5924b331..99335a52 100644 --- a/components/metrics/RealtimeLog.module.css +++ b/components/metrics/RealtimeLog.module.css @@ -34,15 +34,12 @@ .website { text-align: right; - padding-right: 20px; + padding: 0 20px; } .detail { display: flex; flex: 1; -} - -.detail span { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; diff --git a/components/pages/RealtimeDashboard.js b/components/pages/RealtimeDashboard.js index d514e348..d80304d9 100644 --- a/components/pages/RealtimeDashboard.js +++ b/components/pages/RealtimeDashboard.js @@ -80,7 +80,7 @@ export default function RealtimeDashboard() { ); } return []; - }, [realtimeData]); + }, [realtimeData?.sessions]); const referrers = useMemo(() => { if (realtimeData?.pageviews) { @@ -89,12 +89,15 @@ export default function RealtimeDashboard() { .reduce((arr, { referrer }) => { if (referrer?.startsWith('http')) { const { hostname } = new URL(referrer); - const row = arr.find(({ x }) => x === hostname); - if (!row) { - arr.push({ x: hostname, y: 1 }); - } else { - row.y += 1; + if (!data.domains.includes(hostname)) { + const row = arr.find(({ x }) => x === hostname); + + if (!row) { + arr.push({ x: hostname, y: 1 }); + } else { + row.y += 1; + } } } return arr; @@ -103,7 +106,7 @@ export default function RealtimeDashboard() { ); } return []; - }, [realtimeData]); + }, [realtimeData?.pageviews]); useEffect(() => { if (init && !data) { @@ -111,7 +114,11 @@ export default function RealtimeDashboard() { const domains = init.websites.map(({ domain }) => domain); setData({ websites, domains, ...data }); - } else if (updates) { + } + }, [init]); + + useEffect(() => { + if (updates) { const { pageviews, sessions, events, timestamp } = updates; const time = subMinutes(startOfMinute(new Date()), REALTIME_RANGE).getTime(); @@ -123,16 +130,14 @@ export default function RealtimeDashboard() { timestamp, })); } - }, [init, updates]); + }, [updates]); - if (!init || loading || !data) { + if (!init || !data || loading) { return null; } const { websites } = data; - //console.log({realtimeData, countries}); - return ( { if (url && !disabled) { - setTimeout(() => loadData(), delay); + const id = setTimeout(() => loadData(params), delay); + + return () => { + clearTimeout(id); + }; } - }, [url, disabled, ...update]); + }, [url, !!disabled, count, ...update]); useEffect(() => { - const id = interval ? setInterval(() => loadData(), interval) : null; + if (interval && !disabled) { + const id = setInterval(() => setCount(state => state + 1), interval); - return () => { - clearInterval(id); - }; - }, [interval, params]); + return () => { + clearInterval(id); + }; + } + }, [interval, !!disabled]); return { data, status, error, loading }; } diff --git a/lib/constants.js b/lib/constants.js index 721f5e1c..80d8eed1 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -51,6 +51,8 @@ export const EVENT_COLORS = [ '#ffec16', ]; +export const DEFAULT_LOCALE = 'en-US'; +export const DEFAULT_THEME = 'light'; export const DEFAUL_CHART_HEIGHT = 400; export const DEFAULT_ANIMATION_DURATION = 300; export const DEFAULT_DATE_RANGE = '24hour'; diff --git a/redux/actions/app.js b/redux/actions/app.js index 94b77daa..900be56d 100644 --- a/redux/actions/app.js +++ b/redux/actions/app.js @@ -1,13 +1,19 @@ import { createSlice } from '@reduxjs/toolkit'; import { getItem } from 'lib/web'; -import { LOCALE_CONFIG, THEME_CONFIG, VERSION_CHECK } from 'lib/constants'; +import { + DEFAULT_LOCALE, + DEFAULT_THEME, + LOCALE_CONFIG, + THEME_CONFIG, + VERSION_CHECK, +} from 'lib/constants'; import semver from 'semver'; const app = createSlice({ name: 'app', initialState: { - locale: getItem(LOCALE_CONFIG) || 'en-US', - theme: getItem(THEME_CONFIG) || 'light', + locale: getItem(LOCALE_CONFIG) || DEFAULT_LOCALE, + theme: getItem(THEME_CONFIG) || DEFAULT_THEME, versions: { current: process.env.VERSION, latest: null, From 4119e80a9a0fa76841d2ddfeb31c03e4173bc4ae Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 11 Oct 2020 01:33:26 -0700 Subject: [PATCH 21/27] Update table components. --- components/metrics/BrowsersTable.js | 4 ++-- components/metrics/CountriesTable.js | 6 +++--- components/metrics/DataTable.js | 18 +++++++++--------- components/metrics/DevicesTable.js | 4 ++-- components/metrics/EventsTable.js | 5 ++--- components/metrics/MetricsTable.js | 8 +++++--- components/metrics/PagesTable.js | 4 ++-- components/metrics/ReferrersTable.js | 4 ++-- components/pages/RealtimeDashboard.js | 18 ++++++++---------- components/pages/WebsiteDetails.js | 4 ++-- pages/api/website/[id]/metrics.js | 2 +- 11 files changed, 38 insertions(+), 39 deletions(-) diff --git a/components/metrics/BrowsersTable.js b/components/metrics/BrowsersTable.js index 97f9bfbd..3481449c 100644 --- a/components/metrics/BrowsersTable.js +++ b/components/metrics/BrowsersTable.js @@ -3,15 +3,15 @@ import { FormattedMessage } from 'react-intl'; import MetricsTable from './MetricsTable'; import { browserFilter } from 'lib/filters'; -export default function BrowsersTable({ websiteId, token, limit }) { +export default function BrowsersTable({ websiteId, token, ...props }) { return ( } type="browser" metric={} websiteId={websiteId} token={token} - limit={limit} dataFilter={browserFilter} /> ); diff --git a/components/metrics/CountriesTable.js b/components/metrics/CountriesTable.js index d562b464..5b44bd23 100644 --- a/components/metrics/CountriesTable.js +++ b/components/metrics/CountriesTable.js @@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl'; import useCountryNames from 'hooks/useCountryNames'; import useLocale from 'hooks/useLocale'; -export default function CountriesTable({ websiteId, token, limit, onDataLoad = () => {} }) { +export default function CountriesTable({ websiteId, token, onDataLoad, ...props }) { const [locale] = useLocale(); const countryNames = useCountryNames(locale); @@ -15,13 +15,13 @@ export default function CountriesTable({ websiteId, token, limit, onDataLoad = ( return ( } type="country" metric={} websiteId={websiteId} token={token} - limit={limit} - onDataLoad={data => onDataLoad(percentFilter(data))} + onDataLoad={data => onDataLoad?.(percentFilter(data))} renderLabel={renderLabel} /> ); diff --git a/components/metrics/DataTable.js b/components/metrics/DataTable.js index 9e12d887..70eae89f 100644 --- a/components/metrics/DataTable.js +++ b/components/metrics/DataTable.js @@ -11,10 +11,10 @@ export default function DataTable({ title, metric, className, - limit, renderLabel, height = 400, animate = true, + virtualize = false, }) { const [format, setFormat] = useState(true); const formatFunc = format ? formatLongNumber : formatNumber; @@ -30,7 +30,7 @@ export default function DataTable({ label={renderLabel ? renderLabel(row) : label} value={value} percent={percent} - animate={animate} + animate={animate && !virtualize} format={formatFunc} onClick={handleSetFormat} /> @@ -51,13 +51,13 @@ export default function DataTable({
{data?.length === 0 && } - {limit - ? data.map(row => getRow(row)) - : data.length > 0 && ( - - {Row} - - )} + {virtualize && data.length > 0 ? ( + + {Row} + + ) : ( + data.map(row => getRow(row)) + )}
); diff --git a/components/metrics/DevicesTable.js b/components/metrics/DevicesTable.js index 7d87d1c1..e71355db 100644 --- a/components/metrics/DevicesTable.js +++ b/components/metrics/DevicesTable.js @@ -4,15 +4,15 @@ import { deviceFilter } from 'lib/filters'; import { FormattedMessage } from 'react-intl'; import { getDeviceMessage } from 'components/messages'; -export default function DevicesTable({ websiteId, token, limit }) { +export default function DevicesTable({ websiteId, token, ...props }) { return ( } type="device" metric={} websiteId={websiteId} token={token} - limit={limit} dataFilter={deviceFilter} renderLabel={({ x }) => getDeviceMessage(x)} /> diff --git a/components/metrics/EventsTable.js b/components/metrics/EventsTable.js index 43b52c4d..73df5028 100644 --- a/components/metrics/EventsTable.js +++ b/components/metrics/EventsTable.js @@ -3,17 +3,16 @@ import { FormattedMessage } from 'react-intl'; import MetricsTable from './MetricsTable'; import Tag from 'components/common/Tag'; -export default function EventsTable({ websiteId, token, limit, onDataLoad }) { +export default function EventsTable({ websiteId, token, ...props }) { return ( } type="event" metric={} websiteId={websiteId} token={token} - limit={limit} renderLabel={({ x }) =>