feat: enhance dashboard features for custom configurations (#11)

This commit is contained in:
吴晟 Wu Sheng 2022-01-25 16:55:28 +08:00 committed by GitHub
commit e85d819356
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 2386 additions and 897 deletions

View File

@ -13,7 +13,7 @@ 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>
<router-view />
<router-view :key="$route.fullPath" />
</template>
<style>

View File

@ -5,31 +5,14 @@ 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
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>
<input v-show="!imgUrl" type="file" @change="fileChange" accept="image/*" />
<img v-if="imgUrl" :src="imgUrl" alt="" />
</template>
<script lang="ts" setup>
import { ref } from "vue";
const imgUrl = ref<string>("");
const fileChange = (e: any) => {
const fileList = e.target.files;
if (fileList.length === 0) {
imgUrl.value = "";
return;
}
const file = fileList[0];
const reader = new FileReader();
reader.onload = (event: any) => {
imgUrl.value = event.target.result;
};
reader.readAsDataURL(file);
};
</script>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<title>add_circle_outlinecontrol_point</title>
<path d="M12 20.016q3.281 0 5.648-2.367t2.367-5.648-2.367-5.648-5.648-2.367-5.648 2.367-2.367 5.648 2.367 5.648 5.648 2.367zM12 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.93zM12.984 6.984v4.031h4.031v1.969h-4.031v4.031h-1.969v-4.031h-4.031v-1.969h4.031v-4.031h1.969z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,18 @@
<!-- 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">
<title>remove_circle_outline</title>
<path d="M12 20.016q3.281 0 5.648-2.367t2.367-5.648-2.367-5.648-5.648-2.367-5.648 2.367-2.367 5.648 2.367 5.648 5.648 2.367zM12 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.93zM6.984 11.016h10.031v1.969h-10.031v-1.969z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -277,15 +277,9 @@ limitations under the License. -->
</template>
<script lang="ts" setup>
import {
defineProps,
computed,
defineEmits,
onMounted,
watch,
reactive,
} from "vue";
import { computed, onMounted, watch, reactive } from "vue";
import { useI18n } from "vue-i18n";
/*global defineProps, defineEmits */
const emit = defineEmits(["input", "setDates", "ok"]);
const { t } = useI18n();
const props = defineProps({
@ -580,11 +574,13 @@ onMounted(() => {
user-select: none;
color: #3d444f;
}
.calendar + .calendar {
border-left: solid 1px #eaeaea;
margin-left: 5px;
padding-left: 5px;
}
.calendar-head {
line-height: 34px;
height: 34px;
@ -630,10 +626,12 @@ onMounted(() => {
.calendar-next-month-btn {
right: 24px;
}
.calendar-next-month-btn .middle,
.calendar-prev-month-btn .middle {
margin-top: 8px;
}
.calendar-body {
position: relative;
width: 196px;

View File

@ -16,20 +16,12 @@ limitations under the License. -->
<div ref="chartRef" :style="`height:${height};width:${width};`"></div>
</template>
<script lang="ts" setup>
import {
watch,
ref,
defineProps,
Ref,
onMounted,
onBeforeUnmount,
unref,
} from "vue";
import { watch, ref, Ref, onMounted, onBeforeUnmount, unref } from "vue";
import type { PropType } from "vue";
import { useECharts } from "@/hooks/useEcharts";
import { addResizeListener, removeResizeListener } from "@/utils/event";
/*global Nullable*/
/*global Nullable, defineProps*/
const chartRef = ref<Nullable<HTMLDivElement>>(null);
const { setOptions, resize } = useECharts(chartRef as Ref<HTMLDivElement>);
const props = defineProps({
@ -49,8 +41,11 @@ onMounted(() => {
watch(
() => props.option,
(opt) => {
setOptions(opt);
(newVal, oldVal) => {
if (JSON.stringify(newVal) === JSON.stringify(oldVal)) {
return;
}
setOptions(newVal);
}
);

View File

@ -28,8 +28,9 @@ limitations under the License. -->
</svg>
</template>
<script lang="ts" setup>
import { defineProps } from "vue";
import "@/assets/icons/index";
/*global defineProps */
defineProps({
iconName: { type: String, default: "" },
size: { type: String, default: "sm" },
@ -42,25 +43,31 @@ defineProps({
height: 16px;
vertical-align: middle;
fill: currentColor;
&.sm {
width: 14px;
height: 14px;
}
&.middle {
width: 18px;
height: 18px;
}
&.lg {
width: 24px;
height: 24px;
}
&.loading {
animation: loading 1.5s linear infinite;
}
&.logo {
height: 30px;
width: 110px;
}
&.xl {
height: 30px;
width: 30px;

View File

@ -20,6 +20,7 @@ limitations under the License. -->
@change="changeSelected"
filterable
:multiple="multiple"
:disabled="disabled"
:style="{ borderRadius }"
>
<el-option
@ -32,7 +33,7 @@ limitations under the License. -->
</el-select>
</template>
<script lang="ts" setup>
import { ref, defineEmits } from "vue";
import { ref, watch } from "vue";
import type { PropType } from "vue";
import { ElSelect, ElOption } from "element-plus";
@ -41,8 +42,9 @@ interface Option {
value: string;
}
/*global defineProps, defineEmits*/
const emit = defineEmits(["change"]);
/*global defineProps*/
const props = defineProps({
options: {
type: Array as PropType<Option[]>,
@ -56,12 +58,11 @@ const props = defineProps({
placeholder: { type: String, default: "Select a option" },
borderRadius: { type: Number, default: 3 },
multiple: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
});
const selected = ref<string[] | string>(props.value);
function changeSelected() {
if (!props.multiple) {
return;
}
const options = props.options.filter((d: Option) =>
props.multiple
? selected.value.includes(d.value)
@ -69,6 +70,12 @@ function changeSelected() {
);
emit("change", options);
}
watch(
() => props.value,
(data) => {
selected.value = data;
}
);
</script>
<style lang="scss" scope>
.icon {

View File

@ -142,17 +142,10 @@ limitations under the License. -->
</template>
<script lang="ts" setup>
import {
defineProps,
ref,
computed,
defineEmits,
onMounted,
onBeforeUnmount,
watch,
} from "vue";
import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
import { useI18n } from "vue-i18n";
import DateCalendar from "./DateCalendar.vue";
/*global defineProps, defineEmits */
const datepicker = ref(null);
const { t } = useI18n();
const show = ref<boolean>(false);
@ -339,6 +332,30 @@ watch(
</script>
<style lang="scss" scoped>
@keyframes datepicker-anim-in {
0% {
opacity: 0;
transform: scaleY(0.8);
}
to {
opacity: 1;
transform: scaleY(1);
}
}
@keyframes datepicker-anim-out {
0% {
opacity: 1;
transform: scaleY(1);
}
to {
opacity: 0;
transform: scaleY(0.8);
}
}
.datepicker {
display: inline-block;
position: relative;
@ -373,7 +390,6 @@ watch(
margin-left: -8px;
margin-top: -8px;
text-align: center;
background: #ccc;
color: #fff;
border-radius: 50%;
background: #ccc
@ -384,6 +400,7 @@ watch(
.datepicker__clearable:hover:before {
display: none;
}
.datepicker__clearable:hover .datepicker-close {
display: block;
}
@ -436,25 +453,30 @@ watch(
padding: 5px;
overflow: hidden;
z-index: 999;
&.top {
bottom: 30px;
right: 0;
transform-origin: center bottom;
}
&.bottom {
top: 30px;
right: 0;
transform-origin: center top;
}
&.left {
top: 30px;
transform-origin: center top;
}
&.right {
right: -80px;
top: 30px;
transform-origin: center top;
}
&__sidebar {
position: absolute;
top: 0;
@ -464,6 +486,7 @@ watch(
padding: 5px;
border-right: solid 1px #eaeaea;
}
&__shortcut {
display: block;
width: 100%;
@ -476,10 +499,12 @@ watch(
outline: none;
cursor: pointer;
white-space: nowrap;
&:hover {
color: #3f97e3;
}
}
&__body {
margin-left: 100px;
padding-left: 5px;
@ -513,6 +538,7 @@ watch(
display: inline-block;
cursor: pointer;
}
.datepicker-anim-enter-active {
transform-origin: 0 0;
animation: datepicker-anim-in 0.2s cubic-bezier(0.23, 1, 0.32, 1);
@ -545,26 +571,4 @@ watch(
.datepicker__buttons .datepicker__button-cancel {
background: #666;
}
@keyframes datepicker-anim-in {
0% {
opacity: 0;
transform: scaleY(0.8);
}
to {
opacity: 1;
transform: scaleY(1);
}
}
@keyframes datepicker-anim-out {
0% {
opacity: 1;
transform: scaleY(1);
}
to {
opacity: 0;
transform: scaleY(0.8);
}
}
</style>

View File

@ -18,47 +18,16 @@ export const TypeOfMetrics = {
variable: "$name: String!",
query: `typeOfMetrics(name: $name)`,
};
export const queryMetricsValues = {
variable: ["$condition: MetricsCondition!, $duration: Duration!"],
export const listMetrics = {
variable: "$regex: String",
query: `
readMetricsValues: readMetricsValues(condition: $condition, duration: $duration) {
label
values {
values {value}
}
}`,
};
export const queryMetricsValue = {
variable: ["$condition: MetricsCondition!, $duration: Duration!"],
query: `
readMetricsValue: readMetricsValue(condition: $condition, duration: $duration)`,
};
export const querySortMetrics = {
variable: ["$condition: TopNCondition!, $duration: Duration!"],
query: `
sortMetrics: sortMetrics(condition: $condition, duration: $duration) {
name
id
value
refId
}`,
};
export const queryLabeledMetricsValues = {
variable: [
"$condition: MetricsCondition!, $labels: [String!]!, $duration: Duration!",
],
query: `
readLabeledMetricsValues: readLabeledMetricsValues(
condition: $condition,
labels: $labels,
duration: $duration) {
label
values {
values {value}
}
}`,
metrics: listMetrics(regex: $regex) {
value: name
label: name
type
catalog
}
`,
};
export const queryHeatMap = {
@ -75,12 +44,3 @@ export const queryHeatMap = {
}
}`,
};
export const querySampledRecords = {
variable: ["$condition: TopNCondition!, $duration: Duration!"],
query: `
readSampledRecords: readSampledRecords(condition: $condition, duration: $duration) {
name
value
refId
}`,
};

View File

@ -15,14 +15,16 @@
* limitations under the License.
*/
export const Services = {
variable: ["$layer: String!"],
variable: "$layer: String!",
query: `
services: listServices(layer: $layer) {
value: id
services: listServices(layer: $layer) {
id
value: name
label: name
group
layer
}
layers
normal
}
`,
};
export const Layers = {
@ -33,10 +35,13 @@ export const Layers = {
export const Instances = {
variable: "$serviceId: ID!, $duration: Duration!",
query: `
getServiceInstances(duration: $duration, serviceId: $serviceId) {
key: id
pods: listInstances(duration: $duration, serviceId: $serviceId) {
id
value: name
label: name
language
instanceUUID
layer
attributes {
name
value
@ -47,9 +52,55 @@ export const Instances = {
export const Endpoints = {
variable: "$serviceId: ID!, $keyword: String!",
query: `
getEndpoints: searchEndpoint(serviceId: $serviceId, keyword: $keyword, limit: 100) {
key: id
label: name
pods: findEndpoint(serviceId: $serviceId, keyword: $keyword, limit: 100) {
id
value: name
label: name
}
`,
};
export const getService = {
variable: "$serviceId: String!",
query: `
service: getService(serviceId: $serviceId) {
id
value: name
label: name
group
layers
normal
}
`,
};
export const getInstance = {
variable: "$instanceId: String!",
query: `
instance: getInstance(instanceId: $instanceId) {
id
value: name
label: name
language
instanceUUID
layer
attributes {
name
value
}
}
`,
};
export const getEndpoint = {
variable: "$endpointId: ID!",
query: `
endpoint: getEndpointInfo(endpointId: $endpointId) {
id
value: name
label: name
serviceId
serviceName
}
`,
};

View File

@ -16,25 +16,12 @@
*/
import {
TypeOfMetrics,
querySampledRecords,
queryHeatMap,
queryLabeledMetricsValues,
querySortMetrics,
queryMetricsValue,
queryMetricsValues,
listMetrics,
} from "../fragments/dashboard";
export const queryTypeOfMetrics = `query typeOfMetrics(${TypeOfMetrics.variable}) {${TypeOfMetrics.query}}`;
export const readHeatMap = `query queryData(${queryHeatMap.variable}) {${queryHeatMap.query}}`;
export const readSampledRecords = `query queryData(${querySampledRecords.variable}) {${querySampledRecords.query}}`;
export const readLabeledMetricsValues = `query queryData(${queryLabeledMetricsValues.variable}) {
${queryLabeledMetricsValues.query}}`;
export const sortMetrics = `query queryData(${querySortMetrics.variable}) {${querySortMetrics.query}}`;
export const readMetricsValue = `query queryData(${queryMetricsValue.variable}) {${queryMetricsValue.query}}`;
export const readMetricsValues = `query queryData(${queryMetricsValues.variable}) {${queryMetricsValues.query}}`;
export const queryMetrics = `query queryData(${listMetrics.variable}) {${listMetrics.query}}`;

View File

@ -14,10 +14,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Services, Layers, Endpoints, Instances } from "../fragments/selector";
import {
Services,
Layers,
Endpoints,
Instances,
getService,
getInstance,
getEndpoint,
} from "../fragments/selector";
export const queryServices = `query queryServices(${Services.variable}) {${Services.query}}`;
export const queryLayers = `query ${Layers.query}`;
export const queryEndpoints = `query queryEndpoints(${Endpoints.variable}) {${Endpoints.query}}`;
export const queryInstances = `query queryInstances(${Instances.variable}) {${Instances.query}}`;
export const queryLayers = `query listLayer {${Layers.query}}`;
export const queryService = `query queryService(${getService.variable}) {${getService.query}}`;
export const queryInstance = `query queryInstance(${getInstance.variable}) {${getInstance.query}}`;
export const queryEndpoint = `query queryInstance(${getEndpoint.variable}) {${getEndpoint.query}}`;

View File

@ -14,6 +14,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export enum MetricQueryTypes {
ReadMetricsValue = "readMetricsValue",
ReadMetricsValues = "readMetricsValues",
SortMetrics = "sortMetrics",
ReadLabeledMetricsValues = "readLabeledMetricsValues",
READHEATMAP = "readHeatMap",
ReadSampledRecords = "readSampledRecords",
}
export enum sizeEnum {
XS = "XS",
SM = "SM",
@ -32,7 +40,7 @@ export enum screenEnum {
XXL = 1600,
}
const screenMap = new Map<sizeEnum, number>();
export const screenMap = new Map<sizeEnum, number>();
screenMap.set(sizeEnum.XS, screenEnum.XS);
screenMap.set(sizeEnum.SM, screenEnum.SM);
@ -41,4 +49,47 @@ screenMap.set(sizeEnum.LG, screenEnum.LG);
screenMap.set(sizeEnum.XL, screenEnum.XL);
screenMap.set(sizeEnum.XXL, screenEnum.XXL);
export { screenMap };
export const RespFields: any = {
readMetricsValues: `{
label
values {
values {value}
}
}`,
readMetricsValue: "",
sortMetrics: `{
name
id
value
refId
}`,
readLabeledMetricsValues: `{
label
values {
values {value}
}
}`,
readHeatMap: `{
values {
id
values
}
buckets {
min
max
}
}`,
readSampledRecords: `{
name
value
refId
}`,
};
export enum CalculationType {
Plus = "+",
Minus = "-",
Multiplication = "*",
Division = "/",
"Convert Unix Timestamp(milliseconds)" = "milliseconds",
"Convert Unix Timestamp(seconds)" = "seconds",
}

254
src/hooks/useProcessor.ts Normal file
View File

@ -0,0 +1,254 @@
/**
* 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.
*/
import { RespFields, MetricQueryTypes } 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 } from "@/types/selector";
export function useQueryProcessor(config: any) {
if (!(config.metrics && config.metrics[0])) {
return;
}
const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore();
const selectorStore = useSelectorStore();
if (!selectorStore.currentService) {
return;
}
const conditions: { [key: string]: unknown } = {
duration: appStore.durationTime,
};
const variables: string[] = [`$duration: Duration!`];
const isRelation = [
"ServiceRelation",
"ServiceInstanceRelation",
"EndpointRelation",
].includes(dashboardStore.entity);
const fragment = config.metrics.map((name: string, index: number) => {
const metricType = config.metricTypes[index] || "";
const labels = ["0", "1", "2", "3", "4"];
if (
[
MetricQueryTypes.ReadSampledRecords,
MetricQueryTypes.SortMetrics,
].includes(metricType)
) {
variables.push(`$condition${index}: TopNCondition!`);
conditions[`condition${index}`] = {
name,
parentService: ["Service", "All"].includes(dashboardStore.entity)
? null
: selectorStore.currentService.value,
normal: selectorStore.currentService.normal,
scope: dashboardStore.entity,
topN: 10,
order: "DES",
};
} else {
if (metricType === MetricQueryTypes.ReadLabeledMetricsValues) {
variables.push(`$labels${index}: [String!]!`);
conditions[`labels${index}`] = labels;
}
variables.push(`$condition${index}: MetricsCondition!`);
conditions[`condition${index}`] = {
name,
entity: {
scope: dashboardStore.entity,
serviceName:
dashboardStore.entity === "All"
? undefined
: selectorStore.currentService.value,
normal:
dashboardStore.entity === "All"
? undefined
: selectorStore.currentService.normal,
serviceInstanceName: dashboardStore.entity.includes("ServiceInstance")
? selectorStore.currentPod && selectorStore.currentPod.value
: undefined,
endpointName: dashboardStore.entity.includes("Endpoint")
? selectorStore.currentPod && selectorStore.currentPod.value
: undefined,
destNormal: isRelation
? selectorStore.currentDestService.normal
: undefined,
destServiceName: isRelation
? selectorStore.currentDestService.value
: undefined,
destServiceInstanceName:
dashboardStore.entity === "ServiceInstanceRelation"
? selectorStore.currentDestPod &&
selectorStore.currentDestPod.value
: undefined,
destEndpointName:
dashboardStore.entity === "EndpointRelation"
? selectorStore.currentDestPod &&
selectorStore.currentDestPod.value
: undefined,
},
};
}
if (metricType === MetricQueryTypes.ReadLabeledMetricsValues) {
return `${name}${index}: ${metricType}(condition: $condition${index}, labels: $labels${index}, duration: $duration)${RespFields[metricType]}`;
} else {
return `${name}${index}: ${metricType}(condition: $condition${index}, duration: $duration)${RespFields[metricType]}`;
}
});
const queryStr = `query queryData(${variables}) {${fragment}}`;
return {
queryStr,
conditions,
};
}
export function useSourceProcessor(
resp: { errors: string; data: { [key: string]: any } },
config: { metrics: string[]; metricTypes: string[] }
) {
if (resp.errors) {
ElMessage.error(resp.errors);
return {};
}
const source: { [key: string]: unknown } = {};
const keys = Object.keys(resp.data);
config.metricTypes.forEach((type: string, index) => {
const m = config.metrics[index];
if (type === MetricQueryTypes.ReadMetricsValues) {
source[m] = resp.data[keys[index]].values.values.map(
(d: { value: number }) => d.value
);
}
if (type === MetricQueryTypes.ReadLabeledMetricsValues) {
const resVal = Object.values(resp.data)[0] || [];
const labelsIdx = ["0", "1", "2", "3", "4"];
const labels = ["P50", "P75", "P90", "P95", "P99"];
for (const item of resVal) {
const values = item.values.values.map(
(d: { value: number }) => d.value
);
const indexNum = labelsIdx.findIndex((d: string) => d === item.label);
if (labels[indexNum] && indexNum > -1) {
source[labels[indexNum]] = values;
} else {
source[item.label] = values;
}
}
}
if (type === MetricQueryTypes.ReadMetricsValue) {
source[m] = Object.values(resp.data)[0];
}
if (
type === MetricQueryTypes.SortMetrics ||
type === MetricQueryTypes.ReadSampledRecords
) {
source[m] = Object.values(resp.data)[0] || [];
}
if (type === MetricQueryTypes.READHEATMAP) {
const resVal = Object.values(resp.data)[0] || {};
const nodes = [] as any;
if (!(resVal && resVal.values)) {
source[m] = { nodes: [] };
return;
}
resVal.values.forEach((items: { values: number[] }, x: number) => {
const grids = items.values.map((val: number, y: number) => [x, y, val]);
nodes.push(...grids);
});
let buckets = [] as any;
if (resVal.buckets.length) {
buckets = [
resVal.buckets[0].min,
...resVal.buckets.map(
(item: { min: string; max: string }) => item.max
),
];
}
source[m] = { nodes, buckets }; // nodes: number[][]
}
});
return source;
}
export function useQueryPodsMetrics(
pods: Array<Instance | Endpoint>,
config: { metrics: string[]; metricTypes: string[] },
scope: string
) {
const appStore = useAppStoreWithOut();
const selectorStore = useSelectorStore();
const conditions: { [key: string]: unknown } = {
duration: appStore.durationTime,
};
const variables: string[] = [`$duration: Duration!`];
const { currentService } = selectorStore;
const fragmentList = pods.map((d: Instance | Endpoint, index: number) => {
const param = {
scope,
serviceName: currentService.label,
serviceInstanceName: scope === "ServiceInstance" ? d.label : undefined,
endpointName: scope === "Endpoint" ? d.label : undefined,
normal: currentService.normal,
};
const f = config.metrics.map((name: string, idx: number) => {
const metricType = config.metricTypes[idx] || "";
conditions[`condition${index}${idx}`] = {
name,
entity: param,
};
variables.push(`$condition${index}${idx}: MetricsCondition!`);
return `${name}${index}${idx}: ${metricType}(condition: $condition${index}${idx}, duration: $duration)${RespFields[metricType]}`;
});
return f;
});
const fragment = fragmentList.flat(1).join(" ");
const queryStr = `query queryData(${variables}) {${fragment}}`;
return { queryStr, conditions };
}
export function usePodsSource(
pods: Array<Instance | Endpoint>,
resp: { errors: string; data: { [key: string]: any } },
config: { metrics: string[]; metricTypes: string[] }
): any {
if (resp.errors) {
ElMessage.error(resp.errors);
return {};
}
const data = pods.map((d: Instance | any, idx: number) => {
config.metrics.map((name: string, index: number) => {
const key = name + idx + index;
if (config.metricTypes[index] === MetricQueryTypes.ReadMetricsValue) {
d[name] = resp.data[key];
}
if (config.metricTypes[index] === MetricQueryTypes.ReadMetricsValues) {
d[name] = resp.data[key].values.values.map(
(d: { value: number }) => d.value
);
}
});
return d;
});
return data;
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<section class="app-main">
<router-view v-slot="{ Component }">
<router-view v-slot="{ Component }" :key="$route.fullPath">
<keep-alive>
<component :is="Component" />
</keep-alive>

View File

@ -30,7 +30,7 @@ const msg = {
events: "Events",
alerts: "Alerts",
settings: "Settings",
dashboards: "Dashboards",
dashboards: "Dashboard",
profiles: "Profiles",
database: "Database",
serviceName: "Service Name",
@ -53,7 +53,8 @@ const msg = {
instance: "Instance",
create: "Create",
loading: "Loading",
selectVisualization: "Select your visualization",
selectVisualization: "Visualize your metrics",
visualization: "Visualization",
graphStyles: "Graph styles",
widgetOptions: "Widget options",
standardOptions: "Standard options",
@ -72,6 +73,8 @@ const msg = {
fontSize: "Font Size",
showBackground: "Show Background",
areaOpacity: "Area Opacity",
editGraph: "Edit Graph Options",
dashboardName: "Select Dashboard Name",
hourTip: "Select Hour",
minuteTip: "Select Minute",
secondTip: "Select Second",

View File

@ -51,7 +51,8 @@ const msg = {
endpoint: "端点",
create: "新建",
loading: "加载中",
selectVisualization: "选择你的可视化",
selectVisualization: "可视化指标",
visualization: "可视化",
graphStyles: "图形样式",
widgetOptions: "组件选项",
standardOptions: "标准选项",
@ -70,6 +71,8 @@ const msg = {
fontSize: "字体大小",
showBackground: "显示背景",
areaOpacity: "透明度",
editGraph: "编辑图表选项",
dashboardName: "选择仪表板名称",
hourTip: "选择小时",
minuteTip: "选择分钟",
secondTip: "选择秒数",

View File

@ -20,10 +20,10 @@ import router from "./router";
import { store } from "./store";
import components from "@/components";
import i18n from "./locales";
import "element-plus/dist/index.css";
import "./styles/lib.scss";
import "./styles/reset.scss";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
const app = createApp(App);

View File

@ -48,9 +48,29 @@ export const routesDashboard: Array<RouteRecordRaw> = [
},
},
{
path: "/dashboard/edit/:layerId/:entity/:dashboardId",
path: "/dashboard/:layerId/:entity/:name",
component: () => import("@/views/dashboard/Edit.vue"),
name: "Edit",
name: "Create",
meta: {
title: "dashboardEdit",
exact: false,
notShow: true,
},
},
{
path: "/dashboard/:layerId/:entity/:serviceId/:name",
component: () => import("@/views/dashboard/Edit.vue"),
name: "CreateService",
meta: {
title: "dashboardEdit",
exact: false,
notShow: true,
},
},
{
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:name",
component: () => import("@/views/dashboard/Edit.vue"),
name: "ViewPod",
meta: {
title: "dashboardEdit",
exact: false,

View File

@ -26,6 +26,8 @@ export const NewControl = {
},
graph: {},
standard: {},
metrics: [""],
metricTypes: [""],
};
export const ConfigData: any = {
x: 0,
@ -34,14 +36,64 @@ export const ConfigData: any = {
h: 12,
i: "0",
metrics: ["service_resp_time"],
queryMetricType: "readMetricsValues",
metricTypes: ["readMetricsValues"],
type: "Widget",
widget: {
title: "Title",
title: "service_resp_time",
tips: "Tooltip",
},
graph: {
type: "Line",
showXAxis: true,
showYAxis: true,
},
standard: {
sortOrder: "DEC",
unit: "min",
},
children: [],
};
export const ConfigData1: any = {
x: 0,
y: 0,
w: 8,
h: 12,
i: "0",
metrics: ["service_instance_resp_time"],
metricTypes: ["readMetricsValues"],
type: "Widget",
widget: {
title: "service_instance_resp_time",
tips: "Tooltip",
},
graph: {
type: "Line",
showXAxis: true,
showYAxis: true,
},
standard: {
sortOrder: "DEC",
unit: "min",
},
children: [],
};
export const ConfigData2: any = {
x: 0,
y: 0,
w: 8,
h: 12,
i: "0",
metrics: ["endpoint_avg"],
metricTypes: ["readMetricsValues"],
type: "Widget",
widget: {
title: "endpoint_avg",
tips: "Tooltip",
},
graph: {
type: "Line",
showXAxis: true,
showYAxis: true,
},
standard: {
sortOrder: "DEC",

View File

@ -18,10 +18,13 @@ import { defineStore } from "pinia";
import { store } from "@/store";
import { LayoutConfig } from "@/types/dashboard";
import graph from "@/graph";
import { AxiosResponse } from "axios";
import { ConfigData } from "../data";
import { ConfigData, ConfigData1, ConfigData2 } from "../data";
import { useAppStoreWithOut } from "@/store/modules/app";
import { useSelectorStore } from "@/store/modules/selectors";
import { NewControl } from "../data";
import { Duration } from "@/types/app";
import axios, { AxiosResponse } from "axios";
import { cancelToken } from "@/utils/cancelToken";
interface DashboardState {
showConfig: boolean;
layout: LayoutConfig[];
@ -29,6 +32,8 @@ interface DashboardState {
entity: string;
layerId: string;
activedGridItem: string;
durationTime: Duration;
selectorStore: any;
}
export const dashboardStore = defineStore({
@ -40,6 +45,8 @@ export const dashboardStore = defineStore({
entity: "",
layerId: "",
activedGridItem: "",
durationTime: useAppStoreWithOut().durationTime,
selectorStore: useSelectorStore(),
}),
actions: {
setLayout(data: LayoutConfig[]) {
@ -50,6 +57,8 @@ export const dashboardStore = defineStore({
...NewControl,
i: String(this.layout.length),
type,
metricTypes: [""],
metrics: [""],
};
if (type === "Tab") {
newWidget.h = 24;
@ -129,14 +138,20 @@ export const dashboardStore = defineStore({
setConfigPanel(show: boolean) {
this.showConfig = show;
},
selectWidget(widget: Nullable<LayoutConfig>) {
this.selectedGrid = widget;
selectWidget(item: Nullable<LayoutConfig>) {
this.selectedGrid = item;
},
setLayer(id: string) {
this.layerId = id;
},
setEntity(type: string) {
this.entity = type;
if (type === "ServiceInstance") {
this.layout = [ConfigData1];
}
if (type === "Endpoint") {
this.layout = [ConfigData2];
}
},
setConfigs(param: { [key: string]: unknown }) {
const actived = this.activedGridItem.split("-");
@ -165,29 +180,24 @@ export const dashboardStore = defineStore({
return res.data;
},
async fetchMetricValue(config: LayoutConfig) {
// if (!config.queryMetricType) {
// return;
// }
config.queryMetricType = "readMetricsValues";
const appStoreWithOut = useAppStoreWithOut();
const variable = {
condition: {
name: "service_resp_time",
entity: {
normal: true,
scope: "Service",
serviceName: "agentless::app",
},
},
duration: appStoreWithOut.durationTime,
};
async fetchMetricList(regex: string) {
const res: AxiosResponse = await graph
.query(config.queryMetricType)
.params(variable);
.query("queryMetrics")
.params({ regex });
return res.data;
},
async fetchMetricValue(param: {
queryStr: string;
conditions: { [key: string]: unknown };
}) {
const res: AxiosResponse = await axios.post(
"/graphql",
{ query: param.queryStr, variables: { ...param.conditions } },
{ cancelToken: cancelToken() }
);
return res.data;
},
},
});

View File

@ -15,25 +15,45 @@
* limitations under the License.
*/
import { defineStore } from "pinia";
import { Option, Duration } from "@/types/app";
import { Duration } from "@/types/app";
import { Service, Instance, Endpoint } from "@/types/selector";
import { store } from "@/store";
import graph from "@/graph";
import { AxiosResponse } from "axios";
import { useAppStoreWithOut } from "@/store/modules/app";
interface SelectorState {
services: Option[];
services: Service[];
pods: Array<Instance | Endpoint>;
currentService: Nullable<Service>;
currentPod: Nullable<Instance | Endpoint>;
currentDestService: Nullable<Service>;
currentDestPod: Nullable<Instance | Endpoint>;
durationTime: Duration;
}
export const selectorStore = defineStore({
id: "selector",
state: (): SelectorState => ({
services: [],
pods: [],
currentService: null,
currentPod: null,
currentDestService: null,
currentDestPod: null,
durationTime: useAppStoreWithOut().durationTime,
}),
actions: {
setCurrentService(service: Service) {
this.currentService = service;
},
setCurrentPod(pod: Nullable<Instance | Endpoint>) {
this.currentPod = pod;
},
async fetchLayers(): Promise<AxiosResponse> {
const res: AxiosResponse = await graph.query("queryLayers").params({});
return res;
return res.data || {};
},
async fetchServices(layer: string): Promise<AxiosResponse> {
const res: AxiosResponse = await graph
@ -41,30 +61,89 @@ export const selectorStore = defineStore({
.params({ layer });
if (!res.data.errors) {
this.services = res.data.data.services;
this.services = res.data.data.services || [];
}
return res;
return res.data;
},
async getServiceInstances(params: {
async getServiceInstances(param?: {
serviceId: string;
duration: Duration;
}): Promise<AxiosResponse> {
const res: AxiosResponse = await graph
.query("queryInstances")
.params(params);
return res;
}): Promise<Nullable<AxiosResponse>> {
const serviceId = param ? param.serviceId : this.currentService?.id;
if (!serviceId) {
return null;
}
const res: AxiosResponse = await graph.query("queryInstances").params({
serviceId,
duration: this.durationTime,
});
if (!res.data.errors) {
this.pods = res.data.data.pods || [];
}
return res.data;
},
async getEndpoints(params: {
keyword: string;
serviceId: string;
}): Promise<AxiosResponse> {
async getEndpoints(params?: {
keyword?: string;
serviceId?: string;
}): Promise<Nullable<AxiosResponse>> {
if (!params) {
params = {};
}
if (!params.keyword) {
params.keyword = "";
}
const res: AxiosResponse = await graph
.query("queryEndpoints")
.params(params);
return res;
const serviceId = params.serviceId || this.currentService?.id;
if (!serviceId) {
return null;
}
const res: AxiosResponse = await graph.query("queryEndpoints").params({
serviceId,
duration: this.durationTime,
keyword: params.keyword,
});
if (!res.data.errors) {
this.pods = res.data.data.pods || [];
}
return res.data;
},
async getService(serviceId: string) {
if (!serviceId) {
return;
}
const res: AxiosResponse = await graph.query("queryService").params({
serviceId,
});
if (!res.data.errors) {
this.currentService = res.data.data.service || {};
this.services = [res.data.data.service];
}
return res.data;
},
async getInstance(instanceId: string) {
if (!instanceId) {
return;
}
const res: AxiosResponse = await graph.query("queryInstance").params({
instanceId,
});
if (!res.data.errors) {
this.currentPod = res.data.data.instance || null;
}
return res.data;
},
async getEndpoint(endpointId: string) {
if (!endpointId) {
return;
}
const res: AxiosResponse = await graph.query("queryEndpoint").params({
endpointId,
});
if (!res.data.errors) {
this.currentPod = res.data.data.endpoint || null;
}
return res.data;
},
},
});

View File

@ -15,20 +15,22 @@
* limitations under the License.
*/
body {
body {
margin: 0;
line-height: 1.5;
font-size: 14px;
color: #3d444f;
font-family: 'Helvetica', 'Arial', 'Source Han Sans CN', 'Microsoft YaHei', 'sans-serif';
font-family: Helvetica, Arial, "Source Han Sans CN", "Microsoft YaHei",
sans-serif;
text-rendering: optimizeLegibility;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
html,
body {
height: 100%;
}
div,
header,
footer,
@ -53,6 +55,7 @@ a,
img {
box-sizing: border-box;
}
input,
textarea,
select,
@ -60,41 +63,51 @@ button {
font-size: 100%;
font-family: inherit;
}
h1 {
font-size: 26px;
}
h2 {
font-size: 24px;
}
h3 {
font-size: 21px;
}
h4 {
font-size: 18px;
}
h5 {
font-size: 14px;
}
h6 {
font-size: 1em;
}
ul,
ol {
margin: 0;
padding-left: 0;
list-style-type: none;
}
a {
text-decoration: none;
cursor: pointer;
color: inherit;
-webkit-tap-highlight-color: transparent;
-webkit-appearance: none;
appearance: none;
}
hr {
border-width: 0;
border-bottom: 1px solid #e0e0e0;
}
blockquote {
margin-left: 0;
margin-right: 0;
@ -102,76 +115,11 @@ blockquote {
border-left: 4px solid #cacaca;
}
.el-dialog__body {
padding: 10px 20px;
}
code,
pre {
font-family: Consolas, Menlo, Courier, monospace;
}
/*webkit core*/
.scroll_hide::-webkit-scrollbar {
width: 0px;
height: 0px;
}
.scroll_hide::-webkit-scrollbar-button {
background-color: rgba(0, 0, 0, 0);
}
.scroll_hide::-webkit-scrollbar-track {
background-color: rgba(0, 0, 0, 0);
}
.scroll_hide::-webkit-scrollbar-track-piece {
background-color: rgba(0, 0, 0, 0);
}
.scroll_hide::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0);
}
.scroll_hide::-webkit-scrollbar-corner {
background-color: rgba(0, 0, 0, 0);
}
.scroll_hide::-webkit-scrollbar-resizer {
background-color: rgba(0, 0, 0, 0);
}
/*o core*/
.scroll_hide .-o-scrollbar {
-moz-appearance: none !important;
background: rgba(0, 255, 0, 0) !important;
}
.scroll_hide::-o-scrollbar-button {
background-color: rgba(0, 0, 0, 0);
}
.scroll_hide::-o-scrollbar-track {
background-color: rgba(0, 0, 0, 0);
}
.scroll_hide::-o-scrollbar-track-piece {
background-color: rgba(0, 0, 0, 0);
}
.scroll_hide::-o-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0);
}
.scroll_hide::-o-scrollbar-corner {
background-color: rgba(0, 0, 0, 0);
}
.scroll_hide::-o-scrollbar-resizer {
background-color: rgba(0, 0, 0, 0);
}
/*IE10,IE11,IE12*/
.scroll_hide {
-ms-scroll-chaining: chained;
-ms-overflow-style: none;
-ms-content-zooming: zoom;
-ms-scroll-rails: none;
-ms-content-zoom-limit-min: 100%;
-ms-content-zoom-limit-max: 500%;
-ms-scroll-snap-points-x: snapList(100%, 200%, 300%, 400%, 500%);
-ms-overflow-style: none;
overflow: auto;
}
.scroll_bar_style::-webkit-scrollbar {
width: 9px;
height: 6px;
}
.scroll_bar_style::-webkit-scrollbar-track {
background-color: #3d444f;
}
.scroll_bar_style::-webkit-scrollbar-thumb {
border-radius: 5px;
background: rgba(196, 200, 225, .2);
}

2
src/types/app.d.ts vendored
View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
export interface Option {
value: string | number;
value: string;
label: string;
}
export interface Duration {

View File

@ -20,12 +20,12 @@ export interface LayoutConfig {
w: number;
h: number;
i: string;
widget?: WidgetConfig;
graph?: GraphConfig;
standard?: StandardConfig;
metrics?: string[];
type?: string;
queryMetricType?: string;
widget: WidgetConfig;
graph: GraphConfig;
standard: StandardConfig;
metrics: string[];
type: string;
metricTypes: string[];
children?: any;
}
@ -45,9 +45,17 @@ export interface StandardConfig {
divide?: string;
milliseconds?: string;
seconds?: string;
maxItemNum?: number;
}
export type GraphConfig = BarConfig | LineConfig | CardConfig | TableConfig;
export type GraphConfig =
| BarConfig
| LineConfig
| CardConfig
| TableConfig
| EndpointListConfig
| ServiceListConfig
| InstanceListConfig;
export interface BarConfig {
type?: string;
showBackground?: boolean;
@ -57,6 +65,8 @@ export interface LineConfig extends AreaConfig {
smooth?: boolean;
showSymbol?: boolean;
step?: boolean;
showXAxis?: boolean;
showYAxis?: boolean;
}
export interface AreaConfig {
@ -67,7 +77,8 @@ export interface AreaConfig {
export interface CardConfig {
type?: string;
fontSize?: number;
showUint: boolean;
showUint?: boolean;
textAlign?: "center" | "right" | "left";
}
export interface TableConfig {
@ -81,3 +92,21 @@ export interface TopListConfig {
type?: string;
topN: number;
}
export interface ServiceListConfig {
type?: string;
dashboardName: string;
fontSize: number;
}
export interface InstanceListConfig {
type?: string;
dashboardName: string;
fontSize: number;
}
export interface EndpointListConfig {
type?: string;
dashboardName: string;
fontSize: number;
}

49
src/types/selector.d.ts vendored Normal file
View File

@ -0,0 +1,49 @@
/**
* 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 type Service = {
id: string;
label: string;
value: string;
layers: string[];
normal: boolean;
group: string;
};
export type Instance = {
value: string;
label: string;
layer: string;
language: string;
instanceUUID: string;
attributes: { name: string; value: string }[];
};
export type Endpoint = {
id: string;
label: string;
value: string;
};
export type Service = {
id: string;
value: string;
label: string;
group: string;
normal: boolean;
layers: string[];
shortName: string;
};

View File

@ -16,7 +16,7 @@ limitations under the License. -->
<div class="about">{{ props.msg }}</div>
</template>
<script lang="ts" setup>
import { defineProps } from "vue";
/*global defineProps */
const props = defineProps({
msg: String,
});

View File

@ -18,23 +18,25 @@ limitations under the License. -->
<grid-layout />
<el-dialog
v-model="dashboardStore.showConfig"
title="Edit Graph Options"
:title="t('editGraph')"
fullscreen
:destroy-on-close="true"
@closed="dashboardStore.setConfigPanel(false)"
>
<widget-config />
<config-edit />
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import GridLayout from "./panel/Layout.vue";
// import { LayoutConfig } from "@/types/dashboard";
import Tool from "./panel/Tool.vue";
import WidgetConfig from "./configuration/ConfigEdit.vue";
import ConfigEdit from "./configuration/ConfigEdit.vue";
import { useDashboardStore } from "@/store/modules/dashboard";
const dashboardStore = useDashboardStore();
const { t } = useI18n();
// fetch layout data from serve side
// const layout: any[] = [
// { x: 0, y: 0, w: 4, h: 12, i: "0" },
@ -54,6 +56,7 @@ const dashboardStore = useDashboardStore();
// { x: 8, y: 27, w: 4, h: 15, i: "16" },
// ];
// dashboardStore.setLayout(layout);
function handleClick(e: any) {
e.stopPropagation();
if (e.target.className === "ds-main") {

View File

@ -26,8 +26,8 @@ limitations under the License. -->
<div class="item">
<div class="label">{{ t("layer") }}</div>
<Selector
:value="states.layer"
:options="Options"
v-model="states.selectedLayer"
:options="states.layers"
size="small"
placeholder="Select a layer"
@change="changeLayer"
@ -37,7 +37,7 @@ limitations under the License. -->
<div class="item">
<div class="label">{{ t("entityType") }}</div>
<Selector
:value="states.entity"
v-model="states.entity"
:options="EntityType"
size="small"
placeholder="Select a entity"
@ -53,28 +53,38 @@ limitations under the License. -->
</div>
</template>
<script lang="ts" setup>
import { reactive } from "vue";
import { reactive, onBeforeMount } from "vue";
import { useI18n } from "vue-i18n";
import router from "@/router";
import { useSelectorStore } from "@/store/modules/selectors";
import { EntityType, Options } from "./data";
import uuid from "@/utils/uuid";
import { EntityType } from "./data";
import { ElMessage } from "element-plus";
const { t } = useI18n();
const selectorStore = useSelectorStore();
const states = reactive({
name: "",
layer: Options[0].value,
selectedLayer: "",
entity: EntityType[0].value,
layers: [],
});
const onCreate = () => {
const id = uuid();
const path = `/dashboard/edit/${states.layer}/${states.entity}/${id}`;
const name = states.name.split(" ").join("-");
const path = `/dashboard/${states.selectedLayer}/${states.entity}/${name}`;
router.push(path);
};
selectorStore.fetchServices("general");
onBeforeMount(async () => {
const resp = await selectorStore.fetchLayers();
if (resp.errors) {
ElMessage.error(resp.errors);
}
states.selectedLayer = resp.data.layers[0];
states.layers = resp.data.layers.map((d: string) => {
return { label: d, value: d };
});
});
function changeLayer(opt: { label: string; value: string }[]) {
states.layer = opt[0].value;
states.selectedLayer = opt[0].value;
}
function changeEntity(opt: { label: string; value: string }[]) {
states.entity = opt[0].value;

View File

@ -14,23 +14,30 @@ See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="widget-config flex-v">
<div class="graph">
<div class="graph" v-loading="loading">
<div class="header">
<span>{{ states.widget.title }}</span>
<div class="tips" v-show="states.widget.tips">
<el-tooltip :content="states.widget.tips">
<span>{{ dashboardStore.selectedGrid.widget.title }}</span>
<div class="tips" v-show="dashboardStore.selectedGrid.widget.tips">
<el-tooltip :content="dashboardStore.selectedGrid.widget.tips">
<Icon iconName="info_outline" size="sm" />
</el-tooltip>
</div>
</div>
<div class="render-chart">
<component
:is="states.graph.type"
:is="dashboardStore.selectedGrid.graph.type"
:intervalTime="appStoreWithOut.intervalTime"
:data="states.source"
:config="states.graph"
:config="{
...dashboardStore.selectedGrid.graph,
i: dashboardStore.selectedGrid.i,
metrics: dashboardStore.selectedGrid.metrics,
metricTypes: dashboardStore.selectedGrid.metricTypes,
}"
/>
<div v-show="!states.graph.type" class="no-data">{{ t("noData") }}</div>
<div v-show="!dashboardStore.selectedGrid.graph.type" class="no-data">
{{ t("noData") }}
</div>
</div>
</div>
<div class="collapse" :style="{ height: configHeight + 'px' }">
@ -38,59 +45,17 @@ limitations under the License. -->
v-model="states.activeNames"
:style="{ '--el-collapse-header-font-size': '15px' }"
>
<el-collapse-item :title="t('metricName')" name="1">
<div>
<Selector
:value="states.metrics"
:options="metricOpts"
:multiple="true"
size="mini"
placeholder="Select a metric"
@change="changeMetrics"
class="selectors"
/>
<Selector
v-if="states.valueType"
:value="states.valueType"
:options="states.valueTypes"
size="mini"
placeholder="Select a metric"
@change="changeValueType"
class="selectors"
v-loading="loading"
/>
</div>
<el-collapse-item :title="t('selectVisualization')" name="1">
<MetricOptions @update="getSource" @loading="setLoading" />
</el-collapse-item>
<el-collapse-item :title="t('selectVisualization')" name="2">
<div class="chart-types">
<span
v-for="(type, index) in ChartTypes"
:key="index"
@click="changeChartType(type)"
:class="{ active: type.value === states.graph.type }"
>
{{ type.label }}
</span>
</div>
<el-collapse-item :title="t('graphStyles')" name="2">
<component :is="`${dashboardStore.selectedGrid.graph.type}Config`" />
</el-collapse-item>
<el-collapse-item :title="t('graphStyles')" name="3">
<component
:is="`${states.graph.type}Config`"
:config="states.graph"
@update="updateGraphOptions"
/>
<el-collapse-item :title="t('widgetOptions')" name="3">
<WidgetOptions />
</el-collapse-item>
<el-collapse-item :title="t('widgetOptions')" name="4">
<WidgetOptions
:config="states.widget"
@update="updateWidgetOptions"
/>
</el-collapse-item>
<el-collapse-item :title="t('standardOptions')" name="5">
<StandardOptions
:config="states.standard"
@update="updateStandardOptions"
/>
<el-collapse-item :title="t('standardOptions')" name="4">
<StandardOptions />
</el-collapse-item>
</el-collapse>
</div>
@ -109,19 +74,12 @@ import { reactive, defineComponent, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app";
import { ElMessage } from "element-plus";
import {
ValuesTypes,
MetricQueryTypes,
ChartTypes,
DefaultGraphConfig,
} from "../data";
import { Option } from "@/types/app";
import { WidgetConfig, GraphConfig, StandardConfig } from "@/types/dashboard";
import graphs from "../graphs";
import configs from "./graph-styles";
import WidgetOptions from "./WidgetOptions.vue";
import StandardOptions from "./StandardOptions.vue";
import MetricOptions from "./MetricOptions.vue";
export default defineComponent({
name: "ConfigEdit",
@ -130,166 +88,49 @@ export default defineComponent({
...configs,
WidgetOptions,
StandardOptions,
MetricOptions,
},
setup() {
const loading = ref<boolean>(false);
const configHeight = document.documentElement.clientHeight - 520;
const { t } = useI18n();
const dashboardStore = useDashboardStore();
const appStoreWithOut = useAppStoreWithOut();
const { selectedGrid } = dashboardStore;
const loading = ref<boolean>(false);
const states = reactive<{
metrics: string[];
valueTypes: Option[];
valueType: string;
metricQueryType: string;
activeNames: string;
source: any;
index: string;
graph: GraphConfig;
widget: WidgetConfig | any;
standard: StandardConfig;
visType: Option[];
}>({
metrics: selectedGrid.metrics || [],
valueTypes: [],
valueType: "",
metricQueryType: "",
activeNames: "1",
source: {},
index: selectedGrid.i,
graph: selectedGrid.graph,
widget: selectedGrid.widget,
standard: selectedGrid.standard,
index: dashboardStore.selectedGrid.i,
visType: [],
});
if (states.metrics[0]) {
queryMetricType(states.metrics[0]);
function getSource(source: unknown) {
states.source = source;
}
async function changeMetrics(arr: Option[]) {
if (!arr.length) {
states.valueTypes = [];
states.valueType = "";
return;
}
states.metrics = arr.map((d: Option) => String(d.value));
if (arr[0].value) {
queryMetricType(String(arr[0].value));
}
function setLoading(load: boolean) {
loading.value = load;
}
async function queryMetricType(metric: string) {
loading.value = true;
const resp = await dashboardStore.fetchMetricType(metric);
loading.value = false;
if (resp.error) {
ElMessage.error(resp.data.error);
return;
}
const { typeOfMetrics } = resp.data;
states.valueTypes = ValuesTypes[typeOfMetrics];
states.valueType = ValuesTypes[typeOfMetrics][0].value;
queryMetrics();
}
function changeValueType(val: Option[]) {
states.valueType = String(val[0].value);
states.metricQueryType = (MetricQueryTypes as any)[states.valueType];
queryMetrics();
}
function changeChartType(item: Option) {
states.graph = {
...DefaultGraphConfig[item.value],
};
}
const metricOpts = [
{ value: "service_apdex", label: "service_apdex" },
{ value: "service_sla", label: "service_sla" },
{ value: "service_cpm", label: "service_cpm" },
{ value: "service_resp_time", label: "service_resp_time" },
{ value: "service_percentile", label: "service_percentile" },
{
value: "service_mq_consume_latency",
label: "service_mq_consume_latency",
},
{ value: "service_mq_consume_count", label: "service_mq_consume_count" },
];
const configHeight = document.documentElement.clientHeight - 520;
function updateWidgetOptions(param: { [key: string]: unknown }) {
states.widget = {
...states.widget,
...param,
};
}
function updateGraphOptions(param: { [key: string]: unknown }) {
states.graph = {
...states.graph,
...param,
};
}
function updateStandardOptions(param: { [key: string]: unknown }) {
states.standard = {
...states.standard,
...param,
};
}
async function queryMetrics() {
const json = await dashboardStore.fetchMetricValue(
dashboardStore.selectedGrid
);
if (!json) {
return;
}
if (json.error) {
return;
}
const metricVal = json.data.readMetricsValues.values.values.map(
(d: { value: number }) => d.value + 1
);
const m = states.metrics[0];
if (!m) {
return;
}
states.source = {
[m]: metricVal,
};
}
queryMetrics();
function applyConfig() {
const opts = {
...dashboardStore.selectedGrid,
metrics: states.metrics,
queryMetricType: states.valueType,
widget: states.widget,
graph: states.graph,
standard: states.standard,
};
dashboardStore.setConfigs(opts);
dashboardStore.setConfigs(dashboardStore.selectedGrid);
dashboardStore.setConfigPanel(false);
}
return {
states,
changeChartType,
changeValueType,
changeMetrics,
loading,
t,
appStoreWithOut,
ChartTypes,
metricOpts,
updateWidgetOptions,
configHeight,
updateGraphOptions,
updateStandardOptions,
dashboardStore,
applyConfig,
loading,
getSource,
setLoading,
};
},
});
@ -326,7 +167,7 @@ export default defineComponent({
.render-chart {
padding: 5px;
height: 350px;
height: 400px;
width: 100%;
}
@ -343,30 +184,10 @@ export default defineComponent({
min-width: 1280px;
}
.chart-types {
span {
display: inline-block;
padding: 5px 10px;
border: 1px solid #ccc;
background-color: #fff;
border-right: 0;
cursor: pointer;
}
span:nth-last-child(1) {
border-right: 1px solid #ccc;
}
}
.no-data {
font-size: 14px;
text-align: center;
line-height: 350px;
}
span.active {
background-color: #409eff;
color: #fff;
line-height: 400px;
}
.footer {
@ -384,4 +205,8 @@ span.active {
margin-top: 10px;
overflow: auto;
}
.ds-name {
margin-bottom: 10px;
}
</style>

View File

@ -0,0 +1,329 @@
<!-- 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 v-show="states.isTable" class="ds-name">
<div>{{ t("dashboards") }}</div>
<el-input
v-model="states.dashboardName"
placeholder="Please input dashboard name"
@change="changeDashboard"
class="selectors"
/>
</div>
<div>{{ t("metrics") }}</div>
<div
v-for="(metric, index) in states.metrics"
:key="index"
class="metric-item"
>
<Selector
:value="metric"
:options="states.metricList"
size="mini"
placeholder="Select a metric"
@change="changeMetrics(index, $event)"
class="selectors"
/>
<Selector
:value="states.metricTypes[index]"
:options="states.metricTypeList[index]"
size="mini"
:disabled="
dashboardStore.selectedGrid.graph.type && !states.isTable && index !== 0
"
@change="changeMetricType(index, $event)"
class="selectors"
/>
<span
v-show="states.isTable || states.metricTypes[0] === 'readMetricsValues'"
>
<Icon
class="cp mr-5"
v-show="
index === states.metrics.length - 1 && states.metrics.length < 5
"
iconName="add_circle_outlinecontrol_point"
size="middle"
@click="addMetric"
/>
<Icon
class="cp"
v-show="states.metrics.length > 1"
iconName="remove_circle_outline"
size="middle"
@click="deleteMetric(index)"
/>
</span>
</div>
<div>{{ t("visualization") }}</div>
<div class="chart-types">
<span
v-for="(type, index) in states.visTypes"
:key="index"
@click="changeChartType(type)"
:class="{
active: type.value === dashboardStore.selectedGrid.graph.type,
}"
>
{{ type.label }}
</span>
</div>
</template>
<script lang="ts" setup>
import { reactive } from "vue";
import { Option } from "@/types/app";
import { useDashboardStore } from "@/store/modules/dashboard";
import {
MetricTypes,
TableChartTypes,
MetricCatalog,
DefaultGraphConfig,
EntityType,
ChartTypes,
PodsChartTypes,
TableEntity,
} from "../data";
import { ElMessage } from "element-plus";
import Icon from "@/components/Icon.vue";
import { useQueryProcessor, useSourceProcessor } from "@/hooks/useProcessor";
import { useI18n } from "vue-i18n";
/*global defineEmits */
const { t } = useI18n();
const emit = defineEmits(["update", "loading"]);
const dashboardStore = useDashboardStore();
const { metrics, metricTypes, graph } = dashboardStore.selectedGrid;
const states = reactive<{
metrics: string[];
metricTypes: string[];
metricTypeList: Option[][];
visTypes: Option[];
isTable: boolean;
metricList: (Option & { type: string })[];
dashboardName: string;
}>({
metrics: metrics && metrics.length ? metrics : [""],
metricTypes: metricTypes && metricTypes.length ? metricTypes : [""],
metricTypeList: [],
visTypes: [],
isTable: false,
metricList: [],
dashboardName: graph.dashboardName,
});
states.isTable = TableChartTypes.includes(graph.type);
states.visTypes = setVisTypes();
setMetricType();
async function setMetricType(catalog?: string) {
if (states.isTable) {
catalog = catalog || TableEntity[graph.type];
} else {
catalog = catalog || dashboardStore.entity;
}
const json = await dashboardStore.fetchMetricList();
if (json.errors) {
ElMessage.error(json.errors);
return;
}
states.metricList = (json.data.metrics || []).filter(
(d: { catalog: string }) => catalog === (MetricCatalog as any)[d.catalog]
);
const metrics: any = states.metricList.filter(
(d: { value: string; type: string }) => {
const metric = states.metrics.filter((m: string) => m === d.value)[0];
if (metric) {
return d;
}
}
);
for (const metric of metrics) {
states.metricTypeList.push(MetricTypes[metric.type]);
}
if (states.metrics && states.metrics[0]) {
queryMetrics();
}
}
function setVisTypes() {
let graphs = [];
if (dashboardStore.entity === EntityType[0].value) {
graphs = ChartTypes.filter((d: Option) => d.value !== ChartTypes[7].value);
} else if (dashboardStore.entity === EntityType[1].value) {
graphs = ChartTypes.filter(
(d: Option) => !PodsChartTypes.includes(d.value)
);
} else {
graphs = ChartTypes.filter(
(d: Option) => !TableChartTypes.includes(d.value)
);
}
return graphs;
}
function changeChartType(item: Option) {
const graph = DefaultGraphConfig[item.value];
states.isTable = TableChartTypes.includes(graph.type);
dashboardStore.selectWidget({ ...dashboardStore.selectedGrid, graph });
states.isTable = TableChartTypes.includes(graph.type);
if (states.isTable) {
dashboardStore.selectWidget({
...dashboardStore.selectedGrid,
metrics: [""],
metricTypes: [""],
});
states.metrics = [""];
states.metricTypes = [""];
}
const catalog: { [key: string]: string } = {
InstanceList: EntityType[3].value,
EndpointList: EntityType[2].value,
ServiceList: EntityType[0].value,
};
if (catalog[graph.type]) {
setMetricType(catalog[graph.type]);
}
}
function changeMetrics(index: number, arr: (Option & { type: string })[]) {
if (!arr.length) {
states.metricTypeList = [];
states.metricTypes = [];
dashboardStore.selectWidget({
...dashboardStore.selectedGrid,
...{ metricTypes: states.metricTypes, metrics: states.metrics },
});
return;
}
states.metrics[index] = arr[0].value;
const typeOfMetrics = arr[0].type;
states.metricTypeList[index] = MetricTypes[typeOfMetrics];
states.metricTypes[index] = MetricTypes[typeOfMetrics][0].value;
dashboardStore.selectWidget({
...dashboardStore.selectedGrid,
...{ metricTypes: states.metricTypes, metrics: states.metrics },
});
if (states.isTable) {
return;
}
queryMetrics();
}
function changeMetricType(index: number, opt: Option[]) {
const metric =
states.metricList.filter(
(d: Option) => states.metrics[index] === d.value
)[0] || {};
if (states.isTable) {
states.metricTypes[index] = opt[0].value;
states.metricTypeList[index] = (MetricTypes as any)[metric.type];
} else {
states.metricTypes = states.metricTypes.map((d: string) => {
d = opt[0].value;
return d;
});
states.metricTypeList = states.metricTypeList.map((d: Option[]) => {
d = (MetricTypes as any)[metric.type];
return d;
});
}
dashboardStore.selectWidget({
...dashboardStore.selectedGrid,
...{ metricTypes: states.metricTypes },
});
if (states.isTable) {
return;
}
queryMetrics();
}
async function queryMetrics() {
const params = useQueryProcessor(states);
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, states);
emit("update", source);
}
function changeDashboard() {
const graph = {
...dashboardStore.selectedGrid.graph,
dashboardName: states.dashboardName,
};
dashboardStore.selectWidget({
...dashboardStore.selectedGrid,
graph,
});
}
function addMetric() {
states.metrics.push("");
if (!states.isTable) {
states.metricTypes.push(states.metricTypes[0]);
states.metricTypeList.push(states.metricTypeList[0]);
return;
}
states.metricTypes.push("");
}
function deleteMetric(index: number) {
states.metrics.splice(index, 1);
states.metricTypes.splice(index, 1);
}
</script>
<style lang="scss" scoped>
.ds-name {
margin-bottom: 10px;
}
.selectors {
width: 500px;
margin-right: 10px;
}
.metric-item {
margin-bottom: 10px;
}
.chart-types {
span {
display: inline-block;
padding: 5px 10px;
border: 1px solid #ccc;
background-color: #fff;
border-right: 0;
cursor: pointer;
}
span:nth-last-child(1) {
border-right: 1px solid #ccc;
}
}
span.active {
background-color: #409eff;
color: #fff;
}
</style>

View File

@ -116,22 +116,16 @@ limitations under the License. -->
</div>
</template>
<script lang="ts" setup>
import { reactive, defineProps, defineEmits } from "vue";
import { reactive } from "vue";
import { useI18n } from "vue-i18n";
import { SortOrder } from "../data";
import { StandardConfig } from "@/types/dashboard";
import type { PropType } from "vue";
import { useDashboardStore } from "@/store/modules/dashboard";
const props = defineProps({
config: {
type: Object as PropType<StandardConfig>,
default: () => ({ unit: "", sortOrder: "DES" }),
},
});
const emits = defineEmits(["update"]);
const dashboardStore = useDashboardStore();
const { selectedGrid } = dashboardStore;
const { t } = useI18n();
const state = reactive({
unit: props.config.unit,
unit: selectedGrid.standard.unit,
max: "",
min: "",
plus: "",
@ -140,11 +134,15 @@ const state = reactive({
divide: "",
milliseconds: "",
seconds: "",
sortOrder: props.config.sortOrder,
sortOrder: selectedGrid.standard.sortOrder,
});
function changeStandardOpt(param: { [key: string]: unknown }) {
emits("update", param);
const standard = {
...selectedGrid.standard,
...param,
};
dashboardStore.selectWidget({ ...selectedGrid, standard });
}
</script>
<style lang="scss" scoped>

View File

@ -35,24 +35,22 @@ limitations under the License. -->
</div>
</template>
<script lang="ts" setup>
import { ref, defineEmits, defineProps } from "vue";
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { WidgetConfig } from "@/types/dashboard";
import type { PropType } from "vue";
import { useDashboardStore } from "@/store/modules/dashboard";
const props = defineProps({
config: {
type: Object as PropType<WidgetConfig>,
default: () => ({ title: "", tooltips: "" }),
},
});
const emits = defineEmits(["update"]);
const { t } = useI18n();
const title = ref<string>(props.config.title || "");
const tips = ref<string>(props.config.tips || "");
const dashboardStore = useDashboardStore();
const { selectedGrid } = dashboardStore;
const title = ref<string>(selectedGrid.widget.title || "");
const tips = ref<string>(selectedGrid.widget.tips || "");
function updateWidgetConfig(param: { [key: string]: unknown }) {
emits("update", param);
const widget = {
...selectedGrid.widget,
...param,
};
dashboardStore.selectWidget({ ...selectedGrid, widget });
}
</script>
<style lang="scss" scoped>

View File

@ -28,24 +28,22 @@ limitations under the License. -->
</div>
</template>
<script lang="ts" setup>
import { defineProps, ref, defineEmits } from "vue";
import type { PropType } from "vue";
import { AreaConfig } from "@/types/dashboard";
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
const { t } = useI18n();
const dashboardStore = useDashboardStore();
const { selectedGrid } = dashboardStore;
const props = defineProps({
config: {
type: Object as PropType<AreaConfig>,
default: () => ({ opacity: 0.4 }),
},
});
const emits = defineEmits(["update"]);
const opacity = ref(props.config.opacity);
const opacity = ref(selectedGrid.graph.opacity);
function updateConfig(param: { [key: string]: unknown }) {
emits("update", param);
const graph = {
...selectedGrid.graph,
...param,
};
dashboardStore.selectWidget({ ...selectedGrid, graph });
}
</script>
<style lang="scss" scoped>

View File

@ -24,24 +24,21 @@ limitations under the License. -->
</div>
</template>
<script lang="ts" setup>
import { defineProps, ref, defineEmits } from "vue";
import type { PropType } from "vue";
import { BarConfig } from "@/types/dashboard";
import { ref } from "vue";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const props = defineProps({
config: {
type: Object as PropType<BarConfig>,
default: () => ({ showBackground: true, barWidth: 30 }),
},
});
const emits = defineEmits(["update"]);
const showBackground = ref(props.config.showBackground || false);
const dashboardStore = useDashboardStore();
const { selectedGrid } = dashboardStore;
const showBackground = ref(selectedGrid.graph.showBackground || false);
function changeConfig(param: { [key: string]: unknown }) {
emits("update", param);
const graph = {
...selectedGrid.graph,
...param,
};
dashboardStore.selectWidget({ ...selectedGrid, graph });
}
</script>
<style lang="scss" scoped>

View File

@ -20,32 +20,29 @@ limitations under the License. -->
v-model="fontSize"
show-input
input-size="small"
:min="0.1"
:max="1"
:step="0.1"
:min="10"
:max="20"
:step="1"
@change="updateConfig({ fontSize })"
/>
</div>
</template>
<script lang="ts" setup>
import { defineProps, ref, defineEmits } from "vue";
import type { PropType } from "vue";
import { CardConfig } from "@/types/dashboard";
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
const { t } = useI18n();
const props = defineProps({
config: {
type: Object as PropType<CardConfig>,
default: () => ({ fontSize: 12 }),
},
});
const emits = defineEmits(["update"]);
const fontSize = ref(props.config.fontSize);
const dashboardStore = useDashboardStore();
const { selectedGrid } = dashboardStore;
const fontSize = ref(selectedGrid.graph.fontSize);
function updateConfig(param: { [key: string]: unknown }) {
emits("update", param);
const graph = {
...selectedGrid.graph,
...param,
};
dashboardStore.selectWidget({ ...selectedGrid, graph });
}
</script>
<style lang="scss" scoped>

View File

@ -0,0 +1,61 @@
<!-- 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>
<span class="label">{{ t("fontSize") }}</span>
<el-slider
class="slider"
v-model="fontSize"
show-input
input-size="small"
:min="10"
:max="20"
:step="1"
@change="updateConfig({ fontSize })"
/>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
const dashboardStore = useDashboardStore();
const { selectedGrid } = dashboardStore;
const { t } = useI18n();
const fontSize = ref(selectedGrid.graph.fontSize);
function updateConfig(param: { [key: string]: unknown }) {
const graph = {
...selectedGrid.graph,
...param,
};
dashboardStore.selectWidget({ ...selectedGrid, graph });
}
</script>
<style lang="scss" scoped>
.slider {
width: 500px;
margin-top: -13px;
}
.label {
font-size: 13px;
font-weight: 500;
display: block;
margin-bottom: 5px;
}
</style>

View File

@ -0,0 +1,60 @@
<!-- 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>
<span class="label">{{ t("fontSize") }}</span>
<el-slider
class="slider"
v-model="fontSize"
show-input
input-size="small"
:min="10"
:max="20"
:step="1"
@change="updateConfig({ fontSize })"
/>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
const { t } = useI18n();
const dashboardStore = useDashboardStore();
const { selectedGrid } = dashboardStore;
const fontSize = ref(selectedGrid.graph.fontSize);
function updateConfig(param: { [key: string]: unknown }) {
const graph = {
...selectedGrid.graph,
...param,
};
dashboardStore.selectWidget({ ...selectedGrid, graph });
}
</script>
<style lang="scss" scoped>
.slider {
width: 500px;
margin-top: -13px;
}
.label {
font-size: 13px;
font-weight: 500;
display: block;
margin-bottom: 5px;
}
</style>

View File

@ -42,25 +42,23 @@ limitations under the License. -->
</div>
</template>
<script lang="ts" setup>
import { defineProps, ref, defineEmits } from "vue";
import type { PropType } from "vue";
import { LineConfig } from "@/types/dashboard";
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
const { t } = useI18n();
const props = defineProps({
config: {
type: Object as PropType<LineConfig>,
default: () => ({}),
},
});
const emits = defineEmits(["update"]);
const smooth = ref(props.config.smooth);
const showSymbol = ref(props.config.showSymbol);
const step = ref(props.config.step);
const dashboardStore = useDashboardStore();
const { selectedGrid } = dashboardStore;
const smooth = ref(selectedGrid.graph.smooth);
const showSymbol = ref(selectedGrid.graph.showSymbol);
const step = ref(selectedGrid.graph.step);
function updateConfig(param: { [key: string]: unknown }) {
emits("update", param);
const graph = {
...selectedGrid.graph,
...param,
};
dashboardStore.selectWidget({ ...selectedGrid, graph });
}
</script>
<style lang="scss" scoped>

View File

@ -0,0 +1,60 @@
<!-- 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>
<span class="label">{{ t("fontSize") }}</span>
<el-slider
class="slider"
v-model="fontSize"
show-input
input-size="small"
:min="10"
:max="20"
:step="1"
@change="updateConfig({ fontSize })"
/>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
const { t } = useI18n();
const dashboardStore = useDashboardStore();
const { selectedGrid } = dashboardStore;
const fontSize = ref(selectedGrid.graph.fontSize);
function updateConfig(param: { [key: string]: unknown }) {
const graph = {
...selectedGrid.graph,
...param,
};
dashboardStore.selectWidget({ ...selectedGrid, graph });
}
</script>
<style lang="scss" scoped>
.slider {
width: 500px;
margin-top: -13px;
}
.label {
font-size: 13px;
font-weight: 500;
display: block;
margin-bottom: 5px;
}
</style>

View File

@ -44,29 +44,23 @@ limitations under the License. -->
</div>
</template>
<script lang="ts" setup>
import { defineProps, ref, defineEmits } from "vue";
import type { PropType } from "vue";
import { TableConfig } from "@/types/dashboard";
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
const { t } = useI18n();
const props = defineProps({
config: {
type: Object as PropType<TableConfig>,
default: () => ({
showTableValues: true,
tableHeaderCol1: "",
tableHeaderCol2: "",
}),
},
});
const emits = defineEmits(["update"]);
const showTableValues = ref(props.config.showTableValues);
const tableHeaderCol1 = ref(props.config.tableHeaderCol1);
const tableHeaderCol2 = ref(props.config.tableHeaderCol2);
const dashboardStore = useDashboardStore();
const { selectedGrid } = dashboardStore;
const showTableValues = ref(selectedGrid.graph.showTableValues);
const tableHeaderCol1 = ref(selectedGrid.graph.tableHeaderCol1);
const tableHeaderCol2 = ref(selectedGrid.graph.tableHeaderCol2);
function updateConfig(param: { [key: string]: unknown }) {
emits("update", param);
const graph = {
...selectedGrid.graph,
...param,
};
dashboardStore.selectWidget({ ...selectedGrid, graph });
}
</script>
<style lang="scss" scoped>

View File

@ -28,25 +28,21 @@ limitations under the License. -->
</div>
</template>
<script lang="ts" setup>
import { defineProps, ref, defineEmits } from "vue";
import type { PropType } from "vue";
import { TopListConfig } from "@/types/dashboard";
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
const { t } = useI18n();
const props = defineProps({
config: {
type: Object as PropType<TopListConfig>,
default: () => ({
topN: 10,
}),
},
});
const emits = defineEmits(["update"]);
const topN = ref(props.config.topN);
const dashboardStore = useDashboardStore();
const { selectedGrid } = dashboardStore;
const topN = ref(selectedGrid.graph.topN);
function updateConfig(param: { [key: string]: unknown }) {
emits("update", param);
const graph = {
...selectedGrid.graph,
...param,
};
dashboardStore.selectWidget({ ...selectedGrid, graph });
}
</script>
<style lang="scss" scoped>

View File

@ -18,9 +18,21 @@
import AreaConfig from "./Area.vue";
import LineConfig from "./Line.vue";
import BarConfig from "./Bar.vue";
import TableConfig from "./Table.vue";
import CardConfig from "./Card.vue";
import InstanceListConfig from "./InstanceList.vue";
import EndpointListConfig from "./EndpointList.vue";
import ServiceListConfig from "./ServiceList.vue";
import TopListConfig from "./TopList.vue";
export default {
AreaConfig,
LineConfig,
BarConfig,
InstanceListConfig,
EndpointListConfig,
ServiceListConfig,
TableConfig,
CardConfig,
TopListConfig,
};

View File

@ -84,12 +84,12 @@ limitations under the License. -->
</div>
</template>
<script lang="ts" setup>
import { defineProps, reactive, ref, watch } from "vue";
import { ref, watch, reactive } from "vue";
import type { PropType } from "vue";
import Widget from "./Widget.vue";
import { LayoutConfig } from "@/types/dashboard";
import { useDashboardStore } from "@/store/modules/dashboard";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<LayoutConfig>,
@ -150,9 +150,12 @@ function clickTabGrid(e: Event, item: LayoutConfig) {
);
}
document.body.addEventListener("click", handleClick, false);
const children = ref(
dashboardStore.layout[props.data.i].children[activeTabIndex.value].children
);
watch(
() =>
dashboardStore.layout[props.data.i].children[activeTabIndex.value].children,
() => children.value,
(data) => {
state.layout = data;
}

View File

@ -45,9 +45,14 @@ limitations under the License. -->
<div class="body" v-if="data.graph?.type" v-loading="loading">
<component
:is="data.graph.type"
:intervalTime="appStoreWithOut.intervalTime"
:intervalTime="appStore.intervalTime"
:data="state.source"
:config="data.graph"
:config="{
...data.graph,
metrics: data.metrics,
metricTypes: data.metricTypes,
i: data.i,
}"
:standard="data.standard"
/>
</div>
@ -60,9 +65,11 @@ import type { PropType } from "vue";
import { LayoutConfig } from "@/types/dashboard";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app";
import { useSelectorStore } from "@/store/modules/selectors";
import graphs from "../graphs";
import { ElMessage } from "element-plus";
import { useI18n } from "vue-i18n";
import { useQueryProcessor, useSourceProcessor } from "@/hooks/useProcessor";
import { EntityType, TableChartTypes } from "../data";
const props = {
data: {
@ -78,34 +85,27 @@ export default defineComponent({
setup(props) {
const { t } = useI18n();
const loading = ref<boolean>(false);
const state = reactive({
const state = reactive<{ source: { [key: string]: unknown } }>({
source: {},
});
const { data } = toRefs(props);
const appStoreWithOut = useAppStoreWithOut();
const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore();
queryMetrics();
const selectorStore = useSelectorStore();
async function queryMetrics() {
const params = await useQueryProcessor(props.data);
if (!params) {
state.source = {};
return;
}
loading.value = true;
const json = await dashboardStore.fetchMetricValue(props.data);
const json = await dashboardStore.fetchMetricValue(params);
loading.value = false;
if (!json) {
return;
}
if (json.error) {
ElMessage.error(json.error);
return;
}
const metricVal = json.data.readMetricsValues.values.values.map(
(d: any) => d.value
);
const m = props.data.metrics && props.data.metrics[0];
if (!m) {
return;
}
state.source = {
[m]: metricVal,
};
state.source = useSourceProcessor(json, props.data);
}
function removeWidget() {
@ -121,17 +121,41 @@ export default defineComponent({
}
}
watch(
() => [props.data.queryMetricType, props.data.metrics],
(data, old) => {
if (data[0] === old[0] && data[1] === old[1]) {
() => [props.data.metricTypes, props.data.metrics],
() => {
if (props.data.i !== dashboardStore.selectedGrid.i) {
return;
}
if (TableChartTypes.includes(dashboardStore.selectedGrid.graph.type)) {
return;
}
queryMetrics();
}
);
watch(
() => [selectorStore.currentService],
() => {
if (dashboardStore.entity === EntityType[0].value) {
queryMetrics();
}
}
);
watch(
() => [selectorStore.currentPod],
() => {
if (
dashboardStore.entity === EntityType[0].value ||
dashboardStore.entity === EntityType[1].value
) {
return;
}
queryMetrics();
}
);
return {
state,
appStoreWithOut,
appStore,
removeWidget,
editConfig,
data,

View File

@ -14,18 +14,36 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const PodsChartTypes = ["EndpointList", "InstanceList"];
export const TableChartTypes = ["EndpointList", "InstanceList", "ServiceList"];
export const ChartTypes = [
{ label: "Bar", value: "Bar" },
{ label: "Line", value: "Line" },
{ label: "Area", value: "Area" },
{ label: "Heatmap", value: "Heatmap" },
{ label: "Pie", value: "Pie" },
// { label: "Pie", value: "Pie" },
{ label: "Card", value: "Card" },
{ label: "Top List", value: "TopList" },
{ label: "Table", value: "Table" },
{ label: "Heatmap", value: "Heatmap" },
{ label: "Service List", value: "ServiceList" },
{ label: "Endpoint List", value: "EndpointList" },
{ label: "Instance List", value: "InstanceList" },
];
export const MetricChartType: any = {
readMetricsValue: [{ label: "Card", value: "Card" }],
readMetricsValues: [
{ label: "Bar", value: "Bar" },
{ label: "Line", value: "Line" },
{ label: "Area", value: "Area" },
],
sortMetrics: [{ label: "Top List", value: "TopList" }],
readLabeledMetricsValues: [{ label: "Line", value: "Line" }],
readHeatMap: [{ label: "Heatmap", value: "Heatmap" }],
readSampledRecords: [{ label: "Top List", value: "TopList" }],
};
export const DefaultGraphConfig: { [key: string]: any } = {
Bar: {
type: "Bar",
@ -36,36 +54,47 @@ export const DefaultGraphConfig: { [key: string]: any } = {
step: false,
smooth: false,
showSymbol: false,
showXAxis: true,
showYAxis: true,
},
Area: {
type: "Area",
opacity: 0.4,
showXAxis: true,
showYAxis: true,
},
Card: {
type: "Card",
fontSize: 14,
textAlign: "center",
showUint: true,
},
Table: {
type: "Card",
type: "Table",
showTableValues: true,
tableHeaderCol1: "",
tableHeaderCol2: "",
},
TopList: {
type: "TopList",
topN: 10,
},
InstanceList: {
type: "InstanceList",
dashboardName: "",
fontSize: 12,
},
EndpointList: {
type: "EndpointList",
dashboardName: "",
fontSize: 12,
},
ServiceList: {
type: "ServiceList",
dashboardName: "",
fontSize: 12,
},
};
export enum MetricQueryTypes {
ReadMetricsValue = "readMetricsValue",
ReadMetricsValues = "readMetricsValues",
SortMetrics = "sortMetrics",
ReadLabeledMetricsValues = "readLabeledMetricsValues",
READHEATMAP = "readHeatMap",
ReadSampledRecords = "readSampledRecords",
}
export enum MetricsType {
UNKNOWN = "UNKNOWN",
REGULAR_VALUE = "REGULAR_VALUE",
@ -73,7 +102,7 @@ export enum MetricsType {
HEATMAP = "HEATMAP",
SAMPLED_RECORD = "SAMPLED_RECORD",
}
export const ValuesTypes: {
export const MetricTypes: {
[key: string]: Array<{ label: string; value: string }>;
} = {
REGULAR_VALUE: [
@ -97,55 +126,40 @@ export const ValuesTypes: {
{ label: "get sorted topN values", value: "readSampledRecords" },
],
};
export const MetricChartType: { [key: string]: string } = {
readMetricsValue: "ChartNum",
readMetricsValues: "ChartLine",
sortMetrics: "ChartSlow",
readLabeledMetricsValues: "ChartLine",
readHeatMap: "ChartHeatmap",
readSampledRecords: "ChartSlow",
};
export const CalculationType = [
{ label: "Plus", value: "+" },
{ label: "Minus", value: "-" },
{ label: "Multiplication", value: "*" },
{ label: "Division", value: "/" },
{ label: "Convert Unix Timestamp(milliseconds)", value: "milliseconds" },
{ label: "Convert Unix Timestamp(seconds)", value: "seconds" },
];
export const ReadValueChartType = [
{ value: "ChartNum", label: "Digital Card" },
{ value: "ChartSlow", label: "Slow Chart" },
];
export const MaxItemNum = 10;
export enum MetricsName {
SERVICE_RESP_TIME = "service_resp_time",
SERVICE_SLA = "service_sla",
SERVICE_CPM = "service_cpm",
SERVICE_PERCENTILE = "service_percentile",
SERVICE_APDEX = "service_apdex",
export enum MetricCatalog {
SERVICE = "Service",
SERVICE_INSTANCE = "ServiceInstance",
ENDPOINT = "Endpoint",
ALL = "All",
SERVICE_RELATION = "ServiceRelation",
SERVICE_INSTANCE_RELATION = "ServiceInstanceRelation",
ENDPOINT_RELATION = "EndpointRelation",
}
export const EntityType = [
{ value: "service", label: "Service", key: 1 },
{ value: "all", label: "All", key: 10 },
{ value: "endpoint", label: "Service Endpoint", key: 3 },
{ value: "serviceInstance", label: "Service Instance", key: 3 },
{ value: "serviceRelationClient", label: "Service Relation(client)", key: 2 },
{ value: "serviceRelationServer", label: "Service Relation(server)", key: 2 },
{ value: "Service", label: "Service", key: 1 },
{ value: "All", label: "All", key: 10 },
{ value: "Endpoint", label: "Service Endpoint", key: 3 },
{ value: "ServiceInstance", label: "Service Instance", key: 3 },
{ value: "ServiceRelationClient", label: "Service Relation(client)", key: 2 },
{ value: "ServiceRelationServer", label: "Service Relation(server)", key: 2 },
{
value: "serviceInstanceRelationClient",
value: "ServiceInstanceRelationClient",
label: "Service Instance Relation(client)",
key: 4,
},
{
value: "serviceInstanceRelationServer",
value: "ServiceInstanceRelationServer",
label: "Service Instance Relation(server)",
key: 4,
},
{ value: "endpointRelation", label: "Endpoint Relation", key: 4 },
{ value: "EndpointRelation", label: "Endpoint Relation", key: 4 },
];
export const TableEntity: any = {
InstanceList: EntityType[3].value,
EndpointList: EntityType[2].value,
ServiceList: EntityType[0].value,
};
export const SortOrder = [
{ label: "DES", value: "DES" },
{ label: "ASC", value: "ASC" },
@ -159,69 +173,3 @@ export const ToolIcons = [
{ name: "settings", content: "Settings", id: "settings" },
{ name: "save", content: "Apply", id: "applay" },
];
export const Options = [
{
value: "Option1",
label: "Option1",
},
{
value: "Option2",
label: "Option2",
},
{
value: "Option3",
label: "Option3",
},
{
value: "Option4",
label: "Option4",
},
{
value: "Option5",
label: "Option5",
},
];
export const SelectOpts = [
{
value: "guide",
label: "Guide",
children: [
{
value: "disciplines",
label: "Disciplines",
children: [
{
value: "consistency",
label: "Consistency",
},
{
value: "feedback",
label: "Feedback",
},
{
value: "efficiency",
label: "Efficiency",
},
{
value: "controllability",
label: "Controllability",
},
],
},
{
value: "navigation",
label: "Navigation",
children: [
{
value: "side nav",
label: "Side Navigation",
},
{
value: "top nav",
label: "Top Navigation",
},
],
},
],
},
];

View File

@ -17,11 +17,11 @@ limitations under the License. -->
<Line :data="data" :intervalTime="intervalTime" :config="config" />
</template>
<script lang="ts" setup>
import { defineProps } from "vue";
import type { PropType } from "vue";
import Line from "./Line.vue";
import { AreaConfig } from "@/types/dashboard";
/*global defineProps */
defineProps({
data: {
type: Object as PropType<{ [key: string]: number[] }>,

View File

@ -16,11 +16,12 @@ limitations under the License. -->
<Graph :option="option" />
</template>
<script lang="ts" setup>
import { defineProps, computed } from "vue";
import { computed } from "vue";
import type { PropType } from "vue";
import { Event } from "@/types/events";
import { BarConfig } from "@/types/dashboard";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<{ [key: string]: number[] }>,

View File

@ -14,22 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="chart-card" :style="{ fontSize: `${config.fontSize}px` }">
<div
class="chart-card"
:style="{ fontSize: `${config.fontSize}px`, textAlign: config.textAlign }"
>
{{
typeof data[key] === "string"
? data[key]
: isNaN(data[key])
typeof singleVal === "string"
? singleVal
: isNaN(singleVal)
? null
: data[key].toFixed(2)
: singleVal.toFixed(2)
}}
<span v-show="config.showUint">{{ standard.unit }}</span>
</div>
</template>
<script lang="ts" setup>
import { computed, PropType } from "vue";
import { defineProps } from "vue";
import { CardConfig, StandardConfig } from "@/types/dashboard";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<{ [key: string]: number }>,
@ -37,7 +40,7 @@ const props = defineProps({
},
config: {
type: Object as PropType<CardConfig>,
default: () => ({ fontSize: 12, showUint: true }),
default: () => ({ fontSize: 12, showUint: true, textAlign: "center" }),
},
standard: {
type: Object as PropType<StandardConfig>,
@ -45,10 +48,16 @@ const props = defineProps({
},
});
const key = computed(() => Object.keys(props.data)[0]);
const singleVal = computed(() => props.data[key.value]);
</script>
<style lang="scss" scoped>
.chart-card {
box-sizing: border-box;
color: #333;
display: -webkit-box;
-webkit-box-orient: horizontal;
-webkit-box-pack: center;
-webkit-box-align: center;
height: 100%;
}
</style>

View File

@ -13,22 +13,159 @@ 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>
<el-table :data="data" style="width: 100%">
<el-table-column prop="label" label="Endpoints" />
</el-table>
<div class="table">
<div class="search">
<el-input
v-model="searchText"
placeholder="Please input endpoint name"
class="input-with-search"
size="small"
@change="searchList"
>
<template #append>
<el-button size="small" @click="searchList">
<Icon size="lg" iconName="search" />
</el-button>
</template>
</el-input>
</div>
<el-table v-loading="chartLoading" :data="endpoints" style="width: 100%">
<el-table-column label="Endpoints">
<template #default="scope">
<router-link
class="link"
:to="`/dashboard/${dashboardStore.layerId}/${EntityType[2].value}/${selectorStore.currentService.id}/${scope.row.id}/${config.dashboardName}`"
:style="{ fontSize: `${config.fontSize}px` }"
>
{{ scope.row.label }}
</router-link>
</template>
</el-table-column>
<el-table-column
v-for="(metric, index) in config.metrics"
:label="metric"
:key="metric + index"
>
<template #default="scope">
<div class="chart">
<Line
:data="{ metric: scope.row[metric] }"
:intervalTime="intervalTime"
:config="{ showXAxis: false, showYAxis: false }"
/>
</div>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pagination"
background
layout="prev, pager, next"
:page-size="pageSize"
:total="selectorStore.pods.length"
@current-change="changePage"
@prev-click="changePage"
@next-click="changePage"
/>
</div>
</template>
<script setup lang="ts">
import { defineProps } from "vue";
import { ref, watch } from "vue";
import { useSelectorStore } from "@/store/modules/selectors";
import { ElMessage } from "element-plus";
import type { PropType } from "vue";
import { EndpointListConfig } from "@/types/dashboard";
import { Endpoint } from "@/types/selector";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useQueryPodsMetrics, usePodsSource } from "@/hooks/useProcessor";
import Line from "./Line.vue";
import { EntityType } from "../data";
defineProps({
/*global defineProps */
const props = defineProps({
data: {
type: Array as PropType<{ label: string; value: string }[]>,
default: () => [],
type: Object,
},
config: {
type: Object,
default: () => ({}),
type: Object as PropType<
EndpointListConfig & {
i: string;
metrics: string[];
metricTypes: string[];
}
>,
default: () => ({ dashboardName: "", fontSize: 12, i: "" }),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
});
const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore();
const chartLoading = ref<boolean>(false);
const endpoints = ref<Endpoint[]>([]);
const searchEndpoints = ref<Endpoint[]>([]);
const pageSize = 5;
const searchText = ref<string>("");
queryEndpoints();
async function queryEndpoints() {
chartLoading.value = true;
const resp = await selectorStore.getEndpoints();
chartLoading.value = false;
if (resp.errors) {
ElMessage.error(resp.errors);
return;
}
searchEndpoints.value = selectorStore.pods;
endpoints.value = selectorStore.pods.splice(0, pageSize);
queryEndpointMetrics(endpoints.value);
}
async function queryEndpointMetrics(currentPods: Endpoint[]) {
const { metrics } = props.config;
if (metrics.length && metrics[0]) {
const params = await useQueryPodsMetrics(
currentPods,
dashboardStore.selectedGrid,
EntityType[2].value
);
const json = await dashboardStore.fetchMetricValue(params);
if (json.errors) {
ElMessage.error(json.errors);
return;
}
endpoints.value = usePodsSource(
currentPods,
json,
dashboardStore.selectedGrid
);
return;
}
endpoints.value = currentPods;
}
function changePage(pageIndex: number) {
endpoints.value = searchEndpoints.value.splice(pageIndex - 1, pageSize);
}
function searchList() {
const currentEndpoints = selectorStore.pods.filter((d: { label: string }) =>
d.label.includes(searchText.value)
);
searchEndpoints.value = currentEndpoints;
endpoints.value = currentEndpoints.splice(0, pageSize);
}
watch(
() => [props.config.metricTypes, props.config.metrics],
() => {
queryEndpointMetrics(endpoints.value);
}
);
</script>
<style lang="scss" scoped>
@import "./style.scss";
.chart {
height: 39px;
}
</style>

View File

@ -16,10 +16,11 @@ limitations under the License. -->
<Graph :option="option" />
</template>
<script lang="ts" setup>
import { defineProps, computed } from "vue";
import { computed } from "vue";
import type { PropType } from "vue";
import { StandardConfig } from "@/types/dashboard";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<{ nodes: number[][]; buckets: number[][] }>,

View File

@ -13,22 +13,171 @@ 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>
<el-table :data="data" style="width: 100%">
<el-table-column prop="label" label="Service Instances" />
</el-table>
<div class="table">
<div class="search">
<el-input
v-model="searchText"
placeholder="Please input instance name"
class="input-with-search"
size="small"
@change="searchList"
>
<template #append>
<el-button size="small" @click="searchList">
<Icon size="lg" iconName="search" />
</el-button>
</template>
</el-input>
</div>
<el-table v-loading="chartLoading" :data="instances" style="width: 100%">
<el-table-column label="Service Instances">
<template #default="scope">
<router-link
class="link"
:to="`/dashboard/${dashboardStore.layerId}/${EntityType[3].value}/${selectorStore.currentService.id}/${scope.row.id}/${config.dashboardName}`"
:style="{ fontSize: `${config.fontSize}px` }"
>
{{ scope.row.label }}
</router-link>
</template>
</el-table-column>
<el-table-column
v-for="(metric, index) in config.metrics"
:label="metric"
:key="metric + index"
>
<template #default="scope">
<div class="chart">
<Line
v-if="config.metricTypes[index] === 'readMetricsValues'"
:data="metric ? { [metric]: scope.row[metric] } : {}"
:intervalTime="intervalTime"
:config="{ showXAxis: false, showYAxis: false }"
/>
<Card
v-else
:data="{ [metric]: scope.row[metric] }"
:config="{ textAlign: 'left' }"
/>
</div>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pagination"
background
layout="prev, pager, next"
:page-size="pageSize"
:total="searchInstances.length"
@current-change="changePage"
@prev-click="changePage"
@next-click="changePage"
/>
</div>
</template>
<script setup lang="ts">
import { defineProps } from "vue";
import { ref, watch } from "vue";
import { ElMessage } from "element-plus";
import type { PropType } from "vue";
import Line from "./Line.vue";
import Card from "./Card.vue";
import { useSelectorStore } from "@/store/modules/selectors";
import { useDashboardStore } from "@/store/modules/dashboard";
import { InstanceListConfig } from "@/types/dashboard";
import { Instance } from "@/types/selector";
import { useQueryPodsMetrics, usePodsSource } from "@/hooks/useProcessor";
import { EntityType } from "../data";
defineProps({
data: {
type: Array as PropType<{ label: string; value: string }[]>,
default: () => [],
},
/*global defineProps */
const props = defineProps({
config: {
type: Object,
default: () => ({}),
type: Object as PropType<
InstanceListConfig & {
i: string;
metrics: string[];
metricTypes: string[];
}
>,
default: () => ({
dashboardName: "",
fontSize: 12,
i: "",
metrics: [],
metricTypes: [],
}),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
});
const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore();
const chartLoading = ref<boolean>(false);
const instances = ref<Instance[]>([]); // current instances
const searchInstances = ref<Instance[]>([]); // all instances
const pageSize = 5;
const searchText = ref<string>("");
queryInstance();
async function queryInstance() {
chartLoading.value = true;
const resp = await selectorStore.getServiceInstances();
chartLoading.value = false;
if (resp.errors) {
ElMessage.error(resp.errors);
return;
}
searchInstances.value = selectorStore.pods;
instances.value = searchInstances.value.splice(0, pageSize);
queryInstanceMetrics(instances.value);
}
async function queryInstanceMetrics(currentInstances: Instance[]) {
const { metrics } = props.config;
if (metrics.length && metrics[0]) {
const params = await useQueryPodsMetrics(
currentInstances,
dashboardStore.selectedGrid,
EntityType[3].value
);
const json = await dashboardStore.fetchMetricValue(params);
if (json.errors) {
ElMessage.error(json.errors);
return;
}
instances.value = usePodsSource(
currentInstances,
json,
dashboardStore.selectedGrid
);
return;
}
instances.value = currentInstances;
}
function changePage(pageIndex: number) {
instances.value = searchInstances.value.splice(pageIndex - 1, pageSize);
}
function searchList() {
searchInstances.value = selectorStore.pods.filter((d: { label: string }) =>
d.label.includes(searchText.value)
);
instances.value = searchInstances.value.splice(0, pageSize);
}
watch(
() => [props.config.metricTypes, props.config.metrics],
() => {
queryInstanceMetrics(instances.value);
}
);
</script>
<style lang="scss" scoped>
@import "./style.scss";
.chart {
height: 40px;
}
</style>

View File

@ -16,11 +16,12 @@ limitations under the License. -->
<Graph :option="option" />
</template>
<script lang="ts" setup>
import { defineProps, computed } from "vue";
import { computed } from "vue";
import type { PropType } from "vue";
import { Event } from "@/types/events";
import { LineConfig } from "@/types/dashboard";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<{ [key: string]: number[] }>,
@ -36,6 +37,8 @@ const props = defineProps({
smooth: false,
showSymbol: false,
opacity: 0.4,
showXAxis: true,
showYAxis: true,
}),
},
});
@ -142,6 +145,8 @@ function getOption() {
color,
tooltip: {
trigger: "axis",
zlevel: 1000,
z: 60,
backgroundColor: "rgb(50,50,50)",
textStyle: {
fontSize: 13,
@ -170,6 +175,7 @@ function getOption() {
},
xAxis: {
type: "category",
show: props.config.showXAxis,
axisTick: {
lineStyle: { color: "#c1c5ca41" },
alignWithLabel: true,
@ -183,7 +189,11 @@ function getOption() {
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: "#c1c5ca41", type: "dashed" } },
axisLabel: { color: "#9da5b2", fontSize: "11" },
axisLabel: {
color: "#9da5b2",
fontSize: "11",
show: props.config.showYAxis,
},
},
series: temp,
};

View File

@ -16,9 +16,10 @@ limitations under the License. -->
<Graph :option="option" />
</template>
<script lang="ts" setup>
import { defineProps, computed } from "vue";
import { computed } from "vue";
import type { PropType } from "vue";
/*global defineProps */
const props = defineProps({
data: {
type: Array as PropType<{ name: string; value: number }[]>,

View File

@ -0,0 +1,168 @@
<!-- 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="table">
<div class="search">
<el-input
v-model="searchText"
placeholder="Please input service name"
class="input-with-search"
size="small"
@change="searchList"
>
<template #append>
<el-button size="small" @click="searchList">
<Icon size="lg" iconName="search" />
</el-button>
</template>
</el-input>
</div>
<el-table v-loading="chartLoading" :data="services" style="width: 100%">
<el-table-column label="Services">
<template #default="scope">
<router-link
class="link"
:to="`/dashboard/${dashboardStore.layerId}/${EntityType[0].value}/${selectorStore.currentService.id}/${config.dashboardName}`"
:key="1"
:style="{ fontSize: `${config.fontSize}px` }"
>
{{ scope.row.label }}
</router-link>
</template>
</el-table-column>
<el-table-column
v-for="(metric, index) in config.metrics"
:label="metric"
:key="metric + index"
>
<template #default="scope">
<div class="chart">
<Line
:data="{ metric: scope.row[metric] }"
:intervalTime="intervalTime"
:config="{ showXAxis: false, showYAxis: false }"
/>
</div>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pagination"
background
layout="prev, pager, next"
:page-size="pageSize"
:total="selectorStore.services.length"
@current-change="changePage"
@prev-click="changePage"
@next-click="changePage"
/>
</div>
</template>
<script setup lang="ts">
import { watch, ref } from "vue";
import { ElMessage } from "element-plus";
import type { PropType } from "vue";
import { ServiceListConfig } from "@/types/dashboard";
import Line from "./Line.vue";
import { useSelectorStore } from "@/store/modules/selectors";
import { useDashboardStore } from "@/store/modules/dashboard";
import { Service } from "@/types/selector";
import { useQueryPodsMetrics, usePodsSource } from "@/hooks/useProcessor";
import { EntityType } from "../data";
/*global defineProps */
const props = defineProps({
data: {
type: Object,
},
config: {
type: Object as PropType<
ServiceListConfig & {
i: string;
metrics: string[];
metricTypes: string[];
}
>,
default: () => ({ dashboardName: "", fontSize: 12 }),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
});
const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore();
const chartLoading = ref<boolean>(false);
const pageSize = 5;
const services = ref<Service[]>([]);
const searchServices = ref<Service[]>([]);
const searchText = ref<string>("");
queryServices();
async function queryServices() {
chartLoading.value = true;
const resp = await selectorStore.fetchServices();
chartLoading.value = false;
if (resp.errors) {
ElMessage.error(resp.errors);
}
services.value = selectorStore.services.splice(0, pageSize);
}
async function queryServiceMetrics(currentServices: Service[]) {
const { metrics } = props.config;
if (metrics.length && metrics[0]) {
const params = await useQueryPodsMetrics(
currentServices,
dashboardStore.selectedGrid,
EntityType[0].value
);
const json = await dashboardStore.fetchMetricValue(params);
if (json.errors) {
ElMessage.error(json.errors);
return;
}
services.value = usePodsSource(
currentServices,
json,
dashboardStore.selectedGrid
);
return;
}
services.value = currentServices;
}
function changePage(pageIndex: number) {
services.value = selectorStore.services.splice(pageIndex - 1, pageSize);
}
function searchList() {
searchServices.value = selectorStore.services.filter((d: { label: string }) =>
d.label.includes(searchText.value)
);
services.value = searchServices.value.splice(0, pageSize);
}
watch(
() => [props.config.metricTypes, props.config.metrics],
() => {
queryServiceMetrics(services.value);
}
);
</script>
<style lang="scss" scoped>
@import "./style.scss";
.chart {
height: 39px;
}
</style>

View File

@ -22,9 +22,9 @@ limitations under the License. -->
>
<div class="name" :style="`width: ${nameWidth}px`">
{{ config.tableHeaderCol1 || $t("name") }}
<i class="r cp" ref="draggerName"
><rk-icon icon="settings_ethernet"
/></i>
<i class="r cp" ref="draggerName">
<Icon iconName="settings_ethernet" size="middle" />
</i>
</div>
<div class="value-col" v-if="config.showTableValues">
{{ config.tableHeaderCol2 || $t("value") }}
@ -45,11 +45,12 @@ limitations under the License. -->
</div>
</template>
<script lang="ts" setup>
import { defineProps, computed, ref, onMounted } from "vue";
import { computed, ref, onMounted } from "vue";
import type { PropType } from "vue";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<{ [key: string]: number[][] }>,
type: Object as PropType<{ [key: string]: number[] }>,
default: () => ({}),
},
config: {

View File

@ -14,57 +14,68 @@ See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="chart-slow-i" v-for="(i, index) in datas" :key="index">
<Icon
iconName="review-list"
size="sm"
@click="handleClick((i.traceIds && i.traceIds[0]) || i.name)"
/>
<div class="mb-5 ell">
<span class="calls sm mr-10">{{ i.value }}</span>
<span class="cp link-hover">
{{ i.name + getTraceId(i) }}
</span>
<div class="top-list">
<div class="chart-slow-i" v-for="(i, index) in datas" :key="index">
<div class="ell tools flex-h">
<div>
<span class="calls mr-10">{{ i.value }}</span>
<span class="cp mr-20">
{{ i.name + getTraceId(i) }}
</span>
</div>
<div>
<Icon
iconName="review-list"
size="middle"
class="cp"
@click="handleClick((i.traceIds && i.traceIds[0]) || i.name)"
/>
</div>
</div>
<el-progress
:stroke-width="6"
:percentage="(i.value / maxValue) * 100"
color="#bf99f8"
:show-text="false"
/>
</div>
<el-progress
:stroke-width="10"
:percentage="(i.value / maxValue) * 100"
color="#bf99f8"
/>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import { defineProps, computed } from "vue";
import { computed } from "vue";
import copy from "@/utils/copy";
/*global defineProps */
const props = defineProps({
data: {
type: Array as PropType<
{ name: string; value: number; traceIds: string[] }[]
>,
default: () => [],
type: Object as PropType<{
[key: string]: { name: string; value: number; traceIds: string[] }[];
}>,
default: () => ({}),
},
config: {
type: Object as PropType<{ sortOrder: string }>,
default: () => ({}),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
});
const key = computed(() => Object.keys(props.data)[0]);
const maxValue = computed(() => {
if (!props.data.length) {
if (!(props.data[key.value] && props.data[key.value].length)) {
return 0;
}
const temp: number[] = props.data.map((i: any) => i.value);
const temp: number[] = props.data[key.value].map((i: any) => i.value);
return Math.max.apply(null, temp);
});
const getTraceId = (i: { [key: string]: (number | string)[] }): string => {
return i.traceIds && i.traceIds[0] ? ` - ${i.traceIds[0]}` : "";
};
const datas: any = () => {
if (!props.data.length) {
const datas = computed(() => {
if (!(props.data[key.value] && props.data[key.value].length)) {
return [];
}
const { sortOrder } = props.config;
const val: any = props.data;
const val: any = props.data[key.value];
switch (sortOrder) {
case "DES":
@ -74,17 +85,29 @@ const datas: any = () => {
val.sort((a: any, b: any) => a.value - b.value);
break;
default:
val.sort((a: any, b: any) => b.value - a.value);
break;
}
return val;
};
});
function handleClick(i: string) {
copy(i);
}
</script>
<style lang="scss" scoped>
.top-list {
height: 100%;
overflow: auto;
padding: 10px;
}
.tools {
justify-content: space-between;
}
.progress-bar {
font-size: 14px;
font-size: 12px;
color: #333;
}
@ -94,14 +117,15 @@ function handleClick(i: string) {
.chart-slow {
height: 100%;
}
.calls {
padding: 0 5px;
display: inline-block;
background-color: #40454e;
color: #eee;
border-radius: 4px;
}
.calls {
font-size: 12px;
padding: 0 5px;
display: inline-block;
background-color: #40454e;
color: #eee;
border-radius: 4px;
}
.chart-slow-link {

View File

@ -23,6 +23,9 @@ import TopList from "./TopList.vue";
import Table from "./Table.vue";
import Pie from "./Pie.vue";
import Card from "./Card.vue";
import InstanceList from "./InstanceList.vue";
import EndpointList from "./EndpointList.vue";
import ServiceList from "./ServiceList.vue";
export default {
Line,
@ -33,4 +36,7 @@ export default {
Table,
Pie,
Card,
EndpointList,
InstanceList,
ServiceList,
};

View File

@ -0,0 +1,43 @@
/**
* 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.
*/
.table {
height: 100%;
overflow: auto;
padding: 0 10px 5px 0;
}
.pagination {
width: 100%;
text-align: center;
height: 30px;
padding: 8px 0;
}
.link {
cursor: pointer;
color: #409eff;
display: inline-block;
width: 100%;
}
.search {
text-align: right;
}
.input-with-search {
width: 400px;
}

View File

@ -15,48 +15,55 @@ limitations under the License. -->
<template>
<div class="dashboard-tool flex-h">
<div class="flex-h">
<div class="selectors-item" v-if="states.key < 3">
<div class="selectors-item" v-if="states.key !== 10">
<span class="label">$Service</span>
<Selector
:value="states.service"
:options="Options"
v-model="states.currentService"
:options="selectorStore.services"
size="mini"
placeholder="Select a service"
@change="changeService"
class="selectors"
:borderRadius="4"
/>
</div>
<div class="selectors-item" v-if="states.key === 3 || states.key === 4">
<span class="label">$ServiceInstance</span>
<el-cascader
placeholder="Select a instance"
:options="SelectOpts"
<span class="label">
{{
dashboardStore.entity === "Endpoint"
? "$Endpoint"
: "$ServiceInstance"
}}
</span>
<Selector
v-model="states.currentPod"
:options="selectorStore.pods"
size="mini"
filterable
:style="{ minWidth: '300px' }"
placeholder="Select a data"
@change="changePods"
class="selectors"
/>
</div>
<div class="selectors-item" v-if="states.key === 2">
<span class="label">$DestinationService</span>
<Selector
:value="states.service"
:options="Options"
v-model="selectorStore.currentDestService"
:options="selectorStore.services"
size="mini"
placeholder="Select a service"
:borderRadius="0"
@change="changeService"
class="selectors"
/>
</div>
<div class="selectors-item" v-if="states.key === 4">
<span class="label">$DestinationServiceInstance</span>
<el-cascader
placeholder="Select a instance"
:options="SelectOpts"
<Selector
v-model="states.currentPod"
:options="selectorStore.pods"
size="mini"
filterable
:style="{ minWidth: '300px' }"
placeholder="Select a data"
@change="changePods"
class="selectors"
:borderRadius="4"
/>
</div>
</div>
@ -80,33 +87,96 @@ limitations under the License. -->
import { reactive } from "vue";
import { useRoute } from "vue-router";
import { useDashboardStore } from "@/store/modules/dashboard";
import { Options, SelectOpts, EntityType, ToolIcons } from "../data";
import { EntityType, ToolIcons } from "../data";
import { useSelectorStore } from "@/store/modules/selectors";
import { ElMessage } from "element-plus";
import { Option } from "@/types/app";
import { Service } from "@/types/selector";
const dashboardStore = useDashboardStore();
const selectorStore = useSelectorStore();
const params = useRoute().params;
const type = EntityType.filter((d: Option) => d.value === params.entity)[0];
const states = reactive<{
entity: string | string[];
layerId: string | string[];
service: string;
pod: string;
destService: string;
destPod: string;
key: number;
currentService: string;
currentPod: string;
}>({
service: Options[0].value,
pod: Options[0].value, // instances and endpoints
destService: "",
destPod: "",
key: EntityType.filter((d: any) => d.value === params.entity)[0].key || 0,
entity: params.entity,
layerId: params.layerId,
key: (type && type.key) || 0,
currentService: "",
currentPod: "",
});
dashboardStore.setLayer(states.layerId);
dashboardStore.setEntity(states.entity);
dashboardStore.setLayer(String(params.layerId));
dashboardStore.setEntity(String(params.entity));
function changeService(val: { value: string; label: string }) {
states.service = val.value;
if (params.serviceId) {
setSelector();
} else {
getServices();
}
async function setSelector() {
if (params.podId) {
await selectorStore.getService(String(params.serviceId));
states.currentService = selectorStore.currentService.value;
await fetchPods(String(params.entity), false);
const currentPod = selectorStore.pods.filter(
(d: { id: string }) => d.id === String(params.podId)
)[0];
selectorStore.setCurrentPod(currentPod);
states.currentPod = currentPod.label;
return;
}
// entity=Service with serviceId
const json = await selectorStore.fetchServices(dashboardStore.layerId);
if (json.errors) {
ElMessage.error(json.errors);
return;
}
const currentService = selectorStore.services.filter(
(d: { id: string }) => d.id === String(params.serviceId)
)[0];
selectorStore.setCurrentService(currentService);
states.currentService = selectorStore.currentService.value;
}
async function getServices() {
if (!dashboardStore.layerId) {
return;
}
const json = await selectorStore.fetchServices(dashboardStore.layerId);
if (json.errors) {
ElMessage.error(json.errors);
return;
}
selectorStore.setCurrentService(
selectorStore.services.length ? selectorStore.services[0] : null
);
states.currentService = selectorStore.currentService.value;
fetchPods(dashboardStore.entity, true);
}
async function changeService(service: Service[]) {
if (service[0]) {
states.currentService = service[0].value;
selectorStore.setCurrentService(service[0]);
fetchPods(dashboardStore.entity, true);
} else {
selectorStore.setCurrentService("");
}
}
function changePods(pod: Option[]) {
if (pod[0]) {
selectorStore.setCurrentPod(pod[0].value);
} else {
selectorStore.setCurrentPod("");
}
}
function clickIcons(t: { id: string; content: string; name: string }) {
@ -127,6 +197,36 @@ function clickIcons(t: { id: string; content: string; name: string }) {
dashboardStore.addControl("Widget");
}
}
async function fetchPods(type: string, setPod: boolean) {
let resp;
switch (type) {
case "Endpoint":
resp = await selectorStore.getEndpoints();
if (setPod) {
selectorStore.setCurrentPod(
selectorStore.pods.length ? selectorStore.pods[0] : null
);
states.currentPod = selectorStore.currentPod.label;
}
break;
case "ServiceInstance":
resp = await selectorStore.getServiceInstances();
if (setPod) {
selectorStore.setCurrentPod(
selectorStore.pods.length ? selectorStore.pods[0] : null
);
states.currentPod = selectorStore.currentPod.label;
}
break;
default:
resp = {};
}
if (resp.errors) {
ElMessage.error(resp.errors);
return;
}
}
</script>
<style lang="scss" scoped>
.dashboard-tool {

View File

@ -17,7 +17,7 @@ limitations under the License. -->
</template>
<script lang="ts" setup>
import { defineProps } from "vue";
/*global defineProps */
defineProps({
msg: { type: String },
});

View File

@ -17,7 +17,7 @@ limitations under the License. -->
</template>
<script lang="ts" setup>
import { defineProps } from "vue";
/*global defineProps */
defineProps({
msg: { type: String },
});

View File

@ -17,7 +17,7 @@ limitations under the License. -->
</template>
<script lang="ts" setup>
import { defineProps } from "vue";
/*global defineProps */
defineProps({
msg: { type: String },
});

View File

@ -40,9 +40,19 @@ module.exports = {
chunks: "all",
cacheGroups: {
echarts: {
name: "echarts",
test: /[\\/]node_modules[\\/]echarts[\\/]/,
priority: 1,
},
elementPlus: {
name: "element-plus",
test: /[\\/]node_modules[\\/]element-plus[\\/]/,
priority: 2,
},
three: {
name: "three",
test: /[\\/]node_modules[\\/]three[\\/]/,
priority: 2,
priority: 3,
},
},
},