From 968574dda19d2a042984aa59767632c2b6e74e09 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 25 May 2023 20:07:58 -0700 Subject: [PATCH 001/163] add ownerId to non-cache. --- lib/session.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/session.ts b/lib/session.ts index 32f3bdc8..1fedb91b 100644 --- a/lib/session.ts +++ b/lib/session.ts @@ -63,6 +63,7 @@ export async function findSession(req: NextApiRequestCollect) { subdivision1, subdivision2, city, + ownerId: website.userId, }; } From 7e587198dddac20fbb0744adb490797c15f1cafc Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 1 Jun 2023 12:49:28 -0700 Subject: [PATCH 002/163] Add incr on block. --- lib/cache.ts | 6 ++++++ lib/session.ts | 2 ++ 2 files changed, 8 insertions(+) diff --git a/lib/cache.ts b/lib/cache.ts index e63a53bb..7ee7bf28 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -54,6 +54,11 @@ async function fetchUserBlock(userId: string) { return redis.get(key); } +async function incrementUserBlock(userId: string) { + const key = `user:block:${userId}`; + return redis.incr(key); +} + export default { fetchWebsite, storeWebsite, @@ -65,5 +70,6 @@ export default { storeSession, deleteSession, fetchUserBlock, + incrementUserBlock, enabled: redis.enabled, }; diff --git a/lib/session.ts b/lib/session.ts index 1fedb91b..7fa06215 100644 --- a/lib/session.ts +++ b/lib/session.ts @@ -99,6 +99,8 @@ export async function findSession(req: NextApiRequestCollect) { async function checkUserBlock(userId: string) { if (process.env.ENABLE_BLOCKER && (await cache.fetchUserBlock(userId))) { + await cache.incrementUserBlock(userId); + throw new Error('Usage Limit.'); } } From 499ba9634891e88fbca5e8b396c9a71781684107 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 26 Jul 2023 10:49:19 -0700 Subject: [PATCH 003/163] Update reset website logic. --- pages/api/websites/[id]/reset.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/api/websites/[id]/reset.ts b/pages/api/websites/[id]/reset.ts index dc98c591..23b5305d 100644 --- a/pages/api/websites/[id]/reset.ts +++ b/pages/api/websites/[id]/reset.ts @@ -1,5 +1,5 @@ import { NextApiRequestQueryBody } from 'lib/types'; -import { canViewWebsite } from 'lib/auth'; +import { canUpdateWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -19,7 +19,7 @@ export default async ( const { id: websiteId } = req.query; if (req.method === 'POST') { - if (!(await canViewWebsite(req.auth, websiteId))) { + if (!(await canUpdateWebsite(req.auth, websiteId))) { return unauthorized(res); } From d6a27b8e99db9bc4566bfe4f1af58cb519f23c2e Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 25 Aug 2023 11:54:44 -0700 Subject: [PATCH 004/163] Initial dev on DataTable component. --- package.json | 2 +- src/components/common/DataTable.js | 68 ++++++++++++++++++ src/components/common/DataTable.module.css | 17 +++++ src/components/common/Pager.js | 11 +-- src/components/common/Pager.module.css | 5 +- src/components/hooks/useDataTable.js | 13 ++++ src/components/hooks/usePaging.js | 9 +++ src/components/messages.js | 1 + src/components/metrics/EventsChart.js | 6 +- .../metrics/{DataTable.js => ListTable.js} | 6 +- ...aTable.module.css => ListTable.module.css} | 0 src/components/metrics/MetricsTable.js | 4 +- .../pages/realtime/RealtimeCountries.js | 4 +- src/components/pages/realtime/RealtimeUrls.js | 6 +- .../pages/reports/funnel/FunnelTable.js | 4 +- .../pages/settings/websites/WebsitesList.js | 22 +++--- .../pages/settings/websites/WebsitesTable.js | 69 ++++++++++++++++++- src/components/pages/websites/WebsitesPage.js | 21 +++--- yarn.lock | 8 +-- 19 files changed, 223 insertions(+), 53 deletions(-) create mode 100644 src/components/common/DataTable.js create mode 100644 src/components/common/DataTable.module.css create mode 100644 src/components/hooks/useDataTable.js create mode 100644 src/components/hooks/usePaging.js rename src/components/metrics/{DataTable.js => ListTable.js} (96%) rename src/components/metrics/{DataTable.module.css => ListTable.module.css} (100%) diff --git a/package.json b/package.json index 1ff1730d..c23814dc 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", "react": "^18.2.0", - "react-basics": "^0.98.0", + "react-basics": "^0.100.0", "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.4", diff --git a/src/components/common/DataTable.js b/src/components/common/DataTable.js new file mode 100644 index 00000000..cb739344 --- /dev/null +++ b/src/components/common/DataTable.js @@ -0,0 +1,68 @@ +import { createContext } from 'react'; +import { SearchField } from 'react-basics'; +import { useDataTable } from 'components/hooks/useDataTable'; +import { useMessages } from 'components/hooks'; +import Empty from 'components/common/Empty'; +import Pager from 'components/common/Pager'; +import styles from './DataTable.module.css'; + +const DEFAULT_SEARCH_DELAY = 1000; + +export const DataTableStyles = styles; + +export const DataTableContext = createContext(null); + +export function DataTable({ + searchDelay, + showSearch = true, + showPaging = true, + children, + onChange, +}) { + const { formatMessage, labels, messages } = useMessages(); + const dataTable = useDataTable(); + const { query, setQuery, data, pageInfo, setPageInfo } = dataTable; + const { page, pageSize, count } = pageInfo || {}; + const noResults = Boolean(query && data?.length === 0); + + const handleChange = () => { + onChange?.({ query, page }); + }; + + const handleSearch = value => { + setQuery(value); + handleChange(); + }; + + const handlePageChange = page => { + setPageInfo(state => ({ ...state, page })); + }; + + return ( + + {showSearch && ( + + )} + {noResults && } +
{children}
+ {showPaging && ( + + )} +
+ ); +} + +export default DataTable; diff --git a/src/components/common/DataTable.module.css b/src/components/common/DataTable.module.css new file mode 100644 index 00000000..883110da --- /dev/null +++ b/src/components/common/DataTable.module.css @@ -0,0 +1,17 @@ +.search { + max-width: 300px; + margin: 20px 0; +} + +.action { + justify-content: flex-end; + gap: 5px; +} + +.body td { + align-items: center; +} + +.pager { + margin-top: 20px; +} diff --git a/src/components/common/Pager.js b/src/components/common/Pager.js index 7a5e7ed5..3f94edb0 100644 --- a/src/components/common/Pager.js +++ b/src/components/common/Pager.js @@ -1,14 +1,15 @@ -import styles from './Pager.module.css'; +import classNames from 'classnames'; import { Button, Flexbox, Icon, Icons } from 'react-basics'; import useMessages from 'components/hooks/useMessages'; +import styles from './Pager.module.css'; -export function Pager({ page, pageSize, count, onPageChange }) { +export function Pager({ page, pageSize, count, onPageChange, className }) { const { formatMessage, labels } = useMessages(); - const maxPage = Math.ceil(count / pageSize); + const maxPage = pageSize && count ? Math.ceil(count / pageSize) : 0; const lastPage = page === maxPage; const firstPage = page === 1; - if (count === 0) { + if (count === 0 || !maxPage) { return null; } @@ -24,7 +25,7 @@ export function Pager({ page, pageSize, count, onPageChange }) { } return ( - + + + )} + + + + + ); + }} + + + )} + + ); +} + +export function WebsitesTable2({ data = [], filterValue, onFilterChange, diff --git a/src/components/pages/websites/WebsitesPage.js b/src/components/pages/websites/WebsitesPage.js index 2eb060d3..a83d13d5 100644 --- a/src/components/pages/websites/WebsitesPage.js +++ b/src/components/pages/websites/WebsitesPage.js @@ -19,16 +19,19 @@ import { useToasts, } from 'react-basics'; +const TABS = { + myWebsites: 'my-websites', + teamWebsites: 'team-websites', +}; + export function WebsitesPage() { const { formatMessage, labels, messages } = useMessages(); - const [tab, setTab] = useState('my-websites'); - const [fetch, setFetch] = useState(1); + const [tab, setTab] = useState(TABS.myWebsites); const { user } = useUser(); const { cloudMode } = useConfig(); const { showToast } = useToasts(); - const handleSave = async () => { - setFetch(fetch + 1); + const handleSave = () => { showToast({ message: formatMessage(messages.saved), variant: 'success' }); }; @@ -54,18 +57,16 @@ export function WebsitesPage() { {!cloudMode && addButton} - {formatMessage(labels.myWebsites)} - {formatMessage(labels.teamWebsites)} + {formatMessage(labels.myWebsites)} + {formatMessage(labels.teamWebsites)} - - {tab === 'my-websites' && ( + {tab === TABS.myWebsites && ( )} - {tab === 'team-webaites' && ( + {tab === TABS.teamWebsites && ( diff --git a/yarn.lock b/yarn.lock index c20730f3..e824ca90 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7642,10 +7642,10 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-basics@^0.98.0: - version "0.98.0" - resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.98.0.tgz#b207bedbd9dac749d28ea6de2197a0efe648b78c" - integrity sha512-ebUigu+s6Iusq14EZTFTTUzdDPYFQEZjeD4feeq3o7dE+ndOVnajEdQ2va/x6CsRBUsWgjLJipfQi0XIrxYupA== +react-basics@^0.100.0: + version "0.100.0" + resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.100.0.tgz#14a36769af89f3e01641997f897e4073f16f5035" + integrity sha512-ET6DX/FYAcjGRauBE4jwqwVpd/hKmA2Nu/fi1dakwsv17hkyV5FEAhdWhQAxJX3VnaCH//QysN8+ae12KuNA9g== dependencies: classnames "^2.3.1" date-fns "^2.29.3" From 6846355c6355687d9dff1b8e4437ececb797554a Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 22 Sep 2023 00:59:00 -0700 Subject: [PATCH 005/163] DataTable refactor. --- src/components/common/DataTable.js | 6 +-- .../pages/settings/websites/WebsitesList.js | 20 ++++---- .../pages/settings/websites/WebsitesTable.js | 5 +- src/lib/schema.ts | 13 +++++ src/lib/types.ts | 8 ++-- src/lib/yup.ts | 19 -------- src/pages/api/me/teams.ts | 4 +- src/pages/api/me/websites.ts | 4 +- src/pages/api/reports/index.ts | 4 +- src/pages/api/teams/[id]/websites/index.ts | 7 ++- src/pages/api/teams/index.ts | 9 ++-- src/pages/api/users/[id]/teams.ts | 13 ++--- src/pages/api/users/[id]/websites.ts | 8 ++-- src/pages/api/users/index.ts | 4 +- src/pages/api/websites/index.ts | 4 +- src/queries/admin/team.ts | 33 ++++++------- src/queries/admin/website.ts | 47 +++++++------------ 17 files changed, 94 insertions(+), 114 deletions(-) create mode 100644 src/lib/schema.ts delete mode 100644 src/lib/yup.ts diff --git a/src/components/common/DataTable.js b/src/components/common/DataTable.js index cb739344..2662fa2c 100644 --- a/src/components/common/DataTable.js +++ b/src/components/common/DataTable.js @@ -25,13 +25,13 @@ export function DataTable({ const { page, pageSize, count } = pageInfo || {}; const noResults = Boolean(query && data?.length === 0); - const handleChange = () => { - onChange?.({ query, page }); + const handleChange = value => { + onChange?.({ query: value, page }); }; const handleSearch = value => { setQuery(value); - handleChange(); + handleChange(value); }; const handlePageChange = page => { diff --git a/src/components/pages/settings/websites/WebsitesList.js b/src/components/pages/settings/websites/WebsitesList.js index 70bbbd92..4761ad0a 100644 --- a/src/components/pages/settings/websites/WebsitesList.js +++ b/src/components/pages/settings/websites/WebsitesList.js @@ -7,7 +7,7 @@ import useMessages from 'components/hooks/useMessages'; import useUser from 'components/hooks/useUser'; import { ROLES } from 'lib/constants'; import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; export function WebsitesList({ showTeam, @@ -20,16 +20,20 @@ export function WebsitesList({ const { user } = useUser(); const [params, setParams] = useState({}); const { get, useQuery } = useApi(); - const { data, isLoading, error, refetch } = useQuery( - ['websites', includeTeams, onlyTeams], - () => - get(`/users/${user?.id}/websites`, { + const count = useRef(0); + const q = useQuery( + ['websites', includeTeams, onlyTeams, params], + () => { + count.current += 1; + return get(`/users/${user?.id}/websites`, { includeTeams, onlyTeams, ...params, - }), + }); + }, { enabled: !!user }, ); + const { data, refetch, isLoading, error } = q; const { showToast } = useToasts(); const handleChange = params => { @@ -60,10 +64,10 @@ export function WebsitesList({ ); return ( - + {showHeader && {addButton}} {showTable && ( - + {showTeam && ( diff --git a/src/lib/schema.ts b/src/lib/schema.ts new file mode 100644 index 00000000..739128b3 --- /dev/null +++ b/src/lib/schema.ts @@ -0,0 +1,13 @@ +import * as yup from 'yup'; + +export const dateRange = { + startAt: yup.number().integer().required(), + endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), +}; + +export const pageInfo = { + query: yup.string(), + page: yup.number().integer().positive(), + pageSize: yup.number().integer().positive().max(200), + orderBy: yup.string(), +}; diff --git a/src/lib/types.ts b/src/lib/types.ts index 3685753e..58e6aa9e 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -54,11 +54,11 @@ export interface ReportSearchFilter extends SearchFilter } export interface SearchFilter { - filter?: string; - filterType?: T; - pageSize: number; - page: number; + query?: string; + page?: number; + pageSize?: number; orderBy?: string; + data?: T; } export interface FilterResult { diff --git a/src/lib/yup.ts b/src/lib/yup.ts deleted file mode 100644 index a9d21028..00000000 --- a/src/lib/yup.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as yup from 'yup'; - -export function getDateRangeValidation() { - return { - startAt: yup.number().integer().required(), - endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), - }; -} - -// ex: /funnel|insights|retention/i -export function getFilterValidation(matchRegex) { - return { - filter: yup.string(), - filterType: yup.string().matches(matchRegex), - pageSize: yup.number().integer().positive().max(200), - page: yup.number().integer().positive(), - orderBy: yup.string(), - }; -} diff --git a/src/pages/api/me/teams.ts b/src/pages/api/me/teams.ts index d394ef07..131cb262 100644 --- a/src/pages/api/me/teams.ts +++ b/src/pages/api/me/teams.ts @@ -1,6 +1,6 @@ import { useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; -import { getFilterValidation } from 'lib/yup'; +import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed } from 'next-basics'; import userTeams from 'pages/api/users/[id]/teams'; @@ -12,7 +12,7 @@ export interface MyTeamsRequestQuery extends SearchFilter const schema = { GET: yup.object().shape({ - ...getFilterValidation(/All|Name|Owner/i), + ...pageInfo, }), }; diff --git a/src/pages/api/me/websites.ts b/src/pages/api/me/websites.ts index d4a803a0..749af316 100644 --- a/src/pages/api/me/websites.ts +++ b/src/pages/api/me/websites.ts @@ -1,6 +1,6 @@ import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; -import { getFilterValidation } from 'lib/yup'; +import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed } from 'next-basics'; import userWebsites from 'pages/api/users/[id]/websites'; @@ -12,7 +12,7 @@ export interface MyWebsitesRequestQuery extends SearchFilter const schema = { GET: yup.object().shape({ - ...getFilterValidation(/All|Name|Owner/i), + ...pageInfo, }), POST: yup.object().shape({ name: yup.string().max(50).required(), @@ -39,12 +39,11 @@ export default async ( } = req.auth; if (req.method === 'GET') { - const { page, filter, pageSize } = req.query; + const { page, query } = req.query; const results = await getTeamsByUserId(userId, { page, - filter, - pageSize: +pageSize || undefined, + query, }); return ok(res, results); diff --git a/src/pages/api/users/[id]/teams.ts b/src/pages/api/users/[id]/teams.ts index 72b99b86..34a31a0e 100644 --- a/src/pages/api/users/[id]/teams.ts +++ b/src/pages/api/users/[id]/teams.ts @@ -1,10 +1,11 @@ +import * as yup from 'yup'; import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; -import { getFilterValidation } from 'lib/yup'; +import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getTeamsByUserId } from 'queries'; -import * as yup from 'yup'; + export interface UserTeamsRequestQuery extends SearchFilter { id: string; } @@ -18,7 +19,7 @@ export interface UserTeamsRequestBody { const schema = { GET: yup.object().shape({ id: yup.string().uuid().required(), - ...getFilterValidation('/All|Name|Owner/i'), + ...pageInfo, }), }; @@ -40,12 +41,12 @@ export default async ( return unauthorized(res); } - const { page, filter, pageSize } = req.query; + const { page, query, pageSize } = req.query; const teams = await getTeamsByUserId(userId, { + query, page, - filter, - pageSize: +pageSize || undefined, + pageSize, }); return ok(res, teams); diff --git a/src/pages/api/users/[id]/websites.ts b/src/pages/api/users/[id]/websites.ts index ab7d88ef..cc264e7d 100644 --- a/src/pages/api/users/[id]/websites.ts +++ b/src/pages/api/users/[id]/websites.ts @@ -1,6 +1,6 @@ import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; -import { getFilterValidation } from 'lib/yup'; +import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getWebsitesByUserId } from 'queries'; @@ -17,7 +17,7 @@ const schema = { id: yup.string().uuid().required(), includeTeams: yup.boolean(), onlyTeams: yup.boolean(), - ...getFilterValidation(/All|Name|Domain/i), + ...pageInfo, }), }; @@ -32,7 +32,7 @@ export default async ( await useValidate(req, res); const { user } = req.auth; - const { id: userId, page, filter, pageSize, includeTeams, onlyTeams } = req.query; + const { id: userId, page, pageSize, query, includeTeams, onlyTeams } = req.query; if (req.method === 'GET') { if (!user.isAdmin && user.id !== userId) { @@ -40,8 +40,8 @@ export default async ( } const websites = await getWebsitesByUserId(userId, { + query, page, - filter, pageSize: +pageSize || undefined, includeTeams, onlyTeams, diff --git a/src/pages/api/users/index.ts b/src/pages/api/users/index.ts index 991986e8..d37add2f 100644 --- a/src/pages/api/users/index.ts +++ b/src/pages/api/users/index.ts @@ -3,7 +3,7 @@ import { ROLES } from 'lib/constants'; import { uuid } from 'lib/crypto'; import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, Role, SearchFilter, User, UserSearchFilterType } from 'lib/types'; -import { getFilterValidation } from 'lib/yup'; +import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createUser, getUserByUsername, getUsers } from 'queries'; @@ -19,7 +19,7 @@ export interface UsersRequestBody { import * as yup from 'yup'; const schema = { GET: yup.object().shape({ - ...getFilterValidation(/All|Username/i), + ...pageInfo, }), POST: yup.object().shape({ username: yup.string().max(255).required(), diff --git a/src/pages/api/websites/index.ts b/src/pages/api/websites/index.ts index d6009caf..a90f8e46 100644 --- a/src/pages/api/websites/index.ts +++ b/src/pages/api/websites/index.ts @@ -7,7 +7,7 @@ import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createWebsite } from 'queries'; import userWebsites from 'pages/api/users/[id]/websites'; import * as yup from 'yup'; -import { getFilterValidation } from 'lib/yup'; +import { pageInfo } from 'lib/schema'; export interface WebsitesRequestQuery extends SearchFilter {} @@ -19,7 +19,7 @@ export interface WebsitesRequestBody { const schema = { GET: yup.object().shape({ - ...getFilterValidation(/All|Name|Domain/i), + ...pageInfo, }), POST: yup.object().shape({ name: yup.string().max(100).required(), diff --git a/src/queries/admin/team.ts b/src/queries/admin/team.ts index cf731ad4..9947b9a3 100644 --- a/src/queries/admin/team.ts +++ b/src/queries/admin/team.ts @@ -1,5 +1,5 @@ import { Prisma, Team } from '@prisma/client'; -import { ROLES, TEAM_FILTER_TYPES } from 'lib/constants'; +import { ROLES } from 'lib/constants'; import { uuid } from 'lib/crypto'; import prisma from 'lib/prisma'; import { FilterResult, TeamSearchFilter } from 'lib/types'; @@ -82,10 +82,10 @@ export async function deleteTeam( } export async function getTeams( - TeamSearchFilter: TeamSearchFilter, + filters: TeamSearchFilter, options?: { include?: Prisma.TeamInclude }, ): Promise> { - const { userId, filter, filterType = TEAM_FILTER_TYPES.all } = TeamSearchFilter; + const { userId, query } = filters; const mode = prisma.getSearchMode(); const where: Prisma.TeamWhereInput = { @@ -94,29 +94,24 @@ export async function getTeams( some: { userId }, }, }), - ...(filter && { + ...(query && { AND: { OR: [ { - ...((filterType === TEAM_FILTER_TYPES.all || filterType === TEAM_FILTER_TYPES.name) && { - name: { startsWith: filter, ...mode }, - }), + name: { startsWith: query, ...mode }, }, { - ...((filterType === TEAM_FILTER_TYPES.all || - filterType === TEAM_FILTER_TYPES['user:username']) && { - teamUser: { - some: { - role: ROLES.teamOwner, - user: { - username: { - startsWith: filter, - ...mode, - }, + teamUser: { + some: { + role: ROLES.teamOwner, + user: { + username: { + startsWith: query, + ...mode, }, }, }, - }), + }, }, ], }, @@ -125,7 +120,7 @@ export async function getTeams( const [pageFilters, getParameters] = prisma.getPageFilters({ orderBy: 'name', - ...TeamSearchFilter, + ...filters, }); const teams = await prisma.client.team.findMany({ diff --git a/src/queries/admin/website.ts b/src/queries/admin/website.ts index 6417ade6..f4444b53 100644 --- a/src/queries/admin/website.ts +++ b/src/queries/admin/website.ts @@ -1,6 +1,6 @@ import { Prisma, Website } from '@prisma/client'; import cache from 'lib/cache'; -import { ROLES, WEBSITE_FILTER_TYPES } from 'lib/constants'; +import { ROLES } from 'lib/constants'; import prisma from 'lib/prisma'; import { FilterResult, WebsiteSearchFilter } from 'lib/types'; @@ -19,17 +19,10 @@ export async function getWebsiteByShareId(shareId: string) { } export async function getWebsites( - WebsiteSearchFilter: WebsiteSearchFilter, + filters: WebsiteSearchFilter, options?: { include?: Prisma.WebsiteInclude }, ): Promise> { - const { - userId, - teamId, - includeTeams, - onlyTeams, - filter, - filterType = WEBSITE_FILTER_TYPES.all, - } = WebsiteSearchFilter; + const { userId, teamId, includeTeams, onlyTeams, query } = filters; const mode = prisma.getSearchMode(); const where: Prisma.WebsiteWhereInput = { @@ -76,27 +69,23 @@ export async function getWebsites( ], }, { - OR: [ - { - ...((filterType === WEBSITE_FILTER_TYPES.all || - filterType === WEBSITE_FILTER_TYPES.name) && { - name: { startsWith: filter, ...mode }, - }), - }, - { - ...((filterType === WEBSITE_FILTER_TYPES.all || - filterType === WEBSITE_FILTER_TYPES.domain) && { - domain: { startsWith: filter, ...mode }, - }), - }, - ], + OR: query + ? [ + { + name: { startsWith: query, ...mode }, + }, + { + domain: { startsWith: query, ...mode }, + }, + ] + : [], }, ], }; const [pageFilters, getParameters] = prisma.getPageFilters({ orderBy: 'name', - ...WebsiteSearchFilter, + ...filters, }); const websites = await prisma.client.website.findMany({ @@ -115,10 +104,10 @@ export async function getWebsites( export async function getWebsitesByUserId( userId: string, - filter?: WebsiteSearchFilter, + filters?: WebsiteSearchFilter, ): Promise> { return getWebsites( - { userId, ...filter }, + { userId, ...filters }, { include: { teamWebsite: { @@ -143,12 +132,12 @@ export async function getWebsitesByUserId( export async function getWebsitesByTeamId( teamId: string, - filter?: WebsiteSearchFilter, + filters?: WebsiteSearchFilter, ): Promise> { return getWebsites( { teamId, - ...filter, + ...filters, includeTeams: true, }, { From ae01a50d1104681973e7682dcfa5b753826ff64b Mon Sep 17 00:00:00 2001 From: Juanga Covas <1177241+juangacovas@users.noreply.github.com> Date: Sat, 23 Sep 2023 20:25:37 +0200 Subject: [PATCH 006/163] Update es-ES.json Missing translations, improvements --- src/lang/es-ES.json | 66 ++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/src/lang/es-ES.json b/src/lang/es-ES.json index ef8c31a8..e2827bec 100644 --- a/src/lang/es-ES.json +++ b/src/lang/es-ES.json @@ -16,21 +16,21 @@ "label.before": "Antes", "label.bounce-rate": "Porcentaje de rebote", "label.breakdown": "Desglose", - "label.browser": "Browser", + "label.browser": "Navegador", "label.browsers": "Navegadores", "label.cancel": "Cancelar", "label.change-password": "Cambiar contraseña", "label.cities": "Ciudades", - "label.city": "City", + "label.city": "Ciudad", "label.clear-all": "Limpiar todo", "label.confirm": "Confirmar", "label.confirm-password": "Confirmar contraseña", "label.contains": "Contiene", "label.continue": "Continuar", "label.countries": "Países", - "label.country": "Country", - "label.create": "Create", - "label.create-report": "Crear reporte", + "label.country": "País", + "label.create": "Crear", + "label.create-report": "Crear informe", "label.create-team": "Crear equipo", "label.create-user": "Crear usuario", "label.created": "Creado", @@ -38,38 +38,38 @@ "label.custom-range": "Intervalo personalizado", "label.dashboard": "Panel de control", "label.data": "Datos", - "label.date": "Date", + "label.date": "Fecha", "label.date-range": "Intervalo de fechas", - "label.day": "Day", + "label.day": "Día", "label.default-date-range": "Intervalo por defecto", "label.delete": "Eliminar", "label.delete-team": "Eliminar equipo", "label.delete-user": "Eliminar usuario", "label.delete-website": "Eliminar sitio", - "label.description": "Descripciones", + "label.description": "Descripción", "label.desktop": "Escritorio", "label.details": "Detalles", - "label.device": "Device", + "label.device": "Dispositivo", "label.devices": "Dispositivos", - "label.dismiss": "Ignorar", + "label.dismiss": "Cerrar", "label.does-not-contain": "No contiene", "label.domain": "Dominio", - "label.dropoff": "Dropoff", + "label.dropoff": "Abandono", "label.edit": "Editar", "label.edit-dashboard": "Editar panel", "label.enable-share-url": "Habilitar compartir URL", "label.event": "Evento", "label.event-data": "Datos de evento", "label.events": "Eventos", - "label.false": "False", + "label.false": "Falso", "label.field": "Campo", "label.fields": "Campos", - "label.filter": "Filter", + "label.filter": "Filtro", "label.filter-combined": "Combinado", "label.filter-raw": "En crudo", "label.filters": "Filtros", "label.funnel": "Funnel", - "label.funnel-description": "Understand the conversion and drop-off rate of users.", + "label.funnel-description": "Comprender conversión y abandono de usuarios.", "label.greater-than": "Mayor que", "label.greater-than-equals": "Mayor que o igual a", "label.insights": "Insights", @@ -77,7 +77,7 @@ "label.is": "Es igual a", "label.is-not": "No es igual a", "label.is-not-set": "Is not set", - "label.is-set": "Is set", + "label.is-set": "Está establecido", "label.join": "Unir", "label.join-team": "Unirse al equipo", "label.language": "Idioma", @@ -96,57 +96,57 @@ "label.min": "Mín", "label.mobile": "Móvil", "label.more": "Más", - "label.my-websites": "My websites", + "label.my-websites": "Mis sitios web", "label.name": "Nombre", "label.new-password": "Nueva contraseña", "label.none": "Ninguno", - "label.os": "OS", + "label.os": "Sistema", "label.overview": "Resumen", "label.owner": "Propietario", - "label.page-of": "Page {current} of {total}", + "label.page-of": "Página {current} de {total}", "label.page-views": "Vistas", - "label.pageTitle": "Page title", + "label.pageTitle": "Título de página", "label.pages": "Páginas", "label.password": "Contraseña", - "label.powered-by": "Con la ayuda de {name}", + "label.powered-by": "Analíticas de {name}", "label.profile": "Perfil", "label.queries": "Consultas", - "label.query": "Query", + "label.query": "Consulta", "label.query-parameters": "Parámetros de petición", "label.realtime": "Tiempo real", - "label.referrer": "Referrer", + "label.referrer": "Referido", "label.referrers": "Referido desde", "label.refresh": "Actualizar", "label.regenerate": "Regenerar", "label.region": "Region", "label.regions": "Regiones", "label.remove": "Quitar", - "label.reports": "Reportes", + "label.reports": "Informes", "label.required": "Obligatorio", "label.reset": "Reiniciar", - "label.reset-website": "Reiniciar estadísticas", - "label.retention": "Retention", - "label.retention-description": "Measure your website stickiness by tracking how often users return.", + "label.reset-website": "Reiniciar analíticas", + "label.retention": "Retención", + "label.retention-description": "Medir la frecuencia con la que los usuarios vuelven a tu sitio web.", "label.role": "Rol", "label.run-query": "Ejecutar consulta", "label.save": "Guardar", "label.screens": "Pantallas", - "label.search": "Search", + "label.search": "Buscar", "label.select-date": "Seleccionar fecha", "label.select-website": "Seleccionar sitio web", "label.sessions": "Sesiones", - "label.settings": "Configuraciones", + "label.settings": "Ajustes", "label.share-url": "Compartir URL", "label.single-day": "Un solo día", "label.sum": "Suma", "label.tablet": "Tableta", "label.team": "Equipo", "label.team-guest": "Invitado al equipo", - "label.team-id": "ID de equipo", + "label.team-id": "ID del equipo", "label.team-member": "Miembro del equipo", - "label.team-name": "Team name", + "label.team-name": "Nombre del equipo", "label.team-owner": "Admin. del equipo", - "label.team-websites": "Team websites", + "label.team-websites": "Sitios web del equipo", "label.teams": "Equipos", "label.theme": "Tema", "label.this-month": "Este mes", @@ -194,7 +194,7 @@ "message.incorrect-username-password": "Nombre de usuario o contraseña incorrectos.", "message.invalid-domain": "Dominio inválido", "message.min-password-length": "Longitud mínima de {n} caracteres", - "message.new-version-available": "A new version of Umami {version} is available!", + "message.new-version-available": "Una nueva versión de Umami {version} está disponible", "message.no-data-available": "No hay información disponible.", "message.no-event-data": "No hay datos de eventos disponibles.", "message.no-match-password": "Las contraseñas no coinciden", @@ -206,7 +206,7 @@ "message.page-not-found": "Página no encontrada", "message.reset-website": "Para reiniciar este sitio web, escribe {confirmation} a continuación para confirmar.", "message.reset-website-warning": "Todas las estadísticas de esta página serán eliminadas, pero el código de rastreo permanecerá intacto.", - "message.saved": "Guardado.", + "message.saved": "Guardado", "message.share-url": "Esta es la URL pública para {target}.", "message.team-already-member": "Ya eres miembro de este equipo.", "message.team-not-found": "Equipo no encontrado.", From ce2a83a09fb10cf28d4c408b5b08ae8b0e4afc25 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Mon, 25 Sep 2023 13:19:56 -0700 Subject: [PATCH 007/163] More yup validations. --- src/lib/yup.ts | 17 +++++---- src/pages/api/reports/retention.ts | 7 ++-- src/pages/api/teams/[id]/users/[userId].ts | 1 + src/pages/api/teams/[id]/users/index.ts | 17 +++++---- src/pages/api/websites/[id]/events.ts | 6 ++-- src/pages/api/websites/[id]/index.ts | 10 +++--- src/pages/api/websites/[id]/metrics.ts | 12 +++++++ src/pages/api/websites/[id]/pageviews.ts | 31 ++++++++++------ src/pages/api/websites/[id]/reports.ts | 2 ++ src/pages/api/websites/[id]/reset.ts | 5 ++- src/pages/api/websites/[id]/stats.ts | 35 +++++++++++++------ src/queries/analytics/reports/getRetention.ts | 6 ++-- 12 files changed, 99 insertions(+), 50 deletions(-) diff --git a/src/lib/yup.ts b/src/lib/yup.ts index a9d21028..8b2eceee 100644 --- a/src/lib/yup.ts +++ b/src/lib/yup.ts @@ -1,11 +1,10 @@ +import moment from 'moment'; import * as yup from 'yup'; -export function getDateRangeValidation() { - return { - startAt: yup.number().integer().required(), - endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), - }; -} +export const DateRangeValidation = { + startAt: yup.number().integer().required(), + endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), +}; // ex: /funnel|insights|retention/i export function getFilterValidation(matchRegex) { @@ -17,3 +16,9 @@ export function getFilterValidation(matchRegex) { orderBy: yup.string(), }; } + +export const TimezoneTest = yup.string().test( + 'timezone', + () => `Invalid timezone`, + value => !moment.tz.zone(value), +); diff --git a/src/pages/api/reports/retention.ts b/src/pages/api/reports/retention.ts index 4006ab12..c7a5e9af 100644 --- a/src/pages/api/reports/retention.ts +++ b/src/pages/api/reports/retention.ts @@ -1,6 +1,7 @@ import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; +import { TimezoneTest } from 'lib/yup'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getRetention } from 'queries'; @@ -8,7 +9,7 @@ import * as yup from 'yup'; export interface RetentionRequestBody { websiteId: string; - dateRange: { startDate: string; endDate: string }; + dateRange: { startDate: string; endDate: string; timezone: string }; } const schema = { @@ -19,6 +20,7 @@ const schema = { .shape({ startDate: yup.date().required(), endDate: yup.date().required(), + timezone: TimezoneTest, }) .required(), }), @@ -37,7 +39,7 @@ export default async ( if (req.method === 'POST') { const { websiteId, - dateRange: { startDate, endDate }, + dateRange: { startDate, endDate, timezone }, } = req.body; if (!(await canViewWebsite(req.auth, websiteId))) { @@ -47,6 +49,7 @@ export default async ( const data = await getRetention(websiteId, { startDate: new Date(startDate), endDate: new Date(endDate), + timezone, }); return ok(res, data); diff --git a/src/pages/api/teams/[id]/users/[userId].ts b/src/pages/api/teams/[id]/users/[userId].ts index adb635d5..107aba64 100644 --- a/src/pages/api/teams/[id]/users/[userId].ts +++ b/src/pages/api/teams/[id]/users/[userId].ts @@ -5,6 +5,7 @@ import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { deleteTeamUser } from 'queries'; import * as yup from 'yup'; + export interface TeamUserRequestQuery { id: string; userId: string; diff --git a/src/pages/api/teams/[id]/users/index.ts b/src/pages/api/teams/[id]/users/index.ts index d0efba25..36e9f320 100644 --- a/src/pages/api/teams/[id]/users/index.ts +++ b/src/pages/api/teams/[id]/users/index.ts @@ -1,24 +1,27 @@ import { canViewTeam } from 'lib/auth'; -import { useAuth } from 'lib/middleware'; +import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getUsersByTeamId } from 'queries'; - +import * as yup from 'yup'; export interface TeamUserRequestQuery extends SearchFilter { id: string; } -export interface TeamUserRequestBody { - email: string; - roleId: string; -} +const schema = { + GET: yup.object().shape({ + id: yup.string().uuid().required(), + }), +}; export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); + req.yup = schema; + await useValidate(req, res); const { id: teamId } = req.query; diff --git a/src/pages/api/websites/[id]/events.ts b/src/pages/api/websites/[id]/events.ts index 427cb40e..422200f8 100644 --- a/src/pages/api/websites/[id]/events.ts +++ b/src/pages/api/websites/[id]/events.ts @@ -6,6 +6,8 @@ import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getEventMetrics } from 'queries'; import { parseDateRangeQuery } from 'lib/query'; +import * as yup from 'yup'; +import { TimezoneTest } from 'lib/yup'; const unitTypes = ['year', 'month', 'hour', 'day']; @@ -18,15 +20,13 @@ export interface WebsiteEventsRequestQuery { url: string; } -import * as yup from 'yup'; - const schema = { GET: yup.object().shape({ id: yup.string().uuid().required(), startAt: yup.number().integer().required(), endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), unit: yup.string().required(), - timezone: yup.string().required(), + timezone: TimezoneTest.required(), url: yup.string(), }), }; diff --git a/src/pages/api/websites/[id]/index.ts b/src/pages/api/websites/[id]/index.ts index 0e5aacce..e7c7e004 100644 --- a/src/pages/api/websites/[id]/index.ts +++ b/src/pages/api/websites/[id]/index.ts @@ -22,6 +22,12 @@ const schema = { GET: yup.object().shape({ id: yup.string().uuid().required(), }), + POST: yup.object().shape({ + id: yup.string().uuid().required(), + name: yup.string().required(), + domain: yup.string().required(), + shareId: yup.string().matches(SHARE_ID_REGEX, { excludeEmptyString: true }), + }), }; export default async ( @@ -55,10 +61,6 @@ export default async ( let website; - if (shareId && !shareId.match(SHARE_ID_REGEX)) { - return serverError(res, 'Invalid share ID.'); - } - try { website = await updateWebsite(websiteId, { name, domain, shareId }); } catch (e: any) { diff --git a/src/pages/api/websites/[id]/metrics.ts b/src/pages/api/websites/[id]/metrics.ts index b8c37339..89f90fc4 100644 --- a/src/pages/api/websites/[id]/metrics.ts +++ b/src/pages/api/websites/[id]/metrics.ts @@ -33,6 +33,18 @@ const schema = { type: yup.string().required(), startAt: yup.number().required(), endAt: yup.number().required(), + url: yup.string(), + referrer: yup.string(), + title: yup.string(), + query: yup.string(), + os: yup.string(), + browser: yup.string(), + device: yup.string(), + country: yup.string(), + region: yup.string(), + city: yup.string(), + language: yup.string(), + event: yup.string(), }), }; diff --git a/src/pages/api/websites/[id]/pageviews.ts b/src/pages/api/websites/[id]/pageviews.ts index 9985ca89..8c10ffeb 100644 --- a/src/pages/api/websites/[id]/pageviews.ts +++ b/src/pages/api/websites/[id]/pageviews.ts @@ -1,18 +1,17 @@ -import moment from 'moment-timezone'; -import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { NextApiRequestQueryBody, WebsitePageviews } from 'lib/types'; import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { getPageviewStats, getSessionStats } from 'queries'; import { parseDateRangeQuery } from 'lib/query'; +import { NextApiRequestQueryBody, WebsitePageviews } from 'lib/types'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getPageviewStats, getSessionStats } from 'queries'; export interface WebsitePageviewRequestQuery { id: string; startAt: number; endAt: number; - unit: string; - timezone: string; + unit?: string; + timezone?: string; url?: string; referrer?: string; title?: string; @@ -24,10 +23,24 @@ export interface WebsitePageviewRequestQuery { city?: string; } +import { TimezoneTest } from 'lib/yup'; import * as yup from 'yup'; const schema = { GET: yup.object().shape({ id: yup.string().uuid().required(), + startAt: yup.number().required(), + endAt: yup.number().required(), + unit: yup.string(), + timezone: TimezoneTest, + url: yup.string(), + referrer: yup.string(), + title: yup.string(), + os: yup.string(), + browser: yup.string(), + device: yup.string(), + country: yup.string(), + region: yup.string(), + city: yup.string(), }), }; @@ -62,10 +75,6 @@ export default async ( const { startDate, endDate, unit } = await parseDateRangeQuery(req); - if (!moment.tz.zone(timezone)) { - return badRequest(res); - } - const filters = { startDate, endDate, diff --git a/src/pages/api/websites/[id]/reports.ts b/src/pages/api/websites/[id]/reports.ts index 2c7707e8..36e97a46 100644 --- a/src/pages/api/websites/[id]/reports.ts +++ b/src/pages/api/websites/[id]/reports.ts @@ -1,6 +1,7 @@ import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, ReportSearchFilterType, SearchFilter } from 'lib/types'; +import { getFilterValidation } from 'lib/yup'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getReportsByWebsiteId } from 'queries'; @@ -13,6 +14,7 @@ import * as yup from 'yup'; const schema = { GET: yup.object().shape({ id: yup.string().uuid().required(), + ...getFilterValidation(/All|Name|Description|Type|Username|Website Name|Website Domain/i), }), }; diff --git a/src/pages/api/websites/[id]/reset.ts b/src/pages/api/websites/[id]/reset.ts index cfd5e767..b17fdade 100644 --- a/src/pages/api/websites/[id]/reset.ts +++ b/src/pages/api/websites/[id]/reset.ts @@ -4,14 +4,14 @@ import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { resetWebsite } from 'queries'; +import * as yup from 'yup'; export interface WebsiteResetRequestQuery { id: string; } -import * as yup from 'yup'; const schema = { - GET: yup.object().shape({ + POST: yup.object().shape({ id: yup.string().uuid().required(), }), }; @@ -22,7 +22,6 @@ export default async ( ) => { await useCors(req, res); await useAuth(req, res); - req.yup = schema; await useValidate(req, res); diff --git a/src/pages/api/websites/[id]/stats.ts b/src/pages/api/websites/[id]/stats.ts index caf54910..e0c71e40 100644 --- a/src/pages/api/websites/[id]/stats.ts +++ b/src/pages/api/websites/[id]/stats.ts @@ -11,23 +11,36 @@ export interface WebsiteStatsRequestQuery { id: string; startAt: number; endAt: number; - url: string; - referrer: string; - title: string; - query: string; - event: string; - os: string; - browser: string; - device: string; - country: string; - region: string; - city: string; + url?: string; + referrer?: string; + title?: string; + query?: string; + event?: string; + os?: string; + browser?: string; + device?: string; + country?: string; + region?: string; + city?: string; } import * as yup from 'yup'; const schema = { GET: yup.object().shape({ id: yup.string().uuid().required(), + startAt: yup.number().required(), + endAt: yup.number().required(), + url: yup.string(), + referrer: yup.string(), + title: yup.string(), + query: yup.string(), + event: yup.string(), + os: yup.string(), + browser: yup.string(), + device: yup.string(), + country: yup.string(), + region: yup.string(), + city: yup.string(), }), }; diff --git a/src/queries/analytics/reports/getRetention.ts b/src/queries/analytics/reports/getRetention.ts index 3c384b6e..7526644f 100644 --- a/src/queries/analytics/reports/getRetention.ts +++ b/src/queries/analytics/reports/getRetention.ts @@ -8,7 +8,7 @@ export async function getRetention( filters: { startDate: Date; endDate: Date; - timezone: string; + timezone?: string; }, ] ) { @@ -23,7 +23,7 @@ async function relationalQuery( filters: { startDate: Date; endDate: Date; - timezone: string; + timezone?: string; }, ): Promise< { @@ -103,7 +103,7 @@ async function clickhouseQuery( filters: { startDate: Date; endDate: Date; - timezone: string; + timezone?: string; }, ): Promise< { From e6eb9a487e9e0eac32d707b01d07c802dd8e014c Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Mon, 25 Sep 2023 13:31:25 -0700 Subject: [PATCH 008/163] Create unit test. --- src/lib/constants.ts | 2 ++ src/lib/yup.ts | 9 ++++++++- src/pages/api/websites/[id]/events.ts | 25 +++++++++--------------- src/pages/api/websites/[id]/pageviews.ts | 4 ++-- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 888c1484..a548826a 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -30,6 +30,8 @@ export const FILTER_RANGE = 'filter-range'; export const FILTER_REFERRERS = 'filter-referrers'; export const FILTER_PAGES = 'filter-pages'; +export const UNIT_TYPES = ['year', 'month', 'hour', 'day']; + export const USER_FILTER_TYPES = { all: 'All', username: 'Username', diff --git a/src/lib/yup.ts b/src/lib/yup.ts index 8b2eceee..a2ea46d8 100644 --- a/src/lib/yup.ts +++ b/src/lib/yup.ts @@ -1,5 +1,6 @@ import moment from 'moment'; import * as yup from 'yup'; +import { UNIT_TYPES } from './constants'; export const DateRangeValidation = { startAt: yup.number().integer().required(), @@ -20,5 +21,11 @@ export function getFilterValidation(matchRegex) { export const TimezoneTest = yup.string().test( 'timezone', () => `Invalid timezone`, - value => !moment.tz.zone(value), + value => !value || !moment.tz.zone(value), +); + +export const UnitTypeTest = yup.string().test( + 'unit', + () => `Invalid unit`, + value => !value || !UNIT_TYPES.includes(value), ); diff --git a/src/pages/api/websites/[id]/events.ts b/src/pages/api/websites/[id]/events.ts index 422200f8..32288aa5 100644 --- a/src/pages/api/websites/[id]/events.ts +++ b/src/pages/api/websites/[id]/events.ts @@ -1,22 +1,19 @@ -import { WebsiteMetric, NextApiRequestQueryBody } from 'lib/types'; import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors, useValidate } from 'lib/middleware'; -import moment from 'moment-timezone'; -import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventMetrics } from 'queries'; import { parseDateRangeQuery } from 'lib/query'; +import { NextApiRequestQueryBody, WebsiteMetric } from 'lib/types'; +import { TimezoneTest, UnitTypeTest } from 'lib/yup'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getEventMetrics } from 'queries'; import * as yup from 'yup'; -import { TimezoneTest } from 'lib/yup'; - -const unitTypes = ['year', 'month', 'hour', 'day']; export interface WebsiteEventsRequestQuery { id: string; startAt: string; endAt: string; - unit: string; - timezone: string; + unit?: string; + timezone?: string; url: string; } @@ -25,8 +22,8 @@ const schema = { id: yup.string().uuid().required(), startAt: yup.number().integer().required(), endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), - unit: yup.string().required(), - timezone: TimezoneTest.required(), + unit: UnitTypeTest, + timezone: TimezoneTest, url: yup.string(), }), }; @@ -49,10 +46,6 @@ export default async ( return unauthorized(res); } - if (!moment.tz.zone(timezone) || !unitTypes.includes(unit)) { - return badRequest(res); - } - const events = await getEventMetrics(websiteId, { startDate, endDate, diff --git a/src/pages/api/websites/[id]/pageviews.ts b/src/pages/api/websites/[id]/pageviews.ts index 8c10ffeb..0f034cc2 100644 --- a/src/pages/api/websites/[id]/pageviews.ts +++ b/src/pages/api/websites/[id]/pageviews.ts @@ -23,14 +23,14 @@ export interface WebsitePageviewRequestQuery { city?: string; } -import { TimezoneTest } from 'lib/yup'; +import { TimezoneTest, UnitTypeTest } from 'lib/yup'; import * as yup from 'yup'; const schema = { GET: yup.object().shape({ id: yup.string().uuid().required(), startAt: yup.number().required(), endAt: yup.number().required(), - unit: yup.string(), + unit: UnitTypeTest, timezone: TimezoneTest, url: yup.string(), referrer: yup.string(), From febf085aca77eb130669d4505798e253062602c2 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Tue, 26 Sep 2023 12:30:35 -0700 Subject: [PATCH 009/163] Fix yup. --- src/lib/yup.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/yup.ts b/src/lib/yup.ts index a2ea46d8..6c19b089 100644 --- a/src/lib/yup.ts +++ b/src/lib/yup.ts @@ -21,11 +21,11 @@ export function getFilterValidation(matchRegex) { export const TimezoneTest = yup.string().test( 'timezone', () => `Invalid timezone`, - value => !value || !moment.tz.zone(value), + value => moment.tz.zone(value) !== null, ); export const UnitTypeTest = yup.string().test( 'unit', () => `Invalid unit`, - value => !value || !UNIT_TYPES.includes(value), + value => UNIT_TYPES.includes(value), ); From 8e8bf41eb3b2f0bc5034918030ecd08ecd76cf5a Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Tue, 26 Sep 2023 13:29:49 -0700 Subject: [PATCH 010/163] css updates for pager / page --- src/components/common/Pager.module.css | 1 + src/components/layout/Page.module.css | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/common/Pager.module.css b/src/components/common/Pager.module.css index 99eb70ce..70fe2019 100644 --- a/src/components/common/Pager.module.css +++ b/src/components/common/Pager.module.css @@ -1,5 +1,6 @@ .container { margin-top: 20px; + margin-bottom: 20px; } .text { diff --git a/src/components/layout/Page.module.css b/src/components/layout/Page.module.css index c546971b..100be5bb 100644 --- a/src/components/layout/Page.module.css +++ b/src/components/layout/Page.module.css @@ -4,4 +4,5 @@ flex-direction: column; background: var(--base50); position: relative; + height: 100%; } From 7e626dcd525318020ca33a62af1f4cc5fddac8d0 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 26 Sep 2023 23:20:29 -0700 Subject: [PATCH 011/163] Added useFilterQuery. Converted websites and reports pages. --- src/components/common/DataTable.js | 56 +++--- src/components/common/DataTable.module.css | 23 ++- src/components/common/Pager.js | 36 ++-- src/components/common/Pager.module.css | 23 +++ src/components/hooks/useDataTable.js | 13 -- src/components/hooks/useFilterQuery.js | 16 ++ src/components/hooks/usePaging.js | 9 - src/components/messages.js | 4 + src/components/pages/reports/ReportsPage.js | 61 +++--- src/components/pages/reports/ReportsTable.js | 126 ++++++------- .../pages/settings/websites/WebsitesList.js | 70 +++---- .../pages/settings/websites/WebsitesTable.js | 176 ++++-------------- src/lib/constants.ts | 17 +- src/lib/prisma.ts | 10 +- src/lib/types.ts | 24 +-- src/pages/api/me/teams.ts | 4 +- src/pages/api/me/websites.ts | 4 +- src/pages/api/reports/index.ts | 9 +- src/pages/api/scripts/telemetry.js | 17 +- src/pages/api/teams/[id]/users/index.ts | 9 +- src/pages/api/teams/[id]/websites/index.ts | 12 +- src/pages/api/teams/index.ts | 6 +- src/pages/api/users/[id]/teams.ts | 4 +- src/pages/api/users/[id]/websites.ts | 11 +- src/pages/api/users/index.ts | 8 +- src/pages/api/websites/[id]/reports.ts | 9 +- src/pages/api/websites/index.ts | 4 +- src/queries/admin/report.ts | 107 ++++------- src/queries/admin/website.ts | 4 +- 29 files changed, 373 insertions(+), 499 deletions(-) delete mode 100644 src/components/hooks/useDataTable.js create mode 100644 src/components/hooks/useFilterQuery.js delete mode 100644 src/components/hooks/usePaging.js diff --git a/src/components/common/DataTable.js b/src/components/common/DataTable.js index 2662fa2c..94b27281 100644 --- a/src/components/common/DataTable.js +++ b/src/components/common/DataTable.js @@ -1,46 +1,46 @@ -import { createContext } from 'react'; -import { SearchField } from 'react-basics'; -import { useDataTable } from 'components/hooks/useDataTable'; +import { Banner, Loading, SearchField } from 'react-basics'; import { useMessages } from 'components/hooks'; import Empty from 'components/common/Empty'; import Pager from 'components/common/Pager'; import styles from './DataTable.module.css'; +import classNames from 'classnames'; -const DEFAULT_SEARCH_DELAY = 1000; +const DEFAULT_SEARCH_DELAY = 600; export const DataTableStyles = styles; -export const DataTableContext = createContext(null); - export function DataTable({ + data = {}, + params = {}, + setParams, + isLoading, + error, searchDelay, showSearch = true, showPaging = true, children, - onChange, }) { const { formatMessage, labels, messages } = useMessages(); - const dataTable = useDataTable(); - const { query, setQuery, data, pageInfo, setPageInfo } = dataTable; - const { page, pageSize, count } = pageInfo || {}; - const noResults = Boolean(query && data?.length === 0); + const { pageSize, count } = data; + const { query, page } = params; + const hasData = Boolean(!isLoading && data?.data?.length); + const noResults = Boolean(!isLoading && query && !hasData); - const handleChange = value => { - onChange?.({ query: value, page }); - }; - - const handleSearch = value => { - setQuery(value); - handleChange(value); + const handleSearch = query => { + setParams({ ...params, query }); }; const handlePageChange = page => { - setPageInfo(state => ({ ...state, page })); + setParams({ ...params, page }); }; + if (error) { + return {formatMessage(messages.error)}; + } + return ( - - {showSearch && ( + <> + {(hasData || query || isLoading) && showSearch && ( )} - {noResults && } -
{children}
+
+ {hasData && typeof children === 'function' ? children(data) : children} + {isLoading && } + {!isLoading && !hasData && !query && ( + + )} + {noResults && } +
{showPaging && ( )} -
+ ); } diff --git a/src/components/common/DataTable.module.css b/src/components/common/DataTable.module.css index 883110da..b7426a7c 100644 --- a/src/components/common/DataTable.module.css +++ b/src/components/common/DataTable.module.css @@ -1,3 +1,12 @@ +.table { + grid-template-rows: repeat(auto-fit, max-content); +} + +.table td { + align-items: center; + max-height: max-content; +} + .search { max-width: 300px; margin: 20px 0; @@ -8,10 +17,22 @@ gap: 5px; } +.body { + display: flex; + position: relative; +} + .body td { align-items: center; } .pager { - margin-top: 20px; + margin: 20px 0; +} + +.status { + display: flex; + align-items: center; + justify-content: center; + min-height: 200px; } diff --git a/src/components/common/Pager.js b/src/components/common/Pager.js index 3f94edb0..f35c2ab0 100644 --- a/src/components/common/Pager.js +++ b/src/components/common/Pager.js @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import { Button, Flexbox, Icon, Icons } from 'react-basics'; +import { Button, Icon, Icons } from 'react-basics'; import useMessages from 'components/hooks/useMessages'; import styles from './Pager.module.css'; @@ -25,21 +25,25 @@ export function Pager({ page, pageSize, count, onPageChange, className }) { } return ( - - - - {formatMessage(labels.pageOf, { current: page, total: maxPage })} - - - +
+
{formatMessage(labels.numberOfRecords, { x: count })}
+
+ +
+ {formatMessage(labels.pageOf, { current: page, total: maxPage })} +
+ +
+
+
); } diff --git a/src/components/common/Pager.module.css b/src/components/common/Pager.module.css index 9c22f597..0ed5e1f4 100644 --- a/src/components/common/Pager.module.css +++ b/src/components/common/Pager.module.css @@ -1,4 +1,27 @@ +.pager { + display: grid; + grid-template-columns: repeat(3, 1fr); + align-items: center; +} + +.nav { + display: flex; + align-items: center; + justify-content: center; +} + .text { font-size: var(--font-size-md); margin: 0 16px; + justify-content: center; +} + +@media only screen and (max-width: 992px) { + .pager { + grid-template-columns: repeat(2, 1fr); + } + + .nav { + justify-content: end; + } } diff --git a/src/components/hooks/useDataTable.js b/src/components/hooks/useDataTable.js deleted file mode 100644 index 83aa3d68..00000000 --- a/src/components/hooks/useDataTable.js +++ /dev/null @@ -1,13 +0,0 @@ -import { useState } from 'react'; -import { usePaging } from 'components/hooks/usePaging'; - -export function useDataTable(config = {}) { - const { initialData, initialQuery, initialPageInfo } = config; - const [data, setData] = useState(initialData ?? null); - const [query, setQuery] = useState(initialQuery ?? ''); - const { pageInfo, setPageInfo } = usePaging(initialPageInfo); - - return { data, setData, query, setQuery, pageInfo, setPageInfo }; -} - -export default useDataTable; diff --git a/src/components/hooks/useFilterQuery.js b/src/components/hooks/useFilterQuery.js new file mode 100644 index 00000000..5dd9b3df --- /dev/null +++ b/src/components/hooks/useFilterQuery.js @@ -0,0 +1,16 @@ +import { useState } from 'react'; +import { useApi } from 'components/hooks/useApi'; + +export function useFilterQuery(key, fn, options) { + const [params, setParams] = useState({ + query: '', + page: 1, + }); + const { useQuery } = useApi(); + + const result = useQuery([...key, params], fn.bind(null, params), options); + + return { ...result, params, setParams }; +} + +export default useFilterQuery; diff --git a/src/components/hooks/usePaging.js b/src/components/hooks/usePaging.js deleted file mode 100644 index 17c23153..00000000 --- a/src/components/hooks/usePaging.js +++ /dev/null @@ -1,9 +0,0 @@ -import { useState } from 'react'; - -const DEFAULT_PAGE_INFO = { page: 1, pageSize: 10, total: 0 }; - -export function usePaging(initialPageInfo) { - const [pageInfo, setPageInfo] = useState(initialPageInfo ?? { ...DEFAULT_PAGE_INFO }); - - return { pageInfo, setPageInfo }; -} diff --git a/src/components/messages.js b/src/components/messages.js index 7f432eb3..04a29a4c 100644 --- a/src/components/messages.js +++ b/src/components/messages.js @@ -193,6 +193,10 @@ export const labels = defineMessages({ pageOf: { id: 'label.page-of', defaultMessage: 'Page {current} of {total}' }, create: { id: 'label.create', defaultMessage: 'Create' }, search: { id: 'label.search', defaultMessage: 'Search' }, + numberOfRecords: { + id: 'label.number-of-records', + defaultMessage: '{x} {x, plural, one {record} other {records}}', + }, }); export const messages = defineMessages({ diff --git a/src/components/pages/reports/ReportsPage.js b/src/components/pages/reports/ReportsPage.js index bbb15a36..9a48d780 100644 --- a/src/components/pages/reports/ReportsPage.js +++ b/src/components/pages/reports/ReportsPage.js @@ -1,28 +1,39 @@ -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import Page from 'components/layout/Page'; import PageHeader from 'components/layout/PageHeader'; -import { useMessages, useReports } from 'components/hooks'; +import { useMessages, useApi } from 'components/hooks'; import Link from 'next/link'; import { Button, Icon, Icons, Text } from 'react-basics'; import ReportsTable from './ReportsTable'; +import useFilterQuery from 'components/hooks/useFilterQuery'; +import DataTable from 'components/common/DataTable'; + +function useReports() { + const { get, del, useMutation } = useApi(); + const { mutate } = useMutation(reportId => del(`/reports/${reportId}`)); + const reports = useFilterQuery(['reports'], params => get(`/reports`, params)); + + const deleteReport = id => { + mutate(id, { + onSuccess: () => { + reports.refetch(); + }, + }); + }; + + return { reports, deleteReport }; +} export function ReportsPage() { const { formatMessage, labels } = useMessages(); - const { - reports, - error, - isLoading, - deleteReport, - filter, - handleFilterChange, - handlePageChange, - handlePageSizeChange, - } = useReports(); + const { reports, deleteReport } = useReports(); - const hasData = (reports && reports?.data.length !== 0) || filter; + const handleDelete = async (id, callback) => { + await deleteReport(id); + await reports.refetch(); + callback?.(); + }; return ( - + <> - - {hasData && ( - - )} - {!hasData && } - + + {({ data }) => } + + ); } diff --git a/src/components/pages/reports/ReportsTable.js b/src/components/pages/reports/ReportsTable.js index 52488c11..72b0c273 100644 --- a/src/components/pages/reports/ReportsTable.js +++ b/src/components/pages/reports/ReportsTable.js @@ -1,96 +1,74 @@ import ConfirmDeleteForm from 'components/common/ConfirmDeleteForm'; import LinkButton from 'components/common/LinkButton'; -import SettingsTable from 'components/common/SettingsTable'; import { useMessages } from 'components/hooks'; import useUser from 'components/hooks/useUser'; -import { useState } from 'react'; -import { Button, Flexbox, Icon, Icons, Modal, Text } from 'react-basics'; +import { + Button, + Flexbox, + GridColumn, + GridTable, + Icon, + Icons, + Modal, + ModalTrigger, + Text, +} from 'react-basics'; import { REPORT_TYPES } from 'lib/constants'; -export function ReportsTable({ - data = [], - onDelete = () => {}, - filterValue, - onFilterChange, - onPageChange, - onPageSizeChange, - showDomain, -}) { - const [report, setReport] = useState(null); +export function ReportsTable({ data = [], onDelete, showDomain }) { const { formatMessage, labels } = useMessages(); const { user } = useUser(); - const domainColumn = [ - { - name: 'domain', - label: formatMessage(labels.domain), - }, - ]; - - const columns = [ - { name: 'name', label: formatMessage(labels.name) }, - { name: 'description', label: formatMessage(labels.description) }, - { name: 'type', label: formatMessage(labels.type) }, - ...(showDomain ? domainColumn : []), - { name: 'action', label: ' ' }, - ]; - - const cellRender = (row, data, key) => { - if (key === 'type') { - return formatMessage( - labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === row.type)], - ); - } - return data[key]; - }; - - const handleConfirm = () => { - onDelete(report.id); + const handleConfirm = (id, callback) => { + onDelete?.(id, callback); }; return ( - <> - + + + + {row => { - const { id, userId: reportOwnerId, website } = row; - if (showDomain) { - row.domain = website.domain; - } - + return formatMessage( + labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === row.type)], + ); + }} + + {showDomain && ( + + {row => row.website.domain} + + )} + + {row => { + const { id, name, userId, website } = row; return ( {formatMessage(labels.view)} - {!showDomain || user.id === reportOwnerId || user.id === website?.userId} - + {(user.id === userId || user.id === website?.userId) && ( + + + + {close => ( + + )} + + + )} ); }} - - {report && ( - - setReport(null)} - /> - - )} - +
+
); } diff --git a/src/components/pages/settings/websites/WebsitesList.js b/src/components/pages/settings/websites/WebsitesList.js index 4761ad0a..0dd3aa77 100644 --- a/src/components/pages/settings/websites/WebsitesList.js +++ b/src/components/pages/settings/websites/WebsitesList.js @@ -1,13 +1,13 @@ -import Page from 'components/layout/Page'; import PageHeader from 'components/layout/PageHeader'; import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm'; import WebsitesTable from 'components/pages/settings/websites/WebsitesTable'; -import useApi from 'components/hooks/useApi'; import useMessages from 'components/hooks/useMessages'; import useUser from 'components/hooks/useUser'; import { ROLES } from 'lib/constants'; import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; -import { useRef, useState } from 'react'; +import useApi from 'components/hooks/useApi'; +import DataTable from 'components/common/DataTable'; +import useFilterQuery from 'components/hooks/useFilterQuery'; export function WebsitesList({ showTeam, @@ -18,13 +18,10 @@ export function WebsitesList({ }) { const { formatMessage, labels, messages } = useMessages(); const { user } = useUser(); - const [params, setParams] = useState({}); - const { get, useQuery } = useApi(); - const count = useRef(0); - const q = useQuery( - ['websites', includeTeams, onlyTeams, params], - () => { - count.current += 1; + const { get } = useApi(); + const filterQuery = useFilterQuery( + ['websites', { includeTeams, onlyTeams }], + params => { return get(`/users/${user?.id}/websites`, { includeTeams, onlyTeams, @@ -33,46 +30,41 @@ export function WebsitesList({ }, { enabled: !!user }, ); - const { data, refetch, isLoading, error } = q; + const { refetch } = filterQuery; const { showToast } = useToasts(); - const handleChange = params => { - setParams(params); - }; - const handleSave = async () => { await refetch(); showToast({ message: formatMessage(messages.saved), variant: 'success' }); }; const addButton = ( - <> - {user.role !== ROLES.viewOnly && ( - - - - {close => } - - - )} - + + + + {close => } + + ); return ( - - {showHeader && {addButton}} - - + <> + {showHeader && ( + + {user.role !== ROLES.viewOnly && addButton} + + )} + + {({ data }) => ( + + )} + + ); } diff --git a/src/components/pages/settings/websites/WebsitesTable.js b/src/components/pages/settings/websites/WebsitesTable.js index 12f94200..3739de64 100644 --- a/src/components/pages/settings/websites/WebsitesTable.js +++ b/src/components/pages/settings/websites/WebsitesTable.js @@ -1,154 +1,58 @@ import Link from 'next/link'; -import { Button, Text, Icon, Icons, GridTable, GridColumn } from 'react-basics'; -import SettingsTable from 'components/common/SettingsTable'; -import Empty from 'components/common/Empty'; +import { Button, Text, Icon, Icons, GridTable, GridColumn, Flexbox } from 'react-basics'; import useMessages from 'components/hooks/useMessages'; import useUser from 'components/hooks/useUser'; -import DataTable, { DataTableStyles } from 'components/common/DataTable'; -export function WebsitesTable({ - data = [], - showTeam, - showEditButton, - openExternal = false, - onChange, -}) { +export function WebsitesTable({ data = [], showTeam, showEditButton }) { const { formatMessage, labels } = useMessages(); const { user } = useUser(); - const showTable = data.length !== 0; - return ( - - {showTable && ( - - - - {showTeam && ( - - {row => row.teamWebsite[0]?.team.name} - - )} - {showTeam && ( - - {row => row.user.username} - - )} - - {row => { - const { - id, - user: { id: ownerId }, - } = row; - - return ( - <> - {showEditButton && (!showTeam || ownerId === user.id) && ( - - - - )} - - - - - ); - }} - - + + + + {showTeam && ( + + {row => row.teamWebsite[0]?.team.name} + )} - - ); -} + {showTeam && ( + + {row => row.user.username} + + )} + + {row => { + const { + id, + user: { id: ownerId }, + } = row; -export function WebsitesTable2({ - data = [], - filterValue, - onFilterChange, - onPageChange, - onPageSizeChange, - showTeam, - showEditButton, - openExternal = false, -}) { - const { formatMessage, labels } = useMessages(); - const { user } = useUser(); - - const showTable = data && (filterValue || data?.data?.length !== 0); - - const teamColumns = [ - { name: 'teamName', label: formatMessage(labels.teamName) }, - { name: 'owner', label: formatMessage(labels.owner) }, - ]; - - const columns = [ - { name: 'name', label: formatMessage(labels.name) }, - { name: 'domain', label: formatMessage(labels.domain) }, - ...(showTeam ? teamColumns : []), - { name: 'action', label: ' ' }, - ]; - - return ( - <> - {showTable && ( - - {row => { - const { - id, - teamWebsite, - user: { username, id: ownerId }, - } = row; - if (showTeam) { - row.teamName = teamWebsite[0]?.team.name; - row.owner = username; - } - - return ( - <> - {showEditButton && (!showTeam || ownerId === user.id) && ( - - - - )} - + return ( + + {showEditButton && (!showTeam || ownerId === user.id) && ( + - - ); - }} - - )} - {!showTable && } - + )} + + + +
+ ); + }} + + ); } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 888c1484..9ea76d93 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -19,6 +19,7 @@ export const DEFAULT_ANIMATION_DURATION = 300; export const DEFAULT_DATE_RANGE = '24hour'; export const DEFAULT_WEBSITE_LIMIT = 10; export const DEFAULT_RESET_DATE = '2000-01-01'; +export const DEFAULT_PAGE_SIZE = 10; export const REALTIME_RANGE = 30; export const REALTIME_INTERVAL = 5000; @@ -30,22 +31,6 @@ export const FILTER_RANGE = 'filter-range'; export const FILTER_REFERRERS = 'filter-referrers'; export const FILTER_PAGES = 'filter-pages'; -export const USER_FILTER_TYPES = { - all: 'All', - username: 'Username', -} as const; -export const WEBSITE_FILTER_TYPES = { all: 'All', name: 'Name', domain: 'Domain' } as const; -export const TEAM_FILTER_TYPES = { all: 'All', name: 'Name', 'user:username': 'Owner' } as const; -export const REPORT_FILTER_TYPES = { - all: 'All', - name: 'Name', - description: 'Description', - type: 'Type', - 'user:username': 'Username', - 'website:name': 'Website Name', - 'website:domain': 'Website Domain', -} as const; - export const EVENT_COLUMNS = ['url', 'referrer', 'title', 'query', 'event']; export const SESSION_COLUMNS = [ diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 59638dbd..f75ea1fe 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -1,11 +1,11 @@ +import { Prisma } from '@prisma/client'; import prisma from '@umami/prisma-client'; import moment from 'moment-timezone'; import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db'; -import { FILTER_COLUMNS, SESSION_COLUMNS, OPERATORS } from './constants'; +import { FILTER_COLUMNS, SESSION_COLUMNS, OPERATORS, DEFAULT_PAGE_SIZE } from './constants'; import { loadWebsite } from './load'; import { maxDate } from './date'; import { QueryFilters, QueryOptions, SearchFilter } from './types'; -import { Prisma } from '@prisma/client'; const MYSQL_DATE_FORMATS = { minute: '%Y-%m-%d %H:%i:00', @@ -171,7 +171,7 @@ async function rawQuery(sql: string, data: object): Promise { return prisma.rawQuery(query, params); } -function getPageFilters(filters: SearchFilter): [ +function getPageFilters(filters: SearchFilter): [ { orderBy: { [x: string]: string; @@ -185,7 +185,7 @@ function getPageFilters(filters: SearchFilter): [ orderBy: string; }, ] { - const { pageSize = 10, page = 1, orderBy } = filters || {}; + const { page = 1, pageSize = DEFAULT_PAGE_SIZE, orderBy } = filters || {}; return [ { @@ -198,7 +198,7 @@ function getPageFilters(filters: SearchFilter): [ ], }), }, - { pageSize, page: +page, orderBy }, + { page: +page, pageSize, orderBy }, ]; } diff --git a/src/lib/types.ts b/src/lib/types.ts index 58e6aa9e..98fbc29b 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -5,12 +5,8 @@ import { EVENT_TYPE, KAFKA_TOPIC, PERMISSIONS, - REPORT_FILTER_TYPES, REPORT_TYPES, ROLES, - TEAM_FILTER_TYPES, - USER_FILTER_TYPES, - WEBSITE_FILTER_TYPES, } from './constants'; import * as yup from 'yup'; import { TIME_UNIT } from './date'; @@ -27,46 +23,42 @@ export type DynamicDataType = ObjectValues; export type KafkaTopic = ObjectValues; export type ReportType = ObjectValues; -export type ReportSearchFilterType = ObjectValues; -export type UserSearchFilterType = ObjectValues; -export type WebsiteSearchFilterType = ObjectValues; -export type TeamSearchFilterType = ObjectValues; - -export interface WebsiteSearchFilter extends SearchFilter { +export interface WebsiteSearchFilter extends SearchFilter { userId?: string; teamId?: string; includeTeams?: boolean; onlyTeams?: boolean; } -export interface UserSearchFilter extends SearchFilter { +export interface UserSearchFilter extends SearchFilter { teamId?: string; } -export interface TeamSearchFilter extends SearchFilter { +export interface TeamSearchFilter extends SearchFilter { userId?: string; } -export interface ReportSearchFilter extends SearchFilter { +export interface ReportSearchFilter extends SearchFilter { userId?: string; websiteId?: string; includeTeams?: boolean; } -export interface SearchFilter { +export interface SearchFilter { query?: string; page?: number; pageSize?: number; orderBy?: string; - data?: T; + sortDescending?: boolean; } export interface FilterResult { data: T; count: number; - pageSize: number; page: number; + pageSize: number; orderBy?: string; + sortDescending?: boolean; } export interface DynamicData { diff --git a/src/pages/api/me/teams.ts b/src/pages/api/me/teams.ts index 131cb262..14602157 100644 --- a/src/pages/api/me/teams.ts +++ b/src/pages/api/me/teams.ts @@ -1,12 +1,12 @@ import { useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter } from 'lib/types'; import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed } from 'next-basics'; import userTeams from 'pages/api/users/[id]/teams'; import * as yup from 'yup'; -export interface MyTeamsRequestQuery extends SearchFilter { +export interface MyTeamsRequestQuery extends SearchFilter { id: string; } diff --git a/src/pages/api/me/websites.ts b/src/pages/api/me/websites.ts index 749af316..ec6a5556 100644 --- a/src/pages/api/me/websites.ts +++ b/src/pages/api/me/websites.ts @@ -1,12 +1,12 @@ import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter } from 'lib/types'; import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed } from 'next-basics'; import userWebsites from 'pages/api/users/[id]/websites'; import * as yup from 'yup'; -export interface MyWebsitesRequestQuery extends SearchFilter { +export interface MyWebsitesRequestQuery extends SearchFilter { id: string; } diff --git a/src/pages/api/reports/index.ts b/src/pages/api/reports/index.ts index 3c975b76..911d729c 100644 --- a/src/pages/api/reports/index.ts +++ b/src/pages/api/reports/index.ts @@ -1,13 +1,13 @@ import { uuid } from 'lib/crypto'; import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, ReportSearchFilterType, SearchFilter } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter } from 'lib/types'; import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok } from 'next-basics'; import { createReport, getReportsByUserId } from 'queries'; import * as yup from 'yup'; -export interface ReportsRequestQuery extends SearchFilter {} +export interface ReportsRequestQuery extends SearchFilter {} export interface ReportRequestBody { websiteId: string; @@ -52,12 +52,11 @@ export default async ( } = req.auth; if (req.method === 'GET') { - const { page, filter, pageSize } = req.query; + const { page, query } = req.query; const data = await getReportsByUserId(userId, { page, - filter, - pageSize: +pageSize || undefined, + query, includeTeams: true, }); diff --git a/src/pages/api/scripts/telemetry.js b/src/pages/api/scripts/telemetry.js index 954d5058..6a249de0 100644 --- a/src/pages/api/scripts/telemetry.js +++ b/src/pages/api/scripts/telemetry.js @@ -1,18 +1,23 @@ +import { ok } from 'next-basics'; import { CURRENT_VERSION, TELEMETRY_PIXEL } from 'lib/constants'; export default function handler(req, res) { - res.setHeader('content-type', 'text/javascript'); + if (process.env.NODE_ENV === 'production') { + res.setHeader('content-type', 'text/javascript'); - if (process.env.DISABLE_TELEMETRY) { - return res.send('/* telemetry disabled */'); - } + if (process.env.DISABLE_TELEMETRY) { + return res.send('/* telemetry disabled */'); + } - const script = ` + const script = ` (()=>{const i=document.createElement('img'); i.setAttribute('src','${TELEMETRY_PIXEL}?v=${CURRENT_VERSION}'); i.setAttribute('style','width:0;height:0;position:absolute;pointer-events:none;'); document.body.appendChild(i);})(); `; - return res.send(script.replace(/\s\s+/g, '')); + return res.send(script.replace(/\s\s+/g, '')); + } + + return ok(res); } diff --git a/src/pages/api/teams/[id]/users/index.ts b/src/pages/api/teams/[id]/users/index.ts index d0efba25..1c9e8352 100644 --- a/src/pages/api/teams/[id]/users/index.ts +++ b/src/pages/api/teams/[id]/users/index.ts @@ -1,11 +1,11 @@ import { canViewTeam } from 'lib/auth'; import { useAuth } from 'lib/middleware'; -import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getUsersByTeamId } from 'queries'; -export interface TeamUserRequestQuery extends SearchFilter { +export interface TeamUserRequestQuery extends SearchFilter { id: string; } @@ -27,12 +27,11 @@ export default async ( return unauthorized(res); } - const { page, filter, pageSize } = req.query; + const { query, page } = req.query; const users = await getUsersByTeamId(teamId, { + query, page, - filter, - pageSize: +pageSize || undefined, }); return ok(res, users); diff --git a/src/pages/api/teams/[id]/websites/index.ts b/src/pages/api/teams/[id]/websites/index.ts index 23c7390b..4d14c4e9 100644 --- a/src/pages/api/teams/[id]/websites/index.ts +++ b/src/pages/api/teams/[id]/websites/index.ts @@ -1,14 +1,14 @@ import * as yup from 'yup'; import { canViewTeam } from 'lib/auth'; import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter } from 'lib/types'; import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getWebsitesByTeamId } from 'queries'; import { createTeamWebsites } from 'queries/admin/teamWebsite'; -export interface TeamWebsiteRequestQuery extends SearchFilter { +export interface TeamWebsiteRequestQuery extends SearchFilter { id: string; } @@ -43,13 +43,7 @@ export default async ( return unauthorized(res); } - const { page, filter, pageSize } = req.query; - - const websites = await getWebsitesByTeamId(teamId, { - page, - filter, - pageSize: +pageSize || undefined, - }); + const websites = await getWebsitesByTeamId(teamId, { ...req.query }); return ok(res, websites); } diff --git a/src/pages/api/teams/index.ts b/src/pages/api/teams/index.ts index 084d09a2..74cb532e 100644 --- a/src/pages/api/teams/index.ts +++ b/src/pages/api/teams/index.ts @@ -2,19 +2,19 @@ import { Team } from '@prisma/client'; import { canCreateTeam } from 'lib/auth'; import { uuid } from 'lib/crypto'; import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter } from 'lib/types'; import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { getRandomChars, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createTeam, getTeamsByUserId } from 'queries'; import * as yup from 'yup'; -export interface TeamsRequestQuery extends SearchFilter {} +export interface TeamsRequestQuery extends SearchFilter {} export interface TeamsRequestBody { name: string; } -export interface MyTeamsRequestQuery extends SearchFilter {} +export interface MyTeamsRequestQuery extends SearchFilter {} const schema = { GET: yup.object().shape({ diff --git a/src/pages/api/users/[id]/teams.ts b/src/pages/api/users/[id]/teams.ts index 34a31a0e..f9d7f5ea 100644 --- a/src/pages/api/users/[id]/teams.ts +++ b/src/pages/api/users/[id]/teams.ts @@ -1,12 +1,12 @@ import * as yup from 'yup'; import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter } from 'lib/types'; import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getTeamsByUserId } from 'queries'; -export interface UserTeamsRequestQuery extends SearchFilter { +export interface UserTeamsRequestQuery extends SearchFilter { id: string; } diff --git a/src/pages/api/users/[id]/websites.ts b/src/pages/api/users/[id]/websites.ts index cc264e7d..227d1c98 100644 --- a/src/pages/api/users/[id]/websites.ts +++ b/src/pages/api/users/[id]/websites.ts @@ -1,12 +1,12 @@ import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter } from 'lib/types'; import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getWebsitesByUserId } from 'queries'; import * as yup from 'yup'; -export interface UserWebsitesRequestQuery extends SearchFilter { +export interface UserWebsitesRequestQuery extends SearchFilter { id: string; includeTeams?: boolean; onlyTeams?: boolean; @@ -32,7 +32,7 @@ export default async ( await useValidate(req, res); const { user } = req.auth; - const { id: userId, page, pageSize, query, includeTeams, onlyTeams } = req.query; + const { id: userId, page, query, includeTeams, onlyTeams } = req.query; if (req.method === 'GET') { if (!user.isAdmin && user.id !== userId) { @@ -40,9 +40,8 @@ export default async ( } const websites = await getWebsitesByUserId(userId, { - query, - page, - pageSize: +pageSize || undefined, + page: +page, + query: query as string, includeTeams, onlyTeams, }); diff --git a/src/pages/api/users/index.ts b/src/pages/api/users/index.ts index d37add2f..670ddd5d 100644 --- a/src/pages/api/users/index.ts +++ b/src/pages/api/users/index.ts @@ -2,13 +2,13 @@ import { canCreateUser, canViewUsers } from 'lib/auth'; import { ROLES } from 'lib/constants'; import { uuid } from 'lib/crypto'; import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, Role, SearchFilter, User, UserSearchFilterType } from 'lib/types'; +import { NextApiRequestQueryBody, Role, SearchFilter, User } from 'lib/types'; import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createUser, getUserByUsername, getUsers } from 'queries'; -export interface UsersRequestQuery extends SearchFilter {} +export interface UsersRequestQuery extends SearchFilter {} export interface UsersRequestBody { username: string; password: string; @@ -46,9 +46,9 @@ export default async ( return unauthorized(res); } - const { page, filter, pageSize } = req.query; + const { page, query } = req.query; - const users = await getUsers({ page, filter, pageSize: pageSize ? +pageSize : null }); + const users = await getUsers({ page, query }); return ok(res, users); } diff --git a/src/pages/api/websites/[id]/reports.ts b/src/pages/api/websites/[id]/reports.ts index 2c7707e8..ec8109f8 100644 --- a/src/pages/api/websites/[id]/reports.ts +++ b/src/pages/api/websites/[id]/reports.ts @@ -1,11 +1,11 @@ import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, ReportSearchFilterType, SearchFilter } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getReportsByWebsiteId } from 'queries'; -export interface ReportsRequestQuery extends SearchFilter { +export interface ReportsRequestQuery extends SearchFilter { id: string; } @@ -33,12 +33,11 @@ export default async ( return unauthorized(res); } - const { page, filter, pageSize } = req.query; + const { page, query } = req.query; const data = await getReportsByWebsiteId(websiteId, { page, - filter, - pageSize: +pageSize || undefined, + query, }); return ok(res, data); diff --git a/src/pages/api/websites/index.ts b/src/pages/api/websites/index.ts index a90f8e46..dc9ec36d 100644 --- a/src/pages/api/websites/index.ts +++ b/src/pages/api/websites/index.ts @@ -1,7 +1,7 @@ import { canCreateWebsite } from 'lib/auth'; import { uuid } from 'lib/crypto'; import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; +import { NextApiRequestQueryBody, SearchFilter } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createWebsite } from 'queries'; @@ -9,7 +9,7 @@ import userWebsites from 'pages/api/users/[id]/websites'; import * as yup from 'yup'; import { pageInfo } from 'lib/schema'; -export interface WebsitesRequestQuery extends SearchFilter {} +export interface WebsitesRequestQuery extends SearchFilter {} export interface WebsitesRequestBody { name: string; diff --git a/src/queries/admin/report.ts b/src/queries/admin/report.ts index 59eb7035..2f987681 100644 --- a/src/queries/admin/report.ts +++ b/src/queries/admin/report.ts @@ -1,5 +1,4 @@ import { Prisma, Report } from '@prisma/client'; -import { REPORT_FILTER_TYPES } from 'lib/constants'; import prisma from 'lib/prisma'; import { FilterResult, ReportSearchFilter } from 'lib/types'; @@ -27,27 +26,21 @@ export async function deleteReport(reportId: string): Promise { } export async function getReports( - ReportSearchFilter: ReportSearchFilter, + params: ReportSearchFilter, options?: { include?: Prisma.ReportInclude }, ): Promise> { - const { - userId, - websiteId, - includeTeams, - filter, - filterType = REPORT_FILTER_TYPES.all, - } = ReportSearchFilter; + const { query, userId, websiteId, includeTeams } = params; const mode = prisma.getSearchMode(); const where: Prisma.ReportWhereInput = { - ...(userId && { userId: userId }), - ...(websiteId && { websiteId: websiteId }), + userId, + websiteId, AND: [ { OR: [ { - ...(userId && { userId: userId }), + userId, }, { ...(includeTeams && { @@ -71,71 +64,53 @@ export async function getReports( { OR: [ { - ...((filterType === REPORT_FILTER_TYPES.all || - filterType === REPORT_FILTER_TYPES.name) && { + name: { + contains: query, + ...mode, + }, + }, + { + description: { + contains: query, + ...mode, + }, + }, + { + type: { + contains: query, + ...mode, + }, + }, + { + user: { + username: { + contains: query, + ...mode, + }, + }, + }, + { + website: { name: { - startsWith: filter, + contains: query, ...mode, }, - }), + }, }, { - ...((filterType === REPORT_FILTER_TYPES.all || - filterType === REPORT_FILTER_TYPES.description) && { - description: { - startsWith: filter, + website: { + domain: { + contains: query, ...mode, }, - }), - }, - { - ...((filterType === REPORT_FILTER_TYPES.all || - filterType === REPORT_FILTER_TYPES.type) && { - type: { - startsWith: filter, - ...mode, - }, - }), - }, - { - ...((filterType === REPORT_FILTER_TYPES.all || - filterType === REPORT_FILTER_TYPES['user:username']) && { - user: { - username: { - startsWith: filter, - ...mode, - }, - }, - }), - }, - { - ...((filterType === REPORT_FILTER_TYPES.all || - filterType === REPORT_FILTER_TYPES['website:name']) && { - website: { - name: { - startsWith: filter, - ...mode, - }, - }, - }), - }, - { - ...((filterType === REPORT_FILTER_TYPES.all || - filterType === REPORT_FILTER_TYPES['website:domain']) && { - website: { - domain: { - startsWith: filter, - ...mode, - }, - }, - }), + }, }, ], }, ], }; - const [pageFilters, getParameters] = prisma.getPageFilters(ReportSearchFilter); + const [pageFilters, pageInfo] = prisma.getPageFilters(params); const reports = await prisma.client.report.findMany({ where, @@ -150,13 +125,13 @@ export async function getReports( return { data: reports, count, - ...getParameters, + ...pageInfo, }; } export async function getReportsByUserId( userId: string, - filter: ReportSearchFilter, + filter?: ReportSearchFilter, ): Promise> { return getReports( { userId, ...filter }, diff --git a/src/queries/admin/website.ts b/src/queries/admin/website.ts index f4444b53..0e7f5124 100644 --- a/src/queries/admin/website.ts +++ b/src/queries/admin/website.ts @@ -72,10 +72,10 @@ export async function getWebsites( OR: query ? [ { - name: { startsWith: query, ...mode }, + name: { contains: query, ...mode }, }, { - domain: { startsWith: query, ...mode }, + domain: { contains: query, ...mode }, }, ] : [], From 49ad536f246f82c89a3139a652bf74b7784d9894 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 28 Sep 2023 13:14:15 -0700 Subject: [PATCH 012/163] Auto stash before merge of "dev" and "origin/dev" --- src/lib/prisma.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index f75ea1fe..442ee202 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -185,7 +185,7 @@ function getPageFilters(filters: SearchFilter): [ orderBy: string; }, ] { - const { page = 1, pageSize = DEFAULT_PAGE_SIZE, orderBy } = filters || {}; + const { page = 1, pageSize = DEFAULT_PAGE_SIZE, orderBy, sortDescending = false } = filters || {}; return [ { @@ -193,7 +193,7 @@ function getPageFilters(filters: SearchFilter): [ ...(orderBy && { orderBy: [ { - [orderBy]: 'asc', + [orderBy]: sortDescending ? 'desc' : 'asc', }, ], }), From 35d45334df5237afc6f5d06002a36a9ff1e22285 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Thu, 28 Sep 2023 16:45:25 -0700 Subject: [PATCH 013/163] update package --- package.json | 2 +- yarn.lock | 333 +++------------------------------------------------ 2 files changed, 18 insertions(+), 317 deletions(-) diff --git a/package.json b/package.json index 79960eb2..6fc413d4 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ ".next/cache" ], "dependencies": { + "@clickhouse/client": "^0.2.2", "@fontsource/inter": "^4.5.15", "@prisma/client": "5.3.1", "@tanstack/react-query": "^4.33.0", @@ -70,7 +71,6 @@ "chart.js": "^4.2.1", "chartjs-adapter-date-fns": "^3.0.0", "classnames": "^2.3.1", - "clickhouse": "^2.5.0", "colord": "^2.9.2", "cors": "^2.8.5", "cross-spawn": "^7.0.3", diff --git a/yarn.lock b/yarn.lock index ecb1a7eb..d054fefd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1083,6 +1083,18 @@ "@babel/helper-validator-identifier" "^7.22.5" to-fast-properties "^2.0.0" +"@clickhouse/client-common@0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@clickhouse/client-common/-/client-common-0.2.2.tgz#0690046241140a51ba5b0c0b9298c3cb3cf20974" + integrity sha512-jlom9zLfcDzX9E3off93ZD3CPOkClyM213Y7TN1datkuRGKMvVyj1k0KXaMekhbRev+FTe85CqfoD5eq6qOnPg== + +"@clickhouse/client@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@clickhouse/client/-/client-0.2.2.tgz#a6358aa2342ee3f2850cdb2f47a9e1d6fbde5757" + integrity sha512-2faBnDS4x7ZkcOZqi3f6H967kH+nOfJLhBTWWjz0wTSBnEJBXRtePhN/ZY0NJIKc9Ga5w41Pf67mQgm6Dm/1/w== + dependencies: + "@clickhouse/client-common" "0.2.2" + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz" @@ -2869,14 +2881,6 @@ resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.2.36.tgz" integrity sha512-JtB41wXl7Au3+Nl3gD16Cfpj7k/6aCroZ6BbOiCMFCMvrOpkg/qQUXTso2XowaNqBbnkuGHurLAqkLBxNGc1hQ== -JSONStream@1.3.4: - version "1.3.4" - resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.4.tgz" - integrity sha512-Y7vfi3I5oMOYIr+WxV8NZxDSwcbNgzdKYsTNInmycOq9bUYwGg9ryu57Wg5NLmCjqdFPNUmpMBo3kSJN9tCbXg== - dependencies: - jsonparse "^1.2.0" - through ">=2.2.7 <3" - acorn-dynamic-import@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz" @@ -2939,7 +2943,7 @@ ajv-keywords@^5.0.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.12.3, ajv@^6.12.4: +ajv@^6.12.4: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -3138,18 +3142,6 @@ arrify@^1.0.1: resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== -asn1@~0.2.3: - version "0.2.6" - resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz" - integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" - integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== - ast-types-flow@^0.0.7: version "0.0.7" resolved "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz" @@ -3160,11 +3152,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== -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - at-least-node@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" @@ -3187,16 +3174,6 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz" - integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== - -aws4@^1.8.0: - version "1.11.0" - resolved "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz" - integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== - axe-core@^4.4.3: version "4.5.2" resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.5.2.tgz" @@ -3267,13 +3244,6 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" - integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== - dependencies: - tweetnacl "^0.14.3" - bcryptjs@^2.4.3: version "2.4.3" resolved "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz" @@ -3430,11 +3400,6 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001426, can resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001506.tgz" integrity sha512-6XNEcpygZMCKaufIcgpQNZNf00GEqc7VQON+9Rd0K1bMYo8xhMZRAo5zpbnbMNizi4YNgIDAFrdykWsvY3H4Hw== -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" - integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== - chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" @@ -3526,20 +3491,6 @@ cli-truncate@2.1.0, cli-truncate@^2.1.0: slice-ansi "^3.0.0" string-width "^4.2.0" -clickhouse@^2.5.0: - version "2.6.0" - resolved "https://registry.npmjs.org/clickhouse/-/clickhouse-2.6.0.tgz" - integrity sha512-HC5OV99GJOup4qZsTuWWPpXlj+847Z0OeygDU2x22rNYost0V/vWapzFWYZdV/5iRbGMrhFQPOyQEzmGvoaWRQ== - dependencies: - JSONStream "1.3.4" - lodash "4.17.21" - querystring "0.2.0" - request "2.88.0" - stream2asynciter "1.0.3" - through "2.3.8" - tsv "0.2.0" - uuid "3.4.0" - client-only@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz" @@ -3619,13 +3570,6 @@ colorette@^2.0.16: resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== -combined-stream@^1.0.6, combined-stream@~1.0.6: - version "1.0.8" - resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - commander@2, commander@^2.20.0, commander@^2.20.3: version "2.20.3" resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" @@ -3690,11 +3634,6 @@ core-js-pure@^3.25.1: resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.26.1.tgz" integrity sha512-VVXcDpp/xJ21KdULRq/lXdLzQAtX7+37LzpyfFM973il0tWSsDEoyzG38G14AjTpK9VTfiNM9jnFauq/CpaWGQ== -core-util-is@1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" - integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== - cors@^2.8.5: version "2.8.5" resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz" @@ -4029,13 +3968,6 @@ damerau-levenshtein@^1.0.8: resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz" - integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== - dependencies: - assert-plus "^1.0.0" - data-uri-to-buffer@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" @@ -4168,11 +4100,6 @@ del@^6.0.0: rimraf "^3.0.2" slash "^3.0.0" -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - denque@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" @@ -4302,14 +4229,6 @@ dotenv@^10.0.0: resolved "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz" integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz" - integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - ecdsa-sig-formatter@1.0.11: version "1.0.11" resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz" @@ -4808,11 +4727,6 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== -extend@~3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - extract-react-intl-messages@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/extract-react-intl-messages/-/extract-react-intl-messages-4.1.1.tgz" @@ -4834,16 +4748,6 @@ extract-react-intl-messages@^4.1.1: sort-keys "^4.0.0" write-json-file "^4.3.0" -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" - integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== - -extsprintf@^1.2.0: - version "1.4.1" - resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz" - integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -4977,20 +4881,6 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" - integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== - -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - formdata-polyfill@^4.0.10: version "4.0.10" resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" @@ -5132,13 +5022,6 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz" - integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== - dependencies: - assert-plus "^1.0.0" - github-from-package@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" @@ -5319,19 +5202,6 @@ h3@^1.7.1, h3@^1.8.1: uncrypto "^0.1.3" unenv "^1.7.4" -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz" - integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== - -har-validator@~5.1.0: - version "5.1.5" - resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz" - integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== - dependencies: - ajv "^6.12.3" - har-schema "^2.0.0" - hard-rejection@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" @@ -5412,15 +5282,6 @@ http-shutdown@^1.2.2: resolved "https://registry.yarnpkg.com/http-shutdown/-/http-shutdown-1.2.2.tgz#41bc78fc767637c4c95179bc492f312c0ae64c5f" integrity sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw== -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz" - integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ== - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -5840,7 +5701,7 @@ is-typed-array@^1.1.10, is-typed-array@^1.1.12, is-typed-array@^1.1.9: dependencies: which-typed-array "^1.1.11" -is-typedarray@^1.0.0, is-typedarray@~1.0.0: +is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== @@ -5874,11 +5735,6 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" - integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== - jest-worker@^26.2.1: version "26.6.2" resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz" @@ -5918,11 +5774,6 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz" - integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== - jsesc@^2.5.1: version "2.5.2" resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" @@ -5958,11 +5809,6 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== -json-schema@0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz" - integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== - json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" @@ -5975,11 +5821,6 @@ json-stable-stringify@^1.0.1: dependencies: jsonify "~0.0.0" -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" - integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== - json5@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" @@ -6023,11 +5864,6 @@ jsonify@~0.0.0: resolved "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz" integrity sha512-trvBk1ki43VZptdBI5rIlG4YOzyeH/WefQt5rj1grasPn4iiZWKet8nkgc4GlsAylaztn0qZfUYOiTsASJFdNA== -jsonparse@^1.2.0: - version "1.3.1" - resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz" - integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== - jsonwebtoken@^9.0.0: version "9.0.0" resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz" @@ -6038,16 +5874,6 @@ jsonwebtoken@^9.0.0: ms "^2.1.1" semver "^7.3.8" -jsprim@^1.2.2: - version "1.4.2" - resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz" - integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.4.0" - verror "1.10.0" - "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.2: version "3.3.3" resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz" @@ -6281,7 +6107,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz" integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== -lodash@4.17.21, lodash@^4.17.21: +lodash@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -6476,18 +6302,6 @@ micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.35" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - mime@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" @@ -6831,11 +6645,6 @@ nth-check@^2.0.1: dependencies: boolbase "^1.0.0" -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - object-assign@^4, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" @@ -7085,11 +6894,6 @@ pathe@^1.1.0, pathe@^1.1.1: resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.1.tgz#1dd31d382b974ba69809adc9a7a347e65d84829a" integrity sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q== -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" - integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== - picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -7779,11 +7583,6 @@ property-expr@^2.0.4: resolved "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz" integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA== -psl@^1.1.24: - version "1.9.0" - resolved "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz" - integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== - pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -7792,11 +7591,6 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz" - integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== - punycode@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" @@ -7807,16 +7601,6 @@ pure-rand@^6.0.2: resolved "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz" integrity sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ== -qs@~6.5.2: - version "6.5.3" - resolved "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz" - integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz" - integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== - queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -8251,32 +8035,6 @@ request-ip@^3.3.0: resolved "https://registry.npmjs.org/request-ip/-/request-ip-3.3.0.tgz" integrity sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA== -request@2.88.0: - version "2.88.0" - resolved "https://registry.npmjs.org/request/-/request-2.88.0.tgz" - integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.0" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.4.3" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - require-from-string@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" @@ -8464,7 +8222,7 @@ safe-array-concat@^1.0.1: has-symbols "^1.0.3" isarray "^2.0.5" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0: +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -8483,11 +8241,6 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" -safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - scheduler@^0.23.0: version "0.23.0" resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz" @@ -8749,21 +8502,6 @@ sprintf-js@~1.0.2: resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -sshpk@^1.7.0: - version "1.17.0" - resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz" - integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - stable@^0.1.8: version "0.1.8" resolved "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz" @@ -8779,11 +8517,6 @@ std-env@^3.4.3: resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.4.3.tgz#326f11db518db751c83fd58574f449b7c3060910" integrity sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q== -stream2asynciter@1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/stream2asynciter/-/stream2asynciter-1.0.3.tgz" - integrity sha512-9/dEZW+LQjuW6ub5hmWi4n9Pn8W8qA8k7NAE1isecesA164e73xTdy1CJ3S9o9YS+O21HuiK7T+4uS7FgKDy4w== - streamsearch@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" @@ -9177,7 +8910,7 @@ thenby@^1.3.4: resolved "https://registry.npmjs.org/thenby/-/thenby-1.3.4.tgz" integrity sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ== -through@2.3.8, "through@>=2.2.7 <3", through@^2.3.8: +through@^2.3.8: version "2.3.8" resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== @@ -9232,14 +8965,6 @@ toposort@^2.0.2: resolved "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz" integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== -tough-cookie@~2.4.3: - version "2.4.3" - resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz" - integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== - dependencies: - psl "^1.1.24" - punycode "^1.4.1" - tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -9311,11 +9036,6 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" -tsv@0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/tsv/-/tsv-0.2.0.tgz" - integrity sha512-GG6xbOP85giXXom0dS6z9uyDsxktznjpa1AuDlPrIXDqDnbhjr9Vk6Us8iz6U1nENL4CPS2jZDvIjEdaZsmc4Q== - tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -9323,11 +9043,6 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" - integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -9579,11 +9294,6 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -uuid@3.4.0, uuid@^3.3.2: - version "3.4.0" - resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - uuid@^9.0.0: version "9.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" @@ -9607,15 +9317,6 @@ vary@^1: resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= -verror@1.10.0: - version "1.10.0" - resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" - integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - vue@^3.2.23: version "3.2.36" resolved "https://registry.npmjs.org/vue/-/vue-3.2.36.tgz" From 9a52cdd2e11a296022bde2ff1317945b4c3c181b Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 29 Sep 2023 05:29:22 -0700 Subject: [PATCH 014/163] Refactored to use app folder. --- next.config.js | 32 +- package.json | 16 +- src/app/(app)/NavBar.js | 58 + .../layout => app/(app)}/NavBar.module.css | 37 +- src/app/(app)/Shell.tsx | 27 + .../(app)}/console/TestConsole.js | 4 +- .../(app)}/console/TestConsole.module.css | 0 src/app/(app)/console/[[...id]]/page.tsx | 15 + .../(app)}/dashboard/Dashboard.js | 24 +- .../(app)}/dashboard/DashboardEdit.js | 0 .../(app)}/dashboard/DashboardEdit.module.css | 0 .../dashboard/DashboardSettingsButton.js | 0 .../DashboardSettingsButton.module.css | 0 src/app/(app)/dashboard/page.tsx | 10 + .../(app)/layout.module.css} | 0 src/app/(app)/layout.tsx | 20 + .../(app)}/reports/BaseParameters.js | 0 .../(app)}/reports/FieldAddForm.js | 0 .../(app)}/reports/FieldAddForm.module.css | 0 .../(app)}/reports/FieldAggregateForm.js | 0 .../(app)}/reports/FieldFilterForm.js | 0 .../(app)}/reports/FieldFilterForm.module.css | 0 .../(app)}/reports/FieldSelectForm.js | 0 .../(app)}/reports/FieldSelectForm.module.css | 0 .../(app)}/reports/FilterSelectForm.js | 0 .../(app)}/reports/ParameterList.js | 0 .../(app)}/reports/ParameterList.module.css | 0 .../pages => app/(app)}/reports/PopupForm.js | 0 .../(app)}/reports/PopupForm.module.css | 0 .../pages => app/(app)}/reports/Report.js | 10 +- .../(app)/reports/Report.module.css} | 0 .../pages => app/(app)}/reports/ReportBody.js | 2 +- .../(app)}/reports/ReportHeader.js | 4 +- .../(app)}/reports/ReportHeader.module.css | 0 .../pages => app/(app)}/reports/ReportMenu.js | 2 +- src/app/(app)/reports/ReportsHeader.js | 24 + src/app/(app)/reports/ReportsList.js | 37 + .../(app)}/reports/ReportsTable.js | 0 src/app/(app)/reports/[id]/ReportDetails.js | 26 + src/app/(app)/reports/[id]/page.tsx | 14 + .../(app)/reports/create}/ReportTemplates.js | 6 +- .../create}/ReportTemplates.module.css | 0 src/app/(app)/reports/create/page.tsx | 10 + .../reports/event-data/EventDataParameters.js | 2 +- .../event-data/EventDataParameters.module.css | 0 .../reports/event-data/EventDataReport.js | 0 .../reports/event-data/EventDataTable.js | 0 .../(app)}/reports/funnel/FunnelChart.js | 0 .../reports/funnel/FunnelChart.module.css | 0 .../(app)}/reports/funnel/FunnelParameters.js | 2 +- .../(app)}/reports/funnel/FunnelReport.js | 1 + .../reports/funnel/FunnelReport.module.css | 0 .../(app)}/reports/funnel/FunnelTable.js | 0 .../(app)}/reports/funnel/UrlAddForm.js | 0 .../reports/funnel/UrlAddForm.module.css | 0 src/app/(app)/reports/funnel/page.tsx | 10 + .../reports/insights/InsightsParameters.js | 2 +- .../insights/InsightsParameters.module.css | 0 .../(app)}/reports/insights/InsightsReport.js | 1 + .../(app)}/reports/insights/InsightsTable.js | 0 src/app/(app)/reports/insights/page.tsx | 10 + src/app/(app)/reports/page.tsx | 14 + .../reports/retention/RetentionParameters.js | 2 +- .../reports/retention/RetentionReport.js | 1 + .../retention/RetentionReport.module.css | 0 .../reports/retention/RetentionTable.js | 0 .../retention/RetentionTable.module.css | 0 src/app/(app)/reports/retention/page.js | 9 + .../layout => app/(app)/settings}/SideNav.js | 6 +- .../(app)/settings}/SideNav.module.css | 0 .../(app)/settings/layout.module.css} | 9 +- .../(app)/settings/layout.tsx} | 26 +- .../settings/profile/DateRangeSetting.js | 0 .../settings/profile/LanguageSetting.js | 0 .../settings/profile/PasswordChangeButton.js | 2 +- .../settings/profile/PasswordEditForm.js | 0 .../(app)/settings/profile/ProfileHeader.js | 11 + .../settings/profile/ProfileSettings.js} | 13 +- .../(app)}/settings/profile/ThemeSetting.js | 0 .../settings/profile/ThemeSetting.module.css | 0 .../settings/profile/TimezoneSetting.js | 0 src/app/(app)/settings/profile/page.js | 11 + .../(app)}/settings/teams/TeamAddForm.js | 0 .../(app)/settings/teams/TeamDeleteButton.js | 25 + .../(app)}/settings/teams/TeamDeleteForm.js | 0 .../(app)}/settings/teams/TeamJoinForm.js | 0 .../(app)/settings/teams/TeamLeaveButton.js | 35 + .../(app)}/settings/teams/TeamLeaveForm.js | 0 .../settings/teams/TeamWebsiteRemoveButton.js | 0 .../(app)/settings/teams/TeamsAddButton.js | 24 + src/app/(app)/settings/teams/TeamsHeader.js | 24 + .../(app)/settings/teams/TeamsJoinButton.js | 29 + src/app/(app)/settings/teams/TeamsList.js | 19 + src/app/(app)/settings/teams/TeamsTable.js | 46 + .../(app)}/settings/teams/WebsiteTags.js | 0 .../settings/teams/WebsiteTags.module.css | 0 .../teams/[id]}/TeamAddWebsiteForm.js | 2 +- .../settings/teams/[id]}/TeamEditForm.js | 0 .../teams/[id]}/TeamMemberRemoveButton.js | 0 .../(app)/settings/teams/[id]}/TeamMembers.js | 2 +- .../settings/teams/[id]}/TeamMembersTable.js | 0 .../settings/teams/[id]}/TeamSettings.js | 12 +- .../settings/teams/[id]}/TeamWebsites.js | 4 +- .../settings/teams/[id]}/TeamWebsitesTable.js | 2 +- src/app/(app)/settings/teams/[id]/page.js | 9 + src/app/(app)/settings/teams/page.js | 15 + .../(app)}/settings/users/UserAddButton.js | 0 .../(app)}/settings/users/UserAddForm.js | 0 .../(app)/settings/users/UserDeleteButton.js | 27 + .../(app)}/settings/users/UserDeleteForm.js | 0 .../(app)}/settings/users/UserEditForm.js | 0 .../(app)}/settings/users/UserWebsites.js | 2 +- src/app/(app)/settings/users/UsersHeader.js | 16 + src/app/(app)/settings/users/UsersList.js | 25 + src/app/(app)/settings/users/UsersTable.js | 57 + .../settings/users/[id]}/UserSettings.js | 16 +- src/app/(app)/settings/users/[id]/page.js | 9 + src/app/(app)/settings/users/page.tsx | 13 + .../settings/websites/WebsiteAddButton.js | 29 + .../settings/websites/WebsiteAddForm.js | 0 .../settings/websites/WebsiteSettings.js | 25 +- .../(app)/settings/websites/WebsitesHeader.js | 16 + .../(app)/settings/websites/WebsitesList.js | 43 + .../websites/WebsitesList.module.css} | 0 .../(app)}/settings/websites/WebsitesTable.js | 0 .../websites/WebsitesTable.module.css | 0 .../(app)/settings/websites/[id]}/ShareUrl.js | 9 +- .../settings/websites/[id]}/TrackingCode.js | 4 +- .../settings/websites/[id]}/WebsiteData.js | 4 +- .../websites/[id]}/WebsiteDeleteForm.js | 0 .../websites/[id]}/WebsiteEditForm.js | 0 .../websites/[id]}/WebsiteResetForm.js | 0 src/app/(app)/settings/websites/[id]/page.js | 15 + src/app/(app)/settings/websites/page.js | 9 + .../(app)}/websites/WebsiteTableView.js | 0 .../websites/WebsiteTableView.module.css | 0 .../(app)/websites/[id]}/WebsiteChart.js | 0 .../websites/[id]}/WebsiteChart.module.css | 0 .../(app)/websites/[id]}/WebsiteChartList.js | 5 +- .../(app)/websites/[id]/WebsiteDetails.js} | 23 +- .../(app)/websites/[id]}/WebsiteHeader.js | 5 +- .../websites/[id]}/WebsiteHeader.module.css | 0 .../(app)/websites/[id]}/WebsiteMenuView.js | 2 +- .../websites/[id]}/WebsiteMenuView.module.css | 0 .../(app)/websites/[id]}/WebsiteMetricsBar.js | 4 +- .../[id]}/WebsiteMetricsBar.module.css | 0 .../[id]}/event-data/EventDataMetricsBar.js | 0 .../event-data/EventDataMetricsBar.module.css | 0 .../[id]}/event-data/EventDataTable.js | 0 .../[id]}/event-data/EventDataValueTable.js | 0 .../[id]/event-data}/WebsiteEventData.js | 7 +- .../event-data}/WebsiteEventData.module.css | 0 .../(app)/websites/[id]/event-data/page.js | 15 + src/app/(app)/websites/[id]/page.tsx | 9 + .../(app)/websites/[id]/realtime/Realtime.js | 122 ++ .../[id]/realtime/Realtime.module.css} | 0 .../[id]}/realtime/RealtimeCountries.js | 9 +- .../realtime/RealtimeCountries.module.css | 0 .../websites/[id]}/realtime/RealtimeHeader.js | 0 .../[id]}/realtime/RealtimeHeader.module.css | 0 .../websites/[id]}/realtime/RealtimeHome.js | 2 +- .../websites/[id]}/realtime/RealtimeLog.js | 0 .../[id]}/realtime/RealtimeLog.module.css | 1 - .../websites/[id]}/realtime/RealtimePage.js | 12 +- .../websites/[id]}/realtime/RealtimeUrls.js | 0 src/app/(app)/websites/[id]/realtime/page.tsx | 9 + .../websites/[id]/reports/WebsiteReports.js} | 19 +- src/app/(app)/websites/[id]/reports/page.tsx | 9 + src/app/(app)/websites/page.js | 30 + src/app/Providers.tsx | 39 + src/app/layout.tsx | 36 + .../pages => app}/login/LoginForm.js | 3 +- .../pages => app}/login/LoginForm.module.css | 0 .../login/page.module.css} | 3 +- src/app/login/page.tsx | 25 + src/{pages/logout.js => app/logout/page.tsx} | 16 +- src/app/not-found.tsx | 13 + src/app/page.tsx | 6 + .../layout => app/share/[...id]}/Footer.js | 0 .../share/[...id]}/Footer.module.css | 0 src/app/share/[...id]/Header.js | 29 + .../share/[...id]}/Header.module.css | 0 src/app/share/[...id]/page.tsx | 17 + src/{pages/sso.js => app/sso/page.tsx} | 7 +- .../common/{DataTable.js => DataTable.tsx} | 37 +- src/components/common/{Empty.js => Empty.tsx} | 7 +- src/components/common/MobileMenu.js | 4 +- src/components/common/UpdateNotice.js | 5 +- src/components/common/WorldMap.js | 4 +- src/components/hooks/useApi.ts | 4 +- src/components/hooks/useCountryNames.js | 4 +- src/components/hooks/useFilterQuery.js | 16 - src/components/hooks/useFilterQuery.ts | 26 + src/components/hooks/useLanguageNames.js | 4 +- src/components/hooks/useLocale.js | 4 +- src/components/hooks/usePageQuery.js | 26 +- src/components/hooks/useRequireLogin.ts | 6 +- src/components/input/LogoutButton.js | 2 +- src/components/input/ProfileButton.js | 2 +- src/components/input/SettingsButton.js | 4 +- src/components/layout/AppLayout.js | 32 - src/components/layout/Header.js | 31 - src/components/layout/NavBar.js | 63 - src/components/layout/NavGroup.js | 4 +- src/components/layout/Page.module.css | 3 + src/components/layout/{Page.js => Page.tsx} | 15 +- .../layout/{PageHeader.js => PageHeader.tsx} | 10 +- src/components/layout/ReportsLayout.js | 23 - .../layout/ReportsLayout.module.css | 23 - src/components/layout/ShareLayout.js | 15 - src/components/metrics/BrowsersTable.js | 4 +- src/components/metrics/CitiesTable.js | 4 +- src/components/metrics/CountriesTable.js | 7 +- src/components/metrics/DevicesTable.js | 4 +- src/components/metrics/MetricsTable.js | 3 +- src/components/metrics/OSTable.js | 4 +- src/components/metrics/RegionsTable.js | 7 +- src/components/pages/login/LoginLayout.js | 18 - src/components/pages/reports/ReportDetails.js | 17 - src/components/pages/reports/ReportsPage.js | 54 - .../pages/settings/profile/ProfileSettings.js | 17 - .../pages/settings/teams/TeamsList.js | 118 -- .../pages/settings/teams/TeamsTable.js | 111 -- .../pages/settings/users/UsersList.js | 68 -- .../pages/settings/users/UsersTable.js | 93 -- .../pages/settings/websites/WebsitesList.js | 71 -- .../pages/websites/WebsiteEventDataPage.js | 12 - src/components/pages/websites/WebsitesPage.js | 77 -- src/index.ts | 50 +- src/lib/middleware.ts | 23 +- src/pages/404.js | 19 - src/pages/_app.js | 69 -- src/pages/api/auth/login.ts | 3 +- src/pages/console/[[...id]].js | 22 - src/pages/dashboard/index.js | 13 - src/pages/index.js | 12 - src/pages/login.js | 22 - src/pages/reports/[id].js | 24 - src/pages/reports/create.js | 13 - src/pages/reports/funnel.js | 13 - src/pages/reports/index.js | 13 - src/pages/reports/insights.js | 13 - src/pages/reports/retention.js | 13 - src/pages/settings/profile/index.js | 15 - src/pages/settings/teams/[id].js | 31 - src/pages/settings/teams/index.js | 27 - src/pages/settings/users/[id].js | 31 - src/pages/settings/users/index.js | 27 - src/pages/settings/websites/[id].js | 31 - src/pages/settings/websites/index.js | 27 - src/pages/share/[...id].js | 21 - src/pages/websites/[id]/event-data.js | 20 - src/pages/websites/[id]/index.js | 20 - src/pages/websites/[id]/realtime.js | 18 - src/pages/websites/[id]/reports.js | 18 - src/pages/websites/index.js | 13 - tsconfig.json | 9 +- yarn.lock | 1036 ++++++++--------- 258 files changed, 2025 insertions(+), 2258 deletions(-) create mode 100644 src/app/(app)/NavBar.js rename src/{components/layout => app/(app)}/NavBar.module.css (75%) create mode 100644 src/app/(app)/Shell.tsx rename src/{components/pages => app/(app)}/console/TestConsole.js (97%) rename src/{components/pages => app/(app)}/console/TestConsole.module.css (100%) create mode 100644 src/app/(app)/console/[[...id]]/page.tsx rename src/{components/pages => app/(app)}/dashboard/Dashboard.js (79%) rename src/{components/pages => app/(app)}/dashboard/DashboardEdit.js (100%) rename src/{components/pages => app/(app)}/dashboard/DashboardEdit.module.css (100%) rename src/{components/pages => app/(app)}/dashboard/DashboardSettingsButton.js (100%) rename src/{components/pages => app/(app)}/dashboard/DashboardSettingsButton.module.css (100%) create mode 100644 src/app/(app)/dashboard/page.tsx rename src/{components/layout/AppLayout.module.css => app/(app)/layout.module.css} (100%) create mode 100644 src/app/(app)/layout.tsx rename src/{components/pages => app/(app)}/reports/BaseParameters.js (100%) rename src/{components/pages => app/(app)}/reports/FieldAddForm.js (100%) rename src/{components/pages => app/(app)}/reports/FieldAddForm.module.css (100%) rename src/{components/pages => app/(app)}/reports/FieldAggregateForm.js (100%) rename src/{components/pages => app/(app)}/reports/FieldFilterForm.js (100%) rename src/{components/pages => app/(app)}/reports/FieldFilterForm.module.css (100%) rename src/{components/pages => app/(app)}/reports/FieldSelectForm.js (100%) rename src/{components/pages => app/(app)}/reports/FieldSelectForm.module.css (100%) rename src/{components/pages => app/(app)}/reports/FilterSelectForm.js (100%) rename src/{components/pages => app/(app)}/reports/ParameterList.js (100%) rename src/{components/pages => app/(app)}/reports/ParameterList.module.css (100%) rename src/{components/pages => app/(app)}/reports/PopupForm.js (100%) rename src/{components/pages => app/(app)}/reports/PopupForm.module.css (100%) rename src/{components/pages => app/(app)}/reports/Report.js (69%) rename src/{components/pages/reports/reports.module.css => app/(app)/reports/Report.module.css} (100%) rename src/{components/pages => app/(app)}/reports/ReportBody.js (75%) rename src/{components/pages => app/(app)}/reports/ReportHeader.js (96%) rename src/{components/pages => app/(app)}/reports/ReportHeader.module.css (100%) rename src/{components/pages => app/(app)}/reports/ReportMenu.js (75%) create mode 100644 src/app/(app)/reports/ReportsHeader.js create mode 100644 src/app/(app)/reports/ReportsList.js rename src/{components/pages => app/(app)}/reports/ReportsTable.js (100%) create mode 100644 src/app/(app)/reports/[id]/ReportDetails.js create mode 100644 src/app/(app)/reports/[id]/page.tsx rename src/{components/pages/reports => app/(app)/reports/create}/ReportTemplates.js (96%) rename src/{components/pages/reports => app/(app)/reports/create}/ReportTemplates.module.css (100%) create mode 100644 src/app/(app)/reports/create/page.tsx rename src/{components/pages => app/(app)}/reports/event-data/EventDataParameters.js (98%) rename src/{components/pages => app/(app)}/reports/event-data/EventDataParameters.module.css (100%) rename src/{components/pages => app/(app)}/reports/event-data/EventDataReport.js (100%) rename src/{components/pages => app/(app)}/reports/event-data/EventDataTable.js (100%) rename src/{components/pages => app/(app)}/reports/funnel/FunnelChart.js (100%) rename src/{components/pages => app/(app)}/reports/funnel/FunnelChart.module.css (100%) rename src/{components/pages => app/(app)}/reports/funnel/FunnelParameters.js (97%) rename src/{components/pages => app/(app)}/reports/funnel/FunnelReport.js (98%) rename src/{components/pages => app/(app)}/reports/funnel/FunnelReport.module.css (100%) rename src/{components/pages => app/(app)}/reports/funnel/FunnelTable.js (100%) rename src/{components/pages => app/(app)}/reports/funnel/UrlAddForm.js (100%) rename src/{components/pages => app/(app)}/reports/funnel/UrlAddForm.module.css (100%) create mode 100644 src/app/(app)/reports/funnel/page.tsx rename src/{components/pages => app/(app)}/reports/insights/InsightsParameters.js (98%) rename src/{components/pages => app/(app)}/reports/insights/InsightsParameters.module.css (100%) rename src/{components/pages => app/(app)}/reports/insights/InsightsReport.js (98%) rename src/{components/pages => app/(app)}/reports/insights/InsightsTable.js (100%) create mode 100644 src/app/(app)/reports/insights/page.tsx create mode 100644 src/app/(app)/reports/page.tsx rename src/{components/pages => app/(app)}/reports/retention/RetentionParameters.js (95%) rename src/{components/pages => app/(app)}/reports/retention/RetentionReport.js (98%) rename src/{components/pages => app/(app)}/reports/retention/RetentionReport.module.css (100%) rename src/{components/pages => app/(app)}/reports/retention/RetentionTable.js (100%) rename src/{components/pages => app/(app)}/reports/retention/RetentionTable.module.css (100%) create mode 100644 src/app/(app)/reports/retention/page.js rename src/{components/layout => app/(app)/settings}/SideNav.js (85%) rename src/{components/layout => app/(app)/settings}/SideNav.module.css (100%) rename src/{components/layout/SettingsLayout.module.css => app/(app)/settings/layout.module.css} (67%) rename src/{components/layout/SettingsLayout.js => app/(app)/settings/layout.tsx} (64%) rename src/{components/pages => app/(app)}/settings/profile/DateRangeSetting.js (100%) rename src/{components/pages => app/(app)}/settings/profile/LanguageSetting.js (100%) rename src/{components/pages => app/(app)}/settings/profile/PasswordChangeButton.js (91%) rename src/{components/pages => app/(app)}/settings/profile/PasswordEditForm.js (100%) create mode 100644 src/app/(app)/settings/profile/ProfileHeader.js rename src/{components/pages/settings/profile/ProfileDetails.js => app/(app)/settings/profile/ProfileSettings.js} (79%) rename src/{components/pages => app/(app)}/settings/profile/ThemeSetting.js (100%) rename src/{components/pages => app/(app)}/settings/profile/ThemeSetting.module.css (100%) rename src/{components/pages => app/(app)}/settings/profile/TimezoneSetting.js (100%) create mode 100644 src/app/(app)/settings/profile/page.js rename src/{components/pages => app/(app)}/settings/teams/TeamAddForm.js (100%) create mode 100644 src/app/(app)/settings/teams/TeamDeleteButton.js rename src/{components/pages => app/(app)}/settings/teams/TeamDeleteForm.js (100%) rename src/{components/pages => app/(app)}/settings/teams/TeamJoinForm.js (100%) create mode 100644 src/app/(app)/settings/teams/TeamLeaveButton.js rename src/{components/pages => app/(app)}/settings/teams/TeamLeaveForm.js (100%) rename src/{components/pages => app/(app)}/settings/teams/TeamWebsiteRemoveButton.js (100%) create mode 100644 src/app/(app)/settings/teams/TeamsAddButton.js create mode 100644 src/app/(app)/settings/teams/TeamsHeader.js create mode 100644 src/app/(app)/settings/teams/TeamsJoinButton.js create mode 100644 src/app/(app)/settings/teams/TeamsList.js create mode 100644 src/app/(app)/settings/teams/TeamsTable.js rename src/{components/pages => app/(app)}/settings/teams/WebsiteTags.js (100%) rename src/{components/pages => app/(app)}/settings/teams/WebsiteTags.module.css (100%) rename src/{components/pages/settings/teams => app/(app)/settings/teams/[id]}/TeamAddWebsiteForm.js (98%) rename src/{components/pages/settings/teams => app/(app)/settings/teams/[id]}/TeamEditForm.js (100%) rename src/{components/pages/settings/teams => app/(app)/settings/teams/[id]}/TeamMemberRemoveButton.js (100%) rename src/{components/pages/settings/teams => app/(app)/settings/teams/[id]}/TeamMembers.js (94%) rename src/{components/pages/settings/teams => app/(app)/settings/teams/[id]}/TeamMembersTable.js (100%) rename src/{components/pages/settings/teams => app/(app)/settings/teams/[id]}/TeamSettings.js (92%) rename src/{components/pages/settings/teams => app/(app)/settings/teams/[id]}/TeamWebsites.js (92%) rename src/{components/pages/settings/teams => app/(app)/settings/teams/[id]}/TeamWebsitesTable.js (96%) create mode 100644 src/app/(app)/settings/teams/[id]/page.js create mode 100644 src/app/(app)/settings/teams/page.js rename src/{components/pages => app/(app)}/settings/users/UserAddButton.js (100%) rename src/{components/pages => app/(app)}/settings/users/UserAddForm.js (100%) create mode 100644 src/app/(app)/settings/users/UserDeleteButton.js rename src/{components/pages => app/(app)}/settings/users/UserDeleteForm.js (100%) rename src/{components/pages => app/(app)}/settings/users/UserEditForm.js (100%) rename src/{components/pages => app/(app)}/settings/users/UserWebsites.js (92%) create mode 100644 src/app/(app)/settings/users/UsersHeader.js create mode 100644 src/app/(app)/settings/users/UsersList.js create mode 100644 src/app/(app)/settings/users/UsersTable.js rename src/{components/pages/settings/users => app/(app)/settings/users/[id]}/UserSettings.js (84%) create mode 100644 src/app/(app)/settings/users/[id]/page.js create mode 100644 src/app/(app)/settings/users/page.tsx create mode 100644 src/app/(app)/settings/websites/WebsiteAddButton.js rename src/{components/pages => app/(app)}/settings/websites/WebsiteAddForm.js (100%) rename src/{components/pages => app/(app)}/settings/websites/WebsiteSettings.js (80%) create mode 100644 src/app/(app)/settings/websites/WebsitesHeader.js create mode 100644 src/app/(app)/settings/websites/WebsitesList.js rename src/{components/pages/websites/WebsiteList.module.css => app/(app)/settings/websites/WebsitesList.module.css} (100%) rename src/{components/pages => app/(app)}/settings/websites/WebsitesTable.js (100%) rename src/{components/pages => app/(app)}/settings/websites/WebsitesTable.module.css (100%) rename src/{components/pages/settings/websites => app/(app)/settings/websites/[id]}/ShareUrl.js (91%) rename src/{components/pages/settings/websites => app/(app)/settings/websites/[id]}/TrackingCode.js (82%) rename src/{components/pages/settings/websites => app/(app)/settings/websites/[id]}/WebsiteData.js (89%) rename src/{components/pages/settings/websites => app/(app)/settings/websites/[id]}/WebsiteDeleteForm.js (100%) rename src/{components/pages/settings/websites => app/(app)/settings/websites/[id]}/WebsiteEditForm.js (100%) rename src/{components/pages/settings/websites => app/(app)/settings/websites/[id]}/WebsiteResetForm.js (100%) create mode 100644 src/app/(app)/settings/websites/[id]/page.js create mode 100644 src/app/(app)/settings/websites/page.js rename src/{components/pages => app/(app)}/websites/WebsiteTableView.js (100%) rename src/{components/pages => app/(app)}/websites/WebsiteTableView.module.css (100%) rename src/{components/pages/websites => app/(app)/websites/[id]}/WebsiteChart.js (100%) rename src/{components/pages/websites => app/(app)/websites/[id]}/WebsiteChart.module.css (100%) rename src/{components/pages/websites => app/(app)/websites/[id]}/WebsiteChartList.js (90%) rename src/{components/pages/websites/WebsiteDetailsPage.js => app/(app)/websites/[id]/WebsiteDetails.js} (74%) rename src/{components/pages/websites => app/(app)/websites/[id]}/WebsiteHeader.js (95%) rename src/{components/pages/websites => app/(app)/websites/[id]}/WebsiteHeader.module.css (100%) rename src/{components/pages/websites => app/(app)/websites/[id]}/WebsiteMenuView.js (98%) rename src/{components/pages/websites => app/(app)/websites/[id]}/WebsiteMenuView.module.css (100%) rename src/{components/pages/websites => app/(app)/websites/[id]}/WebsiteMetricsBar.js (97%) rename src/{components/pages/websites => app/(app)/websites/[id]}/WebsiteMetricsBar.module.css (100%) rename src/{components/pages => app/(app)/websites/[id]}/event-data/EventDataMetricsBar.js (100%) rename src/{components/pages => app/(app)/websites/[id]}/event-data/EventDataMetricsBar.module.css (100%) rename src/{components/pages => app/(app)/websites/[id]}/event-data/EventDataTable.js (100%) rename src/{components/pages => app/(app)/websites/[id]}/event-data/EventDataValueTable.js (100%) rename src/{components/pages/websites => app/(app)/websites/[id]/event-data}/WebsiteEventData.js (83%) rename src/{components/pages/websites => app/(app)/websites/[id]/event-data}/WebsiteEventData.module.css (100%) create mode 100644 src/app/(app)/websites/[id]/event-data/page.js create mode 100644 src/app/(app)/websites/[id]/page.tsx create mode 100644 src/app/(app)/websites/[id]/realtime/Realtime.js rename src/{components/pages/realtime/RealtimePage.module.css => app/(app)/websites/[id]/realtime/Realtime.module.css} (100%) rename src/{components/pages => app/(app)/websites/[id]}/realtime/RealtimeCountries.js (81%) rename src/{components/pages => app/(app)/websites/[id]}/realtime/RealtimeCountries.module.css (100%) rename src/{components/pages => app/(app)/websites/[id]}/realtime/RealtimeHeader.js (100%) rename src/{components/pages => app/(app)/websites/[id]}/realtime/RealtimeHeader.module.css (100%) rename src/{components/pages => app/(app)/websites/[id]}/realtime/RealtimeHome.js (95%) rename src/{components/pages => app/(app)/websites/[id]}/realtime/RealtimeLog.js (100%) rename src/{components/pages => app/(app)/websites/[id]}/realtime/RealtimeLog.module.css (98%) rename src/{components/pages => app/(app)/websites/[id]}/realtime/RealtimePage.js (90%) rename src/{components/pages => app/(app)/websites/[id]}/realtime/RealtimeUrls.js (100%) create mode 100644 src/app/(app)/websites/[id]/realtime/page.tsx rename src/{components/pages/websites/WebsiteReportsPage.js => app/(app)/websites/[id]/reports/WebsiteReports.js} (79%) create mode 100644 src/app/(app)/websites/[id]/reports/page.tsx create mode 100644 src/app/(app)/websites/page.js create mode 100644 src/app/Providers.tsx create mode 100644 src/app/layout.tsx rename src/{components/pages => app}/login/LoginForm.js (96%) rename src/{components/pages => app}/login/LoginForm.module.css (100%) rename src/{components/pages/login/LoginLayout.module.css => app/login/page.module.css} (76%) create mode 100644 src/app/login/page.tsx rename src/{pages/logout.js => app/logout/page.tsx} (73%) create mode 100644 src/app/not-found.tsx create mode 100644 src/app/page.tsx rename src/{components/layout => app/share/[...id]}/Footer.js (100%) rename src/{components/layout => app/share/[...id]}/Footer.module.css (100%) create mode 100644 src/app/share/[...id]/Header.js rename src/{components/layout => app/share/[...id]}/Header.module.css (100%) create mode 100644 src/app/share/[...id]/page.tsx rename src/{pages/sso.js => app/sso/page.tsx} (71%) rename src/components/common/{DataTable.js => DataTable.tsx} (70%) rename src/components/common/{Empty.js => Empty.tsx} (72%) delete mode 100644 src/components/hooks/useFilterQuery.js create mode 100644 src/components/hooks/useFilterQuery.ts delete mode 100644 src/components/layout/AppLayout.js delete mode 100644 src/components/layout/Header.js delete mode 100644 src/components/layout/NavBar.js rename src/components/layout/{Page.js => Page.tsx} (69%) rename src/components/layout/{PageHeader.js => PageHeader.tsx} (58%) delete mode 100644 src/components/layout/ReportsLayout.js delete mode 100644 src/components/layout/ReportsLayout.module.css delete mode 100644 src/components/layout/ShareLayout.js delete mode 100644 src/components/pages/login/LoginLayout.js delete mode 100644 src/components/pages/reports/ReportDetails.js delete mode 100644 src/components/pages/reports/ReportsPage.js delete mode 100644 src/components/pages/settings/profile/ProfileSettings.js delete mode 100644 src/components/pages/settings/teams/TeamsList.js delete mode 100644 src/components/pages/settings/teams/TeamsTable.js delete mode 100644 src/components/pages/settings/users/UsersList.js delete mode 100644 src/components/pages/settings/users/UsersTable.js delete mode 100644 src/components/pages/settings/websites/WebsitesList.js delete mode 100644 src/components/pages/websites/WebsiteEventDataPage.js delete mode 100644 src/components/pages/websites/WebsitesPage.js delete mode 100644 src/pages/404.js delete mode 100644 src/pages/_app.js delete mode 100644 src/pages/console/[[...id]].js delete mode 100644 src/pages/dashboard/index.js delete mode 100644 src/pages/index.js delete mode 100644 src/pages/login.js delete mode 100644 src/pages/reports/[id].js delete mode 100644 src/pages/reports/create.js delete mode 100644 src/pages/reports/funnel.js delete mode 100644 src/pages/reports/index.js delete mode 100644 src/pages/reports/insights.js delete mode 100644 src/pages/reports/retention.js delete mode 100644 src/pages/settings/profile/index.js delete mode 100644 src/pages/settings/teams/[id].js delete mode 100644 src/pages/settings/teams/index.js delete mode 100644 src/pages/settings/users/[id].js delete mode 100644 src/pages/settings/users/index.js delete mode 100644 src/pages/settings/websites/[id].js delete mode 100644 src/pages/settings/websites/index.js delete mode 100644 src/pages/share/[...id].js delete mode 100644 src/pages/websites/[id]/event-data.js delete mode 100644 src/pages/websites/[id]/index.js delete mode 100644 src/pages/websites/[id]/realtime.js delete mode 100644 src/pages/websites/[id]/reports.js delete mode 100644 src/pages/websites/index.js diff --git a/next.config.js b/next.config.js index cc3cde7c..2ef1c05e 100644 --- a/next.config.js +++ b/next.config.js @@ -6,7 +6,7 @@ const pkg = require('./package.json'); const contentSecurityPolicy = ` default-src 'self'; img-src *; - script-src 'self' 'unsafe-eval'; + script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' api.umami.is; frame-ancestors 'self' ${process.env.ALLOWED_FRAME_URLS}; @@ -74,16 +74,20 @@ if (process.env.CLOUD_MODE && process.env.CLOUD_URL && process.env.DISABLE_LOGIN }); } +const basePath = process.env.BASE_PATH; + +/** @type {import('next').NextConfig} */ const config = { env: { - cloudMode: process.env.CLOUD_MODE, + basePath: basePath || '', + cloudMode: !!process.env.CLOUD_MODE, cloudUrl: process.env.CLOUD_URL, configUrl: '/config', currentVersion: pkg.version, defaultLocale: process.env.DEFAULT_LOCALE, isProduction: process.env.NODE_ENV === 'production', }, - basePath: process.env.BASE_PATH, + basePath, output: 'standalone', eslint: { ignoreDuringBuilds: true, @@ -92,11 +96,23 @@ const config = { ignoreBuildErrors: true, }, webpack(config) { - config.module.rules.push({ - test: /\.svg$/, - issuer: /\.{js|jsx|ts|tsx}$/, - use: ['@svgr/webpack'], - }); + const fileLoaderRule = config.module.rules.find(rule => rule.test?.test?.('.svg')); + + config.module.rules.push( + { + ...fileLoaderRule, + test: /\.svg$/i, + resourceQuery: /url/, + }, + { + test: /\.svg$/i, + issuer: fileLoaderRule.issuer, + resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, + use: ['@svgr/webpack'], + }, + ); + + fileLoaderRule.exclude = /\.svg$/i; config.resolve.alias['public'] = path.resolve('./public'); diff --git a/package.json b/package.json index 79960eb2..5b005f66 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@fontsource/inter": "^4.5.15", "@prisma/client": "5.3.1", "@tanstack/react-query": "^4.33.0", - "@umami/prisma-client": "^0.2.0", + "@umami/prisma-client": "^0.3.0", "@umami/redis-client": "^0.15.0", "chalk": "^4.1.1", "chart.js": "^4.2.1", @@ -91,7 +91,7 @@ "kafkajs": "^2.1.0", "maxmind": "^4.3.6", "moment-timezone": "^0.5.35", - "next": "13.5.2", + "next": "13.5.3", "next-basics": "^0.36.0", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", @@ -100,7 +100,7 @@ "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.4", - "react-intl": "^5.24.7", + "react-intl": "^6.4.7", "react-simple-maps": "^2.3.0", "react-spring": "^9.4.4", "react-use-measure": "^2.0.4", @@ -123,12 +123,12 @@ "@rollup/plugin-node-resolve": "^15.2.0", "@rollup/plugin-replace": "^5.0.2", "@svgr/rollup": "^8.1.0", - "@svgr/webpack": "^6.2.1", + "@svgr/webpack": "^8.1.0", "@types/node": "^18.11.9", "@types/react": "^18.0.25", "@types/react-dom": "^18.0.8", - "@typescript-eslint/eslint-plugin": "^5.50.0", - "@typescript-eslint/parser": "^5.50.0", + "@typescript-eslint/eslint-plugin": "^6.7.3", + "@typescript-eslint/parser": "^6.7.3", "cross-env": "^7.0.3", "esbuild": "^0.17.17", "eslint": "^8.33.0", @@ -138,8 +138,8 @@ "eslint-plugin-import": "^2.26.0", "eslint-plugin-prettier": "^4.0.0", "extract-react-intl-messages": "^4.1.1", - "husky": "^7.0.0", - "lint-staged": "^11.0.0", + "husky": "^8.0.3", + "lint-staged": "^14.0.1", "postcss": "^8.4.21", "postcss-flexbugs-fixes": "^5.0.2", "postcss-import": "^15.1.0", diff --git a/src/app/(app)/NavBar.js b/src/app/(app)/NavBar.js new file mode 100644 index 00000000..211adf5f --- /dev/null +++ b/src/app/(app)/NavBar.js @@ -0,0 +1,58 @@ +'use client'; +import { Icon, Text } from 'react-basics'; +import Link from 'next/link'; +import classNames from 'classnames'; +import Icons from 'components/icons'; +import ThemeButton from 'components/input/ThemeButton'; +import LanguageButton from 'components/input/LanguageButton'; +import ProfileButton from 'components/input/ProfileButton'; +import useMessages from 'components/hooks/useMessages'; +import HamburgerButton from 'components/common/HamburgerButton'; +import { usePathname } from 'next/navigation'; +import styles from './NavBar.module.css'; + +export function NavBar() { + const pathname = usePathname(); + const { formatMessage, labels } = useMessages(); + + const links = [ + { label: formatMessage(labels.dashboard), url: '/dashboard' }, + { label: formatMessage(labels.websites), url: '/websites' }, + { label: formatMessage(labels.reports), url: '/reports' }, + { label: formatMessage(labels.settings), url: '/settings' }, + ].filter(n => n); + + return ( +
+
+ + + + umami +
+
+ {links.map(({ url, label }) => { + return ( + + {label} + + ); + })} +
+
+ + + +
+
+ +
+
+ ); +} + +export default NavBar; diff --git a/src/components/layout/NavBar.module.css b/src/app/(app)/NavBar.module.css similarity index 75% rename from src/components/layout/NavBar.module.css rename to src/app/(app)/NavBar.module.css index dd5085a0..fd022eca 100644 --- a/src/components/layout/NavBar.module.css +++ b/src/app/(app)/NavBar.module.css @@ -1,7 +1,7 @@ .navbar { + display: grid; + grid-template-columns: max-content 1fr 1fr; position: relative; - display: flex; - flex-direction: row; align-items: center; height: 60px; background: var(--base75); @@ -9,17 +9,6 @@ padding: 0 20px; } -.left, -.right { - display: flex; - flex-direction: row; - align-items: center; -} - -.right { - justify-content: flex-end; -} - .logo { display: flex; flex-direction: row; @@ -35,29 +24,24 @@ flex-direction: row; gap: 30px; padding: 0 40px; - flex: 1; font-weight: 700; + max-height: 60px; } -.links a { - display: flex; - align-items: center; - gap: 10px; - line-height: 60px; +.links a, +.links a:active, +.links a:visited { color: var(--font-color200); + line-height: 60px; border-bottom: 2px solid transparent; } -.links span { - white-space: nowrap; -} - .links a:hover { color: var(--font-color100); border-bottom: 2px solid var(--primary400); } -.links .selected { +.links a.selected { color: var(--font-color100); border-bottom: 2px solid var(--primary400); } @@ -68,7 +52,6 @@ flex-direction: row; align-items: center; justify-content: flex-end; - min-width: 0; } .mobile { @@ -76,6 +59,10 @@ } @media only screen and (max-width: 768px) { + .navbar { + grid-template-columns: repeat(2, 1fr); + } + .links, .actions { display: none; diff --git a/src/app/(app)/Shell.tsx b/src/app/(app)/Shell.tsx new file mode 100644 index 00000000..980abb62 --- /dev/null +++ b/src/app/(app)/Shell.tsx @@ -0,0 +1,27 @@ +'use client'; +import Script from 'next/script'; +import { usePathname } from 'next/navigation'; +import UpdateNotice from 'components/common/UpdateNotice'; +import { useRequireLogin, useConfig } from 'components/hooks'; + +export function Shell({ children }) { + const { user } = useRequireLogin(); + const config = useConfig(); + const pathname = usePathname(); + + if (!user || !config) { + return null; + } + + return ( + <> + {children} + + {process.env.NODE_ENV === 'production' && !pathname.includes('/share/') && ( + `; diff --git a/src/components/pages/settings/websites/WebsiteData.js b/src/app/(app)/settings/websites/[id]/WebsiteData.js similarity index 89% rename from src/components/pages/settings/websites/WebsiteData.js rename to src/app/(app)/settings/websites/[id]/WebsiteData.js index 08d6702e..07dc9257 100644 --- a/src/components/pages/settings/websites/WebsiteData.js +++ b/src/app/(app)/settings/websites/[id]/WebsiteData.js @@ -1,6 +1,6 @@ import { Button, Modal, ModalTrigger, ActionForm } from 'react-basics'; -import WebsiteDeleteForm from 'components/pages/settings/websites/WebsiteDeleteForm'; -import WebsiteResetForm from 'components/pages/settings/websites/WebsiteResetForm'; +import WebsiteDeleteForm from './WebsiteDeleteForm'; +import WebsiteResetForm from './WebsiteResetForm'; import useMessages from 'components/hooks/useMessages'; export function WebsiteData({ websiteId, onSave }) { diff --git a/src/components/pages/settings/websites/WebsiteDeleteForm.js b/src/app/(app)/settings/websites/[id]/WebsiteDeleteForm.js similarity index 100% rename from src/components/pages/settings/websites/WebsiteDeleteForm.js rename to src/app/(app)/settings/websites/[id]/WebsiteDeleteForm.js diff --git a/src/components/pages/settings/websites/WebsiteEditForm.js b/src/app/(app)/settings/websites/[id]/WebsiteEditForm.js similarity index 100% rename from src/components/pages/settings/websites/WebsiteEditForm.js rename to src/app/(app)/settings/websites/[id]/WebsiteEditForm.js diff --git a/src/components/pages/settings/websites/WebsiteResetForm.js b/src/app/(app)/settings/websites/[id]/WebsiteResetForm.js similarity index 100% rename from src/components/pages/settings/websites/WebsiteResetForm.js rename to src/app/(app)/settings/websites/[id]/WebsiteResetForm.js diff --git a/src/app/(app)/settings/websites/[id]/page.js b/src/app/(app)/settings/websites/[id]/page.js new file mode 100644 index 00000000..bdf3b076 --- /dev/null +++ b/src/app/(app)/settings/websites/[id]/page.js @@ -0,0 +1,15 @@ +import WebsiteSettings from '../WebsiteSettings'; + +async function getDisabled() { + return !!process.env.CLOUD_MODE; +} + +export default async function WebsiteSettingsPage({ params }) { + const disabled = await getDisabled(); + + if (!params.id || disabled) { + return null; + } + + return ; +} diff --git a/src/app/(app)/settings/websites/page.js b/src/app/(app)/settings/websites/page.js new file mode 100644 index 00000000..ade3e3ad --- /dev/null +++ b/src/app/(app)/settings/websites/page.js @@ -0,0 +1,9 @@ +import WebsitesList from 'app/(app)/settings/websites/WebsitesList'; + +export default function () { + if (process.env.cloudMode) { + return null; + } + + return ; +} diff --git a/src/components/pages/websites/WebsiteTableView.js b/src/app/(app)/websites/WebsiteTableView.js similarity index 100% rename from src/components/pages/websites/WebsiteTableView.js rename to src/app/(app)/websites/WebsiteTableView.js diff --git a/src/components/pages/websites/WebsiteTableView.module.css b/src/app/(app)/websites/WebsiteTableView.module.css similarity index 100% rename from src/components/pages/websites/WebsiteTableView.module.css rename to src/app/(app)/websites/WebsiteTableView.module.css diff --git a/src/components/pages/websites/WebsiteChart.js b/src/app/(app)/websites/[id]/WebsiteChart.js similarity index 100% rename from src/components/pages/websites/WebsiteChart.js rename to src/app/(app)/websites/[id]/WebsiteChart.js diff --git a/src/components/pages/websites/WebsiteChart.module.css b/src/app/(app)/websites/[id]/WebsiteChart.module.css similarity index 100% rename from src/components/pages/websites/WebsiteChart.module.css rename to src/app/(app)/websites/[id]/WebsiteChart.module.css diff --git a/src/components/pages/websites/WebsiteChartList.js b/src/app/(app)/websites/[id]/WebsiteChartList.js similarity index 90% rename from src/components/pages/websites/WebsiteChartList.js rename to src/app/(app)/websites/[id]/WebsiteChartList.js index 56cbe157..23764dbb 100644 --- a/src/components/pages/websites/WebsiteChartList.js +++ b/src/app/(app)/websites/[id]/WebsiteChartList.js @@ -2,9 +2,8 @@ import { Button, Text, Icon } from 'react-basics'; import { useMemo } from 'react'; import { firstBy } from 'thenby'; import Link from 'next/link'; -import WebsiteChart from 'components/pages/websites/WebsiteChart'; +import WebsiteChart from './WebsiteChart'; import useDashboard from 'store/dashboard'; -import styles from './WebsiteList.module.css'; import WebsiteHeader from './WebsiteHeader'; import { WebsiteMetricsBar } from './WebsiteMetricsBar'; import { useMessages, useLocale } from 'components/hooks'; @@ -27,7 +26,7 @@ export default function WebsiteChartList({ websites, showCharts, limit }) {
{ordered.map(({ id }, index) => { return index < limit ? ( -
+
- - - - {({ data }) => } - - - ); -} - -export default ReportsPage; diff --git a/src/components/pages/settings/profile/ProfileSettings.js b/src/components/pages/settings/profile/ProfileSettings.js deleted file mode 100644 index a217e52c..00000000 --- a/src/components/pages/settings/profile/ProfileSettings.js +++ /dev/null @@ -1,17 +0,0 @@ -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import ProfileDetails from './ProfileDetails'; -import useMessages from 'components/hooks/useMessages'; - -export function ProfileSettings() { - const { formatMessage, labels } = useMessages(); - - return ( - - - - - ); -} - -export default ProfileSettings; diff --git a/src/components/pages/settings/teams/TeamsList.js b/src/components/pages/settings/teams/TeamsList.js deleted file mode 100644 index 76a87b0c..00000000 --- a/src/components/pages/settings/teams/TeamsList.js +++ /dev/null @@ -1,118 +0,0 @@ -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import Icons from 'components/icons'; -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import TeamAddForm from 'components/pages/settings/teams/TeamAddForm'; -import TeamsTable from 'components/pages/settings/teams/TeamsTable'; -import useApi from 'components/hooks/useApi'; -import useMessages from 'components/hooks/useMessages'; -import useUser from 'components/hooks/useUser'; -import { ROLES } from 'lib/constants'; -import { useState } from 'react'; -import { Button, Flexbox, Icon, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; -import TeamJoinForm from './TeamJoinForm'; -import useApiFilter from 'components/hooks/useApiFilter'; - -export function TeamsList() { - const { user } = useUser(); - const { formatMessage, labels, messages } = useMessages(); - const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = - useApiFilter(); - const [update, setUpdate] = useState(0); - - const { get, useQuery } = useApi(); - const { data, isLoading, error } = useQuery(['teams', update, filter, page, pageSize], () => { - return get(`/teams`, { - filter, - page, - pageSize, - }); - }); - - const hasData = data && data?.data.length !== 0; - const isFiltered = filter; - - const { showToast } = useToasts(); - - const handleSave = () => { - setUpdate(state => state + 1); - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - const handleJoin = () => { - setUpdate(state => state + 1); - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - const handleDelete = () => { - setUpdate(state => state + 1); - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - const joinButton = ( - - - - {close => } - - - ); - - const createButton = ( - <> - {user.role !== ROLES.viewOnly && ( - - - - {close => } - - - )} - - ); - - return ( - - - {(hasData || isFiltered) && ( - - {joinButton} - {createButton} - - )} - - - {(hasData || isFiltered) && ( - - )} - - {!hasData && !isFiltered && ( - - - {joinButton} - {createButton} - - - )} - - ); -} - -export default TeamsList; diff --git a/src/components/pages/settings/teams/TeamsTable.js b/src/components/pages/settings/teams/TeamsTable.js deleted file mode 100644 index e1710783..00000000 --- a/src/components/pages/settings/teams/TeamsTable.js +++ /dev/null @@ -1,111 +0,0 @@ -import SettingsTable from 'components/common/SettingsTable'; -import useLocale from 'components/hooks/useLocale'; -import useMessages from 'components/hooks/useMessages'; -import useUser from 'components/hooks/useUser'; -import { ROLES } from 'lib/constants'; -import Link from 'next/link'; -import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; -import TeamDeleteForm from './TeamDeleteForm'; -import TeamLeaveForm from './TeamLeaveForm'; - -export function TeamsTable({ - data = { data: [] }, - onDelete, - filterValue, - onFilterChange, - onPageChange, - onPageSizeChange, -}) { - const { formatMessage, labels } = useMessages(); - const { user } = useUser(); - const { dir } = useLocale(); - - const columns = [ - { name: 'name', label: formatMessage(labels.name) }, - { name: 'owner', label: formatMessage(labels.owner) }, - { name: 'action', label: ' ' }, - ]; - - const cellRender = (row, data, key) => { - if (key === 'owner') { - return row.teamUser.find(({ role }) => role === ROLES.teamOwner)?.user?.username; - } - return data[key]; - }; - - return ( - - {row => { - const { id, teamUser } = row; - const owner = teamUser.find(({ role }) => role === ROLES.teamOwner); - const showDelete = user.id === owner?.userId; - - return ( - <> - - - - {showDelete && ( - - - - {close => ( - - )} - - - )} - {!showDelete && ( - - - - {close => ( - - )} - - - )} - - ); - }} - - ); -} - -export default TeamsTable; diff --git a/src/components/pages/settings/users/UsersList.js b/src/components/pages/settings/users/UsersList.js deleted file mode 100644 index 0bc8612e..00000000 --- a/src/components/pages/settings/users/UsersList.js +++ /dev/null @@ -1,68 +0,0 @@ -import { useToasts } from 'react-basics'; -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import UsersTable from './UsersTable'; -import UserAddButton from './UserAddButton'; -import useApi from 'components/hooks/useApi'; -import useUser from 'components/hooks/useUser'; -import useMessages from 'components/hooks/useMessages'; -import useApiFilter from 'components/hooks/useApiFilter'; - -export function UsersList() { - const { formatMessage, labels, messages } = useMessages(); - const { user } = useUser(); - const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = - useApiFilter(); - - const { get, useQuery } = useApi(); - const { data, isLoading, error, refetch } = useQuery( - ['user', filter, page, pageSize], - () => - get(`/users`, { - filter, - page, - pageSize, - }), - { - enabled: !!user, - }, - ); - const { showToast } = useToasts(); - const hasData = data && data.length !== 0; - - const handleSave = () => { - refetch().then(() => showToast({ message: formatMessage(messages.saved), variant: 'success' })); - }; - - const handleDelete = () => { - refetch().then(() => - showToast({ message: formatMessage(messages.userDeleted), variant: 'success' }), - ); - }; - - return ( - - - - - {(hasData || filter) && ( - - )} - {!hasData && !filter && ( - - - - )} - - ); -} - -export default UsersList; diff --git a/src/components/pages/settings/users/UsersTable.js b/src/components/pages/settings/users/UsersTable.js deleted file mode 100644 index 1a93710d..00000000 --- a/src/components/pages/settings/users/UsersTable.js +++ /dev/null @@ -1,93 +0,0 @@ -import { Button, Text, Icon, Icons, ModalTrigger, Modal } from 'react-basics'; -import { formatDistance } from 'date-fns'; -import Link from 'next/link'; -import useUser from 'components/hooks/useUser'; -import UserDeleteForm from './UserDeleteForm'; -import { ROLES } from 'lib/constants'; -import useMessages from 'components/hooks/useMessages'; -import SettingsTable from 'components/common/SettingsTable'; -import useLocale from 'components/hooks/useLocale'; - -export function UsersTable({ - data = { data: [] }, - onDelete, - filterValue, - onFilterChange, - onPageChange, - onPageSizeChange, -}) { - const { formatMessage, labels } = useMessages(); - const { user } = useUser(); - const { dateLocale } = useLocale(); - - const columns = [ - { name: 'username', label: formatMessage(labels.username) }, - { name: 'role', label: formatMessage(labels.role) }, - { name: 'created', label: formatMessage(labels.created) }, - { name: 'action', label: ' ' }, - ]; - - const cellRender = (row, data, key) => { - if (key === 'created') { - return formatDistance(new Date(row.createdAt), new Date(), { - addSuffix: true, - locale: dateLocale, - }); - } - if (key === 'role') { - return formatMessage( - labels[Object.keys(ROLES).find(key => ROLES[key] === row.role)] || labels.unknown, - ); - } - return data[key]; - }; - - return ( - - {row => { - return ( - <> - - - - - - - {close => ( - - )} - - - - ); - }} - - ); -} - -export default UsersTable; diff --git a/src/components/pages/settings/websites/WebsitesList.js b/src/components/pages/settings/websites/WebsitesList.js deleted file mode 100644 index 0dd3aa77..00000000 --- a/src/components/pages/settings/websites/WebsitesList.js +++ /dev/null @@ -1,71 +0,0 @@ -import PageHeader from 'components/layout/PageHeader'; -import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm'; -import WebsitesTable from 'components/pages/settings/websites/WebsitesTable'; -import useMessages from 'components/hooks/useMessages'; -import useUser from 'components/hooks/useUser'; -import { ROLES } from 'lib/constants'; -import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; -import useApi from 'components/hooks/useApi'; -import DataTable from 'components/common/DataTable'; -import useFilterQuery from 'components/hooks/useFilterQuery'; - -export function WebsitesList({ - showTeam, - showEditButton = true, - showHeader = true, - includeTeams, - onlyTeams, -}) { - const { formatMessage, labels, messages } = useMessages(); - const { user } = useUser(); - const { get } = useApi(); - const filterQuery = useFilterQuery( - ['websites', { includeTeams, onlyTeams }], - params => { - return get(`/users/${user?.id}/websites`, { - includeTeams, - onlyTeams, - ...params, - }); - }, - { enabled: !!user }, - ); - const { refetch } = filterQuery; - const { showToast } = useToasts(); - - const handleSave = async () => { - await refetch(); - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - const addButton = ( - - - - {close => } - - - ); - - return ( - <> - {showHeader && ( - - {user.role !== ROLES.viewOnly && addButton} - - )} - - {({ data }) => ( - - )} - - - ); -} - -export default WebsitesList; diff --git a/src/components/pages/websites/WebsiteEventDataPage.js b/src/components/pages/websites/WebsiteEventDataPage.js deleted file mode 100644 index 08acafb5..00000000 --- a/src/components/pages/websites/WebsiteEventDataPage.js +++ /dev/null @@ -1,12 +0,0 @@ -import Page from 'components/layout/Page'; -import WebsiteHeader from './WebsiteHeader'; -import WebsiteEventData from './WebsiteEventData'; - -export default function WebsiteEventDataPage({ websiteId }) { - return ( - - - - - ); -} diff --git a/src/components/pages/websites/WebsitesPage.js b/src/components/pages/websites/WebsitesPage.js deleted file mode 100644 index 4c1ee409..00000000 --- a/src/components/pages/websites/WebsitesPage.js +++ /dev/null @@ -1,77 +0,0 @@ -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm'; -import WebsiteList from 'components/pages/settings/websites/WebsitesList'; -import { useMessages } from 'components/hooks'; -import useUser from 'components/hooks/useUser'; -import { ROLES } from 'lib/constants'; -import { useState } from 'react'; -import { - Button, - Icon, - Icons, - Item, - Modal, - ModalTrigger, - Tabs, - Text, - useToasts, -} from 'react-basics'; - -const TABS = { - myWebsites: 'my-websites', - teamWebsites: 'team-websites', -}; - -export function WebsitesPage() { - const { formatMessage, labels, messages } = useMessages(); - const [tab, setTab] = useState(TABS.myWebsites); - const { user } = useUser(); - const { showToast } = useToasts(); - const cloudMode = Boolean(process.env.cloudMode); - - const handleSave = () => { - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - const addButton = ( - <> - {user.role !== ROLES.viewOnly && ( - - - - {close => } - - - )} - - ); - - return ( - - {!cloudMode && addButton} - - {formatMessage(labels.myWebsites)} - {formatMessage(labels.teamWebsites)} - - {tab === TABS.myWebsites && ( - - )} - {tab === TABS.teamWebsites && ( - - )} - - ); -} - -export default WebsitesPage; diff --git a/src/index.ts b/src/index.ts index f2ef13ca..72fe733b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -89,29 +89,29 @@ export * from 'components/hooks/useUser'; export * from 'components/hooks/useWebsite'; export * from 'components/hooks/useWebsiteReports'; -export * from 'components/pages/settings/teams/TeamAddForm'; -export * from 'components/pages/settings/teams/TeamAddWebsiteForm'; -export * from 'components/pages/settings/teams/TeamDeleteForm'; -export * from 'components/pages/settings/teams/TeamEditForm'; -export * from 'components/pages/settings/teams/TeamJoinForm'; -export * from 'components/pages/settings/teams/TeamLeaveForm'; -export * from 'components/pages/settings/teams/TeamMemberRemoveButton'; -export * from 'components/pages/settings/teams/TeamMembers'; -export * from 'components/pages/settings/teams/TeamMembersTable'; -export * from 'components/pages/settings/teams/TeamSettings'; -export * from 'components/pages/settings/teams/TeamsList'; -export * from 'components/pages/settings/teams/TeamsTable'; -export * from 'components/pages/settings/teams/TeamWebsiteRemoveButton'; -export * from 'components/pages/settings/teams/TeamWebsites'; -export * from 'components/pages/settings/teams/TeamWebsitesTable'; -export * from 'components/pages/settings/teams/WebsiteTags'; +export * from 'app/(app)/settings/teams/TeamAddForm'; +export * from 'app/(app)/settings/teams/[id]/TeamAddWebsiteForm'; +export * from 'app/(app)/settings/teams/TeamDeleteForm'; +export * from 'app/(app)/settings/teams/[id]/TeamEditForm'; +export * from 'app/(app)/settings/teams/TeamJoinForm'; +export * from 'app/(app)/settings/teams/TeamLeaveForm'; +export * from 'app/(app)/settings/teams/[id]/TeamMemberRemoveButton'; +export * from 'app/(app)/settings/teams/[id]/TeamMembers'; +export * from 'app/(app)/settings/teams/[id]/TeamMembersTable'; +export * from 'app/(app)/settings/teams/[id]/TeamSettings'; +export * from 'app/(app)/settings/teams/TeamsList'; +export * from 'app/(app)/settings/teams/TeamsTable'; +export * from 'app/(app)/settings/teams/TeamWebsiteRemoveButton'; +export * from 'app/(app)/settings/teams/[id]/TeamWebsites'; +export * from 'app/(app)/settings/teams/[id]/TeamWebsitesTable'; +export * from 'app/(app)/settings/teams/WebsiteTags'; -export * from 'components/pages/settings/websites/ShareUrl'; -export * from 'components/pages/settings/websites/TrackingCode'; -export * from 'components/pages/settings/websites/WebsiteAddForm'; -export * from 'components/pages/settings/websites/WebsiteDeleteForm'; -export * from 'components/pages/settings/websites/WebsiteEditForm'; -export * from 'components/pages/settings/websites/WebsiteResetForm'; -export * from 'components/pages/settings/websites/WebsiteSettings'; -export * from 'components/pages/settings/websites/WebsitesList'; -export * from 'components/pages/settings/websites/WebsitesTable'; +export * from 'app/(app)/settings/websites/[id]/ShareUrl'; +export * from 'app/(app)/settings/websites/[id]/TrackingCode'; +export * from 'app/(app)/settings/websites/WebsiteAddForm'; +export * from 'app/(app)/settings/websites/[id]/WebsiteDeleteForm'; +export * from 'app/(app)/settings/websites/[id]/WebsiteEditForm'; +export * from 'app/(app)/settings/websites/[id]/WebsiteResetForm'; +export * from 'app/(app)/settings/websites/WebsiteSettings'; +export * from 'app/(app)/settings/websites/WebsitesList'; +export * from 'app/(app)/settings/websites/WebsitesTable'; diff --git a/src/lib/middleware.ts b/src/lib/middleware.ts index 4be958b6..e1e2a38b 100644 --- a/src/lib/middleware.ts +++ b/src/lib/middleware.ts @@ -14,7 +14,6 @@ import { } from 'next-basics'; import { NextApiRequestCollect } from 'pages/api/send'; import { getUserById } from '../queries'; -import { NextApiRequestQueryBody } from './types'; const log = debug('umami:middleware'); @@ -83,14 +82,18 @@ export const useAuth = createMiddleware(async (req, res, next) => { next(); }); -export const useValidate = createMiddleware(async (req: any, res, next) => { - try { - const { yup } = req as NextApiRequestQueryBody; +export const useValidate = async (schema, req, res) => { + return createMiddleware(async (req: any, res, next) => { + try { + const rules = schema[req.method]; - yup[req.method] && yup[req.method].validateSync({ ...req.query, ...req.body }); - } catch (e: any) { - return badRequest(res, e.message); - } + if (rules) { + rules.validateSync(req.method === 'GET' ? { ...req.query } : { ...req.body }); + } + } catch (e: any) { + return badRequest(res, e.message); + } - next(); -}); + next(); + })(req, res); +}; diff --git a/src/pages/404.js b/src/pages/404.js deleted file mode 100644 index 8fa13a9c..00000000 --- a/src/pages/404.js +++ /dev/null @@ -1,19 +0,0 @@ -import { Row, Column, Flexbox } from 'react-basics'; -import AppLayout from 'components/layout/AppLayout'; -import useMessages from 'components/hooks/useMessages'; - -export default function Custom404() { - const { formatMessage, labels } = useMessages(); - - return ( - - - - -

{formatMessage(labels.pageNotFound)}

-
-
-
-
- ); -} diff --git a/src/pages/_app.js b/src/pages/_app.js deleted file mode 100644 index 7022772c..00000000 --- a/src/pages/_app.js +++ /dev/null @@ -1,69 +0,0 @@ -import { IntlProvider } from 'react-intl'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { ReactBasicsProvider } from 'react-basics'; -import Head from 'next/head'; -import Script from 'next/script'; -import { useRouter } from 'next/router'; -import ErrorBoundary from 'components/common/ErrorBoundary'; -import useLocale from 'components/hooks/useLocale'; -import '@fontsource/inter/400.css'; -import '@fontsource/inter/700.css'; -import 'react-basics/dist/styles.css'; -import 'styles/variables.css'; -import 'styles/locale.css'; -import 'styles/index.css'; -import 'chartjs-adapter-date-fns'; - -const client = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - refetchOnWindowFocus: false, - }, - }, -}); - -export default function App({ Component, pageProps }) { - const { locale, messages } = useLocale(); - const { basePath, pathname } = useRouter(); - - return ( - - null}> - - - - - - - - - - - - - - - - - {!pathname.includes('/share/') && `; diff --git a/src/app/(main)/settings/websites/[id]/WebsiteData.tsx b/src/app/(main)/settings/websites/[id]/WebsiteData.tsx index b4bfe609..0ad3b559 100644 --- a/src/app/(main)/settings/websites/[id]/WebsiteData.tsx +++ b/src/app/(main)/settings/websites/[id]/WebsiteData.tsx @@ -29,7 +29,7 @@ export function WebsiteData({ - {close => ( + {(close: () => void) => ( )} @@ -42,7 +42,7 @@ export function WebsiteData({ - {close => ( + {(close: () => void) => ( )} diff --git a/src/app/(main)/settings/websites/[id]/WebsiteDeleteForm.tsx b/src/app/(main)/settings/websites/[id]/WebsiteDeleteForm.tsx index c3b5d74a..e0f71041 100644 --- a/src/app/(main)/settings/websites/[id]/WebsiteDeleteForm.tsx +++ b/src/app/(main)/settings/websites/[id]/WebsiteDeleteForm.tsx @@ -9,6 +9,8 @@ import { } from 'react-basics'; import useApi from 'components/hooks/useApi'; import useMessages from 'components/hooks/useMessages'; +import { useContext } from 'react'; +import SettingsContext from '../../SettingsContext'; const CONFIRM_VALUE = 'DELETE'; @@ -22,12 +24,13 @@ export function WebsiteDeleteForm({ onClose?: () => void; }) { const { formatMessage, labels, messages, FormattedMessage } = useMessages(); + const { websitesUrl } = useContext(SettingsContext); const { del, useMutation } = useApi(); const { mutate, error } = useMutation({ - mutationFn: (data: any) => del(`/websites/${websiteId}`, data), + mutationFn: (data: any) => del(`${websitesUrl}/${websiteId}`, data), }); - const handleSubmit = async data => { + const handleSubmit = async (data: any) => { mutate(data, { onSuccess: async () => { onSave(); diff --git a/src/app/(main)/settings/websites/[id]/WebsiteEditForm.tsx b/src/app/(main)/settings/websites/[id]/WebsiteEditForm.tsx index 9c05905c..80b36cae 100644 --- a/src/app/(main)/settings/websites/[id]/WebsiteEditForm.tsx +++ b/src/app/(main)/settings/websites/[id]/WebsiteEditForm.tsx @@ -1,8 +1,9 @@ import { SubmitButton, Form, FormInput, FormRow, FormButtons, TextField } from 'react-basics'; -import { useRef } from 'react'; +import { useContext, useRef } from 'react'; import useApi from 'components/hooks/useApi'; import { DOMAIN_REGEX } from 'lib/constants'; import useMessages from 'components/hooks/useMessages'; +import SettingsContext from '../../SettingsContext'; export function WebsiteEditForm({ websiteId, @@ -14,13 +15,14 @@ export function WebsiteEditForm({ onSave?: (data: any) => void; }) { const { formatMessage, labels, messages } = useMessages(); + const { websitesUrl } = useContext(SettingsContext); const { post, useMutation } = useApi(); const { mutate, error } = useMutation({ - mutationFn: (data: any) => post(`/websites/${websiteId}`, data), + mutationFn: (data: any) => post(`${websitesUrl}/${websiteId}`, data), }); const ref = useRef(null); - const handleSubmit = async data => { + const handleSubmit = async (data: any) => { mutate(data, { onSuccess: async () => { ref.current.reset(data); diff --git a/src/app/(main)/settings/websites/[id]/WebsiteResetForm.tsx b/src/app/(main)/settings/websites/[id]/WebsiteResetForm.tsx index 76c2bc47..0c02c77b 100644 --- a/src/app/(main)/settings/websites/[id]/WebsiteResetForm.tsx +++ b/src/app/(main)/settings/websites/[id]/WebsiteResetForm.tsx @@ -9,6 +9,8 @@ import { } from 'react-basics'; import useApi from 'components/hooks/useApi'; import useMessages from 'components/hooks/useMessages'; +import { useContext } from 'react'; +import SettingsContext from '../../SettingsContext'; const CONFIRM_VALUE = 'RESET'; @@ -22,9 +24,10 @@ export function WebsiteResetForm({ onClose?: () => void; }) { const { formatMessage, labels, messages, FormattedMessage } = useMessages(); + const { websitesUrl } = useContext(SettingsContext); const { post, useMutation } = useApi(); const { mutate, error } = useMutation({ - mutationFn: (data: any) => post(`/websites/${websiteId}/reset`, data), + mutationFn: (data: any) => post(`${websitesUrl}/${websiteId}/reset`, data), }); const handleSubmit = async (data: any) => { diff --git a/src/app/sso/page.tsx b/src/app/sso/page.tsx index 75ea945d..e577767a 100644 --- a/src/app/sso/page.tsx +++ b/src/app/sso/page.tsx @@ -18,5 +18,5 @@ export default function SSOPage() { } }, [router, url, token]); - return ; + return ; } diff --git a/src/components/layout/Page.tsx b/src/components/layout/Page.tsx index 2f702012..e32a09a3 100644 --- a/src/components/layout/Page.tsx +++ b/src/components/layout/Page.tsx @@ -23,7 +23,7 @@ export function Page({ } if (isLoading) { - return ; + return ; } return
{children}
; diff --git a/src/index.ts b/src/index.ts index de555051..7b192054 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,6 +48,8 @@ export * from 'app/(main)/settings/websites/WebsiteSettings'; export * from 'app/(main)/settings/websites/WebsitesDataTable'; export * from 'app/(main)/settings/websites/WebsitesTable'; +export * from 'app/(main)/settings/SettingsContext'; + export * from 'components/common/ConfirmDeleteForm'; export * from 'components/common/DataTable'; export * from 'components/common/Empty'; From c8eb76c7af8c6aee1a084c805314ff17eb4301c9 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 6 Dec 2023 01:26:58 -0800 Subject: [PATCH 145/163] Fixed share url. --- src/app/(main)/settings/layout.tsx | 3 +-- src/app/(main)/settings/websites/WebsiteSettings.tsx | 2 +- src/app/(main)/settings/websites/[id]/ShareUrl.tsx | 6 +++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/app/(main)/settings/layout.tsx b/src/app/(main)/settings/layout.tsx index f9612361..1c30d2db 100644 --- a/src/app/(main)/settings/layout.tsx +++ b/src/app/(main)/settings/layout.tsx @@ -21,7 +21,7 @@ export default function SettingsLayout({ children }) { const getKey = () => items.find(({ url }) => pathname === url)?.key; - if (cloudMode && pathname != '/settings/profile') { + if (cloudMode && pathname !== '/settings/profile') { return null; } @@ -29,7 +29,6 @@ export default function SettingsLayout({ children }) { const config = { settingsUrl: '/settings/websites', - hostUrl, shareUrl: hostUrl, trackingCodeUrl: hostUrl, websitesUrl: `/websites`, diff --git a/src/app/(main)/settings/websites/WebsiteSettings.tsx b/src/app/(main)/settings/websites/WebsiteSettings.tsx index e925d1cf..4607b423 100644 --- a/src/app/(main)/settings/websites/WebsiteSettings.tsx +++ b/src/app/(main)/settings/websites/WebsiteSettings.tsx @@ -51,7 +51,7 @@ export function WebsiteSettings({ websiteId, openExternal = false }) { }, [data]); if (isLoading || !values) { - return ; + return ; } return ( diff --git a/src/app/(main)/settings/websites/[id]/ShareUrl.tsx b/src/app/(main)/settings/websites/[id]/ShareUrl.tsx index 07a590fa..90ce6ea6 100644 --- a/src/app/(main)/settings/websites/[id]/ShareUrl.tsx +++ b/src/app/(main)/settings/websites/[id]/ShareUrl.tsx @@ -17,15 +17,15 @@ import SettingsContext from '../../SettingsContext'; const generateId = () => getRandomChars(16); export function ShareUrl({ websiteId, data, onSave }) { + const ref = useRef(null); + const { shareUrl, websitesUrl } = useContext(SettingsContext); const { formatMessage, labels, messages } = useMessages(); const { name, shareId } = data; const [id, setId] = useState(shareId); const { post, useMutation } = useApi(); const { mutate, error } = useMutation({ - mutationFn: (data: any) => post(`/websites/${websiteId}`, data), + mutationFn: (data: any) => post(`${websitesUrl}/${websiteId}`, data), }); - const ref = useRef(null); - const { shareUrl } = useContext(SettingsContext); const url = useMemo( () => `${shareUrl}${process.env.basePath}/share/${id}/${encodeURIComponent(name)}`, [id, name], From e67282d7d895a7c430174a6b343d05df8fe54c88 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 8 Dec 2023 22:14:55 -0800 Subject: [PATCH 146/163] Render correct OS names. --- src/components/input/SettingsButton.tsx | 7 +------ src/components/metrics/OSTable.tsx | 10 ++++++++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/input/SettingsButton.tsx b/src/components/input/SettingsButton.tsx index 46c72597..2a076d42 100644 --- a/src/components/input/SettingsButton.tsx +++ b/src/components/input/SettingsButton.tsx @@ -15,12 +15,7 @@ export function SettingsButton() { - e.stopPropagation()} - > +
diff --git a/src/components/metrics/OSTable.tsx b/src/components/metrics/OSTable.tsx index e8b8e81f..c39cba22 100644 --- a/src/components/metrics/OSTable.tsx +++ b/src/components/metrics/OSTable.tsx @@ -2,14 +2,20 @@ import MetricsTable from './MetricsTable'; import FilterLink from 'components/common/FilterLink'; import useMessages from 'components/hooks/useMessages'; +const names = { + 'Mac OS': 'macOS', + 'Chrome OS': 'ChromeOS', + 'Sun OS': 'SunOS', +}; + export function OSTable({ websiteId, limit }: { websiteId: string; limit?: number }) { const { formatMessage, labels } = useMessages(); function renderLink({ x: os }) { return ( - + {os} Date: Sat, 9 Dec 2023 00:35:54 -0800 Subject: [PATCH 147/163] Convert realtime components to TS. --- ...bsiteEventData.js => WebsiteEventData.tsx} | 2 +- .../realtime/{Realtime.js => Realtime.tsx} | 22 +++++----- ...timeCountries.js => RealtimeCountries.tsx} | 0 .../{RealtimeHeader.js => RealtimeHeader.tsx} | 5 ++- .../{RealtimeHome.js => RealtimeHome.tsx} | 0 .../{RealtimeLog.js => RealtimeLog.tsx} | 4 +- .../{RealtimeUrls.js => RealtimeUrls.tsx} | 21 ++++++---- .../{WebsiteReports.js => WebsiteReports.tsx} | 0 src/components/hooks/useDateRange.ts | 14 ++++--- src/components/metrics/RealtimeChart.tsx | 7 ++-- src/lib/date.ts | 40 ++++++++++--------- src/lib/types.ts | 11 ++++- 12 files changed, 75 insertions(+), 51 deletions(-) rename src/app/(main)/websites/[id]/event-data/{WebsiteEventData.js => WebsiteEventData.tsx} (96%) rename src/app/(main)/websites/[id]/realtime/{Realtime.js => Realtime.tsx} (84%) rename src/app/(main)/websites/[id]/realtime/{RealtimeCountries.js => RealtimeCountries.tsx} (100%) rename src/app/(main)/websites/[id]/realtime/{RealtimeHeader.js => RealtimeHeader.tsx} (85%) rename src/app/(main)/websites/[id]/realtime/{RealtimeHome.js => RealtimeHome.tsx} (100%) rename src/app/(main)/websites/[id]/realtime/{RealtimeLog.js => RealtimeLog.tsx} (97%) rename src/app/(main)/websites/[id]/realtime/{RealtimeUrls.js => RealtimeUrls.tsx} (85%) rename src/app/(main)/websites/[id]/reports/{WebsiteReports.js => WebsiteReports.tsx} (100%) diff --git a/src/app/(main)/websites/[id]/event-data/WebsiteEventData.js b/src/app/(main)/websites/[id]/event-data/WebsiteEventData.tsx similarity index 96% rename from src/app/(main)/websites/[id]/event-data/WebsiteEventData.js rename to src/app/(main)/websites/[id]/event-data/WebsiteEventData.tsx index b67ee95e..61a4dc62 100644 --- a/src/app/(main)/websites/[id]/event-data/WebsiteEventData.js +++ b/src/app/(main)/websites/[id]/event-data/WebsiteEventData.tsx @@ -6,7 +6,7 @@ import { EventDataMetricsBar } from './EventDataMetricsBar'; import { useDateRange, useApi, useNavigation } from 'components/hooks'; import styles from './WebsiteEventData.module.css'; -function useData(websiteId, event) { +function useData(websiteId: string, event: string) { const [dateRange] = useDateRange(websiteId); const { startDate, endDate } = dateRange; const { get, useQuery } = useApi(); diff --git a/src/app/(main)/websites/[id]/realtime/Realtime.js b/src/app/(main)/websites/[id]/realtime/Realtime.tsx similarity index 84% rename from src/app/(main)/websites/[id]/realtime/Realtime.js rename to src/app/(main)/websites/[id]/realtime/Realtime.tsx index 37df458c..6de65d7a 100644 --- a/src/app/(main)/websites/[id]/realtime/Realtime.js +++ b/src/app/(main)/websites/[id]/realtime/Realtime.tsx @@ -1,7 +1,7 @@ 'use client'; import { useMemo, useState, useEffect } from 'react'; import { subMinutes, startOfMinute } from 'date-fns'; -import firstBy from 'thenby'; +import thenby from 'thenby'; import { Grid, GridRow } from 'components/layout/Grid'; import Page from 'components/layout/Page'; import RealtimeChart from 'components/metrics/RealtimeChart'; @@ -15,9 +15,10 @@ import useApi from 'components/hooks/useApi'; import { percentFilter } from 'lib/filters'; import { REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants'; import { useWebsite } from 'components/hooks'; +import { RealtimeData } from 'lib/types'; import styles from './Realtime.module.css'; -function mergeData(state = [], data = [], time) { +function mergeData(state = [], data = [], time: number) { const ids = state.map(({ __id }) => __id); return state .concat(data.filter(({ __id }) => !ids.includes(__id))) @@ -25,7 +26,7 @@ function mergeData(state = [], data = [], time) { } export function Realtime({ websiteId }) { - const [currentData, setCurrentData] = useState(); + const [currentData, setCurrentData] = useState(); const { get, useQuery } = useApi(); const { data: website } = useWebsite(websiteId); const { data, isLoading, error } = useQuery({ @@ -33,7 +34,6 @@ export function Realtime({ websiteId }) { queryFn: () => get(`/realtime/${websiteId}`, { startAt: currentData?.timestamp || 0 }), enabled: !!(websiteId && website), refetchInterval: REALTIME_INTERVAL, - cache: false, }); useEffect(() => { @@ -50,9 +50,9 @@ export function Realtime({ websiteId }) { } }, [data]); - const realtimeData = useMemo(() => { + const realtimeData: RealtimeData = useMemo(() => { if (!currentData) { - return { pageviews: [], sessions: [], events: [], countries: [], visitors: [] }; + return { pageviews: [], sessions: [], events: [], countries: [], visitors: [], timestamp: 0 }; } currentData.countries = percentFilter( @@ -75,7 +75,7 @@ export function Realtime({ websiteId }) { } return arr; }, []) - .sort(firstBy('y', -1)), + .sort(thenby.firstBy('y', -1)), ); currentData.visitors = currentData.sessions.reduce((arr, val) => { @@ -89,18 +89,18 @@ export function Realtime({ websiteId }) { }, [currentData]); if (isLoading || error) { - return ; + return ; } return ( <> - + - - + + diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeCountries.js b/src/app/(main)/websites/[id]/realtime/RealtimeCountries.tsx similarity index 100% rename from src/app/(main)/websites/[id]/realtime/RealtimeCountries.js rename to src/app/(main)/websites/[id]/realtime/RealtimeCountries.tsx diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeHeader.js b/src/app/(main)/websites/[id]/realtime/RealtimeHeader.tsx similarity index 85% rename from src/app/(main)/websites/[id]/realtime/RealtimeHeader.js rename to src/app/(main)/websites/[id]/realtime/RealtimeHeader.tsx index 75f2f2d4..ad03efd1 100644 --- a/src/app/(main)/websites/[id]/realtime/RealtimeHeader.js +++ b/src/app/(main)/websites/[id]/realtime/RealtimeHeader.tsx @@ -1,10 +1,11 @@ import MetricCard from 'components/metrics/MetricCard'; import useMessages from 'components/hooks/useMessages'; +import { RealtimeData } from 'lib/types'; import styles from './RealtimeHeader.module.css'; -export function RealtimeHeader({ data = {} }) { +export function RealtimeHeader({ data }: { data: RealtimeData }) { const { formatMessage, labels } = useMessages(); - const { pageviews, visitors, events, countries } = data; + const { pageviews, visitors, events, countries } = data || {}; return (
diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeHome.js b/src/app/(main)/websites/[id]/realtime/RealtimeHome.tsx similarity index 100% rename from src/app/(main)/websites/[id]/realtime/RealtimeHome.js rename to src/app/(main)/websites/[id]/realtime/RealtimeHome.tsx diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeLog.js b/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx similarity index 97% rename from src/app/(main)/websites/[id]/realtime/RealtimeLog.js rename to src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx index b388b37b..e33320ec 100644 --- a/src/app/(main)/websites/[id]/realtime/RealtimeLog.js +++ b/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx @@ -1,7 +1,7 @@ import { useMemo, useState } from 'react'; import { StatusLight, Icon, Text } from 'react-basics'; import { FixedSizeList } from 'react-window'; -import firstBy from 'thenby'; +import thenby from 'thenby'; import FilterButtons from 'components/common/FilterButtons'; import Empty from 'components/common/Empty'; import useLocale from 'components/hooks/useLocale'; @@ -130,7 +130,7 @@ export function RealtimeLog({ data, websiteDomain }) { } const { pageviews, visitors, events } = data; - const logs = [...pageviews, ...visitors, ...events].sort(firstBy('createdAt', -1)); + const logs = [...pageviews, ...visitors, ...events].sort(thenby.firstBy('createdAt', -1)); if (filter !== TYPE_ALL) { return logs.filter(({ __type }) => __type === filter); diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeUrls.js b/src/app/(main)/websites/[id]/realtime/RealtimeUrls.tsx similarity index 85% rename from src/app/(main)/websites/[id]/realtime/RealtimeUrls.js rename to src/app/(main)/websites/[id]/realtime/RealtimeUrls.tsx index 674858b2..27a9ec5a 100644 --- a/src/app/(main)/websites/[id]/realtime/RealtimeUrls.js +++ b/src/app/(main)/websites/[id]/realtime/RealtimeUrls.tsx @@ -1,15 +1,22 @@ -import { useMemo, useState } from 'react'; +import { Key, useMemo, useState } from 'react'; import { ButtonGroup, Button, Flexbox } from 'react-basics'; -import firstBy from 'thenby'; +import thenby from 'thenby'; import { percentFilter } from 'lib/filters'; import ListTable from 'components/metrics/ListTable'; import { FILTER_PAGES, FILTER_REFERRERS } from 'lib/constants'; import useMessages from 'components/hooks/useMessages'; +import { RealtimeData } from 'lib/types'; -export function RealtimeUrls({ websiteDomain, data = {} }) { +export function RealtimeUrls({ + websiteDomain, + data, +}: { + websiteDomain: string; + data: RealtimeData; +}) { const { formatMessage, labels } = useMessages(); - const { pageviews } = data; - const [filter, setFilter] = useState(FILTER_REFERRERS); + const { pageviews } = data || {}; + const [filter, setFilter] = useState(FILTER_REFERRERS); const limit = 15; const buttons = [ @@ -48,7 +55,7 @@ export function RealtimeUrls({ websiteDomain, data = {} }) { } return arr; }, []) - .sort(firstBy('y', -1)) + .sort(thenby.firstBy('y', -1)) .slice(0, limit), ); @@ -64,7 +71,7 @@ export function RealtimeUrls({ websiteDomain, data = {} }) { } return arr; }, []) - .sort(firstBy('y', -1)) + .sort(thenby.firstBy('y', -1)) .slice(0, limit), ); diff --git a/src/app/(main)/websites/[id]/reports/WebsiteReports.js b/src/app/(main)/websites/[id]/reports/WebsiteReports.tsx similarity index 100% rename from src/app/(main)/websites/[id]/reports/WebsiteReports.js rename to src/app/(main)/websites/[id]/reports/WebsiteReports.tsx diff --git a/src/components/hooks/useDateRange.ts b/src/components/hooks/useDateRange.ts index 71361b69..d8a49331 100644 --- a/src/components/hooks/useDateRange.ts +++ b/src/components/hooks/useDateRange.ts @@ -1,9 +1,10 @@ import { getMinimumUnit, parseDateRange } from 'lib/date'; import { setItem } from 'next-basics'; import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants'; -import useLocale from './useLocale'; import websiteStore, { setWebsiteDateRange } from 'store/websites'; import appStore, { setDateRange } from 'store/app'; +import { DateRange } from 'lib/types'; +import useLocale from './useLocale'; import useApi from './useApi'; export function useDateRange(websiteId?: string) { @@ -14,9 +15,9 @@ export function useDateRange(websiteId?: string) { const globalConfig = appStore(state => state.dateRange); const dateRange = parseDateRange(websiteConfig || globalConfig || defaultConfig, locale); - const saveDateRange = async value => { + const saveDateRange = async (value: DateRange | string) => { if (websiteId) { - let dateRange = value; + let dateRange: DateRange | string = value; if (typeof value === 'string') { if (value === 'all') { @@ -37,14 +38,17 @@ export function useDateRange(websiteId?: string) { } } - setWebsiteDateRange(websiteId, dateRange); + setWebsiteDateRange(websiteId, dateRange as DateRange); } else { setItem(DATE_RANGE_CONFIG, value); setDateRange(value); } }; - return [dateRange, saveDateRange]; + return [dateRange, saveDateRange] as [ + { startDate: Date; endDate: Date }, + (value: string | DateRange) => void, + ]; } export default useDateRange; diff --git a/src/components/metrics/RealtimeChart.tsx b/src/components/metrics/RealtimeChart.tsx index f1a781bd..1ca0719a 100644 --- a/src/components/metrics/RealtimeChart.tsx +++ b/src/components/metrics/RealtimeChart.tsx @@ -3,6 +3,7 @@ import { format, startOfMinute, subMinutes, isBefore } from 'date-fns'; import PageviewsChart from './PageviewsChart'; import { getDateArray } from 'lib/date'; import { DEFAULT_ANIMATION_DURATION, REALTIME_RANGE } from 'lib/constants'; +import { RealtimeData } from 'lib/types'; function mapData(data: any[]) { let last = 0; @@ -24,11 +25,9 @@ function mapData(data: any[]) { } export interface RealtimeChartProps { - data: { - pageviews: any[]; - visitors: any[]; - }; + data: RealtimeData; unit: string; + className?: string; } export function RealtimeChart({ data, unit, ...props }: RealtimeChartProps) { diff --git a/src/lib/date.ts b/src/lib/date.ts index 51057309..81c37e69 100644 --- a/src/lib/date.ts +++ b/src/lib/date.ts @@ -32,6 +32,7 @@ import { subWeeks, } from 'date-fns'; import { getDateLocale } from 'lib/lang'; +import { DateRange } from 'lib/types'; export const TIME_UNIT = { minute: 'minute', @@ -54,13 +55,13 @@ export function getTimezone() { return moment.tz.guess(); } -export function getLocalTime(t) { +export function getLocalTime(t: string | number | Date) { return addMinutes(new Date(t), new Date().getTimezoneOffset()); } -export function parseDateRange(value, locale = 'en-US') { +export function parseDateRange(value: string | object, locale = 'en-US'): DateRange { if (typeof value === 'object') { - return value; + return value as DateRange; } if (value === 'all') { @@ -93,7 +94,7 @@ export function parseDateRange(value, locale = 'en-US') { if (!match) return null; const { num, unit } = match.groups; - const selectedUnit = { num, unit }; + const selectedUnit = { num: +num, unit }; if (+num === 1) { switch (unit) { @@ -172,7 +173,7 @@ export function parseDateRange(value, locale = 'en-US') { switch (unit) { case 'day': return { - startDate: subDays(startOfDay(now), num - 1), + startDate: subDays(startOfDay(now), +num - 1), endDate: endOfDay(now), unit, value, @@ -180,7 +181,7 @@ export function parseDateRange(value, locale = 'en-US') { }; case 'hour': return { - startDate: subHours(startOfHour(now), num - 1), + startDate: subHours(startOfHour(now), +num - 1), endDate: endOfHour(now), unit, value, @@ -189,7 +190,10 @@ export function parseDateRange(value, locale = 'en-US') { } } -export function incrementDateRange(value, increment) { +export function incrementDateRange( + value: { startDate: any; endDate: any; selectedUnit: any }, + increment: number, +) { const { startDate, endDate, selectedUnit } = value; const { num, unit } = selectedUnit; @@ -235,7 +239,7 @@ export function incrementDateRange(value, increment) { } } -export function getAllowedUnits(startDate, endDate) { +export function getAllowedUnits(startDate: Date, endDate: Date) { const units = ['minute', 'hour', 'day', 'month', 'year']; const minUnit = getMinimumUnit(startDate, endDate); const index = units.indexOf(minUnit === 'year' ? 'month' : minUnit); @@ -243,7 +247,7 @@ export function getAllowedUnits(startDate, endDate) { return index >= 0 ? units.splice(index) : []; } -export function getMinimumUnit(startDate, endDate) { +export function getMinimumUnit(startDate: number | Date, endDate: number | Date) { if (differenceInMinutes(endDate, startDate) <= 60) { return 'minute'; } else if (differenceInHours(endDate, startDate) <= 48) { @@ -257,25 +261,25 @@ export function getMinimumUnit(startDate, endDate) { return 'year'; } -export function getDateFromString(str) { +export function getDateFromString(str: string) { const [ymd, hms] = str.split(' '); const [year, month, day] = ymd.split('-'); if (hms) { const [hour, min, sec] = hms.split(':'); - return new Date(year, month - 1, day, hour, min, sec); + return new Date(+year, +month - 1, +day, +hour, +min, +sec); } - return new Date(year, month - 1, day); + return new Date(+year, +month - 1, +day); } -export function getDateArray(data, startDate, endDate, unit) { +export function getDateArray(data: any[], startDate: Date, endDate: Date, unit: string) { const arr = []; const [diff, add, normalize] = dateFuncs[unit]; const n = diff(endDate, startDate) + 1; - function findData(date) { + function findData(date: Date) { const d = data.find(({ x }) => { return normalize(getDateFromString(x)).getTime() === date.getTime(); }); @@ -293,7 +297,7 @@ export function getDateArray(data, startDate, endDate, unit) { return arr; } -export function getDateLength(startDate, endDate, unit) { +export function getDateLength(startDate: Date, endDate: Date, unit: string | number) { const [diff] = dateFuncs[unit]; return diff(endDate, startDate) + 1; } @@ -310,7 +314,7 @@ export const CUSTOM_FORMATS = { }, }; -export function formatDate(date, str, locale = 'en-US') { +export function formatDate(date: string | number | Date, str: string, locale = 'en-US') { return format( typeof date === 'string' ? new Date(date) : date, CUSTOM_FORMATS?.[locale]?.[str] || str, @@ -320,10 +324,10 @@ export function formatDate(date, str, locale = 'en-US') { ); } -export function maxDate(...args) { +export function maxDate(...args: Date[]) { return max(args.filter(n => isDate(n))); } -export function minDate(...args) { +export function minDate(...args: any[]) { return min(args.filter(n => isDate(n))); } diff --git a/src/lib/types.ts b/src/lib/types.ts index a7fab1e7..900f2e5a 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -180,7 +180,7 @@ export interface DateRange { endDate: Date; value: string; unit?: TimeUnit; - selectedUnit?: TimeUnit; + selectedUnit?: { num: number; unit: TimeUnit }; } export interface QueryFilters { @@ -207,3 +207,12 @@ export interface QueryOptions { joinSession?: boolean; columns?: { [key: string]: string }; } + +export interface RealtimeData { + pageviews: any[]; + sessions: any[]; + events: any[]; + timestamp: number; + countries?: any[]; + visitors?: any[]; +} From c520a329d25a7c8b66cf8c21372cabebca5adb9e Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 9 Dec 2023 01:25:02 -0800 Subject: [PATCH 148/163] Fixed activity log timestamp. --- package.json | 1 + src/app/(main)/websites/[id]/realtime/Realtime.tsx | 2 +- src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx | 6 +++--- yarn.lock | 7 +++++++ 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a158696c..782e039b 100644 --- a/package.json +++ b/package.json @@ -129,6 +129,7 @@ "@types/node": "^20.9.0", "@types/react": "^18.2.41", "@types/react-dom": "^18.2.17", + "@types/react-window": "^1.8.8", "@typescript-eslint/eslint-plugin": "^6.7.3", "@typescript-eslint/parser": "^6.7.3", "cross-env": "^7.0.3", diff --git a/src/app/(main)/websites/[id]/realtime/Realtime.tsx b/src/app/(main)/websites/[id]/realtime/Realtime.tsx index 6de65d7a..285d9845 100644 --- a/src/app/(main)/websites/[id]/realtime/Realtime.tsx +++ b/src/app/(main)/websites/[id]/realtime/Realtime.tsx @@ -95,7 +95,7 @@ export function Realtime({ websiteId }) { return ( <> - + diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx b/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx index e33320ec..7cae0abc 100644 --- a/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx +++ b/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx @@ -8,11 +8,11 @@ import useLocale from 'components/hooks/useLocale'; import useCountryNames from 'components/hooks/useCountryNames'; import { BROWSERS } from 'lib/constants'; import { stringToColor } from 'lib/format'; -import { formatDate } from 'lib/date'; import { safeDecodeURI } from 'next-basics'; import Icons from 'components/icons'; import styles from './RealtimeLog.module.css'; import useMessages from 'components/hooks/useMessages'; +import { format } from 'date-fns'; const TYPE_ALL = 'all'; const TYPE_PAGEVIEW = 'pageview'; @@ -50,7 +50,7 @@ export function RealtimeLog({ data, websiteDomain }) { }, ]; - const getTime = ({ createdAt }) => formatDate(new Date(createdAt), 'pp', locale); + const getTime = ({ timestamp }) => format(timestamp, 'h:mm:ss'); const getColor = ({ id, sessionId }) => stringToColor(sessionId || id); @@ -146,7 +146,7 @@ export function RealtimeLog({ data, websiteDomain }) {
{logs?.length === 0 && } {logs?.length > 0 && ( - + {Row} )} diff --git a/yarn.lock b/yarn.lock index bdb0d484..1ecdaaf3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2431,6 +2431,13 @@ hoist-non-react-statics "^3.3.0" redux "^4.0.0" +"@types/react-window@^1.8.8": + version "1.8.8" + resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.8.tgz#c20645414d142364fbe735818e1c1e0a145696e3" + integrity sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@16 || 17 || 18": version "18.2.30" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.30.tgz#b84f786864fc46f18545364a54d5e1316308e59b" From 7a5f28870f3f87452cc0d0e33ecba933308228f2 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 9 Dec 2023 01:41:07 -0800 Subject: [PATCH 149/163] Fixed realtime chart rendering of initial payload. --- .../websites/[id]/realtime/Realtime.tsx | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/app/(main)/websites/[id]/realtime/Realtime.tsx b/src/app/(main)/websites/[id]/realtime/Realtime.tsx index 285d9845..e77af289 100644 --- a/src/app/(main)/websites/[id]/realtime/Realtime.tsx +++ b/src/app/(main)/websites/[id]/realtime/Realtime.tsx @@ -38,15 +38,19 @@ export function Realtime({ websiteId }) { useEffect(() => { if (data) { - const date = subMinutes(startOfMinute(new Date()), REALTIME_RANGE); - const time = date.getTime(); + if (!currentData) { + setCurrentData(data); + } else { + const date = subMinutes(startOfMinute(new Date()), REALTIME_RANGE); + const time = date.getTime(); - setCurrentData(state => ({ - pageviews: mergeData(state?.pageviews, data.pageviews, time), - sessions: mergeData(state?.sessions, data.sessions, time), - events: mergeData(state?.events, data.events, time), - timestamp: data.timestamp, - })); + setCurrentData(state => ({ + pageviews: mergeData(state?.pageviews, data.pageviews, time), + sessions: mergeData(state?.sessions, data.sessions, time), + events: mergeData(state?.events, data.events, time), + timestamp: data.timestamp, + })); + } } }, [data]); From 92a513e4d06c861968d337e120eafac837242c6e Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 9 Dec 2023 20:55:50 -0800 Subject: [PATCH 150/163] Fixed realtime chart rendering. --- .../websites/[id]/realtime/Realtime.tsx | 27 +++++++--------- src/pages/api/realtime/[id].ts | 6 ++-- src/queries/analytics/events/getEvents.ts | 4 +++ src/queries/analytics/getRealtimeData.ts | 32 ++++++++++++++----- src/queries/analytics/sessions/getSessions.ts | 6 +++- 5 files changed, 49 insertions(+), 26 deletions(-) diff --git a/src/app/(main)/websites/[id]/realtime/Realtime.tsx b/src/app/(main)/websites/[id]/realtime/Realtime.tsx index e77af289..34e29281 100644 --- a/src/app/(main)/websites/[id]/realtime/Realtime.tsx +++ b/src/app/(main)/websites/[id]/realtime/Realtime.tsx @@ -19,9 +19,9 @@ import { RealtimeData } from 'lib/types'; import styles from './Realtime.module.css'; function mergeData(state = [], data = [], time: number) { - const ids = state.map(({ __id }) => __id); + const ids = state.map(({ id }) => id); return state - .concat(data.filter(({ __id }) => !ids.includes(__id))) + .concat(data.filter(({ id }) => !ids.includes(id))) .filter(({ timestamp }) => timestamp >= time); } @@ -38,21 +38,18 @@ export function Realtime({ websiteId }) { useEffect(() => { if (data) { - if (!currentData) { - setCurrentData(data); - } else { - const date = subMinutes(startOfMinute(new Date()), REALTIME_RANGE); - const time = date.getTime(); + const date = subMinutes(startOfMinute(new Date()), REALTIME_RANGE); + const time = date.getTime(); + const { pageviews, sessions, events, timestamp } = data; - setCurrentData(state => ({ - pageviews: mergeData(state?.pageviews, data.pageviews, time), - sessions: mergeData(state?.sessions, data.sessions, time), - events: mergeData(state?.events, data.events, time), - timestamp: data.timestamp, - })); - } + setCurrentData(state => ({ + pageviews: mergeData(state?.pageviews, pageviews, time), + sessions: mergeData(state?.sessions, sessions, time), + events: mergeData(state?.events, events, time), + timestamp, + })); } - }, [data]); + }, [data, currentData]); const realtimeData: RealtimeData = useMemo(() => { if (!currentData) { diff --git a/src/pages/api/realtime/[id].ts b/src/pages/api/realtime/[id].ts index 212d4a0f..0edd589d 100644 --- a/src/pages/api/realtime/[id].ts +++ b/src/pages/api/realtime/[id].ts @@ -1,4 +1,4 @@ -import { subMinutes } from 'date-fns'; +import { startOfMinute, subMinutes } from 'date-fns'; import { canViewWebsite } from 'lib/auth'; import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, RealtimeInit } from 'lib/types'; @@ -6,6 +6,8 @@ import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getRealtimeData } from 'queries'; import * as yup from 'yup'; +import { REALTIME_RANGE } from 'lib/constants'; + export interface RealtimeRequestQuery { id: string; startAt: number; @@ -32,7 +34,7 @@ export default async ( return unauthorized(res); } - let startTime = subMinutes(new Date(), 30); + let startTime = subMinutes(startOfMinute(new Date()), REALTIME_RANGE); if (+startAt > startTime.getTime()) { startTime = new Date(+startAt); diff --git a/src/queries/analytics/events/getEvents.ts b/src/queries/analytics/events/getEvents.ts index fe074ec2..9ef27973 100644 --- a/src/queries/analytics/events/getEvents.ts +++ b/src/queries/analytics/events/getEvents.ts @@ -18,6 +18,9 @@ function relationalQuery(websiteId: string, startDate: Date, eventType: number) gte: startDate, }, }, + orderBy: { + createdAt: 'asc', + }, }); } @@ -39,6 +42,7 @@ function clickhouseQuery(websiteId: string, startDate: Date, eventType: number) where website_id = {websiteId:UUID} and created_at >= {startDate:DateTime64} and event_type = {eventType:UInt32} + order by created_at asc `, { websiteId, diff --git a/src/queries/analytics/getRealtimeData.ts b/src/queries/analytics/getRealtimeData.ts index 337e7475..868a5c70 100644 --- a/src/queries/analytics/getRealtimeData.ts +++ b/src/queries/analytics/getRealtimeData.ts @@ -1,4 +1,3 @@ -import { md5 } from 'next-basics'; import { getSessions, getEvents } from 'queries/index'; import { EVENT_TYPE } from 'lib/constants'; @@ -9,18 +8,35 @@ export async function getRealtimeData(websiteId: string, startDate: Date) { getEvents(websiteId, startDate, EVENT_TYPE.customEvent), ]); - const decorate = (id: string, data: any[]) => { - return data.map((props: { [key: string]: any }) => ({ - ...props, - __id: md5(id, ...Object.values(props)), - __type: id, - timestamp: props.timestamp ? props.timestamp * 1000 : new Date(props.createdAt).getTime(), + const decorate = (type: string, data: any[]) => { + return data.map((values: { [key: string]: any }) => ({ + ...values, + __type: type, + timestamp: values.timestamp ? values.timestamp * 1000 : new Date(values.createdAt).getTime(), })); }; + const set = new Set(); + const uniques = (type: string, data: any[]) => { + return data.reduce((arr, values: { [key: string]: any }) => { + if (!set.has(values.id)) { + set.add(values.id); + + return arr.concat({ + ...values, + __type: type, + timestamp: values.timestamp + ? values.timestamp * 1000 + : new Date(values.createdAt).getTime(), + }); + } + return arr; + }, []); + }; + return { pageviews: decorate('pageview', pageviews), - sessions: decorate('session', sessions), + sessions: uniques('session', sessions), events: decorate('event', events), timestamp: Date.now(), }; diff --git a/src/queries/analytics/sessions/getSessions.ts b/src/queries/analytics/sessions/getSessions.ts index d67edd5e..b92e3af9 100644 --- a/src/queries/analytics/sessions/getSessions.ts +++ b/src/queries/analytics/sessions/getSessions.ts @@ -17,6 +17,9 @@ async function relationalQuery(websiteId: string, startDate: Date) { gte: startDate, }, }, + orderBy: { + createdAt: 'asc', + }, }); } @@ -25,7 +28,7 @@ async function clickhouseQuery(websiteId: string, startDate: Date) { return rawQuery( ` - select distinct + select session_id as id, website_id as websiteId, created_at as createdAt, @@ -43,6 +46,7 @@ async function clickhouseQuery(websiteId: string, startDate: Date) { from website_event where website_id = {websiteId:UUID} and created_at >= {startDate:DateTime64} + order by created_at asc `, { websiteId, From 44e243ad12b181bcaf05d644feb10e53f8b92acd Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 9 Dec 2023 21:30:57 -0800 Subject: [PATCH 151/163] Moved SettingsProvider to root Providers component. --- src/app/(main)/settings/layout.tsx | 28 ++++++------------- .../websites/[id]/realtime/Realtime.tsx | 2 +- src/app/Providers.tsx | 26 +++++++++++++---- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/app/(main)/settings/layout.tsx b/src/app/(main)/settings/layout.tsx index 1c30d2db..e36b5b53 100644 --- a/src/app/(main)/settings/layout.tsx +++ b/src/app/(main)/settings/layout.tsx @@ -4,7 +4,6 @@ import useUser from 'components/hooks/useUser'; import useMessages from 'components/hooks/useMessages'; import SideNav from 'components/layout/SideNav'; import styles from './layout.module.css'; -import SettingsContext from './SettingsContext'; export default function SettingsLayout({ children }) { const { user } = useUser(); @@ -25,25 +24,14 @@ export default function SettingsLayout({ children }) { return null; } - const hostUrl = process.env.hostUrl || location.origin; - - const config = { - settingsUrl: '/settings/websites', - shareUrl: hostUrl, - trackingCodeUrl: hostUrl, - websitesUrl: `/websites`, - }; - return ( - -
- {!cloudMode && ( -
- -
- )} -
{children}
-
-
+
+ {!cloudMode && ( +
+ +
+ )} +
{children}
+
); } diff --git a/src/app/(main)/websites/[id]/realtime/Realtime.tsx b/src/app/(main)/websites/[id]/realtime/Realtime.tsx index 34e29281..7f17190d 100644 --- a/src/app/(main)/websites/[id]/realtime/Realtime.tsx +++ b/src/app/(main)/websites/[id]/realtime/Realtime.tsx @@ -64,7 +64,7 @@ export function Realtime({ websiteId }) { } return arr; }, []) - .reduce((arr, { country }) => { + .reduce((arr: { x: any; y: number }[], { country }: any) => { if (country) { const row = arr.find(({ x }) => x === country); diff --git a/src/app/Providers.tsx b/src/app/Providers.tsx index c3d62699..0abebf86 100644 --- a/src/app/Providers.tsx +++ b/src/app/Providers.tsx @@ -3,6 +3,7 @@ import { IntlProvider } from 'react-intl'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactBasicsProvider } from 'react-basics'; import ErrorBoundary from 'components/common/ErrorBoundary'; +import SettingsContext from 'app/(main)/settings/SettingsContext'; import useLocale from 'components/hooks/useLocale'; import 'chartjs-adapter-date-fns'; @@ -24,14 +25,29 @@ function MessagesProvider({ children }) { ); } +function SettingsProvider({ children }) { + const hostUrl = process.env.hostUrl || location.origin; + + const config = { + settingsUrl: '/settings/websites', + shareUrl: hostUrl, + trackingCodeUrl: hostUrl, + websitesUrl: `/websites`, + }; + + return {children}; +} + export function Providers({ children }) { return ( - - - {children} - - + + + + {children} + + + ); } From 08bd9e835720901f6d3c8867adfa492d4eb5c128 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 9 Dec 2023 22:18:47 -0800 Subject: [PATCH 152/163] Upgrade to Next 14. --- package.json | 2 +- .../websites/[id]/realtime/Realtime.tsx | 10 +- src/app/Providers.tsx | 19 +-- yarn.lock | 111 +++++++++--------- 4 files changed, 74 insertions(+), 68 deletions(-) diff --git a/package.json b/package.json index 782e039b..db10755c 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "kafkajs": "^2.1.0", "maxmind": "^4.3.6", "moment-timezone": "^0.5.35", - "next": "13.5.6", + "next": "14.0.4", "next-basics": "^0.39.0", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", diff --git a/src/app/(main)/websites/[id]/realtime/Realtime.tsx b/src/app/(main)/websites/[id]/realtime/Realtime.tsx index 7f17190d..ee710d73 100644 --- a/src/app/(main)/websites/[id]/realtime/Realtime.tsx +++ b/src/app/(main)/websites/[id]/realtime/Realtime.tsx @@ -6,16 +6,16 @@ import { Grid, GridRow } from 'components/layout/Grid'; import Page from 'components/layout/Page'; import RealtimeChart from 'components/metrics/RealtimeChart'; import WorldMap from 'components/metrics/WorldMap'; +import useApi from 'components/hooks/useApi'; +import { useWebsite } from 'components/hooks'; +import { percentFilter } from 'lib/filters'; +import { REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants'; +import { RealtimeData } from 'lib/types'; import RealtimeLog from './RealtimeLog'; import RealtimeHeader from './RealtimeHeader'; import RealtimeUrls from './RealtimeUrls'; import RealtimeCountries from './RealtimeCountries'; import WebsiteHeader from '../WebsiteHeader'; -import useApi from 'components/hooks/useApi'; -import { percentFilter } from 'lib/filters'; -import { REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants'; -import { useWebsite } from 'components/hooks'; -import { RealtimeData } from 'lib/types'; import styles from './Realtime.module.css'; function mergeData(state = [], data = [], time: number) { diff --git a/src/app/Providers.tsx b/src/app/Providers.tsx index 0abebf86..c2ddd2ff 100644 --- a/src/app/Providers.tsx +++ b/src/app/Providers.tsx @@ -1,4 +1,5 @@ 'use client'; +import { useEffect, useState } from 'react'; import { IntlProvider } from 'react-intl'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactBasicsProvider } from 'react-basics'; @@ -26,14 +27,18 @@ function MessagesProvider({ children }) { } function SettingsProvider({ children }) { - const hostUrl = process.env.hostUrl || location.origin; + const [config, setConfig] = useState({}); - const config = { - settingsUrl: '/settings/websites', - shareUrl: hostUrl, - trackingCodeUrl: hostUrl, - websitesUrl: `/websites`, - }; + useEffect(() => { + const hostUrl = process.env.hostUrl || window?.location.origin; + + setConfig({ + settingsUrl: '/settings/websites', + shareUrl: hostUrl, + trackingCodeUrl: hostUrl, + websitesUrl: `/websites`, + }); + }, []); return {children}; } diff --git a/yarn.lock b/yarn.lock index 1ecdaaf3..b24830a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1771,10 +1771,10 @@ "@netlify/node-cookies" "^0.1.0" urlpattern-polyfill "8.0.2" -"@next/env@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/env/-/env-13.5.6.tgz#c1148e2e1aa166614f05161ee8f77ded467062bc" - integrity sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw== +"@next/env@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.4.tgz#d5cda0c4a862d70ae760e58c0cd96a8899a2e49a" + integrity sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ== "@next/eslint-plugin-next@12.3.4": version "12.3.4" @@ -1783,50 +1783,50 @@ dependencies: glob "7.1.7" -"@next/swc-darwin-arm64@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.6.tgz#b15d139d8971360fca29be3bdd703c108c9a45fb" - integrity sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA== +"@next/swc-darwin-arm64@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz#27b1854c2cd04eb1d5e75081a1a792ad91526618" + integrity sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg== -"@next/swc-darwin-x64@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.6.tgz#9c72ee31cc356cb65ce6860b658d807ff39f1578" - integrity sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA== +"@next/swc-darwin-x64@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz#9940c449e757d0ee50bb9e792d2600cc08a3eb3b" + integrity sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw== -"@next/swc-linux-arm64-gnu@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.6.tgz#59f5f66155e85380ffa26ee3d95b687a770cfeab" - integrity sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg== +"@next/swc-linux-arm64-gnu@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz#0eafd27c8587f68ace7b4fa80695711a8434de21" + integrity sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w== -"@next/swc-linux-arm64-musl@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.6.tgz#f012518228017052736a87d69bae73e587c76ce2" - integrity sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q== +"@next/swc-linux-arm64-musl@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz#2b0072adb213f36dada5394ea67d6e82069ae7dd" + integrity sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ== -"@next/swc-linux-x64-gnu@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.6.tgz#339b867a7e9e7ee727a700b496b269033d820df4" - integrity sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw== +"@next/swc-linux-x64-gnu@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz#68c67d20ebc8e3f6ced6ff23a4ba2a679dbcec32" + integrity sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A== -"@next/swc-linux-x64-musl@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.6.tgz#ae0ae84d058df758675830bcf70ca1846f1028f2" - integrity sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ== +"@next/swc-linux-x64-musl@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz#67cd81b42fb2caf313f7992fcf6d978af55a1247" + integrity sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw== -"@next/swc-win32-arm64-msvc@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.6.tgz#a5cc0c16920485a929a17495064671374fdbc661" - integrity sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg== +"@next/swc-win32-arm64-msvc@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz#be06585906b195d755ceda28f33c633e1443f1a3" + integrity sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w== -"@next/swc-win32-ia32-msvc@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.6.tgz#6a2409b84a2cbf34bf92fe714896455efb4191e4" - integrity sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg== +"@next/swc-win32-ia32-msvc@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz#e76cabefa9f2d891599c3d85928475bd8d3f6600" + integrity sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg== -"@next/swc-win32-x64-msvc@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.6.tgz#4a3e2a206251abc729339ba85f60bc0433c2865d" - integrity sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ== +"@next/swc-win32-x64-msvc@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz#e74892f1a9ccf41d3bf5979ad6d3d77c07b9cba1" + integrity sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -4949,7 +4949,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -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.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.2, graceful-fs@^4.2.4: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -6330,28 +6330,29 @@ next-basics@^0.39.0: jsonwebtoken "^9.0.0" pure-rand "^6.0.2" -next@13.5.6: - version "13.5.6" - resolved "https://registry.yarnpkg.com/next/-/next-13.5.6.tgz#e964b5853272236c37ce0dd2c68302973cf010b1" - integrity sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw== +next@14.0.4: + version "14.0.4" + resolved "https://registry.yarnpkg.com/next/-/next-14.0.4.tgz#bf00b6f835b20d10a5057838fa2dfced1d0d84dc" + integrity sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA== dependencies: - "@next/env" "13.5.6" + "@next/env" "14.0.4" "@swc/helpers" "0.5.2" busboy "1.6.0" caniuse-lite "^1.0.30001406" + graceful-fs "^4.2.11" postcss "8.4.31" styled-jsx "5.1.1" watchpack "2.4.0" optionalDependencies: - "@next/swc-darwin-arm64" "13.5.6" - "@next/swc-darwin-x64" "13.5.6" - "@next/swc-linux-arm64-gnu" "13.5.6" - "@next/swc-linux-arm64-musl" "13.5.6" - "@next/swc-linux-x64-gnu" "13.5.6" - "@next/swc-linux-x64-musl" "13.5.6" - "@next/swc-win32-arm64-msvc" "13.5.6" - "@next/swc-win32-ia32-msvc" "13.5.6" - "@next/swc-win32-x64-msvc" "13.5.6" + "@next/swc-darwin-arm64" "14.0.4" + "@next/swc-darwin-x64" "14.0.4" + "@next/swc-linux-arm64-gnu" "14.0.4" + "@next/swc-linux-arm64-musl" "14.0.4" + "@next/swc-linux-x64-gnu" "14.0.4" + "@next/swc-linux-x64-musl" "14.0.4" + "@next/swc-win32-arm64-msvc" "14.0.4" + "@next/swc-win32-ia32-msvc" "14.0.4" + "@next/swc-win32-x64-msvc" "14.0.4" nice-try@^1.0.4: version "1.0.5" From 3a28fea8aca027a8c54dfdc943a20d2810229392 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 9 Dec 2023 22:45:55 -0800 Subject: [PATCH 153/163] Fixed broken links behavior. --- src/app/(main)/websites/[id]/realtime/Realtime.tsx | 2 +- src/app/share/[...id]/Header.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/(main)/websites/[id]/realtime/Realtime.tsx b/src/app/(main)/websites/[id]/realtime/Realtime.tsx index ee710d73..bd9f74bc 100644 --- a/src/app/(main)/websites/[id]/realtime/Realtime.tsx +++ b/src/app/(main)/websites/[id]/realtime/Realtime.tsx @@ -49,7 +49,7 @@ export function Realtime({ websiteId }) { timestamp, })); } - }, [data, currentData]); + }, [data]); const realtimeData: RealtimeData = useMemo(() => { if (!currentData) { diff --git a/src/app/share/[...id]/Header.tsx b/src/app/share/[...id]/Header.tsx index 41e93f52..2b82908d 100644 --- a/src/app/share/[...id]/Header.tsx +++ b/src/app/share/[...id]/Header.tsx @@ -19,8 +19,8 @@ export function Header() {
- - + +
From cad719fd235f400d78fe0c4459864498b411f669 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 10 Dec 2023 02:02:24 -0800 Subject: [PATCH 154/163] Added search to metrics table. --- .../(main)/websites/[id]/WebsiteDetails.tsx | 4 +- ...ule.css => WebsiteExpandedView.module.css} | 0 ...teMenuView.tsx => WebsiteExpandedView.tsx} | 11 ++--- src/components/hooks/useDateRange.ts | 2 +- src/components/hooks/useFormat.ts | 11 ++++- src/components/layout/SideNav.tsx | 2 +- src/components/metrics/CitiesTable.tsx | 4 +- .../metrics/MetricsTable.module.css | 20 +++++++++ src/components/metrics/MetricsTable.tsx | 38 +++++++++++++--- src/components/metrics/OSTable.tsx | 9 ++-- src/components/metrics/PagesTable.tsx | 25 +++++------ .../metrics/QueryParametersTable.tsx | 45 +++++++++---------- 12 files changed, 111 insertions(+), 60 deletions(-) rename src/app/(main)/websites/[id]/{WebsiteMenuView.module.css => WebsiteExpandedView.module.css} (100%) rename src/app/(main)/websites/[id]/{WebsiteMenuView.tsx => WebsiteExpandedView.tsx} (93%) diff --git a/src/app/(main)/websites/[id]/WebsiteDetails.tsx b/src/app/(main)/websites/[id]/WebsiteDetails.tsx index 4d3a18e7..7d8d2d99 100644 --- a/src/app/(main)/websites/[id]/WebsiteDetails.tsx +++ b/src/app/(main)/websites/[id]/WebsiteDetails.tsx @@ -6,7 +6,7 @@ import FilterTags from 'components/metrics/FilterTags'; import useNavigation from 'components/hooks/useNavigation'; import { useWebsite } from 'components/hooks'; import WebsiteChart from './WebsiteChart'; -import WebsiteMenuView from './WebsiteMenuView'; +import WebsiteExpandedView from './WebsiteExpandedView'; import WebsiteHeader from './WebsiteHeader'; import WebsiteMetricsBar from './WebsiteMetricsBar'; import WebsiteTableView from './WebsiteTableView'; @@ -34,7 +34,7 @@ export default function WebsiteDetails({ websiteId }: { websiteId: string }) { {website && ( <> {!view && } - {view && } + {view && } )} diff --git a/src/app/(main)/websites/[id]/WebsiteMenuView.module.css b/src/app/(main)/websites/[id]/WebsiteExpandedView.module.css similarity index 100% rename from src/app/(main)/websites/[id]/WebsiteMenuView.module.css rename to src/app/(main)/websites/[id]/WebsiteExpandedView.module.css diff --git a/src/app/(main)/websites/[id]/WebsiteMenuView.tsx b/src/app/(main)/websites/[id]/WebsiteExpandedView.tsx similarity index 93% rename from src/app/(main)/websites/[id]/WebsiteMenuView.tsx rename to src/app/(main)/websites/[id]/WebsiteExpandedView.tsx index 670ea469..e97cd002 100644 --- a/src/app/(main)/websites/[id]/WebsiteMenuView.tsx +++ b/src/app/(main)/websites/[id]/WebsiteExpandedView.tsx @@ -15,7 +15,7 @@ import SideNav from 'components/layout/SideNav'; import useNavigation from 'components/hooks/useNavigation'; import useMessages from 'components/hooks/useMessages'; import LinkButton from 'components/common/LinkButton'; -import styles from './WebsiteMenuView.module.css'; +import styles from './WebsiteExpandedView.module.css'; const views = { url: PagesTable, @@ -33,7 +33,7 @@ const views = { query: QueryParametersTable, }; -export default function WebsiteMenuView({ +export default function WebsiteExpandedView({ websiteId, websiteDomain, }: { @@ -113,11 +113,11 @@ export default function WebsiteMenuView({ const DetailsComponent = views[view] || (() => null); - const handleChange = view => { + const handleChange = (view: any) => { router.push(makeUrl({ view })); }; - const renderValue = value => items.find(({ key }) => key === value)?.label; + const renderValue = (value: string) => items.find(({ key }) => key === value)?.label; return (
@@ -146,9 +146,10 @@ export default function WebsiteMenuView({ websiteDomain={websiteDomain} limit={false} animate={false} - showFilters={true} virtualize={true} itemCount={25} + allowFilter={true} + allowSearch={true} />
diff --git a/src/components/hooks/useDateRange.ts b/src/components/hooks/useDateRange.ts index d8a49331..efaa717f 100644 --- a/src/components/hooks/useDateRange.ts +++ b/src/components/hooks/useDateRange.ts @@ -46,7 +46,7 @@ export function useDateRange(websiteId?: string) { }; return [dateRange, saveDateRange] as [ - { startDate: Date; endDate: Date }, + { startDate: Date; endDate: Date; modified?: number }, (value: string | DateRange) => void, ]; } diff --git a/src/components/hooks/useFormat.ts b/src/components/hooks/useFormat.ts index f804eb72..06585e49 100644 --- a/src/components/hooks/useFormat.ts +++ b/src/components/hooks/useFormat.ts @@ -18,14 +18,19 @@ export function useFormat() { }; const formatRegion = (value: string): string => { - return regions[value] ? regions[value] : value; + const [country] = value.split('-'); + return regions[value] ? `${regions[value]}, ${countryNames[country]}` : value; + }; + + const formatCity = (value: string, country?: string): string => { + return `${value}, ${countryNames[country]}`; }; const formatDevice = (value: string): string => { return formatMessage(labels[value] || labels.unknown); }; - const formatValue = (value: string, type: string): string => { + const formatValue = (value: string, type: string, data?: { [key: string]: any }): string => { switch (type) { case 'browser': return formatBrowser(value); @@ -33,6 +38,8 @@ export function useFormat() { return formatCountry(value); case 'region': return formatRegion(value); + case 'city': + return formatCity(value, data?.country); case 'device': return formatDevice(value); default: diff --git a/src/components/layout/SideNav.tsx b/src/components/layout/SideNav.tsx index f38bdba0..0b5c9856 100644 --- a/src/components/layout/SideNav.tsx +++ b/src/components/layout/SideNav.tsx @@ -9,7 +9,7 @@ export interface SideNavProps { items: any[]; shallow?: boolean; scroll?: boolean; - className?: boolean; + className?: string; onSelect?: () => void; } diff --git a/src/components/metrics/CitiesTable.tsx b/src/components/metrics/CitiesTable.tsx index 69b89962..067e07e9 100644 --- a/src/components/metrics/CitiesTable.tsx +++ b/src/components/metrics/CitiesTable.tsx @@ -11,8 +11,8 @@ export function CitiesTable(props: MetricsTableProps) { const countryNames = useCountryNames(locale); const renderLabel = (city: string, country: string) => { - const name = countryNames[country]; - return name ? `${city}, ${name}` : city; + const countryName = countryNames[country]; + return countryName ? `${city}, ${countryName}` : city; }; const renderLink = ({ x: city, country }) => { diff --git a/src/components/metrics/MetricsTable.module.css b/src/components/metrics/MetricsTable.module.css index c00e4356..f04d9ae4 100644 --- a/src/components/metrics/MetricsTable.module.css +++ b/src/components/metrics/MetricsTable.module.css @@ -6,13 +6,33 @@ flex: 1; } +.actions { + display: flex; + gap: 20px; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; +} + .footer { display: flex; justify-content: center; } +.search { + max-width: 300px; +} + @media only screen and (max-width: 992px) { .container { min-height: auto; } + + .actions { + flex-direction: column; + } + + .search { + max-width: 100%; + } } diff --git a/src/components/metrics/MetricsTable.tsx b/src/components/metrics/MetricsTable.tsx index d4ad793d..48beac68 100644 --- a/src/components/metrics/MetricsTable.tsx +++ b/src/components/metrics/MetricsTable.tsx @@ -1,5 +1,5 @@ -import { useMemo } from 'react'; -import { Loading, Icon, Text } from 'react-basics'; +import { ReactNode, useMemo, useState } from 'react'; +import { Loading, Icon, Text, SearchField } from 'react-basics'; import classNames from 'classnames'; import useApi from 'components/hooks/useApi'; import { percentFilter } from 'lib/filters'; @@ -12,6 +12,7 @@ import { DEFAULT_ANIMATION_DURATION } from 'lib/constants'; import Icons from 'components/icons'; import useMessages from 'components/hooks/useMessages'; import useLocale from 'components/hooks/useLocale'; +import useFormat from 'components//hooks/useFormat'; import styles from './MetricsTable.module.css'; export interface MetricsTableProps extends ListTableProps { @@ -22,6 +23,9 @@ export interface MetricsTableProps extends ListTableProps { limit?: number; delay?: number; onDataLoad?: (data: any) => void; + onSearch?: (search: string) => void; + allowSearch?: boolean; + children?: ReactNode; } export function MetricsTable({ @@ -32,8 +36,12 @@ export function MetricsTable({ limit, onDataLoad, delay = null, + allowSearch = false, + children, ...props }: MetricsTableProps) { + const [search, setSearch] = useState(''); + const { formatValue } = useFormat(); const [{ startDate, endDate, modified }] = useDateRange(websiteId); const { makeUrl, @@ -42,7 +50,6 @@ export function MetricsTable({ const { formatMessage, labels } = useMessages(); const { get, useQuery } = useApi(); const { dir } = useLocale(); - const { data, isLoading, isFetched, error } = useQuery({ queryKey: [ 'websites:metrics', @@ -94,24 +101,43 @@ export function MetricsTable({ } } + if (search) { + items = items.filter(({ x, ...data }) => { + const value = formatValue(x, type, data); + + return value.toLowerCase().includes(search.toLowerCase()); + }); + } + items = percentFilter(items); if (limit) { - items = items.filter((e, i) => i < limit); + items = items.slice(0, limit - 1); } return items; } return []; - }, [data, error, dataFilter, limit]); + }, [data, dataFilter, search, limit, formatValue, type]); return (
- {!data && isLoading && !isFetched && } {error && } +
+ {allowSearch && ( + + )} + {children} +
{data && !error && ( )} + {!data && isLoading && !isFetched && }
{data && !error && limit && ( diff --git a/src/components/metrics/OSTable.tsx b/src/components/metrics/OSTable.tsx index c39cba22..102bafd3 100644 --- a/src/components/metrics/OSTable.tsx +++ b/src/components/metrics/OSTable.tsx @@ -1,4 +1,4 @@ -import MetricsTable from './MetricsTable'; +import MetricsTable, { MetricsTableProps } from './MetricsTable'; import FilterLink from 'components/common/FilterLink'; import useMessages from 'components/hooks/useMessages'; @@ -8,7 +8,7 @@ const names = { 'Sun OS': 'SunOS', }; -export function OSTable({ websiteId, limit }: { websiteId: string; limit?: number }) { +export function OSTable(props: MetricsTableProps) { const { formatMessage, labels } = useMessages(); function renderLink({ x: os }) { @@ -28,12 +28,11 @@ export function OSTable({ websiteId, limit }: { websiteId: string; limit?: numbe return ( ); } diff --git a/src/components/metrics/PagesTable.tsx b/src/components/metrics/PagesTable.tsx index 23676467..11379a2e 100644 --- a/src/components/metrics/PagesTable.tsx +++ b/src/components/metrics/PagesTable.tsx @@ -6,10 +6,10 @@ import useNavigation from 'components/hooks/useNavigation'; import { emptyFilter } from 'lib/filters'; export interface PagesTableProps extends MetricsTableProps { - showFilters?: boolean; + allowFilter?: boolean; } -export function PagesTable({ showFilters, ...props }: PagesTableProps) { +export function PagesTable({ allowFilter, ...props }: PagesTableProps) { const { router, makeUrl, @@ -37,17 +37,16 @@ export function PagesTable({ showFilters, ...props }: PagesTableProps) { }; return ( - <> - {showFilters && } - - + + {allowFilter && } + ); } diff --git a/src/components/metrics/QueryParametersTable.tsx b/src/components/metrics/QueryParametersTable.tsx index 65cac664..90489460 100644 --- a/src/components/metrics/QueryParametersTable.tsx +++ b/src/components/metrics/QueryParametersTable.tsx @@ -13,9 +13,9 @@ const filters = { }; export function QueryParametersTable({ - showFilters, + allowFilter, ...props -}: { showFilters: boolean } & MetricsTableProps) { +}: { allowFilter: boolean } & MetricsTableProps) { const [filter, setFilter] = useState(FILTER_COMBINED); const { formatMessage, labels } = useMessages(); @@ -28,27 +28,26 @@ export function QueryParametersTable({ ]; return ( - <> - {showFilters && } - - filter === FILTER_RAW ? ( - x - ) : ( -
-
{safeDecodeURI(p)}
-
{safeDecodeURI(v)}
-
- ) - } - delay={0} - /> - + + filter === FILTER_RAW ? ( + x + ) : ( +
+
{safeDecodeURI(p)}
+
{safeDecodeURI(v)}
+
+ ) + } + delay={0} + > + {allowFilter && } +
); } From 765874731d51337c7bdac449e11176e6bc8af2e0 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 10 Dec 2023 20:12:13 -0800 Subject: [PATCH 155/163] Added search to real-time activity log. --- .../[id]/realtime/RealtimeLog.module.css | 22 +++++++++ .../websites/[id]/realtime/RealtimeLog.tsx | 49 +++++++++++++++---- 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeLog.module.css b/src/app/(main)/websites/[id]/realtime/RealtimeLog.module.css index f400cc1b..e9c0fc1b 100644 --- a/src/app/(main)/websites/[id]/realtime/RealtimeLog.module.css +++ b/src/app/(main)/websites/[id]/realtime/RealtimeLog.module.css @@ -66,3 +66,25 @@ .row .link:hover { color: var(--primary400); } + +.search { + max-width: 300px; +} + +.actions { + display: flex; + gap: 20px; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; +} + +@media only screen and (max-width: 992px) { + .actions { + flex-direction: column; + } + + .search { + max-width: 100%; + } +} diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx b/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx index 7cae0abc..5293c1f0 100644 --- a/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx +++ b/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx @@ -1,18 +1,19 @@ import { useMemo, useState } from 'react'; -import { StatusLight, Icon, Text } from 'react-basics'; +import { StatusLight, Icon, Text, SearchField } from 'react-basics'; import { FixedSizeList } from 'react-window'; +import { format } from 'date-fns'; import thenby from 'thenby'; +import { safeDecodeURI } from 'next-basics'; import FilterButtons from 'components/common/FilterButtons'; import Empty from 'components/common/Empty'; import useLocale from 'components/hooks/useLocale'; import useCountryNames from 'components/hooks/useCountryNames'; +import Icons from 'components/icons'; +import useMessages from 'components/hooks/useMessages'; +import useFormat from 'components//hooks/useFormat'; import { BROWSERS } from 'lib/constants'; import { stringToColor } from 'lib/format'; -import { safeDecodeURI } from 'next-basics'; -import Icons from 'components/icons'; import styles from './RealtimeLog.module.css'; -import useMessages from 'components/hooks/useMessages'; -import { format } from 'date-fns'; const TYPE_ALL = 'all'; const TYPE_PAGEVIEW = 'pageview'; @@ -26,7 +27,9 @@ const icons = { }; export function RealtimeLog({ data, websiteDomain }) { + const [search, setSearch] = useState(''); const { formatMessage, labels, messages, FormattedMessage } = useMessages(); + const { formatValue } = useFormat(); const { locale } = useLocale(); const countryNames = useCountryNames(locale); const [filter, setFilter] = useState(TYPE_ALL); @@ -56,7 +59,15 @@ export function RealtimeLog({ data, websiteDomain }) { const getIcon = ({ __type }) => icons[__type]; - const getDetail = log => { + const getDetail = (log: { + __type: any; + eventName: any; + urlPath: any; + browser: any; + os: any; + country: any; + device: any; + }) => { const { __type, eventName, urlPath: url, browser, os, country, device } = log; if (__type === TYPE_EVENT) { @@ -130,18 +141,38 @@ export function RealtimeLog({ data, websiteDomain }) { } const { pageviews, visitors, events } = data; - const logs = [...pageviews, ...visitors, ...events].sort(thenby.firstBy('createdAt', -1)); + let logs = [...pageviews, ...visitors, ...events].sort(thenby.firstBy('createdAt', -1)); + + if (search) { + logs = logs.filter(({ eventName, urlPath, browser, os, country, device }) => { + return [ + eventName, + urlPath, + os, + formatValue(browser, 'browser'), + formatValue(country, 'country'), + formatValue(device, 'device'), + ] + .filter(n => n) + .map(n => n.toLowerCase()) + .join('') + .includes(search.toLowerCase()); + }); + } if (filter !== TYPE_ALL) { return logs.filter(({ __type }) => __type === filter); } return logs; - }, [data, filter]); + }, [data, filter, formatValue, search]); return (
- +
+ + +
{formatMessage(labels.activityLog)}
{logs?.length === 0 && } From a851ebf1247ac65e6246a20b72bafd07f31c61d2 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 11 Dec 2023 00:15:26 -0800 Subject: [PATCH 156/163] Bump version 2.9.0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index db10755c..0f437c35 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "2.8.0", + "version": "2.9.0", "description": "A simple, fast, privacy-focused alternative to Google Analytics.", "author": "Mike Cao ", "license": "MIT", From 907685b96e0caae51846fc6dfe55d9ddfe16d7cd Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 12 Dec 2023 19:00:44 -0800 Subject: [PATCH 157/163] Added limit to metrics queries. --- .../(main)/websites/[id]/WebsiteExpandedView.tsx | 1 - src/components/metrics/MetricsTable.tsx | 1 + src/lib/prisma.ts | 6 +++++- src/lib/types.ts | 1 + src/pages/api/websites/[id]/metrics.ts | 7 +++++-- .../analytics/pageviews/getPageviewMetrics.ts | 14 ++++++++++---- .../analytics/sessions/getSessionMetrics.ts | 14 ++++++++++---- 7 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/app/(main)/websites/[id]/WebsiteExpandedView.tsx b/src/app/(main)/websites/[id]/WebsiteExpandedView.tsx index e97cd002..9fb1b3f6 100644 --- a/src/app/(main)/websites/[id]/WebsiteExpandedView.tsx +++ b/src/app/(main)/websites/[id]/WebsiteExpandedView.tsx @@ -144,7 +144,6 @@ export default function WebsiteExpandedView({ relationalQuery(...args), @@ -13,7 +13,12 @@ export async function getPageviewMetrics( }); } -async function relationalQuery(websiteId: string, column: string, filters: QueryFilters) { +async function relationalQuery( + websiteId: string, + column: string, + filters: QueryFilters, + limit: number = 100, +) { const { rawQuery, parseFilters } = prisma; const { filterQuery, joinSession, params } = await parseFilters( websiteId, @@ -42,7 +47,7 @@ async function relationalQuery(websiteId: string, column: string, filters: Query ${filterQuery} group by 1 order by 2 desc - limit 100 + limit ${limit} `, params, ); @@ -52,6 +57,7 @@ async function clickhouseQuery( websiteId: string, column: string, filters: QueryFilters, + limit: number = 100, ): Promise<{ x: string; y: number }[]> { const { rawQuery, parseFilters } = clickhouse; const { filterQuery, params } = await parseFilters(websiteId, { @@ -75,7 +81,7 @@ async function clickhouseQuery( ${filterQuery} group by x order by y desc - limit 100 + limit ${limit} `, params, ).then(a => { diff --git a/src/queries/analytics/sessions/getSessionMetrics.ts b/src/queries/analytics/sessions/getSessionMetrics.ts index 3573ac1e..c6877a3f 100644 --- a/src/queries/analytics/sessions/getSessionMetrics.ts +++ b/src/queries/analytics/sessions/getSessionMetrics.ts @@ -5,7 +5,7 @@ import { EVENT_TYPE, SESSION_COLUMNS } from 'lib/constants'; import { QueryFilters } from 'lib/types'; export async function getSessionMetrics( - ...args: [websiteId: string, column: string, filters: QueryFilters] + ...args: [websiteId: string, column: string, filters: QueryFilters, limit?: number] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -13,7 +13,12 @@ export async function getSessionMetrics( }); } -async function relationalQuery(websiteId: string, column: string, filters: QueryFilters) { +async function relationalQuery( + websiteId: string, + column: string, + filters: QueryFilters, + limit: number = 100, +) { const { parseFilters, rawQuery } = prisma; const { filterQuery, joinSession, params } = await parseFilters( websiteId, @@ -42,7 +47,7 @@ async function relationalQuery(websiteId: string, column: string, filters: Query group by 1 ${includeCountry ? ', 3' : ''} order by 2 desc - limit 100`, + limit ${limit}`, params, ); } @@ -51,6 +56,7 @@ async function clickhouseQuery( websiteId: string, column: string, filters: QueryFilters, + limit: number = 100, ): Promise<{ x: string; y: number }[]> { const { parseFilters, rawQuery } = clickhouse; const { filterQuery, params } = await parseFilters(websiteId, { @@ -73,7 +79,7 @@ async function clickhouseQuery( group by x ${includeCountry ? ', country' : ''} order by y desc - limit 100 + limit ${limit} `, params, ).then(a => { From 9735769413abab1a966249621459d5e9f8b2e5fe Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 12 Dec 2023 19:20:34 -0800 Subject: [PATCH 158/163] Removed Node 16 from GH workflow. --- .github/workflows/ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 775f9ecf..66e16a03 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,10 +16,6 @@ jobs: strategy: matrix: include: - - node-version: 16.x - db-type: postgresql - - node-version: 16.x - db-type: mysql - node-version: 18.x db-type: postgresql - node-version: 18.x From e1c65cdf2ac6db01497a2bab922d8497a94ab457 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 12 Dec 2023 20:05:45 -0800 Subject: [PATCH 159/163] Updated loading for reports. --- src/app/(main)/reports/[id]/Report.tsx | 5 +++-- src/app/(main)/reports/[id]/ReportBody.tsx | 8 ++++++++ src/app/(main)/reports/[id]/ReportMenu.tsx | 8 ++++++++ src/components/hooks/useReport.ts | 10 +++++----- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/app/(main)/reports/[id]/Report.tsx b/src/app/(main)/reports/[id]/Report.tsx index b100ad8e..c1cc502f 100644 --- a/src/app/(main)/reports/[id]/Report.tsx +++ b/src/app/(main)/reports/[id]/Report.tsx @@ -1,5 +1,6 @@ 'use client'; import { createContext, ReactNode } from 'react'; +import { Loading } from 'react-basics'; import { useReport } from 'components/hooks'; import styles from './Report.module.css'; import classNames from 'classnames'; @@ -17,11 +18,11 @@ export function Report({ reportId, defaultParameters, children, className }: Rep const report = useReport(reportId, defaultParameters); if (!report) { - return null; + return reportId ? : null; } return ( - +
{children}
); diff --git a/src/app/(main)/reports/[id]/ReportBody.tsx b/src/app/(main)/reports/[id]/ReportBody.tsx index a116bf8e..6f4627f6 100644 --- a/src/app/(main)/reports/[id]/ReportBody.tsx +++ b/src/app/(main)/reports/[id]/ReportBody.tsx @@ -1,6 +1,14 @@ import styles from './ReportBody.module.css'; +import { useContext } from 'react'; +import { ReportContext } from './Report'; export function ReportBody({ children }) { + const { report } = useContext(ReportContext); + + if (!report) { + return null; + } + return
{children}
; } diff --git a/src/app/(main)/reports/[id]/ReportMenu.tsx b/src/app/(main)/reports/[id]/ReportMenu.tsx index 72bc197a..9478a903 100644 --- a/src/app/(main)/reports/[id]/ReportMenu.tsx +++ b/src/app/(main)/reports/[id]/ReportMenu.tsx @@ -1,6 +1,14 @@ import styles from './ReportMenu.module.css'; +import { useContext } from 'react'; +import { ReportContext } from './Report'; export function ReportMenu({ children }) { + const { report } = useContext(ReportContext); + + if (!report) { + return null; + } + return
{children}
; } diff --git a/src/components/hooks/useReport.ts b/src/components/hooks/useReport.ts index 1686e222..7769ed6c 100644 --- a/src/components/hooks/useReport.ts +++ b/src/components/hooks/useReport.ts @@ -4,7 +4,7 @@ import { useTimezone } from './useTimezone'; import useApi from './useApi'; import useMessages from './useMessages'; -export function useReport(reportId, defaultParameters) { +export function useReport(reportId: string, defaultParameters: { [key: string]: any }) { const [report, setReport] = useState(null); const [isRunning, setIsRunning] = useState(false); const { get, post } = useApi(); @@ -17,7 +17,7 @@ export function useReport(reportId, defaultParameters) { parameters: {}, }; - const loadReport = async id => { + const loadReport = async (id: string) => { const data: any = await get(`/reports/${id}`); const { dateRange } = data?.parameters || {}; @@ -32,7 +32,7 @@ export function useReport(reportId, defaultParameters) { }; const runReport = useCallback( - async parameters => { + async (parameters: { [key: string]: any }) => { setIsRunning(true); const { type } = report; @@ -50,11 +50,11 @@ export function useReport(reportId, defaultParameters) { setIsRunning(false); }, - [report], + [report, timezone], ); const updateReport = useCallback( - async data => { + async (data: { [x: string]: any; parameters: any }) => { setReport( produce((state: any) => { const { parameters, ...rest } = data; From 442ad61779c80224e124a580b68df12715e32100 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 12 Dec 2023 21:23:12 -0800 Subject: [PATCH 160/163] Created admin API endpoints. --- .../(main)/settings/users/UsersDataTable.tsx | 2 +- .../settings/websites/WebsiteSettings.tsx | 6 +- .../settings/websites/WebsitesTable.tsx | 6 +- src/app/Providers.tsx | 5 +- src/pages/api/admin/users.ts | 53 +++++++++++++++ src/pages/api/admin/websites.ts | 66 +++++++++++++++++++ src/pages/api/users/index.ts | 16 +---- src/pages/api/websites/index.ts | 28 +------- 8 files changed, 133 insertions(+), 49 deletions(-) create mode 100644 src/pages/api/admin/users.ts create mode 100644 src/pages/api/admin/websites.ts diff --git a/src/app/(main)/settings/users/UsersDataTable.tsx b/src/app/(main)/settings/users/UsersDataTable.tsx index b7716451..2495d023 100644 --- a/src/app/(main)/settings/users/UsersDataTable.tsx +++ b/src/app/(main)/settings/users/UsersDataTable.tsx @@ -11,7 +11,7 @@ export function UsersDataTable() { const modified = useCache((state: any) => state?.users); const queryResult = useFilterQuery({ queryKey: ['users', { modified }], - queryFn: (params: { [key: string]: any }) => get(`/users`, params), + queryFn: (params: { [key: string]: any }) => get(`/admin/users`, params), }); return ( diff --git a/src/app/(main)/settings/websites/WebsiteSettings.tsx b/src/app/(main)/settings/websites/WebsiteSettings.tsx index 4607b423..0c5ce614 100644 --- a/src/app/(main)/settings/websites/WebsiteSettings.tsx +++ b/src/app/(main)/settings/websites/WebsiteSettings.tsx @@ -17,7 +17,7 @@ export function WebsiteSettings({ websiteId, openExternal = false }) { const { formatMessage, labels, messages } = useMessages(); const { get, useQuery } = useApi(); const { showToast } = useToasts(); - const { websitesUrl, settingsUrl } = useContext(SettingsContext); + const { websitesUrl, websitesPath, settingsPath } = useContext(SettingsContext); const { data, isLoading } = useQuery({ queryKey: ['website', websiteId], queryFn: () => get(`${websitesUrl}/${websiteId}`), @@ -38,7 +38,7 @@ export function WebsiteSettings({ websiteId, openExternal = false }) { const handleReset = async (value: string) => { if (value === 'delete') { - router.push(settingsUrl); + router.push(settingsPath); } else if (value === 'reset') { showSuccess(); } @@ -57,7 +57,7 @@ export function WebsiteSettings({ websiteId, openExternal = false }) { return ( <> - +