PCORE-2143: Showing incidents for each service (#30)

* feat: added incidents for each service

* feat: fixed linting

* feat: fixed linting

* feat: fixed linting

* fix: style changes

* fix: removed commented code

* fix: removed commented code and issue summary
pull/1113/head
Smit Patel 2022-08-10 12:35:15 +05:30 committed by GitHub
parent 609796d782
commit b86b998808
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 317 additions and 284 deletions

View File

@ -1,6 +1,6 @@
<template>
<div class="col-12 mb-3 pb-2 border-bottom" role="alert">
<span class="font-weight-bold text-capitalize" :class="{'text-success': update.type.toLowerCase()==='resolved', 'text-danger': update.type.toLowerCase()==='investigating', 'text-warning': update.type.toLowerCase()==='update'}">{{update.type}}</span>
<span class="font-weight-bold text-capitalize" :class="{'text-success': update.type.toLowerCase()==='resolved', 'text-danger': update.type.toLowerCase()==='issue summary', 'text-warning': update.type.toLowerCase()==='update'}">{{update.type}}</span>
<span class="text-muted">- {{update.message}}
<button v-if="admin" @click="delete_update(update)" type="button" class="close">
<span aria-hidden="true">&times;</span>

View File

@ -41,7 +41,7 @@ export default {
return "badge-success"
case "update":
return "badge-info"
case "investigating":
case "issue summary":
return "badge-danger"
}
},

View File

@ -12,9 +12,8 @@
<form class="row" @submit.prevent="createIncidentUpdate">
<div class="col-12 col-md-3 mb-3 mb-md-0">
<select v-model="incident_update.type" class="form-control">
<option value="Investigating">Investigating</option>
<option value="Issue summary">Issue summary</option>
<option value="Update">Update</option>
<option value="Unknown">Unknown</option>
<option value="Resolved">Resolved</option>
</select>
</div>
@ -53,7 +52,7 @@
incident_update: {
incident: this.incident.id,
message: "",
type: "Investigating" // TODO: default to something.. theres is no error checking for blank submission...
type: "Issue summary"
}
}
},
@ -74,7 +73,7 @@
this.incident_update = {
incident: this.incident.id,
message: "",
type: "Investigating"
type: "Issue summary"
}
},

View File

