mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 08:42:13 +02:00
Compare commits
30 Commits
monitor-gr
...
7.0.973
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a299fca2ba | ||
|
|
396066bf0d | ||
|
|
9debdfafba | ||
|
|
49c4dff44b | ||
|
|
27a8434181 | ||
|
|
c8305ef7c0 | ||
|
|
2f332a64f3 | ||
|
|
fb5291e7c1 | ||
|
|
4034cf6ed8 | ||
|
|
8b37587800 | ||
|
|
03eb1dd1f2 | ||
|
|
ee91526239 | ||
|
|
970c537b96 | ||
|
|
35718ee7b5 | ||
|
|
310cd90714 | ||
|
|
513fa74c59 | ||
|
|
4ba6f714af | ||
|
|
7ab3ba7201 | ||
|
|
e5aa9c9496 | ||
|
|
455ca7b22d | ||
|
|
0b71c8b769 | ||
|
|
19b0a1f2a8 | ||
|
|
4f93dd0f04 | ||
|
|
60d0f188ad | ||
|
|
bd587b210e | ||
|
|
80dd33cd7f | ||
|
|
d675eca50c | ||
|
|
76712e8f89 | ||
|
|
fe68b009eb | ||
|
|
9a6960e154 |
@@ -8,4 +8,25 @@ enum SubscriptionStatus {
|
||||
Unpaid = 'unpaid',
|
||||
}
|
||||
|
||||
export class SubscriptionStatusUtil {
|
||||
public static isSubscriptionActive(
|
||||
status?: SubscriptionStatus | undefined
|
||||
): boolean {
|
||||
if (!status) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
status === SubscriptionStatus.Active ||
|
||||
status === SubscriptionStatus.Trialing
|
||||
);
|
||||
}
|
||||
|
||||
public static isSubscriptionInactive(
|
||||
status?: SubscriptionStatus | undefined
|
||||
): boolean {
|
||||
return !SubscriptionStatusUtil.isSubscriptionActive(status);
|
||||
}
|
||||
}
|
||||
|
||||
export default SubscriptionStatus;
|
||||
|
||||
@@ -55,6 +55,9 @@ export default class ProjectAPI extends BaseAPI<Project, ProjectServiceType> {
|
||||
paymentProviderPlanId: true,
|
||||
resellerId: true,
|
||||
isFeatureFlagMonitorGroupsEnabled: true,
|
||||
paymentProviderMeteredSubscriptionStatus:
|
||||
true,
|
||||
paymentProviderSubscriptionStatus: true,
|
||||
},
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
|
||||
@@ -65,6 +65,10 @@ import Dictionary from 'Common/Types/Dictionary';
|
||||
import MonitorGroupService from '../Services/MonitorGroupService';
|
||||
import MonitorGroupResource from 'Model/Models/MonitorGroupResource';
|
||||
import MonitorGroupResourceService from '../Services/MonitorGroupResourceService';
|
||||
import IncidentState from 'Model/Models/IncidentState';
|
||||
import IncidentStateService from '../Services/IncidentStateService';
|
||||
import ScheduledMaintenanceState from 'Model/Models/ScheduledMaintenanceState';
|
||||
import ScheduledMaintenanceStateService from '../Services/ScheduledMaintenanceStateService';
|
||||
|
||||
export default class StatusPageAPI extends BaseAPI<
|
||||
StatusPage,
|
||||
@@ -426,6 +430,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
isPublicStatusPage: true,
|
||||
overviewPageDescription: true,
|
||||
showIncidentLabelsOnStatusPage: true,
|
||||
showScheduledEventLabelsOnStatusPage: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
@@ -502,6 +507,8 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
currentMonitorStatusId: true,
|
||||
},
|
||||
monitorGroupId: true,
|
||||
showUptimePercent: true,
|
||||
uptimePercentPrecision: true,
|
||||
},
|
||||
sort: {
|
||||
order: SortOrder.Ascending,
|
||||
@@ -594,7 +601,8 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
return (
|
||||
resource.monitorGroupId?.toString() ===
|
||||
monitorGroupId.toString() &&
|
||||
resource.showStatusHistoryChart
|
||||
(resource.showStatusHistoryChart ||
|
||||
resource.showUptimePercent)
|
||||
);
|
||||
}
|
||||
)
|
||||
@@ -684,8 +692,10 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
color: true,
|
||||
},
|
||||
currentIncidentState: {
|
||||
_id: true,
|
||||
name: true,
|
||||
color: true,
|
||||
order: true,
|
||||
},
|
||||
monitors: {
|
||||
_id: true,
|
||||
@@ -822,6 +832,35 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
|
||||
// check if status page has active scheduled events.
|
||||
|
||||
let scheduledEventsSelect: Select<ScheduledMaintenance> = {
|
||||
createdAt: true,
|
||||
title: true,
|
||||
description: true,
|
||||
_id: true,
|
||||
endsAt: true,
|
||||
startsAt: true,
|
||||
currentScheduledMaintenanceState: {
|
||||
name: true,
|
||||
color: true,
|
||||
isScheduledState: true,
|
||||
isResolvedState: true,
|
||||
isOngoingState: true,
|
||||
},
|
||||
monitors: {
|
||||
_id: true,
|
||||
},
|
||||
};
|
||||
|
||||
if (statusPage.showScheduledEventLabelsOnStatusPage) {
|
||||
scheduledEventsSelect = {
|
||||
...scheduledEventsSelect,
|
||||
labels: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const scheduledMaintenanceEvents: Array<ScheduledMaintenance> =
|
||||
await ScheduledMaintenanceService.findBy({
|
||||
query: {
|
||||
@@ -831,24 +870,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
statusPages: objectId as any,
|
||||
projectId: statusPage.projectId!,
|
||||
},
|
||||
select: {
|
||||
createdAt: true,
|
||||
title: true,
|
||||
description: true,
|
||||
_id: true,
|
||||
endsAt: true,
|
||||
startsAt: true,
|
||||
currentScheduledMaintenanceState: {
|
||||
name: true,
|
||||
color: true,
|
||||
isScheduledState: true,
|
||||
isResolvedState: true,
|
||||
isOngoingState: true,
|
||||
},
|
||||
monitors: {
|
||||
_id: true,
|
||||
},
|
||||
},
|
||||
select: scheduledEventsSelect,
|
||||
sort: {
|
||||
createdAt: SortOrder.Ascending,
|
||||
},
|
||||
@@ -869,24 +891,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
statusPages: objectId as any,
|
||||
projectId: statusPage.projectId!,
|
||||
},
|
||||
select: {
|
||||
createdAt: true,
|
||||
title: true,
|
||||
description: true,
|
||||
_id: true,
|
||||
endsAt: true,
|
||||
startsAt: true,
|
||||
currentScheduledMaintenanceState: {
|
||||
name: true,
|
||||
color: true,
|
||||
isScheduledState: true,
|
||||
isResolvedState: true,
|
||||
isOngoingState: true,
|
||||
},
|
||||
monitors: {
|
||||
_id: true,
|
||||
},
|
||||
},
|
||||
select: scheduledEventsSelect,
|
||||
sort: {
|
||||
createdAt: SortOrder.Ascending,
|
||||
},
|
||||
@@ -1330,6 +1335,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
_id: true,
|
||||
projectId: true,
|
||||
showScheduledEventHistoryInDays: true,
|
||||
showScheduledEventLabelsOnStatusPage: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
@@ -1387,27 +1393,40 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
};
|
||||
}
|
||||
|
||||
let scheduledEventsSelect: Select<ScheduledMaintenance> = {
|
||||
createdAt: true,
|
||||
title: true,
|
||||
description: true,
|
||||
_id: true,
|
||||
endsAt: true,
|
||||
startsAt: true,
|
||||
currentScheduledMaintenanceState: {
|
||||
name: true,
|
||||
color: true,
|
||||
isScheduledState: true,
|
||||
isResolvedState: true,
|
||||
isOngoingState: true,
|
||||
order: true,
|
||||
},
|
||||
monitors: {
|
||||
_id: true,
|
||||
},
|
||||
};
|
||||
|
||||
if (statusPage.showScheduledEventLabelsOnStatusPage) {
|
||||
scheduledEventsSelect = {
|
||||
...scheduledEventsSelect,
|
||||
labels: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const scheduledMaintenanceEvents: Array<ScheduledMaintenance> =
|
||||
await ScheduledMaintenanceService.findBy({
|
||||
query: query,
|
||||
select: {
|
||||
createdAt: true,
|
||||
title: true,
|
||||
description: true,
|
||||
_id: true,
|
||||
endsAt: true,
|
||||
startsAt: true,
|
||||
currentScheduledMaintenanceState: {
|
||||
name: true,
|
||||
color: true,
|
||||
isScheduledState: true,
|
||||
isResolvedState: true,
|
||||
isOngoingState: true,
|
||||
},
|
||||
monitors: {
|
||||
_id: true,
|
||||
},
|
||||
},
|
||||
select: scheduledEventsSelect,
|
||||
sort: {
|
||||
startsAt: SortOrder.Descending,
|
||||
},
|
||||
@@ -1432,24 +1451,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
statusPages: [statusPageId] as any,
|
||||
projectId: statusPage.projectId!,
|
||||
},
|
||||
select: {
|
||||
createdAt: true,
|
||||
title: true,
|
||||
description: true,
|
||||
_id: true,
|
||||
endsAt: true,
|
||||
startsAt: true,
|
||||
currentScheduledMaintenanceState: {
|
||||
name: true,
|
||||
color: true,
|
||||
isScheduledState: true,
|
||||
isResolvedState: true,
|
||||
isOngoingState: true,
|
||||
},
|
||||
monitors: {
|
||||
_id: true,
|
||||
},
|
||||
},
|
||||
select: scheduledEventsSelect,
|
||||
sort: {
|
||||
createdAt: SortOrder.Ascending,
|
||||
},
|
||||
@@ -1595,11 +1597,35 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
monitorsInGroup[monitorGroupId.toString()] = monitorsInGroupIds;
|
||||
}
|
||||
|
||||
// get scheduled event states.
|
||||
const scheduledEventStates: Array<ScheduledMaintenanceState> =
|
||||
await ScheduledMaintenanceStateService.findBy({
|
||||
query: {
|
||||
projectId: statusPage.projectId!,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
order: true,
|
||||
isEndedState: true,
|
||||
isOngoingState: true,
|
||||
isScheduledState: true,
|
||||
},
|
||||
});
|
||||
|
||||
const response: JSONObject = {
|
||||
scheduledMaintenanceEventsPublicNotes: JSONFunctions.toJSONArray(
|
||||
scheduledMaintenanceEventsPublicNotes,
|
||||
ScheduledMaintenancePublicNote
|
||||
),
|
||||
scheduledMaintenanceStates: JSONFunctions.toJSONArray(
|
||||
scheduledEventStates,
|
||||
ScheduledMaintenanceState
|
||||
),
|
||||
scheduledMaintenanceEvents: JSONFunctions.toJSONArray(
|
||||
scheduledMaintenanceEvents,
|
||||
ScheduledMaintenance
|
||||
@@ -1881,6 +1907,8 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
currentIncidentState: {
|
||||
name: true,
|
||||
color: true,
|
||||
_id: true,
|
||||
order: true,
|
||||
},
|
||||
monitors: {
|
||||
_id: true,
|
||||
@@ -2002,11 +2030,32 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
});
|
||||
}
|
||||
|
||||
// get all the incident states for this project.
|
||||
const incidentStates: Array<IncidentState> =
|
||||
await IncidentStateService.findBy({
|
||||
query: {
|
||||
projectId: statusPage.projectId!,
|
||||
},
|
||||
select: {
|
||||
isResolvedState: true,
|
||||
order: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const response: JSONObject = {
|
||||
incidentPublicNotes: JSONFunctions.toJSONArray(
|
||||
incidentPublicNotes,
|
||||
IncidentPublicNote
|
||||
),
|
||||
incidentStates: JSONFunctions.toJSONArray(
|
||||
incidentStates,
|
||||
IncidentState
|
||||
),
|
||||
incidents: JSONFunctions.toJSONArray(incidents, Incident),
|
||||
statusPageResources: JSONFunctions.toJSONArray(
|
||||
statusPageResources,
|
||||
|
||||
@@ -9,7 +9,9 @@ import logger from '../Utils/Logger';
|
||||
import Stripe from 'stripe';
|
||||
import { BillingPrivateKey, IsBillingEnabled } from '../EnvironmentConfig';
|
||||
import ServerMeteredPlan from '../Types/Billing/MeteredPlan/ServerMeteredPlan';
|
||||
import SubscriptionStatus from 'Common/Types/Billing/SubscriptionStatus';
|
||||
import SubscriptionStatus, {
|
||||
SubscriptionStatusUtil,
|
||||
} from 'Common/Types/Billing/SubscriptionStatus';
|
||||
import BaseService from './BaseService';
|
||||
import Email from 'Common/Types/Email';
|
||||
import Dictionary from 'Common/Types/Dictionary';
|
||||
@@ -96,14 +98,7 @@ export class BillingService extends BaseService {
|
||||
}
|
||||
|
||||
public isSubscriptionActive(status: SubscriptionStatus): boolean {
|
||||
if (!status) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
status === SubscriptionStatus.Active ||
|
||||
status === SubscriptionStatus.Trialing
|
||||
);
|
||||
return SubscriptionStatusUtil.isSubscriptionActive(status);
|
||||
}
|
||||
|
||||
public async subscribeToMeteredPlan(data: {
|
||||
@@ -451,7 +446,12 @@ export class BillingService extends BaseService {
|
||||
meteredSubscriptionId: string;
|
||||
trialEndsAt?: Date | undefined;
|
||||
}> {
|
||||
logger.info('Changing plan');
|
||||
logger.info(data);
|
||||
|
||||
if (!this.isBillingEnabled()) {
|
||||
logger.info('Billing not enabled');
|
||||
|
||||
throw new BadDataException(
|
||||
'Billing is not enabled for this server.'
|
||||
);
|
||||
@@ -460,26 +460,45 @@ export class BillingService extends BaseService {
|
||||
const subscription: Stripe.Response<Stripe.Subscription> =
|
||||
await this.stripe.subscriptions.retrieve(data.subscriptionId);
|
||||
|
||||
logger.info('Subscription');
|
||||
logger.info(subscription);
|
||||
|
||||
if (!subscription) {
|
||||
logger.info('Subscription not found');
|
||||
throw new BadDataException('Subscription not found');
|
||||
}
|
||||
|
||||
logger.info('Subscription status');
|
||||
logger.info(subscription.status);
|
||||
|
||||
const paymentMethods: Array<PaymentMethod> =
|
||||
await this.getPaymentMethods(subscription.customer.toString());
|
||||
|
||||
logger.info('Payment methods');
|
||||
logger.info(paymentMethods);
|
||||
|
||||
if (paymentMethods.length === 0) {
|
||||
logger.info('No payment methods');
|
||||
|
||||
throw new BadDataException(
|
||||
'No payment methods added. Please add your card to this project to change your plan'
|
||||
);
|
||||
}
|
||||
|
||||
logger.info('Cancelling subscriptions');
|
||||
logger.info(data.subscriptionId);
|
||||
await this.cancelSubscription(data.subscriptionId);
|
||||
|
||||
logger.info('Cancelling metered subscriptions');
|
||||
logger.info(data.meteredSubscriptionId);
|
||||
await this.cancelSubscription(data.meteredSubscriptionId);
|
||||
|
||||
if (data.endTrialAt && !OneUptimeDate.isInTheFuture(data.endTrialAt)) {
|
||||
data.endTrialAt = undefined;
|
||||
}
|
||||
|
||||
logger.info('Subscribing to plan');
|
||||
|
||||
const subscribeToPlan: {
|
||||
subscriptionId: string;
|
||||
meteredSubscriptionId: string;
|
||||
@@ -496,11 +515,19 @@ export class BillingService extends BaseService {
|
||||
promoCode: undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
logger.info('Subscribed to plan');
|
||||
|
||||
const value: {
|
||||
subscriptionId: string;
|
||||
meteredSubscriptionId: string;
|
||||
trialEndsAt?: Date | undefined;
|
||||
} = {
|
||||
subscriptionId: subscribeToPlan.subscriptionId,
|
||||
meteredSubscriptionId: subscribeToPlan.meteredSubscriptionId,
|
||||
trialEndsAt: subscribeToPlan.trialEndsAt || undefined,
|
||||
};
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public async deletePaymentMethod(
|
||||
|
||||
@@ -53,6 +53,10 @@ export class Service extends DatabaseService<MonitorGroup> {
|
||||
},
|
||||
});
|
||||
|
||||
if (monitorGroupResources.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const monitorStatusTimelines: Array<MonitorStatusTimeline> =
|
||||
await MonitorStatusTimelineService.findBy({
|
||||
query: {
|
||||
@@ -71,6 +75,7 @@ export class Service extends DatabaseService<MonitorGroup> {
|
||||
monitorStatus: {
|
||||
name: true,
|
||||
color: true,
|
||||
isOperationalState: true,
|
||||
priority: true,
|
||||
} as any,
|
||||
},
|
||||
|
||||
@@ -296,6 +296,8 @@ export class Service extends DatabaseService<Model> {
|
||||
project.paymentProviderPlanId !==
|
||||
updateBy.data.paymentProviderPlanId
|
||||
) {
|
||||
logger.info('Changing plan for project ' + project.id);
|
||||
|
||||
const plan: SubscriptionPlan | undefined =
|
||||
SubscriptionPlan.getSubscriptionPlanById(
|
||||
updateBy.data.paymentProviderPlanId! as string,
|
||||
@@ -306,6 +308,13 @@ export class Service extends DatabaseService<Model> {
|
||||
throw new BadDataException('Invalid plan');
|
||||
}
|
||||
|
||||
logger.info(
|
||||
'Changing plan for project ' +
|
||||
project.id?.toString() +
|
||||
' to ' +
|
||||
plan.getName()
|
||||
);
|
||||
|
||||
if (!project.paymentProviderSubscriptionSeats) {
|
||||
project.paymentProviderSubscriptionSeats =
|
||||
await TeamMemberService.getUniqueTeamMemberCountInProject(
|
||||
@@ -313,6 +322,15 @@ export class Service extends DatabaseService<Model> {
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
'Changing plan for project ' +
|
||||
project.id?.toString() +
|
||||
' to ' +
|
||||
plan.getName() +
|
||||
' with seats ' +
|
||||
project.paymentProviderSubscriptionSeats
|
||||
);
|
||||
|
||||
const subscription: {
|
||||
subscriptionId: string;
|
||||
meteredSubscriptionId: string;
|
||||
@@ -333,6 +351,16 @@ export class Service extends DatabaseService<Model> {
|
||||
endTrialAt: project.trialEndsAt,
|
||||
});
|
||||
|
||||
logger.info(
|
||||
'Changing plan for project ' +
|
||||
project.id?.toString() +
|
||||
' to ' +
|
||||
plan.getName() +
|
||||
' with seats ' +
|
||||
project.paymentProviderSubscriptionSeats +
|
||||
' completed.'
|
||||
);
|
||||
|
||||
await this.updateOneById({
|
||||
id: new ObjectID(updateBy.query._id! as string),
|
||||
data: {
|
||||
@@ -350,6 +378,16 @@ export class Service extends DatabaseService<Model> {
|
||||
ignoreHooks: true,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
'Changing plan for project ' +
|
||||
project.id?.toString() +
|
||||
' to ' +
|
||||
plan.getName() +
|
||||
' with seats ' +
|
||||
project.paymentProviderSubscriptionSeats +
|
||||
' completed and project updated.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,7 @@ export class Service extends DatabaseService<Model> {
|
||||
phone: true,
|
||||
verificationCode: true,
|
||||
isVerified: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -120,6 +120,7 @@ export class Service extends DatabaseService<Model> {
|
||||
phone: true,
|
||||
verificationCode: true,
|
||||
isVerified: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
43
CommonServer/Tests/Utils/CronTab.test.ts
Normal file
43
CommonServer/Tests/Utils/CronTab.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import CronTab from '../../Utils/CronTab';
|
||||
|
||||
describe('CronTab', () => {
|
||||
it('should return the next execution time for a given cron expression', () => {
|
||||
const crontab: string = '*/5 * * * *';
|
||||
|
||||
const nextExecutionTime: Date = CronTab.getNextExecutionTime(crontab);
|
||||
|
||||
const now: Date = new Date();
|
||||
const expectedNextExecutionTime: Date = new Date(
|
||||
now.getTime() + 5 * 60 * 1000
|
||||
);
|
||||
|
||||
const toleranceInMilliseconds: number = 5000;
|
||||
const differenceInMilliseconds: number =
|
||||
nextExecutionTime.getTime() - expectedNextExecutionTime.getTime();
|
||||
expect(differenceInMilliseconds).toBeLessThan(toleranceInMilliseconds);
|
||||
});
|
||||
|
||||
it('should return the next execution time for a daily cron expression', () => {
|
||||
const crontab: string = '0 0 * * *';
|
||||
|
||||
const nextExecutionTime: Date = CronTab.getNextExecutionTime(crontab);
|
||||
|
||||
const now: Date = new Date();
|
||||
const expectedNextExecutionTime: Date = new Date(
|
||||
now.getTime() + 24 * 60 * 60 * 1000
|
||||
);
|
||||
|
||||
const toleranceInMilliseconds: number = 5000;
|
||||
const differenceInMilliseconds: number =
|
||||
nextExecutionTime.getTime() - expectedNextExecutionTime.getTime();
|
||||
expect(differenceInMilliseconds).toBeLessThan(toleranceInMilliseconds);
|
||||
});
|
||||
|
||||
it('should throw an error when the cron expression is invalid', () => {
|
||||
const crontab: string = 'invalid';
|
||||
|
||||
expect(() => {
|
||||
CronTab.getNextExecutionTime(crontab);
|
||||
}).toThrowError(`Invalid cron expression: ${crontab}`);
|
||||
});
|
||||
});
|
||||
146
CommonServer/Tests/Utils/JsonToCsv.test.ts
Normal file
146
CommonServer/Tests/Utils/JsonToCsv.test.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import csvConverter from '../../Utils/JsonToCsv';
|
||||
import { JSONArray } from 'Common/Types/JSON';
|
||||
|
||||
describe('CSV Converter', () => {
|
||||
it('throws an error when the input JSON array is empty', () => {
|
||||
const emptyJson: JSONArray = [];
|
||||
expect(() => {
|
||||
return csvConverter.ToCsv(emptyJson);
|
||||
}).toThrowError('Cannot convert to CSV when the object length is 0');
|
||||
});
|
||||
|
||||
it('converts a JSON array to CSV', () => {
|
||||
const json: JSONArray = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'test1',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'test2',
|
||||
},
|
||||
];
|
||||
|
||||
const csv: string = csvConverter.ToCsv(json);
|
||||
|
||||
const headerRow: string = csv.split('\n')[0]!;
|
||||
expect(headerRow).toBe(`"id","name"`);
|
||||
|
||||
const dataRows: string[] = csv.split('\n').slice(1);
|
||||
expect(dataRows.length).toBe(2);
|
||||
|
||||
expect(dataRows[0]).toBe(`"1","test1"`);
|
||||
expect(dataRows[1]).toBe(`"2","test2"`);
|
||||
});
|
||||
|
||||
it('handles an empty JSON object', () => {
|
||||
const json: JSONArray = [{}];
|
||||
|
||||
const csv: string = csvConverter.ToCsv(json);
|
||||
|
||||
expect(csv).toBe('');
|
||||
});
|
||||
|
||||
it('handles large JSON arrays', () => {
|
||||
const json: JSONArray = [];
|
||||
|
||||
for (let i: number = 0; i < 100; i++) {
|
||||
json.push({
|
||||
id: i.toString(),
|
||||
name: `test${i}`,
|
||||
});
|
||||
}
|
||||
|
||||
const csv: string = csvConverter.ToCsv(json);
|
||||
|
||||
const headerRow: string = csv.split('\n')[0]!;
|
||||
expect(headerRow).toBe(`"id","name"`);
|
||||
|
||||
const dataRows: string[] = csv.split('\n').slice(1);
|
||||
expect(dataRows.length).toBe(100);
|
||||
|
||||
for (let i: number = 0; i < 100; i++) {
|
||||
expect(dataRows[i]).toBe(`"${i}","test${i}"`);
|
||||
}
|
||||
});
|
||||
|
||||
it('handles a JSON object with an array', () => {
|
||||
const json: JSONArray = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'test1',
|
||||
array: [1, 2, 3],
|
||||
},
|
||||
];
|
||||
|
||||
const csv: string = csvConverter.ToCsv(json);
|
||||
|
||||
const headerRow: string = csv.split('\n')[0]!;
|
||||
expect(headerRow).toBe(`"id","name","array"`);
|
||||
|
||||
const dataRows: string[] = csv.split('\n').slice(1);
|
||||
expect(dataRows.length).toBe(1);
|
||||
|
||||
expect(dataRows[0]).toBe(`"1","test1","[1,2,3]"`);
|
||||
});
|
||||
|
||||
it('handles a JSON object with an object', () => {
|
||||
const json: JSONArray = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'test1',
|
||||
object: { test: 'test' },
|
||||
},
|
||||
];
|
||||
|
||||
const csv: string = csvConverter.ToCsv(json);
|
||||
|
||||
const headerRow: string = csv.split('\n')[0]!;
|
||||
expect(headerRow).toBe(`"id","name","object"`);
|
||||
|
||||
const dataRows: string[] = csv.split('\n').slice(1);
|
||||
expect(dataRows.length).toBe(1);
|
||||
|
||||
expect(dataRows[0]).toBe(`"1","test1","{""test"":""test""}"`);
|
||||
});
|
||||
|
||||
it('handles a JSON object with an empty array', () => {
|
||||
const json: JSONArray = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'test1',
|
||||
array: [],
|
||||
},
|
||||
];
|
||||
|
||||
const csv: string = csvConverter.ToCsv(json);
|
||||
|
||||
const headerRow: string = csv.split('\n')[0]!;
|
||||
expect(headerRow).toBe(`"id","name","array"`);
|
||||
|
||||
const dataRows: string[] = csv.split('\n').slice(1);
|
||||
expect(dataRows.length).toBe(1);
|
||||
|
||||
expect(dataRows[0]).toBe(`"1","test1","[]"`);
|
||||
});
|
||||
|
||||
it('handles a JSON object with an empty object', () => {
|
||||
const json: JSONArray = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'test1',
|
||||
object: {},
|
||||
},
|
||||
];
|
||||
|
||||
const csv: string = csvConverter.ToCsv(json);
|
||||
|
||||
const headerRow: string = csv.split('\n')[0]!;
|
||||
expect(headerRow).toBe(`"id","name","object"`);
|
||||
|
||||
const dataRows: string[] = csv.split('\n').slice(1);
|
||||
expect(dataRows.length).toBe(1);
|
||||
|
||||
expect(dataRows[0]).toBe(`"1","test1","{}"`);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,15 @@
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import CronParser, { CronExpression } from 'cron-parser';
|
||||
|
||||
export default class CronTab {
|
||||
public static getNextExecutionTime(crontab: string): Date {
|
||||
const interval: CronExpression = CronParser.parseExpression(crontab);
|
||||
const nextExecutionTime: Date = interval.next().toDate();
|
||||
return nextExecutionTime;
|
||||
try {
|
||||
const interval: CronExpression =
|
||||
CronParser.parseExpression(crontab);
|
||||
const nextExecutionTime: Date = interval.next().toDate();
|
||||
return nextExecutionTime;
|
||||
} catch (error) {
|
||||
throw new BadDataException(`Invalid cron expression: ${crontab}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,12 @@ const EventHistoryDayList: FunctionComponent<ComponentProps> = (
|
||||
>
|
||||
<div
|
||||
className="text-gray-400 mt-2 text-sm"
|
||||
style={{ padding: '20px', paddingRight: '0px', width: '15%' }}
|
||||
style={{
|
||||
padding: '20px',
|
||||
paddingLeft: '10px',
|
||||
paddingRight: '0px',
|
||||
width: '15%',
|
||||
}}
|
||||
>
|
||||
{OneUptimeDate.getDateAsLocalFormattedString(props.date, true)}
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,11 @@ const Loader: FunctionComponent<ComponentProps> = ({
|
||||
}: ComponentProps) => {
|
||||
if (loaderType === LoaderType.Bar) {
|
||||
return (
|
||||
<div role="bar-loader mt-1" className="justify-center">
|
||||
<div
|
||||
role="bar-loader mt-1"
|
||||
className="justify-center"
|
||||
data-testid="loader"
|
||||
>
|
||||
<BarLoader height={4} width={size} color={color.toString()} />
|
||||
</div>
|
||||
);
|
||||
@@ -30,7 +34,11 @@ const Loader: FunctionComponent<ComponentProps> = ({
|
||||
|
||||
if (loaderType === LoaderType.Beats) {
|
||||
return (
|
||||
<div role="beat-loader mt-1" className="justify-center">
|
||||
<div
|
||||
role="beat-loader mt-1"
|
||||
className="justify-center"
|
||||
data-testid="loader"
|
||||
>
|
||||
<BeatLoader size={size} color={color.toString()} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -72,6 +72,7 @@ const Modal: FunctionComponent<ComponentProps> = (
|
||||
? 'sm:max-w-3xl'
|
||||
: ''
|
||||
} ${!props.modalWidth ? 'sm:max-w-lg' : ''} `}
|
||||
data-testid="modal"
|
||||
>
|
||||
{props.onClose && (
|
||||
<div className="absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
|
||||
@@ -80,6 +81,7 @@ const Modal: FunctionComponent<ComponentProps> = (
|
||||
icon={IconProp.Close}
|
||||
iconSize={SizeProp.Large}
|
||||
title="Close"
|
||||
dataTestId="close-button"
|
||||
onClick={props.onClose}
|
||||
/>
|
||||
</div>
|
||||
@@ -88,6 +90,7 @@ const Modal: FunctionComponent<ComponentProps> = (
|
||||
{props.icon && (
|
||||
<div
|
||||
className={`mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full ${iconBgColor} sm:mx-0 sm:h-10 sm:w-10`}
|
||||
data-testid="icon"
|
||||
>
|
||||
<Icon
|
||||
thick={ThickProp.Thick}
|
||||
@@ -128,7 +131,9 @@ const Modal: FunctionComponent<ComponentProps> = (
|
||||
)}
|
||||
</div>
|
||||
{props.rightElement && (
|
||||
<div>{props.rightElement}</div>
|
||||
<div data-testid="right-element">
|
||||
{props.rightElement}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
|
||||
@@ -7,12 +7,10 @@ import React, {
|
||||
|
||||
import MonitorStatusTimeline from 'Model/Models/MonitorStatusTimeline';
|
||||
import ComponentLoader from '../ComponentLoader/ComponentLoader';
|
||||
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';
|
||||
import UptimeUtil from './UptimeUtil';
|
||||
|
||||
export interface MonitorEvent extends Event {
|
||||
monitorId: ObjectID;
|
||||
@@ -33,173 +31,9 @@ const MonitorUptimeGraph: FunctionComponent<ComponentProps> = (
|
||||
): ReactElement => {
|
||||
const [events, setEvents] = useState<Array<Event>>([]);
|
||||
|
||||
/**
|
||||
* 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: 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;
|
||||
}
|
||||
}
|
||||
|
||||
const eventList: Array<Event> =
|
||||
UptimeUtil.getNonOverlappingMonitorEvents(props.items);
|
||||
setEvents(eventList);
|
||||
}, [props.items]);
|
||||
|
||||
|
||||
274
CommonUI/src/Components/MonitorGraphs/UptimeUtil.ts
Normal file
274
CommonUI/src/Components/MonitorGraphs/UptimeUtil.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import { MonitorEvent } from './Uptime';
|
||||
import MonitorStatusTimeline from 'Model/Models/MonitorStatusTimeline';
|
||||
import OneUptimeDate from 'Common/Types/Date';
|
||||
import { Green } from 'Common/Types/BrandColors';
|
||||
import { Event } from '../Graphs/DayUptimeGraph';
|
||||
import MonitorStatus from 'Model/Models/MonitorStatus';
|
||||
import { UptimePrecision } from 'Model/Models/StatusPageResource';
|
||||
|
||||
export default class UptimeUtil {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static getMonitorEventsForId(
|
||||
monitorId: ObjectID,
|
||||
items: Array<MonitorStatusTimeline>
|
||||
): Array<MonitorEvent> {
|
||||
// Initialize an empty array to store the monitor events.
|
||||
const eventList: Array<MonitorEvent> = [];
|
||||
|
||||
const monitorEvents: Array<MonitorStatusTimeline> = 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: 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;
|
||||
}
|
||||
|
||||
public static getNonOverlappingMonitorEvents(
|
||||
items: Array<MonitorStatusTimeline>
|
||||
): Array<Event> {
|
||||
const monitorEventList: Array<MonitorEvent> =
|
||||
this.getMonitorEvents(items);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return eventList;
|
||||
}
|
||||
|
||||
public static getMonitorEvents(
|
||||
items: Array<MonitorStatusTimeline>
|
||||
): Array<MonitorEvent> {
|
||||
// get all distinct monitor ids.
|
||||
const monitorIds: Array<ObjectID> = [];
|
||||
|
||||
for (let i: number = 0; i < items.length; i++) {
|
||||
if (!items[i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const monitorId: string | undefined =
|
||||
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> =
|
||||
this.getMonitorEventsForId(monitorId, items);
|
||||
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];
|
||||
}
|
||||
|
||||
public static calculateUptimePercentage(
|
||||
items: Array<MonitorStatusTimeline>,
|
||||
monitorStatuses: Array<MonitorStatus>,
|
||||
precision: UptimePrecision
|
||||
): number {
|
||||
const monitorEvents: Array<Event> =
|
||||
this.getNonOverlappingMonitorEvents(items);
|
||||
|
||||
// sort these by start date,
|
||||
monitorEvents.sort((a: Event, b: Event) => {
|
||||
if (OneUptimeDate.isAfter(a.startDate, b.startDate)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (OneUptimeDate.isAfter(b.startDate, a.startDate)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
// calculate number of seconds between start of first event to date time now.
|
||||
let totalSecondsInTimePeriod: number = 0;
|
||||
|
||||
if (monitorEvents.length === 0) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
if (
|
||||
OneUptimeDate.isAfter(
|
||||
monitorEvents[0]!.startDate,
|
||||
OneUptimeDate.getCurrentDate()
|
||||
)
|
||||
) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
totalSecondsInTimePeriod =
|
||||
OneUptimeDate.getSecondsBetweenDates(
|
||||
monitorEvents[0]!.startDate,
|
||||
OneUptimeDate.getCurrentDate()
|
||||
) || 1;
|
||||
|
||||
// get order of operational state.
|
||||
|
||||
const operationalStatePriority: number =
|
||||
monitorStatuses.find((item: MonitorStatus) => {
|
||||
return item.isOperationalState;
|
||||
})?.priority || 0;
|
||||
|
||||
// if the event belongs to less than operationalStatePriority, then add the seconds to the total seconds.
|
||||
|
||||
let totalDowntime: number = 0;
|
||||
|
||||
for (const monitorEvent of monitorEvents) {
|
||||
if (monitorEvent.priority > operationalStatePriority) {
|
||||
totalDowntime += OneUptimeDate.getSecondsBetweenDates(
|
||||
monitorEvent.startDate,
|
||||
monitorEvent.endDate
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// calculate percentage.
|
||||
|
||||
const percentage: number =
|
||||
((totalSecondsInTimePeriod - totalDowntime) /
|
||||
totalSecondsInTimePeriod) *
|
||||
100;
|
||||
|
||||
if (precision === UptimePrecision.NO_DECIMAL) {
|
||||
return Math.round(percentage);
|
||||
}
|
||||
|
||||
if (precision === UptimePrecision.ONE_DECIMAL) {
|
||||
return Math.round(percentage * 10) / 10;
|
||||
}
|
||||
|
||||
if (precision === UptimePrecision.TWO_DECIMAL) {
|
||||
return Math.round(percentage * 100) / 100;
|
||||
}
|
||||
|
||||
if (precision === UptimePrecision.THREE_DECIMAL) {
|
||||
return Math.round(percentage * 1000) / 1000;
|
||||
}
|
||||
|
||||
return percentage;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,27 @@
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
|
||||
export enum TopAlertType {
|
||||
INFO = 'bg-indigo-700',
|
||||
WARNING = 'bg-yellow-700',
|
||||
DANGER = 'bg-red-700',
|
||||
SUCCESS = 'bg-green-700',
|
||||
}
|
||||
|
||||
export interface ComponentProps {
|
||||
title: string;
|
||||
description: string;
|
||||
description: ReactElement | string;
|
||||
alertType?: TopAlertType | undefined;
|
||||
}
|
||||
|
||||
const TopAlert: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
const alertType: TopAlertType = props.alertType || TopAlertType.INFO;
|
||||
|
||||
return (
|
||||
<div className="flex items-center text-center gap-x-6 bg-gray-700 px-6 py-2.5 sm:px-3.5">
|
||||
<div
|
||||
className={`flex items-center text-center gap-x-6 ${alertType.toString()} px-6 py-2.5 sm:px-3.5`}
|
||||
>
|
||||
<div className="text-sm leading-6 text-white w-full">
|
||||
<div className="w-full">
|
||||
<strong className="font-semibold">{props.title}</strong>
|
||||
|
||||
294
CommonUI/src/Tests/Components/Modal.test.tsx
Normal file
294
CommonUI/src/Tests/Components/Modal.test.tsx
Normal file
@@ -0,0 +1,294 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import Modal, { ModalWidth } from '../../Components/Modal/Modal';
|
||||
import { ButtonStyleType } from '../../Components/Button/Button';
|
||||
import ButtonType from '../../Components/Button/ButtonTypes';
|
||||
import IconProp from 'Common/Types/Icon/IconProp';
|
||||
|
||||
describe('Modal', () => {
|
||||
test('renders the modal with the title and description', () => {
|
||||
const onSubmit: jest.Mock = jest.fn();
|
||||
const { getByTestId, getByText } = render(
|
||||
<Modal
|
||||
title="Test Modal Title"
|
||||
description="Test modal description"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<div>Modal content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
expect(getByTestId('modal-title')).toHaveTextContent(
|
||||
'Test Modal Title'
|
||||
);
|
||||
expect(getByText('Test modal description')).toBeInTheDocument();
|
||||
expect(getByText('Modal content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('closes the modal when the close button is clicked', () => {
|
||||
const onCloseMock: jest.Mock = jest.fn();
|
||||
const onSubmit: jest.Mock = jest.fn();
|
||||
|
||||
const { getByTestId } = render(
|
||||
<Modal title="Test Modal" onSubmit={onSubmit} onClose={onCloseMock}>
|
||||
<div>Modal content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const closeButton: HTMLElement = getByTestId('close-button');
|
||||
|
||||
fireEvent.click(closeButton);
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls the onSubmit function when the submit button is clicked', () => {
|
||||
const onSubmitMock: jest.Mock = jest.fn();
|
||||
|
||||
const { getByTestId } = render(
|
||||
<Modal
|
||||
title="Test Modal"
|
||||
onSubmit={onSubmitMock}
|
||||
submitButtonText="Submit"
|
||||
>
|
||||
<div>Modal content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const submitButton: HTMLElement = getByTestId(
|
||||
'modal-footer-submit-button'
|
||||
);
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(onSubmitMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('displays the modal with the default width when modalWidth is not set', () => {
|
||||
const onSubmitMock: jest.Mock = jest.fn();
|
||||
|
||||
const { getByTestId } = render(
|
||||
<Modal title="Test Modal" onSubmit={onSubmitMock}>
|
||||
<div>Modal content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
expect(getByTestId('modal')).toHaveClass('sm:max-w-lg');
|
||||
});
|
||||
|
||||
it('displays the modal with the correct width when modalWidth is set', () => {
|
||||
const onSubmitMock: jest.Mock = jest.fn();
|
||||
|
||||
const { getByTestId } = render(
|
||||
<Modal
|
||||
title="Test Modal"
|
||||
onSubmit={onSubmitMock}
|
||||
modalWidth={ModalWidth.Medium}
|
||||
>
|
||||
<div>Modal content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
expect(getByTestId('modal')).toHaveClass('sm:max-w-3xl');
|
||||
});
|
||||
|
||||
it('displays the children passed to the modal', () => {
|
||||
const onSubmitMock: jest.Mock = jest.fn();
|
||||
|
||||
const { getByText } = render(
|
||||
<Modal title="Test Modal" onSubmit={onSubmitMock}>
|
||||
<div>Modal content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
expect(getByText('Modal content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays the loader when isBodyLoading is true', () => {
|
||||
const onSubmitMock: jest.Mock = jest.fn();
|
||||
|
||||
const { getByTestId } = render(
|
||||
<Modal title="Test Modal" onSubmit={onSubmitMock} isBodyLoading>
|
||||
<div>Modal content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
expect(getByTestId('loader')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not display the loader when isBodyLoading is false', () => {
|
||||
const onSubmitMock: jest.Mock = jest.fn();
|
||||
|
||||
const { queryByTestId } = render(
|
||||
<Modal title="Test Modal" onSubmit={onSubmitMock}>
|
||||
<div>Modal content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
expect(queryByTestId('loader')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not display the loader when isBodyLoading is undefined', () => {
|
||||
const onSubmitMock: jest.Mock = jest.fn();
|
||||
|
||||
const { queryByTestId } = render(
|
||||
<Modal title="Test Modal" onSubmit={onSubmitMock}>
|
||||
<div>Modal content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
expect(queryByTestId('loader')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables the submit button when isLoading is true', () => {
|
||||
const onSubmitMock: jest.Mock = jest.fn();
|
||||
|
||||
const { getByTestId } = render(
|
||||
<Modal
|
||||
title="Test Modal"
|
||||
onSubmit={onSubmitMock}
|
||||
submitButtonText="Submit"
|
||||
isLoading={true}
|
||||
>
|
||||
<div>Modal content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const submitButton: HTMLElement = getByTestId(
|
||||
'modal-footer-submit-button'
|
||||
);
|
||||
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('disables the submit button when disableSubmitButton is true', () => {
|
||||
const onSubmitMock: jest.Mock = jest.fn();
|
||||
|
||||
const { getByTestId } = render(
|
||||
<Modal
|
||||
title="Test Modal"
|
||||
onSubmit={onSubmitMock}
|
||||
submitButtonText="Submit"
|
||||
disableSubmitButton={true}
|
||||
>
|
||||
<div>Modal content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const submitButton: HTMLElement = getByTestId(
|
||||
'modal-footer-submit-button'
|
||||
);
|
||||
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('disables the submit button when isBodyLoading is true', () => {
|
||||
const onSubmitMock: jest.Mock = jest.fn();
|
||||
|
||||
const { getByTestId } = render(
|
||||
<Modal
|
||||
title="Test Modal"
|
||||
onSubmit={onSubmitMock}
|
||||
submitButtonText="Submit"
|
||||
isBodyLoading={true}
|
||||
>
|
||||
<div>Modal content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const submitButton: HTMLElement = getByTestId(
|
||||
'modal-footer-submit-button'
|
||||
);
|
||||
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('displays the icon when icon is set', () => {
|
||||
const onSubmitMock: jest.Mock = jest.fn();
|
||||
|
||||
const { getByTestId } = render(
|
||||
<Modal
|
||||
title="Test Modal"
|
||||
onSubmit={onSubmitMock}
|
||||
icon={IconProp.SMS}
|
||||
>
|
||||
<div>Modal content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
expect(getByTestId('icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays the submit button with the default style when submitButtonStyleType is not set', () => {
|
||||
const onSubmitMock: jest.Mock = jest.fn();
|
||||
|
||||
const { getByTestId } = render(
|
||||
<Modal title="Test Modal" onSubmit={onSubmitMock}>
|
||||
<div>Modal content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const submitButton: HTMLElement = getByTestId(
|
||||
'modal-footer-submit-button'
|
||||
);
|
||||
|
||||
expect(submitButton).toHaveAttribute('type', 'button');
|
||||
});
|
||||
|
||||
it('displays the submit button with the correct style when submitButtonStyleType is set', () => {
|
||||
const onSubmitMock: jest.Mock = jest.fn();
|
||||
|
||||
const { getByTestId } = render(
|
||||
<Modal
|
||||
title="Test Modal"
|
||||
onSubmit={onSubmitMock}
|
||||
submitButtonType={ButtonType.Reset}
|
||||
>
|
||||
<div>Modal content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const submitButton: HTMLElement = getByTestId(
|
||||
'modal-footer-submit-button'
|
||||
);
|
||||
|
||||
expect(submitButton).toHaveAttribute('type', 'reset');
|
||||
});
|
||||
|
||||
it('displays the submit button with the default style when submitButtonStyleType is set', () => {
|
||||
const onSubmitMock: jest.Mock = jest.fn();
|
||||
|
||||
const { getByTestId } = render(
|
||||
<Modal
|
||||
title="Test Modal"
|
||||
onSubmit={onSubmitMock}
|
||||
submitButtonStyleType={ButtonStyleType.DANGER}
|
||||
>
|
||||
<div>Modal content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const submitButton: HTMLElement = getByTestId(
|
||||
'modal-footer-submit-button'
|
||||
);
|
||||
|
||||
expect(submitButton).toHaveClass('bg-red-600');
|
||||
});
|
||||
|
||||
it('displays the right element when rightElement is set', () => {
|
||||
const onSubmitMock: jest.Mock = jest.fn();
|
||||
|
||||
const { getByTestId } = render(
|
||||
<Modal
|
||||
title="Test Modal"
|
||||
onSubmit={onSubmitMock}
|
||||
rightElement={<div>Right element</div>}
|
||||
>
|
||||
<div>Modal content</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
expect(getByTestId('right-element')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
73
CommonUI/src/Tests/Components/Tabs.test.tsx
Normal file
73
CommonUI/src/Tests/Components/Tabs.test.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import Tabs from '../../Components/Tabs/Tabs';
|
||||
|
||||
describe('Tabs', () => {
|
||||
const tabs: Array<string> = ['tab1', 'tab2'];
|
||||
|
||||
test('it should render all props passed', () => {
|
||||
const onTabChange: jest.Mock = jest.fn();
|
||||
|
||||
const { getByText } = render(
|
||||
<Tabs tabs={tabs} onTabChange={onTabChange} />
|
||||
);
|
||||
|
||||
expect(getByText('tab1')).toBeInTheDocument();
|
||||
expect(getByText('tab2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it should render the first tab as active by default', () => {
|
||||
const onTabChange: jest.Mock = jest.fn();
|
||||
|
||||
const { getByText } = render(
|
||||
<Tabs tabs={tabs} onTabChange={onTabChange} />
|
||||
);
|
||||
|
||||
expect(getByText('tab1')).toHaveClass('active');
|
||||
});
|
||||
|
||||
test('it should call onTabChange with the correct tab when a tab is clicked', () => {
|
||||
const onTabChange: jest.Mock = jest.fn();
|
||||
|
||||
const { getByText } = render(
|
||||
<Tabs tabs={tabs} onTabChange={onTabChange} />
|
||||
);
|
||||
|
||||
fireEvent.click(getByText('tab1'));
|
||||
expect(onTabChange).toHaveBeenCalledWith('tab1');
|
||||
|
||||
fireEvent.click(getByText('tab2'));
|
||||
expect(onTabChange).toHaveBeenCalledWith('tab2');
|
||||
});
|
||||
|
||||
test('it should show the correct tab as active when a tab is clicked', () => {
|
||||
const onTabChange: jest.Mock = jest.fn();
|
||||
|
||||
const { getByText } = render(
|
||||
<Tabs tabs={tabs} onTabChange={onTabChange} />
|
||||
);
|
||||
|
||||
fireEvent.click(getByText('tab2'));
|
||||
|
||||
expect(getByText('tab1')).not.toHaveClass('active');
|
||||
expect(getByText('tab2')).toHaveClass('active');
|
||||
});
|
||||
|
||||
test('it should handle empty tabs array gracefully', () => {
|
||||
const tabs: Array<string> = [];
|
||||
const onTabChange: jest.Mock = jest.fn();
|
||||
|
||||
const { getByText } = render(
|
||||
<Tabs tabs={tabs} onTabChange={onTabChange} />
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
return getByText('tab1');
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
return getByText('tab2');
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import SubscriptionPlan, {
|
||||
PlanSelect,
|
||||
} from 'Common/Types/Billing/SubscriptionPlan';
|
||||
import { BILLING_ENABLED, getAllEnvVars } from '../Config';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
|
||||
export default class ProjectUtil {
|
||||
public static getCurrentProject(): Project | null {
|
||||
@@ -18,6 +19,10 @@ export default class ProjectUtil {
|
||||
return JSONFunctions.fromJSON(projectJson, Project) as Project;
|
||||
}
|
||||
|
||||
public static getCurrentProjectId(): ObjectID | null {
|
||||
return this.getCurrentProject()?.id || null;
|
||||
}
|
||||
|
||||
public static setCurrentProject(project: JSONObject | Project): void {
|
||||
if (project instanceof Project) {
|
||||
project = JSONFunctions.toJSON(project, Project);
|
||||
|
||||
@@ -9,6 +9,12 @@ import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import RouteMap, { RouteUtil } from '../../Utils/RouteMap';
|
||||
import PageMap from '../../Utils/PageMap';
|
||||
import SSOAuthorizationException from 'Common/Types/Exception/SsoAuthorizationException';
|
||||
import TopAlert, {
|
||||
TopAlertType,
|
||||
} from 'CommonUI/src/Components/TopAlert/TopAlert';
|
||||
import Link from 'CommonUI/src/Components/Link/Link';
|
||||
import { BILLING_ENABLED } from 'CommonUI/src/Config';
|
||||
import { SubscriptionStatusUtil } from 'Common/Types/Billing/SubscriptionStatus';
|
||||
|
||||
export interface ComponentProps {
|
||||
children: ReactElement | Array<ReactElement>;
|
||||
@@ -46,30 +52,67 @@ const DashboardMasterPage: FunctionComponent<ComponentProps> = (
|
||||
error = props.error;
|
||||
}
|
||||
|
||||
let isSubscriptionInactive: boolean = false;
|
||||
|
||||
if (props.selectedProject) {
|
||||
const isMeteredSubscriptionInactive: boolean =
|
||||
SubscriptionStatusUtil.isSubscriptionInactive(
|
||||
props.selectedProject?.paymentProviderMeteredSubscriptionStatus
|
||||
);
|
||||
const isProjectSubscriptionInactive: boolean =
|
||||
SubscriptionStatusUtil.isSubscriptionInactive(
|
||||
props.selectedProject?.paymentProviderSubscriptionStatus
|
||||
);
|
||||
|
||||
isSubscriptionInactive =
|
||||
isMeteredSubscriptionInactive || isProjectSubscriptionInactive;
|
||||
}
|
||||
|
||||
return (
|
||||
<MasterPage
|
||||
footer={<Footer />}
|
||||
header={
|
||||
<Header
|
||||
projects={props.projects}
|
||||
onProjectSelected={props.onProjectSelected}
|
||||
showProjectModal={props.showProjectModal}
|
||||
onProjectModalClose={props.onProjectModalClose}
|
||||
selectedProject={props.selectedProject || null}
|
||||
paymentMethodsCount={props.paymentMethodsCount}
|
||||
<div>
|
||||
{BILLING_ENABLED && isSubscriptionInactive && (
|
||||
<TopAlert
|
||||
alertType={TopAlertType.DANGER}
|
||||
title="Your project is not active because some invoices are unpaid."
|
||||
description={
|
||||
<Link
|
||||
className="underline"
|
||||
to={RouteUtil.populateRouteParams(
|
||||
RouteMap[
|
||||
PageMap.SETTINGS_BILLING_INVOICES
|
||||
] as Route
|
||||
)}
|
||||
>
|
||||
Click here to pay your unpaid invoices.
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
}
|
||||
navBar={
|
||||
<NavBar
|
||||
show={props.projects.length > 0 && !isOnHideNavbarPage}
|
||||
/>
|
||||
}
|
||||
isLoading={props.isLoading}
|
||||
error={error}
|
||||
className="flex flex-col h-screen justify-between"
|
||||
>
|
||||
{props.children}
|
||||
</MasterPage>
|
||||
)}
|
||||
|
||||
<MasterPage
|
||||
footer={<Footer />}
|
||||
header={
|
||||
<Header
|
||||
projects={props.projects}
|
||||
onProjectSelected={props.onProjectSelected}
|
||||
showProjectModal={props.showProjectModal}
|
||||
onProjectModalClose={props.onProjectModalClose}
|
||||
selectedProject={props.selectedProject || null}
|
||||
paymentMethodsCount={props.paymentMethodsCount}
|
||||
/>
|
||||
}
|
||||
navBar={
|
||||
<NavBar
|
||||
show={props.projects.length > 0 && !isOnHideNavbarPage}
|
||||
/>
|
||||
}
|
||||
isLoading={props.isLoading}
|
||||
error={error}
|
||||
className="flex flex-col h-screen justify-between"
|
||||
>
|
||||
{props.children}
|
||||
</MasterPage>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -31,23 +31,60 @@ import API from 'CommonUI/src/Utils/API/API';
|
||||
import DisabledWarning from '../../../Components/Monitor/DisabledWarning';
|
||||
import MonitorType from 'Common/Types/Monitor/MonitorType';
|
||||
import IncomingMonitorLink from './IncomingMonitorLink';
|
||||
import { Grey } from 'Common/Types/BrandColors';
|
||||
import { Green, Grey } from 'Common/Types/BrandColors';
|
||||
import UptimeUtil from 'CommonUI/src/Components/MonitorGraphs/UptimeUtil';
|
||||
import MonitorStatus from 'Model/Models/MonitorStatus';
|
||||
import { UptimePrecision } from 'Model/Models/StatusPageResource';
|
||||
import ProjectUtil from 'CommonUI/src/Utils/Project';
|
||||
|
||||
const MonitorView: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps
|
||||
): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID();
|
||||
|
||||
const [data, setData] = useState<Array<MonitorStatusTimeline>>([]);
|
||||
const [statusTimelines, setStatusTimelines] = useState<
|
||||
Array<MonitorStatusTimeline>
|
||||
>([]);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const startDate: Date = OneUptimeDate.getSomeDaysAgo(90);
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const [monitorStatuses, setMonitorStatuses] = useState<
|
||||
Array<MonitorStatus>
|
||||
>([]);
|
||||
const [currentMonitorStatus, setCurrentMonitorStatus] = useState<
|
||||
MonitorStatus | undefined
|
||||
>(undefined);
|
||||
|
||||
const [monitorType, setMonitorType] = useState<MonitorType | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const getUptimePercent: () => ReactElement = (): ReactElement => {
|
||||
if (isLoading) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const uptimePercent: number = UptimeUtil.calculateUptimePercentage(
|
||||
statusTimelines,
|
||||
monitorStatuses,
|
||||
UptimePrecision.THREE_DECIMAL
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="font-medium mt-5"
|
||||
style={{
|
||||
color:
|
||||
currentMonitorStatus?.color?.toString() ||
|
||||
Green.toString(),
|
||||
}}
|
||||
>
|
||||
{uptimePercent}% uptime
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
await fetchItem();
|
||||
}, []);
|
||||
@@ -63,6 +100,7 @@ const MonitorView: FunctionComponent<PageComponentProps> = (
|
||||
{
|
||||
createdAt: new InBetween(startDate, endDate),
|
||||
monitorId: modelId,
|
||||
projectId: ProjectUtil.getCurrentProjectId(),
|
||||
},
|
||||
LIMIT_PER_PROJECT,
|
||||
0,
|
||||
@@ -72,6 +110,7 @@ const MonitorView: FunctionComponent<PageComponentProps> = (
|
||||
monitorStatus: {
|
||||
name: true,
|
||||
color: true,
|
||||
isOperationalState: true,
|
||||
priority: true,
|
||||
},
|
||||
},
|
||||
@@ -85,10 +124,34 @@ const MonitorView: FunctionComponent<PageComponentProps> = (
|
||||
modelId,
|
||||
{
|
||||
monitorType: true,
|
||||
currentMonitorStatus: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
} as any,
|
||||
{}
|
||||
);
|
||||
|
||||
const monitorStatuses: ListResult<MonitorStatus> =
|
||||
await ModelAPI.getList(
|
||||
MonitorStatus,
|
||||
{
|
||||
projectId: ProjectUtil.getCurrentProjectId(),
|
||||
},
|
||||
LIMIT_PER_PROJECT,
|
||||
0,
|
||||
{
|
||||
_id: true,
|
||||
priority: true,
|
||||
isOperationalState: true,
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
{
|
||||
priority: SortOrder.Ascending,
|
||||
}
|
||||
);
|
||||
|
||||
if (!item) {
|
||||
setError(`Monitor not found`);
|
||||
|
||||
@@ -96,8 +159,10 @@ const MonitorView: FunctionComponent<PageComponentProps> = (
|
||||
}
|
||||
|
||||
setMonitorType(item.monitorType);
|
||||
setCurrentMonitorStatus(item.currentMonitorStatus);
|
||||
setMonitorStatuses(monitorStatuses.data);
|
||||
|
||||
setData(monitorStatus.data);
|
||||
setStatusTimelines(monitorStatus.data);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
@@ -314,10 +379,11 @@ const MonitorView: FunctionComponent<PageComponentProps> = (
|
||||
<Card
|
||||
title="Uptime Graph"
|
||||
description="Here the 90 day uptime history of this monitor."
|
||||
rightElement={getUptimePercent()}
|
||||
>
|
||||
<MonitorUptimeGraph
|
||||
error={error}
|
||||
items={data}
|
||||
items={statusTimelines}
|
||||
startDate={OneUptimeDate.getSomeDaysAgo(90)}
|
||||
endDate={OneUptimeDate.getCurrentDate()}
|
||||
isLoading={isLoading}
|
||||
|
||||
@@ -15,7 +15,6 @@ import ObjectID from 'Common/Types/ObjectID';
|
||||
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';
|
||||
@@ -26,16 +25,72 @@ 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';
|
||||
import UptimeUtil from 'CommonUI/src/Components/MonitorGraphs/UptimeUtil';
|
||||
import MonitorStatus from 'Model/Models/MonitorStatus';
|
||||
import ProjectUtil from 'CommonUI/src/Utils/Project';
|
||||
import { UptimePrecision } from 'Model/Models/StatusPageResource';
|
||||
import { Green } from 'Common/Types/BrandColors';
|
||||
import Statusbubble from 'CommonUI/src/Components/StatusBubble/StatusBubble';
|
||||
import SortOrder from 'Common/Types/BaseDatabase/SortOrder';
|
||||
import PageLoader from 'CommonUI/src/Components/Loader/PageLoader';
|
||||
import ErrorMessage from 'CommonUI/src/Components/ErrorMessage/ErrorMessage';
|
||||
|
||||
const MonitorGroupView: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps
|
||||
): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID();
|
||||
|
||||
const [data, setData] = useState<Array<MonitorStatusTimeline>>([]);
|
||||
const [currentGroupStatus, setCurrentGroupStatus] =
|
||||
React.useState<MonitorStatus | null>(null);
|
||||
|
||||
const [statusTimelines, setStatusTimelines] = useState<
|
||||
Array<MonitorStatusTimeline>
|
||||
>([]);
|
||||
const [monitorStatuses, setMonitorStatuses] = useState<
|
||||
Array<MonitorStatus>
|
||||
>([]);
|
||||
|
||||
const [error, setError] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
const getUptimePercent: () => ReactElement = (): ReactElement => {
|
||||
if (isLoading) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const uptimePercent: number = UptimeUtil.calculateUptimePercentage(
|
||||
statusTimelines,
|
||||
monitorStatuses,
|
||||
UptimePrecision.THREE_DECIMAL
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="font-medium mt-5"
|
||||
style={{
|
||||
color:
|
||||
currentGroupStatus?.color?.toString() ||
|
||||
Green.toString(),
|
||||
}}
|
||||
>
|
||||
{uptimePercent}% uptime
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getCurrentStatusBubble: () => ReactElement = (): ReactElement => {
|
||||
if (isLoading) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Statusbubble
|
||||
text={currentGroupStatus?.name || 'Operational'}
|
||||
color={currentGroupStatus?.color || Green}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
await fetchItem();
|
||||
}, []);
|
||||
@@ -45,7 +100,7 @@ const MonitorGroupView: FunctionComponent<PageComponentProps> = (
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const monitorStatus: ListResult<MonitorStatusTimeline> =
|
||||
const statusTimelines: ListResult<MonitorStatusTimeline> =
|
||||
await ModelAPI.getList(
|
||||
MonitorStatusTimeline,
|
||||
{},
|
||||
@@ -63,7 +118,38 @@ const MonitorGroupView: FunctionComponent<PageComponentProps> = (
|
||||
}
|
||||
);
|
||||
|
||||
setData(monitorStatus.data);
|
||||
const monitorStatuses: ListResult<MonitorStatus> =
|
||||
await ModelAPI.getList(
|
||||
MonitorStatus,
|
||||
{
|
||||
projectId: ProjectUtil.getCurrentProjectId(),
|
||||
},
|
||||
LIMIT_PER_PROJECT,
|
||||
0,
|
||||
{
|
||||
_id: true,
|
||||
priority: true,
|
||||
isOperationalState: true,
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
{
|
||||
priority: SortOrder.Ascending,
|
||||
}
|
||||
);
|
||||
|
||||
const currentStatus: MonitorStatus | null =
|
||||
await ModelAPI.post<MonitorStatus>(
|
||||
MonitorStatus,
|
||||
URL.fromString(DASHBOARD_API_URL.toString())
|
||||
.addRoute(new MonitorGroup().getCrudApiPath()!)
|
||||
.addRoute('/current-status/')
|
||||
.addRoute(`/${modelId.toString()}`)
|
||||
);
|
||||
|
||||
setCurrentGroupStatus(currentStatus);
|
||||
setStatusTimelines(statusTimelines.data);
|
||||
setMonitorStatuses(monitorStatuses.data);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
@@ -71,6 +157,14 @@ const MonitorGroupView: FunctionComponent<PageComponentProps> = (
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ModelPage
|
||||
title="Monitor Group"
|
||||
@@ -215,12 +309,8 @@ const MonitorGroupView: FunctionComponent<PageComponentProps> = (
|
||||
},
|
||||
fieldType: FieldType.Element,
|
||||
title: 'Current Status',
|
||||
getElement: (): ReactElement => {
|
||||
return (
|
||||
<CurrentStatusElement
|
||||
monitorGroupId={modelId}
|
||||
/>
|
||||
);
|
||||
getElement: () => {
|
||||
return getCurrentStatusBubble();
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -231,10 +321,11 @@ const MonitorGroupView: FunctionComponent<PageComponentProps> = (
|
||||
<Card
|
||||
title="Uptime Graph"
|
||||
description="Here the 90 day uptime history of this monitor group."
|
||||
rightElement={getUptimePercent()}
|
||||
>
|
||||
<MonitorUptimeGraph
|
||||
error={error}
|
||||
items={data}
|
||||
items={statusTimelines}
|
||||
startDate={OneUptimeDate.getSomeDaysAgo(90)}
|
||||
endDate={OneUptimeDate.getCurrentDate()}
|
||||
isLoading={isLoading}
|
||||
|
||||
@@ -12,7 +12,9 @@ import PageComponentProps from '../../PageComponentProps';
|
||||
import SideMenu from './SideMenu';
|
||||
import DashboardNavigation from '../../../Utils/Navigation';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import StatusPageResource from 'Model/Models/StatusPageResource';
|
||||
import StatusPageResource, {
|
||||
UptimePrecision,
|
||||
} from 'Model/Models/StatusPageResource';
|
||||
import FieldType from 'CommonUI/src/Components/Types/FieldType';
|
||||
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType';
|
||||
import ModelTable from 'CommonUI/src/Components/ModelTable/ModelTable';
|
||||
@@ -34,6 +36,8 @@ 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';
|
||||
import DropdownUtil from 'CommonUI/src/Utils/Dropdown';
|
||||
import FormValues from 'CommonUI/src/Components/Forms/Types/FormValues';
|
||||
|
||||
const StatusPageDelete: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps
|
||||
@@ -206,6 +210,33 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
|
||||
'Current Resource Status will be shown beside this resource on your status page.',
|
||||
stepId: 'advanced',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
showUptimePercent: true,
|
||||
},
|
||||
title: 'Show Uptime %',
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
description:
|
||||
'Show uptime percentage for the past 90 days beside this resource on your status page.',
|
||||
stepId: 'advanced',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
uptimePercentPrecision: true,
|
||||
},
|
||||
stepId: 'advanced',
|
||||
fieldType: FormFieldSchemaType.Dropdown,
|
||||
dropdownOptions:
|
||||
DropdownUtil.getDropdownOptionsFromEnum(UptimePrecision),
|
||||
showIf: (item: FormValues<StatusPageResource>): boolean => {
|
||||
return Boolean(item.showUptimePercent);
|
||||
},
|
||||
title: 'Select Uptime Precision',
|
||||
defaultValue: UptimePrecision.ONE_DECIMAL,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
showStatusHistoryChart: true,
|
||||
|
||||
@@ -163,6 +163,14 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
|
||||
required: true,
|
||||
placeholder: '14',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
showScheduledEventLabelsOnStatusPage: true,
|
||||
},
|
||||
title: 'Show Event Labels',
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
},
|
||||
]}
|
||||
modelDetailProps={{
|
||||
showDetailsInNumberOfColumns: 1,
|
||||
@@ -176,6 +184,14 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
|
||||
fieldType: FieldType.Number,
|
||||
title: 'Show Scheduled Event History (in days)',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
showScheduledEventLabelsOnStatusPage: true,
|
||||
},
|
||||
fieldType: FieldType.Boolean,
|
||||
title: 'Show Event Labels',
|
||||
placeholder: 'No',
|
||||
},
|
||||
],
|
||||
modelId: modelId,
|
||||
}}
|
||||
|
||||
@@ -60,10 +60,10 @@ redis:
|
||||
enabled: true
|
||||
master:
|
||||
persistence:
|
||||
size: 25Gi
|
||||
enabled: false # We dont need redis persistence, because we dont do anything with it.
|
||||
replica:
|
||||
persistence:
|
||||
size: 25Gi
|
||||
enabled: false # We dont need redis persistence, because we dont do anything with it.
|
||||
|
||||
|
||||
image:
|
||||
|
||||
@@ -112,6 +112,10 @@
|
||||
<a href="https://github.com/oneuptime/oneuptime" target="_blank" class="text-sm leading-6 text-gray-600 hover:text-gray-900">GitHub</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="https://blog.oneuptime.com" target="_blank" class="text-sm leading-6 text-gray-600 hover:text-gray-900">Blog</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/about" class="text-sm leading-6 text-gray-600 hover:text-gray-900">About Us</a>
|
||||
</li>
|
||||
|
||||
@@ -182,6 +182,19 @@
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="https://blog.oneuptime.com" target="_blank" class="-m-3 flex items-start rounded-lg p-3 hover:bg-gray-50">
|
||||
<!-- Heroicon name: outline/lifebuoy -->
|
||||
|
||||
<svg class="h-6 w-6 flex-shrink-0 text-indigo-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.26 10.147a60.436 60.436 0 00-.491 6.347A48.627 48.627 0 0112 20.904a48.627 48.627 0 018.232-4.41 60.46 60.46 0 00-.491-6.347m-15.482 0a50.57 50.57 0 00-2.658-.813A59.905 59.905 0 0112 3.493a59.902 59.902 0 0110.399 5.84c-.896.248-1.783.52-2.658.814m-15.482 0A50.697 50.697 0 0112 13.489a50.702 50.702 0 017.74-3.342M6.75 15a.75.75 0 100-1.5.75.75 0 000 1.5zm0 0v-3.675A55.378 55.378 0 0112 8.443m-7.007 11.55A5.981 5.981 0 006.75 15.75v-1.5" />
|
||||
</svg>
|
||||
|
||||
|
||||
<div class="ml-4">
|
||||
<p class="text-base font-medium text-gray-900">Learning Resources and Blog</p>
|
||||
<p class="mt-1 text-sm text-gray-500">Learn about observability and keep yourself updated.</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="/support" class="-m-3 flex items-start rounded-lg p-3 hover:bg-gray-50">
|
||||
<!-- Heroicon name: outline/lifebuoy -->
|
||||
|
||||
@@ -918,6 +918,43 @@ export default class StatusPage extends BaseModel {
|
||||
})
|
||||
public showIncidentLabelsOnStatusPage?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CanCreateProjectStatusPage,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CanReadProjectStatusPage,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CanEditProjectStatusPage,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
isDefaultValueColumn: true,
|
||||
type: TableColumnType.Boolean,
|
||||
title: 'Show Scheduled Event Labels',
|
||||
description: 'Show Scheduled Event Labels on Status Page?',
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
default: false,
|
||||
})
|
||||
@ColumnBillingAccessControl({
|
||||
read: PlanSelect.Free,
|
||||
update: PlanSelect.Growth,
|
||||
create: PlanSelect.Free,
|
||||
})
|
||||
public showScheduledEventLabelsOnStatusPage?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
|
||||
@@ -24,6 +24,13 @@ import CanAccessIfCanReadOn from 'Common/Types/Database/CanAccessIfCanReadOn';
|
||||
import EnableDocumentation from 'Common/Types/Database/EnableDocumentation';
|
||||
import MonitorGroup from './MonitorGroup';
|
||||
|
||||
export enum UptimePrecision {
|
||||
NO_DECIMAL = '99% (No Decimal)',
|
||||
ONE_DECIMAL = '99.9% (One Decimal)',
|
||||
TWO_DECIMAL = '99.99% (Two Decimal)',
|
||||
THREE_DECIMAL = '99.999% (Three Decimal)',
|
||||
}
|
||||
|
||||
@EnableDocumentation()
|
||||
@CanAccessIfCanReadOn('statusPage')
|
||||
@TenantColumn('projectId')
|
||||
@@ -569,6 +576,71 @@ export default class StatusPageResource extends BaseModel {
|
||||
})
|
||||
public showCurrentStatus?: boolean = 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({
|
||||
isDefaultValueColumn: true,
|
||||
type: TableColumnType.Boolean,
|
||||
title: 'Show Uptime Percent',
|
||||
description: 'Show uptime percent of this monitor for the last 90 days',
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
default: false,
|
||||
})
|
||||
public showUptimePercent?: boolean = 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({
|
||||
type: TableColumnType.ShortText,
|
||||
title: 'Uptime Percent Precision',
|
||||
required: false,
|
||||
description:
|
||||
'Precision of uptime percent of this monitor for the last 90 days',
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
nullable: true,
|
||||
})
|
||||
public uptimePercentPrecision?: UptimePrecision = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
|
||||
@@ -38,6 +38,8 @@ export default class CallService {
|
||||
userOnCallLogTimelineId?: ObjectID | undefined; // user notification log timeline id
|
||||
}
|
||||
): Promise<void> {
|
||||
logger.info('Call Request received.');
|
||||
|
||||
let callCost: number = 0;
|
||||
|
||||
if (IsBillingEnabled) {
|
||||
@@ -47,6 +49,8 @@ export default class CallService {
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Call Cost: ' + callCost);
|
||||
|
||||
const twilioConfig: TwilioConfig | null = await getTwilioConfig();
|
||||
|
||||
if (!twilioConfig) {
|
||||
@@ -91,6 +95,8 @@ export default class CallService {
|
||||
},
|
||||
});
|
||||
|
||||
logger.info('Project found.');
|
||||
|
||||
if (!project) {
|
||||
callLog.status = CallStatus.Error;
|
||||
callLog.statusMessage = `Project ${options.projectId.toString()} not found.`;
|
||||
@@ -224,6 +230,8 @@ export default class CallService {
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Sending Call Request.');
|
||||
|
||||
const twillioCall: CallInstance = await client.calls.create({
|
||||
twiml: this.generateTwimlForCall(callRequest),
|
||||
to: callRequest.to.toString(),
|
||||
@@ -233,11 +241,17 @@ export default class CallService {
|
||||
: twilioConfig.phoneNumber.toString(), // From a valid Twilio number
|
||||
});
|
||||
|
||||
logger.info('Call Request sent successfully.');
|
||||
|
||||
callLog.status = CallStatus.Success;
|
||||
callLog.statusMessage = 'Call ID: ' + twillioCall.sid;
|
||||
logger.info('Call Request sent successfully.');
|
||||
|
||||
logger.info('Call ID: ' + twillioCall.sid);
|
||||
logger.info(callLog.statusMessage);
|
||||
|
||||
if (IsBillingEnabled && project) {
|
||||
logger.info('Updating Project Balance.');
|
||||
|
||||
callLog.callCostInUSDCents = callCost * 100;
|
||||
|
||||
if (twillioCall && parseInt(twillioCall.duration) > 60) {
|
||||
@@ -247,6 +261,8 @@ export default class CallService {
|
||||
);
|
||||
}
|
||||
|
||||
logger.info('Call Cost: ' + callLog.callCostInUSDCents);
|
||||
|
||||
project.smsOrCallCurrentBalanceInUSDCents = Math.floor(
|
||||
project.smsOrCallCurrentBalanceInUSDCents! - callCost * 100
|
||||
);
|
||||
@@ -262,6 +278,12 @@ export default class CallService {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info("Project's current balance updated.");
|
||||
logger.info(
|
||||
'Current Balance: ' +
|
||||
project.smsOrCallCurrentBalanceInUSDCents
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
callLog.callCostInUSDCents = 0;
|
||||
@@ -273,13 +295,19 @@ export default class CallService {
|
||||
logger.error(callLog.statusMessage);
|
||||
}
|
||||
|
||||
logger.info('Saving Call Log if project id is provided.');
|
||||
|
||||
if (options.projectId) {
|
||||
logger.info('Saving Call Log.');
|
||||
await CallLogService.create({
|
||||
data: callLog,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
logger.info('Call Log saved.');
|
||||
} else {
|
||||
logger.info('Project Id is not provided. Call Log not saved.');
|
||||
}
|
||||
|
||||
if (options.userOnCallLogTimelineId) {
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
maxmemory-policy=noeviction
|
||||
appendonly yes
|
||||
@@ -1,13 +1,13 @@
|
||||
// This script merges config.env.tpl to config.env
|
||||
|
||||
import fs from 'fs';
|
||||
const fs = require('fs');
|
||||
|
||||
const init: Function = (): void => {
|
||||
const tempate: string = fs.readFileSync('./config.example.env', 'utf8');
|
||||
const env: string = fs.readFileSync('./config.env', 'utf8');
|
||||
const init = () => {
|
||||
const tempate = fs.readFileSync('./config.example.env', 'utf8');
|
||||
const env = fs.readFileSync('./config.env', 'utf8');
|
||||
|
||||
const linesInTemplate: Array<string> = tempate.split('\n');
|
||||
const linesInEnv: Array<string> = env.split('\n');
|
||||
const linesInTemplate = tempate.split('\n');
|
||||
const linesInEnv= env.split('\n');
|
||||
|
||||
for (const line of linesInTemplate) {
|
||||
// this is a comment, ignore.
|
||||
@@ -23,7 +23,7 @@ const init: Function = (): void => {
|
||||
// if the line is present in template but is not present in env file then add it to the env file. We assume, values in template file are default values.
|
||||
if (line.split('=').length > 0) {
|
||||
if (
|
||||
linesInEnv.filter((envLine: string) => {
|
||||
linesInEnv.filter((envLine) => {
|
||||
return (
|
||||
envLine.split('=').length > 0 &&
|
||||
envLine.split('=')[0] === line.split('=')[0]
|
||||
@@ -1,15 +1,15 @@
|
||||
// This script merges config.env.tpl to config.env
|
||||
|
||||
import fs from 'fs';
|
||||
const fs = require('fs');
|
||||
|
||||
const init: Function = (): void => {
|
||||
let env: string = '';
|
||||
const init = () => {
|
||||
let env = '';
|
||||
try {
|
||||
env = fs.readFileSync('./config.env', 'utf8');
|
||||
} catch (err) {
|
||||
// do nothing.
|
||||
}
|
||||
const envValToReplace: string | undefined = process.argv[2];
|
||||
const envValToReplace = process.argv[2];
|
||||
|
||||
if (!envValToReplace) {
|
||||
// eslint-disable-next-line
|
||||
@@ -17,7 +17,7 @@ const init: Function = (): void => {
|
||||
return;
|
||||
}
|
||||
|
||||
const envValToReplaceWith: string | undefined = process.argv[3];
|
||||
const envValToReplaceWith= process.argv[3];
|
||||
|
||||
if (!envValToReplaceWith) {
|
||||
// eslint-disable-next-line
|
||||
@@ -25,9 +25,9 @@ const init: Function = (): void => {
|
||||
return;
|
||||
}
|
||||
|
||||
const linesInEnv: Array<string> = env.split('\n');
|
||||
const linesToRender: Array<string> = [];
|
||||
let found: boolean = false;
|
||||
const linesInEnv = env.split('\n');
|
||||
const linesToRender = [];
|
||||
let found = false;
|
||||
|
||||
for (let line of linesInEnv) {
|
||||
// this is a comment, ignore.
|
||||
@@ -35,7 +35,7 @@ const init: Function = (): void => {
|
||||
linesToRender.push(line);
|
||||
} else {
|
||||
found = true;
|
||||
const items: Array<string> = line.split('=');
|
||||
const items = line.split('=');
|
||||
items[1] = envValToReplaceWith;
|
||||
line = items.join('=');
|
||||
linesToRender.push(line);
|
||||
@@ -7,12 +7,14 @@ import Icon from 'CommonUI/src/Components/Icon/Icon';
|
||||
import Tooltip from 'CommonUI/src/Components/Tooltip/Tooltip';
|
||||
import IconProp from 'Common/Types/Icon/IconProp';
|
||||
import MarkdownViewer from 'CommonUI/src/Components/Markdown.tsx/MarkdownViewer';
|
||||
import { UptimePrecision } from 'Model/Models/StatusPageResource';
|
||||
import UptimeUtil from 'CommonUI/src/Components/MonitorGraphs/UptimeUtil';
|
||||
|
||||
export interface ComponentProps {
|
||||
monitorName: string;
|
||||
description?: string | undefined;
|
||||
tooltip?: string | undefined;
|
||||
monitorStatus: MonitorStatus;
|
||||
currentStatus: MonitorStatus;
|
||||
monitorStatusTimeline: Array<MonitorStatusTimelne>;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
@@ -20,11 +22,65 @@ export interface ComponentProps {
|
||||
showCurrentStatus?: boolean | undefined;
|
||||
uptimeGraphHeight?: number | undefined;
|
||||
className?: string | undefined;
|
||||
showUptimePercent: boolean;
|
||||
uptimePrecision?: UptimePrecision | undefined;
|
||||
monitorStatuses: Array<MonitorStatus>;
|
||||
}
|
||||
|
||||
const MonitorOverview: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
const getCurrentStatus: Function = (): ReactElement => {
|
||||
// if the current status is operational then show uptime Percent.
|
||||
|
||||
let precision: UptimePrecision = UptimePrecision.ONE_DECIMAL;
|
||||
|
||||
if (props.uptimePrecision) {
|
||||
precision = props.uptimePrecision;
|
||||
}
|
||||
|
||||
if (
|
||||
props.currentStatus?.isOperationalState &&
|
||||
props.showUptimePercent
|
||||
) {
|
||||
const uptimePercent: number = UptimeUtil.calculateUptimePercentage(
|
||||
props.monitorStatusTimeline,
|
||||
props.monitorStatuses,
|
||||
precision
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="font-medium"
|
||||
style={{
|
||||
color:
|
||||
props.currentStatus?.color?.toString() ||
|
||||
Green.toString(),
|
||||
}}
|
||||
>
|
||||
{uptimePercent}% uptime
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.showCurrentStatus) {
|
||||
return (
|
||||
<div
|
||||
className=""
|
||||
style={{
|
||||
color:
|
||||
props.currentStatus?.color?.toString() ||
|
||||
Green.toString(),
|
||||
}}
|
||||
>
|
||||
{props.currentStatus?.name || 'Operational'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<div>
|
||||
@@ -48,18 +104,7 @@ const MonitorOverview: FunctionComponent<ComponentProps> = (
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
{props.showCurrentStatus && (
|
||||
<div
|
||||
className=""
|
||||
style={{
|
||||
color:
|
||||
props.monitorStatus?.color?.toString() ||
|
||||
Green.toString(),
|
||||
}}
|
||||
>
|
||||
{props.monitorStatus?.name || 'Operational'}
|
||||
</div>
|
||||
)}
|
||||
{getCurrentStatus()}
|
||||
</div>
|
||||
<div className="mb-2 text-sm">
|
||||
{props.description && (
|
||||
|
||||
18
StatusPage/src/Components/Section/Section.tsx
Normal file
18
StatusPage/src/Components/Section/Section.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
|
||||
export interface ComponentProps {
|
||||
title: string;
|
||||
}
|
||||
|
||||
const Section: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
return (
|
||||
<div className="section-label text-gray-500 mt-2 text-md font-medium flex">
|
||||
<div className="section-title">{props.title}</div>
|
||||
<div className="section-border flex-grow border-t border-gray-300 ml-3 mt-3"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Section;
|
||||
@@ -34,6 +34,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 Section from '../../Components/Section/Section';
|
||||
|
||||
const Overview: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps
|
||||
@@ -43,7 +44,10 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
const [announcements, setAnnouncements] = useState<
|
||||
Array<StatusPageAnnouncement>
|
||||
>([]);
|
||||
const [parsedData, setParsedData] =
|
||||
|
||||
const [activeAnnounementsParsedData, setActiveAnnouncementsParsedData] =
|
||||
useState<EventHistoryListComponentProps | null>(null);
|
||||
const [pastAnnouncementsParsedData, setPastAnnouncementsParsedData] =
|
||||
useState<EventHistoryListComponentProps | null>(null);
|
||||
|
||||
StatusPageUtil.checkIfUserHasLoggedIn();
|
||||
@@ -96,13 +100,9 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
// parse data;
|
||||
setParsedData(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const getAnouncementsParsedData: Function = (
|
||||
announcements: Array<StatusPageAnnouncement>
|
||||
): EventHistoryListComponentProps => {
|
||||
const eventHistoryListComponentProps: EventHistoryListComponentProps = {
|
||||
items: [],
|
||||
};
|
||||
@@ -135,8 +135,39 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
days[key] as EventHistoryDayListComponentProps
|
||||
);
|
||||
}
|
||||
return eventHistoryListComponentProps;
|
||||
};
|
||||
|
||||
setParsedData(eventHistoryListComponentProps);
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
// parse data;
|
||||
setActiveAnnouncementsParsedData(null);
|
||||
setPastAnnouncementsParsedData(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const activeAnnouncement: Array<StatusPageAnnouncement> =
|
||||
announcements.filter((announcement: StatusPageAnnouncement) => {
|
||||
return OneUptimeDate.isBefore(
|
||||
OneUptimeDate.getCurrentDate(),
|
||||
announcement.endAnnouncementAt!
|
||||
);
|
||||
});
|
||||
|
||||
const pastAnnouncement: Array<StatusPageAnnouncement> =
|
||||
announcements.filter((announcement: StatusPageAnnouncement) => {
|
||||
return OneUptimeDate.isAfter(
|
||||
OneUptimeDate.getCurrentDate(),
|
||||
announcement.endAnnouncementAt!
|
||||
);
|
||||
});
|
||||
|
||||
setActiveAnnouncementsParsedData(
|
||||
getAnouncementsParsedData(activeAnnouncement)
|
||||
);
|
||||
setPastAnnouncementsParsedData(
|
||||
getAnouncementsParsedData(pastAnnouncement)
|
||||
);
|
||||
}, [isLoading]);
|
||||
|
||||
if (isLoading) {
|
||||
@@ -147,10 +178,6 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
return <ErrorMessage error={error} />;
|
||||
}
|
||||
|
||||
if (!parsedData) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page
|
||||
title="Announcements"
|
||||
@@ -175,8 +202,27 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
},
|
||||
]}
|
||||
>
|
||||
{announcements && announcements.length > 0 ? (
|
||||
<EventHistoryList {...parsedData} />
|
||||
{activeAnnounementsParsedData?.items &&
|
||||
activeAnnounementsParsedData?.items.length > 0 ? (
|
||||
<div>
|
||||
<Section title="Active Announcements" />
|
||||
|
||||
<EventHistoryList
|
||||
items={activeAnnounementsParsedData?.items || []}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{pastAnnouncementsParsedData?.items &&
|
||||
pastAnnouncementsParsedData?.items.length > 0 ? (
|
||||
<div>
|
||||
<Section title="Past Announcements" />
|
||||
<EventHistoryList
|
||||
items={pastAnnouncementsParsedData?.items || []}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
@@ -36,6 +36,8 @@ 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 Section from '../../Components/Section/Section';
|
||||
import IncidentState from 'Model/Models/IncidentState';
|
||||
|
||||
const Overview: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps
|
||||
@@ -52,13 +54,21 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
const [incidentStateTimelines, setIncidentStateTimelines] = useState<
|
||||
Array<IncidentStateTimeline>
|
||||
>([]);
|
||||
const [parsedData, setParsedData] =
|
||||
|
||||
const [parsedActiveIncidentsData, setParsedActiveIncidentsData] =
|
||||
useState<EventHistoryListComponentProps | null>(null);
|
||||
|
||||
const [parsedResolvedIncidentsData, setParsedResolvedIncidentsData] =
|
||||
useState<EventHistoryListComponentProps | null>(null);
|
||||
|
||||
const [monitorsInGroup, setMonitorsInGroup] = useState<
|
||||
Dictionary<Array<ObjectID>>
|
||||
>({});
|
||||
|
||||
const [incidentStates, setIncidentStates] = useState<Array<IncidentState>>(
|
||||
[]
|
||||
);
|
||||
|
||||
StatusPageUtil.checkIfUserHasLoggedIn();
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
@@ -114,7 +124,14 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
(data['monitorsInGroup'] as JSONObject) || {}
|
||||
) as Dictionary<Array<ObjectID>>;
|
||||
|
||||
const incidentStates: Array<IncidentState> =
|
||||
JSONFunctions.fromJSONArray(
|
||||
(data['incidentStates'] as JSONArray) || [],
|
||||
IncidentState
|
||||
);
|
||||
|
||||
setMonitorsInGroup(monitorsInGroup);
|
||||
setIncidentStates(incidentStates);
|
||||
|
||||
// save data. set()
|
||||
setIncidentPublicNotes(incidentPublicNotes);
|
||||
@@ -133,13 +150,9 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
// parse data;
|
||||
setParsedData(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const getEventHistoryListComponentProps: Function = (
|
||||
incidents: Array<Incident>
|
||||
): EventHistoryListComponentProps => {
|
||||
const eventHistoryListComponentProps: EventHistoryListComponentProps = {
|
||||
items: [],
|
||||
};
|
||||
@@ -177,7 +190,46 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
);
|
||||
}
|
||||
|
||||
setParsedData(eventHistoryListComponentProps);
|
||||
return eventHistoryListComponentProps;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
// parse data;
|
||||
setParsedActiveIncidentsData(null);
|
||||
setParsedResolvedIncidentsData(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedIncidentStateOrder: number =
|
||||
incidentStates.find((state: IncidentState) => {
|
||||
return state.isResolvedState;
|
||||
})?.order || 0;
|
||||
|
||||
const activeIncidents: Array<Incident> = incidents.filter(
|
||||
(incident: Incident) => {
|
||||
return (
|
||||
(incident.currentIncidentState?.order || 0) <
|
||||
resolvedIncidentStateOrder
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const resolvedIncidents: Array<Incident> = incidents.filter(
|
||||
(incident: Incident) => {
|
||||
return !(
|
||||
(incident.currentIncidentState?.order || 0) <
|
||||
resolvedIncidentStateOrder
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
setParsedActiveIncidentsData(
|
||||
getEventHistoryListComponentProps(activeIncidents)
|
||||
);
|
||||
setParsedResolvedIncidentsData(
|
||||
getEventHistoryListComponentProps(resolvedIncidents)
|
||||
);
|
||||
}, [isLoading]);
|
||||
|
||||
if (isLoading) {
|
||||
@@ -188,10 +240,6 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
return <ErrorMessage error={error} />;
|
||||
}
|
||||
|
||||
if (!parsedData) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page
|
||||
title={'Incidents'}
|
||||
@@ -214,8 +262,28 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
},
|
||||
]}
|
||||
>
|
||||
{incidents && incidents.length > 0 ? (
|
||||
<EventHistoryList {...parsedData} />
|
||||
{parsedActiveIncidentsData?.items &&
|
||||
parsedActiveIncidentsData?.items.length > 0 ? (
|
||||
<div>
|
||||
<Section title="Active Incidents" />
|
||||
|
||||
<EventHistoryList
|
||||
items={parsedActiveIncidentsData?.items || []}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{parsedResolvedIncidentsData?.items &&
|
||||
parsedResolvedIncidentsData?.items.length > 0 ? (
|
||||
<div>
|
||||
<Section title="Resolved Incidents" />
|
||||
|
||||
<EventHistoryList
|
||||
items={parsedResolvedIncidentsData?.items || []}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
@@ -18,7 +18,9 @@ import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import LocalStorage from 'CommonUI/src/Utils/LocalStorage';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import StatusPageGroup from 'Model/Models/StatusPageGroup';
|
||||
import StatusPageResource from 'Model/Models/StatusPageResource';
|
||||
import StatusPageResource, {
|
||||
UptimePrecision,
|
||||
} from 'Model/Models/StatusPageResource';
|
||||
import MonitorStatus from 'Model/Models/MonitorStatus';
|
||||
import MonitorStatusTimeline from 'Model/Models/MonitorStatusTimeline';
|
||||
import Incident from 'Model/Models/Incident';
|
||||
@@ -49,6 +51,7 @@ import MarkdownViewer from 'CommonUI/src/Components/Markdown.tsx/MarkdownViewer'
|
||||
import StatusPageUtil from '../../Utils/StatusPage';
|
||||
import HTTPErrorResponse from 'Common/Types/API/HTTPErrorResponse';
|
||||
import { STATUS_PAGE_API_URL } from '../../Utils/Config';
|
||||
import Section from '../../Components/Section/Section';
|
||||
|
||||
const Overview: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps
|
||||
@@ -390,7 +393,15 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
}
|
||||
description={resource.displayDescription || ''}
|
||||
tooltip={resource.displayTooltip || ''}
|
||||
monitorStatus={currentStatus}
|
||||
currentStatus={currentStatus}
|
||||
monitorStatuses={monitorStatuses}
|
||||
showUptimePercent={Boolean(
|
||||
resource.showUptimePercent
|
||||
)}
|
||||
uptimePrecision={
|
||||
resource.uptimePercentPrecision ||
|
||||
UptimePrecision.ONE_DECIMAL
|
||||
}
|
||||
monitorStatusTimeline={[
|
||||
...monitorStatusTimelines,
|
||||
].filter((timeline: MonitorStatusTimeline) => {
|
||||
@@ -435,9 +446,17 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
resource.monitor?.name ||
|
||||
''
|
||||
}
|
||||
showUptimePercent={Boolean(
|
||||
resource.showUptimePercent
|
||||
)}
|
||||
uptimePrecision={
|
||||
resource.uptimePercentPrecision ||
|
||||
UptimePrecision.ONE_DECIMAL
|
||||
}
|
||||
description={resource.displayDescription || ''}
|
||||
tooltip={resource.displayTooltip || ''}
|
||||
monitorStatus={currentStatus}
|
||||
currentStatus={currentStatus}
|
||||
monitorStatuses={monitorStatuses}
|
||||
monitorStatusTimeline={[
|
||||
...monitorStatusTimelines,
|
||||
].filter((timeline: MonitorStatusTimeline) => {
|
||||
@@ -634,6 +653,11 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const activeIncidentsInIncidentGroup: Array<IncidentGroup> =
|
||||
getActiveIncidents();
|
||||
const activeScheduledMaintenanceEventsInScheduledMaintenanceGroup: Array<ScheduledMaintenanceGroup> =
|
||||
getOngoingScheduledEvents();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
{isLoading ? <PageLoader isVisible={true} /> : <></>}
|
||||
@@ -675,55 +699,6 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Load Active Incident */}
|
||||
<div id="incidents-list">
|
||||
{getActiveIncidents().map(
|
||||
(incidentGroup: IncidentGroup, i: number) => {
|
||||
return (
|
||||
<EventItem
|
||||
isDetailItem={false}
|
||||
key={i}
|
||||
{...getIncidentEventItem(
|
||||
incidentGroup.incident,
|
||||
incidentGroup.publicNotes,
|
||||
incidentGroup.incidentStateTimelines,
|
||||
incidentGroup.incidentResources,
|
||||
incidentGroup.monitorsInGroup,
|
||||
StatusPageUtil.isPreviewPage(),
|
||||
true
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Load Active ScheduledEvent */}
|
||||
<div id="scheduled-events-list">
|
||||
{getOngoingScheduledEvents().map(
|
||||
(
|
||||
scheduledEventGroup: ScheduledMaintenanceGroup,
|
||||
i: number
|
||||
) => {
|
||||
return (
|
||||
<EventItem
|
||||
key={i}
|
||||
isDetailItem={false}
|
||||
{...getScheduledEventEventItem(
|
||||
scheduledEventGroup.scheduledMaintenance,
|
||||
scheduledEventGroup.publicNotes,
|
||||
scheduledEventGroup.scheduledMaintenanceStateTimelines,
|
||||
scheduledEventGroup.scheduledEventResources,
|
||||
scheduledEventGroup.monitorsInGroup,
|
||||
StatusPageUtil.isPreviewPage(),
|
||||
true
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{currentStatus && statusPageResources.length > 0 && (
|
||||
<Alert
|
||||
@@ -746,7 +721,7 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
</div>
|
||||
|
||||
{statusPageResources.length > 0 && (
|
||||
<div className="bg-white pl-5 pr-5 mt-5 rounded-xl shadow space-y-5">
|
||||
<div className="bg-white pl-5 pr-5 mt-5 rounded-xl shadow space-y-5 mb-6">
|
||||
<AccordionGroup>
|
||||
{statusPageResources.filter(
|
||||
(resources: StatusPageResource) => {
|
||||
@@ -810,8 +785,66 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
</div>
|
||||
)}
|
||||
|
||||
{getActiveIncidents().length === 0 &&
|
||||
getOngoingScheduledEvents().length === 0 &&
|
||||
{/* Load Active Incident */}
|
||||
{activeIncidentsInIncidentGroup.length > 0 && (
|
||||
<div id="incidents-list mt-2">
|
||||
<Section title="Active Incidents" />
|
||||
{activeIncidentsInIncidentGroup.map(
|
||||
(incidentGroup: IncidentGroup, i: number) => {
|
||||
return (
|
||||
<EventItem
|
||||
isDetailItem={false}
|
||||
key={i}
|
||||
{...getIncidentEventItem(
|
||||
incidentGroup.incident,
|
||||
incidentGroup.publicNotes,
|
||||
incidentGroup.incidentStateTimelines,
|
||||
incidentGroup.incidentResources,
|
||||
incidentGroup.monitorsInGroup,
|
||||
StatusPageUtil.isPreviewPage(),
|
||||
true
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Load Active ScheduledEvent */}
|
||||
{activeScheduledMaintenanceEventsInScheduledMaintenanceGroup &&
|
||||
activeScheduledMaintenanceEventsInScheduledMaintenanceGroup.length >
|
||||
0 && (
|
||||
<div id="scheduled-events-list mt-2">
|
||||
<Section title="Scheduled Maintenance Events" />
|
||||
{activeScheduledMaintenanceEventsInScheduledMaintenanceGroup.map(
|
||||
(
|
||||
scheduledEventGroup: ScheduledMaintenanceGroup,
|
||||
i: number
|
||||
) => {
|
||||
return (
|
||||
<EventItem
|
||||
key={i}
|
||||
isDetailItem={false}
|
||||
{...getScheduledEventEventItem(
|
||||
scheduledEventGroup.scheduledMaintenance,
|
||||
scheduledEventGroup.publicNotes,
|
||||
scheduledEventGroup.scheduledMaintenanceStateTimelines,
|
||||
scheduledEventGroup.scheduledEventResources,
|
||||
scheduledEventGroup.monitorsInGroup,
|
||||
StatusPageUtil.isPreviewPage(),
|
||||
true
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeIncidentsInIncidentGroup.length === 0 &&
|
||||
activeScheduledMaintenanceEventsInScheduledMaintenanceGroup.length ===
|
||||
0 &&
|
||||
statusPageResources.length === 0 &&
|
||||
activeAnnouncements.length === 0 &&
|
||||
!isLoading &&
|
||||
|
||||
@@ -40,6 +40,7 @@ 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';
|
||||
import Label from 'Model/Models/Label';
|
||||
|
||||
export const getScheduledEventEventItem: Function = (
|
||||
scheduledMaintenance: ScheduledMaintenance,
|
||||
@@ -234,6 +235,13 @@ export const getScheduledEventEventItem: Function = (
|
||||
scheduledMaintenance.startsAt!
|
||||
)
|
||||
: '',
|
||||
labels:
|
||||
scheduledMaintenance.labels?.map((label: Label) => {
|
||||
return {
|
||||
name: label.name!,
|
||||
color: label.color!,
|
||||
};
|
||||
}) || [],
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ 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 ScheduledMaintenanceState from 'Model/Models/ScheduledMaintenanceState';
|
||||
import Section from '../../Components/Section/Section';
|
||||
|
||||
const Overview: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps
|
||||
@@ -52,7 +54,12 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
scheduledMaintenanceStateTimelines,
|
||||
setscheduledMaintenanceStateTimelines,
|
||||
] = useState<Array<ScheduledMaintenanceStateTimeline>>([]);
|
||||
const [parsedData, setParsedData] =
|
||||
|
||||
const [ongoingEventsParsedData, setOngoingEventsParsedData] =
|
||||
useState<EventHistoryListComponentProps | null>(null);
|
||||
const [scheduledEventsParsedData, setScheduledEventsParsedData] =
|
||||
useState<EventHistoryListComponentProps | null>(null);
|
||||
const [endedEventsParsedData, setEndedEventsParsedData] =
|
||||
useState<EventHistoryListComponentProps | null>(null);
|
||||
|
||||
const [statusPageResources, setStatusPageResources] = useState<
|
||||
@@ -63,8 +70,57 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
Dictionary<Array<ObjectID>>
|
||||
>({});
|
||||
|
||||
const [scheduledMaintenanceStates, setScheduledMaintenanceStates] =
|
||||
useState<Array<ScheduledMaintenanceState>>([]);
|
||||
|
||||
StatusPageUtil.checkIfUserHasLoggedIn();
|
||||
|
||||
const getEventHistoryListComponentProps: Function = (
|
||||
scheduledMaintenanceEvents: ScheduledMaintenance[],
|
||||
scheduledMaintenanceEventsPublicNotes: ScheduledMaintenancePublicNote[],
|
||||
scheduledMaintenanceStateTimelines: ScheduledMaintenanceStateTimeline[],
|
||||
statusPageResources: StatusPageResource[],
|
||||
monitorsInGroup: Dictionary<ObjectID[]>
|
||||
): EventHistoryListComponentProps => {
|
||||
const eventHistoryListComponentProps: EventHistoryListComponentProps = {
|
||||
items: [],
|
||||
};
|
||||
|
||||
const days: Dictionary<EventHistoryDayListComponentProps> = {};
|
||||
|
||||
for (const scheduledMaintenance of scheduledMaintenanceEvents) {
|
||||
const dayString: string = OneUptimeDate.getDateString(
|
||||
scheduledMaintenance.startsAt!
|
||||
);
|
||||
|
||||
if (!days[dayString]) {
|
||||
days[dayString] = {
|
||||
date: scheduledMaintenance.startsAt!,
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
|
||||
days[dayString]?.items.push(
|
||||
getScheduledEventEventItem(
|
||||
scheduledMaintenance,
|
||||
scheduledMaintenanceEventsPublicNotes,
|
||||
scheduledMaintenanceStateTimelines,
|
||||
statusPageResources,
|
||||
monitorsInGroup,
|
||||
Boolean(StatusPageUtil.isPreviewPage()),
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
for (const key in days) {
|
||||
eventHistoryListComponentProps.items.push(
|
||||
days[key] as EventHistoryDayListComponentProps
|
||||
);
|
||||
}
|
||||
return eventHistoryListComponentProps;
|
||||
};
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
try {
|
||||
if (!StatusPageUtil.getStatusPageId()) {
|
||||
@@ -122,6 +178,13 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
(data['monitorsInGroup'] as JSONObject) || {}
|
||||
) as Dictionary<Array<ObjectID>>;
|
||||
|
||||
const scheduledMaintenanceStates: Array<ScheduledMaintenanceState> =
|
||||
JSONFunctions.fromJSONArray(
|
||||
(data['scheduledMaintenanceStates'] as JSONArray) || [],
|
||||
ScheduledMaintenanceState
|
||||
);
|
||||
|
||||
setScheduledMaintenanceStates(scheduledMaintenanceStates);
|
||||
setStatusPageResources(statusPageResources);
|
||||
setMonitorsInGroup(monitorsInGroup);
|
||||
|
||||
@@ -148,48 +211,86 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
// parse data;
|
||||
setParsedData(null);
|
||||
setOngoingEventsParsedData(null);
|
||||
setScheduledEventsParsedData(null);
|
||||
setEndedEventsParsedData(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const eventHistoryListComponentProps: EventHistoryListComponentProps = {
|
||||
items: [],
|
||||
};
|
||||
const ongoingOrder: number =
|
||||
scheduledMaintenanceStates.find(
|
||||
(state: ScheduledMaintenanceState) => {
|
||||
return state.isOngoingState;
|
||||
}
|
||||
)?.order || 0;
|
||||
|
||||
const days: Dictionary<EventHistoryDayListComponentProps> = {};
|
||||
const endedEventOrder: number =
|
||||
scheduledMaintenanceStates.find(
|
||||
(state: ScheduledMaintenanceState) => {
|
||||
return state.isEndedState;
|
||||
}
|
||||
)?.order || 0;
|
||||
|
||||
for (const scheduledMaintenance of scheduledMaintenanceEvents) {
|
||||
const dayString: string = OneUptimeDate.getDateString(
|
||||
scheduledMaintenance.startsAt!
|
||||
// get ongoing events - anything after ongoing state but before ended state
|
||||
|
||||
const ongoingEvents: ScheduledMaintenance[] =
|
||||
scheduledMaintenanceEvents.filter((event: ScheduledMaintenance) => {
|
||||
return (
|
||||
event.currentScheduledMaintenanceState!.order! >=
|
||||
ongoingOrder &&
|
||||
event.currentScheduledMaintenanceState!.order! <
|
||||
endedEventOrder
|
||||
);
|
||||
});
|
||||
|
||||
// get scheduled events - anything before ongoing state
|
||||
|
||||
const scheduledEvents: ScheduledMaintenance[] =
|
||||
scheduledMaintenanceEvents.filter((event: ScheduledMaintenance) => {
|
||||
return (
|
||||
event.currentScheduledMaintenanceState!.order! <
|
||||
ongoingOrder
|
||||
);
|
||||
});
|
||||
|
||||
// get ended events - anythign equalTo or after ended state
|
||||
|
||||
const endedEvents: ScheduledMaintenance[] =
|
||||
scheduledMaintenanceEvents.filter((event: ScheduledMaintenance) => {
|
||||
return (
|
||||
event.currentScheduledMaintenanceState!.order! >=
|
||||
endedEventOrder
|
||||
);
|
||||
});
|
||||
|
||||
const endedEventProps: EventHistoryListComponentProps =
|
||||
getEventHistoryListComponentProps(
|
||||
endedEvents,
|
||||
scheduledMaintenanceEventsPublicNotes,
|
||||
scheduledMaintenanceStateTimelines,
|
||||
statusPageResources,
|
||||
monitorsInGroup
|
||||
);
|
||||
const scheduledEventProps: EventHistoryListComponentProps =
|
||||
getEventHistoryListComponentProps(
|
||||
scheduledEvents,
|
||||
scheduledMaintenanceEventsPublicNotes,
|
||||
scheduledMaintenanceStateTimelines,
|
||||
statusPageResources,
|
||||
monitorsInGroup
|
||||
);
|
||||
const ongoingEventProps: EventHistoryListComponentProps =
|
||||
getEventHistoryListComponentProps(
|
||||
ongoingEvents,
|
||||
scheduledMaintenanceEventsPublicNotes,
|
||||
scheduledMaintenanceStateTimelines,
|
||||
statusPageResources,
|
||||
monitorsInGroup
|
||||
);
|
||||
|
||||
if (!days[dayString]) {
|
||||
days[dayString] = {
|
||||
date: scheduledMaintenance.startsAt!,
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
|
||||
days[dayString]?.items.push(
|
||||
getScheduledEventEventItem(
|
||||
scheduledMaintenance,
|
||||
scheduledMaintenanceEventsPublicNotes,
|
||||
scheduledMaintenanceStateTimelines,
|
||||
statusPageResources,
|
||||
monitorsInGroup,
|
||||
Boolean(StatusPageUtil.isPreviewPage()),
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
for (const key in days) {
|
||||
eventHistoryListComponentProps.items.push(
|
||||
days[key] as EventHistoryDayListComponentProps
|
||||
);
|
||||
}
|
||||
|
||||
setParsedData(eventHistoryListComponentProps);
|
||||
setOngoingEventsParsedData(ongoingEventProps);
|
||||
setScheduledEventsParsedData(scheduledEventProps);
|
||||
setEndedEventsParsedData(endedEventProps);
|
||||
}, [isLoading]);
|
||||
|
||||
if (isLoading) {
|
||||
@@ -200,10 +301,6 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
return <ErrorMessage error={error} />;
|
||||
}
|
||||
|
||||
if (!parsedData) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page
|
||||
title="Scheduled Events"
|
||||
@@ -228,9 +325,41 @@ const Overview: FunctionComponent<PageComponentProps> = (
|
||||
},
|
||||
]}
|
||||
>
|
||||
{scheduledMaintenanceEvents &&
|
||||
scheduledMaintenanceEvents.length > 0 ? (
|
||||
<EventHistoryList {...parsedData} />
|
||||
{ongoingEventsParsedData?.items &&
|
||||
ongoingEventsParsedData?.items.length > 0 ? (
|
||||
<div>
|
||||
<Section title="Ongoing Events" />
|
||||
|
||||
<EventHistoryList
|
||||
items={ongoingEventsParsedData?.items || []}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{scheduledEventsParsedData?.items &&
|
||||
scheduledEventsParsedData?.items.length > 0 ? (
|
||||
<div>
|
||||
<Section title="Scheduled Events" />
|
||||
|
||||
<EventHistoryList
|
||||
items={scheduledEventsParsedData?.items || []}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{endedEventsParsedData?.items &&
|
||||
endedEventsParsedData?.items.length > 0 ? (
|
||||
<div>
|
||||
<Section title="Completed Events" />
|
||||
|
||||
<EventHistoryList
|
||||
items={endedEventsParsedData?.items || []}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
@@ -170,7 +170,7 @@ touch config.env
|
||||
source ~/.bashrc
|
||||
|
||||
#Run a scirpt to merge config.env.tpl to config.env
|
||||
ts-node-esm ./Scripts/Install/MergeEnvTemplate.ts
|
||||
node ./Scripts/Install/MergeEnvTemplate.js
|
||||
|
||||
|
||||
# Load env values from config.env
|
||||
|
||||
@@ -134,7 +134,7 @@ services:
|
||||
restart: always
|
||||
networks:
|
||||
- oneuptime
|
||||
command: redis-server --requirepass "${REDIS_PASSWORD}"
|
||||
command: redis-server --requirepass "${REDIS_PASSWORD}" --save "" --appendonly no
|
||||
environment:
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
|
||||
|
||||
@@ -34,10 +34,10 @@
|
||||
"prettier": "^2.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"config-to-dev": "ts-node-esm ./Scripts/Install/ReplaceValueInConfig.ts ENVIRONMENT development",
|
||||
"config-to-ci": "ts-node-esm ./Scripts/Install/ReplaceValueInConfig.ts ENVIRONMENT ci",
|
||||
"config-to-test": "ts-node-esm ./Scripts/Install/ReplaceValueInConfig.ts ENVIRONMENT test && ts-node-esm ./Scripts/Install/ReplaceValueInConfig.ts APP_TAG test",
|
||||
"config-to-production": "ts-node-esm ./Scripts/Install/ReplaceValueInConfig.ts ENVIRONMENT production",
|
||||
"config-to-dev": "node ./Scripts/Install/ReplaceValueInConfig.js ENVIRONMENT development",
|
||||
"config-to-ci": "node ./Scripts/Install/ReplaceValueInConfig.js ENVIRONMENT ci",
|
||||
"config-to-test": "node ./Scripts/Install/ReplaceValueInConfig.js ENVIRONMENT test && node ./Scripts/Install/ReplaceValueInConfig.js APP_TAG test",
|
||||
"config-to-production": "node ./Scripts/Install/ReplaceValueInConfig.js ENVIRONMENT production",
|
||||
"prerun": "bash configure.sh",
|
||||
"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js",
|
||||
"uninstall": "bash uninstall.sh",
|
||||
|
||||
Reference in New Issue
Block a user