refactor: remove unused code

This commit is contained in:
moonrailgun 2024-05-25 00:56:41 +08:00
parent 52a89276c8
commit 328a4e856c
42 changed files with 186 additions and 1948 deletions

View File

@ -0,0 +1,16 @@
import * as React from 'react';
import { DesktopLayout } from './DesktopLayout';
import { LayoutProps } from './types';
import { useIsMobile } from '@/hooks/useIsMobile';
import { MobileLayout } from './MobileLayout';
export const Layout: React.FC<LayoutProps> = React.memo((props) => {
const isMobile = useIsMobile();
if (isMobile) {
return <MobileLayout {...props} />;
}
return <DesktopLayout {...props} />;
});
Layout.displayName = 'Layout';

View File

@ -0,0 +1,118 @@
import React, { useMemo, useRef, useState } from 'react';
import { Button, Steps, Typography } from 'antd';
import { useCurrentWorkspaceId } from '../../store/user';
import { useWatch } from '../../hooks/useWatch';
import { Loading } from '../Loading';
import { without } from 'lodash-es';
import { useTranslation } from '@i18next-toolkit/react';
import { useSocketSubscribe } from '@/api/socketio';
import { ServerStatusInfo } from '../../../types';
function useServerMap(): Record<string, ServerStatusInfo> {
const serverMap = useSocketSubscribe<Record<string, ServerStatusInfo>>(
'onServerStatusUpdate',
{}
);
return serverMap;
}
export const AddServerStep: React.FC = React.memo(() => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const [current, setCurrent] = useState(0);
const serverMap = useServerMap();
const [checking, setChecking] = useState(false);
const oldServerMapNames = useRef<string[]>([]);
const [diffServerNames, setDiffServerNames] = useState<string[]>([]);
const allServerNames = useMemo(() => Object.keys(serverMap), [serverMap]);
useWatch([checking], () => {
if (checking === true) {
oldServerMapNames.current = [...allServerNames];
}
});
useWatch([allServerNames], () => {
if (checking === true) {
setDiffServerNames(without(allServerNames, ...oldServerMapNames.current));
}
});
const command = `./tianji-reporter --url ${window.location.origin} --workspace ${workspaceId}`;
return (
<Steps
direction="vertical"
current={current}
items={[
{
title: t('Download Client Reportor'),
description: (
<div>
{t('Download reporter from')}{' '}
<Typography.Link
href="https://github.com/msgbyte/tianji/releases"
target="_blank"
onClick={() => {
if (current === 0) {
setCurrent(1);
setChecking(true);
}
}}
>
{t('Releases Page')}
</Typography.Link>
</div>
),
},
{
title: t('Run'),
description: (
<div>
{t('run reporter with')}:{' '}
<Typography.Text
code={true}
copyable={{ format: 'text/plain', text: command }}
>
{command}
</Typography.Text>
<Button
type="link"
size="small"
disabled={current !== 1}
onClick={() => {
if (current === 1) {
setCurrent(2);
setChecking(true);
}
}}
>
{t('Next step')}
</Button>
</div>
),
},
{
title: t('Waiting for receive report pack'),
description: (
<div>
{diffServerNames.length === 0 || checking === false ? (
<Loading />
) : (
<div>
{t('Is this your servers?')}
{diffServerNames.map((n) => (
<div key={n}>- {n}</div>
))}
</div>
)}
</div>
),
},
]}
/>
);
});
AddServerStep.displayName = 'AddServerStep';

View File

@ -0,0 +1,33 @@
import { useCurrentWorkspaceId } from '@/store/user';
import { useTranslation } from '@i18next-toolkit/react';
import { Typography } from 'antd';
import React from 'react';
export const InstallScript: React.FC = React.memo(() => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const command = `curl -o- ${window.location.origin}/serverStatus/${workspaceId}/install.sh?url=${window.location.origin} | bash`;
return (
<div>
<div>{t('Run this command in your linux machine')}</div>
<Typography.Paragraph
copyable={{
format: 'text/plain',
text: command,
}}
className="flex h-[96px] overflow-auto rounded border border-black border-opacity-10 bg-black bg-opacity-5 p-2"
>
<span>{command}</span>
</Typography.Paragraph>
<div>
{t(
'Or you wanna report server status in windows server? switch to Manual tab'
)}
</div>
</div>
);
});
InstallScript.displayName = 'InstallScript';

View File

@ -1,7 +0,0 @@
import React from 'react';
import { Dashboard } from '../components/dashboard/Dashboard';
export const DashboardPage: React.FC = React.memo(() => {
return <Dashboard />;
});
DashboardPage.displayName = 'DashboardPage';

View File

