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 { 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';
|
||||||
|
@ -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={
|
||||||
|
@ -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={
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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={
|
||||||
|
@ -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={
|
||||||
|
@ -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={
|
||||||
|
Loading…
Reference in New Issue
Block a user