mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 08:42:13 +02:00
Compare commits
156 Commits
logging
...
monitor-gr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c1bd10873 | ||
|
|
05a288c761 | ||
|
|
a9f503da9d | ||
|
|
49d3655502 | ||
|
|
1cdcc639b4 | ||
|
|
7568c70b50 | ||
|
|
6259f81a91 | ||
|
|
f40c1daeb8 | ||
|
|
bb73ed14cd | ||
|
|
4b71a81f7c | ||
|
|
d6788c138b | ||
|
|
28f4a1f473 | ||
|
|
ccb4781c06 | ||
|
|
2e27347225 | ||
|
|
e9015f0eff | ||
|
|
6cf8560151 | ||
|
|
7d2e91d867 | ||
|
|
46e0210dcc | ||
|
|
02fc5502eb | ||
|
|
ce3131edaf | ||
|
|
ca4716133a | ||
|
|
9cb254f9d1 | ||
|
|
d51fbdf5f7 | ||
|
|
57b7b5b39e | ||
|
|
2e46ebd0e8 | ||
|
|
4ffe215665 | ||
|
|
e680346f1f | ||
|
|
4faa8d32f6 | ||
|
|
ab07ff0104 | ||
|
|
03dd6fef04 | ||
|
|
31c0ff7dea | ||
|
|
dca1d2c370 | ||
|
|
fc218a970a | ||
|
|
17509225ee | ||
|
|
447bac1d67 | ||
|
|
67b3b224a7 | ||
|
|
48fbf50973 | ||
|
|
a0acb24651 | ||
|
|
c958893d67 | ||
|
|
9e2bd15cf4 | ||
|
|
17e9ad4fcd | ||
|
|
4d5a49f11e | ||
|
|
2d9b9950dd | ||
|
|
c3c0fbc853 | ||
|
|
f970b02e9e | ||
|
|
987394be41 | ||
|
|
34b3dff108 | ||
|
|
b603241d57 | ||
|
|
8df01fc098 | ||
|
|
268305e6cd | ||
|
|
bbb53b3321 | ||
|
|
c79fa88ad1 | ||
|
|
35c5e57752 | ||
|
|
254a9de101 | ||
|
|
c844bf8e43 | ||
|
|
c0288716da | ||
|
|
51e7fa6c9a | ||
|
|
d9eb60017a | ||
|
|
e9d7b36198 | ||
|
|
7308945061 | ||
|
|
3f8e5e4e0a | ||
|
|
3f7d186db0 | ||
|
|
8cb91d94eb | ||
|
|
3337ad2a45 | ||
|
|
438fbf4368 | ||
|
|
ffca1acc9a | ||
|
|
846d5ce104 | ||
|
|
43a075436a | ||
|
|
8fe35d9a29 | ||
|
|
849eeac23a | ||
|
|
01a4cac559 | ||
|
|
b4cd4d2c02 | ||
|
|
329484fb87 | ||
|
|
ee54a324d7 | ||
|
|
ba2feffbee | ||
|
|
4b0b91396b | ||
|
|
f2c6321216 | ||
|
|
67447c0bd7 | ||
|
|
323646ebcd | ||
|
|
81e4b4435c | ||
|
|
842b0664c7 | ||
|
|
0bdab474de | ||
|
|
ef1b22e62b | ||
|
|
3d229a0030 | ||
|
|
e34599d18a | ||
|
|
aa7594f2a8 | ||
|
|
0626669b02 | ||
|
|
35b949e448 | ||
|
|
2bb4086fd1 | ||
|
|
03f9c36f06 | ||
|
|
9fe998a43d | ||
|
|
3841b655e5 | ||
|
|
5ec8ee6dcb | ||
|
|
a1c6121bee | ||
|
|
51c76aa1af | ||
|
|
40ee5d775b | ||
|
|
88f0e2af51 | ||
|
|
e702a0b0d2 | ||
|
|
cfc2f99248 | ||
|
|
f23bb3af41 | ||
|
|
2cdf1236be | ||
|
|
ed5a144735 | ||
|
|
e687a439e6 | ||
|
|
cfa20e2be6 | ||
|
|
9205764deb | ||
|
|
32275837ac | ||
|
|
34568a39f5 | ||
|
|
b7b41dfebb | ||
|
|
9b40011196 | ||
|
|
d644287a0c | ||
|
|
ea7dc0b918 | ||
|
|
c34639a3bb | ||
|
|
41ba37be80 | ||
|
|
954d5be113 | ||
|
|
21a857d912 | ||
|
|
cb0f7bbad5 | ||
|
|
c3c94f3634 | ||
|
|
955141d42e | ||
|
|
352c9ffb8e | ||
|
|
d543757a7d | ||
|
|
b3cfdbf45a | ||
|
|
c629921d01 | ||
|
|
008e0c50b1 | ||
|
|
fcf916bdfe | ||
|
|
9850bcf0e7 | ||
|
|
e1efeec9ec | ||
|
|
7e34393fc6 | ||
|
|
262fffd9ff | ||
|
|
35db6e95ad | ||
|
|
17208b5e26 | ||
|
|
896dce3430 | ||
|
|
d844fa9df2 | ||
|
|
48542c4323 | ||
|
|
f57047c778 | ||
|
|
e471787462 | ||
|
|
dc4721f878 | ||
|
|
4bd4dbf3c1 | ||
|
|
6c0c79dd25 | ||
|
|
a9548858b0 | ||
|
|
6804e94850 | ||
|
|
63736aed6c | ||
|
|
c848032fdc | ||
|
|
22c2231e22 | ||
|
|
7a063d741c | ||
|
|
8a9cc10ff0 | ||
|
|
2e43fa0c02 | ||
|
|
f51a1828ab | ||
|
|
805139055a | ||
|
|
42c85b16e7 | ||
|
|
a59742cddb | ||
|
|
ba426b5580 | ||
|
|
1945bbfd45 | ||
|
|
58debb9959 | ||
|
|
6485f474b2 | ||
|
|
301d7f124c | ||
|
|
985217d2bf |
@@ -54,4 +54,7 @@ tests/coverage
|
||||
|
||||
settings.json
|
||||
|
||||
GoSDK/tester/
|
||||
GoSDK/tester/
|
||||
|
||||
Llama/Models/*
|
||||
|
||||
|
||||
@@ -179,11 +179,13 @@
|
||||
"ignoreReadBeforeAssign": false
|
||||
}
|
||||
],
|
||||
"no-var": "error"
|
||||
"no-var": "error",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"no-unneeded-ternary": "error"
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "18.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Probe Api Test
|
||||
name: Ingestor Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -92,3 +92,8 @@ Haraka/dkim/keys/public_base64.txt
|
||||
.eslintcache
|
||||
|
||||
HelmChart/Values/*.values.yaml
|
||||
|
||||
Llama/Models/tokenizer*
|
||||
Llama/Models/llama*
|
||||
|
||||
Llama/__pycache__/*
|
||||
42
Clickhouse/Docs/ClickhouseOps.md
Normal file
42
Clickhouse/Docs/ClickhouseOps.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Some basic commands for Clickhouse
|
||||
|
||||
## Show tables in the database
|
||||
|
||||
```sql
|
||||
show tables from oneuptime
|
||||
```
|
||||
|
||||
## Show table structure
|
||||
|
||||
```sql
|
||||
DESCRIBE TABLE oneuptime.Span
|
||||
```
|
||||
|
||||
## Show table data
|
||||
|
||||
```sql
|
||||
select * from table_name
|
||||
```
|
||||
|
||||
## Delete table data
|
||||
|
||||
```sql
|
||||
truncate table_name
|
||||
```
|
||||
|
||||
## Delete table
|
||||
|
||||
```sql
|
||||
drop table oneuptime.table_name
|
||||
```
|
||||
|
||||
## Insert for nested data
|
||||
|
||||
```sql
|
||||
INSERT INTO opentelemetry_spans (trace_id, span_id, attributes.key, attributes.value) VALUES
|
||||
('trace1', 'span1', ['key1', 'key2'], ['value1', 'value2']),
|
||||
('trace2', 'span2', ['keyA', 'keyB'], ['valueA', 'valueB']);
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import TableColumnType from '../Types/BaseDatabase/TableColumnType';
|
||||
import TableColumnType from '../Types/AnalyticsDatabase/TableColumnType';
|
||||
import AnalyticsTableColumn from '../Types/AnalyticsDatabase/TableColumn';
|
||||
import BadDataException from '../Types/Exception/BadDataException';
|
||||
import AnalyticsTableEngine from '../Types/AnalyticsDatabase/AnalyticsTableEngine';
|
||||
import { JSONObject, JSONValue } from '../Types/JSON';
|
||||
import ColumnBillingAccessControl from '../Types/BaseDatabase/ColumnBillingAccessControl';
|
||||
import TableBillingAccessControl from '../Types/BaseDatabase/TableBillingAccessControl';
|
||||
import { TableAccessControl } from '../Types/BaseDatabase/AccessControl';
|
||||
import EnableWorkflowOn from '../Types/BaseDatabase/EnableWorkflowOn';
|
||||
import ObjectID from '../Types/ObjectID';
|
||||
import OneUptimeDate from '../Types/Date';
|
||||
|
||||
export default class AnalyticsDataModel {
|
||||
private data: JSONObject = {};
|
||||
import CommonModel from './CommonModel';
|
||||
|
||||
export default class AnalyticsDataModel extends CommonModel {
|
||||
public constructor(data: {
|
||||
tableName: string;
|
||||
singularName: string;
|
||||
@@ -25,6 +22,9 @@ export default class AnalyticsDataModel {
|
||||
primaryKeys: Array<string>; // this should be the subset of tableColumns
|
||||
enableWorkflowOn?: EnableWorkflowOn | undefined;
|
||||
}) {
|
||||
super({
|
||||
tableColumns: data.tableColumns,
|
||||
});
|
||||
const columns: Array<AnalyticsTableColumn> = [...data.tableColumns];
|
||||
|
||||
this.tableName = data.tableName;
|
||||
@@ -70,15 +70,25 @@ export default class AnalyticsDataModel {
|
||||
// check if primary keys are subset of tableColumns
|
||||
|
||||
data.primaryKeys.forEach((primaryKey: string) => {
|
||||
if (
|
||||
!columns.find((column: AnalyticsTableColumn) => {
|
||||
const column: AnalyticsTableColumn | undefined = columns.find(
|
||||
(column: AnalyticsTableColumn) => {
|
||||
return column.key === primaryKey;
|
||||
})
|
||||
) {
|
||||
}
|
||||
);
|
||||
|
||||
if (!column) {
|
||||
throw new BadDataException(
|
||||
'Primary key ' + primaryKey + ' is not part of tableColumns'
|
||||
);
|
||||
}
|
||||
|
||||
if (!column.required) {
|
||||
throw new BadDataException(
|
||||
'Primary key ' +
|
||||
primaryKey +
|
||||
' is not required. Primary keys must be required.'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.primaryKeys = data.primaryKeys;
|
||||
@@ -90,6 +100,13 @@ export default class AnalyticsDataModel {
|
||||
data.allowAccessIfSubscriptionIsUnpaid || false;
|
||||
this.accessControl = data.accessControl;
|
||||
this.enableWorkflowOn = data.enableWorkflowOn;
|
||||
|
||||
// initialize Arrays.
|
||||
for (const column of this.tableColumns) {
|
||||
if (column.type === TableColumnType.NestedModel) {
|
||||
this.setColumnValue(column.key, []);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _enableWorkflowOn: EnableWorkflowOn | undefined;
|
||||
@@ -108,14 +125,6 @@ export default class AnalyticsDataModel {
|
||||
this._accessControl = v;
|
||||
}
|
||||
|
||||
private _tableColumns: Array<AnalyticsTableColumn> = [];
|
||||
public get tableColumns(): Array<AnalyticsTableColumn> {
|
||||
return this._tableColumns;
|
||||
}
|
||||
public set tableColumns(v: Array<AnalyticsTableColumn>) {
|
||||
this._tableColumns = v;
|
||||
}
|
||||
|
||||
private _tableName: string = '';
|
||||
public get tableName(): string {
|
||||
return this._tableName;
|
||||
@@ -176,65 +185,6 @@ export default class AnalyticsDataModel {
|
||||
this._allowAccessIfSubscriptionIsUnpaid = v;
|
||||
}
|
||||
|
||||
public setColumnValue(columnName: string, value: JSONValue): void {
|
||||
const column: AnalyticsTableColumn | null =
|
||||
this.getTableColumn(columnName);
|
||||
|
||||
if (column) {
|
||||
if (
|
||||
column.type === TableColumnType.ObjectID &&
|
||||
typeof value === 'string'
|
||||
) {
|
||||
value = new ObjectID(value);
|
||||
}
|
||||
|
||||
if (
|
||||
column.type === TableColumnType.Date &&
|
||||
typeof value === 'string'
|
||||
) {
|
||||
value = OneUptimeDate.fromString(value);
|
||||
}
|
||||
|
||||
if (
|
||||
column.type === TableColumnType.JSON &&
|
||||
typeof value === 'string'
|
||||
) {
|
||||
value = JSON.parse(value);
|
||||
}
|
||||
|
||||
return (this.data[columnName] = value as any);
|
||||
}
|
||||
throw new BadDataException('Column ' + columnName + ' does not exist');
|
||||
}
|
||||
|
||||
public getColumnValue<T extends JSONValue>(
|
||||
columnName: string
|
||||
): T | undefined {
|
||||
if (this.getTableColumn(columnName)) {
|
||||
return this.data[columnName] as T;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public getTableColumn(name: string): AnalyticsTableColumn | null {
|
||||
const column: AnalyticsTableColumn | undefined = this.tableColumns.find(
|
||||
(column: AnalyticsTableColumn) => {
|
||||
return column.key === name;
|
||||
}
|
||||
);
|
||||
|
||||
if (!column) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return column;
|
||||
}
|
||||
|
||||
public getTableColumns(): Array<AnalyticsTableColumn> {
|
||||
return this.tableColumns;
|
||||
}
|
||||
|
||||
public getTenantColumn(): AnalyticsTableColumn | null {
|
||||
const column: AnalyticsTableColumn | undefined = this.tableColumns.find(
|
||||
(column: AnalyticsTableColumn) => {
|
||||
@@ -308,49 +258,4 @@ export default class AnalyticsDataModel {
|
||||
public set updatedAt(v: Date | undefined) {
|
||||
this.setColumnValue('updatedAt', v);
|
||||
}
|
||||
|
||||
public fromJSON(json: JSONObject): AnalyticsDataModel {
|
||||
for (const key in json) {
|
||||
this.setColumnValue(key, json[key]);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): JSONObject {
|
||||
const json: JSONObject = {};
|
||||
|
||||
this.tableColumns.forEach((column: AnalyticsTableColumn) => {
|
||||
json[column.key] = this.getColumnValue(column.key);
|
||||
});
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
public static fromJSONArray<TBaseModel extends AnalyticsDataModel>(
|
||||
modelType: { new (): AnalyticsDataModel },
|
||||
jsonArray: Array<JSONObject>
|
||||
): Array<TBaseModel> {
|
||||
const models: Array<AnalyticsDataModel> = [];
|
||||
|
||||
jsonArray.forEach((json: JSONObject) => {
|
||||
const model: AnalyticsDataModel = new modelType();
|
||||
model.fromJSON(json);
|
||||
models.push(model);
|
||||
});
|
||||
|
||||
return models as Array<TBaseModel>;
|
||||
}
|
||||
|
||||
public static toJSONArray(
|
||||
models: Array<AnalyticsDataModel>
|
||||
): Array<JSONObject> {
|
||||
const json: Array<JSONObject> = [];
|
||||
|
||||
models.forEach((model: AnalyticsDataModel) => {
|
||||
json.push(model.toJSON());
|
||||
});
|
||||
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
178
Common/AnalyticsModels/CommonModel.ts
Normal file
178
Common/AnalyticsModels/CommonModel.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
// This model will be extended by BaseModel and Nested Mdoel
|
||||
|
||||
import AnalyticsTableColumn from '../Types/AnalyticsDatabase/TableColumn';
|
||||
import TableColumnType from '../Types/AnalyticsDatabase/TableColumnType';
|
||||
import OneUptimeDate from '../Types/Date';
|
||||
import BadDataException from '../Types/Exception/BadDataException';
|
||||
import { JSONObject, JSONValue } from '../Types/JSON';
|
||||
import ObjectID from '../Types/ObjectID';
|
||||
|
||||
export type RecordValue =
|
||||
| ObjectID
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Date
|
||||
| Array<number>
|
||||
| Array<string>
|
||||
| Array<CommonModel>;
|
||||
|
||||
export type Record = Array<RecordValue | Record>;
|
||||
|
||||
export default class CommonModel {
|
||||
protected data: JSONObject = {};
|
||||
|
||||
private _tableColumns: Array<AnalyticsTableColumn> = [];
|
||||
public get tableColumns(): Array<AnalyticsTableColumn> {
|
||||
return this._tableColumns;
|
||||
}
|
||||
public set tableColumns(v: Array<AnalyticsTableColumn>) {
|
||||
this._tableColumns = v;
|
||||
}
|
||||
|
||||
public setColumnValue(
|
||||
columnName: string,
|
||||
value: JSONValue | Array<CommonModel>
|
||||
): void {
|
||||
const column: AnalyticsTableColumn | null =
|
||||
this.getTableColumn(columnName);
|
||||
|
||||
if (column) {
|
||||
if (
|
||||
column.type === TableColumnType.ObjectID &&
|
||||
typeof value === 'string'
|
||||
) {
|
||||
value = new ObjectID(value);
|
||||
}
|
||||
|
||||
if (
|
||||
column.type === TableColumnType.Date &&
|
||||
typeof value === 'string'
|
||||
) {
|
||||
value = OneUptimeDate.fromString(value);
|
||||
}
|
||||
|
||||
if (
|
||||
column.type === TableColumnType.JSON &&
|
||||
typeof value === 'string'
|
||||
) {
|
||||
value = JSON.parse(value);
|
||||
}
|
||||
|
||||
if (
|
||||
column.type === TableColumnType.Number &&
|
||||
typeof value === 'string'
|
||||
) {
|
||||
value = parseInt(value);
|
||||
}
|
||||
|
||||
// decimal
|
||||
if (
|
||||
column.type === TableColumnType.Decimal &&
|
||||
typeof value === 'string'
|
||||
) {
|
||||
value = parseFloat(value);
|
||||
}
|
||||
|
||||
return (this.data[columnName] = value as any);
|
||||
}
|
||||
throw new BadDataException('Column ' + columnName + ' does not exist');
|
||||
}
|
||||
|
||||
public constructor(data: { tableColumns: Array<AnalyticsTableColumn> }) {
|
||||
this.tableColumns = data.tableColumns;
|
||||
}
|
||||
|
||||
public getColumnValue<T extends RecordValue>(
|
||||
columnName: string
|
||||
): T | undefined {
|
||||
if (this.getTableColumn(columnName)) {
|
||||
return this.data[columnName] as T;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public getTableColumn(name: string): AnalyticsTableColumn | null {
|
||||
const column: AnalyticsTableColumn | undefined = this.tableColumns.find(
|
||||
(column: AnalyticsTableColumn) => {
|
||||
return column.key === name;
|
||||
}
|
||||
);
|
||||
|
||||
if (!column) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return column;
|
||||
}
|
||||
|
||||
public getTableColumns(): Array<AnalyticsTableColumn> {
|
||||
return this.tableColumns;
|
||||
}
|
||||
|
||||
public fromJSON(json: JSONObject): CommonModel {
|
||||
for (const key in json) {
|
||||
this.setColumnValue(key, json[key]);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): JSONObject {
|
||||
const json: JSONObject = {};
|
||||
|
||||
this.tableColumns.forEach((column: AnalyticsTableColumn) => {
|
||||
const recordValue: RecordValue | undefined = this.getColumnValue(
|
||||
column.key
|
||||
);
|
||||
|
||||
if (recordValue instanceof CommonModel) {
|
||||
json[column.key] = recordValue.toJSON();
|
||||
return;
|
||||
}
|
||||
|
||||
if (recordValue instanceof Array) {
|
||||
if (
|
||||
recordValue.length > 0 &&
|
||||
recordValue[0] instanceof CommonModel
|
||||
) {
|
||||
json[column.key] = CommonModel.toJSONArray(
|
||||
recordValue as Array<CommonModel>
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
json[column.key] = recordValue;
|
||||
});
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
public static fromJSONArray<TBaseModel extends CommonModel>(
|
||||
modelType: { new (): CommonModel },
|
||||
jsonArray: Array<JSONObject>
|
||||
): Array<TBaseModel> {
|
||||
const models: Array<CommonModel> = [];
|
||||
|
||||
jsonArray.forEach((json: JSONObject) => {
|
||||
const model: CommonModel = new modelType();
|
||||
model.fromJSON(json);
|
||||
models.push(model);
|
||||
});
|
||||
|
||||
return models as Array<TBaseModel>;
|
||||
}
|
||||
|
||||
public static toJSONArray(models: Array<CommonModel>): Array<JSONObject> {
|
||||
const json: Array<JSONObject> = [];
|
||||
|
||||
models.forEach((model: CommonModel) => {
|
||||
json.push(model.toJSON());
|
||||
});
|
||||
|
||||
return json;
|
||||
}
|
||||
}
|
||||
8
Common/AnalyticsModels/NestedModel.ts
Normal file
8
Common/AnalyticsModels/NestedModel.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import AnalyticsTableColumn from '../Types/AnalyticsDatabase/TableColumn';
|
||||
import CommonModel from './CommonModel';
|
||||
|
||||
export default class NestedModel extends CommonModel {
|
||||
public constructor(data: { tableColumns: Array<AnalyticsTableColumn> }) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import Email from '../Types/Email';
|
||||
import Phone from '../Types/Phone';
|
||||
import PositiveNumber from '../Types/PositiveNumber';
|
||||
import Route from '../Types/API/Route';
|
||||
import TableColumnType from '../Types/BaseDatabase/TableColumnType';
|
||||
import TableColumnType from '../Types/Database/TableColumnType';
|
||||
import Permission, {
|
||||
instanceOfUserTenantAccessPermission,
|
||||
PermissionHelper,
|
||||
|
||||
@@ -4,7 +4,7 @@ import ColumnLength from '../Types/Database/ColumnLength';
|
||||
import ColumnType from '../Types/Database/ColumnType';
|
||||
import SlugifyColumn from '../Types/Database/SlugifyColumn';
|
||||
import TableColumn from '../Types/Database/TableColumn';
|
||||
import TableColumnType from '../Types/BaseDatabase/TableColumnType';
|
||||
import TableColumnType from '../Types/Database/TableColumnType';
|
||||
import MimeType from '../Types/File/MimeType';
|
||||
import ObjectID from '../Types/ObjectID';
|
||||
import Permission from '../Types/Permission';
|
||||
|
||||
61
Common/Tests/Types/API/HTTPErrorResponse.test.ts
Normal file
61
Common/Tests/Types/API/HTTPErrorResponse.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import HTTPErrorResponse from '../../../Types/API/HTTPErrorResponse';
|
||||
|
||||
describe('HTTPErrorResponse', () => {
|
||||
it('should return an empty string when data is null', () => {
|
||||
const httpResponse: HTTPErrorResponse = new HTTPErrorResponse(
|
||||
404,
|
||||
{ data: null },
|
||||
{}
|
||||
);
|
||||
expect(httpResponse.message).toBe('');
|
||||
});
|
||||
|
||||
it('should return the message from the "data" property if present', () => {
|
||||
const httpResponse: HTTPErrorResponse = new HTTPErrorResponse(
|
||||
200,
|
||||
{ data: 'Data message' },
|
||||
{}
|
||||
);
|
||||
expect(httpResponse.message).toBe('Data message');
|
||||
});
|
||||
|
||||
it('should return the message from the "message" property if present', () => {
|
||||
const httpResponse: HTTPErrorResponse = new HTTPErrorResponse(
|
||||
200,
|
||||
{ message: 'Message message' },
|
||||
{}
|
||||
);
|
||||
expect(httpResponse.message).toBe('Message message');
|
||||
});
|
||||
|
||||
it('should return the message from the "error" property if no other message properties are present', () => {
|
||||
const httpResponse: HTTPErrorResponse = new HTTPErrorResponse(
|
||||
500,
|
||||
{ error: 'Error message' },
|
||||
{}
|
||||
);
|
||||
expect(httpResponse.message).toBe('Error message');
|
||||
});
|
||||
|
||||
it('should return an empty string when no relevant message properties are present', () => {
|
||||
const httpResponse: HTTPErrorResponse = new HTTPErrorResponse(
|
||||
204,
|
||||
{ otherProperty: 'Other message' },
|
||||
{}
|
||||
);
|
||||
expect(httpResponse.message).toBe('');
|
||||
});
|
||||
|
||||
it('should prioritize "data" > "message" > "error" when multiple message properties are present', () => {
|
||||
const httpResponse: HTTPErrorResponse = new HTTPErrorResponse(
|
||||
201,
|
||||
{
|
||||
data: 'Data message',
|
||||
message: 'Message message',
|
||||
error: 'Error message',
|
||||
},
|
||||
{}
|
||||
);
|
||||
expect(httpResponse.message).toBe('Data message');
|
||||
});
|
||||
});
|
||||
82
Common/Tests/Types/Database/Date.test.ts
Normal file
82
Common/Tests/Types/Database/Date.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import DatabaseDate from '../../../Types/Database/Date';
|
||||
import moment from 'moment';
|
||||
import InBetween from '../../../Types/Database/InBetween';
|
||||
import { JSONObject } from '../../../Types/JSON';
|
||||
|
||||
describe('DatabaseDate', () => {
|
||||
describe('asDateStartOfTheDayEndOfTheDayForDatabaseQuery', () => {
|
||||
it('should return InBetween object for a valid Date input', () => {
|
||||
const inputDate: Date = new Date('2023-10-24T12:00:00Z');
|
||||
const result: JSONObject =
|
||||
DatabaseDate.asDateStartOfTheDayEndOfTheDayForDatabaseQuery(
|
||||
inputDate
|
||||
).toJSON();
|
||||
|
||||
const expectedStart: string = moment(inputDate)
|
||||
.startOf('day')
|
||||
.format('YYYY-MM-DD HH:mm:ss');
|
||||
const expectedEnd: string = moment(inputDate)
|
||||
.endOf('day')
|
||||
.format('YYYY-MM-DD HH:mm:ss');
|
||||
expect(result).toEqual({
|
||||
startValue: expectedStart,
|
||||
endValue: expectedEnd,
|
||||
_type: 'InBetween',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return InBetween object for a valid Date string input', () => {
|
||||
const inputDate: string = '2023-10-24T12:00:00Z';
|
||||
const result: JSONObject =
|
||||
DatabaseDate.asDateStartOfTheDayEndOfTheDayForDatabaseQuery(
|
||||
inputDate
|
||||
).toJSON();
|
||||
const expectedStart: string = moment(inputDate)
|
||||
.startOf('day')
|
||||
.format('YYYY-MM-DD HH:mm:ss');
|
||||
const expectedEnd: string = moment(inputDate)
|
||||
.endOf('day')
|
||||
.format('YYYY-MM-DD HH:mm:ss');
|
||||
expect(result).toEqual({
|
||||
startValue: expectedStart,
|
||||
endValue: expectedEnd,
|
||||
_type: 'InBetween',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle invalid date string gracefully', () => {
|
||||
const inputDate: string = 'invalid-date';
|
||||
const result: JSONObject =
|
||||
DatabaseDate.asDateStartOfTheDayEndOfTheDayForDatabaseQuery(
|
||||
inputDate
|
||||
).toJSON();
|
||||
expect(result).toEqual({
|
||||
startValue: 'Invalid date',
|
||||
endValue: 'Invalid date',
|
||||
_type: 'InBetween',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty string input gracefully', () => {
|
||||
const inputDate: string = '';
|
||||
const result: JSONObject =
|
||||
DatabaseDate.asDateStartOfTheDayEndOfTheDayForDatabaseQuery(
|
||||
inputDate
|
||||
).toJSON();
|
||||
expect(result).toEqual({
|
||||
startValue: 'Invalid date',
|
||||
endValue: 'Invalid date',
|
||||
_type: 'InBetween',
|
||||
});
|
||||
});
|
||||
|
||||
it('should be a type of InBetween', () => {
|
||||
const inputDate: string = '2023-10-24T12:00:00Z';
|
||||
const result: InBetween =
|
||||
DatabaseDate.asDateStartOfTheDayEndOfTheDayForDatabaseQuery(
|
||||
inputDate
|
||||
);
|
||||
expect(result).toBeInstanceOf(InBetween);
|
||||
});
|
||||
});
|
||||
});
|
||||
70
Common/Tests/Types/Database/EqualToOrNull.test.ts
Normal file
70
Common/Tests/Types/Database/EqualToOrNull.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import EqualToOrNull from '../../../Types/Database/EqualToOrNull';
|
||||
import BadDataException from '../../../Types/Exception/BadDataException';
|
||||
import { JSONObject } from '../../../Types/JSON';
|
||||
|
||||
describe('EqualToOrNull', () => {
|
||||
it('should create an EqualToOrNull object with a valid value', () => {
|
||||
const value: string = 'oneuptime';
|
||||
const equalObj: EqualToOrNull = new EqualToOrNull(value);
|
||||
expect(equalObj.value).toBe(value);
|
||||
});
|
||||
|
||||
it('should get the value property of an EqualToOrNull object', () => {
|
||||
const value: string = 'oneuptime';
|
||||
const equalObj: EqualToOrNull = new EqualToOrNull(value);
|
||||
expect(equalObj.value).toBe(value);
|
||||
});
|
||||
|
||||
it('should set the value property of an EqualToOrNull object', () => {
|
||||
const equalObj: EqualToOrNull = new EqualToOrNull('oldValue');
|
||||
equalObj.value = 'newValue';
|
||||
expect(equalObj.value).toBe('newValue');
|
||||
});
|
||||
|
||||
it('should return the correct string representation using toString method', () => {
|
||||
const equalObj: EqualToOrNull = new EqualToOrNull('oneuptime');
|
||||
expect(equalObj.toString()).toBe('oneuptime');
|
||||
});
|
||||
|
||||
it('should generate the correct JSON representation using toJSON method', () => {
|
||||
const equalObj: EqualToOrNull = new EqualToOrNull('oneuptime');
|
||||
const expectedJSON: JSONObject = {
|
||||
_type: 'EqualToOrNull',
|
||||
value: 'oneuptime',
|
||||
};
|
||||
expect(equalObj.toJSON()).toEqual(expectedJSON);
|
||||
});
|
||||
|
||||
it('should create an EqualToOrNull object from valid JSON input', () => {
|
||||
const jsonInput: JSONObject = {
|
||||
_type: 'EqualToOrNull',
|
||||
value: 'oneuptime',
|
||||
};
|
||||
const equalObj: EqualToOrNull = EqualToOrNull.fromJSON(jsonInput);
|
||||
expect(equalObj.value).toBe('oneuptime');
|
||||
});
|
||||
|
||||
it('should throw a BadDataException when using invalid JSON input', () => {
|
||||
const jsonInput: JSONObject = {
|
||||
_type: 'InvalidType',
|
||||
value: 'oneuptime',
|
||||
};
|
||||
expect(() => {
|
||||
return EqualToOrNull.fromJSON(jsonInput);
|
||||
}).toThrow(BadDataException);
|
||||
});
|
||||
|
||||
it('should be a type of EqualToOrNull', () => {
|
||||
const equalObj: EqualToOrNull = new EqualToOrNull('oneuptime');
|
||||
expect(equalObj).toBeInstanceOf(EqualToOrNull);
|
||||
});
|
||||
|
||||
it('should handle null value when using fromJSON method', () => {
|
||||
const jsonInput: JSONObject = {
|
||||
_type: 'EqualToOrNull',
|
||||
value: null,
|
||||
};
|
||||
const equalObj: EqualToOrNull = EqualToOrNull.fromJSON(jsonInput);
|
||||
expect(equalObj.value).toBeNull();
|
||||
});
|
||||
});
|
||||
80
Common/Tests/Types/Database/InBetween.test.ts
Normal file
80
Common/Tests/Types/Database/InBetween.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import InBetween from '../../../Types/Database/InBetween';
|
||||
import BadDataException from '../../../Types/Exception/BadDataException';
|
||||
import { JSONObject } from '../../../Types/JSON';
|
||||
|
||||
describe('InBetween', () => {
|
||||
it('should create an InBetween object with valid start and end values', () => {
|
||||
const startValue: number = 10;
|
||||
const endValue: number = 20;
|
||||
const betweenObj: InBetween = new InBetween(startValue, endValue);
|
||||
expect(betweenObj.startValue).toBe(10);
|
||||
expect(betweenObj.endValue).toBe(20);
|
||||
});
|
||||
|
||||
it('should generate the correct JSON representation using toJSON method', () => {
|
||||
const startValue: number = 10;
|
||||
const endValue: number = 20;
|
||||
const betweenObj: InBetween = new InBetween(startValue, endValue);
|
||||
const expectedJSON: JSONObject = {
|
||||
_type: 'InBetween',
|
||||
startValue: 10,
|
||||
endValue: 20,
|
||||
};
|
||||
expect(betweenObj.toJSON()).toEqual(expectedJSON);
|
||||
});
|
||||
|
||||
it('should create an InBetween object from valid JSON input', () => {
|
||||
const jsonInput: JSONObject = {
|
||||
_type: 'InBetween',
|
||||
startValue: 10,
|
||||
endValue: 20,
|
||||
};
|
||||
const betweenObj: InBetween = InBetween.fromJSON(jsonInput);
|
||||
expect(betweenObj.startValue).toBe(10);
|
||||
expect(betweenObj.endValue).toBe(20);
|
||||
});
|
||||
|
||||
it('should throw a BadDataException when using invalid JSON input', () => {
|
||||
const jsonInput: JSONObject = {
|
||||
_type: 'InvalidType',
|
||||
startValue: 10,
|
||||
endValue: 20,
|
||||
};
|
||||
expect(() => {
|
||||
return InBetween.fromJSON(jsonInput);
|
||||
}).toThrow(BadDataException);
|
||||
});
|
||||
|
||||
it('should return a string with start and end values matching', () => {
|
||||
const startValue: number = 15;
|
||||
const endValue: number = 15;
|
||||
const betweenObj: InBetween = new InBetween(startValue, endValue);
|
||||
expect(betweenObj.toString()).toBe('15');
|
||||
});
|
||||
|
||||
it('should return a string with start and end values different', () => {
|
||||
const startValue: number = 10;
|
||||
const endValue: number = 20;
|
||||
const betweenObj: InBetween = new InBetween(startValue, endValue);
|
||||
expect(betweenObj.toString()).toBe('10 - 20');
|
||||
});
|
||||
|
||||
it('should return the start value as a string', () => {
|
||||
const startValue: number = 10;
|
||||
const endValue: number = 20;
|
||||
const betweenObj: InBetween = new InBetween(startValue, endValue);
|
||||
expect(betweenObj.toStartValueString()).toBe('10');
|
||||
});
|
||||
|
||||
it('should return the end value as a string', () => {
|
||||
const startValue: number = 10;
|
||||
const endValue: number = 20;
|
||||
const betweenObj: InBetween = new InBetween(startValue, endValue);
|
||||
expect(betweenObj.toEndValueString()).toBe('20');
|
||||
});
|
||||
|
||||
it('should be a type of InBetween', () => {
|
||||
const inBetweenObj: InBetween = new InBetween(10, 15);
|
||||
expect(inBetweenObj).toBeInstanceOf(InBetween);
|
||||
});
|
||||
});
|
||||
52
Common/Tests/Types/JSONFunctions.test.ts
Normal file
52
Common/Tests/Types/JSONFunctions.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import BaseModel from '../../Models/BaseModel';
|
||||
import { JSONObject } from '../../Types/JSON';
|
||||
import JSONFunctions from '../../Types/JSONFunctions';
|
||||
|
||||
describe('JSONFunctions Class', () => {
|
||||
let baseModel: BaseModel;
|
||||
|
||||
beforeEach(() => {
|
||||
baseModel = new BaseModel();
|
||||
});
|
||||
|
||||
describe('isEmptyObject Method', () => {
|
||||
test('Returns true for an empty object', () => {
|
||||
const emptyObj: JSONObject = {};
|
||||
expect(JSONFunctions.isEmptyObject(emptyObj)).toBe(true);
|
||||
});
|
||||
|
||||
test('Returns false for a non-empty object', () => {
|
||||
const nonEmptyObj: JSONObject = { key: 'value' };
|
||||
expect(JSONFunctions.isEmptyObject(nonEmptyObj)).toBe(false);
|
||||
});
|
||||
|
||||
test('Returns true for null or undefined', () => {
|
||||
expect(JSONFunctions.isEmptyObject(null)).toBe(true);
|
||||
expect(JSONFunctions.isEmptyObject(undefined)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toJSON and fromJSON Methods', () => {
|
||||
test('toJSON returns a valid JSON object', () => {
|
||||
const json: JSONObject = JSONFunctions.toJSON(baseModel, BaseModel);
|
||||
expect(json).toEqual(expect.objectContaining({}));
|
||||
});
|
||||
|
||||
test('toJSONObject returns a valid JSON object', () => {
|
||||
const json: JSONObject = JSONFunctions.toJSONObject(
|
||||
baseModel,
|
||||
BaseModel
|
||||
);
|
||||
expect(json).toEqual(expect.objectContaining({}));
|
||||
});
|
||||
|
||||
test('fromJSON returns a BaseModel instance', () => {
|
||||
const json: JSONObject = { name: 'oneuptime' };
|
||||
const result: BaseModel | BaseModel[] = JSONFunctions.fromJSON(
|
||||
json,
|
||||
BaseModel
|
||||
);
|
||||
expect(result).toBeInstanceOf(BaseModel);
|
||||
});
|
||||
});
|
||||
});
|
||||
44
Common/Tests/Types/SerializableObject.test.ts
Normal file
44
Common/Tests/Types/SerializableObject.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import NotImplementedException from '../../Types/Exception/NotImplementedException';
|
||||
import { JSONObject } from '../../Types/JSON';
|
||||
import SerializableObject from '../../Types/SerializableObject';
|
||||
|
||||
describe('SerializableObject Class', () => {
|
||||
let serializableObject: SerializableObject;
|
||||
|
||||
beforeEach(() => {
|
||||
serializableObject = new SerializableObject();
|
||||
});
|
||||
|
||||
test('Constructor initializes an instance of SerializableObject', () => {
|
||||
expect(serializableObject).toBeInstanceOf(SerializableObject);
|
||||
});
|
||||
|
||||
describe('toJSON Method', () => {
|
||||
test('Throws NotImplementedException when called', () => {
|
||||
expect(() => {
|
||||
return serializableObject.toJSON();
|
||||
}).toThrow(NotImplementedException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromJSON Method', () => {
|
||||
test('Throws NotImplementedException when called', () => {
|
||||
expect(() => {
|
||||
return SerializableObject.fromJSON({});
|
||||
}).toThrow(NotImplementedException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromJSON Instance Method', () => {
|
||||
test('Returns the result from the static fromJSON method', () => {
|
||||
const json: JSONObject = { key: 'value' };
|
||||
const expectedResult: SerializableObject = new SerializableObject();
|
||||
jest.spyOn(SerializableObject, 'fromJSON').mockReturnValue(
|
||||
expectedResult
|
||||
);
|
||||
const result: SerializableObject =
|
||||
serializableObject.fromJSON(json);
|
||||
expect(result).toBe(expectedResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ColumnAccessControl } from '../BaseDatabase/AccessControl';
|
||||
import ColumnBillingAccessControl from '../BaseDatabase/ColumnBillingAccessControl';
|
||||
import TableColumnType from '../BaseDatabase/TableColumnType';
|
||||
import TableColumnType from '../AnalyticsDatabase/TableColumnType';
|
||||
import { JSONValue } from '../JSON';
|
||||
import NestedModel from '../../AnalyticsModels/NestedModel';
|
||||
|
||||
export default class AnalyticsTableColumn {
|
||||
private _key: string = 'id';
|
||||
@@ -45,7 +46,7 @@ export default class AnalyticsTableColumn {
|
||||
this._isTenantId = v;
|
||||
}
|
||||
|
||||
private _type: TableColumnType = TableColumnType.ShortText;
|
||||
private _type: TableColumnType = TableColumnType.Text;
|
||||
public get type(): TableColumnType {
|
||||
return this._type;
|
||||
}
|
||||
@@ -103,8 +104,17 @@ export default class AnalyticsTableColumn {
|
||||
this._accessControl = v;
|
||||
}
|
||||
|
||||
private _nestedModel?: NestedModel | undefined;
|
||||
public get nestedModel(): NestedModel | undefined {
|
||||
return this._nestedModel;
|
||||
}
|
||||
public set nestedModel(v: NestedModel | undefined) {
|
||||
this._nestedModel = v;
|
||||
}
|
||||
|
||||
public constructor(data: {
|
||||
key: string;
|
||||
nestedModel?: NestedModel | undefined;
|
||||
title: string;
|
||||
description: string;
|
||||
required: boolean;
|
||||
@@ -118,6 +128,10 @@ export default class AnalyticsTableColumn {
|
||||
| (() => Date | string | number | boolean)
|
||||
| undefined;
|
||||
}) {
|
||||
if (data.type === TableColumnType.NestedModel && !data.nestedModel) {
|
||||
throw new Error('NestedModel is required when type is NestedModel');
|
||||
}
|
||||
|
||||
this.accessControl = data.accessControl;
|
||||
this.key = data.key;
|
||||
this.title = data.title;
|
||||
@@ -130,5 +144,6 @@ export default class AnalyticsTableColumn {
|
||||
this.billingAccessControl = data.billingAccessControl;
|
||||
this.allowAccessIfSubscriptionIsUnpaid =
|
||||
data.allowAccessIfSubscriptionIsUnpaid || false;
|
||||
this.nestedModel = data.nestedModel;
|
||||
}
|
||||
}
|
||||
|
||||
15
Common/Types/AnalyticsDatabase/TableColumnType.ts
Normal file
15
Common/Types/AnalyticsDatabase/TableColumnType.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
enum ColumnType {
|
||||
ObjectID = 'Object ID',
|
||||
Date = 'Date',
|
||||
Boolean = 'Boolean',
|
||||
Number = 'Number',
|
||||
Text = 'Text',
|
||||
NestedModel = 'Nested Model',
|
||||
JSON = 'JSON',
|
||||
Decimal = 'Decimal',
|
||||
ArrayNumber = 'Array of Numbers',
|
||||
ArrayText = 'Array of Text',
|
||||
LongNumber = 'Long Number',
|
||||
}
|
||||
|
||||
export default ColumnType;
|
||||
@@ -28,3 +28,14 @@ export interface CallRequestMessage {
|
||||
export default interface CallRequest extends CallRequestMessage {
|
||||
to: Phone;
|
||||
}
|
||||
|
||||
export const isHighRiskPhoneNumber: Function = (
|
||||
phoneNumber: Phone
|
||||
): boolean => {
|
||||
// Pakistan
|
||||
if (phoneNumber.toString().startsWith('+92')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import TableColumnType from '../BaseDatabase/TableColumnType';
|
||||
import TableColumnType from './TableColumnType';
|
||||
|
||||
enum ColumnLength {
|
||||
Version = 30,
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'reflect-metadata';
|
||||
import BaseModel from '../../Models/BaseModel';
|
||||
import Dictionary from '../Dictionary';
|
||||
import { ReflectionMetadataType } from '../Reflection';
|
||||
import TableColumnType from '../BaseDatabase/TableColumnType';
|
||||
import TableColumnType from './TableColumnType';
|
||||
|
||||
const tableColumn: Symbol = Symbol('TableColumn');
|
||||
|
||||
|
||||
@@ -29,6 +29,10 @@ export default class OneUptimeDate {
|
||||
return this.getSomeDaysAgo(new PositiveNumber(1));
|
||||
}
|
||||
|
||||
public static fromUnixNano(timestamp: number): Date {
|
||||
return moment(timestamp / 1000000).toDate();
|
||||
}
|
||||
|
||||
public static getSecondsTo(date: Date): number {
|
||||
date = this.fromString(date);
|
||||
const dif: number = date.getTime() - this.getCurrentDate().getTime();
|
||||
@@ -98,6 +102,10 @@ export default class OneUptimeDate {
|
||||
return days.positiveNumber * 24 * 60 * 60;
|
||||
}
|
||||
|
||||
public static getMillisecondsInDays(days: PositiveNumber | number): number {
|
||||
return this.getSecondsInDays(days) * 1000;
|
||||
}
|
||||
|
||||
public static getSomeHoursAgo(hours: PositiveNumber | number): Date {
|
||||
if (!(hours instanceof PositiveNumber)) {
|
||||
hours = new PositiveNumber(hours);
|
||||
@@ -221,7 +229,7 @@ export default class OneUptimeDate {
|
||||
let hasMins: boolean = false;
|
||||
if (hours !== '00') {
|
||||
hasHours = true;
|
||||
text += hours + ' hours';
|
||||
text += hours + ' hours ';
|
||||
}
|
||||
|
||||
if (mins !== '00' || hasHours) {
|
||||
@@ -231,7 +239,7 @@ export default class OneUptimeDate {
|
||||
text += ', ';
|
||||
}
|
||||
|
||||
text += mins + ' minutes';
|
||||
text += mins + ' minutes ';
|
||||
}
|
||||
|
||||
if (!(hasHours && hasMins)) {
|
||||
@@ -351,6 +359,12 @@ export default class OneUptimeDate {
|
||||
return moment(date).isAfter(startDate);
|
||||
}
|
||||
|
||||
public static isEqualBySeconds(date: Date, startDate: Date): boolean {
|
||||
date = this.fromString(date);
|
||||
startDate = this.fromString(startDate);
|
||||
return moment(date).isSame(startDate, 'seconds');
|
||||
}
|
||||
|
||||
public static hasExpired(expirationDate: Date): boolean {
|
||||
expirationDate = this.fromString(expirationDate);
|
||||
return !moment(this.getCurrentDate()).isBefore(expirationDate);
|
||||
|
||||
@@ -10,12 +10,14 @@ enum IconProp {
|
||||
Settings = 'Settings',
|
||||
Criteria = 'Criteria',
|
||||
Notification = 'Notification',
|
||||
Squares = 'Squares',
|
||||
Help = 'Help',
|
||||
JSON = 'JSON',
|
||||
Signal = 'Signal',
|
||||
Database = 'Database',
|
||||
ChevronDown = 'ChevronDown',
|
||||
Pencil = 'Pencil',
|
||||
Flag = 'Flag',
|
||||
Copy = 'Copy',
|
||||
ChevronRight = 'ChevronRight',
|
||||
ChevronUp = 'ChevronUp',
|
||||
@@ -26,6 +28,7 @@ enum IconProp {
|
||||
Home = 'Home',
|
||||
Graph = 'Graph',
|
||||
Variable = 'Variable',
|
||||
ListBullet = 'ListBullet',
|
||||
Image = 'Image',
|
||||
Grid = 'Grid',
|
||||
More = 'More',
|
||||
|
||||
@@ -123,6 +123,7 @@ export type JSONValue =
|
||||
| Array<JSONValue>
|
||||
| Array<Permission>
|
||||
| Array<JSONValue>
|
||||
| Array<ObjectID>
|
||||
| CallRequest
|
||||
| undefined
|
||||
| null;
|
||||
|
||||
@@ -4,7 +4,7 @@ import OneUptimeDate from './Date';
|
||||
import BaseModel from '../Models/BaseModel';
|
||||
import { JSONArray, JSONObject, JSONValue, ObjectType } from './JSON';
|
||||
import { TableColumnMetadata } from '../Types/Database/TableColumn';
|
||||
import TableColumnType from './BaseDatabase/TableColumnType';
|
||||
import TableColumnType from './Database/TableColumnType';
|
||||
import SerializableObject from './SerializableObject';
|
||||
import SerializableObjectDictionary from './SerializableObjectDictionary';
|
||||
import JSON5 from 'json5';
|
||||
|
||||
@@ -76,7 +76,7 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
|
||||
changeMonitorStatus: true,
|
||||
createIncidents: false,
|
||||
name: 'Check if online',
|
||||
description: 'This criteria checks if the monitor is online',
|
||||
description: `This criteria checks if the ${arg.monitorType} is online`,
|
||||
};
|
||||
|
||||
if (
|
||||
@@ -118,8 +118,8 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
|
||||
],
|
||||
incidents: [
|
||||
{
|
||||
title: `${arg.monitorType} monitor is offline`,
|
||||
description: `${arg.monitorType} monitor is currently offline.`,
|
||||
title: `${arg.monitorType} is offline`,
|
||||
description: `${arg.monitorType} is currently offline.`,
|
||||
incidentSeverityId: arg.incidentSeverityId,
|
||||
autoResolveIncident: true,
|
||||
id: ObjectID.generate().toString(),
|
||||
@@ -129,7 +129,7 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
|
||||
changeMonitorStatus: true,
|
||||
createIncidents: true,
|
||||
name: 'Check if offline',
|
||||
description: 'This criteria checks if the monitor is offline',
|
||||
description: `This criteria checks if the ${arg.monitorType} is offline`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -155,8 +155,8 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
|
||||
],
|
||||
incidents: [
|
||||
{
|
||||
title: `${arg.monitorType} monitor is offline`,
|
||||
description: `${arg.monitorType} monitor is currently offline.`,
|
||||
title: `${arg.monitorType} is offline`,
|
||||
description: `${arg.monitorType} is currently offline.`,
|
||||
incidentSeverityId: arg.incidentSeverityId,
|
||||
autoResolveIncident: true,
|
||||
id: ObjectID.generate().toString(),
|
||||
@@ -166,7 +166,7 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
|
||||
changeMonitorStatus: true,
|
||||
createIncidents: true,
|
||||
name: 'Check if offline',
|
||||
description: 'This criteria checks if the monitor is offline',
|
||||
description: `This criteria checks if the ${arg.monitorType} is offline`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -184,8 +184,8 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
|
||||
],
|
||||
incidents: [
|
||||
{
|
||||
title: `${arg.monitorType} monitor is offline`,
|
||||
description: `${arg.monitorType} monitor is currently offline.`,
|
||||
title: `${arg.monitorType} is offline`,
|
||||
description: `${arg.monitorType} is currently offline.`,
|
||||
incidentSeverityId: arg.incidentSeverityId,
|
||||
autoResolveIncident: true,
|
||||
id: ObjectID.generate().toString(),
|
||||
@@ -195,7 +195,7 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
|
||||
changeMonitorStatus: true,
|
||||
createIncidents: true,
|
||||
name: 'Check if offline',
|
||||
description: 'This criteria checks if the monitor is offline',
|
||||
description: `This criteria checks if the ${arg.monitorType} is offline`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ enum NotificationSettingEventType {
|
||||
SEND_MONITOR_STATUS_CHANGED_OWNER_NOTIFICATION = 'Send monitor status changed notification when I am the owner of the monitor',
|
||||
|
||||
// Scheduled Maintenance
|
||||
SEND_SCHEDULED_MAINTENANCE_CREATED_OWNER_NOTIFICATION = 'Send scheduled maintenance created notification when I am the owner of the scheduled maintenance',
|
||||
SEND_SCHEDULED_MAINTENANCE_NOTE_POSTED_OWNER_NOTIFICATION = 'Send scheduled maintenance note posted notification when I am the owner of the scheduled maintenance',
|
||||
SEND_SCHEDULED_MAINTENANCE_OWNER_ADDED_NOTIFICATION = 'Send notification when I am added as a owner to the scheduled maintenance',
|
||||
SEND_SCHEDULED_MAINTENANCE_STATE_CHANGED_OWNER_NOTIFICATION = 'Send scheduled maintenance state changed notification when I am the owner of the scheduled maintenance',
|
||||
SEND_SCHEDULED_MAINTENANCE_CREATED_OWNER_NOTIFICATION = 'Send event created notification when I am the owner of the event',
|
||||
SEND_SCHEDULED_MAINTENANCE_NOTE_POSTED_OWNER_NOTIFICATION = 'Send event note posted notification when I am the owner of the event',
|
||||
SEND_SCHEDULED_MAINTENANCE_OWNER_ADDED_NOTIFICATION = 'Send notification when I am added as a owner to the event',
|
||||
SEND_SCHEDULED_MAINTENANCE_STATE_CHANGED_OWNER_NOTIFICATION = 'Send event state changed notification when I am the owner of the event',
|
||||
|
||||
// Status Page
|
||||
SEND_STATUS_PAGE_ANNOUNCEMENT_CREATED_OWNER_NOTIFICATION = 'Send status page announcement created notification when I am the owner of the status page',
|
||||
|
||||
@@ -58,6 +58,11 @@ enum Permission {
|
||||
CanEditProjectProbe = 'CanEditProjectProbe',
|
||||
CanReadProjectProbe = 'CanReadProjectProbe',
|
||||
|
||||
CanCreateMonitorGroupResource = 'CanCreateMonitorGroupResource',
|
||||
CanDeleteMonitorGroupResource = 'CanDeleteMonitorGroupResource',
|
||||
CanEditMonitorGroupResource = 'CanEditMonitorGroupResource',
|
||||
CanReadMonitorGroupResource = 'CanReadMonitorGroupResource',
|
||||
|
||||
CanCreateMonitorCustomField = 'CanCreateMonitorCustomField',
|
||||
CanDeleteMonitorCustomField = 'CanDeleteMonitorCustomField',
|
||||
CanEditMonitorCustomField = 'CanEditMonitorCustomField',
|
||||
@@ -147,6 +152,16 @@ enum Permission {
|
||||
CanEditMonitorOwnerUser = 'CanEditMonitorOwnerUser',
|
||||
CanReadMonitorOwnerUser = 'CanReadMonitorOwnerUser',
|
||||
|
||||
CanCreateMonitorGroupOwnerTeam = 'CanCreateMonitorGroupOwnerTeam',
|
||||
CanDeleteMonitorGroupOwnerTeam = 'CanDeleteMonitorGroupOwnerTeam',
|
||||
CanEditMonitorGroupOwnerTeam = 'CanEditMonitorGroupOwnerTeam',
|
||||
CanReadMonitorGroupOwnerTeam = 'CanReadMonitorGroupOwnerTeam',
|
||||
|
||||
CanCreateMonitorGroupOwnerUser = 'CanCreateMonitorGroupOwner',
|
||||
CanDeleteMonitorGroupOwnerUser = 'CanDeleteMonitorGroupOwnerUser',
|
||||
CanEditMonitorGroupOwnerUser = 'CanEditMonitorGroupOwnerUser',
|
||||
CanReadMonitorGroupOwnerUser = 'CanReadMonitorGroupOwnerUser',
|
||||
|
||||
CanCreateStatusPageCustomField = 'CanCreateStatusPageCustomField',
|
||||
CanDeleteStatusPageCustomField = 'CanDeleteStatusPageCustomField',
|
||||
CanEditStatusPageCustomField = 'CanEditStatusPageCustomField',
|
||||
@@ -221,6 +236,11 @@ enum Permission {
|
||||
CanEditStatusPageDomain = 'CanEditStatusPageDomain',
|
||||
CanReadStatusPageDomain = 'CanReadStatusPageDomain',
|
||||
|
||||
CanCreateMonitorGroup = 'CanCreateMonitorGroup',
|
||||
CanDeleteMonitorGroup = 'CanDeleteMonitorGroup',
|
||||
CanEditMonitorGroup = 'CanEditMonitorGroup',
|
||||
CanReadMonitorGroup = 'CanReadMonitorGroup',
|
||||
|
||||
CanCreateProjectSSO = 'CanCreateProjectSSO',
|
||||
CanDeleteProjectSSO = 'CanDeleteProjectSSO',
|
||||
CanEditProjectSSO = 'CanEditProjectSSO',
|
||||
@@ -1174,6 +1194,39 @@ export class PermissionHelper {
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
|
||||
{
|
||||
permission: Permission.CanCreateMonitorGroup,
|
||||
title: 'Can Create Monitor Group',
|
||||
description:
|
||||
'This permission can create Monitor Group in this project.',
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
{
|
||||
permission: Permission.CanDeleteMonitorGroup,
|
||||
title: 'Can Delete Monitor Group',
|
||||
description:
|
||||
'This permission can delete Monitor Group in this project.',
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
{
|
||||
permission: Permission.CanEditMonitorGroup,
|
||||
title: 'Can Edit Monitor Group',
|
||||
description:
|
||||
'This permission can edit Monitor Group in this project.',
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
{
|
||||
permission: Permission.CanReadMonitorGroup,
|
||||
title: 'Can Read Monitor Group',
|
||||
description:
|
||||
'This permission can read Monitor Group in this project.',
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
|
||||
{
|
||||
permission: Permission.CanCreateProjectSSO,
|
||||
title: 'Can Create Project SSO',
|
||||
@@ -1775,6 +1828,37 @@ export class PermissionHelper {
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
|
||||
{
|
||||
permission: Permission.CanCreateMonitorGroupResource,
|
||||
title: 'Can Create Monitor Group Resource',
|
||||
description:
|
||||
'This permission can create monitor group resource.',
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
{
|
||||
permission: Permission.CanDeleteMonitorGroupResource,
|
||||
title: 'Can Delete Monitor Group Resource',
|
||||
description:
|
||||
'This permission can delete monitor group resource.',
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
{
|
||||
permission: Permission.CanEditMonitorGroupResource,
|
||||
title: 'Can Edit Monitor Group Resource',
|
||||
description: 'This permission can edit monitor group resource.',
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
{
|
||||
permission: Permission.CanReadMonitorGroupResource,
|
||||
title: 'Can Read Monitor Group Resource',
|
||||
description: 'This permission can read monitor group resource.',
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
|
||||
{
|
||||
permission: Permission.CanCreateOnCallDutyPolicyCustomField,
|
||||
title: 'Can Create On-Call Policy Custom Field',
|
||||
@@ -2455,6 +2539,72 @@ export class PermissionHelper {
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
|
||||
{
|
||||
permission: Permission.CanCreateMonitorGroupOwnerTeam,
|
||||
title: 'Can Create Monitor Group Team Owner',
|
||||
description:
|
||||
'This permission can create Monitor Group Team Owner this project.',
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
{
|
||||
permission: Permission.CanDeleteMonitorGroupOwnerTeam,
|
||||
title: 'Can Delete Monitor Group Team Owner',
|
||||
description:
|
||||
'This permission can delete Monitor Group Team Owner of this project.',
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
{
|
||||
permission: Permission.CanEditMonitorGroupOwnerTeam,
|
||||
title: 'Can Edit Monitor Group Team Owner',
|
||||
description:
|
||||
'This permission can edit Monitor Group Team Owner of this project.',
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
{
|
||||
permission: Permission.CanReadMonitorGroupOwnerTeam,
|
||||
title: 'Can Read Monitor Group Team Owner',
|
||||
description:
|
||||
'This permission can read Monitor Group Team Owner of this project.',
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
|
||||
{
|
||||
permission: Permission.CanCreateMonitorGroupOwnerUser,
|
||||
title: 'Can Create Monitor Group User Owner',
|
||||
description:
|
||||
'This permission can create Monitor Group User Owner this project.',
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
{
|
||||
permission: Permission.CanDeleteMonitorGroupOwnerUser,
|
||||
title: 'Can Delete Monitor Group User Owner',
|
||||
description:
|
||||
'This permission can delete Monitor Group User Owner of this project.',
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
{
|
||||
permission: Permission.CanEditMonitorGroupOwnerUser,
|
||||
title: 'Can Edit Monitor Group User Owner',
|
||||
description:
|
||||
'This permission can edit Monitor Group User Owner of this project.',
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
{
|
||||
permission: Permission.CanReadMonitorGroupOwnerUser,
|
||||
title: 'Can Read Monitor Group User Owner',
|
||||
description:
|
||||
'This permission can read Monitor Group User Owner of this project.',
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
|
||||
{
|
||||
permission: Permission.CanCreateProjectIncident,
|
||||
title: 'Can Create Incident',
|
||||
|
||||
@@ -154,7 +154,7 @@ export default class API {
|
||||
return Promise.resolve(headers);
|
||||
}
|
||||
|
||||
public static getDefaultHeaders(): Headers {
|
||||
public static getDefaultHeaders(_props?: any): Headers {
|
||||
const defaultHeaders: Headers = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
Accept: 'application/json',
|
||||
|
||||
12
Common/package-lock.json
generated
12
Common/package-lock.json
generated
@@ -1637,9 +1637,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-js": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz",
|
||||
"integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw=="
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||
},
|
||||
"node_modules/cssom": {
|
||||
"version": "0.4.4",
|
||||
@@ -5943,9 +5943,9 @@
|
||||
}
|
||||
},
|
||||
"crypto-js": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz",
|
||||
"integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw=="
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||
},
|
||||
"cssom": {
|
||||
"version": "0.4.4",
|
||||
|
||||
110
CommonServer/API/MonitorGroupAPI.ts
Normal file
110
CommonServer/API/MonitorGroupAPI.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import MonitorGroup from 'Model/Models/MonitorGroup';
|
||||
import MonitorGroupService, {
|
||||
Service as MonitorGroupServiceType,
|
||||
} from '../Services/MonitorGroupService';
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from '../Utils/Express';
|
||||
import Response from '../Utils/Response';
|
||||
import BaseAPI from './BaseAPI';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import UserMiddleware from '../Middleware/UserAuthorization';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import MonitorStatus from 'Model/Models/MonitorStatus';
|
||||
import MonitorStatusTimeline from 'Model/Models/MonitorStatusTimeline';
|
||||
import OneUptimeDate from 'Common/Types/Date';
|
||||
|
||||
export default class MonitorGroupAPI extends BaseAPI<
|
||||
MonitorGroup,
|
||||
MonitorGroupServiceType
|
||||
> {
|
||||
public constructor() {
|
||||
super(MonitorGroup, MonitorGroupService);
|
||||
|
||||
this.router.post(
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/current-status/:monitorGroupId`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction
|
||||
) => {
|
||||
try {
|
||||
// get group id.
|
||||
|
||||
if (!req.params['monitorGroupId']) {
|
||||
throw new BadDataException(
|
||||
'Monitor group id is required.'
|
||||
);
|
||||
}
|
||||
|
||||
const currentStatus: MonitorStatus =
|
||||
await this.service.getCurrentStatus(
|
||||
new ObjectID(
|
||||
req.params['monitorGroupId'].toString()
|
||||
),
|
||||
await this.getDatabaseCommonInteractionProps(req)
|
||||
);
|
||||
|
||||
return Response.sendEntityResponse(
|
||||
req,
|
||||
res,
|
||||
currentStatus,
|
||||
MonitorStatus
|
||||
);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.router.post(
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/timeline/:monitorGroupId`,
|
||||
UserMiddleware.getUserMiddleware,
|
||||
async (
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
next: NextFunction
|
||||
) => {
|
||||
try {
|
||||
// get group id.
|
||||
|
||||
if (!req.params['monitorGroupId']) {
|
||||
throw new BadDataException(
|
||||
'Monitor group id is required.'
|
||||
);
|
||||
}
|
||||
|
||||
const startDate: Date = OneUptimeDate.getSomeDaysAgo(90);
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
|
||||
const timeline: Array<MonitorStatusTimeline> =
|
||||
await this.service.getStatusTimeline(
|
||||
new ObjectID(
|
||||
req.params['monitorGroupId'].toString()
|
||||
),
|
||||
startDate,
|
||||
endDate,
|
||||
await this.getDatabaseCommonInteractionProps(req)
|
||||
);
|
||||
|
||||
return Response.sendEntityArrayResponse(
|
||||
req,
|
||||
res,
|
||||
timeline,
|
||||
timeline.length,
|
||||
MonitorStatusTimeline
|
||||
);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,7 @@ export default class ProjectAPI extends BaseAPI<Project, ProjectServiceType> {
|
||||
trialEndsAt: true,
|
||||
paymentProviderPlanId: true,
|
||||
resellerId: true,
|
||||
isFeatureFlagMonitorGroupsEnabled: true,
|
||||
},
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
|
||||
@@ -61,6 +61,10 @@ import PositiveNumber from 'Common/Types/PositiveNumber';
|
||||
import StatusPageSsoService from '../Services/StatusPageSsoService';
|
||||
import StatusPageSSO from 'Model/Models/StatusPageSso';
|
||||
import ArrayUtil from 'Common/Types/ArrayUtil';
|
||||
import Dictionary from 'Common/Types/Dictionary';
|
||||
import MonitorGroupService from '../Services/MonitorGroupService';
|
||||
import MonitorGroupResource from 'Model/Models/MonitorGroupResource';
|
||||
import MonitorGroupResourceService from '../Services/MonitorGroupResourceService';
|
||||
|
||||
export default class StatusPageAPI extends BaseAPI<
|
||||
StatusPage,
|
||||
@@ -73,7 +77,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
this.router.get(
|
||||
`${new this.entityType()
|
||||
.getCrudApiPath()
|
||||
?.toString()}/status-page-api/cname-verification/:token`,
|
||||
?.toString()}/cname-verification/:token`,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
const host: string | undefined = req.get('host');
|
||||
|
||||
@@ -391,6 +395,9 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
res: ExpressResponse,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const startDate: Date = OneUptimeDate.getSomeDaysAgo(90);
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
|
||||
try {
|
||||
const objectId: ObjectID = new ObjectID(
|
||||
req.params['statusPageId'] as string
|
||||
@@ -418,6 +425,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
projectId: true,
|
||||
isPublicStatusPage: true,
|
||||
overviewPageDescription: true,
|
||||
showIncidentLabelsOnStatusPage: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
@@ -493,8 +501,8 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
_id: true,
|
||||
currentMonitorStatusId: true,
|
||||
},
|
||||
monitorGroupId: true,
|
||||
},
|
||||
|
||||
sort: {
|
||||
order: SortOrder.Ascending,
|
||||
},
|
||||
@@ -505,13 +513,28 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
},
|
||||
});
|
||||
|
||||
const monitorGroupIds: Array<ObjectID> = statusPageResources
|
||||
.map((resource: StatusPageResource) => {
|
||||
return resource.monitorGroupId!;
|
||||
})
|
||||
.filter((id: ObjectID) => {
|
||||
return Boolean(id); // remove nulls
|
||||
});
|
||||
|
||||
// get monitors in the group.
|
||||
const monitorGroupCurrentStatuses: Dictionary<ObjectID> =
|
||||
{};
|
||||
const monitorsInGroup: Dictionary<Array<ObjectID>> = {};
|
||||
|
||||
// get monitor status charts.
|
||||
const monitorsOnStatusPage: Array<ObjectID> =
|
||||
statusPageResources.map(
|
||||
(monitor: StatusPageResource) => {
|
||||
statusPageResources
|
||||
.map((monitor: StatusPageResource) => {
|
||||
return monitor.monitorId!;
|
||||
}
|
||||
);
|
||||
})
|
||||
.filter((id: ObjectID) => {
|
||||
return Boolean(id); // remove nulls
|
||||
});
|
||||
|
||||
const monitorsOnStatusPageForTimeline: Array<ObjectID> =
|
||||
statusPageResources
|
||||
@@ -520,10 +543,98 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
})
|
||||
.map((monitor: StatusPageResource) => {
|
||||
return monitor.monitorId!;
|
||||
})
|
||||
.filter((id: ObjectID) => {
|
||||
return Boolean(id); // remove nulls
|
||||
});
|
||||
|
||||
const startDate: Date = OneUptimeDate.getSomeDaysAgo(90);
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
for (const monitorGroupId of monitorGroupIds) {
|
||||
// get current status of monitors in the group.
|
||||
|
||||
const currentStatus: MonitorStatus =
|
||||
await MonitorGroupService.getCurrentStatus(
|
||||
monitorGroupId,
|
||||
{
|
||||
isRoot: true,
|
||||
}
|
||||
);
|
||||
|
||||
monitorGroupCurrentStatuses[monitorGroupId.toString()] =
|
||||
currentStatus.id!;
|
||||
|
||||
// get monitors in the group.
|
||||
|
||||
const groupResources: Array<MonitorGroupResource> =
|
||||
await MonitorGroupResourceService.findBy({
|
||||
query: {
|
||||
monitorGroupId: monitorGroupId,
|
||||
},
|
||||
select: {
|
||||
monitorId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
});
|
||||
|
||||
const monitorsInGroupIds: Array<ObjectID> =
|
||||
groupResources
|
||||
.map((resource: MonitorGroupResource) => {
|
||||
return resource.monitorId!;
|
||||
})
|
||||
.filter((id: ObjectID) => {
|
||||
return Boolean(id); // remove nulls
|
||||
});
|
||||
|
||||
const shouldShowTimelineForThisGroup: boolean = Boolean(
|
||||
statusPageResources.find(
|
||||
(resource: StatusPageResource) => {
|
||||
return (
|
||||
resource.monitorGroupId?.toString() ===
|
||||
monitorGroupId.toString() &&
|
||||
resource.showStatusHistoryChart
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
for (const monitorId of monitorsInGroupIds) {
|
||||
if (!monitorId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!monitorsOnStatusPage.find((item: ObjectID) => {
|
||||
return (
|
||||
item.toString() === monitorId.toString()
|
||||
);
|
||||
})
|
||||
) {
|
||||
monitorsOnStatusPage.push(monitorId);
|
||||
}
|
||||
|
||||
// add this to the timeline event for this group.
|
||||
|
||||
if (
|
||||
shouldShowTimelineForThisGroup &&
|
||||
!monitorsOnStatusPageForTimeline.find(
|
||||
(item: ObjectID) => {
|
||||
return (
|
||||
item.toString() ===
|
||||
monitorId.toString()
|
||||
);
|
||||
}
|
||||
)
|
||||
) {
|
||||
monitorsOnStatusPageForTimeline.push(monitorId);
|
||||
}
|
||||
}
|
||||
|
||||
monitorsInGroup[monitorGroupId.toString()] =
|
||||
monitorsInGroupIds;
|
||||
}
|
||||
|
||||
let monitorStatusTimelines: Array<MonitorStatusTimeline> =
|
||||
[];
|
||||
@@ -563,6 +674,34 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
// check if status page has active incident.
|
||||
let activeIncidents: Array<Incident> = [];
|
||||
if (monitorsOnStatusPage.length > 0) {
|
||||
let select: Select<Incident> = {
|
||||
createdAt: true,
|
||||
title: true,
|
||||
description: true,
|
||||
_id: true,
|
||||
incidentSeverity: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
currentIncidentState: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
monitors: {
|
||||
_id: true,
|
||||
},
|
||||
};
|
||||
|
||||
if (statusPage.showIncidentLabelsOnStatusPage) {
|
||||
select = {
|
||||
...select,
|
||||
labels: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
activeIncidents = await IncidentService.findBy({
|
||||
query: {
|
||||
monitors: monitorsOnStatusPage as any,
|
||||
@@ -571,23 +710,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
} as any,
|
||||
projectId: statusPage.projectId!,
|
||||
},
|
||||
select: {
|
||||
createdAt: true,
|
||||
title: true,
|
||||
description: true,
|
||||
_id: true,
|
||||
incidentSeverity: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
currentIncidentState: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
monitors: {
|
||||
_id: true,
|
||||
},
|
||||
},
|
||||
select: select,
|
||||
sort: {
|
||||
createdAt: SortOrder.Ascending,
|
||||
},
|
||||
@@ -906,6 +1029,12 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
scheduledMaintenanceStateTimelines,
|
||||
ScheduledMaintenanceStateTimeline
|
||||
),
|
||||
|
||||
monitorGroupCurrentStatuses: JSONFunctions.serialize(
|
||||
monitorGroupCurrentStatuses
|
||||
),
|
||||
monitorsInGroup:
|
||||
JSONFunctions.serialize(monitorsInGroup),
|
||||
};
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, response);
|
||||
@@ -1228,6 +1357,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
_id: true,
|
||||
currentMonitorStatusId: true,
|
||||
},
|
||||
monitorGroupId: true,
|
||||
},
|
||||
|
||||
skip: 0,
|
||||
@@ -1406,6 +1536,65 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
});
|
||||
}
|
||||
|
||||
const monitorGroupIds: Array<ObjectID> = statusPageResources
|
||||
.map((resource: StatusPageResource) => {
|
||||
return resource.monitorGroupId!;
|
||||
})
|
||||
.filter((id: ObjectID) => {
|
||||
return Boolean(id); // remove nulls
|
||||
});
|
||||
|
||||
// get monitors in the group.
|
||||
const monitorsInGroup: Dictionary<Array<ObjectID>> = {};
|
||||
|
||||
// get monitor status charts.
|
||||
const monitorsOnStatusPage: Array<ObjectID> = statusPageResources
|
||||
.map((monitor: StatusPageResource) => {
|
||||
return monitor.monitorId!;
|
||||
})
|
||||
.filter((id: ObjectID) => {
|
||||
return Boolean(id); // remove nulls
|
||||
});
|
||||
|
||||
for (const monitorGroupId of monitorGroupIds) {
|
||||
// get monitors in the group.
|
||||
|
||||
const groupResources: Array<MonitorGroupResource> =
|
||||
await MonitorGroupResourceService.findBy({
|
||||
query: {
|
||||
monitorGroupId: monitorGroupId,
|
||||
},
|
||||
select: {
|
||||
monitorId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
});
|
||||
|
||||
const monitorsInGroupIds: Array<ObjectID> = groupResources
|
||||
.map((resource: MonitorGroupResource) => {
|
||||
return resource.monitorId!;
|
||||
})
|
||||
.filter((id: ObjectID) => {
|
||||
return Boolean(id); // remove nulls
|
||||
});
|
||||
|
||||
for (const monitorId of monitorsInGroupIds) {
|
||||
if (
|
||||
!monitorsOnStatusPage.find((item: ObjectID) => {
|
||||
return item.toString() === monitorId.toString();
|
||||
})
|
||||
) {
|
||||
monitorsOnStatusPage.push(monitorId);
|
||||
}
|
||||
}
|
||||
|
||||
monitorsInGroup[monitorGroupId.toString()] = monitorsInGroupIds;
|
||||
}
|
||||
|
||||
const response: JSONObject = {
|
||||
scheduledMaintenanceEventsPublicNotes: JSONFunctions.toJSONArray(
|
||||
scheduledMaintenanceEventsPublicNotes,
|
||||
@@ -1423,6 +1612,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
scheduledMaintenanceStateTimelines,
|
||||
ScheduledMaintenanceStateTimeline
|
||||
),
|
||||
monitorsInGroup: JSONFunctions.serialize(monitorsInGroup),
|
||||
};
|
||||
|
||||
return response;
|
||||
@@ -1559,6 +1749,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
_id: true,
|
||||
projectId: true,
|
||||
showIncidentHistoryInDays: true,
|
||||
showIncidentLabelsOnStatusPage: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
@@ -1586,6 +1777,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
_id: true,
|
||||
currentMonitorStatusId: true,
|
||||
},
|
||||
monitorGroupId: true,
|
||||
},
|
||||
|
||||
skip: 0,
|
||||
@@ -1595,12 +1787,65 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
},
|
||||
});
|
||||
|
||||
const monitorGroupIds: Array<ObjectID> = statusPageResources
|
||||
.map((resource: StatusPageResource) => {
|
||||
return resource.monitorGroupId!;
|
||||
})
|
||||
.filter((id: ObjectID) => {
|
||||
return Boolean(id); // remove nulls
|
||||
});
|
||||
|
||||
const monitorsInGroup: Dictionary<Array<ObjectID>> = {};
|
||||
|
||||
// get monitor status charts.
|
||||
const monitorsOnStatusPage: Array<ObjectID> = statusPageResources.map(
|
||||
(monitor: StatusPageResource) => {
|
||||
const monitorsOnStatusPage: Array<ObjectID> = statusPageResources
|
||||
.map((monitor: StatusPageResource) => {
|
||||
return monitor.monitorId!;
|
||||
})
|
||||
.filter((id: ObjectID) => {
|
||||
return Boolean(id); // remove nulls
|
||||
});
|
||||
|
||||
for (const monitorGroupId of monitorGroupIds) {
|
||||
// get current status of monitors in the group.
|
||||
|
||||
// get monitors in the group.
|
||||
|
||||
const groupResources: Array<MonitorGroupResource> =
|
||||
await MonitorGroupResourceService.findBy({
|
||||
query: {
|
||||
monitorGroupId: monitorGroupId,
|
||||
},
|
||||
select: {
|
||||
monitorId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
});
|
||||
|
||||
const monitorsInGroupIds: Array<ObjectID> = groupResources
|
||||
.map((resource: MonitorGroupResource) => {
|
||||
return resource.monitorId!;
|
||||
})
|
||||
.filter((id: ObjectID) => {
|
||||
return Boolean(id); // remove nulls
|
||||
});
|
||||
|
||||
for (const monitorId of monitorsInGroupIds) {
|
||||
if (
|
||||
!monitorsOnStatusPage.find((item: ObjectID) => {
|
||||
return item.toString() === monitorId.toString();
|
||||
})
|
||||
) {
|
||||
monitorsOnStatusPage.push(monitorId);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
monitorsInGroup[monitorGroupId.toString()] = monitorsInGroupIds;
|
||||
}
|
||||
|
||||
const today: Date = OneUptimeDate.getCurrentDate();
|
||||
const historyDays: Date = OneUptimeDate.getSomeDaysAgo(
|
||||
@@ -1623,26 +1868,39 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
|
||||
// check if status page has active incident.
|
||||
let incidents: Array<Incident> = [];
|
||||
|
||||
let selectIncidents: Select<Incident> = {
|
||||
createdAt: true,
|
||||
title: true,
|
||||
description: true,
|
||||
_id: true,
|
||||
incidentSeverity: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
currentIncidentState: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
monitors: {
|
||||
_id: true,
|
||||
},
|
||||
};
|
||||
|
||||
if (statusPage.showIncidentLabelsOnStatusPage) {
|
||||
selectIncidents = {
|
||||
...selectIncidents,
|
||||
labels: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (monitorsOnStatusPage.length > 0) {
|
||||
incidents = await IncidentService.findBy({
|
||||
query: incidentQuery,
|
||||
select: {
|
||||
createdAt: true,
|
||||
title: true,
|
||||
description: true,
|
||||
_id: true,
|
||||
incidentSeverity: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
currentIncidentState: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
monitors: {
|
||||
_id: true,
|
||||
},
|
||||
},
|
||||
select: selectIncidents,
|
||||
sort: {
|
||||
createdAt: SortOrder.Descending,
|
||||
},
|
||||
@@ -1665,23 +1923,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
} as any,
|
||||
projectId: statusPage.projectId!,
|
||||
},
|
||||
select: {
|
||||
createdAt: true,
|
||||
title: true,
|
||||
description: true,
|
||||
_id: true,
|
||||
incidentSeverity: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
currentIncidentState: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
monitors: {
|
||||
_id: true,
|
||||
},
|
||||
},
|
||||
select: selectIncidents,
|
||||
sort: {
|
||||
createdAt: SortOrder.Descending,
|
||||
},
|
||||
@@ -1774,6 +2016,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
incidentStateTimelines,
|
||||
IncidentStateTimeline
|
||||
),
|
||||
monitorsInGroup: JSONFunctions.serialize(monitorsInGroup),
|
||||
};
|
||||
|
||||
return response;
|
||||
|
||||
@@ -17,13 +17,15 @@ export default class BearerTokenAuthorization {
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
if (req.headers['authorization'] || req.headers['Authorization']) {
|
||||
if (
|
||||
req.headers?.['authorization'] ||
|
||||
req.headers?.['Authorization']
|
||||
) {
|
||||
let token: string | undefined | Array<string> =
|
||||
req.headers['authorization'] ||
|
||||
req.headers['Authorization'];
|
||||
token = token?.toString().replace('Bearer ', '');
|
||||
if (token) {
|
||||
token = token.toString().replace('Bearer ', '');
|
||||
|
||||
const tokenData: JSONObject =
|
||||
JSONWebToken.decodeJsonPayload(token);
|
||||
|
||||
@@ -31,10 +33,11 @@ export default class BearerTokenAuthorization {
|
||||
|
||||
return next();
|
||||
}
|
||||
throw new NotAuthorizedException('Invalid bearer token.');
|
||||
}
|
||||
|
||||
throw new NotAuthorizedException('Invalid bearer token.');
|
||||
throw new NotAuthorizedException(
|
||||
'Invalid bearer token, or bearer token not provided.'
|
||||
);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import TableColumnType from 'Common/Types/BaseDatabase/TableColumnType';
|
||||
import ClickhouseDatabase, {
|
||||
ClickhouseAppInstance,
|
||||
ClickhouseClient,
|
||||
@@ -35,14 +34,15 @@ import PositiveNumber from 'Common/Types/PositiveNumber';
|
||||
import SortOrder from 'Common/Types/BaseDatabase/SortOrder';
|
||||
import Query from '../Types/AnalyticsDatabase/Query';
|
||||
import Select from '../Types/AnalyticsDatabase/Select';
|
||||
import Sort from '../Types/AnalyticsDatabase/Sort';
|
||||
import { ExecResult } from '@clickhouse/client';
|
||||
import { Stream } from 'node:stream';
|
||||
import StreamUtil from '../Utils/Stream';
|
||||
import { JSONObject, JSONValue } from 'Common/Types/JSON';
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
import FindOneBy from '../Types/AnalyticsDatabase/FindOneBy';
|
||||
import FindOneByID from '../Types/AnalyticsDatabase/FindOneByID';
|
||||
import OneUptimeDate from 'Common/Types/Date';
|
||||
import CreateManyBy from '../Types/AnalyticsDatabase/CreateManyBy';
|
||||
import StatementGenerator from '../Utils/AnalyticsDatabase/StatementGenerator';
|
||||
|
||||
export default class AnalyticsDatabaseService<
|
||||
TBaseModel extends AnalyticsBaseModel
|
||||
@@ -51,6 +51,7 @@ export default class AnalyticsDatabaseService<
|
||||
public database!: ClickhouseDatabase;
|
||||
public model!: TBaseModel;
|
||||
public databaseClient!: ClickhouseClient;
|
||||
public statementGenerator!: StatementGenerator<TBaseModel>;
|
||||
|
||||
public constructor(data: {
|
||||
modelType: { new (): TBaseModel };
|
||||
@@ -66,6 +67,11 @@ export default class AnalyticsDatabaseService<
|
||||
}
|
||||
|
||||
this.databaseClient = this.database.getDataSource() as ClickhouseClient;
|
||||
|
||||
this.statementGenerator = new StatementGenerator<TBaseModel>({
|
||||
modelType: this.modelType,
|
||||
database: this.database,
|
||||
});
|
||||
}
|
||||
|
||||
public async findBy(
|
||||
@@ -202,94 +208,6 @@ export default class AnalyticsDatabaseService<
|
||||
return items;
|
||||
}
|
||||
|
||||
public toWhereStatement(query: Query<TBaseModel>): string {
|
||||
let whereStatement: string = '';
|
||||
|
||||
for (const key in query) {
|
||||
const value: any = query[key];
|
||||
const tableColumn: AnalyticsTableColumn | null =
|
||||
this.model.getTableColumn(key);
|
||||
|
||||
if (!tableColumn) {
|
||||
throw new BadDataException(`Unknown column: ${key}`);
|
||||
}
|
||||
|
||||
whereStatement += `${key} = ${this.sanitizeValue(
|
||||
value,
|
||||
tableColumn
|
||||
)} AND`;
|
||||
}
|
||||
|
||||
// remove last AND.
|
||||
whereStatement = whereStatement.substring(0, whereStatement.length - 4);
|
||||
|
||||
return whereStatement;
|
||||
}
|
||||
|
||||
public toSortStatemennt(sort: Sort<TBaseModel>): string {
|
||||
let sortStatement: string = '';
|
||||
|
||||
for (const key in sort) {
|
||||
const value: any = sort[key];
|
||||
sortStatement += `${key} ${value}`;
|
||||
}
|
||||
|
||||
return sortStatement;
|
||||
}
|
||||
|
||||
public toSelectStatement(select: Select<TBaseModel>): {
|
||||
statement: string;
|
||||
columns: Array<string>;
|
||||
} {
|
||||
let selectStatement: string = '';
|
||||
const columns: Array<string> = [];
|
||||
|
||||
for (const key in select) {
|
||||
const value: any = select[key];
|
||||
if (value) {
|
||||
columns.push(key);
|
||||
selectStatement += `${key}, `;
|
||||
}
|
||||
}
|
||||
|
||||
selectStatement = selectStatement.substring(
|
||||
0,
|
||||
selectStatement.length - 2
|
||||
); // remove last comma.
|
||||
|
||||
return {
|
||||
columns: columns,
|
||||
statement: selectStatement,
|
||||
};
|
||||
}
|
||||
|
||||
public toTableCreateStatement(): string {
|
||||
if (!this.database) {
|
||||
this.useDefaultDatabase();
|
||||
}
|
||||
|
||||
const statement: string = `CREATE TABLE IF NOT EXISTS ${
|
||||
this.database.getDatasourceOptions().database
|
||||
}.${this.model.tableName}
|
||||
(
|
||||
${this.toColumnsCreateStatement()}
|
||||
)
|
||||
ENGINE = ${this.model.tableEngine}
|
||||
PRIMARY KEY (
|
||||
${this.model.primaryKeys
|
||||
.map((key: string) => {
|
||||
return key;
|
||||
})
|
||||
.join(', ')}
|
||||
)
|
||||
`;
|
||||
|
||||
logger.info(`${this.model.tableName} Table Create Statement`);
|
||||
logger.info(statement);
|
||||
|
||||
return statement;
|
||||
}
|
||||
|
||||
protected async onBeforeDelete(
|
||||
deleteBy: DeleteBy<TBaseModel>
|
||||
): Promise<OnDelete<TBaseModel>> {
|
||||
@@ -320,15 +238,15 @@ export default class AnalyticsDatabaseService<
|
||||
}
|
||||
|
||||
const select: { statement: string; columns: Array<string> } =
|
||||
this.toSelectStatement(findBy.select!);
|
||||
this.statementGenerator.toSelectStatement(findBy.select!);
|
||||
|
||||
const statement: string = `SELECT ${select.statement} FROM ${
|
||||
this.database.getDatasourceOptions().database
|
||||
}.${this.model.tableName}
|
||||
${
|
||||
Object.keys(findBy.query).length > 0 ? 'WHERE' : ''
|
||||
} ${this.toWhereStatement(findBy.query)}
|
||||
ORDER BY ${this.toSortStatemennt(findBy.sort!)}
|
||||
} ${this.statementGenerator.toWhereStatement(findBy.query)}
|
||||
ORDER BY ${this.statementGenerator.toSortStatemennt(findBy.sort!)}
|
||||
LIMIT ${findBy.limit}
|
||||
OFFSET ${findBy.skip}
|
||||
`;
|
||||
@@ -349,7 +267,7 @@ export default class AnalyticsDatabaseService<
|
||||
}.${this.model.tableName}
|
||||
DELETE ${
|
||||
Object.keys(deleteBy.query).length > 0 ? 'WHERE' : 'WHERE 1=1'
|
||||
} ${this.toWhereStatement(deleteBy.query)}
|
||||
} ${this.statementGenerator.toWhereStatement(deleteBy.query)}
|
||||
`;
|
||||
|
||||
logger.info(`${this.model.tableName} Delete Statement`);
|
||||
@@ -358,102 +276,6 @@ export default class AnalyticsDatabaseService<
|
||||
return statement;
|
||||
}
|
||||
|
||||
public toUpdateStatement(updateBy: UpdateBy<TBaseModel>): string {
|
||||
if (!this.database) {
|
||||
this.useDefaultDatabase();
|
||||
}
|
||||
|
||||
const statement: string = `ALTER TABLE ${
|
||||
this.database.getDatasourceOptions().database
|
||||
}.${this.model.tableName}
|
||||
UPDATE ${this.toSetStatement(updateBy.data)}
|
||||
${
|
||||
Object.keys(updateBy.query).length > 0 ? 'WHERE' : 'WHERE 1=1'
|
||||
} ${this.toWhereStatement(updateBy.query)}
|
||||
`;
|
||||
|
||||
logger.info(`${this.model.tableName} Update Statement`);
|
||||
logger.info(statement);
|
||||
|
||||
return statement;
|
||||
}
|
||||
|
||||
public toSetStatement(data: TBaseModel): string {
|
||||
let setStatement: string = '';
|
||||
|
||||
for (const column of data.getTableColumns()) {
|
||||
if (data.getColumnValue(column.key) !== undefined) {
|
||||
setStatement += `${column.key} = ${this.sanitizeValue(
|
||||
data.getColumnValue(column.key),
|
||||
column
|
||||
)}, `;
|
||||
}
|
||||
}
|
||||
|
||||
setStatement = setStatement.substring(0, setStatement.length - 2); // remove last comma.
|
||||
|
||||
return setStatement;
|
||||
}
|
||||
|
||||
public toCreateStatement(data: { item: TBaseModel }): string {
|
||||
if (!data.item) {
|
||||
throw new BadDataException('Item cannot be null');
|
||||
}
|
||||
|
||||
const columnNames: Array<string> = [];
|
||||
const values: Array<string | number | boolean | Date> = [];
|
||||
|
||||
for (const column of data.item.getTableColumns()) {
|
||||
columnNames.push(column.key);
|
||||
|
||||
const value: JSONValue = this.sanitizeValue(
|
||||
data.item.getColumnValue(column.key),
|
||||
column
|
||||
);
|
||||
|
||||
values.push(value as string | number | boolean | Date);
|
||||
}
|
||||
|
||||
const statement: string = `INSERT INTO ${
|
||||
this.database.getDatasourceOptions().database
|
||||
}.${this.model.tableName}
|
||||
(
|
||||
${columnNames.join(', ')}
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
${values.join(', ')}
|
||||
)
|
||||
`;
|
||||
|
||||
logger.info(`${this.model.tableName} Create Statement`);
|
||||
logger.info(statement);
|
||||
|
||||
return statement;
|
||||
}
|
||||
|
||||
private sanitizeValue(
|
||||
value: JSONValue,
|
||||
column: AnalyticsTableColumn
|
||||
): JSONValue {
|
||||
if (
|
||||
column.type === TableColumnType.ObjectID ||
|
||||
column.type === TableColumnType.LongText ||
|
||||
column.type === TableColumnType.VeryLongText ||
|
||||
column.type === TableColumnType.ShortText
|
||||
) {
|
||||
value = `'${value?.toString()}'`;
|
||||
}
|
||||
|
||||
if (column.type === TableColumnType.Date && value instanceof Date) {
|
||||
value = `parseDateTimeBestEffortOrNull('${OneUptimeDate.toString(
|
||||
value as Date
|
||||
)}')`;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public async findOneBy(
|
||||
findOneBy: FindOneBy<TBaseModel>
|
||||
): Promise<TBaseModel | null> {
|
||||
@@ -547,7 +369,9 @@ export default class AnalyticsDatabaseService<
|
||||
(select as any)[tenantColumnName] = true;
|
||||
}
|
||||
|
||||
await this.execute(this.toUpdateStatement(beforeUpdateBy));
|
||||
await this.execute(
|
||||
this.statementGenerator.toUpdateStatement(beforeUpdateBy)
|
||||
);
|
||||
} catch (error) {
|
||||
await this.onUpdateError(error as Exception);
|
||||
throw this.getException(error as Exception);
|
||||
@@ -668,82 +492,125 @@ export default class AnalyticsDatabaseService<
|
||||
return await this.onBeforeCreate(createBy);
|
||||
}
|
||||
|
||||
public async create(createBy: CreateBy<TBaseModel>): Promise<TBaseModel> {
|
||||
const onCreate: OnCreate<TBaseModel> = createBy.props.ignoreHooks
|
||||
? { createBy, carryForward: [] }
|
||||
: await this._onBeforeCreate(createBy);
|
||||
|
||||
const _createdBy: CreateBy<TBaseModel> = onCreate.createBy;
|
||||
|
||||
const carryForward: any = onCreate.carryForward;
|
||||
|
||||
let data: TBaseModel = _createdBy.data;
|
||||
|
||||
public async createMany(
|
||||
createBy: CreateManyBy<TBaseModel>
|
||||
): Promise<Array<TBaseModel>> {
|
||||
// add tenantId if present.
|
||||
const tenantColumnName: string | null =
|
||||
data.getTenantColumn()?.key || null;
|
||||
this.model.getTenantColumn()?.key || null;
|
||||
|
||||
if (tenantColumnName && _createdBy.props.tenantId) {
|
||||
data.setColumnValue(tenantColumnName, _createdBy.props.tenantId);
|
||||
const items: Array<TBaseModel> = [];
|
||||
const carryForwards: Array<any> = [];
|
||||
|
||||
for (const item of createBy.items) {
|
||||
let data: TBaseModel = item;
|
||||
|
||||
const onCreate: OnCreate<TBaseModel> = createBy.props.ignoreHooks
|
||||
? {
|
||||
createBy: {
|
||||
data: data,
|
||||
props: createBy.props,
|
||||
},
|
||||
carryForward: [],
|
||||
}
|
||||
: await this._onBeforeCreate({
|
||||
data: data,
|
||||
props: createBy.props,
|
||||
});
|
||||
|
||||
data = onCreate.createBy.data;
|
||||
|
||||
const carryForward: any = onCreate.carryForward;
|
||||
|
||||
carryForwards.push(carryForward);
|
||||
|
||||
if (tenantColumnName && createBy.props.tenantId) {
|
||||
data.setColumnValue(tenantColumnName, createBy.props.tenantId);
|
||||
}
|
||||
|
||||
data = this.sanitizeCreate(data);
|
||||
data = this.generateDefaultValues(data);
|
||||
data = this.checkRequiredFields(data);
|
||||
|
||||
if (!this.isValid(data)) {
|
||||
throw new BadDataException('Data is not valid');
|
||||
}
|
||||
|
||||
// check total items by
|
||||
|
||||
ModelPermission.checkCreatePermissions(
|
||||
this.modelType,
|
||||
data,
|
||||
createBy.props
|
||||
);
|
||||
|
||||
items.push(data);
|
||||
}
|
||||
|
||||
data = this.sanitizeCreate(data);
|
||||
data = this.generateDefaultValues(data);
|
||||
data = this.checkRequiredFields(data);
|
||||
|
||||
if (!this.isValid(data)) {
|
||||
throw new BadDataException('Data is not valid');
|
||||
}
|
||||
|
||||
// check total items by
|
||||
|
||||
ModelPermission.checkCreatePermissions(
|
||||
this.modelType,
|
||||
data,
|
||||
_createdBy.props
|
||||
);
|
||||
|
||||
createBy.data = data;
|
||||
|
||||
try {
|
||||
await this.execute(this.toCreateStatement({ item: createBy.data }));
|
||||
const insertStatement: string =
|
||||
this.statementGenerator.toCreateStatement({ item: items });
|
||||
|
||||
await this.execute(insertStatement);
|
||||
|
||||
if (!createBy.props.ignoreHooks) {
|
||||
createBy.data = await this.onCreateSuccess(
|
||||
{
|
||||
createBy,
|
||||
carryForward,
|
||||
},
|
||||
createBy.data
|
||||
);
|
||||
for (let i: number = 0; i < items.length; i++) {
|
||||
if (!items[i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
items[i] = await this.onCreateSuccess(
|
||||
{
|
||||
createBy: {
|
||||
data: items[i]!,
|
||||
props: createBy.props,
|
||||
},
|
||||
carryForward: carryForwards[i],
|
||||
},
|
||||
items[i]!
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// hit workflow.;
|
||||
if (this.getModel().enableWorkflowOn?.create) {
|
||||
let tenantId: ObjectID | undefined = createBy.props.tenantId;
|
||||
|
||||
if (!tenantId && this.getModel().getTenantColumn()) {
|
||||
tenantId = createBy.data.getColumnValue<ObjectID>(
|
||||
this.getModel().getTenantColumn()!.key
|
||||
);
|
||||
}
|
||||
for (const item of items) {
|
||||
if (!tenantId && this.getModel().getTenantColumn()) {
|
||||
tenantId = item.getColumnValue<ObjectID>(
|
||||
this.getModel().getTenantColumn()!.key
|
||||
);
|
||||
}
|
||||
|
||||
if (tenantId) {
|
||||
await this.onTrigger(
|
||||
createBy.data.id!,
|
||||
tenantId,
|
||||
'on-create'
|
||||
);
|
||||
if (tenantId) {
|
||||
await this.onTrigger(item.id!, tenantId, 'on-create');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return createBy.data;
|
||||
return createBy.items;
|
||||
} catch (error) {
|
||||
await this.onCreateError(error as Exception);
|
||||
throw this.getException(error as Exception);
|
||||
}
|
||||
}
|
||||
|
||||
public async create(createBy: CreateBy<TBaseModel>): Promise<TBaseModel> {
|
||||
const items: Array<TBaseModel> = await this.createMany({
|
||||
props: createBy.props,
|
||||
items: [createBy.data],
|
||||
});
|
||||
|
||||
const item: TBaseModel | undefined = items[0];
|
||||
|
||||
if (!item) {
|
||||
throw new BadDataException('Item not created');
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private sanitizeCreate<TBaseModel extends AnalyticsBaseModel>(
|
||||
data: TBaseModel
|
||||
): TBaseModel {
|
||||
@@ -774,18 +641,6 @@ export default class AnalyticsDatabaseService<
|
||||
return true;
|
||||
}
|
||||
|
||||
public toColumnsCreateStatement(): string {
|
||||
let columns: string = '';
|
||||
|
||||
this.model.tableColumns.forEach((column: AnalyticsTableColumn) => {
|
||||
columns += `${column.key} ${this.toColumnType(column.type)} ${
|
||||
column.required ? 'NOT NULL' : ' NULL'
|
||||
},\n`;
|
||||
});
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
public async onTrigger(
|
||||
id: ObjectID,
|
||||
projectId: ObjectID,
|
||||
@@ -843,44 +698,4 @@ export default class AnalyticsDatabaseService<
|
||||
public getModel(): TBaseModel {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
public toColumnType(type: TableColumnType): string {
|
||||
if (type === TableColumnType.ShortText) {
|
||||
return 'String';
|
||||
}
|
||||
|
||||
if (type === TableColumnType.LongText) {
|
||||
return 'String';
|
||||
}
|
||||
|
||||
if (type === TableColumnType.VeryLongText) {
|
||||
return 'String';
|
||||
}
|
||||
|
||||
if (type === TableColumnType.ObjectID) {
|
||||
return 'String';
|
||||
}
|
||||
|
||||
if (type === TableColumnType.Boolean) {
|
||||
return 'Bool';
|
||||
}
|
||||
|
||||
if (type === TableColumnType.Number) {
|
||||
return 'Int32';
|
||||
}
|
||||
|
||||
if (type === TableColumnType.BigNumber) {
|
||||
return 'Int64';
|
||||
}
|
||||
|
||||
if (type === TableColumnType.Date) {
|
||||
return 'DateTime';
|
||||
}
|
||||
|
||||
if (type === TableColumnType.Array) {
|
||||
return 'Array';
|
||||
}
|
||||
|
||||
throw new BadDataException('Unknown column type: ' + type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,6 +533,17 @@ export class BillingService extends BaseService {
|
||||
return false;
|
||||
}
|
||||
|
||||
public async setDefaultPaymentMethod(
|
||||
customerId: string,
|
||||
paymentMethodId: string
|
||||
): Promise<void> {
|
||||
await this.stripe.customers.update(customerId, {
|
||||
invoice_settings: {
|
||||
default_payment_method: paymentMethodId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async getPaymentMethods(
|
||||
customerId: string
|
||||
): Promise<Array<PaymentMethod>> {
|
||||
@@ -603,6 +614,26 @@ export class BillingService extends BaseService {
|
||||
});
|
||||
});
|
||||
|
||||
// check if there's a default payment method.
|
||||
|
||||
const customer: Stripe.Response<
|
||||
Stripe.Customer | Stripe.DeletedCustomer
|
||||
> = await this.stripe.customers.retrieve(customerId);
|
||||
|
||||
if (
|
||||
(customer as Stripe.Customer).invoice_settings &&
|
||||
!(customer as Stripe.Customer).invoice_settings
|
||||
?.default_payment_method
|
||||
) {
|
||||
// set the first payment method as default.
|
||||
if (paymentMethods.length > 0 && paymentMethods[0]?.id) {
|
||||
await this.setDefaultPaymentMethod(
|
||||
customerId,
|
||||
paymentMethods[0]?.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return paymentMethods;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import DatabaseCommonInteractionProps from 'Common/Types/BaseDatabase/DatabaseCo
|
||||
import QueryHelper from '../Types/Database/QueryHelper';
|
||||
import { getUniqueColumnsBy } from 'Common/Types/Database/UniqueColumnBy';
|
||||
import Typeof from 'Common/Types/Typeof';
|
||||
import TableColumnType from 'Common/Types/BaseDatabase/TableColumnType';
|
||||
import TableColumnType from 'Common/Types/Database/TableColumnType';
|
||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
import LIMIT_MAX from 'Common/Types/Database/LimitMax';
|
||||
import { TableColumnMetadata } from 'Common/Types/Database/TableColumn';
|
||||
|
||||
@@ -119,6 +119,15 @@ import ResellerPlanService from './ResellerPlanService';
|
||||
|
||||
import PromoCodeService from './PromoCodeService';
|
||||
import LogService from './LogService';
|
||||
import SpanService from './SpanService';
|
||||
import MetricSumService from './MetricSumService';
|
||||
import MetricHistogramService from './MetricHistogramService';
|
||||
import MetricGaugeService from './MetricGaugeService';
|
||||
|
||||
import MonitorGroupService from './MonitorGroupService';
|
||||
import MonitorGroupResourceService from './MonitorGroupResourceService';
|
||||
import MonitorGroupOwnerUserService from './MonitorGroupOwnerUserService';
|
||||
import MonitorGroupOwnerTeamService from './MonitorGroupOwnerTeamService';
|
||||
|
||||
const services: Array<BaseService> = [
|
||||
PromoCodeService,
|
||||
@@ -228,10 +237,22 @@ const services: Array<BaseService> = [
|
||||
WorkflowLogService,
|
||||
WorkflowService,
|
||||
WorkflowVariablesService,
|
||||
|
||||
// Monitor Group Service
|
||||
MonitorGroupService,
|
||||
MonitorGroupResourceService,
|
||||
MonitorGroupOwnerUserService,
|
||||
MonitorGroupOwnerTeamService,
|
||||
];
|
||||
|
||||
export const AnalyticsServices: Array<
|
||||
AnalyticsDatabaseService<AnalyticsBaseModel>
|
||||
> = [LogService];
|
||||
> = [
|
||||
LogService,
|
||||
SpanService,
|
||||
MetricSumService,
|
||||
MetricHistogramService,
|
||||
MetricGaugeService,
|
||||
];
|
||||
|
||||
export default services;
|
||||
|
||||
11
CommonServer/Services/MetricGaugeService.ts
Normal file
11
CommonServer/Services/MetricGaugeService.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import MetricGauge from 'Model/AnalyticsModels/MetricGauge';
|
||||
import AnalyticsDatabaseService from './AnalyticsDatabaseService';
|
||||
import ClickhouseDatabase from '../Infrastructure/ClickhouseDatabase';
|
||||
|
||||
export class MetricGaugeService extends AnalyticsDatabaseService<MetricGauge> {
|
||||
public constructor(clickhouseDatabase?: ClickhouseDatabase | undefined) {
|
||||
super({ modelType: MetricGauge, database: clickhouseDatabase });
|
||||
}
|
||||
}
|
||||
|
||||
export default new MetricGaugeService();
|
||||
11
CommonServer/Services/MetricHistogramService.ts
Normal file
11
CommonServer/Services/MetricHistogramService.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import MetricHistogram from 'Model/AnalyticsModels/MetricHistogram';
|
||||
import AnalyticsDatabaseService from './AnalyticsDatabaseService';
|
||||
import ClickhouseDatabase from '../Infrastructure/ClickhouseDatabase';
|
||||
|
||||
export class MetricHistogramService extends AnalyticsDatabaseService<MetricHistogram> {
|
||||
public constructor(clickhouseDatabase?: ClickhouseDatabase | undefined) {
|
||||
super({ modelType: MetricHistogram, database: clickhouseDatabase });
|
||||
}
|
||||
}
|
||||
|
||||
export default new MetricHistogramService();
|
||||
11
CommonServer/Services/MetricSumService.ts
Normal file
11
CommonServer/Services/MetricSumService.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import MetricSum from 'Model/AnalyticsModels/MetricSum';
|
||||
import AnalyticsDatabaseService from './AnalyticsDatabaseService';
|
||||
import ClickhouseDatabase from '../Infrastructure/ClickhouseDatabase';
|
||||
|
||||
export class MetricSumService extends AnalyticsDatabaseService<MetricSum> {
|
||||
public constructor(clickhouseDatabase?: ClickhouseDatabase | undefined) {
|
||||
super({ modelType: MetricSum, database: clickhouseDatabase });
|
||||
}
|
||||
}
|
||||
|
||||
export default new MetricSumService();
|
||||
10
CommonServer/Services/MonitorGroupOwnerTeamService.ts
Normal file
10
CommonServer/Services/MonitorGroupOwnerTeamService.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import PostgresDatabase from '../Infrastructure/PostgresDatabase';
|
||||
import Model from 'Model/Models/MonitorGroupOwnerTeam';
|
||||
import DatabaseService from './DatabaseService';
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor(postgresDatabase?: PostgresDatabase) {
|
||||
super(Model, postgresDatabase);
|
||||
}
|
||||
}
|
||||
export default new Service();
|
||||
10
CommonServer/Services/MonitorGroupOwnerUserService.ts
Normal file
10
CommonServer/Services/MonitorGroupOwnerUserService.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import PostgresDatabase from '../Infrastructure/PostgresDatabase';
|
||||
import Model from 'Model/Models/MonitorGroupOwnerUser';
|
||||
import DatabaseService from './DatabaseService';
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor(postgresDatabase?: PostgresDatabase) {
|
||||
super(Model, postgresDatabase);
|
||||
}
|
||||
}
|
||||
export default new Service();
|
||||
11
CommonServer/Services/MonitorGroupResourceService.ts
Normal file
11
CommonServer/Services/MonitorGroupResourceService.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import PostgresDatabase from '../Infrastructure/PostgresDatabase';
|
||||
import Model from 'Model/Models/MonitorGroupResource';
|
||||
import DatabaseService from './DatabaseService';
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor(postgresDatabase?: PostgresDatabase) {
|
||||
super(Model, postgresDatabase);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
180
CommonServer/Services/MonitorGroupService.ts
Normal file
180
CommonServer/Services/MonitorGroupService.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import PostgresDatabase from '../Infrastructure/PostgresDatabase';
|
||||
import DatabaseService from './DatabaseService';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import MonitorGroup from 'Model/Models/MonitorGroup';
|
||||
import MonitorStatus from 'Model/Models/MonitorStatus';
|
||||
import DatabaseCommonInteractionProps from 'Common/Types/BaseDatabase/DatabaseCommonInteractionProps';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import MonitorGroupResourceService from './MonitorGroupResourceService';
|
||||
import LIMIT_MAX, { LIMIT_PER_PROJECT } from 'Common/Types/Database/LimitMax';
|
||||
import MonitorStatusService from './MonitorStatusService';
|
||||
import MonitorGroupResource from 'Model/Models/MonitorGroupResource';
|
||||
import MonitorStatusTimeline from 'Model/Models/MonitorStatusTimeline';
|
||||
import MonitorStatusTimelineService from './MonitorStatusTimelineService';
|
||||
import QueryHelper from '../Types/Database/QueryHelper';
|
||||
import SortOrder from 'Common/Types/BaseDatabase/SortOrder';
|
||||
|
||||
export class Service extends DatabaseService<MonitorGroup> {
|
||||
public constructor(postgresDatabase?: PostgresDatabase) {
|
||||
super(MonitorGroup, postgresDatabase);
|
||||
}
|
||||
|
||||
public async getStatusTimeline(
|
||||
monitorGroupId: ObjectID,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
props: DatabaseCommonInteractionProps
|
||||
): Promise<Array<MonitorStatusTimeline>> {
|
||||
const monitorGroup: MonitorGroup | null = await this.findOneById({
|
||||
id: monitorGroupId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
},
|
||||
props: props,
|
||||
});
|
||||
|
||||
if (!monitorGroup) {
|
||||
throw new BadDataException('Monitor group not found.');
|
||||
}
|
||||
|
||||
const monitorGroupResources: Array<MonitorGroupResource> =
|
||||
await MonitorGroupResourceService.findBy({
|
||||
query: {
|
||||
monitorGroupId: monitorGroup.id!,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
select: {
|
||||
monitorId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const monitorStatusTimelines: Array<MonitorStatusTimeline> =
|
||||
await MonitorStatusTimelineService.findBy({
|
||||
query: {
|
||||
monitorId: QueryHelper.in(
|
||||
monitorGroupResources.map(
|
||||
(monitorGroupResource: MonitorGroupResource) => {
|
||||
return monitorGroupResource.monitorId!;
|
||||
}
|
||||
)
|
||||
),
|
||||
createdAt: QueryHelper.inBetween(startDate, endDate),
|
||||
},
|
||||
select: {
|
||||
createdAt: true,
|
||||
monitorId: true,
|
||||
monitorStatus: {
|
||||
name: true,
|
||||
color: true,
|
||||
priority: true,
|
||||
} as any,
|
||||
},
|
||||
sort: {
|
||||
createdAt: SortOrder.Ascending,
|
||||
},
|
||||
skip: 0,
|
||||
limit: LIMIT_MAX, // This can be optimized.
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return monitorStatusTimelines;
|
||||
}
|
||||
|
||||
public async getCurrentStatus(
|
||||
monitorGroupId: ObjectID,
|
||||
props: DatabaseCommonInteractionProps
|
||||
): Promise<MonitorStatus> {
|
||||
// get group id.
|
||||
|
||||
const monitorGroup: MonitorGroup | null = await this.findOneById({
|
||||
id: monitorGroupId,
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
},
|
||||
props: props,
|
||||
});
|
||||
|
||||
if (!monitorGroup) {
|
||||
throw new BadDataException('Monitor group not found.');
|
||||
}
|
||||
|
||||
// now get all the monitors in this group with current status.
|
||||
|
||||
const monitorGroupResources: Array<MonitorGroupResource> =
|
||||
await MonitorGroupResourceService.findBy({
|
||||
query: {
|
||||
monitorGroupId: monitorGroup.id!,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
select: {
|
||||
monitor: {
|
||||
currentMonitorStatusId: true,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const monitorStatuses: Array<MonitorStatus> =
|
||||
await MonitorStatusService.findBy({
|
||||
query: {
|
||||
projectId: monitorGroup.projectId!,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
select: {
|
||||
name: true,
|
||||
color: true,
|
||||
priority: true,
|
||||
isOperationalState: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
let currentStatus: MonitorStatus | undefined = monitorStatuses.find(
|
||||
(monitorStatus: MonitorStatus) => {
|
||||
return monitorStatus.isOperationalState;
|
||||
}
|
||||
);
|
||||
|
||||
if (!currentStatus) {
|
||||
throw new BadDataException('Operational state not found.');
|
||||
}
|
||||
|
||||
for (const monitorGroupResource of monitorGroupResources) {
|
||||
if (!monitorGroupResource.monitor) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const monitorStatus: MonitorStatus | undefined =
|
||||
monitorStatuses.find((monitorStatus: MonitorStatus) => {
|
||||
return (
|
||||
monitorStatus.id?.toString() ===
|
||||
monitorGroupResource.monitor!.currentMonitorStatusId?.toString()
|
||||
);
|
||||
});
|
||||
|
||||
if (
|
||||
monitorStatus &&
|
||||
currentStatus.priority! < monitorStatus.priority!
|
||||
) {
|
||||
currentStatus = monitorStatus;
|
||||
}
|
||||
}
|
||||
|
||||
return currentStatus;
|
||||
}
|
||||
}
|
||||
export default new Service();
|
||||
10
CommonServer/Services/SpanService.ts
Normal file
10
CommonServer/Services/SpanService.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import Span from 'Model/AnalyticsModels/Span';
|
||||
import AnalyticsDatabaseService from './AnalyticsDatabaseService';
|
||||
import ClickhouseDatabase from '../Infrastructure/ClickhouseDatabase';
|
||||
|
||||
export class SpanService extends AnalyticsDatabaseService<Span> {
|
||||
public constructor(clickhouseDatabase?: ClickhouseDatabase | undefined) {
|
||||
super({ modelType: Span, database: clickhouseDatabase });
|
||||
}
|
||||
}
|
||||
export default new SpanService();
|
||||
102
CommonServer/Tests/Middleware/BearerTokenAuthorization.test.ts
Normal file
102
CommonServer/Tests/Middleware/BearerTokenAuthorization.test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import BearerTokenAuthorization from '../../Middleware/BearerTokenAuthorization';
|
||||
import { OneUptimeRequest, ExpressResponse } from '../../Utils/Express';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import JSONWebToken from '../../Utils/JsonWebToken';
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
|
||||
describe('BearerTokenAuthorization', () => {
|
||||
describe('isAuthorizedBearerToken', () => {
|
||||
it('adds decoded token data to request', () => {
|
||||
const jsonObj: JSONObject = { test: 'test' };
|
||||
const req: OneUptimeRequest = {
|
||||
id: ObjectID.generate(),
|
||||
headers: {
|
||||
authorization: `Bearer ${JSONWebToken.signJsonPayload(
|
||||
jsonObj,
|
||||
5
|
||||
)}`,
|
||||
},
|
||||
} as OneUptimeRequest;
|
||||
const res: ExpressResponse = {} as ExpressResponse;
|
||||
const next: jest.Mock = jest.fn();
|
||||
void BearerTokenAuthorization.isAuthorizedBearerToken(
|
||||
req,
|
||||
res,
|
||||
next
|
||||
);
|
||||
const jsonObjResult: JSONObject = req.bearerTokenData as JSONObject;
|
||||
expect(jsonObjResult['test']).toMatchInlineSnapshot(`"test"`);
|
||||
});
|
||||
it('calls next without arguments if token is valid', () => {
|
||||
const jsonObj: JSONObject = { test: 'test' };
|
||||
const req: OneUptimeRequest = {
|
||||
id: ObjectID.generate(),
|
||||
headers: {
|
||||
authorization: `Bearer ${JSONWebToken.signJsonPayload(
|
||||
jsonObj,
|
||||
5
|
||||
)}`,
|
||||
},
|
||||
} as OneUptimeRequest;
|
||||
const res: ExpressResponse = {} as ExpressResponse;
|
||||
const next: jest.Mock = jest.fn();
|
||||
void BearerTokenAuthorization.isAuthorizedBearerToken(
|
||||
req,
|
||||
res,
|
||||
next
|
||||
);
|
||||
expect(next.mock.calls[0][0]).toMatchInlineSnapshot(`undefined`);
|
||||
});
|
||||
it('calls next with exception if token is empty', () => {
|
||||
const req: OneUptimeRequest = {
|
||||
id: ObjectID.generate(),
|
||||
headers: {
|
||||
authorization: '',
|
||||
},
|
||||
} as OneUptimeRequest;
|
||||
const res: ExpressResponse = {} as ExpressResponse;
|
||||
const next: jest.Mock = jest.fn();
|
||||
void BearerTokenAuthorization.isAuthorizedBearerToken(
|
||||
req,
|
||||
res,
|
||||
next
|
||||
);
|
||||
expect(next.mock.calls[0][0]).toMatchInlineSnapshot(
|
||||
`[Error: Invalid bearer token, or bearer token not provided.]`
|
||||
);
|
||||
});
|
||||
it('calls next with exception if token is invalid', () => {
|
||||
const req: OneUptimeRequest = {
|
||||
id: ObjectID.generate(),
|
||||
headers: {
|
||||
authorization: 'Bearer ',
|
||||
},
|
||||
} as OneUptimeRequest;
|
||||
const res: ExpressResponse = {} as ExpressResponse;
|
||||
const next: jest.Mock = jest.fn();
|
||||
void BearerTokenAuthorization.isAuthorizedBearerToken(
|
||||
req,
|
||||
res,
|
||||
next
|
||||
);
|
||||
expect(next.mock.calls[0][0]).toMatchInlineSnapshot(
|
||||
`[Error: Invalid bearer token, or bearer token not provided.]`
|
||||
);
|
||||
});
|
||||
it('calls next with exception if token header is not present', () => {
|
||||
const req: OneUptimeRequest = {
|
||||
id: ObjectID.generate(),
|
||||
} as OneUptimeRequest;
|
||||
const res: ExpressResponse = {} as ExpressResponse;
|
||||
const next: jest.Mock = jest.fn();
|
||||
void BearerTokenAuthorization.isAuthorizedBearerToken(
|
||||
req,
|
||||
res,
|
||||
next
|
||||
);
|
||||
expect(next.mock.calls[0][0]).toMatchInlineSnapshot(
|
||||
`[Error: Invalid bearer token, or bearer token not provided.]`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
187
CommonServer/Tests/Middleware/NotificationMiddleware.test.ts
Normal file
187
CommonServer/Tests/Middleware/NotificationMiddleware.test.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import VoiceResponse from 'twilio/lib/twiml/VoiceResponse';
|
||||
|
||||
import NotificationMiddleware from '../../Middleware/NotificationMiddleware';
|
||||
|
||||
// Helpers
|
||||
import Response from '../../Utils/Response';
|
||||
import JSONWebToken from '../../Utils/JsonWebToken';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import JSONFunctions from 'Common/Types/JSONFunctions';
|
||||
|
||||
// Types
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
} from '../../Utils/Express';
|
||||
import { OnCallInputRequest } from 'Common/Types/Call/CallRequest';
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
|
||||
jest.mock('twilio/lib/twiml/VoiceResponse');
|
||||
jest.mock('../../Utils/Response');
|
||||
jest.mock('../../Utils/JsonWebToken', () => {
|
||||
return {
|
||||
decodeJsonPayload: jest.fn(),
|
||||
};
|
||||
});
|
||||
jest.mock('Common/Types/JSONFunctions', () => {
|
||||
return {
|
||||
deserialize: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('NotificationMiddleware', () => {
|
||||
describe('sendResponse', () => {
|
||||
let mockRequest: ExpressRequest;
|
||||
let mockResponse: ExpressResponse;
|
||||
let onCallInputRequest: OnCallInputRequest;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRequest = { body: { Digits: '1234' } } as ExpressRequest;
|
||||
mockResponse = {} as ExpressResponse;
|
||||
onCallInputRequest = {
|
||||
default: { sayMessage: 'default message' },
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('Should return correct message for a valid Digits value', async () => {
|
||||
onCallInputRequest['1234'] = { sayMessage: 'message 1' };
|
||||
const responseInstance: VoiceResponse = new VoiceResponse();
|
||||
|
||||
(
|
||||
VoiceResponse as jest.MockedClass<typeof VoiceResponse>
|
||||
).mockImplementation(() => {
|
||||
return responseInstance;
|
||||
});
|
||||
await NotificationMiddleware.sendResponse(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
onCallInputRequest
|
||||
);
|
||||
|
||||
expect(responseInstance.say).toHaveBeenCalledWith(
|
||||
(onCallInputRequest['1234'] as any).sayMessage
|
||||
);
|
||||
expect(Response.sendXmlResponse).toHaveBeenCalledWith(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
responseInstance.toString()
|
||||
);
|
||||
});
|
||||
|
||||
test('Should return default message for an invalid Digits value', async () => {
|
||||
const responseInstance: VoiceResponse = new VoiceResponse();
|
||||
|
||||
(
|
||||
VoiceResponse as jest.MockedClass<typeof VoiceResponse>
|
||||
).mockImplementation(() => {
|
||||
return responseInstance;
|
||||
});
|
||||
await NotificationMiddleware.sendResponse(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
onCallInputRequest
|
||||
);
|
||||
|
||||
expect(responseInstance.say).toHaveBeenCalledWith(
|
||||
onCallInputRequest['default']?.sayMessage
|
||||
);
|
||||
expect(Response.sendXmlResponse).toHaveBeenCalledWith(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
responseInstance.toString()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidCallNotificationRequest', () => {
|
||||
let mockRequest: ExpressRequest;
|
||||
let mockResponse: ExpressResponse;
|
||||
let mockNext: NextFunction;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRequest = {
|
||||
body: {},
|
||||
query: {},
|
||||
} as ExpressRequest;
|
||||
mockResponse = {} as ExpressResponse;
|
||||
mockNext = jest.fn() as NextFunction;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('Should return error if Digits is not in req body', async () => {
|
||||
await NotificationMiddleware.isValidCallNotificationRequest(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
mockNext
|
||||
);
|
||||
|
||||
expect(Response.sendErrorResponse).toHaveBeenCalledWith(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
new BadDataException('Invalid input')
|
||||
);
|
||||
});
|
||||
|
||||
test('Should return error if Token is not in req query', async () => {
|
||||
mockRequest.body['Digits'] = '1234';
|
||||
|
||||
await NotificationMiddleware.isValidCallNotificationRequest(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
mockNext
|
||||
);
|
||||
|
||||
expect(Response.sendErrorResponse).toHaveBeenCalledWith(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
new BadDataException('Invalid token')
|
||||
);
|
||||
});
|
||||
|
||||
test('Should return error if token decoding fails', async () => {
|
||||
mockRequest.body['Digits'] = '1234';
|
||||
mockRequest.query['token'] = 'token';
|
||||
|
||||
jest.spyOn(JSONWebToken, 'decodeJsonPayload').mockImplementation(
|
||||
() => {
|
||||
throw new Error('Decoding error');
|
||||
}
|
||||
);
|
||||
await NotificationMiddleware.isValidCallNotificationRequest(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
mockNext
|
||||
);
|
||||
|
||||
expect(Response.sendErrorResponse).toHaveBeenCalledWith(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
new BadDataException('Invalid token')
|
||||
);
|
||||
});
|
||||
|
||||
test("Should call 'next' if data is valid", async () => {
|
||||
mockRequest.body['Digits'] = '1234';
|
||||
mockRequest.query['token'] = 'token';
|
||||
const tokenData: JSONObject = { id: 1 };
|
||||
|
||||
jest.spyOn(JSONFunctions, 'deserialize').mockReturnValue(tokenData);
|
||||
await NotificationMiddleware.isValidCallNotificationRequest(
|
||||
mockRequest,
|
||||
mockResponse,
|
||||
mockNext
|
||||
);
|
||||
|
||||
expect(mockNext).toHaveBeenCalled();
|
||||
expect((mockRequest as any).callTokenData).toEqual(tokenData);
|
||||
});
|
||||
});
|
||||
});
|
||||
123
CommonServer/Tests/Utils/Cookie.test.ts
Normal file
123
CommonServer/Tests/Utils/Cookie.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
import CookieUtil from '../../Utils/Cookie';
|
||||
import { ExpressRequest, ExpressResponse } from '../../Utils/Express';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import Dictionary from 'Common/Types/Dictionary';
|
||||
|
||||
describe('CookieUtils', () => {
|
||||
let mockRequest: ExpressRequest;
|
||||
let mockResponse: ExpressResponse;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRequest = {
|
||||
cookies: {},
|
||||
} as ExpressRequest;
|
||||
|
||||
mockResponse = {} as ExpressResponse;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('Should set a cookie', () => {
|
||||
const cookie: JSONObject = {
|
||||
name: 'testName',
|
||||
value: 'testValue',
|
||||
options: {},
|
||||
};
|
||||
|
||||
mockResponse.cookie = jest.fn();
|
||||
CookieUtil.setCookie(
|
||||
mockResponse,
|
||||
cookie['name'] as string,
|
||||
cookie['value'] as string,
|
||||
cookie['options'] as JSONObject
|
||||
);
|
||||
|
||||
expect(mockResponse.cookie).toHaveBeenCalledWith(
|
||||
cookie['name'] as string,
|
||||
cookie['value'] as string,
|
||||
cookie['options'] as JSONObject
|
||||
);
|
||||
});
|
||||
|
||||
test('Should return a cookie', () => {
|
||||
const cookieName: string = 'testName';
|
||||
const cookieValue: string = 'testValue';
|
||||
|
||||
mockRequest.cookies[cookieName] = cookieValue;
|
||||
const value: string | undefined = CookieUtil.getCookie(
|
||||
mockRequest,
|
||||
cookieName
|
||||
);
|
||||
|
||||
expect(value).toBe(value);
|
||||
});
|
||||
|
||||
test('Should remove a cookie', () => {
|
||||
const cookieName: string = 'testName';
|
||||
|
||||
mockResponse.clearCookie = jest.fn();
|
||||
CookieUtil.removeCookie(mockResponse, cookieName);
|
||||
|
||||
expect(mockResponse.clearCookie).toHaveBeenCalledWith(cookieName);
|
||||
});
|
||||
|
||||
test('Should return all cookies', () => {
|
||||
const value: string = 'testValue';
|
||||
mockRequest.cookies = { testName: value };
|
||||
const cookies: Dictionary<string> =
|
||||
CookieUtil.getAllCookies(mockRequest);
|
||||
|
||||
expect(cookies).toEqual({ testName: value });
|
||||
});
|
||||
|
||||
test('Should return empty object if there are no cookies', () => {
|
||||
mockRequest.cookies = undefined;
|
||||
const cookies: Dictionary<string> =
|
||||
CookieUtil.getAllCookies(mockRequest);
|
||||
|
||||
expect(cookies).toEqual({});
|
||||
});
|
||||
|
||||
test('Should return user token key', () => {
|
||||
const id: string = '123456789';
|
||||
const keyWithId: string = CookieUtil.getUserTokenKey(new ObjectID(id));
|
||||
const keyWithoutId: string = CookieUtil.getUserTokenKey();
|
||||
|
||||
expect(keyWithId).toBe(`user-token-${id}`);
|
||||
expect(keyWithoutId).toBe('user-token');
|
||||
});
|
||||
|
||||
test('Should return SSO key', () => {
|
||||
const ssoKey: string = CookieUtil.getSSOKey();
|
||||
|
||||
expect(ssoKey).toBe('sso-');
|
||||
});
|
||||
|
||||
test('Should return user SSO key', () => {
|
||||
const id: string = '123456789';
|
||||
const userSsoKey: string = CookieUtil.getUserSSOKey(new ObjectID(id));
|
||||
|
||||
expect(userSsoKey).toBe(`sso-${id}`);
|
||||
});
|
||||
|
||||
test('Should remove all cookies', () => {
|
||||
const cookies: Dictionary<string> = {
|
||||
testName1: 'testValue1',
|
||||
testName2: 'testValue2',
|
||||
};
|
||||
|
||||
mockRequest.cookies = cookies;
|
||||
mockResponse.clearCookie = jest.fn();
|
||||
CookieUtil.removeAllCookies(mockRequest, mockResponse);
|
||||
|
||||
expect(mockResponse.clearCookie).toHaveBeenCalledWith(
|
||||
Object.keys(cookies)[0]
|
||||
);
|
||||
expect(mockResponse.clearCookie).toHaveBeenCalledWith(
|
||||
Object.keys(cookies)[1]
|
||||
);
|
||||
});
|
||||
});
|
||||
7
CommonServer/Types/AnalyticsDatabase/CreateManyBy.ts
Normal file
7
CommonServer/Types/AnalyticsDatabase/CreateManyBy.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import AnalyticsBaseModel from 'Common/AnalyticsModels/BaseModel';
|
||||
import DatabaseCommonInteractionProps from 'Common/Types/BaseDatabase/DatabaseCommonInteractionProps';
|
||||
|
||||
export default interface CreateBy<TBaseModel extends AnalyticsBaseModel> {
|
||||
items: Array<TBaseModel>;
|
||||
props: DatabaseCommonInteractionProps;
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import { ColumnAccessControl } from 'Common/Types/BaseDatabase/AccessControl';
|
||||
import RelationSelect from './RelationSelect';
|
||||
import Typeof from 'Common/Types/Typeof';
|
||||
import { TableColumnMetadata } from 'Common/Types/Database/TableColumn';
|
||||
import TableColumnType from 'Common/Types/BaseDatabase/TableColumnType';
|
||||
import TableColumnType from 'Common/Types/Database/TableColumnType';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import LessThan from 'Common/Types/Database/LessThan';
|
||||
import IsNull from 'Common/Types/Database/IsNull';
|
||||
|
||||
449
CommonServer/Utils/AnalyticsDatabase/StatementGenerator.ts
Normal file
449
CommonServer/Utils/AnalyticsDatabase/StatementGenerator.ts
Normal file
@@ -0,0 +1,449 @@
|
||||
import AnalyticsBaseModel from 'Common/AnalyticsModels/BaseModel';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import Query from '../../Types/AnalyticsDatabase/Query';
|
||||
import AnalyticsTableColumn from 'Common/Types/AnalyticsDatabase/TableColumn';
|
||||
import ClickhouseDatabase from '../../Infrastructure/ClickhouseDatabase';
|
||||
import Sort from '../../Types/AnalyticsDatabase/Sort';
|
||||
import Select from '../../Types/AnalyticsDatabase/Select';
|
||||
import TableColumnType from 'Common/Types/AnalyticsDatabase/TableColumnType';
|
||||
import logger from '../Logger';
|
||||
import UpdateBy from '../../Types/AnalyticsDatabase/UpdateBy';
|
||||
import OneUptimeDate from 'Common/Types/Date';
|
||||
import CommonModel, {
|
||||
RecordValue,
|
||||
Record,
|
||||
} from 'Common/AnalyticsModels/CommonModel';
|
||||
|
||||
export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
||||
public model!: TBaseModel;
|
||||
public modelType!: { new (): TBaseModel };
|
||||
public database!: ClickhouseDatabase;
|
||||
|
||||
public constructor(data: {
|
||||
modelType: { new (): TBaseModel };
|
||||
database: ClickhouseDatabase;
|
||||
}) {
|
||||
this.modelType = data.modelType;
|
||||
this.model = new this.modelType();
|
||||
this.database = data.database;
|
||||
}
|
||||
|
||||
public toUpdateStatement(updateBy: UpdateBy<TBaseModel>): string {
|
||||
const statement: string = `ALTER TABLE ${
|
||||
this.database.getDatasourceOptions().database
|
||||
}.${this.model.tableName}
|
||||
UPDATE ${this.toSetStatement(updateBy.data)}
|
||||
${
|
||||
Object.keys(updateBy.query).length > 0 ? 'WHERE' : 'WHERE 1=1'
|
||||
} ${this.toWhereStatement(updateBy.query)}
|
||||
`;
|
||||
|
||||
logger.info(`${this.model.tableName} Update Statement`);
|
||||
logger.info(statement);
|
||||
|
||||
return statement;
|
||||
}
|
||||
|
||||
public getColumnNames(
|
||||
tableColumns: Array<AnalyticsTableColumn>
|
||||
): Array<string> {
|
||||
const columnNames: Array<string> = [];
|
||||
for (const column of tableColumns) {
|
||||
if (column.type === TableColumnType.NestedModel) {
|
||||
// Example of nested model query:
|
||||
|
||||
/**
|
||||
*
|
||||
* INSERT INTO opentelemetry_spans (trace_id, span_id, attributes.key, attributes.value) VALUES
|
||||
('trace1', 'span1', ['key1', 'key2'], ['value1', 'value2']),
|
||||
('trace2', 'span2', ['keyA', 'keyB'], ['valueA', 'valueB']);
|
||||
*/
|
||||
|
||||
// Nested Model Support.
|
||||
const nestedModelColumnNames: Array<string> =
|
||||
this.getColumnNames(column.nestedModel!.tableColumns);
|
||||
|
||||
for (const nestedModelColumnName of nestedModelColumnNames) {
|
||||
columnNames.push(`${column.key}.${nestedModelColumnName}`);
|
||||
}
|
||||
} else {
|
||||
columnNames.push(column.key);
|
||||
}
|
||||
}
|
||||
|
||||
return columnNames;
|
||||
}
|
||||
|
||||
public getRecordValuesStatement(record: Record): string {
|
||||
let valueStatement: string = '';
|
||||
|
||||
for (const value of record) {
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) {
|
||||
valueStatement += `[], `;
|
||||
continue;
|
||||
}
|
||||
|
||||
valueStatement += `[${value.join(',')}], `;
|
||||
} else {
|
||||
valueStatement += `${value}, `;
|
||||
}
|
||||
}
|
||||
|
||||
valueStatement = valueStatement.substring(0, valueStatement.length - 2); // remove last comma.
|
||||
|
||||
return valueStatement;
|
||||
}
|
||||
|
||||
public getValuesStatement(records: Array<Record>): string {
|
||||
let statement: string = '';
|
||||
for (const record of records) {
|
||||
statement += `(${this.getRecordValuesStatement(record)}), `;
|
||||
}
|
||||
|
||||
statement = statement.substring(0, statement.length - 2); // remove last comma.
|
||||
|
||||
return statement;
|
||||
}
|
||||
|
||||
public toCreateStatement(data: { item: Array<TBaseModel> }): string {
|
||||
if (!data.item) {
|
||||
throw new BadDataException('Item cannot be null');
|
||||
}
|
||||
|
||||
const columnNames: Array<string> = this.getColumnNames(
|
||||
this.model.getTableColumns()
|
||||
);
|
||||
|
||||
const records: Array<Record> = [];
|
||||
|
||||
for (const item of data.item) {
|
||||
const record: Record = this.getRecord(item);
|
||||
records.push(record);
|
||||
}
|
||||
|
||||
const statement: string = `INSERT INTO ${
|
||||
this.database.getDatasourceOptions().database
|
||||
}.${this.model.tableName}
|
||||
(
|
||||
${columnNames.join(', ')}
|
||||
)
|
||||
VALUES
|
||||
${this.getValuesStatement(records)}
|
||||
`;
|
||||
|
||||
logger.info(`${this.model.tableName} Create Statement`);
|
||||
logger.info(statement);
|
||||
|
||||
return statement;
|
||||
}
|
||||
|
||||
private getRecord(item: CommonModel): Record {
|
||||
const record: Record = [];
|
||||
|
||||
for (const column of item.getTableColumns()) {
|
||||
if (column.type === TableColumnType.NestedModel) {
|
||||
// Nested Model Support.
|
||||
|
||||
// THis is very werid, but the output should work in a query like this:
|
||||
|
||||
/**
|
||||
*
|
||||
* INSERT INTO opentelemetry_spans (trace_id, span_id, attributes.key, attributes.value) VALUES
|
||||
('trace1', 'span1', ['key1', 'key2'], ['value1', 'value2']),
|
||||
('trace2', 'span2', ['keyA', 'keyB'], ['valueA', 'valueB']);
|
||||
*/
|
||||
|
||||
for (const subColumn of column.nestedModel!.tableColumns) {
|
||||
const subRecord: Record = [];
|
||||
|
||||
for (const nestedModelItem of item.getColumnValue(
|
||||
column.key
|
||||
) as Array<CommonModel>) {
|
||||
const value: RecordValue = this.sanitizeValue(
|
||||
nestedModelItem.getColumnValue(subColumn.key),
|
||||
subColumn,
|
||||
{
|
||||
isNestedModel: true,
|
||||
}
|
||||
);
|
||||
|
||||
subRecord.push(value);
|
||||
}
|
||||
|
||||
record.push(subRecord);
|
||||
}
|
||||
} else {
|
||||
const value: RecordValue | undefined = this.sanitizeValue(
|
||||
item.getColumnValue(column.key),
|
||||
column
|
||||
);
|
||||
|
||||
record.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
private sanitizeValue(
|
||||
value: RecordValue | undefined,
|
||||
column: AnalyticsTableColumn,
|
||||
options?: {
|
||||
isNestedModel?: boolean;
|
||||
}
|
||||
): RecordValue {
|
||||
if (!value && value !== 0 && value !== false) {
|
||||
if (options?.isNestedModel) {
|
||||
if (column.type === TableColumnType.Text) {
|
||||
return `''`;
|
||||
}
|
||||
|
||||
if (column.type === TableColumnType.Number) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 'NULL';
|
||||
}
|
||||
|
||||
if (
|
||||
column.type === TableColumnType.ObjectID ||
|
||||
column.type === TableColumnType.Text
|
||||
) {
|
||||
value = `'${value?.toString()}'`;
|
||||
}
|
||||
|
||||
if (column.type === TableColumnType.Date && value instanceof Date) {
|
||||
value = `parseDateTimeBestEffortOrNull('${OneUptimeDate.toString(
|
||||
value as Date
|
||||
)}')`;
|
||||
}
|
||||
|
||||
if (column.type === TableColumnType.Number) {
|
||||
if (typeof value === 'string') {
|
||||
value = parseInt(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (column.type === TableColumnType.Decimal) {
|
||||
if (typeof value === 'string') {
|
||||
value = parseFloat(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (column.type === TableColumnType.ArrayNumber) {
|
||||
value = `[${(value as Array<number>)
|
||||
.map((v: number) => {
|
||||
return v;
|
||||
})
|
||||
.join(', ')}]`;
|
||||
}
|
||||
|
||||
if (column.type === TableColumnType.ArrayText) {
|
||||
value = `[${(value as Array<string>)
|
||||
.map((v: string) => {
|
||||
return `'${v}'`;
|
||||
})
|
||||
.join(', ')}]`;
|
||||
}
|
||||
|
||||
if (column.type === TableColumnType.JSON) {
|
||||
value = `'${JSON.stringify(value)}'`;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public toSetStatement(data: TBaseModel): string {
|
||||
let setStatement: string = '';
|
||||
|
||||
for (const column of data.getTableColumns()) {
|
||||
if (data.getColumnValue(column.key) !== undefined) {
|
||||
setStatement += `${column.key} = ${this.sanitizeValue(
|
||||
data.getColumnValue(column.key),
|
||||
column
|
||||
)}, `;
|
||||
}
|
||||
}
|
||||
|
||||
setStatement = setStatement.substring(0, setStatement.length - 2); // remove last comma.
|
||||
|
||||
return setStatement;
|
||||
}
|
||||
|
||||
public toWhereStatement(query: Query<TBaseModel>): string {
|
||||
let whereStatement: string = '';
|
||||
|
||||
for (const key in query) {
|
||||
const value: any = query[key];
|
||||
const tableColumn: AnalyticsTableColumn | null =
|
||||
this.model.getTableColumn(key);
|
||||
|
||||
if (!tableColumn) {
|
||||
throw new BadDataException(`Unknown column: ${key}`);
|
||||
}
|
||||
|
||||
whereStatement += `${key} = ${this.sanitizeValue(
|
||||
value,
|
||||
tableColumn
|
||||
)} AND`;
|
||||
}
|
||||
|
||||
// remove last AND.
|
||||
whereStatement = whereStatement.substring(0, whereStatement.length - 4);
|
||||
|
||||
return whereStatement;
|
||||
}
|
||||
|
||||
public toSortStatemennt(sort: Sort<TBaseModel>): string {
|
||||
let sortStatement: string = '';
|
||||
|
||||
for (const key in sort) {
|
||||
const value: any = sort[key];
|
||||
sortStatement += `${key} ${value}`;
|
||||
}
|
||||
|
||||
return sortStatement;
|
||||
}
|
||||
|
||||
public toSelectStatement(select: Select<TBaseModel>): {
|
||||
statement: string;
|
||||
columns: Array<string>;
|
||||
} {
|
||||
let selectStatement: string = '';
|
||||
const columns: Array<string> = [];
|
||||
|
||||
for (const key in select) {
|
||||
const value: any = select[key];
|
||||
if (value) {
|
||||
columns.push(key);
|
||||
selectStatement += `${key}, `;
|
||||
}
|
||||
}
|
||||
|
||||
selectStatement = selectStatement.substring(
|
||||
0,
|
||||
selectStatement.length - 2
|
||||
); // remove last comma.
|
||||
|
||||
return {
|
||||
columns: columns,
|
||||
statement: selectStatement,
|
||||
};
|
||||
}
|
||||
|
||||
public toColumnsCreateStatement(
|
||||
tableColumns: Array<AnalyticsTableColumn>,
|
||||
isNestedModel: boolean = false
|
||||
): string {
|
||||
let columns: string = '';
|
||||
|
||||
tableColumns.forEach((column: AnalyticsTableColumn) => {
|
||||
let requiredText: string = `${
|
||||
column.required ? 'NOT NULL' : ' NULL'
|
||||
}`;
|
||||
|
||||
let nestedModelColumns: string = '';
|
||||
|
||||
if (column.type === TableColumnType.NestedModel) {
|
||||
nestedModelColumns = `(
|
||||
${this.toColumnsCreateStatement(
|
||||
column.nestedModel!.tableColumns,
|
||||
true
|
||||
)}
|
||||
)`;
|
||||
|
||||
requiredText = '';
|
||||
}
|
||||
|
||||
if (isNestedModel) {
|
||||
requiredText = '';
|
||||
}
|
||||
|
||||
if (
|
||||
column.type === TableColumnType.ArrayNumber ||
|
||||
column.type === TableColumnType.ArrayText
|
||||
) {
|
||||
requiredText = '';
|
||||
}
|
||||
|
||||
columns += `${column.key} ${this.toColumnType(
|
||||
column.type
|
||||
)} ${nestedModelColumns} ${requiredText},\n`;
|
||||
});
|
||||
|
||||
return columns.substring(0, columns.length - 2); // remove last comma.
|
||||
}
|
||||
|
||||
public toColumnType(type: TableColumnType): string {
|
||||
if (type === TableColumnType.Text) {
|
||||
return 'String';
|
||||
}
|
||||
|
||||
if (type === TableColumnType.ObjectID) {
|
||||
return 'String';
|
||||
}
|
||||
|
||||
if (type === TableColumnType.Boolean) {
|
||||
return 'Bool';
|
||||
}
|
||||
|
||||
if (type === TableColumnType.Number) {
|
||||
return 'Int32';
|
||||
}
|
||||
|
||||
if (type === TableColumnType.Decimal) {
|
||||
return 'Double';
|
||||
}
|
||||
|
||||
if (type === TableColumnType.Date) {
|
||||
return 'DateTime';
|
||||
}
|
||||
|
||||
if (type === TableColumnType.JSON) {
|
||||
return 'JSON';
|
||||
}
|
||||
|
||||
if (type === TableColumnType.NestedModel) {
|
||||
return 'Nested';
|
||||
}
|
||||
|
||||
if (type === TableColumnType.ArrayNumber) {
|
||||
return 'Array(Int32)';
|
||||
}
|
||||
|
||||
if (type === TableColumnType.ArrayText) {
|
||||
return 'Array(String)';
|
||||
}
|
||||
|
||||
if (type === TableColumnType.LongNumber) {
|
||||
return 'Int128';
|
||||
}
|
||||
|
||||
throw new BadDataException('Unknown column type: ' + type);
|
||||
}
|
||||
|
||||
public toTableCreateStatement(): string {
|
||||
const statement: string = `CREATE TABLE IF NOT EXISTS ${
|
||||
this.database.getDatasourceOptions().database
|
||||
}.${this.model.tableName}
|
||||
(
|
||||
${this.toColumnsCreateStatement(this.model.tableColumns)}
|
||||
)
|
||||
ENGINE = ${this.model.tableEngine}
|
||||
PRIMARY KEY (
|
||||
${this.model.primaryKeys
|
||||
.map((key: string) => {
|
||||
return key;
|
||||
})
|
||||
.join(', ')}
|
||||
)
|
||||
`;
|
||||
|
||||
logger.info(`${this.model.tableName} Table Create Statement`);
|
||||
logger.info(statement);
|
||||
|
||||
return statement;
|
||||
}
|
||||
}
|
||||
@@ -175,9 +175,13 @@ export default class Response {
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
list: Array<BaseModel>,
|
||||
count: PositiveNumber,
|
||||
count: PositiveNumber | number,
|
||||
modelType: { new (): BaseModel }
|
||||
): void {
|
||||
if (!(count instanceof PositiveNumber)) {
|
||||
count = new PositiveNumber(count);
|
||||
}
|
||||
|
||||
return this.sendJsonArrayResponse(
|
||||
req,
|
||||
res,
|
||||
|
||||
@@ -26,12 +26,7 @@ import StatusCode from 'Common/Types/API/StatusCode';
|
||||
import Typeof from 'Common/Types/Typeof';
|
||||
import Response from './Response';
|
||||
import JSONFunctions from 'Common/Types/JSONFunctions';
|
||||
import API from 'Common/Utils/API';
|
||||
import URL from 'Common/Types/API/URL';
|
||||
import { AppVersion, DashboardApiHostname } from '../EnvironmentConfig';
|
||||
import { DashboardApiRoute } from 'Common/ServiceRoute';
|
||||
import HTTPResponse from 'Common/Types/API/HTTPResponse';
|
||||
import HTTPErrorResponse from 'Common/Types/API/HTTPErrorResponse';
|
||||
import { AppVersion } from '../EnvironmentConfig';
|
||||
import ServerException from 'Common/Types/Exception/ServerException';
|
||||
import zlib from 'zlib';
|
||||
import CookieParser from 'cookie-parser';
|
||||
@@ -182,26 +177,8 @@ const init: Function = async (
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
// ping api server for database config.
|
||||
|
||||
const databaseConfig:
|
||||
| HTTPResponse<JSONObject>
|
||||
| HTTPErrorResponse = await API.get<JSONObject>(
|
||||
URL.fromString(
|
||||
`http://${DashboardApiHostname}/${DashboardApiRoute}/global-config/vars`
|
||||
)
|
||||
);
|
||||
|
||||
if (databaseConfig instanceof HTTPErrorResponse) {
|
||||
// error getting database config.
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new ServerException('Error getting database config.')
|
||||
);
|
||||
}
|
||||
|
||||
const env: JSONObject = {
|
||||
...process.env,
|
||||
...databaseConfig.data,
|
||||
};
|
||||
|
||||
const script: string = `
|
||||
|
||||
@@ -28,5 +28,5 @@
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
|
||||
"\\.(css|less)$": "identity-obj-proxy",
|
||||
"uuid":"<rootDir>/node_modules/jest-runtime/build/index.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,12 +36,16 @@ const Card: FunctionComponent<ComponentProps> = (
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<h2
|
||||
id="payment-details-heading"
|
||||
data-testid="card-details-heading"
|
||||
id="card-details-heading"
|
||||
className="text-lg font-medium leading-6 text-gray-900"
|
||||
>
|
||||
{props.title}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
<p
|
||||
data-testid="card-description"
|
||||
className="mt-1 text-sm text-gray-500"
|
||||
>
|
||||
{props.description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -78,6 +82,7 @@ const Card: FunctionComponent<ComponentProps> = (
|
||||
shortcutKey={
|
||||
button.shortcutKey
|
||||
}
|
||||
dataTestId="card-button"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -83,6 +83,7 @@ const DictionaryOfStrings: FunctionComponent<ComponentProps> = (
|
||||
</div>
|
||||
<div className="ml-1 mt-1">
|
||||
<Button
|
||||
dataTestId={`delete-${item.key}`}
|
||||
title="Delete"
|
||||
buttonStyle={ButtonStyleType.ICON}
|
||||
icon={IconProp.Trash}
|
||||
|
||||
@@ -25,6 +25,11 @@ export interface TimelineItem {
|
||||
iconColor: Color;
|
||||
}
|
||||
|
||||
export interface EventItemLabel {
|
||||
name: string;
|
||||
color: Color;
|
||||
}
|
||||
|
||||
export interface ComponentProps {
|
||||
eventTitle: string;
|
||||
eventResourcesAffected?: Array<string> | undefined;
|
||||
@@ -40,6 +45,7 @@ export interface ComponentProps {
|
||||
anotherStatus?: string | undefined;
|
||||
anotherStatusColor?: Color | undefined;
|
||||
eventSecondDescription: string;
|
||||
labels?: Array<EventItemLabel> | undefined;
|
||||
}
|
||||
|
||||
const EventItem: FunctionComponent<ComponentProps> = (
|
||||
@@ -105,6 +111,25 @@ const EventItem: FunctionComponent<ComponentProps> = (
|
||||
{props.eventMiniDescription}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{props.labels && props.labels.length > 0 ? (
|
||||
<div className="flex space-x-1 mt-3">
|
||||
{props.labels.map(
|
||||
(label: EventItemLabel, i: number) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<Pill
|
||||
text={label.name}
|
||||
color={label.color}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{props.eventResourcesAffected &&
|
||||
|
||||
@@ -92,6 +92,10 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
|
||||
props.submitButtonText || 'Submit'
|
||||
);
|
||||
|
||||
const [formSteps, setFormSteps] = useState<
|
||||
Array<FormStep<T>> | undefined
|
||||
>(props.steps);
|
||||
|
||||
const isInitialValuesSet: MutableRefObject<boolean> = useRef(false);
|
||||
|
||||
const refCurrentValue: React.MutableRefObject<FormValues<T>> = useRef(
|
||||
@@ -103,8 +107,8 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
|
||||
>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.steps && props.steps.length > 0 && props.steps[0]) {
|
||||
setCurrentFormStepId(props.steps[0].id);
|
||||
if (formSteps && formSteps.length > 0 && formSteps[0]) {
|
||||
setCurrentFormStepId(formSteps[0].id);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -112,11 +116,11 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
|
||||
// if last step,
|
||||
|
||||
if (
|
||||
props.steps &&
|
||||
props.steps.length > 0 &&
|
||||
formSteps &&
|
||||
formSteps.length > 0 &&
|
||||
(
|
||||
(props.steps as Array<FormStep<T>>)[
|
||||
props.steps.length - 1
|
||||
(formSteps as Array<FormStep<T>>)[
|
||||
formSteps.length - 1
|
||||
] as FormStep<T>
|
||||
).id === currentFormStepId
|
||||
) {
|
||||
@@ -141,7 +145,7 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
|
||||
props.onIsLastFormStep(true);
|
||||
}
|
||||
}
|
||||
}, [currentFormStepId]);
|
||||
}, [currentFormStepId, formSteps]);
|
||||
|
||||
const [currentValue, setCurrentValue] = useState<FormValues<T>>(
|
||||
props.initialValues || {}
|
||||
@@ -150,6 +154,18 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
|
||||
const [errors, setErrors] = useState<Dictionary<string>>({});
|
||||
const [touched, setTouched] = useState<Dictionary<boolean>>({});
|
||||
|
||||
useEffect(() => {
|
||||
setFormSteps(
|
||||
props.steps?.filter((step: FormStep<T>) => {
|
||||
if (!step.showIf) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return step.showIf(refCurrentValue.current);
|
||||
})
|
||||
);
|
||||
}, [refCurrentValue.current]);
|
||||
|
||||
const [formFields, setFormFields] = useState<Fields<T>>([]);
|
||||
|
||||
const setFieldTouched: (fieldName: string, value: boolean) => void = (
|
||||
@@ -289,11 +305,11 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
|
||||
// if last step then submit.
|
||||
|
||||
if (
|
||||
(props.steps &&
|
||||
props.steps.length > 0 &&
|
||||
(formSteps &&
|
||||
formSteps.length > 0 &&
|
||||
(
|
||||
(props.steps as Array<FormStep<T>>)[
|
||||
props.steps.length - 1
|
||||
(formSteps as Array<FormStep<T>>)[
|
||||
formSteps.length - 1
|
||||
] as FormStep<T>
|
||||
).id === currentFormStepId) ||
|
||||
currentFormStepId === null
|
||||
@@ -369,16 +385,8 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
|
||||
UiAnalytics.capture('FORM SUBMIT: ' + props.name);
|
||||
|
||||
props.onSubmit(values);
|
||||
} else if (props.steps && props.steps.length > 0) {
|
||||
const steps: Array<FormStep<T>> = props.steps.filter(
|
||||
(step: FormStep<T>) => {
|
||||
if (!step.showIf) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return step.showIf(refCurrentValue.current);
|
||||
}
|
||||
);
|
||||
} else if (formSteps && formSteps.length > 0) {
|
||||
const steps: Array<FormStep<T>> = formSteps;
|
||||
|
||||
const currentStepIndex: number = steps.findIndex(
|
||||
(step: FormStep<T>) => {
|
||||
@@ -487,13 +495,13 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
|
||||
)}
|
||||
|
||||
<div className="flex">
|
||||
{props.steps && currentFormStepId && (
|
||||
{formSteps && currentFormStepId && (
|
||||
<div className="w-1/3">
|
||||
{/* Form Steps */}
|
||||
|
||||
<Steps
|
||||
currentFormStepId={currentFormStepId}
|
||||
steps={props.steps}
|
||||
steps={formSteps}
|
||||
formValues={refCurrentValue.current}
|
||||
onClick={(step: FormStep<T>) => {
|
||||
setCurrentFormStepId(step.id);
|
||||
@@ -503,7 +511,7 @@ const BasicForm: ForwardRefExoticComponent<any> = forwardRef(
|
||||
)}
|
||||
<div
|
||||
className={`${
|
||||
props.steps && currentFormStepId
|
||||
formSteps && currentFormStepId
|
||||
? 'w-2/3 pt-6'
|
||||
: 'w-full pt-1'
|
||||
}`}
|
||||
|
||||
@@ -26,7 +26,7 @@ import { ColumnAccessControl } from 'Common/Types/BaseDatabase/AccessControl';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import { LIMIT_PER_PROJECT } from 'Common/Types/Database/LimitMax';
|
||||
import FileModel from 'Common/Models/FileModel';
|
||||
import TableColumnType from 'Common/Types/BaseDatabase/TableColumnType';
|
||||
import TableColumnType from 'Common/Types/Database/TableColumnType';
|
||||
import Typeof from 'Common/Types/Typeof';
|
||||
import { TableColumnMetadata } from 'Common/Types/Database/TableColumn';
|
||||
import { ButtonStyleType } from '../Button/Button';
|
||||
|
||||
@@ -146,6 +146,14 @@ const Icon: FunctionComponent<ComponentProps> = ({
|
||||
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75"
|
||||
/>
|
||||
);
|
||||
} else if (icon === IconProp.Squares) {
|
||||
return getSvgWrapper(
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z"
|
||||
/>
|
||||
);
|
||||
} else if (icon === IconProp.Pencil) {
|
||||
return getSvgWrapper(
|
||||
<path
|
||||
@@ -154,6 +162,14 @@ const Icon: FunctionComponent<ComponentProps> = ({
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
|
||||
/>
|
||||
);
|
||||
} else if (icon === IconProp.Flag) {
|
||||
return getSvgWrapper(
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M3 3v1.5M3 21v-6m0 0l2.77-.693a9 9 0 016.208.682l.108.054a9 9 0 006.086.71l3.114-.732a48.524 48.524 0 01-.005-10.499l-3.11.732a9 9 0 01-6.085-.711l-.108-.054a9 9 0 00-6.208-.682L3 4.5M3 15V4.5"
|
||||
/>
|
||||
);
|
||||
} else if (icon === IconProp.Bolt) {
|
||||
return getSvgWrapper(
|
||||
<path
|
||||
@@ -162,6 +178,14 @@ const Icon: FunctionComponent<ComponentProps> = ({
|
||||
d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"
|
||||
/>
|
||||
);
|
||||
} else if (icon === IconProp.ListBullet) {
|
||||
return getSvgWrapper(
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm-.375 5.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
|
||||
/>
|
||||
);
|
||||
} else if (icon === IconProp.Template) {
|
||||
return getSvgWrapper(
|
||||
<path
|
||||
|
||||
@@ -9,7 +9,7 @@ import Navigation from '../../Utils/Navigation';
|
||||
export interface ComponentProps {
|
||||
children: ReactElement | Array<ReactElement> | string;
|
||||
className?: undefined | string;
|
||||
to: Route | URL | null | undefined;
|
||||
to?: Route | URL | null | undefined;
|
||||
onClick?: undefined | (() => void);
|
||||
onNavigateComplete?: (() => void) | undefined;
|
||||
openInNewTab?: boolean | undefined;
|
||||
|
||||
@@ -2,6 +2,7 @@ import BarLoader from 'react-spinners/BarLoader';
|
||||
import BeatLoader from 'react-spinners/BeatLoader';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import Color from 'Common/Types/Color';
|
||||
import { VeryLightGrey } from 'Common/Types/BrandColors';
|
||||
|
||||
export enum LoaderType {
|
||||
Bar,
|
||||
@@ -16,12 +17,12 @@ export interface ComponentProps {
|
||||
|
||||
const Loader: FunctionComponent<ComponentProps> = ({
|
||||
size = 50,
|
||||
color = new Color('#000000'),
|
||||
color = VeryLightGrey,
|
||||
loaderType = LoaderType.Bar,
|
||||
}: ComponentProps) => {
|
||||
if (loaderType === LoaderType.Bar) {
|
||||
return (
|
||||
<div role="bar-loader" className="justify-center">
|
||||
<div role="bar-loader mt-1" className="justify-center">
|
||||
<BarLoader height={4} width={size} color={color.toString()} />
|
||||
</div>
|
||||
);
|
||||
@@ -29,7 +30,7 @@ const Loader: FunctionComponent<ComponentProps> = ({
|
||||
|
||||
if (loaderType === LoaderType.Beats) {
|
||||
return (
|
||||
<div role="beat-loader" className="justify-center">
|
||||
<div role="beat-loader mt-1" className="justify-center">
|
||||
<BeatLoader size={size} color={color.toString()} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -38,7 +38,10 @@ const ConfirmModal: FunctionComponent<ComponentProps> = (
|
||||
}
|
||||
error={props.error}
|
||||
>
|
||||
<div className="text-gray-500 mt-5 text-sm">
|
||||
<div
|
||||
data-testid="confirm-modal-description"
|
||||
className="text-gray-500 mt-5 text-sm"
|
||||
>
|
||||
{props.description}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -108,6 +108,7 @@ const Modal: FunctionComponent<ComponentProps> = (
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<h3
|
||||
data-testid="modal-title"
|
||||
className={`text-lg font-medium leading-6 text-gray-900 ${
|
||||
props.icon
|
||||
? 'ml-10 -mt-8 mb-5'
|
||||
@@ -118,7 +119,10 @@ const Modal: FunctionComponent<ComponentProps> = (
|
||||
{props.title}
|
||||
</h3>
|
||||
{props.description && (
|
||||
<h3 className="text-sm leading-6 text-gray-500">
|
||||
<h3
|
||||
data-testid="modal-description"
|
||||
className="text-sm leading-6 text-gray-500"
|
||||
>
|
||||
{props.description}
|
||||
</h3>
|
||||
)}
|
||||
|
||||
@@ -40,6 +40,7 @@ const ModalFooter: FunctionComponent<ComponentProps> = (
|
||||
? props.submitButtonType
|
||||
: ButtonType.Button
|
||||
}
|
||||
dataTestId="modal-footer-submit-button"
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
@@ -57,6 +58,7 @@ const ModalFooter: FunctionComponent<ComponentProps> = (
|
||||
onClick={() => {
|
||||
props.onClose && props.onClose();
|
||||
}}
|
||||
dataTestId="modal-footer-close-button"
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
|
||||
@@ -620,9 +620,14 @@ const ModelTable: <TBaseModel extends BaseModel>(
|
||||
name: true,
|
||||
};
|
||||
} else if (key && model.isEntityColumn(key)) {
|
||||
(relationSelect as JSONObject)[key] = (column.field as any)[
|
||||
key
|
||||
];
|
||||
if (!(relationSelect as JSONObject)[key]) {
|
||||
(relationSelect as JSONObject)[key] = {};
|
||||
}
|
||||
|
||||
(relationSelect as JSONObject)[key] = {
|
||||
...((relationSelect as JSONObject)[key] as JSONObject),
|
||||
...(column.field as any)[key],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@ import OneUptimeDate from 'Common/Types/Date';
|
||||
import DayUptimeGraph, { Event } from '../Graphs/DayUptimeGraph';
|
||||
import { Green } from 'Common/Types/BrandColors';
|
||||
import ErrorMessage from '../ErrorMessage/ErrorMessage';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
|
||||
export interface MonitorEvent extends Event {
|
||||
monitorId: ObjectID;
|
||||
}
|
||||
|
||||
export interface ComponentProps {
|
||||
startDate: Date;
|
||||
@@ -28,27 +33,173 @@ const MonitorUptimeGraph: FunctionComponent<ComponentProps> = (
|
||||
): ReactElement => {
|
||||
const [events, setEvents] = useState<Array<Event>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const eventList: Array<Event> = [];
|
||||
// convert data to events.
|
||||
for (let i: number = 0; i < props.items.length; i++) {
|
||||
if (!props.items[i]) {
|
||||
break;
|
||||
/**
|
||||
* This function, `getMonitorEventsForId`, takes a `monitorId` as an argument and returns an array of `MonitorEvent` objects.
|
||||
* @param {ObjectID} monitorId - The ID of the monitor for which events are to be fetched.
|
||||
* @returns {Array<MonitorEvent>} - An array of `MonitorEvent` objects.
|
||||
*/
|
||||
const getMonitorEventsForId: (
|
||||
monitorId: ObjectID
|
||||
) => Array<MonitorEvent> = (monitorId: ObjectID): Array<MonitorEvent> => {
|
||||
// Initialize an empty array to store the monitor events.
|
||||
const eventList: Array<MonitorEvent> = [];
|
||||
|
||||
const monitorEvents: Array<MonitorStatusTimeline> = props.items.filter(
|
||||
(item: MonitorStatusTimeline) => {
|
||||
return item.monitorId?.toString() === monitorId.toString();
|
||||
}
|
||||
);
|
||||
|
||||
// Loop through the items in the props object.
|
||||
for (let i: number = 0; i < monitorEvents.length; i++) {
|
||||
// If the current item is null or undefined, skip to the next iteration.
|
||||
if (!monitorEvents[i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set the start date of the event to the creation date of the current item. If it doesn't exist, use the current date.
|
||||
const startDate: Date =
|
||||
monitorEvents[i]!.createdAt || OneUptimeDate.getCurrentDate();
|
||||
|
||||
// Initialize the end date as the current date.
|
||||
let endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
|
||||
// If there is a next item and it has a creation date, use that as the end date.
|
||||
if (monitorEvents[i + 1] && monitorEvents[i + 1]!.createdAt) {
|
||||
endDate = monitorEvents[i + 1]!.createdAt!;
|
||||
}
|
||||
|
||||
// Push a new MonitorEvent object to the eventList array with properties from the current item and calculated dates.
|
||||
eventList.push({
|
||||
startDate:
|
||||
props.items[i]!.createdAt || OneUptimeDate.getCurrentDate(),
|
||||
endDate:
|
||||
props.items[i + 1] && props.items[i + 1]!.createdAt
|
||||
? (props.items[i + 1]?.createdAt as Date)
|
||||
: OneUptimeDate.getCurrentDate(),
|
||||
label: props.items[i]?.monitorStatus?.name || 'Operational',
|
||||
priority: props.items[i]?.monitorStatus?.priority || 0,
|
||||
color: props.items[i]?.monitorStatus?.color || Green,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
label: monitorEvents[i]?.monitorStatus?.name || 'Operational',
|
||||
priority: monitorEvents[i]?.monitorStatus?.priority || 0,
|
||||
color: monitorEvents[i]?.monitorStatus?.color || Green,
|
||||
monitorId: monitorEvents[i]!.monitorId!,
|
||||
});
|
||||
}
|
||||
|
||||
// Return the populated eventList array.
|
||||
return eventList;
|
||||
};
|
||||
|
||||
const getMonitorEvents: () => Array<MonitorEvent> =
|
||||
(): Array<MonitorEvent> => {
|
||||
// get all distinct monitor ids.
|
||||
const monitorIds: Array<ObjectID> = [];
|
||||
|
||||
for (let i: number = 0; i < props.items.length; i++) {
|
||||
if (!props.items[i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const monitorId: string | undefined =
|
||||
props.items[i]!.monitorId?.toString();
|
||||
|
||||
if (!monitorId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!monitorIds.find((item: ObjectID) => {
|
||||
return item.toString() === monitorId;
|
||||
})
|
||||
) {
|
||||
monitorIds.push(new ObjectID(monitorId));
|
||||
}
|
||||
}
|
||||
|
||||
const eventList: Array<MonitorEvent> = [];
|
||||
// convert data to events.
|
||||
|
||||
for (const monitorId of monitorIds) {
|
||||
const monitorEvents: Array<MonitorEvent> =
|
||||
getMonitorEventsForId(monitorId);
|
||||
eventList.push(...monitorEvents);
|
||||
}
|
||||
|
||||
// sort event list by start date.
|
||||
eventList.sort((a: MonitorEvent, b: MonitorEvent) => {
|
||||
if (OneUptimeDate.isAfter(a.startDate, b.startDate)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (OneUptimeDate.isAfter(b.startDate, a.startDate)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
return [...eventList];
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const monitorEventList: Array<Event> = getMonitorEvents();
|
||||
|
||||
const eventList: Array<Event> = [];
|
||||
|
||||
for (const monitorEvent of monitorEventList) {
|
||||
// if this event starts after the last event, then add it to the list directly.
|
||||
if (
|
||||
eventList.length === 0 ||
|
||||
OneUptimeDate.isAfter(
|
||||
monitorEvent.startDate,
|
||||
eventList[eventList.length - 1]!.endDate
|
||||
) ||
|
||||
OneUptimeDate.isEqualBySeconds(
|
||||
monitorEvent.startDate,
|
||||
eventList[eventList.length - 1]!.endDate
|
||||
)
|
||||
) {
|
||||
eventList.push(monitorEvent);
|
||||
continue;
|
||||
}
|
||||
|
||||
// if this event starts before the last event, then we need to check if it ends before the last event. If it does, then we can skip this event if the monitrEvent is of lower priority than the last event. If it is of higher priority, then we need to add it to the list and remove the last event from the list.
|
||||
if (
|
||||
OneUptimeDate.isBefore(
|
||||
monitorEvent.startDate,
|
||||
eventList[eventList.length - 1]!.endDate
|
||||
)
|
||||
) {
|
||||
if (
|
||||
monitorEvent.priority >
|
||||
eventList[eventList.length - 1]!.priority
|
||||
) {
|
||||
// end the last event at the start of this event.
|
||||
|
||||
const tempLastEvent: Event = {
|
||||
...eventList[eventList.length - 1],
|
||||
} as Event;
|
||||
|
||||
eventList[eventList.length - 1]!.endDate =
|
||||
monitorEvent.startDate;
|
||||
eventList.push(monitorEvent);
|
||||
|
||||
// if the monitorEvent endDate is before the end of the last event, then we need to add the end of the last event to the list.
|
||||
|
||||
if (
|
||||
OneUptimeDate.isBefore(
|
||||
monitorEvent.endDate,
|
||||
tempLastEvent.endDate
|
||||
)
|
||||
) {
|
||||
eventList.push({
|
||||
startDate: monitorEvent.endDate,
|
||||
endDate: tempLastEvent.endDate,
|
||||
label: tempLastEvent.label,
|
||||
priority: tempLastEvent.priority,
|
||||
color: tempLastEvent.color,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
setEvents(eventList);
|
||||
}, [props.items]);
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ const Statusbubble: FunctionComponent<ComponentProps> = (
|
||||
return (
|
||||
<div className="flex" style={props.style}>
|
||||
<div
|
||||
className="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full mr-2"
|
||||
className="animate-pulse flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full mr-2"
|
||||
style={{
|
||||
backgroundColor: props.color
|
||||
? props.color.toString()
|
||||
|
||||
128
CommonUI/src/Tests/Components/DictionaryOfStrings.test.tsx
Normal file
128
CommonUI/src/Tests/Components/DictionaryOfStrings.test.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
// Libraries
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { Dictionary } from 'lodash';
|
||||
|
||||
// Custom components
|
||||
import DictionaryOfStrings, {
|
||||
ComponentProps,
|
||||
} from '../../Components/Dictionary/DictionaryOfStrings';
|
||||
|
||||
jest.mock('Common/Types/Date', () => {
|
||||
return {
|
||||
toDateTimeLocalString: jest.fn(),
|
||||
asDateForDatabaseQuery: jest.fn(),
|
||||
fromString: jest.fn(),
|
||||
toString: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Dictionary Of Strings', () => {
|
||||
const initialValue: Dictionary<string> = {
|
||||
key: 'value',
|
||||
};
|
||||
|
||||
const defaultProps: ComponentProps = {
|
||||
initialValue,
|
||||
onChange: jest.fn(),
|
||||
keyPlaceholder: 'KeyPlaceholder',
|
||||
valuePlaceholder: 'ValuePlaceholder',
|
||||
addButtonSuffix: 'Attribute',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('Should not show any row if no initialValue is provided', () => {
|
||||
render(
|
||||
<DictionaryOfStrings {...{ ...defaultProps, initialValue: {} }} />
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByPlaceholderText(defaultProps.keyPlaceholder as string)
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: new RegExp(`Add ${defaultProps.addButtonSuffix}`, 'i'),
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should show the rows if initialValue is provided', () => {
|
||||
render(<DictionaryOfStrings {...defaultProps} />);
|
||||
const key: string = Object.keys(initialValue)[0] || '';
|
||||
const value: string = initialValue[key] || '';
|
||||
|
||||
expect(screen.getByDisplayValue(key)).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue(value)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should call onChange function with correct values when input changes', () => {
|
||||
render(<DictionaryOfStrings {...defaultProps} />);
|
||||
|
||||
fireEvent.change(
|
||||
screen.getByPlaceholderText(defaultProps.keyPlaceholder as string),
|
||||
{
|
||||
target: { value: 'testKey' },
|
||||
}
|
||||
);
|
||||
fireEvent.change(
|
||||
screen.getByPlaceholderText(
|
||||
defaultProps.valuePlaceholder as string
|
||||
),
|
||||
{
|
||||
target: { value: 'testValue' },
|
||||
}
|
||||
);
|
||||
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith({
|
||||
testKey: 'testValue',
|
||||
});
|
||||
});
|
||||
|
||||
it('Should add a new row when the Add button is clicked', () => {
|
||||
render(<DictionaryOfStrings {...defaultProps} />);
|
||||
fireEvent.click(
|
||||
screen.getByText(`Add ${defaultProps.addButtonSuffix}`)
|
||||
);
|
||||
|
||||
const keyInputs: HTMLInputElement[] = screen.getAllByPlaceholderText(
|
||||
defaultProps.keyPlaceholder as string
|
||||
);
|
||||
const valueInputs: HTMLInputElement[] = screen.getAllByPlaceholderText(
|
||||
defaultProps.valuePlaceholder as string
|
||||
);
|
||||
|
||||
expect(keyInputs.length).toBe(2);
|
||||
expect(valueInputs.length).toBe(2);
|
||||
});
|
||||
|
||||
it('Should delete a row when the Delete button is clicked', () => {
|
||||
const initialValue: Dictionary<string> = {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
};
|
||||
render(
|
||||
<DictionaryOfStrings
|
||||
{...defaultProps}
|
||||
initialValue={initialValue}
|
||||
/>
|
||||
);
|
||||
|
||||
const key1: string = Object.keys(initialValue)[0] || '';
|
||||
const value1: string = initialValue[key1] || '';
|
||||
const key2: string = Object.keys(initialValue)[1] || '';
|
||||
const value2: string = initialValue[key2] || '';
|
||||
const deleteButtons: HTMLButtonElement = screen.getByTestId(
|
||||
`delete-${key2}`
|
||||
);
|
||||
fireEvent.click(deleteButtons);
|
||||
|
||||
expect(screen.queryByDisplayValue(key1)).toBeInTheDocument();
|
||||
expect(screen.queryByDisplayValue(value1)).toBeInTheDocument();
|
||||
expect(screen.queryByDisplayValue(key2)).not.toBeInTheDocument();
|
||||
expect(screen.queryByDisplayValue(value2)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
322
CommonUI/src/Tests/Components/DuplicateModel.test.tsx
Normal file
322
CommonUI/src/Tests/Components/DuplicateModel.test.tsx
Normal file
@@ -0,0 +1,322 @@
|
||||
import React from 'react';
|
||||
import BaseModel from 'Common/Models/BaseModel';
|
||||
import Select from '../../Utils/ModelAPI/Select';
|
||||
import { ModelField } from '../../Components/Forms/ModelForm';
|
||||
import TableMetaData from 'Common/Types/Database/TableMetadata';
|
||||
import IconProp from 'Common/Types/Icon/IconProp';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
within,
|
||||
fireEvent,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import DuplicateModel from '../../Components/DuplicateModel/DuplicateModel';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import CrudApiEndpoint from 'Common/Types/Database/CrudApiEndpoint';
|
||||
import { act } from 'react-test-renderer';
|
||||
import Route from 'Common/Types/API/Route';
|
||||
|
||||
@TableMetaData({
|
||||
tableName: 'Foo',
|
||||
singularName: 'Foo',
|
||||
pluralName: 'Foos',
|
||||
icon: IconProp.Wrench,
|
||||
tableDescription: 'A test model',
|
||||
})
|
||||
@CrudApiEndpoint(new Route('/testModel'))
|
||||
class TestModel extends BaseModel {
|
||||
public changeThis?: string = 'original';
|
||||
}
|
||||
|
||||
jest.mock('../../Utils/ModelAPI/ModelAPI', () => {
|
||||
return {
|
||||
getItem: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
changeThis: 'changed',
|
||||
setValue: function (key: 'changeThis', value: string) {
|
||||
this[key] = value;
|
||||
},
|
||||
removeValue: jest.fn(),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
changeThis: 'changed',
|
||||
setValue: function (key: 'changeThis', value: string) {
|
||||
this[key] = value;
|
||||
},
|
||||
removeValue: jest.fn(),
|
||||
})
|
||||
.mockResolvedValueOnce(undefined),
|
||||
create: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
id: 'foobar',
|
||||
changeThis: 'changed',
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce(undefined),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../Utils/Navigation', () => {
|
||||
return {
|
||||
navigate: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('DuplicateModel', () => {
|
||||
const fieldsToDuplicate: Select<TestModel> = {};
|
||||
const fieldsToChange: Array<ModelField<TestModel>> = [
|
||||
{
|
||||
field: {
|
||||
changeThis: true,
|
||||
},
|
||||
title: 'Change This',
|
||||
required: false,
|
||||
placeholder: 'You can change this',
|
||||
},
|
||||
];
|
||||
it('renders correctly', () => {
|
||||
render(
|
||||
<DuplicateModel
|
||||
modelType={TestModel}
|
||||
modelId={new ObjectID('foo')}
|
||||
fieldsToDuplicate={fieldsToDuplicate}
|
||||
fieldsToChange={fieldsToChange}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('card-details-heading')?.textContent).toBe(
|
||||
'Duplicate Foo'
|
||||
);
|
||||
expect(screen.getByTestId('card-description')?.textContent).toBe(
|
||||
'Duplicating this foo will create another foo exactly like this one.'
|
||||
);
|
||||
expect(screen.getByTestId('card-button')?.textContent).toBe(
|
||||
'Duplicate Foo'
|
||||
);
|
||||
});
|
||||
it('shows confirmation modal when duplicate button is clicked', () => {
|
||||
render(
|
||||
<DuplicateModel
|
||||
modelType={TestModel}
|
||||
modelId={new ObjectID('foo')}
|
||||
fieldsToDuplicate={fieldsToDuplicate}
|
||||
fieldsToChange={fieldsToChange}
|
||||
/>
|
||||
);
|
||||
const button: HTMLElement = screen.getByRole('button', {
|
||||
name: 'Duplicate Foo',
|
||||
});
|
||||
fireEvent.click(button);
|
||||
expect(screen.getByRole('dialog')).toBeDefined();
|
||||
const confirmDialog: HTMLElement = screen.getByRole('dialog');
|
||||
expect(
|
||||
within(confirmDialog).getByTestId('modal-title')?.textContent
|
||||
).toBe('Duplicate Foo');
|
||||
expect(
|
||||
within(confirmDialog).getByTestId('modal-description')?.textContent
|
||||
).toBe('Are you sure you want to duplicate this foo?');
|
||||
expect(
|
||||
within(confirmDialog).getByTestId('modal-footer-submit-button')
|
||||
?.textContent
|
||||
).toBe('Duplicate Foo');
|
||||
expect(
|
||||
within(confirmDialog).getByTestId('modal-footer-close-button')
|
||||
?.textContent
|
||||
).toBe('Close');
|
||||
});
|
||||
it('duplicates item when confirmation button is clicked', async () => {
|
||||
const onDuplicateSuccess: (item: TestModel) => void = jest.fn();
|
||||
render(
|
||||
<DuplicateModel
|
||||
modelType={TestModel}
|
||||
modelId={new ObjectID('foo')}
|
||||
fieldsToDuplicate={fieldsToDuplicate}
|
||||
fieldsToChange={fieldsToChange}
|
||||
onDuplicateSuccess={onDuplicateSuccess}
|
||||
navigateToOnSuccess={new Route('/done')}
|
||||
/>
|
||||
);
|
||||
const button: HTMLElement = screen.getByRole('button', {
|
||||
name: 'Duplicate Foo',
|
||||
});
|
||||
void act(() => {
|
||||
fireEvent.click(button);
|
||||
});
|
||||
const dialog: HTMLElement = screen.getByRole('dialog');
|
||||
const confirmationButton: HTMLElement = within(dialog).getByRole(
|
||||
'button',
|
||||
{
|
||||
name: 'Duplicate Foo',
|
||||
}
|
||||
);
|
||||
void act(() => {
|
||||
fireEvent.click(confirmationButton);
|
||||
});
|
||||
await waitFor(() => {
|
||||
return expect(onDuplicateSuccess).toBeCalledWith({
|
||||
id: 'foobar',
|
||||
changeThis: 'changed',
|
||||
});
|
||||
});
|
||||
await waitFor(() => {
|
||||
return expect(
|
||||
require('../../Utils/Navigation').navigate
|
||||
).toBeCalledWith(new Route('/done/foobar'));
|
||||
});
|
||||
});
|
||||
it('closes confirmation dialog when close button is clicked', () => {
|
||||
const onDuplicateSuccess: (item: TestModel) => void = jest.fn();
|
||||
render(
|
||||
<DuplicateModel
|
||||
modelType={TestModel}
|
||||
modelId={new ObjectID('foo')}
|
||||
fieldsToDuplicate={fieldsToDuplicate}
|
||||
fieldsToChange={fieldsToChange}
|
||||
onDuplicateSuccess={onDuplicateSuccess}
|
||||
navigateToOnSuccess={new Route('/done')}
|
||||
/>
|
||||
);
|
||||
const button: HTMLElement = screen.getByRole('button', {
|
||||
name: 'Duplicate Foo',
|
||||
});
|
||||
void act(() => {
|
||||
fireEvent.click(button);
|
||||
});
|
||||
const dialog: HTMLElement = screen.getByRole('dialog');
|
||||
const closeButton: HTMLElement = within(dialog).getByRole('button', {
|
||||
name: 'Close',
|
||||
});
|
||||
void act(() => {
|
||||
fireEvent.click(closeButton);
|
||||
});
|
||||
expect(screen.queryByRole('dialog')).toBeFalsy();
|
||||
});
|
||||
it('handles could not create error correctly', async () => {
|
||||
const onDuplicateSuccess: (item: TestModel) => void = jest.fn();
|
||||
render(
|
||||
<DuplicateModel
|
||||
modelType={TestModel}
|
||||
modelId={new ObjectID('foo')}
|
||||
fieldsToDuplicate={fieldsToDuplicate}
|
||||
fieldsToChange={fieldsToChange}
|
||||
onDuplicateSuccess={onDuplicateSuccess}
|
||||
navigateToOnSuccess={new Route('/done')}
|
||||
/>
|
||||
);
|
||||
const button: HTMLElement = screen.getByRole('button', {
|
||||
name: 'Duplicate Foo',
|
||||
});
|
||||
void act(() => {
|
||||
fireEvent.click(button);
|
||||
});
|
||||
const dialog: HTMLElement = screen.getByRole('dialog');
|
||||
const confirmationButton: HTMLElement = within(dialog).getByRole(
|
||||
'button',
|
||||
{
|
||||
name: 'Duplicate Foo',
|
||||
}
|
||||
);
|
||||
void act(() => {
|
||||
fireEvent.click(confirmationButton);
|
||||
});
|
||||
await screen.findByText('Duplicate Error');
|
||||
const errorDialog: HTMLElement = screen.getByRole('dialog');
|
||||
expect(
|
||||
within(errorDialog).getByTestId('modal-title')?.textContent
|
||||
).toBe('Duplicate Error');
|
||||
expect(
|
||||
within(errorDialog).getByTestId('confirm-modal-description')
|
||||
?.textContent
|
||||
).toBe('Error: Could not create Foo');
|
||||
expect(
|
||||
within(errorDialog).getByTestId('modal-footer-submit-button')
|
||||
?.textContent
|
||||
).toBe('Close');
|
||||
});
|
||||
it('handles item not found error correctly', async () => {
|
||||
const onDuplicateSuccess: (item: TestModel) => void = jest.fn();
|
||||
render(
|
||||
<DuplicateModel
|
||||
modelType={TestModel}
|
||||
modelId={new ObjectID('foo')}
|
||||
fieldsToDuplicate={fieldsToDuplicate}
|
||||
fieldsToChange={fieldsToChange}
|
||||
onDuplicateSuccess={onDuplicateSuccess}
|
||||
navigateToOnSuccess={new Route('/done')}
|
||||
/>
|
||||
);
|
||||
const button: HTMLElement = screen.getByRole('button', {
|
||||
name: 'Duplicate Foo',
|
||||
});
|
||||
void act(() => {
|
||||
fireEvent.click(button);
|
||||
});
|
||||
const dialog: HTMLElement = screen.getByRole('dialog');
|
||||
const confirmationButton: HTMLElement = within(dialog).getByRole(
|
||||
'button',
|
||||
{
|
||||
name: 'Duplicate Foo',
|
||||
}
|
||||
);
|
||||
void act(() => {
|
||||
fireEvent.click(confirmationButton);
|
||||
});
|
||||
await screen.findByText('Duplicate Error');
|
||||
const errorDialog: HTMLElement = screen.getByRole('dialog');
|
||||
expect(
|
||||
within(errorDialog).getByTestId('modal-title')?.textContent
|
||||
).toBe('Duplicate Error');
|
||||
expect(
|
||||
within(errorDialog).getByTestId('confirm-modal-description')
|
||||
?.textContent
|
||||
).toBe('Error: Could not find Foo with id foo');
|
||||
expect(
|
||||
within(errorDialog).getByTestId('modal-footer-submit-button')
|
||||
?.textContent
|
||||
).toBe('Close');
|
||||
});
|
||||
it('closes error dialog when close button is clicked', async () => {
|
||||
const onDuplicateSuccess: (item: TestModel) => void = jest.fn();
|
||||
render(
|
||||
<DuplicateModel
|
||||
modelType={TestModel}
|
||||
modelId={new ObjectID('foo')}
|
||||
fieldsToDuplicate={fieldsToDuplicate}
|
||||
fieldsToChange={fieldsToChange}
|
||||
onDuplicateSuccess={onDuplicateSuccess}
|
||||
navigateToOnSuccess={new Route('/done')}
|
||||
/>
|
||||
);
|
||||
const button: HTMLElement = screen.getByRole('button', {
|
||||
name: 'Duplicate Foo',
|
||||
});
|
||||
void act(() => {
|
||||
fireEvent.click(button);
|
||||
});
|
||||
const dialog: HTMLElement = screen.getByRole('dialog');
|
||||
const confirmationButton: HTMLElement = within(dialog).getByRole(
|
||||
'button',
|
||||
{
|
||||
name: 'Duplicate Foo',
|
||||
}
|
||||
);
|
||||
void act(() => {
|
||||
fireEvent.click(confirmationButton);
|
||||
});
|
||||
await screen.findByText('Duplicate Error');
|
||||
const errorDialog: HTMLElement = screen.getByRole('dialog');
|
||||
const closeButton: HTMLElement = within(errorDialog).getByRole(
|
||||
'button',
|
||||
{
|
||||
name: 'Close',
|
||||
}
|
||||
);
|
||||
void act(() => {
|
||||
fireEvent.click(closeButton);
|
||||
});
|
||||
expect(screen.queryByRole('dialog')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
153
CommonUI/src/Tests/Components/SideMenuItem.test.tsx
Normal file
153
CommonUI/src/Tests/Components/SideMenuItem.test.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
// Libraries
|
||||
import React from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
|
||||
// Components
|
||||
import SideMenuItem, {
|
||||
ComponentProps,
|
||||
} from '../../Components/SideMenu/SideMenuItem';
|
||||
import * as Navigation from '../../Utils/Navigation';
|
||||
|
||||
// Types
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import IconProp from 'Common/Types/Icon/IconProp';
|
||||
import { BadgeType } from '../../Components/Badge/Badge';
|
||||
|
||||
const highlightClassList: string =
|
||||
'bg-gray-100 text-indigo-600 hover:bg-white group rounded-md px-3 py-2 flex items-center text-sm font-medium';
|
||||
|
||||
jest.mock('../../Utils/Navigation.ts', () => {
|
||||
return {
|
||||
isOnThisPage: jest.fn().mockReturnValue(false),
|
||||
navigate: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Side Menu Item', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const defaultProps: ComponentProps = {
|
||||
link: {
|
||||
title: 'Home',
|
||||
to: new Route('/home'),
|
||||
},
|
||||
};
|
||||
|
||||
it('Should render the main link with given title', () => {
|
||||
render(<SideMenuItem {...defaultProps} />);
|
||||
|
||||
const mainLink: HTMLAnchorElement | null = screen
|
||||
.getByText(defaultProps.link.title)
|
||||
.closest('a');
|
||||
expect(mainLink).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should call navigate function when clicked', () => {
|
||||
render(<SideMenuItem {...defaultProps} />);
|
||||
|
||||
const mainLink: HTMLAnchorElement = screen
|
||||
.getByText(defaultProps.link.title)
|
||||
.closest('a') as HTMLAnchorElement;
|
||||
|
||||
fireEvent.click(mainLink);
|
||||
|
||||
expect(Navigation.default.navigate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should render icon if provided', () => {
|
||||
render(<SideMenuItem {...defaultProps} icon={IconProp.Home} />);
|
||||
|
||||
expect(screen.getByRole('icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render the sub item link with given title and Icon', () => {
|
||||
const subLink: {
|
||||
title: string;
|
||||
to: Route;
|
||||
} = {
|
||||
title: 'Sub Page',
|
||||
to: new Route('/sub-page'),
|
||||
};
|
||||
render(
|
||||
<SideMenuItem
|
||||
{...defaultProps}
|
||||
subItemLink={subLink}
|
||||
icon={IconProp.Home}
|
||||
subItemIcon={IconProp.ExternalLink}
|
||||
/>
|
||||
);
|
||||
|
||||
const subLinkElement: HTMLAnchorElement | null = screen
|
||||
.getByText(subLink.title)
|
||||
.closest('a');
|
||||
|
||||
expect(subLinkElement).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('icon')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('Should render link badge if provided', () => {
|
||||
const badgeCount: number = 2;
|
||||
render(
|
||||
<SideMenuItem
|
||||
{...defaultProps}
|
||||
badge={badgeCount}
|
||||
badgeType={BadgeType.SUCCESS}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(badgeCount)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should show alert', () => {
|
||||
render(<SideMenuItem {...defaultProps} showAlert={true} />);
|
||||
|
||||
expect(screen.getByRole('icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should show warning', () => {
|
||||
render(<SideMenuItem {...defaultProps} showWarning={true} />);
|
||||
|
||||
expect(screen.getByRole('icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should highlights the main link when on the same page', () => {
|
||||
(Navigation.default.isOnThisPage as jest.Mock).mockReturnValue(true);
|
||||
render(<SideMenuItem {...defaultProps} />);
|
||||
|
||||
const mainLink: HTMLAnchorElement | null = screen
|
||||
.getByText(defaultProps.link.title)
|
||||
.closest('a');
|
||||
expect(mainLink).toHaveClass(highlightClassList);
|
||||
});
|
||||
|
||||
it('Should highlights sub item link when on the same page', () => {
|
||||
const subLink: {
|
||||
title: string;
|
||||
to: Route;
|
||||
} = {
|
||||
title: 'Sub Page',
|
||||
to: new Route('/sub-page'),
|
||||
};
|
||||
Navigation.default.isOnThisPage = jest
|
||||
.fn()
|
||||
.mockImplementation((to: Route) => {
|
||||
return to === subLink.to;
|
||||
});
|
||||
render(
|
||||
<SideMenuItem
|
||||
{...defaultProps}
|
||||
subItemLink={subLink}
|
||||
icon={IconProp.Home}
|
||||
subItemIcon={IconProp.ExternalLink}
|
||||
/>
|
||||
);
|
||||
|
||||
const subLinkElement: HTMLAnchorElement | null = screen
|
||||
.getByText(subLink.title)
|
||||
.closest('a');
|
||||
expect(subLinkElement).toHaveClass(highlightClassList);
|
||||
});
|
||||
});
|
||||
@@ -101,23 +101,31 @@ class BaseAPI extends API {
|
||||
error instanceof HTTPErrorResponse &&
|
||||
(error.statusCode === 401 || error.statusCode === 405)
|
||||
) {
|
||||
const loginRoute: Route = this.getLoginRoute();
|
||||
|
||||
const cookies: Cookies = new Cookies();
|
||||
cookies.remove('admin-data', { path: '/' });
|
||||
cookies.remove('data', { path: '/' });
|
||||
User.clear();
|
||||
|
||||
if (Navigation.getQueryStringByName('sso_token')) {
|
||||
Navigation.navigate(
|
||||
new Route('/accounts/login').addRouteParam('sso', 'true')
|
||||
);
|
||||
if (Navigation.getQueryStringByName('token')) {
|
||||
Navigation.navigate(loginRoute.addRouteParam('sso', 'true'), {
|
||||
forceNavigate: true,
|
||||
});
|
||||
} else {
|
||||
Navigation.navigate(new Route('/accounts/login'));
|
||||
Navigation.navigate(loginRoute, {
|
||||
forceNavigate: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
protected static getLoginRoute(): Route {
|
||||
return new Route('/accounts/login');
|
||||
}
|
||||
|
||||
public static getFriendlyMessage(
|
||||
err: HTTPErrorResponse | Exception | unknown
|
||||
): string {
|
||||
|
||||
@@ -3,8 +3,9 @@ import Email from 'Common/Types/Email';
|
||||
import { JSONObject, JSONValue } from 'Common/Types/JSON';
|
||||
import Typeof from 'Common/Types/Typeof';
|
||||
import JSONFunctions from 'Common/Types/JSONFunctions';
|
||||
import UniversalCookies from 'universal-cookie';
|
||||
import UniversalCookies, { CookieSetOptions } from 'universal-cookie';
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import OneUptimeDate from 'Common/Types/Date';
|
||||
|
||||
export default class Cookie {
|
||||
public static setItem(
|
||||
@@ -14,6 +15,7 @@ export default class Cookie {
|
||||
| {
|
||||
httpOnly?: boolean | undefined;
|
||||
path: Route;
|
||||
maxAgeInDays?: number | undefined;
|
||||
}
|
||||
| undefined
|
||||
): void {
|
||||
@@ -25,10 +27,19 @@ export default class Cookie {
|
||||
}
|
||||
|
||||
const cookies: UniversalCookies = new UniversalCookies();
|
||||
cookies.set(key, value as string, {
|
||||
|
||||
const cookieOptions: CookieSetOptions = {
|
||||
httpOnly: options?.httpOnly || false,
|
||||
path: options?.path ? options.path.toString() : '/',
|
||||
});
|
||||
};
|
||||
|
||||
if (options?.maxAgeInDays) {
|
||||
cookieOptions.maxAge = OneUptimeDate.getMillisecondsInDays(
|
||||
options.maxAgeInDays
|
||||
);
|
||||
}
|
||||
|
||||
cookies.set(key, value as string, cookieOptions);
|
||||
}
|
||||
|
||||
public static getItem(key: string): JSONValue {
|
||||
|
||||
@@ -355,12 +355,21 @@ export default class ModelAPI {
|
||||
);
|
||||
}
|
||||
|
||||
return this.post<TBaseModel>(modelType, apiUrl, select, requestOptions);
|
||||
}
|
||||
|
||||
public static async post<TBaseModel extends BaseModel>(
|
||||
modelType: { new (): TBaseModel },
|
||||
apiUrl: URL,
|
||||
select?: Select<TBaseModel> | undefined,
|
||||
requestOptions?: RequestOptions | undefined
|
||||
): Promise<TBaseModel | null> {
|
||||
const result: HTTPResponse<TBaseModel> | HTTPErrorResponse =
|
||||
await API.fetch<TBaseModel>(
|
||||
HTTPMethod.POST,
|
||||
apiUrl,
|
||||
{
|
||||
select: JSONFunctions.serialize(select as JSONObject),
|
||||
select: JSONFunctions.serialize(select as JSONObject) || {},
|
||||
},
|
||||
this.getCommonHeaders(requestOptions)
|
||||
);
|
||||
|
||||
@@ -90,6 +90,7 @@ import SettingsApiKeyView from './Pages/Settings/APIKeyView';
|
||||
import SettingLabels from './Pages/Settings/Labels';
|
||||
import SettingProbes from './Pages/Settings/Probes';
|
||||
import SettingCustomSMTP from './Pages/Settings/CustomSMTP';
|
||||
import SettingFeatureFlags from './Pages/Settings/FeatureFlags';
|
||||
import SettingsTeams from './Pages/Settings/Teams';
|
||||
import SettingsTeamView from './Pages/Settings/TeamView';
|
||||
import SettingsMonitors from './Pages/Settings/MonitorStatus';
|
||||
@@ -151,6 +152,14 @@ import MonitorViewProbes from './Pages/Monitor/View/Probes';
|
||||
import MonitorViewOwner from './Pages/Monitor/View/Owners';
|
||||
import MonitorViewSettings from './Pages/Monitor/View/Settings';
|
||||
|
||||
// Monitor Groups.
|
||||
import MonitorGroups from './Pages/MonitorGroup/MonitorGroups';
|
||||
import MonitorGroupView from './Pages/MonitorGroup/View/Index';
|
||||
import MonitorGroupViewDelete from './Pages/MonitorGroup/View/Delete';
|
||||
import MonitorGroupViewMonitors from './Pages/MonitorGroup/View/Monitors';
|
||||
import MonitorGroupViewIncidents from './Pages/MonitorGroup/View/Incidents';
|
||||
import MonitorGroupViewOwners from './Pages/MonitorGroup/View/Owners';
|
||||
|
||||
import User from 'CommonUI/src/Utils/User';
|
||||
import Logout from './Pages/Logout/Logout';
|
||||
import ModelAPI, { ListResult } from 'CommonUI/src/Utils/ModelAPI/ModelAPI';
|
||||
@@ -1542,6 +1551,23 @@ const App: () => JSX.Element = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={
|
||||
RouteMap[PageMap.SETTINGS_FEATURE_FLAGS]?.toString() ||
|
||||
''
|
||||
}
|
||||
element={
|
||||
<SettingFeatureFlags
|
||||
{...commonPageProps}
|
||||
pageRoute={
|
||||
RouteMap[
|
||||
PageMap.SETTINGS_FEATURE_FLAGS
|
||||
] as Route
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={
|
||||
RouteMap[
|
||||
@@ -2324,6 +2350,106 @@ const App: () => JSX.Element = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
{/** Monitor Groups */}
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.MONITOR_GROUPS]?.toString() || ''}
|
||||
element={
|
||||
<MonitorGroups
|
||||
{...commonPageProps}
|
||||
pageRoute={
|
||||
RouteMap[PageMap.MONITOR_GROUPS] as Route
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={
|
||||
RouteMap[PageMap.MONITOR_GROUP_VIEW]?.toString() || ''
|
||||
}
|
||||
element={
|
||||
<MonitorGroupView
|
||||
{...commonPageProps}
|
||||
pageRoute={
|
||||
RouteMap[PageMap.MONITOR_GROUP_VIEW] as Route
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={
|
||||
RouteMap[
|
||||
PageMap.MONITOR_GROUP_VIEW_DELETE
|
||||
]?.toString() || ''
|
||||
}
|
||||
element={
|
||||
<MonitorGroupViewDelete
|
||||
{...commonPageProps}
|
||||
pageRoute={
|
||||
RouteMap[
|
||||
PageMap.MONITOR_GROUP_VIEW_DELETE
|
||||
] as Route
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={
|
||||
RouteMap[
|
||||
PageMap.MONITOR_GROUP_VIEW_MONITORS
|
||||
]?.toString() || ''
|
||||
}
|
||||
element={
|
||||
<MonitorGroupViewMonitors
|
||||
{...commonPageProps}
|
||||
pageRoute={
|
||||
RouteMap[
|
||||
PageMap.MONITOR_GROUP_VIEW_MONITORS
|
||||
] as Route
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={
|
||||
RouteMap[
|
||||
PageMap.MONITOR_GROUP_VIEW_INCIDENTS
|
||||
]?.toString() || ''
|
||||
}
|
||||
element={
|
||||
<MonitorGroupViewIncidents
|
||||
{...commonPageProps}
|
||||
pageRoute={
|
||||
RouteMap[
|
||||
PageMap.MONITOR_GROUP_VIEW_INCIDENTS
|
||||
] as Route
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={
|
||||
RouteMap[
|
||||
PageMap.MONITOR_GROUP_VIEW_OWNERS
|
||||
]?.toString() || ''
|
||||
}
|
||||
element={
|
||||
<MonitorGroupViewOwners
|
||||
{...commonPageProps}
|
||||
pageRoute={
|
||||
RouteMap[
|
||||
PageMap.MONITOR_GROUP_VIEW_OWNERS
|
||||
] as Route
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 👇️ only match this when no other routes match */}
|
||||
<PageRoute
|
||||
path="*"
|
||||
|
||||
@@ -5,10 +5,13 @@ import Route from 'Common/Types/API/Route';
|
||||
import RouteMap, { RouteUtil } from '../../Utils/RouteMap';
|
||||
import PageMap from '../../Utils/PageMap';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import Icon from 'CommonUI/src/Components/Icon/Icon';
|
||||
import IconProp from 'Common/Types/Icon/IconProp';
|
||||
|
||||
export interface ComponentProps {
|
||||
monitor: Monitor;
|
||||
onNavigateComplete?: (() => void) | undefined;
|
||||
showIcon?: boolean;
|
||||
}
|
||||
|
||||
const MonitorElement: FunctionComponent<ComponentProps> = (
|
||||
@@ -26,7 +29,17 @@ const MonitorElement: FunctionComponent<ComponentProps> = (
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span>{props.monitor.name}</span>
|
||||
<span className="flex">
|
||||
{props.showIcon ? (
|
||||
<Icon
|
||||
icon={IconProp.AltGlobe}
|
||||
className="w-5 h-5 mr-1"
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}{' '}
|
||||
{props.monitor.name}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ import {
|
||||
import { ModalWidth } from 'CommonUI/src/Components/Modal/Modal';
|
||||
import MonitoringInterval from '../../Utils/MonitorIntervalDropdownOptions';
|
||||
import MonitorStepsType from 'Common/Types/Monitor/MonitorSteps';
|
||||
import Team from 'Model/Models/Team';
|
||||
import ProjectUser from '../../Utils/ProjectUser';
|
||||
import { Grey } from 'Common/Types/BrandColors';
|
||||
|
||||
export interface ComponentProps {
|
||||
@@ -74,14 +72,6 @@ const MonitorsTable: FunctionComponent<ComponentProps> = (
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Owners',
|
||||
id: 'owners',
|
||||
},
|
||||
{
|
||||
title: 'Labels',
|
||||
id: 'labels',
|
||||
},
|
||||
]}
|
||||
cardProps={{
|
||||
title: props.title || 'Monitors',
|
||||
@@ -173,61 +163,6 @@ const MonitorsTable: FunctionComponent<ComponentProps> = (
|
||||
dropdownOptions: MonitoringInterval,
|
||||
placeholder: 'Select Monitoring Interval',
|
||||
},
|
||||
{
|
||||
overrideField: {
|
||||
ownerTeams: true,
|
||||
},
|
||||
forceShow: true,
|
||||
title: 'Owner - Teams',
|
||||
stepId: 'owners',
|
||||
description:
|
||||
'Select which teams own this monitor. They will be notified when monitor status changes.',
|
||||
fieldType: FormFieldSchemaType.MultiSelectDropdown,
|
||||
dropdownModal: {
|
||||
type: Team,
|
||||
labelField: 'name',
|
||||
valueField: '_id',
|
||||
},
|
||||
required: false,
|
||||
placeholder: 'Select Teams',
|
||||
overrideFieldKey: 'ownerTeams',
|
||||
},
|
||||
{
|
||||
overrideField: {
|
||||
ownerUsers: true,
|
||||
},
|
||||
forceShow: true,
|
||||
title: 'Owner - Users',
|
||||
stepId: 'owners',
|
||||
description:
|
||||
'Select which users own this incident. They will be notified when monitor status changes.',
|
||||
fieldType: FormFieldSchemaType.MultiSelectDropdown,
|
||||
fetchDropdownOptions: async () => {
|
||||
return await ProjectUser.fetchProjectUsersAsDropdownOptions(
|
||||
DashboardNavigation.getProjectId()!
|
||||
);
|
||||
},
|
||||
required: false,
|
||||
placeholder: 'Select Users',
|
||||
overrideFieldKey: 'ownerUsers',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
labels: true,
|
||||
},
|
||||
title: 'Labels ',
|
||||
stepId: 'labels',
|
||||
description:
|
||||
'Team members with access to these labels will only be able to access this resource. This is optional and an advanced feature.',
|
||||
fieldType: FormFieldSchemaType.MultiSelectDropdown,
|
||||
dropdownModal: {
|
||||
type: Label,
|
||||
labelField: 'name',
|
||||
valueField: '_id',
|
||||
},
|
||||
required: false,
|
||||
placeholder: 'Labels',
|
||||
},
|
||||
]}
|
||||
showRefreshButton={true}
|
||||
showFilterButton={true}
|
||||
|
||||
@@ -15,10 +15,10 @@ const MonitorsElement: FunctionComponent<ComponentProps> = (
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex">
|
||||
{props.monitors.map((monitor: Monitor, i: number) => {
|
||||
return (
|
||||
<span key={i}>
|
||||
<span key={i} className="flex">
|
||||
<MonitorElement
|
||||
monitor={monitor}
|
||||
onNavigateComplete={props.onNavigateComplete}
|
||||
|
||||
74
Dashboard/src/Components/MonitorGroup/CurrentStatus.tsx
Normal file
74
Dashboard/src/Components/MonitorGroup/CurrentStatus.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { FunctionComponent, ReactElement, useEffect } from 'react';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import API from 'CommonUI/src/Utils/API/API';
|
||||
import ErrorMessage from 'CommonUI/src/Components/ErrorMessage/ErrorMessage';
|
||||
import ModelAPI from 'CommonUI/src/Utils/ModelAPI/ModelAPI';
|
||||
import MonitorStatus from 'Model/Models/MonitorStatus';
|
||||
import URL from 'Common/Types/API/URL';
|
||||
import { DASHBOARD_API_URL } from 'CommonUI/src/Config';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import Statusbubble from 'CommonUI/src/Components/StatusBubble/StatusBubble';
|
||||
import Color from 'Common/Types/Color';
|
||||
import MonitorGroup from 'Model/Models/MonitorGroup';
|
||||
import Loader from 'CommonUI/src/Components/Loader/Loader';
|
||||
|
||||
export interface ComponentProps {
|
||||
monitorGroupId: ObjectID;
|
||||
}
|
||||
|
||||
const CurrentStatusElement: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(true);
|
||||
|
||||
const [currentGroupStatus, setCurrentGroupStatus] =
|
||||
React.useState<MonitorStatus | null>(null);
|
||||
|
||||
const [error, setError] = React.useState<string | undefined>(undefined);
|
||||
|
||||
const loadCurrentStatus: Function = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const currentStatus: MonitorStatus | null =
|
||||
await ModelAPI.post<MonitorStatus>(
|
||||
MonitorStatus,
|
||||
URL.fromString(DASHBOARD_API_URL.toString())
|
||||
.addRoute(new MonitorGroup().getCrudApiPath()!)
|
||||
.addRoute('/current-status/')
|
||||
.addRoute(`/${props.monitorGroupId.toString()}`)
|
||||
);
|
||||
|
||||
setCurrentGroupStatus(currentStatus);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadCurrentStatus().catch(() => {});
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage error={error} />;
|
||||
}
|
||||
|
||||
if (!currentGroupStatus) {
|
||||
throw new BadDataException('Current Group Status not found');
|
||||
}
|
||||
|
||||
return (
|
||||
<Statusbubble
|
||||
color={currentGroupStatus.color! as Color}
|
||||
text={currentGroupStatus.name! as string}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentStatusElement;
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import MonitorGroup from 'Model/Models/MonitorGroup';
|
||||
import Link from 'CommonUI/src/Components/Link/Link';
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import RouteMap, { RouteUtil } from '../../Utils/RouteMap';
|
||||
import PageMap from '../../Utils/PageMap';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import Icon from 'CommonUI/src/Components/Icon/Icon';
|
||||
import IconProp from 'Common/Types/Icon/IconProp';
|
||||
|
||||
export interface ComponentProps {
|
||||
monitorGroup: MonitorGroup;
|
||||
onNavigateComplete?: (() => void) | undefined;
|
||||
showIcon?: boolean;
|
||||
}
|
||||
|
||||
const MonitorGroupElement: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
if (props.monitorGroup._id) {
|
||||
return (
|
||||
<Link
|
||||
onNavigateComplete={props.onNavigateComplete}
|
||||
className="hover:underline"
|
||||
to={RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITOR_GROUP_VIEW] as Route,
|
||||
{
|
||||
modelId: new ObjectID(props.monitorGroup._id as string),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span className="flex">
|
||||
{props.showIcon ? (
|
||||
<Icon
|
||||
icon={IconProp.Squares}
|
||||
className="w-5 h-5 mr-1"
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}{' '}
|
||||
{props.monitorGroup.name}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>{props.monitorGroup.name}</span>;
|
||||
};
|
||||
|
||||
export default MonitorGroupElement;
|
||||
@@ -87,7 +87,7 @@ const IncidentOwners: FunctionComponent<PageComponentProps> = (
|
||||
return Promise.resolve(item);
|
||||
}}
|
||||
cardProps={{
|
||||
title: 'Owners - Teams',
|
||||
title: 'Owners (Teams)',
|
||||
description:
|
||||
'Here is list of teams that own this incident. They will be alerted when this incident is created or updated.',
|
||||
}}
|
||||
@@ -163,7 +163,7 @@ const IncidentOwners: FunctionComponent<PageComponentProps> = (
|
||||
return Promise.resolve(item);
|
||||
}}
|
||||
cardProps={{
|
||||
title: 'Owners - User',
|
||||
title: 'Owners (Users)',
|
||||
description:
|
||||
'Here is list of users that own this incident. They will be alerted when this incident is created or updated.',
|
||||
}}
|
||||
|
||||
@@ -64,6 +64,22 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
}}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
{props.project?.isFeatureFlagMonitorGroupsEnabled ? (
|
||||
<SideMenuSection title="Monitor Groups">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: 'All Groups',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITOR_GROUPS] as Route
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Squares}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</SideMenu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -89,7 +89,7 @@ const MonitorOwners: FunctionComponent<PageComponentProps> = (
|
||||
return Promise.resolve(item);
|
||||
}}
|
||||
cardProps={{
|
||||
title: 'Owners - Teams',
|
||||
title: 'Owners (Teams)',
|
||||
description:
|
||||
'Here is list of teams that own this monitor. They will be alerted when this monitor is created or updated.',
|
||||
}}
|
||||
@@ -163,7 +163,7 @@ const MonitorOwners: FunctionComponent<PageComponentProps> = (
|
||||
return Promise.resolve(item);
|
||||
}}
|
||||
cardProps={{
|
||||
title: 'Owners - User',
|
||||
title: 'Owners (Users)',
|
||||
description:
|
||||
'Here is list of users that own this monitor. They will be alerted when this monitor is created or updated.',
|
||||
}}
|
||||
|
||||
166
Dashboard/src/Pages/MonitorGroup/MonitorGroups.tsx
Normal file
166
Dashboard/src/Pages/MonitorGroup/MonitorGroups.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import Page from 'CommonUI/src/Components/Page/Page';
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import PageMap from '../../Utils/PageMap';
|
||||
import RouteMap, { RouteUtil } from '../../Utils/RouteMap';
|
||||
import PageComponentProps from '../PageComponentProps';
|
||||
import DashboardSideMenu from '../Monitor/SideMenu';
|
||||
import DashboardNavigation from '../../Utils/Navigation';
|
||||
import ModelTable from 'CommonUI/src/Components/ModelTable/ModelTable';
|
||||
import FieldType from 'CommonUI/src/Components/Types/FieldType';
|
||||
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType';
|
||||
import Label from 'Model/Models/Label';
|
||||
import { JSONArray, JSONObject } from 'Common/Types/JSON';
|
||||
import LabelsElement from '../../Components/Label/Labels';
|
||||
import JSONFunctions from 'Common/Types/JSONFunctions';
|
||||
import MonitorGroup from 'Model/Models/MonitorGroup';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import CurrentStatusElement from '../../Components/MonitorGroup/CurrentStatus';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
|
||||
const MonitorGroupPage: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps
|
||||
): ReactElement => {
|
||||
return (
|
||||
<Page
|
||||
title={'Monitors'}
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
title: 'Project',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.HOME] as Route
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Monitors',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITORS] as Route
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Monitor Groups',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITOR_GROUPS] as Route
|
||||
),
|
||||
},
|
||||
]}
|
||||
sideMenu={
|
||||
<DashboardSideMenu
|
||||
project={props.currentProject || undefined}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ModelTable<MonitorGroup>
|
||||
modelType={MonitorGroup}
|
||||
name="Monitor Groups"
|
||||
id="monitors-group-table"
|
||||
isDeleteable={false}
|
||||
showViewIdButton={true}
|
||||
isEditable={false}
|
||||
isCreateable={true}
|
||||
isViewable={true}
|
||||
cardProps={{
|
||||
title: 'Monitor Groups',
|
||||
description:
|
||||
'Here is a list of monitors groups for this project.',
|
||||
}}
|
||||
noItemsMessage={'No monitor groups found.'}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: 'Name',
|
||||
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: 'Monitor Name',
|
||||
validation: {
|
||||
minLength: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
description: true,
|
||||
},
|
||||
|
||||
title: 'Description',
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
required: true,
|
||||
placeholder: 'Description',
|
||||
},
|
||||
]}
|
||||
viewPageRoute={Navigation.getCurrentRoute()}
|
||||
showRefreshButton={true}
|
||||
showFilterButton={true}
|
||||
columns={[
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: 'Group Name',
|
||||
type: FieldType.Text,
|
||||
isFilterable: true,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
_id: true,
|
||||
},
|
||||
title: 'Current Status',
|
||||
type: FieldType.Element,
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
if (!item['_id']) {
|
||||
throw new BadDataException(
|
||||
'Monitor Group ID not found'
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CurrentStatusElement
|
||||
monitorGroupId={
|
||||
new ObjectID(item['_id'].toString())
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
labels: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
},
|
||||
title: 'Labels',
|
||||
type: FieldType.EntityArray,
|
||||
isFilterable: true,
|
||||
filterEntityType: Label,
|
||||
filterQuery: {
|
||||
projectId:
|
||||
DashboardNavigation.getProjectId()?.toString(),
|
||||
},
|
||||
filterDropdownField: {
|
||||
label: 'name',
|
||||
value: '_id',
|
||||
},
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
return (
|
||||
<LabelsElement
|
||||
labels={
|
||||
JSONFunctions.fromJSON(
|
||||
(item['labels'] as JSONArray) || [],
|
||||
Label
|
||||
) as Array<Label>
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonitorGroupPage;
|
||||
72
Dashboard/src/Pages/MonitorGroup/View/Delete.tsx
Normal file
72
Dashboard/src/Pages/MonitorGroup/View/Delete.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import ModelPage from 'CommonUI/src/Components/Page/ModelPage';
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import PageMap from '../../../Utils/PageMap';
|
||||
import RouteMap, { RouteUtil } from '../../../Utils/RouteMap';
|
||||
import PageComponentProps from '../../PageComponentProps';
|
||||
import SideMenu from './SideMenu';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import ModelDelete from 'CommonUI/src/Components/ModelDelete/ModelDelete';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import MonitorGroup from 'Model/Models/MonitorGroup';
|
||||
|
||||
const MonitorGroupDelete: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps
|
||||
): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
return (
|
||||
<ModelPage
|
||||
title="Monitor Group"
|
||||
modelType={MonitorGroup}
|
||||
modelId={modelId}
|
||||
modelNameField="name"
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
title: 'Project',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.HOME] as Route,
|
||||
{ modelId }
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Monitor Groups',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITOR_GROUPS] as Route,
|
||||
{ modelId }
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'View Monitor Group',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITOR_GROUP_VIEW] as Route,
|
||||
{ modelId }
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Delete Monitor Group',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITOR_GROUP_VIEW_DELETE] as Route,
|
||||
{ modelId }
|
||||
),
|
||||
},
|
||||
]}
|
||||
sideMenu={<SideMenu modelId={modelId} />}
|
||||
>
|
||||
<ModelDelete
|
||||
modelType={MonitorGroup}
|
||||
modelId={modelId}
|
||||
onDeleteSuccess={() => {
|
||||
Navigation.navigate(
|
||||
RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITOR_GROUPS] as Route,
|
||||
{ modelId }
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ModelPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonitorGroupDelete;
|
||||
124
Dashboard/src/Pages/MonitorGroup/View/Incidents.tsx
Normal file
124
Dashboard/src/Pages/MonitorGroup/View/Incidents.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import ModelPage from 'CommonUI/src/Components/Page/ModelPage';
|
||||
import React, { FunctionComponent, ReactElement, useEffect } from 'react';
|
||||
import PageMap from '../../../Utils/PageMap';
|
||||
import RouteMap, { RouteUtil } from '../../../Utils/RouteMap';
|
||||
import PageComponentProps from '../../PageComponentProps';
|
||||
import SideMenu from './SideMenu';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import IncidentsTable from '../../../Components/Incident/IncidentsTable';
|
||||
import DashboardNavigation from '../../../Utils/Navigation';
|
||||
import PageLoader from 'CommonUI/src/Components/Loader/PageLoader';
|
||||
import MonitorGroupResource from 'Model/Models/MonitorGroupResource';
|
||||
import ModelAPI, { ListResult } from 'CommonUI/src/Utils/ModelAPI/ModelAPI';
|
||||
import { LIMIT_PER_PROJECT } from 'Common/Types/Database/LimitMax';
|
||||
import API from 'CommonUI/src/Utils/API/API';
|
||||
import ErrorMessage from 'CommonUI/src/Components/ErrorMessage/ErrorMessage';
|
||||
import MonitorGroup from 'Model/Models/MonitorGroup';
|
||||
|
||||
const MonitorIncidents: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps
|
||||
): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(true);
|
||||
|
||||
const [monitorIds, setMonitorIds] = React.useState<ObjectID[]>([]);
|
||||
|
||||
const [error, setError] = React.useState<string | undefined>(undefined);
|
||||
|
||||
const loadMonitorsIds: Function = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const monitorGroupResources: ListResult<MonitorGroupResource> =
|
||||
await ModelAPI.getList(
|
||||
MonitorGroupResource,
|
||||
{
|
||||
monitorGroupId: modelId.toString(),
|
||||
},
|
||||
LIMIT_PER_PROJECT,
|
||||
0,
|
||||
{
|
||||
monitorId: true,
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const monitorIds: Array<ObjectID> = monitorGroupResources.data.map(
|
||||
(monitorGroupResource: MonitorGroupResource): ObjectID => {
|
||||
return monitorGroupResource.monitorId!;
|
||||
}
|
||||
);
|
||||
|
||||
setMonitorIds(monitorIds);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadMonitorsIds().catch(() => {});
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ModelPage
|
||||
title="Monitor Group"
|
||||
modelType={MonitorGroup}
|
||||
modelId={modelId}
|
||||
modelNameField="name"
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
title: 'Project',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.HOME] as Route,
|
||||
{ modelId }
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Monitor Groups',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITOR_GROUPS] as Route,
|
||||
{ modelId }
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'View Monitor Group',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITOR_GROUP_VIEW] as Route,
|
||||
{ modelId }
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Incidents',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITOR_GROUP_VIEW_INCIDENTS] as Route,
|
||||
{ modelId }
|
||||
),
|
||||
},
|
||||
]}
|
||||
sideMenu={<SideMenu modelId={modelId} />}
|
||||
>
|
||||
<IncidentsTable
|
||||
viewPageRoute={Navigation.getCurrentRoute()}
|
||||
query={{
|
||||
projectId: DashboardNavigation.getProjectId()?.toString(),
|
||||
monitors: monitorIds,
|
||||
}}
|
||||
/>
|
||||
</ModelPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonitorIncidents;
|
||||
247
Dashboard/src/Pages/MonitorGroup/View/Index.tsx
Normal file
247
Dashboard/src/Pages/MonitorGroup/View/Index.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import ModelPage from 'CommonUI/src/Components/Page/ModelPage';
|
||||
import React, { FunctionComponent, ReactElement, useState } from 'react';
|
||||
import PageMap from '../../../Utils/PageMap';
|
||||
import RouteMap, { RouteUtil } from '../../../Utils/RouteMap';
|
||||
import PageComponentProps from '../../PageComponentProps';
|
||||
import SideMenu from './SideMenu';
|
||||
import FieldType from 'CommonUI/src/Components/Types/FieldType';
|
||||
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType';
|
||||
import CardModelDetail from 'CommonUI/src/Components/ModelDetail/CardModelDetail';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import Label from 'Model/Models/Label';
|
||||
import { JSONArray, JSONObject } from 'Common/Types/JSON';
|
||||
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';
|
||||
import ModelAPI, { ListResult } from 'CommonUI/src/Utils/ModelAPI/ModelAPI';
|
||||
import MonitorStatusTimeline from 'Model/Models/MonitorStatusTimeline';
|
||||
import { LIMIT_PER_PROJECT } from 'Common/Types/Database/LimitMax';
|
||||
import URL from 'Common/Types/API/URL';
|
||||
import { DASHBOARD_API_URL } from 'CommonUI/src/Config';
|
||||
import API from 'CommonUI/src/Utils/API/API';
|
||||
import OneUptimeDate from 'Common/Types/Date';
|
||||
|
||||
const MonitorGroupView: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps
|
||||
): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID();
|
||||
|
||||
const [data, setData] = useState<Array<MonitorStatusTimeline>>([]);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
await fetchItem();
|
||||
}, []);
|
||||
|
||||
const fetchItem: () => Promise<void> = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const monitorStatus: ListResult<MonitorStatusTimeline> =
|
||||
await ModelAPI.getList(
|
||||
MonitorStatusTimeline,
|
||||
{},
|
||||
LIMIT_PER_PROJECT,
|
||||
0,
|
||||
{},
|
||||
{},
|
||||
{
|
||||
overrideRequestUrl: URL.fromString(
|
||||
DASHBOARD_API_URL.toString()
|
||||
)
|
||||
.addRoute(new MonitorGroup().getCrudApiPath()!)
|
||||
.addRoute('/timeline/')
|
||||
.addRoute(`/${modelId.toString()}`),
|
||||
}
|
||||
);
|
||||
|
||||
setData(monitorStatus.data);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<ModelPage
|
||||
title="Monitor Group"
|
||||
modelType={MonitorGroup}
|
||||
modelId={modelId}
|
||||
modelNameField="name"
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
title: 'Project',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.HOME] as Route,
|
||||
{ modelId }
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Monitor Groups',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITOR_GROUPS] as Route,
|
||||
{ modelId }
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'View Monitor Group',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITOR_GROUP_VIEW] as Route,
|
||||
{ modelId }
|
||||
),
|
||||
},
|
||||
]}
|
||||
sideMenu={<SideMenu modelId={modelId} />}
|
||||
>
|
||||
{/* MonitorGroup View */}
|
||||
<CardModelDetail<MonitorGroup>
|
||||
name="MonitorGroup Details"
|
||||
formSteps={[
|
||||
{
|
||||
title: 'Monitor Group Info',
|
||||
id: 'monitor-info',
|
||||
},
|
||||
{
|
||||
title: 'Labels',
|
||||
id: 'labels',
|
||||
},
|
||||
]}
|
||||
cardProps={{
|
||||
title: 'Monitor Group Details',
|
||||
description:
|
||||
'Here are more details for this monitor group.',
|
||||
}}
|
||||
isEditable={true}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
stepId: 'monitor-info',
|
||||
title: 'Group Name',
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: 'Monitor Group Name',
|
||||
validation: {
|
||||
minLength: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
description: true,
|
||||
},
|
||||
stepId: 'monitor-info',
|
||||
title: 'Group Description',
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
required: true,
|
||||
placeholder: 'Description',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
labels: true,
|
||||
},
|
||||
stepId: 'labels',
|
||||
title: 'Labels ',
|
||||
description:
|
||||
'Team members with access to these labels will only be able to access this resource. This is optional and an advanced feature.',
|
||||
fieldType: FormFieldSchemaType.MultiSelectDropdown,
|
||||
dropdownModal: {
|
||||
type: Label,
|
||||
labelField: 'name',
|
||||
valueField: '_id',
|
||||
},
|
||||
required: false,
|
||||
placeholder: 'Labels',
|
||||
},
|
||||
]}
|
||||
modelDetailProps={{
|
||||
showDetailsInNumberOfColumns: 2,
|
||||
modelType: MonitorGroup,
|
||||
id: 'model-detail-monitors',
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
_id: true,
|
||||
},
|
||||
title: 'Monitor Group ID',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: 'Monitor Group Name',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
labels: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
},
|
||||
title: 'Labels',
|
||||
fieldType: FieldType.Element,
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
return (
|
||||
<LabelsElement
|
||||
labels={
|
||||
JSONFunctions.fromJSON(
|
||||
(item['labels'] as JSONArray) ||
|
||||
[],
|
||||
Label
|
||||
) as Array<Label>
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
description: true,
|
||||
},
|
||||
title: 'Description',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
_id: true,
|
||||
},
|
||||
fieldType: FieldType.Element,
|
||||
title: 'Current Status',
|
||||
getElement: (): ReactElement => {
|
||||
return (
|
||||
<CurrentStatusElement
|
||||
monitorGroupId={modelId}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
modelId: modelId,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Card
|
||||
title="Uptime Graph"
|
||||
description="Here the 90 day uptime history of this monitor group."
|
||||
>
|
||||
<MonitorUptimeGraph
|
||||
error={error}
|
||||
items={data}
|
||||
startDate={OneUptimeDate.getSomeDaysAgo(90)}
|
||||
endDate={OneUptimeDate.getCurrentDate()}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</Card>
|
||||
</ModelPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonitorGroupView;
|
||||
277
Dashboard/src/Pages/MonitorGroup/View/Monitors.tsx
Normal file
277
Dashboard/src/Pages/MonitorGroup/View/Monitors.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import ModelPage from 'CommonUI/src/Components/Page/ModelPage';
|
||||
import React, { FunctionComponent, ReactElement, useEffect } from 'react';
|
||||
import PageMap from '../../../Utils/PageMap';
|
||||
import RouteMap, { RouteUtil } from '../../../Utils/RouteMap';
|
||||
import PageComponentProps from '../../PageComponentProps';
|
||||
import SideMenu from './SideMenu';
|
||||
import DashboardNavigation from '../../../Utils/Navigation';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import MonitorGroupResource from 'Model/Models/MonitorGroupResource';
|
||||
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';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import Monitor from 'Model/Models/Monitor';
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
import MonitorElement from '../../../Components/Monitor/Monitor';
|
||||
import JSONFunctions from 'Common/Types/JSONFunctions';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import MonitorStatus from 'Model/Models/MonitorStatus';
|
||||
import ModelAPI, { ListResult } from 'CommonUI/src/Utils/ModelAPI/ModelAPI';
|
||||
import { LIMIT_PER_PROJECT } from 'Common/Types/Database/LimitMax';
|
||||
import API from 'CommonUI/src/Utils/API/API';
|
||||
import PageLoader from 'CommonUI/src/Components/Loader/PageLoader';
|
||||
import ErrorMessage from 'CommonUI/src/Components/ErrorMessage/ErrorMessage';
|
||||
import Statusbubble from 'CommonUI/src/Components/StatusBubble/StatusBubble';
|
||||
import Color from 'Common/Types/Color';
|
||||
import MonitorGroup from 'Model/Models/MonitorGroup';
|
||||
|
||||
const MonitorGroupResources: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps
|
||||
): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(true);
|
||||
|
||||
const [monitorStatuses, setMonitorStatuses] = React.useState<
|
||||
MonitorStatus[]
|
||||
>([]);
|
||||
|
||||
const [error, setError] = React.useState<string | undefined>(undefined);
|
||||
|
||||
const loadMonitorStatuses: Function = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const monitorStatuses: ListResult<MonitorStatus> =
|
||||
await ModelAPI.getList<MonitorStatus>(
|
||||
MonitorStatus,
|
||||
{
|
||||
projectId:
|
||||
DashboardNavigation.getProjectId()?.toString(),
|
||||
},
|
||||
LIMIT_PER_PROJECT,
|
||||
0,
|
||||
{
|
||||
_id: true,
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
setMonitorStatuses(monitorStatuses.data);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadMonitorStatuses().catch(() => {});
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ModelPage
|
||||
title="Monitor Group"
|
||||
modelType={MonitorGroup}
|
||||
modelId={modelId}
|
||||
modelNameField="name"
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
title: 'Project',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.HOME] as Route,
|
||||
{ modelId }
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Monitor Groups',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITOR_GROUPS] as Route,
|
||||
{ modelId }
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'View Monitor Group',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITOR_GROUP_VIEW] as Route,
|
||||
{ modelId }
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Monitors',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITOR_GROUP_VIEW_MONITORS] as Route,
|
||||
{ modelId }
|
||||
),
|
||||
},
|
||||
]}
|
||||
sideMenu={<SideMenu modelId={modelId} />}
|
||||
>
|
||||
<>
|
||||
<ModelTable<MonitorGroupResource>
|
||||
modelType={MonitorGroupResource}
|
||||
id={`monitor-group-resources`}
|
||||
isDeleteable={true}
|
||||
name="Monitor Group > Resources"
|
||||
showViewIdButton={true}
|
||||
isCreateable={true}
|
||||
isViewable={false}
|
||||
isEditable={true}
|
||||
query={{
|
||||
monitorGroupId: modelId,
|
||||
projectId:
|
||||
DashboardNavigation.getProjectId()?.toString(),
|
||||
}}
|
||||
onBeforeCreate={(
|
||||
item: MonitorGroupResource
|
||||
): Promise<MonitorGroupResource> => {
|
||||
if (
|
||||
!props.currentProject ||
|
||||
!props.currentProject._id
|
||||
) {
|
||||
throw new BadDataException(
|
||||
'Project ID cannot be null'
|
||||
);
|
||||
}
|
||||
item.monitorGroupId = modelId;
|
||||
item.projectId = new ObjectID(props.currentProject._id);
|
||||
|
||||
return Promise.resolve(item);
|
||||
}}
|
||||
cardProps={{
|
||||
title: `Monitor Group Resources`,
|
||||
description:
|
||||
'Resources that belong to this monitor group.',
|
||||
}}
|
||||
noItemsMessage={
|
||||
'No resources have been added to this monitor group.'
|
||||
}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
monitor: true,
|
||||
},
|
||||
title: 'Monitor',
|
||||
description:
|
||||
'Select monitor that will be added to this group.',
|
||||
fieldType: FormFieldSchemaType.Dropdown,
|
||||
dropdownModal: {
|
||||
type: Monitor,
|
||||
labelField: 'name',
|
||||
valueField: '_id',
|
||||
},
|
||||
required: true,
|
||||
placeholder: 'Select Monitor',
|
||||
},
|
||||
]}
|
||||
showRefreshButton={true}
|
||||
showFilterButton={true}
|
||||
viewPageRoute={Navigation.getCurrentRoute()}
|
||||
columns={[
|
||||
{
|
||||
field: {
|
||||
monitor: {
|
||||
name: true,
|
||||
_id: true,
|
||||
projectId: true,
|
||||
},
|
||||
},
|
||||
title: 'Monitor',
|
||||
type: FieldType.Entity,
|
||||
isFilterable: true,
|
||||
filterEntityType: Monitor,
|
||||
filterQuery: {
|
||||
projectId:
|
||||
DashboardNavigation.getProjectId()?.toString(),
|
||||
},
|
||||
filterDropdownField: {
|
||||
label: 'name',
|
||||
value: '_id',
|
||||
},
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
return (
|
||||
<MonitorElement
|
||||
monitor={
|
||||
JSONFunctions.fromJSON(
|
||||
(item[
|
||||
'monitor'
|
||||
] as JSONObject) || [],
|
||||
Monitor
|
||||
) as Monitor
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
monitor: {
|
||||
currentMonitorStatusId: true,
|
||||
},
|
||||
},
|
||||
title: 'Current Status',
|
||||
type: FieldType.Element,
|
||||
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
if (!item['monitor']) {
|
||||
throw new BadDataException(
|
||||
'Monitor not found'
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!(item['monitor'] as JSONObject)[
|
||||
'currentMonitorStatusId'
|
||||
]
|
||||
) {
|
||||
throw new BadDataException(
|
||||
'Monitor Status not found'
|
||||
);
|
||||
}
|
||||
|
||||
const monitorStatus: MonitorStatus | undefined =
|
||||
monitorStatuses.find(
|
||||
(monitorStatus: MonitorStatus) => {
|
||||
return (
|
||||
monitorStatus._id ===
|
||||
(item['monitor'] as JSONObject)[
|
||||
'currentMonitorStatusId'
|
||||
]?.toString()
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
if (!monitorStatus) {
|
||||
throw new BadDataException(
|
||||
'Monitor Status not found'
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Statusbubble
|
||||
color={monitorStatus.color! as Color}
|
||||
text={monitorStatus.name! as string}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
</ModelPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonitorGroupResources;
|
||||
225
Dashboard/src/Pages/MonitorGroup/View/Owners.tsx
Normal file
225
Dashboard/src/Pages/MonitorGroup/View/Owners.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import ModelPage from 'CommonUI/src/Components/Page/ModelPage';
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import PageMap from '../../../Utils/PageMap';
|
||||
import RouteMap, { RouteUtil } from '../../../Utils/RouteMap';
|
||||
import PageComponentProps from '../../PageComponentProps';
|
||||
import SideMenu from './SideMenu';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import MonitorGroup from 'Model/Models/MonitorGroup';
|
||||
import ModelTable from 'CommonUI/src/Components/ModelTable/ModelTable';
|
||||
import MonitorGroupOwnerTeam from 'Model/Models/MonitorGroupOwnerTeam';
|
||||
import DashboardNavigation from '../../../Utils/Navigation';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType';
|
||||
import Team from 'Model/Models/Team';
|
||||
import FieldType from 'CommonUI/src/Components/Types/FieldType';
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
import TeamElement from '../../../Components/Team/Team';
|
||||
import MonitorGroupOwnerUser from 'Model/Models/MonitorGroupOwnerUser';
|
||||
import User from 'Model/Models/User';
|
||||
import UserElement from '../../../Components/User/User';
|
||||
import ProjectUser from '../../../Utils/ProjectUser';
|
||||
|
||||
const MonitorGroupOwners: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps
|
||||
): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
return (
|
||||
<ModelPage
|
||||
title="Monitor Group"
|
||||
modelType={MonitorGroup}
|
||||
modelId={modelId}
|
||||
modelNameField="name"
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
title: 'Project',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.HOME] as Route,
|
||||
{ modelId }
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Monitor Groups',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITOR_GROUPS] as Route,
|
||||
{ modelId }
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'View Monitor Group',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITOR_GROUP_VIEW] as Route,
|
||||
{ modelId }
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Owners',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITOR_GROUP_VIEW_OWNERS] as Route,
|
||||
{ modelId }
|
||||
),
|
||||
},
|
||||
]}
|
||||
sideMenu={<SideMenu modelId={modelId} />}
|
||||
>
|
||||
<ModelTable<MonitorGroupOwnerTeam>
|
||||
modelType={MonitorGroupOwnerTeam}
|
||||
id="table-monitor-group-owner-team"
|
||||
name="MonitorGroup > Owner Team"
|
||||
singularName="Team"
|
||||
isDeleteable={true}
|
||||
createVerb={'Add'}
|
||||
isCreateable={true}
|
||||
isViewable={false}
|
||||
showViewIdButton={true}
|
||||
query={{
|
||||
monitorGroupId: modelId,
|
||||
projectId: DashboardNavigation.getProjectId()?.toString(),
|
||||
}}
|
||||
onBeforeCreate={(
|
||||
item: MonitorGroupOwnerTeam
|
||||
): Promise<MonitorGroupOwnerTeam> => {
|
||||
item.monitorGroupId = modelId;
|
||||
item.projectId = DashboardNavigation.getProjectId()!;
|
||||
return Promise.resolve(item);
|
||||
}}
|
||||
cardProps={{
|
||||
title: 'Owners (Teams)',
|
||||
description:
|
||||
'Here is list of teams that own this monitor group. ',
|
||||
}}
|
||||
noItemsMessage={
|
||||
'No teams associated with this monitor group so far.'
|
||||
}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
team: true,
|
||||
},
|
||||
title: 'Team',
|
||||
fieldType: FormFieldSchemaType.Dropdown,
|
||||
required: true,
|
||||
placeholder: 'Select Team',
|
||||
dropdownModal: {
|
||||
type: Team,
|
||||
labelField: 'name',
|
||||
valueField: '_id',
|
||||
},
|
||||
},
|
||||
]}
|
||||
showRefreshButton={true}
|
||||
showFilterButton={true}
|
||||
viewPageRoute={Navigation.getCurrentRoute()}
|
||||
columns={[
|
||||
{
|
||||
field: {
|
||||
team: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
title: 'Team',
|
||||
type: FieldType.Entity,
|
||||
isFilterable: true,
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
if (!item['team']) {
|
||||
throw new BadDataException('Team not found');
|
||||
}
|
||||
|
||||
return <TeamElement team={item['team'] as Team} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
createdAt: true,
|
||||
},
|
||||
title: 'Owner from',
|
||||
type: FieldType.DateTime,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<ModelTable<MonitorGroupOwnerUser>
|
||||
modelType={MonitorGroupOwnerUser}
|
||||
id="table-monitor-group-owner-team"
|
||||
name="MonitorGroup > Owner Team"
|
||||
isDeleteable={true}
|
||||
singularName="User"
|
||||
isCreateable={true}
|
||||
isViewable={false}
|
||||
showViewIdButton={true}
|
||||
createVerb={'Add'}
|
||||
query={{
|
||||
monitorGroupId: modelId,
|
||||
projectId: DashboardNavigation.getProjectId()?.toString(),
|
||||
}}
|
||||
onBeforeCreate={(
|
||||
item: MonitorGroupOwnerUser
|
||||
): Promise<MonitorGroupOwnerUser> => {
|
||||
item.monitorGroupId = modelId;
|
||||
item.projectId = DashboardNavigation.getProjectId()!;
|
||||
return Promise.resolve(item);
|
||||
}}
|
||||
cardProps={{
|
||||
title: 'Owners (Users)',
|
||||
description:
|
||||
'Here is list of users that own this monitor group.',
|
||||
}}
|
||||
noItemsMessage={
|
||||
'No users associated with this monitor group so far.'
|
||||
}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
user: true,
|
||||
},
|
||||
title: 'User',
|
||||
fieldType: FormFieldSchemaType.Dropdown,
|
||||
required: true,
|
||||
placeholder: 'Select User',
|
||||
fetchDropdownOptions: async () => {
|
||||
return await ProjectUser.fetchProjectUsersAsDropdownOptions(
|
||||
DashboardNavigation.getProjectId()!
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
showRefreshButton={true}
|
||||
showFilterButton={true}
|
||||
viewPageRoute={Navigation.getCurrentRoute()}
|
||||
columns={[
|
||||
{
|
||||
field: {
|
||||
user: {
|
||||
name: true,
|
||||
email: true,
|
||||
profilePictureId: true,
|
||||
},
|
||||
},
|
||||
title: 'User',
|
||||
type: FieldType.Entity,
|
||||
isFilterable: true,
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
if (!item['user']) {
|
||||
throw new BadDataException('User not found');
|
||||
}
|
||||
|
||||
return <UserElement user={item['user'] as User} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
createdAt: true,
|
||||
},
|
||||
title: 'Owner from',
|
||||
type: FieldType.DateTime,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ModelPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonitorGroupOwners;
|
||||
93
Dashboard/src/Pages/MonitorGroup/View/SideMenu.tsx
Normal file
93
Dashboard/src/Pages/MonitorGroup/View/SideMenu.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import IconProp from 'Common/Types/Icon/IconProp';
|
||||
import SideMenu from 'CommonUI/src/Components/SideMenu/SideMenu';
|
||||
import SideMenuItem from 'CommonUI/src/Components/SideMenu/SideMenuItem';
|
||||
import SideMenuSection from 'CommonUI/src/Components/SideMenu/SideMenuSection';
|
||||
import RouteMap, { RouteUtil } from '../../../Utils/RouteMap';
|
||||
import PageMap from '../../../Utils/PageMap';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
|
||||
export interface ComponentProps {
|
||||
modelId: ObjectID;
|
||||
}
|
||||
|
||||
const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
return (
|
||||
<SideMenu>
|
||||
<SideMenuSection title="Basic">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: 'Overview',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.MONITOR_GROUP_VIEW] as Route,
|
||||
{ modelId: props.modelId }
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Info}
|
||||
/>
|
||||
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: 'Owners',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[
|
||||
PageMap.MONITOR_GROUP_VIEW_OWNERS
|
||||
] as Route,
|
||||
{ modelId: props.modelId }
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Team}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Monitors and Incidents">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: 'Monitors',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[
|
||||
PageMap.MONITOR_GROUP_VIEW_MONITORS
|
||||
] as Route,
|
||||
{ modelId: props.modelId }
|
||||
),
|
||||
}}
|
||||
icon={IconProp.AltGlobe}
|
||||
/>
|
||||
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: 'Incidents',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[
|
||||
PageMap.MONITOR_GROUP_VIEW_INCIDENTS
|
||||
] as Route,
|
||||
{ modelId: props.modelId }
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Alert}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Advanced">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: 'Delete Group',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[
|
||||
PageMap.MONITOR_GROUP_VIEW_DELETE
|
||||
] as Route,
|
||||
{ modelId: props.modelId }
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Trash}
|
||||
className="danger-on-hover"
|
||||
/>
|
||||
</SideMenuSection>
|
||||
</SideMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardSideMenu;
|
||||
@@ -89,7 +89,7 @@ const ScheduledMaintenanceOwners: FunctionComponent<PageComponentProps> = (
|
||||
return Promise.resolve(item);
|
||||
}}
|
||||
cardProps={{
|
||||
title: 'Owners - Teams',
|
||||
title: 'Owners (Teams)',
|
||||
description:
|
||||
'Here is list of teams that own this scheduled maintenance event. They will be alerted when this scheduled maintenance event is created or updated.',
|
||||
}}
|
||||
@@ -165,7 +165,7 @@ const ScheduledMaintenanceOwners: FunctionComponent<PageComponentProps> = (
|
||||
return Promise.resolve(item);
|
||||
}}
|
||||
cardProps={{
|
||||
title: 'Owners - User',
|
||||
title: 'Owners (Users)',
|
||||
description:
|
||||
'Here is list of users that own this scheduled maintenance event. They will be alerted when this scheduled maintenance event is created or updated.',
|
||||
}}
|
||||
|
||||
84
Dashboard/src/Pages/Settings/FeatureFlags.tsx
Normal file
84
Dashboard/src/Pages/Settings/FeatureFlags.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import Project from 'Model/Models/Project';
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType';
|
||||
import CardModelDetail from 'CommonUI/src/Components/ModelDetail/CardModelDetail';
|
||||
import Page from 'CommonUI/src/Components/Page/Page';
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import PageMap from '../../Utils/PageMap';
|
||||
import RouteMap, { RouteUtil } from '../../Utils/RouteMap';
|
||||
import DashboardNavigation from '../../Utils/Navigation';
|
||||
import PageComponentProps from '../PageComponentProps';
|
||||
import DashboardSideMenu from './SideMenu';
|
||||
import FieldType from 'CommonUI/src/Components/Types/FieldType';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
|
||||
const Settings: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps
|
||||
): ReactElement => {
|
||||
return (
|
||||
<Page
|
||||
title={'Project Settings'}
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
title: 'Project',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.HOME] as Route
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Feature Flags',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.SETTINGS_FEATURE_FLAGS] as Route
|
||||
),
|
||||
},
|
||||
]}
|
||||
sideMenu={<DashboardSideMenu />}
|
||||
>
|
||||
{/* Project Settings View */}
|
||||
<CardModelDetail
|
||||
name="Feature Flags"
|
||||
cardProps={{
|
||||
title: 'Feature Flags',
|
||||
description:
|
||||
'Feature flags allow you to toggle features on and off for your project.',
|
||||
}}
|
||||
isEditable={true}
|
||||
editButtonText="Edit Feature Flags"
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
isFeatureFlagMonitorGroupsEnabled: true,
|
||||
},
|
||||
title: 'Enable Monitor Groups',
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
description:
|
||||
'Monitor Groups allow you to group monitors together and view them as a group and allows you to add these to your status page.',
|
||||
},
|
||||
]}
|
||||
onSaveSuccess={() => {
|
||||
Navigation.reload();
|
||||
}}
|
||||
modelDetailProps={{
|
||||
modelType: Project,
|
||||
id: 'model-detail-project',
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
isFeatureFlagMonitorGroupsEnabled: true,
|
||||
},
|
||||
fieldType: FieldType.Boolean,
|
||||
title: 'Monitor Groups Enabled',
|
||||
description:
|
||||
'Monitor Groups allow you to group monitors together and view them as a group and allows you to add these to your status page.',
|
||||
placeholder: 'No',
|
||||
},
|
||||
],
|
||||
modelId: DashboardNavigation.getProjectId()!,
|
||||
}}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -412,7 +412,7 @@ const TeamView: FunctionComponent<PageComponentProps> = (
|
||||
return Promise.resolve(item);
|
||||
}}
|
||||
cardProps={{
|
||||
title: 'Owners - Teams',
|
||||
title: 'Owners (Teams)',
|
||||
description:
|
||||
'These are the list of teams that will be added to the incident by default when its created.',
|
||||
}}
|
||||
@@ -488,7 +488,7 @@ const TeamView: FunctionComponent<PageComponentProps> = (
|
||||
return Promise.resolve(item);
|
||||
}}
|
||||
cardProps={{
|
||||
title: 'Owners - User',
|
||||
title: 'Owners (Users)',
|
||||
description:
|
||||
'These are the list of users that will be added to the incident by default when its created.',
|
||||
}}
|
||||
|
||||
@@ -266,6 +266,15 @@ const DashboardSideMenu: () => JSX.Element = (): ReactElement => {
|
||||
}}
|
||||
icon={IconProp.Terminal}
|
||||
/>
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: 'Feature Flags',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.SETTINGS_FEATURE_FLAGS] as Route
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Flag}
|
||||
/>
|
||||
|
||||
{/* <SideMenuItem
|
||||
link={{
|
||||
|
||||
@@ -14,8 +14,6 @@ import LabelsElement from '../../Components/Label/Labels';
|
||||
import JSONFunctions from 'Common/Types/JSONFunctions';
|
||||
import DashboardNavigation from '../../Utils/Navigation';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import Team from 'Model/Models/Team';
|
||||
import ProjectUser from '../../Utils/ProjectUser';
|
||||
|
||||
const StatusPages: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps
|
||||
@@ -52,20 +50,6 @@ const StatusPages: FunctionComponent<PageComponentProps> = (
|
||||
'Here is a list of status page for this project.',
|
||||
}}
|
||||
showViewIdButton={true}
|
||||
formSteps={[
|
||||
{
|
||||
title: 'Status Page Info',
|
||||
id: 'status-page-info',
|
||||
},
|
||||
{
|
||||
title: 'Owners',
|
||||
id: 'owners',
|
||||
},
|
||||
{
|
||||
title: 'Labels',
|
||||
id: 'labels',
|
||||
},
|
||||
]}
|
||||
noItemsMessage={'No status pages found.'}
|
||||
formFields={[
|
||||
{
|
||||
@@ -73,7 +57,6 @@ const StatusPages: FunctionComponent<PageComponentProps> = (
|
||||
name: true,
|
||||
},
|
||||
title: 'Name',
|
||||
stepId: 'status-page-info',
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: 'Status Page Name',
|
||||
@@ -85,66 +68,11 @@ const StatusPages: FunctionComponent<PageComponentProps> = (
|
||||
field: {
|
||||
description: true,
|
||||
},
|
||||
stepId: 'status-page-info',
|
||||
title: 'Description',
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
required: true,
|
||||
placeholder: 'Description',
|
||||
},
|
||||
{
|
||||
overrideField: {
|
||||
ownerTeams: true,
|
||||
},
|
||||
forceShow: true,
|
||||
title: 'Owner - Teams',
|
||||
stepId: 'owners',
|
||||
description:
|
||||
'Select which teams own this status page. ',
|
||||
fieldType: FormFieldSchemaType.MultiSelectDropdown,
|
||||
dropdownModal: {
|
||||
type: Team,
|
||||
labelField: 'name',
|
||||
valueField: '_id',
|
||||
},
|
||||
required: false,
|
||||
placeholder: 'Select Teams',
|
||||
overrideFieldKey: 'ownerTeams',
|
||||
},
|
||||
{
|
||||
overrideField: {
|
||||
ownerUsers: true,
|
||||
},
|
||||
forceShow: true,
|
||||
title: 'Owner - Users',
|
||||
stepId: 'owners',
|
||||
description: 'Select which users own this status page.',
|
||||
fieldType: FormFieldSchemaType.MultiSelectDropdown,
|
||||
fetchDropdownOptions: async () => {
|
||||
return await ProjectUser.fetchProjectUsersAsDropdownOptions(
|
||||
DashboardNavigation.getProjectId()!
|
||||
);
|
||||
},
|
||||
required: false,
|
||||
placeholder: 'Select Users',
|
||||
overrideFieldKey: 'ownerUsers',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
labels: true,
|
||||
},
|
||||
title: 'Labels ',
|
||||
stepId: 'labels',
|
||||
description:
|
||||
'Team members with access to these labels will only be able to access this resource. This is optional and an advanced feature.',
|
||||
fieldType: FormFieldSchemaType.MultiSelectDropdown,
|
||||
dropdownModal: {
|
||||
type: Label,
|
||||
labelField: 'name',
|
||||
valueField: '_id',
|
||||
},
|
||||
required: false,
|
||||
placeholder: 'Labels',
|
||||
},
|
||||
]}
|
||||
showRefreshButton={true}
|
||||
showFilterButton={true}
|
||||
|
||||
@@ -87,7 +87,7 @@ const StatusPageOwners: FunctionComponent<PageComponentProps> = (
|
||||
return Promise.resolve(item);
|
||||
}}
|
||||
cardProps={{
|
||||
title: 'Owners - Teams',
|
||||
title: 'Owners (Teams)',
|
||||
description:
|
||||
'Here is list of teams that own this status page. They will be alerted when this status page is created or updated.',
|
||||
}}
|
||||
@@ -163,7 +163,7 @@ const StatusPageOwners: FunctionComponent<PageComponentProps> = (
|
||||
return Promise.resolve(item);
|
||||
}}
|
||||
cardProps={{
|
||||
title: 'Owners - User',
|
||||
title: 'Owners (Users)',
|
||||
description:
|
||||
'Here is list of users that own this status page. They will be alerted when this status page is created or updated.',
|
||||
}}
|
||||
|
||||
@@ -30,6 +30,10 @@ import JSONFunctions from 'Common/Types/JSONFunctions';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import API from 'CommonUI/src/Utils/API/API';
|
||||
import StatusPage from 'Model/Models/StatusPage';
|
||||
import { ModelField } from 'CommonUI/src/Components/Forms/ModelForm';
|
||||
import MonitorGroup from 'Model/Models/MonitorGroup';
|
||||
import Link from 'CommonUI/src/Components/Link/Link';
|
||||
import MonitorGroupElement from '../../../Components/MonitorGroup/MonitorGroupElement';
|
||||
|
||||
const StatusPageDelete: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps
|
||||
@@ -40,6 +44,8 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const [addMonitorGroup, setAddMonitorGroup] = useState<boolean>(false);
|
||||
|
||||
const fetchGroups: Function = async () => {
|
||||
setError('');
|
||||
setIsLoading(true);
|
||||
@@ -76,6 +82,143 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
|
||||
fetchGroups();
|
||||
}, []);
|
||||
|
||||
const getFooterForMonitor: Function = (): ReactElement => {
|
||||
if (props.currentProject?.isFeatureFlagMonitorGroupsEnabled) {
|
||||
if (!addMonitorGroup) {
|
||||
return (
|
||||
<Link
|
||||
onClick={() => {
|
||||
setAddMonitorGroup(true);
|
||||
}}
|
||||
className="mt-1 text-sm text-gray-500 underline"
|
||||
>
|
||||
<div>
|
||||
<p> Add a Monitor Group instead. </p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
onClick={() => {
|
||||
setAddMonitorGroup(false);
|
||||
}}
|
||||
className="mt-1 text-sm text-gray-500 underline"
|
||||
>
|
||||
<div>
|
||||
<p> Add a Monitor instead. </p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
let formFields: Array<ModelField<StatusPageResource>> = [
|
||||
{
|
||||
field: {
|
||||
monitor: true,
|
||||
},
|
||||
title: 'Monitor',
|
||||
description:
|
||||
'Select monitor that will be shown on the status page.',
|
||||
fieldType: FormFieldSchemaType.Dropdown,
|
||||
dropdownModal: {
|
||||
type: Monitor,
|
||||
labelField: 'name',
|
||||
valueField: '_id',
|
||||
},
|
||||
required: true,
|
||||
placeholder: 'Select Monitor',
|
||||
stepId: 'monitor-details',
|
||||
footerElement: getFooterForMonitor(),
|
||||
},
|
||||
];
|
||||
|
||||
if (addMonitorGroup) {
|
||||
formFields = [
|
||||
{
|
||||
field: {
|
||||
monitorGroup: true,
|
||||
},
|
||||
title: 'Monitor Group',
|
||||
description:
|
||||
'Select monitor group that will be shown on the status page.',
|
||||
fieldType: FormFieldSchemaType.Dropdown,
|
||||
dropdownModal: {
|
||||
type: MonitorGroup,
|
||||
labelField: 'name',
|
||||
valueField: '_id',
|
||||
},
|
||||
required: true,
|
||||
placeholder: 'Select Monitor Group',
|
||||
stepId: 'monitor-details',
|
||||
footerElement: getFooterForMonitor(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
formFields = formFields.concat([
|
||||
{
|
||||
field: {
|
||||
displayName: true,
|
||||
},
|
||||
title: 'Display Name',
|
||||
description:
|
||||
'This will be the name that will be shown on the status page',
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: 'Display Name',
|
||||
stepId: 'monitor-details',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
displayDescription: true,
|
||||
},
|
||||
title: 'Description',
|
||||
fieldType: FormFieldSchemaType.Markdown,
|
||||
required: false,
|
||||
placeholder: '',
|
||||
stepId: 'monitor-details',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
displayTooltip: true,
|
||||
},
|
||||
title: 'Tooltip ',
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
required: false,
|
||||
description:
|
||||
'This will show up as tooltip beside the resource on your status page.',
|
||||
placeholder: 'Tooltip',
|
||||
stepId: 'advanced',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
showCurrentStatus: true,
|
||||
},
|
||||
title: 'Show Current Resource Status',
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
defaultValue: true,
|
||||
description:
|
||||
'Current Resource Status will be shown beside this resource on your status page.',
|
||||
stepId: 'advanced',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
showStatusHistoryChart: true,
|
||||
},
|
||||
title: 'Show Status History Chart',
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
description: 'Show resource status history for the past 90 days. ',
|
||||
defaultValue: true,
|
||||
stepId: 'advanced',
|
||||
},
|
||||
]);
|
||||
|
||||
const getModelTable: Function = (
|
||||
statusPageGroupId: ObjectID | null,
|
||||
statusPageGroupName: string | null
|
||||
@@ -137,86 +280,17 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
|
||||
id: 'advanced',
|
||||
},
|
||||
]}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
monitor: true,
|
||||
},
|
||||
title: 'Monitor',
|
||||
description:
|
||||
'Select monitor that will be shown on the status page.',
|
||||
fieldType: FormFieldSchemaType.Dropdown,
|
||||
dropdownModal: {
|
||||
type: Monitor,
|
||||
labelField: 'name',
|
||||
valueField: '_id',
|
||||
},
|
||||
required: true,
|
||||
placeholder: 'Select Monitor',
|
||||
stepId: 'monitor-details',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
displayName: true,
|
||||
},
|
||||
title: 'Display Name',
|
||||
description:
|
||||
'This will be the name that will be shown on the status page',
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: 'Display Name',
|
||||
stepId: 'monitor-details',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
displayDescription: true,
|
||||
},
|
||||
title: 'Description',
|
||||
fieldType: FormFieldSchemaType.Markdown,
|
||||
required: false,
|
||||
placeholder: '',
|
||||
stepId: 'monitor-details',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
displayTooltip: true,
|
||||
},
|
||||
title: 'Tooltip ',
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
required: false,
|
||||
description:
|
||||
'This will show up as tooltip beside the resource on your status page.',
|
||||
placeholder: 'Tooltip',
|
||||
stepId: 'advanced',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
showCurrentStatus: true,
|
||||
},
|
||||
title: 'Show Current Resource Status',
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
defaultValue: true,
|
||||
description:
|
||||
'Current Resource Status will be shown beside this resource on your status page.',
|
||||
stepId: 'advanced',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
showStatusHistoryChart: true,
|
||||
},
|
||||
title: 'Show Status History Chart',
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
description:
|
||||
'Show resource status history for the past 90 days. ',
|
||||
defaultValue: true,
|
||||
stepId: 'advanced',
|
||||
},
|
||||
]}
|
||||
formFields={formFields}
|
||||
showRefreshButton={true}
|
||||
showFilterButton={true}
|
||||
viewPageRoute={Navigation.getCurrentRoute()}
|
||||
selectMoreFields={{
|
||||
monitorGroup: {
|
||||
name: true,
|
||||
_id: true,
|
||||
projectId: true,
|
||||
},
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
field: {
|
||||
@@ -226,7 +300,10 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
|
||||
projectId: true,
|
||||
},
|
||||
},
|
||||
title: 'Monitor',
|
||||
title: props.currentProject
|
||||
?.isFeatureFlagMonitorGroupsEnabled
|
||||
? 'Resource'
|
||||
: 'Monitor',
|
||||
type: FieldType.Entity,
|
||||
isFilterable: true,
|
||||
filterEntityType: Monitor,
|
||||
@@ -239,17 +316,47 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
|
||||
value: '_id',
|
||||
},
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
return (
|
||||
<MonitorElement
|
||||
monitor={
|
||||
JSONFunctions.fromJSON(
|
||||
(item['monitor'] as JSONObject) ||
|
||||
[],
|
||||
Monitor
|
||||
) as Monitor
|
||||
}
|
||||
/>
|
||||
);
|
||||
if (item['monitor']) {
|
||||
return (
|
||||
<MonitorElement
|
||||
monitor={
|
||||
JSONFunctions.fromJSON(
|
||||
(item[
|
||||
'monitor'
|
||||
] as JSONObject) || [],
|
||||
Monitor
|
||||
) as Monitor
|
||||
}
|
||||
showIcon={
|
||||
props.currentProject
|
||||
?.isFeatureFlagMonitorGroupsEnabled ||
|
||||
false
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (item['monitorGroup']) {
|
||||
return (
|
||||
<MonitorGroupElement
|
||||
monitorGroup={
|
||||
JSONFunctions.fromJSON(
|
||||
(item[
|
||||
'monitorGroup'
|
||||
] as JSONObject) || [],
|
||||
MonitorGroup
|
||||
) as MonitorGroup
|
||||
}
|
||||
showIcon={
|
||||
props.currentProject
|
||||
?.isFeatureFlagMonitorGroupsEnabled ||
|
||||
false
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,6 +7,8 @@ import SideMenuSection from 'CommonUI/src/Components/SideMenu/SideMenuSection';
|
||||
import RouteMap, { RouteUtil } from '../../../Utils/RouteMap';
|
||||
import PageMap from '../../../Utils/PageMap';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import ProjectUtil from 'CommonUI/src/Utils/Project';
|
||||
import Project from 'Model/Models/Project';
|
||||
|
||||
export interface ComponentProps {
|
||||
modelId: ObjectID;
|
||||
@@ -15,6 +17,8 @@ export interface ComponentProps {
|
||||
const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps
|
||||
): ReactElement => {
|
||||
const project: Project | null = ProjectUtil.getCurrentProject();
|
||||
|
||||
return (
|
||||
<SideMenu>
|
||||
<SideMenuSection title="Basic">
|
||||
@@ -56,7 +60,9 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
<SideMenuSection title="Resources">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: 'Monitors',
|
||||
title: project?.isFeatureFlagMonitorGroupsEnabled
|
||||
? 'Resources'
|
||||
: 'Monitors',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[
|
||||
PageMap.STATUS_PAGE_VIEW_RESOURCES
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user