-
Notifications
You must be signed in to change notification settings - Fork 620
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f77b81f
commit b3c5f17
Showing
12 changed files
with
409 additions
and
25 deletions.
There are no files selected for viewing
25 changes: 25 additions & 0 deletions
25
frontend/packages/console-app/src/components/nav/FavoriteNavItemResource.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import * as React from 'react'; | ||
import { NavItem } from '@patternfly/react-core'; | ||
import { NavLink } from 'react-router-dom'; | ||
import { ResourceNSNavItem } from '@console/dynamic-plugin-sdk'; | ||
|
||
export const FavoriteNavItemResource: React.FC<FavoriteNavItemResourceProps> = ({ | ||
className, | ||
dataAttributes, | ||
isActive, | ||
to, | ||
...navLinkProps | ||
}) => { | ||
return ( | ||
<NavItem className={className} isActive={isActive}> | ||
<NavLink {...navLinkProps} {...dataAttributes} to={to} /> | ||
</NavItem> | ||
); | ||
}; | ||
|
||
export type FavoriteNavItemResourceProps = { | ||
to: string; | ||
dataAttributes?: ResourceNSNavItem['properties']['dataAttributes']; | ||
isActive: boolean; | ||
className: string; | ||
}; |
38 changes: 38 additions & 0 deletions
38
frontend/packages/console-app/src/components/nav/FavoriteNavItemResources.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
.oc-favorite-resource.pf-v6-c-nav__item { | ||
.pf-v6-c-nav__link { | ||
display: flex; | ||
align-items: center; | ||
justify-content: space-between; | ||
flex-grow: 1; | ||
overflow: hidden; | ||
position: relative; | ||
text-overflow: ellipsis; | ||
white-space: nowrap; | ||
padding-top: 0; | ||
padding-bottom: 0; | ||
padding-right: 0; | ||
&:hover { | ||
--pf-v6-c-nav__section-title--PaddingRight: 30px; | ||
.oc-favorite-delete-button { | ||
.pf-v6-c-button__text { | ||
opacity: 1; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
.oc-favorite-delete-button { | ||
background-color: transparent !important; | ||
margin-left: auto; | ||
margin-right: var(--pf-t--global--spacer--lg); | ||
} | ||
|
||
.oc-favorite-menu { | ||
padding-top: 0 !important; | ||
padding-bottom: 0 !important; | ||
} | ||
|
||
.oc-no-favorites-message { | ||
padding-left: var(--pf-t--global--spacer--md); | ||
} |
95 changes: 95 additions & 0 deletions
95
frontend/packages/console-app/src/components/nav/FavoriteNavItemResources.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import * as React from 'react'; | ||
import { Nav, NavExpandable, NavList, Button } from '@patternfly/react-core'; | ||
import { StarIcon } from '@patternfly/react-icons'; | ||
import * as classNames from 'classnames'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { FAVORITES_CONFIG_MAP_KEY, FavoritesType } from '@console/internal/components/utils'; | ||
import { FAVORITES_LOCAL_STORAGE_KEY, useUserSettingsCompatibility } from '@console/shared'; | ||
import { FavoriteNavItemResource } from './FavoriteNavItemResource'; | ||
|
||
import './FavoriteNavItemResources.scss'; | ||
|
||
export const FavoriteNavItemResources: React.FC = () => { | ||
const { t } = useTranslation(); | ||
const [activeGroup, setActiveGroup] = React.useState(''); | ||
const [activeItem, setActiveItem] = React.useState(''); | ||
const currentUrlPath = window.location.pathname; | ||
|
||
const onSelect = ( | ||
_event: React.FormEvent<HTMLInputElement>, | ||
result: { itemId: number | string; groupId: number | string | null }, | ||
) => { | ||
setActiveGroup(result.groupId as string); | ||
setActiveItem(result.itemId as string); | ||
}; | ||
|
||
const [favorites, setFavorites, loaded] = useUserSettingsCompatibility<FavoritesType>( | ||
FAVORITES_CONFIG_MAP_KEY, | ||
FAVORITES_LOCAL_STORAGE_KEY, | ||
null, | ||
true, | ||
); | ||
|
||
React.useEffect(() => { | ||
if (loaded && favorites) { | ||
const currentFavorite = favorites.find((favorite) => favorite.url === currentUrlPath); | ||
if (currentFavorite) { | ||
setActiveGroup('favorites-group'); | ||
setActiveItem(`favorites-item-${currentFavorite.url}`); | ||
} | ||
} | ||
}, [loaded, favorites, currentUrlPath]); | ||
|
||
const navList = React.useMemo(() => { | ||
const handleUnfavorite = (favoriteUrl: string) => { | ||
const updatedFavorites = favorites?.filter((favorite) => favorite.url !== favoriteUrl); | ||
setFavorites(updatedFavorites); | ||
if (activeItem === `favorites-item-${favoriteUrl}`) { | ||
setActiveItem(''); | ||
} | ||
}; | ||
if (!loaded) return null; | ||
if (!favorites || favorites.length === 0) { | ||
return <div className="oc-no-favorites-message">{t('public~No favorites added')}</div>; | ||
} | ||
|
||
return favorites.map((favorite) => ( | ||
<FavoriteNavItemResource | ||
key={favorite.url} | ||
dataAttributes={{ | ||
'data-test': 'favorite-resource-item', | ||
}} | ||
className={classNames('oc-favorite-resource')} | ||
to={favorite.url} | ||
isActive={activeItem === `favorites-item-${favorite.url}`} | ||
> | ||
{favorite.name} | ||
<Button | ||
variant="plain" | ||
aria-label={`Unfavorite ${favorite.name}`} | ||
onClick={(e) => { | ||
e.preventDefault(); | ||
handleUnfavorite(favorite.url); | ||
}} | ||
className="oc-favorite-delete-button" | ||
icon={<StarIcon color="gold" />} | ||
/> | ||
</FavoriteNavItemResource> | ||
)); | ||
}, [favorites, activeItem, loaded, t, setFavorites]); | ||
|
||
return ( | ||
<Nav onSelect={onSelect} aria-label="favorite-resources" className="oc-favorite-menu"> | ||
<NavList> | ||
<NavExpandable | ||
title={t('public~Favorites')} | ||
groupId="favorites-group" | ||
isActive={activeGroup === 'favorites-group'} | ||
isExpanded={activeGroup === 'favorites-group'} | ||
> | ||
{navList} | ||
</NavExpandable> | ||
</NavList> | ||
</Nav> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
.co-actions-icon { | ||
background: none; | ||
background-color: transparent !important; | ||
border: none; | ||
cursor: pointer; | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
padding: 0; | ||
font-size: x-large; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
import * as React from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { | ||
Button, | ||
Form, | ||
FormGroup, | ||
FormHelperText, | ||
HelperText, | ||
HelperTextItem, | ||
TextInput, | ||
Tooltip, | ||
} from '@patternfly/react-core'; | ||
import { ModalVariant } from '@patternfly/react-core/deprecated'; | ||
import { | ||
FAVORITES_LOCAL_STORAGE_KEY, | ||
Modal, | ||
RedExclamationCircleIcon, | ||
useUserSettingsCompatibility, | ||
} from '@console/shared'; | ||
import { connectToModel } from '../../kinds'; | ||
import { StarIcon } from '@patternfly/react-icons'; | ||
|
||
export type FavoritesType = Array<{ | ||
name: string; | ||
url: string; | ||
}>; | ||
|
||
export const FAVORITES_CONFIG_MAP_KEY = 'console.favorites'; | ||
const MAX_FAVORITE_COUNT = 10; | ||
|
||
export const FavoriteButton = connectToModel(() => { | ||
const { t } = useTranslation(); | ||
const ref = React.useRef(); | ||
const [isStarred, setIsStarred] = React.useState(false); | ||
const [isModalOpen, setIsModalOpen] = React.useState(false); | ||
const [name, setName] = React.useState<string>(''); | ||
const [error, setError] = React.useState<string | null>(null); | ||
const [favorites, setFavorites, loaded] = useUserSettingsCompatibility<FavoritesType>( | ||
FAVORITES_CONFIG_MAP_KEY, | ||
FAVORITES_LOCAL_STORAGE_KEY, | ||
null, | ||
true, | ||
); | ||
|
||
const currentUrlPath = window.location.pathname; | ||
|
||
React.useEffect(() => { | ||
if (loaded) { | ||
const isCurrentlyFavorited = favorites?.some((favorite) => favorite.url === currentUrlPath); | ||
setIsStarred(isCurrentlyFavorited); | ||
} | ||
}, [loaded, favorites, currentUrlPath]); | ||
|
||
const handleStarClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
const isCurrentlyFavorited = favorites?.some((favorite) => favorite.url === currentUrlPath); | ||
if (isCurrentlyFavorited) { | ||
const updatedFavorites = favorites?.filter((favorite) => favorite.url !== currentUrlPath); | ||
setFavorites(updatedFavorites); | ||
setIsStarred(false); | ||
} else { | ||
setIsModalOpen(true); | ||
} | ||
}; | ||
|
||
const handleModalClose = () => { | ||
setError(''); | ||
setName(''); | ||
setIsModalOpen(false); | ||
}; | ||
|
||
const handleConfirmStar = () => { | ||
if (!name.trim()) { | ||
setError(t('public~Name is required.')); | ||
return; | ||
} | ||
const nameExists = favorites?.some((favorite) => favorite.name === name.trim()); | ||
if (nameExists) { | ||
setError( | ||
t( | ||
'public~The name {{favoriteName}} already exists in your favorites. Choose a unique name to save to your favorites.', | ||
{ favoriteName: name }, | ||
), | ||
); | ||
return; | ||
} | ||
|
||
const newFavorite = { name: name.trim(), url: currentUrlPath }; | ||
const updatedFavorites = [...(favorites || []), newFavorite]; | ||
setFavorites(updatedFavorites); | ||
setIsStarred((prev) => !prev); | ||
setError(''); | ||
setName(''); | ||
setIsModalOpen(false); | ||
}; | ||
|
||
const handleNameChange = (value: string) => { | ||
const alphanumericRegex = /^[a-zA-Z0-9- ]*$/; | ||
if (!alphanumericRegex.test(value)) { | ||
setError(t('Name can only contain letters, numbers, spaces, and hyphens.')); | ||
} else { | ||
setError(null); | ||
setName(value); | ||
} | ||
}; | ||
|
||
const tooltipText = t( | ||
'public~Maximum number of favorites ({{maxCount}}) reached. To add another favorite, remove an existing page from your favorites.', | ||
{ maxCount: MAX_FAVORITE_COUNT }, | ||
); | ||
|
||
return ( | ||
<> | ||
{favorites?.length >= MAX_FAVORITE_COUNT && !isStarred ? ( | ||
<Tooltip content={tooltipText} triggerRef={ref} position="left"> | ||
<Button | ||
ref={ref} | ||
icon={<StarIcon color={isStarred ? 'gold' : 'gray'} />} | ||
className="co-actions-icon" | ||
variant="link" | ||
aria-label="save-favorite" | ||
aria-pressed={isStarred} | ||
onClick={handleStarClick} | ||
isDisabled={true} | ||
/> | ||
</Tooltip> | ||
) : ( | ||
<Button | ||
icon={<StarIcon color={isStarred ? 'gold' : 'gray'} />} | ||
className="co-actions-icon" | ||
variant="link" | ||
aria-label="save-favorite" | ||
aria-pressed={isStarred} | ||
onClick={handleStarClick} | ||
/> | ||
)} | ||
|
||
{isModalOpen && ( | ||
<Modal | ||
title={t('public~Add to favorites')} | ||
isOpen={isModalOpen} | ||
onClose={handleModalClose} | ||
actions={[ | ||
<Button key="confirm" variant="primary" onClick={handleConfirmStar}> | ||
{t('public~Save')} | ||
</Button>, | ||
<Button key="cancel" variant="link" onClick={handleModalClose}> | ||
{t('public~Cancel')} | ||
</Button>, | ||
]} | ||
variant={ModalVariant.small} | ||
> | ||
<Form> | ||
<FormGroup label={t('public~Name')} isRequired fieldId="input-name"> | ||
<TextInput | ||
id="input-name" | ||
data-test="input-name" | ||
name="name" | ||
type="text" | ||
onChange={(e, v) => handleNameChange(v)} | ||
value={name || ''} | ||
autoFocus | ||
required | ||
maxLength={20} | ||
/> | ||
{error && ( | ||
<FormHelperText> | ||
<HelperText> | ||
<HelperTextItem variant="error" icon={<RedExclamationCircleIcon />}> | ||
{error} | ||
</HelperTextItem> | ||
</HelperText> | ||
</FormHelperText> | ||
)} | ||
</FormGroup> | ||
</Form> | ||
</Modal> | ||
)} | ||
</> | ||
); | ||
}); |
Oops, something went wrong.