@ -1,183 +0,0 @@
import React, { useState } from 'react';
import { Outlet, useNavigate, useSearchParams } from 'react-router-dom';
import { NavItem } from '../components/NavItem';
import { MobileNavItem } from '../components/MobileNavItem';
import { UserOutlined } from '@ant-design/icons';
import { Button, Divider, Drawer, Dropdown } from 'antd';
import { useUserStore } from '../store/user';
import { useLogout } from '../api/model/user';
import { ColorSchemeSwitcher } from '../components/ColorSchemeSwitcher';
import { version } from '@/utils/env';
import { useIsMobile } from '../hooks/useIsMobile';
import { RiMenuUnfoldLine } from 'react-icons/ri';
import { useTranslation } from '@i18next-toolkit/react';
import { LanguageSelector } from '../components/LanguageSelector';
export const Layout: React.FC = React.memo(() => {
const [params] = useSearchParams();
const workspaces = useUserStore((state) => {
const userInfo = state.info;
if (userInfo) {
return userInfo.workspaces.map((w) => ({
id: w.workspace.id,
name: w.workspace.name,
role: w.role,
current: userInfo.currentWorkspace?.id === w.workspace.id,
}));
}
return [];
});
const [openDraw, setOpenDraw] = useState(false);
const logout = useLogout();
const isMobile = useIsMobile();
const showHeader = !params.has('hideHeader');
const navigate = useNavigate();
const { t } = useTranslation();
const accountEl = (
<Dropdown
placement="bottomRight"
menu={{
items: [
{
key: 'workspaces',
label: t('Workspaces'),
children: workspaces.map((w) => ({
key: w.id,
label: `${w.name}${w.current ? '(current)' : ''}`,
disabled: w.current,
})),
},
{
key: 'settings',
label: t('Settings'),
onClick: () => {
navigate('/settings');
},
},
{
key: 'logout',
label: t('Logout'),
onClick: () => {
logout();
},
},
{
type: 'divider',
},
{
key: 'version',
label: `v${version}`,
disabled: true,
},
],
}}
>
<Button shape="circle" size="large" icon={<UserOutlined />} />
</Dropdown>
);
return (
<div className="flex h-full flex-col dark:bg-gray-900 dark:text-gray-300">
{showHeader && (
<div className="sticky top-0 z-20 flex h-[62px] items-center bg-gray-100 px-4 dark:bg-gray-800">
{isMobile && (
<>
<Button
className="mr-2"
icon={<RiMenuUnfoldLine className="anticon" />}
onClick={() => setOpenDraw(true)}
/>
<Drawer
open={openDraw}
onClose={() => setOpenDraw(false)}
placement="left"
closeIcon={false}
>
<div className="flex h-full flex-col pt-12">
<div className="flex-1">
<MobileNavItem
to="/dashboard"
label={t('Dashboard')}
onClick={() => setOpenDraw(false)}
/>
<MobileNavItem
to="/monitor"
label={t('Monitor')}
onClick={() => setOpenDraw(false)}
/>
<MobileNavItem
to="/website"
label={t('Website')}
onClick={() => setOpenDraw(false)}
/>
<MobileNavItem
to="/servers"
label={t('Servers')}
onClick={() => setOpenDraw(false)}
/>
<MobileNavItem
to="/telemetry"
label={t('Telemetry')}
onClick={() => setOpenDraw(false)}
/>
<MobileNavItem
to="/settings"
label={t('Settings')}
onClick={() => setOpenDraw(false)}
/>
</div>
<Divider />
<div className="flex justify-between">
<ColorSchemeSwitcher />
{accountEl}
</div>
</div>
</Drawer>
</>
)}
<div className="mr-10 flex items-center px-2 font-bold">
<img src="/icon.svg" className="mr-2 h-10 w-10" />
<span className="text-xl dark:text-gray-200">Tianji</span>
</div>
{!isMobile && (
<>
<div className="flex gap-8">
<NavItem to="/dashboard" label={t('Dashboard')} />
<NavItem to="/monitor" label={t('Monitor')} />
<NavItem to="/website" label={t('Website')} />
<NavItem to="/servers" label={t('Servers')} />
<NavItem to="/telemetry" label={t('Telemetry')} />
<NavItem to="/settings" label={t('Settings')} />
</div>
<div className="flex-1" />
<div className="flex gap-2">
<LanguageSelector />
<ColorSchemeSwitcher />
{accountEl}
</div>
</>
)}
</div>
)}
<div className="w-full flex-1 overflow-hidden">
<div className="h-full overflow-auto px-1 sm:px-4">
<div className="m-auto max-w-7xl">
<Outlet />
</div>
</div>
</div>
</div>
);
});
Layout.displayName = 'Layout';

View File

@ -1,16 +0,0 @@
import * as React from 'react';
import { DesktopLayout } from './Layout/DesktopLayout';
import { LayoutProps } from './Layout/types';
import { useIsMobile } from '@/hooks/useIsMobile';
import { MobileLayout } from './Layout/MobileLayout';
export const LayoutV2: React.FC<LayoutProps> = React.memo((props) => {
const isMobile = useIsMobile();
if (isMobile) {
return <MobileLayout {...props} />;
}
return <DesktopLayout {...props} />;
});
LayoutV2.displayName = 'LayoutV2';

View File

@ -1,83 +0,0 @@
import { Button, Form, Input, Typography } from 'antd';
import React from 'react';
import { useNavigate } from 'react-router';
import { useRequest } from '../hooks/useRequest';
import { trpc } from '../api/trpc';
import { setJWT } from '../api/auth';
import { setUserInfo } from '../store/user';
import { useGlobalConfig } from '../hooks/useConfig';
import { useTranslation } from '@i18next-toolkit/react';
export const Login: React.FC = React.memo(() => {
const navigate = useNavigate();
const { t } = useTranslation();
const mutation = trpc.user.login.useMutation();
const [{ loading }, handleLogin] = useRequest(async (values: any) => {
const res = await mutation.mutateAsync({
username: values.username,
password: values.password,
});
setJWT(res.token);
setUserInfo(res.info);
navigate('/dashboard');
});
const { allowRegister } = useGlobalConfig();
return (
<div className="flex h-full w-full items-center justify-center dark:bg-gray-900">
<div className="w-80 -translate-y-1/4">
<div className="text-center">
<img className="h-24 w-24" src="/icon.svg" />
</div>
<Typography.Title className="text-center" level={2}>
Tianji
</Typography.Title>
<Form layout="vertical" disabled={loading} onFinish={handleLogin}>
<Form.Item
label={t('Username')}
name="username"
rules={[{ required: true }]}
>
<Input size="large" />
</Form.Item>
<Form.Item
label={t('Password')}
name="password"
rules={[{ required: true }]}
>
<Input.Password size="large" />
</Form.Item>
<Form.Item>
<Button
type="primary"
size="large"
htmlType="submit"
block={true}
loading={loading}
>
{t('Login')}
</Button>
</Form.Item>
{allowRegister && (
<Form.Item>
<Button
size="large"
htmlType="button"
block={true}
onClick={() => {
navigate('/register');
}}
>
{t('Register')}
</Button>
</Form.Item>
)}
</Form>
</div>
</div>
);
});
Login.displayName = 'Login';

View File

@ -1,26 +0,0 @@
import React from 'react';
import { useNavigate } from 'react-router';
import { useMonitorUpsert } from '../../api/model/monitor';
import { MonitorInfoEditor } from '../../components/monitor/MonitorInfoEditor';
import { useCurrentWorkspaceId } from '../../store/user';
export const MonitorAdd: React.FC = React.memo(() => {
const currentWorkspaceId = useCurrentWorkspaceId()!;
const mutation = useMonitorUpsert();
const navigate = useNavigate();
return (
<div>
<MonitorInfoEditor
onSave={async (value) => {
await mutation.mutateAsync({
...value,
workspaceId: currentWorkspaceId,
});
navigate('/monitor', { replace: true });
}}
/>
</div>
);
});
MonitorAdd.displayName = 'MonitorAdd';

View File

@ -1,19 +0,0 @@
import React from 'react';
import { useParams } from 'react-router';
import { ErrorTip } from '../../components/ErrorTip';
import { MonitorInfo } from '../../components/monitor/MonitorInfo';
export const MonitorDetail: React.FC = React.memo(() => {
const { monitorId } = useParams();
if (!monitorId) {
return <ErrorTip />;
}
return (
<div className="h-full px-2">
<MonitorInfo monitorId={monitorId} />
</div>
);
});
MonitorDetail.displayName = 'MonitorDetail';

View File

