Add new methods and properties to QueryHelper and MonitorStatusTimeline

This commit is contained in:
Simon Larsen 2024-01-30 18:10:59 +00:00
parent f64a349e4e
commit a9bc14e416
No known key found for this signature in database
GPG Key ID: AB45983AA9C81CDE
9 changed files with 332 additions and 29 deletions

View File

@ -0,0 +1,105 @@
import DataMigrationBase from './DataMigrationBase';
import LIMIT_MAX from 'Common/Types/Database/LimitMax';
import Project from 'Model/Models/Project';
import ProjectService from 'CommonServer/Services/ProjectService';
import Monitor from 'Model/Models/Monitor';
import MonitorService from 'CommonServer/Services/MonitorService';
import MonitorStatusTimeline from 'Model/Models/MonitorStatusTimeline';
import MonitorStatusTimelineService from 'CommonServer/Services/MonitorStatusTimelineService';
import SortOrder from 'Common/Types/BaseDatabase/SortOrder';
export default class AddEndDateToMonitorStatusTimeline extends DataMigrationBase {
public constructor() {
super('AddEndDateToMonitorStatusTimeline');
}
public override async migrate(): Promise<void> {
// get all the users with email isVerified true.
const projects: Array<Project> = await ProjectService.findBy({
query: {},
select: {
_id: true,
},
skip: 0,
limit: LIMIT_MAX,
props: {
isRoot: true,
},
});
for (const project of projects) {
// add ended scheduled maintenance state for each of these projects.
// first fetch resolved state. Ended state order is -1 of resolved state.
const monitors: Array<Monitor> = await MonitorService.findBy({
query: {
projectId: project.id!,
},
select: {
_id: true,
},
skip: 0,
limit: LIMIT_MAX,
props: {
isRoot: true,
},
});
for (const monitor of monitors) {
const statusTimelines: Array<MonitorStatusTimeline> =
await MonitorStatusTimelineService.findBy({
query: {
monitorId: monitor.id!,
},
select: {
_id: true,
createdAt: true,
},
skip: 0,
limit: LIMIT_MAX,
props: {
isRoot: true,
},
sort: {
createdAt: SortOrder.Ascending,
},
});
for (let i: number = 0; i < statusTimelines.length; i++) {
const statusTimeline: MonitorStatusTimeline | undefined =
statusTimelines[i];
if (!statusTimeline) {
continue;
}
let endDate: Date | null = null;
if (
statusTimelines[i + 1] &&
statusTimelines[i + 1]?.createdAt
) {
endDate = statusTimelines[i + 1]!.createdAt!;
}
if (endDate) {
await MonitorStatusTimelineService.updateOneById({
id: statusTimeline!.id!,
data: {
endsAt: endDate,
},
props: {
isRoot: true,
},
});
}
}
}
}
}
public override async rollback(): Promise<void> {
return;
}
}

View File

@ -1,5 +1,6 @@
import AddDefaultGlobalConfig from './AddDefaultGlobalConfig';
import AddDowntimeMonitorStatusToStatusPage from './AddDowntimeMonitorStatusToStatusPage';
import AddEndDateToMonitorStatusTimeline from './AddEndDateToMonitorStatusTimeline';
import AddEndedState from './AddEndedState';
import AddMonitoringDatesToMonitor from './AddMonitoringDatesToMonitors';
import AddOwnerInfoToProjects from './AddOwnerInfoToProject';
@ -27,6 +28,7 @@ const DataMigrations: Array<DataMigrationBase> = [
new AddPostedAtToPublicNotes(),
new MoveEnableSubscribersToEnableEmailSubscribersOnStatusPage(),
new AddDowntimeMonitorStatusToStatusPage(),
new AddEndDateToMonitorStatusTimeline(),
];
export default DataMigrations;

View File

