Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(pf5): upgrade TargetContextSelector #1304

Merged
merged 23 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from 22 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
3 changes: 2 additions & 1 deletion src/app/CreateRecording/CustomRecordingForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,8 @@ export const CustomRecordingForm: React.FC = () => {
<FormHelperText>
<HelperText>
<HelperTextItem>
Write contents of buffer onto disk. If disabled, the buffer acts as circular buffer only keeping the most recent Recording information
Write contents of buffer onto disk. If disabled, the buffer acts as circular buffer only keeping the
most recent Recording information
</HelperTextItem>
</HelperText>
</FormHelperText>
Expand Down
1 change: 1 addition & 0 deletions src/app/Dashboard/Charts/mbean/MBeanMetricsChartCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ export const MBeanMetricsChartCard: DashboardCardFC<MBeanMetricsChartCardProps>

React.useEffect(() => {
resizeObserver.current = getResizeObserver(containerRef.current, handleResize);
handleResize();
return resizeObserver.current;
}, [resizeObserver, containerRef, handleResize]);

Expand Down
7 changes: 4 additions & 3 deletions src/app/Rules/CreateRule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -595,8 +595,8 @@ export const CreateRuleForm: React.FC<CreateRuleFormProps> = (_props) => {
<FormHelperText>
<HelperText>
<HelperTextItem>
Initial delay before archiving starts. The first archived copy will be made this long after the Recording is started.
The second archived copy will occur one Archival period later.
Initial delay before archiving starts. The first archived copy will be made this long after the Recording
is started. The second archived copy will occur one Archival period later.
</HelperTextItem>
</HelperText>
</FormHelperText>
Expand Down Expand Up @@ -631,7 +631,8 @@ export const CreateRuleForm: React.FC<CreateRuleFormProps> = (_props) => {
<FormHelperText>
<HelperText>
<HelperTextItem>
The number of Archived Recording copies to preserve in archives for each target application affected by this rule.
The number of Archived Recording copies to preserve in archives for each target application affected by
this rule.
</HelperTextItem>
</HelperText>
</FormHelperText>
Expand Down
187 changes: 90 additions & 97 deletions src/app/TargetView/TargetContextSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ import {
SearchInput,
Dropdown,
DropdownGroup,
DropdownItem,
DropdownList,
MenuToggle,
MenuSearch,
MenuSearchInput,
SelectOption,
SelectGroup,
Split,
SplitItem,
} from '@patternfly/react-core';
import { SearchIcon } from '@patternfly/react-icons';
import * as React from 'react';
import { Link } from 'react-router-dom';

Expand All @@ -49,20 +50,23 @@ export const TargetContextSelector: React.FC<TargetContextSelectorProps> = ({ cl
const [targets, setTargets] = React.useState<Target[]>([]);
const [selectedTarget, setSelectedTarget] = React.useState<Target>();
const [favorites, setFavorites] = React.useState<string[]>(getFromLocalStorage('TARGET_FAVORITES', []));
const [searchTerm, setSearchTerm] = React.useState<string>('');
const [isTargetOpen, setIsTargetOpen] = React.useState(false);
const [isLoading, setLoading] = React.useState(false);

const handleSelectToggle = React.useCallback(() => setIsTargetOpen((old) => !old), [setIsTargetOpen]);
const onToggleClick = React.useCallback(() => {
setIsTargetOpen((v) => !v);
}, [setIsTargetOpen]);

const handleTargetSelect = React.useCallback(
(_, { target }, isPlaceholder) => {
const onSelect = React.useCallback(
(_, target) => {
setIsTargetOpen(false);
const toSelect: Target = isPlaceholder ? undefined : target;
if (!isEqualTarget(toSelect, selectedTarget)) {
context.target.setTarget(toSelect);
if (!isEqualTarget(target, selectedTarget)) {
setSelectedTarget(target);
context.target.setTarget(target);
}
},
[setIsTargetOpen, selectedTarget, context.target],
[selectedTarget, setSelectedTarget, setIsTargetOpen, context.target],
);

React.useEffect(() => {
Expand Down Expand Up @@ -119,110 +123,102 @@ export const TargetContextSelector: React.FC<TargetContextSelectorProps> = ({ cl
const selectOptions = React.useMemo(() => {
if (noOptions) {
return [
<SelectOption key={'no-target-found'} isDisabled>
<DropdownItem itemId={undefined} key={'no-target-found'} isDisabled>
No target found
</SelectOption>,
</DropdownItem>,
];
}

const favSet = new Set(favorites);
const matchExp = new RegExp(searchTerm, 'i');
const filteredTargets = targets.filter((t) =>
[t.alias, t.connectUrl, getAnnotation(t.annotations.cryostat, 'REALM') ?? ''].some((v) => matchExp.test(v)),
andrewazores marked this conversation as resolved.
Show resolved Hide resolved
);

const groupNames = new Set<string>();
targets.forEach((t) => groupNames.add(getAnnotation(t.annotations.cryostat, 'REALM') || 'Others'));
filteredTargets.forEach((t) => groupNames.add(getAnnotation(t.annotations.cryostat, 'REALM') || 'Others'));

const options = Array.from(groupNames)
.map((name) => (
<SelectGroup key={name} label={name}>
<DropdownGroup key={name} label={name}>
{targets
andrewazores marked this conversation as resolved.
Show resolved Hide resolved
.filter((t) => getAnnotation(t.annotations.cryostat, 'REALM') === name)
.map((t: Target) => (
<SelectOption
isSelected={favSet.has(t.connectUrl)}
id={t.connectUrl}
<DropdownItem
isFavorited={favorites.includes(t.connectUrl)}
itemId={t}
key={t.connectUrl}
value={{
toString: () => getTargetRepresentation(t),
compareTo: (other) => other.target.connectUrl === t.connectUrl,
...{ target: t }, // Bypassing type checks
}}
/>
description={t.connectUrl}
>
{t.alias}
</DropdownItem>
))}
</SelectGroup>
</DropdownGroup>
))
.sort((a, b) => `${a.props['label']}`.localeCompare(`${b.props['label']}`));

const favGroup = favorites.length
? [
<SelectGroup key={'Favorites'} label={'Favorites'}>
{favorites
.map((f) => targets.find((t) => t.connectUrl === f))
.filter((t) => t !== undefined)
.map((t: Target) => (
<SelectOption
//isFavorite
id={t.connectUrl}
key={`favorited-${t.connectUrl}`}
value={{
toString: () => getTargetRepresentation(t),
compareTo: (other) => other.target.connectUrl === t.connectUrl,
...{ target: t },
}}
/>
))}
</SelectGroup>,
<Divider key={'favorite-divider'} />,
]
: [];
const favGroup =
!searchTerm && favorites.length
? [
<DropdownGroup key={'Favorites'} label={'Favorites'}>
{favorites
.map((f) => targets.find((t) => t.connectUrl === f))
.filter((t) => t !== undefined)
.map((t: Target) => (
<DropdownItem isFavorited itemId={t} key={`favorited-${t.connectUrl}`} description={t.connectUrl}>
{t.alias}
</DropdownItem>
))}
</DropdownGroup>,
<Divider key={'favorite-divider'} />,
]
: [];

return favGroup.concat(options);
}, [targets, noOptions, favorites]);
}, [targets, noOptions, favorites, searchTerm]);

const handleTargetFilter = React.useCallback(
(_, value: string) => {
if (!value || noOptions) {
// In case of empty options, placeholder is returned.
return selectOptions;
const onFavoriteClick = React.useCallback(
(_, item: Target, actionId: string) => {
if (!actionId) {
return;
}

const matchExp = new RegExp(value, 'i');
return selectOptions
.filter((grp) => grp.props.children)
.map((grp) =>
React.cloneElement(grp, {
children: grp.props.children.filter((option) => {
const { target } = option.props.value;
return matchExp.test(target.connectUrl) || matchExp.test(target.alias);
}),
}),
)
.filter((grp) => grp.props.children.length > 0);
},
[selectOptions, noOptions],
);

const handleFavorite = React.useCallback(
(itemId: string, isFavorite: boolean) => {
setFavorites((old) => {
const toUpdate = !isFavorite ? [...old, itemId] : old.filter((f) => f !== itemId);
const prevFav = old.includes(item.connectUrl);
const toUpdate = prevFav ? old.filter((f) => f !== item.connectUrl) : [...old, item.connectUrl];
saveToLocalStorage('TARGET_FAVORITES', toUpdate);
return toUpdate;
});
},
[setFavorites],
);

const onClearSelection = React.useCallback(() => {
setIsTargetOpen(false);
removeFromLocalStorage('TARGET');
setSelectedTarget(undefined);
context.target.setTarget(undefined);
}, [setSelectedTarget, setIsTargetOpen, context.target]);

const selectionPrefix = React.useMemo(
() => (!selectedTarget ? undefined : <span style={{ fontWeight: 700 }}>Target:</span>),
[selectedTarget],
);

const selectFooter = React.useMemo(
() => (
<Link to={'/topology/create-custom-target'}>
<Button variant="secondary">Create target</Button>
</Link>
<Split hasGutter>
<SplitItem>
<Button variant="secondary" component={(props) => <Link {...props} to={'/topology/create-custom-target'} />}>
Create target
</Button>
</SplitItem>
<SplitItem>
<Button variant="tertiary" onClick={onClearSelection}>
Clear selection
</Button>
</SplitItem>
</Split>
),
[],
[onClearSelection],
);

return (
Expand All @@ -233,44 +229,41 @@ export const TargetContextSelector: React.FC<TargetContextSelectorProps> = ({ cl
) : (
<Dropdown
className={className}
//onToggle={handleSelectToggle}
//onSelect={handleTargetSelect}
isPlain
placeholder="Select Target"
isScrollable
placeholder="Select a Target"
isOpen={isTargetOpen}
onSelect={handleSelectToggle}
onOpenChange={(isOpen) => setIsTargetOpen(isOpen)}
onOpenChangeKeys={['Escape']}
onSelect={onSelect}
onActionClick={onFavoriteClick}
toggle={(toggleRef) => (
<MenuToggle
aria-label="Select Target"
ref={toggleRef}
onClick={() => handleTargetSelect(undefined, { target: selectedTarget }, undefined)}
variant="plain"
onClick={onToggleClick}
isExpanded={isTargetOpen}
variant="plainText"
icon={selectionPrefix}
>
{!selectedTarget
? undefined
: {
toString: () => getTargetRepresentation(selectedTarget),
compareTo: (other) => other.target.connectUrl === selectedTarget.connectUrl,
...{ target: selectedTarget },
}}
{!selectedTarget ? 'Select a Target' : getTargetRepresentation(selectedTarget)}
</MenuToggle>
)}
popperProps={{
enableFlip: true,
appendTo: portalRoot,
//maxHeight: '30em',
}}
>
<MenuSearch>
<MenuSearchInput>
<SearchIcon />
<SearchInput placeholder="Filter by target..." onSearch={handleTargetFilter} />
<SearchInput
placeholder="Filter by URL, alias, or discovery group..."
value={searchTerm}
onChange={(_, v) => setSearchTerm(v)}
/>
</MenuSearchInput>
{favorites}
{handleFavorite}
</MenuSearch>
<DropdownGroup label="Target Groups">{selectOptions}</DropdownGroup>
<Divider />
<DropdownList>{selectOptions}</DropdownList>
<MenuFooter>{selectFooter}</MenuFooter>
</Dropdown>
)}
Expand Down
Loading