@ -1,51 +0,0 @@
import React from 'react';
import { useNavigate, useParams } from 'react-router';
import { useMonitorUpsert } from '../../api/model/monitor';
import { trpc } from '../../api/trpc';
import { ErrorTip } from '../../components/ErrorTip';
import { Loading } from '../../components/Loading';
import {
MonitorInfoEditor,
MonitorInfoEditorValues,
} from '../../components/monitor/MonitorInfoEditor';
import { useCurrentWorkspaceId } from '../../store/user';
export const MonitorEdit: React.FC = React.memo(() => {
const { monitorId } = useParams<{ monitorId: string }>();
const workspaceId = useCurrentWorkspaceId();
const { data: monitor, isLoading } = trpc.monitor.get.useQuery({
monitorId: monitorId!,
workspaceId,
});
const mutation = useMonitorUpsert();
const navigate = useNavigate();
if (isLoading) {
return <Loading />;
}
if (!monitor) {
return <ErrorTip />;
}
return (
<div>
<MonitorInfoEditor
initialValues={
{
...monitor,
notificationIds: monitor.notifications.map((n) => n.id),
} as MonitorInfoEditorValues
}
onSave={async (value) => {
const monitor = await mutation.mutateAsync({
...value,
workspaceId,
});
navigate(`/monitor/${monitor.id}`, { replace: true });
}}
/>
</div>
);
});
MonitorEdit.displayName = 'MonitorEdit';

View File

@ -1,35 +0,0 @@
import React from 'react';
import { useCurrentWorkspaceId } from '../../store/user';
import { trpc } from '../../api/trpc';
import { Card } from 'antd';
import { MonitorEventList } from '../../components/monitor/MonitorEventList';
import { useTranslation } from '@i18next-toolkit/react';
export const MonitorOverview: React.FC = React.memo(() => {
const { t } = useTranslation();
const currentWorkspaceId = useCurrentWorkspaceId()!;
const { data: monitors = [] } = trpc.monitor.all.useQuery({
workspaceId: currentWorkspaceId,
});
return (
<div className="px-2">
<div className="grid grid-cols-2 gap-4">
<Card hoverable={true}>
<div>{t('Monitors')}</div>
<div className="text-2xl font-semibold">{monitors.length}</div>
</Card>
<Card hoverable={true}>
<div>{t('Available')}</div>
<div className="text-2xl font-semibold">
{monitors.filter((m) => m.active).length}
</div>
</Card>
</div>
<div className="py-2">
<MonitorEventList />
</div>
</div>
);
});
MonitorOverview.displayName = 'MonitorOverview';

View File

@ -1,41 +0,0 @@
import React from 'react';
import { useNavigate } from 'react-router';
import { useCurrentWorkspaceId } from '../../store/user';
import { trpc } from '../../api/trpc';
import { useEvent } from '../../hooks/useEvent';
import {
MonitorStatusPageEditForm,
MonitorStatusPageEditFormValues,
} from '../../components/monitor/StatusPage/EditForm';
export const MonitorPageAdd: React.FC = React.memo(() => {
const workspaceId = useCurrentWorkspaceId()!;
const navigate = useNavigate();
const createPageMutation = trpc.monitor.createPage.useMutation();
const trpcUtils = trpc.useContext();
const handleFinish = useEvent(
async (values: MonitorStatusPageEditFormValues) => {
await createPageMutation.mutateAsync({
...values,
workspaceId,
});
trpcUtils.monitor.getAllPages.refetch();
navigate('/monitor/pages');
}
);
return (
<div className="px-8 py-4">
<MonitorStatusPageEditForm
saveButtonLabel="Next"
isLoading={createPageMutation.isLoading}
onFinish={handleFinish}
/>
</div>
);
});
MonitorPageAdd.displayName = 'MonitorPageAdd';

View File

@ -1,68 +0,0 @@
import React from 'react';
import { useNavigate } from 'react-router';
import { useCurrentWorkspaceId } from '../../store/user';
import { trpc } from '../../api/trpc';
import { Button, Card, Popconfirm } from 'antd';
import { DeleteOutlined, EditOutlined, EyeOutlined } from '@ant-design/icons';
import { useEvent } from '../../hooks/useEvent';
import { useTranslation } from '@i18next-toolkit/react';
export const MonitorPageList: React.FC = React.memo(() => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const navigate = useNavigate();
const { data: pages = [], refetch } = trpc.monitor.getAllPages.useQuery({
workspaceId,
});
const deletePageMutation = trpc.monitor.deletePage.useMutation();
const handleDeletePage = useEvent(async (monitorId: string) => {
await deletePageMutation.mutateAsync({
workspaceId,
id: monitorId,
});
refetch();
});
return (
<div className="px-8 py-4">
<Button type="primary" onClick={() => navigate('/monitor/pages/add')}>
{t('New page')}
</Button>
<div className="mt-4 flex flex-col gap-2">
{pages.map((p) => (
<Card bodyStyle={{ padding: 12 }}>
<div className="flex">
<div className="flex-1">{p.title}</div>
<div className="flex gap-2">
<Popconfirm
title={t('Did you sure delete this page?')}
onConfirm={() => handleDeletePage(p.id)}
okButtonProps={{
danger: true,
loading: deletePageMutation.isLoading,
}}
>
<Button icon={<DeleteOutlined />} />
</Popconfirm>
<Button
icon={<EditOutlined />}
onClick={() => navigate(`/status/${p.slug}?edit=1`)}
/>
<Button
icon={<EyeOutlined />}
onClick={() => navigate(`/status/${p.slug}`)}
/>
</div>
</div>
</Card>
))}
</div>
</div>
);
});
MonitorPageList.displayName = 'MonitorPageList';

View File

@ -1,55 +0,0 @@
import React from 'react';
import { Route, Routes, useNavigate } from 'react-router';
import { MonitorList } from '../../components/monitor/MonitorList';
import { MonitorAdd } from './Add';
import { MonitorDetail } from './Detail';
import { MonitorEdit } from './Edit';
import { MonitorOverview } from './Overview';
import { Button } from 'antd';
import { MonitorPageList } from './PageList';
import { MonitorPageAdd } from './PageAdd';
import { useTranslation } from '@i18next-toolkit/react';
export const MonitorPage: React.FC = React.memo(() => {
const { t } = useTranslation();
const navigate = useNavigate();
return (
<div className="flex h-full flex-col">
<div>
<div className="flex gap-4 px-4 pt-4">
<Button
type="primary"
size="large"
onClick={() => navigate('/monitor/add')}
>
{t('Add new Monitor')}
</Button>
<Button
type="default"
size="large"
onClick={() => navigate('/monitor/pages')}
>
{t('Pages')}
</Button>
</div>
</div>
<div className="flex flex-1 overflow-hidden py-5">
<div className="w-5/12 rounded bg-gray-50 dark:bg-gray-800">
<MonitorList />
</div>
<div className="w-7/12">
<Routes>
<Route path="/" element={<MonitorOverview />} />
<Route path="/:monitorId" element={<MonitorDetail />} />
<Route path="/:monitorId/edit" element={<MonitorEdit />} />
<Route path="/add" element={<MonitorAdd />} />
<Route path="/pages" element={<MonitorPageList />} />
<Route path="/pages/add" element={<MonitorPageAdd />} />
</Routes>
</div>
</div>
</div>
);
});
MonitorPage.displayName = 'MonitorPage';

