Skip to content

Commit b9f96c7

Browse files
alanconwayzhuje
authored andcommitted
feat(#55): context menu for multiple queries.
- context menu shows query or queries associated with a node. - improved panel layout. - improved types wrappers.
1 parent 18c2645 commit b9f96c7

5 files changed

Lines changed: 269 additions & 219 deletions

File tree

web/src/__tests__/types.spec.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import * as api from '../korrel8r/client';
22

3-
import { Class, Constraint, Domain, Domains, Graph, Node, Query, URIRef } from '../korrel8r/types';
3+
import {
4+
Class,
5+
Constraint,
6+
Domain,
7+
Domains,
8+
Edge,
9+
Graph,
10+
Node,
11+
Query,
12+
URIRef,
13+
} from '../korrel8r/types';
414

515
describe('Query', () => {
616
it('converts to/from string', () => {
@@ -122,32 +132,26 @@ describe('Node', () => {
122132
],
123133
}),
124134
).toEqual({
125-
api: {
126-
class: 'a:b',
127-
count: 10,
128-
queries: [
129-
{ query: 'a:b:c', count: 5 },
130-
{ query: 'a:b:d', count: 5 },
131-
],
132-
},
133-
135+
id: 'a:b',
136+
count: 10,
134137
class: { domain: 'a', name: 'b' },
135138
queries: [
136139
{
137-
queryCount: { query: 'a:b:c', count: 5 },
138140
query: { class: { domain: 'a', name: 'b' }, selector: 'c' },
141+
count: 5,
139142
},
140143
{
141-
queryCount: { query: 'a:b:d', count: 5 },
142144
query: { class: { domain: 'a', name: 'b' }, selector: 'd' },
145+
count: 5,
143146
},
144147
],
145148
});
146149
});
147150

148151
it('constructor bad class', () => {
149152
expect(new Node({ class: 'foobar', count: 1 })).toEqual({
150-
api: { class: 'foobar', count: 1 },
153+
id: 'foobar',
154+
count: 1,
151155
error: new TypeError('invalid class: foobar'),
152156
queries: [],
153157
});
@@ -177,9 +181,5 @@ describe('Graph', () => {
177181
const g = new Graph(a);
178182
g.nodes.forEach((n) => expect(g.node(n.id)).toEqual(n)); // Lookup nodes
179183
expect(g.nodes).toEqual(a.nodes.map((n) => new Node(n)));
180-
expect(g.edges).toEqual(
181-
a.edges.map((e: api.Edge) => {
182-
return { api: e, start: g.node(e.start), goal: g.node(e.goal) };
183-
}),
184-
);
184+
expect(g.edges).toEqual(a.edges.map((e) => new Edge(g.node(e.start), g.node(e.goal))));
185185
});

web/src/components/Korrel8rPanel.tsx

Lines changed: 97 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ import {
1010
ExpandableSectionToggle,
1111
Flex,
1212
FlexItem,
13+
Icon,
1314
NumberInput,
1415
Radio,
1516
TextArea,
1617
TextInput,
18+
Title,
1719
Tooltip,
1820
} from '@patternfly/react-core';
19-
import { CubesIcon, ExclamationCircleIcon } from '@patternfly/react-icons';
21+
import { CogIcon, CubesIcon, ExclamationCircleIcon, SyncIcon } from '@patternfly/react-icons';
2022
import * as React from 'react';
2123
import { TFunction, useTranslation } from 'react-i18next';
2224
import { useDispatch, useSelector } from 'react-redux';
@@ -60,7 +62,8 @@ export default function Korrel8rPanel() {
6062
const [result, setResult] = React.useState<Result | null>(null);
6163
const [showQuery, setShowQuery] = React.useState(false);
6264

63-
const cannotFocus = t('The current console page is not supported for correlation.');
65+
const focusTip = t('Generate a correlation graph starting from resources in the current view.');
66+
const cannotFocus = t('The current view does not support correlation.');
6467

6568
React.useEffect(() => {
6669
// Set result = null to trigger a reload, don't run the query till then.
@@ -127,12 +130,10 @@ export default function Korrel8rPanel() {
127130
setSearch(updatedSearch); // Update the search state with the new object
128131
};
129132

130-
const focusTip = locationQuery
131-
? t('Re-calculate the correlation graph starting from resources on the current console page.')
132-
: cannotFocus;
133133
const minDepth = 1;
134134
const maxDepth = 10;
135135
const depthBounds = applyBounds(1, 10);
136+
136137
const runSearch = React.useCallback(
137138
(newSearch: Search) => {
138139
newSearch.depth = depthBounds(newSearch.depth);
@@ -146,7 +147,7 @@ export default function Korrel8rPanel() {
146147
return (
147148
<>
148149
<Flex className="tp-plugin__panel-query-container">
149-
<Tooltip content={focusTip}>
150+
<Tooltip content={locationQuery ? focusTip : cannotFocus}>
150151
<Button
151152
isAriaDisabled={!locationQuery}
152153
onClick={() =>
@@ -160,16 +161,27 @@ export default function Korrel8rPanel() {
160161
{t('Focus')}
161162
</Button>
162163
</Tooltip>
163-
<FlexItem align={{ default: 'alignRight' }}>
164+
<Flex align={{ default: 'alignRight' }}>
164165
<ExpandableSectionToggle
165166
contentId={queryContentID}
166167
toggleId={queryToggleID}
167168
isExpanded={showQuery}
168169
onToggle={(on: boolean) => setShowQuery(on)}
169170
>
170-
{showQuery ? t('Hide Query') : t('Show Query')}
171+
<Icon>
172+
<CogIcon />
173+
</Icon>
171174
</ExpandableSectionToggle>
172-
</FlexItem>
175+
<Tooltip content={t('Refresh the graph using the current search settings')}>
176+
<Button
177+
isAriaDisabled={!search?.queryStr}
178+
onClick={() => runSearch(search)}
179+
variant="secondary"
180+
>
181+
<SyncIcon />
182+
</Button>
183+
</Tooltip>
184+
</Flex>
173185
</Flex>
174186
<ExpandableSection
175187
contentId={queryContentID}
@@ -178,88 +190,96 @@ export default function Korrel8rPanel() {
178190
isDetached
179191
isIndented
180192
>
181-
{/* DateTimeRangePicker section with both date and time */}
182-
<Flex>
183-
<FlexItem>
184-
<b>{t('Date and Time Range')}</b>
185-
<DateTimeRangePicker
186-
// Pass the start date/time
187-
from={search.constraint?.start ? new Date(search.constraint.start) : null}
188-
// Pass the end date/time
189-
to={search.constraint?.end ? new Date(search.constraint.end) : null}
190-
onDateChange={handleDateChange} // Unified handler for both date and time changes
191-
/>
192-
</FlexItem>
193-
<FlexItem>
194-
<b>{t('Korrel8 query selecting the starting points for correlation.')}</b>
195-
<TextArea
196-
className="tp-plugin__panel-query-input"
197-
placeholder="domain:class:querydata"
198-
id={queryInputID}
199-
value={search.queryStr}
200-
onChange={(_event, value) => setSearch({ ...search, queryStr: value })}
201-
resizeOrientation="vertical"
202-
/>
203-
</FlexItem>
193+
<Flex className="tp-plugin__panel-query-container" direction={{ default: 'column' }}>
194+
<Title headingLevel="h3">Time Range</Title>
195+
<DateTimeRangePicker
196+
// Pass the start date/time
197+
from={search.constraint?.start ? new Date(search.constraint.start) : null}
198+
// Pass the end date/time
199+
to={search.constraint?.end ? new Date(search.constraint.end) : null}
200+
onDateChange={handleDateChange} // Unified handler for both date and time changes
201+
/>
202+
203+
<Title headingLevel="h3">Search type</Title>
204204
<Flex>
205-
<Tooltip content={t('Show graph of connected classes up to the specified depth.')}>
206-
<Radio
207-
label={t('Neighbourhood depth: ')}
208-
name={searchTypeOptions}
209-
id="neighbourhood-option"
210-
isChecked={search.type === SearchType.Neighbour}
211-
onChange={(_: React.FormEvent, on: boolean) => {
212-
on && setSearch({ ...search, type: SearchType.Neighbour });
205+
<FlexItem>
206+
<Tooltip content={t('Search for correlated resources up to the specified depth.')}>
207+
<Radio
208+
label={t('Neighbourhood search')}
209+
name={searchTypeOptions}
210+
id="neighbourhood-option"
211+
isChecked={search.type === SearchType.Neighbour}
212+
onChange={(_: React.FormEvent, on: boolean) => {
213+
on && setSearch({ ...search, type: SearchType.Neighbour });
214+
}}
215+
/>
216+
</Tooltip>
217+
</FlexItem>
218+
<FlexItem hidden={search.type !== SearchType.Neighbour}>{t('Depth')}</FlexItem>
219+
<FlexItem hidden={search.type !== SearchType.Neighbour}>
220+
<NumberInput
221+
value={search.depth}
222+
min={minDepth}
223+
max={maxDepth}
224+
onPlus={() => setSearch({ ...search, depth: (search.depth || 0) + 1 })}
225+
onMinus={() =>
226+
search.depth > minDepth && setSearch({ ...search, depth: search.depth - 1 })
227+
}
228+
onChange={(event: React.FormEvent<HTMLInputElement>) => {
229+
const n = Number((event.target as HTMLInputElement).value);
230+
setSearch({ ...search, depth: isNaN(n) ? 1 : n });
213231
}}
214232
/>
215-
</Tooltip>
216-
<NumberInput
217-
value={search.depth}
218-
min={minDepth}
219-
max={maxDepth}
220-
isDisabled={search.type !== SearchType.Neighbour}
221-
onPlus={() => setSearch({ ...search, depth: (search.depth || 0) + 1 })}
222-
onMinus={() =>
223-
(search.depth || 0) > minDepth && setSearch({ ...search, depth: search.depth - 1 })
224-
}
225-
onChange={(event: React.FormEvent<HTMLInputElement>) => {
226-
const n = Number((event.target as HTMLInputElement).value);
227-
setSearch({ ...search, depth: isNaN(n) ? 1 : n });
228-
}}
229-
/>
233+
</FlexItem>
230234
</Flex>
231235
<Flex>
232-
<Tooltip content={t('Show graph of paths to signals of the specified class.')}>
233-
<Radio
234-
label={t('Goal class: ')}
235-
name={searchTypeOptions}
236-
id="goal-option"
237-
isChecked={search.type === SearchType.Goal}
238-
onChange={(_: React.FormEvent, on: boolean) =>
239-
on && setSearch({ ...search, type: SearchType.Goal })
240-
}
241-
/>
242-
</Tooltip>
243236
<FlexItem>
237+
<Tooltip content={t('Search for paths to resources of the specified class.')}>
238+
<Radio
239+
label={t('Goal directed search')}
240+
name={searchTypeOptions}
241+
id="goal-option"
242+
isChecked={search.type === SearchType.Goal}
243+
onChange={(_: React.FormEvent, on: boolean) =>
244+
on && setSearch({ ...search, type: SearchType.Goal })
245+
}
246+
/>
247+
</Tooltip>
248+
</FlexItem>
249+
<FlexItem hidden={search.type !== SearchType.Goal}>Class</FlexItem>
250+
<FlexItem hidden={search.type !== SearchType.Goal}>
244251
<TextInput
252+
label={'Class'}
245253
value={search.goal}
246-
isDisabled={search.type !== SearchType.Goal}
247254
placeholder="domain:class"
248255
onChange={(event: React.FormEvent<HTMLInputElement>) => {
249256
setSearch({ ...search, goal: (event.target as HTMLInputElement).value });
250257
}}
251-
aria-label="Korrel8r Query"
252258
/>
253259
</FlexItem>
254260
</Flex>
261+
<Title headingLevel="h3">Query</Title>
262+
<Tooltip content={t('Query to select the starting resources for correlation.')}>
263+
<TextArea
264+
className="tp-plugin__panel-query-input"
265+
placeholder="domain:class:selector"
266+
id={queryInputID}
267+
value={search.queryStr}
268+
onChange={(_event, value) => setSearch({ ...search, queryStr: value })}
269+
/>
270+
</Tooltip>
271+
<FlexItem align={{ default: 'alignLeft' }}>
272+
<Tooltip content={t('Refresh the graph using the current search settings')}>
273+
<Button
274+
isAriaDisabled={!search?.queryStr}
275+
onClick={() => runSearch(search)}
276+
variant="secondary"
277+
>
278+
{t('Update')}
279+
</Button>
280+
</Tooltip>
281+
</FlexItem>
255282
</Flex>
256-
<Button
257-
isAriaDisabled={!search?.queryStr}
258-
onClick={() => runSearch(search)}
259-
variant="secondary"
260-
>
261-
{t('Query')}
262-
</Button>
263283
</ExpandableSection>
264284
<Divider />
265285
<FlexItem className="tp-plugin__panel-topology-container" grow={{ default: 'grow' }}>
@@ -275,7 +295,7 @@ interface TopologyProps {
275295
setSearch: (search: Search) => void;
276296
}
277297

278-
const Topology: React.FC<TopologyProps> = ({ result, t, setSearch }) => {
298+
const Topology: React.FC<TopologyProps> = ({ result, t }) => {
279299
const [loggingAvailable, loggingAvailableLoading] = usePluginAvailable('logging-view-plugin');
280300
const [netobserveAvailable, netobserveAvailableLoading] = usePluginAvailable('netobserv-plugin');
281301

@@ -284,14 +304,13 @@ const Topology: React.FC<TopologyProps> = ({ result, t, setSearch }) => {
284304
return <Loading />;
285305
}
286306

287-
if (result.graph && result.graph.nodes) {
307+
if (result?.graph?.nodes) {
288308
// Non-empty graph
289309
return (
290310
<Korrel8rTopology
291311
graph={result.graph}
292312
loggingAvailable={loggingAvailable}
293313
netobserveAvailable={netobserveAvailable}
294-
setSearch={setSearch}
295314
/>
296315
);
297316
}

0 commit comments

Comments
 (0)