Added useFilterQuery. Converted websites and reports pages.

This commit is contained in:
Mike Cao 2023-09-26 23:20:29 -07:00
parent 6846355c63
commit 7e626dcd52
29 changed files with 373 additions and 499 deletions

View File

@ -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 <Banner variant="error">{formatMessage(messages.error)}</Banner>;
}
return (
<DataTableContext.Provider value={dataTable}>
{showSearch && (
<>
{(hasData || query || isLoading) && showSearch && (
<SearchField
className={styles.search}
value={query}
@ -50,8 +50,16 @@ export function DataTable({
placeholder={formatMessage(labels.search)}
/>
)}
{noResults && <Empty message={formatMessage(messages.noResultsFound)} />}
<div className={styles.body}>{children}</div>
<div
className={classNames(styles.body, { [styles.status]: isLoading || noResults || !hasData })}
>
{hasData && typeof children === 'function' ? children(data) : children}
{isLoading && <Loading icon="dots" />}
{!isLoading && !hasData && !query && (
<Empty message={formatMessage(messages.noDataAvailable)} />
)}
{noResults && <Empty message={formatMessage(messages.noResultsFound)} />}
</div>
{showPaging && (
<Pager
className={styles.pager}
@ -61,7 +69,7 @@ export function DataTable({
onPageChange={handlePageChange}
/>
)}
</DataTableContext.Provider>
</>
);
}

View File

@ -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;
}

View File

@ -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 (
<Flexbox justifyContent="center" className={classNames(styles.container, className)}>
<Button onClick={() => handlePageChange(-1)} disabled={firstPage}>
<Icon rotate={90}>
<Icons.ChevronDown />
</Icon>
</Button>
<Flexbox alignItems="center" className={styles.text}>
{formatMessage(labels.pageOf, { current: page, total: maxPage })}
</Flexbox>
<Button onClick={() => handlePageChange(1)} disabled={lastPage}>
<Icon rotate={270}>
<Icons.ChevronDown />
</Icon>
</Button>
</Flexbox>
<div className={classNames(styles.pager, className)}>
<div>{formatMessage(labels.numberOfRecords, { x: count })}</div>
<div className={styles.nav}>
<Button onClick={() => handlePageChange(-1)} disabled={firstPage}>
<Icon rotate={90}>
<Icons.ChevronDown />
</Icon>
</Button>
<div className={styles.text}>
{formatMessage(labels.pageOf, { current: page, total: maxPage })}
</div>
<Button onClick={() => handlePageChange(1)} disabled={lastPage}>
<Icon rotate={270}>
<Icons.ChevronDown />
</Icon>
</Button>
</div>
<div></div>
</div>
);
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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 };
}

View File

@ -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({

View File

@ -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 (
<Page loading={isLoading} error={error}>
<>
<PageHeader title={formatMessage(labels.reports)}>
<Link href="/reports/create">
<Button variant="primary">
@ -33,22 +44,10 @@ export function ReportsPage() {
</Button>
</Link>
</PageHeader>
{hasData && (
<ReportsTable
data={reports}
showSearch={true}
showPaging={true}
onFilterChange={handleFilterChange}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
onDelete={deleteReport}
filterValue={filter}
showDomain={true}
/>
)}
{!hasData && <EmptyPlaceholder />}
</Page>
<DataTable {...reports}>
{({ data }) => <ReportsTable data={data} showDomain={true} onDelete={handleDelete} />}
</DataTable>
</>
);
}

View File

@ -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 (
<>
<SettingsTable
columns={columns}
cellRender={cellRender}
data={data}
showSearch={true}
showPaging={true}
onFilterChange={onFilterChange}
onPageChange={onPageChange}
onPageSizeChange={onPageSizeChange}
filterValue={filterValue}
>
<GridTable data={data}>
<GridColumn name="name" label={formatMessage(labels.name)} />
<GridColumn name="description" label={formatMessage(labels.description)} />
<GridColumn name="type" label={formatMessage(labels.type)}>
{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)],
);
}}
</GridColumn>
{showDomain && (
<GridColumn name="domain" label={formatMessage(labels.domain)}>
{row => row.website.domain}
</GridColumn>
)}
<GridColumn name="action" label="" alignment="end">
{row => {
const { id, name, userId, website } = row;
return (
<Flexbox gap={10}>
<LinkButton href={`/reports/${id}`}>{formatMessage(labels.view)}</LinkButton>
{!showDomain || user.id === reportOwnerId || user.id === website?.userId}
<Button onClick={() => setReport(row)}>
<Icon>
<Icons.Trash />
</Icon>
<Text>{formatMessage(labels.delete)}</Text>
</Button>
{(user.id === userId || user.id === website?.userId) && (
<ModalTrigger>
<Button>
<Icon>
<Icons.Trash />
</Icon>
<Text>{formatMessage(labels.delete)}</Text>
</Button>
<Modal>
{close => (
<ConfirmDeleteForm
name={name}
onConfirm={handleConfirm.bind(null, id, close)}
onClose={close}
/>
)}
</Modal>
</ModalTrigger>
)}
</Flexbox>
);
}}
</SettingsTable>
{report && (
<Modal>
<ConfirmDeleteForm
name={report.name}
onConfirm={handleConfirm}
onClose={() => setReport(null)}
/>
</Modal>
)}
</>
</GridColumn>
</GridTable>
);
}