View File

@ -1,67 +0,0 @@
import { Button, Form, Input, Typography } from 'antd';
import React from 'react';
import { useNavigate } from 'react-router';
import { useRequest } from '../hooks/useRequest';
import { trpc } from '../api/trpc';
import { setJWT } from '../api/auth';
import { setUserInfo } from '../store/user';
import { useTranslation } from '@i18next-toolkit/react';
export const Register: React.FC = React.memo(() => {
const { t } = useTranslation();
const navigate = useNavigate();
const mutation = trpc.user.register.useMutation();
const [{ loading }, handleRegister] = useRequest(async (values: any) => {
const res = await mutation.mutateAsync({
username: values.username,
password: values.password,
});
setJWT(res.token);
setUserInfo(res.info);
navigate('/dashboard');
});
return (
<div className="flex h-full w-full items-center justify-center">
<div className="w-80 -translate-y-1/4">
<div className="text-center">
<img className="h-24 w-24" src="/icon.svg" />
</div>
<Typography.Title className="text-center" level={2}>
{t('Register Account')}
</Typography.Title>
<Form layout="vertical" disabled={loading} onFinish={handleRegister}>
<Form.Item
label={t('Username')}
name="username"
rules={[{ required: true }]}
>
<Input size="large" />
</Form.Item>
<Form.Item
label={t('Password')}
name="password"
rules={[{ required: true }]}
>
<Input.Password size="large" />
</Form.Item>
<Form.Item>
<Button
type="primary"
size="large"
htmlType="submit"
block={true}
loading={loading}
>
{t('Register')}
</Button>
</Form.Item>
</Form>
</div>
</div>
);
});
Register.displayName = 'Register';

View File

