在准备环境前提交次全部更改。
This commit is contained in:
159
apps/admin-web/src/api/client.ts
Normal file
159
apps/admin-web/src/api/client.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* axios 实例 & JWT 拦截器。
|
||||
*
|
||||
* - 请求拦截器:自动从 localStorage 读取 access_token 并附加 Authorization header
|
||||
* - 响应拦截器:遇到 401 时尝试用 refresh_token 刷新,刷新失败则清除令牌并跳转 /login
|
||||
* - 并发刷新保护:多个请求同时 401 时只触发一次 refresh,其余排队等待
|
||||
*/
|
||||
|
||||
import axios, {
|
||||
type AxiosError,
|
||||
type AxiosRequestConfig,
|
||||
type InternalAxiosRequestConfig,
|
||||
} from "axios";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 常量 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const ACCESS_TOKEN_KEY = "access_token";
|
||||
const REFRESH_TOKEN_KEY = "refresh_token";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* axios 实例 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export const apiClient = axios.create({
|
||||
baseURL: "/api",
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 请求拦截器 — 附加 JWT */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
apiClient.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||
const token = localStorage.getItem(ACCESS_TOKEN_KEY);
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 响应拦截器 — 401 自动刷新 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/** 是否正在刷新中 */
|
||||
let isRefreshing = false;
|
||||
|
||||
/** 等待刷新完成的请求队列 */
|
||||
let pendingQueue: {
|
||||
resolve: (token: string) => void;
|
||||
reject: (err: unknown) => void;
|
||||
}[] = [];
|
||||
|
||||
/** 刷新完成后,依次重放排队的请求 */
|
||||
function processPendingQueue(token: string | null, error: unknown) {
|
||||
pendingQueue.forEach(({ resolve, reject }) => {
|
||||
if (token) {
|
||||
resolve(token);
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
pendingQueue = [];
|
||||
}
|
||||
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error: AxiosError) => {
|
||||
const originalRequest = error.config as AxiosRequestConfig & {
|
||||
_retried?: boolean;
|
||||
};
|
||||
|
||||
// 非 401、无原始请求、或已重试过 → 直接抛出
|
||||
if (
|
||||
error.response?.status !== 401 ||
|
||||
!originalRequest ||
|
||||
originalRequest._retried
|
||||
) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// 刷新端点本身 401 → 不再递归刷新
|
||||
if (originalRequest.url === "/auth/refresh") {
|
||||
clearTokensAndRedirect();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// 已有刷新请求在飞 → 排队等待
|
||||
if (isRefreshing) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
pendingQueue.push({ resolve, reject });
|
||||
}).then((newToken) => {
|
||||
originalRequest.headers = {
|
||||
...originalRequest.headers,
|
||||
Authorization: `Bearer ${newToken}`,
|
||||
};
|
||||
originalRequest._retried = true;
|
||||
return apiClient(originalRequest);
|
||||
});
|
||||
}
|
||||
|
||||
// 发起刷新
|
||||
isRefreshing = true;
|
||||
originalRequest._retried = true;
|
||||
|
||||
const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY);
|
||||
if (!refreshToken) {
|
||||
isRefreshing = false;
|
||||
processPendingQueue(null, error);
|
||||
clearTokensAndRedirect();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
try {
|
||||
// 用独立 axios 调用避免被自身拦截器干扰
|
||||
const { data } = await axios.post<{
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
}>("/api/auth/refresh", { refresh_token: refreshToken });
|
||||
|
||||
localStorage.setItem(ACCESS_TOKEN_KEY, data.access_token);
|
||||
localStorage.setItem(REFRESH_TOKEN_KEY, data.refresh_token);
|
||||
|
||||
processPendingQueue(data.access_token, null);
|
||||
|
||||
// 重放原始请求
|
||||
originalRequest.headers = {
|
||||
...originalRequest.headers,
|
||||
Authorization: `Bearer ${data.access_token}`,
|
||||
};
|
||||
return apiClient(originalRequest);
|
||||
} catch (refreshError) {
|
||||
processPendingQueue(null, refreshError);
|
||||
clearTokensAndRedirect();
|
||||
return Promise.reject(refreshError);
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 辅助 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function clearTokensAndRedirect() {
|
||||
localStorage.removeItem(ACCESS_TOKEN_KEY);
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||
|
||||
// 派发自定义事件,让 authStore 监听并重置状态
|
||||
// 避免直接 import authStore 导致循环依赖
|
||||
window.dispatchEvent(new Event("auth:force-logout"));
|
||||
|
||||
// 避免在登录页反复跳转
|
||||
if (window.location.pathname !== "/login") {
|
||||
window.location.href = "/login";
|
||||
}
|
||||
}
|
||||
59
apps/admin-web/src/api/dbViewer.ts
Normal file
59
apps/admin-web/src/api/dbViewer.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 数据库查看器相关 API 调用。
|
||||
*
|
||||
* - fetchSchemas:获取 Schema 列表
|
||||
* - fetchTables:获取指定 Schema 下的表列表(含行数)
|
||||
* - fetchColumns:获取指定表的列定义
|
||||
* - executeQuery:执行只读 SQL 查询
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
|
||||
/** 表信息 */
|
||||
export interface TableInfo {
|
||||
name: string;
|
||||
row_count: number;
|
||||
}
|
||||
|
||||
/** 列定义 */
|
||||
export interface ColumnInfo {
|
||||
name: string;
|
||||
data_type: string;
|
||||
is_nullable: boolean;
|
||||
default_value: string | null;
|
||||
}
|
||||
|
||||
/** 查询结果 */
|
||||
export interface QueryResult {
|
||||
columns: string[];
|
||||
rows: unknown[][];
|
||||
row_count: number;
|
||||
}
|
||||
|
||||
/** 获取所有 Schema */
|
||||
export async function fetchSchemas(): Promise<string[]> {
|
||||
const { data } = await apiClient.get<{ schemas: string[] }>('/db/schemas');
|
||||
return data.schemas;
|
||||
}
|
||||
|
||||
/** 获取指定 Schema 下的表列表 */
|
||||
export async function fetchTables(schema: string): Promise<TableInfo[]> {
|
||||
const { data } = await apiClient.get<{ tables: TableInfo[] }>(
|
||||
`/db/schemas/${encodeURIComponent(schema)}/tables`,
|
||||
);
|
||||
return data.tables;
|
||||
}
|
||||
|
||||
/** 获取指定表的列定义 */
|
||||
export async function fetchColumns(schema: string, table: string): Promise<ColumnInfo[]> {
|
||||
const { data } = await apiClient.get<{ columns: ColumnInfo[] }>(
|
||||
`/db/tables/${encodeURIComponent(schema)}/${encodeURIComponent(table)}/columns`,
|
||||
);
|
||||
return data.columns;
|
||||
}
|
||||
|
||||
/** 执行只读 SQL 查询 */
|
||||
export async function executeQuery(sql: string): Promise<QueryResult> {
|
||||
const { data } = await apiClient.post<QueryResult>('/db/query', { sql });
|
||||
return data;
|
||||
}
|
||||
44
apps/admin-web/src/api/envConfig.ts
Normal file
44
apps/admin-web/src/api/envConfig.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 环境配置相关 API 调用。
|
||||
*
|
||||
* - fetchEnvConfig:获取键值对列表(敏感值已掩码)
|
||||
* - updateEnvConfig:批量更新键值对
|
||||
* - exportEnvConfig:导出去敏感值的配置文件(浏览器下载)
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
import type { EnvConfigItem } from '../types';
|
||||
|
||||
/** 获取环境配置列表 */
|
||||
export async function fetchEnvConfig(): Promise<EnvConfigItem[]> {
|
||||
const { data } = await apiClient.get<{ items: EnvConfigItem[] }>('/env-config');
|
||||
return data.items;
|
||||
}
|
||||
|
||||
/** 批量更新环境配置 */
|
||||
export async function updateEnvConfig(items: EnvConfigItem[]): Promise<void> {
|
||||
await apiClient.put('/env-config', { items });
|
||||
}
|
||||
|
||||
/** 导出配置文件(去敏感值),触发浏览器下载 */
|
||||
export async function exportEnvConfig(): Promise<void> {
|
||||
const response = await apiClient.get('/env-config/export', {
|
||||
responseType: 'blob',
|
||||
});
|
||||
// 从响应头提取文件名,回退默认值
|
||||
const disposition = response.headers['content-disposition'] as string | undefined;
|
||||
let filename = 'env-config.txt';
|
||||
if (disposition) {
|
||||
const match = disposition.match(/filename="?([^";\s]+)"?/);
|
||||
if (match) filename = match[1];
|
||||
}
|
||||
// 创建临时链接触发下载
|
||||
const url = URL.createObjectURL(response.data as Blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
38
apps/admin-web/src/api/etlStatus.ts
Normal file
38
apps/admin-web/src/api/etlStatus.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* ETL 状态监控 API 调用。
|
||||
*
|
||||
* - fetchCursors:获取各任务的数据游标(最后抓取时间、记录数)
|
||||
* - fetchRecentRuns:获取最近执行记录
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
|
||||
/** ETL 游标信息 */
|
||||
export interface CursorInfo {
|
||||
task_code: string;
|
||||
last_fetch_time: string | null;
|
||||
record_count: number | null;
|
||||
}
|
||||
|
||||
/** 最近执行记录 */
|
||||
export interface RecentRun {
|
||||
id: string;
|
||||
task_codes: string[];
|
||||
status: string;
|
||||
started_at: string;
|
||||
finished_at: string | null;
|
||||
duration_ms: number | null;
|
||||
exit_code: number | null;
|
||||
}
|
||||
|
||||
/** 获取各任务的数据游标 */
|
||||
export async function fetchCursors(): Promise<CursorInfo[]> {
|
||||
const { data } = await apiClient.get<CursorInfo[]>('/etl-status/cursors');
|
||||
return data;
|
||||
}
|
||||
|
||||
/** 获取最近执行记录 */
|
||||
export async function fetchRecentRuns(): Promise<RecentRun[]> {
|
||||
const { data } = await apiClient.get<RecentRun[]>('/etl-status/recent-runs');
|
||||
return data;
|
||||
}
|
||||
47
apps/admin-web/src/api/execution.ts
Normal file
47
apps/admin-web/src/api/execution.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 任务执行相关 API 调用。
|
||||
*
|
||||
* - submitToQueue:提交任务配置到执行队列
|
||||
* - executeDirectly:直接执行任务
|
||||
* - fetchQueue:获取当前队列
|
||||
* - fetchHistory:获取执行历史
|
||||
* - deleteFromQueue:从队列删除任务
|
||||
* - cancelExecution:取消执行中的任务
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
import type { TaskConfig, QueuedTask, ExecutionLog } from '../types';
|
||||
|
||||
/** 提交任务配置到执行队列 */
|
||||
export async function submitToQueue(config: TaskConfig): Promise<{ id: string }> {
|
||||
const { data } = await apiClient.post<{ id: string }>('/execution/queue', config);
|
||||
return data;
|
||||
}
|
||||
|
||||
/** 直接执行任务(不经过队列) */
|
||||
export async function executeDirectly(config: TaskConfig): Promise<{ execution_id: string }> {
|
||||
const { data } = await apiClient.post<{ execution_id: string }>('/execution/run', config);
|
||||
return data;
|
||||
}
|
||||
|
||||
/** 获取当前任务队列 */
|
||||
export async function fetchQueue(): Promise<QueuedTask[]> {
|
||||
const { data } = await apiClient.get<QueuedTask[]>('/execution/queue');
|
||||
return data;
|
||||
}
|
||||
|
||||
/** 获取执行历史记录 */
|
||||
export async function fetchHistory(limit = 50): Promise<ExecutionLog[]> {
|
||||
const { data } = await apiClient.get<ExecutionLog[]>('/execution/history', { params: { limit } });
|
||||
return data;
|
||||
}
|
||||
|
||||
/** 从队列中删除待执行任务 */
|
||||
export async function deleteFromQueue(id: string): Promise<void> {
|
||||
await apiClient.delete(`/execution/queue/${id}`);
|
||||
}
|
||||
|
||||
/** 取消执行中的任务 */
|
||||
export async function cancelExecution(id: string): Promise<void> {
|
||||
await apiClient.post(`/execution/${id}/cancel`);
|
||||
}
|
||||
48
apps/admin-web/src/api/schedules.ts
Normal file
48
apps/admin-web/src/api/schedules.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 调度任务相关 API 调用。
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
import type { ScheduledTask, ScheduleConfig, TaskConfig } from '../types';
|
||||
|
||||
/** 获取调度任务列表 */
|
||||
export async function fetchSchedules(): Promise<ScheduledTask[]> {
|
||||
const { data } = await apiClient.get<ScheduledTask[]>('/schedules');
|
||||
return data;
|
||||
}
|
||||
|
||||
/** 创建调度任务 */
|
||||
export async function createSchedule(payload: {
|
||||
name: string;
|
||||
task_codes: string[];
|
||||
task_config: TaskConfig;
|
||||
schedule_config: ScheduleConfig;
|
||||
}): Promise<ScheduledTask> {
|
||||
const { data } = await apiClient.post<ScheduledTask>('/schedules', payload);
|
||||
return data;
|
||||
}
|
||||
|
||||
/** 更新调度任务 */
|
||||
export async function updateSchedule(
|
||||
id: string,
|
||||
payload: Partial<{
|
||||
name: string;
|
||||
task_codes: string[];
|
||||
task_config: TaskConfig;
|
||||
schedule_config: ScheduleConfig;
|
||||
}>,
|
||||
): Promise<ScheduledTask> {
|
||||
const { data } = await apiClient.put<ScheduledTask>(`/schedules/${id}`, payload);
|
||||
return data;
|
||||
}
|
||||
|
||||
/** 删除调度任务 */
|
||||
export async function deleteSchedule(id: string): Promise<void> {
|
||||
await apiClient.delete(`/schedules/${id}`);
|
||||
}
|
||||
|
||||
/** 启用/禁用调度任务 */
|
||||
export async function toggleSchedule(id: string): Promise<ScheduledTask> {
|
||||
const { data } = await apiClient.patch<ScheduledTask>(`/schedules/${id}/toggle`);
|
||||
return data;
|
||||
}
|
||||
32
apps/admin-web/src/api/tasks.ts
Normal file
32
apps/admin-web/src/api/tasks.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 任务相关 API 调用。
|
||||
*
|
||||
* - fetchTaskRegistry:获取按业务域分组的任务注册表
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
import type { TaskConfig, TaskDefinition } from '../types';
|
||||
|
||||
/** 获取按业务域分组的任务注册表 */
|
||||
export async function fetchTaskRegistry(): Promise<Record<string, TaskDefinition[]>> {
|
||||
// 后端返回 { groups: { 域名: [TaskItem] } },需要解包
|
||||
const { data } = await apiClient.get<{ groups: Record<string, TaskDefinition[]> }>('/tasks/registry');
|
||||
return data.groups;
|
||||
}
|
||||
|
||||
/** 获取按业务域分组的 DWD 表定义 */
|
||||
export async function fetchDwdTables(): Promise<Record<string, string[]>> {
|
||||
// 后端返回 { groups: { 域名: [DwdTableItem] } },需要解包并提取 table_name
|
||||
const { data } = await apiClient.get<{ groups: Record<string, { table_name: string }[]> }>('/tasks/dwd-tables');
|
||||
const result: Record<string, string[]> = {};
|
||||
for (const [domain, items] of Object.entries(data.groups)) {
|
||||
result[domain] = items.map((item) => item.table_name);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 验证任务配置并返回生成的 CLI 命令预览 */
|
||||
export async function validateTaskConfig(config: TaskConfig): Promise<{ command: string }> {
|
||||
const { data } = await apiClient.post<{ command: string }>('/tasks/validate', { config });
|
||||
return data;
|
||||
}
|
||||
Reference in New Issue
Block a user