View File

@ -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 && (
<ModalTrigger>
<Button variant="primary">
<Icon>
<Icons.Plus />
</Icon>
<Text>{formatMessage(labels.addWebsite)}</Text>
</Button>
<Modal title={formatMessage(labels.addWebsite)}>
{close => <WebsiteAddForm onSave={handleSave} onClose={close} />}
</Modal>
</ModalTrigger>
)}
</>
<ModalTrigger>
<Button variant="primary">
<Icon>
<Icons.Plus />
</Icon>
<Text>{formatMessage(labels.addWebsite)}</Text>
</Button>
<Modal title={formatMessage(labels.addWebsite)}>
{close => <WebsiteAddForm onSave={handleSave} onClose={close} />}
</Modal>
</ModalTrigger>
);
return (
<Page loading={isLoading && count.current === 0} error={error}>
{showHeader && <PageHeader title={formatMessage(labels.websites)}>{addButton}</PageHeader>}
<WebsitesTable
data={data?.data}
showTeam={showTeam}
showEditButton={showEditButton}
onChange={handleChange}
/>
</Page>
<>
{showHeader && (
<PageHeader title={formatMessage(labels.websites)}>
{user.role !== ROLES.viewOnly && addButton}
</PageHeader>
)}
<DataTable {...filterQuery}>
{({ data }) => (
<WebsitesTable data={data} showTeam={showTeam} showEditButton={showEditButton} />
)}
</DataTable>
</>
);
}

View File

@ -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 (
<DataTable onChange={onChange}>
{showTable && (
<GridTable data={data}>
<GridColumn name="name" label={formatMessage(labels.name)} />
<GridColumn name="domain" label={formatMessage(labels.domain)} />
{showTeam && (
<GridColumn name="teamName" label={formatMessage(labels.teamName)}>
{row => row.teamWebsite[0]?.team.name}
</GridColumn>
)}
{showTeam && (
<GridColumn name="owner" label={formatMessage(labels.owner)}>
{row => row.user.username}
</GridColumn>
)}
<GridColumn name="action" label=" " className={DataTableStyles.action}>
{row => {
const {
id,
user: { id: ownerId },
} = row;
return (
<>
{showEditButton && (!showTeam || ownerId === user.id) && (
<Link href={`/settings/websites/${id}`}>
<Button>
<Icon>
<Icons.Edit />
</Icon>
<Text>{formatMessage(labels.edit)}</Text>
</Button>
</Link>
)}
<Link href={`/websites/${id}`} target={openExternal ? '_blank' : null}>
<Button>
<Icon>
<Icons.External />
</Icon>
<Text>{formatMessage(labels.view)}</Text>
</Button>
</Link>
</>
);
}}
</GridColumn>
</GridTable>
<GridTable data={data}>
<GridColumn name="name" label={formatMessage(labels.name)} />
<GridColumn name="domain" label={formatMessage(labels.domain)} />
{showTeam && (
<GridColumn name="teamName" label={formatMessage(labels.teamName)}>
{row => row.teamWebsite[0]?.team.name}
</GridColumn>
)}
</DataTable>
);
}
{showTeam && (
<GridColumn name="owner" label={formatMessage(labels.owner)}>
{row => row.user.username}
</GridColumn>
)}
<GridColumn name="action" label=" " alignment="end">
{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 && (
<SettingsTable
columns={columns}
data={data}
showSearch={true}
showPaging={true}
onFilterChange={onFilterChange}
onPageChange={onPageChange}
onPageSizeChange={onPageSizeChange}
filterValue={filterValue}
>
{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) && (
<Link href={`/settings/websites/${id}`}>
<Button>
<Icon>
<Icons.Edit />
</Icon>
<Text>{formatMessage(labels.edit)}</Text>
</Button>
</Link>
)}
<Link href={`/websites/${id}`} target={openExternal ? '_blank' : null}>
return (
<Flexbox gap={10}>
{showEditButton && (!showTeam || ownerId === user.id) && (
<Link href={`/settings/websites/${id}`}>
<Button>
<Icon>
<Icons.External />
<Icons.Edit />
</Icon>
<Text>{formatMessage(labels.view)}</Text>
<Text>{formatMessage(labels.edit)}</Text>
</Button>
</Link>
</>
);
}}
</SettingsTable>
)}
{!showTable && <Empty />}
</>
)}
<Link href={`/websites/${id}`}>
<Button>
<Icon>
<Icons.External />
</Icon>
<Text>{formatMessage(labels.view)}</Text>
</Button>
</Link>
</Flexbox>
);
}}
</GridColumn>
</GridTable>
);
}

