feat: the log widget and the trace widget associate with each other, remove log tables on the trace widget (#128)

This commit is contained in:
Fine0830 2022-07-27 16:24:34 +08:00 committed by GitHub
parent 673b1a41a8
commit 2ba3c67d31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 230 additions and 168 deletions

View File

@ -14,24 +14,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ElMessage } from "element-plus";
import { useDashboardStore } from "@/store/modules/dashboard";
export default function getDashboard(param: {
import { LayoutConfig } from "@/types/dashboard";
export default function getDashboard(param?: {
name: string;
layer: string;
entity: string;
}) {
const dashboardStore = useDashboardStore();
const opt = param || dashboardStore.currentDashboard;
const list = JSON.parse(sessionStorage.getItem("dashboards") || "[]");
const dashboard = list.find(
(d: { name: string; layer: string; entity: string }) =>
d.name === param.name &&
d.entity === param.entity &&
d.layer === param.layer
d.name === opt.name && d.entity === opt.entity && d.layer === opt.layer
);
const all = dashboardStore.layout;
const widgets = [];
const widgets: LayoutConfig[] = [];
for (const item of all) {
if (item.type === "Tab") {
widgets.push(item);
if (item.children && item.children.length) {
for (const child of item.children) {
if (child.children && child.children.length) {
@ -43,5 +46,36 @@ export default function getDashboard(param: {
widgets.push(item);
}
}
return { dashboard, widgets };
function associationWidget(sourceId: string, filters: unknown, type: string) {
const widget = widgets.find((d: { type: string }) => d.type === type);
if (!widget) {
return ElMessage.info(`There has no a ${type} widget in the dashboard`);
}
const item = {
...widget,
filters,
};
dashboardStore.setWidget(item);
const targetTabIndex = (widget.id || "").split("-");
const sourceTabindex = (sourceId || "").split("-") || [];
let container: Nullable<Element>;
if (targetTabIndex[1] === undefined) {
container = document.querySelector(".ds-main");
} else {
const w = widgets.find((d: any) => d.id === targetTabIndex[0]);
container = document.querySelector(".tab-layout");
const layout: Nullable<Element> = document.querySelector(".ds-main");
if (w && layout) {
layout.scrollTop = w.y * 10 + w.h * 5;
}
}
if (targetTabIndex[1] && targetTabIndex[1] !== sourceTabindex[1]) {
dashboardStore.setActiveTabIndex(Number(targetTabIndex[1]));
}
if (container && widget) {
container.scrollTop = widget.y * 10 + widget.h * 5;
}
}
return { dashboard, widgets, associationWidget };
}

View File

@ -39,6 +39,7 @@ interface DashboardState {
dashboards: DashboardItem[];
currentDashboard: Nullable<DashboardItem>;
editMode: boolean;
currentTabIndex: number;
}
export const dashboardStore = defineStore({
@ -56,6 +57,7 @@ export const dashboardStore = defineStore({
dashboards: [],
currentDashboard: null,
editMode: false,
currentTabIndex: 0,
}),
actions: {
setLayout(data: LayoutConfig[]) {
@ -189,6 +191,7 @@ export const dashboardStore = defineStore({
this.activedGridItem = index;
},
setActiveTabIndex(index: number, target?: number) {
this.currentTabIndex = index;
const m = target || this.activedGridItem;
const idx = this.layout.findIndex((d: LayoutConfig) => d.i === m);
if (idx < 0) {

View File

@ -63,6 +63,14 @@ export const traceStore = defineStore({
setTraceSpans(spans: Span) {
this.traceSpans = spans;
},
resetCondition() {
this.conditions = {
queryDuration: useAppStoreWithOut().durationTime,
paging: { pageNum: 1, pageSize: 20 },
traceState: "ALL",
queryOrder: "BY_START_TIME",
};
},
async getServices(layer: string) {
const res: AxiosResponse = await graphql.query("queryServices").params({
layer,

View File

@ -47,6 +47,9 @@ export interface LayoutConfig {
startTime: string;
endTime: string;
};
traceId?: string;
spanId?: string;
segmentId?: string;
};
}

View File

@ -15,11 +15,7 @@ limitations under the License. -->
<template>
<div>
<span class="grey">{{ t("tags") }}: </span>
<span
v-if="tagsList.length"
class="trace-tags"
:style="type === 'LOG' ? `min-width: 122px;` : ''"
>
<span v-if="tagsList.length" class="trace-tags">
<span class="selected" v-for="(item, index) in tagsList" :key="index">
<span>{{ item }}</span>
<span class="remove-icon" @click="removeTags(index)">×</span>
@ -33,13 +29,7 @@ limitations under the License. -->
@change="addLabels"
:placeholder="t('addTags')"
/>
<el-popover
v-else
trigger="click"
style="margin: 10px 0 0 -130px"
:visible="visible"
width="300px"
>
<el-popover v-else trigger="click" :visible="visible" width="300px">
<template #reference>
<el-input
size="small"
@ -226,7 +216,6 @@ watch(
padding: 2px 5px;
border-radius: 3px;
width: 250px;
z-index: 999;
}
.remove-icon {

View File

@ -30,7 +30,7 @@ limitations under the License. -->
</div>
</el-popover>
<div class="header">
<Header :needQuery="needQuery" />
<Header :needQuery="needQuery" :data="data" />
</div>
<div class="log">
<List />
@ -38,20 +38,24 @@ limitations under the License. -->
</div>
</template>
<script lang="ts" setup>
import { provide } from "vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
import Header from "../related/log/Header.vue";
import List from "../related/log/List.vue";
import { LayoutConfig } from "@/types/dashboard";
import type { PropType } from "vue";
/*global defineProps */
const props = defineProps({
data: {
type: Object,
default: () => ({}),
type: Object as PropType<LayoutConfig>,
default: () => ({ graph: {} }),
},
activeIndex: { type: String, default: "" },
needQuery: { type: Boolean, default: true },
});
provide("options", props.data);
const { t } = useI18n();
const dashboardStore = useDashboardStore();
@ -72,6 +76,7 @@ function removeWidget() {
position: absolute;
top: 5px;
right: 3px;
z-index: 1000;
}
.header {

View File

@ -259,6 +259,26 @@ export default defineComponent({
}
}
);
watch(
() => dashboardStore.currentTabIndex,
() => {
activeTabIndex.value = dashboardStore.currentTabIndex;
dashboardStore.activeGridItem(props.data.i);
dashboardStore.selectWidget(props.data);
const l = dashboardStore.layout.findIndex(
(d: LayoutConfig) => d.i === props.data.i
);
dashboardStore.setCurrentTabItems(
dashboardStore.layout[l].children[activeTabIndex.value].children
);
needQuery.value = true;
if (route.params.activeTabIndex) {
let p = location.href.split("/tab/")[0];
p = p + "/tab/" + activeTabIndex.value;
history.replaceState({}, "", p);
}
}
);
return {
handleClick,
layoutUpdatedEvent,

View File

@ -30,7 +30,7 @@ limitations under the License. -->
</div>
</el-popover>
<div class="header">
<Filter :needQuery="needQuery" />
<Filter :needQuery="needQuery" :data="data" />
</div>
<div class="trace flex-h">
<TraceList />
@ -39,6 +39,7 @@ limitations under the License. -->
</div>
</template>
<script lang="ts" setup>
import { provide } from "vue";
import type { PropType } from "vue";
import Filter from "../related/trace/Filter.vue";
import TraceList from "../related/trace/TraceList.vue";
@ -55,6 +56,7 @@ const props = defineProps({
activeIndex: { type: String, default: "" },
needQuery: { type: Boolean, default: true },
});
provide("options", props.data);
const { t } = useI18n();
const dashboardStore = useDashboardStore();
function removeWidget() {

View File

@ -168,7 +168,7 @@ export default defineComponent({
for (const item of associate) {
const widget = widgets.find(
(d: { id: string }) => d.id === item.widgetId
(d: LayoutConfig) => d.id === item.widgetId
);
if (widget) {
widget.filters = {

View File

@ -131,6 +131,7 @@ limitations under the License. -->
</template>
<script lang="ts" setup>
import { ref, reactive, watch, onUnmounted } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { Option } from "@/types/app";
import { useLogStore } from "@/store/modules/log";
@ -141,17 +142,24 @@ import ConditionTags from "@/views/components/ConditionTags.vue";
import { ElMessage } from "element-plus";
import { EntityType } from "../../data";
import { ErrorCategory } from "./data";
import { LayoutConfig } from "@/types/dashboard";
/*global defineProps */
/*global defineProps, Recordable */
const props = defineProps({
needQuery: { type: Boolean, default: true },
data: {
type: Object as PropType<LayoutConfig>,
default: () => ({ graph: {} }),
},
});
const { t } = useI18n();
const appStore = useAppStoreWithOut();
const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore();
const logStore = useLogStore();
const traceId = ref<string>("");
const traceId = ref<string>(
(props.data.filters && props.data.filters.traceId) || ""
);
const keywordsOfContent = ref<string[]>([]);
const excludingKeywordsOfContent = ref<string[]>([]);
const tagsList = ref<string[]>([]);
@ -159,7 +167,7 @@ const tagsMap = ref<Option[]>([]);
const contentStr = ref<string>("");
const excludingContentStr = ref<string>("");
const isBrowser = ref<boolean>(dashboardStore.layerId === "BROWSER");
const state = reactive<any>({
const state = reactive<Recordable>({
instance: { value: "0", label: "All" },
endpoint: { value: "0", label: "All" },
service: { value: "", label: "" },
@ -248,6 +256,11 @@ function searchLogs() {
category: state.category.value,
});
} else {
let segmentId, spanId;
if (props.data.filters) {
segmentId = props.data.filters.segmentId;
spanId = props.data.filters.spanId;
}
logStore.setLogCondition({
serviceId: selectorStore.currentService
? selectorStore.currentService.id
@ -259,7 +272,9 @@ function searchLogs() {
excludingKeywordsOfContent: excludingKeywordsOfContent.value,
tags: tagsMap.value.length ? tagsMap.value : undefined,
paging: { pageNum: 1, pageSize: 15 },
relatedTrace: traceId.value ? { traceId: traceId.value } : undefined,
relatedTrace: traceId.value
? { traceId: traceId.value, segmentId, spanId }
: undefined,
});
}
queryLogs();
@ -325,6 +340,12 @@ function removeExcludeContent(index: number) {
}
onUnmounted(() => {
logStore.resetCondition();
const item = {
...props.data,
filters: undefined,
};
dashboardStore.setWidget(item);
traceId.value = "";
});
watch(
() => selectorStore.currentService,
@ -351,6 +372,19 @@ watch(
}
}
);
watch(
() => props.data.filters,
(newJson, oldJson) => {
console.log(props.data.filters);
if (props.data.filters) {
if (JSON.stringify(newJson) === JSON.stringify(oldJson)) {
return;
}
traceId.value = props.data.filters.traceId || "";
init();
}
}
);
</script>
<style lang="scss" scoped>
.inputs {

View File

@ -18,7 +18,7 @@ limitations under the License. -->
v-loading="logStore.loadLogs"
:tableData="logStore.logs || []"
:type="type"
:noLink="true"
:noLink="false"
>
<div class="log-tips" v-if="!logStore.logs.length">{{ t("noData") }}</div>
</LogTable>
@ -39,7 +39,7 @@ limitations under the License. -->
<script lang="ts" setup>
import { ref, computed } from "vue";
import { useI18n } from "vue-i18n";
import LogTable from "@/views/dashboard/related/components/LogTable/Index.vue";
import LogTable from "./LogTable/Index.vue";
import { useLogStore } from "@/store/modules/log";
import { useDashboardStore } from "@/store/modules/dashboard";
import { ElMessage } from "element-plus";

View File

@ -14,46 +14,68 @@ See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div @click="showSelectSpan" class="log-item">
<div class="log-item">
<div v-for="(item, index) in columns" :key="index" :class="item.label">
<span v-if="item.label === 'timestamp'">
<span v-if="item.label === 'timestamp'" @click="showSelectSpan">
{{ dateFormat(data.timestamp) }}
</span>
<span v-else-if="item.label === 'tags'">
<span v-else-if="item.label === 'tags'" @click="showSelectSpan">
{{ tags }}
</span>
<!-- <router-link
<span
v-else-if="item.label === 'traceId' && !noLink"
:to="{ name: 'trace', query: { traceid: data[item.label] } }"
:class="noLink ? '' : 'blue'"
@click="linkTrace(data[item.label])"
>
<span :class="noLink ? '' : 'blue'">{{ data[item.label] }}</span>
</router-link> -->
<span v-else>{{ data[item.label] }}</span>
{{ data[item.label] }}
</span>
<span v-else @click="showSelectSpan">{{ data[item.label] }}</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { computed, inject } from "vue";
import dayjs from "dayjs";
import { ServiceLogConstants } from "./data";
/*global defineProps, defineEmits */
import getDashboard from "@/hooks/useDashboardsSession";
import { useDashboardStore } from "@/store/modules/dashboard";
import { LayoutConfig } from "@/types/dashboard";
/*global defineProps, defineEmits, Recordable */
const props = defineProps({
data: { type: Object as any, default: () => ({}) },
noLink: { type: Boolean, default: true },
});
const dashboardStore = useDashboardStore();
const options: Recordable<LayoutConfig> = inject("options") || {};
const emit = defineEmits(["select"]);
const columns = ServiceLogConstants;
const tags = computed(() => {
if (!props.data.tags) {
return "";
}
return String(props.data.tags.map((d: any) => `${d.key}=${d.value}`));
return String(
props.data.tags.map(
(d: { key: string; value: string }) => `${d.key}=${d.value}`
)
);
});
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
function showSelectSpan() {
emit("select", props.data);
}
function linkTrace(id: string) {
const { associationWidget } = getDashboard(dashboardStore.currentDashboard);
associationWidget(
(options.id as any) || "",
{
sourceId: options.id || "",
traceId: id,
},
"Trace"
);
}
</script>
<style lang="scss" scoped>
.log-item {

View File

@ -40,14 +40,14 @@ export const ServiceLogConstants = [
label: "tags",
value: "tags",
},
{
label: "content",
value: "content",
},
{
label: "traceId",
value: "traceID",
},
{
label: "content",
value: "content",
},
];
export const ServiceLogDetail = [
{

View File

@ -33,33 +33,6 @@ limitations under the License. -->
{{ t("viewLogs") }}
</el-button>
</div>
<el-dialog
v-model="showTraceLogs"
:destroy-on-close="true"
fullscreen
@closed="showTraceLogs = false"
>
<div>
<el-pagination
v-model:currentPage="pageNum"
v-model:page-size="pageSize"
:small="true"
layout="prev, pager, next"
:pager-count="5"
:total="total"
@current-change="turnLogsPage"
/>
<LogTable
:tableData="traceStore.traceSpanLogs || []"
:type="`service`"
:noLink="true"
>
<div class="log-tips" v-if="!traceStore.traceSpanLogs.length">
{{ t("noData") }}
</div>
</LogTable>
</div>
</el-dialog>
</h5>
<div class="mb-5 blue sm">
<Selector
@ -148,37 +121,31 @@ limitations under the License. -->
</template>
<script lang="ts">
import dayjs from "dayjs";
import { ref, defineComponent, computed } from "vue";
import { ref, defineComponent, inject } from "vue";
import { useI18n } from "vue-i18n";
import { useTraceStore } from "@/store/modules/trace";
import { Option } from "@/types/app";
import copy from "@/utils/copy";
import graphs from "./components/index";
import LogTable from "@/views/dashboard/related/components/LogTable/Index.vue";
import { ElMessage } from "element-plus";
import getDashboard from "@/hooks/useDashboardsSession";
import { LayoutConfig } from "@/types/dashboard";
export default defineComponent({
name: "TraceDetail",
components: {
...graphs,
LogTable,
},
setup() {
/*global Recordable */
const options: Recordable<LayoutConfig> = inject("options") || {};
const { t } = useI18n();
const traceStore = useTraceStore();
const loading = ref<boolean>(false);
const traceId = ref<string>("");
const displayMode = ref<string>("List");
const pageNum = ref<number>(1);
const pageSize = 10;
const total = computed(() =>
traceStore.traceList.length === pageSize
? pageSize * pageNum.value + 1
: pageSize * pageNum.value
);
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
const showTraceLogs = ref<boolean>(false);
function handleClick() {
copy(traceId.value || traceStore.currentTrace.traceIds[0].value);
@ -195,23 +162,15 @@ export default defineComponent({
}
async function searchTraceLogs() {
showTraceLogs.value = true;
const res = await traceStore.getSpanLogs({
condition: {
relatedTrace: {
traceId: traceId.value || traceStore.currentTrace.traceIds[0].value,
},
paging: { pageNum: pageNum.value, pageSize },
const { associationWidget } = getDashboard();
associationWidget(
(options.id as any) || "",
{
sourceId: options?.id || "",
traceId: traceId.value || traceStore.currentTrace.traceIds[0].value,
},
});
if (res.errors) {
ElMessage.error(res.errors);
}
}
function turnLogsPage(page: number) {
pageNum.value = page;
searchTraceLogs();
"Log"
);
}
return {
traceStore,
@ -221,12 +180,7 @@ export default defineComponent({
handleClick,
t,
searchTraceLogs,
showTraceLogs,
turnLogsPage,
pageSize,
pageNum,
loading,
total,
traceId,
};
},

View File

@ -92,7 +92,8 @@ limitations under the License. -->
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from "vue";
import { ref, reactive, watch, onUnmounted } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { Option } from "@/types/app";
import { Status } from "../../data";
@ -103,22 +104,29 @@ import { useSelectorStore } from "@/store/modules/selectors";
import ConditionTags from "@/views/components/ConditionTags.vue";
import { ElMessage } from "element-plus";
import { EntityType } from "../../data";
import { LayoutConfig } from "@/types/dashboard";
/*global defineProps */
/*global defineProps, Recordable */
const props = defineProps({
needQuery: { type: Boolean, default: true },
data: {
type: Object as PropType<LayoutConfig>,
default: () => ({ graph: {} }),
},
});
const traceId = ref<string>(
(props.data.filters && props.data.filters.traceId) || ""
);
const { t } = useI18n();
const appStore = useAppStoreWithOut();
const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore();
const traceStore = useTraceStore();
const traceId = ref<string>("");
const minTraceDuration = ref<number>();
const maxTraceDuration = ref<number>();
const tagsList = ref<string[]>([]);
const tagsMap = ref<Option[]>([]);
const state = reactive<any>({
const state = reactive<Recordable>({
status: { label: "All", value: "ALL" },
instance: { value: "0", label: "All" },
endpoint: { value: "0", label: "All" },
@ -222,6 +230,15 @@ async function searchEndpoints(keyword: string) {
ElMessage.error(resp.errors);
}
}
onUnmounted(() => {
traceStore.resetCondition();
const item = {
...props.data,
filters: undefined,
};
dashboardStore.setWidget(item);
traceId.value = "";
});
watch(
() => [selectorStore.currentPod],
() => {
@ -248,6 +265,18 @@ watch(
}
}
);
watch(
() => props.data.filters,
(newJson, oldJson) => {
if (props.data.filters) {
if (JSON.stringify(newJson) === JSON.stringify(oldJson)) {
return;
}
traceId.value = props.data.filters.traceId || "";
init();
}
}
);
</script>
<style lang="scss" scoped>
.inputs {

View File

@ -81,77 +81,36 @@ limitations under the License. -->
{{ t("relatedTraceLogs") }}
</el-button>
</div>
<el-dialog
v-model="showRelatedLogs"
:destroy-on-close="true"
fullscreen
@closed="showRelatedLogs = false"
>
<el-pagination
v-model:currentPage="pageNum"
v-model:page-size="pageSize"
:small="true"
layout="prev, pager, next"
:pager-count="5"
:total="total"
@current-change="turnPage"
/>
<LogTable
:tableData="traceStore.traceSpanLogs || []"
:type="`service`"
:noLink="true"
>
<div class="log-tips" v-if="!traceStore.traceSpanLogs.length">
{{ t("noData") }}
</div>
</LogTable>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { inject } from "vue";
import { useI18n } from "vue-i18n";
import type { PropType } from "vue";
import dayjs from "dayjs";
import { useTraceStore } from "@/store/modules/trace";
import copy from "@/utils/copy";
import { ElMessage } from "element-plus";
import LogTable from "@/views/dashboard/related/components/LogTable/Index.vue";
import getDashboard from "@/hooks/useDashboardsSession";
import { LayoutConfig } from "@/types/dashboard";
/* global defineProps */
/*global defineProps, Recordable */
const options: Recordable<LayoutConfig> = inject("options") || {};
const props = defineProps({
currentSpan: { type: Object as PropType<any>, default: () => ({}) },
});
const { t } = useI18n();
const traceStore = useTraceStore();
const pageNum = ref<number>(1);
const showRelatedLogs = ref<boolean>(false);
const pageSize = 10;
const total = computed(() =>
traceStore.traceList.length === pageSize
? pageSize * pageNum.value + 1
: pageSize * pageNum.value
);
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
async function getTaceLogs() {
showRelatedLogs.value = true;
const res = await traceStore.getSpanLogs({
condition: {
relatedTrace: {
traceId: props.currentSpan.traceId,
segmentId: props.currentSpan.segmentId,
spanId: props.currentSpan.spanId,
},
paging: { pageNum: pageNum.value, pageSize },
const { associationWidget } = getDashboard();
associationWidget(
(options.id as any) || "",
{
sourceId: options?.id || "",
traceId: props.currentSpan.traceId,
segmentId: props.currentSpan.segmentId,
spanId: props.currentSpan.spanId,
},
});
if (res.errors) {
ElMessage.error(res.errors);
}
}
function turnPage(p: number) {
pageNum.value = p;
getTaceLogs();
"Log"
);
}
function showCurrentSpanDetail(text: string) {
copy(text);

View File

@ -46,8 +46,8 @@ export default class ListGraph {
.select(this.el)
.append("svg")
.attr("class", "trace-list-dowanload")
.attr("width", this.width)
.attr("height", this.height)
.attr("width", this.width > 0 ? this.width : 10)
.attr("height", this.height > 0 ? this.height : 10)
.attr("transform", `translate(-5, 0)`);
this.tip = (d3tip as any)()
.attr("class", "d3-tip")

View File

@ -57,8 +57,8 @@ export default class TraceMap {
.select(this.el)
.append("svg")
.attr("class", "d3-trace-tree")
.attr("width", this.width)
.attr("height", this.height);
.attr("width", this.width > 0 ? this.width : 10)
.attr("height", this.height > 0 ? this.height : 10);
this.tip = (d3tip as any)()
.attr("class", "d3-tip")
.offset([-8, 0])