refactor: remove unused code
This commit is contained in:
parent
52a89276c8
commit
328a4e856c
16
src/client/components/layout/index.tsx
Normal file
16
src/client/components/layout/index.tsx
Normal 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';
|
118
src/client/components/server/AddServerStep.tsx
Normal file
118
src/client/components/server/AddServerStep.tsx
Normal 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';
|
33
src/client/components/server/InstallScript.tsx
Normal file
33
src/client/components/server/InstallScript.tsx
Normal 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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -1,4 +1,4 @@
|
||||
import { LayoutHeader } from '@/pages/Layout/Header';
|
||||
import { LayoutHeader } from '@/components/layout/Header';
|
||||
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
|
||||
// import { TanStackRouterDevtools } from '@tanstack/router-devtools';
|
||||
import { Suspense } from 'react';
|
||||
|
@ -6,7 +6,7 @@ import { MonitorHealthBar } from '@/components/monitor/MonitorHealthBar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useDataReady } from '@/hooks/useDataReady';
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { LayoutV2 } from '@/pages/LayoutV2';
|
||||
import { Layout } from '@/components/layout';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
import { cn } from '@/utils/style';
|
||||
@ -71,7 +71,7 @@ function MonitorComponent() {
|
||||
});
|
||||
|
||||
return (
|
||||
<LayoutV2
|
||||
<Layout
|
||||
list={
|
||||
<CommonWrapper
|
||||
header={
|
||||
|
@ -5,7 +5,7 @@ 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 { Layout } from '@/components/layout';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
import { cn } from '@/utils/style';
|
||||
@ -61,7 +61,7 @@ function PageComponent() {
|
||||
});
|
||||
|
||||
return (
|
||||
<LayoutV2
|
||||
<Layout
|
||||
list={
|
||||
<CommonWrapper
|
||||
header={
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { defaultErrorHandler, trpc } from '@/api/trpc';
|
||||
import { CommonHeader } from '@/components/CommonHeader';
|
||||
import { CommonWrapper } from '@/components/CommonWrapper';
|
||||
import { AddServerStep } from '@/components/server/AddServerStep';
|
||||
import { InstallScript } from '@/components/server/InstallScript';
|
||||
import { ServerList } from '@/components/server/ServerList';
|
||||
import {
|
||||
AlertDialog,
|
||||
@ -14,8 +16,7 @@ import { Separator } from '@/components/ui/separator';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useEventWithLoading } from '@/hooks/useEvent';
|
||||
import { LayoutV2 } from '@/pages/LayoutV2';
|
||||
import { AddServerStep, InstallScript } from '@/pages/Servers';
|
||||
import { Layout } from '@/components/layout';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
@ -32,9 +33,9 @@ export const Route = createFileRoute('/server')({
|
||||
|
||||
function ServerComponent() {
|
||||
return (
|
||||
<LayoutV2>
|
||||
<Layout>
|
||||
<ServerContent />
|
||||
</LayoutV2>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,66 +1,5 @@
|
||||
import { CommonHeader } from '@/components/CommonHeader';
|
||||
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';
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/settings')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
component: () => <div>Hello /settings!</div>
|
||||
})
|
@ -5,7 +5,7 @@ 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 { Layout } from '@/components/layout';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
import { cn } from '@/utils/style';
|
||||
@ -64,7 +64,7 @@ function PageComponent() {
|
||||
});
|
||||
|
||||
return (
|
||||
<LayoutV2
|
||||
<Layout
|
||||
list={
|
||||
<CommonWrapper
|
||||
header={
|
||||
|
@ -5,7 +5,7 @@ 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 { Layout } from '@/components/layout';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
import { cn } from '@/utils/style';
|
||||
@ -64,7 +64,7 @@ function TelemetryComponent() {
|
||||
});
|
||||
|
||||
return (
|
||||
<LayoutV2
|
||||
<Layout
|
||||
list={
|
||||
<CommonWrapper
|
||||
header={
|
||||
|
@ -5,7 +5,7 @@ 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 { Layout } from '@/components/layout';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
import { cn } from '@/utils/style';
|
||||
@ -62,7 +62,7 @@ function WebsiteComponent() {
|
||||
});
|
||||
|
||||
return (
|
||||
<LayoutV2
|
||||
<Layout
|
||||
list={
|
||||
<CommonWrapper
|
||||
header={
|
||||
|
Loading…
Reference in New Issue
Block a user