diff --git a/.eslintrc.json b/.eslintrc.json
index 7a824ff6..25e83d5a 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -50,7 +50,8 @@
"@next/next/no-img-element": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
- "@typescript-eslint/no-var-requires": "off"
+ "@typescript-eslint/no-var-requires": "off",
+ "@typescript-eslint/no-empty-interface": "off"
},
"globals": {
"React": "writable"
diff --git a/components/common/Pager.js b/components/common/Pager.js
new file mode 100644
index 00000000..584e0669
--- /dev/null
+++ b/components/common/Pager.js
@@ -0,0 +1,37 @@
+import styles from './Pager.module.css';
+import { Button, Flexbox, Icon, Icons } from 'react-basics';
+
+export function Pager({ page, pageSize, count, onPageChange, onPageSizeChange }) {
+ const maxPage = Math.ceil(count / pageSize);
+ const lastPage = page === maxPage;
+ const firstPage = page === 1;
+
+ if (count === 0) {
+ return null;
+ }
+
+ const handlePageChange = value => {
+ const nextPage = page + value;
+ if (nextPage > 0 && nextPage <= maxPage) {
+ onPageChange(nextPage);
+ }
+ };
+
+ return (
+
+
+ {`Page ${page} of ${maxPage}`}
+
+
+ );
+}
+
+export default Pager;
diff --git a/components/common/Pager.module.css b/components/common/Pager.module.css
new file mode 100644
index 00000000..b4ee9f0e
--- /dev/null
+++ b/components/common/Pager.module.css
@@ -0,0 +1,7 @@
+.container {
+ margin-top: 20px;
+}
+
+.text {
+ margin: 0 10px;
+}
diff --git a/components/common/SettingsTable.js b/components/common/SettingsTable.js
index 8f039858..9fb4c2a9 100644
--- a/components/common/SettingsTable.js
+++ b/components/common/SettingsTable.js
@@ -1,37 +1,98 @@
-import { Table, TableHeader, TableBody, TableRow, TableCell, TableColumn } from 'react-basics';
+import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
+import useMessages from 'hooks/useMessages';
+import { useState } from 'react';
+import {
+ SearchField,
+ Table,
+ TableBody,
+ TableCell,
+ TableColumn,
+ TableHeader,
+ TableRow,
+} from 'react-basics';
import styles from './SettingsTable.module.css';
+import Pager from 'components/common/Pager';
+
+export function SettingsTable({
+ columns = [],
+ data,
+ children,
+ cellRender,
+ showSearch,
+ showPaging,
+ onFilterChange,
+ onPageChange,
+ onPageSizeChange,
+ filterValue,
+}) {
+ const { formatMessage, messages } = useMessages();
+ const [filter, setFilter] = useState(filterValue);
+ const { data: value, page, count, pageSize } = data;
+
+ const handleFilterChange = value => {
+ setFilter(value);
+ onFilterChange(value);
+ };
-export function SettingsTable({ columns = [], data = [], children, cellRender }) {
return (
-
-
- {(column, index) => {
- return (
-
- {column.label}
-
- );
- }}
-
-
- {(row, keys, rowIndex) => {
- row.action = children(row, keys, rowIndex);
+ <>
+ {showSearch && (
+
+ )}
+ {value.length === 0 && filterValue && (
+
+ )}
+ {value.length > 0 && (
+
+
+ {(column, index) => {
+ return (
+
+ {column.label}
+
+ );
+ }}
+
+
+ {(row, keys, rowIndex) => {
+ row.action = children(row, keys, rowIndex);
- return (
-
- {(data, key, colIndex) => {
- return (
-
-
- {cellRender ? cellRender(row, data, key, colIndex) : data[key]}
-
- );
- }}
-
- );
- }}
-
-
+ return (
+
+ {(data, key, colIndex) => {
+ return (
+
+
+ {cellRender ? cellRender(row, data, key, colIndex) : data[key]}
+
+ );
+ }}
+
+ );
+ }}
+
+ {showPaging && (
+
+ )}
+
+ )}
+ >
);
}
diff --git a/components/input/WebsiteSelect.js b/components/input/WebsiteSelect.js
index b77ae57c..ae3ceb46 100644
--- a/components/input/WebsiteSelect.js
+++ b/components/input/WebsiteSelect.js
@@ -8,12 +8,12 @@ export function WebsiteSelect({ websiteId, onSelect }) {
const { data } = useQuery(['websites:me'], () => get('/me/websites'));
const renderValue = value => {
- return data?.find(({ id }) => id === value)?.name;
+ return data?.data?.find(({ id }) => id === value)?.name;
};
return (
- get('/websites', { userId, includeTeams: 1 }),
+ get('/websites', { includeTeams: 1 }),
);
- const hasData = data && data.length !== 0;
+ const hasData = data && data?.data.length !== 0;
+
const { dir } = useLocale();
function handleMore() {
@@ -47,8 +48,10 @@ export function Dashboard({ userId }) {
)}
{hasData && (
<>
- {editing && }
- {!editing && }
+ {editing && }
+ {!editing && (
+
+ )}
{max < data.length && (
-
+
);
}
diff --git a/hooks/useApiFilter.ts b/hooks/useApiFilter.ts
new file mode 100644
index 00000000..d411fd43
--- /dev/null
+++ b/hooks/useApiFilter.ts
@@ -0,0 +1,28 @@
+import { useState } from 'react';
+
+export function useApiFilter() {
+ const [filter, setFilter] = useState();
+ const [filterType, setFilterType] = useState('All');
+ const [page, setPage] = useState(1);
+ const [pageSize, setPageSize] = useState(10);
+
+ const handleFilterChange = value => setFilter(value);
+ const handlePageChange = value => setPage(value);
+ const handlePageSizeChange = value => setPageSize(value);
+
+ return {
+ filter,
+ setFilter,
+ filterType,
+ setFilterType,
+ page,
+ setPage,
+ pageSize,
+ setPageSize,
+ handleFilterChange,
+ handlePageChange,
+ handlePageSizeChange,
+ };
+}
+
+export default useApiFilter;
diff --git a/hooks/useReports.js b/hooks/useReports.js
index f4369eec..57d76492 100644
--- a/hooks/useReports.js
+++ b/hooks/useReports.js
@@ -1,12 +1,16 @@
import { useState } from 'react';
import useApi from './useApi';
+import useApiFilter from 'hooks/useApiFilter';
export function useReports(websiteId) {
const [modified, setModified] = useState(Date.now());
const { get, useQuery, del, useMutation } = useApi();
const { mutate } = useMutation(reportId => del(`/reports/${reportId}`));
- const { data, error, isLoading } = useQuery(['reports:website', { websiteId, modified }], () =>
- get(`/reports`, { websiteId }),
+ const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
+ useApiFilter();
+ const { data, error, isLoading } = useQuery(
+ ['reports:website', { websiteId, modified, filter, page, pageSize }],
+ () => get(`/reports`, { websiteId, filter, page, pageSize }),
);
const deleteReport = id => {
@@ -17,7 +21,18 @@ export function useReports(websiteId) {
});
};
- return { reports: data, error, isLoading, deleteReport };
+ return {
+ reports: data,
+ error,
+ isLoading,
+ deleteReport,
+ filter,
+ page,
+ pageSize,
+ handleFilterChange,
+ handlePageChange,
+ handlePageSizeChange,
+ };
}
export default useReports;
diff --git a/lib/constants.ts b/lib/constants.ts
index 887f90a9..9257298c 100644
--- a/lib/constants.ts
+++ b/lib/constants.ts
@@ -30,6 +30,22 @@ 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/lib/prisma.ts b/lib/prisma.ts
index 753f1ae4..c67ce4bc 100644
--- a/lib/prisma.ts
+++ b/lib/prisma.ts
@@ -4,7 +4,7 @@ import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db';
import { FILTER_COLUMNS, SESSION_COLUMNS } from './constants';
import { loadWebsite } from './load';
import { maxDate } from './date';
-import { QueryFilters, QueryOptions } from './types';
+import { QueryFilters, QueryOptions, SearchFilter } from './types';
const MYSQL_DATE_FORMATS = {
minute: '%Y-%m-%d %H:%i:00',
@@ -128,6 +128,37 @@ async function rawQuery(sql: string, data: object): Promise {
return prisma.rawQuery(query, params);
}
+function getPageFilters(filters: SearchFilter): [
+ {
+ orderBy: {
+ [x: string]: string;
+ }[];
+ take: number;
+ skip: number;
+ },
+ {
+ pageSize: number;
+ page: number;
+ orderBy: string;
+ },
+] {
+ const { pageSize = 10, page = 1, orderBy } = filters;
+
+ return [
+ {
+ ...(pageSize > 0 && { take: pageSize, skip: pageSize * (page - 1) }),
+ ...(orderBy && {
+ orderBy: [
+ {
+ [orderBy]: 'asc',
+ },
+ ],
+ }),
+ },
+ { pageSize, page: +page, orderBy },
+ ];
+}
+
export default {
...prisma,
getAddMinutesQuery,
@@ -135,5 +166,6 @@ export default {
getTimestampIntervalQuery,
getFilterQuery,
parseFilters,
+ getPageFilters,
rawQuery,
};
diff --git a/lib/types.ts b/lib/types.ts
index dc54fd47..5a25169a 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -1,17 +1,62 @@
import { NextApiRequest } from 'next';
-import { COLLECTION_TYPE, DATA_TYPE, EVENT_TYPE, KAFKA_TOPIC, ROLES } from './constants';
+import {
+ COLLECTION_TYPE,
+ DATA_TYPE,
+ EVENT_TYPE,
+ KAFKA_TOPIC,
+ REPORT_FILTER_TYPES,
+ ROLES,
+ TEAM_FILTER_TYPES,
+ USER_FILTER_TYPES,
+ WEBSITE_FILTER_TYPES,
+} from './constants';
type ObjectValues = T[keyof T];
export type CollectionType = ObjectValues;
-
export type Role = ObjectValues;
-
export type EventType = ObjectValues;
-
export type DynamicDataType = ObjectValues;
-
export type KafkaTopic = ObjectValues;
+export type ReportSearchFilterType = ObjectValues;
+export type UserSearchFilterType = ObjectValues;
+export type WebsiteSearchFilterType = ObjectValues;
+export type TeamSearchFilterType = ObjectValues;
+
+export interface WebsiteSearchFilter extends SearchFilter {
+ userId?: string;
+ teamId?: string;
+ includeTeams?: boolean;
+}
+
+export interface UserSearchFilter extends SearchFilter {
+ teamId?: string;
+}
+
+export interface TeamSearchFilter extends SearchFilter {
+ userId?: string;
+}
+
+export interface ReportSearchFilter extends SearchFilter {
+ userId?: string;
+ websiteId?: string;
+}
+
+export interface SearchFilter {
+ filter?: string;
+ filterType?: T;
+ pageSize?: number;
+ page?: number;
+ orderBy?: string;
+}
+
+export interface FilterResult {
+ data: T;
+ count: number;
+ pageSize: number;
+ page: number;
+ orderBy?: string;
+}
export interface DynamicData {
[key: string]: number | string | DynamicData | number[] | string[] | DynamicData[];
diff --git a/package.json b/package.json
index 647cdf41..89dc5e97 100644
--- a/package.json
+++ b/package.json
@@ -94,7 +94,7 @@
"node-fetch": "^3.2.8",
"npm-run-all": "^4.1.5",
"react": "^18.2.0",
- "react-basics": "^0.91.0",
+ "react-basics": "^0.92.0",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.4",
diff --git a/pages/api/reports/index.ts b/pages/api/reports/index.ts
index c856b565..8c6825f1 100644
--- a/pages/api/reports/index.ts
+++ b/pages/api/reports/index.ts
@@ -1,10 +1,12 @@
-import { useAuth, useCors } from 'lib/middleware';
-import { NextApiRequestQueryBody } from 'lib/types';
-import { NextApiResponse } from 'next';
-import { methodNotAllowed, ok, unauthorized } from 'next-basics';
-import { createReport, getWebsiteReports } from 'queries';
import { canViewWebsite } from 'lib/auth';
import { uuid } from 'lib/crypto';
+import { useAuth, useCors } from 'lib/middleware';
+import { NextApiRequestQueryBody, ReportSearchFilterType, SearchFilter } from 'lib/types';
+import { NextApiResponse } from 'next';
+import { methodNotAllowed, ok, unauthorized } from 'next-basics';
+import { createReport, getReportsByWebsiteId } from 'queries';
+
+export interface ReportsRequestQuery extends SearchFilter {}
export interface ReportRequestBody {
websiteId: string;
@@ -35,7 +37,13 @@ export default async (
return unauthorized(res);
}
- const data = await getWebsiteReports(websiteId);
+ const { page, filter, pageSize } = req.query;
+
+ const data = await getReportsByWebsiteId(websiteId, {
+ page,
+ filter,
+ pageSize: +pageSize || null,
+ });
return ok(res, data);
}
diff --git a/pages/api/teams/[id]/users/index.ts b/pages/api/teams/[id]/users/index.ts
index c73da683..6f8b077e 100644
--- a/pages/api/teams/[id]/users/index.ts
+++ b/pages/api/teams/[id]/users/index.ts
@@ -1,11 +1,11 @@
import { canUpdateTeam, canViewTeam } from 'lib/auth';
import { useAuth } from 'lib/middleware';
-import { NextApiRequestQueryBody } from 'lib/types';
+import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types';
import { NextApiResponse } from 'next';
import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics';
-import { createTeamUser, getTeamUsers, getUserByUsername } from 'queries';
+import { createTeamUser, getUserByUsername, getUsersByTeamId } from 'queries';
-export interface TeamUserRequestQuery {
+export interface TeamUserRequestQuery extends SearchFilter {
id: string;
}
@@ -27,7 +27,13 @@ export default async (
return unauthorized(res);
}
- const users = await getTeamUsers(teamId);
+ const { page, filter, pageSize } = req.query;
+
+ const users = await getUsersByTeamId(teamId, {
+ page,
+ filter,
+ pageSize: +pageSize || null,
+ });
return ok(res, users);
}
diff --git a/pages/api/teams/[id]/websites/index.ts b/pages/api/teams/[id]/websites/index.ts
index 63be478b..dcd08939 100644
--- a/pages/api/teams/[id]/websites/index.ts
+++ b/pages/api/teams/[id]/websites/index.ts
@@ -1,11 +1,12 @@
import { canViewTeam } from 'lib/auth';
import { useAuth } from 'lib/middleware';
-import { NextApiRequestQueryBody } from 'lib/types';
+import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
-import { createTeamWebsites, getTeamWebsites } from 'queries/admin/teamWebsite';
+import { getWebsites, getWebsitesByTeamId } from 'queries';
+import { createTeamWebsites } from 'queries/admin/teamWebsite';
-export interface TeamWebsiteRequestQuery {
+export interface TeamWebsiteRequestQuery extends SearchFilter {
id: string;
}
@@ -26,7 +27,13 @@ export default async (
return unauthorized(res);
}
- const websites = await getTeamWebsites(teamId);
+ const { page, filter, pageSize } = req.query;
+
+ const websites = await getWebsitesByTeamId(teamId, {
+ page,
+ filter,
+ pageSize: +pageSize || null,
+ });
return ok(res, websites);
}
diff --git a/pages/api/teams/index.ts b/pages/api/teams/index.ts
index 453f1ef3..997ed885 100644
--- a/pages/api/teams/index.ts
+++ b/pages/api/teams/index.ts
@@ -1,18 +1,19 @@
import { Team } from '@prisma/client';
-import { NextApiRequestQueryBody } from 'lib/types';
import { canCreateTeam } from 'lib/auth';
import { uuid } from 'lib/crypto';
import { useAuth } from 'lib/middleware';
+import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types';
import { NextApiResponse } from 'next';
import { getRandomChars, methodNotAllowed, ok, unauthorized } from 'next-basics';
-import { createTeam, getUserTeams } from 'queries';
+import { createTeam, getTeamsByUserId } from 'queries';
-export interface TeamsRequestBody {
+export interface TeamsRequestQuery extends SearchFilter {}
+export interface TeamsRequestBody extends SearchFilter {
name: string;
}
export default async (
- req: NextApiRequestQueryBody,
+ req: NextApiRequestQueryBody,
res: NextApiResponse,
) => {
await useAuth(req, res);
@@ -22,9 +23,11 @@ export default async (
} = req.auth;
if (req.method === 'GET') {
- const teams = await getUserTeams(userId);
+ const { page, filter, pageSize } = req.query;
- return ok(res, teams);
+ const results = await getTeamsByUserId(userId, { page, filter, pageSize: +pageSize || null });
+
+ return ok(res, results);
}
if (req.method === 'POST') {
diff --git a/pages/api/users/[id]/websites.ts b/pages/api/users/[id]/websites.ts
index e94094a4..e1761291 100644
--- a/pages/api/users/[id]/websites.ts
+++ b/pages/api/users/[id]/websites.ts
@@ -1,9 +1,12 @@
import { useAuth, useCors } from 'lib/middleware';
-import { NextApiRequestQueryBody } from 'lib/types';
+import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
-import { getUserWebsites } from 'queries';
+import { getWebsitesByUserId } from 'queries';
+export interface UserWebsitesRequestQuery extends SearchFilter {
+ id: string;
+}
export interface UserWebsitesRequestBody {
name: string;
domain: string;
@@ -17,16 +20,19 @@ export default async (
await useCors(req, res);
await useAuth(req, res);
const { user } = req.auth;
- const { id: userId } = req.query;
+ const { id: userId, page, filter, pageSize, includeTeams } = req.query;
if (req.method === 'GET') {
if (!user.isAdmin && user.id !== userId) {
return unauthorized(res);
}
- const { includeTeams } = req.query;
-
- const websites = await getUserWebsites(userId, { includeTeams });
+ const websites = await getWebsitesByUserId(userId, {
+ page,
+ filter,
+ pageSize: +pageSize || null,
+ includeTeams,
+ });
return ok(res, websites);
}
diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts
index 6f6c205f..5e913c02 100644
--- a/pages/api/users/index.ts
+++ b/pages/api/users/index.ts
@@ -2,11 +2,12 @@ import { canCreateUser, canViewUsers } from 'lib/auth';
import { ROLES } from 'lib/constants';
import { uuid } from 'lib/crypto';
import { useAuth } from 'lib/middleware';
-import { NextApiRequestQueryBody, Role, User } from 'lib/types';
+import { NextApiRequestQueryBody, Role, SearchFilter, User, UserSearchFilterType } from 'lib/types';
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 UsersRequestBody {
username: string;
password: string;
@@ -15,7 +16,7 @@ export interface UsersRequestBody {
}
export default async (
- req: NextApiRequestQueryBody,
+ req: NextApiRequestQueryBody,
res: NextApiResponse,
) => {
await useAuth(req, res);
@@ -25,7 +26,9 @@ export default async (
return unauthorized(res);
}
- const users = await getUsers();
+ const { page, filter, pageSize } = req.query;
+
+ const users = await getUsers({ page, filter, pageSize: +pageSize || null });
return ok(res, users);
}
diff --git a/pages/api/websites/index.ts b/pages/api/websites/index.ts
index c8b5aba2..f94fa037 100644
--- a/pages/api/websites/index.ts
+++ b/pages/api/websites/index.ts
@@ -1,12 +1,14 @@
import { canCreateWebsite } from 'lib/auth';
import { uuid } from 'lib/crypto';
import { useAuth, useCors } from 'lib/middleware';
-import { NextApiRequestQueryBody } from 'lib/types';
+import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createWebsite } from 'queries';
import userWebsites from 'pages/api/users/[id]/websites';
+export interface WebsitesRequestQuery extends SearchFilter {}
+
export interface WebsitesRequestBody {
name: string;
domain: string;
@@ -14,7 +16,7 @@ export interface WebsitesRequestBody {
}
export default async (
- req: NextApiRequestQueryBody,
+ req: NextApiRequestQueryBody,
res: NextApiResponse,
) => {
await useCors(req, res);
@@ -26,6 +28,7 @@ export default async (
if (req.method === 'GET') {
req.query.id = userId;
+ req.query.pageSize = 100;
return userWebsites(req, res);
}
diff --git a/queries/admin/report.ts b/queries/admin/report.ts
index ee7a0592..d2523f82 100644
--- a/queries/admin/report.ts
+++ b/queries/admin/report.ts
@@ -1,5 +1,7 @@
import { Prisma, Report } from '@prisma/client';
+import { REPORT_FILTER_TYPES } from 'lib/constants';
import prisma from 'lib/prisma';
+import { FilterResult, ReportSearchFilter, ReportSearchFilterType, SearchFilter } from 'lib/types';
export async function createReport(data: Prisma.ReportUncheckedCreateInput): Promise {
return prisma.client.report.create({ data });
@@ -13,22 +15,6 @@ export async function getReportById(reportId: string): Promise {
});
}
-export async function getUserReports(userId: string): Promise {
- return prisma.client.report.findMany({
- where: {
- userId,
- },
- });
-}
-
-export async function getWebsiteReports(websiteId: string): Promise {
- return prisma.client.report.findMany({
- where: {
- websiteId,
- },
- });
-}
-
export async function updateReport(
reportId: string,
data: Prisma.ReportUpdateInput,
@@ -39,3 +25,103 @@ export async function updateReport(
export async function deleteReport(reportId: string): Promise {
return prisma.client.report.delete({ where: { id: reportId } });
}
+
+export async function getReports(
+ ReportSearchFilter: ReportSearchFilter,
+): Promise> {
+ const { userId, websiteId, filter, filterType = REPORT_FILTER_TYPES.all } = ReportSearchFilter;
+ const where: Prisma.ReportWhereInput = {
+ ...(userId && { userId: userId }),
+ ...(websiteId && { websiteId: websiteId }),
+ ...(filter && {
+ AND: {
+ OR: [
+ {
+ ...((filterType === REPORT_FILTER_TYPES.all ||
+ filterType === REPORT_FILTER_TYPES.name) && {
+ name: {
+ startsWith: filter,
+ },
+ }),
+ },
+ {
+ ...((filterType === REPORT_FILTER_TYPES.all ||
+ filterType === REPORT_FILTER_TYPES.description) && {
+ description: {
+ startsWith: filter,
+ },
+ }),
+ },
+ {
+ ...((filterType === REPORT_FILTER_TYPES.all ||
+ filterType === REPORT_FILTER_TYPES.type) && {
+ type: {
+ startsWith: filter,
+ },
+ }),
+ },
+ {
+ ...((filterType === REPORT_FILTER_TYPES.all ||
+ filterType === REPORT_FILTER_TYPES['user:username']) && {
+ user: {
+ username: {
+ startsWith: filter,
+ },
+ },
+ }),
+ },
+ {
+ ...((filterType === REPORT_FILTER_TYPES.all ||
+ filterType === REPORT_FILTER_TYPES['website:name']) && {
+ website: {
+ name: {
+ startsWith: filter,
+ },
+ },
+ }),
+ },
+ {
+ ...((filterType === REPORT_FILTER_TYPES.all ||
+ filterType === REPORT_FILTER_TYPES['website:domain']) && {
+ website: {
+ domain: {
+ startsWith: filter,
+ },
+ },
+ }),
+ },
+ ],
+ },
+ }),
+ };
+
+ const [pageFilters, getParameters] = prisma.getPageFilters(ReportSearchFilter);
+
+ const reports = await prisma.client.report.findMany({
+ where,
+ ...pageFilters,
+ });
+ const count = await prisma.client.report.count({
+ where,
+ });
+
+ return {
+ data: reports,
+ count,
+ ...getParameters,
+ };
+}
+
+export async function getReportsByUserId(
+ userId: string,
+ filter: SearchFilter,
+): Promise> {
+ return getReports({ userId, ...filter });
+}
+
+export async function getReportsByWebsiteId(
+ websiteId: string,
+ filter: SearchFilter,
+): Promise> {
+ return getReports({ websiteId, ...filter });
+}
diff --git a/queries/admin/team.ts b/queries/admin/team.ts
index a8b3385c..97838227 100644
--- a/queries/admin/team.ts
+++ b/queries/admin/team.ts
@@ -1,7 +1,8 @@
import { Prisma, Team } from '@prisma/client';
import prisma from 'lib/prisma';
-import { ROLES } from 'lib/constants';
+import { ROLES, TEAM_FILTER_TYPES } from 'lib/constants';
import { uuid } from 'lib/crypto';
+import { FilterResult, TeamSearchFilter, TeamSearchFilterType, SearchFilter } from 'lib/types';
export interface GetTeamOptions {
includeTeamUser?: boolean;
@@ -26,12 +27,6 @@ export function getTeamByAccessCode(accessCode: string, options: GetTeamOptions
return getTeam({ accessCode }, options);
}
-export async function getTeams(where: Prisma.TeamWhereInput): Promise {
- return prisma.client.team.findMany({
- where,
- });
-}
-
export async function createTeam(data: Prisma.TeamCreateInput, userId: string): Promise {
const { id } = data;
@@ -85,3 +80,82 @@ export async function deleteTeam(
}),
]);
}
+
+export async function getTeams(
+ TeamSearchFilter: TeamSearchFilter,
+ options?: { include?: Prisma.TeamInclude },
+): Promise> {
+ const { userId, filter, filterType = TEAM_FILTER_TYPES.all } = TeamSearchFilter;
+ const where: Prisma.TeamWhereInput = {
+ ...(userId && {
+ teamUser: {
+ some: { userId },
+ },
+ }),
+ ...(filter && {
+ AND: {
+ OR: [
+ {
+ ...((filterType === TEAM_FILTER_TYPES.all || filterType === TEAM_FILTER_TYPES.name) && {
+ name: { startsWith: filter },
+ }),
+ },
+ {
+ ...((filterType === TEAM_FILTER_TYPES.all ||
+ filterType === TEAM_FILTER_TYPES['user:username']) && {
+ teamUser: {
+ every: {
+ role: ROLES.teamOwner,
+ user: {
+ username: {
+ startsWith: filter,
+ },
+ },
+ },
+ },
+ }),
+ },
+ ],
+ },
+ }),
+ };
+
+ const [pageFilters, getParameters] = prisma.getPageFilters({
+ orderBy: 'name',
+ ...TeamSearchFilter,
+ });
+
+ const teams = await prisma.client.team.findMany({
+ where: {
+ ...where,
+ },
+ ...pageFilters,
+ ...(options?.include && { include: options?.include }),
+ });
+ const count = await prisma.client.team.count({ where });
+
+ return { data: teams, count, ...getParameters };
+}
+
+export async function getTeamsByUserId(
+ userId: string,
+ filter?: SearchFilter,
+): Promise> {
+ return getTeams(
+ { userId, ...filter },
+ {
+ include: {
+ teamUser: {
+ include: {
+ user: {
+ select: {
+ id: true,
+ username: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ );
+}
diff --git a/queries/admin/user.ts b/queries/admin/user.ts
index f60c4801..f4be4751 100644
--- a/queries/admin/user.ts
+++ b/queries/admin/user.ts
@@ -1,9 +1,9 @@
-import { Prisma, Team, TeamUser } from '@prisma/client';
-import { getRandomChars } from 'next-basics';
+import { Prisma } from '@prisma/client';
import cache from 'lib/cache';
-import { ROLES } from 'lib/constants';
+import { ROLES, USER_FILTER_TYPES } from 'lib/constants';
import prisma from 'lib/prisma';
-import { Website, User, Role } from 'lib/types';
+import { FilterResult, Role, User, UserSearchFilter } from 'lib/types';
+import { getRandomChars } from 'next-basics';
export interface GetUserOptions {
includePassword?: boolean;
@@ -36,125 +36,59 @@ export async function getUserByUsername(username: string, options: GetUserOption
return getUser({ username }, options);
}
-export async function getUsers(): Promise {
- return prisma.client.user.findMany({
- take: 100,
- where: {
- deletedAt: null,
- },
- orderBy: [
- {
- username: 'asc',
- },
- ],
- select: {
- id: true,
- username: true,
- role: true,
- createdAt: true,
- },
- });
-}
-
-export async function getUserTeams(userId: string): Promise<
- (Team & {
- teamUser: (TeamUser & {
- user: { id: string; username: string };
- })[];
- })[]
-> {
- return prisma.client.team.findMany({
- where: {
+export async function getUsers(
+ UserSearchFilter: UserSearchFilter = {},
+ options?: { include?: Prisma.UserInclude },
+): Promise> {
+ const { teamId, filter, filterType = USER_FILTER_TYPES.all } = UserSearchFilter;
+ const where: Prisma.UserWhereInput = {
+ ...(teamId && {
teamUser: {
some: {
- userId,
+ teamId,
},
},
- },
- include: {
- teamUser: {
- include: {
- user: {
- select: {
- id: true,
- username: true,
- },
+ }),
+ ...(filter && {
+ AND: {
+ OR: [
+ {
+ ...((filterType === USER_FILTER_TYPES.all ||
+ filterType === USER_FILTER_TYPES.username) && {
+ username: {
+ startsWith: filter,
+ },
+ }),
},
- },
+ ],
},
- },
+ }),
+ };
+ const [pageFilters, getParameters] = prisma.getPageFilters({
+ orderBy: 'username',
+ ...UserSearchFilter,
});
-}
-export async function getUserWebsites(
- userId: string,
- options?: { includeTeams: boolean },
-): Promise {
- const { rawQuery } = prisma;
-
- if (options?.includeTeams) {
- const websites = await rawQuery(
- `
- select
- website_id as "id",
- name,
- domain,
- share_id as "shareId",
- reset_at as "resetAt",
- user_id as "userId",
- created_at as "createdAt",
- updated_at as "updatedAt",
- deleted_at as "deletedAt",
- null as "teamId",
- null as "teamName"
- from website
- where user_id = {{userId::uuid}}
- and deleted_at is null
- union
- select
- w.website_id as "id",
- w.name,
- w.domain,
- w.share_id as "shareId",
- w.reset_at as "resetAt",
- w.user_id as "userId",
- w.created_at as "createdAt",
- w.updated_at as "updatedAt",
- w.deleted_at as "deletedAt",
- t.team_id as "teamId",
- t.name as "teamName"
- from website w
- inner join team_website tw
- on tw.website_id = w.website_id
- inner join team t
- on t.team_id = tw.team_id
- inner join team_user tu
- on tu.team_id = tw.team_id
- where tu.user_id = {{userId::uuid}}
- and w.deleted_at is null
- `,
- { userId },
- );
-
- return websites.reduce((arr, item) => {
- if (!arr.find(({ id }) => id === item.id)) {
- return arr.concat(item);
- }
- return arr;
- }, []);
- }
-
- return prisma.client.website.findMany({
+ const users = await prisma.client.user.findMany({
where: {
- userId,
+ ...where,
deletedAt: null,
},
- orderBy: [
- {
- name: 'asc',
- },
- ],
+ ...pageFilters,
+ ...(options?.include && { include: options.include }),
});
+ const count = await prisma.client.user.count({
+ where: {
+ ...where,
+ deletedAt: null,
+ },
+ });
+
+ return { data: users as any, count, ...getParameters };
+}
+
+export async function getUsersByTeamId(teamId: string, filter?: UserSearchFilter) {
+ return getUsers({ teamId, ...filter });
}
export async function createUser(data: {
diff --git a/queries/admin/website.ts b/queries/admin/website.ts
index 35f32bac..68f634a6 100644
--- a/queries/admin/website.ts
+++ b/queries/admin/website.ts
@@ -1,6 +1,8 @@
import { Prisma, Website } from '@prisma/client';
import cache from 'lib/cache';
+import { ROLES, WEBSITE_FILTER_TYPES } from 'lib/constants';
import prisma from 'lib/prisma';
+import { FilterResult, WebsiteSearchFilter } from 'lib/types';
async function getWebsite(where: Prisma.WebsiteWhereUniqueInput): Promise {
return prisma.client.website.findUnique({
@@ -16,11 +18,199 @@ export async function getWebsiteByShareId(shareId: string) {
return getWebsite({ shareId });
}
-export async function getWebsites(): Promise {
- return prisma.client.website.findMany({
- orderBy: {
- name: 'asc',
+export async function getWebsites(
+ WebsiteSearchFilter: WebsiteSearchFilter,
+ options?: { include?: Prisma.WebsiteInclude },
+): Promise> {
+ const {
+ userId,
+ teamId,
+ includeTeams,
+ filter,
+ filterType = WEBSITE_FILTER_TYPES.all,
+ } = WebsiteSearchFilter;
+
+ const filterQuery = {
+ AND: {
+ OR: [
+ {
+ ...((filterType === WEBSITE_FILTER_TYPES.all ||
+ filterType === WEBSITE_FILTER_TYPES.name) && {
+ name: { startsWith: filter },
+ }),
+ },
+ {
+ ...((filterType === WEBSITE_FILTER_TYPES.all ||
+ filterType === WEBSITE_FILTER_TYPES.domain) && {
+ domain: { startsWith: filter },
+ }),
+ },
+ ],
},
+ };
+
+ const where: Prisma.WebsiteWhereInput = {
+ ...(teamId && {
+ teamWebsite: {
+ some: {
+ teamId,
+ },
+ },
+ }),
+ AND: {
+ OR: [
+ {
+ ...(userId && {
+ userId,
+ }),
+ },
+ {
+ ...(includeTeams && {
+ teamWebsite: {
+ some: {
+ team: {
+ teamUser: {
+ some: {
+ userId,
+ },
+ },
+ },
+ },
+ },
+ }),
+ },
+ ],
+ },
+ ...(filter && filterQuery),
+ };
+
+ const [pageFilters, getParameters] = prisma.getPageFilters({
+ orderBy: 'name',
+ ...WebsiteSearchFilter,
+ });
+
+ const websites = await prisma.client.website.findMany({
+ where: {
+ ...where,
+ deletedAt: null,
+ },
+ ...pageFilters,
+ ...(options?.include && { include: options.include }),
+ });
+ const count = await prisma.client.website.count({ where });
+
+ return { data: websites, count, ...getParameters };
+}
+
+export async function getWebsitesByUserId(
+ userId: string,
+ filter?: WebsiteSearchFilter,
+): Promise> {
+ return getWebsites({ userId, ...filter });
+}
+
+export async function getWebsitesByTeamId(
+ teamId: string,
+ filter?: WebsiteSearchFilter,
+): Promise> {
+ return getWebsites(
+ {
+ teamId,
+ ...filter,
+ includeTeams: true,
+ },
+ {
+ include: {
+ teamWebsite: {
+ include: {
+ team: {
+ include: {
+ teamUser: {
+ where: { role: ROLES.teamOwner },
+ },
+ },
+ },
+ },
+ },
+ user: {
+ select: {
+ id: true,
+ username: true,
+ },
+ },
+ },
+ },
+ );
+}
+
+export async function getUserWebsites(
+ userId: string,
+ options?: { includeTeams: boolean },
+): Promise {
+ const { rawQuery } = prisma;
+
+ if (options?.includeTeams) {
+ const websites = await rawQuery(
+ `
+ select
+ website_id as "id",
+ name,
+ domain,
+ share_id as "shareId",
+ reset_at as "resetAt",
+ user_id as "userId",
+ created_at as "createdAt",
+ updated_at as "updatedAt",
+ deleted_at as "deletedAt",
+ null as "teamId",
+ null as "teamName"
+ from website
+ where user_id = {{userId::uuid}}
+ and deleted_at is null
+ union
+ select
+ w.website_id as "id",
+ w.name,
+ w.domain,
+ w.share_id as "shareId",
+ w.reset_at as "resetAt",
+ w.user_id as "userId",
+ w.created_at as "createdAt",
+ w.updated_at as "updatedAt",
+ w.deleted_at as "deletedAt",
+ t.team_id as "teamId",
+ t.name as "teamName"
+ from website w
+ inner join team_website tw
+ on tw.website_id = w.website_id
+ inner join team t
+ on t.team_id = tw.team_id
+ inner join team_user tu
+ on tu.team_id = tw.team_id
+ where tu.user_id = {{userId::uuid}}
+ and w.deleted_at is null
+ `,
+ { userId },
+ );
+
+ return websites.reduce((arr, item) => {
+ if (!arr.find(({ id }) => id === item.id)) {
+ return arr.concat(item);
+ }
+ return arr;
+ }, []);
+ }
+
+ return prisma.client.website.findMany({
+ where: {
+ userId,
+ deletedAt: null,
+ },
+ orderBy: [
+ {
+ name: 'asc',
+ },
+ ],
});
}
diff --git a/yarn.lock b/yarn.lock
index d9224c2a..115e3cc9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7557,10 +7557,10 @@ rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
-react-basics@^0.91.0:
- version "0.91.0"
- resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.91.0.tgz#2970529a22a455ec73a1be884eb93a109c9dafc0"
- integrity sha512-vP8LYWiFwA+eguMEuHvHct4Jl5R/2GUjWc1tMujDG0CsAAUGhx68tAJr0K3gBrWjmpJrTPVfX8SdBNKSDAjQsw==
+react-basics@^0.92.0:
+ version "0.92.0"
+ resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.92.0.tgz#02bc6e88bdaf189c30cc6cbd8bbb1c9d12cd089b"
+ integrity sha512-BVUWg5a7R88konA9NedYMBx1hl50d6h/MD7qlKOEO/Cnm8cOC7AYTRKAKhO6kHMWjY4ZpUuvlg0UcF+SJP/uXA==
dependencies:
classnames "^2.3.1"
date-fns "^2.29.3"