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
214 changes: 129 additions & 85 deletions korrel8r/korrel8r-openapi.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
"<0>There are two types of correlation search:</0><1><0><0>Neighbourhood</0>: Find all connected neighbours up to a maximum <2>depth</2> (number of hops.)</0><1><0>Goal</0>: Find paths to a selected <2>goal class</2>. Finds all shortest paths, and some near-shortest paths.</1></1>": "<0>There are two types of correlation search:</0><1><0><0>Neighbourhood</0>: Find all connected neighbours up to a maximum <2>depth</2> (number of hops.)</0><1><0>Goal</0>: Find paths to a selected <2>goal class</2>. Finds all shortest paths, and some near-shortest paths.</1></1>",
"Advanced Search": "Advanced Search",
"Advanced search parameters": "Advanced search parameters",
"Agent Navigation": "Agent Navigation",
"AI Agent settings": "AI Agent settings",
"Cancel": "Cancel",
"Canceled": "Canceled",
"Close": "Close",
"Connected": "Connected",
"Connecting...": "Connecting...",
"Correlation graph already matches search": "Correlation graph already matches search",
"Correlation graph is focused on the current view.": "Correlation graph is focused on the current view.",
"Current view does not provide a starting point for correlation": "Current view does not provide a starting point for correlation",
Expand All @@ -30,11 +34,11 @@
"No correlated data found": "No correlated data found",
"No Correlated Signals Found": "No Correlated Signals Found",
"No results.": "No results.",
"Off": "Off",
"Open the Troubleshooting Panel": "Open the Troubleshooting Panel",
"Other duration": "Other duration",
"Quickly diagnose and resolve issues by exploring correlated observability signals for resources.": "Quickly diagnose and resolve issues by exploring correlated observability signals for resources.",
"Refresh": "Refresh",
"Refresh the graph by re-running the current search.": "Refresh the graph by re-running the current search.",
"Save": "Save",
"Search": "Search",
"Search Error": "Search Error",
Expand Down
2 changes: 1 addition & 1 deletion web/src/__tests__/types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe('Constraint', () => {
it.each([
{ clientC: {}, typesC: {} },
{ clientC: { start: start.toISOString(), end: end.toISOString() }, typesC: { start, end } },
{ clientC: { limit: 50, timeout: '1111111111' }, typesC: { limit: 50, timeoutNS: 1111111111 } },
{ clientC: { limit: 50 }, typesC: { limit: 50 } },
] as Array<{ clientC: api.Constraint; typesC: Partial<Constraint> }>)(
'from/toAPI %s',
({ clientC, typesC }) => {
Expand Down
15 changes: 15 additions & 0 deletions web/src/components/AIExperienceIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { SVGProps } from 'react';

// Created from http://redhat.brand-portal.adobe.com/aem/search.html, file name is rh-ui-icon-ai-experience.
export const AIExperienceIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"
fill="currentColor"
width="1em"
height="1em"
{...props}
>
<path d="M26.03125,16.96191c-5.90527-.46875-10.53076-5.09473-10.99902-11-.0415-.51953-.5166-.96094-1.03809-.96094s-.99658.44238-1.03809.96191c-.46777,5.9043-5.09375,10.53027-10.99121,10.99902-.52344.03711-.96973.51367-.96973,1.03809,0,.52148.44189.99707.96191,1.03809,5.90527.46875,10.53125,5.09473,10.99951,11,.0415.51953.5166.96094,1.0376.96094.52197,0,.99707-.44238,1.03857-.96191.46777-5.9043,5.09326-10.53027,10.99854-10.99902.51953-.04199.96191-.51562.96191-1.03809,0-.52148-.44238-.99707-.96191-1.03809ZM13.99414,25.76465c-1.4126-3.54492-4.21875-6.35059-7.7666-7.76367,3.54639-1.41309,6.354-4.2207,7.76709-7.76562,1.4126,3.54492,4.21826,6.35059,7.76611,7.76367-3.5459,1.41309-6.35352,4.2207-7.7666,7.76562ZM30.50195,7c0,.28906-.20898.53613-.49805.58984-2.2334.4082-4.00879,2.18359-4.41699,4.41699-.05371.28906-.30078.49805-.58984.49805s-.53613-.20898-.58984-.49805c-.40723-2.2334-2.18262-4.00879-4.41699-4.41699-.28906-.05371-.49805-.30078-.49805-.58984s.20898-.53613.49805-.58984c2.23438-.4082,4.00977-2.18359,4.41699-4.41699.05371-.28906.30078-.49805.58984-.49805s.53613.20898.58984.49805c.4082,2.2334,2.18359,4.00879,4.41699,4.41699.28906.05371.49805.30078.49805.58984Z" />
</svg>
);
79 changes: 79 additions & 0 deletions web/src/components/AgentMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {
Dropdown,
DropdownItem,
DropdownList,
Flex,
FlexItem,
Icon,
Label,
MenuToggle,
MenuToggleElement,
Switch,
} from '@patternfly/react-core';
import { Ref, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { setAgentEnabled } from '../redux-actions';
import { State } from '../redux-reducers';
import { AIExperienceIcon } from './AIExperienceIcon';

const AgentMenu = () => {
const { t } = useTranslation('plugin__troubleshooting-panel-console-plugin');
const dispatch = useDispatch();
const [isOpen, setIsOpen] = useState(false);

const agentEnabled: boolean = useSelector((s: State) => s.plugins?.tp?.get('agentEnabled'));
const agentConnected: boolean = useSelector((s: State) => s.plugins?.tp?.get('agentConnected'));

const statusLabel = agentEnabled
? agentConnected
? t('Connected')
: t('Connecting...')
: t('Off');

const statusColor = agentEnabled ? (agentConnected ? 'green' : 'orange') : undefined;

return (
<Dropdown
isOpen={isOpen}
onOpenChange={setIsOpen}
popperProps={{ position: 'end' }}
toggle={(toggleRef: Ref<MenuToggleElement>) => (
<MenuToggle
ref={toggleRef}
variant="plain"
onClick={() => setIsOpen(!isOpen)}
isExpanded={isOpen}
aria-label={t('AI Agent settings')}
>
<Icon>
<AIExperienceIcon />
</Icon>
</MenuToggle>
)}
>
<DropdownList>
<DropdownItem key="header">
<Flex alignItems={{ default: 'alignItemsCenter' }}>
<FlexItem grow={{ default: 'grow' }}>
<Switch
id="agent-enabled"
isChecked={agentEnabled}
hasCheckIcon
label={t('Agent Navigation')}
onChange={(_event, checked: boolean) => dispatch(setAgentEnabled(checked))}
/>
</FlexItem>
<FlexItem>
<Label color={statusColor} isCompact>
{statusLabel}
</Label>
</FlexItem>
</Flex>
</DropdownItem>
</DropdownList>
</Dropdown>
);
};

export default AgentMenu;
32 changes: 17 additions & 15 deletions web/src/components/Korrel8rPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,22 @@ import {
SyncIcon,
UnlinkIcon,
} from '@patternfly/react-icons';
import { useQueryClient } from '@tanstack/react-query';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useLocationQuery } from '../hooks/useLocationQuery';
import { usePluginAvailable } from '../hooks/usePluginAvailable';
import { GraphResult, useKorrel8rGraph } from '../korrel8r-client';
import * as korrel8r from '../korrel8r/types';
import { defaultSearch, Search, setSearch } from '../redux-actions';
import { State } from '../redux-reducers';
import * as time from '../time';
import { AdvancedSearchForm } from './AdvancedSearchForm';
import AgentMenu from './AgentMenu';
import './korrel8rpanel.css';
import { TimeRangeDropdown } from './TimeRangeDropdown';
import { Korrel8rTopology } from './topology/Korrel8rTopology';
import './korrel8rpanel.css';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { GraphResult, useKorrel8rGraph } from '../korrel8r-client';
import { useQueryClient } from '@tanstack/react-query';

export default function Korrel8rPanel() {
const { t } = useTranslation('plugin__troubleshooting-panel-console-plugin');
Expand Down Expand Up @@ -139,6 +140,9 @@ export default function Korrel8rPanel() {

{/* Right aligned buttons */}
<Flex align={{ default: 'alignRight' }} spaceItems={{ default: 'spaceItemsNone' }}>
{/* AI Agent menu */}
<AgentMenu />

{/* Advanced search toggle */}
<Tooltip content={t('Advanced search parameters')} position="bottom-end">
<ExpandableSectionToggle
Expand All @@ -163,17 +167,15 @@ export default function Korrel8rPanel() {
{t('Cancel')}
</Button>
) : (
<Tooltip content={t('Refresh the graph by re-running the current search.')}>
<Button
variant="link"
size="sm"
isAriaDisabled={!search?.queryStr}
onClick={() => refetch()}
aria-label={t('Refresh')}
>
<SyncIcon />
</Button>
</Tooltip>
<Button
variant="link"
size="sm"
isAriaDisabled={!search?.queryStr}
onClick={() => refetch()}
Comment thread
alanconway marked this conversation as resolved.
aria-label={t('Refresh')}
>
<SyncIcon />
</Button>
)}
</Flex>
</Flex>
Expand Down
108 changes: 89 additions & 19 deletions web/src/hooks/useKorrel8r.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,90 @@
import { useEffect, useState } from 'react';
import { listDomains } from '../korrel8r-client';

export const useKorrel8r = () => {
const [isKorrel8rReachable, setIsKorrel8rReachable] = useState<boolean>(false);

useEffect(() => {
listDomains()
.then(() => {
setIsKorrel8rReachable(true);
})
.catch(() => {
setIsKorrel8rReachable(false);
});
}, []);

return {
isKorrel8rReachable,
};
import * as React from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { consoleEvents, retryWithBackoff, setConsole } from '../korrel8r-client';
import { Query } from '../korrel8r/types';
import {
apiSearch,
openTP,
reduxSearch,
Search,
setAgentConnected,
setSearch,
} from '../redux-actions';
import { State } from '../redux-reducers';
import { useLocationQuery } from './useLocationQuery';
import { useNavigateToQuery } from './useNavigateToQuery';

const logErr = (what: string, err: any) => {
if (err?.name !== 'AbortError') {
// eslint-disable-next-line no-console
console.error(`korrel8r console ${what}: ${err?.body?.error || err?.message || String(err)}`);
}
};

const useKorrel8r = ({
Comment thread
alanconway marked this conversation as resolved.
minDelay = 100,
maxDelay = 5000,
}: { minDelay?: number; maxDelay?: number } = {}) => {
const view = useLocationQuery()?.toString();
const search: Search = useSelector((s: State) => s.plugins?.tp?.get('search'), shallowEqual);
const isOpen: boolean = useSelector((s: State) => s.plugins?.tp?.get('isOpen'));
Comment thread
alanconway marked this conversation as resolved.
const agentEnabled: boolean = useSelector((s: State) => s.plugins?.tp?.get('agentEnabled'));
const dispatch = useDispatch();
const navigateToQuery = useNavigateToQuery();

// Send console update to korrel8r if there is one.
React.useEffect(() => {
if (!(agentEnabled && (view || (isOpen && search?.queryStr)))) {
return;
}

const cleanup = retryWithBackoff(
async (signal) => {
const req = setConsole({
view,
search: search?.queryStr ? apiSearch(search) : undefined,
});
signal.addEventListener('abort', () => req.cancel(), { once: true });
await req;
},
(err: any) => logErr('send', err),
{ minDelay, maxDelay },
);

return () => cleanup();
}, [view, search, isOpen, agentEnabled, minDelay, maxDelay]);

const navigateToQueryRef = React.useRef(navigateToQuery);
React.useEffect(() => {
navigateToQueryRef.current = navigateToQuery;
});

// Subscribe to console update events from the agent.
React.useEffect(() => {
if (!agentEnabled) {
return;
}
return consoleEvents(
async (event) => {
if (event.view) {
navigateToQueryRef.current(Query.parse(event.view), null);
}
if (event.search) {
const search = reduxSearch(event.search);
if (search) {
dispatch(setSearch(search));
dispatch(openTP());
}
}
},
Comment thread
alanconway marked this conversation as resolved.
(err: Error) => {
dispatch(setAgentConnected(false));
logErr('disconnected', err);
},
() => dispatch(setAgentConnected(true)),
{ minDelay, maxDelay },
);
}, [agentEnabled, minDelay, maxDelay, dispatch]);
};

export default useKorrel8r;
19 changes: 12 additions & 7 deletions web/src/hooks/useLocationQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,21 @@ const useBrowserLocation = () => {
return location;
};

/** Returns the Korrel8r query for the current browser location or undefined */
/** Returns the Korrel8r query for the current browser location or undefined. */
export const useLocationQuery = (): Query | undefined => {
const domains = useDomains();
const location = useBrowserLocation();
const [lastError, setLastError] = useState('');
try {
const link = new URIRef(location.pathname + location.search);
const q = domains.linkToQuery(link);
return q;
} catch (e) {
// eslint-disable-next-line no-console
console.warn(`korrel8r useLocationQuery: ${e}`);
const query = domains.linkToQuery(new URIRef(location.pathname + location.search));
if (lastError) setLastError('');
return query;
} catch (err) {
const errStr = String(err);
if (errStr !== lastError) {
// eslint-disable-next-line no-console
console.warn(`korrel8r useLocationQuery: ${errStr}`);
setLastError(errStr);
}
}
};
5 changes: 3 additions & 2 deletions web/src/hooks/usePopover.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { useOverlay } from '@openshift-console/dynamic-plugin-sdk';

import { useEffect } from 'react';
import { useSelector } from 'react-redux';
import Popover from '../components/Popover';
import { State } from '../redux-reducers';
import { useEffect } from 'react';
import useKorrel8r from './useKorrel8r';

const usePopover = () => {
const isOpen = useSelector((state: State) => state.plugins?.tp?.get('isOpen'));

const launchModal = useOverlay();
useKorrel8r();

useEffect(() => {
if (launchModal && isOpen) {
Expand Down
8 changes: 3 additions & 5 deletions web/src/hooks/useTroubleshootingPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { Action, ExtensionHook, useActivePerspective } from '@openshift-console/dynamic-plugin-sdk';
import { InfrastructureIcon } from '@patternfly/react-icons';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { openTP } from '../redux-actions';
import { useKorrel8r } from './useKorrel8r';
import { useCallback, useEffect, useState } from 'react';

const useTroubleshootingPanel: ExtensionHook<Array<Action>> = () => {
const { isKorrel8rReachable } = useKorrel8r();
const { t } = useTranslation('plugin__troubleshooting-panel-console-plugin');
const [perspective] = useActivePerspective();
const dispatch = useDispatch();
Expand All @@ -16,7 +14,7 @@ const useTroubleshootingPanel: ExtensionHook<Array<Action>> = () => {
}, [dispatch]);

const getActions = useCallback(() => {
if (!isKorrel8rReachable || perspective === 'dev') {
if (perspective === 'dev') {
Comment thread
alanconway marked this conversation as resolved.
return [];
}
const actions = [
Expand All @@ -34,7 +32,7 @@ const useTroubleshootingPanel: ExtensionHook<Array<Action>> = () => {
},
];
return actions;
}, [open, t, isKorrel8rReachable, perspective]);
}, [open, t, perspective]);

const [actions, setActions] = useState<Array<Action>>(getActions());

Expand Down
Loading