Alanxtl commented on code in PR #1026:
URL: https://github.com/apache/dubbo-go-pixiu/pull/1026#discussion_r3939459924


##########
admin/web/src/api/client.ts:
##########
@@ -0,0 +1,130 @@
+/*
+ * 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 { CODE_OK, RetData } from '../types'
+import { loadSession, touchSession } from '../auth/session'
+
+export class ApiError extends Error {
+  code: string
+
+  constructor(code: string, message: string) {
+    super(message)
+    this.code = code
+  }
+}
+
+export interface RequestOptions {
+  method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
+  /** Query string parameters. */
+  query?: Record<string, string | number | undefined>
+  /** application/x-www-form-urlencoded body fields (the Admin form 
convention). */
+  form?: Record<string, string | number | undefined>
+  /** Raw body (JSON/YAML), used by the route-binding endpoints. */
+  rawBody?: string
+  /** Attach token + username headers. Default true. */
+  auth?: boolean
+}
+
+/**
+ * Minimal fetch wrapper for the Pixiu Admin API.
+ * Every handler returns HTTP 200; success is signalled by body code '10001'.
+ */
+export async function request<T = unknown>(path: string, options: 
RequestOptions = {}): Promise<T> {
+  const { method = 'GET', query, form, rawBody, auth = true } = options
+
+  let url = path
+  if (query) {
+    const params = new URLSearchParams()
+    for (const [key, value] of Object.entries(query)) {
+      if (value !== undefined && value !== '') params.set(key, String(value))
+    }
+    const qs = params.toString()
+    if (qs) url += (url.includes('?') ? '&' : '?') + qs
+  }
+
+  const headers: Record<string, string> = {}
+  if (auth) {
+    const session = loadSession()
+    if (session) {
+      // Config requests carry both token and username (backend convention).
+      headers['token'] = session.token
+      headers['username'] = session.username
+    }
+  }
+
+  let body: string | undefined
+  if (form) {
+    const params = new URLSearchParams()
+    for (const [key, value] of Object.entries(form)) {
+      if (value !== undefined) params.set(key, String(value))
+    }
+    body = params.toString()
+    headers['Content-Type'] = 'application/x-www-form-urlencoded'
+  } else if (rawBody !== undefined) {
+    body = rawBody
+    headers['Content-Type'] = 'application/json'
+  }
+
+  let response: Response
+  try {
+    response = await fetch(url, { method, headers, body })
+  } catch {
+    throw new ApiError('NETWORK', '无法连接 Admin 服务,请确认后端已启动(默认 127.0.0.1:8081)')
+  }
+
+  const text = await response.text()
+  let payload: RetData<T> | null = null
+  try {
+    payload = JSON.parse(text) as RetData<T>
+  } catch {
+    // Some Admin handlers write two envelopes back-to-back (an error envelope
+    // followed by a success one). Honor the first so its message surfaces.
+    const boundary = text.indexOf('}{')
+    if (boundary > 0) {
+      try {
+        payload = JSON.parse(text.slice(0, boundary + 1)) as RetData<T>
+      } catch {
+        payload = null
+      }
+    }
+  }
+  if (!payload) {
+    throw new ApiError('BAD_RESPONSE', `接口返回了非 JSON 内容(HTTP 
${response.status})`)
+  }
+
+  if (payload.code !== CODE_OK) {
+    const message = typeof payload.data === 'string' ? payload.data : 
`请求失败(code ${payload.code})`
+    throw new ApiError(payload.code, message)
+  }
+
+  // Successful requests renew the local sliding session.
+  if (auth) touchSession()
+  return payload.data
+}
+
+/**
+ * Some legacy endpoints double-encode collections: data is a JSON string that
+ * must be parsed again (e.g. resource/method lists).
+ */
+export function parseJsonData<T>(data: unknown, fallback: T): T {
+  if (typeof data !== 'string') return (data as T) ?? fallback
+  try {
+    return JSON.parse(data) as T
+  } catch {
+    return fallback
+  }

Review Comment:
   对字符串 `"null"` 执行 `JSON.parse` 后直接返回 `null`,没有使用 fallback。
   
      后端空列表会被序列化为 `"null"`;随后 `Overview.tsx` 的 `resources.length`、`Mapping.tsx` 
的 `methods.length` 会触发运行时异常。新部署的空 etcd 环境很容易复现。
   
      建议对解析结果做 `parsed ?? fallback` 或 `Array.isArray` 校验



##########
admin/web/src/pages/RouteBinding.tsx:
##########
@@ -0,0 +1,972 @@
+/*
+ * 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 { useCallback, useEffect, useMemo, useState } from 'react'
+import yaml from 'js-yaml'
+import {
+  ArrowClockwise,
+  ArrowCounterClockwise,
+  ClockCounterClockwise,
+  CloudArrowUp,
+  Eye,
+  FilePlus,
+  FloppyDisk,
+  GitDiff,
+  Plus,
+  Trash,
+} from '@phosphor-icons/react'
+import {
+  deleteRouteBindingDraft,
+  getRouteBindingDetail,
+  getRouteBindingHistory,
+  getRouteBindingSchema,
+  listRouteBindings,
+  previewRouteBinding,
+  publishRouteBinding,
+  rollbackRouteBinding,
+  saveRouteBindingDraft,
+} from '../api'
+import {
+  AdminObject,
+  ObjectSchema,
+  RouteBindingPreview,
+  RouteBindingRecord,
+  RouteBindingSummary,
+} from '../types'
+import { Button, EmptyState, ErrorState, Field, formatTime, Loading, Panel, 
Select, Tag, TextInput } from '../components/common'
+import { ConfirmModal, Modal } from '../components/Modal'
+import { YamlEditor } from '../components/YamlEditor'
+import { useToast } from '../components/Toast'
+
+// ---------------------------------------------------------------------------
+// Model helpers
+// ---------------------------------------------------------------------------
+
+const KIND = 'AdminRouteBinding'
+const NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$/
+
+const FALLBACK_ENTRY_PROTOCOLS = ['http']
+const FALLBACK_HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 
'OPTIONS', 'HEAD']
+const FALLBACK_TARGET_PROTOCOLS = ['dubbo']
+const FALLBACK_PARAM_TYPES = [
+  'string',
+  'int',
+  'long',
+  'float',
+  'double',
+  'boolean',
+  'char',
+  'short',
+  'date',
+  'object',
+  'java.lang.String',
+  'java.lang.Integer',
+  'java.lang.Long',
+  'java.lang.Double',
+  'java.lang.Boolean',
+]
+
+function emptyModel(): AdminObject {
+  return {
+    kind: KIND,
+    metadata: { name: '' },
+    spec: {
+      entry: { protocol: 'http', path: '/api/v1/example/:id', method: 'GET' },
+      target: { protocol: 'dubbo', application: '', interface: '', method: '', 
version: '', group: '', cluster: '' },
+      params: [],
+      timeout: '1s',
+      publish: { mode: 'draft', validate: true },
+    },
+  }
+}
+
+function dumpModel(model: AdminObject): string {
+  return yaml.dump(model, { indent: 2, lineWidth: 100, noRefs: true })
+}
+
+/** Parse YAML or JSON text into an AdminObject; throws with a readable 
message. */
+function parseModel(text: string): AdminObject {
+  const value = yaml.load(text)
+  if (value === null || typeof value !== 'object' || Array.isArray(value)) {
+    throw new Error('内容必须是 YAML/JSON 对象')
+  }
+  const object = value as Record<string, unknown>
+  if (typeof object.kind !== 'string' || !object.kind) {
+    throw new Error('缺少 kind 字段')
+  }
+  const metadata = (object.metadata ?? {}) as Record<string, unknown>
+  if (metadata !== null && typeof metadata !== 'object') {
+    throw new Error('metadata 必须是对象')
+  }
+  const spec = (object.spec ?? {}) as Record<string, unknown>
+  if (spec !== null && typeof spec !== 'object') {
+    throw new Error('spec 必须是对象')
+  }
+  return { kind: object.kind, metadata: metadata as AdminObject['metadata'], 
spec }
+}

Review Comment:
   对 `null` 的校验不完整。用户在 YAML 中输入 `metadata: null` 后,后续 `model.metadata.name` 
会抛异常;`spec: null` 也会在 `specOf` 中崩溃。
   
      建议将 `null` 判定为非法对象并展示解析错误,而不是更新页面状态。



##########
admin/web/src/App.tsx:
##########
@@ -0,0 +1,64 @@
+/*
+ * 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 { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'
+import { ToastProvider } from './components/Toast'
+import { Layout } from './components/Layout'
+import { LoginPage } from './pages/Login'
+import { OverviewPage } from './pages/Overview'
+import { MappingPage } from './pages/Mapping'
+import { RouteBindingPage } from './pages/RouteBinding'
+import { PluginGroupPage } from './pages/PluginGroup'
+import { ClusterPage } from './pages/ClusterPage'
+import { ListenerPage } from './pages/ListenerPage'
+import { RateLimiterPage } from './pages/RateLimiter'
+import { OpaPage } from './pages/OpaPage'
+import { ProfilePage } from './pages/Profile'
+
+// Note: intentionally no global route guard. Whether an unauthenticated visit
+// can operate is decided by API auth and each page's own state.
+export default function App() {
+  return (
+    <ToastProvider>
+      <BrowserRouter>
+        <Routes>
+          <Route path="/login" element={<LoginPage />} />
+          <Route
+            path="/*"
+            element={
+              <Layout>
+                <Routes>
+                  <Route path="/" element={<Navigate to="/gateway/overview" 
replace />} />
+                  <Route path="/gateway/overview" element={<OverviewPage />} />
+                  <Route path="/gateway/mapping/:resourceId" 
element={<MappingPage />} />
+                  <Route path="/gateway/route-binding" 
element={<RouteBindingPage />} />
+                  <Route path="/gateway/plugin-group" 
element={<PluginGroupPage />} />
+                  <Route path="/gateway/cluster" element={<ClusterPage />} />
+                  <Route path="/gateway/listener" element={<ListenerPage />} />
+                  <Route path="/flow/ratelimit" element={<RateLimiterPage />} 
/>
+                  <Route path="/opa" element={<OpaPage />} />
+                  <Route path="/profile" element={<ProfilePage />} />
+                  <Route path="*" element={<Navigate to="/gateway/overview" 
replace />} />
+                </Routes>
+              </Layout>
+            }

Review Comment:
   未登录访问根路径不会跳转到登录页, 明确取消了全局路由守卫;访问 `/` 会直接跳转到 `/gateway/overview`。没有 session 
时页面只显示管理布局和 API 错误,顶部仅显示“未登录”,没有登录入口。
   
      这会导致首次访问用户无法正常进入登录流程,除非手动输入 `/login`。建议恢复 `RequireAuth`,未登录统一跳转 `/login`。



-- 
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