Merge pull request #14110 from iLert/feature/iLert-services-statuspages-multiple-responders

Feature: add iLert services, status pages and multiple-responders for alerts
This commit is contained in:
Fredrik Adelöw
2022-10-21 10:46:56 +01:00
committed by GitHub
49 changed files with 1851 additions and 1420 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-ilert': minor
---
Added support for multiple responders in alert list, added new tab with list to support iLert resource 'service', added new tab with list to support iLert resource 'status page'
+244 -204
View File
@@ -16,11 +16,96 @@ import { RouteRef } from '@backstage/core-plugin-api';
// @public (undocumented)
export const ACCEPTED = 'ACCEPTED';
// @public (undocumented)
export interface Alert {
// (undocumented)
alertKey: string;
// (undocumented)
alertSource: AlertSource | null;
// (undocumented)
assignedTo: User | null;
// (undocumented)
commentPublishToSubscribers: boolean;
// (undocumented)
commentText: string;
// (undocumented)
details: string;
// (undocumented)
id: number;
// (undocumented)
images: Image_2[];
// (undocumented)
links: Link[];
// (undocumented)
logEntries: LogEntry[];
// (undocumented)
priority: AlertPriority;
// (undocumented)
reportTime: string;
// (undocumented)
resolvedOn: string;
// (undocumented)
responders: Responder[];
// (undocumented)
status: AlertStatus;
// (undocumented)
subscribers: Subscriber[];
// (undocumented)
summary: string;
}
// @public (undocumented)
export interface AlertAction {
// (undocumented)
extensionId?: string;
// (undocumented)
history?: AlertActionHistory[];
// (undocumented)
name: string;
// (undocumented)
type: string;
// (undocumented)
webhookId: string;
}
// @public (undocumented)
export interface AlertActionHistory {
// (undocumented)
actor: User;
// (undocumented)
alertId: number;
// (undocumented)
id: string;
// (undocumented)
success: boolean;
// (undocumented)
webhookId: string;
}
// @public (undocumented)
export type AlertPriority = 'HIGH' | 'LOW';
// @public (undocumented)
export interface AlertResponder {
// (undocumented)
disabled: boolean;
// (undocumented)
group: 'SUGGESTED' | 'USER' | 'ESCALATION_POLICY' | 'ON_CALL_SCHEDULE';
// (undocumented)
id: number;
// (undocumented)
name: string;
}
// @public (undocumented)
export interface AlertSource {
// (undocumented)
active?: boolean;
// (undocumented)
alertCreation?: AlertSourceAlertCreation;
// (undocumented)
alertPriorityRule?: AlertSourceAlertPriorityRule;
// (undocumented)
autoResolutionTimeout?: string;
// (undocumented)
autotaskMetadata?: AlertSourceAutotaskMetadata;
@@ -45,10 +130,6 @@ export interface AlertSource {
// (undocumented)
id: number;
// (undocumented)
incidentCreation?: AlertSourceIncidentCreation;
// (undocumented)
incidentPriorityRule?: AlertSourceIncidentPriorityRule;
// (undocumented)
integrationKey?: string;
// (undocumented)
integrationType: AlertSourceIntegrationType;
@@ -66,6 +147,21 @@ export interface AlertSource {
teams: TeamShort[];
}
// @public (undocumented)
export type AlertSourceAlertCreation =
| 'ONE_ALERT_PER_EMAIL'
| 'ONE_ALERT_PER_EMAIL_SUBJECT'
| 'ONE_PENDING_ALERT_ALLOWED'
| 'ONE_OPEN_ALERT_ALLOWED'
| 'OPEN_RESOLVE_ON_EXTRACTION';
// @public (undocumented)
export type AlertSourceAlertPriorityRule =
| 'HIGH'
| 'LOW'
| 'HIGH_DURING_SUPPORT_HOURS'
| 'LOW_DURING_SUPPORT_HOURS';
// @public (undocumented)
export interface AlertSourceAutotaskMetadata {
// (undocumented)
@@ -109,21 +205,6 @@ export interface AlertSourceHeartbeat {
summary: string;
}
// @public (undocumented)
export type AlertSourceIncidentCreation =
| 'ONE_INCIDENT_PER_EMAIL'
| 'ONE_INCIDENT_PER_EMAIL_SUBJECT'
| 'ONE_PENDING_INCIDENT_ALLOWED'
| 'ONE_OPEN_INCIDENT_ALLOWED'
| 'OPEN_RESOLVE_ON_EXTRACTION';
// @public (undocumented)
export type AlertSourceIncidentPriorityRule =
| 'HIGH'
| 'LOW'
| 'HIGH_DURING_SUPPORT_HOURS'
| 'LOW_DURING_SUPPORT_HOURS';
// @public (undocumented)
export type AlertSourceIntegrationType =
| 'NAGIOS'
@@ -192,7 +273,7 @@ export interface AlertSourceSupportDay {
// @public (undocumented)
export interface AlertSourceSupportHours {
// (undocumented)
autoRaiseIncidents: boolean;
autoRaiseAlerts: boolean;
// (undocumented)
supportDays: {
MONDAY: AlertSourceSupportDay;
@@ -214,6 +295,12 @@ export type AlertSourceTimeZone =
| 'America/Los_Angeles'
| 'Asia/Istanbul';
// @public (undocumented)
export type AlertStatus = typeof PENDING | typeof ACCEPTED | typeof RESOLVED;
// @public (undocumented)
export const DEGRADED = 'DEGRADED';
// @public (undocumented)
export const EntityILertCard: () => JSX.Element;
@@ -255,72 +342,85 @@ export type EventRequest = {
};
// @public (undocumented)
export type GetIncidentsCountOpts = {
states?: IncidentStatus[];
export type GetAlertsCountOpts = {
states?: AlertStatus[];
};
// @public (undocumented)
export type GetIncidentsOpts = {
export type GetAlertsOpts = {
maxResults?: number;
startIndex?: number;
states?: IncidentStatus[];
states?: AlertStatus[];
alertSources?: number[];
};
// @public (undocumented)
export type GetServicesOpts = {
maxResults?: number;
startIndex?: number;
};
// @public (undocumented)
export type GetStatusPagesOpts = {
maxResults?: number;
startIndex?: number;
};
// @public (undocumented)
export interface ILertApi {
// (undocumented)
acceptIncident(incident: Incident, userName: string): Promise<Incident>;
acceptAlert(alert: Alert, userName: string): Promise<Alert>;
// (undocumented)
addImmediateMaintenance(
alertSourceId: number,
minutes: number,
): Promise<void>;
// (undocumented)
assignIncident(
incident: Incident,
responder: IncidentResponder,
): Promise<Incident>;
assignAlert(alert: Alert, responder: AlertResponder): Promise<Alert>;
// (undocumented)
createIncident(eventRequest: EventRequest): Promise<boolean>;
createAlert(eventRequest: EventRequest): Promise<boolean>;
// (undocumented)
disableAlertSource(alertSource: AlertSource): Promise<AlertSource>;
// (undocumented)
enableAlertSource(alertSource: AlertSource): Promise<AlertSource>;
// (undocumented)
fetchAlert(id: number): Promise<Alert>;
// (undocumented)
fetchAlertActions(alert: Alert): Promise<AlertAction[]>;
// (undocumented)
fetchAlertResponders(alert: Alert): Promise<AlertResponder[]>;
// (undocumented)
fetchAlerts(opts?: GetAlertsOpts): Promise<Alert[]>;
// (undocumented)
fetchAlertsCount(opts?: GetAlertsCountOpts): Promise<number>;
// (undocumented)
fetchAlertSource(idOrIntegrationKey: number | string): Promise<AlertSource>;
// (undocumented)
fetchAlertSourceOnCalls(alertSource: AlertSource): Promise<OnCall[]>;
// (undocumented)
fetchAlertSources(): Promise<AlertSource[]>;
// (undocumented)
fetchIncident(id: number): Promise<Incident>;
// (undocumented)
fetchIncidentActions(incident: Incident): Promise<IncidentAction[]>;
// (undocumented)
fetchIncidentResponders(incident: Incident): Promise<IncidentResponder[]>;
// (undocumented)
fetchIncidents(opts?: GetIncidentsOpts): Promise<Incident[]>;
// (undocumented)
fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise<number>;
// (undocumented)
fetchOnCallSchedules(): Promise<Schedule[]>;
// (undocumented)
fetchUptimeMonitor(id: number): Promise<UptimeMonitor>;
fetchServices(opts?: GetServicesOpts): Promise<Service[]>;
// (undocumented)
fetchUptimeMonitors(): Promise<UptimeMonitor[]>;
fetchStatusPages(opts?: GetStatusPagesOpts): Promise<StatusPage[]>;
// (undocumented)
fetchUsers(): Promise<User[]>;
// (undocumented)
getAlertDetailsURL(alert: Alert): string;
// (undocumented)
getAlertSourceDetailsURL(alertSource: AlertSource | null): string;
// (undocumented)
getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string;
// (undocumented)
getIncidentDetailsURL(incident: Incident): string;
// (undocumented)
getScheduleDetailsURL(schedule: Schedule): string;
// (undocumented)
getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string;
getServiceDetailsURL(service: Service): string;
// (undocumented)
getStatusPageDetailsURL(statusPage: StatusPage): string;
// (undocumented)
getStatusPageURL(statusPage: StatusPage): string;
// (undocumented)
getUserInitials(user: User | null): string;
// (undocumented)
@@ -333,16 +433,9 @@ export interface ILertApi {
end: string,
): Promise<Schedule>;
// (undocumented)
pauseUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise<UptimeMonitor>;
resolveAlert(alert: Alert, userName: string): Promise<Alert>;
// (undocumented)
resolveIncident(incident: Incident, userName: string): Promise<Incident>;
// (undocumented)
resumeUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise<UptimeMonitor>;
// (undocumented)
triggerIncidentAction(
incident: Incident,
action: IncidentAction,
): Promise<void>;
triggerAlertAction(alert: Alert, action: AlertAction): Promise<void>;
}
// @public (undocumented)
@@ -359,45 +452,42 @@ export class ILertClient implements ILertApi {
proxyPath: string;
});
// (undocumented)
acceptIncident(incident: Incident, userName: string): Promise<Incident>;
acceptAlert(alert: Alert, userName: string): Promise<Alert>;
// (undocumented)
addImmediateMaintenance(
alertSourceId: number,
minutes: number,
): Promise<void>;
// (undocumented)
assignIncident(
incident: Incident,
responder: IncidentResponder,
): Promise<Incident>;
assignAlert(alert: Alert, responder: AlertResponder): Promise<Alert>;
// (undocumented)
createIncident(eventRequest: EventRequest): Promise<boolean>;
createAlert(eventRequest: EventRequest): Promise<boolean>;
// (undocumented)
disableAlertSource(alertSource: AlertSource): Promise<AlertSource>;
// (undocumented)
enableAlertSource(alertSource: AlertSource): Promise<AlertSource>;
// (undocumented)
fetchAlert(id: number): Promise<Alert>;
// (undocumented)
fetchAlertActions(alert: Alert): Promise<AlertAction[]>;
// (undocumented)
fetchAlertResponders(alert: Alert): Promise<AlertResponder[]>;
// (undocumented)
fetchAlerts(opts?: GetAlertsOpts): Promise<Alert[]>;
// (undocumented)
fetchAlertsCount(opts?: GetAlertsCountOpts): Promise<number>;
// (undocumented)
fetchAlertSource(idOrIntegrationKey: number | string): Promise<AlertSource>;
// (undocumented)
fetchAlertSourceOnCalls(alertSource: AlertSource): Promise<OnCall[]>;
// (undocumented)
fetchAlertSources(): Promise<AlertSource[]>;
// (undocumented)
fetchIncident(id: number): Promise<Incident>;
// (undocumented)
fetchIncidentActions(incident: Incident): Promise<IncidentAction[]>;
// (undocumented)
fetchIncidentResponders(incident: Incident): Promise<IncidentResponder[]>;
// (undocumented)
fetchIncidents(opts?: GetIncidentsOpts): Promise<Incident[]>;
// (undocumented)
fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise<number>;
// (undocumented)
fetchOnCallSchedules(): Promise<Schedule[]>;
// (undocumented)
fetchUptimeMonitor(id: number): Promise<UptimeMonitor>;
fetchServices(opts?: GetServicesOpts): Promise<Service[]>;
// (undocumented)
fetchUptimeMonitors(): Promise<UptimeMonitor[]>;
fetchStatusPages(opts?: GetStatusPagesOpts): Promise<StatusPage[]>;
// (undocumented)
fetchUsers(): Promise<User[]>;
// (undocumented)
@@ -406,15 +496,19 @@ export class ILertClient implements ILertApi {
discoveryApi: DiscoveryApi,
): ILertClient;
// (undocumented)
getAlertDetailsURL(alert: Alert): string;
// (undocumented)
getAlertSourceDetailsURL(alertSource: AlertSource | null): string;
// (undocumented)
getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string;
// (undocumented)
getIncidentDetailsURL(incident: Incident): string;
// (undocumented)
getScheduleDetailsURL(schedule: Schedule): string;
// (undocumented)
getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string;
getServiceDetailsURL(service: Service): string;
// (undocumented)
getStatusPageDetailsURL(statusPage: StatusPage): string;
// (undocumented)
getStatusPageURL(statusPage: StatusPage): string;
// (undocumented)
getUserInitials(user: User | null): string;
// (undocumented)
@@ -427,16 +521,9 @@ export class ILertClient implements ILertApi {
end: string,
): Promise<Schedule>;
// (undocumented)
pauseUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise<UptimeMonitor>;
resolveAlert(alert: Alert, userName: string): Promise<Alert>;
// (undocumented)
resolveIncident(incident: Incident, userName: string): Promise<Incident>;
// (undocumented)
resumeUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise<UptimeMonitor>;
// (undocumented)
triggerIncidentAction(
incident: Incident,
action: IncidentAction,
): Promise<void>;
triggerAlertAction(alert: Alert, action: AlertAction): Promise<void>;
}
// @public (undocumented)
@@ -470,88 +557,6 @@ interface Image_2 {
}
export { Image_2 as Image };
// @public (undocumented)
export interface Incident {
// (undocumented)
alertSource: AlertSource | null;
// (undocumented)
assignedTo: User | null;
// (undocumented)
commentPublishToSubscribers: boolean;
// (undocumented)
commentText: string;
// (undocumented)
details: string;
// (undocumented)
id: number;
// (undocumented)
images: Image_2[];
// (undocumented)
incidentKey: string;
// (undocumented)
links: Link[];
// (undocumented)
logEntries: LogEntry[];
// (undocumented)
priority: IncidentPriority;
// (undocumented)
reportTime: string;
// (undocumented)
resolvedOn: string;
// (undocumented)
status: IncidentStatus;
// (undocumented)
subscribers: Subscriber[];
// (undocumented)
summary: string;
}
// @public (undocumented)
export interface IncidentAction {
// (undocumented)
extensionId?: string;
// (undocumented)
history?: IncidentActionHistory[];
// (undocumented)
name: string;
// (undocumented)
type: string;
// (undocumented)
webhookId: string;
}
// @public (undocumented)
export interface IncidentActionHistory {
// (undocumented)
actor: User;
// (undocumented)
id: string;
// (undocumented)
incidentId: number;
// (undocumented)
success: boolean;
// (undocumented)
webhookId: string;
}
// @public (undocumented)
export type IncidentPriority = 'HIGH' | 'LOW';
// @public (undocumented)
export interface IncidentResponder {
// (undocumented)
disabled: boolean;
// (undocumented)
group: 'SUGGESTED' | 'USER' | 'ESCALATION_POLICY' | 'ON_CALL_SCHEDULE';
// (undocumented)
id: number;
// (undocumented)
name: string;
}
// @public (undocumented)
export type IncidentStatus = typeof PENDING | typeof ACCEPTED | typeof RESOLVED;
// @public (undocumented)
const isPluginApplicableToEntity: (entity: Entity) => boolean;
export { isPluginApplicableToEntity as isILertAvailable };
@@ -570,6 +575,8 @@ export interface Link {
// @public (undocumented)
export interface LogEntry {
// (undocumented)
alertId?: number;
// (undocumented)
filterTypes?: string[];
// (undocumented)
@@ -579,8 +586,6 @@ export interface LogEntry {
// (undocumented)
id: number;
// (undocumented)
incidentId?: number;
// (undocumented)
logEntryType: string;
// (undocumented)
text: string;
@@ -588,6 +593,9 @@ export interface LogEntry {
timestamp: string;
}
// @public (undocumented)
export const MAJOR_OUTAGE = 'MAJOR_OUTAGE';
// @public (undocumented)
export interface OnCall {
// (undocumented)
@@ -604,6 +612,12 @@ export interface OnCall {
user: User;
}
// @public (undocumented)
export const OPERATIONAL = 'OPERATIONAL';
// @public (undocumented)
export const PARTIAL_OUTAGE = 'PARTIAL_OUTAGE';
// @public (undocumented)
export const PENDING = 'PENDING';
@@ -615,9 +629,25 @@ export interface Phone {
regionCode: string;
}
// @public (undocumented)
export const PRIVATE = 'PRIVATE';
// @public (undocumented)
export const PUBLIC = 'PUBLIC';
// @public (undocumented)
export const RESOLVED = 'RESOLVED';
// @public (undocumented)
export interface Responder {
// (undocumented)
acceptedAt?: string;
// (undocumented)
status: string;
// (undocumented)
user: User;
}
// @public (undocumented)
export const Router: () => JSX.Element;
@@ -643,6 +673,26 @@ export interface Schedule {
timezone: string;
}
// @public (undocumented)
export interface Service {
// (undocumented)
id: number;
// (undocumented)
name: string;
// (undocumented)
status: ServiceStatus;
// (undocumented)
uptime: Uptime;
}
// @public (undocumented)
export type ServiceStatus =
| typeof OPERATIONAL
| typeof UNDER_MAINTENANCE
| typeof DEGRADED
| typeof PARTIAL_OUTAGE
| typeof MAJOR_OUTAGE;
// @public (undocumented)
export interface Shift {
// (undocumented)
@@ -653,6 +703,25 @@ export interface Shift {
user: User;
}
// @public (undocumented)
export interface StatusPage {
// (undocumented)
domain: string;
// (undocumented)
id: number;
// (undocumented)
name: string;
// (undocumented)
status: ServiceStatus;
// (undocumented)
subdomain: string;
// (undocumented)
visibility: StatusPageVisibility;
}
// @public (undocumented)
export type StatusPageVisibility = typeof PRIVATE | typeof PUBLIC;
// @public (undocumented)
export interface Subscriber {
// (undocumented)
@@ -689,47 +758,18 @@ export interface TeamShort {
}
// @public (undocumented)
export interface UptimeMonitor {
export const UNDER_MAINTENANCE = 'UNDER_MAINTENANCE';
// @public (undocumented)
export interface Uptime {
// (undocumented)
checkParams: UptimeMonitorCheckParams;
// (undocumented)
checkType: 'http' | 'tcp' | 'udp' | 'ping';
// (undocumented)
createIncidentAfterFailedChecks: number;
// (undocumented)
embedUrl: string;
// (undocumented)
escalationPolicy: EscalationPolicy;
// (undocumented)
id: number;
// (undocumented)
intervalSec: number;
// (undocumented)
lastStatusChange: string;
// (undocumented)
name: string;
// (undocumented)
paused: boolean;
// (undocumented)
region: 'EU' | 'US';
// (undocumented)
shareUrl: string;
// (undocumented)
status: string;
// (undocumented)
teams: TeamShort[];
// (undocumented)
timeoutMs: number;
uptimePercentage: UptimePercentage;
}
// @public (undocumented)
export interface UptimeMonitorCheckParams {
export interface UptimePercentage {
// (undocumented)
host?: string;
// (undocumented)
port?: number;
// (undocumented)
url?: string;
p90: number;
}
// @public (undocumented)
+121 -142
View File
@@ -13,30 +13,33 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AuthenticationError, ResponseError } from '@backstage/errors';
import {
AlertSource,
EscalationPolicy,
Incident,
IncidentAction,
IncidentResponder,
OnCall,
Schedule,
UptimeMonitor,
User,
} from '../types';
import {
ILertApi,
GetIncidentsOpts,
GetIncidentsCountOpts,
EventRequest,
} from './types';
import { DateTime as dt } from 'luxon';
import {
ConfigApi,
createApiRef,
DiscoveryApi,
} from '@backstage/core-plugin-api';
import { AuthenticationError, ResponseError } from '@backstage/errors';
import { DateTime as dt } from 'luxon';
import {
Alert,
AlertAction,
AlertResponder,
AlertSource,
EscalationPolicy,
OnCall,
Schedule,
Service,
StatusPage,
User,
} from '../types';
import {
EventRequest,
GetAlertsCountOpts,
GetAlertsOpts,
GetServicesOpts,
GetStatusPagesOpts,
ILertApi,
} from './types';
/** @public */
export const ilertApiRef = createApiRef<ILertApi>({
@@ -102,7 +105,7 @@ export class ILertClient implements ILertApi {
return await response.json();
}
async fetchIncidents(opts?: GetIncidentsOpts): Promise<Incident[]> {
async fetchAlerts(opts?: GetAlertsOpts): Promise<Alert[]> {
const init = {
headers: JSON_HEADERS,
};
@@ -126,15 +129,11 @@ export class ILertClient implements ILertApi {
query.append('state', state);
});
}
const response = await this.fetch(
`/api/v1/incidents?${query.toString()}`,
init,
);
const response = await this.fetch(`/api/alerts?${query.toString()}`, init);
return response;
}
async fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise<number> {
async fetchAlertsCount(opts?: GetAlertsCountOpts): Promise<number> {
const init = {
headers: JSON_HEADERS,
};
@@ -145,96 +144,85 @@ export class ILertClient implements ILertApi {
});
}
const response = await this.fetch(
`/api/v1/incidents/count?${query.toString()}`,
`/api/alerts/count?${query.toString()}`,
init,
);
return response && response.count ? response.count : 0;
}
async fetchIncident(id: number): Promise<Incident> {
async fetchAlert(id: number): Promise<Alert> {
const init = {
headers: JSON_HEADERS,
};
const response = await this.fetch(
`/api/v1/incidents/${encodeURIComponent(id)}`,
`/api/alerts/${encodeURIComponent(id)}`,
init,
);
return response;
}
async fetchIncidentResponders(
incident: Incident,
): Promise<IncidentResponder[]> {
async fetchAlertResponders(alert: Alert): Promise<AlertResponder[]> {
const init = {
headers: JSON_HEADERS,
};
const response = await this.fetch(
`/api/v1/incidents/${encodeURIComponent(incident.id)}/responders`,
`/api/alerts/${encodeURIComponent(alert.id)}/responders`,
init,
);
return response;
}
async fetchIncidentActions(incident: Incident): Promise<IncidentAction[]> {
async fetchAlertActions(alert: Alert): Promise<AlertAction[]> {
const init = {
headers: JSON_HEADERS,
};
const response = await this.fetch(
`/api/v1/incidents/${encodeURIComponent(incident.id)}/actions`,
`/api/alerts/${encodeURIComponent(alert.id)}/actions`,
init,
);
return response;
}
async acceptIncident(
incident: Incident,
userName: string,
): Promise<Incident> {
async acceptAlert(alert: Alert, userName: string): Promise<Alert> {
const init = {
method: 'POST',
headers: JSON_HEADERS,
body: JSON.stringify({
apiKey: incident.alertSource?.integrationKey || '',
incidentKey: incident.incidentKey,
apiKey: alert.alertSource?.integrationKey || '',
alertKey: alert.alertKey,
summary: `from ${userName} via Backstage plugin`,
eventType: 'ACCEPT',
}),
};
await this.fetch('/api/v1/events', init);
return this.fetchIncident(incident.id);
await this.fetch('/api/events', init);
return this.fetchAlert(alert.id);
}
async resolveIncident(
incident: Incident,
userName: string,
): Promise<Incident> {
async resolveAlert(alert: Alert, userName: string): Promise<Alert> {
const init = {
method: 'POST',
headers: JSON_HEADERS,
body: JSON.stringify({
apiKey: incident.alertSource?.integrationKey || '',
incidentKey: incident.incidentKey,
apiKey: alert.alertSource?.integrationKey || '',
alertKey: alert.alertKey,
summary: `from ${userName} via Backstage plugin`,
eventType: 'RESOLVE',
}),
};
await this.fetch('/api/v1/events', init);
return this.fetchIncident(incident.id);
await this.fetch('/api/events', init);
return this.fetchAlert(alert.id);
}
async assignIncident(
incident: Incident,
responder: IncidentResponder,
): Promise<Incident> {
async assignAlert(alert: Alert, responder: AlertResponder): Promise<Alert> {
const init = {
method: 'PUT',
headers: JSON_HEADERS,
@@ -254,19 +242,14 @@ export class ILertClient implements ILertApi {
}
const response = await this.fetch(
`/api/v1/incidents/${encodeURIComponent(
incident.id,
)}/assign?${query.toString()}`,
`/api/alerts/${encodeURIComponent(alert.id)}/assign?${query.toString()}`,
init,
);
return response;
}
async triggerIncidentAction(
incident: Incident,
action: IncidentAction,
): Promise<void> {
async triggerAlertAction(alert: Alert, action: AlertAction): Promise<void> {
const init = {
method: 'POST',
headers: JSON_HEADERS,
@@ -279,12 +262,12 @@ export class ILertClient implements ILertApi {
};
await this.fetch(
`/api/v1/incidents/${encodeURIComponent(incident.id)}/actions`,
`/api/alerts/${encodeURIComponent(alert.id)}/actions`,
init,
);
}
async createIncident(eventRequest: EventRequest): Promise<boolean> {
async createAlert(eventRequest: EventRequest): Promise<boolean> {
const init = {
method: 'POST',
headers: JSON_HEADERS,
@@ -305,64 +288,7 @@ export class ILertClient implements ILertApi {
}),
};
const response = await this.fetch('/api/v1/events', init);
return response;
}
async fetchUptimeMonitors(): Promise<UptimeMonitor[]> {
const init = {
headers: JSON_HEADERS,
};
const response = await this.fetch('/api/v1/uptime-monitors', init);
return response;
}
async fetchUptimeMonitor(id: number): Promise<UptimeMonitor> {
const init = {
headers: JSON_HEADERS,
};
const response: UptimeMonitor = await this.fetch(
`/api/v1/uptime-monitors/${encodeURIComponent(id)}`,
init,
);
return response;
}
async pauseUptimeMonitor(
uptimeMonitor: UptimeMonitor,
): Promise<UptimeMonitor> {
const init = {
method: 'PUT',
headers: JSON_HEADERS,
body: JSON.stringify({ ...uptimeMonitor, paused: true }),
};
const response = await this.fetch(
`/api/v1/uptime-monitors/${encodeURIComponent(uptimeMonitor.id)}`,
init,
);
return response;
}
async resumeUptimeMonitor(
uptimeMonitor: UptimeMonitor,
): Promise<UptimeMonitor> {
const init = {
method: 'PUT',
headers: JSON_HEADERS,
body: JSON.stringify({ ...uptimeMonitor, paused: false }),
};
const response = await this.fetch(
`/api/v1/uptime-monitors/${encodeURIComponent(uptimeMonitor.id)}`,
init,
);
const response = await this.fetch('/api/events', init);
return response;
}
@@ -371,7 +297,7 @@ export class ILertClient implements ILertApi {
headers: JSON_HEADERS,
};
const response = await this.fetch('/api/v1/alert-sources', init);
const response = await this.fetch('/api/alert-sources', init);
return response;
}
@@ -384,7 +310,7 @@ export class ILertClient implements ILertApi {
};
const response = await this.fetch(
`/api/v1/alert-sources/${encodeURIComponent(idOrIntegrationKey)}`,
`/api/alert-sources/${encodeURIComponent(idOrIntegrationKey)}`,
init,
);
@@ -397,7 +323,7 @@ export class ILertClient implements ILertApi {
};
const response = await this.fetch(
`/api/v1/on-calls?policies=${encodeURIComponent(
`/api/on-calls?policies=${encodeURIComponent(
alertSource.escalationPolicy.id,
)}&expand=user&expand=escalationPolicy&timezone=${dt.local().zoneName}`,
init,
@@ -414,7 +340,7 @@ export class ILertClient implements ILertApi {
};
const response = await this.fetch(
`/api/v1/alert-sources/${encodeURIComponent(alertSource.id)}`,
`/api/alert-sources/${encodeURIComponent(alertSource.id)}`,
init,
);
@@ -429,7 +355,7 @@ export class ILertClient implements ILertApi {
};
const response = await this.fetch(
`/api/v1/alert-sources/${encodeURIComponent(alertSource.id)}`,
`/api/alert-sources/${encodeURIComponent(alertSource.id)}`,
init,
);
@@ -453,7 +379,7 @@ export class ILertClient implements ILertApi {
}),
};
const response = await this.fetch('/api/v1/maintenance-windows', init);
const response = await this.fetch('/api/maintenance-windows', init);
return response;
}
@@ -463,7 +389,10 @@ export class ILertClient implements ILertApi {
headers: JSON_HEADERS,
};
const response = await this.fetch('/api/v1/schedules', init);
const response = await this.fetch(
'/api/schedules?include=currentShift&include=nextShift',
init,
);
return response;
}
@@ -473,7 +402,7 @@ export class ILertClient implements ILertApi {
headers: JSON_HEADERS,
};
const response = await this.fetch('/api/v1/users', init);
const response = await this.fetch('/api/users', init);
return response;
}
@@ -491,17 +420,57 @@ export class ILertClient implements ILertApi {
};
const response = await this.fetch(
`/api/v1/schedules/${encodeURIComponent(scheduleId)}/overrides`,
`/api/schedules/${encodeURIComponent(scheduleId)}/overrides`,
init,
);
return response;
}
getIncidentDetailsURL(incident: Incident): string {
return `${this.baseUrl}/incident/view.jsf?id=${encodeURIComponent(
incident.id,
)}`;
async fetchServices(opts?: GetServicesOpts): Promise<Service[]> {
const init = {
headers: JSON_HEADERS,
};
const query = new URLSearchParams();
if (opts?.maxResults !== undefined) {
query.append('max-results', String(opts.maxResults));
}
if (opts?.startIndex !== undefined) {
query.append('start-index', String(opts.startIndex));
}
query.append('include', 'uptime');
const response = await this.fetch(
`/api/services?${query.toString()}`,
init,
);
return response;
}
async fetchStatusPages(opts?: GetStatusPagesOpts): Promise<StatusPage[]> {
const init = {
headers: JSON_HEADERS,
};
const query = new URLSearchParams();
if (opts?.maxResults !== undefined) {
query.append('max-results', String(opts.maxResults));
}
if (opts?.startIndex !== undefined) {
query.append('start-index', String(opts.startIndex));
}
query.append('include', 'subscribed');
const response = await this.fetch(
`/api/status-pages?${query.toString()}`,
init,
);
return response;
}
getAlertDetailsURL(alert: Alert): string {
return `${this.baseUrl}/alert/view.jsf?id=${encodeURIComponent(alert.id)}`;
}
getAlertSourceDetailsURL(alertSource: AlertSource | null): string {
@@ -519,18 +488,28 @@ export class ILertClient implements ILertApi {
)}`;
}
getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string {
return `${this.baseUrl}/uptime/view.jsf?id=${encodeURIComponent(
uptimeMonitor.id,
)}`;
}
getScheduleDetailsURL(schedule: Schedule): string {
return `${this.baseUrl}/schedule/view.jsf?id=${encodeURIComponent(
schedule.id,
)}`;
}
getServiceDetailsURL(service: Service): string {
return `${this.baseUrl}/service/view.jsf?id=${encodeURIComponent(
service.id,
)}`;
}
getStatusPageDetailsURL(statusPage: StatusPage): string {
return `${this.baseUrl}/status-page/view.jsf?id=${encodeURIComponent(
statusPage.id,
)}`;
}
getStatusPageURL(statusPage: StatusPage): string {
return statusPage.domain ? statusPage.domain : statusPage.subdomain;
}
getUserPhoneNumber(user: User | null) {
return user?.mobile?.number || user?.landline?.number || '';
}
@@ -542,7 +521,7 @@ export class ILertClient implements ILertApi {
if (!user.firstName && !user.lastName) {
return user.username;
}
return `${user.firstName} ${user.lastName} (${user.username})`;
return `${user.firstName} ${user.lastName}`;
}
private async apiUrl() {
+6 -4
View File
@@ -14,11 +14,13 @@
* limitations under the License.
*/
export { ILertClient, ilertApiRef } from './client';
export { ilertApiRef, ILertClient } from './client';
export type {
EventRequest as EventRequest,
GetAlertsCountOpts,
GetAlertsOpts,
GetServicesOpts,
GetStatusPagesOpts,
ILertApi,
EventRequest,
GetIncidentsCountOpts,
GetIncidentsOpts,
TableState,
} from './types';
+42 -34
View File
@@ -15,16 +15,17 @@
*/
import {
Alert,
AlertAction,
AlertResponder,
AlertSource,
Incident,
User,
IncidentStatus,
UptimeMonitor,
AlertStatus,
EscalationPolicy,
Schedule,
IncidentResponder,
IncidentAction,
OnCall,
Schedule,
Service,
StatusPage,
User,
} from '../types';
/** @public */
@@ -34,16 +35,28 @@ export type TableState = {
};
/** @public */
export type GetIncidentsOpts = {
export type GetAlertsOpts = {
maxResults?: number;
startIndex?: number;
states?: IncidentStatus[];
states?: AlertStatus[];
alertSources?: number[];
};
/** @public */
export type GetIncidentsCountOpts = {
states?: IncidentStatus[];
export type GetAlertsCountOpts = {
states?: AlertStatus[];
};
/** @public */
export type GetServicesOpts = {
maxResults?: number;
startIndex?: number;
};
/** @public */
export type GetStatusPagesOpts = {
maxResults?: number;
startIndex?: number;
};
/** @public */
@@ -57,27 +70,16 @@ export type EventRequest = {
/** @public */
export interface ILertApi {
fetchIncidents(opts?: GetIncidentsOpts): Promise<Incident[]>;
fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise<number>;
fetchIncident(id: number): Promise<Incident>;
fetchIncidentResponders(incident: Incident): Promise<IncidentResponder[]>;
fetchIncidentActions(incident: Incident): Promise<IncidentAction[]>;
acceptIncident(incident: Incident, userName: string): Promise<Incident>;
resolveIncident(incident: Incident, userName: string): Promise<Incident>;
assignIncident(
incident: Incident,
responder: IncidentResponder,
): Promise<Incident>;
createIncident(eventRequest: EventRequest): Promise<boolean>;
triggerIncidentAction(
incident: Incident,
action: IncidentAction,
): Promise<void>;
fetchUptimeMonitors(): Promise<UptimeMonitor[]>;
pauseUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise<UptimeMonitor>;
resumeUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise<UptimeMonitor>;
fetchUptimeMonitor(id: number): Promise<UptimeMonitor>;
fetchAlerts(opts?: GetAlertsOpts): Promise<Alert[]>;
fetchAlertsCount(opts?: GetAlertsCountOpts): Promise<number>;
fetchAlert(id: number): Promise<Alert>;
fetchAlertResponders(alert: Alert): Promise<AlertResponder[]>;
fetchAlertActions(alert: Alert): Promise<AlertAction[]>;
acceptAlert(alert: Alert, userName: string): Promise<Alert>;
resolveAlert(alert: Alert, userName: string): Promise<Alert>;
assignAlert(alert: Alert, responder: AlertResponder): Promise<Alert>;
createAlert(eventRequest: EventRequest): Promise<boolean>;
triggerAlertAction(alert: Alert, action: AlertAction): Promise<void>;
fetchAlertSources(): Promise<AlertSource[]>;
fetchAlertSource(idOrIntegrationKey: number | string): Promise<AlertSource>;
@@ -100,11 +102,17 @@ export interface ILertApi {
end: string,
): Promise<Schedule>;
getIncidentDetailsURL(incident: Incident): string;
fetchServices(opts?: GetServicesOpts): Promise<Service[]>;
fetchStatusPages(opts?: GetStatusPagesOpts): Promise<StatusPage[]>;
getAlertDetailsURL(alert: Alert): string;
getAlertSourceDetailsURL(alertSource: AlertSource | null): string;
getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string;
getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string;
getScheduleDetailsURL(schedule: Schedule): string;
getServiceDetailsURL(service: Service): string;
getStatusPageDetailsURL(statusPage: StatusPage): string;
getStatusPageURL(statusPage: StatusPage): string;
getUserPhoneNumber(user: User | null): string;
getUserInitials(user: User | null): string;
}
@@ -13,42 +13,42 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { IconButton, Menu, MenuItem, Typography } from '@material-ui/core';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import React from 'react';
import { ilertApiRef } from '../../api';
import { Incident, IncidentAction } from '../../types';
import { IncidentAssignModal } from './IncidentAssignModal';
import { useIncidentActions } from '../../hooks/useIncidentActions';
import { useAlertActions } from '../../hooks/useAlertActions';
import { Alert, AlertAction } from '../../types';
import { AlertAssignModal } from './AlertAssignModal';
import { DEFAULT_NAMESPACE, parseEntityRef } from '@backstage/catalog-model';
import { Link, Progress } from '@backstage/core-components';
import {
alertApiRef,
useApi,
identityApiRef,
useApi,
} from '@backstage/core-plugin-api';
import { Progress, Link } from '@backstage/core-components';
import { DEFAULT_NAMESPACE, parseEntityRef } from '@backstage/catalog-model';
export const IncidentActionsMenu = ({
incident,
onIncidentChanged,
export const AlertActionsMenu = ({
alert,
onAlertChanged,
setIsLoading,
}: {
incident: Incident;
onIncidentChanged?: (incident: Incident) => void;
alert: Alert;
onAlertChanged?: (alert: Alert) => void;
setIsLoading?: (isLoading: boolean) => void;
}) => {
const ilertApi = useApi(ilertApiRef);
const alertApi = useApi(alertApiRef);
const identityApi = useApi(identityApiRef);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const callback = onIncidentChanged || ((_: Incident): void => {});
const callback = onAlertChanged || ((_: Alert): void => {});
const setProcessing = setIsLoading || ((_: boolean): void => {});
const [isAssignIncidentModalOpened, setIsAssignIncidentModalOpened] =
const [isAssignAlertModalOpened, setIsAssignAlertModalOpened] =
React.useState(false);
const [{ incidentActions, isLoading }] = useIncidentActions(
incident,
const [{ alertActions, isLoading }] = useAlertActions(
alert,
Boolean(anchorEl),
);
@@ -70,10 +70,10 @@ export const IncidentActionsMenu = ({
defaultKind: 'User',
defaultNamespace: DEFAULT_NAMESPACE,
});
const newIncident = await ilertApi.acceptIncident(incident, userName);
alertApi.post({ message: 'Incident accepted.' });
const newAlert = await ilertApi.acceptAlert(alert, userName);
alertApi.post({ message: 'Alert accepted.' });
callback(newIncident);
callback(newAlert);
setProcessing(false);
} catch (err) {
setProcessing(false);
@@ -90,10 +90,10 @@ export const IncidentActionsMenu = ({
defaultKind: 'User',
defaultNamespace: DEFAULT_NAMESPACE,
});
const newIncident = await ilertApi.resolveIncident(incident, userName);
alertApi.post({ message: 'Incident resolved.' });
const newAlert = await ilertApi.resolveAlert(alert, userName);
alertApi.post({ message: 'Alert resolved.' });
callback(newIncident);
callback(newAlert);
setProcessing(false);
} catch (err) {
setProcessing(false);
@@ -103,15 +103,15 @@ export const IncidentActionsMenu = ({
const handleAssign = () => {
handleCloseMenu();
setIsAssignIncidentModalOpened(true);
setIsAssignAlertModalOpened(true);
};
const handleTriggerAction = (action: IncidentAction) => async () => {
const handleTriggerAction = (action: AlertAction) => async () => {
try {
handleCloseMenu();
setProcessing(true);
await ilertApi.triggerIncidentAction(incident, action);
alertApi.post({ message: 'Incident action triggered.' });
await ilertApi.triggerAlertAction(alert, action);
alertApi.post({ message: 'Alert action triggered.' });
setProcessing(false);
} catch (err) {
setProcessing(false);
@@ -119,7 +119,7 @@ export const IncidentActionsMenu = ({
}
};
const actions: React.ReactNode[] = incidentActions.map(a => {
const actions: React.ReactNode[] = alertActions.map(a => {
const successTrigger = a.history
? a.history.find(h => h.success)
: undefined;
@@ -152,7 +152,7 @@ export const IncidentActionsMenu = ({
<MoreVertIcon />
</IconButton>
<Menu
id={`incident-actions-menu-${incident.id}`}
id={`alert-actions-menu-${alert.id}`}
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
@@ -161,7 +161,7 @@ export const IncidentActionsMenu = ({
style: { maxHeight: 48 * 5.5 },
}}
>
{incident.status === 'PENDING' ? (
{alert.status === 'PENDING' ? (
<MenuItem key="ack" onClick={handleAccept}>
<Typography variant="inherit" noWrap>
Accept
@@ -169,7 +169,7 @@ export const IncidentActionsMenu = ({
</MenuItem>
) : null}
{incident.status !== 'RESOLVED' ? (
{alert.status !== 'RESOLVED' ? (
<MenuItem key="close" onClick={handleResolve}>
<Typography variant="inherit" noWrap>
Resolve
@@ -177,7 +177,7 @@ export const IncidentActionsMenu = ({
</MenuItem>
) : null}
{incident.status !== 'RESOLVED' ? (
{alert.status !== 'RESOLVED' ? (
<MenuItem key="assign" onClick={handleAssign}>
<Typography variant="inherit" noWrap>
Assign
@@ -195,17 +195,15 @@ export const IncidentActionsMenu = ({
<MenuItem key="details" onClick={handleCloseMenu}>
<Typography variant="inherit" noWrap>
<Link to={ilertApi.getIncidentDetailsURL(incident)}>
View in iLert
</Link>
<Link to={ilertApi.getAlertDetailsURL(alert)}>View in iLert</Link>
</Typography>
</MenuItem>
</Menu>
<IncidentAssignModal
incident={incident}
setIsModalOpened={setIsAssignIncidentModalOpened}
isModalOpened={isAssignIncidentModalOpened}
onIncidentChanged={onIncidentChanged}
<AlertAssignModal
alert={alert}
setIsModalOpened={setIsAssignAlertModalOpened}
isModalOpened={isAssignAlertModalOpened}
onAlertChanged={onAlertChanged}
/>
</>
);
@@ -13,21 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Alert from '@material-ui/lab/Alert';
import { alertApiRef, useApi } from '@backstage/core-plugin-api';
import { Typography } from '@material-ui/core';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import { makeStyles } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
import MUIAlert from '@material-ui/lab/Alert';
import Autocomplete from '@material-ui/lab/Autocomplete';
import { useAssignIncident } from '../../hooks/useAssignIncident';
import { Typography } from '@material-ui/core';
import React from 'react';
import { ilertApiRef } from '../../api';
import { Incident } from '../../types';
import { alertApiRef, useApi } from '@backstage/core-plugin-api';
import { useAssignAlert } from '../../hooks/useAssignAlert';
import { Alert } from '../../types';
const useStyles = makeStyles(() => ({
container: {
@@ -55,45 +55,42 @@ const useStyles = makeStyles(() => ({
},
}));
export const IncidentAssignModal = ({
incident,
export const AlertAssignModal = ({
alert,
isModalOpened,
setIsModalOpened,
onIncidentChanged,
onAlertChanged,
}: {
incident: Incident | null;
alert: Alert | null;
isModalOpened: boolean;
setIsModalOpened: (open: boolean) => void;
onIncidentChanged?: (incident: Incident) => void;
onAlertChanged?: (alert: Alert) => void;
}) => {
const [
{ incidentRespondersList, incidentResponder, isLoading },
{ setIsLoading, setIncidentResponder, setIncidentRespondersList },
] = useAssignIncident(incident, isModalOpened);
const callback = onIncidentChanged || ((_: Incident): void => {});
{ alertRespondersList, alertResponder, isLoading },
{ setIsLoading, setAlertResponder, setAlertRespondersList },
] = useAssignAlert(alert, isModalOpened);
const callback = onAlertChanged || ((_: Alert): void => {});
const ilertApi = useApi(ilertApiRef);
const alertApi = useApi(alertApiRef);
const classes = useStyles();
const handleClose = () => {
setIncidentRespondersList([]);
setAlertRespondersList([]);
setIsModalOpened(false);
};
const handleAssign = () => {
if (!incident || !incidentResponder) {
if (!alert || !alertResponder) {
return;
}
setIsLoading(true);
setIncidentRespondersList([]);
setAlertRespondersList([]);
setTimeout(async () => {
try {
const newIncident = await ilertApi.assignIncident(
incident,
incidentResponder,
);
callback(newIncident);
alertApi.post({ message: 'Incident assigned.' });
const newAlert = await ilertApi.assignAlert(alert, alertResponder);
callback(newAlert);
alertApi.post({ message: 'Alert assigned.' });
} catch (err) {
alertApi.post({ message: err, severity: 'error' });
}
@@ -102,33 +99,33 @@ export const IncidentAssignModal = ({
}, 250);
};
const canAssign = !!incidentResponder;
const canAssign = !!alertResponder;
return (
<Dialog
open={isModalOpened}
onClose={handleClose}
aria-labelledby="assign-incident-form-title"
aria-labelledby="assign-alert-form-title"
>
<DialogTitle id="assign-incident-form-title">
<DialogTitle id="assign-alert-form-title">
Select responder to assign
</DialogTitle>
<DialogContent>
<Alert severity="info">
<MUIAlert severity="info">
<Typography variant="body1" gutterBottom align="justify">
This action will assign the incident to the selected responder.
This action will assign the alert to the selected responder.
</Typography>
</Alert>
</MUIAlert>
<Autocomplete
disabled={isLoading}
options={incidentRespondersList}
value={incidentResponder}
options={alertRespondersList}
value={alertResponder}
classes={{
root: classes.formControl,
option: classes.option,
}}
onChange={(_event: any, newValue: any) => {
setIncidentResponder(newValue);
setAlertResponder(newValue);
}}
autoHighlight
groupBy={option => {
@@ -0,0 +1,31 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { ilertApiRef } from '../../api';
import { Alert } from '../../types';
import { Link } from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
export const AlertLink = ({ alert }: { alert: Alert | null }) => {
const ilertApi = useApi(ilertApiRef);
if (!alert) {
return null;
}
return <Link to={ilertApi.getAlertDetailsURL(alert)}>#{alert.id}</Link>;
};
@@ -13,27 +13,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { DEFAULT_NAMESPACE, parseEntityRef } from '@backstage/catalog-model';
import { makeStyles } from '@material-ui/core/styles';
import Alert from '@material-ui/lab/Alert';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import Autocomplete from '@material-ui/lab/Autocomplete';
import { useNewIncident } from '../../hooks/useNewIncident';
import { Typography } from '@material-ui/core';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import { ilertApiRef } from '../../api';
import { AlertSource } from '../../types';
import {
alertApiRef,
identityApiRef,
useApi,
} from '@backstage/core-plugin-api';
import { Typography } from '@material-ui/core';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import { makeStyles } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import Alert from '@material-ui/lab/Alert';
import Autocomplete from '@material-ui/lab/Autocomplete';
import React from 'react';
import { ilertApiRef } from '../../api';
import { useNewAlert } from '../../hooks/useNewAlert';
import { AlertSource } from '../../types';
const useStyles = makeStyles(() => ({
container: {
@@ -61,23 +61,23 @@ const useStyles = makeStyles(() => ({
},
}));
export const IncidentNewModal = ({
export const AlertNewModal = ({
isModalOpened,
setIsModalOpened,
refetchIncidents,
refetchAlerts,
initialAlertSource,
entityName,
}: {
isModalOpened: boolean;
setIsModalOpened: (open: boolean) => void;
refetchIncidents: () => void;
refetchAlerts: () => void;
initialAlertSource?: AlertSource | null;
entityName?: string;
}) => {
const [
{ alertSources, alertSource, summary, details, isLoading },
{ setAlertSource, setSummary, setDetails, setIsLoading },
] = useNewIncident(isModalOpened, initialAlertSource);
] = useNewAlert(isModalOpened, initialAlertSource);
const ilertApi = useApi(ilertApiRef);
const alertApi = useApi(alertApiRef);
const identityApi = useApi(identityApiRef);
@@ -107,15 +107,15 @@ export const IncidentNewModal = ({
defaultKind: 'User',
defaultNamespace: DEFAULT_NAMESPACE,
});
await ilertApi.createIncident({
await ilertApi.createAlert({
integrationKey,
summary,
details,
userName,
source,
});
alertApi.post({ message: 'Incident created.' });
refetchIncidents();
alertApi.post({ message: 'Alert created.' });
refetchAlerts();
} catch (err) {
alertApi.post({ message: err, severity: 'error' });
}
@@ -129,16 +129,16 @@ export const IncidentNewModal = ({
<Dialog
open={isModalOpened}
onClose={handleClose}
aria-labelledby="create-incident-form-title"
aria-labelledby="create-alert-form-title"
>
<DialogTitle id="create-incident-form-title">
<DialogTitle id="create-alert-form-title">
{entityName ? (
<div>
This action will trigger an incident for{' '}
This action will trigger an alert for{' '}
<strong>"{entityName}"</strong>.
</div>
) : (
'New incident'
'New alert'
)}
</DialogTitle>
<DialogContent>
@@ -13,4 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './UptimeMonitorActionsMenu';
export * from './AlertActionsMenu';
@@ -13,42 +13,41 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { AuthenticationError } from '@backstage/errors';
import Button from '@material-ui/core/Button';
import AddIcon from '@material-ui/icons/Add';
import { IncidentsTable } from './IncidentsTable';
import { MissingAuthorizationHeaderError } from '../Errors';
import { useIncidents } from '../../hooks/useIncidents';
import { IncidentNewModal } from '../Incident/IncidentNewModal';
import {
Content,
ContentHeader,
SupportButton,
ResponseErrorPanel,
SupportButton,
} from '@backstage/core-components';
import Button from '@material-ui/core/Button';
import AddIcon from '@material-ui/icons/Add';
import React from 'react';
import { useAlerts } from '../../hooks/useAlerts';
import { AlertNewModal } from '../Alert/AlertNewModal';
import { MissingAuthorizationHeaderError } from '../Errors';
import { AlertsTable } from './AlertsTable';
export const IncidentsPage = () => {
export const AlertsPage = () => {
const [
{ tableState, states, incidents, incidentsCount, isLoading, error },
{ tableState, states, alerts, alertsCount, isLoading, error },
{
onIncidentStatesChange,
onAlertStatesChange,
onChangePage,
onChangeRowsPerPage,
onIncidentChanged,
refetchIncidents,
onAlertChanged,
refetchAlerts,
setIsLoading,
},
] = useIncidents(true);
] = useAlerts(true);
const [isModalOpened, setIsModalOpened] = React.useState(false);
const handleCreateNewIncidentClick = () => {
const handleCreateNewAlertClick = () => {
setIsModalOpened(true);
};
if (error) {
if (error instanceof AuthenticationError) {
if (error.name === 'AuthenticationError') {
return (
<Content>
<MissingAuthorizationHeaderError />
@@ -65,32 +64,32 @@ export const IncidentsPage = () => {
return (
<Content>
<ContentHeader title="Incidents">
<ContentHeader title="Alerts">
<Button
variant="contained"
color="primary"
size="small"
startIcon={<AddIcon />}
onClick={handleCreateNewIncidentClick}
onClick={handleCreateNewAlertClick}
>
Create Incident
Create Alert
</Button>
<IncidentNewModal
<AlertNewModal
isModalOpened={isModalOpened}
setIsModalOpened={setIsModalOpened}
refetchIncidents={refetchIncidents}
refetchAlerts={refetchAlerts}
/>
<SupportButton>
This helps you to bring iLert into your developer portal.
</SupportButton>
</ContentHeader>
<IncidentsTable
incidents={incidents}
incidentsCount={incidentsCount}
<AlertsTable
alerts={alerts}
alertsCount={alertsCount}
tableState={tableState}
states={states}
onIncidentChanged={onIncidentChanged}
onIncidentStatesChange={onIncidentStatesChange}
onAlertChanged={onAlertChanged}
onAlertStatesChange={onAlertStatesChange}
onChangePage={onChangePage}
onChangeRowsPerPage={onChangeRowsPerPage}
isLoading={isLoading}
@@ -13,18 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import { ilertApiRef, TableState } from '../../api';
import { Incident, IncidentStatus } from '../../types';
import { StatusChip } from './StatusChip';
import { AlertSourceLink } from '../AlertSource/AlertSourceLink';
import { TableTitle } from './TableTitle';
import Typography from '@material-ui/core/Typography';
import { DateTime as dt, Interval } from 'luxon';
import humanizeDuration from 'humanize-duration';
import { IncidentActionsMenu } from '../Incident/IncidentActionsMenu';
import { IncidentLink } from '../Incident/IncidentLink';
import { DateTime as dt, Interval } from 'luxon';
import React from 'react';
import { ilertApiRef, TableState } from '../../api';
import { Alert, AlertStatus } from '../../types';
import { AlertActionsMenu } from '../Alert/AlertActionsMenu';
import { AlertLink } from '../Alert/AlertLink';
import { AlertSourceLink } from '../AlertSource/AlertSourceLink';
import { StatusChip } from './StatusChip';
import { TableTitle } from './TableTitle';
import { Table, TableColumn } from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
@@ -37,27 +37,27 @@ const useStyles = makeStyles(theme => ({
},
}));
export const IncidentsTable = ({
incidents,
incidentsCount,
export const AlertsTable = ({
alerts,
alertsCount,
tableState,
states,
isLoading,
onIncidentChanged,
onAlertChanged,
setIsLoading,
onIncidentStatesChange,
onAlertStatesChange,
onChangePage,
onChangeRowsPerPage,
compact,
}: {
incidents: Incident[];
incidentsCount: number;
alerts: Alert[];
alertsCount: number;
tableState: TableState;
states: IncidentStatus[];
states: AlertStatus[];
isLoading: boolean;
onIncidentChanged: (incident: Incident) => void;
onAlertChanged: (alert: Alert) => void;
setIsLoading: (isLoading: boolean) => void;
onIncidentStatesChange: (states: IncidentStatus[]) => void;
onAlertStatesChange: (states: AlertStatus[]) => void;
onChangePage: (page: number) => void;
onChangeRowsPerPage: (pageSize: number) => void;
compact?: boolean;
@@ -86,31 +86,29 @@ export const IncidentsTable = ({
maxWidth: '30%',
};
const idColumn: TableColumn = {
const idColumn: TableColumn<Alert> = {
title: 'ID',
field: 'id',
highlight: true,
cellStyle: smColumnStyle,
headerStyle: smColumnStyle,
render: rowData => <IncidentLink incident={rowData as Incident} />,
render: rowData => <AlertLink alert={rowData} />,
};
const summaryColumn: TableColumn = {
const summaryColumn: TableColumn<Alert> = {
title: 'Summary',
field: 'summary',
cellStyle: !compact ? xlColumnStyle : undefined,
headerStyle: !compact ? xlColumnStyle : undefined,
render: rowData => <Typography>{(rowData as Incident).summary}</Typography>,
render: rowData => <Typography>{rowData.summary}</Typography>,
};
const sourceColumn: TableColumn = {
const sourceColumn: TableColumn<Alert> = {
title: 'Source',
field: 'source',
cellStyle: mdColumnStyle,
headerStyle: mdColumnStyle,
render: rowData => (
<AlertSourceLink alertSource={(rowData as Incident).alertSource} />
),
render: rowData => <AlertSourceLink alertSource={rowData.alertSource} />,
};
const durationColumn: TableColumn = {
const durationColumn: TableColumn<Alert> = {
title: 'Duration',
field: 'reportTime',
type: 'datetime',
@@ -118,20 +116,17 @@ export const IncidentsTable = ({
headerStyle: smColumnStyle,
render: rowData => (
<Typography noWrap>
{(rowData as Incident).status !== 'RESOLVED'
{rowData.status !== 'RESOLVED'
? humanizeDuration(
Interval.fromDateTimes(
dt.fromISO((rowData as Incident).reportTime),
dt.now(),
)
Interval.fromDateTimes(dt.fromISO(rowData.reportTime), dt.now())
.toDuration()
.valueOf(),
{ units: ['h', 'm', 's'], largest: 2, round: true },
)
: humanizeDuration(
Interval.fromDateTimes(
dt.fromISO((rowData as Incident).reportTime),
dt.fromISO((rowData as Incident).resolvedOn),
dt.fromISO(rowData.reportTime),
dt.fromISO(rowData.resolvedOn),
)
.toDuration()
.valueOf(),
@@ -140,54 +135,59 @@ export const IncidentsTable = ({
</Typography>
),
};
const assignedToColumn: TableColumn = {
title: 'Assigned to',
field: 'assignedTo',
const respondersColumn: TableColumn<Alert> = {
title: 'Responders',
field: 'responders',
cellStyle: !compact ? mdColumnStyle : lgColumnStyle,
headerStyle: !compact ? mdColumnStyle : lgColumnStyle,
render: rowData => (
<Typography noWrap>
{ilertApi.getUserInitials((rowData as Incident).assignedTo)}
<Typography>
{rowData.responders.map((value, i, arr) => {
return (
ilertApi.getUserInitials(value.user) +
(arr.length - 1 !== i ? ', ' : '')
);
})}
</Typography>
),
};
const priorityColumn: TableColumn = {
const priorityColumn: TableColumn<Alert> = {
title: 'Priority',
field: 'priority',
cellStyle: smColumnStyle,
headerStyle: smColumnStyle,
render: rowData => (
<Typography noWrap>
{(rowData as Incident).priority === 'HIGH' ? 'High' : 'Low'}
{rowData.priority === 'HIGH' ? 'High' : 'Low'}
</Typography>
),
};
const statusColumn: TableColumn = {
const statusColumn: TableColumn<Alert> = {
title: 'Status',
field: 'status',
cellStyle: xsColumnStyle,
headerStyle: xsColumnStyle,
render: rowData => <StatusChip incident={rowData as Incident} />,
render: rowData => <StatusChip alert={rowData} />,
};
const actionsColumn: TableColumn = {
const actionsColumn: TableColumn<Alert> = {
title: '',
field: '',
cellStyle: xsColumnStyle,
headerStyle: xsColumnStyle,
render: rowData => (
<IncidentActionsMenu
incident={rowData as Incident}
onIncidentChanged={onIncidentChanged}
<AlertActionsMenu
alert={rowData}
onAlertChanged={onAlertChanged}
setIsLoading={setIsLoading}
/>
),
};
const columns: TableColumn[] = compact
const columns: TableColumn<Alert>[] = compact
? [
summaryColumn,
durationColumn,
assignedToColumn,
respondersColumn,
statusColumn,
actionsColumn,
]
@@ -196,31 +196,14 @@ export const IncidentsTable = ({
summaryColumn,
sourceColumn,
durationColumn,
assignedToColumn,
respondersColumn,
priorityColumn,
statusColumn,
actionsColumn,
];
let tableStyle: React.CSSProperties = {};
if (compact) {
tableStyle = {
width: '100%',
maxWidth: '100%',
minWidth: '0',
height: 'calc(100% - 10px)',
boxShadow: 'none !important',
borderRadius: 'none !important',
};
} else {
tableStyle = {
width: '100%',
maxWidth: '100%',
};
}
return (
<Table
style={tableStyle}
options={{
sorting: false,
search: !compact,
@@ -236,28 +219,27 @@ export const IncidentsTable = ({
}}
emptyContent={
<Typography color="textSecondary" className={classes.empty}>
No incidents right now
No alerts right now
</Typography>
}
title={
!compact ? (
<TableTitle
incidentStates={states}
onIncidentStatesChange={onIncidentStatesChange}
alertStates={states}
onAlertStatesChange={onAlertStatesChange}
/>
) : (
<Typography variant="button" color="textSecondary">
INCIDENTS
ALERTS
</Typography>
)
}
page={tableState.page}
totalCount={incidentsCount}
totalCount={alertsCount}
onPageChange={onChangePage}
onRowsPerPageChange={onChangeRowsPerPage}
// localization={{ header: { actions: undefined } }}
columns={columns}
data={incidents}
data={alerts}
isLoading={isLoading}
/>
);
@@ -13,10 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Chip, withStyles } from '@material-ui/core';
import { Incident, PENDING, ACCEPTED, RESOLVED } from '../../types';
import { incidentStatusLabels } from '../Incident/IncidentStatus';
import React from 'react';
import { ACCEPTED, Alert, PENDING, RESOLVED } from '../../types';
const ResolvedChip = withStyles({
root: {
@@ -41,10 +40,16 @@ const PendingChip = withStyles({
},
})(Chip);
export const StatusChip = ({ incident }: { incident: Incident }) => {
const label = `${incidentStatusLabels[incident.status]}`;
export const alertStatusLabels = {
[RESOLVED]: 'Resolved',
[ACCEPTED]: 'Accepted',
[PENDING]: 'Pending',
} as Record<string, string>;
switch (incident.status) {
export const StatusChip = ({ alert }: { alert: Alert }) => {
const label = `${alertStatusLabels[alert.status]}`;
switch (alert.status) {
case RESOLVED:
return <ResolvedChip label={label} size="small" />;
case ACCEPTED:
@@ -13,16 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { PENDING, ACCEPTED, RESOLVED, IncidentStatus } from '../../types';
import { incidentStatusLabels } from '../Incident/IncidentStatus';
import Checkbox from '@material-ui/core/Checkbox';
import FormControl from '@material-ui/core/FormControl';
import ListItemText from '@material-ui/core/ListItemText';
import Select from '@material-ui/core/Select';
import Typography from '@material-ui/core/Typography';
import MenuItem from '@material-ui/core/MenuItem';
import Checkbox from '@material-ui/core/Checkbox';
import Select from '@material-ui/core/Select';
import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import React from 'react';
import { ACCEPTED, AlertStatus, PENDING, RESOLVED } from '../../types';
import { alertStatusLabels } from './StatusChip';
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
@@ -53,15 +53,15 @@ const useStyles = makeStyles({
});
export const TableTitle = ({
incidentStates,
onIncidentStatesChange,
alertStates,
onAlertStatesChange,
}: {
incidentStates: IncidentStatus[];
onIncidentStatesChange: (states: IncidentStatus[]) => void;
alertStates: AlertStatus[];
onAlertStatesChange: (states: AlertStatus[]) => void;
}) => {
const classes = useStyles();
const handleIncidentStatusSelectChange = (event: any) => {
onIncidentStatesChange(event.target.value);
const handleAlertStatusSelectChange = (event: any) => {
onAlertStatesChange(event.target.value);
};
return (
@@ -75,19 +75,19 @@ export const TableTitle = ({
size="small"
>
<Select
id="incidents-status-select"
id="alerts-status-select"
multiple
value={incidentStates}
onChange={handleIncidentStatusSelectChange}
value={alertStates}
onChange={handleAlertStatusSelectChange}
renderValue={(selected: any) => selected.join(', ')}
MenuProps={MenuProps}
>
{[PENDING, ACCEPTED, RESOLVED].map(state => (
<MenuItem key={state} value={state}>
<Checkbox
checked={incidentStates.indexOf(state as IncidentStatus) > -1}
checked={alertStates.indexOf(state as AlertStatus) > -1}
/>
<ListItemText primary={incidentStatusLabels[state]} />
<ListItemText primary={alertStatusLabels[state]} />
</MenuItem>
))}
</Select>
@@ -13,5 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './IncidentsPage';
export * from './IncidentsTable';
export * from './AlertsPage';
export * from './AlertsTable';
@@ -13,27 +13,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Entity } from '@backstage/catalog-model';
import { ResponseErrorPanel } from '@backstage/core-components';
import { AuthenticationError } from '@backstage/errors';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import CardHeader from '@material-ui/core/CardHeader';
import Divider from '@material-ui/core/Divider';
import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
import { ILERT_INTEGRATION_KEY_ANNOTATION } from '../../constants';
import { MissingAuthorizationHeaderError } from '../Errors';
import { useIncidents } from '../../hooks/useIncidents';
import { IncidentsTable } from '../IncidentsPage';
import { IncidentNewModal } from '../Incident/IncidentNewModal';
import { ILertCardActionsHeader } from './ILertCardActionsHeader';
import { useAlertSource } from '../../hooks/useAlertSource';
import { useILertEntity } from '../../hooks';
import { useAlerts } from '../../hooks/useAlerts';
import { useAlertSource } from '../../hooks/useAlertSource';
import { AlertNewModal } from '../Alert/AlertNewModal';
import { AlertsTable } from '../AlertsPage';
import { MissingAuthorizationHeaderError } from '../Errors';
import { ILertCardActionsHeader } from './ILertCardActionsHeader';
import { ILertCardEmptyState } from './ILertCardEmptyState';
import { ILertCardHeaderStatus } from './ILertCardHeaderStatus';
import { ILertCardMaintenanceModal } from './ILertCardMaintenanceModal';
import { ILertCardEmptyState } from './ILertCardEmptyState';
import { ILertCardOnCall } from './ILertCardOnCall';
import { ResponseErrorPanel } from '@backstage/core-components';
/** @public */
export const isPluginApplicableToEntity = (entity: Entity) =>
@@ -55,23 +55,21 @@ const useStyles = makeStyles({
export const ILertCard = () => {
const classes = useStyles();
const { integrationKey, name } = useILertEntity();
const [{ alertSource }, { setAlertSource, refetchAlertSource }] =
useAlertSource(integrationKey);
const [
{ alertSource, uptimeMonitor },
{ setAlertSource, refetchAlertSource },
] = useAlertSource(integrationKey);
const [
{ tableState, states, incidents, incidentsCount, isLoading, error },
{ tableState, states, alerts, alertsCount, isLoading, error },
{
onIncidentStatesChange,
onAlertStatesChange,
onChangePage,
onChangeRowsPerPage,
onIncidentChanged,
refetchIncidents,
onAlertChanged,
refetchAlerts,
setIsLoading,
},
] = useIncidents(false, true, alertSource);
] = useAlerts(false, true, alertSource);
const [isNewIncidentModalOpened, setIsNewIncidentModalOpened] =
const [isNewAlertModalOpened, setIsNewAlertModalOpened] =
React.useState(false);
const [isMaintenanceModalOpened, setIsMaintenanceModalOpened] =
React.useState(false);
@@ -97,9 +95,8 @@ export const ILertCard = () => {
<ILertCardActionsHeader
alertSource={alertSource}
setAlertSource={setAlertSource}
setIsNewIncidentModalOpened={setIsNewIncidentModalOpened}
setIsNewAlertModalOpened={setIsNewAlertModalOpened}
setIsMaintenanceModalOpened={setIsMaintenanceModalOpened}
uptimeMonitor={uptimeMonitor}
/>
}
action={<ILertCardHeaderStatus alertSource={alertSource} />}
@@ -107,13 +104,13 @@ export const ILertCard = () => {
<Divider />
<CardContent className={classes.content}>
<ILertCardOnCall alertSource={alertSource} />
<IncidentsTable
incidents={incidents}
incidentsCount={incidentsCount}
<AlertsTable
alerts={alerts}
alertsCount={alertsCount}
tableState={tableState}
states={states}
onIncidentChanged={onIncidentChanged}
onIncidentStatesChange={onIncidentStatesChange}
onAlertChanged={onAlertChanged}
onAlertStatesChange={onAlertStatesChange}
onChangePage={onChangePage}
onChangeRowsPerPage={onChangeRowsPerPage}
isLoading={isLoading}
@@ -122,10 +119,10 @@ export const ILertCard = () => {
/>
</CardContent>
</Card>
<IncidentNewModal
isModalOpened={isNewIncidentModalOpened}
setIsModalOpened={setIsNewIncidentModalOpened}
refetchIncidents={refetchIncidents}
<AlertNewModal
isModalOpened={isNewAlertModalOpened}
setIsModalOpened={setIsNewAlertModalOpened}
refetchAlerts={refetchAlerts}
initialAlertSource={alertSource}
entityName={name}
/>
@@ -13,49 +13,46 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import Alert from '@material-ui/lab/Alert';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import Typography from '@material-ui/core/Typography';
import AlarmAddIcon from '@material-ui/icons/AlarmAdd';
import BuildIcon from '@material-ui/icons/Build';
import PauseIcon from '@material-ui/icons/Pause';
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
import TimelineIcon from '@material-ui/icons/Timeline';
import WebIcon from '@material-ui/icons/Web';
import Typography from '@material-ui/core/Typography';
import Alert from '@material-ui/lab/Alert';
import React from 'react';
import { ilertApiRef } from '../../api';
import { AlertSource, UptimeMonitor } from '../../types';
import { AlertSource } from '../../types';
import {
HeaderIconLinkRow,
IconLinkVerticalProps,
} from '@backstage/core-components';
import { useApi, alertApiRef } from '@backstage/core-plugin-api';
import { alertApiRef, useApi } from '@backstage/core-plugin-api';
export const ILertCardActionsHeader = ({
alertSource,
setAlertSource,
setIsNewIncidentModalOpened,
setIsNewAlertModalOpened,
setIsMaintenanceModalOpened,
uptimeMonitor,
}: {
alertSource: AlertSource | null;
setAlertSource: (alertSource: AlertSource) => void;
setIsNewIncidentModalOpened: (isOpen: boolean) => void;
setIsNewAlertModalOpened: (isOpen: boolean) => void;
setIsMaintenanceModalOpened: (isOpen: boolean) => void;
uptimeMonitor: UptimeMonitor | null;
}) => {
const ilertApi = useApi(ilertApiRef);
const alertApi = useApi(alertApiRef);
const [isLoading, setIsLoading] = React.useState(false);
const [isDisableModalOpened, setIsDisableModalOpened] = React.useState(false);
const handleCreateNewIncident = () => {
setIsNewIncidentModalOpened(true);
const handleCreateNewAlert = () => {
setIsNewAlertModalOpened(true);
};
const handleEnableAlertSource = async () => {
@@ -108,9 +105,9 @@ export const ILertCardActionsHeader = ({
icon: <WebIcon />,
};
const createIncidentLink: IconLinkVerticalProps = {
label: 'Create Incident',
onClick: handleCreateNewIncident,
const createAlertLink: IconLinkVerticalProps = {
label: 'Create Alert',
onClick: handleCreateNewAlert,
icon: <AlarmAddIcon />,
color: 'secondary',
disabled:
@@ -140,25 +137,14 @@ export const ILertCardActionsHeader = ({
disabled: !alertSource || isLoading,
};
const uptimeMonitorReportLink: IconLinkVerticalProps = {
label: 'Uptime Report',
href: uptimeMonitor ? uptimeMonitor.shareUrl : '',
icon: <TimelineIcon />,
disabled: !alertSource || !uptimeMonitor || isLoading,
};
const links: IconLinkVerticalProps[] = [
alertSourceLink,
createIncidentLink,
createAlertLink,
alertSource && alertSource.active
? disableAlertSourceLink
: enableAlertSourceLink,
];
if (alertSource && alertSource.integrationType === 'MONITOR') {
links.push(uptimeMonitorReportLink);
}
if (alertSource && alertSource.status !== 'IN_MAINTENANCE') {
links.push(maintenanceAlertSourceLink);
}
@@ -178,7 +164,7 @@ export const ILertCardActionsHeader = ({
<Alert severity="info">
<Typography variant="body1" align="justify">
Do you really want to disable this alert source? A disabled alert
source cannot create new incidents.
source cannot create new alerts.
</Typography>
</Alert>
</DialogContent>
@@ -13,34 +13,38 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { IncidentsPage } from '../IncidentsPage';
import { UptimeMonitorsPage } from '../UptimeMonitorsPage';
import { OnCallSchedulesPage } from '../OnCallSchedulesPage';
import {
Page,
Header,
HeaderTabs,
HeaderLabel,
Content,
Header,
HeaderLabel,
HeaderTabs,
Page,
} from '@backstage/core-components';
import React from 'react';
import { AlertsPage } from '../AlertsPage';
import { OnCallSchedulesPage } from '../OnCallSchedulesPage';
import { ServicesPage } from '../ServicesPage';
import { StatusPagesPage } from '../StatusPagePage';
/** @public */
export const ILertPage = () => {
const [selectedTab, setSelectedTab] = React.useState<number>(0);
const tabs = [
{ label: 'Who is on call?' },
{ label: 'Incidents' },
{ label: 'Uptime Monitors' },
{ label: 'Alerts' },
{ label: 'Services' },
{ label: 'Status pages' },
];
const renderTab = () => {
switch (selectedTab) {
case 0:
return <OnCallSchedulesPage />;
case 1:
return <IncidentsPage />;
return <AlertsPage />;
case 2:
return <UptimeMonitorsPage />;
return <ServicesPage />;
case 3:
return <StatusPagesPage />;
default:
return null;
}
@@ -1,48 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Tooltip from '@material-ui/core/Tooltip';
import { ACCEPTED, Incident, PENDING, RESOLVED } from '../../types';
import { StatusError, StatusOK } from '@backstage/core-components';
const useStyles = makeStyles({
denseListIcon: {
marginRight: 0,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
},
});
export const incidentStatusLabels = {
[RESOLVED]: 'Resolved',
[ACCEPTED]: 'Accepted',
[PENDING]: 'Pending',
} as Record<string, string>;
export const IncidentStatus = ({ incident }: { incident: Incident }) => {
const classes = useStyles();
return (
<Tooltip title={incident.status} placement="top">
<div className={classes.denseListIcon}>
{incident.status === 'PENDING' ? <StatusError /> : <StatusOK />}
</div>
</Tooltip>
);
};
@@ -13,14 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { useApi } from '@backstage/core-plugin-api';
import Button from '@material-ui/core/Button';
import Grid from '@material-ui/core/Grid';
import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import RepeatIcon from '@material-ui/icons/Repeat';
import { Shift } from '../../types';
import { DateTime as dt } from 'luxon';
import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
import { ilertApiRef } from '../../api';
import { Shift } from '../../types';
import { ShiftOverrideModal } from '../Shift/ShiftOverrideModal';
const useStyles = makeStyles({
@@ -48,6 +50,7 @@ export const OnCallShiftItem = ({
refetchOnCallSchedules: () => void;
}) => {
const classes = useStyles();
const ilertApi = useApi(ilertApiRef);
const [isModalOpened, setIsModalOpened] = React.useState(false);
const handleOverride = () => {
@@ -71,7 +74,7 @@ export const OnCallShiftItem = ({
{shift && shift.user ? (
<Grid item sm={12}>
<Typography variant="subtitle1" noWrap>
{`${shift.user.firstName} ${shift.user.lastName} (${shift.user.username})`}
{ilertApi.getUserInitials(shift.user)}
</Typography>
</Grid>
) : null}
@@ -0,0 +1,68 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { IconButton, Menu, MenuItem, Typography } from '@material-ui/core';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import React from 'react';
import { ilertApiRef } from '../../api';
import { Service } from '../../types';
import { Link } from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
export const ServiceActionsMenu = ({ service }: { service: Service }) => {
const ilertApi = useApi(ilertApiRef);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleCloseMenu = () => {
setAnchorEl(null);
};
return (
<>
<IconButton
aria-label="more"
aria-controls="long-menu"
aria-haspopup="true"
onClick={handleClick}
size="small"
>
<MoreVertIcon />
</IconButton>
<Menu
id={`service-actions-menu-${service.id}`}
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleCloseMenu}
PaperProps={{
style: { maxHeight: 48 * 5.5 },
}}
>
<MenuItem key="details" onClick={handleCloseMenu}>
<Typography variant="inherit" noWrap>
<Link to={ilertApi.getServiceDetailsURL(service)}>
View in iLert
</Link>
</Typography>
</MenuItem>
</Menu>
</>
);
};
@@ -0,0 +1,31 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { ilertApiRef } from '../../api';
import { Service } from '../../types';
import { Link } from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
export const ServiceLink = ({ service }: { service: Service | null }) => {
const ilertApi = useApi(ilertApiRef);
if (!service) {
return null;
}
return <Link to={ilertApi.getServiceDetailsURL(service)}>#{service.id}</Link>;
};
@@ -13,5 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './IncidentActionsMenu';
export * from './IncidentStatus';
export * from './ServiceActionsMenu';
@@ -13,26 +13,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { AuthenticationError } from '@backstage/errors';
import { UptimeMonitorsTable } from './UptimeMonitorsTable';
import { MissingAuthorizationHeaderError } from '../Errors';
import { useUptimeMonitors } from '../../hooks/useUptimeMonitors';
import {
Content,
ContentHeader,
SupportButton,
ResponseErrorPanel,
SupportButton,
} from '@backstage/core-components';
import React from 'react';
import { useServices } from '../../hooks/useServices';
import { MissingAuthorizationHeaderError } from '../Errors';
import { ServicesTable } from './ServicesTable';
export const UptimeMonitorsPage = () => {
export const ServicesPage = () => {
const [
{ tableState, uptimeMonitors, isLoading, error },
{ onChangePage, onChangeRowsPerPage, onUptimeMonitorChanged },
] = useUptimeMonitors();
{ tableState, services, isLoading, error },
{ onChangePage, onChangeRowsPerPage, setIsLoading },
] = useServices(true);
if (error) {
if (error instanceof AuthenticationError) {
if (error.name === 'AuthenticationError') {
return (
<Content>
<MissingAuthorizationHeaderError />
@@ -49,18 +48,18 @@ export const UptimeMonitorsPage = () => {
return (
<Content>
<ContentHeader title="Uptime Monitors">
<ContentHeader title="Services">
<SupportButton>
This helps you to bring iLert into your developer portal.
</SupportButton>
</ContentHeader>
<UptimeMonitorsTable
uptimeMonitors={uptimeMonitors}
<ServicesTable
services={services}
tableState={tableState}
isLoading={isLoading}
onChangePage={onChangePage}
onChangeRowsPerPage={onChangeRowsPerPage}
onUptimeMonitorChanged={onUptimeMonitorChanged}
isLoading={isLoading}
setIsLoading={setIsLoading}
/>
</Content>
);
@@ -0,0 +1,137 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import React from 'react';
import { TableState } from '../../api';
import { Service } from '../../types';
import { StatusChip } from './StatusChip';
import { Table, TableColumn } from '@backstage/core-components';
import { ServiceActionsMenu } from '../Service/ServiceActionsMenu';
import { ServiceLink } from '../Service/ServiceLink';
const useStyles = makeStyles(theme => ({
empty: {
padding: theme.spacing(2),
display: 'flex',
justifyContent: 'center',
},
}));
export const ServicesTable = ({
services,
tableState,
isLoading,
onChangePage,
onChangeRowsPerPage,
compact,
}: {
services: Service[];
tableState: TableState;
isLoading: boolean;
setIsLoading: (isLoading: boolean) => void;
onChangePage: (page: number) => void;
onChangeRowsPerPage: (pageSize: number) => void;
compact?: boolean;
}) => {
const classes = useStyles();
const smColumnStyle = {
width: '10%',
maxWidth: '10%',
};
const xlColumnStyle = {
width: '30%',
maxWidth: '30%',
};
const idColumn: TableColumn<Service> = {
title: 'ID',
field: 'id',
highlight: true,
cellStyle: smColumnStyle,
headerStyle: smColumnStyle,
render: rowData => <ServiceLink service={rowData} />,
};
const nameColumn: TableColumn<Service> = {
title: 'Name',
field: 'name',
cellStyle: !compact ? xlColumnStyle : undefined,
headerStyle: !compact ? xlColumnStyle : undefined,
render: rowData => <Typography>{rowData.name}</Typography>,
};
const statusColumn: TableColumn<Service> = {
title: 'Status',
field: 'status',
cellStyle: smColumnStyle,
headerStyle: smColumnStyle,
render: rowData => <StatusChip service={rowData} />,
};
const uptimeColumn: TableColumn<Service> = {
title: 'Uptime in the last 90 days',
field: 'uptimePercentage',
cellStyle: smColumnStyle,
headerStyle: smColumnStyle,
render: rowData => (
<Typography>{rowData.uptime.uptimePercentage.p90}</Typography>
),
};
const actionsColumn: TableColumn<Service> = {
title: '',
field: '',
cellStyle: smColumnStyle,
headerStyle: smColumnStyle,
render: rowData => <ServiceActionsMenu service={rowData} />,
};
const columns: TableColumn<Service>[] = compact
? [nameColumn, statusColumn, uptimeColumn, actionsColumn]
: [idColumn, nameColumn, statusColumn, uptimeColumn, actionsColumn];
return (
<Table
options={{
sorting: false,
search: !compact,
paging: !compact,
actionsColumnIndex: -1,
pageSize: tableState.pageSize,
pageSizeOptions: !compact ? [10, 20, 50, 100] : [3, 10, 20, 50, 100],
padding: 'dense',
loadingType: 'overlay',
showEmptyDataSourceMessage: !isLoading,
showTitle: true,
toolbar: true,
}}
emptyContent={
<Typography color="textSecondary" className={classes.empty}>
No services
</Typography>
}
title={
<Typography variant="button" color="textSecondary">
SERVICES
</Typography>
}
page={tableState.page}
onPageChange={onChangePage}
onRowsPerPageChange={onChangeRowsPerPage}
columns={columns}
data={services}
isLoading={isLoading}
/>
);
};
@@ -0,0 +1,89 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Chip, withStyles } from '@material-ui/core';
import React from 'react';
import {
DEGRADED,
MAJOR_OUTAGE,
OPERATIONAL,
PARTIAL_OUTAGE,
Service,
UNDER_MAINTENANCE,
} from '../../types';
const OperationalChip = withStyles({
root: {
backgroundColor: '#388E3D',
color: 'white',
margin: 0,
},
})(Chip);
const UnderMaintenanceChip = withStyles({
root: {
backgroundColor: '#616161',
color: 'white',
margin: 0,
},
})(Chip);
const DegradedChip = withStyles({
root: {
backgroundColor: '#FBC02D',
color: 'white',
margin: 0,
},
})(Chip);
const PartialOutageChip = withStyles({
root: {
backgroundColor: '#F57C02',
color: 'white',
margin: 0,
},
})(Chip);
const MajorOutageChip = withStyles({
root: {
backgroundColor: '#D22F2E',
color: 'white',
margin: 0,
},
})(Chip);
const serviceStatusLabels = {
[OPERATIONAL]: 'Operational',
[UNDER_MAINTENANCE]: 'Under maintenance',
[DEGRADED]: 'Degraded',
[PARTIAL_OUTAGE]: 'Partial outage',
[MAJOR_OUTAGE]: 'Major outage',
} as Record<string, string>;
export const StatusChip = ({ service }: { service: Service }) => {
const label = `${serviceStatusLabels[service.status]}`;
switch (service.status) {
case OPERATIONAL:
return <OperationalChip label={label} size="small" />;
case UNDER_MAINTENANCE:
return <UnderMaintenanceChip label={label} size="small" />;
case DEGRADED:
return <DegradedChip label={label} size="small" />;
case PARTIAL_OUTAGE:
return <PartialOutageChip label={label} size="small" />;
case MAJOR_OUTAGE:
return <MajorOutageChip label={label} size="small" />;
default:
return <Chip label={label} size="small" />;
}
};
@@ -13,5 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './UptimeMonitorsPage';
export * from './UptimeMonitorsTable';
export * from './ServicesPage';
export * from './ServicesTable';
@@ -0,0 +1,79 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { IconButton, Menu, MenuItem, Typography } from '@material-ui/core';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import React from 'react';
import { ilertApiRef } from '../../api';
import { StatusPage } from '../../types';
import { Link } from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
export const StatusPageActionsMenu = ({
statusPage,
}: {
statusPage: StatusPage;
}) => {
const ilertApi = useApi(ilertApiRef);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleCloseMenu = () => {
setAnchorEl(null);
};
return (
<>
<IconButton
aria-label="more"
aria-controls="long-menu"
aria-haspopup="true"
onClick={handleClick}
size="small"
>
<MoreVertIcon />
</IconButton>
<Menu
id={`statusPage-actions-menu-${statusPage.id}`}
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleCloseMenu}
PaperProps={{
style: { maxHeight: 48 * 5.5 },
}}
>
<MenuItem key="details" onClick={handleCloseMenu}>
<Typography variant="inherit" noWrap>
<Link to={ilertApi.getStatusPageDetailsURL(statusPage)}>
View in iLert
</Link>
</Typography>
</MenuItem>
<MenuItem>
<Typography variant="inherit" noWrap>
<Link to={`https://${ilertApi.getStatusPageURL(statusPage)}`}>
View status page
</Link>
</Typography>
</MenuItem>
</Menu>
</>
);
};
@@ -14,33 +14,26 @@
* limitations under the License.
*/
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import { Incident } from '../../types';
import { ilertApiRef } from '../../api';
import { StatusPage } from '../../types';
import { useApi } from '@backstage/core-plugin-api';
import { Link } from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
const useStyles = makeStyles({
link: {
lineHeight: '22px',
},
});
export const IncidentLink = ({ incident }: { incident: Incident | null }) => {
export const StatusPageLink = ({
statusPage,
}: {
statusPage: StatusPage | null;
}) => {
const ilertApi = useApi(ilertApiRef);
const classes = useStyles();
if (!incident) {
if (!statusPage) {
return null;
}
return (
<Link
className={classes.link}
to={ilertApi.getIncidentDetailsURL(incident)}
>
#{incident.id}
<Link to={ilertApi.getStatusPageDetailsURL(statusPage)}>
#{statusPage.id}
</Link>
);
};
@@ -14,37 +14,24 @@
* limitations under the License.
*/
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import { UptimeMonitor } from '../../types';
import { ilertApiRef } from '../../api';
import { StatusPage } from '../../types';
import { useApi } from '@backstage/core-plugin-api';
import { Link } from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
const useStyles = makeStyles({
link: {
lineHeight: '22px',
},
});
export const UptimeMonitorLink = ({
uptimeMonitor,
export const StatusPageURL = ({
statusPage,
}: {
uptimeMonitor: UptimeMonitor | null;
statusPage: StatusPage | null;
}) => {
const ilertApi = useApi(ilertApiRef);
const classes = useStyles();
if (!uptimeMonitor) {
if (!statusPage) {
return null;
}
return (
<Link
className={classes.link}
to={ilertApi.getUptimeMonitorDetailsURL(uptimeMonitor)}
>
#{uptimeMonitor.id}
</Link>
);
const url = ilertApi.getStatusPageURL(statusPage);
return <Link to={`https://${url}`}>{url}</Link>;
};
@@ -0,0 +1,16 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './StatusPageActionsMenu';
@@ -0,0 +1,89 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Chip, withStyles } from '@material-ui/core';
import React from 'react';
import {
DEGRADED,
MAJOR_OUTAGE,
OPERATIONAL,
PARTIAL_OUTAGE,
StatusPage,
UNDER_MAINTENANCE,
} from '../../types';
const OperationalChip = withStyles({
root: {
backgroundColor: '#388E3D',
color: 'white',
margin: 0,
},
})(Chip);
const UnderMaintenanceChip = withStyles({
root: {
backgroundColor: '#616161',
color: 'white',
margin: 0,
},
})(Chip);
const DegradedChip = withStyles({
root: {
backgroundColor: '#FBC02D',
color: 'white',
margin: 0,
},
})(Chip);
const PartialOutageChip = withStyles({
root: {
backgroundColor: '#F57C02',
color: 'white',
margin: 0,
},
})(Chip);
const MajorOutageChip = withStyles({
root: {
backgroundColor: '#D22F2E',
color: 'white',
margin: 0,
},
})(Chip);
const statusPageStatusLabels = {
[OPERATIONAL]: 'Operational',
[UNDER_MAINTENANCE]: 'Under maintenance',
[DEGRADED]: 'Degraded',
[PARTIAL_OUTAGE]: 'Partial outage',
[MAJOR_OUTAGE]: 'Major outage',
} as Record<string, string>;
export const StatusChip = ({ statusPage }: { statusPage: StatusPage }) => {
const label = `${statusPageStatusLabels[statusPage.status]}`;
switch (statusPage.status) {
case OPERATIONAL:
return <OperationalChip label={label} size="small" />;
case UNDER_MAINTENANCE:
return <UnderMaintenanceChip label={label} size="small" />;
case DEGRADED:
return <DegradedChip label={label} size="small" />;
case PARTIAL_OUTAGE:
return <PartialOutageChip label={label} size="small" />;
case MAJOR_OUTAGE:
return <MajorOutageChip label={label} size="small" />;
default:
return <Chip label={label} size="small" />;
}
};
@@ -0,0 +1,66 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Content,
ContentHeader,
ResponseErrorPanel,
SupportButton,
} from '@backstage/core-components';
import React from 'react';
import { useStatusPages } from '../../hooks/useStatusPages';
import { MissingAuthorizationHeaderError } from '../Errors';
import { StatusPagesTable } from './StatusPagesTable';
export const StatusPagesPage = () => {
const [
{ tableState, statusPages, isLoading, error },
{ onChangePage, onChangeRowsPerPage, setIsLoading },
] = useStatusPages(true);
if (error) {
if (error.name === 'AuthenticationError') {
return (
<Content>
<MissingAuthorizationHeaderError />
</Content>
);
}
return (
<Content>
<ResponseErrorPanel error={error} />
</Content>
);
}
return (
<Content>
<ContentHeader title="Status pages">
<SupportButton>
This helps you to bring iLert into your developer portal.
</SupportButton>
</ContentHeader>
<StatusPagesTable
statusPages={statusPages}
tableState={tableState}
onChangePage={onChangePage}
onChangeRowsPerPage={onChangeRowsPerPage}
isLoading={isLoading}
setIsLoading={setIsLoading}
/>
</Content>
);
};
@@ -0,0 +1,152 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import React from 'react';
import { TableState } from '../../api';
import { StatusPage } from '../../types';
import { VisibilityChip } from './VisibilityChip';
import { Table, TableColumn } from '@backstage/core-components';
import { StatusPageActionsMenu } from '../StatusPage/StatusPageActionsMenu';
import { StatusPageLink } from '../StatusPage/StatusPageLink';
import { StatusPageURL } from '../StatusPage/StatusPageURL';
import { StatusChip } from './StatusChip';
const useStyles = makeStyles(theme => ({
empty: {
padding: theme.spacing(2),
display: 'flex',
justifyContent: 'center',
},
}));
export const StatusPagesTable = ({
statusPages,
tableState,
isLoading,
onChangePage,
onChangeRowsPerPage,
compact,
}: {
statusPages: StatusPage[];
tableState: TableState;
isLoading: boolean;
setIsLoading: (isLoading: boolean) => void;
onChangePage: (page: number) => void;
onChangeRowsPerPage: (pageSize: number) => void;
compact?: boolean;
}) => {
const classes = useStyles();
const smColumnStyle = {
width: '10%',
maxWidth: '10%',
};
const xlColumnStyle = {
width: '30%',
maxWidth: '30%',
};
const idColumn: TableColumn<StatusPage> = {
title: 'ID',
field: 'id',
highlight: true,
cellStyle: smColumnStyle,
headerStyle: smColumnStyle,
render: rowData => <StatusPageLink statusPage={rowData} />,
};
const nameColumn: TableColumn<StatusPage> = {
title: 'Name',
field: 'name',
cellStyle: !compact ? xlColumnStyle : undefined,
headerStyle: !compact ? xlColumnStyle : undefined,
render: rowData => <Typography>{rowData.name}</Typography>,
};
const urlColumn: TableColumn<StatusPage> = {
title: 'URL',
field: 'url',
cellStyle: smColumnStyle,
headerStyle: smColumnStyle,
render: rowData => <StatusPageURL statusPage={rowData} />,
};
const visibilityColumn: TableColumn<StatusPage> = {
title: 'Visibility',
field: 'visibility',
cellStyle: smColumnStyle,
headerStyle: smColumnStyle,
render: rowData => <VisibilityChip statusPage={rowData} />,
};
const statusColumn: TableColumn<StatusPage> = {
title: 'Status',
field: 'status',
cellStyle: smColumnStyle,
headerStyle: smColumnStyle,
render: rowData => <StatusChip statusPage={rowData} />,
};
const actionsColumn: TableColumn<StatusPage> = {
title: '',
field: '',
cellStyle: smColumnStyle,
headerStyle: smColumnStyle,
render: rowData => <StatusPageActionsMenu statusPage={rowData} />,
};
const columns: TableColumn<StatusPage>[] = compact
? [nameColumn, statusColumn, urlColumn, actionsColumn]
: [
idColumn,
nameColumn,
statusColumn,
urlColumn,
visibilityColumn,
actionsColumn,
];
return (
<Table
options={{
sorting: false,
search: !compact,
paging: !compact,
actionsColumnIndex: -1,
pageSize: tableState.pageSize,
pageSizeOptions: !compact ? [10, 20, 50, 100] : [3, 10, 20, 50, 100],
padding: 'dense',
loadingType: 'overlay',
showEmptyDataSourceMessage: !isLoading,
showTitle: true,
toolbar: true,
}}
emptyContent={
<Typography color="textSecondary" className={classes.empty}>
No status pages
</Typography>
}
title={
<Typography variant="button" color="textSecondary">
STATUS PAGES
</Typography>
}
page={tableState.page}
onPageChange={onChangePage}
onRowsPerPageChange={onChangeRowsPerPage}
columns={columns}
data={statusPages}
isLoading={isLoading}
/>
);
};
@@ -0,0 +1,52 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Chip, withStyles } from '@material-ui/core';
import React from 'react';
import { PRIVATE, PUBLIC, StatusPage } from '../../types';
const PrivateChip = withStyles({
root: {
backgroundColor: '#4caf50',
color: 'white',
margin: 0,
},
})(Chip);
const PublicChip = withStyles({
root: {
backgroundColor: '#ffb74d',
color: 'white',
margin: 0,
},
})(Chip);
const statusPageVisibilityLabels = {
[PUBLIC]: 'Public',
[PRIVATE]: 'Private',
} as Record<string, string>;
export const VisibilityChip = ({ statusPage }: { statusPage: StatusPage }) => {
const label = `${statusPageVisibilityLabels[statusPage.visibility]}`;
switch (statusPage.visibility) {
case PRIVATE:
return <PrivateChip label={label} size="small" />;
case PUBLIC:
return <PublicChip label={label} size="small" />;
default:
return <Chip label={label} size="small" />;
}
};
@@ -0,0 +1,17 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './StatusPagesPage';
export * from './StatusPagesTable';
@@ -1,137 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { IconButton, Menu, MenuItem, Typography } from '@material-ui/core';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import { ilertApiRef } from '../../api';
import { UptimeMonitor } from '../../types';
import { alertApiRef, useApi } from '@backstage/core-plugin-api';
import { Link } from '@backstage/core-components';
export const UptimeMonitorActionsMenu = ({
uptimeMonitor,
onUptimeMonitorChanged,
}: {
uptimeMonitor: UptimeMonitor;
onUptimeMonitorChanged?: (uptimeMonitor: UptimeMonitor) => void;
}) => {
const ilertApi = useApi(ilertApiRef);
const alertApi = useApi(alertApiRef);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const callback = onUptimeMonitorChanged || ((_: UptimeMonitor): void => {});
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleCloseMenu = () => {
setAnchorEl(null);
};
const handlePause = async (): Promise<void> => {
try {
const newUptimeMonitor = await ilertApi.pauseUptimeMonitor(uptimeMonitor);
handleCloseMenu();
alertApi.post({ message: 'Uptime monitor paused.' });
callback(newUptimeMonitor);
} catch (err) {
alertApi.post({ message: err, severity: 'error' });
}
};
const handleResume = async (): Promise<void> => {
try {
const newUptimeMonitor = await ilertApi.resumeUptimeMonitor(
uptimeMonitor,
);
handleCloseMenu();
alertApi.post({ message: 'Uptime monitor resumed.' });
callback(newUptimeMonitor);
} catch (err) {
alertApi.post({ message: err, severity: 'error' });
}
};
const handleOpenReport = async (): Promise<void> => {
try {
const um = await ilertApi.fetchUptimeMonitor(uptimeMonitor.id);
handleCloseMenu();
window.open(um.shareUrl, '_blank');
} catch (err) {
alertApi.post({ message: err, severity: 'error' });
}
};
return (
<>
<IconButton
aria-label="more"
aria-controls="long-menu"
aria-haspopup="true"
onClick={handleClick}
size="small"
>
<MoreVertIcon />
</IconButton>
<Menu
id={`uptime-monitor-actions-menu-${uptimeMonitor.id}`}
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleCloseMenu}
PaperProps={{
style: { maxHeight: 48 * 4.5 },
}}
>
{uptimeMonitor.paused ? (
<MenuItem key="ack" onClick={handleResume}>
<Typography variant="inherit" noWrap>
Resume
</Typography>
</MenuItem>
) : null}
{!uptimeMonitor.paused ? (
<MenuItem key="close" onClick={handlePause}>
<Typography variant="inherit" noWrap>
Pause
</Typography>
</MenuItem>
) : null}
<MenuItem key="report" onClick={handleCloseMenu}>
<Typography variant="inherit" noWrap>
<Link to="#" onClick={handleOpenReport}>
View Report
</Link>
</Typography>
</MenuItem>
<MenuItem key="details" onClick={handleCloseMenu}>
<Typography variant="inherit" noWrap>
<Link to={ilertApi.getUptimeMonitorDetailsURL(uptimeMonitor)}>
View in iLert
</Link>
</Typography>
</MenuItem>
</Menu>
</>
);
};
@@ -1,73 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import Chip from '@material-ui/core/Chip';
import { withStyles } from '@material-ui/core/styles';
import { UptimeMonitor } from '../../types';
const UpChip = withStyles({
root: {
backgroundColor: '#4caf50',
color: 'white',
margin: 0,
},
})(Chip);
const DownChip = withStyles({
root: {
backgroundColor: '#d32f2f',
color: 'white',
margin: 0,
},
})(Chip);
const UnknownChip = withStyles({
root: {
backgroundColor: '#92949c',
color: 'white',
margin: 0,
},
})(Chip);
export const uptimeMonitorStatusLabels = {
['up']: 'Up',
['down']: 'Down',
['unknown']: 'Unknown',
} as Record<string, string>;
export const StatusChip = ({
uptimeMonitor,
}: {
uptimeMonitor: UptimeMonitor;
}) => {
let label = `${uptimeMonitorStatusLabels[uptimeMonitor.status]}`;
if (uptimeMonitor.paused) {
label = 'Paused';
return <UnknownChip label={label} size="small" />;
}
switch (uptimeMonitor.status) {
case 'up':
return <UpChip label={label} size="small" />;
case 'down':
return <DownChip label={label} size="small" />;
case 'unknown':
return <UnknownChip label={label} size="small" />;
default:
return <Chip label={label} size="small" />;
}
};
@@ -1,42 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { UptimeMonitor } from '../../types';
import Typography from '@material-ui/core/Typography';
export const UptimeMonitorCheckType = ({
uptimeMonitor,
}: {
uptimeMonitor: UptimeMonitor;
}) => {
switch (uptimeMonitor.region) {
case 'EU':
return (
<Typography
noWrap
// eslint-disable-next-line no-restricted-syntax
>{`${uptimeMonitor.checkType.toUpperCase()} 🇩🇪`}</Typography>
);
default:
return (
<Typography
noWrap
// eslint-disable-next-line no-restricted-syntax
>{`${uptimeMonitor.checkType.toUpperCase()} 🇺🇸`}</Typography>
);
}
};
@@ -1,176 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import { TableState } from '../../api';
import { UptimeMonitor } from '../../types';
import { StatusChip } from './StatusChip';
import Typography from '@material-ui/core/Typography';
import { DateTime as dt, Interval } from 'luxon';
import humanizeDuration from 'humanize-duration';
import { EscalationPolicyLink } from '../EscalationPolicy/EscalationPolicyLink';
import { UptimeMonitorCheckType } from './UptimeMonitorCheckType';
import { UptimeMonitorActionsMenu } from '../UptimeMonitor/UptimeMonitorActionsMenu';
import { UptimeMonitorLink } from '../UptimeMonitor/UptimeMonitorLink';
import { Table, TableColumn } from '@backstage/core-components';
const useStyles = makeStyles(theme => ({
empty: {
padding: theme.spacing(2),
display: 'flex',
justifyContent: 'center',
},
}));
export const UptimeMonitorsTable = ({
uptimeMonitors,
tableState,
isLoading,
onChangePage,
onChangeRowsPerPage,
onUptimeMonitorChanged,
}: {
uptimeMonitors: UptimeMonitor[];
tableState: TableState;
isLoading: boolean;
onChangePage: (page: number) => void;
onChangeRowsPerPage: (pageSize: number) => void;
onUptimeMonitorChanged: (uptimeMonitor: UptimeMonitor) => void;
}) => {
const classes = useStyles();
const smColumnStyle = {
width: '5%',
maxWidth: '5%',
};
const mdColumnStyle = {
width: '10%',
maxWidth: '10%',
};
const lgColumnStyle = {
width: '15%',
maxWidth: '15%',
};
const columns: TableColumn[] = [
{
title: 'ID',
field: 'id',
highlight: true,
cellStyle: mdColumnStyle,
headerStyle: mdColumnStyle,
render: rowData => (
<UptimeMonitorLink uptimeMonitor={rowData as UptimeMonitor} />
),
},
{
title: 'Name',
field: 'name',
render: rowData => (
<Typography>{(rowData as UptimeMonitor).name}</Typography>
),
},
{
title: 'Check Type',
field: 'checkType',
cellStyle: lgColumnStyle,
headerStyle: lgColumnStyle,
render: rowData => (
<UptimeMonitorCheckType uptimeMonitor={rowData as UptimeMonitor} />
),
},
{
title: 'Last state change',
field: 'lastStatusChange',
type: 'datetime',
cellStyle: mdColumnStyle,
headerStyle: mdColumnStyle,
render: rowData => (
<Typography noWrap>
{humanizeDuration(
Interval.fromDateTimes(
dt.fromISO((rowData as UptimeMonitor).lastStatusChange),
dt.now(),
)
.toDuration()
.valueOf(),
{ units: ['h', 'm', 's'], largest: 2, round: true },
)}
</Typography>
),
},
{
title: 'Escalation policy',
field: 'assignedTo',
cellStyle: lgColumnStyle,
headerStyle: lgColumnStyle,
render: rowData => (
<EscalationPolicyLink
escalationPolicy={(rowData as UptimeMonitor).escalationPolicy}
/>
),
},
{
title: 'Status',
field: 'status',
cellStyle: smColumnStyle,
headerStyle: smColumnStyle,
render: rowData => (
<StatusChip uptimeMonitor={rowData as UptimeMonitor} />
),
},
{
title: '',
field: '',
cellStyle: smColumnStyle,
headerStyle: smColumnStyle,
render: rowData => (
<UptimeMonitorActionsMenu
uptimeMonitor={rowData as UptimeMonitor}
onUptimeMonitorChanged={onUptimeMonitorChanged}
/>
),
},
];
return (
<Table
options={{
sorting: true,
search: true,
paging: true,
actionsColumnIndex: -1,
pageSize: tableState.pageSize,
pageSizeOptions: [10, 20, 50, 100],
padding: 'dense',
loadingType: 'overlay',
showEmptyDataSourceMessage: !isLoading,
}}
emptyContent={
<Typography color="textSecondary" className={classes.empty}>
No uptime monitor
</Typography>
}
page={tableState.page}
onPageChange={onChangePage}
onRowsPerPageChange={onChangeRowsPerPage}
localization={{ header: { actions: undefined } }}
isLoading={isLoading}
columns={columns}
data={uptimeMonitors}
/>
);
};
@@ -13,48 +13,45 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { ilertApiRef } from '../api';
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
import { AuthenticationError } from '@backstage/errors';
import React from 'react';
import useAsyncRetry from 'react-use/lib/useAsyncRetry';
import { Incident, IncidentAction } from '../types';
import { useApi, errorApiRef } from '@backstage/core-plugin-api';
import { ilertApiRef } from '../api';
import { Alert, AlertAction } from '../types';
export const useIncidentActions = (
incident: Incident | null,
open: boolean,
) => {
export const useAlertActions = (alert: Alert | null, open: boolean) => {
const ilertApi = useApi(ilertApiRef);
const errorApi = useApi(errorApiRef);
const [incidentActionsList, setIncidentActionsList] = React.useState<
IncidentAction[]
>([]);
const [alertActionsList, setAlertActionsList] = React.useState<AlertAction[]>(
[],
);
const [isLoading, setIsLoading] = React.useState(false);
const { error, retry } = useAsyncRetry(async () => {
try {
if (!incident || !open) {
if (!alert || !open) {
return;
}
const data = await ilertApi.fetchIncidentActions(incident);
setIncidentActionsList(data);
const data = await ilertApi.fetchAlertActions(alert);
setAlertActionsList(data);
} catch (e) {
if (!(e instanceof AuthenticationError)) {
errorApi.post(e);
}
throw e;
}
}, [incident, open]);
}, [alert, open]);
return [
{
incidentActions: incidentActionsList,
alertActions: alertActionsList,
error,
isLoading,
},
{
setIncidentActionsList,
setAlertActionsList,
setIsLoading,
retry,
},
+7 -36
View File
@@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { ilertApiRef } from '../api';
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
import { AuthenticationError } from '@backstage/errors';
import React from 'react';
import useAsyncRetry from 'react-use/lib/useAsyncRetry';
import { AlertSource, UptimeMonitor } from '../types';
import { useApi, errorApiRef } from '@backstage/core-plugin-api';
import { ilertApiRef } from '../api';
import { AlertSource } from '../types';
export const useAlertSource = (integrationKey: string) => {
const ilertApi = useApi(ilertApiRef);
@@ -28,10 +28,6 @@ export const useAlertSource = (integrationKey: string) => {
null,
);
const [isAlertSourceLoading, setIsAlertSourceLoading] = React.useState(false);
const [uptimeMonitor, setUptimeMonitor] =
React.useState<UptimeMonitor | null>(null);
const [isUptimeMonitorLoading, setIsUptimeMonitorLoading] =
React.useState(false);
const fetchAlertSourceCall = async () => {
try {
@@ -56,38 +52,13 @@ export const useAlertSource = (integrationKey: string) => {
[integrationKey],
);
const fetchUptimeMonitorCall = async () => {
try {
if (!alertSource || alertSource.integrationType !== 'MONITOR') {
return;
}
setIsUptimeMonitorLoading(true);
const data = await ilertApi.fetchUptimeMonitor(alertSource.id);
setUptimeMonitor(data || null);
setIsUptimeMonitorLoading(false);
} catch (e) {
setIsUptimeMonitorLoading(false);
if (!(e instanceof AuthenticationError)) {
errorApi.post(e);
}
throw e;
}
};
const { error: uptimeMonitorError, retry: uptimeMonitorRetry } =
useAsyncRetry(fetchUptimeMonitorCall, [alertSource]);
const retry = () => {
alertSourceRetry();
uptimeMonitorRetry();
};
const retry = () => alertSourceRetry();
return [
{
alertSource,
uptimeMonitor,
error: alertSourceError || uptimeMonitorError,
isLoading: isAlertSourceLoading || isUptimeMonitorLoading,
error: alertSourceError,
isLoading: isAlertSourceLoading,
},
{
retry,
@@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { ilertApiRef } from '../api';
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
import { AuthenticationError } from '@backstage/errors';
import React from 'react';
import useAsyncRetry from 'react-use/lib/useAsyncRetry';
import { ilertApiRef } from '../api';
import { AlertSource, OnCall } from '../types';
import { useApi, errorApiRef } from '@backstage/core-plugin-api';
export const useAlertSourceOnCalls = (alertSource?: AlertSource | null) => {
const ilertApi = useApi(ilertApiRef);
@@ -13,20 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { GetIncidentsOpts, ilertApiRef, TableState } from '../api';
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
import { AuthenticationError } from '@backstage/errors';
import React from 'react';
import useAsyncRetry from 'react-use/lib/useAsyncRetry';
import {
ACCEPTED,
PENDING,
Incident,
IncidentStatus,
AlertSource,
} from '../types';
import { useApi, errorApiRef } from '@backstage/core-plugin-api';
import { GetAlertsOpts, ilertApiRef, TableState } from '../api';
import { ACCEPTED, Alert, AlertSource, AlertStatus, PENDING } from '../types';
export const useIncidents = (
export const useAlerts = (
paging: boolean,
singleSource?: boolean,
alertSource?: AlertSource | null,
@@ -38,21 +32,21 @@ export const useIncidents = (
page: 0,
pageSize: 10,
});
const [states, setStates] = React.useState<IncidentStatus[]>([
const [states, setStates] = React.useState<AlertStatus[]>([
ACCEPTED,
PENDING,
]);
const [incidentsList, setIncidentsList] = React.useState<Incident[]>([]);
const [incidentsCount, setIncidentsCount] = React.useState(0);
const [alertsList, setAlertsList] = React.useState<Alert[]>([]);
const [alertsCount, setAlertsCount] = React.useState(0);
const [isLoading, setIsLoading] = React.useState(false);
const fetchIncidentsCall = async () => {
const fetchAlertsCall = async () => {
try {
if (singleSource && !alertSource) {
return;
}
setIsLoading(true);
const opts: GetIncidentsOpts = {
const opts: GetAlertsOpts = {
states,
alertSources: alertSource ? [alertSource.id] : [],
};
@@ -60,8 +54,8 @@ export const useIncidents = (
opts.maxResults = tableState.pageSize;
opts.startIndex = tableState.page * tableState.pageSize;
}
const data = await ilertApi.fetchIncidents(opts);
setIncidentsList(data || []);
const data = await ilertApi.fetchAlerts(opts);
setAlertsList(data || []);
setIsLoading(false);
} catch (e) {
if (!(e instanceof AuthenticationError)) {
@@ -72,10 +66,10 @@ export const useIncidents = (
}
};
const fetchIncidentsCountCall = async () => {
const fetchAlertsCountCall = async () => {
try {
const count = await ilertApi.fetchIncidentsCount({ states });
setIncidentsCount(count || 0);
const count = await ilertApi.fetchAlertsCount({ states });
setAlertsCount(count || 0);
} catch (e) {
if (!(e instanceof AuthenticationError)) {
errorApi.post(e);
@@ -83,44 +77,44 @@ export const useIncidents = (
throw e;
}
};
const fetchIncidents = useAsyncRetry(fetchIncidentsCall, [
const fetchAlerts = useAsyncRetry(fetchAlertsCall, [
tableState,
states,
singleSource,
alertSource,
]);
const refetchIncidents = () => {
const refetchAlerts = () => {
setTableState({ ...tableState, page: 0 });
Promise.all([fetchIncidentsCall(), fetchIncidentsCountCall()]);
Promise.all([fetchAlertsCall(), fetchAlertsCountCall()]);
};
const fetchIncidentsCount = useAsyncRetry(fetchIncidentsCountCall, [states]);
const fetchAlertsCount = useAsyncRetry(fetchAlertsCountCall, [states]);
const error = fetchIncidents.error || fetchIncidentsCount.error;
const error = fetchAlerts.error || fetchAlertsCount.error;
const retry = () => {
fetchIncidents.retry();
fetchIncidentsCount.retry();
fetchAlerts.retry();
fetchAlertsCount.retry();
};
const onIncidentChanged = (newIncident: Incident) => {
let shouldRefetchIncidents = false;
setIncidentsList(
incidentsList.reduce((acc: Incident[], incident: Incident) => {
if (newIncident.id === incident.id) {
if (states.includes(newIncident.status)) {
acc.push(newIncident);
const onAlertChanged = (newAlert: Alert) => {
let shouldRefetchAlerts = false;
setAlertsList(
alertsList.reduce((acc: Alert[], alert: Alert) => {
if (newAlert.id === alert.id) {
if (states.includes(newAlert.status)) {
acc.push(newAlert);
} else {
shouldRefetchIncidents = true;
shouldRefetchAlerts = true;
}
return acc;
}
acc.push(incident);
acc.push(alert);
return acc;
}, []),
);
if (shouldRefetchIncidents) {
refetchIncidents();
if (shouldRefetchAlerts) {
refetchAlerts();
}
};
@@ -130,7 +124,7 @@ export const useIncidents = (
const onChangeRowsPerPage = (p: number) => {
setTableState({ ...tableState, pageSize: p });
};
const onIncidentStatesChange = (s: IncidentStatus[]) => {
const onAlertStatesChange = (s: AlertStatus[]) => {
setStates(s);
};
@@ -138,22 +132,22 @@ export const useIncidents = (
{
tableState,
states,
incidents: incidentsList,
incidentsCount,
alerts: alertsList,
alertsCount,
error,
isLoading,
},
{
setTableState,
setStates,
setIncidentsList,
setAlertsList,
setIsLoading,
retry,
onIncidentChanged,
refetchIncidents,
onAlertChanged,
refetchAlerts,
onChangePage,
onChangeRowsPerPage,
onIncidentStatesChange,
onAlertStatesChange,
},
] as const;
};
@@ -13,30 +13,30 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { ilertApiRef } from '../api';
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
import { AuthenticationError } from '@backstage/errors';
import React from 'react';
import useAsyncRetry from 'react-use/lib/useAsyncRetry';
import { Incident, IncidentResponder } from '../types';
import { useApi, errorApiRef } from '@backstage/core-plugin-api';
import { ilertApiRef } from '../api';
import { Alert, AlertResponder } from '../types';
export const useAssignIncident = (incident: Incident | null, open: boolean) => {
export const useAssignAlert = (alert: Alert | null, open: boolean) => {
const ilertApi = useApi(ilertApiRef);
const errorApi = useApi(errorApiRef);
const [incidentRespondersList, setIncidentRespondersList] = React.useState<
IncidentResponder[]
const [alertRespondersList, setAlertRespondersList] = React.useState<
AlertResponder[]
>([]);
const [incidentResponder, setIncidentResponder] =
React.useState<IncidentResponder | null>(null);
const [alertResponder, setAlertResponder] =
React.useState<AlertResponder | null>(null);
const [isLoading, setIsLoading] = React.useState(false);
const { error, retry } = useAsyncRetry(async () => {
try {
if (!incident || !open) {
if (!alert || !open) {
return;
}
const data = await ilertApi.fetchIncidentResponders(incident);
const data = await ilertApi.fetchAlertResponders(alert);
if (data && Array.isArray(data)) {
const groups = [
'SUGGESTED',
@@ -45,7 +45,7 @@ export const useAssignIncident = (incident: Incident | null, open: boolean) => {
'ON_CALL_SCHEDULE',
];
data.sort((a, b) => groups.indexOf(a.group) - groups.indexOf(b.group));
setIncidentRespondersList(data);
setAlertRespondersList(data);
}
} catch (e) {
if (!(e instanceof AuthenticationError)) {
@@ -53,18 +53,18 @@ export const useAssignIncident = (incident: Incident | null, open: boolean) => {
}
throw e;
}
}, [incident, open]);
}, [alert, open]);
return [
{
incidentRespondersList,
incidentResponder,
alertRespondersList,
alertResponder,
error,
isLoading,
},
{
setIncidentRespondersList,
setIncidentResponder,
setAlertRespondersList,
setAlertResponder,
setIsLoading,
retry,
},
@@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { ilertApiRef } from '../api';
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
import { AuthenticationError } from '@backstage/errors';
import React from 'react';
import useAsyncRetry from 'react-use/lib/useAsyncRetry';
import { ilertApiRef } from '../api';
import { AlertSource } from '../types';
import { useApi, errorApiRef } from '@backstage/core-plugin-api';
export const useNewIncident = (
export const useNewAlert = (
open: boolean,
initialAlertSource?: AlertSource | null,
) => {
@@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { ilertApiRef, TableState } from '../api';
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
import { AuthenticationError } from '@backstage/errors';
import React from 'react';
import useAsyncRetry from 'react-use/lib/useAsyncRetry';
import { UptimeMonitor } from '../types';
import { useApi, errorApiRef } from '@backstage/core-plugin-api';
import { GetServicesOpts, ilertApiRef, TableState } from '../api';
import { Service } from '../types';
export const useUptimeMonitors = () => {
export const useServices = (paging: boolean) => {
const ilertApi = useApi(ilertApiRef);
const errorApi = useApi(errorApiRef);
@@ -28,60 +28,64 @@ export const useUptimeMonitors = () => {
page: 0,
pageSize: 10,
});
const [uptimeMonitorsList, setUptimeMonitorsList] = React.useState<
UptimeMonitor[]
>([]);
const [servicesList, setServicesList] = React.useState<Service[]>([]);
const [isLoading, setIsLoading] = React.useState(false);
const { error, retry } = useAsyncRetry(async () => {
const fetchServicesCall = async () => {
try {
setIsLoading(true);
const data = await ilertApi.fetchUptimeMonitors();
setUptimeMonitorsList(data || []);
const opts: GetServicesOpts = {};
if (paging) {
opts.maxResults = tableState.pageSize;
opts.startIndex = tableState.page * tableState.pageSize;
}
const data = await ilertApi.fetchServices(opts);
setServicesList(data || []);
setIsLoading(false);
} catch (e) {
setIsLoading(false);
if (!(e instanceof AuthenticationError)) {
errorApi.post(e);
}
setIsLoading(false);
throw e;
}
}, [tableState]);
};
const onUptimeMonitorChanged = (newUptimeMonitor: UptimeMonitor) => {
setUptimeMonitorsList(
uptimeMonitorsList.map((uptimeMonitor: UptimeMonitor): UptimeMonitor => {
if (newUptimeMonitor.id === uptimeMonitor.id) {
return newUptimeMonitor;
}
const fetchServices = useAsyncRetry(fetchServicesCall, [tableState]);
return uptimeMonitor;
}),
);
const refetchServices = () => {
setTableState({ ...tableState, page: 0 });
Promise.all([fetchServicesCall()]);
};
const error = fetchServices.error;
const retry = () => {
fetchServices.retry();
};
const onChangePage = (page: number) => {
setTableState({ ...tableState, page });
};
const onChangeRowsPerPage = (pageSize: number) => {
setTableState({ ...tableState, pageSize });
const onChangeRowsPerPage = (p: number) => {
setTableState({ ...tableState, pageSize: p });
};
return [
{
tableState,
uptimeMonitors: uptimeMonitorsList,
error,
services: servicesList,
isLoading,
error,
},
{
setTableState,
setUptimeMonitorsList,
setServicesList,
setIsLoading,
retry,
onUptimeMonitorChanged,
refetchServices,
onChangePage,
onChangeRowsPerPage,
setIsLoading,
},
] as const;
};
+93
View File
@@ -0,0 +1,93 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
import { AuthenticationError } from '@backstage/errors';
import React from 'react';
import useAsyncRetry from 'react-use/lib/useAsyncRetry';
import { GetStatusPagesOpts, ilertApiRef, TableState } from '../api';
import { StatusPage } from '../types';
export const useStatusPages = (paging: boolean) => {
const ilertApi = useApi(ilertApiRef);
const errorApi = useApi(errorApiRef);
const [tableState, setTableState] = React.useState<TableState>({
page: 0,
pageSize: 10,
});
const [statusPagesList, setStatusPagesList] = React.useState<StatusPage[]>(
[],
);
const [isLoading, setIsLoading] = React.useState(false);
const fetchStatusPagesCall = async () => {
try {
setIsLoading(true);
const opts: GetStatusPagesOpts = {};
if (paging) {
opts.maxResults = tableState.pageSize;
opts.startIndex = tableState.page * tableState.pageSize;
}
const data = await ilertApi.fetchStatusPages(opts);
setStatusPagesList(data || []);
setIsLoading(false);
} catch (e) {
if (!(e instanceof AuthenticationError)) {
errorApi.post(e);
}
setIsLoading(false);
throw e;
}
};
const fetchStatusPages = useAsyncRetry(fetchStatusPagesCall, [tableState]);
const refetchStatusPages = () => {
setTableState({ ...tableState, page: 0 });
Promise.all([fetchStatusPagesCall()]);
};
const error = fetchStatusPages.error;
const retry = () => {
fetchStatusPages.retry();
};
const onChangePage = (page: number) => {
setTableState({ ...tableState, page });
};
const onChangeRowsPerPage = (p: number) => {
setTableState({ ...tableState, pageSize: p });
};
return [
{
tableState,
statusPages: statusPagesList,
isLoading,
error,
},
{
setTableState,
setStatusPagesList,
setIsLoading,
retry,
refetchStatusPages,
onChangePage,
onChangeRowsPerPage,
},
] as const;
};
+85 -47
View File
@@ -15,17 +15,18 @@
*/
/** @public */
export interface Incident {
export interface Alert {
id: number;
summary: string;
details: string;
reportTime: string;
resolvedOn: string;
status: IncidentStatus;
priority: IncidentPriority;
incidentKey: string;
status: AlertStatus;
priority: AlertPriority;
alertKey: string;
alertSource: AlertSource | null;
assignedTo: User | null;
responders: Responder[];
logEntries: LogEntry[];
links: Link[];
images: Image[];
@@ -42,10 +43,10 @@ export const ACCEPTED = 'ACCEPTED';
export const RESOLVED = 'RESOLVED';
/** @public */
export type IncidentStatus = typeof PENDING | typeof ACCEPTED | typeof RESOLVED;
export type AlertStatus = typeof PENDING | typeof ACCEPTED | typeof RESOLVED;
/** @public */
export type IncidentPriority = 'HIGH' | 'LOW';
export type AlertPriority = 'HIGH' | 'LOW';
/** @public */
export interface Link {
@@ -76,7 +77,7 @@ export interface LogEntry {
timestamp: string;
logEntryType: string;
text: string;
incidentId?: number;
alertId?: number;
iconName?: string;
iconClass?: string;
filterTypes?: string[];
@@ -99,6 +100,13 @@ export interface User {
department: string;
}
/** @public */
export interface Responder {
acceptedAt?: string;
status: string;
user: User;
}
/** @public */
export type UserRole =
| 'USER'
@@ -125,8 +133,8 @@ export interface AlertSource {
iconUrl?: string;
lightIconUrl?: string;
darkIconUrl?: string;
incidentCreation?: AlertSourceIncidentCreation;
incidentPriorityRule?: AlertSourceIncidentPriorityRule;
alertCreation?: AlertSourceAlertCreation;
alertPriorityRule?: AlertSourceAlertPriorityRule;
emailFiltered?: boolean;
emailResolveFiltered?: boolean;
active?: boolean;
@@ -209,20 +217,48 @@ export type AlertSourceIntegrationType =
| 'CORTEXXSOAR'
| string;
/** @public */
export type AlertSourceIncidentCreation =
| 'ONE_INCIDENT_PER_EMAIL'
| 'ONE_INCIDENT_PER_EMAIL_SUBJECT'
| 'ONE_PENDING_INCIDENT_ALLOWED'
| 'ONE_OPEN_INCIDENT_ALLOWED'
export type AlertSourceAlertCreation =
| 'ONE_ALERT_PER_EMAIL'
| 'ONE_ALERT_PER_EMAIL_SUBJECT'
| 'ONE_PENDING_ALERT_ALLOWED'
| 'ONE_OPEN_ALERT_ALLOWED'
| 'OPEN_RESOLVE_ON_EXTRACTION';
/** @public */
export type AlertSourceFilterOperator = 'AND' | 'OR';
/** @public */
export type AlertSourceIncidentPriorityRule =
export type AlertSourceAlertPriorityRule =
| 'HIGH'
| 'LOW'
| 'HIGH_DURING_SUPPORT_HOURS'
| 'LOW_DURING_SUPPORT_HOURS';
/** @public */
export const OPERATIONAL = 'OPERATIONAL';
/** @public */
export const UNDER_MAINTENANCE = 'UNDER_MAINTENANCE';
/** @public */
export const DEGRADED = 'DEGRADED';
/** @public */
export const PARTIAL_OUTAGE = 'PARTIAL_OUTAGE';
/** @public */
export const MAJOR_OUTAGE = 'MAJOR_OUTAGE';
/** @public */
export type ServiceStatus =
| typeof OPERATIONAL
| typeof UNDER_MAINTENANCE
| typeof DEGRADED
| typeof PARTIAL_OUTAGE
| typeof MAJOR_OUTAGE;
/** @public */
export const PRIVATE = 'PRIVATE';
/** @public */
export const PUBLIC = 'PUBLIC';
/** @public */
export type StatusPageVisibility = typeof PRIVATE | typeof PUBLIC;
/** @public */
export interface AlertSourceEmailPredicate {
field: 'EMAIL_FROM' | 'EMAIL_SUBJECT' | 'EMAIL_BODY';
@@ -251,7 +287,7 @@ export interface AlertSourceSupportDay {
/** @public */
export interface AlertSourceSupportHours {
timezone: AlertSourceTimeZone;
autoRaiseIncidents: boolean;
autoRaiseAlerts: boolean;
supportDays: {
MONDAY: AlertSourceSupportDay;
TUESDAY: AlertSourceSupportDay;
@@ -316,33 +352,7 @@ export interface Shift {
}
/** @public */
export interface UptimeMonitor {
id: number;
name: string;
region: 'EU' | 'US';
checkType: 'http' | 'tcp' | 'udp' | 'ping';
checkParams: UptimeMonitorCheckParams;
intervalSec: number;
timeoutMs: number;
createIncidentAfterFailedChecks: number;
paused: boolean;
embedUrl: string;
shareUrl: string;
status: string;
lastStatusChange: string;
escalationPolicy: EscalationPolicy;
teams: TeamShort[];
}
/** @public */
export interface UptimeMonitorCheckParams {
host?: string;
port?: number;
url?: string;
}
/** @public */
export interface IncidentResponder {
export interface AlertResponder {
group: 'SUGGESTED' | 'USER' | 'ESCALATION_POLICY' | 'ON_CALL_SCHEDULE';
id: number;
name: string;
@@ -350,19 +360,19 @@ export interface IncidentResponder {
}
/** @public */
export interface IncidentAction {
export interface AlertAction {
name: string;
type: string;
webhookId: string;
extensionId?: string;
history?: IncidentActionHistory[];
history?: AlertActionHistory[];
}
/** @public */
export interface IncidentActionHistory {
export interface AlertActionHistory {
id: string;
webhookId: string;
incidentId: number;
alertId: number;
actor: User;
success: boolean;
}
@@ -376,3 +386,31 @@ export interface OnCall {
end: string;
escalationLevel: number;
}
/** @public */
export interface Service {
id: number;
name: string;
status: ServiceStatus;
uptime: Uptime;
}
/** @public */
export interface Uptime {
uptimePercentage: UptimePercentage;
}
/** @public */
export interface UptimePercentage {
p90: number;
}
/** @public */
export interface StatusPage {
id: number;
name: string;
domain: string;
subdomain: string;
visibility: StatusPageVisibility;
status: ServiceStatus;
}