mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 08:42:13 +02:00
Compare commits
42 Commits
feature-fl
...
monitor-gr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c1bd10873 | ||
|
|
05a288c761 | ||
|
|
a9f503da9d | ||
|
|
49d3655502 | ||
|
|
1cdcc639b4 | ||
|
|
7568c70b50 | ||
|
|
6259f81a91 | ||
|
|
f40c1daeb8 | ||
|
|
bb73ed14cd | ||
|
|
4b71a81f7c | ||
|
|
d6788c138b | ||
|
|
28f4a1f473 | ||
|
|
ccb4781c06 | ||
|
|
2e27347225 | ||
|
|
e9015f0eff | ||
|
|
6cf8560151 | ||
|
|
7d2e91d867 | ||
|
|
46e0210dcc | ||
|
|
02fc5502eb | ||
|
|
ce3131edaf | ||
|
|
ca4716133a | ||
|
|
9cb254f9d1 | ||
|
|
d51fbdf5f7 | ||
|
|
57b7b5b39e | ||
|
|
2e46ebd0e8 | ||
|
|
4ffe215665 | ||
|
|
e680346f1f | ||
|
|
4faa8d32f6 | ||
|
|
ab07ff0104 | ||
|
|
03dd6fef04 | ||
|
|
31c0ff7dea | ||
|
|
fc218a970a | ||
|
|
a0acb24651 | ||
|
|
c958893d67 | ||
|
|
254a9de101 | ||
|
|
5ec8ee6dcb | ||
|
|
a1c6121bee | ||
|
|
51c76aa1af | ||
|
|
e702a0b0d2 | ||
|
|
cfc2f99248 | ||
|
|
f23bb3af41 | ||
|
|
9205764deb |
@@ -179,11 +179,13 @@
|
||||
"ignoreReadBeforeAssign": false
|
||||
}
|
||||
],
|
||||
"no-var": "error"
|
||||
"no-var": "error",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"no-unneeded-ternary": "error"
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "18.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Common/Tests/Types/JSONFunctions.test.ts
Normal file
52
Common/Tests/Types/JSONFunctions.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import BaseModel from '../../Models/BaseModel';
|
||||
import { JSONObject } from '../../Types/JSON';
|
||||
import JSONFunctions from '../../Types/JSONFunctions';
|
||||
|
||||
describe('JSONFunctions Class', () => {
|
||||
let baseModel: BaseModel;
|
||||
|
||||
beforeEach(() => {
|
||||
baseModel = new BaseModel();
|
||||
});
|
||||
|
||||
describe('isEmptyObject Method', () => {
|
||||
test('Returns true for an empty object', () => {
|
||||
const emptyObj: JSONObject = {};
|
||||
expect(JSONFunctions.isEmptyObject(emptyObj)).toBe(true);
|
||||
});
|
||||
|
||||
test('Returns false for a non-empty object', () => {
|
||||
const nonEmptyObj: JSONObject = { key: 'value' };
|
||||
expect(JSONFunctions.isEmptyObject(nonEmptyObj)).toBe(false);
|
||||
});
|
||||
|
||||
test('Returns true for null or undefined', () => {
|
||||
expect(JSONFunctions.isEmptyObject(null)).toBe(true);
|
||||
expect(JSONFunctions.isEmptyObject(undefined)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toJSON and fromJSON Methods', () => {
|
||||
test('toJSON returns a valid JSON object', () => {
|
||||
const json: JSONObject = JSONFunctions.toJSON(baseModel, BaseModel);
|
||||
expect(json).toEqual(expect.objectContaining({}));
|
||||
});
|
||||
|
||||
test('toJSONObject returns a valid JSON object', () => {
|
||||
const json: JSONObject = JSONFunctions.toJSONObject(
|
||||
baseModel,
|
||||
BaseModel
|
||||
);
|
||||
expect(json).toEqual(expect.objectContaining({}));
|
||||
});
|
||||
|
||||
test('fromJSON returns a BaseModel instance', () => {
|
||||
const json: JSONObject = { name: 'oneuptime' };
|
||||
const result: BaseModel | BaseModel[] = JSONFunctions.fromJSON(
|
||||
json,
|
||||
BaseModel
|
||||
);
|
||||
expect(result).toBeInstanceOf(BaseModel);
|
||||
});
|
||||
});
|
||||
});
|
||||
44
Common/Tests/Types/SerializableObject.test.ts
Normal file
44
Common/Tests/Types/SerializableObject.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import NotImplementedException from '../../Types/Exception/NotImplementedException';
|
||||
import { JSONObject } from '../../Types/JSON';
|
||||
import SerializableObject from '../../Types/SerializableObject';
|
||||
|
||||
describe('SerializableObject Class', () => {
|
||||
let serializableObject: SerializableObject;
|
||||
|
||||
beforeEach(() => {
|
||||
serializableObject = new SerializableObject();
|
||||
});
|
||||
|
||||
test('Constructor initializes an instance of SerializableObject', () => {
|
||||
expect(serializableObject).toBeInstanceOf(SerializableObject);
|
||||
});
|
||||
|
||||
describe('toJSON Method', () => {
|
||||
test('Throws NotImplementedException when called', () => {
|
||||
expect(() => {
|
||||
return serializableObject.toJSON();
|
||||
}).toThrow(NotImplementedException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromJSON Method', () => {
|
||||
test('Throws NotImplementedException when called', () => {
|
||||
expect(() => {
|
||||
return SerializableObject.fromJSON({});
|
||||
}).toThrow(NotImplementedException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromJSON Instance Method', () => {
|
||||
test('Returns the result from the static fromJSON method', () => {
|
||||
const json: JSONObject = { key: 'value' };
|
||||
const expectedResult: SerializableObject = new SerializableObject();
|
||||
jest.spyOn(SerializableObject, 'fromJSON').mockReturnValue(
|
||||
expectedResult
|
||||
);
|
||||
const result: SerializableObject =
|
||||
serializableObject.fromJSON(json);
|
||||
expect(result).toBe(expectedResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -29,7 +29,9 @@ export default interface CallRequest extends CallRequestMessage {
|
||||
to: Phone;
|
||||
}
|
||||
|
||||
export const isHighRiskPhoneNumber: Function = (phoneNumber: Phone): boolean => {
|
||||
export const isHighRiskPhoneNumber: Function = (
|
||||
phoneNumber: Phone
|
||||
): boolean => {
|
||||
// Pakistan
|
||||
if (phoneNumber.toString().startsWith('+92')) {
|
||||
return true;
|
||||
|
||||
@@ -229,7 +229,7 @@ export default class OneUptimeDate {
|
||||
let hasMins: boolean = false;
|
||||
if (hours !== '00') {
|
||||
hasHours = true;
|
||||
text += hours + ' hours';
|
||||
text += hours + ' hours ';
|
||||
}
|
||||
|
||||
if (mins !== '00' || hasHours) {
|
||||
@@ -239,7 +239,7 @@ export default class OneUptimeDate {
|
||||
text += ', ';
|
||||
}
|
||||
|
||||
text += mins + ' minutes';
|
||||
text += mins + ' minutes ';
|
||||
}
|
||||
|
||||
if (!(hasHours && hasMins)) {
|
||||
@@ -359,6 +359,12 @@ export default class OneUptimeDate {
|
||||
return moment(date).isAfter(startDate);
|
||||
}
|
||||
|
||||
public static isEqualBySeconds(date: Date, startDate: Date): boolean {
|
||||
date = this.fromString(date);
|
||||
startDate = this.fromString(startDate);
|
||||
return moment(date).isSame(startDate, 'seconds');
|
||||
}
|
||||
|
||||
public static hasExpired(expirationDate: Date): boolean {
|
||||
expirationDate = this.fromString(expirationDate);
|
||||
return !moment(this.getCurrentDate()).isBefore(expirationDate);
|
||||
|
||||
@@ -10,6 +10,7 @@ enum IconProp {
|
||||
Settings = 'Settings',
|
||||
Criteria = 'Criteria',
|
||||
Notification = 'Notification',
|
||||
Squares = 'Squares',
|
||||
Help = 'Help',
|
||||
JSON = 'JSON',
|
||||
Signal = 'Signal',
|
||||
@@ -27,6 +28,7 @@ enum IconProp {
|
||||
Home = 'Home',
|
||||
Graph = 'Graph',
|
||||
Variable = 'Variable',
|
||||
ListBullet = 'ListBullet',
|
||||
Image = 'Image',
|
||||
Grid = 'Grid',
|
||||
More = 'More',
|
||||
|
||||
@@ -123,6 +123,7 @@ export type JSONValue =
|
||||
| Array<JSONValue>
|
||||
| Array<Permission>
|
||||
| Array<JSONValue>
|
||||
| Array<ObjectID>
|
||||
| CallRequest
|
||||
| undefined
|
||||
| null;
|
||||
|
||||
@@ -11,10 +11,10 @@ enum NotificationSettingEventType {
|
||||
SEND_MONITOR_STATUS_CHANGED_OWNER_NOTIFICATION = 'Send monitor status changed notification when I am the owner of the monitor',
|
||||
|
||||
// Scheduled Maintenance
|
||||
SEND_SCHEDULED_MAINTENANCE_CREATED_OWNER_NOTIFICATION = 'Send scheduled maintenance created notification when I am the owner of the scheduled maintenance',
|
||||
SEND_SCHEDULED_MAINTENANCE_NOTE_POSTED_OWNER_NOTIFICATION = 'Send scheduled maintenance note posted notification when I am the owner of the scheduled maintenance',
|
||||
SEND_SCHEDULED_MAINTENANCE_OWNER_ADDED_NOTIFICATION = 'Send notification when I am added as a owner to the scheduled maintenance',
|
||||
SEND_SCHEDULED_MAINTENANCE_STATE_CHANGED_OWNER_NOTIFICATION = 'Send scheduled maintenance state changed notification when I am the owner of the scheduled maintenance',
|
||||
SEND_SCHEDULED_MAINTENANCE_CREATED_OWNER_NOTIFICATION = 'Send event created notification when I am the owner of the event',
|
||||
SEND_SCHEDULED_MAINTENANCE_NOTE_POSTED_OWNER_NOTIFICATION = 'Send event note posted notification when I am the owner of the event',
|
||||
SEND_SCHEDULED_MAINTENANCE_OWNER_ADDED_NOTIFICATION = 'Send notification when I am added as a owner to the event',
|
||||
SEND_SCHEDULED_MAINTENANCE_STATE_CHANGED_OWNER_NOTIFICATION = 'Send event state changed notification when I am the owner of the event',
|
||||
|
||||
// Status Page
|
||||
SEND_STATUS_PAGE_ANNOUNCEMENT_CREATED_OWNER_NOTIFICATION = 'Send status page announcement created notification when I am the owner of the status page',
|
||||
|
||||
12
Common/package-lock.json
generated
12
Common/package-lock.json
generated
@@ -1637,9 +1637,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-js": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz",
|
||||
"integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw=="
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||
},
|
||||
"node_modules/cssom": {
|
||||
"version": "0.4.4",
|
||||
@@ -5943,9 +5943,9 @@
|
||||
}
|
||||
},
|
||||
"crypto-js": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz",
|
||||
"integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw=="
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||
},
|
||||
"cssom": {
|
||||
"version": "0.4.4",
|
||||
|
||||
@@ -13,6 +13,8 @@ import ObjectID from 'Common/Types/ObjectID';
|
||||
import UserMiddleware from '../Middleware/UserAuthorization';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import MonitorStatus from 'Model/Models/MonitorStatus';
|
||||
import MonitorStatusTimeline from 'Model/Models/MonitorStatusTimeline';
|
||||
import OneUptimeDate from 'Common/Types/Date';
|
||||
|
||||
export default class MonitorGroupAPI extends BaseAPI<
|
||||
MonitorGroup,
|
||||
@@ -59,5 +61,50 @@ export default class MonitorGroupAPI extends BaseAPI<
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.router.post(
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/timeline/:monitorGroupId`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction
|
||||
) => {
|
||||
try {
|
||||
// get group id.
|
||||
|
||||
if (!req.params['monitorGroupId']) {
|
||||
throw new BadDataException(
|
||||
'Monitor group id is required.'
|
||||
);
|
||||
}
|
||||
|
||||
const startDate: Date = OneUptimeDate.getSomeDaysAgo(90);
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
|
||||
const timeline: Array<MonitorStatusTimeline> =
|
||||
await this.service.getStatusTimeline(
|
||||
new ObjectID(
|
||||
req.params['monitorGroupId'].toString()
|
||||
),
|
||||
startDate,
|
||||
endDate,
|
||||
await this.getDatabaseCommonInteractionProps(req)
|
||||
);
|
||||
|
||||
return Response.sendEntityArrayResponse(
|
||||
req,
|
||||
res,
|
||||
timeline,
|
||||
timeline.length,
|
||||
MonitorStatusTimeline
|
||||
);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,10 @@ import PositiveNumber from 'Common/Types/PositiveNumber';
|
||||
import StatusPageSsoService from '../Services/StatusPageSsoService';
|
||||
import StatusPageSSO from 'Model/Models/StatusPageSso';
|
||||
import ArrayUtil from 'Common/Types/ArrayUtil';
|
||||
import Dictionary from 'Common/Types/Dictionary';
|
||||
import MonitorGroupService from '../Services/MonitorGroupService';
|
||||
import MonitorGroupResource from 'Model/Models/MonitorGroupResource';
|
||||
import MonitorGroupResourceService from '../Services/MonitorGroupResourceService';
|
||||
|
||||
export default class StatusPageAPI extends BaseAPI<
|
||||
StatusPage,
|
||||
@@ -391,6 +395,9 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
res: ExpressResponse,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const startDate: Date = OneUptimeDate.getSomeDaysAgo(90);
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
|
||||
try {
|
||||
const objectId: ObjectID = new ObjectID(
|
||||
req.params['statusPageId'] as string
|
||||
@@ -494,8 +501,8 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
_id: true,
|
||||
currentMonitorStatusId: true,
|
||||
},
|
||||
monitorGroupId: true,
|
||||
},
|
||||
|
||||
sort: {
|
||||
order: SortOrder.Ascending,
|
||||
},
|
||||
@@ -506,13 +513,28 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
},
|
||||
});
|
||||
|
||||
const monitorGroupIds: Array<ObjectID> = statusPageResources
|
||||
.map((resource: StatusPageResource) => {
|
||||
return resource.monitorGroupId!;
|
||||
})
|
||||
.filter((id: ObjectID) => {
|
||||
return Boolean(id); // remove nulls
|
||||
});
|
||||
|
||||
// get monitors in the group.
|
||||
const monitorGroupCurrentStatuses: Dictionary<ObjectID> =
|
||||
{};
|
||||
const monitorsInGroup: Dictionary<Array<ObjectID>> = {};
|
||||
|
||||
// get monitor status charts.
|
||||
const monitorsOnStatusPage: Array<ObjectID> =
|
||||
statusPageResources.map(
|
||||
(monitor: StatusPageResource) => {
|
||||
statusPageResources
|
||||
.map((monitor: StatusPageResource) => {
|
||||
return monitor.monitorId!;
|
||||
}
|
||||
);
|
||||
})
|
||||
.filter((id: ObjectID) => {
|
||||
return Boolean(id); // remove nulls
|
||||
});
|
||||
|
||||
const monitorsOnStatusPageForTimeline: Array<ObjectID> =
|
||||
statusPageResources
|
||||
@@ -521,10 +543,98 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
})
|
||||
.map((monitor: StatusPageResource) => {
|
||||
return monitor.monitorId!;
|
||||
})
|
||||
.filter((id: ObjectID) => {
|
||||
return Boolean(id); // remove nulls
|
||||
});
|
||||
|
||||
const startDate: Date = OneUptimeDate.getSomeDaysAgo(90);
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
for (const monitorGroupId of monitorGroupIds) {
|
||||
// get current status of monitors in the group.
|
||||
|
||||
const currentStatus: MonitorStatus =
|
||||
await MonitorGroupService.getCurrentStatus(
|
||||
monitorGroupId,
|
||||
{
|
||||
isRoot: true,
|
||||
}
|
||||
);
|
||||
|
||||
monitorGroupCurrentStatuses[monitorGroupId.toString()] =
|
||||
currentStatus.id!;
|
||||
|
||||
// get monitors in the group.
|
||||
|
||||
const groupResources: Array<MonitorGroupResource> =
|
||||
await MonitorGroupResourceService.findBy({
|
||||
query: {
|
||||
monitorGroupId: monitorGroupId,
|
||||
},
|
||||
select: {
|
||||
monitorId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
});
|
||||
|
||||
const monitorsInGroupIds: Array<ObjectID> =
|
||||
groupResources
|
||||
.map((resource: MonitorGroupResource) => {
|
||||
return resource.monitorId!;
|
||||
})
|
||||
.filter((id: ObjectID) => {
|
||||
return Boolean(id); // remove nulls
|
||||
});
|
||||
|
||||
const shouldShowTimelineForThisGroup: boolean = Boolean(
|
||||
statusPageResources.find(
|
||||
(resource: StatusPageResource) => {
|
||||
return (
|
||||
resource.monitorGroupId?.toString() ===
|
||||
monitorGroupId.toString() &&
|
||||
resource.showStatusHistoryChart
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
for (const monitorId of monitorsInGroupIds) {
|
||||
if (!monitorId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!monitorsOnStatusPage.find((item: ObjectID) => {
|
||||
return (
|
||||
item.toString() === monitorId.toString()
|
||||
);
|
||||
})
|
||||
) {
|
||||
monitorsOnStatusPage.push(monitorId);
|
||||
}
|
||||
|
||||
// add this to the timeline event for this group.
|
||||
|
||||
if (
|
||||
shouldShowTimelineForThisGroup &&
|
||||
!monitorsOnStatusPageForTimeline.find(
|
||||
(item: ObjectID) => {
|
||||
return (
|
||||
item.toString() ===
|
||||
monitorId.toString()
|
||||
);
|
||||
}
|
||||
)
|
||||
) {
|
||||
monitorsOnStatusPageForTimeline.push(monitorId);
|
||||
}
|
||||
}
|
||||
|
||||
monitorsInGroup[monitorGroupId.toString()] =
|
||||
monitorsInGroupIds;
|
||||
}
|
||||
|
||||
let monitorStatusTimelines: Array<MonitorStatusTimeline> =
|
||||
[];
|
||||
@@ -919,6 +1029,12 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
scheduledMaintenanceStateTimelines,
|
||||
ScheduledMaintenanceStateTimeline
|
||||
),
|
||||
|
||||
monitorGroupCurrentStatuses: JSONFunctions.serialize(
|
||||
monitorGroupCurrentStatuses
|
||||
),
|
||||
monitorsInGroup:
|
||||
JSONFunctions.serialize(monitorsInGroup),
|
||||
};
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, response);
|
||||
@@ -1241,6 +1357,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
_id: true,
|
||||
currentMonitorStatusId: true,
|
||||
},
|
||||
monitorGroupId: true,
|
||||
},
|
||||
|
||||
skip: 0,
|
||||
@@ -1419,6 +1536,65 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
});
|
||||
}
|
||||
|
||||
const monitorGroupIds: Array<ObjectID> = statusPageResources
|
||||
.map((resource: StatusPageResource) => {
|
||||
return resource.monitorGroupId!;
|
||||
})
|
||||
.filter((id: ObjectID) => {
|
||||
return Boolean(id); // remove nulls
|
||||
});
|
||||
|
||||
// get monitors in the group.
|
||||
const monitorsInGroup: Dictionary<Array<ObjectID>> = {};
|
||||
|
||||
// get monitor status charts.
|
||||
const monitorsOnStatusPage: Array<ObjectID> = statusPageResources
|
||||
.map((monitor: StatusPageResource) => {
|
||||
return monitor.monitorId!;
|
||||
})
|
||||
.filter((id: ObjectID) => {
|
||||
return Boolean(id); // remove nulls
|
||||
});
|
||||
|
||||
for (const monitorGroupId of monitorGroupIds) {
|
||||
// get monitors in the group.
|
||||
|
||||
const groupResources: Array<MonitorGroupResource> =
|
||||
await MonitorGroupResourceService.findBy({
|
||||
query: {
|
||||
monitorGroupId: monitorGroupId,
|
||||
},
|
||||
select: {
|
||||
monitorId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
});
|
||||
|
||||
const monitorsInGroupIds: Array<ObjectID> = groupResources
|
||||
.map((resource: MonitorGroupResource) => {
|
||||
return resource.monitorId!;
|
||||
})
|
||||
.filter((id: ObjectID) => {
|
||||
return Boolean(id); // remove nulls
|
||||
});
|
||||
|
||||
for (const monitorId of monitorsInGroupIds) {
|
||||
if (
|
||||
!monitorsOnStatusPage.find((item: ObjectID) => {
|
||||
return item.toString() === monitorId.toString();
|
||||
})
|
||||
) {
|
||||
monitorsOnStatusPage.push(monitorId);
|
||||
}
|
||||
}
|
||||
|
||||
monitorsInGroup[monitorGroupId.toString()] = monitorsInGroupIds;
|
||||
}
|
||||
|
||||
const response: JSONObject = {
|
||||
scheduledMaintenanceEventsPublicNotes: JSONFunctions.toJSONArray(
|
||||
scheduledMaintenanceEventsPublicNotes,
|
||||
@@ -1436,6 +1612,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
scheduledMaintenanceStateTimelines,
|
||||
ScheduledMaintenanceStateTimeline
|
||||
),
|
||||
monitorsInGroup: JSONFunctions.serialize(monitorsInGroup),
|
||||
};
|
||||
|
||||
return response;
|
||||
@@ -1600,6 +1777,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
_id: true,
|
||||
currentMonitorStatusId: true,
|
||||
},
|
||||
monitorGroupId: true,
|
||||
},
|
||||
|
||||
skip: 0,
|
||||
@@ -1609,12 +1787,65 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
},
|
||||
});
|
||||
|
||||
const monitorGroupIds: Array<ObjectID> = statusPageResources
|
||||
.map((resource: StatusPageResource) => {
|
||||
return resource.monitorGroupId!;
|
||||
})
|
||||
.filter((id: ObjectID) => {
|
||||
return Boolean(id); // remove nulls
|
||||
});
|
||||
|
||||
const monitorsInGroup: Dictionary<Array<ObjectID>> = {};
|
||||
|
||||
// get monitor status charts.
|
||||
const monitorsOnStatusPage: Array<ObjectID> = statusPageResources.map(
|
||||
(monitor: StatusPageResource) => {
|
||||
const monitorsOnStatusPage: Array<ObjectID> = statusPageResources
|
||||
.map((monitor: StatusPageResource) => {
|
||||
return monitor.monitorId!;
|
||||
})
|
||||
.filter((id: ObjectID) => {
|
||||
return Boolean(id); // remove nulls
|
||||
});
|
||||
|
||||
for (const monitorGroupId of monitorGroupIds) {
|
||||
// get current status of monitors in the group.
|
||||
|
||||
// get monitors in the group.
|
||||
|
||||
const groupResources: Array<MonitorGroupResource> =
|
||||
await MonitorGroupResourceService.findBy({
|
||||
query: {
|
||||
monitorGroupId: monitorGroupId,
|
||||
},
|
||||
select: {
|
||||
monitorId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
});
|
||||
|
||||
const monitorsInGroupIds: Array<ObjectID> = groupResources
|
||||
.map((resource: MonitorGroupResource) => {
|
||||
return resource.monitorId!;
|
||||
})
|
||||
.filter((id: ObjectID) => {
|
||||
return Boolean(id); // remove nulls
|
||||
});
|
||||
|
||||
for (const monitorId of monitorsInGroupIds) {
|
||||
if (
|
||||
!monitorsOnStatusPage.find((item: ObjectID) => {
|
||||
return item.toString() === monitorId.toString();
|
||||
})
|
||||
) {
|
||||
monitorsOnStatusPage.push(monitorId);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
monitorsInGroup[monitorGroupId.toString()] = monitorsInGroupIds;
|
||||
}
|
||||
|
||||
const today: Date = OneUptimeDate.getCurrentDate();
|
||||
const historyDays: Date = OneUptimeDate.getSomeDaysAgo(
|
||||
@@ -1785,6 +2016,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
incidentStateTimelines,
|
||||
IncidentStateTimeline
|
||||
),
|
||||
monitorsInGroup: JSONFunctions.serialize(monitorsInGroup),
|
||||
};
|
||||
|
||||
return response;
|
||||
|
||||
@@ -6,15 +6,87 @@ import MonitorStatus from 'Model/Models/MonitorStatus';
|
||||
import DatabaseCommonInteractionProps from 'Common/Types/BaseDatabase/DatabaseCommonInteractionProps';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import MonitorGroupResourceService from './MonitorGroupResourceService';
|
||||
import { LIMIT_PER_PROJECT } from 'Common/Types/Database/LimitMax';
|
||||
import LIMIT_MAX, { LIMIT_PER_PROJECT } from 'Common/Types/Database/LimitMax';
|
||||
import MonitorStatusService from './MonitorStatusService';
|
||||
import MonitorGroupResource from 'Model/Models/MonitorGroupResource';
|
||||
import MonitorStatusTimeline from 'Model/Models/MonitorStatusTimeline';
|
||||
import MonitorStatusTimelineService from './MonitorStatusTimelineService';
|
||||
import QueryHelper from '../Types/Database/QueryHelper';
|
||||
import SortOrder from 'Common/Types/BaseDatabase/SortOrder';
|
||||
|
||||
export class Service extends DatabaseService<MonitorGroup> {
|
||||
public constructor(postgresDatabase?: PostgresDatabase) {
|
||||
super(MonitorGroup, postgresDatabase);
|
||||
}
|
||||
|
||||
public async getStatusTimeline(
|
||||
monitorGroupId: ObjectID,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
props: DatabaseCommonInteractionProps
|
||||
): Promise<Array<MonitorStatusTimeline>> {
|
||||
const monitorGroup: MonitorGroup | null = await this.findOneById({
|
||||
id: monitorGroupId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
},
|
||||
props: props,
|
||||
});
|
||||
|
||||
if (!monitorGroup) {
|
||||
throw new BadDataException('Monitor group not found.');
|
||||
}
|
||||
|
||||
const monitorGroupResources: Array<MonitorGroupResource> =
|
||||
await MonitorGroupResourceService.findBy({
|
||||
query: {
|
||||
monitorGroupId: monitorGroup.id!,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
select: {
|
||||
monitorId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const monitorStatusTimelines: Array<MonitorStatusTimeline> =
|
||||
await MonitorStatusTimelineService.findBy({
|
||||
query: {
|
||||
monitorId: QueryHelper.in(
|
||||
monitorGroupResources.map(
|
||||
(monitorGroupResource: MonitorGroupResource) => {
|
||||
return monitorGroupResource.monitorId!;
|
||||
}
|
||||
)
|
||||
),
|
||||
createdAt: QueryHelper.inBetween(startDate, endDate),
|
||||
},
|
||||
select: {
|
||||
createdAt: true,
|
||||
monitorId: true,
|
||||
monitorStatus: {
|
||||
name: true,
|
||||
color: true,
|
||||
priority: true,
|
||||
} as any,
|
||||
},
|
||||
sort: {
|
||||
createdAt: SortOrder.Ascending,
|
||||
},
|
||||
skip: 0,
|
||||
limit: LIMIT_MAX, // This can be optimized.
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return monitorStatusTimelines;
|
||||
}
|
||||
|
||||
public async getCurrentStatus(
|
||||
monitorGroupId: ObjectID,
|
||||
props: DatabaseCommonInteractionProps
|
||||
|
||||
187
CommonServer/Tests/Middleware/NotificationMiddleware.test.ts
Normal file
187
CommonServer/Tests/Middleware/NotificationMiddleware.test.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import VoiceResponse from 'twilio/lib/twiml/VoiceResponse';
|
||||
|
||||
import NotificationMiddleware from '../../Middleware/NotificationMiddleware';
|
||||
|
||||
// Helpers
|
||||
import Response from '../../Utils/Response';
|
||||
import JSONWebToken from '../../Utils/JsonWebToken';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import JSONFunctions from 'Common/Types/JSONFunctions';
|
||||
|
||||
// Types
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from '../../Utils/Express';
|
||||
import { OnCallInputRequest } from 'Common/Types/Call/CallRequest';
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
|
||||
jest.mock('twilio/lib/twiml/VoiceResponse');
|
||||
jest.mock('../../Utils/Response');
|
||||
jest.mock('../../Utils/JsonWebToken', () => {
|
||||
return {
|
||||
decodeJsonPayload: jest.fn(),
|
||||
};
|
||||
});
|
||||
jest.mock('Common/Types/JSONFunctions', () => {
|
||||
return {
|
||||
deserialize: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('NotificationMiddleware', () => {
|
||||
describe('sendResponse', () => {
|
||||
let mockRequest: ExpressRequest;
|
||||
let mockResponse: ExpressResponse;
|
||||
let onCallInputRequest: OnCallInputRequest;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRequest = { body: { Digits: '1234' } } as ExpressRequest;
|
||||
mockResponse = {} as ExpressResponse;
|
||||
onCallInputRequest = {
|
||||
default: { sayMessage: 'default message' },
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('Should return correct message for a valid Digits value', async () => {
|
||||
onCallInputRequest['1234'] = { sayMessage: 'message 1' };
|
||||
const responseInstance: VoiceResponse = new VoiceResponse();
|
||||
|
||||
(
|
||||
VoiceResponse as jest.MockedClass<typeof VoiceResponse>
|
||||
).mockImplementation(() => {
|
||||
return responseInstance;
|
||||
});
|
||||
await NotificationMiddleware.sendResponse(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
onCallInputRequest
|
||||
);
|
||||
|
||||
expect(responseInstance.say).toHaveBeenCalledWith(
|
||||
(onCallInputRequest['1234'] as any).sayMessage
|
||||
);
|
||||
expect(Response.sendXmlResponse).toHaveBeenCalledWith(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
responseInstance.toString()
|
||||
);
|
||||
});
|
||||
|
||||
test('Should return default message for an invalid Digits value', async () => {
|
||||
const responseInstance: VoiceResponse = new VoiceResponse();
|
||||
|
||||
(
|
||||
VoiceResponse as jest.MockedClass<typeof VoiceResponse>
|
||||
).mockImplementation(() => {
|
||||
return responseInstance;
|
||||
});
|
||||
await NotificationMiddleware.sendResponse(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
onCallInputRequest
|
||||
);
|
||||
|
||||
expect(responseInstance.say).toHaveBeenCalledWith(
|
||||
onCallInputRequest['default']?.sayMessage
|
||||
);
|
||||
expect(Response.sendXmlResponse).toHaveBeenCalledWith(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
responseInstance.toString()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidCallNotificationRequest', () => {
|
||||
let mockRequest: ExpressRequest;
|
||||
let mockResponse: ExpressResponse;
|
||||
let mockNext: NextFunction;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRequest = {
|
||||
body: {},
|
||||
query: {},
|
||||
} as ExpressRequest;
|
||||
mockResponse = {} as ExpressResponse;
|
||||
mockNext = jest.fn() as NextFunction;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('Should return error if Digits is not in req body', async () => {
|
||||
await NotificationMiddleware.isValidCallNotificationRequest(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
mockNext
|
||||
);
|
||||
|
||||
expect(Response.sendErrorResponse).toHaveBeenCalledWith(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
new BadDataException('Invalid input')
|
||||
);
|
||||
});
|
||||
|
||||
test('Should return error if Token is not in req query', async () => {
|
||||
mockRequest.body['Digits'] = '1234';
|
||||
|
||||
await NotificationMiddleware.isValidCallNotificationRequest(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
mockNext
|
||||
);
|
||||
|
||||
expect(Response.sendErrorResponse).toHaveBeenCalledWith(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
new BadDataException('Invalid token')
|
||||
);
|
||||
});
|
||||
|
||||
test('Should return error if token decoding fails', async () => {
|
||||
mockRequest.body['Digits'] = '1234';
|
||||
mockRequest.query['token'] = 'token';
|
||||
|
||||
jest.spyOn(JSONWebToken, 'decodeJsonPayload').mockImplementation(
|
||||
() => {
|
||||
throw new Error('Decoding error');
|
||||
}
|
||||
);
|
||||
await NotificationMiddleware.isValidCallNotificationRequest(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
mockNext
|
||||
);
|
||||
|
||||
expect(Response.sendErrorResponse).toHaveBeenCalledWith(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
new BadDataException('Invalid token')
|
||||
);
|
||||
});
|
||||
|
||||
test("Should call 'next' if data is valid", async () => {
|
||||
mockRequest.body['Digits'] = '1234';
|
||||
mockRequest.query['token'] = 'token';
|
||||
const tokenData: JSONObject = { id: 1 };
|
||||
|
||||
jest.spyOn(JSONFunctions, 'deserialize').mockReturnValue(tokenData);
|
||||
await NotificationMiddleware.isValidCallNotificationRequest(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
mockNext
|
||||
);
|
||||
|
||||
expect(mockNext).toHaveBeenCalled();
|
||||
expect((mockRequest as any).callTokenData).toEqual(tokenData);
|
||||
});
|
||||
});
|
||||
});
|
||||
123
CommonServer/Tests/Utils/Cookie.test.ts
Normal file
123
CommonServer/Tests/Utils/Cookie.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
import CookieUtil from '../../Utils/Cookie';
|
||||
import { ExpressRequest, ExpressResponse } from '../../Utils/Express';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import Dictionary from 'Common/Types/Dictionary';
|
||||
|
||||
describe('CookieUtils', () => {
|
||||
let mockRequest: ExpressRequest;
|
||||
let mockResponse: ExpressResponse;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRequest = {
|
||||
cookies: {},
|
||||
} as ExpressRequest;
|
||||
|
||||
mockResponse = {} as ExpressResponse;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('Should set a cookie', () => {
|
||||
const cookie: JSONObject = {
|
||||
name: 'testName',
|
||||
value: 'testValue',
|
||||
options: {},
|
||||
};
|
||||
|
||||
mockResponse.cookie = jest.fn();
|
||||
CookieUtil.setCookie(
|
||||
mockResponse,
|
||||
cookie['name'] as string,
|
||||
cookie['value'] as string,
|
||||
cookie['options'] as JSONObject
|
||||
);
|
||||
|
||||
expect(mockResponse.cookie).toHaveBeenCalledWith(
|
||||
cookie['name'] as string,
|
||||
cookie['value'] as string,
|
||||
cookie['options'] as JSONObject
|
||||
);
|
||||
});
|
||||
|
||||
test('Should return a cookie', () => {
|
||||
const cookieName: string = 'testName';
|
||||
const cookieValue: string = 'testValue';
|
||||
|
||||
mockRequest.cookies[cookieName] = cookieValue;
|
||||
const value: string | undefined = CookieUtil.getCookie(
|
||||
mockRequest,
|
||||
cookieName
|
||||
);
|
||||
|
||||
expect(value).toBe(value);
|
||||
});
|
||||
|
||||
test('Should remove a cookie', () => {
|
||||
const cookieName: string = 'testName';
|
||||
|
||||
mockResponse.clearCookie = jest.fn();
|
||||
CookieUtil.removeCookie(mockResponse, cookieName);
|
||||
|
||||
expect(mockResponse.clearCookie).toHaveBeenCalledWith(cookieName);
|
||||
});
|
||||
|
||||
test('Should return all cookies', () => {
|
||||
const value: string = 'testValue';
|
||||
mockRequest.cookies = { testName: value };
|
||||
const cookies: Dictionary<string> =
|
||||
CookieUtil.getAllCookies(mockRequest);
|
||||
|
||||
expect(cookies).toEqual({ testName: value });
|
||||
});
|
||||
|
||||
test('Should return empty object if there are no cookies', () => {
|
||||
mockRequest.cookies = undefined;
|
||||
const cookies: Dictionary<string> =
|
||||
CookieUtil.getAllCookies(mockRequest);
|
||||
|
||||
expect(cookies).toEqual({});
|
||||
});
|
||||
|
||||
test('Should return user token key', () => {
|
||||
const id: string = '123456789';
|
||||
const keyWithId: string = CookieUtil.getUserTokenKey(new ObjectID(id));
|
||||
const keyWithoutId: string = CookieUtil.getUserTokenKey();
|
||||
|
||||
expect(keyWithId).toBe(`user-token-${id}`);
|
||||
expect(keyWithoutId).toBe('user-token');
|
||||
});
|
||||
|
||||
test('Should return SSO key', () => {
|
||||
const ssoKey: string = CookieUtil.getSSOKey();
|
||||
|
||||
expect(ssoKey).toBe('sso-');
|
||||
});
|
||||
|
||||
test('Should return user SSO key', () => {
|
||||
const id: string = '123456789';
|
||||
const userSsoKey: string = CookieUtil.getUserSSOKey(new ObjectID(id));
|
||||
|
||||
expect(userSsoKey).toBe(`sso-${id}`);
|
||||
});
|
||||
|
||||
test('Should remove all cookies', () => {
|
||||
const cookies: Dictionary<string> = {
|
||||
testName1: 'testValue1',
|
||||
testName2: 'testValue2',
|
||||
};
|
||||
|
||||
mockRequest.cookies = cookies;
|
||||
mockResponse.clearCookie = jest.fn();
|
||||
CookieUtil.removeAllCookies(mockRequest, mockResponse);
|
||||
|
||||
expect(mockResponse.clearCookie).toHaveBeenCalledWith(
|
||||
Object.keys(cookies)[0]
|
||||
);
|
||||
expect(mockResponse.clearCookie).toHaveBeenCalledWith(
|
||||
Object.keys(cookies)[1]
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -175,9 +175,13 @@ export default class Response {
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
list: Array<BaseModel>,
|
||||
count: PositiveNumber,
|
||||
count: PositiveNumber | number,
|
||||
modelType: { new (): BaseModel }
|
||||
): void {
|
||||
if (!(count instanceof PositiveNumber)) {
|
||||
count = new PositiveNumber(count);
|
||||
}
|
||||
|
||||
return this.sendJsonArrayResponse(
|
||||
req,
|
||||
res,
|
||||
|
||||
@@ -28,5 +28,5 @@
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
|
||||
"\\.(css|less)$": "identity-obj-proxy",
|
||||
"uuid":"<rootDir>/node_modules/jest-runtime/build/index.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,12 +36,16 @@ const Card: FunctionComponent<ComponentProps> = (
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<h2
|
||||
id="payment-details-heading"
|
||||
data-testid="card-details-heading"
|
||||
id="card-details-heading"
|
||||
className="text-lg font-medium leading-6 text-gray-900"
|
||||
>
|
||||
{props.title}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
<p
|
||||
data-testid="card-description"
|
||||
className="mt-1 text-sm text-gray-500"
|
||||
>
|
||||
{props.description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -78,6 +82,7 @@ const Card: FunctionComponent<ComponentProps> = (
|
||||
shortcutKey={
|
||||
button.shortcutKey
|
||||
}
|
||||
dataTestId="card-button"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -146,6 +146,14 @@ const Icon: FunctionComponent<ComponentProps> = ({
|
||||
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75"
|
||||
/>
|
||||
);
|
||||
} else if (icon === IconProp.Squares) {
|
||||
return getSvgWrapper(
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z"
|
||||
/>
|
||||
);
|
||||
} else if (icon === IconProp.Pencil) {
|
||||
return getSvgWrapper(
|
||||
<path
|
||||
@@ -170,6 +178,14 @@ const Icon: FunctionComponent<ComponentProps> = ({
|
||||
d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"
|
||||
/>
|
||||
);
|
||||
} else if (icon === IconProp.ListBullet) {
|
||||
return getSvgWrapper(
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm-.375 5.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
|
||||
/>
|
||||
);
|
||||
} else if (icon === IconProp.Template) {
|
||||
return getSvgWrapper(
|
||||
<path
|
||||
|
||||
@@ -9,7 +9,7 @@ import Navigation from '../../Utils/Navigation';
|
||||
export interface ComponentProps {
|
||||
children: ReactElement | Array<ReactElement> | string;
|
||||
className?: undefined | string;
|
||||
to: Route | URL | null | undefined;
|
||||
to?: Route | URL | null | undefined;
|
||||
onClick?: undefined | (() => void);
|
||||
onNavigateComplete?: (() => void) | undefined;
|
||||
openInNewTab?: boolean | undefined;
|
||||
|
||||
@@ -38,7 +38,10 @@ const ConfirmModal: FunctionComponent<ComponentProps> = (
|
||||
}
|
||||
error={props.error}
|
||||
>
|
||||
<div className="text-gray-500 mt-5 text-sm">
|
||||
<div
|
||||
data-testid="confirm-modal-description"
|
||||
className="text-gray-500 mt-5 text-sm"
|
||||
>
|
||||
{props.description}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -108,6 +108,7 @@ const Modal: FunctionComponent<ComponentProps> = (
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<h3
|
||||
data-testid="modal-title"
|
||||
className={`text-lg font-medium leading-6 text-gray-900 ${
|
||||
props.icon
|
||||
? 'ml-10 -mt-8 mb-5'
|
||||
@@ -118,7 +119,10 @@ const Modal: FunctionComponent<ComponentProps> = (
|
||||
{props.title}
|
||||
</h3>
|
||||
{props.description && (
|
||||
<h3 className="text-sm leading-6 text-gray-500">
|
||||
<h3
|
||||
data-testid="modal-description"
|
||||
className="text-sm leading-6 text-gray-500"
|
||||
>
|
||||
{props.description}
|
||||
</h3>
|
||||
)}
|
||||
|
||||
@@ -40,6 +40,7 @@ const ModalFooter: FunctionComponent<ComponentProps> = (
|
||||
? props.submitButtonType
|
||||
: ButtonType.Button
|
||||
}
|
||||
dataTestId="modal-footer-submit-button"
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
@@ -57,6 +58,7 @@ const ModalFooter: FunctionComponent<ComponentProps> = (
|
||||
onClick={() => {
|
||||
props.onClose && props.onClose();
|
||||
}}
|
||||
dataTestId="modal-footer-close-button"
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
|
||||
@@ -12,6 +12,11 @@ import OneUptimeDate from 'Common/Types/Date';
|
||||
import DayUptimeGraph, { Event } from '../Graphs/DayUptimeGraph';
|
||||
import { Green } from 'Common/Types/BrandColors';
|
||||
import ErrorMessage from '../ErrorMessage/ErrorMessage';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
|
||||
export interface MonitorEvent extends Event {
|
||||
monitorId: ObjectID;
|
||||
}
|
||||
|
||||
export interface ComponentProps {
|
||||
startDate: Date;
|
||||
@@ -28,27 +33,173 @@ const MonitorUptimeGraph: FunctionComponent<ComponentProps> = (
|
||||
): ReactElement => {
|
||||
const [events, setEvents] = useState<Array<Event>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const eventList: Array<Event> = [];
|
||||
// convert data to events.
|
||||
for (let i: number = 0; i < props.items.length; i++) {
|
||||
if (!props.items[i]) {
|
||||
break;
|
||||
/**
|
||||
* This function, `getMonitorEventsForId`, takes a `monitorId` as an argument and returns an array of `MonitorEvent` objects.
|
||||
* @param {ObjectID} monitorId - The ID of the monitor for which events are to be fetched.
|
||||
* @returns {Array<MonitorEvent>} - An array of `MonitorEvent` objects.
|
||||
*/
|
||||
const getMonitorEventsForId: (
|
||||
monitorId: ObjectID
|
||||
) => Array<MonitorEvent> = (monitorId: ObjectID): Array<MonitorEvent> => {
|
||||
// Initialize an empty array to store the monitor events.
|
||||
const eventList: Array<MonitorEvent> = [];
|
||||
|
||||
const monitorEvents: Array<MonitorStatusTimeline> = props.items.filter(
|
||||
(item: MonitorStatusTimeline) => {
|
||||
return item.monitorId?.toString() === monitorId.toString();
|
||||
}
|
||||
);
|
||||
|
||||
// Loop through the items in the props object.
|
||||
for (let i: number = 0; i < monitorEvents.length; i++) {
|
||||
// If the current item is null or undefined, skip to the next iteration.
|
||||
if (!monitorEvents[i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set the start date of the event to the creation date of the current item. If it doesn't exist, use the current date.
|
||||
const startDate: Date =
|
||||
monitorEvents[i]!.createdAt || OneUptimeDate.getCurrentDate();
|
||||
|
||||
// Initialize the end date as the current date.
|
||||
let endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
|
||||
// If there is a next item and it has a creation date, use that as the end date.
|
||||
if (monitorEvents[i + 1] && monitorEvents[i + 1]!.createdAt) {
|
||||
endDate = monitorEvents[i + 1]!.createdAt!;
|
||||
}
|
||||
|
||||
// Push a new MonitorEvent object to the eventList array with properties from the current item and calculated dates.
|
||||
eventList.push({
|
||||
startDate:
|
||||
props.items[i]!.createdAt || OneUptimeDate.getCurrentDate(),
|
||||
endDate:
|
||||
props.items[i + 1] && props.items[i + 1]!.createdAt
|
||||
? (props.items[i + 1]?.createdAt as Date)
|
||||
: OneUptimeDate.getCurrentDate(),
|
||||
label: props.items[i]?.monitorStatus?.name || 'Operational',
|
||||
priority: props.items[i]?.monitorStatus?.priority || 0,
|
||||
color: props.items[i]?.monitorStatus?.color || Green,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
label: monitorEvents[i]?.monitorStatus?.name || 'Operational',
|
||||
priority: monitorEvents[i]?.monitorStatus?.priority || 0,
|
||||
color: monitorEvents[i]?.monitorStatus?.color || Green,
|
||||
monitorId: monitorEvents[i]!.monitorId!,
|
||||
});
|
||||
}
|
||||
|
||||
// Return the populated eventList array.
|
||||
return eventList;
|
||||
};
|
||||
|
||||
const getMonitorEvents: () => Array<MonitorEvent> =
|
||||
(): Array<MonitorEvent> => {
|
||||
// get all distinct monitor ids.
|
||||
const monitorIds: Array<ObjectID> = [];
|
||||
|
||||
for (let i: number = 0; i < props.items.length; i++) {
|
||||
if (!props.items[i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const monitorId: string | undefined =
|
||||
props.items[i]!.monitorId?.toString();
|
||||
|
||||
if (!monitorId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!monitorIds.find((item: ObjectID) => {
|
||||
return item.toString() === monitorId;
|
||||
})
|
||||
) {
|
||||
monitorIds.push(new ObjectID(monitorId));
|
||||
}
|
||||
}
|
||||
|
||||
const eventList: Array<MonitorEvent> = [];
|
||||
// convert data to events.
|
||||
|
||||
for (const monitorId of monitorIds) {
|
||||
const monitorEvents: Array<MonitorEvent> =
|
||||
getMonitorEventsForId(monitorId);
|
||||
eventList.push(...monitorEvents);
|
||||
}
|
||||
|
||||
// sort event list by start date.
|
||||
eventList.sort((a: MonitorEvent, b: MonitorEvent) => {
|
||||
if (OneUptimeDate.isAfter(a.startDate, b.startDate)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (OneUptimeDate.isAfter(b.startDate, a.startDate)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
return [...eventList];
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const monitorEventList: Array<Event> = getMonitorEvents();
|
||||
|
||||
const eventList: Array<Event> = [];
|
||||
|
||||
for (const monitorEvent of monitorEventList) {
|
||||
// if this event starts after the last event, then add it to the list directly.
|
||||
if (
|
||||
eventList.length === 0 ||
|
||||
OneUptimeDate.isAfter(
|
||||
monitorEvent.startDate,
|
||||
eventList[eventList.length - 1]!.endDate
|
||||
) ||
|
||||
OneUptimeDate.isEqualBySeconds(
|
||||
monitorEvent.startDate,
|
||||
eventList[eventList.length - 1]!.endDate
|
||||
)
|
||||
) {
|
||||
eventList.push(monitorEvent);
|
||||
continue;
|
||||
}
|
||||
|
||||
// if this event starts before the last event, then we need to check if it ends before the last event. If it does, then we can skip this event if the monitrEvent is of lower priority than the last event. If it is of higher priority, then we need to add it to the list and remove the last event from the list.
|
||||
if (
|
||||
OneUptimeDate.isBefore(
|
||||
monitorEvent.startDate,
|
||||
eventList[eventList.length - 1]!.endDate
|
||||
)
|
||||
) {
|
||||
if (
|
||||
monitorEvent.priority >
|
||||
eventList[eventList.length - 1]!.priority
|
||||
) {
|
||||
// end the last event at the start of this event.
|
||||
|
||||
const tempLastEvent: Event = {
|
||||
...eventList[eventList.length - 1],
|
||||
} as Event;
|
||||
|
||||
eventList[eventList.length - 1]!.endDate =
|
||||
monitorEvent.startDate;
|
||||
eventList.push(monitorEvent);
|
||||
|
||||
// if the monitorEvent endDate is before the end of the last event, then we need to add the end of the last event to the list.
|
||||
|
||||
if (
|
||||
OneUptimeDate.isBefore(
|
||||
monitorEvent.endDate,
|
||||
tempLastEvent.endDate
|
||||
)
|
||||
) {
|
||||
eventList.push({
|
||||
startDate: monitorEvent.endDate,
|
||||
endDate: tempLastEvent.endDate,
|
||||
label: tempLastEvent.label,
|
||||
priority: tempLastEvent.priority,
|
||||
color: tempLastEvent.color,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
setEvents(eventList);
|
||||
}, [props.items]);
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ const Statusbubble: FunctionComponent<ComponentProps> = (
|
||||
return (
|
||||
<div className="flex" style={props.style}>
|
||||
<div
|
||||
className="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full mr-2"
|
||||
className="animate-pulse flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full mr-2"
|
||||
style={{
|
||||
backgroundColor: props.color
|
||||
? props.color.toString()
|
||||
|
||||
322
CommonUI/src/Tests/Components/DuplicateModel.test.tsx
Normal file
322
CommonUI/src/Tests/Components/DuplicateModel.test.tsx
Normal file
@@ -0,0 +1,322 @@
|
||||
import React from 'react';
|
||||
import BaseModel from 'Common/Models/BaseModel';
|
||||
import Select from '../../Utils/ModelAPI/Select';
|
||||
import { ModelField } from '../../Components/Forms/ModelForm';
|
||||
import TableMetaData from 'Common/Types/Database/TableMetadata';
|
||||
import IconProp from 'Common/Types/Icon/IconProp';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
within,
|
||||
fireEvent,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import DuplicateModel from '../../Components/DuplicateModel/DuplicateModel';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import CrudApiEndpoint from 'Common/Types/Database/CrudApiEndpoint';
|
||||
import { act } from 'react-test-renderer';
|
||||
import Route from 'Common/Types/API/Route';
|
||||
|
||||
@TableMetaData({
|
||||
tableName: 'Foo',
|
||||
singularName: 'Foo',
|
||||
pluralName: 'Foos',
|
||||
icon: IconProp.Wrench,
|
||||
tableDescription: 'A test model',
|
||||
})
|
||||
@CrudApiEndpoint(new Route('/testModel'))
|
||||
class TestModel extends BaseModel {
|
||||
public changeThis?: string = 'original';
|
||||
}
|
||||
|
||||
jest.mock('../../Utils/ModelAPI/ModelAPI', () => {
|
||||
return {
|
||||
getItem: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
changeThis: 'changed',
|
||||
setValue: function (key: 'changeThis', value: string) {
|
||||
this[key] = value;
|
||||
},
|
||||
removeValue: jest.fn(),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
changeThis: 'changed',
|
||||
setValue: function (key: 'changeThis', value: string) {
|
||||
this[key] = value;
|
||||
},
|
||||
removeValue: jest.fn(),
|
||||
})
|
||||
.mockResolvedValueOnce(undefined),
|
||||
create: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
id: 'foobar',
|
||||
changeThis: 'changed',
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce(undefined),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../Utils/Navigation', () => {
|
||||
return {
|
||||
navigate: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('DuplicateModel', () => {
|
||||
const fieldsToDuplicate: Select<TestModel> = {};
|
||||
const fieldsToChange: Array<ModelField<TestModel>> = [
|
||||
{
|
||||
field: {
|
||||
changeThis: true,
|
||||
},
|
||||
title: 'Change This',
|
||||
required: false,
|
||||
placeholder: 'You can change this',
|
||||
},
|
||||
];
|
||||
it('renders correctly', () => {
|
||||
render(
|
||||
<DuplicateModel
|
||||
modelType={TestModel}
|
||||
modelId={new ObjectID('foo')}
|
||||
fieldsToDuplicate={fieldsToDuplicate}
|
||||
fieldsToChange={fieldsToChange}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('card-details-heading')?.textContent).toBe(
|
||||
'Duplicate Foo'
|
||||
);
|
||||
expect(screen.getByTestId('card-description')?.textContent).toBe(
|
||||
'Duplicating this foo will create another foo exactly like this one.'
|
||||
);
|
||||
expect(screen.getByTestId('card-button')?.textContent).toBe(
|
||||
'Duplicate Foo'
|
||||
);
|
||||
});
|
||||
it('shows confirmation modal when duplicate button is clicked', () => {
|
||||
render(
|
||||
<DuplicateModel
|
||||
modelType={TestModel}
|
||||
modelId={new ObjectID('foo')}
|
||||
fieldsToDuplicate={fieldsToDuplicate}
|
||||
fieldsToChange={fieldsToChange}
|
||||
/>
|
||||
);
|
||||
const button: HTMLElement = screen.getByRole('button', {
|
||||
name: 'Duplicate Foo',
|
||||
});
|
||||
fireEvent.click(button);
|
||||
expect(screen.getByRole('dialog')).toBeDefined();
|
||||
const confirmDialog: HTMLElement = screen.getByRole('dialog');
|
||||
expect(
|
||||
within(confirmDialog).getByTestId('modal-title')?.textContent
|
||||
).toBe('Duplicate Foo');
|
||||
expect(
|
||||
within(confirmDialog).getByTestId('modal-description')?.textContent
|
||||
).toBe('Are you sure you want to duplicate this foo?');
|
||||
expect(
|
||||
within(confirmDialog).getByTestId('modal-footer-submit-button')
|
||||
?.textContent
|
||||
).toBe('Duplicate Foo');
|
||||
expect(
|
||||
within(confirmDialog).getByTestId('modal-footer-close-button')
|
||||
?.textContent
|
||||
).toBe('Close');
|
||||
});
|
||||
it('duplicates item when confirmation button is clicked', async () => {
|
||||
const onDuplicateSuccess: (item: TestModel) => void = jest.fn();
|
||||
render(
|
||||
<DuplicateModel
|
||||
modelType={TestModel}
|
||||
modelId={new ObjectID('foo')}
|
||||
fieldsToDuplicate={fieldsToDuplicate}
|
||||
fieldsToChange={fieldsToChange}
|
||||
onDuplicateSuccess={onDuplicateSuccess}
|
||||
navigateToOnSuccess={new Route('/done')}
|
||||
/>
|
||||
);
|
||||
const button: HTMLElement = screen.getByRole('button', {
|
||||
name: 'Duplicate Foo',
|
||||
});
|
||||
void act(() => {
|
||||
fireEvent.click(button);
|
||||
});
|
||||
const dialog: HTMLElement = screen.getByRole('dialog');
|
||||
const confirmationButton: HTMLElement = within(dialog).getByRole(
|
||||
'button',
|
||||
{
|
||||
name: 'Duplicate Foo',
|
||||
}
|
||||
);
|
||||
void act(() => {
|
||||
fireEvent.click(confirmationButton);
|
||||
});
|
||||
await waitFor(() => {
|
||||
return expect(onDuplicateSuccess).toBeCalledWith({
|
||||
id: 'foobar',
|
||||
changeThis: 'changed',
|
||||
});
|
||||
});
|
||||
await waitFor(() => {
|
||||
return expect(
|
||||
require('../../Utils/Navigation').navigate
|
||||
).toBeCalledWith(new Route('/done/foobar'));
|
||||
});
|
||||
});
|
||||
it('closes confirmation dialog when close button is clicked', () => {
|
||||
const onDuplicateSuccess: (item: TestModel) => void = jest.fn();
|
||||
render(
|
||||
<DuplicateModel
|
||||
modelType={TestModel}
|
||||
modelId={new ObjectID('foo')}
|
||||
fieldsToDuplicate={fieldsToDuplicate}
|
||||
fieldsToChange={fieldsToChange}
|
||||
onDuplicateSuccess={onDuplicateSuccess}
|
||||
navigateToOnSuccess={new Route('/done')}
|
||||
/>
|
||||
);
|
||||
const button: HTMLElement = screen.getByRole('button', {
|
||||
name: 'Duplicate Foo',
|
||||
});
|
||||
void act(() => {
|
||||
fireEvent.click(button);
|
||||
});
|
||||
const dialog: HTMLElement = screen.getByRole('dialog');
|
||||
const closeButton: HTMLElement = within(dialog).getByRole('button', {
|
||||
name: 'Close',
|
||||
});
|
||||
void act(() => {
|
||||
fireEvent.click(closeButton);
|
||||
});
|
||||
expect(screen.queryByRole('dialog')).toBeFalsy();
|
||||
});
|
||||
it('handles could not create error correctly', async () => {
|
||||
const onDuplicateSuccess: (item: TestModel) => void = jest.fn();
|
||||
render(
|
||||
<DuplicateModel
|
||||
modelType={TestModel}
|
||||
modelId={new ObjectID('foo')}
|
||||
fieldsToDuplicate={fieldsToDuplicate}
|
||||
fieldsToChange={fieldsToChange}
|
||||
onDuplicateSuccess={onDuplicateSuccess}
|
||||
navigateToOnSuccess={new Route('/done')}
|
||||
/>
|
||||
);
|
||||
const button: HTMLElement = screen.getByRole('button', {
|
||||
name: 'Duplicate Foo',
|
||||
});
|
||||
void act(() => {
|
||||
fireEvent.click(button);
|
||||
});
|
||||
const dialog: HTMLElement = screen.getByRole('dialog');
|
||||
const confirmationButton: HTMLElement = within(dialog).getByRole(
|
||||
'button',
|
||||
{
|
||||
name: 'Duplicate Foo',
|
||||
}
|
||||
);
|
||||
void act(() => {
|
||||
fireEvent.click(confirmationButton);
|
||||
});
|
||||
await screen.findByText('Duplicate Error');
|
||||
const errorDialog: HTMLElement = screen.getByRole('dialog');
|
||||
expect(
|
||||
within(errorDialog).getByTestId('modal-title')?.textContent
|
||||
).toBe('Duplicate Error');
|
||||
expect(
|
||||
within(errorDialog).getByTestId('confirm-modal-description')
|
||||
?.textContent
|
||||
).toBe('Error: Could not create Foo');
|
||||
expect(
|
||||
within(errorDialog).getByTestId('modal-footer-submit-button')
|
||||
?.textContent
|
||||
).toBe('Close');
|
||||
});
|
||||
it('handles item not found error correctly', async () => {
|
||||
const onDuplicateSuccess: (item: TestModel) => void = jest.fn();
|
||||
render(
|
||||
<DuplicateModel
|
||||
modelType={TestModel}
|
||||
modelId={new ObjectID('foo')}
|
||||
fieldsToDuplicate={fieldsToDuplicate}
|
||||
fieldsToChange={fieldsToChange}
|
||||
onDuplicateSuccess={onDuplicateSuccess}
|
||||
navigateToOnSuccess={new Route('/done')}
|
||||
/>
|
||||
);
|
||||
const button: HTMLElement = screen.getByRole('button', {
|
||||
name: 'Duplicate Foo',
|
||||
});
|
||||
void act(() => {
|
||||
fireEvent.click(button);
|
||||
});
|
||||
const dialog: HTMLElement = screen.getByRole('dialog');
|
||||
const confirmationButton: HTMLElement = within(dialog).getByRole(
|
||||
'button',
|
||||
{
|
||||
name: 'Duplicate Foo',
|
||||
}
|
||||
);
|
||||
void act(() => {
|
||||
fireEvent.click(confirmationButton);
|
||||
});
|
||||
await screen.findByText('Duplicate Error');
|
||||
const errorDialog: HTMLElement = screen.getByRole('dialog');
|
||||
expect(
|
||||
within(errorDialog).getByTestId('modal-title')?.textContent
|
||||
).toBe('Duplicate Error');
|
||||
expect(
|
||||
within(errorDialog).getByTestId('confirm-modal-description')
|
||||
?.textContent
|
||||
).toBe('Error: Could not find Foo with id foo');
|
||||
expect(
|
||||
within(errorDialog).getByTestId('modal-footer-submit-button')
|
||||
?.textContent
|
||||
).toBe('Close');
|
||||
});
|
||||
it('closes error dialog when close button is clicked', async () => {
|
||||
const onDuplicateSuccess: (item: TestModel) => void = jest.fn();
|
||||
render(
|
||||
<DuplicateModel
|
||||
modelType={TestModel}
|
||||
modelId={new ObjectID('foo')}
|
||||
fieldsToDuplicate={fieldsToDuplicate}
|
||||
fieldsToChange={fieldsToChange}
|
||||
onDuplicateSuccess={onDuplicateSuccess}
|
||||
navigateToOnSuccess={new Route('/done')}
|
||||
/>
|
||||
);
|
||||
const button: HTMLElement = screen.getByRole('button', {
|
||||
name: 'Duplicate Foo',
|
||||
});
|
||||
void act(() => {
|
||||
fireEvent.click(button);
|
||||
});
|
||||
const dialog: HTMLElement = screen.getByRole('dialog');
|
||||
const confirmationButton: HTMLElement = within(dialog).getByRole(
|
||||
'button',
|
||||
{
|
||||
name: 'Duplicate Foo',
|
||||
}
|
||||
);
|
||||
void act(() => {
|
||||
fireEvent.click(confirmationButton);
|
||||
});
|
||||
await screen.findByText('Duplicate Error');
|
||||
const errorDialog: HTMLElement = screen.getByRole('dialog');
|
||||
const closeButton: HTMLElement = within(errorDialog).getByRole(
|
||||
'button',
|
||||
{
|
||||
name: 'Close',
|
||||
}
|
||||
);
|
||||
void act(() => {
|
||||
fireEvent.click(closeButton);
|
||||
});
|
||||
expect(screen.queryByRole('dialog')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -5,10 +5,13 @@ import Route from 'Common/Types/API/Route';
|
||||
import RouteMap, { RouteUtil } from '../../Utils/RouteMap';
|
||||
import PageMap from '../../Utils/PageMap';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import Icon from 'CommonUI/src/Components/Icon/Icon';
|
||||
import IconProp from 'Common/Types/Icon/IconProp';
|
||||
|
||||
export interface ComponentProps {
|
||||
monitor: Monitor;
|
||||
onNavigateComplete?: (() => void) | undefined;
|
||||
showIcon?: boolean;
|
||||
}
|
||||
|
||||
const MonitorElement: FunctionComponent<ComponentProps> = (
|
||||
@@ -26,7 +29,17 @@ const MonitorElement: FunctionComponent<ComponentProps> = (
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span>{props.monitor.name}</span>
|
||||
<span className="flex">
|
||||
{props.showIcon ? (
|
||||
<Icon
|
||||
icon={IconProp.AltGlobe}
|
||||
className="w-5 h-5 mr-1"
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}{' '}
|
||||
{props.monitor.name}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ const MonitorsElement: FunctionComponent<ComponentProps> = (
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex">
|
||||
{props.monitors.map((monitor: Monitor, i: number) => {
|
||||
return (
|
||||
<span key={i}>
|
||||
<span key={i} className="flex">
|
||||
<MonitorElement
|
||||
monitor={monitor}
|
||||
onNavigateComplete={props.onNavigateComplete}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import MonitorGroup from 'Model/Models/MonitorGroup';
|
||||
import Link from 'CommonUI/src/Components/Link/Link';
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import RouteMap, { RouteUtil } from '../../Utils/RouteMap';
|
||||
import PageMap from '../../Utils/PageMap';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import Icon from 'CommonUI/src/Components/Icon/Icon';
|
||||
import IconProp from 'Common/Types/Icon/IconProp';
|
||||
|
||||
export interface ComponentProps {
|
||||
monitorGroup: MonitorGroup;
|
||||
onNavigateComplete?: (() => void) | undefined;
|
||||
showIcon?: boolean;
|
||||
}
|
||||
|
||||
const MonitorGroupElement: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
if (props.monitorGroup._id) {
|
||||
return (
|
||||
<Link
|
||||
onNavigateComplete={props.onNavigateComplete}
|
||||
className="hover:underline"
|
||||
to={RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITOR_GROUP_VIEW] as Route,
|
||||
{
|
||||
modelId: new ObjectID(props.monitorGroup._id as string),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span className="flex">
|
||||
{props.showIcon ? (
|
||||
<Icon
|
||||
icon={IconProp.Squares}
|
||||
className="w-5 h-5 mr-1"
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}{' '}
|
||||
{props.monitorGroup.name}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>{props.monitorGroup.name}</span>;
|
||||
};
|
||||
|
||||
export default MonitorGroupElement;
|
||||
@@ -74,7 +74,7 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
RouteMap[PageMap.MONITOR_GROUPS] as Route
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Folder}
|
||||
icon={IconProp.Squares}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
) : (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import ModelPage from 'CommonUI/src/Components/Page/ModelPage';
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import React, { FunctionComponent, ReactElement, useState } from 'react';
|
||||
import PageMap from '../../../Utils/PageMap';
|
||||
import RouteMap, { RouteUtil } from '../../../Utils/RouteMap';
|
||||
import PageComponentProps from '../../PageComponentProps';
|
||||
@@ -16,12 +16,61 @@ import LabelsElement from '../../../Components/Label/Labels';
|
||||
import MonitorGroup from 'Model/Models/MonitorGroup';
|
||||
import JSONFunctions from 'Common/Types/JSONFunctions';
|
||||
import CurrentStatusElement from '../../../Components/MonitorGroup/CurrentStatus';
|
||||
import Card from 'CommonUI/src/Components/Card/Card';
|
||||
import MonitorUptimeGraph from 'CommonUI/src/Components/MonitorGraphs/Uptime';
|
||||
import useAsyncEffect from 'use-async-effect';
|
||||
import ModelAPI, { ListResult } from 'CommonUI/src/Utils/ModelAPI/ModelAPI';
|
||||
import MonitorStatusTimeline from 'Model/Models/MonitorStatusTimeline';
|
||||
import { LIMIT_PER_PROJECT } from 'Common/Types/Database/LimitMax';
|
||||
import URL from 'Common/Types/API/URL';
|
||||
import { DASHBOARD_API_URL } from 'CommonUI/src/Config';
|
||||
import API from 'CommonUI/src/Utils/API/API';
|
||||
import OneUptimeDate from 'Common/Types/Date';
|
||||
|
||||
const MonitorGroupView: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps
|
||||
): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID();
|
||||
|
||||
const [data, setData] = useState<Array<MonitorStatusTimeline>>([]);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
await fetchItem();
|
||||
}, []);
|
||||
|
||||
const fetchItem: () => Promise<void> = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const monitorStatus: ListResult<MonitorStatusTimeline> =
|
||||
await ModelAPI.getList(
|
||||
MonitorStatusTimeline,
|
||||
{},
|
||||
LIMIT_PER_PROJECT,
|
||||
0,
|
||||
{},
|
||||
{},
|
||||
{
|
||||
overrideRequestUrl: URL.fromString(
|
||||
DASHBOARD_API_URL.toString()
|
||||
)
|
||||
.addRoute(new MonitorGroup().getCrudApiPath()!)
|
||||
.addRoute('/timeline/')
|
||||
.addRoute(`/${modelId.toString()}`),
|
||||
}
|
||||
);
|
||||
|
||||
setData(monitorStatus.data);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<ModelPage
|
||||
title="Monitor Group"
|
||||
@@ -178,6 +227,19 @@ const MonitorGroupView: FunctionComponent<PageComponentProps> = (
|
||||
modelId: modelId,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Card
|
||||
title="Uptime Graph"
|
||||
description="Here the 90 day uptime history of this monitor group."
|
||||
>
|
||||
<MonitorUptimeGraph
|
||||
error={error}
|
||||
items={data}
|
||||
startDate={OneUptimeDate.getSomeDaysAgo(90)}
|
||||
endDate={OneUptimeDate.getCurrentDate()}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</Card>
|
||||
</ModelPage>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,6 +30,10 @@ import JSONFunctions from 'Common/Types/JSONFunctions';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import API from 'CommonUI/src/Utils/API/API';
|
||||
import StatusPage from 'Model/Models/StatusPage';
|
||||
import { ModelField } from 'CommonUI/src/Components/Forms/ModelForm';
|
||||
import MonitorGroup from 'Model/Models/MonitorGroup';
|
||||
import Link from 'CommonUI/src/Components/Link/Link';
|
||||
import MonitorGroupElement from '../../../Components/MonitorGroup/MonitorGroupElement';
|
||||
|
||||
const StatusPageDelete: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps
|
||||
@@ -40,6 +44,8 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const [addMonitorGroup, setAddMonitorGroup] = useState<boolean>(false);
|
||||
|
||||
const fetchGroups: Function = async () => {
|
||||
setError('');
|
||||
setIsLoading(true);
|
||||
@@ -76,6 +82,143 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
|
||||
fetchGroups();
|
||||
}, []);
|
||||
|
||||
const getFooterForMonitor: Function = (): ReactElement => {
|
||||
if (props.currentProject?.isFeatureFlagMonitorGroupsEnabled) {
|
||||
if (!addMonitorGroup) {
|
||||
return (
|
||||
<Link
|
||||
onClick={() => {
|
||||
setAddMonitorGroup(true);
|
||||
}}
|
||||
className="mt-1 text-sm text-gray-500 underline"
|
||||
>
|
||||
<div>
|
||||
<p> Add a Monitor Group instead. </p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
onClick={() => {
|
||||
setAddMonitorGroup(false);
|
||||
}}
|
||||
className="mt-1 text-sm text-gray-500 underline"
|
||||
>
|
||||
<div>
|
||||
<p> Add a Monitor instead. </p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
let formFields: Array<ModelField<StatusPageResource>> = [
|
||||
{
|
||||
field: {
|
||||
monitor: true,
|
||||
},
|
||||
title: 'Monitor',
|
||||
description:
|
||||
'Select monitor that will be shown on the status page.',
|
||||
fieldType: FormFieldSchemaType.Dropdown,
|
||||
dropdownModal: {
|
||||
type: Monitor,
|
||||
labelField: 'name',
|
||||
valueField: '_id',
|
||||
},
|
||||
required: true,
|
||||
placeholder: 'Select Monitor',
|
||||
stepId: 'monitor-details',
|
||||
footerElement: getFooterForMonitor(),
|
||||
},
|
||||
];
|
||||
|
||||
if (addMonitorGroup) {
|
||||
formFields = [
|
||||
{
|
||||
field: {
|
||||
monitorGroup: true,
|
||||
},
|
||||
title: 'Monitor Group',
|
||||
description:
|
||||
'Select monitor group that will be shown on the status page.',
|
||||
fieldType: FormFieldSchemaType.Dropdown,
|
||||
dropdownModal: {
|
||||
type: MonitorGroup,
|
||||
labelField: 'name',
|
||||
valueField: '_id',
|
||||
},
|
||||
required: true,
|
||||
placeholder: 'Select Monitor Group',
|
||||
stepId: 'monitor-details',
|
||||
footerElement: getFooterForMonitor(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
formFields = formFields.concat([
|
||||
{
|
||||
field: {
|
||||
displayName: true,
|
||||
},
|
||||
title: 'Display Name',
|
||||
description:
|
||||
'This will be the name that will be shown on the status page',
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: 'Display Name',
|
||||
stepId: 'monitor-details',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
displayDescription: true,
|
||||
},
|
||||
title: 'Description',
|
||||
fieldType: FormFieldSchemaType.Markdown,
|
||||
required: false,
|
||||
placeholder: '',
|
||||
stepId: 'monitor-details',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
displayTooltip: true,
|
||||
},
|
||||
title: 'Tooltip ',
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
required: false,
|
||||
description:
|
||||
'This will show up as tooltip beside the resource on your status page.',
|
||||
placeholder: 'Tooltip',
|
||||
stepId: 'advanced',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
showCurrentStatus: true,
|
||||
},
|
||||
title: 'Show Current Resource Status',
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
defaultValue: true,
|
||||
description:
|
||||
'Current Resource Status will be shown beside this resource on your status page.',
|
||||
stepId: 'advanced',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
showStatusHistoryChart: true,
|
||||
},
|
||||
title: 'Show Status History Chart',
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
description: 'Show resource status history for the past 90 days. ',
|
||||
defaultValue: true,
|
||||
stepId: 'advanced',
|
||||
},
|
||||
]);
|
||||
|
||||
const getModelTable: Function = (
|
||||
statusPageGroupId: ObjectID | null,
|
||||
statusPageGroupName: string | null
|
||||
@@ -137,86 +280,17 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
|
||||
id: 'advanced',
|
||||
},
|
||||
]}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
monitor: true,
|
||||
},
|
||||
title: 'Monitor',
|
||||
description:
|
||||
'Select monitor that will be shown on the status page.',
|
||||
fieldType: FormFieldSchemaType.Dropdown,
|
||||
dropdownModal: {
|
||||
type: Monitor,
|
||||
labelField: 'name',
|
||||
valueField: '_id',
|
||||
},
|
||||
required: true,
|
||||
placeholder: 'Select Monitor',
|
||||
stepId: 'monitor-details',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
displayName: true,
|
||||
},
|
||||
title: 'Display Name',
|
||||
description:
|
||||
'This will be the name that will be shown on the status page',
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: 'Display Name',
|
||||
stepId: 'monitor-details',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
displayDescription: true,
|
||||
},
|
||||
title: 'Description',
|
||||
fieldType: FormFieldSchemaType.Markdown,
|
||||
required: false,
|
||||
placeholder: '',
|
||||
stepId: 'monitor-details',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
displayTooltip: true,
|
||||
},
|
||||
title: 'Tooltip ',
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
required: false,
|
||||
description:
|
||||
'This will show up as tooltip beside the resource on your status page.',
|
||||
placeholder: 'Tooltip',
|
||||
stepId: 'advanced',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
showCurrentStatus: true,
|
||||
},
|
||||
title: 'Show Current Resource Status',
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
defaultValue: true,
|
||||
description:
|
||||
'Current Resource Status will be shown beside this resource on your status page.',
|
||||
stepId: 'advanced',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
showStatusHistoryChart: true,
|
||||
},
|
||||
title: 'Show Status History Chart',
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
description:
|
||||
'Show resource status history for the past 90 days. ',
|
||||
defaultValue: true,
|
||||
stepId: 'advanced',
|
||||
},
|
||||
]}
|
||||
formFields={formFields}
|
||||
showRefreshButton={true}
|
||||
showFilterButton={true}
|
||||
viewPageRoute={Navigation.getCurrentRoute()}
|
||||
selectMoreFields={{
|
||||
monitorGroup: {
|
||||
name: true,
|
||||
_id: true,
|
||||
projectId: true,
|
||||
},
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
field: {
|
||||
@@ -226,7 +300,10 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
|
||||
projectId: true,
|
||||
},
|
||||
},
|
||||
title: 'Monitor',
|
||||
title: props.currentProject
|
||||
?.isFeatureFlagMonitorGroupsEnabled
|
||||
? 'Resource'
|
||||
: 'Monitor',
|
||||
type: FieldType.Entity,
|
||||
isFilterable: true,
|
||||
filterEntityType: Monitor,
|
||||
@@ -239,17 +316,47 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
|
||||
value: '_id',
|
||||
},
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
return (
|
||||
<MonitorElement
|
||||
monitor={
|
||||
JSONFunctions.fromJSON(
|
||||
(item['monitor'] as JSONObject) ||
|
||||
[],
|
||||
Monitor
|
||||
) as Monitor
|
||||
}
|
||||
/>
|
||||
);
|
||||
if (item['monitor']) {
|
||||
return (
|
||||
<MonitorElement
|
||||
monitor={
|
||||
JSONFunctions.fromJSON(
|
||||
(item[
|
||||
'monitor'
|
||||
] as JSONObject) || [],
|
||||
Monitor
|
||||
) as Monitor
|
||||
}
|
||||
showIcon={
|
||||
props.currentProject
|
||||
?.isFeatureFlagMonitorGroupsEnabled ||
|
||||
false
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (item['monitorGroup']) {
|
||||
return (
|
||||
<MonitorGroupElement
|
||||
monitorGroup={
|
||||
JSONFunctions.fromJSON(
|
||||
(item[
|
||||
'monitorGroup'
|
||||
] as JSONObject) || [],
|
||||
MonitorGroup
|
||||
) as MonitorGroup
|
||||
}
|
||||
showIcon={
|
||||
props.currentProject
|
||||
?.isFeatureFlagMonitorGroupsEnabled ||
|
||||
false
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,6 +7,8 @@ import SideMenuSection from 'CommonUI/src/Components/SideMenu/SideMenuSection';
|
||||
import RouteMap, { RouteUtil } from '../../../Utils/RouteMap';
|
||||
import PageMap from '../../../Utils/PageMap';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import ProjectUtil from 'CommonUI/src/Utils/Project';
|
||||
import Project from 'Model/Models/Project';
|
||||
|
||||
export interface ComponentProps {
|
||||
modelId: ObjectID;
|
||||
@@ -15,6 +17,8 @@ export interface ComponentProps {
|
||||
const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
const project: Project | null = ProjectUtil.getCurrentProject();
|
||||
|
||||
return (
|
||||
<SideMenu>
|
||||
<SideMenuSection title="Basic">
|
||||
@@ -56,7 +60,9 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
<SideMenuSection title="Resources">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: 'Monitors',
|
||||
title: project?.isFeatureFlagMonitorGroupsEnabled
|
||||
? 'Resources'
|
||||
: 'Monitors',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[
|
||||
PageMap.STATUS_PAGE_VIEW_RESOURCES
|
||||
|
||||
@@ -22,6 +22,7 @@ import Monitor from './Monitor';
|
||||
import StatusPageGroup from './StatusPageGroup';
|
||||
import CanAccessIfCanReadOn from 'Common/Types/Database/CanAccessIfCanReadOn';
|
||||
import EnableDocumentation from 'Common/Types/Database/EnableDocumentation';
|
||||
import MonitorGroup from './MonitorGroup';
|
||||
|
||||
@EnableDocumentation()
|
||||
@CanAccessIfCanReadOn('statusPage')
|
||||
@@ -271,17 +272,95 @@ export default class StatusPageResource extends BaseModel {
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
required: false,
|
||||
title: 'Monitor ID',
|
||||
description:
|
||||
'Relation to Monitor ID Resource in which this object belongs',
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public monitorId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CanCreateStatusPageResource,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CanReadStatusPageResource,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CanEditStatusPageResource,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: 'monitorGroupId',
|
||||
type: TableColumnType.Entity,
|
||||
modelType: MonitorGroup,
|
||||
title: 'Monitor Group',
|
||||
description:
|
||||
'Relation to Monitor Group Resource in which this object belongs',
|
||||
})
|
||||
@ManyToOne(
|
||||
(_type: string) => {
|
||||
return MonitorGroup;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: 'CASCADE',
|
||||
orphanedRowAction: 'nullify',
|
||||
}
|
||||
)
|
||||
@JoinColumn({ name: 'monitorGroupId' })
|
||||
public monitorGroup?: MonitorGroup = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CanCreateStatusPageResource,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CanReadStatusPageResource,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CanEditStatusPageResource,
|
||||
],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
title: 'Monitor Group ID',
|
||||
description:
|
||||
'Relation to Monitor Group ID Resource in which this object belongs',
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public monitorGroupId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
|
||||
@@ -62,6 +62,11 @@ upstream otel-collector {
|
||||
|
||||
server {
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain application/xml text/html application/javascript text/javascript text/css application/json;
|
||||
gzip_proxied no-cache no-store private expired auth;
|
||||
gzip_min_length 1000;
|
||||
|
||||
listen 80 default_server;
|
||||
|
||||
server_name _; # All domains.
|
||||
@@ -73,6 +78,7 @@ server {
|
||||
fastcgi_buffers 16 16k;
|
||||
fastcgi_buffer_size 32k;
|
||||
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
@@ -142,6 +148,11 @@ server {
|
||||
|
||||
server {
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain application/xml text/html application/javascript text/javascript text/css application/json;
|
||||
gzip_proxied no-cache no-store private expired auth;
|
||||
gzip_min_length 1000;
|
||||
|
||||
listen 443 default_server ssl; # Port HTTPS
|
||||
|
||||
|
||||
@@ -232,6 +243,11 @@ server {
|
||||
}
|
||||
|
||||
server {
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain application/xml text/html application/javascript text/javascript text/css application/json;
|
||||
gzip_proxied no-cache no-store private expired auth;
|
||||
gzip_min_length 1000;
|
||||
|
||||
listen 80;
|
||||
|
||||
@@ -403,6 +419,7 @@ server {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
proxy_pass http://status-page;
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ export default class WebsiteMonitor {
|
||||
let startTime: [number, number] = process.hrtime();
|
||||
let result: WebsiteResponse = await WebsiteRequest.fetch(url, {
|
||||
isHeadRequest: options.isHeadRequest,
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
if (
|
||||
@@ -123,6 +124,7 @@ export default class WebsiteMonitor {
|
||||
startTime = process.hrtime();
|
||||
result = await WebsiteRequest.fetch(url, {
|
||||
isHeadRequest: false,
|
||||
timeout: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,10 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
{},
|
||||
API.getDefaultHeaders(StatusPageUtil.getStatusPageId()!)
|
||||
);
|
||||
|
||||
if (!response.isSuccess()) {
|
||||
throw response;
|
||||
}
|
||||
const data: JSONObject = response.data;
|
||||
|
||||
const rawAnnouncements: JSONArray =
|
||||
|
||||
@@ -69,6 +69,10 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
{},
|
||||
API.getDefaultHeaders(StatusPageUtil.getStatusPageId()!)
|
||||
);
|
||||
|
||||
if (!response.isSuccess()) {
|
||||
throw response;
|
||||
}
|
||||
const data: JSONObject = response.data;
|
||||
|
||||
const announcements: Array<StatusPageAnnouncement> =
|
||||
|
||||
@@ -41,12 +41,14 @@ import StatusPageUtil from '../../Utils/StatusPage';
|
||||
import HTTPErrorResponse from 'Common/Types/API/HTTPErrorResponse';
|
||||
import { STATUS_PAGE_API_URL } from '../../Utils/Config';
|
||||
import Label from 'Model/Models/Label';
|
||||
import Dictionary from 'Common/Types/Dictionary';
|
||||
|
||||
export const getIncidentEventItem: Function = (
|
||||
incident: Incident,
|
||||
incidentPublicNotes: Array<IncidentPublicNote>,
|
||||
incidentStateTimelines: Array<IncidentStateTimeline>,
|
||||
statusPageResources: Array<StatusPageResource>,
|
||||
monitorsInGroup: Dictionary<Array<ObjectID>>,
|
||||
isPreviewPage: boolean,
|
||||
isSummary: boolean
|
||||
): EventItemComponentProps => {
|
||||
@@ -135,16 +137,44 @@ export const getIncidentEventItem: Function = (
|
||||
return OneUptimeDate.isAfter(a.date, b.date) === true ? 1 : -1;
|
||||
});
|
||||
|
||||
const monitorIds: Array<string | undefined> =
|
||||
const monitorIdsInThisIncident: Array<string | undefined> =
|
||||
incident.monitors?.map((monitor: Monitor) => {
|
||||
return monitor._id;
|
||||
}) || [];
|
||||
|
||||
const namesOfResources: Array<StatusPageResource> =
|
||||
let namesOfResources: Array<StatusPageResource> =
|
||||
statusPageResources.filter((resource: StatusPageResource) => {
|
||||
return monitorIds.includes(resource.monitorId?.toString());
|
||||
return monitorIdsInThisIncident.includes(
|
||||
resource.monitorId?.toString()
|
||||
);
|
||||
});
|
||||
|
||||
// add names of the groups as well.
|
||||
namesOfResources = namesOfResources.concat(
|
||||
statusPageResources.filter((resource: StatusPageResource) => {
|
||||
if (!resource.monitorGroupId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const monitorGroupId: string = resource.monitorGroupId.toString();
|
||||
|
||||
const monitorIdsInThisGroup: Array<ObjectID> =
|
||||
monitorsInGroup[monitorGroupId]! || [];
|
||||
|
||||
for (const monitorId of monitorIdsInThisGroup) {
|
||||
if (
|
||||
monitorIdsInThisIncident.find((id: string | undefined) => {
|
||||
return id?.toString() === monitorId.toString();
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
);
|
||||
|
||||
const data: EventItemComponentProps = {
|
||||
eventTitle: incident.title || '',
|
||||
eventDescription: incident.description,
|
||||
@@ -205,6 +235,10 @@ const Detail: FunctionComponent<PageComponentProps> = (
|
||||
const [parsedData, setParsedData] =
|
||||
useState<EventItemComponentProps | null>(null);
|
||||
|
||||
const [monitorsInGroup, setMonitorsInGroup] = useState<
|
||||
Dictionary<Array<ObjectID>>
|
||||
>({});
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
try {
|
||||
if (!StatusPageUtil.getStatusPageId()) {
|
||||
@@ -230,6 +264,10 @@ const Detail: FunctionComponent<PageComponentProps> = (
|
||||
{},
|
||||
API.getDefaultHeaders(StatusPageUtil.getStatusPageId()!)
|
||||
);
|
||||
|
||||
if (!response.isSuccess()) {
|
||||
throw response;
|
||||
}
|
||||
const data: JSONObject = response.data;
|
||||
|
||||
const incidentPublicNotes: Array<IncidentPublicNote> =
|
||||
@@ -249,12 +287,20 @@ const Detail: FunctionComponent<PageComponentProps> = (
|
||||
(data['statusPageResources'] as JSONArray) || [],
|
||||
StatusPageResource
|
||||
);
|
||||
|
||||
const incidentStateTimelines: Array<IncidentStateTimeline> =
|
||||
JSONFunctions.fromJSONArray(
|
||||
(data['incidentStateTimelines'] as JSONArray) || [],
|
||||
IncidentStateTimeline
|
||||
);
|
||||
|
||||
const monitorsInGroup: Dictionary<Array<ObjectID>> =
|
||||
JSONFunctions.deserialize(
|
||||
(data['monitorsInGroup'] as JSONObject) || {}
|
||||
) as Dictionary<Array<ObjectID>>;
|
||||
|
||||
setMonitorsInGroup(monitorsInGroup);
|
||||
|
||||
// save data. set()
|
||||
setIncidentPublicNotes(incidentPublicNotes);
|
||||
setIncident(incident);
|
||||
@@ -289,6 +335,7 @@ const Detail: FunctionComponent<PageComponentProps> = (
|
||||
incidentPublicNotes,
|
||||
incidentStateTimelines,
|
||||
statusPageResources,
|
||||
monitorsInGroup,
|
||||
StatusPageUtil.isPreviewPage()
|
||||
)
|
||||
);
|
||||
|
||||
@@ -55,6 +55,10 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
const [parsedData, setParsedData] =
|
||||
useState<EventHistoryListComponentProps | null>(null);
|
||||
|
||||
const [monitorsInGroup, setMonitorsInGroup] = useState<
|
||||
Dictionary<Array<ObjectID>>
|
||||
>({});
|
||||
|
||||
StatusPageUtil.checkIfUserHasLoggedIn();
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
@@ -78,6 +82,11 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
{},
|
||||
API.getDefaultHeaders(StatusPageUtil.getStatusPageId()!)
|
||||
);
|
||||
|
||||
if (!response.isSuccess()) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
const data: JSONObject = response.data;
|
||||
|
||||
const incidentPublicNotes: Array<IncidentPublicNote> =
|
||||
@@ -100,6 +109,13 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
IncidentStateTimeline
|
||||
);
|
||||
|
||||
const monitorsInGroup: Dictionary<Array<ObjectID>> =
|
||||
JSONFunctions.deserialize(
|
||||
(data['monitorsInGroup'] as JSONObject) || {}
|
||||
) as Dictionary<Array<ObjectID>>;
|
||||
|
||||
setMonitorsInGroup(monitorsInGroup);
|
||||
|
||||
// save data. set()
|
||||
setIncidentPublicNotes(incidentPublicNotes);
|
||||
setIncidents(incidents);
|
||||
@@ -148,6 +164,7 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
incidentPublicNotes,
|
||||
incidentStateTimelines,
|
||||
statusPageResources,
|
||||
monitorsInGroup,
|
||||
StatusPageUtil.isPreviewPage(),
|
||||
true
|
||||
)
|
||||
|
||||
@@ -37,7 +37,6 @@ import Route from 'Common/Types/API/Route';
|
||||
import ScheduledMaintenanceGroup from '../../Types/ScheduledMaintenanceGroup';
|
||||
import EventItem from 'CommonUI/src/Components/EventItem/EventItem';
|
||||
import HTTPResponse from 'Common/Types/API/HTTPResponse';
|
||||
import Monitor from 'Model/Models/Monitor';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import { getIncidentEventItem } from '../Incidents/Detail';
|
||||
import { getScheduledEventEventItem } from '../ScheduledEvent/Detail';
|
||||
@@ -110,6 +109,13 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
null
|
||||
);
|
||||
|
||||
const [monitorsInGroup, setMonitorsInGroup] = useState<
|
||||
Dictionary<Array<ObjectID>>
|
||||
>({});
|
||||
|
||||
const [monitorGroupCurrentStatuses, setMonitorGroupCurrentStatuses] =
|
||||
useState<Dictionary<ObjectID>>({});
|
||||
|
||||
StatusPageUtil.checkIfUserHasLoggedIn();
|
||||
|
||||
const loadPage: Function = async () => {
|
||||
@@ -133,6 +139,15 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
{},
|
||||
API.getDefaultHeaders(StatusPageUtil.getStatusPageId()!)
|
||||
);
|
||||
|
||||
if (!response.isSuccess()) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
if (!response.isSuccess()) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
const data: JSONObject = response.data;
|
||||
|
||||
const scheduledMaintenanceEventsPublicNotes: Array<ScheduledMaintenancePublicNote> =
|
||||
@@ -192,6 +207,7 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
(data['statusPage'] as JSONObject) || [],
|
||||
StatusPage
|
||||
);
|
||||
|
||||
const scheduledMaintenanceStateTimelines: Array<ScheduledMaintenanceStateTimeline> =
|
||||
JSONFunctions.fromJSONArray(
|
||||
(data['scheduledMaintenanceStateTimelines'] as JSONArray) ||
|
||||
@@ -199,6 +215,19 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
ScheduledMaintenanceStateTimeline
|
||||
);
|
||||
|
||||
const monitorsInGroup: Dictionary<Array<ObjectID>> =
|
||||
JSONFunctions.deserialize(
|
||||
(data['monitorsInGroup'] as JSONObject) || {}
|
||||
) as Dictionary<Array<ObjectID>>;
|
||||
|
||||
const monitorGroupCurrentStatuses: Dictionary<ObjectID> =
|
||||
JSONFunctions.deserialize(
|
||||
(data['monitorGroupCurrentStatuses'] as JSONObject) || {}
|
||||
) as Dictionary<ObjectID>;
|
||||
|
||||
setMonitorsInGroup(monitorsInGroup);
|
||||
setMonitorGroupCurrentStatuses(monitorGroupCurrentStatuses);
|
||||
|
||||
// save data. set()
|
||||
setScheduledMaintenanceEventsPublicNotes(
|
||||
scheduledMaintenanceEventsPublicNotes
|
||||
@@ -221,7 +250,11 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
|
||||
// Parse Data.
|
||||
setCurrentStatus(
|
||||
getOverallMonitorStatus(statusPageResources, monitorStatuses)
|
||||
getOverallMonitorStatus(
|
||||
statusPageResources,
|
||||
monitorStatuses,
|
||||
monitorGroupCurrentStatuses
|
||||
)
|
||||
);
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -250,12 +283,14 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
|
||||
const getOverallMonitorStatus: Function = (
|
||||
statusPageResources: Array<StatusPageResource>,
|
||||
monitorStatuses: Array<MonitorStatus>
|
||||
monitorStatuses: Array<MonitorStatus>,
|
||||
monitorGroupCurrentStatuses: Dictionary<ObjectID>
|
||||
): MonitorStatus | null => {
|
||||
let currentStatus: MonitorStatus | null =
|
||||
monitorStatuses.length > 0 && monitorStatuses[0]
|
||||
? monitorStatuses[0]
|
||||
: null;
|
||||
|
||||
const dict: Dictionary<number> = {};
|
||||
|
||||
for (const resource of statusPageResources) {
|
||||
@@ -275,6 +310,21 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
}
|
||||
}
|
||||
|
||||
// check status of monitor groups.
|
||||
|
||||
for (const groupId in monitorGroupCurrentStatuses) {
|
||||
const statusId: ObjectID | undefined =
|
||||
monitorGroupCurrentStatuses[groupId];
|
||||
|
||||
if (statusId) {
|
||||
if (!Object.keys(dict).includes(statusId.toString() || '')) {
|
||||
dict[statusId.toString()] = 1;
|
||||
} else {
|
||||
dict[statusId.toString()]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const monitorStatus of monitorStatuses) {
|
||||
if (monitorStatus._id && dict[monitorStatus._id]) {
|
||||
currentStatus = monitorStatus;
|
||||
@@ -307,44 +357,119 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
resource.statusPageGroupId.toString()) ||
|
||||
(!resource.statusPageGroupId && !group)
|
||||
) {
|
||||
let currentStatus: MonitorStatus | undefined =
|
||||
monitorStatuses.find((status: MonitorStatus) => {
|
||||
return (
|
||||
status._id?.toString() ===
|
||||
resource.monitor?.currentMonitorStatusId?.toString()
|
||||
);
|
||||
});
|
||||
// if its not a monitor or a monitor group, then continue. This should ideally not happen.
|
||||
|
||||
if (!currentStatus) {
|
||||
currentStatus = new MonitorStatus();
|
||||
currentStatus.name = 'Operational';
|
||||
currentStatus.color = Green;
|
||||
if (!resource.monitor && !resource.monitorGroupId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
elements.push(
|
||||
<MonitorOverview
|
||||
key={Math.random()}
|
||||
monitorName={
|
||||
resource.displayName || resource.monitor?.name || ''
|
||||
}
|
||||
description={resource.displayDescription || ''}
|
||||
tooltip={resource.displayTooltip || ''}
|
||||
monitorStatus={currentStatus}
|
||||
monitorStatusTimeline={[
|
||||
...monitorStatusTimelines,
|
||||
].filter((timeline: MonitorStatusTimeline) => {
|
||||
// if its a monitor
|
||||
|
||||
if (resource.monitor) {
|
||||
let currentStatus: MonitorStatus | undefined =
|
||||
monitorStatuses.find((status: MonitorStatus) => {
|
||||
return (
|
||||
timeline.monitorId?.toString() ===
|
||||
resource.monitorId?.toString()
|
||||
status._id?.toString() ===
|
||||
resource.monitor?.currentMonitorStatusId?.toString()
|
||||
);
|
||||
})}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
showHistoryChart={resource.showStatusHistoryChart}
|
||||
showCurrentStatus={resource.showCurrentStatus}
|
||||
uptimeGraphHeight={10}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
if (!currentStatus) {
|
||||
currentStatus = new MonitorStatus();
|
||||
currentStatus.name = 'Operational';
|
||||
currentStatus.color = Green;
|
||||
}
|
||||
|
||||
elements.push(
|
||||
<MonitorOverview
|
||||
key={Math.random()}
|
||||
monitorName={
|
||||
resource.displayName ||
|
||||
resource.monitor?.name ||
|
||||
''
|
||||
}
|
||||
description={resource.displayDescription || ''}
|
||||
tooltip={resource.displayTooltip || ''}
|
||||
monitorStatus={currentStatus}
|
||||
monitorStatusTimeline={[
|
||||
...monitorStatusTimelines,
|
||||
].filter((timeline: MonitorStatusTimeline) => {
|
||||
return (
|
||||
timeline.monitorId?.toString() ===
|
||||
resource.monitorId?.toString()
|
||||
);
|
||||
})}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
showHistoryChart={resource.showStatusHistoryChart}
|
||||
showCurrentStatus={resource.showCurrentStatus}
|
||||
uptimeGraphHeight={10}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// if its a monitor group, then...
|
||||
|
||||
if (resource.monitorGroupId) {
|
||||
let currentStatus: MonitorStatus | undefined =
|
||||
monitorStatuses.find((status: MonitorStatus) => {
|
||||
return (
|
||||
status._id?.toString() ===
|
||||
monitorGroupCurrentStatuses[
|
||||
resource.monitorGroupId?.toString() || ''
|
||||
]?.toString()
|
||||
);
|
||||
});
|
||||
|
||||
if (!currentStatus) {
|
||||
currentStatus = new MonitorStatus();
|
||||
currentStatus.name = 'Operational';
|
||||
currentStatus.color = Green;
|
||||
}
|
||||
|
||||
elements.push(
|
||||
<MonitorOverview
|
||||
key={Math.random()}
|
||||
monitorName={
|
||||
resource.displayName ||
|
||||
resource.monitor?.name ||
|
||||
''
|
||||
}
|
||||
description={resource.displayDescription || ''}
|
||||
tooltip={resource.displayTooltip || ''}
|
||||
monitorStatus={currentStatus}
|
||||
monitorStatusTimeline={[
|
||||
...monitorStatusTimelines,
|
||||
].filter((timeline: MonitorStatusTimeline) => {
|
||||
const monitorsInThisGroup:
|
||||
| Array<ObjectID>
|
||||
| undefined =
|
||||
monitorsInGroup[
|
||||
resource.monitorGroupId?.toString() ||
|
||||
''
|
||||
];
|
||||
|
||||
if (!monitorsInThisGroup) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return monitorsInThisGroup.find(
|
||||
(monitorId: ObjectID) => {
|
||||
return (
|
||||
monitorId.toString() ===
|
||||
timeline.monitorId?.toString()
|
||||
);
|
||||
}
|
||||
);
|
||||
})}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
showHistoryChart={resource.showStatusHistoryChart}
|
||||
showCurrentStatus={resource.showCurrentStatus}
|
||||
uptimeGraphHeight={10}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,20 +506,10 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
throw new BadDataException('Incident Timeline not found.');
|
||||
}
|
||||
|
||||
const monitorIds: Array<string | undefined> =
|
||||
activeIncident.monitors?.map((monitor: Monitor) => {
|
||||
return monitor._id;
|
||||
}) || [];
|
||||
|
||||
const namesOfResources: Array<StatusPageResource> =
|
||||
statusPageResources.filter((resource: StatusPageResource) => {
|
||||
return monitorIds.includes(resource.monitorId?.toString());
|
||||
});
|
||||
|
||||
const group: IncidentGroup = {
|
||||
incident: activeIncident,
|
||||
incidentState: activeIncident.currentIncidentState,
|
||||
incidentResources: namesOfResources,
|
||||
incidentResources: statusPageResources,
|
||||
publicNotes: incidentPublicNotes.filter(
|
||||
(publicNote: IncidentPublicNote) => {
|
||||
return (
|
||||
@@ -405,6 +520,7 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
),
|
||||
incidentSeverity: activeIncident.incidentSeverity!,
|
||||
incidentStateTimelines: [timeline],
|
||||
monitorsInGroup: monitorsInGroup,
|
||||
};
|
||||
|
||||
groups.push(group);
|
||||
@@ -438,25 +554,11 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
throw new BadDataException('Incident Timeline not found.');
|
||||
}
|
||||
|
||||
const monitorIds: Array<string | undefined> =
|
||||
activeEvent.monitors?.map((monitor: Monitor) => {
|
||||
return monitor._id;
|
||||
}) || [];
|
||||
|
||||
const namesOfResources: Array<StatusPageResource> =
|
||||
statusPageResources.filter(
|
||||
(resource: StatusPageResource) => {
|
||||
return monitorIds.includes(
|
||||
resource.monitorId?.toString()
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const group: ScheduledMaintenanceGroup = {
|
||||
scheduledMaintenance: activeEvent,
|
||||
scheduledMaintenanceState:
|
||||
activeEvent.currentScheduledMaintenanceState,
|
||||
scheduledEventResources: namesOfResources,
|
||||
scheduledEventResources: statusPageResources,
|
||||
publicNotes: scheduledMaintenanceEventsPublicNotes.filter(
|
||||
(publicNote: ScheduledMaintenancePublicNote) => {
|
||||
return (
|
||||
@@ -466,6 +568,7 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
}
|
||||
),
|
||||
scheduledMaintenanceStateTimelines: [timeline],
|
||||
monitorsInGroup: monitorsInGroup,
|
||||
};
|
||||
|
||||
groups.push(group);
|
||||
@@ -585,6 +688,7 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
incidentGroup.publicNotes,
|
||||
incidentGroup.incidentStateTimelines,
|
||||
incidentGroup.incidentResources,
|
||||
incidentGroup.monitorsInGroup,
|
||||
StatusPageUtil.isPreviewPage(),
|
||||
true
|
||||
)}
|
||||
@@ -610,6 +714,7 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
scheduledEventGroup.publicNotes,
|
||||
scheduledEventGroup.scheduledMaintenanceStateTimelines,
|
||||
scheduledEventGroup.scheduledEventResources,
|
||||
scheduledEventGroup.monitorsInGroup,
|
||||
StatusPageUtil.isPreviewPage(),
|
||||
true
|
||||
)}
|
||||
@@ -715,7 +820,7 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
id="overview-empty-state"
|
||||
icon={IconProp.CheckCircle}
|
||||
title={'Everything looks great'}
|
||||
description="Everything is great. Nothing posted on this status page so far."
|
||||
description="No resources added to this status page yet. Please add some resources from the dashboard."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -37,11 +37,16 @@ import API from '../../Utils/API';
|
||||
import StatusPageUtil from '../../Utils/StatusPage';
|
||||
import HTTPErrorResponse from 'Common/Types/API/HTTPErrorResponse';
|
||||
import { STATUS_PAGE_API_URL } from '../../Utils/Config';
|
||||
import StatusPageResource from 'Model/Models/StatusPageResource';
|
||||
import Dictionary from 'Common/Types/Dictionary';
|
||||
import Monitor from 'Model/Models/Monitor';
|
||||
|
||||
export const getScheduledEventEventItem: Function = (
|
||||
scheduledMaintenance: ScheduledMaintenance,
|
||||
scheduledMaintenanceEventsPublicNotes: Array<ScheduledMaintenancePublicNote>,
|
||||
scheduledMaintenanceStateTimelines: Array<ScheduledMaintenanceStateTimeline>,
|
||||
statusPageResources: Array<StatusPageResource>,
|
||||
monitorsInGroup: Dictionary<Array<ObjectID>>,
|
||||
isPreviewPage: boolean,
|
||||
isSummary: boolean
|
||||
): EventItemComponentProps => {
|
||||
@@ -146,11 +151,69 @@ export const getScheduledEventEventItem: Function = (
|
||||
return OneUptimeDate.isAfter(a.date, b.date) === true ? 1 : -1;
|
||||
});
|
||||
|
||||
let namesOfResources: Array<StatusPageResource> = [];
|
||||
|
||||
if (scheduledMaintenance.monitors) {
|
||||
const monitorIdsInThisScheduledMaintenance: Array<string> =
|
||||
scheduledMaintenance.monitors
|
||||
.map((monitor: Monitor) => {
|
||||
return monitor.id!.toString();
|
||||
})
|
||||
.filter((id: string) => {
|
||||
return Boolean(id);
|
||||
});
|
||||
|
||||
namesOfResources = statusPageResources.filter(
|
||||
(resource: StatusPageResource) => {
|
||||
return (
|
||||
resource.monitorId &&
|
||||
monitorIdsInThisScheduledMaintenance.includes(
|
||||
resource.monitorId.toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// add names of the groups as well.
|
||||
namesOfResources = namesOfResources.concat(
|
||||
statusPageResources.filter((resource: StatusPageResource) => {
|
||||
if (!resource.monitorGroupId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const monitorGroupId: string =
|
||||
resource.monitorGroupId.toString();
|
||||
|
||||
const monitorIdsInThisGroup: Array<ObjectID> =
|
||||
monitorsInGroup[monitorGroupId]!;
|
||||
|
||||
for (const monitorId of monitorIdsInThisGroup) {
|
||||
if (
|
||||
monitorIdsInThisScheduledMaintenance.find(
|
||||
(id: string | undefined) => {
|
||||
return id?.toString() === monitorId.toString();
|
||||
}
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
eventTitle: scheduledMaintenance.title || '',
|
||||
eventDescription: scheduledMaintenance.description,
|
||||
eventTimeline: timeline,
|
||||
eventType: 'Scheduled Maintenance',
|
||||
eventResourcesAffected: namesOfResources.map(
|
||||
(i: StatusPageResource) => {
|
||||
return i.displayName || '';
|
||||
}
|
||||
),
|
||||
eventViewRoute: !isSummary
|
||||
? undefined
|
||||
: RouteUtil.populateRouteParams(
|
||||
@@ -192,6 +255,14 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
const [parsedData, setParsedData] =
|
||||
useState<EventItemComponentProps | null>(null);
|
||||
|
||||
const [monitorsInGroup, setMonitorsInGroup] = useState<
|
||||
Dictionary<Array<ObjectID>>
|
||||
>({});
|
||||
|
||||
const [statusPageResources, setStatusPageResources] = useState<
|
||||
Array<StatusPageResource>
|
||||
>([]);
|
||||
|
||||
StatusPageUtil.checkIfUserHasLoggedIn();
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
@@ -219,6 +290,10 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
{},
|
||||
API.getDefaultHeaders(StatusPageUtil.getStatusPageId()!)
|
||||
);
|
||||
|
||||
if (!response.isSuccess()) {
|
||||
throw response;
|
||||
}
|
||||
const data: JSONObject = response.data;
|
||||
|
||||
const scheduledMaintenanceEventsPublicNotes: Array<ScheduledMaintenancePublicNote> =
|
||||
@@ -244,15 +319,30 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
ScheduledMaintenanceStateTimeline
|
||||
);
|
||||
|
||||
const statusPageResources: Array<StatusPageResource> =
|
||||
JSONFunctions.fromJSONArray(
|
||||
(data['statusPageResources'] as JSONArray) || [],
|
||||
StatusPageResource
|
||||
);
|
||||
|
||||
const monitorsInGroup: Dictionary<Array<ObjectID>> =
|
||||
JSONFunctions.deserialize(
|
||||
(data['monitorsInGroup'] as JSONObject) || {}
|
||||
) as Dictionary<Array<ObjectID>>;
|
||||
|
||||
// save data. set()
|
||||
setscheduledMaintenanceEventsPublicNotes(
|
||||
scheduledMaintenanceEventsPublicNotes
|
||||
);
|
||||
setscheduledMaintenanceEvent(scheduledMaintenanceEvent);
|
||||
setStatusPageResources(statusPageResources);
|
||||
|
||||
setscheduledMaintenanceStateTimelines(
|
||||
scheduledMaintenanceStateTimelines
|
||||
);
|
||||
|
||||
setMonitorsInGroup(monitorsInGroup);
|
||||
|
||||
setIsLoading(false);
|
||||
props.onLoadComplete();
|
||||
} catch (err) {
|
||||
@@ -279,6 +369,8 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
scheduledMaintenanceEvent,
|
||||
scheduledMaintenanceEventsPublicNotes,
|
||||
scheduledMaintenanceStateTimelines,
|
||||
statusPageResources,
|
||||
monitorsInGroup,
|
||||
Boolean(StatusPageUtil.isPreviewPage())
|
||||
)
|
||||
);
|
||||
|
||||
@@ -35,6 +35,7 @@ import API from '../../Utils/API';
|
||||
import StatusPageUtil from '../../Utils/StatusPage';
|
||||
import HTTPErrorResponse from 'Common/Types/API/HTTPErrorResponse';
|
||||
import { STATUS_PAGE_API_URL } from '../../Utils/Config';
|
||||
import StatusPageResource from 'Model/Models/StatusPageResource';
|
||||
|
||||
const Overview: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps
|
||||
@@ -54,6 +55,14 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
const [parsedData, setParsedData] =
|
||||
useState<EventHistoryListComponentProps | null>(null);
|
||||
|
||||
const [statusPageResources, setStatusPageResources] = useState<
|
||||
Array<StatusPageResource>
|
||||
>([]);
|
||||
|
||||
const [monitorsInGroup, setMonitorsInGroup] = useState<
|
||||
Dictionary<Array<ObjectID>>
|
||||
>({});
|
||||
|
||||
StatusPageUtil.checkIfUserHasLoggedIn();
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
@@ -77,6 +86,10 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
{},
|
||||
API.getDefaultHeaders(StatusPageUtil.getStatusPageId()!)
|
||||
);
|
||||
|
||||
if (!response.isSuccess()) {
|
||||
throw response;
|
||||
}
|
||||
const data: JSONObject = response.data;
|
||||
|
||||
const scheduledMaintenanceEventsPublicNotes: Array<ScheduledMaintenancePublicNote> =
|
||||
@@ -98,6 +111,20 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
ScheduledMaintenanceStateTimeline
|
||||
);
|
||||
|
||||
const statusPageResources: Array<StatusPageResource> =
|
||||
JSONFunctions.fromJSONArray(
|
||||
(data['statusPageResources'] as JSONArray) || [],
|
||||
StatusPageResource
|
||||
);
|
||||
|
||||
const monitorsInGroup: Dictionary<Array<ObjectID>> =
|
||||
JSONFunctions.deserialize(
|
||||
(data['monitorsInGroup'] as JSONObject) || {}
|
||||
) as Dictionary<Array<ObjectID>>;
|
||||
|
||||
setStatusPageResources(statusPageResources);
|
||||
setMonitorsInGroup(monitorsInGroup);
|
||||
|
||||
// save data. set()
|
||||
setscheduledMaintenanceEventsPublicNotes(
|
||||
scheduledMaintenanceEventsPublicNotes
|
||||
@@ -148,6 +175,8 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
scheduledMaintenance,
|
||||
scheduledMaintenanceEventsPublicNotes,
|
||||
scheduledMaintenanceStateTimelines,
|
||||
statusPageResources,
|
||||
monitorsInGroup,
|
||||
Boolean(StatusPageUtil.isPreviewPage()),
|
||||
true
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import Dictionary from 'Common/Types/Dictionary';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import Incident from 'Model/Models/Incident';
|
||||
import IncidentPublicNote from 'Model/Models/IncidentPublicNote';
|
||||
import IncidentSeverity from 'Model/Models/IncidentSeverity';
|
||||
@@ -12,4 +14,5 @@ export default interface IncidentGroup {
|
||||
incidentState: IncidentState;
|
||||
incidentStateTimelines: Array<IncidentStateTimeline>;
|
||||
incidentResources: Array<StatusPageResource>;
|
||||
monitorsInGroup: Dictionary<Array<ObjectID>>;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import Dictionary from 'Common/Types/Dictionary';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import ScheduledMaintenance from 'Model/Models/ScheduledMaintenance';
|
||||
import ScheduledMaintenancePublicNote from 'Model/Models/ScheduledMaintenancePublicNote';
|
||||
import ScheduledMaintenanceState from 'Model/Models/ScheduledMaintenanceState';
|
||||
@@ -10,4 +12,5 @@ export default interface ScheduledMaintenanceGroup {
|
||||
scheduledMaintenanceState: ScheduledMaintenanceState;
|
||||
scheduledMaintenanceStateTimelines: Array<ScheduledMaintenanceStateTimeline>;
|
||||
scheduledEventResources: Array<StatusPageResource>;
|
||||
monitorsInGroup: Dictionary<Array<ObjectID>>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user