@ -1,5 +1,4 @@
import React from "react";
// import { groups } from "../utils/data";
import GroupItem from "./GroupItem";
import { isObject, isObjectEmpty } from "../utils/helper";
@ -17,9 +16,6 @@ function showPlus(service) {
}
const Group = ({ services }) => {
// const data = groups.sort((a, b) => a.order_id - b.order_id);
// if (!data.length > 0) return <></>;
return (
<div className="list-group">
{services?.map((service) => {

View File

@ -8,6 +8,7 @@ import GroupServiceFailures from "./GroupServiceFailures";
import SubServiceCard from "./SubServiceCard";
import infoIcon from "../static/info.svg";
import { analyticsTrack } from "../utils/trackers";
import IncidentsBlock from "./IncidentsBlock";
const GroupItem = ({ service, showPlusButton }) => {
const [collapse, setCollapse] = useState(false);
@ -39,26 +40,26 @@ const GroupItem = ({ service, showPlusButton }) => {
}
analyticsTrack({
objectName: 'Service Expand',
actionName: 'clicked',
screen: 'Home page',
properties:{
objectName: "Service Expand",
actionName: "clicked",
screen: "Home page",
properties: {
serviceName: event.target.name,
}
})
},
});
};
const closeCollapse = (event) => {
setCollapse(false);
analyticsTrack({
objectName: 'Service Collapse',
actionName: 'clicked',
screen: 'Home page',
properties:{
objectName: "Service Collapse",
actionName: "clicked",
screen: "Home page",
properties: {
serviceName: event.target.name,
}
})
},
});
};
const handleMouseOver = (service) => {
@ -68,28 +69,31 @@ const GroupItem = ({ service, showPlusButton }) => {
const handleMouseOut = () => setHoverText("");
return (
<div className="service-card service_item card-bg pb-0">
<div className="service-parent service-card service_item card-bg">
{/** TODO: change span to navlink */}
<div className="service_item--header mb-3">
<div className="service_item--right">
{!loading && showPlusButton && (
<>
{collapse ? (
<button className="square-minus" name={service.name} onClick={closeCollapse} />
<button
className="square-minus"
name={service.name}
onClick={closeCollapse}
/>
) : (
<button className="square-plus" name={service.name} onClick={openCollapse} />
<button
className="square-plus"
name={service.name}
onClick={openCollapse}
/>
)}
</>
)}
{loading && <FontAwesomeIcon icon={faCircleNotch} spin />}
<span
className="subtitle no-decoration font-14 mr-1"
// to="/service/1"
>
{service.name}
</span>
<span className="subtitle no-decoration mr-1">{service.name}</span>
{service?.description && (
<>
<ReactTooltip
@ -110,89 +114,44 @@ const GroupItem = ({ service, showPlusButton }) => {
</>
)}
</div>
<div className="service_item--left">
<span
className={`badge float-right font-12 ${
service.online ? "uptime" : "downtime"
}`}
style={{ display: collapse ? "none" : "block" }}
>
{service.online ? langs("online") : langs("offline")}
</span>
</div>
</div>
<GroupServiceFailures service={service} collapse={collapse} />
{/*<IncidentsBlock service={service} /> */}
<div
className="list-group online_list"
style={{ display: collapse ? "block" : "none" }}
>
{subServices && subServices?.length > 0 ? (
subServices.map((sub_service, i) => {
return (
<SubServiceCard
key={i}
group={service}
service={sub_service}
collapse={collapse}
/>
);
})
) : (
<div className="subtitle text-align-center">No Services</div>
{!collapse && (
<div className="service_item--left">
<span
className={`badge float-right font-12 ${
service.online ? "uptime" : "downtime"
}`}>
{service.online ? langs("online") : langs("offline")}
</span>
</div>
)}
</div>
{!collapse && (
<GroupServiceFailures service={service} collapse={collapse} />
)}
{!collapse && <IncidentsBlock service={service} />}
{collapse && (
<div className="sub-service-wrapper list-group online_list">
{subServices && subServices?.length > 0 ? (
subServices.map((sub_service, i) => {
return (
<SubServiceCard
key={i}
group={service}
service={sub_service}
collapse={collapse}
/>
);
})
) : (
<div className="subtitle text-align-center">No Services</div>
)}
</div>
)}
</div>
);
};
export default React.memo(GroupItem);
// import React from "react";
// import langs from "../config/langs";
// import { services } from "../data";
// import GroupServiceFailures from "./GroupServiceFailures";
// import IncidentsBlock from "./IncidentsBlock";
// // import DateUtils from "../utils/DateUtils";
// const GroupItem = ({ group }) => {
// const groupServices = services
// .filter((s) => s.group_id === group.id)
// .sort((a, b) => a.order_id - b.order_id);
// if (!groupServices.length > 0) return null;
// return (
// <div className="col-12 full-col-12">
// {group.name !== "Empty Group" && (
// <h4 className="group_header mb-3 mt-4">{group.name}</h4>
// )}
// <div className="list-group online_list mb-4">
// {groupServices.map((service, i) => {
// return (
// <div key={i} className="service-card service-card-action">
// <span
// className="no-decoration font-3"
// // to={DateUtils.serviceLink(service)}
// >
// {service.name}
// </span>
// <span
// className={`badge text-uppercase float-right ${
// service.online ? "bg-success" : "bg-danger"
// }`}
// >
// {service.online ? langs("online") : langs("offline")}
// </span>
// <GroupServiceFailures service={service} />
// <IncidentsBlock service={service} />
// </div>
// );
// })}
// </div>
// </div>
// );
// };
// export default GroupItem;

View File

@ -57,12 +57,6 @@ async function fetchFailureSeries(url) {
}
const GroupServiceFailures = ({ group = null, service, collapse }) => {
// const [containerRef, isVisible] = useIntersectionObserver({
// root: null,
// rootMargin: "0px",
// threshold: 1.0,
// });
const [hoverText, setHoverText] = useState("");
const [loaded, setLoaded] = useState(true);
const [failureData, setFailureData] = useState([]);
@ -98,7 +92,7 @@ const GroupServiceFailures = ({ group = null, service, collapse }) => {
}
}
fetchData();
}, [service]);
}, [service, group]);
const handleTooltip = (d) => {
let txt = "";
@ -133,7 +127,6 @@ const GroupServiceFailures = ({ group = null, service, collapse }) => {
if (loaded) return <ServiceLoader text="Loading series.." />;
return (
// transition div
<div name="fade" style={{ display: collapse ? "none" : "block" }}>
<div className="block-chart">
<ReactTooltip
@ -150,8 +143,7 @@ const GroupServiceFailures = ({ group = null, service, collapse }) => {
onMouseOver={() => handleMouseOver(d)}
onMouseOut={handleMouseOut}
key={i}
data-tip={hoverText}
>
data-tip={hoverText}>
{d.status !== 0 && (
<span className="d-none d-md-block text-center small"></span>
)}

View File

@ -16,32 +16,31 @@ const IncidentUpdate = ({ update, admin }) => {
};
return (
<div className="col-12 mb-3 pb-2 border-bottom" role="alert">
<span
className={`
font-weight-bold text-capitalize
${update.type.toLowerCase() === "resolved" ? "text-success" : ""}
${update.type.toLowerCase() === "investigating" ? "text-danger" : ""}
${update.type.toLowerCase() === "update" ? "text-warning" : ""}
`}
>
{update.type}
</span>
<span className="text-muted">
- {update.message}
{admin && (
<button
onClick={deleteUpdate(update)}
type="button"
className="close"
>
<span aria-hidden="true">&times;</span>
</button>
)}
</span>
<span className="d-block small">
{DateUtils.ago(update.created_at)} ago
</span>
<div className="incident-wrapper mb-3 pb-2 d-flex" role="alert">
<div className="time-line mr-2">
<span class="dot"></span>
</div>
<div>
<span className="font-14">
{update.message}
{admin && (
<button
onClick={deleteUpdate(update)}
type="button"
className="close">
<span aria-hidden="true">&times;</span>
</button>
)}
</span>
<span className="d-block small text-muted">
Posted {DateUtils.ago(update.created_at)} ago.{" "}
{DateUtils.format(
DateUtils.parseISO(update.created_at),
"MMM d, yyyy - HH:mm"
)}
</span>
</div>
</div>
);
};

View File

@ -3,36 +3,96 @@ import API from "../config/API";
import DateUtils from "../utils/DateUtils";
import IncidentUpdate from "./IncidentUpdate";
const IncidentsBlock = ({ service }) => {
const IncidentsBlock = ({ service, group }) => {
const [incidents, setIncidents] = useState([]);
const [incidentsShow, setIncidentsShow] = useState(false);
useEffect(() => {
async function fetchData() {
const data = await API.incidents_service(service.id);
setIncidents(data);
let data = [];
if (group?.id) {
data = await API.sub_incidents_service(group.id, service.id);
} else {
data = await API.incidents_service(service.id);
}
setIncidents(data || []);
}
fetchData();
}, [service.id]);
}, [service.id, group?.id]);
const handleIncidentShow = (event) => {
const { id } = event.target;
setIncidentsShow({ ...incidentsShow, [id]: !incidentsShow[id] });
};
return (
<div className="row">
{incidents?.map((incident, i) => {
return (
<div className="col-12 mt-2" key={i}>
<span className="braker mt-1 mb-3"></span>
<h6>
{incident.title}
<span className="font-2 float-right">
{DateUtils.niceDate(incident.created_at)}
</span>
</h6>
<div className="font-2 mb-3" v-html="incident.description"></div>
{incident.updates.map((update, i) => {
return <IncidentUpdate key={i} update={update} admin={false} />;
})}
<div className="incidents-wrapper row">
<div className="col-12 mt-2">
{incidents?.length > 0 ? (
incidents?.map((incident) => {
const { id, title, description, updated_at } = incident;
return (
<>
<span className="braker mt-1 mb-3"></span>
<div
className={`incident-title col-12 ${
incidentsShow[id] && "mb-3"
}`}>
{incidentsShow[id] ? (
<button
className="square-minus"
type="button"
id={id}
onClick={handleIncidentShow}
/>
) : (
<button
className="square-plus"
type="button"
id={id}
onClick={handleIncidentShow}
/>
)}
<div className="title-wrapper">
<span class="subtitle no-decoration">{title}</span>
<span className="d-block small text-dark">
{description}
</span>
<span className="d-block small text-muted">
Updated {DateUtils.ago(updated_at)} ago.{" "}
{DateUtils.format(
DateUtils.parseISO(updated_at),
"MMM d, yyyy - HH:mm"
)}
</span>
</div>
</div>
{incidentsShow[id] && (
<div className="incident-updates-wrapper col-12">
{incident?.updates.map((update) => {
return (
<IncidentUpdate
key={update.id}
update={update}
admin={false}
/>
);
})}
</div>
)}
</>
);
})
) : (
<div className="col-12">
<span class="font-14 text-muted">No recent incidents</span>
</div>
);
})}
)}
</div>
</div>
);
};

View File

@ -1,36 +1,26 @@
import React, { useState, useEffect } from "react";
// import { NavLink } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import DateUtils from "../utils/DateUtils";
import Group from "./Group";
import ContentHeader from "./ContentHeader";
import ServiceLoader from "./ServiceLoader";
// import IncidentService from "./IncidentService";
// import MessageBlock from "./MessageBlock";
// import ServiceBlock from "./ServiceBlock";
// import ServicesList from "./ServicesList";
import API from "../config/API";
import { STATUS_COLOR, STATUS_ICON, STATUS_TEXT } from "../utils/constants";
import { findStatus } from "../utils/helper";
import { analyticsTrack } from "../utils/trackers";
const ServicesPage = () => {
// const data = messages.filter((m) => inRange(m) && m.service === 0);
const [services, setServices] = useState([]);
const [status, setStatus] = useState("uptime");
const [loading, setLoading] = useState(true);
const [poll, setPolling] = useState(1);
const today = DateUtils.format(new Date(), "d MMMM yyyy, hh:mm aaa");
useEffect(() => {
if(!loading) {
if (!loading) {
analyticsTrack({
objectName: 'Status Page',
actionName: 'displayed',
screen: 'Home page'
})
objectName: "Status Page",
actionName: "displayed",
screen: "Home page",
});
}
}, [loading])
}, [loading]);
useEffect(() => {
const timer = setInterval(() => {
@ -59,47 +49,20 @@ const ServicesPage = () => {
return (
<div className="container col-md-7 col-sm-12 sm-container">
<ContentHeader />
<div className="app-content">
<div className="service">
<h2 className="title font-20 fw-700">Razorpay Payments</h2>
<div className="d-flex align-items-center subtitle font-12 mt-2">
<FontAwesomeIcon
icon={STATUS_ICON[status]}
style={{
fontSize: "16px",
color: STATUS_COLOR[status],
}}
/>
<span className="mx-1">{STATUS_TEXT[status]}</span>
</div>
<div>
<span className="date font-12">{today}</span>
</div>
</div>
{loading && <ServiceLoader text="Loading Services" />}
{/* <ServicesList loading={loading} services={services} /> */}
{/* TODO --> Grouped Services to Accordian*/}
{services && services.length > 0 ? (
<Group services={services} />
) : (
<div className="description text-align-center">No Services</div>
)}
{/* <div>
{data.map((message) => {
return <MessageBlock key={message.id} message={message} />;
})}
</div>
<div>
{services.map((service) => {
return <ServiceBlock key={service.id} service={service} />;
})}
</div> */}
<div className="app-footer">
<div className="service-status">
<span className="service-status-badge uptime"></span>

View File

@ -2,7 +2,7 @@ import React, { useState } from "react";
import ReactTooltip from "react-tooltip";
import langs from "../config/langs";
import GroupServiceFailures from "./GroupServiceFailures";
// import IncidentsBlock from "./IncidentsBlock";
import IncidentsBlock from "./IncidentsBlock";
import infoIcon from "../static/info.svg";
const SubServiceCard = ({ group, service }) => {
@ -50,15 +50,14 @@ const SubServiceCard = ({ group, service }) => {
<span
className={`badge float-right font-12 ${
service.online ? "status-green" : "status-red"
}`}
>
}`}>
{service.online ? langs("online") : langs("offline")}
</span>
</div>
</div>
<GroupServiceFailures group={group} service={service} />
{/* <IncidentsBlock service={service} /> */}
<IncidentsBlock group={group} service={service} />
</div>
);
};

View File

@ -213,10 +213,15 @@ class Api {
}
async incidents_service(id) {
return incidents[id];
// return axios
// .get("api/services/" + id + "/incidents")
// .then((response) => response.data);
return axios
.get("/services/" + id + "/active_incidents")
.then((response) => response.data);
}
async sub_incidents_service(id, sub_id) {
return axios
.get(`/services/${id}/sub_services/${sub_id}/active_incidents`)
.then((response) => response.data);
}
async incident_create(service_id, data) {

View File

@ -13,6 +13,48 @@ a {
color: $text-color;
}
.dot {
height: 8px;
width: 8px;
background-color: #bbb;
border-radius: 50%;
display: inline-block;
}
.square-plus,
.square-minus {
color: $blue;
border: 2px solid $blue;
border-radius: 2px;
width: 14px;
height: 14px;
font-size: 8px;
margin-right: 5px;
position: relative;
background-color: $white;
outline: none;
padding: 0;
cursor: pointer;
&:after {
font-size: 12px;
line-height: 10px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-weight: bold;
}
}
.square-plus:after {
content: "+";
}
.square-minus:after {
content: "-";
}
.app-layout {
background: $primary-bg;
min-width: 100vw;
@ -116,25 +158,73 @@ a {
transition-duration: 300ms;
}
.service-card {
position: relative;
min-height: 130px;
padding: 1.25rem 0;
background-color: $white;
.list-group .service-parent {
border-radius: 4px;
margin-bottom: 1rem;
border: 1px solid #eff2f7;
border-bottom-width: 0;
overflow: hidden;
.sub-service-wrapper {
border-top: 1px solid #eff2f7;
.service-card {
&:last-child {
padding-bottom: 0;
}
}
}
}
.list-group .service-card:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
.service-card {
position: relative;
padding: 1.25rem 0;
background-color: $white;
overflow: hidden;
&:not(:last-child) {
border-bottom: 1px solid #eff2f7;
}
.incidents-wrapper {
.incident-title {
display: flex;
.title-wrapper {
flex: 1;
}
.square-plus,
.square-minus {
top: 5px;
}
}
.incident-updates-wrapper {
margin-left: 3px;
.incident-wrapper {
position: relative;
margin-right: 3px;
&:not(:last-child):after {
content: "";
height: 100%;
width: 1px;
top: 21px;
left: 3px;
background-color: #bbb;
position: absolute;
}
&:last-child {
margin-bottom: 0 !important;
}
}
}
}
}
.list-group .service-card:last-child {
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
border-bottom-width: 1px;
margin-bottom: 0;
}
.service-card a {
@ -157,40 +247,6 @@ a {
&--right {
display: flex;
align-items: center;
.square-plus,
.square-minus {
color: $blue;
border: 2px solid $blue;
border-radius: 2px;
width: 14px;
height: 14px;
font-size: 8px;
margin-right: 5px;
position: relative;
background-color: $white;
outline: none;
padding: 0;
cursor: pointer;
&:after {
font-size: 12px;
line-height: 10px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-weight: bold;
}
}
.square-plus:after {
content: "+";
}
.square-minus:after {
content: "-";
}
}
}
@ -353,10 +409,12 @@ a {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 10px 15px;
.service-status {
display: flex;
align-items: center;
margin-right: 1.875rem;
&-badge {
width: 0.75rem;

View File

@ -1,5 +1,3 @@
// import DateUtils from "./DateUtils";
export function findStatus(data) {
if (!Array.isArray(data)) return null;
if (data.length === 0) return null;
@ -12,15 +10,18 @@ export function findStatus(data) {
return "";
}
// export function inRange(message) {
// return DateUtils.isBetween(
// DateUtils.now(),
// message.start_on,
// message.start_on === message.end_on
// ? DateUtils.maxDate().toISOString()
// : message.end_on
// );
// }
export function getIncidentTextType(type) {
switch (type.toLowerCase()) {
case "resolved":
return "text-success";
case "issue summary":
return "text-danger";
case "update":
return "text-warning";
default:
return "";
}
}
export const isObject = (obj) => {
if (Object.prototype.toString.call(obj) === "[object Object]") {
@ -59,13 +60,15 @@ export const calcPer = (uptime, downtime) => {
// }
export const setUerId = (id) => {
localStorage.setItem('stat_user_id',id);
}
localStorage.setItem("stat_user_id", id);
};
export const getUserId = () => {
return localStorage.getItem('stat_user_id');
}
return localStorage.getItem("stat_user_id");
};
export const generateUUID = (length) => {
return Array.from(Array(length), () => Math.floor(Math.random() * 36).toString(36)).join('')
}
return Array.from(Array(length), () =>
Math.floor(Math.random() * 36).toString(36)
).join("");
};