feat: add timezone support #114
This commit is contained in:
parent
83850f2981
commit
c7e20df516
@ -38,13 +38,22 @@ import {
|
|||||||
} from '@/components/ui/form';
|
} from '@/components/ui/form';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useEventWithLoading } from '@/hooks/useEvent';
|
import { useEvent, useEventWithLoading } from '@/hooks/useEvent';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { AlertConfirm } from '@/components/AlertConfirm';
|
import { AlertConfirm } from '@/components/AlertConfirm';
|
||||||
import { ROLES } from '@tianji/shared';
|
import { ROLES } from '@tianji/shared';
|
||||||
import { cn } from '@/utils/style';
|
import { cn } from '@/utils/style';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { getTimezoneList } from '@/utils/date';
|
||||||
|
|
||||||
export const Route = createFileRoute('/settings/workspace')({
|
export const Route = createFileRoute('/settings/workspace')({
|
||||||
beforeLoad: routeAuthBeforeLoad,
|
beforeLoad: routeAuthBeforeLoad,
|
||||||
@ -62,7 +71,7 @@ const columnHelper = createColumnHelper<MemberInfo>();
|
|||||||
|
|
||||||
function PageComponent() {
|
function PageComponent() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { id: workspaceId, name, role } = useCurrentWorkspace();
|
const { id: workspaceId, name, role, settings } = useCurrentWorkspace();
|
||||||
const hasAdminPermission = useHasAdminPermission();
|
const hasAdminPermission = useHasAdminPermission();
|
||||||
const { data: members = [], refetch: refetchMembers } =
|
const { data: members = [], refetch: refetchMembers } =
|
||||||
trpc.workspace.members.useQuery({
|
trpc.workspace.members.useQuery({
|
||||||
@ -71,6 +80,9 @@ function PageComponent() {
|
|||||||
const updateCurrentWorkspaceName = useUserStore(
|
const updateCurrentWorkspaceName = useUserStore(
|
||||||
(state) => state.updateCurrentWorkspaceName
|
(state) => state.updateCurrentWorkspaceName
|
||||||
);
|
);
|
||||||
|
const updateCurrentWorkspaceSettings = useUserStore(
|
||||||
|
(state) => state.updateCurrentWorkspaceSettings
|
||||||
|
);
|
||||||
const form = useForm<InviteFormValues>({
|
const form = useForm<InviteFormValues>({
|
||||||
resolver: zodResolver(inviteFormSchema),
|
resolver: zodResolver(inviteFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -89,6 +101,10 @@ function PageComponent() {
|
|||||||
onSuccess: defaultSuccessHandler,
|
onSuccess: defaultSuccessHandler,
|
||||||
onError: defaultErrorHandler,
|
onError: defaultErrorHandler,
|
||||||
});
|
});
|
||||||
|
const updateSettings = trpc.workspace.updateSettings.useMutation({
|
||||||
|
onSuccess: defaultSuccessHandler,
|
||||||
|
onError: defaultErrorHandler,
|
||||||
|
});
|
||||||
|
|
||||||
const [renameWorkspaceName, setRenameWorkspaceName] = useState('');
|
const [renameWorkspaceName, setRenameWorkspaceName] = useState('');
|
||||||
const [handleRename, isRenameLoading] = useEventWithLoading(async () => {
|
const [handleRename, isRenameLoading] = useEventWithLoading(async () => {
|
||||||
@ -112,6 +128,19 @@ function PageComponent() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleUpdateSettings = useEvent(async (key: string, value: string) => {
|
||||||
|
const { settings } = await updateSettings.mutateAsync({
|
||||||
|
workspaceId,
|
||||||
|
settings: {
|
||||||
|
[key]: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
updateCurrentWorkspaceSettings(settings);
|
||||||
|
});
|
||||||
|
|
||||||
|
const timezoneList = useMemo(() => getTimezoneList(), []);
|
||||||
|
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
columnHelper.accessor(
|
columnHelper.accessor(
|
||||||
@ -167,6 +196,36 @@ function PageComponent() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="text-lg font-bold">
|
||||||
|
{t('General')}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex-1">{t('Timezone')}</div>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
value={settings['timezone'] ?? dayjs.tz.guess()}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
handleUpdateSettings('timezone', value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[240px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{timezoneList.map((item) => (
|
||||||
|
<SelectItem key={item.value} value={item.value}>
|
||||||
|
{item.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(handleInvite)}
|
onSubmit={form.handleSubmit(handleInvite)}
|
||||||
|
1678
src/client/utils/__snapshots__/date.spec.ts.snap
Normal file
1678
src/client/utils/__snapshots__/date.spec.ts.snap
Normal file
File diff suppressed because it is too large
Load Diff
10
src/client/utils/date.spec.ts
Normal file
10
src/client/utils/date.spec.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
import { getTimezoneList } from './date';
|
||||||
|
|
||||||
|
describe('getTimezoneList', () => {
|
||||||
|
test('should return timezone list with correct labels and values', () => {
|
||||||
|
const result = getTimezoneList();
|
||||||
|
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
@ -68,3 +68,25 @@ export function formatDateWithUnit(val: dayjs.ConfigType, unit: DateUnit) {
|
|||||||
|
|
||||||
return formatDate(val);
|
return formatDate(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatOffset(offset: number) {
|
||||||
|
const sign = offset >= 0 ? '+' : '-';
|
||||||
|
const absOffset = Math.abs(offset);
|
||||||
|
const hours = String(Math.floor(absOffset / 60)).padStart(2, '0');
|
||||||
|
const minutes = String(absOffset % 60).padStart(2, '0');
|
||||||
|
|
||||||
|
return `${sign}${hours}:${minutes}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTimezoneList() {
|
||||||
|
const timezones = Intl.supportedValuesOf('timeZone');
|
||||||
|
|
||||||
|
return timezones.map((timezone) => {
|
||||||
|
const offset = dayjs().tz(timezone).utcOffset();
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: `${timezone} (${formatOffset(offset)})`,
|
||||||
|
value: timezone,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
import { version } from '@tianji/shared';
|
import { version } from '@tianji/shared';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import utc from 'dayjs/plugin/utc.js';
|
||||||
|
import timezone from 'dayjs/plugin/timezone.js';
|
||||||
|
|
||||||
axios.defaults.headers.common['User-Agent'] = `tianji/${version}`;
|
axios.defaults.headers.common['User-Agent'] = `tianji/${version}`;
|
||||||
|
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
(BigInt.prototype as any).toJSON = function () {
|
(BigInt.prototype as any).toJSON = function () {
|
||||||
const int = Number.parseInt(this.toString());
|
const int = Number.parseInt(this.toString());
|
||||||
return int ?? this.toString();
|
return int ?? this.toString();
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Monitor, Notification } from '@prisma/client';
|
import { Monitor } from '@prisma/client';
|
||||||
import { prisma } from '../_client.js';
|
import { prisma } from '../_client.js';
|
||||||
import { MonitorRunner } from './runner.js';
|
import { MonitorRunner } from './runner.js';
|
||||||
import { logger } from '../../utils/logger.js';
|
import { logger } from '../../utils/logger.js';
|
||||||
|
import { MonitorWithNotification } from './types.js';
|
||||||
|
|
||||||
export type MonitorUpsertData = Pick<
|
export type MonitorUpsertData = Pick<
|
||||||
Monitor,
|
Monitor,
|
||||||
@ -13,8 +14,6 @@ export type MonitorUpsertData = Pick<
|
|||||||
payload: Record<string, any>;
|
payload: Record<string, any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MonitorWithNotification = Monitor & { notifications: Notification[] };
|
|
||||||
|
|
||||||
export class MonitorManager {
|
export class MonitorManager {
|
||||||
private monitorRunner: Record<string, MonitorRunner> = {};
|
private monitorRunner: Record<string, MonitorRunner> = {};
|
||||||
private isStarted = false;
|
private isStarted = false;
|
||||||
@ -64,9 +63,7 @@ export class MonitorManager {
|
|||||||
delete this.monitorRunner[monitor.id];
|
delete this.monitorRunner[monitor.id];
|
||||||
}
|
}
|
||||||
|
|
||||||
const runner = (this.monitorRunner[monitor.id] = new MonitorRunner(
|
const runner = await this.createRunner(monitor);
|
||||||
monitor
|
|
||||||
));
|
|
||||||
runner.startMonitor();
|
runner.startMonitor();
|
||||||
|
|
||||||
return monitor;
|
return monitor;
|
||||||
@ -112,7 +109,7 @@ export class MonitorManager {
|
|||||||
Promise.all(
|
Promise.all(
|
||||||
monitors.map(async (m) => {
|
monitors.map(async (m) => {
|
||||||
try {
|
try {
|
||||||
const runner = new MonitorRunner(m);
|
const runner = await this.createRunner(m);
|
||||||
this.monitorRunner[m.id] = runner;
|
this.monitorRunner[m.id] = runner;
|
||||||
await runner.startMonitor();
|
await runner.startMonitor();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -128,8 +125,32 @@ export class MonitorManager {
|
|||||||
return this.monitorRunner[monitorId];
|
return this.monitorRunner[monitorId];
|
||||||
}
|
}
|
||||||
|
|
||||||
createRunner(monitor: MonitorWithNotification) {
|
/**
|
||||||
|
* Restart all runner basic on workspace id
|
||||||
|
*/
|
||||||
|
restartWithWorkspaceId(workspaceId: string) {
|
||||||
|
Object.values(this.monitorRunner).map((runner) => {
|
||||||
|
if (runner.workspace.id === workspaceId) {
|
||||||
|
this.createRunner(runner.monitor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create runner
|
||||||
|
*/
|
||||||
|
async createRunner(monitor: MonitorWithNotification) {
|
||||||
|
if (this.monitorRunner[monitor.id]) {
|
||||||
|
this.monitorRunner[monitor.id].stopMonitor();
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspace = await prisma.workspace.findUniqueOrThrow({
|
||||||
|
where: {
|
||||||
|
id: monitor.workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
const runner = (this.monitorRunner[monitor.id] = new MonitorRunner(
|
const runner = (this.monitorRunner[monitor.id] = new MonitorRunner(
|
||||||
|
workspace,
|
||||||
monitor
|
monitor
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Monitor, Notification } from '@prisma/client';
|
import { Notification, Workspace } from '@prisma/client';
|
||||||
import { subscribeEventBus } from '../../ws/shared.js';
|
import { subscribeEventBus } from '../../ws/shared.js';
|
||||||
import { prisma } from '../_client.js';
|
import { prisma } from '../_client.js';
|
||||||
import { monitorProviders } from './provider/index.js';
|
import { monitorProviders } from './provider/index.js';
|
||||||
@ -8,6 +8,8 @@ import { logger } from '../../utils/logger.js';
|
|||||||
import { token } from '../notification/token/index.js';
|
import { token } from '../notification/token/index.js';
|
||||||
import { ContentToken } from '../notification/token/type.js';
|
import { ContentToken } from '../notification/token/type.js';
|
||||||
import { createAuditLog } from '../auditLog.js';
|
import { createAuditLog } from '../auditLog.js';
|
||||||
|
import { MonitorWithNotification } from './types.js';
|
||||||
|
import { get } from 'lodash-es';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class which actually run monitor data collect
|
* Class which actually run monitor data collect
|
||||||
@ -17,7 +19,14 @@ export class MonitorRunner {
|
|||||||
timer: NodeJS.Timeout | null = null;
|
timer: NodeJS.Timeout | null = null;
|
||||||
retriedNum = 0;
|
retriedNum = 0;
|
||||||
|
|
||||||
constructor(public monitor: Monitor & { notifications: Notification[] }) {}
|
constructor(
|
||||||
|
public workspace: Workspace,
|
||||||
|
public monitor: MonitorWithNotification
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getTimezone(): string {
|
||||||
|
return get(this.workspace, ['settings', 'timezone']) || 'utc';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start single monitor
|
* Start single monitor
|
||||||
@ -74,9 +83,9 @@ export class MonitorRunner {
|
|||||||
);
|
);
|
||||||
await this.notify(`[${monitor.name}] 🔴 Down`, [
|
await this.notify(`[${monitor.name}] 🔴 Down`, [
|
||||||
token.text(
|
token.text(
|
||||||
`[${monitor.name}] 🔴 Down\nTime: ${dayjs().format(
|
`[${monitor.name}] 🔴 Down\nTime: ${dayjs()
|
||||||
'YYYY-MM-DD HH:mm:ss (z)'
|
.tz(this.getTimezone())
|
||||||
)}`
|
.format('YYYY-MM-DD HH:mm:ss (z)')}`
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
currentStatus = 'DOWN';
|
currentStatus = 'DOWN';
|
||||||
@ -88,9 +97,9 @@ export class MonitorRunner {
|
|||||||
);
|
);
|
||||||
await this.notify(`[${monitor.name}] ✅ Up`, [
|
await this.notify(`[${monitor.name}] ✅ Up`, [
|
||||||
token.text(
|
token.text(
|
||||||
`[${monitor.name}] ✅ Up\nTime: ${dayjs().format(
|
`[${monitor.name}] ✅ Up\nTime: ${dayjs()
|
||||||
'YYYY-MM-DD HH:mm:ss (z)'
|
.tz(this.getTimezone())
|
||||||
)}`
|
.format('YYYY-MM-DD HH:mm:ss (z)')}`
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
currentStatus = 'UP';
|
currentStatus = 'UP';
|
||||||
|
5
src/server/model/monitor/types.ts
Normal file
5
src/server/model/monitor/types.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { Monitor, Notification } from '@prisma/client';
|
||||||
|
|
||||||
|
export type MonitorWithNotification = Monitor & {
|
||||||
|
notifications: Notification[];
|
||||||
|
};
|
@ -292,7 +292,7 @@ export const monitorRouter = router({
|
|||||||
});
|
});
|
||||||
let runner = monitorManager.getRunner(monitorId);
|
let runner = monitorManager.getRunner(monitorId);
|
||||||
if (!runner) {
|
if (!runner) {
|
||||||
runner = monitorManager.createRunner(monitor);
|
runner = await monitorManager.createRunner(monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (active === true) {
|
if (active === true) {
|
||||||
|
@ -270,6 +270,15 @@ export const workspaceRouter = router({
|
|||||||
settings: merge({}, prev.settings, settings),
|
settings: merge({}, prev.settings, settings),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
'timezone' in settings &&
|
||||||
|
get(prev, ['settings', 'timezone']) !== settings.timezone
|
||||||
|
) {
|
||||||
|
// should be restart all monitor
|
||||||
|
monitorManager.restartWithWorkspaceId(workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}),
|
}),
|
||||||
invite: workspaceAdminProcedure
|
invite: workspaceAdminProcedure
|
||||||
|
Loading…
Reference in New Issue
Block a user