@ -1,429 +0,0 @@
import React, { useMemo, useRef, useState } from 'react';
import {
Badge,
Button,
Divider,
Empty,
Modal,
Popconfirm,
Steps,
Switch,
Table,
Tabs,
Tooltip,
Typography,
} from 'antd';
import { ColumnsType } from 'antd/es/table';
import { PlusOutlined } from '@ant-design/icons';
import { ServerStatusInfo } from '../../types';
import { useSocketSubscribe } from '../api/socketio';
import { filesize } from 'filesize';
import prettyMilliseconds from 'pretty-ms';
import { UpDownCounter } from '../components/UpDownCounter';
import { max } from 'lodash-es';
import dayjs from 'dayjs';
import { useCurrentWorkspaceId } from '../store/user';
import { useWatch } from '../hooks/useWatch';
import { Loading } from '../components/Loading';
import { without } from 'lodash-es';
import { useIntervalUpdate } from '../hooks/useIntervalUpdate';
import clsx from 'clsx';
import { isServerOnline } from '@tianji/shared';
import { defaultErrorHandler, trpc } from '../api/trpc';
import { useRequest } from '../hooks/useRequest';
import { useTranslation } from '@i18next-toolkit/react';
export const Servers: React.FC = React.memo(() => {
const { t } = useTranslation();
const [isModalOpen, setIsModalOpen] = useState(false);
const [hideOfflineServer, setHideOfflineServer] = useState(false);
const workspaceId = useCurrentWorkspaceId();
const handleOk = () => {
setIsModalOpen(false);
};
const clearOfflineNodeMutation =
trpc.serverStatus.clearOfflineServerStatus.useMutation({
onError: defaultErrorHandler,
});
const [{ loading }, handleClearOfflineNode] = useRequest(async (e) => {
await clearOfflineNodeMutation.mutateAsync({
workspaceId,
});
});
return (
<div>
<div className="flex h-24 items-center">
<div className="flex-1 text-2xl">{t('Servers')}</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-1 text-gray-500">
<Switch
checked={hideOfflineServer}
onChange={setHideOfflineServer}
/>
{t('Hide Offline')}
</div>
<div>
<Popconfirm
title={t('Clear Offline Node')}
description={t('Are you sure to clear all offline node?')}
disabled={loading}
onConfirm={handleClearOfflineNode}
>
<Button size="large" loading={loading}>
{t('Clear Offline')}
</Button>
</Popconfirm>
</div>
<Divider type="vertical" />
<Button
type="primary"
icon={<PlusOutlined />}
size="large"
onClick={() => setIsModalOpen(true)}
>
{t('Add Server')}
</Button>
</div>
</div>
<ServerList hideOfflineServer={hideOfflineServer} />
<Modal
title={t('Add Server')}
open={isModalOpen}
destroyOnClose={true}
okText="Done"
onOk={handleOk}
onCancel={() => setIsModalOpen(false)}
>
<div>
<Tabs
items={[
{
key: 'auto',
label: t('Auto'),
children: <InstallScript />,
},
{
key: 'manual',
label: t('Manual'),
children: <AddServerStep />,
},
]}
/>
</div>
</Modal>
</div>
);
});
Servers.displayName = 'Servers';
function useServerMap(): Record<string, ServerStatusInfo> {
const serverMap = useSocketSubscribe<Record<string, ServerStatusInfo>>(
'onServerStatusUpdate',
{}
);
return serverMap;
}
/**
* @deprecated
*/
export const ServerList: React.FC<{
hideOfflineServer: boolean;
}> = React.memo((props) => {
const { t } = useTranslation();
const serverMap = useServerMap();
const inc = useIntervalUpdate(2 * 1000);
const { hideOfflineServer } = props;
const dataSource = useMemo(
() =>
Object.values(serverMap)
.sort((info) => (isServerOnline(info) ? -1 : 1))
.filter((info) => {
if (hideOfflineServer) {
return isServerOnline(info);
}
return true;
}), // make online server is up and offline is down
[serverMap, inc, hideOfflineServer]
);
const lastUpdatedAt = max(dataSource.map((d) => d.updatedAt));
const columns = useMemo((): ColumnsType<ServerStatusInfo> => {
return [
{
key: 'status',
title: t('Status'),
width: 90,
render: (val, record) => {
return isServerOnline(record) ? (
<Badge status="success" text={t('online')} />
) : (
<Tooltip
title={t('Last online: {{time}}', {
time: dayjs(record.updatedAt).format('YYYY-MM-DD HH:mm:ss'),
})}
>
<Badge status="error" text="offline" />
</Tooltip>
);
},
},
{
dataIndex: 'name',
title: t('Node Name'),
width: 150,
ellipsis: true,
},
{
dataIndex: 'hostname',
title: t('Host Name'),
width: 150,
ellipsis: true,
},
// {
// dataIndex: ['payload', 'system'],
// title: 'System',
// },
{
dataIndex: ['payload', 'uptime'],
title: t('Uptime'),
width: 150,
render: (val) => prettyMilliseconds(Number(val) * 1000),
},
{
dataIndex: ['payload', 'load'],
title: t('Load'),
width: 70,
},
{
key: 'nework',
title: t('Network'),
width: 110,
render: (_, record) => {
return (
<UpDownCounter
up={filesize(record.payload.network_out)}
down={filesize(record.payload.network_in)}
/>
);
},
},
{
key: 'traffic',
title: t('Traffic'),
width: 130,
render: (_, record) => {
return (
<UpDownCounter
up={filesize(record.payload.network_tx) + '/s'}
down={filesize(record.payload.network_rx) + '/s'}
/>
);
},
},
{
dataIndex: ['payload', 'cpu'],
title: 'CPU',
width: 80,
render: (val) => `${val}%`,
},
{
key: 'ram',
title: 'RAM',
width: 120,
render: (_, record) => {
return (
<div className="text-xs">
<div>{filesize(record.payload.memory_used * 1000)} / </div>
<div>{filesize(record.payload.memory_total * 1000)}</div>
</div>
);
},
},
{
key: 'hdd',
title: 'HDD',
width: 120,
render: (_, record) => {
return (
<div className="text-xs">
<div>{filesize(record.payload.hdd_used * 1000 * 1000)} / </div>
<div>{filesize(record.payload.hdd_total * 1000 * 1000)}</div>
</div>
);
},
},
{
dataIndex: 'updatedAt',
title: t('updatedAt'),
width: 130,
render: (val) => {
return dayjs(val).format('MMM D HH:mm:ss');
},
},
];
}, []);
return (
<div>
<div className="text-right text-sm opacity-80">
{t('Last updated at: {{date}}', {
date: dayjs(lastUpdatedAt).format('YYYY-MM-DD HH:mm:ss'),
})}
</div>
<div className="overflow-auto">
<Table
rowKey="hostname"
columns={columns}
dataSource={dataSource}
pagination={false}
locale={{ emptyText: <Empty description={t('No server online')} /> }}
rowClassName={(record) =>
clsx(!isServerOnline(record) && 'opacity-60')
}
/>
</div>
</div>
);
});
ServerList.displayName = 'ServerList';
export const InstallScript: React.FC = React.memo(() => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const command = `curl -o- ${window.location.origin}/serverStatus/${workspaceId}/install.sh?url=${window.location.origin} | bash`;
return (
<div>
<div>{t('Run this command in your linux machine')}</div>
<Typography.Paragraph
copyable={{
format: 'text/plain',
text: command,
}}
className="flex h-[96px] overflow-auto rounded border border-black border-opacity-10 bg-black bg-opacity-5 p-2"
>
<span>{command}</span>
</Typography.Paragraph>
<div>
{t(
'Or you wanna report server status in windows server? switch to Manual tab'
)}
</div>
</div>
);
});
export const AddServerStep: React.FC = React.memo(() => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const [current, setCurrent] = useState(0);
const serverMap = useServerMap();
const [checking, setChecking] = useState(false);
const oldServerMapNames = useRef<string[]>([]);
const [diffServerNames, setDiffServerNames] = useState<string[]>([]);
const allServerNames = useMemo(() => Object.keys(serverMap), [serverMap]);
useWatch([checking], () => {
if (checking === true) {
oldServerMapNames.current = [...allServerNames];
}
});
useWatch([allServerNames], () => {
if (checking === true) {
setDiffServerNames(without(allServerNames, ...oldServerMapNames.current));
}
});
const command = `./tianji-reporter --url ${window.location.origin} --workspace ${workspaceId}`;
return (
<Steps
direction="vertical"
current={current}
items={[
{
title: t('Download Client Reportor'),
description: (
<div>
{t('Download reporter from')}{' '}
<Typography.Link
href="https://github.com/msgbyte/tianji/releases"
target="_blank"
onClick={() => {
if (current === 0) {
setCurrent(1);
setChecking(true);
}
}}
>
{t('Releases Page')}
</Typography.Link>
</div>
),
},
{
title: t('Run'),
description: (
<div>
{t('run reporter with')}:{' '}
<Typography.Text
code={true}
copyable={{ format: 'text/plain', text: command }}
>
{command}
</Typography.Text>
<Button
type="link"
size="small"
disabled={current !== 1}
onClick={() => {
if (current === 1) {
setCurrent(2);
setChecking(true);
}
}}
>
{t('Next step')}
</Button>
</div>
),
},
{
title: t('Waiting for receive report pack'),
description: (
<div>
{diffServerNames.length === 0 || checking === false ? (
<Loading />
) : (
<div>
{t('Is this your servers?')}
{diffServerNames.map((n) => (
<div key={n}>- {n}</div>
))}
</div>
)}
</div>
),
},
]}
/>
);
});
AddServerStep.displayName = 'AddServerStep';

View File

@ -1,118 +0,0 @@
import { Card, Empty, List } from 'antd';
import React, { useMemo, useRef } from 'react';
import { useCurrentWorkspaceId } from '../../store/user';
import { PageHeader } from '../../components/PageHeader';
import { trpc } from '../../api/trpc';
import { useVirtualizer } from '@tanstack/react-virtual';
import { last } from 'lodash-es';
import { useWatch } from '../../hooks/useWatch';
import { ColorTag } from '../../components/ColorTag';
import dayjs from 'dayjs';
import { useTranslation } from '@i18next-toolkit/react';
export const AuditLog: React.FC = React.memo(() => {
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 (
<div>
<PageHeader title={t('Audit Log')} />
<Card>
<List>
<div ref={parentRef} className="h-[560px] 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>
</Card>
</div>
);
});
AuditLog.displayName = 'AuditLog';

View File

