diff --git a/next.config.ts b/next.config.ts
index b63abebb..b57a65a9 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -138,6 +138,11 @@ const redirects = [
destination: '/teams/:id/settings/team',
permanent: true,
},
+ {
+ source: '/admin',
+ destination: '/admin/users',
+ permanent: true,
+ },
];
// Adding rewrites + headers for all alternative tracker script names.
diff --git a/src/app/(main)/MenuBar.tsx b/src/app/(main)/MenuBar.tsx
index 7b951865..dc5c6cb4 100644
--- a/src/app/(main)/MenuBar.tsx
+++ b/src/app/(main)/MenuBar.tsx
@@ -8,7 +8,7 @@ import { useNavigation, useGlobalState } from '@/components/hooks';
export function MenuBar() {
const [isCollapsed, setCollapsed] = useGlobalState('sidenav-collapsed');
- const { websiteId } = useNavigation();
+ const { teamId, websiteId } = useNavigation();
const handleSelect = () => {};
@@ -35,7 +35,12 @@ export function MenuBar() {
-
+
>
)}
diff --git a/src/app/(main)/admin/AdminLayout.tsx b/src/app/(main)/admin/AdminLayout.tsx
new file mode 100644
index 00000000..5ba75d81
--- /dev/null
+++ b/src/app/(main)/admin/AdminLayout.tsx
@@ -0,0 +1,54 @@
+'use client';
+import { ReactNode } from 'react';
+import { Grid, Column } from '@umami/react-zen';
+import { useLoginQuery, useMessages, useNavigation } from '@/components/hooks';
+import { SideMenu } from '@/components/common/SideMenu';
+import { PageHeader } from '@/components/common/PageHeader';
+import { Panel } from '@/components/common/Panel';
+import { PageBody } from '@/components/common/PageBody';
+
+export function AdminLayout({ children }: { children: ReactNode }) {
+ const { user } = useLoginQuery();
+ const { formatMessage, labels } = useMessages();
+ const { pathname } = useNavigation();
+
+ if (!user.isAdmin) {
+ return null;
+ }
+
+ const items = [
+ {
+ id: 'users',
+ label: formatMessage(labels.users),
+ url: '/admin/users',
+ },
+ {
+ id: 'websites',
+ label: formatMessage(labels.websites),
+ url: '/admin/websites',
+ },
+ {
+ id: 'teams',
+ label: formatMessage(labels.teams),
+ url: '/admin/teams',
+ },
+ ];
+
+ const value = items.find(({ url }) => pathname.includes(url))?.id;
+
+ return (
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/admin/layout.tsx b/src/app/(main)/admin/layout.tsx
new file mode 100644
index 00000000..634fc658
--- /dev/null
+++ b/src/app/(main)/admin/layout.tsx
@@ -0,0 +1,17 @@
+import { Metadata } from 'next';
+import { AdminLayout } from './AdminLayout';
+
+export default function ({ children }) {
+ if (process.env.cloudMode) {
+ return null;
+ }
+
+ return {children};
+}
+
+export const metadata: Metadata = {
+ title: {
+ template: '%s | Admin | Umami',
+ default: 'Admin | Umami',
+ },
+};
diff --git a/src/app/(main)/settings/users/UserAddButton.tsx b/src/app/(main)/admin/users/UserAddButton.tsx
similarity index 100%
rename from src/app/(main)/settings/users/UserAddButton.tsx
rename to src/app/(main)/admin/users/UserAddButton.tsx
diff --git a/src/app/(main)/settings/users/UserAddForm.tsx b/src/app/(main)/admin/users/UserAddForm.tsx
similarity index 100%
rename from src/app/(main)/settings/users/UserAddForm.tsx
rename to src/app/(main)/admin/users/UserAddForm.tsx
diff --git a/src/app/(main)/settings/users/UserDeleteButton.tsx b/src/app/(main)/admin/users/UserDeleteButton.tsx
similarity index 100%
rename from src/app/(main)/settings/users/UserDeleteButton.tsx
rename to src/app/(main)/admin/users/UserDeleteButton.tsx
diff --git a/src/app/(main)/admin/users/UserDeleteForm.tsx b/src/app/(main)/admin/users/UserDeleteForm.tsx
new file mode 100644
index 00000000..59b12721
--- /dev/null
+++ b/src/app/(main)/admin/users/UserDeleteForm.tsx
@@ -0,0 +1,43 @@
+import { AlertDialog, Row } from '@umami/react-zen';
+import { useApi, useMessages, useModified } from '@/components/hooks';
+
+export function UserDeleteForm({
+ userId,
+ username,
+ onSave,
+ onClose,
+}: {
+ userId: string;
+ username: string;
+ onSave?: () => void;
+ onClose?: () => void;
+}) {
+ const { messages, labels, formatMessage } = useMessages();
+ const { del, useMutation } = useApi();
+ const { mutate } = useMutation({ mutationFn: () => del(`/users/${userId}`) });
+ const { touch } = useModified();
+
+ const handleConfirm = async () => {
+ mutate(null, {
+ onSuccess: async () => {
+ touch('users');
+ onSave?.();
+ onClose?.();
+ },
+ });
+ };
+
+ return (
+
+
+ {formatMessage(messages.confirmDelete, { target: {username} })}
+
+
+ );
+}
diff --git a/src/app/(main)/settings/users/UsersDataTable.tsx b/src/app/(main)/admin/users/UsersDataTable.tsx
similarity index 100%
rename from src/app/(main)/settings/users/UsersDataTable.tsx
rename to src/app/(main)/admin/users/UsersDataTable.tsx
diff --git a/src/app/(main)/settings/users/UsersSettingsPage.tsx b/src/app/(main)/admin/users/UsersSettingsPage.tsx
similarity index 87%
rename from src/app/(main)/settings/users/UsersSettingsPage.tsx
rename to src/app/(main)/admin/users/UsersSettingsPage.tsx
index d5be5922..b2e53dac 100644
--- a/src/app/(main)/settings/users/UsersSettingsPage.tsx
+++ b/src/app/(main)/admin/users/UsersSettingsPage.tsx
@@ -2,8 +2,8 @@
import { UsersDataTable } from './UsersDataTable';
import { Column } from '@umami/react-zen';
import { SectionHeader } from '@/components/common/SectionHeader';
-import { UserAddButton } from '@/app/(main)/settings/users/UserAddButton';
import { useMessages } from '@/components/hooks';
+import { UserAddButton } from './UserAddButton';
export function UsersSettingsPage() {
const { formatMessage, labels } = useMessages();
diff --git a/src/app/(main)/settings/users/UsersTable.tsx b/src/app/(main)/admin/users/UsersTable.tsx
similarity index 79%
rename from src/app/(main)/settings/users/UsersTable.tsx
rename to src/app/(main)/admin/users/UsersTable.tsx
index 24b8afc9..ffa0ec33 100644
--- a/src/app/(main)/settings/users/UsersTable.tsx
+++ b/src/app/(main)/admin/users/UsersTable.tsx
@@ -8,7 +8,6 @@ import {
MenuItem,
MenuSeparator,
Modal,
- Dialog,
} from '@umami/react-zen';
import Link from 'next/link';
import { formatDistance } from 'date-fns';
@@ -17,7 +16,7 @@ import { Trash } from '@/components/icons';
import { useMessages, useLocale } from '@/components/hooks';
import { Edit } from '@/components/icons';
import { MenuButton } from '@/components/input/MenuButton';
-import { UserDeleteForm } from '@/app/(main)/settings/users/UserDeleteForm';
+import { UserDeleteForm } from './UserDeleteForm';
export function UsersTable({
data = [],
@@ -29,13 +28,12 @@ export function UsersTable({
const { formatMessage, labels } = useMessages();
const { dateLocale } = useLocale();
const [deleteUser, setDeleteUser] = useState(null);
- const handleDelete = () => {};
return (
<>
- {(row: any) => {row.username}}
+ {(row: any) => {row.username}}
{(row: any) =>
@@ -62,7 +60,7 @@ export function UsersTable({
return (
-
-
+ {
+ setDeleteUser(null);
+ }}
+ />
>
);
diff --git a/src/app/(main)/settings/users/[userId]/UserEditForm.tsx b/src/app/(main)/admin/users/[userId]/UserEditForm.tsx
similarity index 100%
rename from src/app/(main)/settings/users/[userId]/UserEditForm.tsx
rename to src/app/(main)/admin/users/[userId]/UserEditForm.tsx
diff --git a/src/app/(main)/settings/users/[userId]/UserPage.tsx b/src/app/(main)/admin/users/[userId]/UserPage.tsx
similarity index 100%
rename from src/app/(main)/settings/users/[userId]/UserPage.tsx
rename to src/app/(main)/admin/users/[userId]/UserPage.tsx
diff --git a/src/app/(main)/settings/users/[userId]/UserProvider.tsx b/src/app/(main)/admin/users/[userId]/UserProvider.tsx
similarity index 100%
rename from src/app/(main)/settings/users/[userId]/UserProvider.tsx
rename to src/app/(main)/admin/users/[userId]/UserProvider.tsx
diff --git a/src/app/(main)/settings/users/[userId]/UserSettings.tsx b/src/app/(main)/admin/users/[userId]/UserSettings.tsx
similarity index 100%
rename from src/app/(main)/settings/users/[userId]/UserSettings.tsx
rename to src/app/(main)/admin/users/[userId]/UserSettings.tsx
diff --git a/src/app/(main)/settings/users/[userId]/UserWebsites.tsx b/src/app/(main)/admin/users/[userId]/UserWebsites.tsx
similarity index 100%
rename from src/app/(main)/settings/users/[userId]/UserWebsites.tsx
rename to src/app/(main)/admin/users/[userId]/UserWebsites.tsx
index 8f096353..63a06867 100644
--- a/src/app/(main)/settings/users/[userId]/UserWebsites.tsx
+++ b/src/app/(main)/admin/users/[userId]/UserWebsites.tsx
@@ -1,6 +1,6 @@
-import { WebsitesTable } from '@/app/(main)/settings/websites/WebsitesTable';
import { DataGrid } from '@/components/common/DataGrid';
import { useWebsitesQuery } from '@/components/hooks';
+import { WebsitesTable } from '@/app/(main)/settings/websites/WebsitesTable';
export function UserWebsites({ userId }) {
const queryResult = useWebsitesQuery({ userId });
diff --git a/src/app/(main)/settings/users/[userId]/page.tsx b/src/app/(main)/admin/users/[userId]/page.tsx
similarity index 100%
rename from src/app/(main)/settings/users/[userId]/page.tsx
rename to src/app/(main)/admin/users/[userId]/page.tsx
diff --git a/src/app/(main)/settings/users/page.tsx b/src/app/(main)/admin/users/page.tsx
similarity index 100%
rename from src/app/(main)/settings/users/page.tsx
rename to src/app/(main)/admin/users/page.tsx
diff --git a/src/app/(main)/settings/users/UserDeleteForm.tsx b/src/app/(main)/settings/users/UserDeleteForm.tsx
deleted file mode 100644
index 18baf99b..00000000
--- a/src/app/(main)/settings/users/UserDeleteForm.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { useToast } from '@umami/react-zen';
-import { useApi, useMessages, useModified } from '@/components/hooks';
-import { ConfirmationForm } from '@/components/common/ConfirmationForm';
-
-export function UserDeleteForm({ userId, username, onSave, onClose }) {
- const { messages, labels, formatMessage } = useMessages();
- const { del, useMutation } = useApi();
- const { mutate, error, isPending } = useMutation({ mutationFn: () => del(`/users/${userId}`) });
- const { touch } = useModified();
- const { toast } = useToast();
-
- const handleConfirm = async () => {
- mutate(null, {
- onSuccess: async () => {
- touch('users');
- toast(formatMessage(messages.successMessage));
- onSave?.();
- onClose?.();
- },
- });
- };
-
- return (
- {username},
- })}
- onConfirm={handleConfirm}
- onClose={onClose}
- buttonLabel={formatMessage(labels.delete)}
- buttonVariant="danger"
- isLoading={isPending}
- error={error}
- />
- );
-}
diff --git a/src/app/(main)/settings/websites/WebsitesDataTable.tsx b/src/app/(main)/settings/websites/WebsitesDataTable.tsx
index c1aa0cd4..042c0923 100644
--- a/src/app/(main)/settings/websites/WebsitesDataTable.tsx
+++ b/src/app/(main)/settings/websites/WebsitesDataTable.tsx
@@ -1,5 +1,4 @@
-import { ReactNode } from 'react';
-import { WebsitesTable } from '@/app/(main)/settings/websites/WebsitesTable';
+import { WebsitesTable } from './WebsitesTable';
import { DataGrid } from '@/components/common/DataGrid';
import { useWebsitesQuery } from '@/components/hooks';
@@ -8,18 +7,16 @@ export function WebsitesDataTable({
allowEdit = true,
allowView = true,
showActions = true,
- children,
}: {
teamId?: string;
allowEdit?: boolean;
allowView?: boolean;
showActions?: boolean;
- children?: ReactNode;
}) {
const queryResult = useWebsitesQuery({ teamId });
return (
- children} allowSearch allowPaging>
+
{({ data }) => (
}) {
const schema = z.object({
...pagingParams,
+ ...searchParams,
});
const { teamId } = await params;
const { auth, query, error } = await parseRequest(request, schema);
@@ -20,9 +21,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ team
return unauthorized();
}
- const filters = await getQueryFilters(query);
-
- const websites = await getTeamWebsites(teamId, filters);
+ const websites = await getTeamWebsites(teamId, query);
return json(websites);
}
diff --git a/src/app/api/users/[userId]/websites/route.ts b/src/app/api/users/[userId]/websites/route.ts
index d19d754f..8634fd4b 100644
--- a/src/app/api/users/[userId]/websites/route.ts
+++ b/src/app/api/users/[userId]/websites/route.ts
@@ -1,12 +1,13 @@
import { z } from 'zod';
import { unauthorized, json } from '@/lib/response';
import { getUserWebsites } from '@/queries/prisma/website';
-import { pagingParams } from '@/lib/schema';
-import { getQueryFilters, parseRequest } from '@/lib/request';
+import { pagingParams, searchParams } from '@/lib/schema';
+import { parseRequest } from '@/lib/request';
export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) {
const schema = z.object({
...pagingParams,
+ ...searchParams,
});
const { auth, query, error } = await parseRequest(request, schema);
@@ -21,9 +22,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ user
return unauthorized();
}
- const filters = await getQueryFilters(query);
-
- const websites = await getUserWebsites(userId, filters);
+ const websites = await getUserWebsites(userId, query);
return json(websites);
}
diff --git a/src/app/api/websites/route.ts b/src/app/api/websites/route.ts
index b8fb2a0b..78140864 100644
--- a/src/app/api/websites/route.ts
+++ b/src/app/api/websites/route.ts
@@ -3,22 +3,9 @@ import { canCreateTeamWebsite, canCreateWebsite } from '@/lib/auth';
import { json, unauthorized } from '@/lib/response';
import { uuid } from '@/lib/crypto';
import { parseRequest } from '@/lib/request';
-import { createWebsite, getUserWebsites } from '@/queries';
-import { pagingParams } from '@/lib/schema';
+import { createWebsite } from '@/queries';
-export async function GET(request: Request) {
- const schema = z.object({ ...pagingParams });
-
- const { auth, query, error } = await parseRequest(request, schema);
-
- if (error) {
- return error();
- }
-
- const websites = await getUserWebsites(auth.user.id, query);
-
- return json(websites);
-}
+export { GET } from '@/app/api/users/[userId]/websites/route';
export async function POST(request: Request) {
const schema = z.object({
diff --git a/src/components/hooks/queries/useTeamsQuery.ts b/src/components/hooks/queries/useTeamsQuery.ts
index a70aca24..1a9e9d11 100644
--- a/src/components/hooks/queries/useTeamsQuery.ts
+++ b/src/components/hooks/queries/useTeamsQuery.ts
@@ -7,8 +7,8 @@ export function useTeamsQuery(userId: string) {
return useQuery({
queryKey: ['teams', { userId, modified }],
- queryFn: (params: any) => {
- return get(`/users/${userId}/teams`, params);
+ queryFn: () => {
+ return get(`/users/${userId}/teams`, { userId });
},
enabled: !!userId,
});
diff --git a/src/components/input/TeamsButton.tsx b/src/components/input/TeamsButton.tsx
index c8151b12..682bd916 100644
--- a/src/components/input/TeamsButton.tsx
+++ b/src/components/input/TeamsButton.tsx
@@ -25,17 +25,17 @@ export function TeamsButton({
}) {
const { user } = useLoginQuery();
const { formatMessage, labels } = useMessages();
- const { result } = useTeamsQuery(user.id);
+ const { data } = useTeamsQuery(user.id);
const { teamId } = useNavigation();
const router = useRouter();
- const team = result?.data?.find(({ id }) => id === teamId);
+ const team = data?.data?.find(({ id }) => id === teamId);
const selectedKeys = new Set([teamId || user.id]);
const handleSelect = (id: Key) => {
router.push(id === user.id ? '/websites' : `/teams/${id}/websites`);
};
- if (!result?.count) {
+ if (!data?.count) {
return null;
}
@@ -68,7 +68,7 @@ export function TeamsButton({
- {result?.data?.map(({ id, name }) => (
+ {data?.data?.map(({ id, name }) => (
diff --git a/src/components/input/WebsiteSelect.tsx b/src/components/input/WebsiteSelect.tsx
index b0b0f881..d858ca5f 100644
--- a/src/components/input/WebsiteSelect.tsx
+++ b/src/components/input/WebsiteSelect.tsx
@@ -29,10 +29,6 @@ export function WebsiteSelect({
setSearch(value);
};
- if (!data) {
- return null;
- }
-
return (