@ -79,6 +79,14 @@ export default class OneUptimeDate {
return moment(date).fromNow();
}
public static differenceBetweenTwoDatesAsFromattedString(
date1: Date,
date2: Date
): string {
const seconds: number = this.getSecondsBetweenTwoDates(date1, date2);
return this.secondsToFormattedFriendlyTimeString(seconds);
}
public static toTimeString(date: Date | string): string {
if (typeof date === 'string') {
date = this.fromString(date);
@ -497,34 +505,104 @@ export default class OneUptimeDate {
public static secondsToFormattedFriendlyTimeString(
seconds: number
): string {
const startDate: moment.Moment = moment.utc(0);
const date: moment.Moment = moment.utc(seconds * 1000);
const hours: string = date.format('HH');
const mins: string = date.format('mm');
const secs: string = date.format('ss');
let text: string = '';
let hasHours: boolean = false;
let hasMins: boolean = false;
if (hours !== '00') {
hasHours = true;
text += hours + ' hours ';
}
// get the difference between the two dates as friendly formatted string
if (mins !== '00' || hasHours) {
hasMins = true;
let formattedString: string = '';
if (hasHours) {
text += ', ';
// years between two dates
const years: number = date.diff(startDate, 'years');
if (years > 0) {
let text: string = 'years ';
if (years === 1) {
text = 'year ';
}
text += mins + ' minutes ';
// add years to start date
startDate.add(years, 'years');
formattedString += years + ' ' + text;
}
if (!(hasHours && hasMins)) {
text += secs + ' seconds. ';
const months: number = date.diff(startDate, 'months');
if (months > 0) {
let text: string = 'months ';
if (months === 1) {
text = 'month ';
}
// add months to start date
startDate.add(months, 'months');
formattedString += months + ' ' + text;
}
return text;
const days: number = date.diff(startDate, 'days');
if (days > 0) {
let text: string = 'days ';
if (days === 1) {
text = 'day ';
}
// add days to start date
startDate.add(days, 'days');
formattedString += days + ' ' + text;
}
const hours: number = date.diff(startDate, 'hours');
if (hours > 0) {
let text: string = 'hours ';
if (hours === 1) {
text = 'hour ';
}
// add hours to start date
startDate.add(hours, 'hours');
formattedString += hours + ' ' + text;
}
const minutes: number = date.diff(startDate, 'minutes');
if (minutes > 0) {
let text: string = 'mins ';
if (minutes === 1) {
text = 'min ';
}
// add minutes to start date
startDate.add(minutes, 'minutes');
formattedString += minutes + ' ' + text;
}
const secondsLeft: number = date.diff(startDate, 'seconds');
if (secondsLeft > 0) {
let text: string = 'secs ';
if (secondsLeft === 1) {
text = 'sec ';
}
// add seconds to start date
startDate.add(secondsLeft, 'seconds');
formattedString += secondsLeft + ' ' + text;
}
return formattedString.trim();
}
public static getGreaterDate(a: Date, b: Date): Date {
@ -810,7 +888,7 @@ export default class OneUptimeDate {
momentDate.format(formatstring) +
' ' +
(onlyShowDate ? '' : this.getCurrentTimezoneString())
);
).trim();
}
public static getDayInSeconds(days?: number | undefined): number {

View File

@ -736,7 +736,7 @@ export default class StatusPageAPI extends BaseAPI<
monitorId: QueryHelper.in(
monitorsOnStatusPageForTimeline
),
createdAt: QueryHelper.inBetween(
endsAt: QueryHelper.inBetweenOrNull(
startDate,
endDate
),
@ -744,6 +744,7 @@ export default class StatusPageAPI extends BaseAPI<
select: {
monitorId: true,
createdAt: true,
endsAt: true,
monitorStatus: {
name: true,
color: true,

View File

@ -12,6 +12,7 @@ import PositiveNumber from 'Common/Types/PositiveNumber';
import CreateBy from '../Types/Database/CreateBy';
import UserService from './UserService';
import User from 'Model/Models/User';
import OneUptimeDate from 'Common/Types/Date';
export class Service extends DatabaseService<MonitorStatusTimeline> {
public constructor(postgresDatabase?: PostgresDatabase) {
@ -22,6 +23,10 @@ export class Service extends DatabaseService<MonitorStatusTimeline> {
protected override async onBeforeCreate(
createBy: CreateBy<MonitorStatusTimeline>
): Promise<OnCreate<MonitorStatusTimeline>> {
if (!createBy.data.monitorId) {
throw new BadDataException('monitorId is null');
}
if (
(createBy.data.createdByUserId ||
createBy.data.createdByUser ||
@ -57,7 +62,29 @@ export class Service extends DatabaseService<MonitorStatusTimeline> {
}
}
return { createBy, carryForward: null };
const lastMonitorStatusTimeline: MonitorStatusTimeline | null =
await this.findOneBy({
query: {
monitorId: createBy.data.monitorId,
},
sort: {
createdAt: SortOrder.Descending,
},
props: {
isRoot: true,
},
select: {
_id: true,
},
});
return {
createBy,
carryForward: {
lastMonitorStatusTimelineId:
lastMonitorStatusTimeline?.id || null,
},
};
}
protected override async onCreateSuccess(
@ -72,6 +99,21 @@ export class Service extends DatabaseService<MonitorStatusTimeline> {
throw new BadDataException('monitorStatusId is null');
}
// update the last status as ended.
if (onCreate.carryForward.lastMonitorStatusTimelineId) {
await this.updateOneById({
id: onCreate.carryForward.lastMonitorStatusTimelineId!,
data: {
endsAt:
createdItem.createdAt || OneUptimeDate.getCurrentDate(),
},
props: {
isRoot: true,
},
});
}
await MonitorService.updateOneBy({
query: {
_id: createdItem.monitorId?.toString(),

View File

@ -241,6 +241,23 @@ export default class QueryHelper {
);
}
public static inBetweenOrNull(
startValue: number | Date,
endValue: number | Date
): FindOperator<any> {
const rid1: string = Text.generateRandomText(10);
const rid2: string = Text.generateRandomText(10);
return Raw(
(alias: string) => {
return `((${alias} >= :${rid1} and ${alias} <= :${rid2})) or (${alias} IS NULL)`;
},
{
[rid1]: startValue,
[rid2]: endValue,
}
);
}
public static queryJson(value: JSONObject): FindOperator<any> {
// seed random text
const values: JSONObject = {};

View File

@ -164,16 +164,23 @@ const DayUptimeGraph: FunctionComponent<ComponentProps> = (
for (const key in secondsOfEvent) {
hasEvents = true;
toolTipText += `, ${
eventLabels[key]
} for ${OneUptimeDate.secondsToFormattedFriendlyTimeString(
secondsOfEvent[key] || 0
)}`;
// TODO: Add rules here.
const eventStatusId: string = key;
// if this is downtime state then, include tooltip.
if (
(props.downtimeEventStatusIds?.filter((id: ObjectID) => {
return id.toString() === eventStatusId.toString();
}).length || 0) > 0
) {
toolTipText += `, ${
eventLabels[key]
} for ${OneUptimeDate.secondsToFormattedFriendlyTimeString(
secondsOfEvent[key] || 0
)}`;
}
const isDowntimeEvent: boolean = Boolean(
props.downtimeEventStatusIds?.find((id: ObjectID) => {
return id.toString() === eventStatusId;

View File

@ -22,6 +22,7 @@ import DisabledWarning from '../../../Components/Monitor/DisabledWarning';
import { ButtonStyleType } from 'CommonUI/src/Components/Button/Button';
import Modal, { ModalWidth } from 'CommonUI/src/Components/Modal/Modal';
import ConfirmModal from 'CommonUI/src/Components/Modal/ConfirmModal';
import OneUptimeDate from 'Common/Types/Date';
const StatusTimeline: FunctionComponent<PageComponentProps> = (
props: PageComponentProps
@ -170,9 +171,35 @@ const StatusTimeline: FunctionComponent<PageComponentProps> = (
field: {
createdAt: true,
},
title: 'Reported At',
title: 'Starts At',
type: FieldType.DateTime,
},
{
field: {
endsAt: true,
},
title: 'Ends At',
type: FieldType.DateTime,
noValueMessage: 'Currently Active',
},
{
field: {
endsAt: true,
},
title: 'Duration',
type: FieldType.Text,
getElement: (item: JSONObject): ReactElement => {
return (
<p>
{OneUptimeDate.differenceBetweenTwoDatesAsFromattedString(
item['createdAt'] as Date,
(item['endsAt'] as Date) ||
OneUptimeDate.getCurrentDate()
)}
</p>
);
},
},
]}
/>
{showViewLogsModal ? (

View File

@ -472,4 +472,28 @@ export default class MonitorStatusTimeline extends BaseModel {
nullable: true,
})
public rootCause?: string = undefined;
@Index()
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CanCreateMonitorStatusTimeline,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CanReadMonitorStatusTimeline,
],
update: [],
})
@TableColumn({ type: TableColumnType.Date })
@Column({
type: ColumnType.Date,
nullable: true,
unique: false,
})
public endsAt?: Date = undefined;
}