Copilot commented on code in PR #1426:
URL: https://github.com/apache/dubbo-admin/pull/1426#discussion_r2936380163


##########
ui-vue3/src/views/resources/applications/tabs/topology.vue:
##########
@@ -0,0 +1,298 @@
+<!--
+  ~ 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="__container_app_topology">
+    <a-flex>
+      <a-card class="topology-warpper"> <div id="topology"></div> </a-card>

Review Comment:
   Typo in class name `topology-warpper` ("warpper" -> "wrapper"). Renaming to 
`topology-wrapper` (and updating both template + style) will avoid propagating 
the misspelling across the codebase.
   



##########
ui-vue3/src/views/resources/applications/tabs/topology.vue:
##########
@@ -0,0 +1,298 @@
+<!--
+  ~ 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="__container_app_topology">
+    <a-flex>
+      <a-card class="topology-warpper"> <div id="topology"></div> </a-card>
+    </a-flex>
+    <a-drawer v-model:open="detailDrawerOpen" :title="detailTitle" 
placement="right" width="520">
+      <a-spin :spinning="detailLoading">
+        <a-typography-text v-if="detailError" type="danger">{{ detailError 
}}</a-typography-text>
+        <a-descriptions
+          v-else
+          :column="1"
+          size="small"
+          bordered
+          :labelStyle="{ fontWeight: 'bold', width: '160px' }"
+        >
+          <a-descriptions-item
+            v-for="(v, k) in detailData"
+            :key="k"
+            v-show="v !== undefined && v !== null"
+          >
+            <template #label>{{ k }}</template>
+            <a-typography-paragraph style="margin-bottom: 0">{{
+              formatValueForDisplay(v)
+            }}</a-typography-paragraph>
+          </a-descriptions-item>
+        </a-descriptions>
+      </a-spin>
+    </a-drawer>
+  </div>
+</template>
+
+<script setup lang="tsx">
+import { PRIMARY_COLOR } from '@/base/constants'
+import { getApplicationDetail, getInterfaceGraph } from '@/api/service/app'
+import { HTTP_STATUS } from '@/base/http/constants'
+import { computed, defineComponent, onBeforeUnmount, onMounted, ref, 
shallowRef, watch } from 'vue'
+import type { PropType } from 'vue'
+import { useRoute } from 'vue-router'
+import { ExtensionCategory, register, Graph, NodeEvent } from '@antv/g6'
+import { VueNode } from 'g6-extension-vue'
+
+const route = useRoute()
+
+const graphRef = shallowRef<Graph | null>(null)
+
+const detailDrawerOpen = ref(false)
+const detailLoading = ref(false)
+const detailError = ref('')
+const detailData = shallowRef<Record<string, unknown>>({})
+const currentDetailKey = ref('')
+const selectedNodeId = ref('')
+
+const clearSelectedNode = () => {
+  const id = selectedNodeId.value
+  if (id && graphRef.value) {
+    graphRef.value.setElementState(id, [])
+  }
+  selectedNodeId.value = ''
+}
+
+watch(detailDrawerOpen, (open) => {
+  if (!open) {
+    clearSelectedNode()
+    currentDetailKey.value = ''
+  }
+})
+
+type VueNodeViewData = {
+  id?: string | number
+  label?: string
+  states?: string[]
+  data?: Record<string, unknown>
+}
+
+const StatefulNode = defineComponent({
+  props: {
+    data: { type: Object as PropType<VueNodeViewData>, required: true }
+  },
+  setup(props) {
+    const label = computed(() => {
+      return String(props.data?.label ?? props.data?.data?.label ?? 
props.data?.id ?? '')
+    })
+
+    const isSelected = computed(() => {
+      const states = props.data?.states
+      if (!Array.isArray(states)) return false
+      return states.includes('selected') || states.includes('active')
+    })
+
+    return () => (
+      <div
+        style={{
+          display: 'flex',
+          flexDirection: 'column',
+          alignItems: 'center',
+          justifyContent: 'center'
+        }}
+      >
+        <div
+          style={{
+            color: isSelected.value ? PRIMARY_COLOR.value : 'rgba(0,0,0,0.65)',
+            filter: isSelected.value ? `drop-shadow(0 0 6px 
${PRIMARY_COLOR.value}88)` : 'none',
+            transform: `scale(${isSelected.value ? 1.06 : 1})`,
+            transition: 'all 0.12s ease-in-out'
+          }}
+        >
+          <span
+            class={['iconfont', 'icon-yingyong']}
+            style={{ fontSize: '40px', lineHeight: '40px' }}
+          ></span>
+        </div>
+        <div
+          style={{
+            paddingTop: '4px',
+            userSelect: 'none',
+            textAlign: 'center',
+            fontSize: '12px',
+            lineHeight: '16px'
+          }}
+          onPointerdown={(e: any) => e.stopPropagation()}
+          onMousedown={(e: any) => e.stopPropagation()}
+          onClick={(e: any) => e.stopPropagation()}
+        >
+          {label.value}
+        </div>
+      </div>
+    )
+  }
+})
+
+register(ExtensionCategory.NODE, 'vue-node', VueNode)
+
+const detailTitle = computed(() => {
+  return currentDetailKey.value ? `应用详情:${currentDetailKey.value}` : '应用详情'
+})
+
+const formatValueForDisplay = (v: unknown) => {
+  if (v === null || v === undefined) return ''
+  if (typeof v === 'string' || typeof v === 'number' || typeof v === 
'boolean') return String(v)
+  if (Array.isArray(v))
+    return v
+      .map((x) => formatValueForDisplay(x))
+      .filter(Boolean)
+      .join(', ')
+  if (typeof v === 'object') {
+    try {
+      return JSON.stringify(v)
+    } catch {
+      return String(v)
+    }
+  }
+  return String(v)
+}
+
+const buildGraphData = (raw: any) => {
+  const nodes = Array.isArray(raw?.nodes)
+    ? raw.nodes.map((n: any) => ({
+        id: String(n?.id ?? ''),
+        label: n?.label ?? n?.id,
+        data: n?.data
+      }))
+    : []
+
+  const edges = Array.isArray(raw?.edges)
+    ? raw.edges.map((e: any, idx: number) => ({
+        id: e?.id ?? `edge-${idx}`,
+        source: String(e?.source ?? ''),
+        target: String(e?.target ?? '')
+      }))
+    : []
+
+  return { nodes, edges }
+}
+
+const renderTopology = (graphData: any) => {
+  const root = document.getElementById('topology')
+  if (!root) return
+
+  graphRef.value?.destroy()
+  graphRef.value = null
+
+  const primaryColor = PRIMARY_COLOR.value
+  const graph = new Graph({
+    container: root,
+    width: root.clientWidth || 800,
+    height: Math.max(root.clientHeight || 500, 500),
+    autoFit: 'view',
+    padding: 20,
+    data: graphData,
+    layout: {
+      type: 'd3-force',
+      link: { distance: 180, strength: 1 },
+      collide: { radius: 40 }
+    },
+    node: {
+      type: 'vue-node',
+      style: {
+        component: (data) => <StatefulNode data={Object.assign({}, data)} />
+      }
+    },
+    edge: {
+      style: {
+        stroke: primaryColor,
+        endArrow: true,
+        lineWidth: 1.2,
+        strokeOpacity: 0.8
+      }
+    },
+
+    behaviors: [
+      'drag-canvas',
+      'zoom-canvas',
+      'click-select',
+      { type: 'drag-element-force', fixed: true }
+    ]
+  })
+
+  const handleNodeClick = async (e: any) => {
+    const appName = String(e?.target?.id)

Review Comment:
   `const appName = String(e?.target?.id)` will produce the literal string 
"undefined" when the event target has no id, so the `if (!appName) return` 
guard won’t trigger and the code may request details for an invalid appName. 
Prefer a null-safe extraction (e.g., default to ''), or use the G6 node/item id 
from the event payload if available.
   



##########
ui-vue3/src/assets/iconfont/demo_index.html:
##########
@@ -0,0 +1,257 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8"/>
+  <title>iconfont Demo</title>
+  <link rel="shortcut icon" 
href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg"
 type="image/x-icon"/>
+  <link rel="icon" type="image/svg+xml" 
href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg"/>
+  <link rel="stylesheet" 
href="https://g.alicdn.com/thx/cube/1.3.2/cube.min.css";>
+  <link rel="stylesheet" href="demo.css">
+  <link rel="stylesheet" href="iconfont.css">
+  <script src="iconfont.js"></script>
+  <!-- jQuery -->
+  <script 
src="https://a1.alicdn.com/oss/uploads/2018/12/26/7bfddb60-08e8-11e9-9b04-53e73bb6408b.js";></script>
+  <!-- 代码高亮 -->
+  <script 
src="https://a1.alicdn.com/oss/uploads/2018/12/26/a3f714d0-08e6-11e9-8a15-ebf944d7534c.js";></script>

Review Comment:
   This demo file pulls remote scripts/styles from external CDNs (alicdn, 
jQuery, highlighter) and is not referenced by the app runtime. Keeping it under 
`src/assets` increases repo surface area and can become a supply-chain/security 
concern if it’s ever served accidentally. Consider removing demo_* files (and 
iconfont.js/json) from the production source tree or moving them to 
documentation/examples outside the shipped app.
   



##########
ui-vue3/src/main.ts:
##########
@@ -38,6 +37,10 @@ const pinia = createPinia()
 
 pinia.use(piniaPluginPersistedstate)
 
+if (import.meta.env.DEV) {
+  import('./api/mock/index')
+}

Review Comment:
   This un-awaited dynamic import enables Mock.js for every dev build and can 
silently override real backend calls, which makes it hard to test against a 
live API. Consider gating this behind an explicit flag (e.g. `VITE_USE_MOCK`) 
and using `void import(...)` / awaiting it to avoid a floating promise and 
ensure mocks load deterministically before the first requests.



##########
ui-vue3/src/views/resources/services/tabs/topology.vue:
##########
@@ -0,0 +1,351 @@
+<!--
+  ~ 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="__container_app_topology">
+    <a-flex>
+      <a-card class="topology-warpper"> <div id="topology"></div> </a-card>

Review Comment:
   Typo in class name `topology-warpper` ("warpper" -> "wrapper"). Renaming to 
`topology-wrapper` (and updating both template + style) will avoid propagating 
the misspelling across the codebase.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to