Merge pull request #25 from razorpay/PCORE-1632

PCORE-1632: implemented tree structure for statping
pull/1097/head
Smit Patel 2022-03-17 11:36:45 +05:30 committed by GitHub
commit 932f3fdb33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 552 additions and 4 deletions

View File

@ -28,6 +28,7 @@
"querystring": "^0.2.0",
"sass": "^1.26.10",
"semver": "^7.3.2",
"v-tooltip": "^2.1.3",
"vue": "^2.6.11",
"vue-apexcharts": "^1.6.0",
"vue-clipboard2": "^0.3.1",

View File

@ -303,6 +303,10 @@ class Api {
async downtime_delete (id) {
return axios.delete(`/api/downtimes/${id}`).then((response) => response.data);
}
async service_status (sec) {
return axios.get(`/api/services/status${sec && `?time=${sec}`}`).then((response) => response.data);
}
}
const api = new Api()
export default api

View File

@ -232,3 +232,113 @@ A {
top:0;
background: darken($card-background, 10%);
}
.tooltip {
display: block !important;
z-index: 10000;
.tooltip-inner {
background: black;
color: white;
border-radius: 5px;
padding: 5px 10px 4px;
}
.tooltip-arrow {
width: 0;
height: 0;
border-style: solid;
position: absolute;
margin: 5px;
border-color: black;
z-index: 1;
}
&[x-placement^="top"] {
margin-bottom: 5px;
.tooltip-arrow {
border-width: 5px 5px 0 5px;
border-left-color: transparent !important;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
bottom: -5px;
left: calc(50% - 5px);
margin-top: 0;
margin-bottom: 0;
}
}
&[x-placement^="bottom"] {
margin-top: 5px;
.tooltip-arrow {
border-width: 0 5px 5px 5px;
border-left-color: transparent !important;
border-right-color: transparent !important;
border-top-color: transparent !important;
top: -5px;
left: calc(50% - 5px);
margin-top: 0;
margin-bottom: 0;
}
}
&[x-placement^="right"] {
margin-left: 5px;
.tooltip-arrow {
border-width: 5px 5px 5px 0;
border-left-color: transparent !important;
border-top-color: transparent !important;
border-bottom-color: transparent !important;
left: -5px;
top: calc(50% - 5px);
margin-left: 0;
margin-right: 0;
}
}
&[x-placement^="left"] {
margin-right: 5px;
.tooltip-arrow {
border-width: 5px 0 5px 5px;
border-top-color: transparent !important;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
right: -5px;
top: calc(50% - 5px);
margin-left: 0;
margin-right: 0;
}
}
&.popover {
$color: #f9f9f9;
.popover-inner {
background: $color;
color: black;
padding: 24px;
border-radius: 5px;
box-shadow: 0 5px 30px rgba(black, .1);
}
.popover-arrow {
border-color: $color;
}
}
&[aria-hidden='true'] {
visibility: hidden;
opacity: 0;
transition: opacity .15s, visibility .15s;
}
&[aria-hidden='false'] {
visibility: visible;
opacity: 1;
transition: opacity .15s;
}
}

View File

