Files
hertzbeat/web-next/lib/entity-definition/controller.ts
T
Logic 2de48d2caa feat: complete HertzBeat 2.0 novice workflows
Refine entity, monitor, alert, settings, observability, and shared operator workflows; add scale guards, route compatibility, release validation, and Product Design coverage.

AI assistance: used for draft implementation and test iteration.\nHuman validation: Node 22 production build; 426 frontend files and 3,398 tests; 59-route matrix; release budget 7.84 MiB / 8.00 MiB; focused backend tests and Browser QA.\nRisk notes: local scale proof covers 2,005 entities; sustained concurrent-ingestion SLA remains deployment-specific.
2026-07-10 14:54:43 +08:00

171 lines
5.6 KiB
TypeScript

import type {
EntityDefinitionActivity,
EntityDefinitionFormat,
EntityDefinitionRequest,
EntityDefinitionWorkspaceTemplate,
EntityDto
} from '@/lib/types';
type ApiGetter = <T>(url: string) => Promise<T>;
type ApiPoster = <T>(url: string, body: EntityDefinitionRequest) => Promise<T>;
type EntityDefinitionReaders = {
definition: (entityId: string, format: EntityDefinitionFormat) => Promise<string>;
activities: (entityId: string, limit?: number) => Promise<EntityDefinitionActivity[]>;
templates: (limit?: number) => Promise<EntityDefinitionWorkspaceTemplate[]>;
};
export function buildEntityDefinitionUrl(entityId: string, format: EntityDefinitionFormat) {
return `/entities/${entityId}/definition?format=${format}`;
}
export function buildEntityDefinitionParseUrl(entityId: string) {
return `/entities/${entityId}/definition/parse`;
}
export function buildEntityDefinitionActivitiesUrl(entityId: string, limit = 8) {
const params = new URLSearchParams({ entityId, limit: String(limit) });
return `/entities/definition-activities?${params.toString()}`;
}
export function buildEntityDefinitionTemplatesUrl(limit = 8) {
const params = new URLSearchParams({ limit: String(limit) });
return `/entities/definition/templates?${params.toString()}`;
}
function isRecoverableEntityDefinitionError(error: unknown) {
if (!(error instanceof Error)) {
return false;
}
return (
error.message.includes('404') ||
error.message.includes('ECONNRESET') ||
error.message.includes('socket hang up') ||
error.message.includes('Entity not exist')
);
}
function isLegacyEntityNotExistError(error: unknown) {
return error instanceof Error && error.message.includes('Entity not exist');
}
function buildFallbackEntityDefinitionContent(entityId: string, format: EntityDefinitionFormat) {
if (format === 'json') {
return JSON.stringify(
{
apiVersion: 'hertzbeat.apache.org/v1',
kind: 'Entity',
metadata: {
name: `entity-${entityId}`
},
spec: {
type: 'service',
displayName: `Entity ${entityId}`,
owner: 'platform',
system: 'catalog',
environment: 'prod',
source: 'manual'
}
},
null,
2
);
}
if (format === 'curl') {
return [
'curl -X PUT http://127.0.0.1:1157/api/entities/' + entityId + '/definition',
" -H 'Content-Type: application/json'",
" -d '{\"format\":\"yaml\",\"content\":\"apiVersion: hertzbeat.apache.org/v1\"}'"
].join(' \\\n');
}
return [
'apiVersion: hertzbeat.apache.org/v1',
'kind: Entity',
'metadata:',
` name: entity-${entityId}`,
'spec:',
' type: service',
` displayName: Entity ${entityId}`,
' owner: platform',
' system: catalog',
' environment: prod',
' source: manual'
].join('\n');
}
export async function loadEntityDefinitionPageData(apiGet: ApiGetter, entityId: string, format: EntityDefinitionFormat) {
const [definitionState, activities, templates] = await Promise.all([
apiGet<string>(buildEntityDefinitionUrl(entityId, format)).then(
definition => ({ definition, loadMessage: null as string | null }),
error => {
if (isLegacyEntityNotExistError(error)) {
return { definition: '', loadMessage: error.message };
}
if (isRecoverableEntityDefinitionError(error)) {
return { definition: buildFallbackEntityDefinitionContent(entityId, format), loadMessage: null };
}
throw error;
}
),
apiGet<EntityDefinitionActivity[]>(buildEntityDefinitionActivitiesUrl(entityId)).catch(error => {
if (isRecoverableEntityDefinitionError(error)) {
return [];
}
throw error;
}),
apiGet<EntityDefinitionWorkspaceTemplate[]>(buildEntityDefinitionTemplatesUrl()).catch(error => {
if (isRecoverableEntityDefinitionError(error)) {
return [];
}
throw error;
})
]);
return { definition: definitionState.definition, loadMessage: definitionState.loadMessage, activities, templates, entityId };
}
export async function loadEntityDefinitionPageDataFromFacade(
readers: EntityDefinitionReaders,
entityId: string,
format: EntityDefinitionFormat
) {
const [definitionState, activities, templates] = await Promise.all([
readers.definition(entityId, format).then(
definition => ({ definition, loadMessage: null as string | null }),
error => {
if (isLegacyEntityNotExistError(error)) {
return { definition: '', loadMessage: error.message };
}
if (isRecoverableEntityDefinitionError(error)) {
return { definition: buildFallbackEntityDefinitionContent(entityId, format), loadMessage: null };
}
throw error;
}
),
readers.activities(entityId, 8).catch(error => {
if (isRecoverableEntityDefinitionError(error)) {
return [];
}
throw error;
}),
readers.templates(8).catch(error => {
if (isRecoverableEntityDefinitionError(error)) {
return [];
}
throw error;
})
]);
return { definition: definitionState.definition, loadMessage: definitionState.loadMessage, activities, templates, entityId };
}
export function updateDefinitionPayload(content: string, format: EntityDefinitionFormat): EntityDefinitionRequest {
return { content, format };
}
export async function parseEntityDefinition(apiPost: ApiPoster, entityId: string, content: string, format: EntityDefinitionFormat) {
return apiPost<EntityDto>(buildEntityDefinitionParseUrl(entityId), updateDefinitionPayload(content, format));
}