feat: add workspace role permission check, hide non permission action

This commit is contained in:
moonrailgun 2024-09-22 01:05:16 +08:00
parent d29785a311
commit 4f4f9b5d3f
19 changed files with 466 additions and 334 deletions

View File

@ -3,7 +3,7 @@ import { Button } from '../ui/button';
import { LuArchive, LuArchiveRestore } from 'react-icons/lu'; import { LuArchive, LuArchiveRestore } from 'react-icons/lu';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { AppRouterOutput, trpc } from '@/api/trpc'; import { AppRouterOutput, trpc } from '@/api/trpc';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId, useHasAdminPermission } from '@/store/user';
import { DynamicVirtualList } from '../DynamicVirtualList'; import { DynamicVirtualList } from '../DynamicVirtualList';
import { get } from 'lodash-es'; import { get } from 'lodash-es';
import { FeedEventItem } from './FeedEventItem'; import { FeedEventItem } from './FeedEventItem';
@ -28,6 +28,7 @@ export const FeedArchivePageButton: React.FC<FeedArchivePageButtonProps> =
const clearAllArchivedEventsMutation = const clearAllArchivedEventsMutation =
trpc.feed.clearAllArchivedEvents.useMutation(); trpc.feed.clearAllArchivedEvents.useMutation();
const trpcUtils = trpc.useUtils(); const trpcUtils = trpc.useUtils();
const hasAdminPermission = useHasAdminPermission();
const { const {
data, data,
@ -87,9 +88,11 @@ export const FeedArchivePageButton: React.FC<FeedArchivePageButtonProps> =
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-lg font-bold">{t('Archived Events')}</h1> <h1 className="text-lg font-bold">{t('Archived Events')}</h1>
{hasAdminPermission && (
<AlertConfirm onConfirm={handleClear}> <AlertConfirm onConfirm={handleClear}>
<Button size="sm">{t('Clear')}</Button> <Button size="sm">{t('Clear')}</Button>
</AlertConfirm> </AlertConfirm>
)}
</div> </div>
<Separator className="my-2" /> <Separator className="my-2" />

View File

@ -6,7 +6,7 @@ import {
defaultSuccessHandler, defaultSuccessHandler,
trpc, trpc,
} from '../../api/trpc'; } from '../../api/trpc';
import { useCurrentWorkspaceId } from '../../store/user'; import { useCurrentWorkspaceId, useHasAdminPermission } from '../../store/user';
import { Loading } from '../Loading'; import { Loading } from '../Loading';
import { getMonitorLink } from './provider'; import { getMonitorLink } from './provider';
import { NotFoundTip } from '../NotFoundTip'; import { NotFoundTip } from '../NotFoundTip';
@ -35,6 +35,7 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [showBadge, setShowBadge] = useState(false); const [showBadge, setShowBadge] = useState(false);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const hasAdminPermission = useHasAdminPermission();
const { const {
data: monitorInfo, data: monitorInfo,
@ -186,6 +187,7 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
</span> </span>
</div> </div>
{hasAdminPermission && (
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
type="primary" type="primary"
@ -256,6 +258,7 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
/> />
</Modal> </Modal>
</div> </div>
)}
</Space> </Space>
<div className="text-black text-opacity-75 dark:text-gray-200"> <div className="text-black text-opacity-75 dark:text-gray-200">
@ -298,6 +301,7 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
<MonitorDataChart monitorId={monitorId} /> <MonitorDataChart monitorId={monitorId} />
</Card> </Card>
{hasAdminPermission && (
<div className="text-right"> <div className="text-right">
<Dropdown <Dropdown
trigger={['click']} trigger={['click']}
@ -321,6 +325,7 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
</Button> </Button>
</Dropdown> </Dropdown>
</div> </div>
)}
<MonitorEventList monitorId={monitorId} /> <MonitorEventList monitorId={monitorId} />
</Space> </Space>

View File

@ -15,5 +15,5 @@ export function useAllowEdit(workspaceId?: string): boolean {
} }
); );
return role === ROLES.owner; return role === ROLES.owner || role === ROLES.admin;
} }

View File

