Compare commits

...

42 Commits

Author SHA1 Message Date
Simon Larsen
6c1bd10873 fix current status on sp 2023-11-02 13:39:59 +00:00
Simon Larsen
05a288c761 fix jest types 2023-11-02 11:11:36 +00:00
Simon Larsen
a9f503da9d fix probe tests 2023-11-02 10:21:31 +00:00
Simon Larsen
49d3655502 fix fmt 2023-11-01 20:49:00 +00:00
Simon Larsen
1cdcc639b4 fix fmt 2023-11-01 20:45:23 +00:00
Simon Larsen
7568c70b50 fix fmt 2023-11-01 20:15:18 +00:00
Simon Larsen
6259f81a91 add ui changes 2023-11-01 19:38:58 +00:00
Simon Larsen
f40c1daeb8 fix fmt 2023-11-01 14:15:43 +00:00
Simon Larsen
bb73ed14cd add animate in status bubble 2023-11-01 10:44:01 +00:00
Simon Larsen
4b71a81f7c fix fmt on uptime 2023-11-01 10:40:03 +00:00
Simon Larsen
d6788c138b fix uptime graph 2023-11-01 10:34:38 +00:00
Simon Larsen
28f4a1f473 add status api 2023-10-31 17:05:56 +00:00
Simon Larsen
ccb4781c06 enable compression 2023-10-31 14:10:57 +00:00
Simon Larsen
2e27347225 fix fmt 2023-10-31 12:19:47 +00:00
Simon Larsen
e9015f0eff Merge branch 'master' of github.com-simon:OneUptime/oneuptime 2023-10-31 12:06:27 +00:00
Simon Larsen
6cf8560151 fix eslint 2023-10-31 12:06:24 +00:00
Simon Larsen
7d2e91d867 Merge pull request #854 from hasannadeem/tests/notification-middleware-and-cookie-utils
Tests for notification middleware and cookie utils
2023-10-31 11:57:11 +00:00
Simon Larsen
46e0210dcc Merge pull request #869 from fakharj/eslint-object-curly-spacing
add eslint object-curly-spacing rule
2023-10-31 09:58:24 +00:00
Simon Larsen
02fc5502eb Merge branch 'master' into eslint-object-curly-spacing 2023-10-31 09:58:17 +00:00
Simon Larsen
ce3131edaf Merge pull request #865 from fakharj/eslint-unneeded-ternary
added no unneeded ternary in eslint
2023-10-30 13:56:03 +00:00
fakharj
ca4716133a add eslint object-curly-spacing rule 2023-10-30 18:43:43 +05:00
Simon Larsen
9cb254f9d1 Merge pull request #862 from OneUptime/dependabot/npm_and_yarn/Common/crypto-js-4.2.0
Bump crypto-js from 4.1.1 to 4.2.0 in /Common
2023-10-30 11:13:47 +00:00
Simon Larsen
d51fbdf5f7 Merge pull request #868 from cheese-framework/master
Add test suites for JSONFunctions and SerializableObject
2023-10-30 11:10:51 +00:00
Simon Larsen
57b7b5b39e Merge pull request #855 from hammadfauz/duplicateModalTest
Duplicate modal test
2023-10-30 08:24:18 +00:00
Drantaz
2e46ebd0e8 Merge branch 'master' of https://github.com/cheese-framework/oneuptime 2023-10-27 20:25:23 +00:00
Drantaz
4ffe215665 Add test suites for JSONFunctions and SerializableObject 2023-10-27 20:24:33 +00:00
Hammad
e680346f1f fixes lint 2023-10-27 23:48:27 +05:00
Hammad
4faa8d32f6 adds test ids to key elements 2023-10-27 23:27:33 +05:00
Simon Larsen
ab07ff0104 fix fmt 2023-10-27 17:22:20 +01:00
Simon Larsen
03dd6fef04 Merge branch 'master' of github.com-simon:OneUptime/oneuptime 2023-10-27 17:15:29 +01:00
Simon Larsen
31c0ff7dea Merge branch 'feature-flags' 2023-10-27 17:15:18 +01:00
Simon Larsen
fc218a970a Merge pull request #861 from OneUptime/feature-flags
add feature flag page
2023-10-27 16:40:30 +01:00
fakharj
a0acb24651 added no unneeded ternary in eslint 2023-10-27 13:03:07 +05:00
Simon Larsen
c958893d67 increase timeout to 30 secs 2023-10-26 20:51:00 +01:00
dependabot[bot]
254a9de101 Bump crypto-js from 4.1.1 to 4.2.0 in /Common
Bumps [crypto-js](https://github.com/brix/crypto-js) from 4.1.1 to 4.2.0.
- [Commits](https://github.com/brix/crypto-js/compare/4.1.1...4.2.0)

---
updated-dependencies:
- dependency-name: crypto-js
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-25 22:47:32 +00:00
Hammad
5ec8ee6dcb removes jest config for ignoring snapshots 2023-10-23 19:17:19 +05:00
Hammad
a1c6121bee tests UI by querying key elements and removes snapshots 2023-10-23 19:15:10 +05:00
Hammad
51c76aa1af moves test suite into Components as per convention 2023-10-23 19:14:04 +05:00
Hammad
e702a0b0d2 Fixes lint errors 2023-10-21 15:49:54 +05:00
Hammad
cfc2f99248 adds tests for DuplicateModel 2023-10-21 15:07:20 +05:00
Hammad
f23bb3af41 fixes jest mistaking snapshots for test suites 2023-10-21 15:06:46 +05:00
hasan
9205764deb Add tests for notification middleware and cookie utils 2023-10-20 18:44:33 +05:00
44 changed files with 2129 additions and 212 deletions

View File

@@ -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"
}
}
}
}

View 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);
});
});
});

View 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);
});
});
});

View File

@@ -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;

View File

@@ -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);

View File

@@ -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',

View File

@@ -123,6 +123,7 @@ export type JSONValue =
| Array<JSONValue>
| Array<Permission>
| Array<JSONValue>
| Array<ObjectID>
| CallRequest
| undefined
| null;

View File

@@ -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',

View File

@@ -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",

View File

@@ -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);
}
}
);
}
}

View File

@@ -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;

View File

@@ -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

View 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);
});
});
});

View 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]
);
});
});

View File

@@ -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,

View File

@@ -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"
}
}
}

View File

@@ -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>
);

View File

@@ -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

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>
)}

View File

@@ -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"
/>
) : (
<></>

View File

@@ -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]);

View File

@@ -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()

View 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();
});
});

View File

@@ -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>
);
}

View File

@@ -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}

View File

@@ -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;

View File

@@ -74,7 +74,7 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
RouteMap[PageMap.MONITOR_GROUPS] as Route
),
}}
icon={IconProp.Folder}
icon={IconProp.Squares}
/>
</SideMenuSection>
) : (

View File

@@ -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>
);
};

View File

@@ -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 <></>;
},
},
{

View File

@@ -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

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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,
});
}

View File

@@ -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 =

View File

@@ -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> =

View File

@@ -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()
)
);

View File

@@ -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
)

View File

@@ -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>

View File

@@ -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())
)
);

View File

@@ -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
)

View File

@@ -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>>;
}

View File

@@ -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>>;
}