Compare commits

...

30 Commits

Author SHA1 Message Date
Simon Larsen
a299fca2ba Merge pull request #874 from dulajkavinda/tests/added-tests-crontab-jsontocsv
Added tests for JsonToCsv and CronTab util functions
2023-11-07 16:58:30 +00:00
Simon Larsen
396066bf0d fix util 2023-11-07 16:57:00 +00:00
Simon Larsen
9debdfafba Merge branch 'master' of github.com-simon:OneUptime/oneuptime 2023-11-07 16:39:40 +00:00
Simon Larsen
49c4dff44b add uptime percent to monitor groups. 2023-11-07 16:39:38 +00:00
Simon Larsen
27a8434181 add uptime to monitors page 2023-11-07 16:06:27 +00:00
Simon Larsen
c8305ef7c0 uptime percent 2023-11-07 15:23:57 +00:00
Simon Larsen
2f332a64f3 add uptime percent 2023-11-07 15:18:29 +00:00
Dulaj Kavinda
fb5291e7c1 tests: added testts for CronTab 2023-11-07 19:36:36 +05:30
Dulaj Kavinda
4034cf6ed8 tests: added tests for JsonToCsv 2023-11-07 18:17:44 +05:30
Simon Larsen
8b37587800 Merge pull request #873 from dulajkavinda/tests/added-tests-tabs-and-modal
Added tests for Tabs and Modal components
2023-11-07 09:05:45 +00:00
Dulaj Kavinda
03eb1dd1f2 tests: added test cases for CommonUI/Modal 2023-11-07 12:40:15 +05:30
Dulaj Kavinda
ee91526239 tests: added test cases for CommonUI/Tabs 2023-11-07 12:39:29 +05:30
Simon Larsen
970c537b96 add uptime precision 2023-11-06 20:17:37 +00:00
Simon Larsen
35718ee7b5 fix blog icon 2023-11-05 13:38:14 +00:00
Simon Larsen
310cd90714 add blog links 2023-11-05 13:36:38 +00:00
Simon Larsen
513fa74c59 add unpaid banner 2023-11-05 12:57:39 +00:00
Simon Larsen
4ba6f714af add project id 2023-11-05 12:15:42 +00:00
Simon Larsen
7ab3ba7201 add logs in call service 2023-11-03 18:11:58 +00:00
Simon Larsen
e5aa9c9496 remove persistence from redis 2023-11-03 17:42:37 +00:00
Simon Larsen
455ca7b22d add appendonly no to redis 2023-11-03 17:35:13 +00:00
Simon Larsen
0b71c8b769 add log statements 2023-11-03 16:39:05 +00:00
Simon Larsen
19b0a1f2a8 require fs 2023-11-03 16:00:35 +00:00
Simon Larsen
4f93dd0f04 move files in install to js 2023-11-03 15:57:05 +00:00
Simon Larsen
60d0f188ad add categorization to overview 2023-11-02 19:00:30 +00:00
Simon Larsen
bd587b210e add sections to announcments 2023-11-02 18:50:18 +00:00
Simon Larsen
80dd33cd7f add categories to pages 2023-11-02 16:39:40 +00:00
Simon Larsen
d675eca50c add categories to incidents 2023-11-02 15:23:43 +00:00
Simon Larsen
76712e8f89 add labels to events 2023-11-02 14:05:40 +00:00
Simon Larsen
fe68b009eb Merge branch 'master' of github.com-simon:OneUptime/oneuptime 2023-11-02 13:40:54 +00:00
Simon Larsen
9a6960e154 Merge pull request #870 from OneUptime/monitor-groups
Monitor groups
2023-11-02 11:45:28 +00:00
44 changed files with 2063 additions and 467 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -120,6 +120,7 @@ export class Service extends DatabaseService<Model> {
phone: true,
verificationCode: true,
isVerified: true,
projectId: true,
},
});

View File

@@ -120,6 +120,7 @@ export class Service extends DatabaseService<Model> {
phone: true,
verificationCode: true,
isVerified: true,
projectId: true,
},
});

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

View 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","{}"`);
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,2 +0,0 @@
maxmemory-policy=noeviction
appendonly yes

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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