Skip to content

Commit 04e9092

Browse files
Merge pull request #119 from openshift-cherrypick-robot/cherry-pick-117-to-release-0.4
[release-0.4] COO-395: Add DatePicker to Korrel8r
2 parents 740ca51 + 2d4ef1a commit 04e9092

15 files changed

Lines changed: 289 additions & 34 deletions

web/src/__tests__/log-node.spec.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { LogNode } from '../korrel8r/log';
2+
import { Constraint } from '../redux-actions';
23

34
describe('LogNode.fromURL', () => {
45
it.each([
@@ -51,15 +52,25 @@ describe('LogNode.fromQuery', () => {
5152
`log:infrastructure:{kubernetes_namespace_name="default",` + `kubernetes_pod_name="foo"}`,
5253
url: `monitoring/logs?q=${encodeURIComponent(
5354
'{kubernetes_namespace_name="default",kubernetes_pod_name="foo"}',
54-
)}&tenant=infrastructure`,
55+
)}&tenant=infrastructure&start=1742896800000&end=1742940000000`,
56+
constraint: {
57+
start: '2025-03-25T10:00:00.000Z',
58+
end: '2025-03-25T22:00:00.000Z',
59+
} as Constraint,
5560
},
5661
{
5762
query: 'log:infrastructure:{kubernetes_namespace_name="default",log_type="infrastructure"}',
5863
url: `monitoring/logs?q=${encodeURIComponent(
5964
'{kubernetes_namespace_name="default",log_type="infrastructure"}',
60-
)}&tenant=infrastructure`,
65+
)}&tenant=infrastructure&start=1742896800000&end=1742940000000`,
66+
constraint: {
67+
start: '2025-03-25T10:00:00.000Z',
68+
end: '2025-03-25T22:00:00.000Z',
69+
} as Constraint,
6170
},
62-
])('$query', ({ url, query }) => expect(LogNode.fromQuery(query)?.toURL()).toEqual(url));
71+
])('$query', ({ url, query, constraint }) =>
72+
expect(LogNode.fromQuery(query, constraint)?.toURL()).toEqual(url),
73+
);
6374
});
6475

6576
describe('expected errors', () => {
@@ -86,6 +97,6 @@ describe('expected errors', () => {
8697
expected: 'Expected log class in query',
8798
},
8899
])('error from query: $query', ({ query, expected }) => {
89-
expect(() => LogNode.fromQuery(query)).toThrow(expected);
100+
expect(() => LogNode.fromQuery(query, null)).toThrow(expected);
90101
});
91102
});

web/src/__tests__/netflow-node.spec.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { NetflowNode } from '../korrel8r/netflow';
2+
import { Constraint } from '../redux-actions';
23

34
// Korrel8r queries contain less information than console URLs.
45
// Round-trip conversion is not always equal.
@@ -9,17 +10,29 @@ const roundTrip = [
910
query: 'netflow:network:{SrcK8S_Type="Pod",SrcK8S_Namespace="myNamespace"}',
1011
url: `netflow-traffic?tenant=network&filters=${encodeURIComponent(
1112
'src_kind=Pod;src_namespace=myNamespace',
12-
)}`,
13+
)}&startTime=1742896800&endTime=1742940000`,
14+
constraint: {
15+
start: '2025-03-25T10:00:00.000Z',
16+
end: '2025-03-25T22:00:00.000Z',
17+
} as Constraint,
1318
},
1419
{
15-
url: 'netflow-traffic?tenant=network&filters=src_namespace%3Dnetobserv',
20+
url: 'netflow-traffic?tenant=network&filters=src_namespace%3Dnetobserv&startTime=1742896800&endTime=1742940000',
1621
query: 'netflow:network:{SrcK8S_Namespace="netobserv"}',
22+
constraint: {
23+
start: '2025-03-25T10:00:00.000Z',
24+
end: '2025-03-25T22:00:00.000Z',
25+
} as Constraint,
1726
},
1827
{
1928
query: 'netflow:network:{SrcK8S_Type!="Pod",SrcK8S_Namespace!="myNamespace"}',
2029
url: `netflow-traffic?tenant=network&filters=${encodeURIComponent(
2130
'src_kind!=Pod;src_namespace!=myNamespace',
22-
)}`,
31+
)}&startTime=1742896800&endTime=1742940000`,
32+
constraint: {
33+
start: '2025-03-25T10:00:00.000Z',
34+
end: '2025-03-25T22:00:00.000Z',
35+
} as Constraint,
2336
},
2437
];
2538

