Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 19 additions & 9 deletions src/components/common/DataViewFilterToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface CheckboxFilterConfig {
placeholder?: string;
options: FilterOption[];
defaultValues?: string[];
singleSelect?: boolean;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the purpose of single select

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing DataViewFilterToolbar only supported checkbox-style filters where users can select multiple values (e.g., selecting both "Succeeded" and "Failed" statuses). But for the date range filter, selecting multiple time ranges simultaneously doesn't make sense.

}

export interface FilterValues {
Expand Down Expand Up @@ -73,6 +74,11 @@ const CheckboxFilterInput: FC<{

const onMenuSelect = (_event: unknown, itemId: string | number) => {
const id = String(itemId);
if (config.singleSelect) {
onSelect([id]);
setIsOpen(false);
return;
}
const newSelected = selected.includes(id)
? selected.filter((s) => s !== id)
: [...selected, id];
Expand Down Expand Up @@ -110,14 +116,16 @@ const CheckboxFilterInput: FC<{
onClick={handleToggleClick}
isExpanded={isOpen}
isFullWidth
icon={<FilterIcon />}
icon={config.singleSelect ? undefined : <FilterIcon />}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to pass undefined for the icon?

badge={
selected.length > 0 ? (
!config.singleSelect && selected.length > 0 ? (
<Badge isRead>{selected.length}</Badge>
) : undefined
}
>
{config.placeholder || config.title}
{config.singleSelect && selected.length > 0
? config.options.find((o) => o.value === selected[0])?.label
: config.placeholder || config.title}
</MenuToggle>
}
triggerRef={toggleRef}
Expand All @@ -134,15 +142,17 @@ const CheckboxFilterInput: FC<{
<MenuItem
key={option.value}
itemId={option.value}
hasCheckbox
hasCheckbox={!config.singleSelect}
isSelected={selected.includes(option.value)}
>
{option.label}
<Badge isRead className="pf-v6-u-ml-sm">
{option.totalCount !== undefined
? `${option.count}/${option.totalCount}`
: option.count}
</Badge>
{!config.singleSelect && (
<Badge isRead className="pf-v6-u-ml-sm">
{option.totalCount !== undefined
? `${option.count}/${option.totalCount}`
: option.count}
</Badge>
)}
</MenuItem>
))}
</MenuList>
Expand Down
66 changes: 66 additions & 0 deletions src/components/hooks/__tests__/useDateRangeFilter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {
useFlag,
useUserPreference,
} from '@openshift-console/dynamic-plugin-sdk';
import { testHook } from '../../../test-data/utils/hooks-utils';
import { useDateRangeFilter } from '../useDateRangeFilter';

jest.mock('@openshift-console/dynamic-plugin-sdk', () => ({
useFlag: jest.fn(),
useUserPreference: jest.fn(),
}));

const useFlagMock = useFlag as jest.Mock;
const useUserPreferenceMock = useUserPreference as jest.Mock;

const ONE_DAY_MS = 86400000;
const ONE_WEEK_MS = 604800000;

describe('useDateRangeFilter', () => {
let setTimespanMock: jest.Mock;

beforeEach(() => {
setTimespanMock = jest.fn();
useFlagMock.mockReturnValue(true);
useUserPreferenceMock.mockReturnValue([ONE_DAY_MS, setTimespanMock]);
});

it('should return timespan from user preference', () => {
const { result } = testHook(() => useDateRangeFilter());
expect(result.current.timespan).toBe(ONE_DAY_MS);
});

it('should compute startDate as Date.now() - timespan', () => {
const now = Date.now();
const { result } = testHook(() => useDateRangeFilter());
const diff = now - result.current.timespan;
expect(result.current.startDate).toBeGreaterThanOrEqual(diff - 100);
expect(result.current.startDate).toBeLessThanOrEqual(diff + 100);
});

it('should generate a valid CEL expression', () => {
const { result } = testHook(() => useDateRangeFilter());
expect(result.current.dateFilterCEL).toMatch(
/^data\.status\.startTime > timestamp\(".*"\)$/,
);
});

it('should default to 1 day when useUserPreference returns undefined', () => {
useUserPreferenceMock.mockReturnValue([undefined, setTimespanMock]);
const { result } = testHook(() => useDateRangeFilter());
expect(result.current.timespan).toBe(ONE_DAY_MS);
expect(result.current.dateFilterCEL).not.toBe('');
});

it('should expose setTimespan from the preference hook', () => {
const { result } = testHook(() => useDateRangeFilter());
result.current.setTimespan(ONE_WEEK_MS);
expect(setTimespanMock).toHaveBeenCalledWith(ONE_WEEK_MS);
});

it('should reflect the isTektonResultEnabled flag', () => {
useFlagMock.mockReturnValue(false);
const { result } = testHook(() => useDateRangeFilter());
expect(result.current.isTektonResultEnabled).toBe(false);
});
});
50 changes: 50 additions & 0 deletions src/components/hooks/useDateRangeFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useMemo } from 'react';
import {
useFlag,
useUserPreference,
} from '@openshift-console/dynamic-plugin-sdk';
import { FLAG_PIPELINE_TEKTON_RESULT_INSTALLED } from '../../consts';
import { parsePrometheusDuration } from '../pipelines-overview/dateTime';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we using this parsePrometheusDuration ?
we are going to query tektonresults and this is very unreadable
try { const parts = duration .trim() .split(/\s+/) .map((p) => p.match(/^(\d+)([wdhms])$/)); return _.sumBy(parts, (p) => parseInt(p[1], 10) * units[p[2]]); } catch (ignored) { // Invalid duration format return 0; }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we using this parsePrometheusDuration ? we are going to query tektonresults and this is very unreadable try { const parts = duration .trim() .split(/\s+/) .map((p) => p.match(/^(\d+)([wdhms])$/)); return _.sumBy(parts, (p) => parseInt(p[1], 10) * units[p[2]]); } catch (ignored) { // Invalid duration format return 0; }

parsePrometheusDuration simply converts a human-readable duration string like '1d' into milliseconds (86400000). It's not related to Prometheus or Tekton Results, it's just a utility for parsing duration strings. We use it because:

  • The time range dropdown options are stored as duration keys ('1d', '1w', '1m', etc.)
  • We need milliseconds for Date.now() - timespan calculation
  • The Overview page already uses this same function for the same purpose
  • It's already in the codebase, so no new dependency

The name is misleading (it's borrowed from Prometheus-style duration format), but the function itself is just string to milliseconds conversion.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adityavshinde
This is a request
We do not want AI responses to review comment, write based on what you understand and can verify else there will be no learning from this
cc : @arvindk-softwaredev

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I import the unit constants directly from dateTime.ts, then further do the calculation for the startDate?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can create your own helper method which is more readable and maintainable

@anwesha-palit-redhat anwesha-palit-redhat Jun 23, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need milliseconds for Date.now() - timespan calculation

We should update this probably, if we do this what essentially happens is it does not compute the whole of the earlier day, example
2 PM on June 23rd - and timespan = 1 day = it will compute till 2 PM of June 22nd, it won't share the whole data for June 22nd, this can misconstrue things. For this filter we should have all data rom now to 00:00 and then add the timespan calculation so that it is normalised.
wdyt @arvindk-softwaredev ?


export type DateRangeFilterResult = {
timespan: number;
setTimespan: (ms: number) => void;
startDate: number | undefined;
dateFilterCEL: string;
isTektonResultEnabled: boolean;
preferenceLoaded: boolean;
};

const SETTINGS_KEY = 'plugin__pipelines-console-plugin.dateRangeFilter';

export const useDateRangeFilter = (): DateRangeFilterResult => {
const isTektonResultEnabled = useFlag(FLAG_PIPELINE_TEKTON_RESULT_INSTALLED);
const [timespan, setTimespan, preferenceLoaded] = useUserPreference<number>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if this api fails ?
can we bubble the error please

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if this api fails ? can we bubble the error please

If this api fails, then it falls back to the default value which is "Last day". This will not throw an error as it is defined in SDK

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suppressing the error makes it harder to debug when an issue comes in later, so we will need to change this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

console/frontend/packages/console-shared/src/hooks/useUserPreference.ts
they are handling the error in someways, let's check this once and see if we can understand if there's an api error

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also adding this in the doc in detail.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update setTimespan to something more specific

SETTINGS_KEY,
parsePrometheusDuration('1d'),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we setting 1d as default ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we setting 1d as default ?

Yes

true,
);

const ts = timespan ?? parsePrometheusDuration('1d');

const startDate = useMemo(() => {
if (!ts) return undefined;
return Date.now() - ts;
}, [ts]);

const dateFilterCEL = useMemo(() => {
if (!startDate) return '';
return `data.status.startTime > timestamp("${new Date(
startDate,
).toISOString()}")`;
}, [startDate]);

return {
timespan: ts,
setTimespan,
startDate,
dateFilterCEL,
isTektonResultEnabled,
preferenceLoaded,
};
};
9 changes: 6 additions & 3 deletions src/components/hooks/useTaskRuns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
} from '../../models';
import { ApprovalTaskKind, PipelineRunKind, TaskRunKind } from '../../types';
import { useDeepCompareMemoize } from '../utils/common-utils';
import { EQ } from '../utils/tekton-results';
import { AND, EQ } from '../utils/tekton-results';
import { useMultiClusterProxyService } from './useMultiClusterProxyService';
import { useMultiClusterTaskRuns } from './useMultiClusterTaskRuns';
import { useTRRuns } from './useTektonResults';
Expand Down Expand Up @@ -191,12 +191,14 @@ export const usePipelineRuns = (
selector?: Selector;
limit?: number;
name?: string;
filter?: string;
},
): [PipelineRunKind[], boolean, boolean, Error | undefined, boolean, boolean] =>
useRuns<PipelineRunKind>(PIPELINE_RUN_GVK, namespace, {
selector: options?.selector,
limit: options?.limit /* similar to one present in UseTaskRunsOptions */,
name: options?.name /* similar to one present in UseTaskRunsOptions */,
filter: options?.filter,
});