@ -1,125 +0,0 @@
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, List, Popconfirm } from 'antd';
import React, { useState } from 'react';
import { trpc } from '../../api/trpc';
import {
NotificationFormValues,
NotificationInfoModal,
} from '../../components/modals/NotificationInfo';
import { NoWorkspaceTip } from '../../components/NoWorkspaceTip';
import { PageHeader } from '../../components/PageHeader';
import { useEvent } from '../../hooks/useEvent';
import { useCurrentWorkspaceId } from '../../store/user';
import { useTranslation } from '@i18next-toolkit/react';
export const NotificationList: React.FC = React.memo(() => {
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();
});
if (!currentWorkspaceId) {
return <NoWorkspaceTip />;
}
return (
<div>
<PageHeader
title={t('Notification List')}
action={
<div>
<Button
type="primary"
icon={<PlusOutlined />}
size="large"
onClick={() => handleOpenModal()}
>
{t('New')}
</Button>
</div>
}
/>
<List
bordered={true}
dataSource={list}
renderItem={(item) => (
<List.Item
actions={[
<Button
icon={<EditOutlined />}
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 danger={true} icon={<DeleteOutlined />} />
</Popconfirm>,
]}
>
<List.Item.Meta title={item.name} />
</List.Item>
)}
/>
<NotificationInfoModal
key={editingFormData?.id}
open={open}
initialValues={editingFormData}
onSubmit={handleSubmit}
onCancel={() => handleCloseModal()}
/>
</div>
);
});
NotificationList.displayName = 'NotificationList';

View File

@ -1,116 +0,0 @@
import { Button, Card, Form, Input, Modal, Typography } from 'antd';
import React, { useState } from 'react';
import { useUserStore } from '../../store/user';
import { PageHeader } from '../../components/PageHeader';
import {
defaultErrorHandler,
defaultSuccessHandler,
trpc,
} from '../../api/trpc';
import { useLogout } from '../../api/model/user';
import { useTranslation } from '@i18next-toolkit/react';
export const Profile: React.FC = React.memo(() => {
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 (
<div>
<PageHeader title={t('Profile')} />
<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>
);
});
Profile.displayName = 'Profile';

View File

@ -1,62 +0,0 @@
import { Card, Statistic } from 'antd';
import React, { useMemo } from 'react';
import { PageHeader } from '../../components/PageHeader';
import { useTranslation } from '@i18next-toolkit/react';
import { trpc } from '../../api/trpc';
import { useCurrentWorkspaceId } from '../../store/user';
import dayjs from 'dayjs';
import { formatNumber } from '../../utils/common';
export const Usage: React.FC = React.memo(() => {
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 (
<div>
<PageHeader title={t('Usage')} />
<Card>
<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>
<div className="flex gap-2">
<Card className="flex-1">
<Statistic
title={t('Website Accepted Count')}
value={formatNumber(data?.websiteAcceptedCount ?? 0)}
/>
</Card>
<Card className="flex-1">
<Statistic
title={t('Website Event Count')}
value={formatNumber(data?.websiteEventCount ?? 0)}
/>
</Card>
<Card className="flex-1">
<Statistic
title={t('Monitor Execution Count')}
value={formatNumber(data?.monitorExecutionCount ?? 0)}
/>
</Card>
</div>
</Card>
</div>
);
});
Usage.displayName = 'Usage';

View File

@ -1,81 +0,0 @@
import { Menu, MenuProps } from 'antd';
import React, { useMemo } from 'react';
import { Routes, Route, useLocation, useNavigate } from 'react-router-dom';
import { WebsiteInfo } from '../../components/website/WebsiteInfo';
import { WebsiteList } from '../../components/website/WebsiteList';
import { useEvent } from '../../hooks/useEvent';
import { NotificationList } from './NotificationList';
import { Profile } from './Profile';
import { AuditLog } from './AuditLog';
import { Trans } from '@i18next-toolkit/react';
import { compact } from 'lodash-es';
import { useGlobalConfig } from '../../hooks/useConfig';
import { Usage } from './Usage';
export const SettingsPage: React.FC = React.memo(() => {
const navigate = useNavigate();
const { pathname } = useLocation();
const { alphaMode } = useGlobalConfig();
const onClick: MenuProps['onClick'] = useEvent((e) => {
navigate(`/settings/${e.key}`);
});
const items: MenuProps['items'] = useMemo(
() =>
compact([
{
key: 'websites',
label: <Trans>Websites</Trans>,
},
{
key: 'notifications',
label: <Trans>Notifications</Trans>,
},
{
key: 'auditLog',
label: <Trans>Audit Log</Trans>,
},
{
key: 'profile',
label: <Trans>Profile</Trans>,
},
alphaMode && {
key: 'usage',
label: <Trans>Usage</Trans>,
},
]),
[alphaMode]
);
const selectedKey =
(items.find((item) => pathname.startsWith(`/settings/${item?.key}`))
?.key as string) ?? 'websites';
return (
<div className="flex h-full">
<div className="w-full pt-10 md:w-1/6">
<Menu
className="h-full"
onClick={onClick}
selectedKeys={[selectedKey]}
mode="vertical"
items={items}
/>
</div>
<div className="w-full px-4 py-2 md:w-5/6">
<Routes>
<Route path="/" element={<WebsiteList />} />
<Route path="/websites" element={<WebsiteList />} />
<Route path="/website/:websiteId" element={<WebsiteInfo />} />
<Route path="/notifications" element={<NotificationList />} />
<Route path="/auditLog" element={<AuditLog />} />
<Route path="/profile" element={<Profile />} />
{alphaMode && <Route path="/usage" element={<Usage />} />}
</Routes>
</div>
</div>
);
});
SettingsPage.displayName = 'SettingsPage';

View File

@ -1,10 +0,0 @@
import React from 'react';
import { useParams } from 'react-router';
import { MonitorStatusPage } from '../../components/monitor/StatusPage';
export const StatusPage: React.FC = React.memo(() => {
const { slug } = useParams<{ slug: string }>();
return <MonitorStatusPage slug={slug!} />;
});
StatusPage.displayName = 'StatusPage';

View File

@ -1,68 +0,0 @@
import { Card } from 'antd';
import React from 'react';
import { useParams } from 'react-router';
import { NotFoundTip } from '../../components/NotFoundTip';
import { useCurrentWorkspaceId } from '../../store/user';
import { TelemetryOverview } from '../../components/telemetry/TelemetryOverview';
import { TelemetryMetricsTable } from '../../components/telemetry/TelemetryMetricsTable';
import { useGlobalRangeDate } from '../../hooks/useGlobalRangeDate';
import { useTranslation } from '@i18next-toolkit/react';
export const TelemetryDetailPage: React.FC = React.memo(() => {
const { telemetryId } = useParams();
const workspaceId = useCurrentWorkspaceId();
const { t } = useTranslation();
const { startDate, endDate } = useGlobalRangeDate();
const startAt = startDate.valueOf();
const endAt = endDate.valueOf();
if (!telemetryId) {
return <NotFoundTip />;
}
return (
<div className="py-6">
<Card>
<Card.Grid hoverable={false} className="!w-full">
<TelemetryOverview
telemetryId={telemetryId}
showDateFilter={true}
workspaceId={workspaceId}
/>
</Card.Grid>
<Card.Grid hoverable={false} className="min-h-[470px] !w-1/3">
<TelemetryMetricsTable
telemetryId={telemetryId}
type="source"
title={[t('Source'), t('Views')]}
startAt={startAt}
endAt={endAt}
/>
</Card.Grid>
<Card.Grid hoverable={false} className="min-h-[470px] !w-1/3">
<TelemetryMetricsTable
telemetryId={telemetryId}
type="event"
title={[t('Events'), t('Views')]}
startAt={startAt}
endAt={endAt}
/>
</Card.Grid>
<Card.Grid hoverable={false} className="min-h-[470px] !w-1/3">
<TelemetryMetricsTable
telemetryId={telemetryId}
type="country"
title={[t('Countries'), t('Visitors')]}
startAt={startAt}
endAt={endAt}
/>
</Card.Grid>
</Card>
</div>
);
});
TelemetryDetailPage.displayName = 'TelemetryDetailPage';