@@ -29,9 +42,17 @@ describe('NetflowNode.fromQuery', () => {
2942
{
3043
// Ignores unknown keys
3144
query: 'netflow:network:{InvalidKey="Pod",SrcK8S_Namespace="foo"}',
32-
url: `netflow-traffic?tenant=network&filters=${encodeURIComponent('src_namespace=foo')}`,
45+
url: `netflow-traffic?tenant=network&filters=${encodeURIComponent(
46+
'src_namespace=foo',
47+
)}&startTime=1742896800&endTime=1742940000`,
48+
constraint: {
49+
start: '2025-03-25T10:00:00.000Z',
50+
end: '2025-03-25T22:00:00.000Z',
51+
} as Constraint,
3352
},
34-
])(`from $query`, ({ query, url }) => expect(NetflowNode.fromQuery(query).toURL()).toEqual(url));
53+
])(`from $query`, ({ query, url, constraint }) =>
54+
expect(NetflowNode.fromQuery(query, constraint).toURL()).toEqual(url),
55+
);
3556
});
3657

3758
describe('NetflowNode.fromURL', () => {
@@ -82,6 +103,6 @@ describe('', () => {
82103
expected: 'Expected filter to be key="value": SrcK8S_Type',
83104
},
84105
])('expect error fromQuery($query)', ({ query, expected }) => {
85-
expect(() => NetflowNode.fromQuery(query)).toThrow(expected);
106+
expect(() => NetflowNode.fromQuery(query, null)).toThrow(expected);
86107
});
87108
});

web/src/__tests__/node-factory.spec.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Korrel8rNodeFactory } from '../korrel8r/node-factory';
2+
import { Constraint } from '../redux-actions';
23

34
beforeAll(() => {
45
// Mock API discovery resources.
@@ -22,28 +23,48 @@ const testdata = [
2223
query:
2324
'alert:alert:{"alertname":"KubePodCrashLooping","container":"bad-deployment","namespace":"default","pod":"bad-deployment"}',
2425
url: 'monitoring/alerts?alerts=alertname%3DKubePodCrashLooping%2Ccontainer%3Dbad-deployment%2Cnamespace%3Ddefault%2Cpod%3Dbad-deployment',
26+
constraint: {
27+
start: null,
28+
end: null,
29+
} as Constraint,
2530
},
2631
{
2732
query: 'k8s:Pod.v1.:{"namespace":"default","name":"bad-deployment-000000000-00000"}',
2833
url: 'k8s/ns/default/pods/bad-deployment-000000000-00000',
34+
constraint: {
35+
start: null,
36+
end: null,
37+
} as Constraint,
2938
},
3039
{
3140
query: 'netflow:network:{SrcK8S_Type="Pod",SrcK8S_Namespace="myNamespace"}',
3241
url: `netflow-traffic?tenant=network&filters=${encodeURIComponent(
3342
'src_kind=Pod;src_namespace=myNamespace',
34-
)}`,
43+
)}&startTime=1742896800&endTime=1742940000`,
44+
constraint: {
45+
start: '2025-03-25T10:00:00.000Z',
46+
end: '2025-03-25T22:00:00.000Z',
47+
} as Constraint,
3548
},
3649
{
3750
url:
3851
`observe/traces/1599dfd76bc896101a9811857ae3c3c9?` +
3952
`namespace=openshift-tracing&name=platform&tenant=platform`,
4053
query: `trace:span:{trace:id="1599dfd76bc896101a9811857ae3c3c9"}`,
54+
constraint: {
55+
start: null,
56+
end: null,
57+
} as Constraint,
4158
},
4259
{
4360
url: `monitoring/logs?q=${encodeURIComponent(
4461
'{kubernetes_namespace_name="default",log_type="infrastructure"}',
45-
)}&tenant=infrastructure`,
62+
)}&tenant=infrastructure&start=1742896800000&end=1742940000000`,
4663
query: 'log:infrastructure:{kubernetes_namespace_name="default",log_type="infrastructure"}',
64+
constraint: {
65+
start: '2025-03-25T10:00:00.000Z',
66+
end: '2025-03-25T22:00:00.000Z',
67+
} as Constraint,
4768
},
4869
];
4970

