feat: 展示集成变量说明与流程详情
This commit is contained in:
+180
-10
@@ -13,6 +13,7 @@ import type {
|
||||
IntegrationConfigKey,
|
||||
IntegrationConfigState,
|
||||
IntegrationConfigStatus,
|
||||
IntegrationVariableHelp,
|
||||
IntegrationStatus,
|
||||
JenkinsSyncSummary,
|
||||
PipelineStep,
|
||||
@@ -28,6 +29,13 @@ import { http } from './http'
|
||||
type DataSource = 'backend'
|
||||
type BackendHealthStatus = 'ok' | 'not_configured' | 'unavailable'
|
||||
type BackendRunStatus = 'pending' | 'queued' | 'running' | 'success' | 'failed' | 'canceled'
|
||||
type BackendVariableHelpValue =
|
||||
| string
|
||||
| {
|
||||
description?: unknown
|
||||
example?: unknown
|
||||
docUrl?: unknown
|
||||
}
|
||||
|
||||
interface BackendHealth {
|
||||
status: BackendHealthStatus
|
||||
@@ -99,6 +107,31 @@ interface BackendProcessDefinition {
|
||||
nodeIds: string[]
|
||||
}
|
||||
|
||||
interface BackendIntegrationConfigItem {
|
||||
key: IntegrationConfigKey
|
||||
name: string
|
||||
status: IntegrationConfigState
|
||||
required?: string[]
|
||||
configured?: string[]
|
||||
missing?: string[]
|
||||
optional?: string[]
|
||||
note?: string
|
||||
variableHelp?: Record<string, BackendVariableHelpValue>
|
||||
variableDescriptions?: Record<string, unknown>
|
||||
variables?: Array<{
|
||||
name?: unknown
|
||||
key?: unknown
|
||||
description?: unknown
|
||||
example?: unknown
|
||||
docUrl?: unknown
|
||||
}>
|
||||
}
|
||||
|
||||
interface BackendIntegrationConfigStatus {
|
||||
checkedAt?: string
|
||||
integrations?: BackendIntegrationConfigItem[]
|
||||
}
|
||||
|
||||
let lastDataSource: DataSource = 'backend'
|
||||
const integrationConfigKeys: IntegrationConfigKey[] = [
|
||||
'jenkins',
|
||||
@@ -283,6 +316,14 @@ export const devopsApi = {
|
||||
return fetchIntegrationConfigStatus()
|
||||
},
|
||||
|
||||
async getReleaseProcessXml(): Promise<string> {
|
||||
const { data } = await http.get<BackendProcessDefinition>(
|
||||
'/process-definitions/release',
|
||||
)
|
||||
lastDataSource = 'backend'
|
||||
return data.xml
|
||||
},
|
||||
|
||||
async createAgentInvocation(
|
||||
payload: CreateAgentInvocationPayload,
|
||||
): Promise<AgentInvocation> {
|
||||
@@ -300,13 +341,6 @@ export const devopsApi = {
|
||||
return toSystemSettings(health, integrationConfig)
|
||||
},
|
||||
|
||||
async getReleaseProcessXml(): Promise<string> {
|
||||
const { data } = await http.get<BackendProcessDefinition>(
|
||||
'/process-definitions/release',
|
||||
)
|
||||
lastDataSource = 'backend'
|
||||
return data.xml
|
||||
},
|
||||
}
|
||||
|
||||
async function fetchHealth(): Promise<BackendHealth> {
|
||||
@@ -325,7 +359,9 @@ async function fetchRuns(): Promise<BackendDeployRun[]> {
|
||||
}
|
||||
|
||||
async function fetchIntegrationConfigStatus(): Promise<IntegrationConfigStatus> {
|
||||
const { data } = await http.get<IntegrationConfigStatus>('/settings/integration-config')
|
||||
const { data } = await http.get<BackendIntegrationConfigStatus>(
|
||||
'/settings/integration-config',
|
||||
)
|
||||
return sanitizeIntegrationConfigStatus(data)
|
||||
}
|
||||
|
||||
@@ -358,7 +394,7 @@ function toIntegrationStatus(
|
||||
}
|
||||
|
||||
function sanitizeIntegrationConfigStatus(
|
||||
payload: IntegrationConfigStatus,
|
||||
payload: BackendIntegrationConfigStatus,
|
||||
): IntegrationConfigStatus {
|
||||
const integrations = Array.isArray(payload?.integrations) ? payload.integrations : []
|
||||
|
||||
@@ -373,6 +409,8 @@ function sanitizeIntegrationConfigStatus(
|
||||
const required = sanitizeVariableNames(item.required)
|
||||
const configured = sanitizeVariableNames(item.configured)
|
||||
const missing = sanitizeVariableNames(item.missing)
|
||||
const optional = sanitizeVariableNames(item.optional)
|
||||
const variableHelp = sanitizeVariableHelp(item)
|
||||
|
||||
return {
|
||||
key: item.key,
|
||||
@@ -381,8 +419,9 @@ function sanitizeIntegrationConfigStatus(
|
||||
required,
|
||||
configured,
|
||||
missing,
|
||||
optional: sanitizeVariableNames(item.optional),
|
||||
optional,
|
||||
note: sanitizeIntegrationNote(item.note),
|
||||
...(Object.keys(variableHelp).length > 0 ? { variableHelp } : {}),
|
||||
}
|
||||
}),
|
||||
}
|
||||
@@ -423,6 +462,137 @@ function sanitizeIntegrationNote(note?: string): string | undefined {
|
||||
.replace(/key=[^&\s]+/g, 'key=[已隐藏]')
|
||||
}
|
||||
|
||||
function sanitizeVariableHelp(
|
||||
item: BackendIntegrationConfigItem,
|
||||
): Record<string, IntegrationVariableHelp> {
|
||||
const help: Record<string, IntegrationVariableHelp> = {}
|
||||
|
||||
collectVariableHelpMap(help, item.variableHelp)
|
||||
collectVariableDescriptionMap(help, item.variableDescriptions)
|
||||
collectVariableHelpArray(help, item.variables)
|
||||
|
||||
return help
|
||||
}
|
||||
|
||||
function collectVariableHelpMap(
|
||||
target: Record<string, IntegrationVariableHelp>,
|
||||
value?: Record<string, BackendVariableHelpValue>,
|
||||
) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return
|
||||
}
|
||||
|
||||
Object.entries(value).forEach(([rawName, rawHelp]) => {
|
||||
const name = sanitizeVariableName(rawName)
|
||||
if (!name) {
|
||||
return
|
||||
}
|
||||
|
||||
const help =
|
||||
typeof rawHelp === 'string'
|
||||
? sanitizeVariableHelpValue({ description: rawHelp })
|
||||
: sanitizeVariableHelpValue(rawHelp)
|
||||
|
||||
if (help) {
|
||||
target[name] = help
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function collectVariableDescriptionMap(
|
||||
target: Record<string, IntegrationVariableHelp>,
|
||||
value?: Record<string, unknown>,
|
||||
) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return
|
||||
}
|
||||
|
||||
Object.entries(value).forEach(([rawName, rawDescription]) => {
|
||||
const name = sanitizeVariableName(rawName)
|
||||
const description = sanitizeVariableDescription(rawDescription)
|
||||
|
||||
if (name && description) {
|
||||
target[name] = { description }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function collectVariableHelpArray(
|
||||
target: Record<string, IntegrationVariableHelp>,
|
||||
value?: BackendIntegrationConfigItem['variables'],
|
||||
) {
|
||||
if (!Array.isArray(value)) {
|
||||
return
|
||||
}
|
||||
|
||||
value.forEach((item) => {
|
||||
const name = sanitizeVariableName(item.name ?? item.key)
|
||||
const help = sanitizeVariableHelpValue(item)
|
||||
|
||||
if (name && help) {
|
||||
target[name] = help
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function sanitizeVariableHelpValue(
|
||||
value?: {
|
||||
description?: unknown
|
||||
example?: unknown
|
||||
docUrl?: unknown
|
||||
},
|
||||
): IntegrationVariableHelp | undefined {
|
||||
const description = sanitizeVariableDescription(value?.description)
|
||||
|
||||
if (!description) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
description,
|
||||
...(typeof value?.example === 'string' && value.example.trim()
|
||||
? { example: sanitizeIntegrationNote(value.example.trim()) ?? value.example.trim() }
|
||||
: {}),
|
||||
...(typeof value?.docUrl === 'string' && isSafeDocUrl(value.docUrl)
|
||||
? { docUrl: value.docUrl.trim() }
|
||||
: {}),
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeVariableDescription(value: unknown): string | undefined {
|
||||
if (typeof value !== 'string' || !value.trim()) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return sanitizeIntegrationNote(value.trim())
|
||||
}
|
||||
|
||||
function sanitizeVariableName(value: unknown): string | undefined {
|
||||
if (typeof value !== 'string') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const name = value.trim()
|
||||
return /^[A-Z][A-Z0-9_]*$/.test(name) ? name : undefined
|
||||
}
|
||||
|
||||
function isSafeDocUrl(value: string): boolean {
|
||||
try {
|
||||
const url = new URL(value)
|
||||
const normalized = url.toString().toLowerCase()
|
||||
|
||||
return (
|
||||
url.protocol === 'https:' &&
|
||||
!url.searchParams.has('key') &&
|
||||
!normalized.includes('/cgi-bin/webhook/') &&
|
||||
!normalized.includes('/open-apis/bot/') &&
|
||||
!normalized.includes('/webhook/')
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function safeText(value: unknown, fallback: string): string {
|
||||
return typeof value === 'string' && value.trim() ? value : fallback
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user