diff --git a/console-extensions.json b/console-extensions.json
index 7a6b06d6..90d190a3 100644
--- a/console-extensions.json
+++ b/console-extensions.json
@@ -156,6 +156,23 @@
"required": ["PIPELINE_TEKTON_RESULT_INSTALLED"]
}
},
+ {
+ "type": "console.page/route",
+ "properties": {
+ "exact": true,
+ "path": [
+ "/pipelines-overview/ns/:ns",
+ "/pipelines-overview/all-namespaces"
+ ],
+ "component": {
+ "$codeRef": "pipelinesComponent.PipelinesOverviewPageK8s"
+ }
+ },
+ "flags": {
+ "required": ["OPENSHIFT_PIPELINE"],
+ "disallowed": ["PIPELINE_TEKTON_RESULT_INSTALLED"]
+ }
+ },
{
"type": "console.navigation/href",
"properties": {
@@ -168,7 +185,7 @@
"namespaced": true
},
"flags": {
- "required": ["OPENSHIFT_PIPELINE", "PIPELINE_TEKTON_RESULT_INSTALLED"]
+ "required": ["OPENSHIFT_PIPELINE"]
}
},
{
@@ -189,6 +206,42 @@
"required": ["PIPELINE_TEKTON_RESULT_INSTALLED"]
}
},
+ {
+ "type": "console.tab/horizontalNav",
+ "properties": {
+ "model": {
+ "group": "tekton.dev",
+ "version": "v1beta1",
+ "kind": "Pipeline"
+ },
+ "page": {
+ "name": "%plugin__pipelines-console-plugin~Metrics%",
+ "href": "metrics"
+ },
+ "component": { "$codeRef": "metricsComponent.PipelinesMetricsPageK8s" }
+ },
+ "flags": {
+ "disallowed": ["PIPELINE_TEKTON_RESULT_INSTALLED"]
+ }
+ },
+ {
+ "type": "console.tab/horizontalNav",
+ "properties": {
+ "model": {
+ "group": "tekton.dev",
+ "version": "v1",
+ "kind": "Pipeline"
+ },
+ "page": {
+ "name": "%plugin__pipelines-console-plugin~Metrics%",
+ "href": "metrics"
+ },
+ "component": { "$codeRef": "metricsComponent.PipelinesMetricsPageK8s" }
+ },
+ "flags": {
+ "disallowed": ["PIPELINE_TEKTON_RESULT_INSTALLED"]
+ }
+ },
{
"type": "console.tab/horizontalNav",
"properties": {
diff --git a/locales/en/plugin__pipelines-console-plugin.json b/locales/en/plugin__pipelines-console-plugin.json
index cbc89cc4..a284d5d0 100644
--- a/locales/en/plugin__pipelines-console-plugin.json
+++ b/locales/en/plugin__pipelines-console-plugin.json
@@ -100,6 +100,7 @@
"Custom Task": "Custom Task",
"CustomRun": "CustomRun",
"CustomRuns": "CustomRuns",
+ "Data is incomplete. To see the full view, please enable ": "Data is incomplete. To see the full view, please enable ",
"Data source": "Data source",
"Decrement": "Decrement",
"Default value": "Default value",
@@ -172,6 +173,7 @@
"Image Registry": "Image Registry",
"Image Registry Credentials": "Image Registry Credentials",
"Increment": "Increment",
+ "Info": "Info",
"Init containers": "Init containers",
"Install Cosign": "Install Cosign",
"Installing": "Installing",
@@ -247,6 +249,7 @@
"Output": "Output",
"Overview": "Overview",
"Owner": "Owner",
+ "Page Not Found (404)": "Page Not Found (404)",
"Parameters": "Parameters",
"Partially approved": "Partially approved",
"Password": "Password",
@@ -272,6 +275,7 @@
"PipelineRun not started yet": "PipelineRun not started yet",
"PipelineRun status": "PipelineRun status",
"PipelineRun Status shows the % of PipelineRuns for various status like \"Succeeded\", \"Failed\", \"Running\", \"Cancelled\" and \"Others\". Here, Others includes statuses like \"Started\", \"CreateRunFailed\", \"PipelineRunTimeout\"": "PipelineRun Status shows the % of PipelineRuns for various status like \"Succeeded\", \"Failed\", \"Running\", \"Cancelled\" and \"Others\". Here, Others includes statuses like \"Started\", \"CreateRunFailed\", \"PipelineRunTimeout\"",
+ "PipelineRun status shows the % of PipelineRuns for various statuses like \"Succeeded\", \"Failed\" and \"Cancelled\".": "PipelineRun status shows the % of PipelineRuns for various statuses like \"Succeeded\", \"Failed\" and \"Cancelled\".",
"PipelineRuns": "PipelineRuns",
"Pipelines": "Pipelines",
"Please <2>try again2>.": "Please <2>try again2>.",
@@ -301,6 +305,7 @@
"Rerun": "Rerun",
"Reset": "Reset",
"Resource is being fetched from Tekton Results.": "Resource is being fetched from Tekton Results.",
+ "Resource is deleted.": "Resource is deleted.",
"Route": "Route",
"Routes": "Routes",
"run{{plural}} in other namespaces.": "run{{plural}} in other namespaces.",
@@ -371,6 +376,7 @@
"TaskRun name": "TaskRun name",
"TaskRuns": "TaskRuns",
"Tasks": "Tasks",
+ "Tekton results": "Tekton results",
"TektonResult": "TektonResult",
"TektonResults": "TektonResults",
"The base server url (e.g. https://github.com)": "The base server url (e.g. https://github.com)",
diff --git a/src/components/common/error.tsx b/src/components/common/error.tsx
new file mode 100644
index 00000000..ef9175ab
--- /dev/null
+++ b/src/components/common/error.tsx
@@ -0,0 +1,25 @@
+import * as React from 'react';
+import { Helmet } from 'react-helmet';
+import { useTranslation } from 'react-i18next';
+import {
+ EmptyState,
+ EmptyStateHeader,
+ EmptyStateVariant,
+} from '@patternfly/react-core';
+
+export const ErrorPage404: React.FC = () => {
+ const { t } = useTranslation('plugin__pipelines-console-plugin');
+ return (
+
+
+ {t('Page Not Found (404)')}
+
+
+
+
+
+ );
+};
diff --git a/src/components/hooks/flagHookProvider.ts b/src/components/hooks/flagHookProvider.ts
index 828c37ac..2929b264 100644
--- a/src/components/hooks/flagHookProvider.ts
+++ b/src/components/hooks/flagHookProvider.ts
@@ -85,6 +85,10 @@ export const useFlagHookProvider = (setFeatureFlag: SetFeatureFlag) => {
FLAG_HIDE_STATIC_PIPELINE_PLUGIN_PIPELINERUN_DETAIL_APPROVALS_TAB,
true,
);
+ setFeatureFlag(
+ FLAG_HIDE_STATIC_PIPELINE_PLUGIN_PIPELINE_DETAIL_METRICS_TAB,
+ true,
+ );
};
export const useTektonResultInstallProvider = (
@@ -106,8 +110,4 @@ export const useTektonResultInstallProvider = (
fetch();
}, []);
setFeatureFlag(FLAG_PIPELINE_TEKTON_RESULT_INSTALLED, data ? true : false);
- setFeatureFlag(
- FLAG_HIDE_STATIC_PIPELINE_PLUGIN_PIPELINE_DETAIL_METRICS_TAB,
- data ? true : false,
- );
};
diff --git a/src/components/pipelines-details/PipelineDetailsPage.tsx b/src/components/pipelines-details/PipelineDetailsPage.tsx
index 028a3fda..9af0abcf 100644
--- a/src/components/pipelines-details/PipelineDetailsPage.tsx
+++ b/src/components/pipelines-details/PipelineDetailsPage.tsx
@@ -33,6 +33,7 @@ import { triggerPipeline } from '../pipelines-list/PipelineKebab';
import { StartedByAnnotation } from '../../consts';
import { usePipelineTriggerTemplateNames } from '../utils/triggers';
import { resourcePathFromModel } from '../utils/utils';
+import { ErrorPage404 } from '../common/error';
const PipelineDetailsPage = () => {
const { t } = useTranslation('plugin__pipelines-console-plugin');
@@ -40,7 +41,7 @@ const PipelineDetailsPage = () => {
const history = useHistory();
const navigate = useNavigate();
const { name, ns: namespace } = params;
- const [pipeline, loaded] = useK8sWatchResource({
+ const [pipeline, loaded, loadError] = useK8sWatchResource({
groupVersionKind: getGroupVersionKindForModel(PipelineModel),
namespace,
name,
@@ -125,7 +126,7 @@ const PipelineDetailsPage = () => {
};
if (!loaded) {
- return ;
+ return loadError ? : ;
}
return (
{
+ const chartData = tickValues?.map((value, index) => {
+ if (index === 0) {
+ // Ensure the first value is always 0
+ return {
+ x: value,
+ y: 0,
+ };
+ }
+ const s = data?.find((d) => {
+ const group_date = new Date(Number(d.group_value) * 1000);
+ if (type == 'hour') {
+ return group_date.getHours() === value;
+ }
+ if (type == 'day' || type == 'week') {
+ return group_date.toDateString() === new Date(value).toDateString();
+ }
+ if (type == 'month') {
+ return group_date.getMonth() === value.getMonth();
+ }
+ });
+ return {
+ x: value,
+ y: s?.avg_duration || 0,
+ };
+ });
+ return chartData;
+};
+
+const PipelinesAverageDurationK8s: React.FC = ({
+ timespan,
+ domain,
+ bordered,
+ interval,
+ parentName,
+ namespace,
+}) => {
+ const { t } = useTranslation('plugin__pipelines-console-plugin');
+ const startTimespan = timespan - parsePrometheusDuration('1d');
+ const endDate = new Date(Date.now()).setHours(0, 0, 0, 0);
+ const startDate = new Date(Date.now() - startTimespan).setHours(0, 0, 0, 0);
+ const { x: domainX, y: domainY } = (domain as DomainType) || {};
+ const domainValue: DomainPropType = {
+ x: domainX || [startDate, endDate],
+ y: domainY || undefined,
+ };
+ const [tickValues, type] = getXaxisValues(timespan);
+
+ const [totalPipelineRunsCountData] =
+ parentName && namespace
+ ? usePipelineMetricsForNamespaceForPipelinePoll({
+ namespace,
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ name: parentName,
+ metricsQuery:
+ PipelineQuery.PIPELINERUN_COUNT_FOR_NAMESPACE_FOR_PIPELINE,
+ })
+ : namespace == ALL_NAMESPACES_KEY
+ ? usePipelineMetricsForAllNamespacePoll({
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ metricsQuery: PipelineQuery.PIPELINERUN_COUNT_FOR_ALL_NAMESPACE,
+ })
+ : usePipelineMetricsForNamespacePoll({
+ namespace,
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ metricsQuery: PipelineQuery.PIPELINERUN_COUNT_FOR_NAMESPACE,
+ });
+
+ const [totalPipelineRunsDurationData] =
+ parentName && namespace
+ ? usePipelineMetricsForNamespaceForPipelinePoll({
+ namespace,
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ name: parentName,
+ metricsQuery:
+ PipelineQuery.PIPELINERUN_DURATION_FOR_NAMESPACE_FOR_PIPELINE,
+ })
+ : namespace == ALL_NAMESPACES_KEY
+ ? usePipelineMetricsForAllNamespacePoll({
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ metricsQuery: PipelineQuery.PIPELINERUN_DURATION_FOR_ALL_NAMESPACE,
+ })
+ : usePipelineMetricsForNamespacePoll({
+ namespace,
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ metricsQuery: PipelineQuery.PIPELINERUN_DURATION_FOR_NAMESPACE,
+ });
+
+ const combinedData = totalPipelineRunsCountData?.data?.result[0]?.values?.map(
+ (value1) => {
+ const group_value = value1[0];
+ const runs = parseInt(value1[1]);
+ const value2 =
+ totalPipelineRunsDurationData?.data?.result[0]?.values?.find(
+ (val) =>
+ roundToNearestSecond(val[0]) === roundToNearestSecond(group_value),
+ );
+ const duration = value2 ? parseFloat(value2[1]) : 0;
+
+ return { group_value, runs, duration };
+ },
+ );
+
+ const result = combinedData?.map(({ group_value, runs, duration }, index) => {
+ if (index === 0) {
+ return {
+ group_value,
+ avg_duration: secondsToMinutesK8s(duration / runs),
+ };
+ } else {
+ const prevDuration = combinedData[index - 1].duration;
+ const prevRuns = combinedData[index - 1].runs;
+ if (duration === prevDuration && runs === prevRuns) {
+ // If both runs and duration are the same as the previous, set avg_duration to 0
+ return { group_value, avg_duration: 0 };
+ } else if (duration < prevDuration || runs < prevRuns) {
+ // Reset condition
+ return {
+ group_value,
+ avg_duration: secondsToMinutesK8s(duration / runs),
+ };
+ } else {
+ // Increment condition
+ const incrementalDuration = duration - prevDuration;
+ const incrementalRuns = runs - prevRuns;
+ const avg_duration = incrementalDuration / incrementalRuns;
+ return { group_value, avg_duration: secondsToMinutesK8s(avg_duration) };
+ }
+ }
+ });
+
+ let xTickFormat;
+ let dayLabel;
+ let showLabel = false;
+ let chartData = [];
+ switch (type) {
+ case 'hour':
+ xTickFormat = (d) => hourformat(d);
+ showLabel = true;
+ domainValue.x = [0, 23];
+ dayLabel = formatDate(new Date());
+ chartData = getChartData(tickValues, result, 'hour');
+ break;
+ case 'day':
+ xTickFormat = (d) => formatDate(d);
+ domainValue.x = [startDate, endDate];
+ chartData = getChartData(tickValues, result, 'day');
+ break;
+ case 'week':
+ xTickFormat = (d) => formatDate(d);
+ domainValue.x = [new Date(tickValues[0]), new Date(tickValues[11])];
+ chartData = getChartData(tickValues, result, 'week');
+ break;
+ case 'month':
+ xTickFormat = (d) => monthYear(d);
+ domainValue.x = [new Date(tickValues[0]), new Date(tickValues[11])];
+ chartData = getChartData(tickValues, result, 'month');
+ break;
+ default:
+ console.log('Received wrong data');
+ break;
+ }
+ const max = Math.max(...chartData.map((yVal) => yVal.y));
+ const roundUp = (value, nearest) => {
+ return Math.ceil(value / nearest) * nearest;
+ };
+ const nearest = max > 10 ? 10 : 5;
+ const roundedMax = roundUp(max, nearest);
+ domainValue.y =
+ !isNaN(roundedMax) && roundedMax > 5 ? [0, roundedMax] : [0, 5];
+
+ if (!domainY) {
+ let minY: number = _.minBy(chartData, 'y')?.y ?? 0;
+ let maxY: number = _.maxBy(chartData, 'y')?.y ?? 0;
+ if (minY === 0 && maxY === 0) {
+ minY = -1;
+ maxY = 1;
+ } else if (minY > 0 && maxY > 0) {
+ minY = 0;
+ } else if (minY < 0 && maxY < 0) {
+ maxY = 0;
+ }
+ domainValue.y = [minY, maxY];
+ }
+
+ let xAxisStyle: ChartAxisProps['style'] = {
+ tickLabels: { fill: 'var(--pf-v5-global--Color--100)', fontSize: 12 },
+ };
+ const yAxisStyle: ChartAxisProps['style'] = {
+ tickLabels: { fill: 'var(--pf-v5-global--Color--100)', fontSize: 12 },
+ };
+ if (tickValues.length > 7) {
+ xAxisStyle = {
+ tickLabels: {
+ fill: 'var(--pf-v5-global--Color--100)',
+ angle: 320,
+ fontSize: 10,
+ textAnchor: 'end',
+ verticalAnchor: 'end',
+ },
+ };
+ }
+
+ return (
+ <>
+
+
+ {t('Average duration')}
+
+
+
+ `${datum.y}m`}
+ constrainToVisibleArea
+ />
+ }
+ scale={{ x: 'time', y: 'linear' }}
+ domain={domainValue}
+ domainPadding={{ x: [30, 25] }}
+ height={145}
+ width={400}
+ padding={{
+ top: 10,
+ bottom: 55,
+ left: 50,
+ }}
+ themeColor={ChartThemeColor.blue}
+ >
+
+ `${v}m`}
+ />
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default PipelinesAverageDurationK8s;
diff --git a/src/components/pipelines-metrics/PipelinesMetrics.scss b/src/components/pipelines-metrics/PipelinesMetrics.scss
index 4064ef13..768de1aa 100644
--- a/src/components/pipelines-metrics/PipelinesMetrics.scss
+++ b/src/components/pipelines-metrics/PipelinesMetrics.scss
@@ -25,3 +25,10 @@
}
}
}
+
+.k8s-overview-info-alert {
+ margin-top: var(--pf-v5-global--spacer--md);
+ margin-left: var(--pf-v5-global--spacer--md);
+ margin-right: var(--pf-v5-global--spacer--md);
+ margin-bottom: 0;
+}
diff --git a/src/components/pipelines-metrics/PipelinesMetricsPageK8s.tsx b/src/components/pipelines-metrics/PipelinesMetricsPageK8s.tsx
new file mode 100644
index 00000000..ee71b9ca
--- /dev/null
+++ b/src/components/pipelines-metrics/PipelinesMetricsPageK8s.tsx
@@ -0,0 +1,124 @@
+import * as React from 'react';
+import { Flex, FlexItem } from '@patternfly/react-core';
+import {
+ formatPrometheusDuration,
+ parsePrometheusDuration,
+} from '../pipelines-overview/dateTime';
+import TimeRangeDropdown from '../pipelines-overview/TimeRangeDropdown';
+import RefreshDropdown from '../pipelines-overview/RefreshDropdown';
+import {
+ IntervalOptions,
+ TimeRangeOptionsK8s,
+ useQueryParams,
+} from '../pipelines-overview/utils';
+import { PipelineKind } from '../../types';
+import './PipelinesMetrics.scss';
+import PipelineRunsStatusCardK8s from '../pipelines-overview/PipelineRunsStatusCardK8s';
+import PipelineRunsNumbersChartK8s from '../pipelines-overview/PipelineRunsNumbersChartK8s';
+import PipelineRunsDurationCardK8s from '../pipelines-overview/PipelineRunsDurationCardK8s';
+import PipelinesAverageDurationK8s from './PipelinesAverageDurationK8s';
+import { K8sDataLimitationAlert } from '../pipelines-overview/K8sDataLimitationAlert';
+
+type PipelinesMetricsPageProps = {
+ obj: PipelineKind;
+};
+
+const PipelinesMetricsPageK8s: React.FC = ({
+ obj,
+}) => {
+ const {
+ metadata: { namespace, name: parentName },
+ } = obj;
+ const [timespan, setTimespan] = React.useState(parsePrometheusDuration('1d'));
+ const [interval, setInterval] = React.useState(
+ parsePrometheusDuration('30s'),
+ );
+
+ useQueryParams({
+ key: 'refreshinterval',
+ value: interval,
+ setValue: setInterval,
+ defaultValue: parsePrometheusDuration('30s'),
+ options: { ...IntervalOptions(), off: 'OFF_KEY' },
+ displayFormat: (v) => (v ? formatPrometheusDuration(v) : 'off'),
+ loadFormat: (v) => (v == 'off' ? null : parsePrometheusDuration(v)),
+ });
+
+ useQueryParams({
+ key: 'timerange',
+ value: timespan,
+ setValue: setTimespan,
+ defaultValue: parsePrometheusDuration('1w'),
+ options: TimeRangeOptionsK8s(),
+ displayFormat: formatPrometheusDuration,
+ loadFormat: parsePrometheusDuration,
+ });
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default PipelinesMetricsPageK8s;
diff --git a/src/components/pipelines-metrics/const.ts b/src/components/pipelines-metrics/const.ts
new file mode 100644
index 00000000..b77142e6
--- /dev/null
+++ b/src/components/pipelines-metrics/const.ts
@@ -0,0 +1,7 @@
+export const ONE_SECOND = 1000;
+export const ONE_MINUTE = 60 * ONE_SECOND;
+export const ONE_HOUR = 60 * ONE_MINUTE;
+export const DEFAULT_SAMPLES = 60;
+export const PROMETHEUS_TENANCY_BASE_PATH = '/api/prometheus-tenancy';
+export const DEFAULT_PROMETHEUS_SAMPLES = 60;
+export const DEFAULT_PROMETHEUS_TIMESPAN = ONE_HOUR;
diff --git a/src/components/pipelines-metrics/helpers.ts b/src/components/pipelines-metrics/helpers.ts
new file mode 100644
index 00000000..9cd021e0
--- /dev/null
+++ b/src/components/pipelines-metrics/helpers.ts
@@ -0,0 +1,118 @@
+import { PrometheusEndpoint } from '@openshift-console/dynamic-plugin-sdk';
+import * as _ from 'lodash-es';
+import {
+ DEFAULT_PROMETHEUS_SAMPLES,
+ DEFAULT_PROMETHEUS_TIMESPAN,
+ PROMETHEUS_TENANCY_BASE_PATH,
+} from './const';
+
+export { PrometheusEndpoint };
+
+const getRangeVectorSearchParams = (
+ endTime: number = Date.now(),
+ samples: number = DEFAULT_PROMETHEUS_SAMPLES,
+ timespan: number = DEFAULT_PROMETHEUS_TIMESPAN,
+): URLSearchParams => {
+ const params = new URLSearchParams();
+ const now = new Date();
+ if (timespan === 86400000) {
+ // Set start to 12 AM today
+ const startOfDay = new Date(
+ now.getFullYear(),
+ now.getMonth(),
+ now.getDate(),
+ 0,
+ 0,
+ 0,
+ );
+ params.append('start', `${startOfDay.getTime() / 1000}`);
+
+ // Set end to 11 PM today
+ const endOfDay = new Date(
+ now.getFullYear(),
+ now.getMonth(),
+ now.getDate(),
+ 23,
+ 0,
+ 0,
+ );
+ params.append('end', `${endOfDay.getTime() / 1000}`);
+
+ // Set step to 3600 seconds (1 hour)
+ params.append('step', '3600');
+ } else if (timespan === 1209600000) {
+ // 14 days in milliseconds
+ // Set start to 14 days ago
+ const startOfFourteenDaysAgo = new Date(
+ now.getTime() - 14 * 24 * 60 * 60 * 1000,
+ );
+ params.append('start', `${startOfFourteenDaysAgo.getTime() / 1000}`);
+
+ // Set end to now
+ params.append('end', `${endTime / 1000}`);
+ params.append('step', '86400');
+ } else if (timespan === 2592000000) {
+ // 30 days in milliseconds
+ // Set start to 30 days ago
+ const startOfThirtyDaysAgo = new Date(
+ now.getTime() - 30 * 24 * 60 * 60 * 1000,
+ );
+ params.append('start', `${startOfThirtyDaysAgo.getTime() / 1000}`);
+
+ // Set end to now
+ params.append('end', `${endTime / 1000}`);
+ params.append('step', '86400');
+ } else if (timespan === 7257600000) {
+ // 3 months in milliseconds
+ // Set start to 3 months ago
+ const startOfThreeMonthsAgo = new Date(now.setMonth(now.getMonth() - 3));
+ params.append('start', `${startOfThreeMonthsAgo.getTime() / 1000}`);
+
+ // Set end to now
+ params.append('end', `${endTime / 1000}`);
+ params.append('step', '604800');
+ } else {
+ params.append('start', `${(endTime - timespan) / 1000}`);
+ params.append('end', `${endTime / 1000}`);
+ params.append('step', `${timespan / samples / 1000}`);
+ }
+ return params;
+};
+
+const getSearchParams = ({
+ endpoint,
+ endTime,
+ timespan,
+ samples,
+ ...params
+}: PrometheusURLProps): URLSearchParams => {
+ const searchParams =
+ endpoint === PrometheusEndpoint.QUERY_RANGE
+ ? getRangeVectorSearchParams(endTime, samples, timespan)
+ : new URLSearchParams();
+ _.each(
+ params,
+ (value, key) => value && searchParams.append(key, value.toString()),
+ );
+ return searchParams;
+};
+
+export const getPrometheusURL = (props: PrometheusURLProps): string => {
+ if (props.endpoint !== PrometheusEndpoint.RULES && !props.query) {
+ return '';
+ }
+ const params = getSearchParams(props);
+ return `${PROMETHEUS_TENANCY_BASE_PATH}/${
+ props.endpoint
+ }?${params.toString()}`;
+};
+
+type PrometheusURLProps = {
+ endpoint: PrometheusEndpoint;
+ endTime?: number;
+ namespace?: string;
+ query?: string;
+ samples?: number;
+ timeout?: string;
+ timespan?: number;
+};
diff --git a/src/components/pipelines-metrics/hooks.ts b/src/components/pipelines-metrics/hooks.ts
new file mode 100644
index 00000000..2b454cf6
--- /dev/null
+++ b/src/components/pipelines-metrics/hooks.ts
@@ -0,0 +1,155 @@
+import {
+ PrometheusEndpoint,
+ PrometheusResponse,
+} from '@openshift-console/dynamic-plugin-sdk';
+import _ from 'lodash';
+import { getPrometheusURL } from './helpers';
+import { useURLPoll } from './url-poll-hook';
+import { metricsQueries } from './utils';
+
+export const calculateTotalValues = (
+ prometheusResponse: PrometheusResponse,
+ timerangeSeconds: number,
+): number => {
+ const currentTime = Date.now() / 1000;
+
+ const totalValues = prometheusResponse?.data?.result?.reduce(
+ (total, current) => {
+ const valuesInRange = current.values.filter(
+ (value) => value[0] >= currentTime - timerangeSeconds,
+ );
+ const sumValues = _.sumBy(valuesInRange, (value) =>
+ parseInt(value[1], 10),
+ );
+ return total + sumValues;
+ },
+ 0,
+ );
+
+ return totalValues;
+};
+
+export const usePipelineMetricsForAllNamespacePoll = ({
+ delay,
+ timespan,
+ queryPrefix,
+ metricsQuery,
+}) => {
+ const queries = metricsQueries(queryPrefix);
+ return useURLPoll(
+ getPrometheusURL({
+ endpoint: PrometheusEndpoint.QUERY_RANGE,
+ query: queries[metricsQuery](),
+ samples: 1,
+ endTime: Date.now(),
+ timespan,
+ }),
+ delay,
+ timespan,
+ );
+};
+
+export const usePipelineMetricsForNamespaceForPipelinePoll = ({
+ delay,
+ namespace,
+ timespan,
+ queryPrefix,
+ name,
+ metricsQuery,
+}) => {
+ const queries = metricsQueries(queryPrefix);
+ return useURLPoll(
+ getPrometheusURL({
+ endpoint: PrometheusEndpoint.QUERY_RANGE,
+ query: queries[metricsQuery]({
+ name,
+ namespace,
+ }),
+ samples: 1,
+ endTime: Date.now(),
+ timespan,
+ namespace,
+ }),
+ delay,
+ namespace,
+ timespan,
+ );
+};
+
+export const usePipelineMetricsForNamespacePoll = ({
+ delay,
+ namespace,
+ timespan,
+ queryPrefix,
+ metricsQuery,
+}) => {
+ const queries = metricsQueries(queryPrefix);
+ return useURLPoll(
+ getPrometheusURL({
+ endpoint: PrometheusEndpoint.QUERY_RANGE,
+ query: queries[metricsQuery]({
+ namespace,
+ }),
+ samples: 1,
+ endTime: Date.now(),
+ timespan,
+ namespace,
+ }),
+ delay,
+ namespace,
+ timespan,
+ );
+};
+
+export const getStatusCounts = (prometheusResponse: PrometheusResponse) => {
+ const result = prometheusResponse?.data?.result;
+ const counts = {
+ success: 0,
+ failed: 0,
+ cancelled: 0,
+ };
+ if (!result) {
+ return counts;
+ }
+ result.forEach((item) => {
+ const status = item.metric.status;
+ const value = parseInt(item.values[0][1], 10); // parse the count value from the string
+
+ if (status === 'success') {
+ counts.success += value;
+ } else if (status === 'failed') {
+ counts.failed += value;
+ } else if (status === 'cancelled') {
+ counts.cancelled += value;
+ }
+ });
+
+ return counts;
+};
+
+export const calculateTotalDuration = (
+ prometheusResponse: PrometheusResponse,
+) => {
+ const results = prometheusResponse?.data?.result;
+
+ if (!results) {
+ return 0;
+ }
+
+ const totalDuration = results.reduce((total, result) => {
+ const values = result.values;
+ return (
+ total +
+ values.reduce((resultTotal, value, index, array) => {
+ if (index < array.length - 1) {
+ const currentTimestamp = value[0];
+ const nextTimestamp = array[index + 1][0];
+ return resultTotal + (nextTimestamp - currentTimestamp);
+ }
+ return resultTotal;
+ }, 0)
+ );
+ }, 0);
+
+ return totalDuration;
+};
diff --git a/src/components/pipelines-metrics/index.ts b/src/components/pipelines-metrics/index.ts
index 6a191aae..db8b1fb1 100644
--- a/src/components/pipelines-metrics/index.ts
+++ b/src/components/pipelines-metrics/index.ts
@@ -1 +1,2 @@
export { default as PipelinesMetricsPage } from './PipelinesMetricsPage';
+export { default as PipelinesMetricsPageK8s } from './PipelinesMetricsPageK8s';
diff --git a/src/components/pipelines-metrics/poll-hook.ts b/src/components/pipelines-metrics/poll-hook.ts
new file mode 100644
index 00000000..4771c9ec
--- /dev/null
+++ b/src/components/pipelines-metrics/poll-hook.ts
@@ -0,0 +1,25 @@
+import { useEffect, useRef } from 'react';
+
+// Slightly modified from Dan Abramov's blog post about using React hooks for polling
+// https://overreacted.io/making-setinterval-declarative-with-react-hooks/
+export const usePoll = (callback, delay, ...dependencies) => {
+ const savedCallback = useRef(null);
+
+ // Remember the latest callback.
+ useEffect(() => {
+ savedCallback.current = callback;
+ }, [callback]);
+
+ // Set up the interval.
+ useEffect(() => {
+ const tick = () => savedCallback.current();
+
+ tick(); // Run first tick immediately.
+
+ if (delay) {
+ // Only start interval if a delay is provided.
+ const id = setInterval(tick, delay);
+ return () => clearInterval(id);
+ }
+ }, [delay, ...dependencies]);
+};
diff --git a/src/components/pipelines-metrics/safe-fetch-hook.ts b/src/components/pipelines-metrics/safe-fetch-hook.ts
new file mode 100644
index 00000000..95ac809b
--- /dev/null
+++ b/src/components/pipelines-metrics/safe-fetch-hook.ts
@@ -0,0 +1,15 @@
+import { consoleFetchJSON } from '@openshift-console/dynamic-plugin-sdk';
+import { useEffect, useRef } from 'react';
+
+export const useSafeFetch = () => {
+ const controller = useRef();
+ useEffect(() => {
+ controller.current = new AbortController();
+ return () => controller.current.abort();
+ }, []);
+
+ return (url) =>
+ consoleFetchJSON(url, 'get', {
+ signal: controller.current.signal as AbortSignal,
+ });
+};
diff --git a/src/components/pipelines-metrics/url-poll-hook.ts b/src/components/pipelines-metrics/url-poll-hook.ts
new file mode 100644
index 00000000..2ce841b7
--- /dev/null
+++ b/src/components/pipelines-metrics/url-poll-hook.ts
@@ -0,0 +1,41 @@
+import { UseURLPoll } from '@openshift-console/dynamic-plugin-sdk/lib/api/internal-types';
+import { useCallback, useState } from 'react';
+import { usePoll } from './poll-hook';
+import { useSafeFetch } from './safe-fetch-hook';
+
+export const URL_POLL_DEFAULT_DELAY = 15000; // 15 seconds
+
+export const useURLPoll: UseURLPoll = (
+ url: string,
+ delay = URL_POLL_DEFAULT_DELAY,
+ ...dependencies: any[]
+) => {
+ const [error, setError] = useState();
+ const [response, setResponse] = useState();
+ const [loading, setLoading] = useState(true);
+ const safeFetch = useSafeFetch();
+ const tick = useCallback(() => {
+ if (url) {
+ safeFetch(url)
+ .then((data) => {
+ setResponse(data);
+ setError(null);
+ })
+ .catch((err) => {
+ if (err.name !== 'AbortError') {
+ setResponse(null);
+ setError(err);
+ // eslint-disable-next-line no-console
+ console.error(`Error polling URL: ${err}`);
+ }
+ })
+ .finally(() => setLoading(false));
+ } else {
+ setLoading(false);
+ }
+ }, [url]);
+
+ usePoll(tick, delay, ...dependencies);
+
+ return [response, error, loading];
+};
diff --git a/src/components/pipelines-metrics/utils.ts b/src/components/pipelines-metrics/utils.ts
new file mode 100644
index 00000000..96f169ec
--- /dev/null
+++ b/src/components/pipelines-metrics/utils.ts
@@ -0,0 +1,78 @@
+import _ from 'lodash';
+
+export enum PipelineQuery {
+ PIPELINERUN_COUNT_FOR_STATUS_FOR_NAMESPACE = 'PIPELINERUN_COUNT_FOR_STATUS_FOR_NAMESPACE',
+ PIPELINERUN_COUNT_FOR_STATUS_FOR_ALL_NAMESPACE = 'PIPELINERUN_COUNT_FOR_STATUS_FOR_ALL_NAMESPACE',
+ PIPELINERUN_COUNT_FOR_STATUS_FOR_NAMESPACE_FOR_PIPELINE = 'PIPELINERUN_COUNT_FOR_STATUS_FOR_NAMESPACE_FOR_PIPELINE',
+ PIPELINERUN_COUNT_FOR_NAMESPACE = 'PIPELINERUN_COUNT_FOR_NAMESPACE',
+ PIPELINERUN_COUNT_FOR_ALL_NAMESPACE = 'PIPELINERUN_COUNT_FOR_ALL_NAMESPACE',
+ PIPELINERUN_COUNT_FOR_NAMESPACE_FOR_PIPELINE = 'PIPELINERUN_COUNT_FOR_NAMESPACE_FOR_PIPELINE',
+ PIPELINERUN_DURATION_FOR_NAMESPACE = 'PIPELINERUN_DURATION_FOR_NAMESPACE',
+ PIPELINERUN_DURATION_FOR_ALL_NAMESPACE = 'PIPELINERUN_DURATION_FOR_ALL_NAMESPACE',
+ PIPELINERUN_DURATION_FOR_NAMESPACE_FOR_PIPELINE = 'PIPELINERUN_DURATION_FOR_NAMESPACE_FOR_PIPELINE',
+ PIPELINERUN_COUNT_WITH_METRIC_FOR_ALL_NAMESPACE = 'PIPELINERUN_COUNT_WITH_METRIC_FOR_ALL_NAMESPACE',
+ PIPELINERUN_COUNT_WITH_METRIC_FOR_NAMESPACE = 'PIPELINERUN_COUNT_WITH_METRIC_FOR_NAMESPACE',
+ PIPELINERUN_SUM_WITH_METRIC_FOR_ALL_NAMESPACE = 'PIPELINERUN_SUM_WITH_METRIC_FOR_ALL_NAMESPACE',
+ PIPELINERUN_SUM_WITH_METRIC_FOR_NAMESPACE = 'PIPELINERUN_SUM_WITH_METRIC_FOR_NAMESPACE',
+}
+
+export enum MetricsQueryPrefix {
+ TEKTON = 'tekton',
+ TEKTON_PIPELINES_CONTROLLER = 'tekton_pipelines_controller',
+}
+
+export const metricsQueries = (
+ prefix: string = MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+) => ({
+ [PipelineQuery.PIPELINERUN_COUNT_FOR_STATUS_FOR_NAMESPACE]: _.template(
+ `sum by (status) (${prefix}_pipelinerun_duration_seconds_count{namespace="<%= namespace %>"})`,
+ ),
+ [PipelineQuery.PIPELINERUN_COUNT_FOR_STATUS_FOR_NAMESPACE_FOR_PIPELINE]:
+ _.template(
+ `sum by (status) (${prefix}_pipelinerun_duration_seconds_count{pipeline="<%= name %>",namespace="<%= namespace %>"})`,
+ ),
+ [PipelineQuery.PIPELINERUN_COUNT_FOR_STATUS_FOR_ALL_NAMESPACE]: _.template(
+ `sum by (status) (${prefix}_pipelinerun_duration_seconds_count)`,
+ ),
+ [PipelineQuery.PIPELINERUN_COUNT_FOR_NAMESPACE]: _.template(
+ `sum(${prefix}_pipelinerun_duration_seconds_count{namespace="<%= namespace %>"})`,
+ ),
+ [PipelineQuery.PIPELINERUN_COUNT_FOR_NAMESPACE_FOR_PIPELINE]: _.template(
+ `sum(${prefix}_pipelinerun_duration_seconds_count{pipeline="<%= name %>",namespace="<%= namespace %>"})`,
+ ),
+ [PipelineQuery.PIPELINERUN_COUNT_FOR_ALL_NAMESPACE]: _.template(
+ `sum(${prefix}_pipelinerun_duration_seconds_count)`,
+ ),
+ [PipelineQuery.PIPELINERUN_DURATION_FOR_NAMESPACE]: _.template(
+ `sum(${prefix}_pipelinerun_duration_seconds_sum{namespace="<%= namespace %>"})`,
+ ),
+ [PipelineQuery.PIPELINERUN_DURATION_FOR_NAMESPACE_FOR_PIPELINE]: _.template(
+ `sum(${prefix}_pipelinerun_duration_seconds_sum{pipeline="<%= name %>",namespace="<%= namespace %>"})`,
+ ),
+ [PipelineQuery.PIPELINERUN_DURATION_FOR_ALL_NAMESPACE]: _.template(
+ `sum(${prefix}_pipelinerun_duration_seconds_sum)`,
+ ),
+ [PipelineQuery.PIPELINERUN_COUNT_WITH_METRIC_FOR_ALL_NAMESPACE]: _.template(
+ `${prefix}_pipelinerun_duration_seconds_count`,
+ ),
+ [PipelineQuery.PIPELINERUN_COUNT_WITH_METRIC_FOR_NAMESPACE]: _.template(
+ `${prefix}_pipelinerun_duration_seconds_count{namespace="<%= namespace %>"}`,
+ ),
+ [PipelineQuery.PIPELINERUN_SUM_WITH_METRIC_FOR_ALL_NAMESPACE]: _.template(
+ `${prefix}_pipelinerun_duration_seconds_sum`,
+ ),
+ [PipelineQuery.PIPELINERUN_SUM_WITH_METRIC_FOR_NAMESPACE]: _.template(
+ `${prefix}_pipelinerun_duration_seconds_sum{namespace="<%= namespace %>"}`,
+ ),
+});
+
+export const adjustToStartOfWeek = (date: Date): Date => {
+ const day = date.getDay();
+ const diff = date.getDate() - day + (day === 0 ? -6 : 1); // adjust when day is Sunday
+ return new Date(date.setDate(diff));
+};
+
+export const secondsToMinutesK8s = (seconds: number): number => {
+ const minutes = seconds / 60;
+ return parseFloat(minutes.toFixed(2));
+};
diff --git a/src/components/pipelines-overview/K8sDataLimitationAlert.tsx b/src/components/pipelines-overview/K8sDataLimitationAlert.tsx
new file mode 100644
index 00000000..9ee61d5e
--- /dev/null
+++ b/src/components/pipelines-overview/K8sDataLimitationAlert.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { Alert } from '@patternfly/react-core';
+
+export const K8sDataLimitationAlert: React.FC = () => {
+ const { t } = useTranslation('plugin__pipelines-console-plugin');
+ return (
+
+
+ {t('Data is incomplete. To see the full view, please enable ')}
+
+ {t('Tekton results')}
+
+
+ .
+
+
+ );
+};
diff --git a/src/components/pipelines-overview/PipelineRunsDurationCardK8s.tsx b/src/components/pipelines-overview/PipelineRunsDurationCardK8s.tsx
new file mode 100644
index 00000000..7f4ed26f
--- /dev/null
+++ b/src/components/pipelines-overview/PipelineRunsDurationCardK8s.tsx
@@ -0,0 +1,184 @@
+import * as React from 'react';
+import * as classNames from 'classnames';
+import { useTranslation } from 'react-i18next';
+import {
+ HistoryIcon,
+ InfoCircleIcon,
+ MonitoringIcon,
+} from '@patternfly/react-icons';
+import {
+ Card,
+ CardBody,
+ CardTitle,
+ Divider,
+ Grid,
+ GridItem,
+} from '@patternfly/react-core';
+import {
+ SummaryProps,
+ getPipelineRunAverageDuration,
+ getTotalPipelineRuns,
+ getTotalPipelineRunsDuration,
+} from './utils';
+
+import { ALL_NAMESPACES_KEY } from '../../consts';
+
+import './PipelineRunsDurationCard.scss';
+import {
+ usePipelineMetricsForAllNamespacePoll,
+ usePipelineMetricsForNamespaceForPipelinePoll,
+ usePipelineMetricsForNamespacePoll,
+} from '../pipelines-metrics/hooks';
+import { MetricsQueryPrefix, PipelineQuery } from '../pipelines-metrics/utils';
+import { getXaxisValues } from './dateTime';
+
+interface PipelinesRunsDurationProps {
+ namespace: string;
+ timespan: number;
+ interval: number;
+ parentName?: string;
+ summaryData?: SummaryProps;
+ bordered?: boolean;
+}
+
+const PipelineRunsDurationCardK8s: React.FC = ({
+ namespace,
+ timespan,
+ parentName,
+ interval,
+ bordered,
+}) => {
+ const { t } = useTranslation('plugin__pipelines-console-plugin');
+
+ const [totalPipelineRunsCountData] =
+ parentName && namespace
+ ? usePipelineMetricsForNamespaceForPipelinePoll({
+ namespace,
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ name: parentName,
+ metricsQuery:
+ PipelineQuery.PIPELINERUN_COUNT_FOR_NAMESPACE_FOR_PIPELINE,
+ })
+ : namespace == ALL_NAMESPACES_KEY
+ ? usePipelineMetricsForAllNamespacePoll({
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ metricsQuery: PipelineQuery.PIPELINERUN_COUNT_FOR_ALL_NAMESPACE,
+ })
+ : usePipelineMetricsForNamespacePoll({
+ namespace,
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ metricsQuery: PipelineQuery.PIPELINERUN_COUNT_FOR_NAMESPACE,
+ });
+ const [tickValues, type] = getXaxisValues(timespan);
+
+ const totalPipelineRuns = getTotalPipelineRuns(
+ totalPipelineRunsCountData,
+ tickValues,
+ type,
+ );
+
+ const [totalPipelineRunsDurationData] =
+ parentName && namespace
+ ? usePipelineMetricsForNamespaceForPipelinePoll({
+ namespace,
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ name: parentName,
+ metricsQuery:
+ PipelineQuery.PIPELINERUN_DURATION_FOR_NAMESPACE_FOR_PIPELINE,
+ })
+ : namespace == ALL_NAMESPACES_KEY
+ ? usePipelineMetricsForAllNamespacePoll({
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ metricsQuery: PipelineQuery.PIPELINERUN_DURATION_FOR_ALL_NAMESPACE,
+ })
+ : usePipelineMetricsForNamespacePoll({
+ namespace,
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ metricsQuery: PipelineQuery.PIPELINERUN_DURATION_FOR_NAMESPACE,
+ });
+
+ const [totalPipelineRunsDuration, totalPipelineRunsDurationValue] =
+ getTotalPipelineRunsDuration(
+ totalPipelineRunsDurationData,
+ tickValues,
+ type,
+ );
+
+ const averageDuration = getPipelineRunAverageDuration(
+ totalPipelineRunsDurationValue,
+ totalPipelineRuns,
+ );
+
+ return (
+ <>
+
+
+ {t('Duration')}
+
+
+
+
+
+
+
+ {t('Average duration')}
+
+
+
+ {averageDuration}
+
+
+
+
+
+
+ {t('Maximum')}
+
+
+
+ {'-'}
+
+
+
+
+
+
+ {t('Total duration')}
+
+
+
+ {totalPipelineRunsDuration ?? '-'}
+
+
+
+
+ >
+ );
+};
+
+export default PipelineRunsDurationCardK8s;
diff --git a/src/components/pipelines-overview/PipelineRunsNumbersChartK8s.tsx b/src/components/pipelines-overview/PipelineRunsNumbersChartK8s.tsx
new file mode 100644
index 00000000..76d5c536
--- /dev/null
+++ b/src/components/pipelines-overview/PipelineRunsNumbersChartK8s.tsx
@@ -0,0 +1,272 @@
+import * as React from 'react';
+import * as _ from 'lodash';
+import * as classNames from 'classnames';
+import { useTranslation } from 'react-i18next';
+import { DomainPropType, DomainTuple } from 'victory-core';
+import {
+ Chart,
+ ChartAxis,
+ ChartAxisProps,
+ ChartBar,
+ ChartGroup,
+ ChartThemeColor,
+ ChartVoronoiContainer,
+} from '@patternfly/react-charts';
+import { Card, CardBody, CardTitle } from '@patternfly/react-core';
+import {
+ formatDate,
+ getXaxisValues,
+ hourformat,
+ parsePrometheusDuration,
+ monthYear,
+} from './dateTime';
+import { ALL_NAMESPACES_KEY } from '../../consts';
+import {
+ usePipelineMetricsForAllNamespacePoll,
+ usePipelineMetricsForNamespaceForPipelinePoll,
+ usePipelineMetricsForNamespacePoll,
+} from '../pipelines-metrics/hooks';
+import {
+ MetricsQueryPrefix,
+ PipelineQuery,
+ adjustToStartOfWeek,
+} from '../pipelines-metrics/utils';
+
+interface PipelinesRunsNumbersChartProps {
+ namespace?: string;
+ timespan?: number;
+ interval?: number;
+ domain?: DomainPropType;
+ parentName?: string;
+ bordered?: boolean;
+ width?: number;
+}
+type DomainType = { x?: DomainTuple; y?: DomainTuple };
+
+const metricsToSummary = (
+ prometheusResult: any,
+): { group_value: number; total: number }[] => {
+ let summaryResponse = [];
+ if (prometheusResult?.data?.result[0]?.values) {
+ summaryResponse = prometheusResult?.data?.result[0]?.values.map(
+ (value, index, array) => {
+ const previousValue = index > 0 ? parseInt(array[index - 1][1]) : 0;
+ const currentValue = parseInt(value[1]);
+ const total = currentValue - previousValue;
+ return {
+ group_value: value[0],
+ total: total < 0 ? currentValue : total,
+ };
+ },
+ );
+ return summaryResponse;
+ }
+ return summaryResponse;
+};
+
+const getChartData = (
+ tickValues: number[] | Date[],
+ data: any,
+ type: string,
+) => {
+ const sortedTickValues = tickValues.slice().sort((a, b) => a - b);
+ const chartData = sortedTickValues?.map((value, index) => {
+ if (index === 0) {
+ // Ensure the first value is always 0
+ return {
+ x: value,
+ y: 0,
+ };
+ }
+ const s = data?.find((d) => {
+ const group_date = new Date(Number(d.group_value) * 1000);
+ if (type == 'hour') {
+ return group_date.getHours() === value;
+ }
+ if (type == 'week') {
+ const adjustedGroupDate = adjustToStartOfWeek(new Date(group_date));
+ return (
+ adjustedGroupDate.toDateString() === new Date(value).toDateString()
+ );
+ }
+ if (type == 'day') {
+ return group_date.toDateString() === new Date(value).toDateString();
+ }
+ if (type == 'month') {
+ return group_date.getMonth() === value.getMonth();
+ }
+ });
+ return {
+ x: value,
+ y: s?.total || 0,
+ };
+ });
+ return chartData;
+};
+
+const PipelineRunsNumbersChartK8s: React.FC = ({
+ namespace,
+ timespan,
+ interval,
+ domain,
+ parentName,
+ bordered,
+ width = 530,
+}) => {
+ const { t } = useTranslation('plugin__pipelines-console-plugin');
+ const startTimespan = timespan - parsePrometheusDuration('1d');
+ const endDate = new Date(Date.now()).setHours(0, 0, 0, 0);
+ const startDate = new Date(Date.now() - startTimespan).setHours(0, 0, 0, 0);
+ const { x: domainX, y: domainY } = (domain as DomainType) || {};
+ const domainValue: DomainPropType = {
+ x: domainX || [startDate, endDate],
+ y: domainY || undefined,
+ };
+
+ const [runSuccessRatioData] =
+ parentName && namespace
+ ? usePipelineMetricsForNamespaceForPipelinePoll({
+ namespace,
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ name: parentName,
+ metricsQuery:
+ PipelineQuery.PIPELINERUN_COUNT_FOR_NAMESPACE_FOR_PIPELINE,
+ })
+ : namespace == ALL_NAMESPACES_KEY
+ ? usePipelineMetricsForAllNamespacePoll({
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ metricsQuery: PipelineQuery.PIPELINERUN_COUNT_FOR_ALL_NAMESPACE,
+ })
+ : usePipelineMetricsForNamespacePoll({
+ namespace,
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ metricsQuery: PipelineQuery.PIPELINERUN_COUNT_FOR_NAMESPACE,
+ });
+ const convertToSummaryData = metricsToSummary(runSuccessRatioData);
+
+ const [tickValues, type] = getXaxisValues(timespan);
+
+ let xTickFormat;
+ let dayLabel;
+ let showLabel = false;
+ let chartData = [];
+ switch (type) {
+ case 'hour':
+ xTickFormat = (d) => hourformat(d);
+ showLabel = true;
+ domainValue.x = [0, 23];
+ dayLabel = formatDate(new Date());
+ chartData = getChartData(tickValues, convertToSummaryData, 'hour');
+ break;
+ case 'day':
+ xTickFormat = (d) => formatDate(d);
+ domainValue.x = [startDate, endDate];
+ chartData = getChartData(tickValues, convertToSummaryData, 'day');
+ break;
+ case 'week':
+ xTickFormat = (d) => formatDate(d);
+ domainValue.x = [new Date(tickValues[0]), new Date(tickValues[11])];
+ chartData = getChartData(tickValues, convertToSummaryData, 'week');
+ break;
+ case 'month':
+ xTickFormat = (d) => monthYear(d);
+ domainValue.x = [new Date(tickValues[0]), new Date(tickValues[11])];
+ chartData = getChartData(tickValues, convertToSummaryData, 'month');
+ break;
+ default:
+ console.log('Received wrong data');
+ break;
+ }
+ const max: number = Math.max(...chartData.map((yVal) => yVal.y));
+ !isNaN(max) && max > 5
+ ? (domainValue.y = [0, max])
+ : (domainValue.y = [0, 5]);
+
+ if (!domainY) {
+ let minY: number = _.minBy(chartData, 'y')?.y ?? 0;
+ let maxY: number = _.maxBy(chartData, 'y')?.y ?? 0;
+ if (minY === 0 && maxY === 0) {
+ minY = -1;
+ maxY = 1;
+ } else if (minY > 0 && maxY > 0) {
+ minY = 0;
+ } else if (minY < 0 && maxY < 0) {
+ maxY = 0;
+ }
+ domainValue.y = [minY, maxY];
+ }
+
+ let xAxisStyle: ChartAxisProps['style'] = {
+ tickLabels: { fill: 'var(--pf-v5-global--Color--100)', fontSize: 12 },
+ };
+ const yAxisStyle: ChartAxisProps['style'] = {
+ tickLabels: { fill: 'var(--pf-v5-global--Color--100)', fontSize: 12 },
+ };
+ if (tickValues.length > 7) {
+ xAxisStyle = {
+ tickLabels: {
+ fill: 'var(--pf-v5-global--Color--100)',
+ angle: 320,
+ fontSize: 10,
+ textAnchor: 'end',
+ verticalAnchor: 'end',
+ },
+ };
+ }
+
+ return (
+ <>
+
+
+ {t('Number of PipelineRuns')}
+
+
+
+ `${datum.y}`}
+ constrainToVisibleArea
+ />
+ }
+ scale={{ x: 'time', y: 'linear' }}
+ domain={domainValue}
+ domainPadding={{ x: [30, 25] }}
+ height={145}
+ width={width}
+ padding={{
+ top: 10,
+ bottom: 55,
+ left: 50,
+ }}
+ themeColor={ChartThemeColor.blue}
+ >
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default PipelineRunsNumbersChartK8s;
diff --git a/src/components/pipelines-overview/PipelineRunsStatusCardK8s.tsx b/src/components/pipelines-overview/PipelineRunsStatusCardK8s.tsx
new file mode 100644
index 00000000..28c13c2b
--- /dev/null
+++ b/src/components/pipelines-overview/PipelineRunsStatusCardK8s.tsx
@@ -0,0 +1,597 @@
+import * as React from 'react';
+import * as classNames from 'classnames';
+import { useTranslation } from 'react-i18next';
+import { DomainPropType, DomainTuple } from 'victory-core';
+import {
+ Chart,
+ ChartAxis,
+ ChartAxisProps,
+ ChartDonut,
+ ChartGroup,
+ ChartLabel,
+ ChartLegend,
+ ChartLine,
+ ChartVoronoiContainer,
+} from '@patternfly/react-charts';
+import {
+ Card,
+ CardBody,
+ CardTitle,
+ Grid,
+ GridItem,
+ Popover,
+} from '@patternfly/react-core';
+import { chart_color_black_200 as othersColor } from '@patternfly/react-tokens/dist/js/chart_color_black_200';
+import { chart_color_black_500 as cancelledColor } from '@patternfly/react-tokens/dist/js/chart_color_black_500';
+import { chart_color_green_400 as successColor } from '@patternfly/react-tokens/dist/js/chart_color_green_400';
+import { global_danger_color_100 as failureColor } from '@patternfly/react-tokens/dist/js/global_danger_color_100';
+import { chart_color_blue_300 as runningColor } from '@patternfly/react-tokens/dist/js/chart_color_blue_300';
+import {
+ formatDate,
+ getXaxisValues,
+ hourformat,
+ parsePrometheusDuration,
+ monthYear,
+} from './dateTime';
+import './PipelinesOverview.scss';
+import { ALL_NAMESPACES_KEY } from '../../consts';
+import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
+import { PrometheusResponse } from '@openshift-console/dynamic-plugin-sdk';
+import {
+ usePipelineMetricsForAllNamespacePoll,
+ usePipelineMetricsForNamespacePoll,
+ usePipelineMetricsForNamespaceForPipelinePoll,
+} from '../pipelines-metrics/hooks';
+import {
+ MetricsQueryPrefix,
+ PipelineQuery,
+ adjustToStartOfWeek,
+} from '../pipelines-metrics/utils';
+import { getTotalPipelineRuns, isMatchingFirstTickValue } from './utils';
+
+interface PipelinesRunsStatusCardProps {
+ timespan?: number;
+ domain?: DomainPropType;
+ bordered?: boolean;
+ namespace: string;
+ interval: number;
+ parentName?: string;
+}
+
+type DomainType = { x?: DomainTuple; y?: DomainTuple };
+
+const getStatusSummary = (promQueryToSummaryResponse) => {
+ const result = {
+ cancelled: 0,
+ failed: 0,
+ succeeded: 0,
+ total: 0,
+ };
+ promQueryToSummaryResponse?.forEach((item) => {
+ result.cancelled += item.cancelled;
+ result.failed += item.failed;
+ result.succeeded += item.succeeded;
+ result.total = result.cancelled + result.failed + result.succeeded;
+ });
+
+ return result;
+};
+
+export const getChartDataK8s = (
+ tickValues: number[] | Date[],
+ data: any,
+ key: string,
+ type: string,
+): {
+ x: number;
+ y: number;
+ name: string;
+}[] => {
+ const { t } = useTranslation('plugin__pipelines-console-plugin');
+ const k = key.toLowerCase();
+ const sortedTickValues = tickValues.slice().sort((a, b) => a - b);
+ const chartData = sortedTickValues?.map((value, index) => {
+ if (index === 0) {
+ // Ensure the first value is always 0
+ return {
+ x: value,
+ y: 0,
+ name: t(key),
+ };
+ }
+ const s = data?.find((d) => {
+ const group_date = new Date(Number(d.group_value) * 1000);
+ if (type == 'hour') {
+ return group_date.getHours() === value;
+ }
+ if (type == 'week') {
+ const adjustedGroupDate = adjustToStartOfWeek(new Date(group_date));
+ return (
+ adjustedGroupDate.toDateString() === new Date(value).toDateString()
+ );
+ }
+ if (type == 'day') {
+ return group_date.toDateString() === new Date(value).toDateString();
+ }
+ if (type == 'month') {
+ return group_date.getMonth() === value.getMonth();
+ }
+ });
+ return {
+ x: value,
+ y: Math.round((100 * s?.[k]) / s?.total) || 0,
+ name: t(key),
+ };
+ });
+ return chartData;
+};
+
+export const getIncrementedValues = (
+ data,
+ tickValues: number[] | Date[],
+ type: string,
+) => {
+ const originalData = JSON.parse(JSON.stringify(data));
+ for (let i = 1; i < data.length; i++) {
+ const prevCancelled = originalData[i - 1].cancelled;
+ const currentCancelled = originalData[i].cancelled;
+ data[i].cancelled =
+ currentCancelled >= prevCancelled
+ ? currentCancelled - prevCancelled
+ : currentCancelled;
+
+ const prevFailed = originalData[i - 1].failed;
+ const currentFailed = originalData[i].failed;
+ data[i].failed =
+ currentFailed >= prevFailed ? currentFailed - prevFailed : currentFailed;
+
+ const prevSucceeded = originalData[i - 1].succeeded;
+ const currentSucceeded = originalData[i].succeeded;
+ data[i].succeeded =
+ currentSucceeded >= prevSucceeded
+ ? currentSucceeded - prevSucceeded
+ : currentSucceeded;
+ }
+
+ const firstTickValue = tickValues[0];
+ data.forEach((item) => {
+ item.total = item.cancelled + item.failed + item.succeeded;
+ const isMatch = isMatchingFirstTickValue(
+ firstTickValue,
+ item.group_value,
+ type,
+ );
+ if (isMatch) {
+ data[0].cancelled = 0;
+ data[0].failed = 0;
+ data[0].succeeded = 0;
+ }
+ });
+
+ return data;
+};
+
+const transformPrometheusResultToSummary = (
+ prometheusResult: PrometheusResponse,
+ tickValues: number[] | Date[],
+ type: string,
+) => {
+ const summary = [];
+ if (
+ prometheusResult &&
+ prometheusResult.data &&
+ prometheusResult.data.result
+ ) {
+ prometheusResult.data.result.forEach((metric) => {
+ metric.values.forEach((value) => {
+ const groupValue = value[0];
+
+ let summaryObj = summary.find((obj) => obj.group_value === groupValue);
+
+ if (!summaryObj) {
+ summaryObj = {
+ group_value: groupValue,
+ cancelled: 0,
+ failed: 0,
+ succeeded: 0,
+ };
+ summary.push(summaryObj);
+ }
+
+ switch (metric.metric.status) {
+ case 'cancelled':
+ summaryObj.cancelled = parseInt(value[1], 10);
+ break;
+ case 'failed':
+ summaryObj.failed = parseInt(value[1], 10);
+ break;
+ case 'success':
+ summaryObj.succeeded = parseInt(value[1], 10);
+ break;
+ default:
+ break;
+ }
+ });
+ });
+ }
+ summary.sort((a, b) => a.group_value - b.group_value);
+ const finalResult = getIncrementedValues(summary, tickValues, type);
+ return finalResult;
+};
+
+const PipelineRunsStatusCardK8s: React.FC = ({
+ timespan,
+ domain,
+ bordered,
+ namespace,
+ interval,
+ parentName,
+}) => {
+ const { t } = useTranslation('plugin__pipelines-console-plugin');
+ const startTimespan = timespan - parsePrometheusDuration('1d');
+ const endDate = new Date(Date.now()).setHours(0, 0, 0, 0);
+ const startDate = new Date(Date.now() - startTimespan).setHours(0, 0, 0, 0);
+ const { x: domainX, y: domainY } = (domain as DomainType) || {};
+ const domainValue: DomainPropType = {
+ x: domainX || [startDate, endDate],
+ y: domainY || undefined,
+ };
+ const [runSuccessRatioData] =
+ parentName && namespace
+ ? usePipelineMetricsForNamespaceForPipelinePoll({
+ namespace,
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ name: parentName,
+ metricsQuery:
+ PipelineQuery.PIPELINERUN_COUNT_FOR_STATUS_FOR_NAMESPACE_FOR_PIPELINE,
+ })
+ : namespace == ALL_NAMESPACES_KEY
+ ? usePipelineMetricsForAllNamespacePoll({
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ metricsQuery:
+ PipelineQuery.PIPELINERUN_COUNT_FOR_STATUS_FOR_ALL_NAMESPACE,
+ })
+ : usePipelineMetricsForNamespacePoll({
+ namespace,
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ metricsQuery:
+ PipelineQuery.PIPELINERUN_COUNT_FOR_STATUS_FOR_NAMESPACE,
+ });
+ const [totalPipelineRunsData] =
+ parentName && namespace
+ ? usePipelineMetricsForNamespaceForPipelinePoll({
+ namespace,
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ name: parentName,
+ metricsQuery:
+ PipelineQuery.PIPELINERUN_COUNT_FOR_NAMESPACE_FOR_PIPELINE,
+ })
+ : namespace == ALL_NAMESPACES_KEY
+ ? usePipelineMetricsForAllNamespacePoll({
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ metricsQuery: PipelineQuery.PIPELINERUN_COUNT_FOR_ALL_NAMESPACE,
+ })
+ : usePipelineMetricsForNamespacePoll({
+ namespace,
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ metricsQuery: PipelineQuery.PIPELINERUN_COUNT_FOR_NAMESPACE,
+ });
+
+ const [tickValues, type] = getXaxisValues(timespan);
+
+ const totalPipelineRuns = getTotalPipelineRuns(
+ totalPipelineRunsData,
+ tickValues,
+ type,
+ );
+
+ const promQueryToSummaryResponse = transformPrometheusResultToSummary(
+ runSuccessRatioData,
+ tickValues,
+ type,
+ );
+
+ let xTickFormat;
+ let dayLabel;
+ let showLabel = false;
+ let chartDataSucceededK8s = [];
+ let chartDataFailedK8s = [];
+ let chartDataCancelledK8s = [];
+ switch (type) {
+ case 'hour':
+ xTickFormat = (d) => hourformat(d);
+ showLabel = true;
+ domainValue.x = [0, 23];
+
+ dayLabel = formatDate(new Date());
+
+ chartDataCancelledK8s = getChartDataK8s(
+ tickValues,
+ promQueryToSummaryResponse,
+ 'Cancelled',
+ 'hour',
+ );
+ chartDataSucceededK8s = getChartDataK8s(
+ tickValues,
+ promQueryToSummaryResponse,
+ 'Succeeded',
+ 'hour',
+ );
+ chartDataFailedK8s = getChartDataK8s(
+ tickValues,
+ promQueryToSummaryResponse,
+ 'Failed',
+ 'hour',
+ );
+ break;
+ case 'day':
+ xTickFormat = (d) => formatDate(d);
+ domainValue.x = [startDate, endDate];
+
+ chartDataCancelledK8s = getChartDataK8s(
+ tickValues,
+ promQueryToSummaryResponse,
+ 'Cancelled',
+ 'day',
+ );
+ chartDataSucceededK8s = getChartDataK8s(
+ tickValues,
+ promQueryToSummaryResponse,
+ 'Succeeded',
+ 'day',
+ );
+ chartDataFailedK8s = getChartDataK8s(
+ tickValues,
+ promQueryToSummaryResponse,
+ 'Failed',
+ 'day',
+ );
+
+ break;
+ case 'week':
+ xTickFormat = (d) => formatDate(d);
+ domainValue.x = [new Date(tickValues[0]), new Date(tickValues[11])];
+
+ chartDataCancelledK8s = getChartDataK8s(
+ tickValues,
+ promQueryToSummaryResponse,
+ 'Cancelled',
+ 'week',
+ );
+ chartDataSucceededK8s = getChartDataK8s(
+ tickValues,
+ promQueryToSummaryResponse,
+ 'Succeeded',
+ 'week',
+ );
+ chartDataFailedK8s = getChartDataK8s(
+ tickValues,
+ promQueryToSummaryResponse,
+ 'Failed',
+ 'week',
+ );
+
+ break;
+ case 'month':
+ xTickFormat = (d) => monthYear(d);
+ domainValue.x = [new Date(tickValues[0]), new Date(tickValues[11])];
+
+ chartDataCancelledK8s = getChartDataK8s(
+ tickValues,
+ promQueryToSummaryResponse,
+ 'Cancelled',
+ 'month',
+ );
+ chartDataSucceededK8s = getChartDataK8s(
+ tickValues,
+ promQueryToSummaryResponse,
+ 'Succeeded',
+ 'month',
+ );
+ chartDataFailedK8s = getChartDataK8s(
+ tickValues,
+ promQueryToSummaryResponse,
+ 'Failed',
+ 'month',
+ );
+
+ break;
+ default:
+ console.log('Received wrong data');
+ break;
+ }
+
+ let xAxisStyle: ChartAxisProps['style'] = {
+ tickLabels: { fill: 'var(--pf-v5-global--Color--100)' },
+ };
+ const yAxisStyle: ChartAxisProps['style'] = {
+ tickLabels: { fill: 'var(--pf-v5-global--Color--100)' },
+ };
+ if (tickValues?.length > 7) {
+ xAxisStyle = {
+ tickLabels: {
+ fill: 'var(--pf-v5-global--Color--100)',
+ angle: 320,
+ fontSize: 10,
+ textAnchor: 'end',
+ verticalAnchor: 'end',
+ },
+ };
+ }
+
+ const colorScale = [
+ successColor.value,
+ failureColor.value,
+ runningColor.value,
+ cancelledColor.value,
+ othersColor.value,
+ ];
+
+ const colorScaleLineChart = [
+ successColor.value,
+ failureColor.value,
+ cancelledColor.value,
+ othersColor.value,
+ ];
+ const donutDataObjK8s = getStatusSummary(promQueryToSummaryResponse);
+ const donutDataK8s = [
+ {
+ x: t('Succeeded'),
+ y: Math.round((100 * donutDataObjK8s.succeeded) / donutDataObjK8s.total),
+ },
+ {
+ x: t('Failed'),
+ y: Math.round((100 * donutDataObjK8s.failed) / donutDataObjK8s.total),
+ },
+
+ {
+ x: t('Cancelled'),
+ y: Math.round((100 * donutDataObjK8s.cancelled) / donutDataObjK8s.total),
+ },
+ ];
+
+ const legendData = donutDataK8s.map((data) => {
+ return {
+ name: `${data.x}: ${isNaN(data.y) ? 0 : data.y}%`,
+ };
+ });
+ return (
+ <>
+
+
+
+ {t('PipelineRun status')}{' '}
+
+ {t(
+ 'PipelineRun status shows the % of PipelineRuns for various statuses like "Succeeded", "Failed" and "Cancelled".',
+ )}
+ >
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+ `${datum.x}: ${datum.y}%`}
+ legendData={legendData}
+ colorScale={colorScale}
+ legendOrientation="vertical"
+ legendPosition="right"
+ padding={{
+ bottom: 30,
+ right: 140, // Adjusted to accommodate legend
+ top: 20,
+ }}
+ legendComponent={
+
+ }
+ subTitle={t('Succeeded')}
+ subTitleComponent={
+
+ }
+ title={`${donutDataObjK8s.succeeded}/${totalPipelineRuns}`}
+ titleComponent={
+
+ }
+ width={350}
+ />
+
+
+
+
+ `${datum.name}: ${datum.y}%`}
+ constrainToVisibleArea
+ />
+ }
+ scale={{ x: 'time', y: 'linear' }}
+ domain={domainValue}
+ domainPadding={{ x: [30, 25] }}
+ height={200}
+ padding={{
+ top: 20,
+ bottom: 40,
+ right: 40,
+ left: 50,
+ }}
+ colorScale={colorScaleLineChart}
+ width={1000}
+ >
+
+ `${v}%`}
+ style={yAxisStyle}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default PipelineRunsStatusCardK8s;
diff --git a/src/components/pipelines-overview/PipelineRunsTotalCardK8s.tsx b/src/components/pipelines-overview/PipelineRunsTotalCardK8s.tsx
new file mode 100644
index 00000000..22d64cda
--- /dev/null
+++ b/src/components/pipelines-overview/PipelineRunsTotalCardK8s.tsx
@@ -0,0 +1,135 @@
+import * as React from 'react';
+import * as classNames from 'classnames';
+import { useTranslation } from 'react-i18next';
+import { CheckIcon } from '@patternfly/react-icons';
+import {
+ Card,
+ CardBody,
+ CardTitle,
+ Divider,
+ Grid,
+ GridItem,
+ Label,
+} from '@patternfly/react-core';
+import { SummaryProps, getTotalPipelineRuns } from './utils';
+import { PipelineModel, RepositoryModel } from '../../models';
+import { ALL_NAMESPACES_KEY } from '../../consts';
+
+import './PipelineRunsTotalCard.scss';
+import { MetricsQueryPrefix, PipelineQuery } from '../pipelines-metrics/utils';
+import {
+ usePipelineMetricsForAllNamespacePoll,
+ usePipelineMetricsForNamespacePoll,
+} from '../pipelines-metrics/hooks';
+import { getXaxisValues } from './dateTime';
+
+interface PipelinesRunsDurationProps {
+ namespace: string;
+ timespan: number;
+ interval: number;
+ summaryData?: SummaryProps;
+ bordered?: boolean;
+}
+
+const PipelineRunsTotalCardK8s: React.FC = ({
+ namespace,
+ timespan,
+ interval,
+ bordered,
+}) => {
+ const { t } = useTranslation('plugin__pipelines-console-plugin');
+
+ const [totalPipelineRunsData] =
+ namespace == ALL_NAMESPACES_KEY
+ ? usePipelineMetricsForAllNamespacePoll({
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ metricsQuery: PipelineQuery.PIPELINERUN_COUNT_FOR_ALL_NAMESPACE,
+ })
+ : usePipelineMetricsForNamespacePoll({
+ namespace,
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ metricsQuery: PipelineQuery.PIPELINERUN_COUNT_FOR_NAMESPACE,
+ });
+ const [tickValues, type] = getXaxisValues(timespan);
+
+ const totalPipelineRuns = getTotalPipelineRuns(
+ totalPipelineRunsData,
+ tickValues,
+ type,
+ );
+
+ return (
+ <>
+
+
+ {t('Total runs')}
+
+
+
+
+
+
+
+ {t('Runs in pipelines')}
+
+
+
+ {'-'}
+
+
+
+
+
+
+ {t('Runs in repositories')}
+
+
+
+ {'-'}
+
+
+
+
+
+
+ {t('Total runs')}
+
+
+
+ {totalPipelineRuns}
+
+
+
+
+ >
+ );
+};
+
+export default PipelineRunsTotalCardK8s;
diff --git a/src/components/pipelines-overview/PipelinesOverview.scss b/src/components/pipelines-overview/PipelinesOverview.scss
index 1ade8f54..0aecc8cb 100644
--- a/src/components/pipelines-overview/PipelinesOverview.scss
+++ b/src/components/pipelines-overview/PipelinesOverview.scss
@@ -149,3 +149,9 @@
padding-top: var(--pf-v5-global--spacer--md);
padding-bottom: var(--pf-v5-global--spacer--sm);
}
+
+.k8s-overview-info-alert {
+ margin-bottom: var(--pf-v5-global--spacer--md);
+ margin-left: var(--pf-v5-global--spacer--md);
+ margin-right: var(--pf-v5-global--spacer--md);
+}
diff --git a/src/components/pipelines-overview/PipelinesOverviewPageK8s.tsx b/src/components/pipelines-overview/PipelinesOverviewPageK8s.tsx
new file mode 100644
index 00000000..60e95872
--- /dev/null
+++ b/src/components/pipelines-overview/PipelinesOverviewPageK8s.tsx
@@ -0,0 +1,131 @@
+import * as React from 'react';
+import { useTranslation } from 'react-i18next';
+import { Flex, FlexItem } from '@patternfly/react-core';
+import { formatPrometheusDuration, parsePrometheusDuration } from './dateTime';
+import NameSpaceDropdown from './NamespaceDropdown';
+import TimeRangeDropdown from './TimeRangeDropdown';
+import RefreshDropdown from './RefreshDropdown';
+import { IntervalOptions, TimeRangeOptionsK8s, useQueryParams } from './utils';
+import { useActiveNamespace } from '@openshift-console/dynamic-plugin-sdk';
+import PipelineRunsStatusCardK8s from './PipelineRunsStatusCardK8s';
+import PipelineRunsNumbersChartK8s from './PipelineRunsNumbersChartK8s';
+import PipelineRunsTotalCardK8s from './PipelineRunsTotalCardK8s';
+import PipelineRunsDurationCardK8s from './PipelineRunsDurationCardK8s';
+import PipelineRunsListPageK8s from './list-pages/PipelineRunsListPageK8s';
+import { K8sDataLimitationAlert } from './K8sDataLimitationAlert';
+import './PipelinesOverview.scss';
+
+const PipelinesOverviewPageK8s: React.FC = () => {
+ const { t } = useTranslation('plugin__pipelines-console-plugin');
+ const [activeNamespace, setActiveNamespace] = useActiveNamespace();
+ const [namespace, setNamespace] = React.useState(activeNamespace);
+ const [timespan, setTimespan] = React.useState(parsePrometheusDuration('1d'));
+ const [interval, setInterval] = React.useState(
+ parsePrometheusDuration('30s'),
+ );
+ React.useEffect(() => {
+ setActiveNamespace(namespace);
+ }, [namespace]);
+
+ useQueryParams({
+ key: 'refreshinterval',
+ value: interval,
+ setValue: setInterval,
+ defaultValue: parsePrometheusDuration('30s'),
+ options: { ...IntervalOptions(), off: 'OFF_KEY' },
+ displayFormat: (v) => (v ? formatPrometheusDuration(v) : 'off'),
+ loadFormat: (v) => (v == 'off' ? null : parsePrometheusDuration(v)),
+ });
+
+ useQueryParams({
+ key: 'timerange',
+ value: timespan,
+ setValue: setTimespan,
+ defaultValue: parsePrometheusDuration('1w'),
+ options: TimeRangeOptionsK8s(),
+ displayFormat: formatPrometheusDuration,
+ loadFormat: parsePrometheusDuration,
+ });
+
+ return (
+ <>
+
+
+ {t('Overview')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default PipelinesOverviewPageK8s;
diff --git a/src/components/pipelines-overview/TimeRangeDropdown.tsx b/src/components/pipelines-overview/TimeRangeDropdown.tsx
index b2684eec..9e82d7a6 100644
--- a/src/components/pipelines-overview/TimeRangeDropdown.tsx
+++ b/src/components/pipelines-overview/TimeRangeDropdown.tsx
@@ -8,8 +8,10 @@ import {
} from '@patternfly/react-core';
import { map } from 'lodash';
import { useTranslation } from 'react-i18next';
+import { useFlag } from '@openshift-console/dynamic-plugin-sdk';
import { formatPrometheusDuration, parsePrometheusDuration } from './dateTime';
-import { TimeRangeOptions } from './utils';
+import { TimeRangeOptions, TimeRangeOptionsK8s } from './utils';
+import { FLAG_PIPELINE_TEKTON_RESULT_INSTALLED } from '../../consts';
interface TimeRangeDropdownProps {
timespan: number;
@@ -28,7 +30,11 @@ const TimeRangeDropdown: React.FC = ({
[setTimespan],
);
const { t } = useTranslation('plugin__pipelines-console-plugin');
- const timeRangeOptions = TimeRangeOptions();
+ const isTektonResultEnabled = useFlag(FLAG_PIPELINE_TEKTON_RESULT_INSTALLED);
+
+ const timeRangeOptions = isTektonResultEnabled
+ ? TimeRangeOptions()
+ : TimeRangeOptionsK8s();
return (
diff --git a/src/components/pipelines-overview/__tests__/PipelinesOverview.spec.tsx b/src/components/pipelines-overview/__tests__/PipelinesOverview.spec.tsx
index dbebdb56..016ff926 100644
--- a/src/components/pipelines-overview/__tests__/PipelinesOverview.spec.tsx
+++ b/src/components/pipelines-overview/__tests__/PipelinesOverview.spec.tsx
@@ -5,6 +5,7 @@ import {
useK8sWatchResource,
useActiveColumns,
useActiveNamespace,
+ useFlag,
} from '@openshift-console/dynamic-plugin-sdk';
import PipelinesOverviewPage from '../PipelinesOverviewPage';
import { getResultsSummary } from '../../utils/summary-api';
@@ -18,6 +19,7 @@ jest.mock('@openshift-console/dynamic-plugin-sdk', () => ({
useActiveColumns: jest.fn(),
VirtualizedTable: jest.fn(),
k8sGet: jest.fn(),
+ useFlag: jest.fn(),
}));
jest.mock('../../utils/tekton-results', () => ({
createTektonResultsSummaryUrl: jest.fn(),
@@ -37,6 +39,7 @@ const useActiveNamespaceMock = useActiveNamespace as jest.Mock;
const useK8sWatchResourceMock = useK8sWatchResource as jest.Mock;
const useActiveColumnsMock = useActiveColumns as jest.Mock;
const getResultsSummaryMock = getResultsSummary as jest.Mock;
+const useFlagMock = useFlag as jest.Mock;
describe('Pipeline Overview page', () => {
beforeEach(() => {
@@ -48,6 +51,7 @@ describe('Pipeline Overview page', () => {
useK8sWatchResourceMock.mockReturnValue([[], true]);
useActiveColumnsMock.mockReturnValue([[]]);
getResultsSummaryMock.mockReturnValue(Promise.resolve({}));
+ useFlagMock.mockReturnValue(true);
});
it('should render Pipeline Overview', async () => {
diff --git a/src/components/pipelines-overview/dateTime.ts b/src/components/pipelines-overview/dateTime.ts
index ebcea892..be8bfae1 100644
--- a/src/components/pipelines-overview/dateTime.ts
+++ b/src/components/pipelines-overview/dateTime.ts
@@ -161,6 +161,19 @@ export const formatTime = (time: string): string => {
return timestring;
};
+export const secondsToHms = (seconds: number): string => {
+ const h = Math.floor(seconds / 3600)
+ .toString()
+ .padStart(2, '0');
+ const m = Math.floor((seconds % 3600) / 60)
+ .toString()
+ .padStart(2, '0');
+ const s = Math.floor(seconds % 60)
+ .toString()
+ .padStart(2, '0');
+ return `${h}:${m}:${s}`;
+};
+
export const formatTimeLastRunTime = (time: number): string => {
if (!time) {
return '-';
@@ -206,7 +219,9 @@ export const formatDate = (date: Date) => {
export const timeToMinutes = (timeString: string): number => {
// Parse the time string
const match = timeString?.split(/[:]+/);
-
+ if (!timeString) {
+ return null;
+ }
if (match) {
// Extract components
const hours = parseInt(match[0]);
diff --git a/src/components/pipelines-overview/index.ts b/src/components/pipelines-overview/index.ts
index d1dcd739..e87504fd 100644
--- a/src/components/pipelines-overview/index.ts
+++ b/src/components/pipelines-overview/index.ts
@@ -1 +1,2 @@
export { default as PipelinesOverviewPage } from './PipelinesOverviewPage';
+export { default as PipelinesOverviewPageK8s } from './PipelinesOverviewPageK8s';
diff --git a/src/components/pipelines-overview/list-pages/PipelineRunsForPipelinesList.tsx b/src/components/pipelines-overview/list-pages/PipelineRunsForPipelinesList.tsx
index d12b6c06..789d536a 100644
--- a/src/components/pipelines-overview/list-pages/PipelineRunsForPipelinesList.tsx
+++ b/src/components/pipelines-overview/list-pages/PipelineRunsForPipelinesList.tsx
@@ -21,11 +21,12 @@ type PipelineRunsForPipelinesListProps = {
summaryData: SummaryProps[];
summaryDataFiltered?: SummaryProps[];
loaded: boolean;
+ hideLastRunTime?: boolean;
};
const PipelineRunsForPipelinesList: React.FC<
PipelineRunsForPipelinesListProps
-> = ({ summaryData, summaryDataFiltered, loaded }) => {
+> = ({ summaryData, summaryDataFiltered, loaded, hideLastRunTime }) => {
const { t } = useTranslation('plugin__pipelines-console-plugin');
const EmptyMsg = () => (
@@ -33,8 +34,8 @@ const PipelineRunsForPipelinesList: React.FC<
);
- const plrColumns = React.useMemo
[]>(
- () => [
+ const plrColumns = React.useMemo[]>(() => {
+ const columns: TableColumn[] = [
{
id: 'pipelineName',
title: t('Pipeline'),
@@ -98,17 +99,20 @@ const PipelineRunsForPipelinesList: React.FC<
},
},
},
- {
+ ];
+ if (!hideLastRunTime) {
+ columns.push({
id: 'lastRunTime',
title: t('Last run time'),
sort: (summary, direction: 'asc' | 'desc') =>
sortByTimestamp(summary, 'last_runtime', direction),
transforms: [sortable],
props: { className: tableColumnClasses[6] },
- },
- ],
- [t],
- );
+ });
+ }
+
+ return columns;
+ }, [t, hideLastRunTime]);
const [columns] = useActiveColumns({
columns: plrColumns,
@@ -125,6 +129,7 @@ const PipelineRunsForPipelinesList: React.FC<
loadError={false}
unfilteredData={summaryData}
EmptyMsg={EmptyMsg}
+ rowData={{ hideLastRunTime }}
/>
);
};
diff --git a/src/components/pipelines-overview/list-pages/PipelineRunsForPipelinesListK8s.tsx b/src/components/pipelines-overview/list-pages/PipelineRunsForPipelinesListK8s.tsx
new file mode 100644
index 00000000..56798d15
--- /dev/null
+++ b/src/components/pipelines-overview/list-pages/PipelineRunsForPipelinesListK8s.tsx
@@ -0,0 +1,147 @@
+import * as React from 'react';
+import { useTranslation } from 'react-i18next';
+import { EmptyState, EmptyStateVariant } from '@patternfly/react-core';
+import { sortable } from '@patternfly/react-table';
+import {
+ TableColumn,
+ VirtualizedTable,
+ useActiveColumns,
+} from '@openshift-console/dynamic-plugin-sdk';
+import {
+ SummaryProps,
+ sortByNumbers,
+ sortByProperty,
+ sortByTimestamp,
+ sortTimeStrings,
+ listPageTableColumnClasses as tableColumnClasses,
+} from '../utils';
+import PipelineRunsForPipelinesRowK8s from './PipelineRunsForPipelinesRowK8s';
+import { Project } from '../../../types';
+
+type PipelineRunsForPipelinesListProps = {
+ summaryData: SummaryProps[];
+ summaryDataFiltered?: SummaryProps[];
+ loaded: boolean;
+ hideLastRunTime?: boolean;
+ projects?: Project[];
+ projectsLoaded?: boolean;
+};
+
+const PipelineRunsForPipelinesListK8s: React.FC<
+ PipelineRunsForPipelinesListProps
+> = ({
+ summaryData,
+ summaryDataFiltered,
+ loaded,
+ hideLastRunTime,
+ projects,
+ projectsLoaded,
+}) => {
+ const { t } = useTranslation('plugin__pipelines-console-plugin');
+ const EmptyMsg = () => (
+
+ {t('No PipelineRuns found')}
+
+ );
+
+ const plrColumns = React.useMemo[]>(() => {
+ const columns: TableColumn[] = [
+ {
+ id: 'pipelineName',
+ title: t('Pipeline'),
+ sort: (summary, direction: 'asc' | 'desc') =>
+ sortByProperty(summary, 'pipelineName', direction),
+ transforms: [sortable],
+ props: { className: tableColumnClasses[0] },
+ },
+ {
+ id: 'namespace',
+ title: t('Project'),
+ sort: (summary, direction: 'asc' | 'desc') =>
+ sortByProperty(summary, 'namespace', direction),
+ transforms: [sortable],
+ props: { className: tableColumnClasses[1] },
+ },
+ {
+ id: 'total',
+ title: t('Total Pipelineruns'),
+ sort: 'total',
+ transforms: [sortable],
+ props: { className: tableColumnClasses[2] },
+ },
+ {
+ id: 'totalDuration',
+ title: t('Total duration'),
+ sort: (summary, direction: 'asc' | 'desc') =>
+ sortTimeStrings(summary, 'total_duration', direction),
+ transforms: [sortable],
+ props: { className: tableColumnClasses[3] },
+ },
+ {
+ id: 'avgDuration',
+ title: t('Average duration'),
+ sort: (summary, direction: 'asc' | 'desc') =>
+ sortTimeStrings(summary, 'avg_duration', direction),
+ transforms: [sortable],
+ props: {
+ className: tableColumnClasses[4],
+ info: {
+ tooltip: t(
+ 'An average of the time taken to run PipelineRuns. The trending shown is based on the time range selected. This metric does not show runs that are running or pending.',
+ ),
+ className: 'pipeline-overview__for-pipelines-list__tooltip',
+ },
+ },
+ },
+ {
+ id: 'successRate',
+ title: t('Success rate'),
+ sort: (summary, direction: 'asc' | 'desc') =>
+ sortByNumbers(summary, 'succeeded', direction),
+ transforms: [sortable],
+ props: {
+ className: tableColumnClasses[5],
+ info: {
+ tooltip: t(
+ 'Success rate measure the % of successfully completed pipeline runs in relation to the total number of pipeline runs',
+ ),
+ className: 'pipeline-overview__for-pipelines-list__tooltip',
+ },
+ },
+ },
+ ];
+ if (!hideLastRunTime) {
+ columns.push({
+ id: 'lastRunTime',
+ title: t('Last run time'),
+ sort: (summary, direction: 'asc' | 'desc') =>
+ sortByTimestamp(summary, 'last_runtime', direction),
+ transforms: [sortable],
+ props: { className: tableColumnClasses[6] },
+ });
+ }
+
+ return columns;
+ }, [t, hideLastRunTime]);
+
+ const [columns] = useActiveColumns({
+ columns: plrColumns,
+ showNamespaceOverride: false,
+ columnManagementID: '',
+ });
+
+ return (
+
+ );
+};
+
+export default PipelineRunsForPipelinesListK8s;
diff --git a/src/components/pipelines-overview/list-pages/PipelineRunsForPipelinesRow.tsx b/src/components/pipelines-overview/list-pages/PipelineRunsForPipelinesRow.tsx
index 6a6ebf6f..b5c9406b 100644
--- a/src/components/pipelines-overview/list-pages/PipelineRunsForPipelinesRow.tsx
+++ b/src/components/pipelines-overview/list-pages/PipelineRunsForPipelinesRow.tsx
@@ -16,9 +16,14 @@ import { formatTime, formatTimeLastRunTime } from '../dateTime';
import { ALL_NAMESPACES_KEY } from '../../../consts';
import { PipelineModel, PipelineModelV1Beta1 } from '../../../models';
-const PipelineRunsForPipelinesRow: React.FC> = ({
- obj,
-}) => {
+const PipelineRunsForPipelinesRow: React.FC<
+ RowProps<
+ SummaryProps,
+ {
+ hideLastRunTime?: boolean;
+ }
+ >
+> = ({ obj, rowData: { hideLastRunTime } }) => {
const [activeNamespace] = useActiveNamespace();
const [namespace, name] = obj.group_value.split('/');
const clusterVersion = (window as any).SERVER_FLAGS?.releaseVersion;
@@ -58,9 +63,11 @@ const PipelineRunsForPipelinesRow: React.FC> = ({
{`${Math.round(
(100 * obj.succeeded) / obj.total,
)}%`} |
- {`${formatTimeLastRunTime(
- obj.last_runtime,
- )}`} |
+ {!hideLastRunTime && (
+ {`${formatTimeLastRunTime(
+ obj.last_runtime,
+ )}`} |
+ )}
>
);
};
diff --git a/src/components/pipelines-overview/list-pages/PipelineRunsForPipelinesRowK8s.tsx b/src/components/pipelines-overview/list-pages/PipelineRunsForPipelinesRowK8s.tsx
new file mode 100644
index 00000000..4d22c2b0
--- /dev/null
+++ b/src/components/pipelines-overview/list-pages/PipelineRunsForPipelinesRowK8s.tsx
@@ -0,0 +1,120 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import * as React from 'react';
+import { Link } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { Tooltip } from '@patternfly/react-core';
+import {
+ SummaryProps,
+ getReferenceForModel,
+ listPageTableColumnClasses as tableColumnClasses,
+} from '../utils';
+import {
+ ResourceIcon,
+ ResourceLink,
+ RowProps,
+ getGroupVersionKindForModel,
+ useActiveNamespace,
+} from '@openshift-console/dynamic-plugin-sdk';
+import { formatTime, formatTimeLastRunTime } from '../dateTime';
+import { ALL_NAMESPACES_KEY } from '../../../consts';
+import {
+ PipelineModel,
+ PipelineModelV1Beta1,
+ ProjectModel,
+} from '../../../models';
+import { Project } from '../../../types';
+
+const PipelineRunsForPipelinesRowK8s: React.FC<
+ RowProps<
+ SummaryProps,
+ {
+ hideLastRunTime?: boolean;
+ projects: Project[];
+ projectsLoaded: boolean;
+ }
+ >
+> = ({ obj, rowData: { hideLastRunTime, projects, projectsLoaded } }) => {
+ const { t } = useTranslation('plugin__pipelines-console-plugin');
+ const [activeNamespace] = useActiveNamespace();
+ const [namespace, name] = obj.group_value.split('/');
+ const clusterVersion = (window as any).SERVER_FLAGS?.releaseVersion;
+ const isV1SupportCluster =
+ clusterVersion?.split('.')[0] === '4' &&
+ clusterVersion?.split('.')[1] > '13';
+ const pipelineReference = getReferenceForModel(
+ isV1SupportCluster ? PipelineModel : PipelineModelV1Beta1,
+ );
+ const projectReference = getReferenceForModel(ProjectModel);
+
+ const isNamespaceExists = (namespaceName: string) => {
+ if (!projectsLoaded) {
+ return false;
+ }
+ return projects.some(
+ (project) =>
+ project?.metadata && project?.metadata?.name === namespaceName,
+ );
+ };
+
+ return (
+ <>
+
+ {isNamespaceExists(namespace) ? (
+
+ ) : (
+
+
+
+ {name}
+
+
+ )}
+ |
+ {activeNamespace === ALL_NAMESPACES_KEY && (
+
+ {isNamespaceExists(namespace) ? (
+
+ ) : (
+
+
+
+ {namespace}
+
+
+ )}
+ |
+ )}
+
+ {isNamespaceExists(namespace) ? (
+
+ {obj.total}
+
+ ) : (
+ {obj.total}
+ )}
+ |
+
+ {formatTime(obj.total_duration)}
+ |
+ {formatTime(obj.avg_duration)} |
+ {`${Math.round(
+ (100 * obj.succeeded) / obj.total,
+ )}%`} |
+ {!hideLastRunTime && (
+ {`${formatTimeLastRunTime(
+ obj.last_runtime,
+ )}`} |
+ )}
+ >
+ );
+};
+
+export default PipelineRunsForPipelinesRowK8s;
diff --git a/src/components/pipelines-overview/list-pages/PipelineRunsListPageK8s.tsx b/src/components/pipelines-overview/list-pages/PipelineRunsListPageK8s.tsx
new file mode 100644
index 00000000..41cc2423
--- /dev/null
+++ b/src/components/pipelines-overview/list-pages/PipelineRunsListPageK8s.tsx
@@ -0,0 +1,274 @@
+import * as React from 'react';
+import * as classNames from 'classnames';
+import { Card, CardBody, Grid, GridItem } from '@patternfly/react-core';
+import {
+ PrometheusResponse,
+ useK8sWatchResource,
+} from '@openshift-console/dynamic-plugin-sdk';
+import PipelineRunsForRepositoriesList from './PipelineRunsForRepositoriesList';
+import PipelineRunsForPipelinesListK8s from './PipelineRunsForPipelinesListK8s';
+import SearchInputField from '../SearchInput';
+import { isMatchingFirstTickValue, useQueryParams } from '../utils';
+import { ALL_NAMESPACES_KEY } from '../../../consts';
+import {
+ usePipelineMetricsForAllNamespacePoll,
+ usePipelineMetricsForNamespacePoll,
+} from '../../pipelines-metrics/hooks';
+import {
+ MetricsQueryPrefix,
+ PipelineQuery,
+} from '../../pipelines-metrics/utils';
+import { getXaxisValues, secondsToHms } from '../dateTime';
+import { Project } from '../../../types';
+
+type PipelineRunsListPageProps = {
+ bordered?: boolean;
+ namespace: string;
+ timespan: number;
+ interval: number;
+};
+
+const processData = (
+ countData: PrometheusResponse,
+ durationData: PrometheusResponse,
+ tickValues: number[] | Date[],
+ type: string,
+) => {
+ if (!countData?.data?.result || !durationData?.data?.result) {
+ return [];
+ }
+ const firstTickValue = tickValues[0];
+ const grouped: {
+ [key: string]: {
+ group_value: string;
+ total: number;
+ succeeded: number;
+ total_duration: number;
+ };
+ } = {};
+
+ countData?.data?.result?.forEach((item) => {
+ const { namespace, pipeline, status } = item.metric;
+ if (pipeline === 'anonymous' || !pipeline) return;
+
+ const key = `${namespace}/${pipeline}`;
+ const lastTimestamp = item.values[item.values.length - 1][0];
+ const lastValue = parseInt(item.values[item.values.length - 1][1], 10);
+
+ const isMatch = isMatchingFirstTickValue(
+ firstTickValue,
+ lastTimestamp,
+ type,
+ );
+ if (isMatch) return;
+
+ if (!grouped[key]) {
+ grouped[key] = {
+ group_value: key,
+ total: 0,
+ succeeded: 0,
+ total_duration: 0,
+ };
+ }
+
+ grouped[key].total += lastValue;
+
+ if (status === 'success') {
+ grouped[key].succeeded += lastValue;
+ }
+ });
+
+ durationData?.data?.result?.forEach((item) => {
+ const { namespace, pipeline } = item.metric;
+ if (pipeline === 'anonymous' || !pipeline) return;
+
+ const key = `${namespace}/${pipeline}`;
+ const lastTimestamp = item.values[item.values.length - 1][0];
+ const lastDuration = parseFloat(item.values[item.values.length - 1][1]);
+
+ const isMatch = isMatchingFirstTickValue(
+ firstTickValue,
+ lastTimestamp,
+ type,
+ );
+ if (isMatch) return;
+
+ if (!grouped[key]) {
+ grouped[key] = {
+ group_value: key,
+ total: 0,
+ succeeded: 0,
+ total_duration: 0,
+ };
+ }
+
+ grouped[key].total_duration += lastDuration;
+ });
+
+ return Object.values(grouped).map((group) => {
+ const avgDuration =
+ group.total > 0 ? group.total_duration / group.total : 0;
+ return {
+ ...group,
+ total_duration: secondsToHms(group.total_duration),
+ avg_duration: secondsToHms(avgDuration),
+ };
+ });
+};
+
+const PipelineRunsListPageK8s: React.FC = ({
+ bordered,
+ namespace,
+ timespan,
+ interval,
+}) => {
+ const [pageFlag, setPageFlag] = React.useState(1);
+ const [searchText, setSearchText] = React.useState('');
+ const [tickValues, type] = getXaxisValues(timespan);
+
+ const [projects, projectsLoaded] = useK8sWatchResource({
+ isList: true,
+ kind: 'Project',
+ optional: true,
+ });
+ const [pipelineRunsMetricsCountData] =
+ namespace == ALL_NAMESPACES_KEY
+ ? usePipelineMetricsForAllNamespacePoll({
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ metricsQuery:
+ PipelineQuery.PIPELINERUN_COUNT_WITH_METRIC_FOR_ALL_NAMESPACE,
+ })
+ : usePipelineMetricsForNamespacePoll({
+ namespace,
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ metricsQuery:
+ PipelineQuery.PIPELINERUN_COUNT_WITH_METRIC_FOR_NAMESPACE,
+ });
+
+ const [pipelineRunsMetricsSumData] =
+ namespace == ALL_NAMESPACES_KEY
+ ? usePipelineMetricsForAllNamespacePoll({
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ metricsQuery:
+ PipelineQuery.PIPELINERUN_SUM_WITH_METRIC_FOR_ALL_NAMESPACE,
+ })
+ : usePipelineMetricsForNamespacePoll({
+ namespace,
+ timespan,
+ delay: interval,
+ queryPrefix: MetricsQueryPrefix.TEKTON_PIPELINES_CONTROLLER,
+ metricsQuery: PipelineQuery.PIPELINERUN_SUM_WITH_METRIC_FOR_NAMESPACE,
+ });
+
+ const summaryDataK8s = React.useMemo(() => {
+ return processData(
+ pipelineRunsMetricsCountData,
+ pipelineRunsMetricsSumData,
+ tickValues,
+ type,
+ );
+ }, [pipelineRunsMetricsCountData, pipelineRunsMetricsSumData]);
+
+ const summaryDataFiltered = React.useMemo(() => {
+ return summaryDataK8s.filter((summary) =>
+ summary.group_value
+ .split('/')[1]
+ .toLowerCase()
+ .includes(searchText.toLowerCase()),
+ );
+ }, [searchText, summaryDataK8s]);
+
+ useQueryParams({
+ key: 'search',
+ value: searchText,
+ setValue: setSearchText,
+ defaultValue: '',
+ });
+
+ useQueryParams({
+ key: 'list',
+ value: pageFlag,
+ setValue: setPageFlag,
+ defaultValue: 1,
+ options: { perpipeline: 1, perrepository: 2 },
+ displayFormat: (v) => (v == 1 ? 'perpipeline' : 'perrepository'),
+ loadFormat: (v) => (v == 'perrepository' ? 2 : 1),
+ });
+
+ // const handlePageChange = (pageNumber: number) => {
+ // setloaded(false);
+ // setSummaryData([]);
+ // setSummaryDataFiltered([]);
+ // setPageFlag(pageNumber);
+ // };
+ const handleNameChange = (value: string) => {
+ setSearchText(value);
+ };
+ return (
+
+
+
+
+ {/* Lastrun Status is not provided by API */}
+ {/* */}
+
+
+ {/*
+ Since Pipeline metrics for PAC is not available, commenting this
+
+
+ handlePageChange(1)}
+ />
+ handlePageChange(2)}
+ />
+
+ */}
+
+
+
+ {pageFlag === 1 ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+};
+
+export default PipelineRunsListPageK8s;
diff --git a/src/components/pipelines-overview/utils.ts b/src/components/pipelines-overview/utils.ts
index e3be3562..a71d428a 100644
--- a/src/components/pipelines-overview/utils.ts
+++ b/src/components/pipelines-overview/utils.ts
@@ -2,11 +2,13 @@ import {
K8sGroupVersionKind,
K8sModel,
K8sResourceKindReference,
+ PrometheusResponse,
} from '@openshift-console/dynamic-plugin-sdk';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { ALL_NAMESPACES_KEY } from '../../consts';
+import { adjustToStartOfWeek } from '../pipelines-metrics/utils';
export const alphanumericCompare = (a: string, b: string): number => {
return a.localeCompare(b, undefined, {
@@ -58,6 +60,16 @@ export const TimeRangeOptions = () => {
};
};
+export const TimeRangeOptionsK8s = () => {
+ const { t } = useTranslation('plugin__pipelines-console-plugin');
+ return {
+ '1d': t('Last day'),
+ '2w': t('Last weeks'),
+ '4w 2d': t('Last month'),
+ '12w': t('Last quarter'),
+ };
+};
+
export const StatusOptions = () => {
const { t } = useTranslation('plugin__pipelines-console-plugin');
return {
@@ -386,3 +398,154 @@ export const formatNamespaceRoute = (
return path;
};
+
+export const isMatchingFirstTickValue = (
+ firstTickValue: number | Date,
+ firstPrometheusValue: number | Date,
+ type: string,
+) => {
+ const group_date = new Date(Number(firstPrometheusValue) * 1000);
+ const tickDate =
+ typeof firstTickValue === 'number'
+ ? new Date(firstTickValue * 1000)
+ : firstTickValue;
+ let isMatch = false;
+ if (type === 'hour') {
+ isMatch = group_date.getHours() === tickDate.getHours();
+ } else if (type === 'week') {
+ const adjustedGroupDate = adjustToStartOfWeek(new Date(group_date));
+ isMatch = adjustedGroupDate.toDateString() === tickDate.toDateString();
+ } else if (type === 'day') {
+ isMatch = group_date.toDateString() === tickDate.toDateString();
+ } else if (type === 'month') {
+ isMatch = group_date.getMonth() === tickDate.getMonth();
+ }
+ return isMatch;
+};
+
+export const getTotalPipelineRuns = (
+ prometheusResult: PrometheusResponse,
+ tickValues: number[] | Date[],
+ type: string,
+): number => {
+ let totalPLRCount = 0;
+
+ if (prometheusResult?.data?.result[0]?.values) {
+ const values = prometheusResult.data.result[0].values;
+ let lastValue = parseInt(values[0][1], 10);
+ let hasDecrement = false;
+ let sum = 0;
+
+ for (let i = 1; i < values.length; i++) {
+ const currentValue = parseInt(values[i][1], 10);
+ if (currentValue < lastValue) {
+ hasDecrement = true;
+ sum += lastValue;
+ }
+ lastValue = currentValue;
+ }
+
+ // If there's any decrement, add the last value to the sum
+ if (hasDecrement) {
+ sum += lastValue;
+ totalPLRCount = sum;
+ } else {
+ // If no decrement, just take the last value
+ totalPLRCount = lastValue;
+ }
+
+ // Check if the first tick element matches the first Prometheus value
+ const firstTickValue = tickValues[0];
+ const firstPrometheusValue = values[0];
+ const isMatch = isMatchingFirstTickValue(
+ firstTickValue,
+ firstPrometheusValue[0],
+ type,
+ );
+ if (isMatch) {
+ totalPLRCount -= parseInt(firstPrometheusValue[1], 10);
+ }
+ }
+
+ return totalPLRCount;
+};
+
+const formatDuration = (seconds: number): string => {
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ const remainingSeconds = Math.floor(seconds % 60);
+
+ let formatted = '';
+ if (hours > 0) {
+ formatted += `${hours}h `;
+ }
+ if (minutes > 0 || hours > 0) {
+ formatted += `${minutes}m `;
+ }
+ formatted += `${remainingSeconds}s`;
+
+ return formatted.trim();
+};
+
+export const getTotalPipelineRunsDuration = (
+ prometheusResult: PrometheusResponse,
+ tickValues: number[] | Date[],
+ type: string,
+): [string, number] => {
+ let totalPLRDuration = 0;
+
+ if (prometheusResult?.data?.result[0]?.values) {
+ const values = prometheusResult.data.result[0].values;
+
+ // Calculate total duration and check for decrements
+ let lastValue = parseFloat(values[0][1]);
+ let hasDecrement = false;
+ let sum = 0;
+
+ for (let i = 1; i < values.length; i++) {
+ const currentValue = parseFloat(values[i][1]);
+ if (currentValue < lastValue) {
+ hasDecrement = true;
+ sum += lastValue;
+ }
+ lastValue = currentValue;
+ }
+
+ // If there's any decrement, add the last value to the sum
+ if (hasDecrement) {
+ sum += lastValue;
+ totalPLRDuration = sum;
+ } else {
+ // If no decrement, just take the last value
+ totalPLRDuration = lastValue;
+ }
+
+ // Adjust total duration if the first tick element matches the first Prometheus value
+ const firstTickValue = tickValues[0];
+ const firstPrometheusValue = values[0];
+ const isMatch = isMatchingFirstTickValue(
+ firstTickValue,
+ firstPrometheusValue[0],
+ type,
+ );
+ if (isMatch) {
+ totalPLRDuration -= parseFloat(firstPrometheusValue[1]);
+ }
+ }
+
+ return [formatDuration(totalPLRDuration), totalPLRDuration];
+};
+
+export const getPipelineRunAverageDuration = (
+ totalDuration: number,
+ totalPLRRuns: number,
+): string => {
+ if (totalPLRRuns === 0) return '-';
+
+ const averageDuration = totalDuration / totalPLRRuns;
+ return formatDuration(averageDuration);
+};
+
+export const roundToNearestSecond = (timestamp) => {
+ return Math.round(timestamp);
+};
diff --git a/src/components/utils/__tests__/pipeline-utils.spec.ts b/src/components/utils/__tests__/pipeline-utils.spec.ts
index 606dcb79..0d4c06a5 100644
--- a/src/components/utils/__tests__/pipeline-utils.spec.ts
+++ b/src/components/utils/__tests__/pipeline-utils.spec.ts
@@ -23,6 +23,7 @@ import {
} from '../../../test-data/taskrun-test-data';
import { ComputedStatus, ContainerStatus } from '../../../types';
import {
+ LatestPipelineRunStatus,
appendPipelineRunStatus,
containerToLogSourceStatus,
getImageUrl,
@@ -35,7 +36,6 @@ import {
getSbomTaskRun,
getSecretAnnotations,
hasExternalLink,
- LatestPipelineRunStatus,
pipelineRunDuration,
updateServiceAccount,
} from '../pipeline-utils';
@@ -62,7 +62,7 @@ jest.mock('@openshift-console/dynamic-plugin-sdk');
beforeAll(() => {
jest
.spyOn(k8sResourceModule, 'k8sUpdate')
- .mockImplementation(({ model, data }) => Promise.resolve(data));
+ .mockImplementation(({ data }) => Promise.resolve(data));
});
describe('pipeline-utils ', () => {