Compare commits

...

156 Commits

Author SHA1 Message Date
Simon Larsen
6c1bd10873 fix current status on sp 2023-11-02 13:39:59 +00:00
Simon Larsen
05a288c761 fix jest types 2023-11-02 11:11:36 +00:00
Simon Larsen
a9f503da9d fix probe tests 2023-11-02 10:21:31 +00:00
Simon Larsen
49d3655502 fix fmt 2023-11-01 20:49:00 +00:00
Simon Larsen
1cdcc639b4 fix fmt 2023-11-01 20:45:23 +00:00
Simon Larsen
7568c70b50 fix fmt 2023-11-01 20:15:18 +00:00
Simon Larsen
6259f81a91 add ui changes 2023-11-01 19:38:58 +00:00
Simon Larsen
f40c1daeb8 fix fmt 2023-11-01 14:15:43 +00:00
Simon Larsen
bb73ed14cd add animate in status bubble 2023-11-01 10:44:01 +00:00
Simon Larsen
4b71a81f7c fix fmt on uptime 2023-11-01 10:40:03 +00:00
Simon Larsen
d6788c138b fix uptime graph 2023-11-01 10:34:38 +00:00
Simon Larsen
28f4a1f473 add status api 2023-10-31 17:05:56 +00:00
Simon Larsen
ccb4781c06 enable compression 2023-10-31 14:10:57 +00:00
Simon Larsen
2e27347225 fix fmt 2023-10-31 12:19:47 +00:00
Simon Larsen
e9015f0eff Merge branch 'master' of github.com-simon:OneUptime/oneuptime 2023-10-31 12:06:27 +00:00
Simon Larsen
6cf8560151 fix eslint 2023-10-31 12:06:24 +00:00
Simon Larsen
7d2e91d867 Merge pull request #854 from hasannadeem/tests/notification-middleware-and-cookie-utils
Tests for notification middleware and cookie utils
2023-10-31 11:57:11 +00:00
Simon Larsen
46e0210dcc Merge pull request #869 from fakharj/eslint-object-curly-spacing
add eslint object-curly-spacing rule
2023-10-31 09:58:24 +00:00
Simon Larsen
02fc5502eb Merge branch 'master' into eslint-object-curly-spacing 2023-10-31 09:58:17 +00:00
Simon Larsen
ce3131edaf Merge pull request #865 from fakharj/eslint-unneeded-ternary
added no unneeded ternary in eslint
2023-10-30 13:56:03 +00:00
fakharj
ca4716133a add eslint object-curly-spacing rule 2023-10-30 18:43:43 +05:00
Simon Larsen
9cb254f9d1 Merge pull request #862 from OneUptime/dependabot/npm_and_yarn/Common/crypto-js-4.2.0
Bump crypto-js from 4.1.1 to 4.2.0 in /Common
2023-10-30 11:13:47 +00:00
Simon Larsen
d51fbdf5f7 Merge pull request #868 from cheese-framework/master
Add test suites for JSONFunctions and SerializableObject
2023-10-30 11:10:51 +00:00
Simon Larsen
57b7b5b39e Merge pull request #855 from hammadfauz/duplicateModalTest
Duplicate modal test
2023-10-30 08:24:18 +00:00
Drantaz
2e46ebd0e8 Merge branch 'master' of https://github.com/cheese-framework/oneuptime 2023-10-27 20:25:23 +00:00
Drantaz
4ffe215665 Add test suites for JSONFunctions and SerializableObject 2023-10-27 20:24:33 +00:00
Hammad
e680346f1f fixes lint 2023-10-27 23:48:27 +05:00
Hammad
4faa8d32f6 adds test ids to key elements 2023-10-27 23:27:33 +05:00
Simon Larsen
ab07ff0104 fix fmt 2023-10-27 17:22:20 +01:00
Simon Larsen
03dd6fef04 Merge branch 'master' of github.com-simon:OneUptime/oneuptime 2023-10-27 17:15:29 +01:00
Simon Larsen
31c0ff7dea Merge branch 'feature-flags' 2023-10-27 17:15:18 +01:00
Simon Larsen
dca1d2c370 add call and sms cost 2023-10-27 17:14:58 +01:00
Simon Larsen
fc218a970a Merge pull request #861 from OneUptime/feature-flags
add feature flag page
2023-10-27 16:40:30 +01:00
Simon Larsen
17509225ee add monitor groups. 2023-10-27 16:39:33 +01:00
Simon Larsen
447bac1d67 fix undefined in page title 2023-10-27 16:20:04 +01:00
Simon Larsen
67b3b224a7 fix monitor group api 2023-10-27 16:17:28 +01:00
Simon Larsen
48fbf50973 add current status 2023-10-27 13:13:32 +01:00
fakharj
a0acb24651 added no unneeded ternary in eslint 2023-10-27 13:03:07 +05:00
Simon Larsen
c958893d67 increase timeout to 30 secs 2023-10-26 20:51:00 +01:00
Simon Larsen
9e2bd15cf4 fix fmt 2023-10-26 19:58:47 +01:00
Simon Larsen
17e9ad4fcd Merge branch 'master' into feature-flags 2023-10-26 19:10:30 +01:00
Simon Larsen
4d5a49f11e fix fmt 2023-10-26 19:09:29 +01:00
Simon Larsen
2d9b9950dd when monitors are timeout mark them as offline. 2023-10-26 19:09:03 +01:00
Simon Larsen
c3c0fbc853 fix fmt 2023-10-26 15:24:15 +01:00
Simon Larsen
f970b02e9e monitor group view 2023-10-26 15:20:38 +01:00
Simon Larsen
987394be41 monitor groups page 2023-10-26 15:02:48 +01:00
Simon Larsen
34b3dff108 add service 2023-10-26 14:53:26 +01:00
Simon Larsen
b603241d57 add incidents page 2023-10-26 14:51:45 +01:00
Simon Larsen
8df01fc098 add owners page 2023-10-26 14:26:58 +01:00
Simon Larsen
268305e6cd add services and perms 2023-10-26 14:14:52 +01:00
Simon Larsen
bbb53b3321 make delete work 2023-10-26 13:52:05 +01:00
Simon Larsen
c79fa88ad1 add monitor group resource 2023-10-26 13:37:31 +01:00
Simon Larsen
35c5e57752 add read perms to domain 2023-10-26 13:09:06 +01:00
dependabot[bot]
254a9de101 Bump crypto-js from 4.1.1 to 4.2.0 in /Common
Bumps [crypto-js](https://github.com/brix/crypto-js) from 4.1.1 to 4.2.0.
- [Commits](https://github.com/brix/crypto-js/compare/4.1.1...4.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-25 22:47:32 +00:00
Simon Larsen
c844bf8e43 add feature flag page 2023-10-25 20:05:28 +01:00
Simon Larsen
c0288716da add labels to status page 2023-10-25 18:06:39 +01:00
Simon Larsen
51e7fa6c9a add labels to status page 2023-10-25 17:46:30 +01:00
Simon Larsen
d9eb60017a fix fmt 2023-10-25 17:37:02 +01:00
Simon Larsen
e9d7b36198 fix labels on status page. 2023-10-25 14:50:28 +01:00
Simon Larsen
7308945061 show incident labels on status page 2023-10-25 14:39:43 +01:00
Simon Larsen
3f8e5e4e0a Merge pull request #860 from cheese-framework/master
Add test suites for Database and API
2023-10-25 12:40:20 +01:00
Drantaz
3f7d186db0 Add test suites for Database and API 2023-10-25 11:10:52 +00:00
Simon Larsen
8cb91d94eb remove auto table creation 2023-10-25 09:21:11 +01:00
Simon Larsen
3337ad2a45 Merge pull request #859 from OneUptime/otel-save
Otel save
2023-10-25 09:20:24 +01:00
Simon Larsen
438fbf4368 add historgram in program 2023-10-24 20:49:10 +01:00
Simon Larsen
ffca1acc9a Merge pull request #858 from cheese-framework/Add-Jest-test-suite-for-Common/Database/Date-#857
Add Test cases for Common/Database/Date
2023-10-24 20:25:24 +01:00
Drantaz
846d5ce104 Add Test cases for Common/Database/Date -- Add linting 2023-10-24 17:14:26 +00:00
Drantaz
43a075436a Add Test cases for Common/Database/Date 2023-10-24 16:55:11 +00:00
Simon Larsen
8fe35d9a29 remove unneeded files 2023-10-24 16:54:14 +01:00
Simon Larsen
849eeac23a add auto table creation 2023-10-24 13:52:37 +01:00
Simon Larsen
01a4cac559 Merge pull request #830 from OneUptime/otel-save
Otel save
2023-10-24 13:50:38 +01:00
Simon Larsen
b4cd4d2c02 comment out table creation 2023-10-24 13:50:15 +01:00
Simon Larsen
329484fb87 fix fmt 2023-10-24 13:49:27 +01:00
Simon Larsen
ee54a324d7 add guage 2023-10-24 13:35:26 +01:00
Simon Larsen
ba2feffbee Merge pull request #826 from hammadfauz/BearerTokenAuthTest
Bearer token auth test
2023-10-24 12:30:07 +01:00
Hammad
4b0b91396b lint fixes 2023-10-24 16:26:53 +05:00
Hammad
f2c6321216 raises proper exception when token is invalid or empty 2023-10-24 16:22:45 +05:00
Simon Larsen
67447c0bd7 fix copy in criteria 2023-10-24 11:57:06 +01:00
Simon Larsen
323646ebcd docker file for otel proj 2023-10-24 11:56:52 +01:00
Simon Larsen
81e4b4435c fix array save 2023-10-24 11:32:02 +01:00
Simon Larsen
842b0664c7 update long numbers 2023-10-24 10:56:41 +01:00
Simon Larsen
0bdab474de fix models 2023-10-24 10:46:44 +01:00
Simon Larsen
ef1b22e62b Merge pull request #856 from OneUptime/simlarsen-patch-1
Update DockerCompose.md
2023-10-24 10:22:51 +01:00
Simon Larsen
3d229a0030 Update DockerCompose.md 2023-10-24 10:22:37 +01:00
Simon Larsen
e34599d18a fix fmt 2023-10-23 20:41:33 +01:00
Simon Larsen
aa7594f2a8 fix fmt 2023-10-23 20:24:36 +01:00
Simon Larsen
0626669b02 add metrics histogram and sum 2023-10-23 20:19:20 +01:00
Simon Larsen
35b949e448 add new models to clickhouse 2023-10-23 20:02:44 +01:00
Simon Larsen
2bb4086fd1 refactor service code 2023-10-23 15:58:16 +01:00
Simon Larsen
03f9c36f06 fix fmt 2023-10-23 15:51:38 +01:00
Simon Larsen
9fe998a43d save logs to otel 2023-10-23 15:27:22 +01:00
Simon Larsen
3841b655e5 add dotnet otel project 2023-10-23 15:17:25 +01:00
Hammad
5ec8ee6dcb removes jest config for ignoring snapshots 2023-10-23 19:17:19 +05:00
Hammad
a1c6121bee tests UI by querying key elements and removes snapshots 2023-10-23 19:15:10 +05:00
Hammad
51c76aa1af moves test suite into Components as per convention 2023-10-23 19:14:04 +05:00
Simon Larsen
40ee5d775b fix fmt 2023-10-23 12:57:02 +01:00
Simon Larsen
88f0e2af51 set default card for customers if they dont have it. 2023-10-23 11:27:19 +01:00
Hammad
e702a0b0d2 Fixes lint errors 2023-10-21 15:49:54 +05:00
Hammad
cfc2f99248 adds tests for DuplicateModel 2023-10-21 15:07:20 +05:00
Hammad
f23bb3af41 fixes jest mistaking snapshots for test suites 2023-10-21 15:06:46 +05:00
Simon Larsen
2cdf1236be fix fmt 2023-10-20 19:52:58 +01:00
Simon Larsen
ed5a144735 fix insert into db 2023-10-20 19:51:23 +01:00
Simon Larsen
e687a439e6 fix fmt 2023-10-20 17:58:45 +01:00
Simon Larsen
cfa20e2be6 generate insert statement 2023-10-20 17:43:50 +01:00
hasan
9205764deb Add tests for notification middleware and cookie utils 2023-10-20 18:44:33 +05:00
Simon Larsen
32275837ac fix nested type 2023-10-20 14:26:01 +01:00
Simon Larsen
34568a39f5 refactor staement generator into a new file 2023-10-20 13:01:51 +01:00
Simon Larsen
b7b41dfebb Merge branch 'master' into otel-save 2023-10-20 11:27:21 +01:00
Simon Larsen
9b40011196 add more shadow 2023-10-20 11:24:02 +01:00
Simon Larsen
d644287a0c fix video close 2023-10-20 11:18:19 +01:00
Simon Larsen
ea7dc0b918 add hiring link in footer 2023-10-19 18:52:24 +01:00
Simon Larsen
c34639a3bb add watch demo to status page 2023-10-19 18:23:54 +01:00
Simon Larsen
41ba37be80 nested model 2023-10-19 16:06:09 +01:00
Simon Larsen
954d5be113 fix clickhouse 2023-10-18 18:18:00 +01:00
Simon Larsen
21a857d912 fix import 2023-10-18 17:47:45 +01:00
Simon Larsen
cb0f7bbad5 fix fmt 2023-10-18 17:44:54 +01:00
Simon Larsen
c3c94f3634 add table 2023-10-18 17:43:18 +01:00
Simon Larsen
955141d42e add logs, metrics and spans 2023-10-18 17:28:49 +01:00
Simon Larsen
352c9ffb8e otel save 2023-10-18 13:59:19 +01:00
Simon Larsen
d543757a7d open collector port 2023-10-18 12:19:52 +01:00
Simon Larsen
b3cfdbf45a enable gpu on llama docker 2023-10-18 12:07:37 +01:00
Simon Larsen
c629921d01 fix llama docker file. 2023-10-18 11:01:15 +01:00
Simon Larsen
008e0c50b1 fix cluster domain in helm chart 2023-10-18 10:16:23 +01:00
Simon Larsen
fcf916bdfe change to alert emoji 2023-10-17 14:48:54 +01:00
Simon Larsen
9850bcf0e7 fix fmt 2023-10-17 14:30:25 +01:00
Simon Larsen
e1efeec9ec add cookie set 2023-10-17 13:49:43 +01:00
Simon Larsen
7e34393fc6 fix api url 2023-10-17 12:59:14 +01:00
Simon Larsen
262fffd9ff fix url 2023-10-17 12:48:46 +01:00
Simon Larsen
35db6e95ad add httponly cookie to status page 2023-10-17 12:10:50 +01:00
Simon Larsen
17208b5e26 fix token cookie on master page 2023-10-16 21:16:11 +01:00
Simon Larsen
896dce3430 add pycache to gitignore 2023-10-16 20:54:21 +01:00
Simon Larsen
d844fa9df2 fix api 2023-10-16 20:06:21 +01:00
Simon Larsen
48542c4323 fix typo 2023-10-16 19:26:13 +01:00
Simon Larsen
f57047c778 fix sttaus page api 2023-10-16 19:01:45 +01:00
Simon Larsen
e471787462 set cookie for host 2023-10-16 18:15:25 +01:00
Hammad
dc4721f878 fixes lint errors 2023-10-16 22:01:05 +05:00
Simon Larsen
4bd4dbf3c1 fix domains cookie set on status page 2023-10-16 17:41:49 +01:00
Hammad
6c0c79dd25 removes line that will never be run
this brings code coverage to 100%
2023-10-16 21:14:36 +05:00
Hammad
a9548858b0 adds tests for CommonServer/Middleware/BearerTokenAuthorization 2023-10-16 21:12:31 +05:00
Simon Larsen
6804e94850 add ingestor status check 2023-10-16 12:55:54 +01:00
Simon Larsen
63736aed6c fix path 2023-10-16 12:53:15 +01:00
Simon Larsen
c848032fdc add otel to helm chart 2023-10-16 12:41:27 +01:00
Simon Larsen
22c2231e22 make llama work with rest api 2023-10-16 11:45:15 +01:00
Simon Larsen
7a063d741c fix otel collector 2023-10-16 11:06:11 +01:00
Simon Larsen
8a9cc10ff0 fix otel collector 2023-10-16 11:04:47 +01:00
Simon Larsen
2e43fa0c02 Merge pull request #820 from hasannadeem/test/side-menu-item-and-dictionary-of-string-components
Added tests for SideMenuItem and DictionaryOfStrings components
2023-10-16 10:54:11 +01:00
Simon Larsen
f51a1828ab fix fmt 2023-10-15 21:41:49 +01:00
Simon Larsen
805139055a make llama work 2023-10-15 21:04:58 +01:00
Simon Larsen
42c85b16e7 delete nodejs adaptor from llama 2023-10-15 18:34:25 +01:00
Simon Larsen
a59742cddb add python app for llama. 2023-10-15 18:14:15 +01:00
Simon Larsen
ba426b5580 fix llama 2023-10-14 17:59:52 +01:00
Simon Larsen
1945bbfd45 fix llama compile err 2023-10-14 16:45:17 +01:00
Simon Larsen
58debb9959 male llama work 2023-10-14 16:36:12 +01:00
hasan
6485f474b2 Add tests for SideMenuItem and DictionaryOfStrings components 2023-10-14 18:11:51 +05:00
Simon Larsen
301d7f124c fix fmt 2023-10-14 12:01:06 +01:00
Simon Larsen
985217d2bf rename helm files for ingestor 2023-10-13 20:14:04 +01:00
311 changed files with 13138 additions and 1604 deletions

View File

@@ -54,4 +54,7 @@ tests/coverage
settings.json
GoSDK/tester/
GoSDK/tester/
Llama/Models/*

View File

@@ -179,11 +179,13 @@
"ignoreReadBeforeAssign": false
}
],
"no-var": "error"
"no-var": "error",
"object-curly-spacing": ["error", "always"],
"no-unneeded-ternary": "error"
},
"settings": {
"react": {
"version": "18.1.0"
}
}
}
}

View File

@@ -1,4 +1,4 @@
name: Probe Api Test
name: Ingestor Test
on:
pull_request:

5
.gitignore vendored
View File

@@ -92,3 +92,8 @@ Haraka/dkim/keys/public_base64.txt
.eslintcache
HelmChart/Values/*.values.yaml
Llama/Models/tokenizer*
Llama/Models/llama*
Llama/__pycache__/*

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

View File

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

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

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

View File

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

View File

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

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

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

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

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

View File

@@ -0,0 +1,52 @@
import BaseModel from '../../Models/BaseModel';
import { JSONObject } from '../../Types/JSON';
import JSONFunctions from '../../Types/JSONFunctions';
describe('JSONFunctions Class', () => {
let baseModel: BaseModel;
beforeEach(() => {
baseModel = new BaseModel();
});
describe('isEmptyObject Method', () => {
test('Returns true for an empty object', () => {
const emptyObj: JSONObject = {};
expect(JSONFunctions.isEmptyObject(emptyObj)).toBe(true);
});
test('Returns false for a non-empty object', () => {
const nonEmptyObj: JSONObject = { key: 'value' };
expect(JSONFunctions.isEmptyObject(nonEmptyObj)).toBe(false);
});
test('Returns true for null or undefined', () => {
expect(JSONFunctions.isEmptyObject(null)).toBe(true);
expect(JSONFunctions.isEmptyObject(undefined)).toBe(true);
});
});
describe('toJSON and fromJSON Methods', () => {
test('toJSON returns a valid JSON object', () => {
const json: JSONObject = JSONFunctions.toJSON(baseModel, BaseModel);
expect(json).toEqual(expect.objectContaining({}));
});
test('toJSONObject returns a valid JSON object', () => {
const json: JSONObject = JSONFunctions.toJSONObject(
baseModel,
BaseModel
);
expect(json).toEqual(expect.objectContaining({}));
});
test('fromJSON returns a BaseModel instance', () => {
const json: JSONObject = { name: 'oneuptime' };
const result: BaseModel | BaseModel[] = JSONFunctions.fromJSON(
json,
BaseModel
);
expect(result).toBeInstanceOf(BaseModel);
});
});
});

View File

@@ -0,0 +1,44 @@
import NotImplementedException from '../../Types/Exception/NotImplementedException';
import { JSONObject } from '../../Types/JSON';
import SerializableObject from '../../Types/SerializableObject';
describe('SerializableObject Class', () => {
let serializableObject: SerializableObject;
beforeEach(() => {
serializableObject = new SerializableObject();
});
test('Constructor initializes an instance of SerializableObject', () => {
expect(serializableObject).toBeInstanceOf(SerializableObject);
});
describe('toJSON Method', () => {
test('Throws NotImplementedException when called', () => {
expect(() => {
return serializableObject.toJSON();
}).toThrow(NotImplementedException);
});
});
describe('fromJSON Method', () => {
test('Throws NotImplementedException when called', () => {
expect(() => {
return SerializableObject.fromJSON({});
}).toThrow(NotImplementedException);
});
});
describe('fromJSON Instance Method', () => {
test('Returns the result from the static fromJSON method', () => {
const json: JSONObject = { key: 'value' };
const expectedResult: SerializableObject = new SerializableObject();
jest.spyOn(SerializableObject, 'fromJSON').mockReturnValue(
expectedResult
);
const result: SerializableObject =
serializableObject.fromJSON(json);
expect(result).toBe(expectedResult);
});
});
});

View File

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

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

View File

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

View File

@@ -1,4 +1,4 @@
import TableColumnType from '../BaseDatabase/TableColumnType';
import TableColumnType from './TableColumnType';
enum ColumnLength {
Version = 30,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,10 +11,10 @@ enum NotificationSettingEventType {
SEND_MONITOR_STATUS_CHANGED_OWNER_NOTIFICATION = 'Send monitor status changed notification when I am the owner of the monitor',
// Scheduled Maintenance
SEND_SCHEDULED_MAINTENANCE_CREATED_OWNER_NOTIFICATION = 'Send scheduled maintenance created notification when I am the owner of the scheduled maintenance',
SEND_SCHEDULED_MAINTENANCE_NOTE_POSTED_OWNER_NOTIFICATION = 'Send scheduled maintenance note posted notification when I am the owner of the scheduled maintenance',
SEND_SCHEDULED_MAINTENANCE_OWNER_ADDED_NOTIFICATION = 'Send notification when I am added as a owner to the scheduled maintenance',
SEND_SCHEDULED_MAINTENANCE_STATE_CHANGED_OWNER_NOTIFICATION = 'Send scheduled maintenance state changed notification when I am the owner of the scheduled maintenance',
SEND_SCHEDULED_MAINTENANCE_CREATED_OWNER_NOTIFICATION = 'Send event created notification when I am the owner of the event',
SEND_SCHEDULED_MAINTENANCE_NOTE_POSTED_OWNER_NOTIFICATION = 'Send event note posted notification when I am the owner of the event',
SEND_SCHEDULED_MAINTENANCE_OWNER_ADDED_NOTIFICATION = 'Send notification when I am added as a owner to the event',
SEND_SCHEDULED_MAINTENANCE_STATE_CHANGED_OWNER_NOTIFICATION = 'Send event state changed notification when I am the owner of the event',
// Status Page
SEND_STATUS_PAGE_ANNOUNCEMENT_CREATED_OWNER_NOTIFICATION = 'Send status page announcement created notification when I am the owner of the status page',

View File

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

View File

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

View File

@@ -1637,9 +1637,9 @@
}
},
"node_modules/crypto-js": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz",
"integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw=="
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"node_modules/cssom": {
"version": "0.4.4",
@@ -5943,9 +5943,9 @@
}
},
"crypto-js": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz",
"integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw=="
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"cssom": {
"version": "0.4.4",

View File

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

View File

@@ -54,6 +54,7 @@ export default class ProjectAPI extends BaseAPI<Project, ProjectServiceType> {
trialEndsAt: true,
paymentProviderPlanId: true,
resellerId: true,
isFeatureFlagMonitorGroupsEnabled: true,
},
},
limit: LIMIT_PER_PROJECT,

View File

@@ -61,6 +61,10 @@ import PositiveNumber from 'Common/Types/PositiveNumber';
import StatusPageSsoService from '../Services/StatusPageSsoService';
import StatusPageSSO from 'Model/Models/StatusPageSso';
import ArrayUtil from 'Common/Types/ArrayUtil';
import Dictionary from 'Common/Types/Dictionary';
import MonitorGroupService from '../Services/MonitorGroupService';
import MonitorGroupResource from 'Model/Models/MonitorGroupResource';
import MonitorGroupResourceService from '../Services/MonitorGroupResourceService';
export default class StatusPageAPI extends BaseAPI<
StatusPage,
@@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

View File

@@ -0,0 +1,187 @@
import VoiceResponse from 'twilio/lib/twiml/VoiceResponse';
import NotificationMiddleware from '../../Middleware/NotificationMiddleware';
// Helpers
import Response from '../../Utils/Response';
import JSONWebToken from '../../Utils/JsonWebToken';
import BadDataException from 'Common/Types/Exception/BadDataException';
import JSONFunctions from 'Common/Types/JSONFunctions';
// Types
import {
ExpressRequest,
ExpressResponse,
NextFunction,
} from '../../Utils/Express';
import { OnCallInputRequest } from 'Common/Types/Call/CallRequest';
import { JSONObject } from 'Common/Types/JSON';
jest.mock('twilio/lib/twiml/VoiceResponse');
jest.mock('../../Utils/Response');
jest.mock('../../Utils/JsonWebToken', () => {
return {
decodeJsonPayload: jest.fn(),
};
});
jest.mock('Common/Types/JSONFunctions', () => {
return {
deserialize: jest.fn(),
};
});
describe('NotificationMiddleware', () => {
describe('sendResponse', () => {
let mockRequest: ExpressRequest;
let mockResponse: ExpressResponse;
let onCallInputRequest: OnCallInputRequest;
beforeEach(() => {
mockRequest = { body: { Digits: '1234' } } as ExpressRequest;
mockResponse = {} as ExpressResponse;
onCallInputRequest = {
default: { sayMessage: 'default message' },
};
});
afterEach(() => {
jest.resetAllMocks();
});
test('Should return correct message for a valid Digits value', async () => {
onCallInputRequest['1234'] = { sayMessage: 'message 1' };
const responseInstance: VoiceResponse = new VoiceResponse();
(
VoiceResponse as jest.MockedClass<typeof VoiceResponse>
).mockImplementation(() => {
return responseInstance;
});
await NotificationMiddleware.sendResponse(
mockRequest,
mockResponse,
onCallInputRequest
);
expect(responseInstance.say).toHaveBeenCalledWith(
(onCallInputRequest['1234'] as any).sayMessage
);
expect(Response.sendXmlResponse).toHaveBeenCalledWith(
mockRequest,
mockResponse,
responseInstance.toString()
);
});
test('Should return default message for an invalid Digits value', async () => {
const responseInstance: VoiceResponse = new VoiceResponse();
(
VoiceResponse as jest.MockedClass<typeof VoiceResponse>
).mockImplementation(() => {
return responseInstance;
});
await NotificationMiddleware.sendResponse(
mockRequest,
mockResponse,
onCallInputRequest
);
expect(responseInstance.say).toHaveBeenCalledWith(
onCallInputRequest['default']?.sayMessage
);
expect(Response.sendXmlResponse).toHaveBeenCalledWith(
mockRequest,
mockResponse,
responseInstance.toString()
);
});
});
describe('isValidCallNotificationRequest', () => {
let mockRequest: ExpressRequest;
let mockResponse: ExpressResponse;
let mockNext: NextFunction;
beforeEach(() => {
mockRequest = {
body: {},
query: {},
} as ExpressRequest;
mockResponse = {} as ExpressResponse;
mockNext = jest.fn() as NextFunction;
});
afterEach(() => {
jest.resetAllMocks();
});
test('Should return error if Digits is not in req body', async () => {
await NotificationMiddleware.isValidCallNotificationRequest(
mockRequest,
mockResponse,
mockNext
);
expect(Response.sendErrorResponse).toHaveBeenCalledWith(
mockRequest,
mockResponse,
new BadDataException('Invalid input')
);
});
test('Should return error if Token is not in req query', async () => {
mockRequest.body['Digits'] = '1234';
await NotificationMiddleware.isValidCallNotificationRequest(
mockRequest,
mockResponse,
mockNext
);
expect(Response.sendErrorResponse).toHaveBeenCalledWith(
mockRequest,
mockResponse,
new BadDataException('Invalid token')
);
});
test('Should return error if token decoding fails', async () => {
mockRequest.body['Digits'] = '1234';
mockRequest.query['token'] = 'token';
jest.spyOn(JSONWebToken, 'decodeJsonPayload').mockImplementation(
() => {
throw new Error('Decoding error');
}
);
await NotificationMiddleware.isValidCallNotificationRequest(
mockRequest,
mockResponse,
mockNext
);
expect(Response.sendErrorResponse).toHaveBeenCalledWith(
mockRequest,
mockResponse,
new BadDataException('Invalid token')
);
});
test("Should call 'next' if data is valid", async () => {
mockRequest.body['Digits'] = '1234';
mockRequest.query['token'] = 'token';
const tokenData: JSONObject = { id: 1 };
jest.spyOn(JSONFunctions, 'deserialize').mockReturnValue(tokenData);
await NotificationMiddleware.isValidCallNotificationRequest(
mockRequest,
mockResponse,
mockNext
);
expect(mockNext).toHaveBeenCalled();
expect((mockRequest as any).callTokenData).toEqual(tokenData);
});
});
});

View File

@@ -0,0 +1,123 @@
import { JSONObject } from 'Common/Types/JSON';
import CookieUtil from '../../Utils/Cookie';
import { ExpressRequest, ExpressResponse } from '../../Utils/Express';
import ObjectID from 'Common/Types/ObjectID';
import Dictionary from 'Common/Types/Dictionary';
describe('CookieUtils', () => {
let mockRequest: ExpressRequest;
let mockResponse: ExpressResponse;
beforeEach(() => {
mockRequest = {
cookies: {},
} as ExpressRequest;
mockResponse = {} as ExpressResponse;
});
afterEach(() => {
jest.clearAllMocks();
});
test('Should set a cookie', () => {
const cookie: JSONObject = {
name: 'testName',
value: 'testValue',
options: {},
};
mockResponse.cookie = jest.fn();
CookieUtil.setCookie(
mockResponse,
cookie['name'] as string,
cookie['value'] as string,
cookie['options'] as JSONObject
);
expect(mockResponse.cookie).toHaveBeenCalledWith(
cookie['name'] as string,
cookie['value'] as string,
cookie['options'] as JSONObject
);
});
test('Should return a cookie', () => {
const cookieName: string = 'testName';
const cookieValue: string = 'testValue';
mockRequest.cookies[cookieName] = cookieValue;
const value: string | undefined = CookieUtil.getCookie(
mockRequest,
cookieName
);
expect(value).toBe(value);
});
test('Should remove a cookie', () => {
const cookieName: string = 'testName';
mockResponse.clearCookie = jest.fn();
CookieUtil.removeCookie(mockResponse, cookieName);
expect(mockResponse.clearCookie).toHaveBeenCalledWith(cookieName);
});
test('Should return all cookies', () => {
const value: string = 'testValue';
mockRequest.cookies = { testName: value };
const cookies: Dictionary<string> =
CookieUtil.getAllCookies(mockRequest);
expect(cookies).toEqual({ testName: value });
});
test('Should return empty object if there are no cookies', () => {
mockRequest.cookies = undefined;
const cookies: Dictionary<string> =
CookieUtil.getAllCookies(mockRequest);
expect(cookies).toEqual({});
});
test('Should return user token key', () => {
const id: string = '123456789';
const keyWithId: string = CookieUtil.getUserTokenKey(new ObjectID(id));
const keyWithoutId: string = CookieUtil.getUserTokenKey();
expect(keyWithId).toBe(`user-token-${id}`);
expect(keyWithoutId).toBe('user-token');
});
test('Should return SSO key', () => {
const ssoKey: string = CookieUtil.getSSOKey();
expect(ssoKey).toBe('sso-');
});
test('Should return user SSO key', () => {
const id: string = '123456789';
const userSsoKey: string = CookieUtil.getUserSSOKey(new ObjectID(id));
expect(userSsoKey).toBe(`sso-${id}`);
});
test('Should remove all cookies', () => {
const cookies: Dictionary<string> = {
testName1: 'testValue1',
testName2: 'testValue2',
};
mockRequest.cookies = cookies;
mockResponse.clearCookie = jest.fn();
CookieUtil.removeAllCookies(mockRequest, mockResponse);
expect(mockResponse.clearCookie).toHaveBeenCalledWith(
Object.keys(cookies)[0]
);
expect(mockResponse.clearCookie).toHaveBeenCalledWith(
Object.keys(cookies)[1]
);
});
});

View File

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

View File

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

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

View File

@@ -175,9 +175,13 @@ export default class Response {
req: ExpressRequest,
res: ExpressResponse,
list: Array<BaseModel>,
count: PositiveNumber,
count: PositiveNumber | number,
modelType: { new (): BaseModel }
): void {
if (!(count instanceof PositiveNumber)) {
count = new PositiveNumber(count);
}
return this.sendJsonArrayResponse(
req,
res,

View File

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

View File

@@ -28,5 +28,5 @@
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css|less)$": "identity-obj-proxy",
"uuid":"<rootDir>/node_modules/jest-runtime/build/index.js"
}
}
}

View File

@@ -36,12 +36,16 @@ const Card: FunctionComponent<ComponentProps> = (
<div className="flex justify-between">
<div>
<h2
id="payment-details-heading"
data-testid="card-details-heading"
id="card-details-heading"
className="text-lg font-medium leading-6 text-gray-900"
>
{props.title}
</h2>
<p className="mt-1 text-sm text-gray-500">
<p
data-testid="card-description"
className="mt-1 text-sm text-gray-500"
>
{props.description}
</p>
</div>
@@ -78,6 +82,7 @@ const Card: FunctionComponent<ComponentProps> = (
shortcutKey={
button.shortcutKey
}
dataTestId="card-button"
/>
</div>
);

View File

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

View File

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

View File

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

View File

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

View File

@@ -146,6 +146,14 @@ const Icon: FunctionComponent<ComponentProps> = ({
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75"
/>
);
} else if (icon === IconProp.Squares) {
return getSvgWrapper(
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z"
/>
);
} else if (icon === IconProp.Pencil) {
return getSvgWrapper(
<path
@@ -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

View File

@@ -9,7 +9,7 @@ import Navigation from '../../Utils/Navigation';
export interface ComponentProps {
children: ReactElement | Array<ReactElement> | string;
className?: undefined | string;
to: Route | URL | null | undefined;
to?: Route | URL | null | undefined;
onClick?: undefined | (() => void);
onNavigateComplete?: (() => void) | undefined;
openInNewTab?: boolean | undefined;

View File

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

View File

@@ -38,7 +38,10 @@ const ConfirmModal: FunctionComponent<ComponentProps> = (
}
error={props.error}
>
<div className="text-gray-500 mt-5 text-sm">
<div
data-testid="confirm-modal-description"
className="text-gray-500 mt-5 text-sm"
>
{props.description}
</div>
</Modal>

View File

@@ -108,6 +108,7 @@ const Modal: FunctionComponent<ComponentProps> = (
<div className="flex justify-between">
<div>
<h3
data-testid="modal-title"
className={`text-lg font-medium leading-6 text-gray-900 ${
props.icon
? 'ml-10 -mt-8 mb-5'
@@ -118,7 +119,10 @@ const Modal: FunctionComponent<ComponentProps> = (
{props.title}
</h3>
{props.description && (
<h3 className="text-sm leading-6 text-gray-500">
<h3
data-testid="modal-description"
className="text-sm leading-6 text-gray-500"
>
{props.description}
</h3>
)}

View File

@@ -40,6 +40,7 @@ const ModalFooter: FunctionComponent<ComponentProps> = (
? props.submitButtonType
: ButtonType.Button
}
dataTestId="modal-footer-submit-button"
/>
) : (
<></>
@@ -57,6 +58,7 @@ const ModalFooter: FunctionComponent<ComponentProps> = (
onClick={() => {
props.onClose && props.onClose();
}}
dataTestId="modal-footer-close-button"
/>
) : (
<></>

View File

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

View File

@@ -12,6 +12,11 @@ import OneUptimeDate from 'Common/Types/Date';
import DayUptimeGraph, { Event } from '../Graphs/DayUptimeGraph';
import { Green } from 'Common/Types/BrandColors';
import ErrorMessage from '../ErrorMessage/ErrorMessage';
import ObjectID from 'Common/Types/ObjectID';
export interface MonitorEvent extends Event {
monitorId: ObjectID;
}
export interface ComponentProps {
startDate: Date;
@@ -28,27 +33,173 @@ const MonitorUptimeGraph: FunctionComponent<ComponentProps> = (
): ReactElement => {
const [events, setEvents] = useState<Array<Event>>([]);
useEffect(() => {
const eventList: Array<Event> = [];
// convert data to events.
for (let i: number = 0; i < props.items.length; i++) {
if (!props.items[i]) {
break;
/**
* This function, `getMonitorEventsForId`, takes a `monitorId` as an argument and returns an array of `MonitorEvent` objects.
* @param {ObjectID} monitorId - The ID of the monitor for which events are to be fetched.
* @returns {Array<MonitorEvent>} - An array of `MonitorEvent` objects.
*/
const getMonitorEventsForId: (
monitorId: ObjectID
) => Array<MonitorEvent> = (monitorId: ObjectID): Array<MonitorEvent> => {
// Initialize an empty array to store the monitor events.
const eventList: Array<MonitorEvent> = [];
const monitorEvents: Array<MonitorStatusTimeline> = props.items.filter(
(item: MonitorStatusTimeline) => {
return item.monitorId?.toString() === monitorId.toString();
}
);
// Loop through the items in the props object.
for (let i: number = 0; i < monitorEvents.length; i++) {
// If the current item is null or undefined, skip to the next iteration.
if (!monitorEvents[i]) {
continue;
}
// Set the start date of the event to the creation date of the current item. If it doesn't exist, use the current date.
const startDate: Date =
monitorEvents[i]!.createdAt || OneUptimeDate.getCurrentDate();
// Initialize the end date as the current date.
let endDate: Date = OneUptimeDate.getCurrentDate();
// If there is a next item and it has a creation date, use that as the end date.
if (monitorEvents[i + 1] && monitorEvents[i + 1]!.createdAt) {
endDate = monitorEvents[i + 1]!.createdAt!;
}
// Push a new MonitorEvent object to the eventList array with properties from the current item and calculated dates.
eventList.push({
startDate:
props.items[i]!.createdAt || OneUptimeDate.getCurrentDate(),
endDate:
props.items[i + 1] && props.items[i + 1]!.createdAt
? (props.items[i + 1]?.createdAt as Date)
: OneUptimeDate.getCurrentDate(),
label: props.items[i]?.monitorStatus?.name || 'Operational',
priority: props.items[i]?.monitorStatus?.priority || 0,
color: props.items[i]?.monitorStatus?.color || Green,
startDate: startDate,
endDate: endDate,
label: monitorEvents[i]?.monitorStatus?.name || 'Operational',
priority: monitorEvents[i]?.monitorStatus?.priority || 0,
color: monitorEvents[i]?.monitorStatus?.color || Green,
monitorId: monitorEvents[i]!.monitorId!,
});
}
// Return the populated eventList array.
return eventList;
};
const getMonitorEvents: () => Array<MonitorEvent> =
(): Array<MonitorEvent> => {
// get all distinct monitor ids.
const monitorIds: Array<ObjectID> = [];
for (let i: number = 0; i < props.items.length; i++) {
if (!props.items[i]) {
continue;
}
const monitorId: string | undefined =
props.items[i]!.monitorId?.toString();
if (!monitorId) {
continue;
}
if (
!monitorIds.find((item: ObjectID) => {
return item.toString() === monitorId;
})
) {
monitorIds.push(new ObjectID(monitorId));
}
}
const eventList: Array<MonitorEvent> = [];
// convert data to events.
for (const monitorId of monitorIds) {
const monitorEvents: Array<MonitorEvent> =
getMonitorEventsForId(monitorId);
eventList.push(...monitorEvents);
}
// sort event list by start date.
eventList.sort((a: MonitorEvent, b: MonitorEvent) => {
if (OneUptimeDate.isAfter(a.startDate, b.startDate)) {
return 1;
}
if (OneUptimeDate.isAfter(b.startDate, a.startDate)) {
return -1;
}
return 0;
});
return [...eventList];
};
useEffect(() => {
const monitorEventList: Array<Event> = getMonitorEvents();
const eventList: Array<Event> = [];
for (const monitorEvent of monitorEventList) {
// if this event starts after the last event, then add it to the list directly.
if (
eventList.length === 0 ||
OneUptimeDate.isAfter(
monitorEvent.startDate,
eventList[eventList.length - 1]!.endDate
) ||
OneUptimeDate.isEqualBySeconds(
monitorEvent.startDate,
eventList[eventList.length - 1]!.endDate
)
) {
eventList.push(monitorEvent);
continue;
}
// if this event starts before the last event, then we need to check if it ends before the last event. If it does, then we can skip this event if the monitrEvent is of lower priority than the last event. If it is of higher priority, then we need to add it to the list and remove the last event from the list.
if (
OneUptimeDate.isBefore(
monitorEvent.startDate,
eventList[eventList.length - 1]!.endDate
)
) {
if (
monitorEvent.priority >
eventList[eventList.length - 1]!.priority
) {
// end the last event at the start of this event.
const tempLastEvent: Event = {
...eventList[eventList.length - 1],
} as Event;
eventList[eventList.length - 1]!.endDate =
monitorEvent.startDate;
eventList.push(monitorEvent);
// if the monitorEvent endDate is before the end of the last event, then we need to add the end of the last event to the list.
if (
OneUptimeDate.isBefore(
monitorEvent.endDate,
tempLastEvent.endDate
)
) {
eventList.push({
startDate: monitorEvent.endDate,
endDate: tempLastEvent.endDate,
label: tempLastEvent.label,
priority: tempLastEvent.priority,
color: tempLastEvent.color,
});
}
}
continue;
}
}
setEvents(eventList);
}, [props.items]);

View File

@@ -14,7 +14,7 @@ const Statusbubble: FunctionComponent<ComponentProps> = (
return (
<div className="flex" style={props.style}>
<div
className="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full mr-2"
className="animate-pulse flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full mr-2"
style={{
backgroundColor: props.color
? props.color.toString()

View File

@@ -0,0 +1,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();
});
});

View File

@@ -0,0 +1,322 @@
import React from 'react';
import BaseModel from 'Common/Models/BaseModel';
import Select from '../../Utils/ModelAPI/Select';
import { ModelField } from '../../Components/Forms/ModelForm';
import TableMetaData from 'Common/Types/Database/TableMetadata';
import IconProp from 'Common/Types/Icon/IconProp';
import {
render,
screen,
within,
fireEvent,
waitFor,
} from '@testing-library/react';
import DuplicateModel from '../../Components/DuplicateModel/DuplicateModel';
import ObjectID from 'Common/Types/ObjectID';
import CrudApiEndpoint from 'Common/Types/Database/CrudApiEndpoint';
import { act } from 'react-test-renderer';
import Route from 'Common/Types/API/Route';
@TableMetaData({
tableName: 'Foo',
singularName: 'Foo',
pluralName: 'Foos',
icon: IconProp.Wrench,
tableDescription: 'A test model',
})
@CrudApiEndpoint(new Route('/testModel'))
class TestModel extends BaseModel {
public changeThis?: string = 'original';
}
jest.mock('../../Utils/ModelAPI/ModelAPI', () => {
return {
getItem: jest
.fn()
.mockResolvedValueOnce({
changeThis: 'changed',
setValue: function (key: 'changeThis', value: string) {
this[key] = value;
},
removeValue: jest.fn(),
})
.mockResolvedValueOnce({
changeThis: 'changed',
setValue: function (key: 'changeThis', value: string) {
this[key] = value;
},
removeValue: jest.fn(),
})
.mockResolvedValueOnce(undefined),
create: jest
.fn()
.mockResolvedValueOnce({
data: {
id: 'foobar',
changeThis: 'changed',
},
})
.mockResolvedValueOnce(undefined),
};
});
jest.mock('../../Utils/Navigation', () => {
return {
navigate: jest.fn(),
};
});
describe('DuplicateModel', () => {
const fieldsToDuplicate: Select<TestModel> = {};
const fieldsToChange: Array<ModelField<TestModel>> = [
{
field: {
changeThis: true,
},
title: 'Change This',
required: false,
placeholder: 'You can change this',
},
];
it('renders correctly', () => {
render(
<DuplicateModel
modelType={TestModel}
modelId={new ObjectID('foo')}
fieldsToDuplicate={fieldsToDuplicate}
fieldsToChange={fieldsToChange}
/>
);
expect(screen.getByTestId('card-details-heading')?.textContent).toBe(
'Duplicate Foo'
);
expect(screen.getByTestId('card-description')?.textContent).toBe(
'Duplicating this foo will create another foo exactly like this one.'
);
expect(screen.getByTestId('card-button')?.textContent).toBe(
'Duplicate Foo'
);
});
it('shows confirmation modal when duplicate button is clicked', () => {
render(
<DuplicateModel
modelType={TestModel}
modelId={new ObjectID('foo')}
fieldsToDuplicate={fieldsToDuplicate}
fieldsToChange={fieldsToChange}
/>
);
const button: HTMLElement = screen.getByRole('button', {
name: 'Duplicate Foo',
});
fireEvent.click(button);
expect(screen.getByRole('dialog')).toBeDefined();
const confirmDialog: HTMLElement = screen.getByRole('dialog');
expect(
within(confirmDialog).getByTestId('modal-title')?.textContent
).toBe('Duplicate Foo');
expect(
within(confirmDialog).getByTestId('modal-description')?.textContent
).toBe('Are you sure you want to duplicate this foo?');
expect(
within(confirmDialog).getByTestId('modal-footer-submit-button')
?.textContent
).toBe('Duplicate Foo');
expect(
within(confirmDialog).getByTestId('modal-footer-close-button')
?.textContent
).toBe('Close');
});
it('duplicates item when confirmation button is clicked', async () => {
const onDuplicateSuccess: (item: TestModel) => void = jest.fn();
render(
<DuplicateModel
modelType={TestModel}
modelId={new ObjectID('foo')}
fieldsToDuplicate={fieldsToDuplicate}
fieldsToChange={fieldsToChange}
onDuplicateSuccess={onDuplicateSuccess}
navigateToOnSuccess={new Route('/done')}
/>
);
const button: HTMLElement = screen.getByRole('button', {
name: 'Duplicate Foo',
});
void act(() => {
fireEvent.click(button);
});
const dialog: HTMLElement = screen.getByRole('dialog');
const confirmationButton: HTMLElement = within(dialog).getByRole(
'button',
{
name: 'Duplicate Foo',
}
);
void act(() => {
fireEvent.click(confirmationButton);
});
await waitFor(() => {
return expect(onDuplicateSuccess).toBeCalledWith({
id: 'foobar',
changeThis: 'changed',
});
});
await waitFor(() => {
return expect(
require('../../Utils/Navigation').navigate
).toBeCalledWith(new Route('/done/foobar'));
});
});
it('closes confirmation dialog when close button is clicked', () => {
const onDuplicateSuccess: (item: TestModel) => void = jest.fn();
render(
<DuplicateModel
modelType={TestModel}
modelId={new ObjectID('foo')}
fieldsToDuplicate={fieldsToDuplicate}
fieldsToChange={fieldsToChange}
onDuplicateSuccess={onDuplicateSuccess}
navigateToOnSuccess={new Route('/done')}
/>
);
const button: HTMLElement = screen.getByRole('button', {
name: 'Duplicate Foo',
});
void act(() => {
fireEvent.click(button);
});
const dialog: HTMLElement = screen.getByRole('dialog');
const closeButton: HTMLElement = within(dialog).getByRole('button', {
name: 'Close',
});
void act(() => {
fireEvent.click(closeButton);
});
expect(screen.queryByRole('dialog')).toBeFalsy();
});
it('handles could not create error correctly', async () => {
const onDuplicateSuccess: (item: TestModel) => void = jest.fn();
render(
<DuplicateModel
modelType={TestModel}
modelId={new ObjectID('foo')}
fieldsToDuplicate={fieldsToDuplicate}
fieldsToChange={fieldsToChange}
onDuplicateSuccess={onDuplicateSuccess}
navigateToOnSuccess={new Route('/done')}
/>
);
const button: HTMLElement = screen.getByRole('button', {
name: 'Duplicate Foo',
});
void act(() => {
fireEvent.click(button);
});
const dialog: HTMLElement = screen.getByRole('dialog');
const confirmationButton: HTMLElement = within(dialog).getByRole(
'button',
{
name: 'Duplicate Foo',
}
);
void act(() => {
fireEvent.click(confirmationButton);
});
await screen.findByText('Duplicate Error');
const errorDialog: HTMLElement = screen.getByRole('dialog');
expect(
within(errorDialog).getByTestId('modal-title')?.textContent
).toBe('Duplicate Error');
expect(
within(errorDialog).getByTestId('confirm-modal-description')
?.textContent
).toBe('Error: Could not create Foo');
expect(
within(errorDialog).getByTestId('modal-footer-submit-button')
?.textContent
).toBe('Close');
});
it('handles item not found error correctly', async () => {
const onDuplicateSuccess: (item: TestModel) => void = jest.fn();
render(
<DuplicateModel
modelType={TestModel}
modelId={new ObjectID('foo')}
fieldsToDuplicate={fieldsToDuplicate}
fieldsToChange={fieldsToChange}
onDuplicateSuccess={onDuplicateSuccess}
navigateToOnSuccess={new Route('/done')}
/>
);
const button: HTMLElement = screen.getByRole('button', {
name: 'Duplicate Foo',
});
void act(() => {
fireEvent.click(button);
});
const dialog: HTMLElement = screen.getByRole('dialog');
const confirmationButton: HTMLElement = within(dialog).getByRole(
'button',
{
name: 'Duplicate Foo',
}
);
void act(() => {
fireEvent.click(confirmationButton);
});
await screen.findByText('Duplicate Error');
const errorDialog: HTMLElement = screen.getByRole('dialog');
expect(
within(errorDialog).getByTestId('modal-title')?.textContent
).toBe('Duplicate Error');
expect(
within(errorDialog).getByTestId('confirm-modal-description')
?.textContent
).toBe('Error: Could not find Foo with id foo');
expect(
within(errorDialog).getByTestId('modal-footer-submit-button')
?.textContent
).toBe('Close');
});
it('closes error dialog when close button is clicked', async () => {
const onDuplicateSuccess: (item: TestModel) => void = jest.fn();
render(
<DuplicateModel
modelType={TestModel}
modelId={new ObjectID('foo')}
fieldsToDuplicate={fieldsToDuplicate}
fieldsToChange={fieldsToChange}
onDuplicateSuccess={onDuplicateSuccess}
navigateToOnSuccess={new Route('/done')}
/>
);
const button: HTMLElement = screen.getByRole('button', {
name: 'Duplicate Foo',
});
void act(() => {
fireEvent.click(button);
});
const dialog: HTMLElement = screen.getByRole('dialog');
const confirmationButton: HTMLElement = within(dialog).getByRole(
'button',
{
name: 'Duplicate Foo',
}
);
void act(() => {
fireEvent.click(confirmationButton);
});
await screen.findByText('Duplicate Error');
const errorDialog: HTMLElement = screen.getByRole('dialog');
const closeButton: HTMLElement = within(errorDialog).getByRole(
'button',
{
name: 'Close',
}
);
void act(() => {
fireEvent.click(closeButton);
});
expect(screen.queryByRole('dialog')).toBeFalsy();
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,10 +5,13 @@ import Route from 'Common/Types/API/Route';
import RouteMap, { RouteUtil } from '../../Utils/RouteMap';
import PageMap from '../../Utils/PageMap';
import ObjectID from 'Common/Types/ObjectID';
import Icon from 'CommonUI/src/Components/Icon/Icon';
import IconProp from 'Common/Types/Icon/IconProp';
export interface ComponentProps {
monitor: Monitor;
onNavigateComplete?: (() => void) | undefined;
showIcon?: boolean;
}
const MonitorElement: FunctionComponent<ComponentProps> = (
@@ -26,7 +29,17 @@ const MonitorElement: FunctionComponent<ComponentProps> = (
}
)}
>
<span>{props.monitor.name}</span>
<span className="flex">
{props.showIcon ? (
<Icon
icon={IconProp.AltGlobe}
className="w-5 h-5 mr-1"
/>
) : (
<></>
)}{' '}
{props.monitor.name}
</span>
</Link>
);
}

View File

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

View File

@@ -15,10 +15,10 @@ const MonitorsElement: FunctionComponent<ComponentProps> = (
}
return (
<div>
<div className="flex">
{props.monitors.map((monitor: Monitor, i: number) => {
return (
<span key={i}>
<span key={i} className="flex">
<MonitorElement
monitor={monitor}
onNavigateComplete={props.onNavigateComplete}

View File

@@ -0,0 +1,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;

View File

@@ -0,0 +1,50 @@
import React, { FunctionComponent, ReactElement } from 'react';
import MonitorGroup from 'Model/Models/MonitorGroup';
import Link from 'CommonUI/src/Components/Link/Link';
import Route from 'Common/Types/API/Route';
import RouteMap, { RouteUtil } from '../../Utils/RouteMap';
import PageMap from '../../Utils/PageMap';
import ObjectID from 'Common/Types/ObjectID';
import Icon from 'CommonUI/src/Components/Icon/Icon';
import IconProp from 'Common/Types/Icon/IconProp';
export interface ComponentProps {
monitorGroup: MonitorGroup;
onNavigateComplete?: (() => void) | undefined;
showIcon?: boolean;
}
const MonitorGroupElement: FunctionComponent<ComponentProps> = (
props: ComponentProps
): ReactElement => {
if (props.monitorGroup._id) {
return (
<Link
onNavigateComplete={props.onNavigateComplete}
className="hover:underline"
to={RouteUtil.populateRouteParams(
RouteMap[PageMap.MONITOR_GROUP_VIEW] as Route,
{
modelId: new ObjectID(props.monitorGroup._id as string),
}
)}
>
<span className="flex">
{props.showIcon ? (
<Icon
icon={IconProp.Squares}
className="w-5 h-5 mr-1"
/>
) : (
<></>
)}{' '}
{props.monitorGroup.name}
</span>
</Link>
);
}
return <span>{props.monitorGroup.name}</span>;
};
export default MonitorGroupElement;

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -30,6 +30,10 @@ import JSONFunctions from 'Common/Types/JSONFunctions';
import Navigation from 'CommonUI/src/Utils/Navigation';
import API from 'CommonUI/src/Utils/API/API';
import StatusPage from 'Model/Models/StatusPage';
import { ModelField } from 'CommonUI/src/Components/Forms/ModelForm';
import MonitorGroup from 'Model/Models/MonitorGroup';
import Link from 'CommonUI/src/Components/Link/Link';
import MonitorGroupElement from '../../../Components/MonitorGroup/MonitorGroupElement';
const StatusPageDelete: FunctionComponent<PageComponentProps> = (
props: PageComponentProps
@@ -40,6 +44,8 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>('');
const [addMonitorGroup, setAddMonitorGroup] = useState<boolean>(false);
const fetchGroups: Function = async () => {
setError('');
setIsLoading(true);
@@ -76,6 +82,143 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
fetchGroups();
}, []);
const getFooterForMonitor: Function = (): ReactElement => {
if (props.currentProject?.isFeatureFlagMonitorGroupsEnabled) {
if (!addMonitorGroup) {
return (
<Link
onClick={() => {
setAddMonitorGroup(true);
}}
className="mt-1 text-sm text-gray-500 underline"
>
<div>
<p> Add a Monitor Group instead. </p>
</div>
</Link>
);
}
return (
<Link
onClick={() => {
setAddMonitorGroup(false);
}}
className="mt-1 text-sm text-gray-500 underline"
>
<div>
<p> Add a Monitor instead. </p>
</div>
</Link>
);
}
return <></>;
};
let formFields: Array<ModelField<StatusPageResource>> = [
{
field: {
monitor: true,
},
title: 'Monitor',
description:
'Select monitor that will be shown on the status page.',
fieldType: FormFieldSchemaType.Dropdown,
dropdownModal: {
type: Monitor,
labelField: 'name',
valueField: '_id',
},
required: true,
placeholder: 'Select Monitor',
stepId: 'monitor-details',
footerElement: getFooterForMonitor(),
},
];
if (addMonitorGroup) {
formFields = [
{
field: {
monitorGroup: true,
},
title: 'Monitor Group',
description:
'Select monitor group that will be shown on the status page.',
fieldType: FormFieldSchemaType.Dropdown,
dropdownModal: {
type: MonitorGroup,
labelField: 'name',
valueField: '_id',
},
required: true,
placeholder: 'Select Monitor Group',
stepId: 'monitor-details',
footerElement: getFooterForMonitor(),
},
];
}
formFields = formFields.concat([
{
field: {
displayName: true,
},
title: 'Display Name',
description:
'This will be the name that will be shown on the status page',
fieldType: FormFieldSchemaType.Text,
required: true,
placeholder: 'Display Name',
stepId: 'monitor-details',
},
{
field: {
displayDescription: true,
},
title: 'Description',
fieldType: FormFieldSchemaType.Markdown,
required: false,
placeholder: '',
stepId: 'monitor-details',
},
{
field: {
displayTooltip: true,
},
title: 'Tooltip ',
fieldType: FormFieldSchemaType.LongText,
required: false,
description:
'This will show up as tooltip beside the resource on your status page.',
placeholder: 'Tooltip',
stepId: 'advanced',
},
{
field: {
showCurrentStatus: true,
},
title: 'Show Current Resource Status',
fieldType: FormFieldSchemaType.Toggle,
required: false,
defaultValue: true,
description:
'Current Resource Status will be shown beside this resource on your status page.',
stepId: 'advanced',
},
{
field: {
showStatusHistoryChart: true,
},
title: 'Show Status History Chart',
fieldType: FormFieldSchemaType.Toggle,
required: false,
description: 'Show resource status history for the past 90 days. ',
defaultValue: true,
stepId: 'advanced',
},
]);
const getModelTable: Function = (
statusPageGroupId: ObjectID | null,
statusPageGroupName: string | null
@@ -137,86 +280,17 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
id: 'advanced',
},
]}
formFields={[
{
field: {
monitor: true,
},
title: 'Monitor',
description:
'Select monitor that will be shown on the status page.',
fieldType: FormFieldSchemaType.Dropdown,
dropdownModal: {
type: Monitor,
labelField: 'name',
valueField: '_id',
},
required: true,
placeholder: 'Select Monitor',
stepId: 'monitor-details',
},
{
field: {
displayName: true,
},
title: 'Display Name',
description:
'This will be the name that will be shown on the status page',
fieldType: FormFieldSchemaType.Text,
required: true,
placeholder: 'Display Name',
stepId: 'monitor-details',
},
{
field: {
displayDescription: true,
},
title: 'Description',
fieldType: FormFieldSchemaType.Markdown,
required: false,
placeholder: '',
stepId: 'monitor-details',
},
{
field: {
displayTooltip: true,
},
title: 'Tooltip ',
fieldType: FormFieldSchemaType.LongText,
required: false,
description:
'This will show up as tooltip beside the resource on your status page.',
placeholder: 'Tooltip',
stepId: 'advanced',
},
{
field: {
showCurrentStatus: true,
},
title: 'Show Current Resource Status',
fieldType: FormFieldSchemaType.Toggle,
required: false,
defaultValue: true,
description:
'Current Resource Status will be shown beside this resource on your status page.',
stepId: 'advanced',
},
{
field: {
showStatusHistoryChart: true,
},
title: 'Show Status History Chart',
fieldType: FormFieldSchemaType.Toggle,
required: false,
description:
'Show resource status history for the past 90 days. ',
defaultValue: true,
stepId: 'advanced',
},
]}
formFields={formFields}
showRefreshButton={true}
showFilterButton={true}
viewPageRoute={Navigation.getCurrentRoute()}
selectMoreFields={{
monitorGroup: {
name: true,
_id: true,
projectId: true,
},
}}
columns={[
{
field: {
@@ -226,7 +300,10 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
projectId: true,
},
},
title: 'Monitor',
title: props.currentProject
?.isFeatureFlagMonitorGroupsEnabled
? 'Resource'
: 'Monitor',
type: FieldType.Entity,
isFilterable: true,
filterEntityType: Monitor,
@@ -239,17 +316,47 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
value: '_id',
},
getElement: (item: JSONObject): ReactElement => {
return (
<MonitorElement
monitor={
JSONFunctions.fromJSON(
(item['monitor'] as JSONObject) ||
[],
Monitor
) as Monitor
}
/>
);
if (item['monitor']) {
return (
<MonitorElement
monitor={
JSONFunctions.fromJSON(
(item[
'monitor'
] as JSONObject) || [],
Monitor
) as Monitor
}
showIcon={
props.currentProject
?.isFeatureFlagMonitorGroupsEnabled ||
false
}
/>
);
}
if (item['monitorGroup']) {
return (
<MonitorGroupElement
monitorGroup={
JSONFunctions.fromJSON(
(item[
'monitorGroup'
] as JSONObject) || [],
MonitorGroup
) as MonitorGroup
}
showIcon={
props.currentProject
?.isFeatureFlagMonitorGroupsEnabled ||
false
}
/>
);
}
return <></>;
},
},
{

View File

@@ -7,6 +7,8 @@ import SideMenuSection from 'CommonUI/src/Components/SideMenu/SideMenuSection';
import RouteMap, { RouteUtil } from '../../../Utils/RouteMap';
import PageMap from '../../../Utils/PageMap';
import ObjectID from 'Common/Types/ObjectID';
import ProjectUtil from 'CommonUI/src/Utils/Project';
import Project from 'Model/Models/Project';
export interface ComponentProps {
modelId: ObjectID;
@@ -15,6 +17,8 @@ export interface ComponentProps {
const DashboardSideMenu: FunctionComponent<ComponentProps> = (
props: ComponentProps
): ReactElement => {
const project: Project | null = ProjectUtil.getCurrentProject();
return (
<SideMenu>
<SideMenuSection title="Basic">
@@ -56,7 +60,9 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
<SideMenuSection title="Resources">
<SideMenuItem
link={{
title: 'Monitors',
title: project?.isFeatureFlagMonitorGroupsEnabled
? 'Resources'
: 'Monitors',
to: RouteUtil.populateRouteParams(
RouteMap[
PageMap.STATUS_PAGE_VIEW_RESOURCES

Some files were not shown because too many files have changed in this diff Show More