feat(v2): add all settings pages
This commit is contained in:
parent
20e19633ae
commit
fa7534a8e0
@ -3,18 +3,27 @@ import { TipIcon } from './TipIcon';
|
||||
|
||||
interface CommonHeaderProps {
|
||||
title: string;
|
||||
desc?: React.ReactNode;
|
||||
desc?: string;
|
||||
tip?: React.ReactNode;
|
||||
actions?: React.ReactNode;
|
||||
}
|
||||
export const CommonHeader: React.FC<CommonHeaderProps> = React.memo((props) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full items-center">
|
||||
<div className="flex flex-1 items-center">
|
||||
<h1 className="text-xl font-bold">{props.title}</h1>
|
||||
|
||||
{props.desc && <TipIcon className="ml-1" content={props.desc} />}
|
||||
{props.desc && (
|
||||
<span className="text-muted-foreground ml-2 self-end text-sm">
|
||||
{props.desc}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{props.tip && <TipIcon className="ml-1" content={props.tip} />}
|
||||
</div>
|
||||
|
||||
{props.actions && <div className="ml-auto">{props.actions}</div>}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
CommonHeader.displayName = 'CommonHeader';
|
||||
|
@ -48,7 +48,7 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{props.hasSearch && (
|
||||
<div className="bg-background/95 supports-[backdrop-filter]:bg-background/60 p-4 backdrop-blur">
|
||||
<div className="bg-background/95 supports-[backdrop-filter]:bg-background/60 px-4 pt-4 backdrop-blur">
|
||||
<form>
|
||||
<div className="relative">
|
||||
<LuSearch className="text-muted-foreground absolute left-2 top-2.5 h-4 w-4" />
|
||||
@ -64,7 +64,7 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
|
||||
)}
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="flex flex-col gap-2 p-4 pt-0">
|
||||
<div className="flex flex-col gap-2 p-4">
|
||||
{finalList.map((item) => {
|
||||
const isSelected = item.href === location.pathname;
|
||||
|
||||
@ -88,9 +88,13 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{item.content && (
|
||||
<div className="text-muted-foreground line-clamp-2 w-full text-xs">
|
||||
{item.content}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Array.isArray(item.tags) && item.tags.length > 0 ? (
|
||||
<div className="flex items-center gap-2">
|
||||
{item.tags.map((tag) => (
|
||||
|
@ -60,7 +60,9 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
|
||||
const icon = Icon ? <Icon className="mr-1" /> : undefined;
|
||||
const icon = Icon ? (
|
||||
<Icon className={cn(props.children && 'mr-1')} />
|
||||
) : undefined;
|
||||
const children = (
|
||||
<>
|
||||
{loading ? <Spinner className="mr-1" /> : icon}
|
||||
|
151
src/client/components/website/WebsiteConfig.tsx
Normal file
151
src/client/components/website/WebsiteConfig.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import { Button, Form, Input, message, Popconfirm, Tabs } from 'antd';
|
||||
import React from 'react';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import { deleteWorkspaceWebsite } from '../../api/model/website';
|
||||
import { useRequest } from '../../hooks/useRequest';
|
||||
import { useCurrentWorkspaceId } from '../../store/user';
|
||||
import { ErrorTip } from '../ErrorTip';
|
||||
import { Loading } from '../Loading';
|
||||
import { NoWorkspaceTip } from '../NoWorkspaceTip';
|
||||
import { MonitorPicker } from '../monitor/MonitorPicker';
|
||||
import {
|
||||
defaultErrorHandler,
|
||||
defaultSuccessHandler,
|
||||
getQueryKey,
|
||||
trpc,
|
||||
} from '../../api/trpc';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useEvent } from '../../hooks/useEvent';
|
||||
import { hostnameValidator } from '../../utils/validator';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
|
||||
export const WebsiteConfig: React.FC<{ websiteId: string }> = React.memo(
|
||||
(props) => {
|
||||
const { websiteId } = props;
|
||||
const { t } = useTranslation();
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: website, isLoading } = trpc.website.info.useQuery({
|
||||
workspaceId,
|
||||
websiteId,
|
||||
});
|
||||
|
||||
const updateMutation = trpc.website.updateInfo.useMutation({
|
||||
onSuccess: () => {
|
||||
queryClient.resetQueries(getQueryKey(trpc.website.info));
|
||||
defaultSuccessHandler();
|
||||
},
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
|
||||
const handleSave = useEvent(
|
||||
async (values: { name: string; domain: string; monitorId: string }) => {
|
||||
await updateMutation.mutateAsync({
|
||||
workspaceId,
|
||||
websiteId,
|
||||
name: values.name,
|
||||
domain: values.domain,
|
||||
monitorId: values.monitorId,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const [, handleDeleteWebsite] = useRequest(async () => {
|
||||
await deleteWorkspaceWebsite(workspaceId, websiteId!);
|
||||
|
||||
message.success(t('Delete Success'));
|
||||
|
||||
navigate('/settings/websites');
|
||||
});
|
||||
|
||||
if (!workspaceId) {
|
||||
return <NoWorkspaceTip />;
|
||||
}
|
||||
|
||||
if (!websiteId) {
|
||||
return <ErrorTip />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!website) {
|
||||
return <ErrorTip />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Tabs>
|
||||
<Tabs.TabPane key={'detail'} tab={'Detail'}>
|
||||
<Form
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
id: website.id,
|
||||
name: website.name,
|
||||
domain: website.domain,
|
||||
monitorId: website.monitorId,
|
||||
}}
|
||||
onFinish={handleSave}
|
||||
>
|
||||
<Form.Item label={t('Website ID')} name="id">
|
||||
<Input size="large" disabled={true} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('Name')}
|
||||
name="name"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('Domain')}
|
||||
name="domain"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{
|
||||
validator: hostnameValidator,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input size="large" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('Monitor')}
|
||||
name="monitorId"
|
||||
tooltip={t(
|
||||
'You can bind a monitor which will display health status in website overview'
|
||||
)}
|
||||
>
|
||||
<MonitorPicker size="large" allowClear={true} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button size="large" htmlType="submit">
|
||||
{t('Save')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane key={'data'} tab={'Data'}>
|
||||
<Popconfirm
|
||||
title={t('Delete Website')}
|
||||
onConfirm={() => handleDeleteWebsite()}
|
||||
>
|
||||
<Button type="primary" danger={true}>
|
||||
{t('Delete Website')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
WebsiteConfig.displayName = 'WebsiteConfig';
|
@ -13,9 +13,12 @@ import {
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { useSettingsStore } from '@/store/settings';
|
||||
import { useUserInfo } from '@/store/user';
|
||||
import { languages } from '@/utils/constants';
|
||||
import { useTranslation, setLanguage } from '@i18next-toolkit/react';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import React from 'react';
|
||||
import { LuMoreVertical } from 'react-icons/lu';
|
||||
|
||||
@ -24,7 +27,15 @@ interface UserConfigProps {
|
||||
}
|
||||
export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
|
||||
const userInfo = useUserInfo();
|
||||
const { i18n } = useTranslation();
|
||||
const { i18n, t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const colorScheme = useSettingsStore((state) => state.colorScheme);
|
||||
|
||||
const handleChangeColorSchema = useEvent((colorScheme) => {
|
||||
useSettingsStore.setState({
|
||||
colorScheme,
|
||||
});
|
||||
});
|
||||
|
||||
const avatar = (
|
||||
<Avatar>
|
||||
@ -64,10 +75,28 @@ export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
|
||||
)}
|
||||
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>Profile</DropdownMenuItem>
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: '/settings/profile',
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('Profile')}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: '/settings/notifications',
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('Notifications')}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>Language</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubTrigger>{t('Language')}</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuRadioGroup
|
||||
@ -87,14 +116,24 @@ export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="cursor-default"
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>{t('Theme')}</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuRadioGroup
|
||||
value={colorScheme}
|
||||
onValueChange={handleChangeColorSchema}
|
||||
>
|
||||
<ColorSchemeSwitcher />
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuRadioItem value={'dark'}>
|
||||
{t('Dark')}
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value={'light'}>
|
||||
{t('Light')}
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
@ -13,6 +13,7 @@
|
||||
import { Route as rootRoute } from './routes/__root'
|
||||
import { Route as WebsiteImport } from './routes/website'
|
||||
import { Route as TelemetryImport } from './routes/telemetry'
|
||||
import { Route as SettingsImport } from './routes/settings'
|
||||
import { Route as ServerImport } from './routes/server'
|
||||
import { Route as RegisterImport } from './routes/register'
|
||||
import { Route as PageImport } from './routes/page'
|
||||
@ -22,13 +23,18 @@ import { Route as DashboardImport } from './routes/dashboard'
|
||||
import { Route as IndexImport } from './routes/index'
|
||||
import { Route as WebsiteOverviewImport } from './routes/website/overview'
|
||||
import { Route as WebsiteAddImport } from './routes/website/add'
|
||||
import { Route as WebsiteWebsiteIdImport } from './routes/website/$websiteId'
|
||||
import { Route as TelemetryAddImport } from './routes/telemetry/add'
|
||||
import { Route as TelemetryTelemetryIdImport } from './routes/telemetry/$telemetryId'
|
||||
import { Route as SettingsUsageImport } from './routes/settings/usage'
|
||||
import { Route as SettingsProfileImport } from './routes/settings/profile'
|
||||
import { Route as SettingsNotificationsImport } from './routes/settings/notifications'
|
||||
import { Route as SettingsAuditLogImport } from './routes/settings/auditLog'
|
||||
import { Route as PageAddImport } from './routes/page/add'
|
||||
import { Route as PageSlugImport } from './routes/page/$slug'
|
||||
import { Route as MonitorAddImport } from './routes/monitor/add'
|
||||
import { Route as MonitorMonitorIdImport } from './routes/monitor/$monitorId'
|
||||
import { Route as WebsiteWebsiteIdIndexImport } from './routes/website/$websiteId/index'
|
||||
import { Route as WebsiteWebsiteIdConfigImport } from './routes/website/$websiteId/config'
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
@ -42,6 +48,11 @@ const TelemetryRoute = TelemetryImport.update({
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const SettingsRoute = SettingsImport.update({
|
||||
path: '/settings',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const ServerRoute = ServerImport.update({
|
||||
path: '/server',
|
||||
getParentRoute: () => rootRoute,
|
||||
@ -87,11 +98,6 @@ const WebsiteAddRoute = WebsiteAddImport.update({
|
||||
getParentRoute: () => WebsiteRoute,
|
||||
} as any)
|
||||
|
||||
const WebsiteWebsiteIdRoute = WebsiteWebsiteIdImport.update({
|
||||
path: '/$websiteId',
|
||||
getParentRoute: () => WebsiteRoute,
|
||||
} as any)
|
||||
|
||||
const TelemetryAddRoute = TelemetryAddImport.update({
|
||||
path: '/add',
|
||||
getParentRoute: () => TelemetryRoute,
|
||||
@ -102,6 +108,26 @@ const TelemetryTelemetryIdRoute = TelemetryTelemetryIdImport.update({
|
||||
getParentRoute: () => TelemetryRoute,
|
||||
} as any)
|
||||
|
||||
const SettingsUsageRoute = SettingsUsageImport.update({
|
||||
path: '/usage',
|
||||
getParentRoute: () => SettingsRoute,
|
||||
} as any)
|
||||
|
||||
const SettingsProfileRoute = SettingsProfileImport.update({
|
||||
path: '/profile',
|
||||
getParentRoute: () => SettingsRoute,
|
||||
} as any)
|
||||
|
||||
const SettingsNotificationsRoute = SettingsNotificationsImport.update({
|
||||
path: '/notifications',
|
||||
getParentRoute: () => SettingsRoute,
|
||||
} as any)
|
||||
|
||||
const SettingsAuditLogRoute = SettingsAuditLogImport.update({
|
||||
path: '/auditLog',
|
||||
getParentRoute: () => SettingsRoute,
|
||||
} as any)
|
||||
|
||||
const PageAddRoute = PageAddImport.update({
|
||||
path: '/add',
|
||||
getParentRoute: () => PageRoute,
|
||||
@ -122,6 +148,16 @@ const MonitorMonitorIdRoute = MonitorMonitorIdImport.update({
|
||||
getParentRoute: () => MonitorRoute,
|
||||
} as any)
|
||||
|
||||
const WebsiteWebsiteIdIndexRoute = WebsiteWebsiteIdIndexImport.update({
|
||||
path: '/$websiteId/',
|
||||
getParentRoute: () => WebsiteRoute,
|
||||
} as any)
|
||||
|
||||
const WebsiteWebsiteIdConfigRoute = WebsiteWebsiteIdConfigImport.update({
|
||||
path: '/$websiteId/config',
|
||||
getParentRoute: () => WebsiteRoute,
|
||||
} as any)
|
||||
|
||||
// Populate the FileRoutesByPath interface
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
@ -154,6 +190,10 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof ServerImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/settings': {
|
||||
preLoaderRoute: typeof SettingsImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/telemetry': {
|
||||
preLoaderRoute: typeof TelemetryImport
|
||||
parentRoute: typeof rootRoute
|
||||
@ -178,6 +218,22 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof PageAddImport
|
||||
parentRoute: typeof PageImport
|
||||
}
|
||||
'/settings/auditLog': {
|
||||
preLoaderRoute: typeof SettingsAuditLogImport
|
||||
parentRoute: typeof SettingsImport
|
||||
}
|
||||
'/settings/notifications': {
|
||||
preLoaderRoute: typeof SettingsNotificationsImport
|
||||
parentRoute: typeof SettingsImport
|
||||
}
|
||||
'/settings/profile': {
|
||||
preLoaderRoute: typeof SettingsProfileImport
|
||||
parentRoute: typeof SettingsImport
|
||||
}
|
||||
'/settings/usage': {
|
||||
preLoaderRoute: typeof SettingsUsageImport
|
||||
parentRoute: typeof SettingsImport
|
||||
}
|
||||
'/telemetry/$telemetryId': {
|
||||
preLoaderRoute: typeof TelemetryTelemetryIdImport
|
||||
parentRoute: typeof TelemetryImport
|
||||
@ -186,10 +242,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof TelemetryAddImport
|
||||
parentRoute: typeof TelemetryImport
|
||||
}
|
||||
'/website/$websiteId': {
|
||||
preLoaderRoute: typeof WebsiteWebsiteIdImport
|
||||
parentRoute: typeof WebsiteImport
|
||||
}
|
||||
'/website/add': {
|
||||
preLoaderRoute: typeof WebsiteAddImport
|
||||
parentRoute: typeof WebsiteImport
|
||||
@ -198,6 +250,14 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof WebsiteOverviewImport
|
||||
parentRoute: typeof WebsiteImport
|
||||
}
|
||||
'/website/$websiteId/config': {
|
||||
preLoaderRoute: typeof WebsiteWebsiteIdConfigImport
|
||||
parentRoute: typeof WebsiteImport
|
||||
}
|
||||
'/website/$websiteId/': {
|
||||
preLoaderRoute: typeof WebsiteWebsiteIdIndexImport
|
||||
parentRoute: typeof WebsiteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -211,11 +271,18 @@ export const routeTree = rootRoute.addChildren([
|
||||
PageRoute.addChildren([PageSlugRoute, PageAddRoute]),
|
||||
RegisterRoute,
|
||||
ServerRoute,
|
||||
SettingsRoute.addChildren([
|
||||
SettingsAuditLogRoute,
|
||||
SettingsNotificationsRoute,
|
||||
SettingsProfileRoute,
|
||||
SettingsUsageRoute,
|
||||
]),
|
||||
TelemetryRoute.addChildren([TelemetryTelemetryIdRoute, TelemetryAddRoute]),
|
||||
WebsiteRoute.addChildren([
|
||||
WebsiteWebsiteIdRoute,
|
||||
WebsiteAddRoute,
|
||||
WebsiteOverviewRoute,
|
||||
WebsiteWebsiteIdConfigRoute,
|
||||
WebsiteWebsiteIdIndexRoute,
|
||||
]),
|
||||
])
|
||||
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useEventWithLoading } from '@/hooks/useEvent';
|
||||
@ -77,11 +78,11 @@ export const ServerContent: React.FC = React.memo(() => {
|
||||
disabled={loading}
|
||||
onConfirm={handleClearOfflineNode}
|
||||
>
|
||||
<Button size="large" loading={loading}>
|
||||
{t('Clear Offline')}
|
||||
</Button>
|
||||
<Button loading={loading}>{t('Clear Offline')}</Button>
|
||||
</Popconfirm>
|
||||
|
||||
<Separator orientation="vertical" className="h-6" />
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline" Icon={LuPlus}>
|
||||
|
71
src/client/routes/settings.tsx
Normal file
71
src/client/routes/settings.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { trpc } from '@/api/trpc';
|
||||
import { CommonHeader } from '@/components/CommonHeader';
|
||||
import { CommonList } from '@/components/CommonList';
|
||||
import { CommonWrapper } from '@/components/CommonWrapper';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useDataReady } from '@/hooks/useDataReady';
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { LayoutV2 } from '@/pages/LayoutV2';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
import { Trans, useTranslation } from '@i18next-toolkit/react';
|
||||
import {
|
||||
createFileRoute,
|
||||
useNavigate,
|
||||
useRouterState,
|
||||
} from '@tanstack/react-router';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const Route = createFileRoute('/settings')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
component: TelemetryComponent,
|
||||
});
|
||||
|
||||
function TelemetryComponent() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const pathname = useRouterState({
|
||||
select: (state) => state.location.pathname,
|
||||
});
|
||||
|
||||
const items = [
|
||||
{
|
||||
id: 'profile',
|
||||
title: t('Profile'),
|
||||
href: '/settings/profile',
|
||||
},
|
||||
{
|
||||
id: 'notifications',
|
||||
title: t('Notifications'),
|
||||
href: '/settings/notifications',
|
||||
},
|
||||
{
|
||||
id: 'auditLog',
|
||||
title: t('Audit Log'),
|
||||
href: '/settings/auditLog',
|
||||
},
|
||||
{
|
||||
id: 'usage',
|
||||
title: t('Usage'),
|
||||
href: '/settings/usage',
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (pathname === Route.fullPath) {
|
||||
navigate({
|
||||
to: '/settings/profile',
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LayoutV2
|
||||
list={
|
||||
<CommonWrapper header={<CommonHeader title={t('Settings')} />}>
|
||||
<CommonList items={items} />
|
||||
</CommonWrapper>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
124
src/client/routes/settings/auditLog.tsx
Normal file
124
src/client/routes/settings/auditLog.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
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 { Empty, List } from 'antd';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { trpc } from '../../api/trpc';
|
||||
import { useCurrentWorkspaceId } from '../../store/user';
|
||||
import { CommonHeader } from '@/components/CommonHeader';
|
||||
import { last } from 'lodash-es';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { useWatch } from '@/hooks/useWatch';
|
||||
import dayjs from 'dayjs';
|
||||
import { ColorTag } from '@/components/ColorTag';
|
||||
|
||||
export const Route = createFileRoute('/settings/auditLog')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
component: PageComponent,
|
||||
});
|
||||
|
||||
function PageComponent() {
|
||||
const { t } = useTranslation();
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { data, hasNextPage, fetchNextPage, isFetchingNextPage } =
|
||||
trpc.auditLog.fetchByCursor.useInfiniteQuery({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const allData = useMemo(() => {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [...data.pages.flatMap((p) => p.items)];
|
||||
}, [data]);
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: hasNextPage ? allData.length + 1 : allData.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 48,
|
||||
overscan: 5,
|
||||
});
|
||||
|
||||
const virtualItems = rowVirtualizer.getVirtualItems();
|
||||
|
||||
useWatch([virtualItems], () => {
|
||||
const lastItem = last(virtualItems);
|
||||
|
||||
if (!lastItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
lastItem.index >= allData.length - 1 &&
|
||||
hasNextPage &&
|
||||
!isFetchingNextPage
|
||||
) {
|
||||
fetchNextPage();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<CommonWrapper header={<CommonHeader title={t('Audit Log')} />}>
|
||||
<ScrollArea className="h-full overflow-hidden p-4">
|
||||
<List>
|
||||
<div ref={parentRef} className="h-full w-full overflow-auto">
|
||||
{virtualItems.length === 0 && <Empty />}
|
||||
|
||||
<div
|
||||
className="relative w-full"
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
}}
|
||||
>
|
||||
{virtualItems.map((virtualRow) => {
|
||||
const isLoaderRow = virtualRow.index > allData.length - 1;
|
||||
const item = allData[virtualRow.index];
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
key={virtualRow.index}
|
||||
className="absolute left-0 top-0 w-full"
|
||||
style={{
|
||||
height: `${virtualRow.size}px`,
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
{isLoaderRow ? (
|
||||
hasNextPage ? (
|
||||
t('Loading more...')
|
||||
) : (
|
||||
t('Nothing more to load')
|
||||
)
|
||||
) : (
|
||||
<div className="flex h-7 items-center overflow-hidden">
|
||||
{item.relatedType && (
|
||||
<ColorTag label={item.relatedType} />
|
||||
)}
|
||||
<div
|
||||
className="mr-2 w-9 text-xs opacity-60"
|
||||
title={dayjs(item.createdAt).format(
|
||||
'YYYY-MM-DD HH:mm:ss'
|
||||
)}
|
||||
>
|
||||
{dayjs(item.createdAt).format('MM-DD HH:mm')}
|
||||
</div>
|
||||
<div className="h-full flex-1 overflow-auto">
|
||||
{item.content}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</List.Item>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</List>
|
||||
</ScrollArea>
|
||||
</CommonWrapper>
|
||||
);
|
||||
}
|
137
src/client/routes/settings/notifications.tsx
Normal file
137
src/client/routes/settings/notifications.tsx
Normal file
@ -0,0 +1,137 @@
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
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 { List, Popconfirm } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import { trpc } from '../../api/trpc';
|
||||
import {
|
||||
NotificationFormValues,
|
||||
NotificationInfoModal,
|
||||
} from '../../components/modals/NotificationInfo';
|
||||
import { useEvent } from '../../hooks/useEvent';
|
||||
import { useCurrentWorkspaceId } from '../../store/user';
|
||||
import { CommonHeader } from '@/components/CommonHeader';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { LuFileEdit, LuPlus, LuTrash2 } from 'react-icons/lu';
|
||||
|
||||
export const Route = createFileRoute('/settings/notifications')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
component: PageComponent,
|
||||
});
|
||||
|
||||
function PageComponent() {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const currentWorkspaceId = useCurrentWorkspaceId();
|
||||
const { data: list = [], refetch } = trpc.notification.all.useQuery({
|
||||
workspaceId: currentWorkspaceId!,
|
||||
});
|
||||
const [editingFormData, setEditingFormData] = useState<
|
||||
NotificationFormValues | undefined
|
||||
>(undefined);
|
||||
|
||||
const upsertMutation = trpc.notification.upsert.useMutation();
|
||||
const deleteMutation = trpc.notification.delete.useMutation();
|
||||
|
||||
const handleOpenModal = useEvent((initValues?: NotificationFormValues) => {
|
||||
setEditingFormData(initValues);
|
||||
setOpen(true);
|
||||
});
|
||||
|
||||
const handleCloseModal = useEvent(() => {
|
||||
setEditingFormData(undefined);
|
||||
setOpen(false);
|
||||
});
|
||||
|
||||
const handleSubmit = useEvent(async (values: NotificationFormValues) => {
|
||||
await upsertMutation.mutateAsync({
|
||||
workspaceId: currentWorkspaceId!,
|
||||
...values,
|
||||
});
|
||||
handleCloseModal();
|
||||
refetch();
|
||||
});
|
||||
|
||||
const handleDelete = useEvent(async (notificationId: string) => {
|
||||
await deleteMutation.mutateAsync({
|
||||
workspaceId: currentWorkspaceId!,
|
||||
id: notificationId,
|
||||
});
|
||||
refetch();
|
||||
});
|
||||
|
||||
return (
|
||||
<CommonWrapper
|
||||
header={
|
||||
<CommonHeader
|
||||
title={t('Notifications')}
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
Icon={LuPlus}
|
||||
onClick={() => handleOpenModal()}
|
||||
>
|
||||
{t('New')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ScrollArea className="h-full overflow-hidden p-4">
|
||||
<div>
|
||||
<List
|
||||
bordered={true}
|
||||
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 />
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta title={item.name} />
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
|
||||
<NotificationInfoModal
|
||||
key={editingFormData?.id}
|
||||
open={open}
|
||||
initialValues={editingFormData}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => handleCloseModal()}
|
||||
/>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CommonWrapper>
|
||||
);
|
||||
}
|
128
src/client/routes/settings/profile.tsx
Normal file
128
src/client/routes/settings/profile.tsx
Normal file
@ -0,0 +1,128 @@
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
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 { Button, Card, Form, Input, Modal, Typography } from 'antd';
|
||||
import { useLogout } from '@/api/model/user';
|
||||
import { trpc, defaultSuccessHandler, defaultErrorHandler } from '@/api/trpc';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useState } from 'react';
|
||||
import { CommonHeader } from '@/components/CommonHeader';
|
||||
|
||||
export const Route = createFileRoute('/settings/profile')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
component: PageComponent,
|
||||
});
|
||||
|
||||
function PageComponent() {
|
||||
const { t } = useTranslation();
|
||||
const userInfo = useUserStore((state) => state.info);
|
||||
const [openChangePassword, setOpenChangePassword] = useState(false);
|
||||
|
||||
const changePasswordMutation = trpc.user.changePassword.useMutation({
|
||||
onSuccess: defaultSuccessHandler,
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
|
||||
const logout = useLogout();
|
||||
|
||||
return (
|
||||
<CommonWrapper header={<CommonHeader title={t('Profile')} />}>
|
||||
<ScrollArea className="h-full overflow-hidden p-4">
|
||||
<div>
|
||||
<Card>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label={t('Current Workspace Id')}>
|
||||
<Typography.Text copyable={true} code={true}>
|
||||
{userInfo?.currentWorkspace?.id}
|
||||
</Typography.Text>
|
||||
</Form.Item>
|
||||
<Form.Item label={t('User Id')}>
|
||||
<Typography.Text copyable={true} code={true}>
|
||||
{userInfo?.id}
|
||||
</Typography.Text>
|
||||
</Form.Item>
|
||||
<Form.Item label={t('Password')}>
|
||||
<Button
|
||||
danger={true}
|
||||
onClick={() => setOpenChangePassword(true)}
|
||||
>
|
||||
{t('Change Password')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
open={openChangePassword}
|
||||
title={t('Change password')}
|
||||
footer={null}
|
||||
maskClosable={false}
|
||||
onCancel={() => setOpenChangePassword(false)}
|
||||
destroyOnClose={true}
|
||||
>
|
||||
<Form
|
||||
layout="vertical"
|
||||
onFinish={async (values) => {
|
||||
const { oldPassword, newPassword } = values;
|
||||
await changePasswordMutation.mutateAsync({
|
||||
oldPassword,
|
||||
newPassword,
|
||||
});
|
||||
logout();
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
label={t('Old Password')}
|
||||
name="oldPassword"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('New Password')}
|
||||
name="newPassword"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('New Password Repeat')}
|
||||
name="newPasswordRepeat"
|
||||
rules={[
|
||||
{ required: true },
|
||||
(form) => ({
|
||||
validator(rule, value) {
|
||||
if (
|
||||
!value ||
|
||||
form.getFieldValue('newPassword') === value
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return Promise.reject(
|
||||
t('The two passwords are not consistent')
|
||||
);
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Form.Item className="text-right">
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={changePasswordMutation.isLoading}
|
||||
>
|
||||
{t('Submit')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CommonWrapper>
|
||||
);
|
||||
}
|
81
src/client/routes/settings/usage.tsx
Normal file
81
src/client/routes/settings/usage.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
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 { Statistic } from 'antd';
|
||||
import { useMemo } from 'react';
|
||||
import { trpc } from '../../api/trpc';
|
||||
import { useCurrentWorkspaceId } from '../../store/user';
|
||||
import { CommonHeader } from '@/components/CommonHeader';
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||
import dayjs from 'dayjs';
|
||||
import { formatNumber } from '@/utils/common';
|
||||
|
||||
export const Route = createFileRoute('/settings/usage')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
component: PageComponent,
|
||||
});
|
||||
|
||||
function PageComponent() {
|
||||
const { t } = useTranslation();
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const [startDate, endDate] = useMemo(
|
||||
() => [dayjs().startOf('month'), dayjs().endOf('day')],
|
||||
[]
|
||||
);
|
||||
|
||||
const { data } = trpc.billing.usage.useQuery({
|
||||
workspaceId,
|
||||
startAt: startDate.valueOf(),
|
||||
endAt: endDate.valueOf(),
|
||||
});
|
||||
|
||||
return (
|
||||
<CommonWrapper header={<CommonHeader title={t('Usage')} />}>
|
||||
<ScrollArea className="h-full overflow-hidden p-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="mb-2 text-lg">
|
||||
{t('Statistic Date')}:
|
||||
<span className="ml-2 font-bold">
|
||||
{startDate.format('YYYY/MM/DD')} -{' '}
|
||||
{endDate.format('YYYY/MM/DD')}
|
||||
</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex gap-2">
|
||||
<Card className="flex-1">
|
||||
<CardHeader className="text-muted-foreground">
|
||||
{t('Website Accepted Count')}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{formatNumber(data?.websiteAcceptedCount ?? 0)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="flex-1">
|
||||
<CardHeader className="text-muted-foreground">
|
||||
{t('Website Event Count')}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{formatNumber(data?.websiteEventCount ?? 0)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="flex-1">
|
||||
<CardHeader className="text-muted-foreground">
|
||||
{t('Monitor Execution Count')}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{formatNumber(data?.monitorExecutionCount ?? 0)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ScrollArea>
|
||||
</CommonWrapper>
|
||||
);
|
||||
}
|
@ -65,7 +65,7 @@ function TelemetryComponent() {
|
||||
header={
|
||||
<CommonHeader
|
||||
title={t('Telemetry')}
|
||||
desc={
|
||||
tip={
|
||||
<div className="space-y-2">
|
||||
<p>
|
||||
<Trans>
|
||||
|
@ -78,11 +78,10 @@ function WebsiteComponent() {
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
Icon={LuPlus}
|
||||
onClick={handleClickAdd}
|
||||
>
|
||||
{t('Add')}
|
||||
</Button>
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
49
src/client/routes/website/$websiteId/config.tsx
Normal file
49
src/client/routes/website/$websiteId/config.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { trpc } from '@/api/trpc';
|
||||
import { CommonHeader } from '@/components/CommonHeader';
|
||||
import { CommonWrapper } from '@/components/CommonWrapper';
|
||||
import { ErrorTip } from '@/components/ErrorTip';
|
||||
import { Loading } from '@/components/Loading';
|
||||
import { NotFoundTip } from '@/components/NotFoundTip';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { WebsiteConfig } from '@/components/website/WebsiteConfig';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/website/$websiteId/config')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
component: WebsiteDetailComponent,
|
||||
});
|
||||
|
||||
function WebsiteDetailComponent() {
|
||||
const { websiteId } = Route.useParams<{ websiteId: string }>();
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const { data: website, isLoading } = trpc.website.info.useQuery({
|
||||
workspaceId,
|
||||
websiteId,
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!websiteId) {
|
||||
return <ErrorTip />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!website) {
|
||||
return <NotFoundTip />;
|
||||
}
|
||||
|
||||
return (
|
||||
<CommonWrapper
|
||||
header={<CommonHeader title={website.name} desc={t('Config')} />}
|
||||
>
|
||||
<ScrollArea className="h-full overflow-hidden p-4">
|
||||
<WebsiteConfig websiteId={websiteId} />
|
||||
</ScrollArea>
|
||||
</CommonWrapper>
|
||||
);
|
||||
}
|
@ -4,6 +4,7 @@ import { CommonWrapper } from '@/components/CommonWrapper';
|
||||
import { ErrorTip } from '@/components/ErrorTip';
|
||||
import { Loading } from '@/components/Loading';
|
||||
import { NotFoundTip } from '@/components/NotFoundTip';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
|
||||
import { WebsiteCodeBtn } from '@/components/website/WebsiteCodeBtn';
|
||||
import { WebsiteMetricsTable } from '@/components/website/WebsiteMetricsTable';
|
||||
@ -13,10 +14,11 @@ import { useGlobalRangeDate } from '@/hooks/useGlobalRangeDate';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { Card } from 'antd';
|
||||
import { LuSettings } from 'react-icons/lu';
|
||||
|
||||
export const Route = createFileRoute('/website/$websiteId')({
|
||||
export const Route = createFileRoute('/website/$websiteId/')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
component: WebsiteDetailComponent,
|
||||
});
|
||||
@ -30,6 +32,7 @@ function WebsiteDetailComponent() {
|
||||
websiteId,
|
||||
});
|
||||
const { startDate, endDate } = useGlobalRangeDate();
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!websiteId) {
|
||||
return <ErrorTip />;
|
||||
@ -52,9 +55,23 @@ function WebsiteDetailComponent() {
|
||||
<CommonHeader
|
||||
title={website.name}
|
||||
actions={
|
||||
<>
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: '/website/$websiteId/config',
|
||||
params: {
|
||||
websiteId,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<LuSettings />
|
||||
</Button>
|
||||
<WebsiteCodeBtn websiteId={website.id} />
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
}
|
@ -41,7 +41,7 @@ function WebsiteOverviewComponent() {
|
||||
|
||||
return (
|
||||
<CommonWrapper
|
||||
header={<h1 className="text-xl font-bold">{t('Add Website')}</h1>}
|
||||
header={<h1 className="text-xl font-bold">{t('Website Overview')}</h1>}
|
||||
>
|
||||
<ScrollArea className="h-full overflow-hidden p-4">
|
||||
{websites.length === 0 && isLoading === false && (
|
||||
|
Loading…
Reference in New Issue
Block a user