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 {
|
interface CommonHeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
desc?: React.ReactNode;
|
desc?: string;
|
||||||
|
tip?: React.ReactNode;
|
||||||
actions?: React.ReactNode;
|
actions?: React.ReactNode;
|
||||||
}
|
}
|
||||||
export const CommonHeader: React.FC<CommonHeaderProps> = React.memo((props) => {
|
export const CommonHeader: React.FC<CommonHeaderProps> = React.memo((props) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex w-full items-center">
|
||||||
<h1 className="text-xl font-bold">{props.title}</h1>
|
<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>}
|
{props.actions && <div className="ml-auto">{props.actions}</div>}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
CommonHeader.displayName = 'CommonHeader';
|
CommonHeader.displayName = 'CommonHeader';
|
||||||
|
@ -48,7 +48,7 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
{props.hasSearch && (
|
{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>
|
<form>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<LuSearch className="text-muted-foreground absolute left-2 top-2.5 h-4 w-4" />
|
<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">
|
<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) => {
|
{finalList.map((item) => {
|
||||||
const isSelected = item.href === location.pathname;
|
const isSelected = item.href === location.pathname;
|
||||||
|
|
||||||
@ -88,9 +88,13 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground line-clamp-2 w-full text-xs">
|
|
||||||
{item.content}
|
{item.content && (
|
||||||
</div>
|
<div className="text-muted-foreground line-clamp-2 w-full text-xs">
|
||||||
|
{item.content}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{Array.isArray(item.tags) && item.tags.length > 0 ? (
|
{Array.isArray(item.tags) && item.tags.length > 0 ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{item.tags.map((tag) => (
|
{item.tags.map((tag) => (
|
||||||
|
@ -60,7 +60,9 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
) => {
|
) => {
|
||||||
const Comp = asChild ? Slot : 'button';
|
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 = (
|
const children = (
|
||||||
<>
|
<>
|
||||||
{loading ? <Spinner className="mr-1" /> : icon}
|
{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,
|
DropdownMenuRadioGroup,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuRadioItem,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import { useEvent } from '@/hooks/useEvent';
|
||||||
|
import { useSettingsStore } from '@/store/settings';
|
||||||
import { useUserInfo } from '@/store/user';
|
import { useUserInfo } from '@/store/user';
|
||||||
import { languages } from '@/utils/constants';
|
import { languages } from '@/utils/constants';
|
||||||
import { useTranslation, setLanguage } from '@i18next-toolkit/react';
|
import { useTranslation, setLanguage } from '@i18next-toolkit/react';
|
||||||
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { LuMoreVertical } from 'react-icons/lu';
|
import { LuMoreVertical } from 'react-icons/lu';
|
||||||
|
|
||||||
@ -24,7 +27,15 @@ interface UserConfigProps {
|
|||||||
}
|
}
|
||||||
export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
|
export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
|
||||||
const userInfo = useUserInfo();
|
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 = (
|
const avatar = (
|
||||||
<Avatar>
|
<Avatar>
|
||||||
@ -64,10 +75,28 @@ export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuItem>Profile</DropdownMenuItem>
|
<DropdownMenuItem
|
||||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
onClick={() =>
|
||||||
|
navigate({
|
||||||
|
to: '/settings/profile',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('Profile')}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
navigate({
|
||||||
|
to: '/settings/notifications',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('Notifications')}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuSub>
|
<DropdownMenuSub>
|
||||||
<DropdownMenuSubTrigger>Language</DropdownMenuSubTrigger>
|
<DropdownMenuSubTrigger>{t('Language')}</DropdownMenuSubTrigger>
|
||||||
<DropdownMenuPortal>
|
<DropdownMenuPortal>
|
||||||
<DropdownMenuSubContent>
|
<DropdownMenuSubContent>
|
||||||
<DropdownMenuRadioGroup
|
<DropdownMenuRadioGroup
|
||||||
@ -87,14 +116,24 @@ export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
|
|||||||
</DropdownMenuPortal>
|
</DropdownMenuPortal>
|
||||||
</DropdownMenuSub>
|
</DropdownMenuSub>
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuSub>
|
||||||
className="cursor-default"
|
<DropdownMenuSubTrigger>{t('Theme')}</DropdownMenuSubTrigger>
|
||||||
onSelect={(e) => {
|
<DropdownMenuPortal>
|
||||||
e.preventDefault();
|
<DropdownMenuSubContent>
|
||||||
}}
|
<DropdownMenuRadioGroup
|
||||||
>
|
value={colorScheme}
|
||||||
<ColorSchemeSwitcher />
|
onValueChange={handleChangeColorSchema}
|
||||||
</DropdownMenuItem>
|
>
|
||||||
|
<DropdownMenuRadioItem value={'dark'}>
|
||||||
|
{t('Dark')}
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
<DropdownMenuRadioItem value={'light'}>
|
||||||
|
{t('Light')}
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
</DropdownMenuRadioGroup>
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuPortal>
|
||||||
|
</DropdownMenuSub>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
import { Route as rootRoute } from './routes/__root'
|
import { Route as rootRoute } from './routes/__root'
|
||||||
import { Route as WebsiteImport } from './routes/website'
|
import { Route as WebsiteImport } from './routes/website'
|
||||||
import { Route as TelemetryImport } from './routes/telemetry'
|
import { Route as TelemetryImport } from './routes/telemetry'
|
||||||
|
import { Route as SettingsImport } from './routes/settings'
|
||||||
import { Route as ServerImport } from './routes/server'
|
import { Route as ServerImport } from './routes/server'
|
||||||
import { Route as RegisterImport } from './routes/register'
|
import { Route as RegisterImport } from './routes/register'
|
||||||
import { Route as PageImport } from './routes/page'
|
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 IndexImport } from './routes/index'
|
||||||
import { Route as WebsiteOverviewImport } from './routes/website/overview'
|
import { Route as WebsiteOverviewImport } from './routes/website/overview'
|
||||||
import { Route as WebsiteAddImport } from './routes/website/add'
|
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 TelemetryAddImport } from './routes/telemetry/add'
|
||||||
import { Route as TelemetryTelemetryIdImport } from './routes/telemetry/$telemetryId'
|
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 PageAddImport } from './routes/page/add'
|
||||||
import { Route as PageSlugImport } from './routes/page/$slug'
|
import { Route as PageSlugImport } from './routes/page/$slug'
|
||||||
import { Route as MonitorAddImport } from './routes/monitor/add'
|
import { Route as MonitorAddImport } from './routes/monitor/add'
|
||||||
import { Route as MonitorMonitorIdImport } from './routes/monitor/$monitorId'
|
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
|
// Create/Update Routes
|
||||||
|
|
||||||
@ -42,6 +48,11 @@ const TelemetryRoute = TelemetryImport.update({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const SettingsRoute = SettingsImport.update({
|
||||||
|
path: '/settings',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const ServerRoute = ServerImport.update({
|
const ServerRoute = ServerImport.update({
|
||||||
path: '/server',
|
path: '/server',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
@ -87,11 +98,6 @@ const WebsiteAddRoute = WebsiteAddImport.update({
|
|||||||
getParentRoute: () => WebsiteRoute,
|
getParentRoute: () => WebsiteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
const WebsiteWebsiteIdRoute = WebsiteWebsiteIdImport.update({
|
|
||||||
path: '/$websiteId',
|
|
||||||
getParentRoute: () => WebsiteRoute,
|
|
||||||
} as any)
|
|
||||||
|
|
||||||
const TelemetryAddRoute = TelemetryAddImport.update({
|
const TelemetryAddRoute = TelemetryAddImport.update({
|
||||||
path: '/add',
|
path: '/add',
|
||||||
getParentRoute: () => TelemetryRoute,
|
getParentRoute: () => TelemetryRoute,
|
||||||
@ -102,6 +108,26 @@ const TelemetryTelemetryIdRoute = TelemetryTelemetryIdImport.update({
|
|||||||
getParentRoute: () => TelemetryRoute,
|
getParentRoute: () => TelemetryRoute,
|
||||||
} as any)
|
} 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({
|
const PageAddRoute = PageAddImport.update({
|
||||||
path: '/add',
|
path: '/add',
|
||||||
getParentRoute: () => PageRoute,
|
getParentRoute: () => PageRoute,
|
||||||
@ -122,6 +148,16 @@ const MonitorMonitorIdRoute = MonitorMonitorIdImport.update({
|
|||||||
getParentRoute: () => MonitorRoute,
|
getParentRoute: () => MonitorRoute,
|
||||||
} as any)
|
} 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
|
// Populate the FileRoutesByPath interface
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
@ -154,6 +190,10 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ServerImport
|
preLoaderRoute: typeof ServerImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/settings': {
|
||||||
|
preLoaderRoute: typeof SettingsImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/telemetry': {
|
'/telemetry': {
|
||||||
preLoaderRoute: typeof TelemetryImport
|
preLoaderRoute: typeof TelemetryImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
@ -178,6 +218,22 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof PageAddImport
|
preLoaderRoute: typeof PageAddImport
|
||||||
parentRoute: typeof PageImport
|
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': {
|
'/telemetry/$telemetryId': {
|
||||||
preLoaderRoute: typeof TelemetryTelemetryIdImport
|
preLoaderRoute: typeof TelemetryTelemetryIdImport
|
||||||
parentRoute: typeof TelemetryImport
|
parentRoute: typeof TelemetryImport
|
||||||
@ -186,10 +242,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof TelemetryAddImport
|
preLoaderRoute: typeof TelemetryAddImport
|
||||||
parentRoute: typeof TelemetryImport
|
parentRoute: typeof TelemetryImport
|
||||||
}
|
}
|
||||||
'/website/$websiteId': {
|
|
||||||
preLoaderRoute: typeof WebsiteWebsiteIdImport
|
|
||||||
parentRoute: typeof WebsiteImport
|
|
||||||
}
|
|
||||||
'/website/add': {
|
'/website/add': {
|
||||||
preLoaderRoute: typeof WebsiteAddImport
|
preLoaderRoute: typeof WebsiteAddImport
|
||||||
parentRoute: typeof WebsiteImport
|
parentRoute: typeof WebsiteImport
|
||||||
@ -198,6 +250,14 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof WebsiteOverviewImport
|
preLoaderRoute: typeof WebsiteOverviewImport
|
||||||
parentRoute: typeof WebsiteImport
|
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]),
|
PageRoute.addChildren([PageSlugRoute, PageAddRoute]),
|
||||||
RegisterRoute,
|
RegisterRoute,
|
||||||
ServerRoute,
|
ServerRoute,
|
||||||
|
SettingsRoute.addChildren([
|
||||||
|
SettingsAuditLogRoute,
|
||||||
|
SettingsNotificationsRoute,
|
||||||
|
SettingsProfileRoute,
|
||||||
|
SettingsUsageRoute,
|
||||||
|
]),
|
||||||
TelemetryRoute.addChildren([TelemetryTelemetryIdRoute, TelemetryAddRoute]),
|
TelemetryRoute.addChildren([TelemetryTelemetryIdRoute, TelemetryAddRoute]),
|
||||||
WebsiteRoute.addChildren([
|
WebsiteRoute.addChildren([
|
||||||
WebsiteWebsiteIdRoute,
|
|
||||||
WebsiteAddRoute,
|
WebsiteAddRoute,
|
||||||
WebsiteOverviewRoute,
|
WebsiteOverviewRoute,
|
||||||
|
WebsiteWebsiteIdConfigRoute,
|
||||||
|
WebsiteWebsiteIdIndexRoute,
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
} from '@/components/ui/alert-dialog';
|
} from '@/components/ui/alert-dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { useEventWithLoading } from '@/hooks/useEvent';
|
import { useEventWithLoading } from '@/hooks/useEvent';
|
||||||
@ -77,11 +78,11 @@ export const ServerContent: React.FC = React.memo(() => {
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
onConfirm={handleClearOfflineNode}
|
onConfirm={handleClearOfflineNode}
|
||||||
>
|
>
|
||||||
<Button size="large" loading={loading}>
|
<Button loading={loading}>{t('Clear Offline')}</Button>
|
||||||
{t('Clear Offline')}
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
|
|
||||||
|
<Separator orientation="vertical" className="h-6" />
|
||||||
|
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="outline" Icon={LuPlus}>
|
<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={
|
header={
|
||||||
<CommonHeader
|
<CommonHeader
|
||||||
title={t('Telemetry')}
|
title={t('Telemetry')}
|
||||||
desc={
|
tip={
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p>
|
<p>
|
||||||
<Trans>
|
<Trans>
|
||||||
|
@ -78,11 +78,10 @@ function WebsiteComponent() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
Icon={LuPlus}
|
Icon={LuPlus}
|
||||||
onClick={handleClickAdd}
|
onClick={handleClickAdd}
|
||||||
>
|
/>
|
||||||
{t('Add')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</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 { ErrorTip } from '@/components/ErrorTip';
|
||||||
import { Loading } from '@/components/Loading';
|
import { Loading } from '@/components/Loading';
|
||||||
import { NotFoundTip } from '@/components/NotFoundTip';
|
import { NotFoundTip } from '@/components/NotFoundTip';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
|
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
|
||||||
import { WebsiteCodeBtn } from '@/components/website/WebsiteCodeBtn';
|
import { WebsiteCodeBtn } from '@/components/website/WebsiteCodeBtn';
|
||||||
import { WebsiteMetricsTable } from '@/components/website/WebsiteMetricsTable';
|
import { WebsiteMetricsTable } from '@/components/website/WebsiteMetricsTable';
|
||||||
@ -13,10 +14,11 @@ import { useGlobalRangeDate } from '@/hooks/useGlobalRangeDate';
|
|||||||
import { useCurrentWorkspaceId } 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 } from '@tanstack/react-router';
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||||
import { Card } from 'antd';
|
import { Card } from 'antd';
|
||||||
|
import { LuSettings } from 'react-icons/lu';
|
||||||
|
|
||||||
export const Route = createFileRoute('/website/$websiteId')({
|
export const Route = createFileRoute('/website/$websiteId/')({
|
||||||
beforeLoad: routeAuthBeforeLoad,
|
beforeLoad: routeAuthBeforeLoad,
|
||||||
component: WebsiteDetailComponent,
|
component: WebsiteDetailComponent,
|
||||||
});
|
});
|
||||||
@ -30,6 +32,7 @@ function WebsiteDetailComponent() {
|
|||||||
websiteId,
|
websiteId,
|
||||||
});
|
});
|
||||||
const { startDate, endDate } = useGlobalRangeDate();
|
const { startDate, endDate } = useGlobalRangeDate();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
if (!websiteId) {
|
if (!websiteId) {
|
||||||
return <ErrorTip />;
|
return <ErrorTip />;
|
||||||
@ -52,9 +55,23 @@ function WebsiteDetailComponent() {
|
|||||||
<CommonHeader
|
<CommonHeader
|
||||||
title={website.name}
|
title={website.name}
|
||||||
actions={
|
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} />
|
<WebsiteCodeBtn websiteId={website.id} />
|
||||||
</>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
@ -41,7 +41,7 @@ function WebsiteOverviewComponent() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CommonWrapper
|
<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">
|
<ScrollArea className="h-full overflow-hidden p-4">
|
||||||
{websites.length === 0 && isLoading === false && (
|
{websites.length === 0 && isLoading === false && (
|
||||||
|
Loading…
Reference in New Issue
Block a user