diff --git a/components/layout/NavBar.js b/components/layout/NavBar.js
index 97eaa46c..e896b404 100644
--- a/components/layout/NavBar.js
+++ b/components/layout/NavBar.js
@@ -18,6 +18,8 @@ export function NavBar() {
const links = [
{ label: formatMessage(labels.dashboard), url: '/dashboard' },
+ { label: formatMessage(labels.websites), url: '/websites' },
+ { label: formatMessage(labels.reports), url: '/reports' },
!cloudMode && { label: formatMessage(labels.settings), url: '/settings' },
].filter(n => n);
diff --git a/components/messages.js b/components/messages.js
index f47513e8..c0024810 100644
--- a/components/messages.js
+++ b/components/messages.js
@@ -21,6 +21,8 @@ export const labels = defineMessages({
details: { id: 'label.details', defaultMessage: 'Details' },
website: { id: 'label.website', defaultMessage: 'Website' },
websites: { id: 'label.websites', defaultMessage: 'Websites' },
+ myWebsites: { id: 'label.my-websites', defaultMessage: 'My Websites' },
+ teamWebsites: { id: 'label.team-websites', defaultMessage: 'Team Websites' },
created: { id: 'label.created', defaultMessage: 'Created' },
edit: { id: 'label.edit', defaultMessage: 'Edit' },
name: { id: 'label.name', defaultMessage: 'Name' },
@@ -28,6 +30,7 @@ export const labels = defineMessages({
accessCode: { id: 'label.access-code', defaultMessage: 'Access code' },
teamId: { id: 'label.team-id', defaultMessage: 'Team ID' },
team: { id: 'label.team', defaultMessage: 'Team' },
+ teamName: { id: 'label.team-name', defaultMessage: 'Team Name' },
regenerate: { id: 'label.regenerate', defaultMessage: 'Regenerate' },
remove: { id: 'label.remove', defaultMessage: 'Remove' },
join: { id: 'label.join', defaultMessage: 'Join' },
diff --git a/components/pages/reports/ReportsPage.js b/components/pages/reports/ReportsPage.js
index 470e1b08..d63fc77f 100644
--- a/components/pages/reports/ReportsPage.js
+++ b/components/pages/reports/ReportsPage.js
@@ -1,13 +1,24 @@
+import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
-import Link from 'next/link';
-import { Button, Icon, Icons, Text } from 'react-basics';
import { useMessages, useReports } from 'hooks';
+import Link from 'next/link';
+import { Button, Flexbox, Icon, Icons, Text } from 'react-basics';
import ReportsTable from './ReportsTable';
export function ReportsPage() {
- const { formatMessage, labels } = useMessages();
- const { reports, error, isLoading } = useReports();
+ const { formatMessage, labels, messages } = useMessages();
+ const {
+ reports,
+ error,
+ isLoading,
+ filter,
+ handleFilterChange,
+ handlePageChange,
+ handlePageSizeChange,
+ } = useReports();
+
+ const hasData = (reports && reports?.data.length !== 0) || filter;
return (
@@ -21,7 +32,22 @@ export function ReportsPage() {
-
+
+ {hasData && (
+
+ )}
+ {!hasData && (
+
+ )}
);
}
diff --git a/components/pages/reports/ReportsTable.js b/components/pages/reports/ReportsTable.js
index 529f5359..e59e4069 100644
--- a/components/pages/reports/ReportsTable.js
+++ b/components/pages/reports/ReportsTable.js
@@ -12,14 +12,23 @@ export function ReportsTable({
onFilterChange,
onPageChange,
onPageSizeChange,
+ showDomain,
}) {
const [report, setReport] = useState(null);
const { formatMessage, labels } = useMessages();
+ 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: ' ' },
];
@@ -41,6 +50,9 @@ export function ReportsTable({
>
{row => {
const { id } = row;
+ if (showDomain) {
+ row.domain = row.website.domain;
+ }
return (
diff --git a/components/pages/settings/websites/WebsitesList.js b/components/pages/settings/websites/WebsitesList.js
index 310b481f..f99b2d6e 100644
--- a/components/pages/settings/websites/WebsitesList.js
+++ b/components/pages/settings/websites/WebsitesList.js
@@ -10,19 +10,21 @@ import useMessages from 'hooks/useMessages';
import { ROLES } from 'lib/constants';
import useApiFilter from 'hooks/useApiFilter';
-export function WebsitesList() {
+export function WebsitesList({ showTeam, showHeader = true, includeTeams, onlyTeams, fetch }) {
const { formatMessage, labels, messages } = useMessages();
const { user } = useUser();
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
useApiFilter();
const { get, useQuery } = useApi();
const { data, isLoading, error, refetch } = useQuery(
- ['websites', user?.id, filter, page, pageSize],
+ ['websites', fetch, user?.id, filter, page, pageSize, includeTeams, onlyTeams],
() =>
get(`/users/${user?.id}/websites`, {
filter,
page,
pageSize,
+ includeTeams,
+ onlyTeams,
}),
{ enabled: !!user },
);
@@ -54,10 +56,11 @@ export function WebsitesList() {
return (
- {addButton}
+ {showHeader && {addButton}}
{hasData && (
{row => {
- const { id } = row;
+ const {
+ id,
+ teamWebsite,
+ user: { username },
+ } = row;
+ if (showTeam) {
+ row.teamName = teamWebsite[0]?.team.name;
+ row.owner = username;
+ }
return (
<>
diff --git a/components/pages/websites/WebsiteReportsPage.js b/components/pages/websites/WebsiteReportsPage.js
index beb9bc4f..85a002e6 100644
--- a/components/pages/websites/WebsiteReportsPage.js
+++ b/components/pages/websites/WebsiteReportsPage.js
@@ -1,7 +1,7 @@
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import Page from 'components/layout/Page';
import ReportsTable from 'components/pages/reports/ReportsTable';
-import { useMessages, useReports } from 'hooks';
+import { useMessages, useWebsiteReports } from 'hooks';
import Link from 'next/link';
import { Button, Flexbox, Icon, Icons, Text } from 'react-basics';
import WebsiteHeader from './WebsiteHeader';
@@ -17,9 +17,9 @@ export function WebsiteReportsPage({ websiteId }) {
handleFilterChange,
handlePageChange,
handlePageSizeChange,
- } = useReports(websiteId);
+ } = useWebsiteReports(websiteId);
- const hasData = reports && reports.data.length !== 0;
+ const hasData = (reports && reports.data.length !== 0) || filter;
const handleDelete = async id => {
await deleteReport(id);
@@ -48,11 +48,7 @@ export function WebsiteReportsPage({ websiteId }) {
filterValue={filter}
/>
)}
- {!hasData && (
-
- {/* {addButton} */}
-
- )}
+ {!hasData && }
);
}
diff --git a/components/pages/websites/WebsitesPage.js b/components/pages/websites/WebsitesPage.js
new file mode 100644
index 00000000..4fdd025d
--- /dev/null
+++ b/components/pages/websites/WebsitesPage.js
@@ -0,0 +1,67 @@
+import Page from 'components/layout/Page';
+import PageHeader from 'components/layout/PageHeader';
+import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm';
+import WebsiteList from 'components/pages/settings/websites/WebsitesList';
+import { useMessages } from 'hooks';
+import useUser from 'hooks/useUser';
+import { ROLES } from 'lib/constants';
+import { useState } from 'react';
+import {
+ Button,
+ Icon,
+ Icons,
+ Item,
+ Modal,
+ ModalTrigger,
+ Tabs,
+ Text,
+ useToasts,
+} from 'react-basics';
+
+export function WebsitesPage() {
+ const { formatMessage, labels, messages } = useMessages();
+ const [tab, setTab] = useState('my-websites');
+ const [fetch, setFetch] = useState(1);
+ const { user } = useUser();
+ const { showToast } = useToasts();
+
+ const handleSave = async () => {
+ setFetch(fetch + 1);
+ showToast({ message: formatMessage(messages.saved), variant: 'success' });
+ };
+
+ const addButton = (
+ <>
+ {user.role !== ROLES.viewOnly && (
+
+
+
+ {close => }
+
+
+ )}
+ >
+ );
+
+ return (
+
+ {addButton}
+
+ - {formatMessage(labels.myWebsites)}
+ - {formatMessage(labels.teamWebsites)}
+
+
+ {tab === 'my-websites' && }
+ {tab === 'team-webaites' && (
+
+ )}
+
+ );
+}
+
+export default WebsitesPage;
diff --git a/hooks/index.js b/hooks/index.js
index 004260b0..2596ba57 100644
--- a/hooks/index.js
+++ b/hooks/index.js
@@ -20,3 +20,4 @@ export * from './useTheme';
export * from './useTimezone';
export * from './useUser';
export * from './useWebsite';
+export * from './useWebsiteReports';
diff --git a/hooks/useReports.js b/hooks/useReports.js
index 57d76492..932fa6dc 100644
--- a/hooks/useReports.js
+++ b/hooks/useReports.js
@@ -2,15 +2,15 @@ import { useState } from 'react';
import useApi from './useApi';
import useApiFilter from 'hooks/useApiFilter';
-export function useReports(websiteId) {
+export function useReports() {
const [modified, setModified] = useState(Date.now());
const { get, useQuery, del, useMutation } = useApi();
const { mutate } = useMutation(reportId => del(`/reports/${reportId}`));
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 }),
+ ['reports', { modified, filter, page, pageSize }],
+ () => get(`/reports`, { filter, page, pageSize }),
);
const deleteReport = id => {
diff --git a/hooks/useWebsiteReports.js b/hooks/useWebsiteReports.js
new file mode 100644
index 00000000..3b7ec415
--- /dev/null
+++ b/hooks/useWebsiteReports.js
@@ -0,0 +1,38 @@
+import { useState } from 'react';
+import useApi from './useApi';
+import useApiFilter from 'hooks/useApiFilter';
+
+export function useWebsiteReports(websiteId) {
+ const [modified, setModified] = useState(Date.now());
+ const { get, useQuery, del, useMutation } = useApi();
+ const { mutate } = useMutation(reportId => del(`/reports/${reportId}`));
+ const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
+ useApiFilter();
+ const { data, error, isLoading } = useQuery(
+ ['reports:website', { websiteId, modified, filter, page, pageSize }],
+ () => get(`/websites/${websiteId}/reports`, { websiteId, filter, page, pageSize }),
+ );
+
+ const deleteReport = id => {
+ mutate(id, {
+ onSuccess: () => {
+ setModified(Date.now());
+ },
+ });
+ };
+
+ return {
+ reports: data,
+ error,
+ isLoading,
+ deleteReport,
+ filter,
+ page,
+ pageSize,
+ handleFilterChange,
+ handlePageChange,
+ handlePageSizeChange,
+ };
+}
+
+export default useWebsiteReports;
diff --git a/lib/types.ts b/lib/types.ts
index 5a25169a..65bef8fb 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -27,6 +27,7 @@ export interface WebsiteSearchFilter extends SearchFilter {
@@ -40,6 +41,7 @@ export interface TeamSearchFilter extends SearchFilter {
export interface ReportSearchFilter extends SearchFilter {
userId?: string;
websiteId?: string;
+ includeTeams?: boolean;
}
export interface SearchFilter {
diff --git a/pages/api/reports/index.ts b/pages/api/reports/index.ts
index 8c6825f1..db83e6ed 100644
--- a/pages/api/reports/index.ts
+++ b/pages/api/reports/index.ts
@@ -4,7 +4,7 @@ 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';
+import { createReport, getReportsByUserId, getReportsByWebsiteId } from 'queries';
export interface ReportsRequestQuery extends SearchFilter {}
@@ -26,20 +26,14 @@ export default async (
await useCors(req, res);
await useAuth(req, res);
- const { websiteId } = req.query;
-
const {
user: { id: userId },
} = req.auth;
if (req.method === 'GET') {
- if (!(websiteId && (await canViewWebsite(req.auth, websiteId)))) {
- return unauthorized(res);
- }
-
const { page, filter, pageSize } = req.query;
- const data = await getReportsByWebsiteId(websiteId, {
+ const data = await getReportsByUserId(userId, {
page,
filter,
pageSize: +pageSize || null,
diff --git a/pages/api/users/[id]/websites.ts b/pages/api/users/[id]/websites.ts
index 72d793d1..0e9231f7 100644
--- a/pages/api/users/[id]/websites.ts
+++ b/pages/api/users/[id]/websites.ts
@@ -21,7 +21,7 @@ export default async (
await useAuth(req, res);
const { user } = req.auth;
- const { id: userId, page, filter, pageSize, includeTeams } = req.query;
+ const { id: userId, page, filter, pageSize, includeTeams, onlyTeams } = req.query;
if (req.method === 'GET') {
if (!user.isAdmin && user.id !== userId) {
@@ -33,6 +33,7 @@ export default async (
filter,
pageSize: +pageSize || null,
includeTeams,
+ onlyTeams,
});
return ok(res, websites);
diff --git a/pages/api/websites/[id]/reports.ts b/pages/api/websites/[id]/reports.ts
new file mode 100644
index 00000000..60c6f714
--- /dev/null
+++ b/pages/api/websites/[id]/reports.ts
@@ -0,0 +1,38 @@
+import { canViewWebsite } from 'lib/auth';
+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 { getReportsByWebsiteId } from 'queries';
+
+export interface ReportsRequestQuery extends SearchFilter {
+ id: string;
+}
+
+export default async (
+ req: NextApiRequestQueryBody,
+ res: NextApiResponse,
+) => {
+ await useCors(req, res);
+ await useAuth(req, res);
+
+ const { id: websiteId } = req.query;
+
+ if (req.method === 'GET') {
+ if (!(websiteId && (await canViewWebsite(req.auth, websiteId)))) {
+ return unauthorized(res);
+ }
+
+ const { page, filter, pageSize } = req.query;
+
+ const data = await getReportsByWebsiteId(websiteId, {
+ page,
+ filter,
+ pageSize: +pageSize || null,
+ });
+
+ return ok(res, data);
+ }
+
+ return methodNotAllowed(res);
+};
diff --git a/pages/reports/index.js b/pages/reports/index.js
new file mode 100644
index 00000000..ff3b4e86
--- /dev/null
+++ b/pages/reports/index.js
@@ -0,0 +1,13 @@
+import AppLayout from 'components/layout/AppLayout';
+import ReportsPage from 'components/pages/reports/ReportsPage';
+import { useMessages } from 'hooks';
+
+export default function () {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+
+
+ );
+}
diff --git a/pages/websites/index.js b/pages/websites/index.js
new file mode 100644
index 00000000..42a327bc
--- /dev/null
+++ b/pages/websites/index.js
@@ -0,0 +1,13 @@
+import AppLayout from 'components/layout/AppLayout';
+import WebsitesPage from 'components/pages/websites/WebsitesPage';
+import useMessages from 'hooks/useMessages';
+
+export default function () {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+
+
+ );
+}
diff --git a/queries/admin/report.ts b/queries/admin/report.ts
index 7ca2f2b2..3c50c2cb 100644
--- a/queries/admin/report.ts
+++ b/queries/admin/report.ts
@@ -28,13 +28,45 @@ export async function deleteReport(reportId: string): Promise {
export async function getReports(
ReportSearchFilter: ReportSearchFilter,
+ options?: { include?: Prisma.ReportInclude },
): Promise> {
- const { userId, websiteId, filter, filterType = REPORT_FILTER_TYPES.all } = ReportSearchFilter;
+ const {
+ userId,
+ websiteId,
+ includeTeams,
+ filter,
+ filterType = REPORT_FILTER_TYPES.all,
+ } = ReportSearchFilter;
+
const where: Prisma.ReportWhereInput = {
...(userId && { userId: userId }),
...(websiteId && { websiteId: websiteId }),
- ...(filter && {
- AND: {
+ AND: [
+ {
+ OR: [
+ {
+ ...(userId && { userId: userId }),
+ },
+ {
+ ...(includeTeams && {
+ website: {
+ teamWebsite: {
+ some: {
+ team: {
+ teamUser: {
+ some: {
+ userId,
+ },
+ },
+ },
+ },
+ },
+ },
+ }),
+ },
+ ],
+ },
+ {
OR: [
{
...((filterType === REPORT_FILTER_TYPES.all ||
@@ -98,7 +130,7 @@ export async function getReports(
},
],
},
- }),
+ ],
};
const [pageFilters, getParameters] = prisma.getPageFilters(ReportSearchFilter);
@@ -106,6 +138,7 @@ export async function getReports(
const reports = await prisma.client.report.findMany({
where,
...pageFilters,
+ ...(options?.include && { include: options.include }),
});
const count = await prisma.client.report.count({
where,
@@ -122,7 +155,18 @@ export async function getReportsByUserId(
userId: string,
filter: SearchFilter,
): Promise> {
- return getReports({ userId, ...filter });
+ return getReports(
+ { userId, ...filter },
+ {
+ include: {
+ website: {
+ select: {
+ domain: true,
+ },
+ },
+ },
+ },
+ );
}
export async function getReportsByWebsiteId(
diff --git a/queries/admin/website.ts b/queries/admin/website.ts
index a55db814..d7b98b45 100644
--- a/queries/admin/website.ts
+++ b/queries/admin/website.ts
@@ -26,29 +26,11 @@ export async function getWebsites(
userId,
teamId,
includeTeams,
+ onlyTeams,
filter,
filterType = WEBSITE_FILTER_TYPES.all,
} = WebsiteSearchFilter;
- const filterQuery = {
- AND: {
- OR: [
- {
- ...((filterType === WEBSITE_FILTER_TYPES.all ||
- filterType === WEBSITE_FILTER_TYPES.name) && {
- name: { startsWith: filter, mode: 'insensitive' },
- }),
- },
- {
- ...((filterType === WEBSITE_FILTER_TYPES.all ||
- filterType === WEBSITE_FILTER_TYPES.domain) && {
- domain: { startsWith: filter, mode: 'insensitive' },
- }),
- },
- ],
- },
- };
-
const where: Prisma.WebsiteWhereInput = {
...(teamId && {
teamWebsite: {
@@ -61,28 +43,53 @@ export async function getWebsites(
{
OR: [
{
- ...(userId && {
- userId,
- }),
+ ...(userId &&
+ !onlyTeams && {
+ userId,
+ }),
},
{
- ...(includeTeams && {
- teamWebsite: {
- some: {
- team: {
- teamUser: {
- some: {
- userId,
+ ...((includeTeams || onlyTeams) && {
+ AND: [
+ {
+ teamWebsite: {
+ some: {
+ team: {
+ teamUser: {
+ some: {
+ userId,
+ },
+ },
},
},
},
},
- },
+ {
+ userId: {
+ not: userId,
+ },
+ },
+ ],
+ }),
+ },
+ ],
+ },
+ {
+ OR: [
+ {
+ ...((filterType === WEBSITE_FILTER_TYPES.all ||
+ filterType === WEBSITE_FILTER_TYPES.name) && {
+ name: { startsWith: filter, mode: 'insensitive' },
+ }),
+ },
+ {
+ ...((filterType === WEBSITE_FILTER_TYPES.all ||
+ filterType === WEBSITE_FILTER_TYPES.domain) && {
+ domain: { startsWith: filter, mode: 'insensitive' },
}),
},
],
},
- { ...(filter && filterQuery) },
],
};
@@ -108,7 +115,27 @@ export async function getWebsitesByUserId(
userId: string,
filter?: WebsiteSearchFilter,
): Promise> {
- return getWebsites({ userId, ...filter });
+ return getWebsites(
+ { userId, ...filter },
+ {
+ include: {
+ teamWebsite: {
+ include: {
+ team: {
+ select: {
+ name: true,
+ },
+ },
+ },
+ },
+ user: {
+ select: {
+ username: true,
+ },
+ },
+ },
+ },
+ );
}
export async function getWebsitesByTeamId(