View File

@ -1,14 +0,0 @@
import React from 'react';
import { TelemetryList } from '../../components/telemetry/TelemetryList';
import { Route, Routes } from 'react-router-dom';
import { TelemetryDetailPage } from './Detail';
export const TelemetryPage: React.FC = React.memo(() => {
return (
<Routes>
<Route path="/" element={<TelemetryList />} />
<Route path="/:telemetryId" element={<TelemetryDetailPage />} />
</Routes>
);
});
TelemetryPage.displayName = 'TelemetryPage';

View File

@ -1,123 +0,0 @@
import { Button, Card } from 'antd';
import React from 'react';
import { useNavigate, useParams } from 'react-router';
import { trpc } from '../../api/trpc';
import { ErrorTip } from '../../components/ErrorTip';
import { Loading } from '../../components/Loading';
import { NotFoundTip } from '../../components/NotFoundTip';
import { WebsiteMetricsTable } from '../../components/website/WebsiteMetricsTable';
import { WebsiteOverview } from '../../components/website/WebsiteOverview';
import { useGlobalRangeDate } from '../../hooks/useGlobalRangeDate';
import { useCurrentWorkspaceId } from '../../store/user';
import { RightOutlined } from '@ant-design/icons';
import { useTranslation } from '@i18next-toolkit/react';
export const WebsiteDetail: React.FC = React.memo(() => {
const { t } = useTranslation();
const { websiteId } = useParams();
const workspaceId = useCurrentWorkspaceId();
const { data: website, isLoading } = trpc.website.info.useQuery({
workspaceId,
websiteId: websiteId!,
});
const { startDate, endDate } = useGlobalRangeDate();
const navigate = useNavigate();
if (!websiteId) {
return <ErrorTip />;
}
if (isLoading) {
return <Loading />;
}
if (!website) {
return <NotFoundTip />;
}
const startAt = startDate.unix() * 1000;
const endAt = endDate.unix() * 1000;
return (
<div className="py-6">
<Card>
<Card.Grid hoverable={false} className="!w-full">
<WebsiteOverview website={website} showDateFilter={true} />
</Card.Grid>
<Card.Grid hoverable={false} className="min-h-[470px] !w-1/2">
<WebsiteMetricsTable
websiteId={websiteId}
type="url"
title={[t('Pages'), t('Views')]}
startAt={startAt}
endAt={endAt}
/>
</Card.Grid>
<Card.Grid hoverable={false} className="min-h-[470px] !w-1/2">
<WebsiteMetricsTable
websiteId={websiteId}
type="referrer"
title={[t('Referrers'), t('Views')]}
startAt={startAt}
endAt={endAt}
/>
</Card.Grid>
<Card.Grid hoverable={false} className="min-h-[470px] !w-1/3">
<WebsiteMetricsTable
websiteId={websiteId}
type="browser"
title={[t('Browser'), t('Visitors')]}
startAt={startAt}
endAt={endAt}
/>
</Card.Grid>
<Card.Grid hoverable={false} className="min-h-[470px] !w-1/3">
<WebsiteMetricsTable
websiteId={websiteId}
type="os"
title={[t('OS'), t('Visitors')]}
startAt={startAt}
endAt={endAt}
/>
</Card.Grid>
<Card.Grid hoverable={false} className="min-h-[470px] !w-1/3">
<WebsiteMetricsTable
websiteId={websiteId}
type="device"
title={[t('Devices'), t('Visitors')]}
startAt={startAt}
endAt={endAt}
/>
</Card.Grid>
<Card.Grid hoverable={false} className="min-h-[470px] !w-1/2">
<WebsiteMetricsTable
websiteId={websiteId}
type="country"
title={[t('Countries'), t('Visitors')]}
startAt={startAt}
endAt={endAt}
/>
<Button
size="small"
className="m-auto mt-1 flex flex-row-reverse items-center"
styles={{ icon: { marginRight: 0, marginLeft: 8 } }}
icon={<RightOutlined className="m-0" />}
onClick={() => navigate(`/website/${websiteId}/map`)}
>
{t('Visitor Map')}
</Button>
</Card.Grid>
<Card.Grid hoverable={false} className="min-h-[470px] !w-1/2">
<WebsiteMetricsTable
websiteId={websiteId}
type="event"
title={[t('Events'), t('Actions')]}
startAt={startAt}
endAt={endAt}
/>
</Card.Grid>
</Card>
</div>
);
});
WebsiteDetail.displayName = 'WebsiteDetail';

View File

