add incident page

This commit is contained in:
Simon Larsen
2022-08-25 14:12:11 +01:00
parent 01f93eca98
commit 73324ef700
20 changed files with 813 additions and 49 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -42,8 +42,6 @@ export class Service extends DatabaseService<Model> {
);
}
debugger;
createBy.data.currentMonitorStatusId = monitorStatus.id;
return { createBy, carryForward: null };

View File

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

View File

@@ -140,7 +140,7 @@ export default class ModelAPI {
httpMethod,
apiUrl,
{
data: model.toJSON(),
data: JSONFunctions.serialize(model.toJSON()),
miscDataProps: miscDataProps || {},
},
this.getCommonHeaders(requestOptions)

View File

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

View File

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

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

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

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

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

View File

@@ -98,6 +98,12 @@ const MonitorView: FunctionComponent<PageComponentProps> = (
modelType: Monitor,
id: 'model-detail-monitors',
fields: [
{
field: {
_id: true,
},
title: 'Monitor ID',
},
{
field: {
name: true,

View File

@@ -137,7 +137,7 @@ const MonitorDelete: FunctionComponent<PageComponentProps> = (
field: {
createdAt: true
},
title: 'Starts at',
title: 'Reported At',
type: FieldType.Date,
},
]}

View File

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

View File

@@ -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/`
),