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
4 changes: 2 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@
"@patternfly/react-code-editor": "~6.4.0",
"@patternfly/react-component-groups": "~6.4.0",
"@patternfly/react-core": "~6.4.0",
"@patternfly/react-data-view": "6.4.0-prerelease.12",
"@patternfly/react-drag-drop": "~6.4.0",
"@patternfly/react-data-view": "~6.4.0-prerelease.12",
"@patternfly/react-drag-drop": "~6.5.0-prerelease.38",
Copy link
Member Author

@logonoff logonoff Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to pick up patternfly/patternfly-react#12217 / patternfly/patternfly-react#12240. no impact on plugins as this is not a shared module and does not provide CSS

"@patternfly/react-icons": "~6.4.0",
"@patternfly/react-log-viewer": "~6.3.0",
"@patternfly/react-styles": "~6.4.0",
Expand Down
1 change: 0 additions & 1 deletion frontend/packages/console-app/locales/en/console-app.json
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,6 @@
"Navigation": "Navigation",
"Pinned resources": "Pinned resources",
"Main navigation": "Main navigation",
"Drag to reorder": "Drag to reorder",
"Unpin": "Unpin",
"Remove from navigation?": "Remove from navigation?",
"Remove": "Remove",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@
border: none;
}
}
&--dragging {
.oc-pinned-resource__unpin-button,
.oc-pinned-resource__drag-button {
opacity: 0;
}
}
}

