@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
378 lines (333 loc) • 10.1 kB
text/typescript
/**
* 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
);
}