From d6a27b8e99db9bc4566bfe4f1af58cb519f23c2e Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 25 Aug 2023 11:54:44 -0700 Subject: [PATCH 01/53] 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 02/53] 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 ce2a83a09fb10cf28d4c408b5b08ae8b0e4afc25 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Mon, 25 Sep 2023 13:19:56 -0700 Subject: [PATCH 03/53] 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 04/53] 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 05/53] 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 06/53] 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 07/53] 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 08/53] 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 09/53] 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 10/53] 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/') &&