From d6a27b8e99db9bc4566bfe4f1af58cb519f23c2e Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 25 Aug 2023 11:54:44 -0700 Subject: [PATCH 01/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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 9a52cdd2e11a296022bde2ff1317945b4c3c181b Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 29 Sep 2023 05:29:22 -0700 Subject: [PATCH 09/13] 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/') &&