View File

@ -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 = [

View File

@ -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<any> {
return prisma.rawQuery(query, params);
}
function getPageFilters(filters: SearchFilter<any>): [
function getPageFilters(filters: SearchFilter): [
{
orderBy: {
[x: string]: string;
@ -185,7 +185,7 @@ function getPageFilters(filters: SearchFilter<any>): [
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<any>): [
],
}),
},
{ pageSize, page: +page, orderBy },
{ page: +page, pageSize, orderBy },
];
}

View File

@ -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<typeof DATA_TYPE>;
export type KafkaTopic = ObjectValues<typeof KAFKA_TOPIC>;
export type ReportType = ObjectValues<typeof REPORT_TYPES>;
export type ReportSearchFilterType = ObjectValues<typeof REPORT_FILTER_TYPES>;
export type UserSearchFilterType = ObjectValues<typeof USER_FILTER_TYPES>;
export type WebsiteSearchFilterType = ObjectValues<typeof WEBSITE_FILTER_TYPES>;
export type TeamSearchFilterType = ObjectValues<typeof TEAM_FILTER_TYPES>;
export interface WebsiteSearchFilter extends SearchFilter<WebsiteSearchFilterType> {
export interface WebsiteSearchFilter extends SearchFilter {
userId?: string;
teamId?: string;
includeTeams?: boolean;
onlyTeams?: boolean;
}
export interface UserSearchFilter extends SearchFilter<UserSearchFilterType> {
export interface UserSearchFilter extends SearchFilter {
teamId?: string;
}
export interface TeamSearchFilter extends SearchFilter<TeamSearchFilterType> {
export interface TeamSearchFilter extends SearchFilter {
userId?: string;
}
export interface ReportSearchFilter extends SearchFilter<ReportSearchFilterType> {
export interface ReportSearchFilter extends SearchFilter {
userId?: string;
websiteId?: string;
includeTeams?: boolean;
}
export interface SearchFilter<T> {
export interface SearchFilter {
query?: string;
page?: number;
pageSize?: number;
orderBy?: string;
data?: T;
sortDescending?: boolean;
}
export interface FilterResult<T> {
data: T;
count: number;
pageSize: number;
page: number;
pageSize: number;
orderBy?: string;
sortDescending?: boolean;
}
export interface DynamicData {

View File

@ -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<TeamSearchFilterType> {
export interface MyTeamsRequestQuery extends SearchFilter {
id: string;
}

View File

@ -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<WebsiteSearchFilterType> {
export interface MyWebsitesRequestQuery extends SearchFilter {
id: string;
}

View File

@ -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<ReportSearchFilterType> {}
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,
});

View File

@ -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);
}

View File

@ -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<TeamSearchFilterType> {
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);

View File

@ -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<WebsiteSearchFilterType> {
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);
}

View File

@ -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<TeamSearchFilterType> {}
export interface TeamsRequestQuery extends SearchFilter {}
export interface TeamsRequestBody {
name: string;
}
export interface MyTeamsRequestQuery extends SearchFilter<TeamSearchFilterType> {}
export interface MyTeamsRequestQuery extends SearchFilter {}
const schema = {
GET: yup.object().shape({

View File

@ -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<TeamSearchFilterType> {
export interface UserTeamsRequestQuery extends SearchFilter {
id: string;
}

View File

@ -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<WebsiteSearchFilterType> {
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,
});

View File

@ -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<UserSearchFilterType> {}
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);
}

View File

@ -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<ReportSearchFilterType> {
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);

View File

@ -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<WebsiteSearchFilterType> {}
export interface WebsitesRequestQuery extends SearchFilter {}
export interface WebsitesRequestBody {
name: string;

View File

@ -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<Report> {
}
export async function getReports(
ReportSearchFilter: ReportSearchFilter,
params: ReportSearchFilter,
options?: { include?: Prisma.ReportInclude },
): Promise<FilterResult<Report[]>> {
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<FilterResult<Report[]>> {
return getReports(
{ userId, ...filter },

View File

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