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 { 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>
|
||||||
|
|
||||||
<AlertConfirm onConfirm={handleClear}>
|
{hasAdminPermission && (
|
||||||
<Button size="sm">{t('Clear')}</Button>
|
<AlertConfirm onConfirm={handleClear}>
|
||||||
</AlertConfirm>
|
<Button size="sm">{t('Clear')}</Button>
|
||||||
|
</AlertConfirm>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator className="my-2" />
|
<Separator className="my-2" />
|
||||||
|
@ -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,76 +187,78 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
{hasAdminPermission && (
|
||||||
<Button
|
<div className="flex gap-2">
|
||||||
type="primary"
|
|
||||||
onClick={() => {
|
|
||||||
navigate({
|
|
||||||
to: '/monitor/$monitorId/edit',
|
|
||||||
params: {
|
|
||||||
monitorId: monitorInfo.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('Edit')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{monitorInfo.active ? (
|
|
||||||
<Button
|
<Button
|
||||||
loading={changeActiveMutation.isLoading}
|
type="primary"
|
||||||
onClick={handleStop}
|
onClick={() => {
|
||||||
|
navigate({
|
||||||
|
to: '/monitor/$monitorId/edit',
|
||||||
|
params: {
|
||||||
|
monitorId: monitorInfo.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t('Stop')}
|
{t('Edit')}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
|
||||||
<Button
|
{monitorInfo.active ? (
|
||||||
loading={changeActiveMutation.isLoading}
|
<Button
|
||||||
onClick={handleStart}
|
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 icon={<MoreOutlined />} />
|
||||||
</Button>
|
</Dropdown>
|
||||||
)}
|
|
||||||
|
|
||||||
<Dropdown
|
<Modal
|
||||||
trigger={['click']}
|
open={showBadge}
|
||||||
placement="bottomRight"
|
onCancel={() => setShowBadge(false)}
|
||||||
menu={{
|
onOk={() => setShowBadge(false)}
|
||||||
items: [
|
destroyOnClose={true}
|
||||||
{
|
centered={true}
|
||||||
key: 'badge',
|
>
|
||||||
label: t('Show Badge'),
|
<MonitorBadgeView
|
||||||
onClick: () => setShowBadge(true),
|
workspaceId={workspaceId}
|
||||||
},
|
monitorId={monitorId}
|
||||||
{
|
monitorName={monitorInfo.name}
|
||||||
type: 'divider',
|
/>
|
||||||
},
|
</Modal>
|
||||||
{
|
</div>
|
||||||
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>
|
|
||||||
</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,29 +301,31 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
|
|||||||
<MonitorDataChart monitorId={monitorId} />
|
<MonitorDataChart monitorId={monitorId} />
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="text-right">
|
{hasAdminPermission && (
|
||||||
<Dropdown
|
<div className="text-right">
|
||||||
trigger={['click']}
|
<Dropdown
|
||||||
menu={{
|
trigger={['click']}
|
||||||
items: [
|
menu={{
|
||||||
{
|
items: [
|
||||||
key: 'events',
|
{
|
||||||
label: t('Events'),
|
key: 'events',
|
||||||
onClick: handleClearEvents,
|
label: t('Events'),
|
||||||
},
|
onClick: handleClearEvents,
|
||||||
{
|
},
|
||||||
key: 'heartbeats',
|
{
|
||||||
label: t('Heartbeats'),
|
key: 'heartbeats',
|
||||||
onClick: handleClearData,
|
label: t('Heartbeats'),
|
||||||
},
|
onClick: handleClearData,
|
||||||
],
|
},
|
||||||
}}
|
],
|
||||||
>
|
}}
|
||||||
<Button icon={<DeleteOutlined />} danger={true}>
|
>
|
||||||
{t('Clear Data')}
|
<Button icon={<DeleteOutlined />} danger={true}>
|
||||||
</Button>
|
{t('Clear Data')}
|
||||||
</Dropdown>
|
</Button>
|
||||||
</div>
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<MonitorEventList monitorId={monitorId} />
|
<MonitorEventList monitorId={monitorId} />
|
||||||
</Space>
|
</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,
|
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,47 +94,51 @@ 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">
|
||||||
<div>
|
{hasAdminPermission && (
|
||||||
<Dialog
|
<div>
|
||||||
open={isCreateDialogOpen}
|
<Dialog
|
||||||
onOpenChange={setIsCreateDialogOpen}
|
open={isCreateDialogOpen}
|
||||||
>
|
onOpenChange={setIsCreateDialogOpen}
|
||||||
<DialogTrigger>
|
>
|
||||||
<Button
|
<DialogTrigger>
|
||||||
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>
|
|
||||||
<Button
|
<Button
|
||||||
|
variant="outline"
|
||||||
loading={createMutation.isLoading}
|
loading={createMutation.isLoading}
|
||||||
onClick={handleGenerateReport}
|
Icon={LuPlus}
|
||||||
>
|
>
|
||||||
{t('Create')}
|
{t('Create Report')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogTrigger>
|
||||||
</DialogContent>
|
<DialogContent>
|
||||||
</Dialog>
|
<DialogHeader>
|
||||||
</div>
|
<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">
|
<div className="flex flex-col gap-2">
|
||||||
{allData.map((report) => {
|
{allData.map((report) => {
|
||||||
|
@ -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,14 +69,18 @@ function PageComponent() {
|
|||||||
<CommonHeader
|
<CommonHeader
|
||||||
title={t('Feed')}
|
title={t('Feed')}
|
||||||
actions={
|
actions={
|
||||||
<Button
|
<>
|
||||||
className={cn(pathname === '/feed/add' && '!bg-muted')}
|
{hasAdminPermission && (
|
||||||
variant="outline"
|
<Button
|
||||||
Icon={LuPlus}
|
className={cn(pathname === '/feed/add' && '!bg-muted')}
|
||||||
onClick={handleClickAdd}
|
variant="outline"
|
||||||
>
|
Icon={LuPlus}
|
||||||
{t('Add')}
|
onClick={handleClickAdd}
|
||||||
</Button>
|
>
|
||||||
|
{t('Add')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -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,28 +122,32 @@ function PageComponent() {
|
|||||||
|
|
||||||
<FeedArchivePageButton channelId={channelId} />
|
<FeedArchivePageButton channelId={channelId} />
|
||||||
|
|
||||||
<Button
|
{hasAdminPermission && (
|
||||||
variant="outline"
|
<Button
|
||||||
size="icon"
|
variant="outline"
|
||||||
Icon={LuPencil}
|
size="icon"
|
||||||
onClick={() =>
|
Icon={LuPencil}
|
||||||
navigate({
|
onClick={() =>
|
||||||
to: '/feed/$channelId/edit',
|
navigate({
|
||||||
params: {
|
to: '/feed/$channelId/edit',
|
||||||
channelId,
|
params: {
|
||||||
},
|
channelId,
|
||||||
})
|
},
|
||||||
}
|
})
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<AlertConfirm
|
{hasAdminPermission && (
|
||||||
title={t('Confirm to delete this channel?')}
|
<AlertConfirm
|
||||||
description={t('All feed will be remove')}
|
title={t('Confirm to delete this channel?')}
|
||||||
content={t('It will permanently delete the relevant data')}
|
description={t('All feed will be remove')}
|
||||||
onConfirm={handleDelete}
|
content={t('It will permanently delete the relevant data')}
|
||||||
>
|
onConfirm={handleDelete}
|
||||||
<Button variant="outline" size="icon" Icon={LuTrash} />
|
>
|
||||||
</AlertConfirm>
|
<Button variant="outline" size="icon" Icon={LuTrash} />
|
||||||
|
</AlertConfirm>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -174,14 +178,16 @@ function PageComponent() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
{hasAdminPermission && (
|
||||||
size="icon"
|
<Button
|
||||||
variant="secondary"
|
size="icon"
|
||||||
className="h-6 w-6 overflow-hidden"
|
variant="secondary"
|
||||||
onClick={() => handleArchive(item)}
|
className="h-6 w-6 overflow-hidden"
|
||||||
>
|
onClick={() => handleArchive(item)}
|
||||||
<LuArchive size={12} />
|
>
|
||||||
</Button>
|
<LuArchive size={12} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -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,14 +79,18 @@ function MonitorComponent() {
|
|||||||
<CommonHeader
|
<CommonHeader
|
||||||
title={t('Monitor')}
|
title={t('Monitor')}
|
||||||
actions={
|
actions={
|
||||||
<Button
|
<>
|
||||||
className={cn(pathname === '/monitor/add' && '!bg-muted')}
|
{hasAdminPermission && (
|
||||||
variant="outline"
|
<Button
|
||||||
Icon={LuPlus}
|
className={cn(pathname === '/monitor/add' && '!bg-muted')}
|
||||||
onClick={handleClickAdd}
|
variant="outline"
|
||||||
>
|
Icon={LuPlus}
|
||||||
{t('Add')}
|
onClick={handleClickAdd}
|
||||||
</Button>
|
>
|
||||||
|
{t('Add')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -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,35 +54,42 @@ function MonitorDetailComponent() {
|
|||||||
<CommonHeader
|
<CommonHeader
|
||||||
title={monitor.name}
|
title={monitor.name}
|
||||||
actions={
|
actions={
|
||||||
<DropdownMenu>
|
<>
|
||||||
<DropdownMenuTrigger asChild={true} className="cursor-pointer">
|
{hasAdminPermission && (
|
||||||
<Button variant="outline" size="icon" className="shrink-0">
|
<DropdownMenu>
|
||||||
<LuMoreVertical />
|
<DropdownMenuTrigger
|
||||||
</Button>
|
asChild={true}
|
||||||
</DropdownMenuTrigger>
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<Button variant="outline" size="icon" className="shrink-0">
|
||||||
|
<LuMoreVertical />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigate({
|
navigate({
|
||||||
to: '/monitor/add',
|
to: '/monitor/add',
|
||||||
search: pick(monitor, [
|
search: pick(monitor, [
|
||||||
'name',
|
'name',
|
||||||
'type',
|
'type',
|
||||||
'notifications',
|
'notifications',
|
||||||
'interval',
|
'interval',
|
||||||
'maxRetries',
|
'maxRetries',
|
||||||
'trendingMode',
|
'trendingMode',
|
||||||
'payload',
|
'payload',
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<LuCopy className="mr-2" />
|
<LuCopy className="mr-2" />
|
||||||
{t('Duplicate')}
|
{t('Duplicate')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -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,14 +69,18 @@ function PageComponent() {
|
|||||||
<CommonHeader
|
<CommonHeader
|
||||||
title={t('Pages')}
|
title={t('Pages')}
|
||||||
actions={
|
actions={
|
||||||
<Button
|
<>
|
||||||
className={cn(pathname === '/page/add' && '!bg-muted')}
|
{hasAdminPermission && (
|
||||||
variant="outline"
|
<Button
|
||||||
Icon={LuPlus}
|
className={cn(pathname === '/page/add' && '!bg-muted')}
|
||||||
onClick={handleClickAdd}
|
variant="outline"
|
||||||
>
|
Icon={LuPlus}
|
||||||
{t('Add')}
|
onClick={handleClickAdd}
|
||||||
</Button>
|
>
|
||||||
|
{t('Add')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -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,13 +70,15 @@ function PageComponent() {
|
|||||||
title={pageInfo.title}
|
title={pageInfo.title}
|
||||||
actions={
|
actions={
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
<AlertConfirm
|
{hasAdminPermission && (
|
||||||
title={t('Confirm to delete this page?')}
|
<AlertConfirm
|
||||||
content={t('It will permanently delete the relevant data')}
|
title={t('Confirm to delete this page?')}
|
||||||
onConfirm={handleDelete}
|
content={t('It will permanently delete the relevant data')}
|
||||||
>
|
onConfirm={handleDelete}
|
||||||
<Button variant="outline" size="icon" Icon={LuTrash} />
|
>
|
||||||
</AlertConfirm>
|
<Button variant="outline" size="icon" Icon={LuTrash} />
|
||||||
|
</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}>
|
||||||
|
@ -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,13 +71,15 @@ function PageComponent() {
|
|||||||
title={t('Notifications')}
|
title={t('Notifications')}
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<Button
|
{hasAdminPermission && (
|
||||||
variant="outline"
|
<Button
|
||||||
Icon={LuPlus}
|
variant="outline"
|
||||||
onClick={() => handleOpenModal()}
|
Icon={LuPlus}
|
||||||
>
|
onClick={() => handleOpenModal()}
|
||||||
{t('New')}
|
>
|
||||||
</Button>
|
{t('New')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -88,35 +92,39 @@ function PageComponent() {
|
|||||||
dataSource={list}
|
dataSource={list}
|
||||||
renderItem={(item) => (
|
renderItem={(item) => (
|
||||||
<List.Item
|
<List.Item
|
||||||
actions={[
|
actions={compact([
|
||||||
<Button
|
hasAdminPermission && (
|
||||||
variant="default"
|
<Button
|
||||||
Icon={LuFileEdit}
|
variant="default"
|
||||||
onClick={() => {
|
Icon={LuFileEdit}
|
||||||
handleOpenModal({
|
onClick={() => {
|
||||||
id: item.id,
|
handleOpenModal({
|
||||||
name: item.name,
|
id: item.id,
|
||||||
type: item.type,
|
name: item.name,
|
||||||
payload: item.payload as Record<string, any>,
|
type: item.type,
|
||||||
});
|
payload: item.payload as Record<string, any>,
|
||||||
}}
|
});
|
||||||
>
|
}}
|
||||||
{t('Edit')}
|
>
|
||||||
</Button>,
|
{t('Edit')}
|
||||||
<Popconfirm
|
|
||||||
title={t('Is delete this item?')}
|
|
||||||
okButtonProps={{
|
|
||||||
danger: true,
|
|
||||||
}}
|
|
||||||
onConfirm={() => {
|
|
||||||
handleDelete(item.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="destructive" size="icon">
|
|
||||||
<LuTrash2 />
|
|
||||||
</Button>
|
</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.Meta title={item.name} />
|
||||||
</List.Item>
|
</List.Item>
|
||||||
|
@ -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>
|
||||||
|
@ -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,14 +72,18 @@ function PageComponent() {
|
|||||||
<CommonHeader
|
<CommonHeader
|
||||||
title={t('Survey')}
|
title={t('Survey')}
|
||||||
actions={
|
actions={
|
||||||
<Button
|
<>
|
||||||
className={cn(pathname === '/survey/add' && '!bg-muted')}
|
{hasAdminPermission && (
|
||||||
variant="outline"
|
<Button
|
||||||
Icon={LuPlus}
|
className={cn(pathname === '/survey/add' && '!bg-muted')}
|
||||||
onClick={handleClickAdd}
|
variant="outline"
|
||||||
>
|
Icon={LuPlus}
|
||||||
{t('Add')}
|
onClick={handleClickAdd}
|
||||||
</Button>
|
>
|
||||||
|
{t('Add')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -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,31 +104,38 @@ function PageComponent() {
|
|||||||
title={info?.name ?? ''}
|
title={info?.name ?? ''}
|
||||||
actions={
|
actions={
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
<Button
|
{hasAdminPermission && (
|
||||||
variant="outline"
|
<Button
|
||||||
size="icon"
|
variant="outline"
|
||||||
Icon={LuPencil}
|
size="icon"
|
||||||
onClick={() =>
|
Icon={LuPencil}
|
||||||
navigate({
|
onClick={() =>
|
||||||
to: '/survey/$surveyId/edit',
|
navigate({
|
||||||
params: {
|
to: '/survey/$surveyId/edit',
|
||||||
surveyId,
|
params: {
|
||||||
},
|
surveyId,
|
||||||
})
|
},
|
||||||
}
|
})
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<AlertConfirm
|
{hasAdminPermission && (
|
||||||
title={t('Confirm to delete this survey?')}
|
<AlertConfirm
|
||||||
description={t('Survey name: {{name}} | data count: {{num}}', {
|
title={t('Confirm to delete this survey?')}
|
||||||
name: info?.name ?? '',
|
description={t(
|
||||||
num: count ?? 0,
|
'Survey name: {{name}} | data count: {{num}}',
|
||||||
})}
|
{
|
||||||
content={t('It will permanently delete the relevant data')}
|
name: info?.name ?? '',
|
||||||
onConfirm={handleDelete}
|
num: count ?? 0,
|
||||||
>
|
}
|
||||||
<Button variant="outline" size="icon" Icon={LuTrash} />
|
)}
|
||||||
</AlertConfirm>
|
content={t('It will permanently delete the relevant data')}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
>
|
||||||
|
<Button variant="outline" size="icon" Icon={LuTrash} />
|
||||||
|
</AlertConfirm>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -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={
|
||||||
<Button
|
<>
|
||||||
className={cn(pathname === '/telemetry/add' && '!bg-muted')}
|
{hasAdminPermission && (
|
||||||
variant="outline"
|
<Button
|
||||||
Icon={LuPlus}
|
className={cn(
|
||||||
onClick={handleClickAdd}
|
pathname === '/telemetry/add' && '!bg-muted'
|
||||||
>
|
)}
|
||||||
{t('Add')}
|
variant="outline"
|
||||||
</Button>
|
Icon={LuPlus}
|
||||||
|
onClick={handleClickAdd}
|
||||||
|
>
|
||||||
|
{t('Add')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -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,13 +84,16 @@ function WebsiteComponent() {
|
|||||||
>
|
>
|
||||||
{t('Overview')}
|
{t('Overview')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
className={cn(pathname === '/website/add' && '!bg-muted')}
|
{hasAdminPermission && (
|
||||||
variant="outline"
|
<Button
|
||||||
size="icon"
|
className={cn(pathname === '/website/add' && '!bg-muted')}
|
||||||
Icon={LuPlus}
|
variant="outline"
|
||||||
onClick={handleClickAdd}
|
size="icon"
|
||||||
/>
|
Icon={LuPlus}
|
||||||
|
onClick={handleClickAdd}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -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,20 +62,22 @@ function WebsiteDetailComponent() {
|
|||||||
title={website.name}
|
title={website.name}
|
||||||
actions={
|
actions={
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
<Button
|
{hasAdminPermission && (
|
||||||
size="icon"
|
<Button
|
||||||
variant="outline"
|
size="icon"
|
||||||
onClick={() =>
|
variant="outline"
|
||||||
navigate({
|
onClick={() =>
|
||||||
to: '/website/$websiteId/config',
|
navigate({
|
||||||
params: {
|
to: '/website/$websiteId/config',
|
||||||
websiteId,
|
params: {
|
||||||
},
|
websiteId,
|
||||||
})
|
},
|
||||||
}
|
})
|
||||||
>
|
}
|
||||||
<LuSettings />
|
>
|
||||||
</Button>
|
<LuSettings />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<WebsiteLighthouseBtn websiteId={website.id} />
|
<WebsiteLighthouseBtn websiteId={website.id} />
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -5,5 +5,6 @@ export enum SYSTEM_ROLES {
|
|||||||
|
|
||||||
export enum ROLES {
|
export enum ROLES {
|
||||||
owner = 'owner',
|
owner = 'owner',
|
||||||
|
admin = 'admin',
|
||||||
readOnly = 'readOnly',
|
readOnly = 'readOnly',
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user