mirror of
https://github.com/apache/skywalking-booster-ui.git
synced 2025-10-15 12:49:17 +00:00
feat: Implement the Event widget (#112)
This commit is contained in:
129
src/views/dashboard/related/event/Content.vue
Normal file
129
src/views/dashboard/related/event/Content.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You 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. -->
|
||||
<template>
|
||||
<div ref="timeline" class="events"></div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import { useThrottleFn } from "@vueuse/core";
|
||||
import { useEventStore } from "@/store/modules/event";
|
||||
import { DataSet, Timeline } from "vis-timeline/standalone";
|
||||
import "vis-timeline/styles/vis-timeline-graph2d.css";
|
||||
|
||||
const eventStore = useEventStore();
|
||||
/*global Nullable */
|
||||
const timeline = ref<Nullable<HTMLDivElement>>(null);
|
||||
const visGraph = ref<Nullable<any>>(null);
|
||||
const oldVal = ref<{ width: number; height: number }>({ width: 0, height: 0 });
|
||||
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
|
||||
new Date(dayjs(date).format(pattern));
|
||||
const visDate = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
|
||||
dayjs(date).format(pattern);
|
||||
|
||||
onMounted(() => {
|
||||
oldVal.value = (timeline.value && timeline.value.getBoundingClientRect()) || {
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
useThrottleFn(resize, 500)();
|
||||
});
|
||||
|
||||
function visTimeline() {
|
||||
if (!timeline.value) {
|
||||
return;
|
||||
}
|
||||
if (visGraph.value) {
|
||||
visGraph.value.destroy();
|
||||
}
|
||||
const h = timeline.value.getBoundingClientRect().height;
|
||||
const events = eventStore.events.map((d, index) => {
|
||||
return {
|
||||
id: index + 1,
|
||||
content: d.name,
|
||||
start: dateFormat(d.startTime),
|
||||
end: dateFormat(d.endTime),
|
||||
data: d,
|
||||
className: d.type,
|
||||
};
|
||||
});
|
||||
const items: any = new DataSet(events);
|
||||
const options: any = {
|
||||
height: h,
|
||||
width: "100%",
|
||||
locale: "en",
|
||||
groupHeightMode: "fitItems",
|
||||
autoResize: false,
|
||||
tooltip: {
|
||||
overflowMethod: "cap",
|
||||
template(item) {
|
||||
const data = item.data || {};
|
||||
let tmp = `<div>ID: ${data.uuid || ""}</div>
|
||||
<div>Name: ${data.name || ""}</div>
|
||||
<div>Event Type: ${data.type || ""}</div>
|
||||
<div>Start Time: ${data.startTime ? visDate(data.startTime) : ""}</div>
|
||||
<div>End Time: ${data.endTime ? visDate(data.endTime) : ""}</div>
|
||||
<div>Message: ${data.message || ""}</div>
|
||||
<div>Service: ${data.source.service || ""}</div>`;
|
||||
if (data.source.endpoint) {
|
||||
tmp += `<div>Endpoint: ${data.source.endpoint}</div>`;
|
||||
}
|
||||
if (data.source.instance) {
|
||||
tmp += `<div>Service Instance: ${data.source.instance}</div>`;
|
||||
}
|
||||
return tmp;
|
||||
},
|
||||
},
|
||||
};
|
||||
visGraph.value = new Timeline(timeline.value, items, options);
|
||||
}
|
||||
function resize() {
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
const cr = entry.contentRect;
|
||||
if (
|
||||
Math.abs(cr.width - oldVal.value.width) < 3 &&
|
||||
Math.abs(cr.height - oldVal.value.height) < 3
|
||||
) {
|
||||
return;
|
||||
}
|
||||
visTimeline();
|
||||
oldVal.value = { width: cr.width, height: cr.height };
|
||||
});
|
||||
if (timeline.value) {
|
||||
observer.observe(timeline.value);
|
||||
}
|
||||
}
|
||||
watch(
|
||||
() => eventStore.events,
|
||||
() => {
|
||||
visTimeline();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.events {
|
||||
width: calc(100% - 5px);
|
||||
margin: 0 5px 5px 0;
|
||||
height: 100%;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 400px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
235
src/views/dashboard/related/event/Header.vue
Normal file
235
src/views/dashboard/related/event/Header.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You 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. -->
|
||||
<template>
|
||||
<div class="flex-h row">
|
||||
<div class="mr-5" v-if="dashboardStore.entity !== EntityType[3].value">
|
||||
<span class="grey mr-5"> {{ t("instance") }}: </span>
|
||||
<Selector
|
||||
size="small"
|
||||
:value="state.instance.value"
|
||||
:options="eventStore.instances"
|
||||
placeholder="Select a instance"
|
||||
@change="changeField('instance', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="mr-5" v-if="dashboardStore.entity !== EntityType[2].value">
|
||||
<span class="grey mr-5"> {{ t("endpoint") }}: </span>
|
||||
<Selector
|
||||
size="small"
|
||||
:value="state.endpoint.value"
|
||||
:options="eventStore.endpoints"
|
||||
placeholder="Select a endpoint"
|
||||
@change="changeField('endpoint', $event)"
|
||||
:isRemote="true"
|
||||
@query="searchEndpoints"
|
||||
/>
|
||||
</div>
|
||||
<div class="mr-5">
|
||||
<span class="grey">{{ t("eventsType") }}: </span>
|
||||
<Selector
|
||||
v-model="state.eventType"
|
||||
:options="EventTypes"
|
||||
placeholder="Select a type"
|
||||
@change="changeField('eventType', $event)"
|
||||
class="event-tool-input"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<el-pagination
|
||||
v-model:currentPage="pageNum"
|
||||
v-model:page-size="pageSize"
|
||||
layout="prev, pager, next"
|
||||
:total="total"
|
||||
@current-change="updatePage"
|
||||
:pager-count="5"
|
||||
small
|
||||
/>
|
||||
<el-button
|
||||
class="search-btn"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="queryEvents"
|
||||
>
|
||||
{{ t("search") }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, reactive, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useEventStore } from "@/store/modules/event";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
import { useSelectorStore } from "@/store/modules/selectors";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { EntityType } from "../../data";
|
||||
import { EventTypes } from "./data";
|
||||
|
||||
/*global defineProps */
|
||||
const props = defineProps({
|
||||
needQuery: { type: Boolean, default: true },
|
||||
});
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStoreWithOut();
|
||||
const selectorStore = useSelectorStore();
|
||||
const dashboardStore = useDashboardStore();
|
||||
const eventStore = useEventStore();
|
||||
const pageSize = 20;
|
||||
const pageNum = ref<number>(1);
|
||||
const state = reactive<any>({
|
||||
instance: { value: "", label: "All", id: "" },
|
||||
endpoint: { value: "", label: "All", id: "" },
|
||||
eventType: { value: "", label: "All" },
|
||||
});
|
||||
const total = computed(() =>
|
||||
eventStore.events.length === pageSize
|
||||
? pageSize * pageNum.value + 1
|
||||
: pageSize * pageNum.value
|
||||
);
|
||||
if (props.needQuery) {
|
||||
init();
|
||||
}
|
||||
async function init() {
|
||||
fetchSelectors();
|
||||
await queryEvents();
|
||||
state.instance = { value: "", label: "All" };
|
||||
state.endpoint = { value: "", label: "All" };
|
||||
}
|
||||
|
||||
function fetchSelectors() {
|
||||
if (dashboardStore.entity === EntityType[2].value) {
|
||||
getInstances();
|
||||
return;
|
||||
}
|
||||
if (dashboardStore.entity === EntityType[3].value) {
|
||||
getEndpoints();
|
||||
return;
|
||||
}
|
||||
if (dashboardStore.entity === EntityType[0].value) {
|
||||
getInstances();
|
||||
getEndpoints();
|
||||
}
|
||||
}
|
||||
|
||||
async function getEndpoints(id?: string) {
|
||||
const resp = await eventStore.getEndpoints(id);
|
||||
if (resp.errors) {
|
||||
ElMessage.error(resp.errors);
|
||||
return;
|
||||
}
|
||||
state.endpoint = eventStore.endpoints[0];
|
||||
}
|
||||
async function getInstances(id?: string) {
|
||||
const resp = await eventStore.getInstances(id);
|
||||
if (resp.errors) {
|
||||
ElMessage.error(resp.errors);
|
||||
return;
|
||||
}
|
||||
state.instance = eventStore.instances[0];
|
||||
}
|
||||
async function queryEvents() {
|
||||
let endpoint = state.endpoint.value,
|
||||
instance = state.instance.value;
|
||||
if (dashboardStore.entity === EntityType[2].value) {
|
||||
endpoint = selectorStore.currentPod.id;
|
||||
}
|
||||
if (dashboardStore.entity === EntityType[3].value) {
|
||||
instance = selectorStore.currentPod.id;
|
||||
}
|
||||
eventStore.setEventCondition({
|
||||
// layer: dashboardStore.layerId,
|
||||
paging: {
|
||||
pageNum: pageNum.value,
|
||||
pageSize: pageSize,
|
||||
},
|
||||
source: {
|
||||
service: selectorStore.currentService.value || "",
|
||||
endpoint: endpoint || "",
|
||||
serviceInstance: instance || "",
|
||||
},
|
||||
type: state.eventType.value || undefined,
|
||||
});
|
||||
const res = await eventStore.getEvents();
|
||||
if (res && res.errors) {
|
||||
ElMessage.error(res.errors);
|
||||
}
|
||||
}
|
||||
function changeField(type: string, opt: any[]) {
|
||||
state[type] = opt[0];
|
||||
}
|
||||
async function searchEndpoints(keyword: string) {
|
||||
const resp = await eventStore.getEndpoints(keyword);
|
||||
if (resp.errors) {
|
||||
ElMessage.error(resp.errors);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePage(p: number) {
|
||||
pageNum.value = p;
|
||||
queryEvents();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [selectorStore.currentService],
|
||||
() => {
|
||||
if (dashboardStore.entity === EntityType[0].value) {
|
||||
init();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [selectorStore.currentPod],
|
||||
() => {
|
||||
if (dashboardStore.entity === EntityType[0].value) {
|
||||
return;
|
||||
}
|
||||
init();
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => appStore.durationTime,
|
||||
() => {
|
||||
if (dashboardStore.entity === EntityType[1].value) {
|
||||
init();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.inputs {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.inputs-max {
|
||||
width: 270px;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
cursor: pointer;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.selected {
|
||||
display: inline-block;
|
||||
padding: 0 3px;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
color: #3d444f;
|
||||
border: 1px dashed #aaa;
|
||||
font-size: 12px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
</style>
|
30
src/views/dashboard/related/event/data.ts
Normal file
30
src/views/dashboard/related/event/data.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 const EventsDetailKeys = [
|
||||
{ text: "eventID", class: "uuid" },
|
||||
{ text: "eventName", class: "name" },
|
||||
{ text: "eventsType", class: "type" },
|
||||
{ text: "startTime", class: "startTime" },
|
||||
{ text: "endTime", class: "endTime" },
|
||||
{ text: "eventsMessage", class: "message" },
|
||||
{ text: "eventSource", class: "source" },
|
||||
];
|
||||
export const EventTypes = [
|
||||
{ label: "All", value: "" },
|
||||
{ label: "Normal", value: "Normal" },
|
||||
{ label: "Error", value: "Error" },
|
||||
];
|
Reference in New Issue
Block a user