mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
add incident page
This commit is contained in:
@@ -327,7 +327,7 @@ export default class BaseModel extends BaseEntity {
|
||||
return Boolean(getTableColumn(this, columnName).isDefaultValueColumn);
|
||||
}
|
||||
|
||||
public getColumnValue(columnName: string): JSONValue | null {
|
||||
public getColumnValue(columnName: string): JSONValue | BaseModel | Array<BaseModel> | null {
|
||||
if (getTableColumn(this, columnName) && (this as any)[columnName]) {
|
||||
return (this as any)[columnName] as JSONValue;
|
||||
}
|
||||
@@ -335,7 +335,7 @@ export default class BaseModel extends BaseEntity {
|
||||
return null;
|
||||
}
|
||||
|
||||
public setColumnValue(columnName: string, value: JSONValue): void {
|
||||
public setColumnValue(columnName: string, value: JSONValue | BaseModel | Array<BaseModel>): void {
|
||||
if (getTableColumn(this, columnName)) {
|
||||
return ((this as any)[columnName] = value as any);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,14 @@ export default class PositiveNumber {
|
||||
return this.positiveNumber.toString();
|
||||
}
|
||||
|
||||
public isZero(): boolean {
|
||||
return this.positiveNumber === 0;
|
||||
}
|
||||
|
||||
public isOne(): boolean {
|
||||
return this.positiveNumber === 1;
|
||||
}
|
||||
|
||||
public toNumber(): number {
|
||||
return this.positiveNumber;
|
||||
}
|
||||
|
||||
@@ -79,11 +79,11 @@ export interface OnUpdate<TBaseModel extends BaseModel> {
|
||||
|
||||
class DatabaseService<TBaseModel extends BaseModel> {
|
||||
private postgresDatabase!: PostgresDatabase;
|
||||
private entityType!: { new (): TBaseModel };
|
||||
private entityType!: { new(): TBaseModel };
|
||||
private model!: TBaseModel;
|
||||
|
||||
public constructor(
|
||||
modelType: { new (): TBaseModel },
|
||||
modelType: { new(): TBaseModel },
|
||||
postgresDatabase?: PostgresDatabase
|
||||
) {
|
||||
this.entityType = modelType;
|
||||
@@ -129,6 +129,18 @@ class DatabaseService<TBaseModel extends BaseModel> {
|
||||
protected checkRequiredFields(data: TBaseModel): void {
|
||||
// Check required fields.
|
||||
|
||||
|
||||
const relatationalColumns: Dictionary<string> = {};
|
||||
|
||||
const tableColumns = data.getTableColumns().columns;
|
||||
|
||||
for (const column of tableColumns) {
|
||||
const metadata = data.getTableColumnMetadata(column);
|
||||
if (metadata.manyToOneRelationColumn) {
|
||||
relatationalColumns[metadata.manyToOneRelationColumn] = column;
|
||||
}
|
||||
}
|
||||
|
||||
for (const requiredField of data.getRequiredColumns().columns) {
|
||||
if (typeof (data as any)[requiredField] === Typeof.Boolean) {
|
||||
if (
|
||||
@@ -142,6 +154,11 @@ class DatabaseService<TBaseModel extends BaseModel> {
|
||||
!(data as any)[requiredField] &&
|
||||
!data.isDefaultValueColumn(requiredField)
|
||||
) {
|
||||
|
||||
if (relatationalColumns[requiredField] && data.getColumnValue(relatationalColumns[requiredField] as string)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new BadDataException(`${requiredField} is required`);
|
||||
}
|
||||
}
|
||||
@@ -336,7 +353,7 @@ class DatabaseService<TBaseModel extends BaseModel> {
|
||||
createBy.data.getSaveSlugToColumn() as string
|
||||
] = Slug.getSlug(
|
||||
(createBy.data as any)[
|
||||
createBy.data.getSlugifyColumn() as string
|
||||
createBy.data.getSlugifyColumn() as string
|
||||
] as string
|
||||
);
|
||||
}
|
||||
@@ -354,31 +371,33 @@ class DatabaseService<TBaseModel extends BaseModel> {
|
||||
const tableColumnMetadata: TableColumnMetadata =
|
||||
this.model.getTableColumnMetadata(columnName);
|
||||
|
||||
|
||||
const columnValue = (data as any)[columnName];
|
||||
|
||||
if (
|
||||
data &&
|
||||
columnName &&
|
||||
tableColumnMetadata.modelType &&
|
||||
(data as any)[columnName] &&
|
||||
columnValue &&
|
||||
tableColumnMetadata.type === TableColumnType.Entity &&
|
||||
(typeof (data as any)[columnName] === 'string' ||
|
||||
(data as any)[columnName] instanceof ObjectID)
|
||||
(typeof columnValue === 'string' ||
|
||||
columnValue instanceof ObjectID)
|
||||
) {
|
||||
(data as any)[columnName] =
|
||||
new tableColumnMetadata.modelType();
|
||||
(data as any)[columnName]._id = (data as any)[
|
||||
columnName
|
||||
].toString();
|
||||
const relatedType = new tableColumnMetadata.modelType();
|
||||
relatedType._id = columnValue.toString();
|
||||
(data as any)[columnName] = relatedType;
|
||||
}
|
||||
|
||||
if (
|
||||
data &&
|
||||
Array.isArray((data as any)[columnName]) &&
|
||||
(data as any)[columnName].length > 0 &&
|
||||
Array.isArray(columnValue) &&
|
||||
columnValue.length > 0 &&
|
||||
tableColumnMetadata.modelType &&
|
||||
(data as any)[columnName] &&
|
||||
columnValue &&
|
||||
tableColumnMetadata.type === TableColumnType.EntityArray
|
||||
) {
|
||||
const itemsArray: Array<BaseModel> = [];
|
||||
for (const item of (data as any)[columnName]) {
|
||||
for (const item of columnValue) {
|
||||
if (
|
||||
typeof item === 'string' ||
|
||||
item instanceof ObjectID
|
||||
@@ -387,7 +406,7 @@ class DatabaseService<TBaseModel extends BaseModel> {
|
||||
new tableColumnMetadata.modelType();
|
||||
basemodelItem._id = item.toString();
|
||||
itemsArray.push(basemodelItem);
|
||||
} else {
|
||||
} else if(item instanceof BaseModel) {
|
||||
itemsArray.push(item);
|
||||
}
|
||||
}
|
||||
@@ -400,6 +419,9 @@ class DatabaseService<TBaseModel extends BaseModel> {
|
||||
}
|
||||
|
||||
public async create(createBy: CreateBy<TBaseModel>): Promise<TBaseModel> {
|
||||
|
||||
debugger;
|
||||
|
||||
const onCreate: OnCreate<TBaseModel> = await this._onBeforeCreate(
|
||||
createBy
|
||||
);
|
||||
@@ -456,6 +478,7 @@ class DatabaseService<TBaseModel extends BaseModel> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async checkUniqueColumnBy(
|
||||
createBy: CreateBy<TBaseModel>
|
||||
): Promise<CreateBy<TBaseModel>> {
|
||||
@@ -581,8 +604,7 @@ class DatabaseService<TBaseModel extends BaseModel> {
|
||||
)
|
||||
) {
|
||||
throw new NotAuthorizedException(
|
||||
`You do not have permissions to ${type} ${
|
||||
this.model.singularName
|
||||
`You do not have permissions to ${type} ${this.model.singularName
|
||||
}. You need one of these permissions: ${PermissionHelper.getPermissionTitles(
|
||||
modelPermissions
|
||||
).join(',')}`
|
||||
@@ -1186,10 +1208,10 @@ class DatabaseService<TBaseModel extends BaseModel> {
|
||||
if (!tableColumnMetadata.modelType) {
|
||||
throw new BadDataException(
|
||||
'Populate not supported on ' +
|
||||
key +
|
||||
' of ' +
|
||||
this.model.singularName +
|
||||
' because this column modelType is not found.'
|
||||
key +
|
||||
' of ' +
|
||||
this.model.singularName +
|
||||
' because this column modelType is not found.'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1241,10 +1263,10 @@ class DatabaseService<TBaseModel extends BaseModel> {
|
||||
if (!tableColumnMetadata.modelType) {
|
||||
throw new BadDataException(
|
||||
'Populate not supported on ' +
|
||||
key +
|
||||
' of ' +
|
||||
this.model.singularName +
|
||||
' because this column modelType is not found.'
|
||||
key +
|
||||
' of ' +
|
||||
this.model.singularName +
|
||||
' because this column modelType is not found.'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1261,7 +1283,7 @@ class DatabaseService<TBaseModel extends BaseModel> {
|
||||
// check for permissions.
|
||||
if (
|
||||
typeof (onBeforeFind.populate as any)[key][
|
||||
innerKey
|
||||
innerKey
|
||||
] === Typeof.Object
|
||||
) {
|
||||
throw new BadDataException(
|
||||
@@ -1280,10 +1302,9 @@ class DatabaseService<TBaseModel extends BaseModel> {
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new NotAuthorizedException(
|
||||
`You do not have permissions to read ${
|
||||
onBeforeFind.limit === 1
|
||||
? this.model.singularName
|
||||
: this.model.pluralName
|
||||
`You do not have permissions to read ${onBeforeFind.limit === 1
|
||||
? this.model.singularName
|
||||
: this.model.pluralName
|
||||
}. You need one of these permissions: ${PermissionHelper.getPermissionTitles(
|
||||
this.model.getColumnAccessControlFor(
|
||||
innerKey
|
||||
@@ -1301,10 +1322,10 @@ class DatabaseService<TBaseModel extends BaseModel> {
|
||||
} else {
|
||||
throw new BadDataException(
|
||||
'Populate not supported on ' +
|
||||
key +
|
||||
' of ' +
|
||||
this.model.singularName +
|
||||
' because this column is not of type Entity or EntityArray'
|
||||
key +
|
||||
' of ' +
|
||||
this.model.singularName +
|
||||
' because this column is not of type Entity or EntityArray'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import PostgresDatabase from '../Infrastructure/PostgresDatabase';
|
||||
import Model from 'Model/Models/IncidentStateTimeline';
|
||||
import DatabaseService, { OnCreate } from './DatabaseService';
|
||||
import DatabaseService, { OnCreate, OnDelete } from './DatabaseService';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import IncidentService from './IncidentService';
|
||||
import DeleteBy from '../Types/Database/DeleteBy';
|
||||
import IncidentStateTimeline from 'Model/Models/IncidentStateTimeline';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import PositiveNumber from 'Common/Types/PositiveNumber';
|
||||
import SortOrder from 'Common/Types/Database/SortOrder';
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor(postgresDatabase?: PostgresDatabase) {
|
||||
@@ -33,6 +38,85 @@ export class Service extends DatabaseService<Model> {
|
||||
|
||||
return createdItem;
|
||||
}
|
||||
|
||||
protected override async onBeforeDelete(deleteBy: DeleteBy<Model>): Promise<OnDelete<Model>> {
|
||||
|
||||
if (deleteBy.query._id) {
|
||||
|
||||
const incidentStateTimeline: IncidentStateTimeline | null = await this.findOneById({
|
||||
id: new ObjectID(deleteBy.query._id as string),
|
||||
select: {
|
||||
incidentId: true
|
||||
},
|
||||
props: {
|
||||
isRoot: true
|
||||
}
|
||||
})
|
||||
|
||||
const incidentId: ObjectID | undefined = incidentStateTimeline?.incidentId;
|
||||
|
||||
if (incidentId) {
|
||||
const incidentStateTimeline: PositiveNumber = await this.countBy({
|
||||
query: {
|
||||
incidentId: incidentId
|
||||
},
|
||||
props: {
|
||||
isRoot: true
|
||||
}
|
||||
})
|
||||
|
||||
if (incidentStateTimeline.isOne()) {
|
||||
throw new BadDataException("Cannot delete the only state timeline. Incident should have atleast one state in its timeline.");
|
||||
}
|
||||
}
|
||||
|
||||
return { deleteBy, carryForward: incidentId}
|
||||
|
||||
}
|
||||
|
||||
return { deleteBy, carryForward: null }
|
||||
}
|
||||
|
||||
protected override async onDeleteSuccess(onDelete: OnDelete<Model>, _itemIdsBeforeDelete: ObjectID[]): Promise<OnDelete<Model>> {
|
||||
if (onDelete.carryForward) {
|
||||
// this is incidentId.
|
||||
const incidentId = onDelete.carryForward as ObjectID;
|
||||
|
||||
// get last status of this monitor.
|
||||
const incidentStateTimeline: IncidentStateTimeline | null = await this.findOneBy({
|
||||
query: {
|
||||
incidentId: incidentId
|
||||
},
|
||||
sort: {
|
||||
createdAt: SortOrder.Descending
|
||||
},
|
||||
props: {
|
||||
isRoot: true
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
incidentStateId: true
|
||||
}
|
||||
});
|
||||
|
||||
if (incidentStateTimeline && incidentStateTimeline.incidentStateId) {
|
||||
await IncidentService.updateBy({
|
||||
query: {
|
||||
_id: incidentId.toString(),
|
||||
},
|
||||
data: {
|
||||
currentIncidentStateId: incidentStateTimeline.incidentStateId
|
||||
},
|
||||
props: {
|
||||
isRoot: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return onDelete;
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
|
||||
@@ -7,16 +7,67 @@ import MonitorService from './MonitorService';
|
||||
import DatabaseCommonInteractionProps from 'Common/Types/Database/DatabaseCommonInteractionProps';
|
||||
import IncidentStateTimeline from 'Model/Models/IncidentStateTimeline';
|
||||
import IncidentStateTimelineService from './IncidentStateTimelineService';
|
||||
import CreateBy from '../Types/Database/CreateBy';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import IncidentState from 'Model/Models/IncidentState';
|
||||
import IncidentStateService from './IncidentStateService';
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor(postgresDatabase?: PostgresDatabase) {
|
||||
super(Model, postgresDatabase);
|
||||
}
|
||||
|
||||
|
||||
protected override async onBeforeCreate(
|
||||
createBy: CreateBy<Model>
|
||||
): Promise<OnCreate<Model>> {
|
||||
if (!createBy.props.tenantId) {
|
||||
throw new BadDataException('ProjectId required to create monitor.');
|
||||
}
|
||||
|
||||
const incidentState: IncidentState | null =
|
||||
await IncidentStateService.findOneBy({
|
||||
query: {
|
||||
projectId: createBy.props.tenantId,
|
||||
isCreatedState: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!incidentState || !incidentState.id) {
|
||||
throw new BadDataException(
|
||||
'Created state not found for this project. Please add an operational status'
|
||||
);
|
||||
}
|
||||
|
||||
createBy.data.currentIncidentStateId = incidentState.id;
|
||||
|
||||
return { createBy, carryForward: null };
|
||||
}
|
||||
|
||||
protected override async onCreateSuccess(
|
||||
onCreate: OnCreate<Model>,
|
||||
createdItem: Model
|
||||
): Promise<Model> {
|
||||
|
||||
if (!createdItem.projectId) {
|
||||
throw new BadDataException("projectId is required");
|
||||
}
|
||||
|
||||
if (!createdItem.id) {
|
||||
throw new BadDataException("id is required");
|
||||
}
|
||||
|
||||
if (!createdItem.currentIncidentStateId) {
|
||||
throw new BadDataException("currentIncidentStateId is required");
|
||||
}
|
||||
|
||||
|
||||
if (createdItem.changeMonitorStatusToId && createdItem.projectId) {
|
||||
// change status of all the monitors.
|
||||
await MonitorService.changeMonitorStatus(
|
||||
@@ -29,6 +80,8 @@ export class Service extends DatabaseService<Model> {
|
||||
);
|
||||
}
|
||||
|
||||
await this.changeIncidentState(createdItem.projectId, createdItem.id, createdItem.currentIncidentStateId, onCreate.createBy.props);
|
||||
|
||||
return createdItem;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,8 +42,6 @@ export class Service extends DatabaseService<Model> {
|
||||
);
|
||||
}
|
||||
|
||||
debugger;
|
||||
|
||||
createBy.data.currentMonitorStatusId = monitorStatus.id;
|
||||
|
||||
return { createBy, carryForward: null };
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import PostgresDatabase from '../Infrastructure/PostgresDatabase';
|
||||
import Model from 'Model/Models/MonitorStatusTimeline';
|
||||
import DatabaseService, { OnCreate } from './DatabaseService';
|
||||
import DatabaseService, { OnCreate, OnDelete } from './DatabaseService';
|
||||
import MonitorService from './MonitorService';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import DeleteBy from '../Types/Database/DeleteBy';
|
||||
import MonitorStatusTimeline from 'Model/Models/MonitorStatusTimeline';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import SortOrder from 'Common/Types/Database/SortOrder';
|
||||
import PositiveNumber from 'Common/Types/PositiveNumber';
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor(postgresDatabase?: PostgresDatabase) {
|
||||
@@ -33,6 +38,85 @@ export class Service extends DatabaseService<Model> {
|
||||
|
||||
return createdItem;
|
||||
}
|
||||
|
||||
protected override async onBeforeDelete(deleteBy: DeleteBy<Model>): Promise<OnDelete<Model>> {
|
||||
|
||||
if (deleteBy.query._id) {
|
||||
|
||||
const monitorStatusTimeline: MonitorStatusTimeline | null = await this.findOneById({
|
||||
id: new ObjectID(deleteBy.query._id as string),
|
||||
select: {
|
||||
monitorId: true
|
||||
},
|
||||
props: {
|
||||
isRoot: true
|
||||
}
|
||||
})
|
||||
|
||||
const monitorId: ObjectID | undefined = monitorStatusTimeline?.monitorId;
|
||||
|
||||
if (monitorId) {
|
||||
const monitorStatusTimeline: PositiveNumber = await this.countBy({
|
||||
query: {
|
||||
monitorId: monitorId
|
||||
},
|
||||
props: {
|
||||
isRoot: true
|
||||
}
|
||||
})
|
||||
|
||||
if (monitorStatusTimeline.isOne()) {
|
||||
throw new BadDataException("Cannot delete the only status timeline. Monitor should have atleast one status timeline.");
|
||||
}
|
||||
}
|
||||
|
||||
return { deleteBy, carryForward: monitorId}
|
||||
|
||||
}
|
||||
|
||||
return { deleteBy, carryForward: null }
|
||||
}
|
||||
|
||||
protected override async onDeleteSuccess(onDelete: OnDelete<Model>, _itemIdsBeforeDelete: ObjectID[]): Promise<OnDelete<Model>> {
|
||||
if (onDelete.carryForward) {
|
||||
// this is monitorId.
|
||||
const monitorId = onDelete.carryForward as ObjectID;
|
||||
|
||||
// get last status of this monitor.
|
||||
const monitorStatusTimeline: MonitorStatusTimeline | null = await this.findOneBy({
|
||||
query: {
|
||||
monitorId: monitorId
|
||||
},
|
||||
sort: {
|
||||
createdAt: SortOrder.Descending
|
||||
},
|
||||
props: {
|
||||
isRoot: true
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
monitorStatusId: true
|
||||
}
|
||||
});
|
||||
|
||||
if (monitorStatusTimeline && monitorStatusTimeline.monitorStatusId) {
|
||||
await MonitorService.updateBy({
|
||||
query: {
|
||||
_id: monitorId.toString(),
|
||||
},
|
||||
data: {
|
||||
currentMonitorStatusId: monitorStatusTimeline.monitorStatusId
|
||||
},
|
||||
props: {
|
||||
isRoot: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return onDelete;
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
|
||||
@@ -140,7 +140,7 @@ export default class ModelAPI {
|
||||
httpMethod,
|
||||
apiUrl,
|
||||
{
|
||||
data: model.toJSON(),
|
||||
data: JSONFunctions.serialize(model.toJSON()),
|
||||
miscDataProps: miscDataProps || {},
|
||||
},
|
||||
this.getCommonHeaders(requestOptions)
|
||||
|
||||
@@ -13,7 +13,14 @@ import Init from './Pages/Init/Init';
|
||||
import Home from './Pages/Home/Home';
|
||||
import useAsyncEffect from 'use-async-effect';
|
||||
import StatusPages from './Pages/StatusPages/StatusPages';
|
||||
|
||||
|
||||
import Incidents from './Pages/Incidents/Incidents';
|
||||
import IncidentView from './Pages/Incidents/View/Index';
|
||||
import IncidentViewDelete from './Pages/Incidents/View/Delete';
|
||||
import IncidentViewStateTimeline from './Pages/Incidents/View/StateTimeline';
|
||||
|
||||
|
||||
import Logs from './Pages/Logs/Logs';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import RouteMap from './Utils/RouteMap';
|
||||
@@ -28,8 +35,8 @@ import SettingLabels from './Pages/Settings/Labels';
|
||||
import SettingCustomSMTP from './Pages/Settings/CustomSMTP';
|
||||
import SettingsTeams from './Pages/Settings/Teams';
|
||||
import SettingsTeamView from './Pages/Settings/TeamView';
|
||||
import SettingsMonitors from './Pages/Settings/Monitors';
|
||||
import SettingsIncidents from './Pages/Settings/Incidents';
|
||||
import SettingsMonitors from './Pages/Settings/MonitorStatus';
|
||||
import SettingsIncidents from './Pages/Settings/IncidentState';
|
||||
|
||||
// On Call Duty
|
||||
import OnCallDutyPage from './Pages/OnCallDuty/OnCallDuties';
|
||||
@@ -194,6 +201,9 @@ const App: FunctionComponent = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
{/* Incidents */}
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.INCIDENTS]?.toString()}
|
||||
element={
|
||||
@@ -203,6 +213,40 @@ const App: FunctionComponent = () => {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.INCIDENT_VIEW]?.toString()}
|
||||
element={
|
||||
<IncidentView
|
||||
pageRoute={RouteMap[PageMap.INCIDENT_VIEW] as Route}
|
||||
currentProject={selectedProject}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.INCIDENT_VIEW_DELETE]?.toString()}
|
||||
element={
|
||||
<IncidentViewDelete
|
||||
pageRoute={RouteMap[PageMap.INCIDENT_VIEW_DELETE] as Route}
|
||||
currentProject={selectedProject}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.INCIDENT_VIEW_STATE_TIMELINE]?.toString()}
|
||||
element={
|
||||
<IncidentViewStateTimeline
|
||||
pageRoute={RouteMap[PageMap.INCIDENT_VIEW_STATE_TIMELINE] as Route}
|
||||
currentProject={selectedProject}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Logs */}
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.LOGS]?.toString()}
|
||||
element={
|
||||
|
||||
@@ -57,7 +57,6 @@ const IncidentsPage: FunctionComponent<PageComponentProps> = (
|
||||
required: true,
|
||||
placeholder: 'Incident Title',
|
||||
validation: {
|
||||
noSpaces: true,
|
||||
minLength: 2,
|
||||
},
|
||||
},
|
||||
@@ -122,14 +121,14 @@ const IncidentsPage: FunctionComponent<PageComponentProps> = (
|
||||
color: true,
|
||||
},
|
||||
},
|
||||
title: 'Incident State',
|
||||
title: 'Current State',
|
||||
type: FieldType.Text,
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
if (item['currentIncidentState']) {
|
||||
return (
|
||||
<Pill
|
||||
color={item['color'] as Color}
|
||||
text={item['name'] as string}
|
||||
color={(item['currentIncidentState'] as JSONObject)['color'] as Color}
|
||||
text={(item['currentIncidentState'] as JSONObject)['name'] as string}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -160,6 +159,13 @@ const IncidentsPage: FunctionComponent<PageComponentProps> = (
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
createdAt: true
|
||||
},
|
||||
title: 'Created At',
|
||||
type: FieldType.Date,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Page>
|
||||
|
||||
60
Dashboard/src/Pages/Incidents/View/Delete.tsx
Normal file
60
Dashboard/src/Pages/Incidents/View/Delete.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
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 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 Incident from 'Model/Models/Incident';
|
||||
|
||||
const IncidentDelete: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps
|
||||
): ReactElement => {
|
||||
const modelId: ObjectID = new ObjectID(
|
||||
Navigation.getLastParam(1)?.toString().substring(1) || ''
|
||||
);
|
||||
|
||||
return (
|
||||
<Page
|
||||
title={'Incidents'}
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
title: 'Project',
|
||||
to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route, modelId)
|
||||
},
|
||||
{
|
||||
title: 'Incidents',
|
||||
to: RouteUtil.populateRouteParams(RouteMap[PageMap.INCIDENTS] as Route, modelId),
|
||||
},
|
||||
{
|
||||
title: 'View Incident',
|
||||
to: RouteUtil.populateRouteParams(RouteMap[PageMap.INCIDENT_VIEW] as Route, modelId),
|
||||
},
|
||||
{
|
||||
title: 'Delete Incident',
|
||||
to: RouteUtil.populateRouteParams(RouteMap[PageMap.INCIDENT_VIEW_DELETE] as Route, modelId),
|
||||
},
|
||||
]}
|
||||
sideMenu={<SideMenu modelId={modelId} />}
|
||||
>
|
||||
|
||||
|
||||
<ModelDelete
|
||||
modelType={Incident}
|
||||
modelId={
|
||||
modelId
|
||||
}
|
||||
onDeleteSuccess={() => {
|
||||
Navigation.navigate(
|
||||
RouteMap[PageMap.INCIDENTS] as Route
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default IncidentDelete;
|
||||
172
Dashboard/src/Pages/Incidents/View/Index.tsx
Normal file
172
Dashboard/src/Pages/Incidents/View/Index.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
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 SideMenu from './SideMenu';
|
||||
import FieldType from 'CommonUI/src/Components/Types/FieldType';
|
||||
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType';
|
||||
import { IconProp } from 'CommonUI/src/Components/Icon/Icon';
|
||||
import CardModelDetail from 'CommonUI/src/Components/ModelDetail/CardModelDetail';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import { JSONArray, JSONObject } from 'Common/Types/JSON';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import Incident from 'Model/Models/Incident';
|
||||
import Color from 'Common/Types/Color';
|
||||
import Pill from 'CommonUI/src/Components/Pill/Pill';
|
||||
import MonitorsElement from '../../../Components/Monitor/Monitors';
|
||||
import Monitor from 'Model/Models/Monitor';
|
||||
|
||||
const IncidentView: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps
|
||||
): ReactElement => {
|
||||
const modelId: ObjectID = new ObjectID(
|
||||
Navigation.getLastParam()?.toString().substring(1) || ''
|
||||
);
|
||||
|
||||
return (
|
||||
<Page
|
||||
title={'Incidents'}
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
title: 'Project',
|
||||
to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route, modelId)
|
||||
},
|
||||
{
|
||||
title: 'Incidents',
|
||||
to: RouteUtil.populateRouteParams(RouteMap[PageMap.INCIDENTS] as Route, modelId),
|
||||
},
|
||||
{
|
||||
title: 'View Incident',
|
||||
to: RouteUtil.populateRouteParams(RouteMap[PageMap.INCIDENT_VIEW] as Route, modelId),
|
||||
},
|
||||
]}
|
||||
sideMenu={<SideMenu modelId={modelId}/>}
|
||||
>
|
||||
{/* Incident View */}
|
||||
<CardModelDetail
|
||||
cardProps={{
|
||||
title: 'Incident Details',
|
||||
description: "Here's more details for this monitor.",
|
||||
icon: IconProp.Activity,
|
||||
}}
|
||||
|
||||
isEditable={true}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
title: true,
|
||||
},
|
||||
title: 'Incident Title',
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: 'Incident Title',
|
||||
validation: {
|
||||
minLength: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
description: true,
|
||||
},
|
||||
title: 'Description',
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
required: true,
|
||||
placeholder: 'Description',
|
||||
},
|
||||
]}
|
||||
modelDetailProps={{
|
||||
showDetailsInNumberOfColumns: 2,
|
||||
modelType: Incident,
|
||||
id: 'model-detail-incidents',
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
_id: true,
|
||||
},
|
||||
title: 'Incident ID',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
title: true,
|
||||
},
|
||||
title: 'Incident Title',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
description: true,
|
||||
},
|
||||
title: 'Description',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
currentIncidentState: {
|
||||
color: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
title: 'Current State',
|
||||
type: FieldType.Text,
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
if (!item['currentIncidentState']) {
|
||||
throw new BadDataException(
|
||||
'Incident Status not found'
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Pill
|
||||
color={
|
||||
(
|
||||
item[
|
||||
'currentIncidentState'
|
||||
] as JSONObject
|
||||
)['color'] as Color
|
||||
}
|
||||
text={
|
||||
(
|
||||
item[
|
||||
'currentIncidentState'
|
||||
] as JSONObject
|
||||
)['name'] as string
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
monitors: {
|
||||
name: true,
|
||||
_id: true,
|
||||
},
|
||||
},
|
||||
title: 'Monitors Affected',
|
||||
type: FieldType.Text,
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
return (
|
||||
<MonitorsElement
|
||||
monitors={
|
||||
Monitor.fromJSON(
|
||||
(item['monitors'] as JSONArray) ||
|
||||
[],
|
||||
Monitor
|
||||
) as Array<Monitor>
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
modelId: modelId,
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default IncidentView;
|
||||
56
Dashboard/src/Pages/Incidents/View/SideMenu.tsx
Normal file
56
Dashboard/src/Pages/Incidents/View/SideMenu.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React, { FunctionComponent, ReactElement } from 'react';
|
||||
import Route from 'Common/Types/API/Route';
|
||||
import { IconProp } from 'CommonUI/src/Components/Icon/Icon';
|
||||
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.INCIDENT_VIEW] as Route
|
||||
, props.modelId),
|
||||
}}
|
||||
icon={IconProp.Info}
|
||||
/>
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: 'State Timeline',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.INCIDENT_VIEW_STATE_TIMELINE] as Route
|
||||
, props.modelId),
|
||||
}}
|
||||
icon={IconProp.List}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Advanced">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: 'Delete Incident',
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.INCIDENT_VIEW_DELETE] as Route
|
||||
, props.modelId),
|
||||
}}
|
||||
icon={IconProp.Trash}
|
||||
className="danger-on-hover"
|
||||
/>
|
||||
</SideMenuSection>
|
||||
</SideMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardSideMenu;
|
||||
150
Dashboard/src/Pages/Incidents/View/StateTimeline.tsx
Normal file
150
Dashboard/src/Pages/Incidents/View/StateTimeline.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
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 SideMenu from './SideMenu';
|
||||
import Navigation from 'CommonUI/src/Utils/Navigation';
|
||||
import ObjectID from 'Common/Types/ObjectID';
|
||||
import ModelTable from 'CommonUI/src/Components/ModelTable/ModelTable';
|
||||
import IncidentStateTimeline from "Model/Models/IncidentStateTimeline"
|
||||
import { IconProp } from 'CommonUI/src/Components/Icon/Icon';
|
||||
import BadDataException from 'Common/Types/Exception/BadDataException';
|
||||
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType';
|
||||
import IncidentState from 'Model/Models/IncidentState';
|
||||
import FieldType from 'CommonUI/src/Components/Types/FieldType';
|
||||
import { JSONObject } from 'Common/Types/JSON';
|
||||
import Color from 'Common/Types/Color';
|
||||
import Pill from 'CommonUI/src/Components/Pill/Pill';
|
||||
|
||||
const IncidentDelete: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps
|
||||
): ReactElement => {
|
||||
const modelId: ObjectID = new ObjectID(
|
||||
Navigation.getLastParam(1)?.toString().substring(1) || ''
|
||||
);
|
||||
|
||||
return (
|
||||
<Page
|
||||
title={'Incidents'}
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
title: 'Project',
|
||||
to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route, modelId)
|
||||
},
|
||||
{
|
||||
title: 'Incidents',
|
||||
to: RouteUtil.populateRouteParams(RouteMap[PageMap.INCIDENTS] as Route, modelId),
|
||||
},
|
||||
{
|
||||
title: 'View Incident',
|
||||
to: RouteUtil.populateRouteParams(RouteMap[PageMap.INCIDENT_VIEW] as Route, modelId),
|
||||
},
|
||||
{
|
||||
title: 'Status Timeline',
|
||||
to: RouteUtil.populateRouteParams(RouteMap[PageMap.INCIDENT_VIEW_STATE_TIMELINE] as Route, modelId),
|
||||
},
|
||||
]}
|
||||
sideMenu={<SideMenu modelId={modelId} />}
|
||||
>
|
||||
|
||||
<ModelTable<IncidentStateTimeline>
|
||||
modelType={IncidentStateTimeline}
|
||||
id="table-incident-status-timeline"
|
||||
isDeleteable={true}
|
||||
isCreateable={true}
|
||||
isViewable={false}
|
||||
query={{
|
||||
incidentId: modelId,
|
||||
projectId: props.currentProject?._id,
|
||||
}}
|
||||
onBeforeCreate={(
|
||||
item: IncidentStateTimeline
|
||||
): Promise<IncidentStateTimeline> => {
|
||||
if (!props.currentProject || !props.currentProject.id) {
|
||||
throw new BadDataException('Project ID cannot be null');
|
||||
}
|
||||
item.incidentId = modelId;
|
||||
item.projectId = props.currentProject.id;
|
||||
return Promise.resolve(item);
|
||||
}}
|
||||
cardProps={{
|
||||
icon: IconProp.List,
|
||||
title: 'Status Timeline',
|
||||
description:
|
||||
'Here is the status timeline for this incident',
|
||||
}}
|
||||
noItemsMessage={'No status timeline created for this incident so far.'}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
incidentState: true,
|
||||
},
|
||||
title: 'Incident Status',
|
||||
fieldType: FormFieldSchemaType.Dropdown,
|
||||
required: true,
|
||||
placeholder: 'Incident Status',
|
||||
dropdownModal: {
|
||||
type: IncidentState,
|
||||
labelField: 'name',
|
||||
valueField: '_id',
|
||||
},
|
||||
}
|
||||
]}
|
||||
showRefreshButton={true}
|
||||
showFilterButton={true}
|
||||
currentPageRoute={props.pageRoute}
|
||||
columns={[
|
||||
{
|
||||
field: {
|
||||
incidentState: {
|
||||
name: true,
|
||||
color: true,
|
||||
},
|
||||
},
|
||||
title: 'Incident Status',
|
||||
type: FieldType.Text,
|
||||
isFilterable: true,
|
||||
getElement: (item: JSONObject): ReactElement => {
|
||||
if (!item['incidentState']) {
|
||||
throw new BadDataException(
|
||||
'Incident Status not found'
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Pill
|
||||
color={
|
||||
(
|
||||
item[
|
||||
'incidentState'
|
||||
] as JSONObject
|
||||
)['color'] as Color
|
||||
}
|
||||
text={
|
||||
(
|
||||
item[
|
||||
'incidentState'
|
||||
] as JSONObject
|
||||
)['name'] as string
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
createdAt: true
|
||||
},
|
||||
title: 'Reported At',
|
||||
type: FieldType.Date,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default IncidentDelete;
|
||||
@@ -98,6 +98,12 @@ const MonitorView: FunctionComponent<PageComponentProps> = (
|
||||
modelType: Monitor,
|
||||
id: 'model-detail-monitors',
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
_id: true,
|
||||
},
|
||||
title: 'Monitor ID',
|
||||
},
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
|
||||
@@ -137,7 +137,7 @@ const MonitorDelete: FunctionComponent<PageComponentProps> = (
|
||||
field: {
|
||||
createdAt: true
|
||||
},
|
||||
title: 'Starts at',
|
||||
title: 'Reported At',
|
||||
type: FieldType.Date,
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
enum PageMap {
|
||||
INIT = 'INIT',
|
||||
HOME = 'HOME',
|
||||
|
||||
|
||||
INCIDENTS = 'INCIDENTS',
|
||||
INCIDENT_VIEW = 'INCIDENT_VIEW',
|
||||
INCIDENT_VIEW_DELETE = 'INCIDENT_VIEW_DELETE',
|
||||
INCIDENT_VIEW_STATE_TIMELINE = 'INCIDENT_VIEW_STATE_TIMELINE',
|
||||
|
||||
|
||||
MONITORS = 'MONITORS',
|
||||
MONITOR_VIEW = 'MONITOR_VIEW',
|
||||
MONITOR_VIEW_DELETE = 'MONITOR_VIEW_DELETE',
|
||||
MONITOR_VIEW_STATUS_TIMELINE = 'MONITOR_VIEW_STATUS_TIMELINE',
|
||||
|
||||
|
||||
STATUS_PAGE = 'STATUSPAGE',
|
||||
LOGS = 'LOGS',
|
||||
ON_CALL_DUTY = 'ON_CALL_DUTY',
|
||||
|
||||
@@ -9,9 +9,24 @@ import ObjectID from 'Common/Types/ObjectID';
|
||||
const RouteMap: Dictionary<Route> = {
|
||||
[PageMap.INIT]: new Route(`/dashboard`),
|
||||
[PageMap.HOME]: new Route(`/dashboard/${RouteParams.ProjectID}/home/`),
|
||||
|
||||
[PageMap.INCIDENTS]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/incidents/`
|
||||
),
|
||||
|
||||
[PageMap.INCIDENT_VIEW]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/incidents/${RouteParams.ModelID}`
|
||||
),
|
||||
|
||||
[PageMap.INCIDENT_VIEW_STATE_TIMELINE]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/incidents/${RouteParams.ModelID}/state-timeline`
|
||||
),
|
||||
|
||||
[PageMap.INCIDENT_VIEW_DELETE]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/incidents/${RouteParams.ModelID}/delete`
|
||||
),
|
||||
|
||||
|
||||
[PageMap.STATUS_PAGE]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/status-pages/`
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user