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 }, }, ] : [],