@ -20,13 +20,14 @@ import {
SheetTrigger, SheetTrigger,
} from '../ui/sheet'; } from '../ui/sheet';
import { defaultErrorHandler, defaultSuccessHandler, trpc } from '@/api/trpc'; import { defaultErrorHandler, defaultSuccessHandler, trpc } from '@/api/trpc';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId, useHasPermission } from '@/store/user';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { Input } from '../ui/input'; import { Input } from '../ui/input';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useEvent } from '@/hooks/useEvent'; import { useEvent } from '@/hooks/useEvent';
import { Badge } from '../ui/badge'; import { Badge } from '../ui/badge';
import { LuArrowRight, LuPlus } from 'react-icons/lu'; import { LuArrowRight, LuPlus } from 'react-icons/lu';
import { ROLES } from '@tianji/shared';
interface WebsiteLighthouseBtnProps { interface WebsiteLighthouseBtnProps {
websiteId: string; websiteId: string;
@ -37,6 +38,7 @@ export const WebsiteLighthouseBtn: React.FC<WebsiteLighthouseBtnProps> =
const { t } = useTranslation(); const { t } = useTranslation();
const [url, setUrl] = useState(''); const [url, setUrl] = useState('');
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const hasAdminPermission = useHasPermission(ROLES.admin);
const { data, hasNextPage, fetchNextPage, isFetchingNextPage, refetch } = const { data, hasNextPage, fetchNextPage, isFetchingNextPage, refetch } =
trpc.website.getLighthouseReport.useInfiniteQuery( trpc.website.getLighthouseReport.useInfiniteQuery(
@ -92,6 +94,7 @@ export const WebsiteLighthouseBtn: React.FC<WebsiteLighthouseBtnProps> =
</SheetHeader> </SheetHeader>
<div className="mt-2 flex flex-col gap-2"> <div className="mt-2 flex flex-col gap-2">
{hasAdminPermission && (
<div> <div>
<Dialog <Dialog
open={isCreateDialogOpen} open={isCreateDialogOpen}
@ -108,7 +111,9 @@ export const WebsiteLighthouseBtn: React.FC<WebsiteLighthouseBtnProps> =
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>{t('Generate Lighthouse Report')}</DialogTitle> <DialogTitle>
{t('Generate Lighthouse Report')}
</DialogTitle>
<DialogDescription> <DialogDescription>
{t('Its will take a while to generate the report.')} {t('Its will take a while to generate the report.')}
</DialogDescription> </DialogDescription>
@ -133,6 +138,7 @@ export const WebsiteLighthouseBtn: React.FC<WebsiteLighthouseBtnProps> =
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</div> </div>
)}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{allData.map((report) => { {allData.map((report) => {

View File

@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button';
import { useDataReady } from '@/hooks/useDataReady'; import { useDataReady } from '@/hooks/useDataReady';
import { useEvent } from '@/hooks/useEvent'; import { useEvent } from '@/hooks/useEvent';
import { Layout } from '@/components/layout'; import { Layout } from '@/components/layout';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId, useHasAdminPermission } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { cn } from '@/utils/style'; import { cn } from '@/utils/style';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
@ -32,6 +32,7 @@ function PageComponent() {
const pathname = useRouterState({ const pathname = useRouterState({
select: (state) => state.location.pathname, select: (state) => state.location.pathname,
}); });
const hasAdminPermission = useHasAdminPermission();
const items = channels.map((item) => ({ const items = channels.map((item) => ({
id: item.id, id: item.id,
@ -68,6 +69,8 @@ function PageComponent() {
<CommonHeader <CommonHeader
title={t('Feed')} title={t('Feed')}
actions={ actions={
<>
{hasAdminPermission && (
<Button <Button
className={cn(pathname === '/feed/add' && '!bg-muted')} className={cn(pathname === '/feed/add' && '!bg-muted')}
variant="outline" variant="outline"
@ -76,6 +79,8 @@ function PageComponent() {
> >
{t('Add')} {t('Add')}
</Button> </Button>
)}
</>
} }
/> />
} }

View File

@ -6,7 +6,7 @@ import {
} from '@/api/trpc'; } from '@/api/trpc';
import { CommonHeader } from '@/components/CommonHeader'; import { CommonHeader } from '@/components/CommonHeader';
import { CommonWrapper } from '@/components/CommonWrapper'; import { CommonWrapper } from '@/components/CommonWrapper';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId, useHasAdminPermission } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { createFileRoute, useNavigate } from '@tanstack/react-router';
@ -46,6 +46,7 @@ function PageComponent() {
workspaceId, workspaceId,
channelId, channelId,
}); });
const hasAdminPermission = useHasAdminPermission();
const { const {
data, data,
@ -53,7 +54,6 @@ function PageComponent() {
hasNextPage, hasNextPage,
fetchNextPage, fetchNextPage,
isFetchingNextPage, isFetchingNextPage,
refetch,
} = trpc.feed.fetchEventsByCursor.useInfiniteQuery( } = trpc.feed.fetchEventsByCursor.useInfiniteQuery(
{ {
workspaceId, workspaceId,
@ -122,6 +122,7 @@ function PageComponent() {
<FeedArchivePageButton channelId={channelId} /> <FeedArchivePageButton channelId={channelId} />
{hasAdminPermission && (
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
@ -135,7 +136,9 @@ function PageComponent() {
}) })
} }
/> />
)}
{hasAdminPermission && (
<AlertConfirm <AlertConfirm
title={t('Confirm to delete this channel?')} title={t('Confirm to delete this channel?')}
description={t('All feed will be remove')} description={t('All feed will be remove')}
@ -144,6 +147,7 @@ function PageComponent() {
> >
<Button variant="outline" size="icon" Icon={LuTrash} /> <Button variant="outline" size="icon" Icon={LuTrash} />
</AlertConfirm> </AlertConfirm>
)}
</div> </div>
} }
/> />
@ -174,6 +178,7 @@ function PageComponent() {
</Button> </Button>
)} )}
{hasAdminPermission && (
<Button <Button
size="icon" size="icon"
variant="secondary" variant="secondary"
@ -182,6 +187,7 @@ function PageComponent() {
> >
<LuArchive size={12} /> <LuArchive size={12} />
</Button> </Button>
)}
</> </>
} }
/> />

View File

@ -7,7 +7,7 @@ import { Button } from '@/components/ui/button';
import { useDataReady } from '@/hooks/useDataReady'; import { useDataReady } from '@/hooks/useDataReady';
import { useEvent } from '@/hooks/useEvent'; import { useEvent } from '@/hooks/useEvent';
import { Layout } from '@/components/layout'; import { Layout } from '@/components/layout';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId, useHasAdminPermission } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { cn } from '@/utils/style'; import { cn } from '@/utils/style';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
@ -34,6 +34,7 @@ function MonitorComponent() {
const pathname = useRouterState({ const pathname = useRouterState({
select: (state) => state.location.pathname, select: (state) => state.location.pathname,
}); });
const hasAdminPermission = useHasAdminPermission();
const items = data.map((item) => ({ const items = data.map((item) => ({
id: item.id, id: item.id,
@ -78,6 +79,8 @@ function MonitorComponent() {
<CommonHeader <CommonHeader
title={t('Monitor')} title={t('Monitor')}
actions={ actions={
<>
{hasAdminPermission && (
<Button <Button
className={cn(pathname === '/monitor/add' && '!bg-muted')} className={cn(pathname === '/monitor/add' && '!bg-muted')}
variant="outline" variant="outline"
@ -86,6 +89,8 @@ function MonitorComponent() {
> >
{t('Add')} {t('Add')}
</Button> </Button>
)}
</>
} }
/> />
} }

View File

@ -13,7 +13,7 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId, useHasAdminPermission } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { createFileRoute, useNavigate } from '@tanstack/react-router';
@ -34,6 +34,7 @@ function MonitorDetailComponent() {
}); });
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const hasAdminPermission = useHasAdminPermission();
if (!monitorId) { if (!monitorId) {
return <ErrorTip />; return <ErrorTip />;
@ -53,8 +54,13 @@ function MonitorDetailComponent() {
<CommonHeader <CommonHeader
title={monitor.name} title={monitor.name}
actions={ actions={
<>
{hasAdminPermission && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild={true} className="cursor-pointer"> <DropdownMenuTrigger
asChild={true}
className="cursor-pointer"
>
<Button variant="outline" size="icon" className="shrink-0"> <Button variant="outline" size="icon" className="shrink-0">
<LuMoreVertical /> <LuMoreVertical />
</Button> </Button>
@ -82,6 +88,8 @@ function MonitorDetailComponent() {
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)}
</>
} }
/> />
} }

View File

@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button';
import { useDataReady } from '@/hooks/useDataReady'; import { useDataReady } from '@/hooks/useDataReady';
import { useEvent } from '@/hooks/useEvent'; import { useEvent } from '@/hooks/useEvent';
import { Layout } from '@/components/layout'; import { Layout } from '@/components/layout';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId, useHasAdminPermission } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { cn } from '@/utils/style'; import { cn } from '@/utils/style';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
@ -32,6 +32,7 @@ function PageComponent() {
const pathname = useRouterState({ const pathname = useRouterState({
select: (state) => state.location.pathname, select: (state) => state.location.pathname,
}); });
const hasAdminPermission = useHasAdminPermission();
const items = data.map((item) => ({ const items = data.map((item) => ({
id: item.id, id: item.id,
@ -68,6 +69,8 @@ function PageComponent() {
<CommonHeader <CommonHeader
title={t('Pages')} title={t('Pages')}
actions={ actions={
<>
{hasAdminPermission && (
<Button <Button
className={cn(pathname === '/page/add' && '!bg-muted')} className={cn(pathname === '/page/add' && '!bg-muted')}
variant="outline" variant="outline"
@ -76,6 +79,8 @@ function PageComponent() {
> >
{t('Add')} {t('Add')}
</Button> </Button>
)}
</>
} }
/> />
} }

View File

@ -8,6 +8,7 @@ import { NotFoundTip } from '@/components/NotFoundTip';
import { MonitorStatusPage } from '@/components/monitor/StatusPage'; import { MonitorStatusPage } from '@/components/monitor/StatusPage';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useEvent } from '@/hooks/useEvent'; import { useEvent } from '@/hooks/useEvent';
import { useHasAdminPermission } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { Link, createFileRoute, useNavigate } from '@tanstack/react-router'; import { Link, createFileRoute, useNavigate } from '@tanstack/react-router';
@ -26,6 +27,7 @@ function PageComponent() {
slug, slug,
}); });
const trpcUtils = trpc.useUtils(); const trpcUtils = trpc.useUtils();
const hasAdminPermission = useHasAdminPermission();
const deletePageMutation = trpc.monitor.deletePage.useMutation(); const deletePageMutation = trpc.monitor.deletePage.useMutation();
@ -68,6 +70,7 @@ function PageComponent() {
title={pageInfo.title} title={pageInfo.title}
actions={ actions={
<div className="space-x-2"> <div className="space-x-2">
{hasAdminPermission && (
<AlertConfirm <AlertConfirm
title={t('Confirm to delete this page?')} title={t('Confirm to delete this page?')}
content={t('It will permanently delete the relevant data')} content={t('It will permanently delete the relevant data')}
@ -75,6 +78,7 @@ function PageComponent() {
> >
<Button variant="outline" size="icon" Icon={LuTrash} /> <Button variant="outline" size="icon" Icon={LuTrash} />
</AlertConfirm> </AlertConfirm>
)}
<Link to="/status/$slug" params={{ slug }} target="_blank"> <Link to="/status/$slug" params={{ slug }} target="_blank">
<Button variant="outline" Icon={LuEye}> <Button variant="outline" Icon={LuEye}>

View File

@ -11,10 +11,11 @@ import {
NotificationInfoModal, NotificationInfoModal,
} from '../../components/modals/NotificationInfo'; } from '../../components/modals/NotificationInfo';
import { useEvent } from '../../hooks/useEvent'; import { useEvent } from '../../hooks/useEvent';
import { useCurrentWorkspaceId } from '../../store/user'; import { useCurrentWorkspaceId, useHasAdminPermission } from '../../store/user';
import { CommonHeader } from '@/components/CommonHeader'; import { CommonHeader } from '@/components/CommonHeader';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { LuFileEdit, LuPlus, LuTrash2 } from 'react-icons/lu'; import { LuFileEdit, LuPlus, LuTrash2 } from 'react-icons/lu';
import { compact } from 'lodash-es';
export const Route = createFileRoute('/settings/notifications')({ export const Route = createFileRoute('/settings/notifications')({
beforeLoad: routeAuthBeforeLoad, beforeLoad: routeAuthBeforeLoad,
@ -31,6 +32,7 @@ function PageComponent() {
const [editingFormData, setEditingFormData] = useState< const [editingFormData, setEditingFormData] = useState<
NotificationFormValues | undefined NotificationFormValues | undefined
>(undefined); >(undefined);
const hasAdminPermission = useHasAdminPermission();
const upsertMutation = trpc.notification.upsert.useMutation(); const upsertMutation = trpc.notification.upsert.useMutation();
const deleteMutation = trpc.notification.delete.useMutation(); const deleteMutation = trpc.notification.delete.useMutation();
@ -69,6 +71,7 @@ function PageComponent() {
title={t('Notifications')} title={t('Notifications')}
actions={ actions={
<> <>
{hasAdminPermission && (
<Button <Button
variant="outline" variant="outline"
Icon={LuPlus} Icon={LuPlus}
@ -76,6 +79,7 @@ function PageComponent() {
> >
{t('New')} {t('New')}
</Button> </Button>
)}
</> </>
} }
/> />
@ -88,7 +92,8 @@ function PageComponent() {
dataSource={list} dataSource={list}
renderItem={(item) => ( renderItem={(item) => (
<List.Item <List.Item
actions={[ actions={compact([
hasAdminPermission && (
<Button <Button
variant="default" variant="default"
Icon={LuFileEdit} Icon={LuFileEdit}
@ -102,7 +107,9 @@ function PageComponent() {
}} }}
> >
{t('Edit')} {t('Edit')}
</Button>, </Button>
),
hasAdminPermission && (
<Popconfirm <Popconfirm
title={t('Is delete this item?')} title={t('Is delete this item?')}
okButtonProps={{ okButtonProps={{
@ -115,8 +122,9 @@ function PageComponent() {
<Button variant="destructive" size="icon"> <Button variant="destructive" size="icon">
<LuTrash2 /> <LuTrash2 />
</Button> </Button>
</Popconfirm>, </Popconfirm>
]} ),
])}
> >
<List.Item.Meta title={item.name} /> <List.Item.Meta title={item.name} />
</List.Item> </List.Item>

View File

@ -3,7 +3,7 @@ import { createFileRoute } from '@tanstack/react-router';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { CommonWrapper } from '@/components/CommonWrapper'; import { CommonWrapper } from '@/components/CommonWrapper';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { useCurrentWorkspace } from '../../store/user'; import { useCurrentWorkspace, useHasAdminPermission } from '../../store/user';
import { CommonHeader } from '@/components/CommonHeader'; import { CommonHeader } from '@/components/CommonHeader';
import { import {
Card, Card,
@ -39,6 +39,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod'; import { z } from 'zod';
import { AlertConfirm } from '@/components/AlertConfirm'; import { AlertConfirm } from '@/components/AlertConfirm';
import { ROLES } from '@tianji/shared'; import { ROLES } from '@tianji/shared';
import { cn } from '@/utils/style';
export const Route = createFileRoute('/settings/workspace')({ export const Route = createFileRoute('/settings/workspace')({
beforeLoad: routeAuthBeforeLoad, beforeLoad: routeAuthBeforeLoad,
@ -57,6 +58,7 @@ const columnHelper = createColumnHelper<MemberInfo>();
function PageComponent() { function PageComponent() {
const { t } = useTranslation(); const { t } = useTranslation();
const { id: workspaceId, name, role } = useCurrentWorkspace(); const { id: workspaceId, name, role } = useCurrentWorkspace();
const hasAdminPermission = useHasAdminPermission();
const { data: members = [], refetch: refetchMembers } = const { data: members = [], refetch: refetchMembers } =
trpc.workspace.members.useQuery({ trpc.workspace.members.useQuery({
workspaceId, workspaceId,
@ -128,6 +130,10 @@ function PageComponent() {
{t('Current Workspace:')} {name} {t('Current Workspace:')} {name}
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div>
<span className="mr-2">{t('Current Role')}:</span>
<span className="font-semibold">{role}</span>
</div>
<div> <div>
<span className="mr-2">{t('Workspace ID')}:</span> <span className="mr-2">{t('Workspace ID')}:</span>
<span> <span>
@ -140,7 +146,10 @@ function PageComponent() {
</Card> </Card>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(handleInvite)}> <form
onSubmit={form.handleSubmit(handleInvite)}
className={cn(!hasAdminPermission && 'opacity-50')}
>
<Card> <Card>
<CardHeader className="text-lg font-bold"> <CardHeader className="text-lg font-bold">
{t('Invite new members by email address')} {t('Invite new members by email address')}
@ -166,7 +175,11 @@ function PageComponent() {
</CardContent> </CardContent>
<CardFooter> <CardFooter>
<Button type="submit" loading={isLoading}> <Button
type="submit"
loading={isLoading}
disabled={!hasAdminPermission}
>
{t('Invite')} {t('Invite')}
</Button> </Button>
</CardFooter> </CardFooter>

View File

@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button';
import { useDataReady } from '@/hooks/useDataReady'; import { useDataReady } from '@/hooks/useDataReady';
import { useEvent } from '@/hooks/useEvent'; import { useEvent } from '@/hooks/useEvent';
import { Layout } from '@/components/layout'; import { Layout } from '@/components/layout';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId, useHasAdminPermission } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { cn } from '@/utils/style'; import { cn } from '@/utils/style';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
@ -35,6 +35,7 @@ function PageComponent() {
const pathname = useRouterState({ const pathname = useRouterState({
select: (state) => state.location.pathname, select: (state) => state.location.pathname,
}); });
const hasAdminPermission = useHasAdminPermission();
const items = data.map((item) => ({ const items = data.map((item) => ({
id: item.id, id: item.id,
@ -71,6 +72,8 @@ function PageComponent() {
<CommonHeader <CommonHeader
title={t('Survey')} title={t('Survey')}
actions={ actions={
<>
{hasAdminPermission && (
<Button <Button
className={cn(pathname === '/survey/add' && '!bg-muted')} className={cn(pathname === '/survey/add' && '!bg-muted')}
variant="outline" variant="outline"
@ -79,6 +82,8 @@ function PageComponent() {
> >
{t('Add')} {t('Add')}
</Button> </Button>
)}
</>
} }
/> />
} }

View File

@ -6,8 +6,7 @@ import {
} from '@/api/trpc'; } from '@/api/trpc';
import { CommonHeader } from '@/components/CommonHeader'; import { CommonHeader } from '@/components/CommonHeader';
import { CommonWrapper } from '@/components/CommonWrapper'; import { CommonWrapper } from '@/components/CommonWrapper';
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'; import { useCurrentWorkspaceId, useHasAdminPermission } from '@/store/user';
import { useCurrentWorkspaceId } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { createFileRoute, useNavigate } from '@tanstack/react-router';
@ -16,12 +15,11 @@ import { AlertConfirm } from '@/components/AlertConfirm';
import { LuPencil, LuTrash } from 'react-icons/lu'; import { LuPencil, LuTrash } from 'react-icons/lu';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { DataTable, createColumnHelper } from '@/components/DataTable'; import { createColumnHelper } from '@/components/DataTable';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { SurveyDownloadBtn } from '@/components/survey/SurveyDownloadBtn'; import { SurveyDownloadBtn } from '@/components/survey/SurveyDownloadBtn';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { SurveyUsageBtn } from '@/components/survey/SurveyUsageBtn'; import { SurveyUsageBtn } from '@/components/survey/SurveyUsageBtn';
import { Scrollbar } from '@radix-ui/react-scroll-area';
import { VirtualizedInfiniteDataTable } from '@/components/VirtualizedInfiniteDataTable'; import { VirtualizedInfiniteDataTable } from '@/components/VirtualizedInfiniteDataTable';
import { Loading } from '@/components/Loading'; import { Loading } from '@/components/Loading';
@ -39,6 +37,7 @@ function PageComponent() {
const { surveyId } = Route.useParams<{ surveyId: string }>(); const { surveyId } = Route.useParams<{ surveyId: string }>();
const workspaceId = useCurrentWorkspaceId(); const workspaceId = useCurrentWorkspaceId();
const { t } = useTranslation(); const { t } = useTranslation();
const hasAdminPermission = useHasAdminPermission();
const { data: info } = trpc.survey.get.useQuery({ const { data: info } = trpc.survey.get.useQuery({
workspaceId, workspaceId,
surveyId, surveyId,
@ -105,6 +104,7 @@ function PageComponent() {
title={info?.name ?? ''} title={info?.name ?? ''}
actions={ actions={
<div className="space-x-2"> <div className="space-x-2">
{hasAdminPermission && (
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
@ -118,18 +118,24 @@ function PageComponent() {
}) })
} }
/> />
)}
{hasAdminPermission && (
<AlertConfirm <AlertConfirm
title={t('Confirm to delete this survey?')} title={t('Confirm to delete this survey?')}
description={t('Survey name: {{name}} | data count: {{num}}', { description={t(
'Survey name: {{name}} | data count: {{num}}',
{
name: info?.name ?? '', name: info?.name ?? '',
num: count ?? 0, num: count ?? 0,
})} }
)}
content={t('It will permanently delete the relevant data')} content={t('It will permanently delete the relevant data')}
onConfirm={handleDelete} onConfirm={handleDelete}
> >
<Button variant="outline" size="icon" Icon={LuTrash} /> <Button variant="outline" size="icon" Icon={LuTrash} />
</AlertConfirm> </AlertConfirm>
)}
</div> </div>
} }
/> />

View File

@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button';
import { useDataReady } from '@/hooks/useDataReady'; import { useDataReady } from '@/hooks/useDataReady';
import { useEvent } from '@/hooks/useEvent'; import { useEvent } from '@/hooks/useEvent';
import { Layout } from '@/components/layout'; import { Layout } from '@/components/layout';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId, useHasAdminPermission } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { cn } from '@/utils/style'; import { cn } from '@/utils/style';
import { Trans, useTranslation } from '@i18next-toolkit/react'; import { Trans, useTranslation } from '@i18next-toolkit/react';
@ -35,6 +35,7 @@ function TelemetryComponent() {
const pathname = useRouterState({ const pathname = useRouterState({
select: (state) => state.location.pathname, select: (state) => state.location.pathname,
}); });
const hasAdminPermission = useHasAdminPermission();
const items = data.map((item) => ({ const items = data.map((item) => ({
id: item.id, id: item.id,
@ -101,14 +102,20 @@ function TelemetryComponent() {
</div> </div>
} }
actions={ actions={
<>
{hasAdminPermission && (
<Button <Button
className={cn(pathname === '/telemetry/add' && '!bg-muted')} className={cn(
pathname === '/telemetry/add' && '!bg-muted'
)}
variant="outline" variant="outline"
Icon={LuPlus} Icon={LuPlus}
onClick={handleClickAdd} onClick={handleClickAdd}
> >
{t('Add')} {t('Add')}
</Button> </Button>
)}
</>
} }
/> />
} }

View File

@ -5,7 +5,7 @@ import { CommonWrapper } from '@/components/CommonWrapper';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useEvent } from '@/hooks/useEvent'; import { useEvent } from '@/hooks/useEvent';
import { Layout } from '@/components/layout'; import { Layout } from '@/components/layout';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId, useHasAdminPermission } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { cn } from '@/utils/style'; import { cn } from '@/utils/style';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
@ -24,6 +24,7 @@ export const Route = createFileRoute('/website')({
function WebsiteComponent() { function WebsiteComponent() {
const workspaceId = useCurrentWorkspaceId(); const workspaceId = useCurrentWorkspaceId();
const hasAdminPermission = useHasAdminPermission();
const { t } = useTranslation(); const { t } = useTranslation();
const { data = [], isLoading } = trpc.website.all.useQuery({ const { data = [], isLoading } = trpc.website.all.useQuery({
workspaceId, workspaceId,
@ -83,6 +84,8 @@ function WebsiteComponent() {
> >
{t('Overview')} {t('Overview')}
</Button> </Button>
{hasAdminPermission && (
<Button <Button
className={cn(pathname === '/website/add' && '!bg-muted')} className={cn(pathname === '/website/add' && '!bg-muted')}
variant="outline" variant="outline"
@ -90,6 +93,7 @@ function WebsiteComponent() {
Icon={LuPlus} Icon={LuPlus}
onClick={handleClickAdd} onClick={handleClickAdd}
/> />
)}
</div> </div>
} }
/> />

View File

@ -12,7 +12,11 @@ import { WebsiteMetricsTable } from '@/components/website/WebsiteMetricsTable';
import { WebsiteOverview } from '@/components/website/WebsiteOverview'; import { WebsiteOverview } from '@/components/website/WebsiteOverview';
import { WebsiteVisitorMapBtn } from '@/components/website/WebsiteVisitorMapBtn'; import { WebsiteVisitorMapBtn } from '@/components/website/WebsiteVisitorMapBtn';
import { useGlobalRangeDate } from '@/hooks/useGlobalRangeDate'; import { useGlobalRangeDate } from '@/hooks/useGlobalRangeDate';
import { useCurrentWorkspaceId } from '@/store/user'; import {
useCurrentWorkspaceId,
useHasAdminPermission,
useHasPermission,
} from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { createFileRoute, useNavigate } from '@tanstack/react-router';
@ -34,6 +38,7 @@ function WebsiteDetailComponent() {
}); });
const { startDate, endDate } = useGlobalRangeDate(); const { startDate, endDate } = useGlobalRangeDate();
const navigate = useNavigate(); const navigate = useNavigate();
const hasAdminPermission = useHasAdminPermission();
if (!websiteId) { if (!websiteId) {
return <ErrorTip />; return <ErrorTip />;
@ -57,6 +62,7 @@ function WebsiteDetailComponent() {
title={website.name} title={website.name}
actions={ actions={
<div className="space-x-2"> <div className="space-x-2">
{hasAdminPermission && (
<Button <Button
size="icon" size="icon"
variant="outline" variant="outline"
@ -71,6 +77,7 @@ function WebsiteDetailComponent() {
> >
<LuSettings /> <LuSettings />
</Button> </Button>
)}
<WebsiteLighthouseBtn websiteId={website.id} /> <WebsiteLighthouseBtn websiteId={website.id} />

View File

@ -2,6 +2,7 @@ import { shallow } from 'zustand/shallow';
import { createWithEqualityFn } from 'zustand/traditional'; import { createWithEqualityFn } from 'zustand/traditional';
import { createSocketIOClient } from '../api/socketio'; import { createSocketIOClient } from '../api/socketio';
import { AppRouterOutput } from '../api/trpc'; import { AppRouterOutput } from '../api/trpc';
import { ROLES } from '@tianji/shared';
export type UserLoginInfo = NonNullable<AppRouterOutput['user']['info']>; export type UserLoginInfo = NonNullable<AppRouterOutput['user']['info']>;
@ -108,3 +109,36 @@ export function useCurrentWorkspaceId() {
return currentWorkspaceId; return currentWorkspaceId;
} }
/**
* Direct return current workspace role
*/
export function useCurrentWorkspaceRole(): ROLES {
const workspace = useCurrentWorkspace();
return (workspace.role as ROLES) || ROLES.readOnly;
}
export function useHasPermission(role: ROLES): boolean {
const currentWorkspaceRole = useCurrentWorkspaceRole();
if (currentWorkspaceRole === ROLES.owner) {
return true;
}
if (currentWorkspaceRole === ROLES.admin && role !== ROLES.owner) {
return true;
}
if (currentWorkspaceRole === ROLES.readOnly && role === ROLES.readOnly) {
return true;
}
return false;
}
export function useHasAdminPermission(): boolean {
const hasAdminPermission = useHasPermission(ROLES.admin);
return hasAdminPermission;
}

View File

@ -5,5 +5,6 @@ export enum SYSTEM_ROLES {
export enum ROLES { export enum ROLES {
owner = 'owner', owner = 'owner',
admin = 'admin',
readOnly = 'readOnly', readOnly = 'readOnly',
} }