docs: reorganize documentation structure
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
ef599f9465
commit
8e0448d5c2
299
apps/web-ele/src/api/fengling/gateway.ts
Normal file
299
apps/web-ele/src/api/fengling/gateway.ts
Normal file
@ -0,0 +1,299 @@
|
||||
import type { Api } from '#/api/typings';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace GatewayApi {
|
||||
const apiPrefix = '/api/gateway';
|
||||
|
||||
// ========== 租户管理 ==========
|
||||
export async function getTenantList(params?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
}) {
|
||||
return requestClient.get<Api.PaginatedResponse<GatewayApi.Tenant>>(`${apiPrefix}/tenants`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getTenantById(id: number) {
|
||||
return requestClient.get<GatewayApi.Tenant>(`${apiPrefix}/tenants/${id}`);
|
||||
}
|
||||
|
||||
export async function createTenant(data: GatewayApi.CreateTenantRequest) {
|
||||
return requestClient.post<GatewayApi.Tenant, GatewayApi.CreateTenantRequest>(
|
||||
`${apiPrefix}/tenants`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateTenant(id: number, data: GatewayApi.UpdateTenantRequest) {
|
||||
return requestClient.put<void, GatewayApi.UpdateTenantRequest>(
|
||||
`${apiPrefix}/tenants/${id}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteTenant(id: number) {
|
||||
return requestClient.delete<void>(`${apiPrefix}/tenants/${id}`);
|
||||
}
|
||||
|
||||
// ========== 路由管理 ==========
|
||||
export async function getRouteList(params?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
tenantCode?: string;
|
||||
isGlobal?: boolean;
|
||||
}) {
|
||||
return requestClient.get<Api.PaginatedResponse<GatewayApi.Route>>(`${apiPrefix}/routes`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getGlobalRoutes() {
|
||||
return requestClient.get<GatewayApi.Route[]>(`${apiPrefix}/routes/global`);
|
||||
}
|
||||
|
||||
export async function getTenantRoutes(tenantCode: string) {
|
||||
return requestClient.get<GatewayApi.Route[]>(`${apiPrefix}/routes/tenant/${tenantCode}`);
|
||||
}
|
||||
|
||||
export async function getRouteById(id: number) {
|
||||
return requestClient.get<GatewayApi.Route>(`${apiPrefix}/routes/${id}`);
|
||||
}
|
||||
|
||||
export async function createRoute(data: GatewayApi.CreateRouteRequest) {
|
||||
return requestClient.post<GatewayApi.Route, GatewayApi.CreateRouteRequest>(
|
||||
`${apiPrefix}/routes`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function batchCreateRoutes(data: GatewayApi.BatchCreateRoutesRequest) {
|
||||
return requestClient.post<{ successCount: number }, GatewayApi.BatchCreateRoutesRequest>(
|
||||
`${apiPrefix}/routes/batch`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateRoute(id: number, data: GatewayApi.CreateRouteRequest) {
|
||||
return requestClient.put<void, GatewayApi.CreateRouteRequest>(
|
||||
`${apiPrefix}/routes/${id}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteRoute(id: number) {
|
||||
return requestClient.delete<void>(`${apiPrefix}/routes/${id}`);
|
||||
}
|
||||
|
||||
// ========== 集群管理 ==========
|
||||
export async function getClusterList() {
|
||||
return requestClient.get<GatewayApi.Cluster[]>(`${apiPrefix}/clusters`);
|
||||
}
|
||||
|
||||
export async function getClusterById(clusterId: string) {
|
||||
return requestClient.get<GatewayApi.Cluster>(`${apiPrefix}/clusters/${clusterId}`);
|
||||
}
|
||||
|
||||
export async function createCluster(data: GatewayApi.CreateClusterRequest) {
|
||||
return requestClient.post<GatewayApi.Cluster, GatewayApi.CreateClusterRequest>(
|
||||
`${apiPrefix}/clusters`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateCluster(
|
||||
clusterId: string,
|
||||
data: GatewayApi.CreateClusterRequest
|
||||
) {
|
||||
return requestClient.put<void, GatewayApi.CreateClusterRequest>(
|
||||
`${apiPrefix}/clusters/${clusterId}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteCluster(clusterId: string) {
|
||||
return requestClient.delete<void>(`${apiPrefix}/clusters/${clusterId}`);
|
||||
}
|
||||
|
||||
// ========== 实例管理 ==========
|
||||
export async function getInstanceList(clusterId: string) {
|
||||
return requestClient.get<GatewayApi.Instance[]>(
|
||||
`${apiPrefix}/clusters/${clusterId}/instances`
|
||||
);
|
||||
}
|
||||
|
||||
export async function getInstanceById(id: number) {
|
||||
return requestClient.get<GatewayApi.Instance>(`${apiPrefix}/instances/${id}`);
|
||||
}
|
||||
|
||||
export async function createInstance(clusterId: string, data: GatewayApi.CreateInstanceRequest) {
|
||||
return requestClient.post<GatewayApi.Instance, GatewayApi.CreateInstanceRequest>(
|
||||
`${apiPrefix}/clusters/${clusterId}/instances`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateInstance(id: number, data: GatewayApi.CreateInstanceRequest) {
|
||||
return requestClient.put<void, GatewayApi.CreateInstanceRequest>(
|
||||
`${apiPrefix}/instances/${id}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteInstance(id: number) {
|
||||
return requestClient.delete<void>(`${apiPrefix}/instances/${id}`);
|
||||
}
|
||||
|
||||
// ========== 配置热更新 ==========
|
||||
export async function reloadConfig() {
|
||||
return requestClient.post<{ message: string }>(`${apiPrefix}/config/reload`);
|
||||
}
|
||||
|
||||
export async function getConfigStatus() {
|
||||
return requestClient.get<GatewayApi.ConfigStatus>(`${apiPrefix}/config/status`);
|
||||
}
|
||||
|
||||
export async function getVersionInfo() {
|
||||
return requestClient.get<GatewayApi.VersionInfo>(`${apiPrefix}/config/versions`);
|
||||
}
|
||||
|
||||
// ========== 统计 ==========
|
||||
export async function getOverviewStats() {
|
||||
return requestClient.get<GatewayApi.OverviewStats>(`${apiPrefix}/stats/overview`);
|
||||
}
|
||||
|
||||
// ========== 类型定义 ==========
|
||||
export interface Tenant {
|
||||
id: number;
|
||||
tenantCode: string;
|
||||
tenantName: string;
|
||||
contactName?: string;
|
||||
contactEmail?: string;
|
||||
contactPhone?: string;
|
||||
maxUsers?: number;
|
||||
status: number;
|
||||
description?: string;
|
||||
routeCount: number;
|
||||
version: number;
|
||||
createdTime: string;
|
||||
updatedTime?: string;
|
||||
}
|
||||
|
||||
export interface CreateTenantRequest {
|
||||
tenantCode: string;
|
||||
tenantName: string;
|
||||
contactName?: string;
|
||||
contactEmail?: string;
|
||||
contactPhone?: string;
|
||||
maxUsers?: number;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface UpdateTenantRequest {
|
||||
tenantName?: string;
|
||||
contactName?: string;
|
||||
contactEmail?: string;
|
||||
contactPhone?: string;
|
||||
maxUsers?: number;
|
||||
description?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface Route {
|
||||
id: number;
|
||||
tenantCode: string;
|
||||
serviceName: string;
|
||||
clusterId: string;
|
||||
pathPattern: string;
|
||||
priority: number;
|
||||
isGlobal: boolean;
|
||||
status: number;
|
||||
metadata?: Record<string, string>;
|
||||
version: number;
|
||||
createdTime: string;
|
||||
updatedTime?: string;
|
||||
}
|
||||
|
||||
export interface CreateRouteRequest {
|
||||
tenantCode?: string;
|
||||
serviceName: string;
|
||||
clusterId: string;
|
||||
pathPattern: string;
|
||||
priority?: number;
|
||||
isGlobal?: boolean;
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface BatchCreateRoutesRequest {
|
||||
routes: CreateRouteRequest[];
|
||||
}
|
||||
|
||||
export interface Cluster {
|
||||
clusterId: string;
|
||||
clusterName: string;
|
||||
description?: string;
|
||||
loadBalancingPolicy: string;
|
||||
instanceCount: number;
|
||||
healthyInstanceCount: number;
|
||||
version: number;
|
||||
createdTime: string;
|
||||
updatedTime?: string;
|
||||
instances: Instance[];
|
||||
}
|
||||
|
||||
export interface CreateClusterRequest {
|
||||
clusterId: string;
|
||||
clusterName: string;
|
||||
description?: string;
|
||||
loadBalancingPolicy?: string;
|
||||
}
|
||||
|
||||
export interface Instance {
|
||||
id: number;
|
||||
clusterId: string;
|
||||
destinationId: string;
|
||||
address: string;
|
||||
weight: number;
|
||||
health: number;
|
||||
status: number;
|
||||
version: number;
|
||||
createdTime: string;
|
||||
updatedTime?: string;
|
||||
}
|
||||
|
||||
export interface CreateInstanceRequest {
|
||||
destinationId: string;
|
||||
address: string;
|
||||
weight?: number;
|
||||
isHealthy?: boolean;
|
||||
}
|
||||
|
||||
export interface ConfigStatus {
|
||||
routeCount: number;
|
||||
clusterCount: number;
|
||||
instanceCount: number;
|
||||
healthyInstanceCount: number;
|
||||
lastReloadTime: string;
|
||||
isListening: boolean;
|
||||
listenerStatus: string;
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
routeVersion: number;
|
||||
clusterVersion: number;
|
||||
routeVersionUpdatedAt: string;
|
||||
clusterVersionUpdatedAt: string;
|
||||
}
|
||||
|
||||
export interface OverviewStats {
|
||||
totalTenants: number;
|
||||
activeTenants: number;
|
||||
totalRoutes: number;
|
||||
totalClusters: number;
|
||||
totalInstances: number;
|
||||
healthyInstances: number;
|
||||
lastUpdated: string;
|
||||
}
|
||||
}
|
||||
215
apps/web-ele/src/views/fengling/gateway/clusters.vue
Normal file
215
apps/web-ele/src/views/fengling/gateway/clusters.vue
Normal file
@ -0,0 +1,215 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
import {
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElTag,
|
||||
ElPopconfirm,
|
||||
ElMessage,
|
||||
ElCard,
|
||||
} from 'element-plus';
|
||||
import { Plus, Edit, Delete, Refresh, View } from '@element-plus/icons-vue';
|
||||
|
||||
import { GatewayApi, type GatewayApi as GApi } from '#/api/fengling/gateway';
|
||||
|
||||
const loading = ref(false);
|
||||
const tableData = ref<GApi.Cluster[]>([]);
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('Create Cluster');
|
||||
const formData = ref<Partial<GApi.CreateClusterRequest>>({});
|
||||
const selectedCluster = ref<GApi.Cluster | null>(null);
|
||||
const instanceDialogVisible = ref(false);
|
||||
|
||||
const loadClusters = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await GatewayApi.getClusterList();
|
||||
tableData.value = response || [];
|
||||
} catch (error: any) {
|
||||
console.error('[Gateway] Error loading clusters:', error);
|
||||
ElMessage.error(error?.response?.data?.message || 'Failed to load clusters');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
dialogTitle.value = 'Create Cluster';
|
||||
formData.value = {
|
||||
loadBalancingPolicy: 'DistributedWeightedRoundRobin',
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEdit = (row: GApi.Cluster) => {
|
||||
dialogTitle.value = 'Edit Cluster';
|
||||
formData.value = { ...row };
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDelete = async (clusterId: string) => {
|
||||
try {
|
||||
await GatewayApi.deleteCluster(clusterId);
|
||||
ElMessage.success('Cluster deleted successfully');
|
||||
await loadClusters();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || 'Failed to delete cluster');
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewInstances = async (row: GApi.Cluster) => {
|
||||
selectedCluster.value = row;
|
||||
instanceDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (formData.value.clusterId) {
|
||||
await GatewayApi.updateCluster(formData.value.clusterId, formData.value as GApi.CreateClusterRequest);
|
||||
ElMessage.success('Cluster updated successfully');
|
||||
} else {
|
||||
await GatewayApi.createCluster(formData.value as GApi.CreateClusterRequest);
|
||||
ElMessage.success('Cluster created successfully');
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
await loadClusters();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || 'Operation failed');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteInstance = async (id: number) => {
|
||||
try {
|
||||
await GatewayApi.deleteInstance(id);
|
||||
ElMessage.success('Instance deleted successfully');
|
||||
if (selectedCluster.value) {
|
||||
const updated = await GatewayApi.getClusterById(selectedCluster.value.clusterId);
|
||||
selectedCluster.value = updated;
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || 'Failed to delete instance');
|
||||
}
|
||||
};
|
||||
|
||||
const getHealthTag = (health: number) => {
|
||||
return health === 1 ? 'success' : 'danger';
|
||||
};
|
||||
|
||||
const getHealthText = (health: number) => {
|
||||
return health === 1 ? 'Healthy' : 'Unhealthy';
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadClusters();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gateway-clusters">
|
||||
<div class="toolbar">
|
||||
<ElButton type="primary" :icon="Plus" @click="handleCreate">Create Cluster</ElButton>
|
||||
<ElButton :icon="Refresh" @click="loadClusters">Refresh</ElButton>
|
||||
</div>
|
||||
|
||||
<ElTable :data="tableData" v-loading="loading" style="width: 100%">
|
||||
<ElTableColumn prop="clusterId" label="Cluster ID" width="200" />
|
||||
<ElTableColumn prop="clusterName" label="Name" width="200" />
|
||||
<ElTableColumn prop="loadBalancingPolicy" label="Load Balancing" width="220" />
|
||||
<ElTableColumn prop="instanceCount" label="Instances" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.instanceCount }} ({{ row.healthyInstanceCount }} healthy)
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="description" label="Description" min-width="200" />
|
||||
<ElTableColumn label="Actions" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<ElButton type="primary" link :icon="View" @click="handleViewInstances(row)">Instances</ElButton>
|
||||
<ElButton type="primary" link :icon="Edit" @click="handleEdit(row)">Edit</ElButton>
|
||||
<ElPopconfirm title="Are you sure to delete this cluster?" @confirm="handleDelete(row.clusterId)">
|
||||
<template #reference>
|
||||
<ElButton type="danger" link :icon="Delete">Delete</ElButton>
|
||||
</template>
|
||||
</ElPopconfirm>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<ElDialog v-model="dialogVisible" :title="dialogTitle" width="500px">
|
||||
<ElForm :model="formData" label-width="120px">
|
||||
<ElFormItem label="Cluster ID" required>
|
||||
<ElInput v-model="formData.clusterId" :disabled="!!formData.clusterId" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="Cluster Name" required>
|
||||
<ElInput v-model="formData.clusterName" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="Load Balancing">
|
||||
<ElInput v-model="formData.loadBalancingPolicy" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="Description">
|
||||
<ElInput v-model="formData.description" type="textarea" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">Cancel</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">Submit</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<ElDialog v-model="instanceDialogVisible" title="Service Instances" width="800px">
|
||||
<ElCard v-if="selectedCluster" class="cluster-info">
|
||||
<h3>{{ selectedCluster.clusterName }}</h3>
|
||||
<p>Cluster ID: {{ selectedCluster.clusterId }}</p>
|
||||
</ElCard>
|
||||
|
||||
<ElTable :data="selectedCluster?.instances || []" style="width: 100%">
|
||||
<ElTableColumn prop="destinationId" label="Destination ID" width="150" />
|
||||
<ElTableColumn prop="address" label="Address" min-width="200" />
|
||||
<ElTableColumn prop="weight" label="Weight" width="100" />
|
||||
<ElTableColumn prop="health" label="Health" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="getHealthTag(row.health)">
|
||||
{{ getHealthText(row.health) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="status" label="Status" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.status === 1 ? 'success' : 'danger'">
|
||||
{{ row.status === 1 ? 'Active' : 'Inactive' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="Actions" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElPopconfirm title="Delete this instance?" @confirm="handleDeleteInstance(row.id)">
|
||||
<template #reference>
|
||||
<ElButton type="danger" link :icon="Delete">Delete</ElButton>
|
||||
</template>
|
||||
</ElPopconfirm>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.gateway-clusters {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.cluster-info {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
149
apps/web-ele/src/views/fengling/gateway/index.vue
Normal file
149
apps/web-ele/src/views/fengling/gateway/index.vue
Normal file
@ -0,0 +1,149 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
import { ElRow, ElCol, ElCard, ElStatistic, ElButton, ElTag, ElMessage } from 'element-plus';
|
||||
import { Refresh } from '@element-plus/icons-vue';
|
||||
|
||||
import { GatewayApi } from '#/api/fengling/gateway';
|
||||
|
||||
const loading = ref(false);
|
||||
const stats = ref<GatewayApi.OverviewStats | null>(null);
|
||||
const configStatus = ref<GatewayApi.ConfigStatus | null>(null);
|
||||
|
||||
const loadStats = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [statsRes, statusRes] = await Promise.all([
|
||||
GatewayApi.getOverviewStats(),
|
||||
GatewayApi.getConfigStatus(),
|
||||
]);
|
||||
stats.value = statsRes;
|
||||
configStatus.value = statusRes;
|
||||
} catch (error: any) {
|
||||
console.error('[Gateway] Error loading stats:', error);
|
||||
ElMessage.error(error?.response?.data?.message || 'Failed to load gateway stats');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleReload = async () => {
|
||||
try {
|
||||
await GatewayApi.reloadConfig();
|
||||
ElMessage.success('Configuration reloaded successfully');
|
||||
await loadStats();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || 'Failed to reload config');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadStats();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gateway-dashboard">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="24">
|
||||
<ElCard class="header-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>Gateway Overview</span>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:icon="Refresh"
|
||||
:loading="loading"
|
||||
@click="handleReload"
|
||||
>
|
||||
Reload Config
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="20" class="stats-row">
|
||||
<ElCol :span="6">
|
||||
<ElCard shadow="hover">
|
||||
<ElStatistic title="Total Tenants" :value="stats?.totalTenants ?? 0" />
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<ElCard shadow="hover">
|
||||
<ElStatistic title="Active Tenants" :value="stats?.activeTenants ?? 0" />
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<ElCard shadow="hover">
|
||||
<ElStatistic title="Total Routes" :value="stats?.totalRoutes ?? 0" />
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<ElCard shadow="hover">
|
||||
<ElStatistic title="Total Clusters" :value="stats?.totalClusters ?? 0" />
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="20" class="stats-row">
|
||||
<ElCol :span="6">
|
||||
<ElCard shadow="hover">
|
||||
<ElStatistic title="Total Instances" :value="stats?.totalInstances ?? 0" />
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<ElCard shadow="hover">
|
||||
<ElStatistic title="Healthy Instances" :value="stats?.healthyInstances ?? 0" />
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<ElCard shadow="hover">
|
||||
<ElStatistic title="Route Version" :value="configStatus?.routeCount ?? 0" />
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<ElCard shadow="hover">
|
||||
<template #title>
|
||||
<span>Listener Status</span>
|
||||
</template>
|
||||
<ElTag :type="configStatus?.isListening ? 'success' : 'danger'" size="large">
|
||||
{{ configStatus?.isListening ? 'Active' : 'Inactive' }}
|
||||
</ElTag>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="20" class="stats-row">
|
||||
<ElCol :span="24">
|
||||
<ElCard>
|
||||
<template #header>
|
||||
<span>Last Updated</span>
|
||||
</template>
|
||||
<p>{{ stats?.lastUpdated ?? 'N/A' }}</p>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.gateway-dashboard {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
224
apps/web-ele/src/views/fengling/gateway/routes.vue
Normal file
224
apps/web-ele/src/views/fengling/gateway/routes.vue
Normal file
@ -0,0 +1,224 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
import {
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElTag,
|
||||
ElPopconfirm,
|
||||
ElPagination,
|
||||
ElMessage,
|
||||
ElSwitch,
|
||||
} from 'element-plus';
|
||||
import { Plus, Edit, Delete, Refresh } from '@element-plus/icons-vue';
|
||||
|
||||
import { GatewayApi, type GatewayApi as GApi } from '#/api/fengling/gateway';
|
||||
|
||||
const loading = ref(false);
|
||||
const tableData = ref<GApi.Route[]>([]);
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('Create Route');
|
||||
const formData = ref<Partial<GApi.CreateRouteRequest>>({});
|
||||
const editingId = ref<number | null>(null);
|
||||
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const isGlobalOptions = [
|
||||
{ label: 'Yes', value: true },
|
||||
{ label: 'No', value: false },
|
||||
];
|
||||
|
||||
const loadRoutes = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await GatewayApi.getRouteList({
|
||||
page: pagination.value.page,
|
||||
pageSize: pagination.value.pageSize,
|
||||
});
|
||||
tableData.value = response.items || [];
|
||||
pagination.value.total = response.totalCount || 0;
|
||||
} catch (error: any) {
|
||||
console.error('[Gateway] Error loading routes:', error);
|
||||
ElMessage.error(error?.response?.data?.message || 'Failed to load routes');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
dialogTitle.value = 'Create Route';
|
||||
editingId.value = null;
|
||||
formData.value = {
|
||||
isGlobal: false,
|
||||
priority: 10,
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEdit = (row: GApi.Route) => {
|
||||
dialogTitle.value = 'Edit Route';
|
||||
editingId.value = row.id;
|
||||
formData.value = {
|
||||
tenantCode: row.tenantCode,
|
||||
serviceName: row.serviceName,
|
||||
clusterId: row.clusterId,
|
||||
pathPattern: row.pathPattern,
|
||||
priority: row.priority,
|
||||
isGlobal: row.isGlobal,
|
||||
metadata: row.metadata,
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await GatewayApi.deleteRoute(id);
|
||||
ElMessage.success('Route deleted successfully');
|
||||
await loadRoutes();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || 'Failed to delete route');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (editingId.value) {
|
||||
await GatewayApi.updateRoute(editingId.value, formData.value as GApi.CreateRouteRequest);
|
||||
ElMessage.success('Route updated successfully');
|
||||
} else {
|
||||
await GatewayApi.createRoute(formData.value as GApi.CreateRouteRequest);
|
||||
ElMessage.success('Route created successfully');
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
await loadRoutes();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || 'Operation failed');
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
pagination.value.page = page;
|
||||
loadRoutes();
|
||||
};
|
||||
|
||||
const getStatusTag = (status: number) => {
|
||||
return status === 1 ? 'success' : 'danger';
|
||||
};
|
||||
|
||||
const getStatusText = (status: number) => {
|
||||
return status === 1 ? 'Active' : 'Inactive';
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadRoutes();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gateway-routes">
|
||||
<div class="toolbar">
|
||||
<ElButton type="primary" :icon="Plus" @click="handleCreate">Create Route</ElButton>
|
||||
<ElButton :icon="Refresh" @click="loadRoutes">Refresh</ElButton>
|
||||
</div>
|
||||
|
||||
<ElTable :data="tableData" v-loading="loading" style="width: 100%">
|
||||
<ElTableColumn prop="id" label="ID" width="80" />
|
||||
<ElTableColumn prop="tenantCode" label="Tenant" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.isGlobal ? '-' : row.tenantCode }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="serviceName" label="Service" width="150" />
|
||||
<ElTableColumn prop="clusterId" label="Cluster" width="150" />
|
||||
<ElTableColumn prop="pathPattern" label="Path Pattern" min-width="200" />
|
||||
<ElTableColumn prop="priority" label="Priority" width="100" />
|
||||
<ElTableColumn prop="isGlobal" label="Global" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.isGlobal ? 'warning' : 'info'">
|
||||
{{ row.isGlobal ? 'Yes' : 'No' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="status" label="Status" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="getStatusTag(row.status)">
|
||||
{{ getStatusText(row.status) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="Actions" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<ElButton type="primary" link :icon="Edit" @click="handleEdit(row)">Edit</ElButton>
|
||||
<ElPopconfirm title="Are you sure to delete this route?" @confirm="handleDelete(row.id)">
|
||||
<template #reference>
|
||||
<ElButton type="danger" link :icon="Delete">Delete</ElButton>
|
||||
</template>
|
||||
</ElPopconfirm>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<ElPagination
|
||||
class="pagination"
|
||||
v-model:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
|
||||
<ElDialog v-model="dialogVisible" :title="dialogTitle" width="600px">
|
||||
<ElForm :model="formData" label-width="120px">
|
||||
<ElFormItem label="Tenant Code">
|
||||
<ElInput v-model="formData.tenantCode" placeholder="Leave empty for global routes" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="Service Name">
|
||||
<ElInput v-model="formData.serviceName" required />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="Cluster ID">
|
||||
<ElInput v-model="formData.clusterId" required />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="Path Pattern">
|
||||
<ElInput v-model="formData.pathPattern" placeholder="/api/{**path}" required />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="Priority">
|
||||
<ElInputNumber v-model="formData.priority" :min="0" :max="100" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="Is Global">
|
||||
<ElSwitch v-model="formData.isGlobal" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">Cancel</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">Submit</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.gateway-routes {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
20884
pnpm-lock.yaml
Normal file
20884
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user