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

View File

@ -6,7 +6,7 @@ import {
defaultSuccessHandler,
trpc,
} from '../../api/trpc';
import { useCurrentWorkspaceId } from '../../store/user';
import { useCurrentWorkspaceId, useHasAdminPermission } from '../../store/user';
import { Loading } from '../Loading';
import { getMonitorLink } from './provider';
import { NotFoundTip } from '../NotFoundTip';
@ -35,6 +35,7 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
const navigate = useNavigate();
const [showBadge, setShowBadge] = useState(false);
const isMobile = useIsMobile();
const hasAdminPermission = useHasAdminPermission();
const {
data: monitorInfo,
@ -186,76 +187,78 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
</span>
</div>
<div className="flex gap-2">
<Button
type="primary"
onClick={() => {
navigate({
to: '/monitor/$monitorId/edit',
params: {
monitorId: monitorInfo.id,
},
});
}}
>
{t('Edit')}
</Button>
{monitorInfo.active ? (
{hasAdminPermission && (
<div className="flex gap-2">
<Button
loading={changeActiveMutation.isLoading}
onClick={handleStop}
type="primary"
onClick={() => {
navigate({
to: '/monitor/$monitorId/edit',
params: {
monitorId: monitorInfo.id,
},
});
}}
>
{t('Stop')}
{t('Edit')}
</Button>
) : (
<Button
loading={changeActiveMutation.isLoading}
onClick={handleStart}
{monitorInfo.active ? (
<Button
loading={changeActiveMutation.isLoading}
onClick={handleStop}
>
{t('Stop')}
</Button>
) : (
<Button
loading={changeActiveMutation.isLoading}
onClick={handleStart}
>
{t('Start')}
</Button>
)}
<Dropdown
trigger={['click']}
placement="bottomRight"
menu={{
items: [
{
key: 'badge',
label: t('Show Badge'),
onClick: () => setShowBadge(true),
},
{
type: 'divider',
},
{
key: 'delete',
label: t('Delete'),
danger: true,
onClick: handleDelete,
},
],
}}
>
{t('Start')}
</Button>
)}
<Button icon={<MoreOutlined />} />
</Dropdown>
<Dropdown
trigger={['click']}
placement="bottomRight"
menu={{
items: [
{
key: 'badge',
label: t('Show Badge'),
onClick: () => setShowBadge(true),
},
{
type: 'divider',
},
{
key: 'delete',
label: t('Delete'),
danger: true,
onClick: handleDelete,
},
],
}}
>
<Button icon={<MoreOutlined />} />
</Dropdown>
<Modal
open={showBadge}
onCancel={() => setShowBadge(false)}
onOk={() => setShowBadge(false)}
destroyOnClose={true}
centered={true}
>
<MonitorBadgeView
workspaceId={workspaceId}
monitorId={monitorId}
monitorName={monitorInfo.name}
/>
</Modal>
</div>
<Modal
open={showBadge}
onCancel={() => setShowBadge(false)}
onOk={() => setShowBadge(false)}
destroyOnClose={true}
centered={true}
>
<MonitorBadgeView
workspaceId={workspaceId}
monitorId={monitorId}
monitorName={monitorInfo.name}
/>
</Modal>
</div>
)}
</Space>
<div className="text-black text-opacity-75 dark:text-gray-200">
@ -298,29 +301,31 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
<MonitorDataChart monitorId={monitorId} />
</Card>
<div className="text-right">
<Dropdown
trigger={['click']}
menu={{
items: [
{
key: 'events',
label: t('Events'),
onClick: handleClearEvents,
},
{
key: 'heartbeats',
label: t('Heartbeats'),
onClick: handleClearData,
},
],
}}
>
<Button icon={<DeleteOutlined />} danger={true}>
{t('Clear Data')}
</Button>
</Dropdown>
</div>
{hasAdminPermission && (
<div className="text-right">
<Dropdown
trigger={['click']}
menu={{
items: [
{
key: 'events',
label: t('Events'),
onClick: handleClearEvents,
},
{
key: 'heartbeats',
label: t('Heartbeats'),
onClick: handleClearData,
},
],
}}
>
<Button icon={<DeleteOutlined />} danger={true}>
{t('Clear Data')}
</Button>
</Dropdown>
</div>
)}
<MonitorEventList monitorId={monitorId} />
</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,
} from '../ui/sheet';
import { defaultErrorHandler, defaultSuccessHandler, trpc } from '@/api/trpc';
import { useCurrentWorkspaceId } from '@/store/user';
import { useCurrentWorkspaceId, useHasPermission } from '@/store/user';
import { formatDate } from '@/utils/date';
import { Input } from '../ui/input';
import { toast } from 'sonner';
import { useEvent } from '@/hooks/useEvent';
import { Badge } from '../ui/badge';
import { LuArrowRight, LuPlus } from 'react-icons/lu';
import { ROLES } from '@tianji/shared';
interface WebsiteLighthouseBtnProps {
websiteId: string;
@ -37,6 +38,7 @@ export const WebsiteLighthouseBtn: React.FC<WebsiteLighthouseBtnProps> =
const { t } = useTranslation();
const [url, setUrl] = useState('');
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const hasAdminPermission = useHasPermission(ROLES.admin);
const { data, hasNextPage, fetchNextPage, isFetchingNextPage, refetch } =
trpc.website.getLighthouseReport.useInfiniteQuery(
@ -92,47 +94,51 @@ export const WebsiteLighthouseBtn: React.FC<WebsiteLighthouseBtnProps> =
</SheetHeader>
<div className="mt-2 flex flex-col gap-2">
<div>
<Dialog
open={isCreateDialogOpen}
onOpenChange={setIsCreateDialogOpen}
>
<DialogTrigger>
<Button
variant="outline"
loading={createMutation.isLoading}
Icon={LuPlus}
>
{t('Create Report')}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('Generate Lighthouse Report')}</DialogTitle>
<DialogDescription>
{t('Its will take a while to generate the report.')}
</DialogDescription>
</DialogHeader>
<div>
<Input
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://google.com"
/>
</div>
<DialogFooter>
{hasAdminPermission && (
<div>
<Dialog
open={isCreateDialogOpen}
onOpenChange={setIsCreateDialogOpen}
>
<DialogTrigger>
<Button
variant="outline"
loading={createMutation.isLoading}
onClick={handleGenerateReport}
Icon={LuPlus}
>
{t('Create')}
{t('Create Report')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
{t('Generate Lighthouse Report')}
</DialogTitle>
<DialogDescription>
{t('Its will take a while to generate the report.')}
</DialogDescription>
</DialogHeader>
<div>
<Input
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://google.com"
/>
</div>
<DialogFooter>
<Button
loading={createMutation.isLoading}
onClick={handleGenerateReport}
>
{t('Create')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)}
<div className="flex flex-col gap-2">
{allData.map((report) => {

View File

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

View File

@ -6,7 +6,7 @@ import {
} from '@/api/trpc';
import { CommonHeader } from '@/components/CommonHeader';
import { CommonWrapper } from '@/components/CommonWrapper';
import { useCurrentWorkspaceId } from '@/store/user';
import { useCurrentWorkspaceId, useHasAdminPermission } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route';
import { useTranslation } from '@i18next-toolkit/react';
import { createFileRoute, useNavigate } from '@tanstack/react-router';
@ -46,6 +46,7 @@ function PageComponent() {
workspaceId,
channelId,
});
const hasAdminPermission = useHasAdminPermission();
const {
data,
@ -53,7 +54,6 @@ function PageComponent() {
hasNextPage,
fetchNextPage,
isFetchingNextPage,
refetch,
} = trpc.feed.fetchEventsByCursor.useInfiniteQuery(
{
workspaceId,
@ -122,28 +122,32 @@ function PageComponent() {
<FeedArchivePageButton channelId={channelId} />
<Button
variant="outline"
size="icon"
Icon={LuPencil}
onClick={() =>
navigate({
to: '/feed/$channelId/edit',
params: {
channelId,
},
})
}
/>
{hasAdminPermission && (
<Button
variant="outline"
size="icon"
Icon={LuPencil}
onClick={() =>
navigate({
to: '/feed/$channelId/edit',
params: {
channelId,
},
})
}
/>
)}
<AlertConfirm
title={t('Confirm to delete this channel?')}
description={t('All feed will be remove')}
content={t('It will permanently delete the relevant data')}
onConfirm={handleDelete}
>
<Button variant="outline" size="icon" Icon={LuTrash} />
</AlertConfirm>
{hasAdminPermission && (
<AlertConfirm
title={t('Confirm to delete this channel?')}
description={t('All feed will be remove')}
content={t('It will permanently delete the relevant data')}
onConfirm={handleDelete}
>
<Button variant="outline" size="icon" Icon={LuTrash} />
</AlertConfirm>
)}
</div>
}
/>
@ -174,14 +178,16 @@ function PageComponent() {
</Button>
)}
<Button
size="icon"
variant="secondary"
className="h-6 w-6 overflow-hidden"
onClick={() => handleArchive(item)}
>
<LuArchive size={12} />
</Button>
{hasAdminPermission && (
<Button
size="icon"
variant="secondary"
className="h-6 w-6 overflow-hidden"
onClick={() => handleArchive(item)}
>
<LuArchive size={12} />
</Button>
)}
</>
}
/>

View File

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

View File

@ -13,7 +13,7 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useCurrentWorkspaceId } from '@/store/user';
import { useCurrentWorkspaceId, useHasAdminPermission } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route';
import { useTranslation } from '@i18next-toolkit/react';
import { createFileRoute, useNavigate } from '@tanstack/react-router';
@ -34,6 +34,7 @@ function MonitorDetailComponent() {
});
const { t } = useTranslation();
const navigate = useNavigate();
const hasAdminPermission = useHasAdminPermission();
if (!monitorId) {
return <ErrorTip />;
@ -53,35 +54,42 @@ function MonitorDetailComponent() {
<CommonHeader
title={monitor.name}
actions={
<DropdownMenu>
<DropdownMenuTrigger asChild={true} className="cursor-pointer">
<Button variant="outline" size="icon" className="shrink-0">
<LuMoreVertical />
</Button>
</DropdownMenuTrigger>
<>
{hasAdminPermission && (
<DropdownMenu>
<DropdownMenuTrigger
asChild={true}
className="cursor-pointer"
>
<Button variant="outline" size="icon" className="shrink-0">
<LuMoreVertical />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
onClick={() =>
navigate({
to: '/monitor/add',
search: pick(monitor, [
'name',
'type',
'notifications',
'interval',
'maxRetries',
'trendingMode',
'payload',
]),
})
}
>
<LuCopy className="mr-2" />
{t('Duplicate')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenuContent>
<DropdownMenuItem
onClick={() =>
navigate({
to: '/monitor/add',
search: pick(monitor, [
'name',
'type',
'notifications',
'interval',
'maxRetries',
'trendingMode',
'payload',
]),
})
}
>
<LuCopy className="mr-2" />
{t('Duplicate')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</>
}
/>
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import { shallow } from 'zustand/shallow';
import { createWithEqualityFn } from 'zustand/traditional';
import { createSocketIOClient } from '../api/socketio';
import { AppRouterOutput } from '../api/trpc';
import { ROLES } from '@tianji/shared';
export type UserLoginInfo = NonNullable<AppRouterOutput['user']['info']>;
@ -108,3 +109,36 @@ export function useCurrentWorkspaceId() {
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 {
owner = 'owner',
admin = 'admin',
readOnly = 'readOnly',
}