.co-pinned-resource-item {
display: flex;
margin-bottom: var(--pf-t--global--spacer--sm);
}
67 changes: 39 additions & 28 deletions frontend/packages/console-app/src/components/nav/PerspectiveNav.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { FC, ReactElement } from 'react';
import { useState, useEffect, useMemo } from 'react';
import type { FC } from 'react';
import { useCallback, useState, useEffect, useMemo } from 'react';
import { NavGroup, NavList } from '@patternfly/react-core';
import { css } from '@patternfly/react-styles';
import type { DragDropSortProps, DraggableObject } from '@patternfly/react-drag-drop';
import { DragDropSort } from '@patternfly/react-drag-drop';
import { useTranslation } from 'react-i18next';
import {
useActivePerspective,
isNavSection,
isNavItem,
} from '@console/dynamic-plugin-sdk/src/lib-core';
import withDragDropContext from '@console/internal/components/utils/drag-drop-context';
import { modelFor } from '@console/internal/module/k8s';
import { usePinnedResources } from '@console/shared/src/hooks/usePinnedResources';
import PinnedResource from './PinnedResource';
Expand All @@ -23,7 +23,6 @@ const PerspectiveNav: FC<{}> = () => {
const allNavExtensions = useNavExtensionsForPerspective(activePerspective);
const [pinnedResources, setPinnedResources, pinnedResourcesLoaded] = usePinnedResources();
const [validPinnedResources, setValidPinnedResources] = useState<string[]>([]);
const [isDragged, setIsDragged] = useState(false);
const { t } = useTranslation();

useEffect(() => {
Expand All @@ -36,29 +35,29 @@ const PerspectiveNav: FC<{}> = () => {
return getSortedNavExtensions(topLevelNavExtensions);
}, [allNavExtensions]);

const getPinnedItems = (): ReactElement[] =>
validPinnedResources.map((resource, idx) => (
<PinnedResource
key={`${resource}_${idx.toString()}`}
idx={idx}
resourceRef={resource}
onChange={setPinnedResources}
onReorder={setValidPinnedResources}
onDrag={setIsDragged}
navResources={validPinnedResources}
draggable={validPinnedResources.length > 1}
/>
));
const draggableItems = useMemo<DraggableObject[]>(() => {
return validPinnedResources.map((res, idx) => ({
id: res,
props: { className: 'co-pinned-resource-item' },
content: (
<PinnedResource
key={`${res}_${idx.toString()}`}
idx={idx}
resourceRef={res}
onChange={setPinnedResources}
navResources={validPinnedResources}
/>
),
}));
}, [validPinnedResources, setPinnedResources]);

const NavGroupWithDnd = withDragDropContext(() => (
<NavGroup
title=""
aria-label={t('console-app~Pinned resources')}
className={css('no-title', { 'oc-perspective-nav--dragging': isDragged })}
>
{getPinnedItems()}
</NavGroup>
));
const onDrop = useCallback<DragDropSortProps['onDrop']>(
(_, newItems) => {
const newOrder = newItems.map((item) => item.id as string);
setPinnedResources(newOrder);
},
[setPinnedResources],
);

// We have to use NavList if there is at least one extension that will render an <li>, but we
// can't use NavList if there are no extensions that render an <li>
Expand All @@ -71,7 +70,19 @@ const PerspectiveNav: FC<{}> = () => {
{orderedNavExtensions.map((extension) => (
<PluginNavItem key={extension.uid} extension={extension} />
))}
{pinnedResourcesLoaded && validPinnedResources?.length > 0 ? <NavGroupWithDnd /> : null}
{pinnedResourcesLoaded && validPinnedResources?.length > 0 ? (
<NavGroup
className="co-draggable-nav-group no-title"
title=""
aria-label={t('console-app~Pinned resources')}
>
{draggableItems.length === 1 ? (
draggableItems[0].content
) : (
<DragDropSort items={draggableItems} onDrop={onDrop} />
)}
</NavGroup>
) : null}
</>
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,60 +0,0 @@
.oc-pinned-resource.pf-v6-c-nav__item {
.pf-v6-c-nav__link {
display: block;
flex-grow: 1;
overflow: hidden;
position: relative;
text-overflow: ellipsis;
white-space: nowrap;

&:hover {
--pf-v6-c-nav__section-title--PaddingRight: 30px;
.oc-pinned-resource__unpin-button,
.oc-pinned-resource__drag-button {
.oc-pinned-resource__delete-icon,
.oc-pinned-resource__drag-icon {
opacity: 1;
}
}
}
}

.oc-pinned-resource__unpin-button {
background-color: transparent !important;
padding: 0;
position: absolute;
right: 0;

.oc-pinned-resource__delete-icon {
opacity: 0;
}
}

.oc-pinned-resource__drag-button {
background-color: transparent !important;
cursor: move;
left: -10px;
padding: 0;
position: absolute;

.oc-pinned-resource__drag-icon {
opacity: 0;
}
}

.oc-pinned-resource__unpin-button:focus-visible,
.oc-pinned-resource__drag-button:focus-visible {
.oc-pinned-resource__delete-icon,
.oc-pinned-resource__drag-icon {
opacity: 1;
}
}
}

.oc-pinned-resource--dragging {
outline: 1px dashed var(--pf-t--global--border--color--on-secondary);
overflow: hidden;
div {
opacity: 0.5;
}
}
126 changes: 31 additions & 95 deletions frontend/packages/console-app/src/components/nav/PinnedResource.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,32 @@
import type { FC, MouseEvent, ReactElement } from 'react';
import { Button } from '@patternfly/react-core';
import { GripVerticalIcon } from '@patternfly/react-icons/dist/esm/icons/grip-vertical-icon';
import type { FC, MouseEvent } from 'react';
import { Button, Flex, FlexItem, Truncate } from '@patternfly/react-core';
import { MinusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/minus-circle-icon';
import { css } from '@patternfly/react-styles';
import { debounce } from 'lodash';
import { useDrag, useDrop } from 'react-dnd';
import { useTranslation } from 'react-i18next';
import type { K8sModel } from '@console/internal/module/k8s';
import { modelFor } from '@console/internal/module/k8s';
import { useK8sModel } from '@console/shared/src/hooks/useK8sModel';
import './PinnedResource.scss';
import { NavItemResource } from './NavItemResource';
import useConfirmNavUnpinModal from './useConfirmNavUnpinModal';

type PinnedResourceProps = {
resourceRef?: string;
navResources?: string[];
onChange?: (pinnedResources: string[]) => void;
idx?: number;
draggable?: boolean;
onReorder?: (pinnedResources: string[]) => void;
onDrag?: (dragging: boolean) => void;
};

type DraggableButtonProps = {
dragRef?: (node) => void | null;
};
interface PinnedResourceProps {
resourceRef: string;
navResources: string[];
onChange: (pinnedResources: string[]) => void;
idx: number;
}

type RemoveButtonProps = {
resourceRef?: string;
navResources?: string[];
onChange?: (pinnedResources: string[]) => void;
};
interface RemoveButtonProps {
resourceRef: string;
navResources: string[];
onChange: (pinnedResources: string[]) => void;
}

export type DragItem = {
idx: number;
id: string;
type: string;
};

const DraggableButton: FC<DraggableButtonProps> = ({ dragRef }) => {
const { t } = useTranslation();
return (
<Button
icon={<GripVerticalIcon className="oc-pinned-resource__drag-icon" />}
ref={dragRef}
className="oc-pinned-resource__drag-button"
variant="link"
type="button"
aria-label={t('console-app~Drag to reorder')}
/>
);
};

const RemoveButton: FC<RemoveButtonProps> = ({ resourceRef, navResources, onChange }) => {
const { t } = useTranslation();
const confirmNavUnpinModal = useConfirmNavUnpinModal(navResources, onChange);
Expand All @@ -63,61 +37,16 @@ const RemoveButton: FC<RemoveButtonProps> = ({ resourceRef, navResources, onChan
};
return (
<Button
icon={<MinusCircleIcon className="oc-pinned-resource__delete-icon" />}
className="oc-pinned-resource__unpin-button"
icon={<MinusCircleIcon />}
variant="link"
aria-label={t('console-app~Unpin')}
onClick={(e) => unPin(e, resourceRef)}
/>
);
};

const reorder = (list: string[], startIndex: number, destIndex: number) => {
const result = [...list];
const [removed] = result.splice(startIndex, 1);
result.splice(destIndex, 0, removed);
return result;
};

const PinnedResource: FC<PinnedResourceProps> = ({
resourceRef,
onChange,
navResources,
idx,
draggable,
onReorder,
onDrag,
}) => {
const PinnedResource: FC<PinnedResourceProps> = ({ resourceRef, onChange, navResources }) => {
const { t } = useTranslation();
const [, drag, preview] = useDrag({
item: { type: 'NavItem', id: `NavItem-${idx}`, idx },
end: (item, monitor) => {
const didDrop = monitor.didDrop();
if (!didDrop) {
onDrag(false);
}
},
});

const [{ isOver }, drop] = useDrop({
accept: 'NavItem',
collect: (monitor) => ({
isOver: monitor.isOver(),
}),
hover: debounce((item: DragItem) => {
if (item.idx === idx) {
return;
}
onReorder(reorder(navResources, item.idx, idx));
// monitor item updated here to avoid expensive index searches.
item.idx = idx;
onDrag(true);
}, 10),
drop() {
onChange(navResources); // update user-settings when the resource is dropped
onDrag(false);
},
});

const [model] = useK8sModel(resourceRef);
if (!model) {
Expand All @@ -137,25 +66,32 @@ const PinnedResource: FC<PinnedResourceProps> = ({
};
const label = getLabelForResourceRef(resourceRef);
const duplicates = navResources.filter((res) => getLabelForResourceRef(res) === label).length > 1;
const previewRef = draggable ? (node: ReactElement) => preview(drop(node)) : null;
return (
<NavItemResource
key={`pinned-${resourceRef}`}
namespaced={namespaced}
title={duplicates ? `${label}: ${apiGroup || 'core'}/${apiVersion}` : null}
model={{ group: apiGroup, version: apiVersion, kind }}
id={resourceRef}
dragRef={previewRef}
className="pf-v6-u-flex-grow-1"
dataAttributes={{
'data-test': draggable ? 'draggable-pinned-resource-item' : 'pinned-resource-item',
className: 'pf-v6-u-py-0 pf-v6-u-pr-0',
'data-test': 'draggable-pinned-resource-item',
}}
className={css('oc-pinned-resource', {
'oc-pinned-resource--dragging': draggable && isOver,
})}
>
{draggable ? <DraggableButton dragRef={drag} /> : null}
{label}
<RemoveButton onChange={onChange} navResources={navResources} resourceRef={resourceRef} />
<Flex
justifyContent={{ default: 'justifyContentSpaceBetween' }}
alignItems={{ default: 'alignItemsCenter' }}
flexWrap={{ default: 'nowrap' }}
style={{ width: '100%' }}
>
<FlexItem className="pf-v6-u-m-0">
<Truncate content={label} />
</FlexItem>
<FlexItem className="pf-v6-u-mr-xs">
<RemoveButton onChange={onChange} navResources={navResources} resourceRef={resourceRef} />
</FlexItem>
</Flex>
</NavItemResource>
);
};
Expand Down
Loading