diff --git a/src/client/components/feed/FeedArchivePageButton.tsx b/src/client/components/feed/FeedArchivePageButton.tsx index 50f8131..6aa9572 100644 --- a/src/client/components/feed/FeedArchivePageButton.tsx +++ b/src/client/components/feed/FeedArchivePageButton.tsx @@ -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 = const clearAllArchivedEventsMutation = trpc.feed.clearAllArchivedEvents.useMutation(); const trpcUtils = trpc.useUtils(); + const hasAdminPermission = useHasAdminPermission(); const { data, @@ -87,9 +88,11 @@ export const FeedArchivePageButton: React.FC =

{t('Archived Events')}

- - - + {hasAdminPermission && ( + + + + )}
diff --git a/src/client/components/monitor/MonitorInfo.tsx b/src/client/components/monitor/MonitorInfo.tsx index bc816e5..d5c68ff 100644 --- a/src/client/components/monitor/MonitorInfo.tsx +++ b/src/client/components/monitor/MonitorInfo.tsx @@ -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 = 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 = React.memo((props) => { -
- - - {monitorInfo.active ? ( + {hasAdminPermission && ( +
- ) : ( - + ) : ( + + )} + + setShowBadge(true), + }, + { + type: 'divider', + }, + { + key: 'delete', + label: t('Delete'), + danger: true, + onClick: handleDelete, + }, + ], + }} > - {t('Start')} - - )} +
+ setShowBadge(false)} + onOk={() => setShowBadge(false)} + destroyOnClose={true} + centered={true} + > + + +
+ )}
@@ -298,29 +301,31 @@ export const MonitorInfo: React.FC = React.memo((props) => { -
- - - -
+ {hasAdminPermission && ( +
+ + + +
+ )} diff --git a/src/client/components/monitor/StatusPage/useAllowEdit.ts b/src/client/components/monitor/StatusPage/useAllowEdit.ts index 9d87294..66b2592 100644 --- a/src/client/components/monitor/StatusPage/useAllowEdit.ts +++ b/src/client/components/monitor/StatusPage/useAllowEdit.ts @@ -15,5 +15,5 @@ export function useAllowEdit(workspaceId?: string): boolean { } ); - return role === ROLES.owner; + return role === ROLES.owner || role === ROLES.admin; } diff --git a/src/client/components/website/WebsiteLighthouseBtn.tsx b/src/client/components/website/WebsiteLighthouseBtn.tsx index 219a3bf..e88e629 100644 --- a/src/client/components/website/WebsiteLighthouseBtn.tsx +++ b/src/client/components/website/WebsiteLighthouseBtn.tsx @@ -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 = 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 =
-
- - - - - - - {t('Generate Lighthouse Report')} - - {t('Its will take a while to generate the report.')} - - - -
- setUrl(e.target.value)} - placeholder="https://google.com" - /> -
- - + {hasAdminPermission && ( +
+ + - - - -
+ + + + + {t('Generate Lighthouse Report')} + + + {t('Its will take a while to generate the report.')} + + + +
+ setUrl(e.target.value)} + placeholder="https://google.com" + /> +
+ + + + +
+
+
+ )}
{allData.map((report) => { diff --git a/src/client/routes/feed.tsx b/src/client/routes/feed.tsx index 17fa967..f66baca 100644 --- a/src/client/routes/feed.tsx +++ b/src/client/routes/feed.tsx @@ -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() { - {t('Add')} - + <> + {hasAdminPermission && ( + + )} + } /> } diff --git a/src/client/routes/feed/$channelId/index.tsx b/src/client/routes/feed/$channelId/index.tsx index c8b32b9..a29331b 100644 --- a/src/client/routes/feed/$channelId/index.tsx +++ b/src/client/routes/feed/$channelId/index.tsx @@ -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() { -
} /> @@ -174,14 +178,16 @@ function PageComponent() { )} - + {hasAdminPermission && ( + + )} } /> diff --git a/src/client/routes/monitor.tsx b/src/client/routes/monitor.tsx index c4ff671..28c80dd 100644 --- a/src/client/routes/monitor.tsx +++ b/src/client/routes/monitor.tsx @@ -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() { - {t('Add')} - + <> + {hasAdminPermission && ( + + )} + } /> } diff --git a/src/client/routes/monitor/$monitorId/index.tsx b/src/client/routes/monitor/$monitorId/index.tsx index caf183b..060abdd 100644 --- a/src/client/routes/monitor/$monitorId/index.tsx +++ b/src/client/routes/monitor/$monitorId/index.tsx @@ -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 ; @@ -53,35 +54,42 @@ function MonitorDetailComponent() { - - - + <> + {hasAdminPermission && ( + + + + - - - navigate({ - to: '/monitor/add', - search: pick(monitor, [ - 'name', - 'type', - 'notifications', - 'interval', - 'maxRetries', - 'trendingMode', - 'payload', - ]), - }) - } - > - - {t('Duplicate')} - - - + + + navigate({ + to: '/monitor/add', + search: pick(monitor, [ + 'name', + 'type', + 'notifications', + 'interval', + 'maxRetries', + 'trendingMode', + 'payload', + ]), + }) + } + > + + {t('Duplicate')} + + + + )} + } /> } diff --git a/src/client/routes/page.tsx b/src/client/routes/page.tsx index ae009a0..3375d5a 100644 --- a/src/client/routes/page.tsx +++ b/src/client/routes/page.tsx @@ -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() { - {t('Add')} - + <> + {hasAdminPermission && ( + + )} + } /> } diff --git a/src/client/routes/page/$slug.tsx b/src/client/routes/page/$slug.tsx index da728d3..bc4847b 100644 --- a/src/client/routes/page/$slug.tsx +++ b/src/client/routes/page/$slug.tsx @@ -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={
- - + {hasAdminPermission && ( + + )} } /> @@ -88,35 +92,39 @@ function PageComponent() { dataSource={list} renderItem={(item) => ( { - handleOpenModal({ - id: item.id, - name: item.name, - type: item.type, - payload: item.payload as Record, - }); - }} - > - {t('Edit')} - , - { - handleDelete(item.id); - }} - > - - , - ]} + ), + hasAdminPermission && ( + { + handleDelete(item.id); + }} + > + + + ), + ])} > diff --git a/src/client/routes/settings/workspace.tsx b/src/client/routes/settings/workspace.tsx index d37af05..27079d1 100644 --- a/src/client/routes/settings/workspace.tsx +++ b/src/client/routes/settings/workspace.tsx @@ -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(); 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} +
+ {t('Current Role')}: + {role} +
{t('Workspace ID')}: @@ -140,7 +146,10 @@ function PageComponent() {
- + {t('Invite new members by email address')} @@ -166,7 +175,11 @@ function PageComponent() { - diff --git a/src/client/routes/survey.tsx b/src/client/routes/survey.tsx index 15e649c..182eacd 100644 --- a/src/client/routes/survey.tsx +++ b/src/client/routes/survey.tsx @@ -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() { - {t('Add')} - + <> + {hasAdminPermission && ( + + )} + } /> } diff --git a/src/client/routes/survey/$surveyId/index.tsx b/src/client/routes/survey/$surveyId/index.tsx index 0bc8033..e3c1846 100644 --- a/src/client/routes/survey/$surveyId/index.tsx +++ b/src/client/routes/survey/$surveyId/index.tsx @@ -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={
-
} /> diff --git a/src/client/routes/telemetry.tsx b/src/client/routes/telemetry.tsx index edc504a..4234468 100644 --- a/src/client/routes/telemetry.tsx +++ b/src/client/routes/telemetry.tsx @@ -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() {
} actions={ - + <> + {hasAdminPermission && ( + + )} + } /> } diff --git a/src/client/routes/website.tsx b/src/client/routes/website.tsx index 6605d7e..40fb617 100644 --- a/src/client/routes/website.tsx +++ b/src/client/routes/website.tsx @@ -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')} -
} /> diff --git a/src/client/routes/website/$websiteId/index.tsx b/src/client/routes/website/$websiteId/index.tsx index 9bc72fd..05f2a3d 100644 --- a/src/client/routes/website/$websiteId/index.tsx +++ b/src/client/routes/website/$websiteId/index.tsx @@ -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 ; @@ -57,20 +62,22 @@ function WebsiteDetailComponent() { title={website.name} actions={
- + {hasAdminPermission && ( + + )} diff --git a/src/client/store/user.ts b/src/client/store/user.ts index eaf577e..f285463 100644 --- a/src/client/store/user.ts +++ b/src/client/store/user.ts @@ -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; @@ -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; +} diff --git a/src/shared/src/role.ts b/src/shared/src/role.ts index 2939236..c2670d7 100644 --- a/src/shared/src/role.ts +++ b/src/shared/src/role.ts @@ -5,5 +5,6 @@ export enum SYSTEM_ROLES { export enum ROLES { owner = 'owner', + admin = 'admin', readOnly = 'readOnly', }