From b78ff3b477e9daa804446d9bf0f912317b176d7a Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 6 Jul 2025 08:22:29 -0700 Subject: [PATCH] New admin section. --- next.config.ts | 5 ++ src/app/(main)/MenuBar.tsx | 9 +++- src/app/(main)/admin/AdminLayout.tsx | 54 +++++++++++++++++++ src/app/(main)/admin/layout.tsx | 17 ++++++ .../users/UserAddButton.tsx | 0 .../{settings => admin}/users/UserAddForm.tsx | 0 .../users/UserDeleteButton.tsx | 0 src/app/(main)/admin/users/UserDeleteForm.tsx | 43 +++++++++++++++ .../users/UsersDataTable.tsx | 0 .../users/UsersSettingsPage.tsx | 2 +- .../{settings => admin}/users/UsersTable.tsx | 28 ++++------ .../users/[userId]/UserEditForm.tsx | 0 .../users/[userId]/UserPage.tsx | 0 .../users/[userId]/UserProvider.tsx | 0 .../users/[userId]/UserSettings.tsx | 0 .../users/[userId]/UserWebsites.tsx | 2 +- .../users/[userId]/page.tsx | 0 .../(main)/{settings => admin}/users/page.tsx | 0 .../(main)/settings/users/UserDeleteForm.tsx | 36 ------------- .../settings/websites/WebsitesDataTable.tsx | 7 +-- .../websites/WebsitesSettingsPage.tsx | 4 +- src/app/api/teams/[teamId]/websites/route.ts | 9 ++-- src/app/api/users/[userId]/websites/route.ts | 9 ++-- src/app/api/websites/route.ts | 17 +----- src/components/hooks/queries/useTeamsQuery.ts | 4 +- src/components/input/TeamsButton.tsx | 8 +-- src/components/input/WebsiteSelect.tsx | 4 -- src/lib/schema.ts | 3 ++ 28 files changed, 161 insertions(+), 100 deletions(-) create mode 100644 src/app/(main)/admin/AdminLayout.tsx create mode 100644 src/app/(main)/admin/layout.tsx rename src/app/(main)/{settings => admin}/users/UserAddButton.tsx (100%) rename src/app/(main)/{settings => admin}/users/UserAddForm.tsx (100%) rename src/app/(main)/{settings => admin}/users/UserDeleteButton.tsx (100%) create mode 100644 src/app/(main)/admin/users/UserDeleteForm.tsx rename src/app/(main)/{settings => admin}/users/UsersDataTable.tsx (100%) rename src/app/(main)/{settings => admin}/users/UsersSettingsPage.tsx (87%) rename src/app/(main)/{settings => admin}/users/UsersTable.tsx (79%) rename src/app/(main)/{settings => admin}/users/[userId]/UserEditForm.tsx (100%) rename src/app/(main)/{settings => admin}/users/[userId]/UserPage.tsx (100%) rename src/app/(main)/{settings => admin}/users/[userId]/UserProvider.tsx (100%) rename src/app/(main)/{settings => admin}/users/[userId]/UserSettings.tsx (100%) rename src/app/(main)/{settings => admin}/users/[userId]/UserWebsites.tsx (100%) rename src/app/(main)/{settings => admin}/users/[userId]/page.tsx (100%) rename src/app/(main)/{settings => admin}/users/page.tsx (100%) delete mode 100644 src/app/(main)/settings/users/UserDeleteForm.tsx 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 ( - + @@ -90,19 +88,13 @@ export function UsersTable({ )} - - {({ close }) => ( - { - close(); - setDeleteUser(null); - }} - /> - )} - + { + 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 (