@ -1,5 +1,13 @@
<template>
<div class="col-12">
<div class="col-12">
<div class="card contain-card mb-4">
<div class="card-header">
Services Tree View
</div>
<div class="card-body">
<ServiceTreeView />
</div>
</div>
<div class="card contain-card mb-4">
<div class="card-header">{{ $t('services') }}
@ -64,7 +72,6 @@
<FormGroup v-if="$store.state.admin" :edit="editChange" :in_group="group"/>
</div>
</template>
<script>
@ -73,7 +80,8 @@
const ToggleSwitch = () => import(/* webpackChunkName: "dashboard" */ '@/forms/ToggleSwitch')
const ServicesList = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/ServicesList')
import Api from "../../API";
const draggable = () => import(/* webpackChunkName: "dashboard" */ 'vuedraggable')
const draggable = () => import(/* webpackChunkName: "dashboard" */ 'vuedraggable');
const ServiceTreeView = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/ServiceTreeView');
export default {
name: 'DashboardServices',
@ -82,7 +90,8 @@
ServicesList,
ToggleSwitch,
FormGroup,
draggable
draggable,
ServiceTreeView
},
data() {
return {

View File

@ -0,0 +1,251 @@
<template>
<div>
<div class="row">
<div class="col-sm-6">
<form>
<div class="form-row">
<div class="form-group col-sm-6 mb-md-0">
<FlatPickr
id="dateTime"
ref="dateTimeRef"
v-model="dateTime"
type="text"
name="dateTime"
class="form-control form-control-plaintext"
:config="{
altFormat: 'J M, Y, h:iK',
altInput: true,
enableTime: true,
dateFormat: 'Z',
maxDate: new Date().toJSON(),
}"
placeholder="Select Start Date"
/>
</div>
<div class="form-group col-sm-6 mb-md-0">
<div role="group">
<button
type="submit"
class="btn btn-primary mr-1"
:disabled="dateTime === '' || isLoading"
@click.prevent="handleFilterSearch"
>
{{ $t('search') }}
</button>
<button
type="button"
class="btn btn-outline-secondary"
@click.prevent="handleClearFilter"
>
Reset
</button>
</div>
</div>
</div>
</form>
</div>
<div class="col-sm-6">
<ul class="d-flex justify-content-end align-items-center">
<li class="d-flex">
<div class="mr-1 text-shade-success">
<FontAwesomeIcon
icon="circle"
class="border border-secondary rounded-circle"
/>
</div>
<div>Up</div>
</li>
<li class="d-flex ml-3">
<div class="mr-1 text-shade-warning">
<FontAwesomeIcon
icon="circle"
class="border border-secondary rounded-circle"
/>
</div>
<div>Degraded</div>
</li>
<li class="d-flex ml-3">
<div class="mr-1 text-shade-danger">
<FontAwesomeIcon
icon="circle"
class="border border-secondary rounded-circle"
/>
</div>
<div>Down</div>
</li>
</ul>
</div>
</div>
<div class="mt-3">
<div v-if="isLoading">
<div class="col-12 text-center">
<FontAwesomeIcon
icon="circle-notch"
size="3x"
spin
/>
</div>
<div class="col-12 text-center mt-3 mb-3">
<span class="text-muted">
Loading Services
</span>
</div>
</div>
<ul
v-else
class="parent-list-group pl-0 mb-0 overflow-auto"
>
<TreeItem
v-for="service in treeData"
:key="service.id"
:item="service"
/>
</ul>
</div>
</div>
</template>
<script>
import TreeItem from '../Elements/TreeItem.vue';
import FlatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css';
const getRootNodes = (data) => {
if (!data || data.length === 0) {
return;
}
const rootNode = data.reduce((acc, service) => {
const isChild = data.find((item) => {
if (item.sub_services_detail) {
return Object.keys(item.sub_services_detail).includes(String(service.id));
}
return false;
});
if (!isChild) {
acc.push(service);
}
return acc;
}, []);
return rootNode;
};
const getTreeData = (parentServices, serviceStatus) => {
const treeData = [];
parentServices.forEach((parentService) => {
if (!parentService.sub_services_detail) {
treeData.push({ parent: parentService, children: [] });
} else {
const subServices = Object.keys(parentService.sub_services_detail).reduce((acc, key) => {
const service = serviceStatus.find((item) => item.id == key);
if (service) {
acc.push({ ...service, ...parentService.sub_services_detail[key] });
}
return acc;
}, []);
const children = getTreeData(subServices, serviceStatus);
treeData.push({ parent: parentService, children });
}
});
return treeData;
};
export default {
name: 'ServiceTreeView',
components: {
TreeItem,
FlatPickr
},
data: function () {
return {
isLoading: false,
dateTime: '',
treeData: [],
};
},
computed: {
serviceStatus () {
return this.$store.state.serviceStatus;
}
},
created: async function () {
await this.getServiceStatus(this.dateTime);
this.treeInitialize();
},
methods: {
treeInitialize: function () {
const rootNode = getRootNodes(this.serviceStatus);
const treeData = getTreeData(rootNode, this.serviceStatus);
this.treeData = treeData;
},
getServiceStatus: async function (dateTime) {
let sec = null;
this.isLoading = true;
if (!dateTime) {
sec = '';
} else {
sec = this.convertDateObjToSec(dateTime);
}
await this.$store.dispatch({ type: 'getServiceStatus', payload: sec });
this.treeInitialize();
this.isLoading = false;
},
handleFilterSearch: function () {
// Remove focus and close date time picker on enter click.
this.$refs.dateTimeRef.$el.nextSibling.blur();
this.$refs.dateTimeRef.fp.close();
this.getServiceStatus(this.dateTime);
},
handleClearFilter: function () {
this.dateTime = new Date().toJSON();
}
}
};
</script>
<style scoped>
ul, li {
margin: 0;
padding: 0;
list-style: none;
display: block;
}
.text-shade-danger {
color: #f7cecc;
border: red
}
.text-shade-warning {
color: #FFE6CC;
}
.text-shade-success {
color: #D6E7D4;
}
.parent-list-group li {
position: relative;
}
</style>

View File

@ -0,0 +1,156 @@
<template>
<li
v-if="item"
>
<button
class="toggle btn btn-sm d-flex align-items-stretch px-0"
type="button"
@click="handleIsOpen"
>
<div
v-if="item.children.length > 0"
class="toggle-icon pl-2 pr-2 border border-dark rounded-left bg-white d-flex align-items-center"
>
<FontAwesomeIcon
v-if="!isOpen"
icon="plus-circle"
class="fa-lg"
/>
<FontAwesomeIcon
v-if="isOpen"
icon="minus-circle"
class="fa-lg"
/>
</div>
<div
v-tooltip="{ content: `${item.parent.downtime ? `${niceDateWithYear(startDate)} - ${niceDateWithYear(endDate)}` : ''}`, offset: 5, autoHide: true}"
class="parent-name rounded-right"
:class="[{ 'cursor-text rounded': item.children.length === 0 }, item.parent.downtime ? downtimeStatus === 'down' ? 'bg-shade-danger' : 'bg-shade-warning' : 'bg-shade-success']"
>
{{ item.parent.name }}
</div>
</button>
<ul
v-if="item.children.length > 0 && isOpen"
class="list-child pl-0"
>
<TreeItem
v-for="service in item.children"
:key="service.id"
class="item"
:item="service"
/>
</ul>
</li>
</template>
<script>
export default {
name: 'TreeItem',
props: {
item: {
type: Object,
default: () => null
}
},
data: function () {
return {
isOpen: false,
};
},
computed: {
startDate: function () {
return this.item.parent.downtime?.start;
},
endDate: function () {
return this.item.parent.downtime?.end;
},
downtimeStatus: function () {
return this.item.parent.downtime?.sub_status;
}
},
methods: {
handleIsOpen: function () {
this.isOpen = !this.isOpen;
}
}
};
</script>
<style scoped>
ul, li {
margin: 0;
padding: 0;
list-style: none;
display: block;
}
.bg-shade-success {
background-color: #D6E7D4;
}
.bg-shade-danger {
background-color: #f7cecc;
}
.bg-shade-warning {
background-color: #FFE6CC;
}
.cursor-text {
cursor: text;
}
.toggle {
line-height: 2 !important;
}
.toggle .toggle-icon {
border-right: 0 !important;
z-index: 1;
}
.toggle .parent-name {
font-size: 16px;
padding: 0px 5px;
border: 1px solid;
}
.toggle:focus {
box-shadow: none;
}
.list-child {
margin-left: 13px;
min-width: 195px;
}
.list-child li {
padding: 0 0 0 28px;
position: relative;
}
.list-child li::before {
content: "";
position: absolute;
top: 0px;
left: 5px;
border-left: 1px solid #A9A9A9;
border-bottom: 1px solid #A9A9A9;
width: 15px;
height: 22px;
border-radius: 0 0 0 0.3em;
}
.list-child li:not(:last-child)::after{
position: absolute;
content: "";
top: 5px;
left: 5px;
border-left: 1px solid #A9A9A9;
width: 20px;
height: 100%;
}
</style>

View File

@ -5,6 +5,7 @@ import VueObserveVisibility from 'vue-observe-visibility'
import VueClipboard from 'vue-clipboard2'
import VueCookies from 'vue-cookies'
import VueI18n from 'vue-i18n'
import VTooltip from 'v-tooltip'
import router from './routes'
import "./mixin"
import "./icons"
@ -20,6 +21,7 @@ Vue.use(VueRouter);
Vue.use(VueObserveVisibility);
Vue.use(VueCookies);
Vue.use(VueI18n);
Vue.use(VTooltip);
const i18n = new VueI18n({
fallbackLocale: "en",

View File

@ -258,7 +258,14 @@ export default Vue.mixin({
return addSeconds(date, amount)
},
niceDateWithYear (val) {
if(!val) {
return '';
}
return format(parseISO(val), 'do MMM, yyyy h:mma');
},
convertDateObjToSec (val) {
return Math.floor((new Date(val).getTime())/1000);
}
}
});

View File

@ -33,6 +33,7 @@ export default new Vuex.Store({
admin: false,
user: false,
loggedIn: false,
serviceStatus: [],
modal: {
visible: false,
title: "Modal Header",
@ -155,6 +156,9 @@ export default new Vuex.Store({
},
setDowntimes (state, downtimes) {
state.downtimes = downtimes;
},
setServiceStatus (state, serviceStatus) {
state.serviceStatus = serviceStatus;
}
},
actions: {
@ -166,6 +170,10 @@ export default new Vuex.Store({
const { output } = await Api.downtimes(payload);
context.commit('setDowntimes', output ?? []);
},
async getServiceStatus (context, { payload }) {
const { output } = await Api.service_status(payload);
context.commit("setServiceStatus", output ?? []);
},
async loadCore(context) {
const core = await Api.core()
const token = await Api.token()