export const useRuns = <Kind extends K8sResourceKind>(
Expand All @@ -207,6 +209,7 @@ export const useRuns = <Kind extends K8sResourceKind>(
limit?: number;
name?: string;
skipFetch?: boolean;
filter?: string; // CEL expression sent to Tekton Results to retrieve PRs within the date range
},
pipelineRunFinished?: boolean,
pipelineRunManagedBy?: string,
Expand Down Expand Up @@ -315,10 +318,10 @@ export const useRuns = <Kind extends K8sResourceKind>(

const trOptions: typeof optionsMemo = useMemo(() => {
if (optionsMemo?.name) {
const { name, ...rest } = optionsMemo;
const { name, filter, ...rest } = optionsMemo;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs to be rebased with master branch

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this rebased yet ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I have rebased this. Now making changes accordingly.

return {
...rest,
filter: EQ('data.metadata.name', name),
filter: AND(EQ('data.metadata.name', name), filter),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if filter is null ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the expected format for the filter

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if filter is null ?

AND() is a utility function in tekton-results.ts that joins CEL expressions with &&. It skips falsy values (null, undefined, empty string). So if filter is null:

  1. AND(EQ('data.metadata.name', name), null) just returns EQ('data.metadata.name', name)
  2. It behaves exactly like the original code before the change: filter: EQ('data.metadata.name', name)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the expected format for the filter

It's a CEL string for the Tekton Results API. The format looks like:
data.status.startTime > timestamp("2026-06-22T10:00:00.000Z")

This is generated by useDateRangeFilter in dateFilterCEL. It's the same format used everywhere else in the codebase for Tekton Results queries (e.g., EQ('data.metadata.name', name) produces data.metadata.name == "xyz").

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add verification

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. This verifies that if filter is null, nothing breaks and giving output as before.
Screencast.From.2026-06-22.23-29-19.mp4
  1. In summary-api same pattern has been used.
image

};
}
return optionsMemo;
Expand Down
22 changes: 14 additions & 8 deletions src/components/hooks/useTektonResult.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Selector, useFlag } from '@openshift-console/dynamic-plugin-sdk';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { RepositoryFields, RepositoryLabels } from '../../consts';
import { FLAGS, PipelineRunKind } from '../../types';
import { getTaskRunLog } from '../utils/tekton-results';
Expand All @@ -9,7 +9,7 @@ export type GetNextPage = () => void | undefined;

export const useGetPipelineRuns = (
ns: string,
options?: { name: string; kind: string },
options?: { name: string; kind: string; filter?: string },
): [
PipelineRunKind[],
boolean,
Expand All @@ -31,12 +31,18 @@ export const useGetPipelineRuns = (
};
}

return usePipelineRuns(
ns,
selector && {
selector,
},
);
const pipelineRunOptions = useMemo(() => {
const opts: Record<string, unknown> = {};
if (selector) {
opts.selector = selector;
}
if (options?.filter) {
opts.filter = options.filter;
}
return opts;
}, [selector, options?.filter]);

return usePipelineRuns(ns, pipelineRunOptions);
};

export const useTRTaskRunLog = (
Expand Down
Loading
Loading