@ -1,54 +0,0 @@
import React from 'react';
import { useNavigate, useParams } from 'react-router';
import { trpc } from '../../api/trpc';
import { ErrorTip } from '../../components/ErrorTip';
import { Loading } from '../../components/Loading';
import { NotFoundTip } from '../../components/NotFoundTip';
import { useCurrentWorkspaceId } from '../../store/user';
import { WebsiteVisitorMap } from '../../components/website/WebsiteVisitorMap';
import { DateFilter } from '../../components/DateFilter';
import { LeftOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import { useTranslation } from '@i18next-toolkit/react';
export const WebsiteVisitorMapPage: React.FC = React.memo(() => {
const { websiteId } = useParams();
const workspaceId = useCurrentWorkspaceId();
const { t } = useTranslation();
const { data: website, isLoading } = trpc.website.info.useQuery({
workspaceId,
websiteId: websiteId!,
});
const navigate = useNavigate();
if (!websiteId) {
return <ErrorTip />;
}
if (isLoading) {
return <Loading />;
}
if (!website) {
return <NotFoundTip />;
}
return (
<div className="py-6">
<div className="flex items-center justify-between pb-2">
<Button
size="large"
icon={<LeftOutlined />}
onClick={() => navigate(`/website/${websiteId}`)}
/>
<div>
<span className="font-bold">{website.name}</span>
{t("'s visitor map")}
</div>
<DateFilter />
</div>
<WebsiteVisitorMap websiteId={websiteId} />
</div>
);
});
WebsiteVisitorMapPage.displayName = 'WebsiteVisitorMapPage';

View File

@ -1,18 +0,0 @@
import React from 'react';
import { Route, Routes } from 'react-router';
import { WebsiteList } from '../../components/website/WebsiteList';
import { WebsiteDetail } from './Detail';
import { WebsiteVisitorMapPage } from './Map';
export const WebsitePage: React.FC = React.memo(() => {
return (
<div className="h-full">
<Routes>
<Route path="/" element={<WebsiteList />} />
<Route path="/:websiteId" element={<WebsiteDetail />} />
<Route path="/:websiteId/map" element={<WebsiteVisitorMapPage />} />
</Routes>
</div>
);
});
WebsitePage.displayName = 'WebsitePage';

View File

@ -1,4 +1,4 @@
import { LayoutHeader } from '@/pages/Layout/Header'; import { LayoutHeader } from '@/components/layout/Header';
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'; import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
// import { TanStackRouterDevtools } from '@tanstack/router-devtools'; // import { TanStackRouterDevtools } from '@tanstack/router-devtools';
import { Suspense } from 'react'; import { Suspense } from 'react';

View File

@ -6,7 +6,7 @@ import { MonitorHealthBar } from '@/components/monitor/MonitorHealthBar';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useDataReady } from '@/hooks/useDataReady'; import { useDataReady } from '@/hooks/useDataReady';
import { useEvent } from '@/hooks/useEvent'; import { useEvent } from '@/hooks/useEvent';
import { LayoutV2 } from '@/pages/LayoutV2'; import { Layout } from '@/components/layout';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { cn } from '@/utils/style'; import { cn } from '@/utils/style';
@ -71,7 +71,7 @@ function MonitorComponent() {
}); });
return ( return (
<LayoutV2 <Layout
list={ list={
<CommonWrapper <CommonWrapper
header={ header={

View File

@ -5,7 +5,7 @@ import { CommonWrapper } from '@/components/CommonWrapper';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useDataReady } from '@/hooks/useDataReady'; import { useDataReady } from '@/hooks/useDataReady';
import { useEvent } from '@/hooks/useEvent'; import { useEvent } from '@/hooks/useEvent';
import { LayoutV2 } from '@/pages/LayoutV2'; import { Layout } from '@/components/layout';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { cn } from '@/utils/style'; import { cn } from '@/utils/style';
@ -61,7 +61,7 @@ function PageComponent() {
}); });
return ( return (
<LayoutV2 <Layout
list={ list={
<CommonWrapper <CommonWrapper
header={ header={

View File

@ -1,6 +1,8 @@
import { defaultErrorHandler, trpc } from '@/api/trpc'; import { defaultErrorHandler, trpc } from '@/api/trpc';
import { CommonHeader } from '@/components/CommonHeader'; import { CommonHeader } from '@/components/CommonHeader';
import { CommonWrapper } from '@/components/CommonWrapper'; import { CommonWrapper } from '@/components/CommonWrapper';
import { AddServerStep } from '@/components/server/AddServerStep';
import { InstallScript } from '@/components/server/InstallScript';
import { ServerList } from '@/components/server/ServerList'; import { ServerList } from '@/components/server/ServerList';
import { import {
AlertDialog, AlertDialog,
@ -14,8 +16,7 @@ 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';
import { LayoutV2 } from '@/pages/LayoutV2'; import { Layout } from '@/components/layout';
import { AddServerStep, InstallScript } from '@/pages/Servers';
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';
@ -32,9 +33,9 @@ export const Route = createFileRoute('/server')({
function ServerComponent() { function ServerComponent() {
return ( return (
<LayoutV2> <Layout>
<ServerContent /> <ServerContent />
</LayoutV2> </Layout>
); );
} }

View File

@ -1,66 +1,5 @@
import { CommonHeader } from '@/components/CommonHeader'; import { createFileRoute } from '@tanstack/react-router'
import { CommonList } from '@/components/CommonList';
import { CommonWrapper } from '@/components/CommonWrapper';
import { LayoutV2 } from '@/pages/LayoutV2';
import { routeAuthBeforeLoad } from '@/utils/route';
import { useTranslation } from '@i18next-toolkit/react';
import {
createFileRoute,
useNavigate,
useRouterState,
} from '@tanstack/react-router';
import { useEffect } from 'react';
export const Route = createFileRoute('/settings')({ export const Route = createFileRoute('/settings')({
beforeLoad: routeAuthBeforeLoad, component: () => <div>Hello /settings!</div>
component: PageComponent, })
});
function PageComponent() {
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>
}
/>
);
}

View File

@ -5,7 +5,7 @@ import { CommonWrapper } from '@/components/CommonWrapper';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useDataReady } from '@/hooks/useDataReady'; import { useDataReady } from '@/hooks/useDataReady';
import { useEvent } from '@/hooks/useEvent'; import { useEvent } from '@/hooks/useEvent';
import { LayoutV2 } from '@/pages/LayoutV2'; import { Layout } from '@/components/layout';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { cn } from '@/utils/style'; import { cn } from '@/utils/style';
@ -64,7 +64,7 @@ function PageComponent() {
}); });
return ( return (
<LayoutV2 <Layout
list={ list={
<CommonWrapper <CommonWrapper
header={ header={

View File

@ -5,7 +5,7 @@ import { CommonWrapper } from '@/components/CommonWrapper';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useDataReady } from '@/hooks/useDataReady'; import { useDataReady } from '@/hooks/useDataReady';
import { useEvent } from '@/hooks/useEvent'; import { useEvent } from '@/hooks/useEvent';
import { LayoutV2 } from '@/pages/LayoutV2'; import { Layout } from '@/components/layout';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { cn } from '@/utils/style'; import { cn } from '@/utils/style';
@ -64,7 +64,7 @@ function TelemetryComponent() {
}); });
return ( return (
<LayoutV2 <Layout
list={ list={
<CommonWrapper <CommonWrapper
header={ header={

View File

@ -5,7 +5,7 @@ import { CommonWrapper } from '@/components/CommonWrapper';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useDataReady } from '@/hooks/useDataReady'; import { useDataReady } from '@/hooks/useDataReady';
import { useEvent } from '@/hooks/useEvent'; import { useEvent } from '@/hooks/useEvent';
import { LayoutV2 } from '@/pages/LayoutV2'; import { Layout } from '@/components/layout';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { cn } from '@/utils/style'; import { cn } from '@/utils/style';
@ -62,7 +62,7 @@ function WebsiteComponent() {
}); });
return ( return (
<LayoutV2 <Layout
list={ list={
<CommonWrapper <CommonWrapper
header={ header={