feat: Implement custom configurations for metrics on dashboards and topology (#39)

This commit is contained in:
Fine0830 2022-03-26 22:52:43 +08:00 committed by GitHub
parent c369de2cb1
commit c00d5d2a05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 965 additions and 364 deletions

View File

@ -0,0 +1,17 @@
<!-- 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. -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M6.984 9.984h10.031l-5.016 5.016z"></path>
</svg>

After

Width:  |  Height:  |  Size: 922 B

View File

@ -13,6 +13,5 @@ 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. -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<title>cancel</title>
<path d="M17.016 15.609l-3.609-3.609 3.609-3.609-1.406-1.406-3.609 3.609-3.609-3.609-1.406 1.406 3.609 3.609-3.609 3.609 1.406 1.406 3.609-3.609 3.609 3.609zM12 2.016q4.125 0 7.055 2.93t2.93 7.055-2.93 7.055-7.055 2.93-7.055-2.93-2.93-7.055 2.93-7.055 7.055-2.93z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -13,6 +13,5 @@ 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. -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<title>clearclose</title>
<path d="M18.984 6.422l-5.578 5.578 5.578 5.578-1.406 1.406-5.578-5.578-5.578 5.578-1.406-1.406 5.578-5.578-5.578-5.578 1.406-1.406 5.578 5.578 5.578-5.578z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -13,6 +13,5 @@ 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. -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<title>createmode_editedit</title>
<path d="M20.719 7.031l-1.828 1.828-3.75-3.75 1.828-1.828q0.281-0.281 0.703-0.281t0.703 0.281l2.344 2.344q0.281 0.281 0.281 0.703t-0.281 0.703zM3 17.25l11.063-11.063 3.75 3.75-11.063 11.063h-3.75v-3.75z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

177
src/components/Select.vue Normal file
View File

@ -0,0 +1,177 @@
<!-- 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="bar-select cp flex-h" :class="{ active: visible }">
<div class="bar-i" @click="setPopper">
<span v-if="selected.value">
{{ selected.label }}
</span>
<span class="no-data" v-else>Please select a option</span>
<span class="remove-icon" @click="removeSelected" v-if="clearable">
×
</span>
</div>
<div class="opt-wrapper" v-show="visible">
<div
class="opt ell"
@click="handleSelect(i)"
:class="{ 'select-disabled': selected.value === i.value }"
v-for="i in options"
:key="i.value"
>
{{ i.label }}
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import type { PropType } from "vue";
import { Option } from "@/types/app";
/*global defineProps, defineEmits*/
const emit = defineEmits(["change"]);
const props = defineProps({
options: {
type: Array as PropType<(Option & { disabled: boolean })[]>,
default: () => [],
},
value: {
type: String as PropType<string>,
default: () => "",
},
clearable: { type: Boolean, default: false },
});
const visible = ref<boolean>(false);
const opt = props.options.find((d: Option) => props.value === d.value);
const selected = ref<Option>(opt || { label: "", value: "" });
function handleSelect(i: Option) {
selected.value = i;
emit("change", i.value);
}
function removeSelected() {
selected.value = { label: "", value: "" };
emit("change", "");
}
watch(
() => props.value,
(data) => {
const opt = props.options.find((d: Option) => data === d.value);
selected.value = opt || { label: "", value: "" };
}
);
document.body.addEventListener("click", handleClick, false);
function handleClick() {
visible.value = false;
}
function setPopper(event: any) {
event.stopPropagation();
visible.value = !visible.value;
}
</script>
<style lang="scss" scoped>
.bar-select {
position: relative;
justify-content: space-between;
border: 1px solid #ddd;
background: #fff;
border-radius: 3px;
color: #000;
font-size: 12px;
height: 24px;
.selected {
padding: 0 3px;
border-radius: 3px;
margin: 3px;
color: #409eff;
background-color: #fafafa;
border: 1px solid #e8e8e8;
text-align: center;
}
}
.no-data {
color: #c0c4cc;
}
.bar-i {
height: 100%;
width: 100%;
padding: 2px 10px;
overflow: auto;
color: #606266;
position: relative;
&:hover {
.remove-icon {
display: block;
}
}
}
.remove-icon {
position: absolute;
right: 5px;
top: 0;
font-size: 14px;
display: none;
color: #aaa;
cursor: pointer;
}
.opt-wrapper {
color: #606266;
position: absolute;
top: 26px;
left: 0;
background: #fff;
box-shadow: 0 1px 6px rgba(99, 99, 99, 0.2);
border: 1px solid #ddd;
width: 100%;
border-radius: 0 0 3px 3px;
border-right-width: 1px !important;
z-index: 10;
overflow: auto;
max-height: 200px;
padding-bottom: 2px;
.close {
position: absolute;
right: 10px;
top: 12px;
opacity: 0.6;
&:hover {
opacity: 1;
}
}
}
.opt {
padding: 7px 15px;
&.select-disabled {
color: #409eff;
cursor: not-allowed;
}
&:hover {
background-color: #f5f5f5;
}
}
</style>

View File

@ -19,6 +19,7 @@ import TimePicker from "./TimePicker.vue";
import Selector from "./Selector.vue";
import Graph from "./Graph.vue";
import Radio from "./Radio.vue";
import SelectSingle from "./Select.vue";
import type { App } from "vue";
import VueGridLayout from "vue-grid-layout";
@ -29,6 +30,7 @@ const components: { [key: string]: any } = {
Selector,
Graph,
Radio,
SelectSingle,
};
const componentsName: string[] = Object.keys(components);

View File

@ -22,6 +22,15 @@ export enum MetricQueryTypes {
READHEATMAP = "readHeatMap",
ReadSampledRecords = "readSampledRecords",
}
export enum Calculations {
Percentage = "percentage",
ByteToKB = "byteToKB",
Apdex = "apdex",
Precision = "precision",
ConvertSeconds = "convertSeconds",
ConvertMilliseconds = "convertMilliseconds",
}
export enum sizeEnum {
XS = "XS",
SM = "SM",

View File

@ -15,13 +15,13 @@
* limitations under the License.
*/
import dayjs from "dayjs";
import { RespFields, MetricQueryTypes } from "./data";
import { RespFields, MetricQueryTypes, Calculations } from "./data";
import { ElMessage } from "element-plus";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useSelectorStore } from "@/store/modules/selectors";
import { useAppStoreWithOut } from "@/store/modules/app";
import { Instance, Endpoint, Service } from "@/types/selector";
import { StandardConfig } from "@/types/dashboard";
import { MetricConfigOpt } from "@/types/dashboard";
export function useQueryProcessor(config: any) {
if (!(config.metrics && config.metrics[0])) {
@ -48,6 +48,7 @@ export function useQueryProcessor(config: any) {
}
const fragment = config.metrics.map((name: string, index: number) => {
const metricType = config.metricTypes[index] || "";
const c = (config.metricConfig && config.metricConfig[index]) || {};
if (
[
MetricQueryTypes.ReadSampledRecords,
@ -63,11 +64,11 @@ export function useQueryProcessor(config: any) {
normal: selectorStore.currentService.normal,
scope: dashboardStore.entity,
topN: 10,
order: config.standard.sortOrder || "DES",
order: c.sortOrder || "DES",
};
} else {
if (metricType === MetricQueryTypes.ReadLabeledMetricsValues) {
const labels = (config.labelsIndex || "")
const labels = (c.labelsIndex || "")
.split(",")
.map((item: string) => item.replace(/^\s*|\s*$/g, ""));
variables.push(`$labels${index}: [String!]!`);
@ -128,7 +129,7 @@ export function useSourceProcessor(
config: {
metrics: string[];
metricTypes: string[];
standard: StandardConfig;
metricConfig: MetricConfigOpt[];
}
) {
if (resp.errors) {
@ -140,23 +141,24 @@ export function useSourceProcessor(
config.metricTypes.forEach((type: string, index) => {
const m = config.metrics[index];
const c = (config.metricConfig && config.metricConfig[index]) || {};
if (type === MetricQueryTypes.ReadMetricsValues) {
source[m] = resp.data[keys[index]].values.values.map(
(d: { value: number }) => aggregation(d.value, config.standard)
(d: { value: number }) => aggregation(d.value, c)
);
}
if (type === MetricQueryTypes.ReadLabeledMetricsValues) {
const resVal = Object.values(resp.data)[0] || [];
const labels = (config.standard.metricLabels || "")
const labels = (c.label || "")
.split(",")
.map((item: string) => item.replace(/^\s*|\s*$/g, ""));
const labelsIdx = (config.standard.labelsIndex || "")
const labelsIdx = (c.labelsIndex || "")
.split(",")
.map((item: string) => item.replace(/^\s*|\s*$/g, ""));
for (const item of resVal) {
const values = item.values.values.map((d: { value: number }) =>
aggregation(Number(d.value), config.standard)
aggregation(Number(d.value), c)
);
const indexNum = labelsIdx.findIndex((d: string) => d === item.label);
@ -168,10 +170,7 @@ export function useSourceProcessor(
}
}
if (type === MetricQueryTypes.ReadMetricsValue) {
source[m] = aggregation(
Number(Object.values(resp.data)[0]),
config.standard
);
source[m] = aggregation(Number(Object.values(resp.data)[0]), c);
}
if (
type === MetricQueryTypes.SortMetrics ||
@ -179,7 +178,7 @@ export function useSourceProcessor(
) {
source[m] = (Object.values(resp.data)[0] || []).map(
(d: { value: unknown; name: string }) => {
d.value = aggregation(Number(d.value), config.standard);
d.value = aggregation(Number(d.value), c);
return d;
}
@ -258,7 +257,11 @@ export function useQueryPodsMetrics(
export function usePodsSource(
pods: Array<Instance | Endpoint>,
resp: { errors: string; data: { [key: string]: any } },
config: { metrics: string[]; metricTypes: string[] }
config: {
metrics: string[];
metricTypes: string[];
metricConfig: MetricConfigOpt[];
}
): any {
if (resp.errors) {
ElMessage.error(resp.errors);
@ -266,13 +269,14 @@ export function usePodsSource(
}
const data = pods.map((d: Instance | any, idx: number) => {
config.metrics.map((name: string, index: number) => {
const c = (config.metricConfig && config.metricConfig[index]) || {};
const key = name + idx + index;
if (config.metricTypes[index] === MetricQueryTypes.ReadMetricsValue) {
d[name] = resp.data[key];
d[name] = aggregation(resp.data[key], c);
}
if (config.metricTypes[index] === MetricQueryTypes.ReadMetricsValues) {
d[name] = resp.data[key].values.values.map(
(d: { value: number }) => d.value
d[name] = resp.data[key].values.values.map((d: { value: number }) =>
aggregation(d.value, c)
);
}
});
@ -307,32 +311,31 @@ export function useQueryTopologyMetrics(metrics: string[], ids: string[]) {
return { queryStr, conditions };
}
function aggregation(val: number, standard: any): number | string {
let data: number | string = val;
export function aggregation(val: number, config: any): number | string {
let data: number | string = Number(val);
if (!isNaN(standard.plus)) {
data = val + Number(standard.plus);
return data;
}
if (!isNaN(standard.minus)) {
data = val - Number(standard.plus);
return data;
}
if (!isNaN(standard.multiply) && standard.divide !== 0) {
data = val * Number(standard.multiply);
return data;
}
if (!isNaN(standard.divide) && standard.divide !== 0) {
data = val / Number(standard.divide);
return data;
}
if (standard.milliseconds) {
switch (config.calculation) {
case Calculations.Percentage:
data = val / 100;
break;
case Calculations.ByteToKB:
data = val / 1024;
break;
case Calculations.Apdex:
data = val / 10000;
break;
case Calculations.ConvertSeconds:
data = dayjs(val).format("YYYY-MM-DD HH:mm:ss");
return data;
}
if (standard.milliseconds) {
break;
case Calculations.ConvertMilliseconds:
data = dayjs.unix(val).format("YYYY-MM-DD HH:mm:ss");
return data;
break;
case Calculations.Precision:
data = data.toFixed(2);
break;
default:
data;
break;
}
return data;

View File

@ -126,6 +126,9 @@ const msg = {
kubernetesCluster: "Cluster",
kubernetes: "Kubernetes",
textUrl: "Text Hyperlink",
textAlign: "Text Align",
metricLabel: "Metric Label",
showUnit: "Show Unit",
hourTip: "Select Hour",
minuteTip: "Select Minute",
secondTip: "Select Second",
@ -258,7 +261,7 @@ const msg = {
independentSelector: "Selectors",
unknownMetrics: "Unknown Metrics",
labels: "Labels",
aggregation: "Data Calculation",
aggregation: "Calculation",
unit: "Unit",
labelsIndex: "Label Subscript",
parentService: "Parent Service",

View File

@ -126,6 +126,9 @@ const msg = {
kubernetesCluster: "集群",
kubernetes: "Kubernetes",
textUrl: "文本超链接",
textAlign: "文本对齐",
metricLabel: "指标标签",
showUnit: "显示单位",
hourTip: "选择小时",
minuteTip: "选择分钟",
secondTip: "选择秒数",
@ -260,7 +263,7 @@ const msg = {
independentSelector: "独立选择器",
unknownMetrics: "未知指标",
labels: "标签",
aggregation: "数据计算",
aggregation: "计算",
unit: "单位",
labelsIndex: "标签下标",
parentService: "父级服务",

View File

@ -28,6 +28,7 @@ export const NewControl = {
standard: {},
metrics: [""],
metricTypes: [""],
metricConfig: [],
};
export const TextConfig = {
fontColor: "white",

View File

@ -36,8 +36,17 @@ export interface LayoutConfig {
metricTypes: string[];
children?: any;
activedTabIndex?: number;
metricConfig?: MetricConfigOpt[];
}
export type MetricConfigOpt = {
unit: string;
label: string;
calculation: string;
labelsIndex: string;
sortOrder: string;
};
export interface WidgetConfig {
title?: string;
tips?: string;
@ -87,7 +96,7 @@ export interface AreaConfig {
export interface CardConfig {
type?: string;
fontSize?: number;
showUint?: boolean;
showUnit?: boolean;
textAlign?: "center" | "right" | "left";
}

View File

@ -29,6 +29,17 @@ limitations under the License. -->
@change="changeConfig({ content })"
/>
</div>
<div class="item">
<span class="label">{{ t("textAlign") }}</span>
<Selector
:value="textAlign"
:options="AlignStyle"
size="small"
placeholder="Select a color"
class="input"
@change="changeConfig({ textAlign: $event[0].value })"
/>
</div>
<div class="item">
<span class="label">{{ t("backgroundColors") }}</span>
<Selector
@ -85,6 +96,7 @@ const backgroundColor = ref(originConfig.graph.backgroundColor || "green");
const fontColor = ref(originConfig.graph.fontColor || "white");
const content = ref<string>(originConfig.graph.content || "");
const fontSize = ref<number>(originConfig.graph.fontSize || 12);
const textAlign = ref(originConfig.graph.textAlign || "left");
const Colors = [
{
label: "Green",
@ -97,6 +109,14 @@ const Colors = [
{ label: "Black", value: "black" },
{ label: "Orange", value: "orange" },
];
const AlignStyle = [
{
label: "Left",
value: "left",
},
{ label: "Center", value: "center" },
{ label: "Right", value: "right" },
];
function changeConfig(param: { [key: string]: unknown }) {
const { selectedGrid } = dashboardStore;
const graph = {

View File

@ -36,7 +36,7 @@ limitations under the License. -->
i: dashboardStore.selectedGrid.i,
metrics: dashboardStore.selectedGrid.metrics,
metricTypes: dashboardStore.selectedGrid.metricTypes,
standard: dashboardStore.selectedGrid.standard,
metricConfig: dashboardStore.selectedGrid.metricConfig,
}"
:isEdit="isEdit"
@changeOpt="setStatus"
@ -64,9 +64,6 @@ limitations under the License. -->
<el-collapse-item :title="t('widgetOptions')" name="3">
<WidgetOptions />
</el-collapse-item>
<el-collapse-item :title="t('standardOptions')" name="4">
<StandardOptions @update="getSource" @loading="setLoading" />
</el-collapse-item>
</el-collapse>
</div>
<div class="footer">
@ -88,16 +85,14 @@ import { Option } from "@/types/app";
import graphs from "../graphs";
import configs from "./widget/graph-styles";
import WidgetOptions from "./widget/WidgetOptions.vue";
import StandardOptions from "./widget/StandardOptions.vue";
import MetricOptions from "./widget/MetricOptions.vue";
import MetricOptions from "./widget/metric/Index.vue";
export default defineComponent({
name: "ConfigEdit",
name: "WidgetEdit",
components: {
...graphs,
...configs,
WidgetOptions,
StandardOptions,
MetricOptions,
},
setup() {

View File

@ -1,186 +0,0 @@
<!-- 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="item">
<span class="label">{{ t("unit") }}</span>
<el-input
class="input"
v-model="selectedGrid.standard.unit"
size="small"
placeholder="Please input Unit"
/>
</div>
<div class="item">
<span class="label">{{ t("sortOrder") }}</span>
<Selector
:value="sortOrder"
:options="SortOrder"
size="small"
placeholder="Select a sort order"
class="selector"
@change="changeStandardOpt({ sortOrder })"
/>
</div>
<div class="item">
<span class="label">{{ t("labels") }}</span>
<el-input
class="input"
v-model="selectedGrid.standard.metricLabels"
size="small"
placeholder="auto"
@change="changeStandardOpt"
/>
</div>
<div class="item">
<span class="label">{{ t("labelsIndex") }}</span>
<el-input
class="input"
v-model="selectedGrid.standard.labelsIndex"
size="small"
placeholder="auto"
@change="changeStandardOpt"
/>
</div>
<div class="item">
<span class="label">{{ t("plus") }}</span>
<el-input-number
class="input"
v-model="selectedGrid.standard.plus"
:min="0"
size="small"
placeholder="Please input"
@change="changeStandardOpt"
/>
</div>
<div class="item">
<span class="label">{{ t("minus") }}</span>
<el-input-number
class="input"
v-model="selectedGrid.standard.minus"
:min="0"
size="small"
placeholder="Please input"
@change="changeStandardOpt"
/>
</div>
<div class="item">
<span class="label">{{ t("multiply") }}</span>
<el-input-number
class="input"
v-model="selectedGrid.standard.multiply"
:min="1"
size="small"
placeholder="Please input"
@change="changeStandardOpt"
/>
</div>
<div class="item">
<span class="label">{{ t("divide") }}</span>
<el-input-number
class="input"
v-model="selectedGrid.standard.divide"
size="small"
placeholder="Please input"
:min="1"
@change="changeStandardOpt"
/>
</div>
<div class="item">
<span class="label">{{ t("convertToMilliseconds") }}</span>
<el-input-number
class="input"
:min="0"
v-model="selectedGrid.standard.milliseconds"
size="small"
placeholder="Please input"
@change="changeStandardOpt"
/>
</div>
<div class="item">
<span class="label">{{ t("convertToSeconds") }}</span>
<el-input-number
class="input"
:min="0"
v-model="selectedGrid.standard.seconds"
size="small"
placeholder="Please input"
@change="changeStandardOpt"
/>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { SortOrder } from "../../data";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useQueryProcessor, useSourceProcessor } from "@/hooks/useProcessor";
import { ElMessage } from "element-plus";
/*global defineEmits */
const { t } = useI18n();
const emit = defineEmits(["update", "loading"]);
const dashboardStore = useDashboardStore();
const { selectedGrid } = dashboardStore;
const sortOrder = ref<string>(selectedGrid.standard.sortOrder || "DES");
function changeStandardOpt(param?: any) {
let standard = dashboardStore.selectedGrid.standard;
if (param) {
standard = {
...dashboardStore.selectedGrid.standard,
...param,
};
dashboardStore.selectWidget({ ...dashboardStore.selectedGrid, standard });
}
queryMetrics();
}
async function queryMetrics() {
const params = useQueryProcessor(dashboardStore.selectedGrid);
if (!params) {
emit("update", {});
return;
}
emit("loading", true);
const json = await dashboardStore.fetchMetricValue(params);
emit("loading", false);
if (json.errors) {
ElMessage.error(json.errors);
return;
}
const source = useSourceProcessor(json, dashboardStore.selectedGrid);
emit("update", source);
}
</script>
<style lang="scss" scoped>
.label {
font-size: 13px;
font-weight: 500;
display: block;
margin-bottom: 5px;
}
.input {
width: 500px;
}
.item {
margin-bottom: 10px;
}
.selector {
width: 500px;
}
</style>

View File

@ -26,6 +26,15 @@ limitations under the License. -->
@change="updateConfig({ fontSize })"
/>
</div>
<div class="item">
<span class="label">{{ t("showUnit") }}</span>
<el-switch
v-model="showUnit"
active-text="Yes"
inactive-text="No"
@change="updateConfig({ showUnit })"
/>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
@ -36,13 +45,14 @@ const { t } = useI18n();
const dashboardStore = useDashboardStore();
const { selectedGrid } = dashboardStore;
const fontSize = ref(selectedGrid.graph.fontSize);
const showUnit = ref<boolean>(selectedGrid.graph.showUnit);
function updateConfig(param: { [key: string]: unknown }) {
const graph = {
...selectedGrid.graph,
...dashboardStore.selectedGrid.graph,
...param,
};
dashboardStore.selectWidget({ ...selectedGrid, graph });
dashboardStore.selectWidget({ ...dashboardStore.selectedGrid, graph });
}
</script>
<style lang="scss" scoped>

View File

@ -49,6 +49,18 @@ limitations under the License. -->
@change="changeMetricType(index, $event)"
class="selectors"
/>
<el-popover placement="top" :width="400" trigger="click">
<template #reference>
<span @click="setMetricConfig(index)">
<Icon class="cp mr-5" iconName="mode_edit" size="middle" />
</span>
</template>
<Standard
@update="queryMetrics"
:currentMetricConfig="currentMetricConfig"
:index="index"
/>
</el-popover>
<span
v-show="states.isList || states.metricTypes[0] === 'readMetricsValues'"
>
@ -96,12 +108,13 @@ import {
ChartTypes,
PodsChartTypes,
MetricsType,
} from "../../data";
} from "../../../data";
import { ElMessage } from "element-plus";
import Icon from "@/components/Icon.vue";
import { useQueryProcessor, useSourceProcessor } from "@/hooks/useProcessor";
import { useI18n } from "vue-i18n";
import { DashboardItem } from "@/types/dashboard";
import { DashboardItem, MetricConfigOpt } from "@/types/dashboard";
import Standard from "./Standard.vue";
/*global defineEmits */
const { t } = useI18n();
@ -127,6 +140,13 @@ const states = reactive<{
dashboardName: graph.dashboardName,
dashboardList: [{ label: "", value: "" }],
});
const currentMetricConfig = ref<MetricConfigOpt>({
unit: "",
label: "",
labelsIndex: "",
calculation: "",
sortOrder: "DES",
});
states.isList = ListChartTypes.includes(graph.type);
const defaultLen = ref<number>(states.isList ? 5 : 20);
@ -323,8 +343,8 @@ async function queryMetrics() {
if (states.isList) {
return;
}
const { standard } = dashboardStore.selectedGrid;
const params = useQueryProcessor({ ...states, standard });
const { metricConfig } = dashboardStore.selectedGrid;
const params = useQueryProcessor({ ...states, metricConfig });
if (!params) {
emit("update", {});
return;
@ -337,7 +357,7 @@ async function queryMetrics() {
ElMessage.error(json.errors);
return;
}
const source = useSourceProcessor(json, { ...states, standard });
const source = useSourceProcessor(json, { ...states, metricConfig });
emit("update", source);
}
@ -372,11 +392,20 @@ function deleteMetric(index: number) {
dashboardStore.selectWidget({
...dashboardStore.selectedGrid,
...{ metricTypes: states.metricTypes, metrics: states.metrics },
metricConfig: [],
});
return;
}
states.metrics.splice(index, 1);
states.metricTypes.splice(index, 1);
const config = dashboardStore.selectedGrid.metricConfig || [];
const metricConfig = config[index] ? config.splice(index, 1) : config;
dashboardStore.selectWidget({
...dashboardStore.selectedGrid,
...{ metricTypes: states.metricTypes, metrics: states.metrics },
metricConfig,
});
console.log(dashboardStore.selectedGrid);
}
function setMetricTypeList(type: string) {
if (type !== MetricsType.REGULAR_VALUE) {
@ -393,6 +422,26 @@ function setMetricTypeList(type: string) {
}
return MetricTypes[type];
}
function setMetricConfig(index: number) {
const n = {
unit: "",
label: "",
calculation: "",
labelsIndex: "",
sortOrder: "DES",
};
if (
!dashboardStore.selectedGrid.metricConfig ||
!dashboardStore.selectedGrid.metricConfig[index]
) {
currentMetricConfig.value = n;
return;
}
currentMetricConfig.value = {
...n,
...dashboardStore.selectedGrid.metricConfig[index],
};
}
</script>
<style lang="scss" scoped>
.ds-name {

View 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 class="config-panel">
<div class="item mb-10">
<span class="label">{{ t("unit") }}</span>
<el-input
class="input"
v-model="currentMetric.unit"
size="small"
placeholder="Please input unit"
@change="changeConfigs(index, { unit: currentMetric.unit })"
/>
</div>
<div class="item mb-10" v-show="metricType === 'readLabeledMetricsValues'">
<span class="label">{{ t("labels") }}</span>
<el-input
class="input"
v-model="currentMetric.label"
size="small"
placeholder="Please input a name"
@change="changeConfigs(index, { label: currentMetric.label })"
/>
</div>
<div class="item mb-10" v-show="metricType === 'readLabeledMetricsValues'">
<span class="label">{{ t("labelsIndex") }}</span>
<el-input
class="input"
v-model="currentMetric.labelsIndex"
size="small"
placeholder="auto"
@change="
changeConfigs(index, { labelsIndex: currentMetric.labelsIndex })
"
/>
</div>
<div class="item mb-10">
<span class="label">{{ t("aggregation") }}</span>
<SelectSingle
:value="currentMetric.calculation"
:options="CalculationOpts"
@change="changeConfigs(index, { calculation: $event })"
class="selectors"
:clearable="true"
/>
</div>
<div
class="item"
v-show="['sortMetrics', 'readSampledRecords'].includes(metricType)"
>
<span class="label">{{ t("sortOrder") }}</span>
<SelectSingle
:value="currentMetric.sortOrder || 'DES'"
:options="SortOrder"
class="selectors"
@change="changeConfigs(index, { sortOrder: $event })"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { SortOrder, CalculationOpts } from "../../../data";
import { useDashboardStore } from "@/store/modules/dashboard";
import { MetricConfigOpt } from "@/types/dashboard";
/*global defineEmits, defineProps */
const props = defineProps({
currentMetricConfig: {
type: Object as PropType<MetricConfigOpt>,
default: () => ({ unit: "" }),
},
index: { type: Number, default: 0 },
});
const { t } = useI18n();
const emit = defineEmits(["update"]);
const dashboardStore = useDashboardStore();
const currentMetric = ref<MetricConfigOpt>(props.currentMetricConfig);
const metricType = ref<string>(
dashboardStore.selectedGrid.metricTypes[props.index]
);
function changeConfigs(index: number, param: { [key: string]: string }) {
const metricConfig = dashboardStore.selectedGrid.metricConfig || [];
metricConfig[index] = { ...metricConfig[index], ...param };
dashboardStore.selectWidget({
...dashboardStore.selectedGrid,
metricConfig,
});
emit("update");
}
</script>
<style lang="scss" scoped>
.config-panel {
padding: 10px 5px;
position: relative;
}
.label {
width: 150px;
display: inline-block;
font-size: 12px;
}
.close {
position: absolute;
top: -8px;
right: -15px;
}
.selectors {
width: 365px;
}
</style>

View File

@ -36,12 +36,12 @@ limitations under the License. -->
</div>
<div
class="body"
:style="{ backgroundColor: TextColors[data.graph.backgroundColor] }"
>
<div
:class="data.graph.textAlign === 'center' ? 'center' : ''"
:style="{
backgroundColor: TextColors[data.graph.backgroundColor],
color: TextColors[data.graph.fontColor],
fontSize: data.graph.fontSize + 'px',
textAlign: data.graph.textAlign,
}"
>
<a :href="data.graph.url" target="_blank">
@ -49,7 +49,6 @@ limitations under the License. -->
</a>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
@ -75,14 +74,6 @@ function editConfig() {
dashboardStore.setConfigPanel(true);
dashboardStore.selectWidget(props.data);
}
function viewText() {
const path = props.data.graph.url;
console.log(path);
if (!path) {
return;
}
window.open(path, "_blank");
}
</script>
<style lang="scss" scoped>
.topology {
@ -97,21 +88,21 @@ function viewText() {
.header {
position: absolute;
top: 10px;
right: 10px;
top: 5px;
right: 5px;
}
.body {
text-align: center;
width: 100%;
height: 100%;
line-height: 100%;
cursor: pointer;
box-sizing: border-box;
color: #333;
display: -webkit-box;
-webkit-box-orient: horizontal;
-webkit-box-pack: center;
-webkit-box-align: center;
box-sizing: border-box;
display: -webkit-box;
// -webkit-box-align: left;
}
.tools {

View File

@ -64,8 +64,8 @@ limitations under the License. -->
metrics: data.metrics,
metricTypes: data.metricTypes,
i: data.i,
metricConfig: data.metricConfig,
}"
:standard="data.standard"
:needQuery="needQuery"
/>
</div>
@ -128,7 +128,7 @@ export default defineComponent({
const d = {
metrics: props.data.metrics,
metricTypes: props.data.metricTypes,
standard: props.data.standard,
metricConfig: props.data.metricConfig || [],
};
state.source = useSourceProcessor(json, d);
}

View File

@ -244,7 +244,7 @@ export enum LegendOpt {
}
export const DepthList = [1, 2, 3, 4, 5].map((item: number) => ({
value: item,
label: item,
label: String(item),
}));
export const Status = [
{ label: "All", value: "ALL" },
@ -264,3 +264,15 @@ export const TextColors: { [key: string]: string } = {
black: "#000",
orange: "#E6A23C",
};
export const CalculationOpts = [
{ label: "Percentage", value: "percentage" },
{ label: "Byte to KB", value: "byteToKB" },
{ label: "Apdex", value: "apdex" },
{
label: "Convert milliseconds to YYYY-MM-DD HH:mm:ss",
value: "convertMilliseconds",
},
{ label: "Convert seconds to YYYY-MM-DD HH:mm:ss", value: "convertSeconds" },
{ label: "Precision is 2", value: "precision" },
];

View File

@ -26,12 +26,14 @@ limitations under the License. -->
? null
: singleVal.toFixed(2)
}}
<span v-show="config.showUint">{{ standard.unit }}</span>
<span v-show="config.showUnit">
{{ metricConfig[0]?.unit }}
</span>
</div>
</template>
<script lang="ts" setup>
import { computed, PropType } from "vue";
import { CardConfig, StandardConfig } from "@/types/dashboard";
import { CardConfig, MetricConfigOpt } from "@/types/dashboard";
/*global defineProps */
const props = defineProps({
@ -40,14 +42,16 @@ const props = defineProps({
default: () => ({}),
},
config: {
type: Object as PropType<CardConfig>,
default: () => ({ fontSize: 12, showUint: true, textAlign: "center" }),
},
standard: {
type: Object as PropType<StandardConfig>,
default: () => ({ unit: "" }),
type: Object as PropType<CardConfig & { metricConfig?: MetricConfigOpt[] }>,
default: () => ({
fontSize: 12,
showUnit: true,
textAlign: "center",
metricConfig: [],
}),
},
});
const metricConfig = computed(() => props.config.metricConfig || []);
const key = computed(() => Object.keys(props.data)[0]);
const singleVal = computed(() => props.data[key.value]);
</script>

View File

@ -44,7 +44,7 @@ limitations under the License. -->
</el-table-column>
<el-table-column
v-for="(metric, index) in config.metrics"
:label="metric"
:label="`${metric} ${getUnit(index)}`"
:key="metric + index"
>
<template #default="scope">
@ -92,6 +92,7 @@ import Card from "./Card.vue";
import { EntityType } from "../data";
import router from "@/router";
import getDashboard from "@/hooks/useDashboardsSession";
import { MetricConfigOpt } from "@/types/dashboard";
/*global defineProps */
const props = defineProps({
@ -104,7 +105,7 @@ const props = defineProps({
i: string;
metrics: string[];
metricTypes: string[];
}
} & { metricConfig: MetricConfigOpt[] }
>,
default: () => ({ dashboardName: "", fontSize: 12, i: "" }),
},
@ -155,7 +156,12 @@ async function queryEndpointMetrics(currentPods: Endpoint[]) {
ElMessage.error(json.errors);
return;
}
endpoints.value = usePodsSource(currentPods, json, props.config);
const metricConfig = props.config.metricConfig || [];
endpoints.value = usePodsSource(currentPods, json, {
...props.config,
metricConfig: metricConfig,
});
return;
}
endpoints.value = currentPods;
@ -184,12 +190,21 @@ async function searchList() {
const limit = searchText.value ? undefined : total;
await queryEndpoints(limit);
}
function getUnit(index: number) {
const u =
(props.config.metricConfig &&
props.config.metricConfig[index] &&
props.config.metricConfig[index].unit) ||
"";
if (u) {
return `(${u})`;
}
return u;
}
watch(
() => [props.config.metricTypes, props.config.metrics],
async () => {
if (props.isEdit) {
queryEndpointMetrics(endpoints.value);
}
// emit("changeOpt", false);
}
);

View File

@ -44,7 +44,7 @@ limitations under the License. -->
</el-table-column>
<el-table-column
v-for="(metric, index) in config.metrics"
:label="metric"
:label="`${metric} ${getUnit(index)}`"
:key="metric + index"
>
<template #default="scope">
@ -112,6 +112,7 @@ import { useQueryPodsMetrics, usePodsSource } from "@/hooks/useProcessor";
import { EntityType } from "../data";
import router from "@/router";
import getDashboard from "@/hooks/useDashboardsSession";
import { MetricConfigOpt } from "@/types/dashboard";
/*global defineProps */
const props = defineProps({
@ -122,7 +123,7 @@ const props = defineProps({
metrics: string[];
metricTypes: string[];
isEdit: boolean;
}
} & { metricConfig: MetricConfigOpt[] }
>,
default: () => ({
dashboardName: "",
@ -180,7 +181,11 @@ async function queryInstanceMetrics(currentInstances: Instance[]) {
ElMessage.error(json.errors);
return;
}
instances.value = usePodsSource(currentInstances, json, props.config);
const metricConfig = props.config.metricConfig || [];
instances.value = usePodsSource(currentInstances, json, {
...props.config,
metricConfig,
});
return;
}
instances.value = currentInstances;
@ -214,6 +219,18 @@ function searchList() {
instances.value = searchInstances.value.splice(0, pageSize);
}
function getUnit(index: number) {
const u =
(props.config.metricConfig &&
props.config.metricConfig[index] &&
props.config.metricConfig[index].unit) ||
"";
if (u) {
return `(${u})`;
}
return u;
}
watch(
() => [props.config.metricTypes, props.config.metrics],
() => {

View File

@ -56,7 +56,7 @@ limitations under the License. -->
</el-table-column>
<el-table-column
v-for="(metric, index) in config.metrics"
:label="metric"
:label="`${metric} ${getUnit(index)}`"
:key="metric + index"
>
<template #default="scope">
@ -104,6 +104,7 @@ import { useQueryPodsMetrics, usePodsSource } from "@/hooks/useProcessor";
import { EntityType } from "../data";
import router from "@/router";
import getDashboard from "@/hooks/useDashboardsSession";
import { MetricConfigOpt } from "@/types/dashboard";
/*global defineProps */
const props = defineProps({
@ -117,7 +118,7 @@ const props = defineProps({
metrics: string[];
metricTypes: string[];
isEdit: boolean;
}
} & { metricConfig: MetricConfigOpt[] }
>,
default: () => ({ dashboardName: "", fontSize: 12 }),
},
@ -214,7 +215,11 @@ async function queryServiceMetrics(currentServices: Service[]) {
ElMessage.error(json.errors);
return;
}
services.value = usePodsSource(currentServices, json, props.config);
const metricConfig = props.config.metricConfig || [];
services.value = usePodsSource(currentServices, json, {
...props.config,
metricConfig: metricConfig || [],
});
return;
}
services.value = currentServices;
@ -250,6 +255,17 @@ function searchList() {
);
services.value = searchServices.splice(0, pageSize);
}
function getUnit(index: number) {
const u =
(props.config.metricConfig &&
props.config.metricConfig[index] &&
props.config.metricConfig[index].unit) ||
"";
if (u) {
return `(${u})`;
}
return u;
}
watch(
() => [props.config.metricTypes, props.config.metrics],
() => {

View File

@ -90,6 +90,8 @@ import { Option } from "@/types/app";
import { Service } from "@/types/selector";
import { useAppStoreWithOut } from "@/store/modules/app";
import getDashboard from "@/hooks/useDashboardsSession";
import { MetricConfigOpt } from "@/types/dashboard";
import { aggregation } from "@/hooks/useProcessor";
/*global Nullable, defineProps */
const props = defineProps({
@ -275,13 +277,17 @@ function update() {
handleNodeClick: handleNodeClick,
tipHtml: (data: Node) => {
const nodeMetrics: string[] = settings.value.nodeMetrics || [];
const html = nodeMetrics.map((m) => {
const nodeMetricConfig = settings.value.nodeMetricConfig || [];
const html = nodeMetrics.map((m, index) => {
const metric =
topologyStore.nodeMetricValue[m].values.filter(
topologyStore.nodeMetricValue[m].values.find(
(val: { id: string; value: unknown }) => val.id === data.id
)[0] || {};
const val = m.includes("_sla") ? metric.value / 100 : metric.value;
return ` <div class="mb-5"><span class="grey">${m}: </span>${val}</div>`;
) || {};
const opt: MetricConfigOpt = nodeMetricConfig[index] || {};
const v = aggregation(metric.value, opt);
return ` <div class="mb-5"><span class="grey">${
opt.label || m
}: </span>${v} ${opt.unit || ""}</div>`;
});
return [
` <div class="mb-5"><span class="grey">name: </span>${data.name}</div>`,
@ -306,24 +312,34 @@ function update() {
tipHtml: (data: Call) => {
const linkClientMetrics: string[] =
settings.value.linkClientMetrics || [];
const linkServerMetricConfig: MetricConfigOpt[] =
settings.value.linkServerMetricConfig || [];
const linkClientMetricConfig: MetricConfigOpt[] =
settings.value.linkClientMetricConfig || [];
const linkServerMetrics: string[] =
settings.value.linkServerMetrics || [];
const htmlServer = linkServerMetrics.map((m) => {
const metric = topologyStore.linkServerMetrics[m].values.filter(
const htmlServer = linkServerMetrics.map((m, index) => {
const metric = topologyStore.linkServerMetrics[m].values.find(
(val: { id: string; value: unknown }) => val.id === data.id
)[0];
);
if (metric) {
const val = m.includes("_sla") ? metric.value / 100 : metric.value;
return ` <div class="mb-5"><span class="grey">${m}: </span>${val}</div>`;
const opt: MetricConfigOpt = linkServerMetricConfig[index] || {};
const v = aggregation(metric.value, opt);
return ` <div class="mb-5"><span class="grey">${
opt.label || m
}: </span>${v} ${opt.unit || ""}</div>`;
}
});
const htmlClient = linkClientMetrics.map((m) => {
const metric = topologyStore.linkClientMetrics[m].values.filter(
const htmlClient = linkClientMetrics.map((m: string, index: number) => {
const opt: MetricConfigOpt = linkClientMetricConfig[index] || {};
const metric = topologyStore.linkClientMetrics[m].values.find(
(val: { id: string; value: unknown }) => val.id === data.id
)[0];
);
if (metric) {
const val = m.includes("_sla") ? metric.value / 100 : metric.value;
return ` <div class="mb-5"><span class="grey">${m}: </span>${val}</div>`;
const v = aggregation(metric.value, opt);
return ` <div class="mb-5"><span class="grey">${
opt.label || m
}: </span>${v} ${opt.unit || ""}</div>`;
}
});
const html = [

View File

@ -0,0 +1,170 @@
<!-- 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="config-panel">
<div class="item mb-10">
<span class="label">{{ t("metrics") }}</span>
<SelectSingle
:value="currentMetric"
:options="metrics"
@change="changeMetric"
class="selectors"
/>
</div>
<div class="item mb-10">
<span class="label">{{ t("unit") }}</span>
<el-input
class="input"
v-model="currentConfig.unit"
size="small"
placeholder="Please input unit"
@change="changeConfigs({ unit: currentConfig.unit })"
/>
</div>
<div class="item mb-10">
<span class="label">{{ t("labels") }}</span>
<el-input
class="input"
v-model="currentConfig.label"
size="small"
placeholder="Please input a label"
@change="changeConfigs({ label: currentConfig.label })"
/>
</div>
<div class="item mb-10">
<span class="label">{{ t("aggregation") }}</span>
<SelectSingle
:value="currentConfig.calculation"
:options="CalculationOpts"
@change="changeConfigs({ calculation: $event })"
class="selectors"
:clearable="true"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { CalculationOpts } from "../../../data";
import { useDashboardStore } from "@/store/modules/dashboard";
import { MetricConfigOpt } from "@/types/dashboard";
import { Option } from "element-plus/es/components/select-v2/src/select.types";
/*global defineEmits, defineProps */
const props = defineProps({
currentMetricConfig: {
type: Object as PropType<MetricConfigOpt>,
default: () => ({ unit: "" }),
},
type: { type: String, default: "" },
metrics: { type: Array as PropType<string[]>, default: () => [] },
});
const { t } = useI18n();
const emit = defineEmits(["update"]);
const dashboardStore = useDashboardStore();
const m = props.metrics.map((d: string) => {
return { label: d, value: d };
});
const metrics = ref<Option[]>(m.length ? m : [{ label: "", value: "" }]);
const currentMetric = ref<string>(metrics.value[0].value);
const currentConfig = ref<{ unit: string; calculation: string; label: string }>(
{
unit: "",
calculation: "",
label: "",
}
);
const currentIndex = ref<number>(0);
const getMetricConfig = computed(() => {
let config = [];
switch (props.type) {
case "linkServerMetricConfig":
config = dashboardStore.selectedGrid.linkServerMetricConfig;
break;
case "linkClientMetricConfig":
config = dashboardStore.selectedGrid.linkClientMetricConfig;
break;
case "nodeMetricConfig":
config = dashboardStore.selectedGrid.nodeMetricConfig;
break;
}
return config || [];
});
function changeConfigs(param: { [key: string]: string }) {
const metricConfig = getMetricConfig.value;
metricConfig[currentIndex.value] = {
...metricConfig[currentIndex.value],
...param,
};
emit("update", { [props.type]: metricConfig });
}
function changeMetric(val: string) {
currentMetric.value = val;
const index = metrics.value.findIndex((d: Option) => d.value === val);
currentIndex.value = index || 0;
const config = getMetricConfig.value || [];
currentConfig.value = {
unit: "",
label: "",
calculation: "",
...config[index],
};
}
watch(
() => props.type,
() => {
const m = props.metrics.map((d: string) => {
return { label: d, value: d };
});
metrics.value = m.length ? m : [{ label: "", value: "" }];
currentMetric.value = metrics.value[0].value;
const config = getMetricConfig.value || [];
currentIndex.value = 0;
currentConfig.value = {
unit: "",
label: "",
calculation: "",
...config[0],
};
}
);
</script>
<style lang="scss" scoped>
.config-panel {
padding: 10px 5px;
position: relative;
min-height: 300px;
}
.label {
width: 150px;
display: inline-block;
font-size: 12px;
}
.close {
position: absolute;
top: -8px;
right: -15px;
}
.selectors {
width: 365px;
}
</style>

View File

@ -54,7 +54,7 @@ limitations under the License. -->
element-loading-background="rgba(0, 0, 0, 0)"
@click="handleClick"
>
<Sankey @click="selectNodeLink" />
<Sankey @click="selectNodeLink" :settings="settings" />
</div>
<div
class="operations-list"

View File

@ -17,11 +17,19 @@ limitations under the License. -->
<Graph :option="option" @select="clickChart" />
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { computed, PropType } from "vue";
import { useTopologyStore } from "@/store/modules/topology";
import { Node, Call } from "@/types/topology";
import { MetricConfigOpt } from "@/types/dashboard";
import { aggregation } from "@/hooks/useProcessor";
/*global defineEmits */
/*global defineEmits, defineProps */
const props = defineProps({
settings: {
type: Object as PropType<any>,
default: () => ({}),
},
});
const emit = defineEmits(["click"]);
const topologyStore = useTopologyStore();
const option = computed(() => getOption());
@ -77,23 +85,34 @@ function getOption() {
function linkTooltip(data: Call) {
const clientMetrics: string[] = Object.keys(topologyStore.linkClientMetrics);
const serverMetrics: string[] = Object.keys(topologyStore.linkServerMetrics);
const htmlServer = serverMetrics.map((m) => {
const metric = topologyStore.linkServerMetrics[m].values.filter(
const linkServerMetricConfig: MetricConfigOpt[] =
props.settings.linkServerMetricConfig || [];
const linkClientMetricConfig: MetricConfigOpt[] =
props.settings.linkClientMetricConfig || [];
const htmlServer = serverMetrics.map((m, index) => {
const metric =
topologyStore.linkServerMetrics[m].values.find(
(val: { id: string; value: unknown }) => val.id === data.id
)[0];
) || {};
if (metric) {
const val = m.includes("_sla") ? metric.value / 100 : metric.value;
return ` <div><span>${m}: </span>${val}</div>`;
const opt: MetricConfigOpt = linkServerMetricConfig[index] || {};
const v = aggregation(metric.value, opt);
return ` <div class="mb-5"><span class="grey">${
opt.label || m
}: </span>${v} ${opt.unit || ""}</div>`;
}
});
const htmlClient = clientMetrics.map((m) => {
const metric = topologyStore.linkClientMetrics[m].values.filter(
const htmlClient = clientMetrics.map((m, index) => {
const opt: MetricConfigOpt = linkClientMetricConfig[index] || {};
const metric =
topologyStore.linkClientMetrics[m].values.find(
(val: { id: string; value: unknown }) => val.id === data.id
)[0];
if (metric) {
const val = m.includes("_sla") ? metric.value / 100 : metric.value;
return ` <div><span>${m}: </span>${val}</div>`;
}
) || {};
const v = aggregation(metric.value, opt);
return ` <div class="mb-5"><span class="grey">${
opt.label || m
}: </span>${v} ${opt.unit || ""}</div>`;
});
const html = [
`<div>${data.sourceObj.serviceName} -> ${data.targetObj.serviceName}</div>`,
@ -106,13 +125,17 @@ function linkTooltip(data: Call) {
function nodeTooltip(data: Node) {
const nodeMetrics: string[] = Object.keys(topologyStore.nodeMetricValue);
const html = nodeMetrics.map((m) => {
const nodeMetricConfig = props.settings.nodeMetricConfig || [];
const html = nodeMetrics.map((m, index) => {
const metric =
topologyStore.nodeMetricValue[m].values.filter(
topologyStore.nodeMetricValue[m].values.find(
(val: { id: string; value: unknown }) => val.id === data.id
)[0] || {};
const val = m.includes("_sla") ? metric.value / 100 : metric.value;
return ` <div><span>${m}: </span>${val}</div>`;
) || {};
const opt: MetricConfigOpt = nodeMetricConfig[index] || {};
const v = aggregation(metric.value, opt);
return ` <div class="mb-5"><span class="grey">${
opt.label || m
}: </span>${v} ${opt.unit || ""}</div>`;
});
return [` <div><span>name: </span>${data.serviceName}</div>`, ...html].join(
" "

View File

@ -25,7 +25,27 @@ limitations under the License. -->
class="inputs"
:clearable="true"
/>
<div class="label">{{ t("linkServerMetrics") }}</div>
<div class="label">
<span>{{ t("linkServerMetrics") }}</span>
<el-popover
placement="left"
:width="400"
trigger="click"
effect="dark"
v-if="states.linkServerMetrics.length"
>
<template #reference>
<span @click="setConfigType('linkServerMetricConfig')">
<Icon class="cp ml-5" iconName="mode_edit" size="middle" />
</span>
</template>
<Metrics
:type="configType"
:metrics="states.linkServerMetrics"
@update="changeLinkServerMetrics"
/>
</el-popover>
</div>
<Selector
class="inputs"
:multiple="true"
@ -33,11 +53,29 @@ limitations under the License. -->
:options="states.linkMetricList"
size="small"
placeholder="Select metrics"
@change="changeLinkServerMetrics"
@change="updateLinkServerMetrics"
/>
<span v-show="dashboardStore.entity !== EntityType[2].value">
<div class="label">
{{ t("linkClientMetrics") }}
<span>{{ t("linkClientMetrics") }}</span>
<el-popover
placement="left"
:width="400"
trigger="click"
effect="dark"
v-if="states.linkClientMetrics.length"
>
<template #reference>
<span @click="setConfigType('linkClientMetricConfig')">
<Icon class="cp ml-5" iconName="mode_edit" size="middle" />
</span>
</template>
<Metrics
:type="configType"
:metrics="states.linkClientMetrics"
@update="changeLinkClientMetrics"
/>
</el-popover>
</div>
<Selector
class="inputs"
@ -46,7 +84,7 @@ limitations under the License. -->
:options="states.linkMetricList"
size="small"
placeholder="Select metrics"
@change="changeLinkClientMetrics"
@change="updateLinkClientMetrics"
/>
</span>
</div>
@ -100,7 +138,27 @@ limitations under the License. -->
/>
</span>
</div>
<div class="label">{{ t("nodeMetrics") }}</div>
<div class="label">
<span>{{ t("nodeMetrics") }}</span>
<el-popover
placement="left"
:width="400"
trigger="click"
effect="dark"
v-if="states.nodeMetrics.length"
>
<template #reference>
<span @click="setConfigType('nodeMetricConfig')">
<Icon class="cp ml-5" iconName="mode_edit" size="middle" />
</span>
</template>
<Metrics
:type="configType"
:metrics="states.nodeMetrics"
@update="changeNodeMetrics"
/>
</el-popover>
</div>
<Selector
class="inputs"
:multiple="true"
@ -108,7 +166,7 @@ limitations under the License. -->
:options="states.nodeMetricList"
size="small"
placeholder="Select metrics"
@change="changeNodeMetrics"
@change="updateNodeMetrics"
/>
</div>
<div class="legend-settings" v-show="isService">
@ -157,15 +215,6 @@ limitations under the License. -->
</span>
<div v-show="index !== legend.metric.length - 1">&&</div>
</div>
<!-- <div class="label">{{ t("conditions") }}</div>
<Selector
class="inputs"
:value="legend.condition"
:options="LegendConditions"
size="small"
placeholder="Select a condition"
@change="changeCondition"
/> -->
<el-button
@click="setLegend"
class="legend-btn"
@ -177,7 +226,7 @@ limitations under the License. -->
</div>
</template>
<script lang="ts" setup>
import { reactive } from "vue";
import { reactive, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useTopologyStore } from "@/store/modules/topology";
@ -186,8 +235,9 @@ import { MetricCatalog, ScopeType, MetricConditions } from "../../../data";
import { Option } from "@/types/app";
import { useQueryTopologyMetrics } from "@/hooks/useProcessor";
import { Node } from "@/types/topology";
import { DashboardItem } from "@/types/dashboard";
import { DashboardItem, MetricConfigOpt } from "@/types/dashboard";
import { EntityType, LegendOpt, MetricsType } from "../../../data";
import Metrics from "./Metrics.vue";
/*global defineEmits */
const emit = defineEmits(["update", "updateNodes"]);
@ -238,6 +288,7 @@ const legend = reactive<{
}>({
metric: l ? selectedGrid.legend : [{ name: "", condition: "", value: "" }],
});
const configType = ref<string>("");
getMetricList();
async function getMetricList() {
@ -347,11 +398,12 @@ function deleteItem(index: number) {
items.splice(index, 1);
updateSettings();
}
function updateSettings() {
function updateSettings(metricConfig?: { [key: string]: MetricConfigOpt[] }) {
const metrics = legend.metric.filter(
(d: any) => d.name && d.value && d.condition
);
const param = {
...dashboardStore.selectedGrid,
linkDashboard: states.linkDashboard,
nodeDashboard: isService
? items.filter((d: { scope: string; dashboard: string }) => d.dashboard)
@ -360,32 +412,76 @@ function updateSettings() {
linkClientMetrics: states.linkClientMetrics,
nodeMetrics: states.nodeMetrics,
legend: metrics,
...metricConfig,
};
dashboardStore.selectWidget({ ...dashboardStore.selectedGrid, ...param });
dashboardStore.setConfigs({ ...dashboardStore.selectedGrid, ...param });
dashboardStore.selectWidget(param);
dashboardStore.setConfigs(param);
emit("update", param);
}
async function changeLinkServerMetrics(options: Option[] | any) {
states.linkServerMetrics = options.map((d: Option) => d.value);
updateSettings();
function updateLinkServerMetrics(options: Option[] | any) {
const opt = options.map((d: Option) => d.value);
const index = states.linkServerMetrics.findIndex(
(d: any) => !opt.includes(d)
);
states.linkServerMetrics = opt;
if (index < 0) {
changeLinkServerMetrics();
return;
}
const origin = dashboardStore.selectedGrid.linkServerMetricConfig || [];
const config = origin.length === 1 ? [] : origin.splice(index, 1);
changeLinkServerMetrics({ linkServerMetricConfig: config });
}
async function changeLinkServerMetrics(config?: {
[key: string]: MetricConfigOpt[];
}) {
updateSettings(config);
if (!states.linkServerMetrics.length) {
topologyStore.setLinkServerMetrics({});
return;
}
topologyStore.getLinkServerMetrics(states.linkServerMetrics);
}
async function changeLinkClientMetrics(options: Option[] | any) {
states.linkClientMetrics = options.map((d: Option) => d.value);
updateSettings();
function updateLinkClientMetrics(options: Option[] | any) {
const opt = options.map((d: Option) => d.value);
const index = states.linkClientMetrics.findIndex(
(d: any) => !opt.includes(d)
);
states.linkClientMetrics = opt;
if (index < 0) {
changeLinkClientMetrics();
return;
}
const origin = dashboardStore.selectedGrid.linkClientMetricConfig || [];
const config = origin.length === 1 ? [] : origin.splice(index, 1);
changeLinkClientMetrics({ linkClientMetricConfig: config });
}
async function changeLinkClientMetrics(config?: {
[key: string]: MetricConfigOpt[];
}) {
updateSettings(config);
if (!states.linkClientMetrics.length) {
topologyStore.setLinkClientMetrics({});
return;
}
topologyStore.getLinkClientMetrics(states.linkClientMetrics);
}
async function changeNodeMetrics(options: Option[] | any) {
states.nodeMetrics = options.map((d: Option) => d.value);
updateSettings();
function updateNodeMetrics(options: Option[] | any) {
const opt = options.map((d: Option) => d.value);
const index = states.nodeMetrics.findIndex((d: any) => !opt.includes(d));
states.nodeMetrics = opt;
if (index < 0) {
changeNodeMetrics();
return;
}
const origin = dashboardStore.selectedGrid.nodeMetricConfig || [];
const config = origin.length === 1 ? [] : origin.splice(index, 1);
changeNodeMetrics({ nodeMetricConfig: config });
}
async function changeNodeMetrics(config?: {
[key: string]: MetricConfigOpt[];
}) {
updateSettings(config);
if (!states.nodeMetrics.length) {
topologyStore.setNodeMetricValue({});
return;
@ -402,6 +498,9 @@ function deleteMetric(index: number) {
function addMetric() {
legend.metric.push({ name: "", condition: "", value: "" });
}
function setConfigType(type: string) {
configType.value = type;
}
</script>
<style lang="scss" scoped>
.link-settings {