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 See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<router-view /> <router-view :key="$route.fullPath" />
</template> </template>
<style> <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 not use this file except in compliance with
the License. You may obtain a copy of the License at 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 Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<input v-show="!imgUrl" type="file" @change="fileChange" accept="image/*" /> <title>add_circle_outlinecontrol_point</title>
<img v-if="imgUrl" :src="imgUrl" alt="" /> <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>
</template> </svg>
<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>

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,47 +18,16 @@ export const TypeOfMetrics = {
variable: "$name: String!", variable: "$name: String!",
query: `typeOfMetrics(name: $name)`, query: `typeOfMetrics(name: $name)`,
}; };
export const queryMetricsValues = { export const listMetrics = {
variable: ["$condition: MetricsCondition!, $duration: Duration!"], variable: "$regex: String",
query: ` query: `
readMetricsValues: readMetricsValues(condition: $condition, duration: $duration) { metrics: listMetrics(regex: $regex) {
label value: name
values { label: name
values {value} type
} catalog
}`, }
}; `,
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}
}
}`,
}; };
export const queryHeatMap = { 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. * limitations under the License.
*/ */
export const Services = { export const Services = {
variable: ["$layer: String!"], variable: "$layer: String!",
query: ` query: `
services: listServices(layer: $layer) { services: listServices(layer: $layer) {
value: id id
value: name
label: name label: name
group group
layer layers
} normal
}
`, `,
}; };
export const Layers = { export const Layers = {
@ -33,10 +35,13 @@ export const Layers = {
export const Instances = { export const Instances = {
variable: "$serviceId: ID!, $duration: Duration!", variable: "$serviceId: ID!, $duration: Duration!",
query: ` query: `
getServiceInstances(duration: $duration, serviceId: $serviceId) { pods: listInstances(duration: $duration, serviceId: $serviceId) {
key: id id
value: name
label: name label: name
language language
instanceUUID
layer
attributes { attributes {
name name
value value
@ -47,9 +52,55 @@ export const Instances = {
export const Endpoints = { export const Endpoints = {
variable: "$serviceId: ID!, $keyword: String!", variable: "$serviceId: ID!, $keyword: String!",
query: ` query: `
getEndpoints: searchEndpoint(serviceId: $serviceId, keyword: $keyword, limit: 100) { pods: findEndpoint(serviceId: $serviceId, keyword: $keyword, limit: 100) {
key: id id
label: name 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 { import {
TypeOfMetrics, TypeOfMetrics,
querySampledRecords,
queryHeatMap, queryHeatMap,
queryLabeledMetricsValues, listMetrics,
querySortMetrics,
queryMetricsValue,
queryMetricsValues,
} from "../fragments/dashboard"; } from "../fragments/dashboard";
export const queryTypeOfMetrics = `query typeOfMetrics(${TypeOfMetrics.variable}) {${TypeOfMetrics.query}}`; export const queryTypeOfMetrics = `query typeOfMetrics(${TypeOfMetrics.variable}) {${TypeOfMetrics.query}}`;
export const readHeatMap = `query queryData(${queryHeatMap.variable}) {${queryHeatMap.query}}`; export const readHeatMap = `query queryData(${queryHeatMap.variable}) {${queryHeatMap.query}}`;
export const readSampledRecords = `query queryData(${querySampledRecords.variable}) {${querySampledRecords.query}}`; export const queryMetrics = `query queryData(${listMetrics.variable}) {${listMetrics.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}}`;

View File

@ -14,10 +14,20 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * 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 queryServices = `query queryServices(${Services.variable}) {${Services.query}}`;
export const queryLayers = `query ${Layers.query}`;
export const queryEndpoints = `query queryEndpoints(${Endpoints.variable}) {${Endpoints.query}}`; export const queryEndpoints = `query queryEndpoints(${Endpoints.variable}) {${Endpoints.query}}`;
export const queryInstances = `query queryInstances(${Instances.variable}) {${Instances.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 * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
export enum MetricQueryTypes {
ReadMetricsValue = "readMetricsValue",
ReadMetricsValues = "readMetricsValues",
SortMetrics = "sortMetrics",
ReadLabeledMetricsValues = "readLabeledMetricsValues",
READHEATMAP = "readHeatMap",
ReadSampledRecords = "readSampledRecords",
}
export enum sizeEnum { export enum sizeEnum {
XS = "XS", XS = "XS",
SM = "SM", SM = "SM",
@ -32,7 +40,7 @@ export enum screenEnum {
XXL = 1600, XXL = 1600,
} }
const screenMap = new Map<sizeEnum, number>(); export const screenMap = new Map<sizeEnum, number>();
screenMap.set(sizeEnum.XS, screenEnum.XS); screenMap.set(sizeEnum.XS, screenEnum.XS);
screenMap.set(sizeEnum.SM, screenEnum.SM); 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.XL, screenEnum.XL);
screenMap.set(sizeEnum.XXL, screenEnum.XXL); 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. --> limitations under the License. -->
<template> <template>
<section class="app-main"> <section class="app-main">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }" :key="$route.fullPath">
<keep-alive> <keep-alive>
<component :is="Component" /> <component :is="Component" />
</keep-alive> </keep-alive>

View File

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

View File

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

View File

@ -20,10 +20,10 @@ import router from "./router";
import { store } from "./store"; import { store } from "./store";
import components from "@/components"; import components from "@/components";
import i18n from "./locales"; import i18n from "./locales";
import "element-plus/dist/index.css";
import "./styles/lib.scss"; import "./styles/lib.scss";
import "./styles/reset.scss"; import "./styles/reset.scss";
import ElementPlus from "element-plus"; import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
const app = createApp(App); 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"), 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: { meta: {
title: "dashboardEdit", title: "dashboardEdit",
exact: false, exact: false,

View File

@ -26,6 +26,8 @@ export const NewControl = {
}, },
graph: {}, graph: {},
standard: {}, standard: {},
metrics: [""],
metricTypes: [""],
}; };
export const ConfigData: any = { export const ConfigData: any = {
x: 0, x: 0,
@ -34,14 +36,64 @@ export const ConfigData: any = {
h: 12, h: 12,
i: "0", i: "0",
metrics: ["service_resp_time"], metrics: ["service_resp_time"],
queryMetricType: "readMetricsValues", metricTypes: ["readMetricsValues"],
type: "Widget", type: "Widget",
widget: { widget: {
title: "Title", title: "service_resp_time",
tips: "Tooltip", tips: "Tooltip",
}, },
graph: { graph: {
type: "Line", 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: { standard: {
sortOrder: "DEC", sortOrder: "DEC",

View File

@ -18,10 +18,13 @@ import { defineStore } from "pinia";
import { store } from "@/store"; import { store } from "@/store";
import { LayoutConfig } from "@/types/dashboard"; import { LayoutConfig } from "@/types/dashboard";
import graph from "@/graph"; import graph from "@/graph";
import { AxiosResponse } from "axios"; import { ConfigData, ConfigData1, ConfigData2 } from "../data";
import { ConfigData } from "../data";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import { useSelectorStore } from "@/store/modules/selectors";
import { NewControl } from "../data"; import { NewControl } from "../data";
import { Duration } from "@/types/app";
import axios, { AxiosResponse } from "axios";
import { cancelToken } from "@/utils/cancelToken";
interface DashboardState { interface DashboardState {
showConfig: boolean; showConfig: boolean;
layout: LayoutConfig[]; layout: LayoutConfig[];
@ -29,6 +32,8 @@ interface DashboardState {
entity: string; entity: string;
layerId: string; layerId: string;
activedGridItem: string; activedGridItem: string;
durationTime: Duration;
selectorStore: any;
} }
export const dashboardStore = defineStore({ export const dashboardStore = defineStore({
@ -40,6 +45,8 @@ export const dashboardStore = defineStore({
entity: "", entity: "",
layerId: "", layerId: "",
activedGridItem: "", activedGridItem: "",
durationTime: useAppStoreWithOut().durationTime,
selectorStore: useSelectorStore(),
}), }),
actions: { actions: {
setLayout(data: LayoutConfig[]) { setLayout(data: LayoutConfig[]) {
@ -50,6 +57,8 @@ export const dashboardStore = defineStore({
...NewControl, ...NewControl,
i: String(this.layout.length), i: String(this.layout.length),
type, type,
metricTypes: [""],
metrics: [""],
}; };
if (type === "Tab") { if (type === "Tab") {
newWidget.h = 24; newWidget.h = 24;
@ -129,14 +138,20 @@ export const dashboardStore = defineStore({
setConfigPanel(show: boolean) { setConfigPanel(show: boolean) {
this.showConfig = show; this.showConfig = show;
}, },
selectWidget(widget: Nullable<LayoutConfig>) { selectWidget(item: Nullable<LayoutConfig>) {
this.selectedGrid = widget; this.selectedGrid = item;
}, },
setLayer(id: string) { setLayer(id: string) {
this.layerId = id; this.layerId = id;
}, },
setEntity(type: string) { setEntity(type: string) {
this.entity = type; this.entity = type;
if (type === "ServiceInstance") {
this.layout = [ConfigData1];
}
if (type === "Endpoint") {
this.layout = [ConfigData2];
}
}, },
setConfigs(param: { [key: string]: unknown }) { setConfigs(param: { [key: string]: unknown }) {
const actived = this.activedGridItem.split("-"); const actived = this.activedGridItem.split("-");
@ -165,29 +180,24 @@ export const dashboardStore = defineStore({
return res.data; return res.data;
}, },
async fetchMetricValue(config: LayoutConfig) { async fetchMetricList(regex: string) {
// 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,
};
const res: AxiosResponse = await graph const res: AxiosResponse = await graph
.query(config.queryMetricType) .query("queryMetrics")
.params(variable); .params({ regex });
return res.data; 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. * limitations under the License.
*/ */
import { defineStore } from "pinia"; 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 { store } from "@/store";
import graph from "@/graph"; import graph from "@/graph";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { useAppStoreWithOut } from "@/store/modules/app";
interface SelectorState { 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({ export const selectorStore = defineStore({
id: "selector", id: "selector",
state: (): SelectorState => ({ state: (): SelectorState => ({
services: [], services: [],
pods: [],
currentService: null,
currentPod: null,
currentDestService: null,
currentDestPod: null,
durationTime: useAppStoreWithOut().durationTime,
}), }),
actions: { actions: {
setCurrentService(service: Service) {
this.currentService = service;
},
setCurrentPod(pod: Nullable<Instance | Endpoint>) {
this.currentPod = pod;
},
async fetchLayers(): Promise<AxiosResponse> { async fetchLayers(): Promise<AxiosResponse> {
const res: AxiosResponse = await graph.query("queryLayers").params({}); const res: AxiosResponse = await graph.query("queryLayers").params({});
return res; return res.data || {};
}, },
async fetchServices(layer: string): Promise<AxiosResponse> { async fetchServices(layer: string): Promise<AxiosResponse> {
const res: AxiosResponse = await graph const res: AxiosResponse = await graph
@ -41,30 +61,89 @@ export const selectorStore = defineStore({
.params({ layer }); .params({ layer });
if (!res.data.errors) { 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; serviceId: string;
duration: Duration; }): Promise<Nullable<AxiosResponse>> {
}): Promise<AxiosResponse> { const serviceId = param ? param.serviceId : this.currentService?.id;
const res: AxiosResponse = await graph if (!serviceId) {
.query("queryInstances") return null;
.params(params); }
return res; 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: { async getEndpoints(params?: {
keyword: string; keyword?: string;
serviceId: string; serviceId?: string;
}): Promise<AxiosResponse> { }): Promise<Nullable<AxiosResponse>> {
if (!params) {
params = {};
}
if (!params.keyword) { if (!params.keyword) {
params.keyword = ""; params.keyword = "";
} }
const res: AxiosResponse = await graph const serviceId = params.serviceId || this.currentService?.id;
.query("queryEndpoints") if (!serviceId) {
.params(params); return null;
return res; }
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. * limitations under the License.
*/ */
body { body {
margin: 0; margin: 0;
line-height: 1.5; line-height: 1.5;
font-size: 14px; font-size: 14px;
color: #3d444f; 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; text-rendering: optimizeLegibility;
-ms-text-size-adjust: 100%; text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
} }
html, html,
body { body {
height: 100%; height: 100%;
} }
div, div,
header, header,
footer, footer,
@ -53,6 +55,7 @@ a,
img { img {
box-sizing: border-box; box-sizing: border-box;
} }
input, input,
textarea, textarea,
select, select,
@ -60,41 +63,51 @@ button {
font-size: 100%; font-size: 100%;
font-family: inherit; font-family: inherit;
} }
h1 { h1 {
font-size: 26px; font-size: 26px;
} }
h2 { h2 {
font-size: 24px; font-size: 24px;
} }
h3 { h3 {
font-size: 21px; font-size: 21px;
} }
h4 { h4 {
font-size: 18px; font-size: 18px;
} }
h5 { h5 {
font-size: 14px; font-size: 14px;
} }
h6 { h6 {
font-size: 1em; font-size: 1em;
} }
ul, ul,
ol { ol {
margin: 0; margin: 0;
padding-left: 0; padding-left: 0;
list-style-type: none; list-style-type: none;
} }
a { a {
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
color: inherit; color: inherit;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
-webkit-appearance: none; appearance: none;
} }
hr { hr {
border-width: 0; border-width: 0;
border-bottom: 1px solid #e0e0e0; border-bottom: 1px solid #e0e0e0;
} }
blockquote { blockquote {
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;
@ -102,76 +115,11 @@ blockquote {
border-left: 4px solid #cacaca; border-left: 4px solid #cacaca;
} }
.el-dialog__body {
padding: 10px 20px;
}
code, code,
pre { pre {
font-family: Consolas, Menlo, Courier, monospace; 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. * limitations under the License.
*/ */
export interface Option { export interface Option {
value: string | number; value: string;
label: string; label: string;
} }
export interface Duration { export interface Duration {

View File

@ -20,12 +20,12 @@ export interface LayoutConfig {
w: number; w: number;
h: number; h: number;
i: string; i: string;
widget?: WidgetConfig; widget: WidgetConfig;
graph?: GraphConfig; graph: GraphConfig;
standard?: StandardConfig; standard: StandardConfig;
metrics?: string[]; metrics: string[];
type?: string; type: string;
queryMetricType?: string; metricTypes: string[];
children?: any; children?: any;
} }
@ -45,9 +45,17 @@ export interface StandardConfig {
divide?: string; divide?: string;
milliseconds?: string; milliseconds?: string;
seconds?: 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 { export interface BarConfig {
type?: string; type?: string;
showBackground?: boolean; showBackground?: boolean;
@ -57,6 +65,8 @@ export interface LineConfig extends AreaConfig {
smooth?: boolean; smooth?: boolean;
showSymbol?: boolean; showSymbol?: boolean;
step?: boolean; step?: boolean;
showXAxis?: boolean;
showYAxis?: boolean;
} }
export interface AreaConfig { export interface AreaConfig {
@ -67,7 +77,8 @@ export interface AreaConfig {
export interface CardConfig { export interface CardConfig {
type?: string; type?: string;
fontSize?: number; fontSize?: number;
showUint: boolean; showUint?: boolean;
textAlign?: "center" | "right" | "left";
} }
export interface TableConfig { export interface TableConfig {
@ -81,3 +92,21 @@ export interface TopListConfig {
type?: string; type?: string;
topN: number; 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> <div class="about">{{ props.msg }}</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineProps } from "vue"; /*global defineProps */
const props = defineProps({ const props = defineProps({
msg: String, msg: String,
}); });

View File

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

View File

@ -26,8 +26,8 @@ limitations under the License. -->
<div class="item"> <div class="item">
<div class="label">{{ t("layer") }}</div> <div class="label">{{ t("layer") }}</div>
<Selector <Selector
:value="states.layer" v-model="states.selectedLayer"
:options="Options" :options="states.layers"
size="small" size="small"
placeholder="Select a layer" placeholder="Select a layer"
@change="changeLayer" @change="changeLayer"
@ -37,7 +37,7 @@ limitations under the License. -->
<div class="item"> <div class="item">
<div class="label">{{ t("entityType") }}</div> <div class="label">{{ t("entityType") }}</div>
<Selector <Selector
:value="states.entity" v-model="states.entity"
:options="EntityType" :options="EntityType"
size="small" size="small"
placeholder="Select a entity" placeholder="Select a entity"
@ -53,28 +53,38 @@ limitations under the License. -->
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive } from "vue"; import { reactive, onBeforeMount } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import router from "@/router"; import router from "@/router";
import { useSelectorStore } from "@/store/modules/selectors"; import { useSelectorStore } from "@/store/modules/selectors";
import { EntityType, Options } from "./data"; import { EntityType } from "./data";
import uuid from "@/utils/uuid"; import { ElMessage } from "element-plus";
const { t } = useI18n(); const { t } = useI18n();
const selectorStore = useSelectorStore(); const selectorStore = useSelectorStore();
const states = reactive({ const states = reactive({
name: "", name: "",
layer: Options[0].value, selectedLayer: "",
entity: EntityType[0].value, entity: EntityType[0].value,
layers: [],
}); });
const onCreate = () => { const onCreate = () => {
const id = uuid(); const name = states.name.split(" ").join("-");
const path = `/dashboard/edit/${states.layer}/${states.entity}/${id}`; const path = `/dashboard/${states.selectedLayer}/${states.entity}/${name}`;
router.push(path); 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 }[]) { function changeLayer(opt: { label: string; value: string }[]) {
states.layer = opt[0].value; states.selectedLayer = opt[0].value;
} }
function changeEntity(opt: { label: string; value: string }[]) { function changeEntity(opt: { label: string; value: string }[]) {
states.entity = opt[0].value; 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. --> limitations under the License. -->
<template> <template>
<div class="widget-config flex-v"> <div class="widget-config flex-v">
<div class="graph"> <div class="graph" v-loading="loading">
<div class="header"> <div class="header">
<span>{{ states.widget.title }}</span> <span>{{ dashboardStore.selectedGrid.widget.title }}</span>
<div class="tips" v-show="states.widget.tips"> <div class="tips" v-show="dashboardStore.selectedGrid.widget.tips">
<el-tooltip :content="states.widget.tips"> <el-tooltip :content="dashboardStore.selectedGrid.widget.tips">
<Icon iconName="info_outline" size="sm" /> <Icon iconName="info_outline" size="sm" />
</el-tooltip> </el-tooltip>
</div> </div>
</div> </div>
<div class="render-chart"> <div class="render-chart">
<component <component
:is="states.graph.type" :is="dashboardStore.selectedGrid.graph.type"
:intervalTime="appStoreWithOut.intervalTime" :intervalTime="appStoreWithOut.intervalTime"
:data="states.source" :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> </div>
<div class="collapse" :style="{ height: configHeight + 'px' }"> <div class="collapse" :style="{ height: configHeight + 'px' }">
@ -38,59 +45,17 @@ limitations under the License. -->
v-model="states.activeNames" v-model="states.activeNames"
:style="{ '--el-collapse-header-font-size': '15px' }" :style="{ '--el-collapse-header-font-size': '15px' }"
> >
<el-collapse-item :title="t('metricName')" name="1"> <el-collapse-item :title="t('selectVisualization')" name="1">
<div> <MetricOptions @update="getSource" @loading="setLoading" />
<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> </el-collapse-item>
<el-collapse-item :title="t('selectVisualization')" name="2"> <el-collapse-item :title="t('graphStyles')" name="2">
<div class="chart-types"> <component :is="`${dashboardStore.selectedGrid.graph.type}Config`" />
<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> </el-collapse-item>
<el-collapse-item :title="t('graphStyles')" name="3"> <el-collapse-item :title="t('widgetOptions')" name="3">
<component <WidgetOptions />
:is="`${states.graph.type}Config`"
:config="states.graph"
@update="updateGraphOptions"
/>
</el-collapse-item> </el-collapse-item>
<el-collapse-item :title="t('widgetOptions')" name="4"> <el-collapse-item :title="t('standardOptions')" name="4">
<WidgetOptions <StandardOptions />
: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> </el-collapse-item>
</el-collapse> </el-collapse>
</div> </div>
@ -109,19 +74,12 @@ import { reactive, defineComponent, ref } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import { ElMessage } from "element-plus";
import {
ValuesTypes,
MetricQueryTypes,
ChartTypes,
DefaultGraphConfig,
} from "../data";
import { Option } from "@/types/app"; import { Option } from "@/types/app";
import { WidgetConfig, GraphConfig, StandardConfig } from "@/types/dashboard";
import graphs from "../graphs"; import graphs from "../graphs";
import configs from "./graph-styles"; import configs from "./graph-styles";
import WidgetOptions from "./WidgetOptions.vue"; import WidgetOptions from "./WidgetOptions.vue";
import StandardOptions from "./StandardOptions.vue"; import StandardOptions from "./StandardOptions.vue";
import MetricOptions from "./MetricOptions.vue";
export default defineComponent({ export default defineComponent({
name: "ConfigEdit", name: "ConfigEdit",
@ -130,166 +88,49 @@ export default defineComponent({
...configs, ...configs,
WidgetOptions, WidgetOptions,
StandardOptions, StandardOptions,
MetricOptions,
}, },
setup() { setup() {
const loading = ref<boolean>(false); const configHeight = document.documentElement.clientHeight - 520;
const { t } = useI18n(); const { t } = useI18n();
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
const appStoreWithOut = useAppStoreWithOut(); const appStoreWithOut = useAppStoreWithOut();
const { selectedGrid } = dashboardStore; const loading = ref<boolean>(false);
const states = reactive<{ const states = reactive<{
metrics: string[];
valueTypes: Option[];
valueType: string;
metricQueryType: string;
activeNames: string; activeNames: string;
source: any; source: any;
index: string; index: string;
graph: GraphConfig; visType: Option[];
widget: WidgetConfig | any;
standard: StandardConfig;
}>({ }>({
metrics: selectedGrid.metrics || [],
valueTypes: [],
valueType: "",
metricQueryType: "",
activeNames: "1", activeNames: "1",
source: {}, source: {},
index: selectedGrid.i, index: dashboardStore.selectedGrid.i,
graph: selectedGrid.graph, visType: [],
widget: selectedGrid.widget,
standard: selectedGrid.standard,
}); });
if (states.metrics[0]) {
queryMetricType(states.metrics[0]); function getSource(source: unknown) {
states.source = source;
} }
async function changeMetrics(arr: Option[]) { function setLoading(load: boolean) {
if (!arr.length) { loading.value = load;
states.valueTypes = [];
states.valueType = "";
return;
}
states.metrics = arr.map((d: Option) => String(d.value));
if (arr[0].value) {
queryMetricType(String(arr[0].value));
}
} }
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() { function applyConfig() {
const opts = { dashboardStore.setConfigs(dashboardStore.selectedGrid);
...dashboardStore.selectedGrid,
metrics: states.metrics,
queryMetricType: states.valueType,
widget: states.widget,
graph: states.graph,
standard: states.standard,
};
dashboardStore.setConfigs(opts);
dashboardStore.setConfigPanel(false); dashboardStore.setConfigPanel(false);
} }
return { return {
states, states,
changeChartType, loading,
changeValueType,
changeMetrics,
t, t,
appStoreWithOut, appStoreWithOut,
ChartTypes,
metricOpts,
updateWidgetOptions,
configHeight, configHeight,
updateGraphOptions, dashboardStore,
updateStandardOptions,
applyConfig, applyConfig,
loading, getSource,
setLoading,
}; };
}, },
}); });
@ -326,7 +167,7 @@ export default defineComponent({
.render-chart { .render-chart {
padding: 5px; padding: 5px;
height: 350px; height: 400px;
width: 100%; width: 100%;
} }
@ -343,30 +184,10 @@ export default defineComponent({
min-width: 1280px; 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 { .no-data {
font-size: 14px; font-size: 14px;
text-align: center; text-align: center;
line-height: 350px; line-height: 400px;
}
span.active {
background-color: #409eff;
color: #fff;
} }
.footer { .footer {
@ -384,4 +205,8 @@ span.active {
margin-top: 10px; margin-top: 10px;
overflow: auto; overflow: auto;
} }
.ds-name {
margin-bottom: 10px;
}
</style> </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> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, defineProps, defineEmits } from "vue"; import { reactive } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { SortOrder } from "../data"; import { SortOrder } from "../data";
import { StandardConfig } from "@/types/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import type { PropType } from "vue";
const props = defineProps({ const dashboardStore = useDashboardStore();
config: { const { selectedGrid } = dashboardStore;
type: Object as PropType<StandardConfig>,
default: () => ({ unit: "", sortOrder: "DES" }),
},
});
const emits = defineEmits(["update"]);
const { t } = useI18n(); const { t } = useI18n();
const state = reactive({ const state = reactive({
unit: props.config.unit, unit: selectedGrid.standard.unit,
max: "", max: "",
min: "", min: "",
plus: "", plus: "",
@ -140,11 +134,15 @@ const state = reactive({
divide: "", divide: "",
milliseconds: "", milliseconds: "",
seconds: "", seconds: "",
sortOrder: props.config.sortOrder, sortOrder: selectedGrid.standard.sortOrder,
}); });
function changeStandardOpt(param: { [key: string]: unknown }) { function changeStandardOpt(param: { [key: string]: unknown }) {
emits("update", param); const standard = {
...selectedGrid.standard,
...param,
};
dashboardStore.selectWidget({ ...selectedGrid, standard });
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,9 +18,21 @@
import AreaConfig from "./Area.vue"; import AreaConfig from "./Area.vue";
import LineConfig from "./Line.vue"; import LineConfig from "./Line.vue";
import BarConfig from "./Bar.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 { export default {
AreaConfig, AreaConfig,
LineConfig, LineConfig,
BarConfig, BarConfig,
InstanceListConfig,
EndpointListConfig,
ServiceListConfig,
TableConfig,
CardConfig,
TopListConfig,
}; };

View File

@ -84,12 +84,12 @@ limitations under the License. -->
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineProps, reactive, ref, watch } from "vue"; import { ref, watch, reactive } from "vue";
import type { PropType } from "vue"; import type { PropType } from "vue";
import Widget from "./Widget.vue"; import Widget from "./Widget.vue";
import { LayoutConfig } from "@/types/dashboard"; import { LayoutConfig } from "@/types/dashboard";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
/*global defineProps */
const props = defineProps({ const props = defineProps({
data: { data: {
type: Object as PropType<LayoutConfig>, type: Object as PropType<LayoutConfig>,
@ -150,9 +150,12 @@ function clickTabGrid(e: Event, item: LayoutConfig) {
); );
} }
document.body.addEventListener("click", handleClick, false); document.body.addEventListener("click", handleClick, false);
const children = ref(
dashboardStore.layout[props.data.i].children[activeTabIndex.value].children
);
watch( watch(
() => () => children.value,
dashboardStore.layout[props.data.i].children[activeTabIndex.value].children,
(data) => { (data) => {
state.layout = 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"> <div class="body" v-if="data.graph?.type" v-loading="loading">
<component <component
:is="data.graph.type" :is="data.graph.type"
:intervalTime="appStoreWithOut.intervalTime" :intervalTime="appStore.intervalTime"
:data="state.source" :data="state.source"
:config="data.graph" :config="{
...data.graph,
metrics: data.metrics,
metricTypes: data.metricTypes,
i: data.i,
}"
:standard="data.standard" :standard="data.standard"
/> />
</div> </div>
@ -60,9 +65,11 @@ import type { PropType } from "vue";
import { LayoutConfig } from "@/types/dashboard"; import { LayoutConfig } from "@/types/dashboard";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import { useSelectorStore } from "@/store/modules/selectors";
import graphs from "../graphs"; import graphs from "../graphs";
import { ElMessage } from "element-plus";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useQueryProcessor, useSourceProcessor } from "@/hooks/useProcessor";
import { EntityType, TableChartTypes } from "../data";
const props = { const props = {
data: { data: {
@ -78,34 +85,27 @@ export default defineComponent({
setup(props) { setup(props) {
const { t } = useI18n(); const { t } = useI18n();
const loading = ref<boolean>(false); const loading = ref<boolean>(false);
const state = reactive({ const state = reactive<{ source: { [key: string]: unknown } }>({
source: {}, source: {},
}); });
const { data } = toRefs(props); const { data } = toRefs(props);
const appStoreWithOut = useAppStoreWithOut(); const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
queryMetrics(); const selectorStore = useSelectorStore();
async function queryMetrics() { async function queryMetrics() {
const params = await useQueryProcessor(props.data);
if (!params) {
state.source = {};
return;
}
loading.value = true; loading.value = true;
const json = await dashboardStore.fetchMetricValue(props.data); const json = await dashboardStore.fetchMetricValue(params);
loading.value = false; loading.value = false;
if (!json) { if (!json) {
return; return;
} }
if (json.error) { state.source = useSourceProcessor(json, props.data);
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,
};
} }
function removeWidget() { function removeWidget() {
@ -121,17 +121,41 @@ export default defineComponent({
} }
} }
watch( watch(
() => [props.data.queryMetricType, props.data.metrics], () => [props.data.metricTypes, props.data.metrics],
(data, old) => { () => {
if (data[0] === old[0] && data[1] === old[1]) { if (props.data.i !== dashboardStore.selectedGrid.i) {
return;
}
if (TableChartTypes.includes(dashboardStore.selectedGrid.graph.type)) {
return; return;
} }
queryMetrics(); 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 { return {
state, state,
appStoreWithOut, appStore,
removeWidget, removeWidget,
editConfig, editConfig,
data, data,

View File

@ -14,18 +14,36 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
export const PodsChartTypes = ["EndpointList", "InstanceList"];
export const TableChartTypes = ["EndpointList", "InstanceList", "ServiceList"];
export const ChartTypes = [ export const ChartTypes = [
{ label: "Bar", value: "Bar" }, { label: "Bar", value: "Bar" },
{ label: "Line", value: "Line" }, { label: "Line", value: "Line" },
{ label: "Area", value: "Area" }, { label: "Area", value: "Area" },
{ label: "Heatmap", value: "Heatmap" }, // { label: "Pie", value: "Pie" },
{ label: "Pie", value: "Pie" },
{ label: "Card", value: "Card" }, { label: "Card", value: "Card" },
{ label: "Top List", value: "TopList" }, { label: "Top List", value: "TopList" },
{ label: "Table", value: "Table" }, { label: "Table", value: "Table" },
{ label: "Heatmap", value: "Heatmap" },
{ label: "Service List", value: "ServiceList" },
{ label: "Endpoint List", value: "EndpointList" }, { label: "Endpoint List", value: "EndpointList" },
{ label: "Instance List", value: "InstanceList" }, { 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 } = { export const DefaultGraphConfig: { [key: string]: any } = {
Bar: { Bar: {
type: "Bar", type: "Bar",
@ -36,36 +54,47 @@ export const DefaultGraphConfig: { [key: string]: any } = {
step: false, step: false,
smooth: false, smooth: false,
showSymbol: false, showSymbol: false,
showXAxis: true,
showYAxis: true,
}, },
Area: { Area: {
type: "Area", type: "Area",
opacity: 0.4, opacity: 0.4,
showXAxis: true,
showYAxis: true,
}, },
Card: { Card: {
type: "Card", type: "Card",
fontSize: 14, fontSize: 14,
textAlign: "center",
showUint: true, showUint: true,
}, },
Table: { Table: {
type: "Card", type: "Table",
showTableValues: true, showTableValues: true,
tableHeaderCol1: "", tableHeaderCol1: "",
tableHeaderCol2: "", tableHeaderCol2: "",
}, },
TopList: { TopList: {
type: "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 { export enum MetricsType {
UNKNOWN = "UNKNOWN", UNKNOWN = "UNKNOWN",
REGULAR_VALUE = "REGULAR_VALUE", REGULAR_VALUE = "REGULAR_VALUE",
@ -73,7 +102,7 @@ export enum MetricsType {
HEATMAP = "HEATMAP", HEATMAP = "HEATMAP",
SAMPLED_RECORD = "SAMPLED_RECORD", SAMPLED_RECORD = "SAMPLED_RECORD",
} }
export const ValuesTypes: { export const MetricTypes: {
[key: string]: Array<{ label: string; value: string }>; [key: string]: Array<{ label: string; value: string }>;
} = { } = {
REGULAR_VALUE: [ REGULAR_VALUE: [
@ -97,55 +126,40 @@ export const ValuesTypes: {
{ label: "get sorted topN values", value: "readSampledRecords" }, { 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 MetricCatalog {
SERVICE = "Service",
export enum MetricsName { SERVICE_INSTANCE = "ServiceInstance",
SERVICE_RESP_TIME = "service_resp_time", ENDPOINT = "Endpoint",
SERVICE_SLA = "service_sla", ALL = "All",
SERVICE_CPM = "service_cpm", SERVICE_RELATION = "ServiceRelation",
SERVICE_PERCENTILE = "service_percentile", SERVICE_INSTANCE_RELATION = "ServiceInstanceRelation",
SERVICE_APDEX = "service_apdex", ENDPOINT_RELATION = "EndpointRelation",
} }
export const EntityType = [ export const EntityType = [
{ value: "service", label: "Service", key: 1 }, { value: "Service", label: "Service", key: 1 },
{ value: "all", label: "All", key: 10 }, { value: "All", label: "All", key: 10 },
{ value: "endpoint", label: "Service Endpoint", key: 3 }, { value: "Endpoint", label: "Service Endpoint", key: 3 },
{ value: "serviceInstance", label: "Service Instance", key: 3 }, { value: "ServiceInstance", label: "Service Instance", key: 3 },
{ value: "serviceRelationClient", label: "Service Relation(client)", key: 2 }, { value: "ServiceRelationClient", label: "Service Relation(client)", key: 2 },
{ value: "serviceRelationServer", label: "Service Relation(server)", key: 2 }, { value: "ServiceRelationServer", label: "Service Relation(server)", key: 2 },
{ {
value: "serviceInstanceRelationClient", value: "ServiceInstanceRelationClient",
label: "Service Instance Relation(client)", label: "Service Instance Relation(client)",
key: 4, key: 4,
}, },
{ {
value: "serviceInstanceRelationServer", value: "ServiceInstanceRelationServer",
label: "Service Instance Relation(server)", label: "Service Instance Relation(server)",
key: 4, 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 = [ export const SortOrder = [
{ label: "DES", value: "DES" }, { label: "DES", value: "DES" },
{ label: "ASC", value: "ASC" }, { label: "ASC", value: "ASC" },
@ -159,69 +173,3 @@ export const ToolIcons = [
{ name: "settings", content: "Settings", id: "settings" }, { name: "settings", content: "Settings", id: "settings" },
{ name: "save", content: "Apply", id: "applay" }, { 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" /> <Line :data="data" :intervalTime="intervalTime" :config="config" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineProps } from "vue";
import type { PropType } from "vue"; import type { PropType } from "vue";
import Line from "./Line.vue"; import Line from "./Line.vue";
import { AreaConfig } from "@/types/dashboard"; import { AreaConfig } from "@/types/dashboard";
/*global defineProps */
defineProps({ defineProps({
data: { data: {
type: Object as PropType<{ [key: string]: number[] }>, type: Object as PropType<{ [key: string]: number[] }>,

View File

@ -16,11 +16,12 @@ limitations under the License. -->
<Graph :option="option" /> <Graph :option="option" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineProps, computed } from "vue"; import { computed } from "vue";
import type { PropType } from "vue"; import type { PropType } from "vue";
import { Event } from "@/types/events"; import { Event } from "@/types/events";
import { BarConfig } from "@/types/dashboard"; import { BarConfig } from "@/types/dashboard";
/*global defineProps */
const props = defineProps({ const props = defineProps({
data: { data: {
type: Object as PropType<{ [key: string]: number[] }>, 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. --> limitations under the License. -->
<template> <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" typeof singleVal === "string"
? data[key] ? singleVal
: isNaN(data[key]) : isNaN(singleVal)
? null ? null
: data[key].toFixed(2) : singleVal.toFixed(2)
}} }}
<span v-show="config.showUint">{{ standard.unit }}</span> <span v-show="config.showUint">{{ standard.unit }}</span>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, PropType } from "vue"; import { computed, PropType } from "vue";
import { defineProps } from "vue";
import { CardConfig, StandardConfig } from "@/types/dashboard"; import { CardConfig, StandardConfig } from "@/types/dashboard";
/*global defineProps */
const props = defineProps({ const props = defineProps({
data: { data: {
type: Object as PropType<{ [key: string]: number }>, type: Object as PropType<{ [key: string]: number }>,
@ -37,7 +40,7 @@ const props = defineProps({
}, },
config: { config: {
type: Object as PropType<CardConfig>, type: Object as PropType<CardConfig>,
default: () => ({ fontSize: 12, showUint: true }), default: () => ({ fontSize: 12, showUint: true, textAlign: "center" }),
}, },
standard: { standard: {
type: Object as PropType<StandardConfig>, type: Object as PropType<StandardConfig>,
@ -45,10 +48,16 @@ const props = defineProps({
}, },
}); });
const key = computed(() => Object.keys(props.data)[0]); const key = computed(() => Object.keys(props.data)[0]);
const singleVal = computed(() => props.data[key.value]);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.chart-card { .chart-card {
box-sizing: border-box; box-sizing: border-box;
color: #333; color: #333;
display: -webkit-box;
-webkit-box-orient: horizontal;
-webkit-box-pack: center;
-webkit-box-align: center;
height: 100%;
} }
</style> </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 See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<el-table :data="data" style="width: 100%"> <div class="table">
<el-table-column prop="label" label="Endpoints" /> <div class="search">
</el-table> <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> </template>
<script setup lang="ts"> <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 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: { data: {
type: Array as PropType<{ label: string; value: string }[]>, type: Object,
default: () => [],
}, },
config: { config: {
type: Object, type: Object as PropType<
default: () => ({}), 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> </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" /> <Graph :option="option" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineProps, computed } from "vue"; import { computed } from "vue";
import type { PropType } from "vue"; import type { PropType } from "vue";
import { StandardConfig } from "@/types/dashboard"; import { StandardConfig } from "@/types/dashboard";
/*global defineProps */
const props = defineProps({ const props = defineProps({
data: { data: {
type: Object as PropType<{ nodes: number[][]; buckets: number[][] }>, 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 See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<el-table :data="data" style="width: 100%"> <div class="table">
<el-table-column prop="label" label="Service Instances" /> <div class="search">
</el-table> <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> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps } from "vue"; import { ref, watch } from "vue";
import { ElMessage } from "element-plus";
import type { PropType } from "vue"; 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({ /*global defineProps */
data: { const props = defineProps({
type: Array as PropType<{ label: string; value: string }[]>,
default: () => [],
},
config: { config: {
type: Object, type: Object as PropType<
default: () => ({}), 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> </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" /> <Graph :option="option" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineProps, computed } from "vue"; import { computed } from "vue";
import type { PropType } from "vue"; import type { PropType } from "vue";
import { Event } from "@/types/events"; import { Event } from "@/types/events";
import { LineConfig } from "@/types/dashboard"; import { LineConfig } from "@/types/dashboard";
/*global defineProps */
const props = defineProps({ const props = defineProps({
data: { data: {
type: Object as PropType<{ [key: string]: number[] }>, type: Object as PropType<{ [key: string]: number[] }>,
@ -36,6 +37,8 @@ const props = defineProps({
smooth: false, smooth: false,
showSymbol: false, showSymbol: false,
opacity: 0.4, opacity: 0.4,
showXAxis: true,
showYAxis: true,
}), }),
}, },
}); });
@ -142,6 +145,8 @@ function getOption() {
color, color,
tooltip: { tooltip: {
trigger: "axis", trigger: "axis",
zlevel: 1000,
z: 60,
backgroundColor: "rgb(50,50,50)", backgroundColor: "rgb(50,50,50)",
textStyle: { textStyle: {
fontSize: 13, fontSize: 13,
@ -170,6 +175,7 @@ function getOption() {
}, },
xAxis: { xAxis: {
type: "category", type: "category",
show: props.config.showXAxis,
axisTick: { axisTick: {
lineStyle: { color: "#c1c5ca41" }, lineStyle: { color: "#c1c5ca41" },
alignWithLabel: true, alignWithLabel: true,
@ -183,7 +189,11 @@ function getOption() {
axisLine: { show: false }, axisLine: { show: false },
axisTick: { show: false }, axisTick: { show: false },
splitLine: { lineStyle: { color: "#c1c5ca41", type: "dashed" } }, splitLine: { lineStyle: { color: "#c1c5ca41", type: "dashed" } },
axisLabel: { color: "#9da5b2", fontSize: "11" }, axisLabel: {
color: "#9da5b2",
fontSize: "11",
show: props.config.showYAxis,
},
}, },
series: temp, series: temp,
}; };

View File

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

View File

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

View File

@ -23,6 +23,9 @@ import TopList from "./TopList.vue";
import Table from "./Table.vue"; import Table from "./Table.vue";
import Pie from "./Pie.vue"; import Pie from "./Pie.vue";
import Card from "./Card.vue"; import Card from "./Card.vue";
import InstanceList from "./InstanceList.vue";
import EndpointList from "./EndpointList.vue";
import ServiceList from "./ServiceList.vue";
export default { export default {
Line, Line,
@ -33,4 +36,7 @@ export default {
Table, Table,
Pie, Pie,
Card, 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> <template>
<div class="dashboard-tool flex-h"> <div class="dashboard-tool flex-h">
<div class="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> <span class="label">$Service</span>
<Selector <Selector
:value="states.service" v-model="states.currentService"
:options="Options" :options="selectorStore.services"
size="mini" size="mini"
placeholder="Select a service" placeholder="Select a service"
@change="changeService" @change="changeService"
class="selectors" class="selectors"
:borderRadius="4"
/> />
</div> </div>
<div class="selectors-item" v-if="states.key === 3 || states.key === 4"> <div class="selectors-item" v-if="states.key === 3 || states.key === 4">
<span class="label">$ServiceInstance</span> <span class="label">
<el-cascader {{
placeholder="Select a instance" dashboardStore.entity === "Endpoint"
:options="SelectOpts" ? "$Endpoint"
: "$ServiceInstance"
}}
</span>
<Selector
v-model="states.currentPod"
:options="selectorStore.pods"
size="mini" size="mini"
filterable placeholder="Select a data"
:style="{ minWidth: '300px' }" @change="changePods"
class="selectors"
/> />
</div> </div>
<div class="selectors-item" v-if="states.key === 2"> <div class="selectors-item" v-if="states.key === 2">
<span class="label">$DestinationService</span> <span class="label">$DestinationService</span>
<Selector <Selector
:value="states.service" v-model="selectorStore.currentDestService"
:options="Options" :options="selectorStore.services"
size="mini" size="mini"
placeholder="Select a service" placeholder="Select a service"
:borderRadius="0"
@change="changeService" @change="changeService"
class="selectors" class="selectors"
/> />
</div> </div>
<div class="selectors-item" v-if="states.key === 4"> <div class="selectors-item" v-if="states.key === 4">
<span class="label">$DestinationServiceInstance</span> <span class="label">$DestinationServiceInstance</span>
<el-cascader <Selector
placeholder="Select a instance" v-model="states.currentPod"
:options="SelectOpts" :options="selectorStore.pods"
size="mini" size="mini"
filterable placeholder="Select a data"
:style="{ minWidth: '300px' }" @change="changePods"
class="selectors"
:borderRadius="4"
/> />
</div> </div>
</div> </div>
@ -80,33 +87,96 @@ limitations under the License. -->
import { reactive } from "vue"; import { reactive } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { useDashboardStore } from "@/store/modules/dashboard"; 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 dashboardStore = useDashboardStore();
const selectorStore = useSelectorStore();
const params = useRoute().params; const params = useRoute().params;
const type = EntityType.filter((d: Option) => d.value === params.entity)[0];
const states = reactive<{ const states = reactive<{
entity: string | string[];
layerId: string | string[];
service: string;
pod: string;
destService: string; destService: string;
destPod: string; destPod: string;
key: number; key: number;
currentService: string;
currentPod: string;
}>({ }>({
service: Options[0].value,
pod: Options[0].value, // instances and endpoints
destService: "", destService: "",
destPod: "", destPod: "",
key: EntityType.filter((d: any) => d.value === params.entity)[0].key || 0, key: (type && type.key) || 0,
entity: params.entity, currentService: "",
layerId: params.layerId, currentPod: "",
}); });
dashboardStore.setLayer(states.layerId); dashboardStore.setLayer(String(params.layerId));
dashboardStore.setEntity(states.entity); dashboardStore.setEntity(String(params.entity));
function changeService(val: { value: string; label: string }) { if (params.serviceId) {
states.service = val.value; 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 }) { 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"); 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.dashboard-tool { .dashboard-tool {

View File

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

View File

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

View File

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

View File

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