@@ -54,7 +75,7 @@ describe('Korrel8rNodeFactory.fromURL', () => {
5475
});
5576

5677
describe('Korrel8rNodeFactory.fromQuery', () => {
57-
it.each(testdata)('converts $query', ({ url, query }) => {
58-
expect(Korrel8rNodeFactory.fromQuery(query).toURL()).toEqual(url);
78+
it.each(testdata)('converts $query', ({ url, query, constraint }) => {
79+
expect(Korrel8rNodeFactory.fromQuery(query, constraint).toURL()).toEqual(url);
5980
});
6081
});
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import * as React from 'react';
2+
import {
3+
Flex,
4+
FlexItem,
5+
InputGroup,
6+
InputGroupItem,
7+
DatePicker,
8+
isValidDate,
9+
TimePicker,
10+
yyyyMMddFormat,
11+
} from '@patternfly/react-core';
12+
13+
interface DateTimeRangePickerProps {
14+
from: Date | null;
15+
to: Date | null;
16+
onDateChange: (type: 'start' | 'end', newDate: Date, hour?: number, minute?: number) => void;
17+
}
18+
19+
const DateTimeRangePicker: React.FC<DateTimeRangePickerProps> = ({ from, to, onDateChange }) => {
20+
const toValidator = (date: Date): string => {
21+
// Date comparison validation
22+
return isValidDate(from) && yyyyMMddFormat(date) >= yyyyMMddFormat(from)
23+
? ''
24+
: 'The "to" date must be after the "from" date';
25+
};
26+
27+
return (
28+
<Flex direction={{ default: 'column', lg: 'column' }}>
29+
<FlexItem>
30+
<InputGroup>
31+
<InputGroupItem>
32+
<DatePicker
33+
value={isValidDate(from) ? yyyyMMddFormat(from) : ''}
34+
onChange={(_event, inputDate, newFromDate) => {
35+
if (isValidDate(newFromDate)) {
36+
onDateChange('start', newFromDate); // Pass 'start' type to handler
37+
}
38+
}}
39+
aria-label="Start date"
40+
placeholder="YYYY-MM-DD"
41+
/>
42+
</InputGroupItem>
43+
<InputGroupItem>
44+
<TimePicker
45+
aria-label="Start time"
46+
style={{ width: '150px' }}
47+
onChange={(_event, time, hour, minute) => {
48+
if (isValidDate(from)) {
49+
onDateChange('start', from as Date, hour, minute); // Update start time
50+
}
51+
}}
52+
/>
53+
</InputGroupItem>
54+
</InputGroup>
55+
</FlexItem>
56+
<FlexItem>
57+
<div>to</div>
58+
</FlexItem>
59+
<FlexItem>
60+
<InputGroup>
61+
<InputGroupItem>
62+
<DatePicker
63+
value={isValidDate(to) ? yyyyMMddFormat(to as Date) : ''}
64+
onChange={(_event, inputDate, newToDate) => {
65+
if (isValidDate(newToDate)) {
66+
onDateChange('end', newToDate); // Pass 'end' type to handler
67+
}
68+
}}
69+
isDisabled={!isValidDate(from)}
70+
rangeStart={from}
71+
validators={[toValidator]}
72+
aria-label="End date"
73+
placeholder="YYYY-MM-DD"
74+
/>
75+
</InputGroupItem>
76+
<InputGroupItem>
77+
<TimePicker
78+
style={{ width: '150px' }}
79+
onChange={(_event, time, hour, minute) => {
80+
if (isValidDate(to)) {
81+
onDateChange('end', to as Date, hour, minute); // Update end time
82+
}
83+
}}
84+
isDisabled={!isValidDate(from)}
85+
/>
86+
</InputGroupItem>
87+
</InputGroup>
88+
</FlexItem>
89+
</Flex>
90+
);
91+
};
92+
93+
export default DateTimeRangePicker;

web/src/components/Korrel8rPanel.tsx

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from '@patternfly/react-core';
1919
import { CubesIcon, ExclamationCircleIcon } from '@patternfly/react-icons';
2020
import * as React from 'react';
21+
import DateTimeRangePicker from './DateTimeRangePicker';
2122
import { TFunction, useTranslation } from 'react-i18next';
2223
import { useDispatch, useSelector } from 'react-redux';
2324
import { usePluginAvailable } from '../hooks/usePluginAvailable';
@@ -43,6 +44,10 @@ const focusQuery = (urlQuery: string): Query => {
4344
queryType: QueryType.Neighbour,
4445
depth: 3,
4546
goal: null,
47+
constraint: {
48+
start: null, // Initially null
49+
end: null, // Initially null
50+
},
4651
};
4752
};
4853

@@ -104,6 +109,32 @@ export default function Korrel8rPanel() {
104109
const queryInputID = 'query-input';
105110
const queryTypeOptions = 'query-type-options';
106111

112+
// Handler for both 'start' and 'end' date/time changes
113+
const handleDateChange = (
114+
type: 'start' | 'end',
115+
newDate: Date,
116+
hour?: number,
117+
minute?: number,
118+
): void => {
119+
// Adjust time if hour and minute are provided
120+
const updatedDate = new Date(newDate);
121+
if (hour !== undefined && minute !== undefined) {
122+
updatedDate.setHours(hour);
123+
updatedDate.setMinutes(minute);
124+
}
125+
126+
// Update the constraint based on 'start' or 'end' type
127+
const updatedQuery = {
128+
...query,
129+
constraint: {
130+
...query.constraint,
131+
[type]: updatedDate.toISOString(), // Update the corresponding date in query
132+
},
133+
};
134+
135+
setQuery(updatedQuery); // Update the query state with the new object
136+
};
137+
107138
const focusTip = korrel8rQueryFromURL
108139
? t('Re-calculate the correlation graph starting from resources on the current console page.')
109140
: cannotFocus;
@@ -149,7 +180,18 @@ export default function Korrel8rPanel() {
149180
isDetached
150181
isIndented
151182
>
152-
<Flex direction={{ default: 'column' }}>
183+
{/* DateTimeRangePicker section with both date and time */}
184+
<Flex>
185+
<FlexItem>
186+
<h3>{t('Select Date and Time Range')}</h3>
187+
<DateTimeRangePicker
188+
// Pass the start date/time
189+
from={query.constraint?.start ? new Date(query.constraint.start) : null}
190+
// Pass the end date/time
191+
to={query.constraint?.end ? new Date(query.constraint.end) : null}
192+
onDateChange={handleDateChange} // Unified handler for both date and time changes
193+
/>
194+
</FlexItem>
153195
<Tooltip content={t('Korrel8 query selecting the starting points for correlation.')}>
154196
<TextArea
155197
className="tp-plugin__panel-query-input"

0 commit comments

Comments
 (0)