feat: add timezone support #114
This commit is contained in:
parent
83850f2981
commit
c7e20df516
@ -38,13 +38,22 @@ import {
|
||||
} from '@/components/ui/form';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useEventWithLoading } from '@/hooks/useEvent';
|
||||
import { useEvent, useEventWithLoading } from '@/hooks/useEvent';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { AlertConfirm } from '@/components/AlertConfirm';
|
||||
import { ROLES } from '@tianji/shared';
|
||||
import { cn } from '@/utils/style';
|
||||
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')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
@ -62,7 +71,7 @@ const columnHelper = createColumnHelper<MemberInfo>();
|
||||
|
||||
function PageComponent() {
|
||||
const { t } = useTranslation();
|
||||
const { id: workspaceId, name, role } = useCurrentWorkspace();
|
||||
const { id: workspaceId, name, role, settings } = useCurrentWorkspace();
|
||||
const hasAdminPermission = useHasAdminPermission();
|
||||
const { data: members = [], refetch: refetchMembers } =
|
||||
trpc.workspace.members.useQuery({
|
||||
@ -71,6 +80,9 @@ function PageComponent() {
|
||||
const updateCurrentWorkspaceName = useUserStore(
|
||||
(state) => state.updateCurrentWorkspaceName
|
||||
);
|
||||
const updateCurrentWorkspaceSettings = useUserStore(
|
||||
(state) => state.updateCurrentWorkspaceSettings
|
||||
);
|
||||
const form = useForm<InviteFormValues>({
|
||||
resolver: zodResolver(inviteFormSchema),
|
||||
defaultValues: {
|
||||
@ -89,6 +101,10 @@ function PageComponent() {
|
||||
onSuccess: defaultSuccessHandler,
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
const updateSettings = trpc.workspace.updateSettings.useMutation({
|
||||
onSuccess: defaultSuccessHandler,
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
|
||||
const [renameWorkspaceName, setRenameWorkspaceName] = useState('');
|
||||
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(() => {
|
||||
return [
|
||||
columnHelper.accessor(
|
||||
@ -167,6 +196,36 @@ function PageComponent() {
|
||||
</CardContent>
|
||||
</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
|
||||
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);
|
||||
}
|
||||
|
||||
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 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}`;
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
(BigInt.prototype as any).toJSON = function () {
|
||||
const int = Number.parseInt(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 { MonitorRunner } from './runner.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
import { MonitorWithNotification } from './types.js';
|
||||
|
||||
export type MonitorUpsertData = Pick<
|
||||
Monitor,
|
||||
@ -13,8 +14,6 @@ export type MonitorUpsertData = Pick<
|
||||
payload: Record<string, any>;
|
||||
};
|
||||
|
||||
type MonitorWithNotification = Monitor & { notifications: Notification[] };
|
||||
|
||||
export class MonitorManager {
|
||||
private monitorRunner: Record<string, MonitorRunner> = {};
|
||||
private isStarted = false;
|
||||
@ -64,9 +63,7 @@ export class MonitorManager {
|
||||
delete this.monitorRunner[monitor.id];
|
||||
}
|
||||
|
||||
const runner = (this.monitorRunner[monitor.id] = new MonitorRunner(
|
||||
monitor
|
||||
));
|
||||
const runner = await this.createRunner(monitor);
|
||||
runner.startMonitor();
|
||||
|
||||
return monitor;
|
||||
@ -112,7 +109,7 @@ export class MonitorManager {
|
||||
Promise.all(
|
||||
monitors.map(async (m) => {
|
||||
try {
|
||||
const runner = new MonitorRunner(m);
|
||||
const runner = await this.createRunner(m);
|
||||
this.monitorRunner[m.id] = runner;
|
||||
await runner.startMonitor();
|
||||
} catch (err) {
|
||||
@ -128,8 +125,32 @@ export class MonitorManager {
|
||||
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(
|
||||
workspace,
|
||||
monitor
|
||||
));
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Monitor, Notification } from '@prisma/client';
|
||||
import { Notification, Workspace } from '@prisma/client';
|
||||
import { subscribeEventBus } from '../../ws/shared.js';
|
||||
import { prisma } from '../_client.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 { ContentToken } from '../notification/token/type.js';
|
||||
import { createAuditLog } from '../auditLog.js';
|
||||
import { MonitorWithNotification } from './types.js';
|
||||
import { get } from 'lodash-es';
|
||||
|
||||
/**
|
||||
* Class which actually run monitor data collect
|
||||
@ -17,7 +19,14 @@ export class MonitorRunner {
|
||||
timer: NodeJS.Timeout | null = null;
|
||||
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
|
||||
@ -74,9 +83,9 @@ export class MonitorRunner {
|
||||
);
|
||||
await this.notify(`[${monitor.name}] 🔴 Down`, [
|
||||
token.text(
|
||||
`[${monitor.name}] 🔴 Down\nTime: ${dayjs().format(
|
||||
'YYYY-MM-DD HH:mm:ss (z)'
|
||||
)}`
|
||||
`[${monitor.name}] 🔴 Down\nTime: ${dayjs()
|
||||
.tz(this.getTimezone())
|
||||
.format('YYYY-MM-DD HH:mm:ss (z)')}`
|
||||
),
|
||||
]);
|
||||
currentStatus = 'DOWN';
|
||||
@ -88,9 +97,9 @@ export class MonitorRunner {
|
||||
);
|
||||
await this.notify(`[${monitor.name}] ✅ Up`, [
|
||||
token.text(
|
||||
`[${monitor.name}] ✅ Up\nTime: ${dayjs().format(
|
||||
'YYYY-MM-DD HH:mm:ss (z)'
|
||||
)}`
|
||||
`[${monitor.name}] ✅ Up\nTime: ${dayjs()
|
||||
.tz(this.getTimezone())
|
||||
.format('YYYY-MM-DD HH:mm:ss (z)')}`
|
||||
),
|
||||
]);
|
||||
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);
|
||||
if (!runner) {
|
||||
runner = monitorManager.createRunner(monitor);
|
||||
runner = await monitorManager.createRunner(monitor);
|
||||
}
|
||||
|
||||
if (active === true) {
|
||||
|
@ -270,6 +270,15 @@ export const workspaceRouter = router({
|
||||
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;
|
||||
}),
|
||||
invite: workspaceAdminProcedure
|
||||
|
Loading…
Reference in New Issue
Block a user