feat: add workspace role permission check, hide non permission action
This commit is contained in:
parent
d29785a311
commit
4f4f9b5d3f
@ -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" />
|
||||
|
@ -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>
|
||||
|
@ -15,5 +15,5 @@ export function useAllowEdit(workspaceId?: string): boolean {
|
||||
}
|
||||
);
|
||||
|
||||
return role === ROLES.owner;
|
||||
return role === ROLES.owner || role === ROLES.admin;
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
@ -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}>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
||||
/>
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
||||
/>
|
||||
|
@ -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} />
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -5,5 +5,6 @@ export enum SYSTEM_ROLES {
|
||||
|
||||
export enum ROLES {
|
||||
owner = 'owner',
|
||||
admin = 'admin',
|
||||
readOnly = 'readOnly',
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user