UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

378 lines (333 loc) 10.1 kB
/** * Copyright 2025 The Headlamp Authors. * * Licensed 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 { StatusLabelProps } from '@kinvolk/headlamp-plugin/lib/components/common'; import { formatDuration } from '@kinvolk/headlamp-plugin/lib/Utils'; import { KubeflowResourceCondition } from '../../resources/common'; /** * Minimal status-bearing shape used by Pipeline and PipelineVersion UI helpers. */ export interface PipelineStatusResource { status?: { phase?: string; state?: string; conditions?: KubeflowResourceCondition[]; }; } /** * Minimal Run resource shape used for run-specific helpers. */ export interface PipelineRunResource { spec?: { runtimeConfig?: { pipelineRoot?: string }; pipelineRoot?: string; }; status?: { startTime?: string; completionTime?: string; lastTransitionTime?: string; }; } /** * Minimal RecurringRun resource shape used for schedule helpers. */ export type PipelineCronSchedule = string | { cron?: string }; export interface PipelineRecurringRunResource { spec?: { trigger?: { cronSchedule?: PipelineCronSchedule; intervalSecond?: number }; cronSchedule?: PipelineCronSchedule; intervalSecond?: number; }; } /** * Minimal PipelineVersion reference shape used for version counting. */ export interface PipelineVersionReference { metadata?: { name?: string; namespace?: string; creationTimestamp?: string; }; pipelineName?: string; } /** * UI metadata derived from a Kubeflow Pipelines family resource. */ export interface PipelineResourceStatus { label: string; status: StatusLabelProps['status']; icon: string; reason?: string; } function getStatusVariant(value: string): StatusLabelProps['status'] { const normalizedValue = value.toLowerCase(); if ( normalizedValue.includes('failed') || normalizedValue.includes('error') || normalizedValue.includes('invalid') ) { return 'error'; } if ( normalizedValue.includes('ready') || normalizedValue.includes('running') || normalizedValue.includes('succeeded') || normalizedValue.includes('available') || /\bcomplete(?:d)?\b/.test(normalizedValue) ) { return 'success'; } return ''; } function getStatusIcon(status: StatusLabelProps['status']): string { if (status === 'error') { return 'mdi:alert-circle'; } if (status === 'success') { return 'mdi:check-circle'; } return 'mdi:clock-outline'; } /** * Derives a consistent user-facing status badge for Pipeline and PipelineVersion resources. */ export function getPipelineResourceStatus( resource: PipelineStatusResource | null | undefined ): PipelineResourceStatus { const conditions = resource?.status?.conditions ?? []; const phase = resource?.status?.phase ?? resource?.status?.state ?? ''; const failedCondition = conditions.find(condition => { return condition.type === 'Failed' && condition.status === 'True'; }); if (failedCondition) { return { label: 'Failed', status: 'error', icon: 'mdi:alert-circle', reason: failedCondition.reason ?? failedCondition.message, }; } const positiveCondition = conditions.find(condition => { return ( (condition.type === 'Ready' || condition.type === 'Running' || condition.type === 'Succeeded' || condition.type === 'Available') && condition.status === 'True' ); }); if (positiveCondition?.type) { return { label: positiveCondition.type, status: 'success', icon: 'mdi:check-circle', reason: positiveCondition.reason ?? positiveCondition.message, }; } if (phase) { const phaseStatus = getStatusVariant(phase); return { label: phase, status: phaseStatus, icon: getStatusIcon(phaseStatus), }; } const latestTrueCondition = [...conditions].reverse().find(condition => { return condition.status === 'True' && Boolean(condition.type); }); if (latestTrueCondition?.type) { const conditionStatus = getStatusVariant(latestTrueCondition.type); return { label: latestTrueCondition.type, status: conditionStatus, icon: getStatusIcon(conditionStatus), reason: latestTrueCondition.reason ?? latestTrueCondition.message, }; } const latestCondition = conditions[conditions.length - 1]; if (latestCondition?.type) { const conditionStatus = getStatusVariant(latestCondition.type); return { label: latestCondition.type, status: conditionStatus, icon: getStatusIcon(conditionStatus), reason: latestCondition.reason ?? latestCondition.message, }; } return { label: 'Pending', status: '', icon: 'mdi:clock-outline', }; } /** * Counts PipelineVersion resources that belong to the given Pipeline name and namespace. */ export function countPipelineVersionsForPipeline( versions: PipelineVersionReference[], pipelineName: string, namespace?: string ): number { if (!pipelineName) { return 0; } return versions.filter(version => { if (version.pipelineName !== pipelineName) { return false; } if (namespace) { return version.metadata?.namespace === namespace; } return true; }).length; } /** * Returns the pipeline root configured for a resource, if available. * Supports both individual Runs and Recurring Runs. */ export function getPipelineRunRoot( resource: | { spec?: { runtimeConfig?: { pipelineRoot?: string }; pipelineRoot?: string } } | null | undefined ): string { return resource?.spec?.runtimeConfig?.pipelineRoot ?? resource?.spec?.pipelineRoot ?? ''; } /** * Returns the run duration in milliseconds. */ export function getPipelineRunDurationMs(resource: PipelineRunResource | null | undefined): number { const startTime = resource?.status?.startTime; const completionTime = resource?.status?.completionTime ?? resource?.status?.lastTransitionTime; if (!startTime) { return 0; } const start = Date.parse(startTime); const end = completionTime ? Date.parse(completionTime) : Date.now(); if (Number.isNaN(start) || Number.isNaN(end) || end < start) { return 0; } return end - start; } /** * Returns a human-friendly run duration label. */ export function getPipelineRunDurationLabel( resource: PipelineRunResource | null | undefined ): string { const durationMs = getPipelineRunDurationMs(resource); if (durationMs <= 0) { return '-'; } return formatDuration(durationMs, { format: 'mini' }); } /** * Returns a displayable schedule label for a RecurringRun. */ export function getRecurringRunSchedule( resource: PipelineRecurringRunResource | null | undefined ): string { const cronValue = resource?.spec?.trigger?.cronSchedule ?? resource?.spec?.cronSchedule ?? ''; const cron = typeof cronValue === 'object' ? (cronValue as any).cron : cronValue; if (cron) { return cron; } const intervalSeconds = resource?.spec?.trigger?.intervalSecond ?? resource?.spec?.intervalSecond ?? 0; if (intervalSeconds > 0) { return `Every ${formatDuration(intervalSeconds * 1000, { format: 'mini' })}`; } return '-'; } /** * Returns the Headlamp route name for the Pipeline detail page. */ export function getPipelineDetailsPath(): string { return 'kubeflow-pipelines-detail'; } /** * Returns the Headlamp route name for the PipelineVersion detail page. */ export function getPipelineVersionDetailsPath(): string { return 'kubeflow-pipeline-versions-detail'; } /** * Returns the Headlamp route name for the Pipeline Run detail page. */ export function getPipelineRunDetailsPath(): string { return 'kubeflow-pipeline-runs-detail'; } /** * Returns the Headlamp route name for the Recurring Run detail page. */ export function getPipelineRecurringRunDetailsPath(): string { return 'kubeflow-pipeline-recurring-detail'; } /** * Returns the Headlamp route name for the Experiment detail page. */ export function getPipelineExperimentDetailsPath(): string { return 'kubeflow-pipeline-experiments-detail'; } /** * Returns related PipelineVersions for a Pipeline, scoped to the same namespace. */ export function getPipelineVersionsForPipeline<T extends PipelineVersionReference>( versions: T[], pipelineName: string, namespace?: string ): T[] { return versions.filter(version => { if (version.pipelineName !== pipelineName) { return false; } if (namespace) { return version.metadata?.namespace === namespace; } return true; }); } /** * Returns the most recently created PipelineVersion for a Pipeline. */ export function getLatestPipelineVersionForPipeline<T extends PipelineVersionReference>( versions: T[], pipelineName: string, namespace?: string ): T | null { const relatedVersions = getPipelineVersionsForPipeline(versions, pipelineName, namespace); if (relatedVersions.length === 0) { return null; } return [...relatedVersions].sort((left, right) => { const leftTimestamp = Date.parse(left.metadata?.creationTimestamp ?? ''); const rightTimestamp = Date.parse(right.metadata?.creationTimestamp ?? ''); return rightTimestamp - leftTimestamp; })[0]; } /** * Returns whether a PipelineVersion exposes any source-specific fields worth rendering. */ export function hasPipelineVersionSource(resource: { spec?: { pipelineSpecURI?: string; codeSourceURL?: string; pipelineSpec?: unknown; }; }): boolean { return Boolean( resource.spec?.pipelineSpecURI || resource.spec?.codeSourceURL || resource.spec?.pipelineSpec ); }