diff --git a/.stylelintrc.json b/.stylelintrc.json index 117fac2a..9a05af14 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -13,5 +13,5 @@ } ] }, - "ignoreFiles": ["**/*.js"] + "ignoreFiles": ["**/*.js", "**/*.md"] } diff --git a/README.md b/README.md index 5a72e3c8..bb83c665 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # umami -Umami is a simple, fast, website analytics alternative to Google Analytics. +Umami is a simple, fast, privacy-focused alternative to Google Analytics. ## Getting started diff --git a/components/common/FilterLink.js b/components/common/FilterLink.js new file mode 100644 index 00000000..459a8ae1 --- /dev/null +++ b/components/common/FilterLink.js @@ -0,0 +1,34 @@ +import React from 'react'; +import Link from 'next/link'; +import classNames from 'classnames'; +import usePageQuery from 'hooks/usePageQuery'; +import { safeDecodeURI } from 'lib/url'; +import Icon from './Icon'; +import External from 'assets/arrow-up-right-from-square.svg'; +import styles from './FilterLink.module.css'; + +export default function FilterLink({ id, value, label, externalUrl }) { + const { resolve, query } = usePageQuery(); + const active = query[id] !== undefined; + const selected = query[id] === value; + + return ( +
+ + + {safeDecodeURI(label || value)} + + + {externalUrl && ( + + } className={styles.icon} /> + + )} +
+ ); +} diff --git a/components/metrics/ReferrersTable.module.css b/components/common/FilterLink.module.css similarity index 79% rename from components/metrics/ReferrersTable.module.css rename to components/common/FilterLink.module.css index 238667f3..45b049da 100644 --- a/components/metrics/ReferrersTable.module.css +++ b/components/common/FilterLink.module.css @@ -1,19 +1,20 @@ -body .inactive { +.row { + display: flex; + align-items: center; +} + +.row .inactive { color: var(--gray500); } -body .active { +.row .active { color: var(--gray900); font-weight: 600; } -.row { - display: flex; - justify-content: space-between; -} - .row .link { display: none; + margin-left: 20px; } .row .label { diff --git a/components/metrics/ActiveUsers.js b/components/metrics/ActiveUsers.js index b739861b..7718b587 100644 --- a/components/metrics/ActiveUsers.js +++ b/components/metrics/ActiveUsers.js @@ -9,14 +9,18 @@ import styles from './ActiveUsers.module.css'; export default function ActiveUsers({ websiteId, className, value, interval = 60000 }) { const shareToken = useShareToken(); - const url = value !== undefined && websiteId ? `/website/${websiteId}/active` : null; + const url = websiteId ? `/website/${websiteId}/active` : null; const { data } = useFetch(url, { interval, headers: { [TOKEN_HEADER]: shareToken?.token }, }); const count = useMemo(() => { - return value || data?.[0]?.x || 0; - }, [data, value]); + if (websiteId) { + return data?.[0]?.x || 0 + } + + return value !== undefined ? value : 0; + }, [data, value, websiteId]); if (count === 0) { return null; diff --git a/components/metrics/BrowsersTable.js b/components/metrics/BrowsersTable.js index 12c1087b..3933eda0 100644 --- a/components/metrics/BrowsersTable.js +++ b/components/metrics/BrowsersTable.js @@ -1,9 +1,14 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; -import MetricsTable from './MetricsTable'; -import { browserFilter } from 'lib/filters'; +import FilterLink from 'components/common/FilterLink'; +import MetricsTable from 'components/metrics/MetricsTable'; +import { BROWSERS } from 'lib/constants'; export default function BrowsersTable({ websiteId, ...props }) { + function renderLink({ x: browser }) { + return ; + } + return ( } websiteId={websiteId} - dataFilter={browserFilter} + renderLabel={renderLink} /> ); } diff --git a/components/metrics/CountriesTable.js b/components/metrics/CountriesTable.js index 01e7c7c7..17d9127a 100644 --- a/components/metrics/CountriesTable.js +++ b/components/metrics/CountriesTable.js @@ -2,6 +2,7 @@ import React from 'react'; import MetricsTable from './MetricsTable'; import { percentFilter } from 'lib/filters'; import { FormattedMessage } from 'react-intl'; +import FilterLink from 'components/common/FilterLink'; import useCountryNames from 'hooks/useCountryNames'; import useLocale from 'hooks/useLocale'; @@ -9,10 +10,16 @@ export default function CountriesTable({ websiteId, onDataLoad, ...props }) { const { locale } = useLocale(); const countryNames = useCountryNames(locale); - function renderLabel({ x }) { + function renderLink({ x: code }) { return (
- {countryNames[x] ?? } + + } + />
); } @@ -25,7 +32,7 @@ export default function CountriesTable({ websiteId, onDataLoad, ...props }) { metric={} websiteId={websiteId} onDataLoad={data => onDataLoad?.(percentFilter(data))} - renderLabel={renderLabel} + renderLabel={renderLink} /> ); } diff --git a/components/metrics/DevicesTable.js b/components/metrics/DevicesTable.js index d09774b9..c704e08d 100644 --- a/components/metrics/DevicesTable.js +++ b/components/metrics/DevicesTable.js @@ -1,9 +1,18 @@ import React from 'react'; import MetricsTable from './MetricsTable'; -import { FormattedMessage } from 'react-intl'; +import { useIntl, FormattedMessage } from 'react-intl'; import { getDeviceMessage } from 'components/messages'; +import FilterLink from 'components/common/FilterLink'; export default function DevicesTable({ websiteId, ...props }) { + const { formatMessage } = useIntl(); + + function renderLink({ x: device }) { + return ( + + ); + } + return ( } websiteId={websiteId} - renderLabel={({ x }) => } + renderLabel={renderLink} /> ); } diff --git a/components/metrics/MetricsBar.js b/components/metrics/MetricsBar.js index 492f2a88..290cde81 100644 --- a/components/metrics/MetricsBar.js +++ b/components/metrics/MetricsBar.js @@ -18,7 +18,7 @@ export default function MetricsBar({ websiteId, className }) { const { startDate, endDate, modified } = dateRange; const [format, setFormat] = useState(true); const { - query: { url, ref }, + query: { url, referrer, os, browser, device, country }, } = usePageQuery(); const { data, error, loading } = useFetch( @@ -28,11 +28,15 @@ export default function MetricsBar({ websiteId, className }) { start_at: +startDate, end_at: +endDate, url, - ref, + referrer, + os, + browser, + device, + country, }, headers: { [TOKEN_HEADER]: shareToken?.token }, }, - [modified, url, ref], + [modified, url, referrer, os, browser, device, country], ); const formatFunc = format diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js index e1fa6891..48e004cb 100644 --- a/components/metrics/MetricsTable.js +++ b/components/metrics/MetricsTable.js @@ -30,7 +30,7 @@ export default function MetricsTable({ const { resolve, router, - query: { url, referrer }, + query: { url, referrer, os, browser, device, country }, } = usePageQuery(); const { data, loading, error } = useFetch( @@ -42,12 +42,16 @@ export default function MetricsTable({ end_at: +endDate, url, referrer, + os, + browser, + device, + country, }, onDataLoad, delay: DEFAULT_ANIMATION_DURATION, headers: { [TOKEN_HEADER]: shareToken?.token }, }, - [modified, url, referrer], + [modified, url, referrer, os, browser, device, country], ); const filteredData = useMemo(() => { diff --git a/components/metrics/OSTable.js b/components/metrics/OSTable.js index c77ae074..18bc2499 100644 --- a/components/metrics/OSTable.js +++ b/components/metrics/OSTable.js @@ -1,14 +1,20 @@ import React from 'react'; import MetricsTable from './MetricsTable'; import { FormattedMessage } from 'react-intl'; +import FilterLink from 'components/common/FilterLink'; export default function OSTable({ websiteId, ...props }) { + function renderLink({ x: os }) { + return ; + } + return ( } type="os" metric={} + renderLabel={renderLink} websiteId={websiteId} /> ); diff --git a/components/metrics/PagesTable.js b/components/metrics/PagesTable.js index 6fe8c139..98a0fd72 100644 --- a/components/metrics/PagesTable.js +++ b/components/metrics/PagesTable.js @@ -1,23 +1,15 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import classNames from 'classnames'; -import Link from 'next/link'; +import FilterLink from 'components/common/FilterLink'; import FilterButtons from 'components/common/FilterButtons'; import { urlFilter } from 'lib/filters'; -import { safeDecodeURI } from 'lib/url'; -import usePageQuery from 'hooks/usePageQuery'; import MetricsTable from './MetricsTable'; -import styles from './PagesTable.module.css'; export const FILTER_COMBINED = 0; export const FILTER_RAW = 1; export default function PagesTable({ websiteId, websiteDomain, showFilters, ...props }) { const [filter, setFilter] = useState(FILTER_COMBINED); - const { - resolve, - query: { url: currentUrl }, - } = usePageQuery(); const buttons = [ { @@ -28,18 +20,7 @@ export default function PagesTable({ websiteId, websiteDomain, showFilters, ...p ]; const renderLink = ({ x: url }) => { - return ( - - - {safeDecodeURI(url)} - - - ); + return ; }; return ( diff --git a/components/metrics/PagesTable.module.css b/components/metrics/PagesTable.module.css deleted file mode 100644 index 3c592a74..00000000 --- a/components/metrics/PagesTable.module.css +++ /dev/null @@ -1,8 +0,0 @@ -body .inactive { - color: var(--gray500); -} - -body .active { - color: var(--gray900); - font-weight: 600; -} diff --git a/components/metrics/RealtimeHeader.js b/components/metrics/RealtimeHeader.js index 33c7b576..010196c1 100644 --- a/components/metrics/RealtimeHeader.js +++ b/components/metrics/RealtimeHeader.js @@ -24,7 +24,7 @@ export default function RealtimeHeader({ websites, data, websiteId, onSelect }) return sessions.filter( ({ created_at }) => differenceInMinutes(new Date(), new Date(created_at)) <= 5, ).length; - }, [sessions]); + }, [sessions, websiteId]); return ( <> diff --git a/components/metrics/ReferrersTable.js b/components/metrics/ReferrersTable.js index 93cdb956..1a9e3f2d 100644 --- a/components/metrics/ReferrersTable.js +++ b/components/metrics/ReferrersTable.js @@ -2,14 +2,8 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import MetricsTable from './MetricsTable'; import FilterButtons from 'components/common/FilterButtons'; +import FilterLink from 'components/common/FilterLink'; import { refFilter } from 'lib/filters'; -import { safeDecodeURI } from 'lib/url'; -import Link from 'next/link'; -import classNames from 'classnames'; -import usePageQuery from 'hooks/usePageQuery'; -import External from 'assets/arrow-up-right-from-square.svg'; -import Icon from '../common/Icon'; -import styles from './ReferrersTable.module.css'; export const FILTER_DOMAIN_ONLY = 0; export const FILTER_COMBINED = 1; @@ -17,10 +11,6 @@ export const FILTER_RAW = 2; export default function ReferrersTable({ websiteId, websiteDomain, showFilters, ...props }) { const [filter, setFilter] = useState(FILTER_COMBINED); - const { - resolve, - query: { referrer: currentRef }, - } = usePageQuery(); const buttons = [ { @@ -34,24 +24,8 @@ export default function ReferrersTable({ websiteId, websiteDomain, showFilters, { label: , value: FILTER_RAW }, ]; - const renderLink = ({ w: link, x: label }) => { - return ( - - ); + const renderLink = ({ w: link, x: referrer }) => { + return ; }; return ( diff --git a/components/metrics/WebsiteChart.js b/components/metrics/WebsiteChart.js index 817addc1..03cc7b58 100644 --- a/components/metrics/WebsiteChart.js +++ b/components/metrics/WebsiteChart.js @@ -33,7 +33,7 @@ export default function WebsiteChart({ const { router, resolve, - query: { url, referrer }, + query: { url, referrer, os, browser, device, country }, } = usePageQuery(); const { get } = useApi(); @@ -47,11 +47,15 @@ export default function WebsiteChart({ tz: timezone, url, referrer, + os, + browser, + device, + country, }, onDataLoad, headers: { [TOKEN_HEADER]: shareToken?.token }, }, - [modified, url, referrer], + [modified, url, referrer, os, browser, device, country], ); const chartData = useMemo(() => { @@ -88,7 +92,10 @@ export default function WebsiteChart({ stickyClassName={styles.sticky} enabled={stickyHeader} > - +
diff --git a/lang/es-MX.json b/lang/es-MX.json index 690a3bd9..3f1f30c4 100644 --- a/lang/es-MX.json +++ b/lang/es-MX.json @@ -28,7 +28,7 @@ "label.enable-share-url": "Habilitar compartir URL", "label.invalid": "Inválido", "label.invalid-domain": "Dominio inválido", - "label.language": "Language", + "label.language": "Idioma", "label.last-days": "Últimos {x} días", "label.last-hours": "Últimas {x} horas", "label.logged-in-as": "Sesión iniciada como {username}", @@ -51,7 +51,7 @@ "label.settings": "Configuraciones", "label.share-url": "Compartir URL", "label.single-day": "Dia", - "label.theme": "Theme", + "label.theme": "Tema", "label.this-month": "Este mes", "label.this-week": "Esta semana", "label.this-year": "Este año", @@ -64,7 +64,7 @@ "label.websites": "Sitios", "message.active-users": "{x} {x, plural, one {activo} other {activos}}", "message.confirm-delete": "¿Estás seguro(a) de querer eliminar {target}?", - "message.confirm-reset": "¿Seguro que deseas restablecer las estadisticas de {target}?", + "message.confirm-reset": "¿Seguro que deseas restablecer las estadísticas de {target}?", "message.copied": "¡Copiado!", "message.delete-warning": "Toda la información relacionada será eliminada.", "message.failure": "Algo falló.", @@ -73,7 +73,7 @@ "message.go-to-settings": "Ir a la configuración", "message.incorrect-username-password": "Nombre de usuario o contraseña incorrectos.", "message.log.visitor": "Visitante desde {country} usando {browser} en {os} {device}", - "message.new-version-available": "¡Una nueva versíon de umami {version} esta disponible!", + "message.new-version-available": "¡Una nueva versión de umami {version} esta disponible!", "message.no-data-available": "Sin información disponible.", "message.no-websites-configured": "No tienes ningún sitio configurado.", "message.page-not-found": "Página no encontrada", diff --git a/lang/zh-CN.json b/lang/zh-CN.json index 506c99b8..4d787519 100644 --- a/lang/zh-CN.json +++ b/lang/zh-CN.json @@ -99,7 +99,7 @@ "metrics.filter.combined": "总和", "metrics.filter.domain-only": "只看域名", "metrics.filter.raw": "原始", - "metrics.languages": "语言", + "metrics.languages": "Languages", "metrics.operating-systems": "操作系统", "metrics.page-views": "页面浏览量", "metrics.pages": "网页", diff --git a/lang/zh-TW.json b/lang/zh-TW.json index 280dd9ad..d1964b21 100644 --- a/lang/zh-TW.json +++ b/lang/zh-TW.json @@ -4,8 +4,8 @@ "label.add-website": "增加網站", "label.administrator": "管理員", "label.all": "所有", - "label.all-events": "All events", - "label.all-time": "All time", + "label.all-events": "所有事件", + "label.all-time": "所有時間段", "label.all-websites": "全部網站", "label.back": "返回", "label.cancel": "取消", @@ -46,7 +46,7 @@ "label.refresh": "刷新", "label.required": "必填", "label.reset": "重置", - "label.reset-website": "Reset statistics", + "label.reset-website": "重置統計數據", "label.save": "保存", "label.settings": "設置", "label.share-url": "分享連結", @@ -64,7 +64,7 @@ "label.websites": "網站", "message.active-users": "當前線上 {x} 人", "message.confirm-delete": "你確定要刪除 {target} 嗎?", - "message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", + "message.confirm-reset": "您確定要重置 {target} 的數據嗎?", "message.copied": "複製成功!", "message.delete-warning": "所有相關數據將會被刪除。", "message.failure": "出現錯誤。", @@ -78,13 +78,13 @@ "message.no-websites-configured": "目前無任何網站設定。", "message.page-not-found": "網頁未找到。", "message.powered-by": "運行 {name}", - "message.reset-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.", + "message.reset-warning": "本網站的所有統計數據將被刪除,但您的跟蹤代碼將保持不變。", "message.save-success": "成功保存。", "message.share-url": "這是 {target} 的分享連結。", "message.toggle-charts": "Toggle charts", "message.track-stats": "將以下代碼放入被設定網站的 {head} 部分來收集 {target} 的資料。", - "message.type-delete": "在下方空格輸入 {delete} 確認", - "message.type-reset": "在下方空格輸入 {reset} 確認", + "message.type-delete": "在下方空格輸入 {delete} 確認删除", + "message.type-reset": "在下方空格輸入 {reset} 確認删除", "metrics.actions": "用戶行為", "metrics.average-visit-time": "平均訪問時間", "metrics.bounce-rate": "跳出率", diff --git a/lib/filters.js b/lib/filters.js index fb6b435a..f3237a0a 100644 --- a/lib/filters.js +++ b/lib/filters.js @@ -1,4 +1,3 @@ -import { BROWSERS } from './constants'; import { removeTrailingSlash, removeWWW, getDomainName } from './url'; export const urlFilter = (data, { raw }) => { @@ -113,8 +112,6 @@ export const refFilter = (data, { domain, domainOnly, raw }) => { return Object.keys(map).map(key => ({ x: key, y: map[key], w: links[key] })); }; -export const browserFilter = data => data.map(({ x, y }) => ({ x: BROWSERS[x] ?? x, y })); - export const eventTypeFilter = (data, types) => { if (!types || types.length === 0) { return data; diff --git a/lib/queries.js b/lib/queries.js index 7d3dd7a2..ffbb6655 100644 --- a/lib/queries.js +++ b/lib/queries.js @@ -21,24 +21,6 @@ export function getDatabase() { return type; } -export async function runQuery(query) { - return query.catch(e => { - throw e; - }); -} - -export async function rawQuery(query, params = []) { - const db = getDatabase(); - - if (db !== POSTGRESQL && db !== MYSQL) { - return Promise.reject(new Error('Unknown database.')); - } - - const sql = db === MYSQL ? query.replace(/\$[0-9]+/g, '?') : query; - - return prisma.$queryRawUnsafe.apply(prisma, [sql, ...params]); -} - export function getDateQuery(field, unit, timezone) { const db = getDatabase(); @@ -72,6 +54,101 @@ export function getTimestampInterval(field) { } } +export function getFilterQuery(table, filters = {}, params = []) { + const query = Object.keys(filters).reduce((arr, key) => { + const value = filters[key]; + + if (value === undefined) { + return arr; + } + + switch (key) { + case 'url': + if (table === 'session' || table === 'pageview') { + arr.push(`and ${table}.${key}=$${params.length + 1}`); + params.push(decodeURIComponent(value)); + } + break; + + case 'os': + case 'browser': + case 'device': + case 'country': + if (table === 'session') { + arr.push(`and ${table}.${key}=$${params.length + 1}`); + params.push(decodeURIComponent(value)); + } + break; + + case 'event_type': + if (table === 'event') { + arr.push(`and ${table}.${key}=$${params.length + 1}`); + params.push(decodeURIComponent(value)); + } + break; + + case 'referrer': + if (table === 'pageview') { + arr.push(`and ${table}.referrer like $${params.length + 1}`); + params.push(`%${decodeURIComponent(value)}%`); + } + break; + + case 'domain': + if (table === 'pageview') { + arr.push(`and ${table}.referrer not like $${params.length + 1}`); + arr.push(`and ${table}.referrer not like '/%'`); + params.push(`%://${value}/%`); + } + break; + } + + return arr; + }, []); + + return query.join('\n'); +} + +export function parseFilters(table, filters = {}, params = []) { + const { domain, url, referrer, os, browser, device, country, event_type } = filters; + + const pageviewFilters = { domain, url, referrer }; + const sessionFilters = { os, browser, device, country }; + const eventFilters = { event_type }; + + return { + pageviewFilters, + sessionFilters, + eventFilters, + event: { event_type }, + joinSession: + os || browser || device || country + ? `inner join session on ${table}.session_id = session.session_id` + : '', + pageviewQuery: getFilterQuery('pageview', pageviewFilters, params), + sessionQuery: getFilterQuery('session', sessionFilters, params), + eventQuery: getFilterQuery('event', eventFilters, params), + }; +} + +export async function runQuery(query) { + return query.catch(e => { + throw e; + }); +} + +export async function rawQuery(query, params = []) { + const db = getDatabase(); + + if (db !== POSTGRESQL && db !== MYSQL) { + return Promise.reject(new Error('Unknown database.')); + } + + const sql = db === MYSQL ? query.replace(/\$[0-9]+/g, '?') : query; + + return runQuery(prisma.$queryRawUnsafe.apply(prisma, [sql, ...params])); +} + export async function getWebsiteById(website_id) { return runQuery( prisma.website.findUnique({ @@ -344,19 +421,7 @@ export async function getEvents(websites, start_at) { export function getWebsiteStats(website_id, start_at, end_at, filters = {}) { const params = [website_id, start_at, end_at]; - const { url, referrer } = filters; - let urlFilter = ''; - let refFilter = ''; - - if (url) { - urlFilter = `and url=$${params.length + 1}`; - params.push(decodeURIComponent(url)); - } - - if (referrer) { - refFilter = `and referrer like $${params.length + 1}`; - params.push(`%${decodeURIComponent(referrer)}%`); - } + const { pageviewQuery, sessionQuery, joinSession } = parseFilters('pageview', filters, params); return rawQuery( ` @@ -365,15 +430,16 @@ export function getWebsiteStats(website_id, start_at, end_at, filters = {}) { sum(case when t.c = 1 then 1 else 0 end) as "bounces", sum(t.time) as "totaltime" from ( - select session_id, - ${getDateQuery('created_at', 'hour')}, + select pageview.session_id, + ${getDateQuery('pageview.created_at', 'hour')}, count(*) c, - ${getTimestampInterval('created_at')} as "time" + ${getTimestampInterval('pageview.created_at')} as "time" from pageview - where website_id=$1 - and created_at between $2 and $3 - ${urlFilter} - ${refFilter} + ${joinSession} + where pageview.website_id=$1 + and pageview.created_at between $2 and $3 + ${pageviewQuery} + ${sessionQuery} group by 1, 2 ) t `, @@ -391,30 +457,18 @@ export function getPageviewStats( filters = {}, ) { const params = [website_id, start_at, end_at]; - const { url, referrer } = filters; - - let urlFilter = ''; - let refFilter = ''; - - if (url) { - urlFilter = `and url=$${params.length + 1}`; - params.push(decodeURIComponent(url)); - } - - if (referrer) { - refFilter = `and referrer like $${params.length + 1}`; - params.push(`%${decodeURIComponent(referrer)}%`); - } + const { pageviewQuery, sessionQuery, joinSession } = parseFilters('pageview', filters, params); return rawQuery( ` - select ${getDateQuery('created_at', unit, timezone)} t, + select ${getDateQuery('pageview.created_at', unit, timezone)} t, count(${count}) y from pageview - where website_id=$1 - and created_at between $2 and $3 - ${urlFilter} - ${refFilter} + ${joinSession} + where pageview.website_id=$1 + and pageview.created_at between $2 and $3 + ${pageviewQuery} + ${sessionQuery} group by 1 order by 1 `, @@ -424,25 +478,20 @@ export function getPageviewStats( export function getSessionMetrics(website_id, start_at, end_at, field, filters = {}) { const params = [website_id, start_at, end_at]; - const { url } = filters; - - let urlFilter = ''; - - if (url) { - urlFilter = `and url=$${params.length + 1}`; - params.push(decodeURIComponent(url)); - } + const { pageviewQuery, sessionQuery, joinSession } = parseFilters('pageview', filters, params); return rawQuery( ` select ${field} x, count(*) y - from session - where session_id in ( - select session_id + from session as x + where x.session_id in ( + select pageview.session_id from pageview - where website_id=$1 - and created_at between $2 and $3 - ${urlFilter} + ${joinSession} + where pageview.website_id=$1 + and pageview.created_at between $2 and $3 + ${pageviewQuery} + ${sessionQuery} ) group by 1 order by 2 desc @@ -453,36 +502,18 @@ export function getSessionMetrics(website_id, start_at, end_at, field, filters = export function getPageviewMetrics(website_id, start_at, end_at, field, table, filters = {}) { const params = [website_id, start_at, end_at]; - const { domain, url, referrer } = filters; - - let domainFilter = ''; - let urlFilter = ''; - let refFilter = ''; - - if (domain) { - domainFilter = `and referrer not like $${params.length + 1} and referrer not like '/%'`; - params.push(`%://${domain}/%`); - } - - if (url) { - urlFilter = `and url=$${params.length + 1}`; - params.push(decodeURIComponent(url)); - } - - if (referrer) { - refFilter = `and referrer like $${params.length + 1}`; - params.push(`%${decodeURIComponent(referrer)}%`); - } + console.log({ table, filters }); + const { pageviewQuery, sessionQuery, joinSession } = parseFilters(table, filters, params); return rawQuery( ` select ${field} x, count(*) y from ${table} - where website_id=$1 - and created_at between $2 and $3 - ${domainFilter} - ${urlFilter} - ${refFilter} + ${joinSession} + where ${table}.website_id=$1 + and ${table}.created_at between $2 and $3 + ${pageviewQuery} + ${joinSession && sessionQuery} group by 1 order by 2 desc `, @@ -514,20 +545,6 @@ export function getEventMetrics( filters = {}, ) { const params = [website_id, start_at, end_at]; - const { url, event_type } = filters; - - let urlFilter = ''; - let eventTypeFilter = ''; - - if (url) { - urlFilter = `and url=$${params.length + 1}`; - params.push(decodeURIComponent(url)); - } - - if (event_type) { - eventTypeFilter = `and event_type=$${params.length + 1}`; - params.push(event_type); - } return rawQuery( ` @@ -538,8 +555,7 @@ export function getEventMetrics( from event where website_id=$1 and created_at between $2 and $3 - ${urlFilter} - ${eventTypeFilter} + ${getFilterQuery('event', filters, params)} group by 1, 2 order by 2 `, diff --git a/lib/session.js b/lib/session.js index 73d02e59..a08eec79 100644 --- a/lib/session.js +++ b/lib/session.js @@ -49,6 +49,10 @@ export async function getSession(req) { country, device, }); + + if (!session) { + return null; + } } catch (e) { if (!e.message.includes('Unique constraint')) { throw e; diff --git a/package.json b/package.json index 7ec9b3db..625fb0c2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "umami", - "version": "1.29.0", - "description": "A simple, fast, website analytics alternative to Google Analytics.", + "version": "1.30.0", + "description": "A simple, fast, privacy-focused alternative to Google Analytics.", "author": "Mike Cao ", "license": "MIT", "homepage": "https://umami.is", @@ -13,7 +13,6 @@ "dev": "next dev", "build": "npm-run-all build-tracker build-geo build-db build-app", "start": "next start", - "start-app": "next start", "start-env": "node -r dotenv/config scripts/start-env.js", "build-app": "next build", "build-tracker": "rollup -c rollup.tracker.config.js", @@ -38,8 +37,7 @@ "change-password": "node scripts/change-password.js", "lint": "next lint --quiet", "prepare": "husky install", - "postbuild": "node scripts/postbuild.js", - "init": "node scripts/prestart.js" + "postbuild": "node scripts/postbuild.js" }, "lint-staged": { "**/*.js": [ @@ -55,8 +53,8 @@ ] }, "dependencies": { - "@fontsource/inter": "4.5.5", - "@prisma/client": "3.11.1", + "@fontsource/inter": "4.5.7", + "@prisma/client": "3.12.0", "bcryptjs": "^2.4.3", "chalk": "^4.1.1", "chart.js": "^2.9.4", @@ -65,12 +63,16 @@ "cors": "^2.8.5", "date-fns": "^2.23.0", "date-fns-tz": "^1.1.4", + "del": "^6.0.0", "detect-browser": "^5.2.0", "dotenv": "^10.0.0", + "dotenv-cli": "^4.0.0", "formik": "^2.2.9", "fs-extra": "^10.0.1", "immer": "^9.0.12", "ipaddr.js": "^2.0.1", + "is-ci": "^3.0.1", + "is-docker": "^3.0.0", "is-localhost-ip": "^1.4.0", "isbot": "^3.4.5", "jose": "2.0.5", @@ -89,41 +91,36 @@ "react-use-measure": "^2.0.4", "react-window": "^1.8.6", "request-ip": "^2.1.3", - "semver": "^7.3.5", + "semver": "^7.3.6", "thenby": "^1.3.4", "timezone-support": "^2.0.2", "uuid": "^8.3.2", - "zustand": "^3.7.0" + "zustand": "^3.7.2" }, "devDependencies": { "@formatjs/cli": "^4.2.29", "@rollup/plugin-buble": "^0.21.3", "@svgr/webpack": "^6.2.1", - "async-retry": "^1.3.3", "cross-env": "^7.0.3", - "del": "^6.0.0", - "dotenv-cli": "^4.0.0", "eslint": "^7.32.0", "eslint-config-next": "^12.0.1", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.0.0", "extract-react-intl-messages": "^4.1.1", "husky": "^7.0.0", - "is-ci": "^3.0.1", - "is-docker": "^3.0.0", "lint-staged": "^11.0.0", "postcss": "^8.4.12", "postcss-flexbugs-fixes": "^5.0.2", "postcss-import": "^14.0.2", "postcss-preset-env": "^7.4.2", - "postcss-rtlcss": "^3.5.3", - "prettier": "^2.6.0", - "prisma": "3.11.1", + "postcss-rtlcss": "^3.6.1", + "prettier": "^2.6.2", + "prisma": "3.12.0", "prompts": "2.4.2", "rollup": "^2.70.1", "rollup-plugin-terser": "^7.0.2", "stylelint": "^14.5.3", - "stylelint-config-css-modules": "^3.0.0", + "stylelint-config-css-modules": "^4.1.0", "stylelint-config-prettier": "^9.0.3", "stylelint-config-recommended": "^7.0.0", "tar": "^6.1.2" diff --git a/pages/_app.js b/pages/_app.js index 5c83c264..1e03db15 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -24,6 +24,7 @@ const Intl = ({ children }) => { export default function App({ Component, pageProps }) { const { basePath } = useRouter(); const { dir } = useLocale(); + const version = process.env.VERSION; return ( @@ -34,6 +35,12 @@ export default function App({ Component, pageProps }) { + diff --git a/pages/_middleware.js b/pages/_middleware.js index 64dce867..0c58633b 100644 --- a/pages/_middleware.js +++ b/pages/_middleware.js @@ -1,24 +1,14 @@ import { NextResponse } from 'next/server'; -function redirectHTTPS(req) { - const host = req.headers.get('host'); - if ( - process.env.FORCE_SSL && - process.env.NODE_ENV === 'production' && - req.nextUrl.protocol === 'http:' - ) { - return NextResponse.redirect(`https://${host}${req.nextUrl.pathname}`, 301); - } -} - function customScriptName(req) { const scriptName = process.env.TRACKER_SCRIPT_NAME; if (scriptName) { const url = req.nextUrl.clone(); const { pathname } = url; + const names = scriptName.split(',').map(name => (name + '.js').trim()); - if (pathname.endsWith(`/${scriptName}.js`)) { + if (names.find(name => pathname.endsWith(name))) { url.pathname = '/umami.js'; return NextResponse.rewrite(url); } @@ -31,8 +21,16 @@ function disableLogin(req) { } } +function forceSSL(req, res) { + if (process.env.FORCE_SSL && req.nextUrl.protocol === 'http:') { + res.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); + } + + return res; +} + export function middleware(req) { - const fns = [redirectHTTPS, customScriptName, disableLogin]; + const fns = [customScriptName, disableLogin]; for (const fn of fns) { const res = fn(req); @@ -41,5 +39,5 @@ export function middleware(req) { } } - return NextResponse.next(); + return forceSSL(req, NextResponse.next()); } diff --git a/pages/api/collect.js b/pages/api/collect.js index 683a1c93..c9159858 100644 --- a/pages/api/collect.js +++ b/pages/api/collect.js @@ -14,8 +14,9 @@ export default async (req, res) => { return ok(res); } - if (process.env.IGNORE_IP) { - const ips = process.env.IGNORE_IP.split(',').map(n => n.trim()); + const ignoreIps = process.env.IGNORE_IP; + if (ignoreIps) { + const ips = ignoreIps.split(',').map(n => n.trim()); const ip = getIpAddress(req); const blocked = ips.find(i => { if (i === ip) return true; diff --git a/pages/api/website/[id]/metrics.js b/pages/api/website/[id]/metrics.js index 7e74f044..645a8707 100644 --- a/pages/api/website/[id]/metrics.js +++ b/pages/api/website/[id]/metrics.js @@ -33,22 +33,31 @@ export default async (req, res) => { return unauthorized(res); } - const { id, type, start_at, end_at, url, referrer } = req.query; + const { id, type, start_at, end_at, url, referrer, os, browser, device, country } = req.query; const websiteId = +id; const startDate = new Date(+start_at); const endDate = new Date(+end_at); if (sessionColumns.includes(type)) { - let data = await getSessionMetrics(websiteId, startDate, endDate, type, { url }); + let data = await getSessionMetrics(websiteId, startDate, endDate, type, { + os, + browser, + device, + country, + }); if (type === 'language') { let combined = {}; for (let { x, y } of data) { x = String(x).toLowerCase().split('-')[0]; - if (!combined[x]) combined[x] = { x, y }; - else combined[x].y += y; + + if (!combined[x]) { + combined[x] = { x, y }; + } else { + combined[x].y += y; + } } data = Object.values(combined); @@ -69,18 +78,18 @@ export default async (req, res) => { domain = website.domain; } - const data = await getPageviewMetrics( - websiteId, - startDate, - endDate, - getColumn(type), - getTable(type), - { - domain, - url: type !== 'url' && url, - referrer, - }, - ); + const column = getColumn(type); + const table = getTable(type); + + const data = await getPageviewMetrics(websiteId, startDate, endDate, column, table, { + domain, + url: type !== 'url' ? url : undefined, + referrer: type !== 'referrer' ? referrer : undefined, + os: type !== 'os' ? os : undefined, + browser: type !== 'browser' ? browser : undefined, + device: type !== 'device' ? device : undefined, + country: type !== 'country' ? country : undefined, + }); return ok(res, data); } diff --git a/pages/api/website/[id]/pageviews.js b/pages/api/website/[id]/pageviews.js index 41ea06eb..bc663ce1 100644 --- a/pages/api/website/[id]/pageviews.js +++ b/pages/api/website/[id]/pageviews.js @@ -14,7 +14,8 @@ export default async (req, res) => { return unauthorized(res); } - const { id, start_at, end_at, unit, tz, url, referrer } = req.query; + const { id, start_at, end_at, unit, tz, url, referrer, os, browser, device, country } = + req.query; const websiteId = +id; const startDate = new Date(+start_at); @@ -25,10 +26,20 @@ export default async (req, res) => { } const [pageviews, sessions] = await Promise.all([ - getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', { url, referrer }), - getPageviewStats(websiteId, startDate, endDate, tz, unit, 'distinct session_id', { + getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', { url, referrer, + os, + browser, + device, + country, + }), + getPageviewStats(websiteId, startDate, endDate, tz, unit, 'distinct pageview.session_id', { + url, + os, + browser, + device, + country, }), ]); diff --git a/pages/api/website/[id]/stats.js b/pages/api/website/[id]/stats.js index 7b1c5cf0..15cc45ad 100644 --- a/pages/api/website/[id]/stats.js +++ b/pages/api/website/[id]/stats.js @@ -11,7 +11,7 @@ export default async (req, res) => { return unauthorized(res); } - const { id, start_at, end_at, url, referrer } = req.query; + const { id, start_at, end_at, url, referrer, os, browser, device, country } = req.query; const websiteId = +id; const startDate = new Date(+start_at); @@ -21,10 +21,21 @@ export default async (req, res) => { const prevStartDate = new Date(+start_at - distance); const prevEndDate = new Date(+end_at - distance); - const metrics = await getWebsiteStats(websiteId, startDate, endDate, { url, referrer }); + const metrics = await getWebsiteStats(websiteId, startDate, endDate, { + url, + referrer, + os, + browser, + device, + country, + }); const prevPeriod = await getWebsiteStats(websiteId, prevStartDate, prevEndDate, { url, referrer, + os, + browser, + device, + country, }); const stats = Object.keys(metrics[0]).reduce((obj, key) => { diff --git a/public/intl/messages/es-MX.json b/public/intl/messages/es-MX.json index 6440920d..6c2d9dc7 100644 --- a/public/intl/messages/es-MX.json +++ b/public/intl/messages/es-MX.json @@ -176,7 +176,7 @@ "label.language": [ { "type": 0, - "value": "Language" + "value": "Idioma" } ], "label.last-days": [ @@ -334,7 +334,7 @@ "label.theme": [ { "type": 0, - "value": "Theme" + "value": "Tema" } ], "label.this-month": [ @@ -448,7 +448,7 @@ "message.confirm-reset": [ { "type": 0, - "value": "¿Seguro que deseas restablecer las estadisticas de " + "value": "¿Seguro que deseas restablecer las estadísticas de " }, { "type": 1, @@ -538,7 +538,7 @@ "message.new-version-available": [ { "type": 0, - "value": "¡Una nueva versíon de umami " + "value": "¡Una nueva versión de umami " }, { "type": 1, diff --git a/public/intl/messages/zh-CN.json b/public/intl/messages/zh-CN.json index a91b79e7..a6ef879c 100644 --- a/public/intl/messages/zh-CN.json +++ b/public/intl/messages/zh-CN.json @@ -734,7 +734,7 @@ "metrics.languages": [ { "type": 0, - "value": "语言" + "value": "Languages" } ], "metrics.operating-systems": [ diff --git a/public/intl/messages/zh-TW.json b/public/intl/messages/zh-TW.json index 5c3eb34c..aa77bd37 100644 --- a/public/intl/messages/zh-TW.json +++ b/public/intl/messages/zh-TW.json @@ -32,13 +32,13 @@ "label.all-events": [ { "type": 0, - "value": "All events" + "value": "所有事件" } ], "label.all-time": [ { "type": 0, - "value": "All time" + "value": "所有時間段" } ], "label.all-websites": [ @@ -304,7 +304,7 @@ "label.reset-website": [ { "type": 0, - "value": "Reset statistics" + "value": "重置統計數據" } ], "label.save": [ @@ -428,7 +428,7 @@ "message.confirm-reset": [ { "type": 0, - "value": "Are your sure you want to reset " + "value": "您確定要重置 " }, { "type": 1, @@ -436,7 +436,7 @@ }, { "type": 0, - "value": "'s statistics?" + "value": " 的數據嗎?" } ], "message.copied": [ @@ -564,7 +564,7 @@ "message.reset-warning": [ { "type": 0, - "value": "All statistics for this website will be deleted, but your tracking code will remain intact." + "value": "本網站的所有統計數據將被刪除,但您的跟蹤代碼將保持不變。" } ], "message.save-success": [ @@ -626,7 +626,7 @@ }, { "type": 0, - "value": " 確認" + "value": " 確認删除" } ], "message.type-reset": [ @@ -640,7 +640,7 @@ }, { "type": 0, - "value": " 確認" + "value": " 確認删除" } ], "metrics.actions": [ diff --git a/scripts/prestart.js b/scripts/prestart.js deleted file mode 100644 index a13af855..00000000 --- a/scripts/prestart.js +++ /dev/null @@ -1,10 +0,0 @@ -require('dotenv').config(); -const { sendTelemetry } = require('./telemetry'); - -async function run() { - if (!process.env.DISABLE_TELEMETRY) { - await sendTelemetry('start'); - } -} - -run(); diff --git a/scripts/telemetry.js b/scripts/telemetry.js index ee44746a..100e6864 100644 --- a/scripts/telemetry.js +++ b/scripts/telemetry.js @@ -1,7 +1,6 @@ const fs = require('fs-extra'); const path = require('path'); const os = require('os'); -const retry = require('async-retry'); const isCI = require('is-ci'); const pkg = require('../package.json'); @@ -9,8 +8,6 @@ const dest = path.resolve(__dirname, '../.next/cache/umami.json'); const url = 'https://telemetry.umami.is/api/collect'; async function sendTelemetry(action) { - await fs.ensureFile(dest); - let json = {}; try { @@ -19,7 +16,11 @@ async function sendTelemetry(action) { // Ignore } - await fs.writeJSON(dest, { version: pkg.version }); + try { + await fs.writeJSON(dest, { version: pkg.version }); + } catch { + // Ignore + } const { default: isDocker } = await import('is-docker'); const { default: fetch } = await import('node-fetch'); @@ -38,19 +39,18 @@ async function sendTelemetry(action) { upgrade, }; - await retry( - async () => { - await fetch(url, { - method: 'post', - cache: 'no-cache', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }); - }, - { minTimeout: 500, retries: 1, factor: 1 }, - ).catch(() => {}); + try { + await fetch(url, { + method: 'post', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + } catch { + // Ignore + } } module.exports = { diff --git a/store/app.js b/store/app.js index 6aa85fa6..65295fd0 100644 --- a/store/app.js +++ b/store/app.js @@ -7,7 +7,7 @@ import { THEME_CONFIG, DEFAULT_WEBSITE_LIMIT, } from 'lib/constants'; -import { getItem } from 'lib/web'; +import { getItem, setItem } from 'lib/web'; export const defaultDashboardConfig = { showCharts: true, @@ -42,6 +42,7 @@ export function setUser(user) { export function setDashboard(dashboard) { store.setState({ dashboard }); + setItem(DASHBOARD_CONFIG, dashboard); } export default store; diff --git a/yarn.lock b/yarn.lock index 50480f8d..aede6d16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1080,10 +1080,10 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@fontsource/inter@4.5.5": - version "4.5.5" - resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-4.5.5.tgz#d4e52e8a0c84b93abae46efd4c43bd7a7f23907f" - integrity sha512-mWnePEroLfaJQWmynipzOVcH6JwT8Jta3+yLsC5Pm/snHBXnOiAOnjBqYjKnvXwJ4eUPt2AaAhyrtwCgWQRGOg== +"@fontsource/inter@4.5.7": + version "4.5.7" + resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-4.5.7.tgz#f0657c52105ed51e04ac636a25d6016896c5ee9d" + integrity sha512-25k3thupaOEBexuU+jAkGqieKPbuhSuA+sinDwp1iBNhqQPiJ9QHDvsXgoCgCbZ4sGlE8aCwZmSlDJrPdJHNkw== "@formatjs/cli@^4.2.29": version "4.8.3" @@ -1349,22 +1349,22 @@ resolved "https://registry.yarnpkg.com/@panva/asn1.js/-/asn1.js-1.0.0.tgz#dd55ae7b8129e02049f009408b97c61ccf9032f6" integrity sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw== -"@prisma/client@3.11.1": - version "3.11.1" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.11.1.tgz#bde6dec71ae133d04ce1c6658e3d76627a3c6dc7" - integrity sha512-B3C7zQG4HbjJzUr2Zg9UVkBJutbqq9/uqkl1S138+keZCubJrwizx3RuIvGwI+s+pm3qbsyNqXiZgL3Ir0fSng== +"@prisma/client@3.12.0": + version "3.12.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.12.0.tgz#a0eb49ffea5c128dd11dffb896d7139a60073d12" + integrity sha512-4NEQjUcWja/NVBvfuDFscWSk1/rXg3+wj+TSkqXCb1tKlx/bsUE00rxsvOvGg7VZ6lw1JFpGkwjwmsOIc4zvQw== dependencies: - "@prisma/engines-version" "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9" + "@prisma/engines-version" "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" -"@prisma/engines-version@3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9": - version "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9.tgz#81a1835b495ad287ad7824dbd62f74e9eee90fb9" - integrity sha512-HkcsDniA4iNb/gi0iuyOJNAM7nD/LwQ0uJm15v360O5dee3TM4lWdSQiTYBMK6FF68ACUItmzSur7oYuUZ2zkQ== +"@prisma/engines-version@3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980": + version "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980.tgz#829ca3d9d0d92555f44644606d4edfd45b2f5886" + integrity sha512-o+jo8d7ZEiVpcpNWUDh3fj2uPQpBxl79XE9ih9nkogJbhw6P33274SHnqheedZ7PyvPIK/mvU8MLNYgetgXPYw== -"@prisma/engines@3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9": - version "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9.tgz#09ac23f8f615a8586d8d44538060ada199fe872c" - integrity sha512-MILbsGnvmnhCbFGa2/iSnsyGyazU3afzD7ldjCIeLIGKkNBMSZgA2IvpYsAXl+6qFHKGrS3B2otKfV31dwMSQw== +"@prisma/engines@3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980": + version "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980.tgz#e52e364084c4d05278f62768047b788665e64a45" + integrity sha512-zULjkN8yhzS7B3yeEz4aIym4E2w1ChrV12i14pht3ePFufvsAvBSoZ+tuXMvfSoNTgBS5E4bolRzLbMmbwkkMQ== "@react-spring/animated@~9.4.4": version "9.4.4" @@ -2031,13 +2031,6 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== -async-retry@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" - integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== - dependencies: - retry "0.13.1" - at-least-node@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" @@ -4119,6 +4112,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-cache@^7.4.0: + version "7.8.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.8.0.tgz#649aaeb294a56297b5cbc5d70f198dcc5ebe5747" + integrity sha512-AmXqneQZL3KZMIgBpaPTeI6pfwh+xQ2vutMsyqOu1TBdEXFZgpG/80wuJ531w2ZN7TI0/oc8CPxzh/DKQudZqg== + magic-string@^0.25.0, magic-string@^0.25.7: version "0.25.9" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" @@ -4931,10 +4929,10 @@ postcss-resolve-nested-selector@^0.1.1: resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e" integrity sha1-Kcy8fDfe36wwTp//C/FZaz9qDk4= -postcss-rtlcss@^3.5.3: - version "3.5.4" - resolved "https://registry.yarnpkg.com/postcss-rtlcss/-/postcss-rtlcss-3.5.4.tgz#b0aac0641cbba1fc566cbdc044239b3da7d8f9f0" - integrity sha512-5GpWTmBqeM10rRcFBwaKYCg7M6jiytfSS6pwrphTZnNSioxYNUMZH+88xh1oO4AF7Ix9y2qC76/gQq1HIuUf+g== +postcss-rtlcss@^3.6.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/postcss-rtlcss/-/postcss-rtlcss-3.6.1.tgz#e530e08bfe36b01a49167e015e5f047a632ca9b4" + integrity sha512-eliUNvupb9AYn3JpW+L3aaOWeAPh8n6NPVynAoju2mimhO1dz5PTPQAlnhnzYwpqSusTxgZxYjr+RviE5MmSLg== dependencies: rtlcss "^3.5.0" @@ -4958,7 +4956,15 @@ postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: +postcss-selector-parser@^6.0.6: + version "6.0.10" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d" + integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== @@ -4993,17 +4999,17 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@^2.6.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.1.tgz#d472797e0d7461605c1609808e27b80c0f9cfe17" - integrity sha512-8UVbTBYGwN37Bs9LERmxCPjdvPxlEowx2urIL6urHzdb3SDq4B/Z6xLFCblrSnE4iKWcS6ziJ3aOYrc1kz/E2A== +prettier@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032" + integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew== -prisma@3.11.1: - version "3.11.1" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.11.1.tgz#fff9c0bcf83cb30c2e1d650882d5eb3c5565e028" - integrity sha512-aYn8bQwt1xwR2oSsVNHT4PXU7EhsThIwmpNB/MNUaaMx5OPLTro6VdNJe/sJssXFLxhamfWeMjwmpXjljo6xkg== +prisma@3.12.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.12.0.tgz#9675e0e72407122759d3eadcb6d27cdccd3497bd" + integrity sha512-ltCMZAx1i0i9xuPM692Srj8McC665h6E5RqJom999sjtVSccHSD8Z+HSdBN2183h9PJKvC5dapkn78dd0NWMBg== dependencies: - "@prisma/engines" "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9" + "@prisma/engines" "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" progress@^2.0.0: version "2.0.3" @@ -5327,11 +5333,6 @@ restore-cursor@^3.1.0: onetime "^5.1.0" signal-exit "^3.0.2" -retry@0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" - integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== - reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -5454,6 +5455,13 @@ semver@^7.2.1, semver@^7.3.4, semver@^7.3.5: dependencies: lru-cache "^6.0.0" +semver@^7.3.6: + version "7.3.6" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.6.tgz#5d73886fb9c0c6602e79440b97165c29581cbb2b" + integrity sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w== + dependencies: + lru-cache "^7.4.0" + serialize-javascript@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" @@ -5719,10 +5727,12 @@ styled-jsx@5.0.1: resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.1.tgz#78fecbbad2bf95ce6cd981a08918ce4696f5fc80" integrity sha512-+PIZ/6Uk40mphiQJJI1202b+/dYeTVd9ZnMPR80pgiWbjIwvN2zIp4r9et0BgqBuShh48I0gttPlAXA7WVvBxw== -stylelint-config-css-modules@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/stylelint-config-css-modules/-/stylelint-config-css-modules-3.0.0.tgz#441bdca77d2ca3cb978524134fdfeb8f51c236ee" - integrity sha512-NDvOK4M98r6XGZyYLEwd+1LSqVm8ReXERbdppnhRy25QzuGcjgYvBNA1v/BxeMXq8DCljg0h4uEbQy+av/37JQ== +stylelint-config-css-modules@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/stylelint-config-css-modules/-/stylelint-config-css-modules-4.1.0.tgz#b507bc074ba5bfda9f40f0be79b540db249f0c78" + integrity sha512-w6d552NscwvpUEaUcmq8GgWXKRv6lVHLbDj6QIHSM2vCWr83qRqRvXBJCfXDyaG/J3Zojw2inU9VvU99ZlXuUw== + optionalDependencies: + stylelint-scss "^4.2.0" stylelint-config-prettier@^9.0.3: version "9.0.3" @@ -5734,6 +5744,17 @@ stylelint-config-recommended@^7.0.0: resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-7.0.0.tgz#7497372ae83ab7a6fffc18d7d7b424c6480ae15e" integrity sha512-yGn84Bf/q41J4luis1AZ95gj0EQwRX8lWmGmBwkwBNSkpGSpl66XcPTulxGa/Z91aPoNGuIGBmFkcM1MejMo9Q== +stylelint-scss@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-4.2.0.tgz#e25fd390ee38a7e89fcfaec2a8f9dce2ec6ddee8" + integrity sha512-HHHMVKJJ5RM9pPIbgJ/XA67h9H0407G68Rm69H4fzFbFkyDMcTV1Byep3qdze5+fJ3c0U7mJrbj6S0Fg072uZA== + dependencies: + lodash "^4.17.21" + postcss-media-query-parser "^0.2.3" + postcss-resolve-nested-selector "^0.1.1" + postcss-selector-parser "^6.0.6" + postcss-value-parser "^4.1.0" + stylelint@^14.5.3: version "14.6.1" resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-14.6.1.tgz#aff137b0254515fc36b91921d88a3eb2edc194bf" @@ -6236,7 +6257,7 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -zustand@^3.7.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.7.1.tgz#7388f0a7175a6c2fd9a2880b383a4bf6cdf6b7c6" - integrity sha512-wHBCZlKj+bg03/hP+Tzv24YhnqqP8MCeN9ECPDXoF01062SIbnfl3j9O0znkDw1lNTY0a8WN3F///a0UhhaEqg== +zustand@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.7.2.tgz#7b44c4f4a5bfd7a8296a3957b13e1c346f42514d" + integrity sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==