Skip to content

Commit

Permalink
CNV-45822: Tree view part 1
Browse files Browse the repository at this point in the history
Signed-off-by: Aviv Turgeman <[email protected]>
  • Loading branch information
avivtur committed Nov 24, 2024
1 parent e56ac98 commit 656f212
Show file tree
Hide file tree
Showing 17 changed files with 735 additions and 16 deletions.
5 changes: 5 additions & 0 deletions locales/en/plugin__kubevirt-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@
"Clone template": "Clone template",
"Clone volume": "Clone volume",
"Cloning": "Cloning",
"Close": "Close",
"Close header": "Close header",
"Closing it will cause the upload to fail. You may still navigate the console.": "Closing it will cause the upload to fail. You may still navigate the console.",
"Cloud-init": "Cloud-init",
Expand All @@ -273,6 +274,7 @@
"Cluster provided": "Cluster provided",
"Cluster scope migrations": "Cluster scope migrations",
"Collapse": "Collapse",
"Collapse all": "Collapse all",
"Column management": "Column management",
"Complete time": "Complete time",
"Complete time:": "Complete time:",
Expand Down Expand Up @@ -521,6 +523,7 @@
"Example: For Windows, get a link to the ": "Example: For Windows, get a link to the ",
"Example: quay.io/containerdisks/centos:7-2009": "Example: quay.io/containerdisks/centos:7-2009",
"Example: your company name": "Example: your company name",
"Expand all": "Expand all",
"Explore {{kind}} list": "Explore {{kind}} list",
"Expose RDP Service": "Expose RDP Service",
"Failed": "Failed",
Expand Down Expand Up @@ -875,6 +878,7 @@
"Only one disk can be bootable at a time, this option is disabled if the VirtualMachine is running or if this disk is the current boot source": "Only one disk can be bootable at a time, this option is disabled if the VirtualMachine is running or if this disk is the current boot source",
"Only one disk can be bootable at a time. The bootable flag will be removed from \"{{initialBootDiskName}}\" and placed on this disk.": "Only one disk can be bootable at a time. The bootable flag will be removed from \"{{initialBootDiskName}}\" and placed on this disk.",
"Only VM-related alerts in your project will be shown": "Only VM-related alerts in your project will be shown",
"Open": "Open",
"Open Console": "Open Console",
"Open logs in a new window": "Open logs in a new window",
"Open web console": "Open web console",
Expand Down Expand Up @@ -1040,6 +1044,7 @@
"Scripts": "Scripts",
"SCSI": "SCSI",
"SCSI persistent reservation": "SCSI persistent reservation",
"Search": "Search",
"Search by {{filterName}}...": "Search by {{filterName}}...",
"Search by labels...": "Search by labels...",
"Search by name...": "Search by name...",
Expand Down
29 changes: 17 additions & 12 deletions src/views/virtualmachines/details/VirtualMachineNavPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { SidebarEditorProvider } from '@kubevirt-utils/components/SidebarEditor/
import useInstanceTypeExpandSpec from '@kubevirt-utils/resources/vm/hooks/useInstanceTypeExpandSpec';
import { isInstanceTypeVM } from '@kubevirt-utils/resources/vm/utils/instanceTypes';
import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk';
import { useSignals } from '@preact/signals-react/runtime';
import VirtualMachineTreeView from '@virtualmachines/tree/VirtualMachineTreeView';

import { useVirtualMachineTabs } from './hooks/useVirtualMachineTabs';
import VirtualMachineNavPageTitle from './VirtualMachineNavPageTitle';
Expand All @@ -32,21 +34,24 @@ const VirtualMachineNavPage: React.FC<VirtualMachineDetailsPageProps> = ({
const [instanceTypeExpandedSpec] = useInstanceTypeExpandSpec(vm);

const pages = useVirtualMachineTabs();
useSignals();

return (
<SidebarEditorProvider>
<VirtualMachineNavPageTitle
name={name}
vm={isInstanceTypeVM(vm) ? instanceTypeExpandedSpec : vm}
/>
<div className="VirtualMachineNavPage--tabs__main">
<HorizontalNavbar
instanceTypeExpandedSpec={instanceTypeExpandedSpec}
pages={pages}
vm={vm}
<VirtualMachineTreeView>
<SidebarEditorProvider>
<VirtualMachineNavPageTitle
name={name}
vm={isInstanceTypeVM(vm) ? instanceTypeExpandedSpec : vm}
/>
</div>
</SidebarEditorProvider>
<div className="VirtualMachineNavPage--tabs__main">
<HorizontalNavbar
instanceTypeExpandedSpec={instanceTypeExpandedSpec}
pages={pages}
vm={vm}
/>
</div>
</SidebarEditorProvider>
</VirtualMachineTreeView>
);
};

Expand Down
13 changes: 9 additions & 4 deletions src/views/virtualmachines/list/VirtualMachinesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
import { Flex, FlexItem, Pagination } from '@patternfly/react-core';
import { useSignals } from '@preact/signals-react/runtime';
import useQuery from '@virtualmachines/details/tabs/metrics/NetworkCharts/hook/useQuery';
import VirtualMachineTreeView from '@virtualmachines/tree/VirtualMachineTreeView';
import { OBJECTS_FETCHING_LIMIT } from '@virtualmachines/utils';

import { useVMListFilters } from '../utils';
Expand Down Expand Up @@ -162,12 +163,16 @@ const VirtualMachinesList: FC<VirtualMachinesListProps> = ({ kind, namespace })
const allVMsSelected = data?.length === selectedVMs.value.length;

if (loaded && noVMs) {
return <VirtualMachineEmptyState catalogURL={catalogURL} namespace={namespace} />;
return (
<VirtualMachineTreeView>
<VirtualMachineEmptyState catalogURL={catalogURL} namespace={namespace} />
</VirtualMachineTreeView>
);
}

return (
<>
{/* All of this table and components should be replaced to our own fitted components */}
/* All of this table and components should be replaced to our own fitted components */
<VirtualMachineTreeView onFilterChange={onFilterChange}>
<ListPageHeader title={t('VirtualMachines')}>
<Flex>
<FlexItem>
Expand Down Expand Up @@ -246,7 +251,7 @@ const VirtualMachinesList: FC<VirtualMachinesListProps> = ({ kind, namespace })
/>
</div>
</ListPageBody>
</>
</VirtualMachineTreeView>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Checkbox } from '@patternfly/react-core';
import VirtualMachineActions from '@virtualmachines/actions/components/VirtualMachineActions/VirtualMachineActions';
import useVirtualMachineActionsProvider from '@virtualmachines/actions/hooks/useVirtualMachineActionsProvider';
import { deselectVM, isVMSelected, selectVM } from '@virtualmachines/list/selectedVMs';
import { setSelectedTreeItem, treeDataMap } from '@virtualmachines/tree/utils/utils';

import VirtualMachineStatus from '../VirtualMachineStatus/VirtualMachineStatus';
import { VMStatusConditionLabelList } from '../VMStatusConditionLabel';
Expand Down Expand Up @@ -43,6 +44,9 @@ const VirtualMachineRowLayout: React.FC<
</TableData>
<TableData activeColumnIDs={activeColumnIDs} className="pf-m-width-15 vm-column" id="name">
<ResourceLink
onClick={() => {
setSelectedTreeItem(treeDataMap.value[`${getNamespace(obj)}/${getName(obj)}`]);
}}
groupVersionKind={VirtualMachineModelGroupVersionKind}
name={getName(obj)}
namespace={getNamespace(obj)}
Expand Down
25 changes: 25 additions & 0 deletions src/views/virtualmachines/tree/VirtualMachineTreeView.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.vms-tree-view {
overflow-x: hidden;
}

.vms-tree-view-body,
.vms-tree-view-toolbar,
.vms-tree-view-toolbar-content {
padding: 0;
}

.pf-v5-c-tree-view__node .pf-v5-c-tree-view__node-count,
.vms-tree-view-toolbar-buttons {
display: flex;
flex-grow: 1;
flex-direction: row-reverse;
}

.vms-tree-view-toolbar-buttons {
align-self: center;
margin-right: var(--pf-global--spacer--sm);

&__padding {
padding: var(--pf-global--spacer--sm);
}
}
137 changes: 137 additions & 0 deletions src/views/virtualmachines/tree/VirtualMachineTreeView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import React, { FC, MouseEvent, useMemo } from 'react';
import { useLocation, useNavigate } from 'react-router-dom-v5-compat';

import VirtualMachineModel from '@kubevirt-ui/kubevirt-api/console/models/VirtualMachineModel';
import { useQueryParamsMethods } from '@kubevirt-utils/components/ListPageFilter/hooks/useQueryParamsMethods';
import { convertResourceArrayToMap, getResourceUrl } from '@kubevirt-utils/resources/shared';
import { getContentScrollableElement } from '@kubevirt-utils/utils/utils';
import { FilterValue, useActiveNamespace } from '@openshift-console/dynamic-plugin-sdk';
import {
Drawer,
DrawerContent,
DrawerContentBody,
DrawerPanelBody,
DrawerPanelContent,
TreeView,
TreeViewDataItem,
} from '@patternfly/react-core';
import { TEXT_FILTER_LABELS_ID } from '@virtualmachines/list/hooks/constants';

import TreeViewToolbar from './components/TreeViewToolbar';
import { useHideNamespaceBar } from './hooks/useHideNamespaceBar';
import { useSyncClicksEffects } from './hooks/useSyncClicksEffects';
import { useTreeViewData } from './hooks/useTreeViewData';
import { useTreeViewSearch } from './hooks/useTreeViewSearch';
import {
FOLDER_SELECTOR_PREFIX,
PROJECT_SELECTOR_PREFIX,
TREE_VIEW_PANEL_ID,
VM_FOLDER_LABEL,
} from './utils/constants';
import {
createTreeViewData,
selectedTreeItem,
setSelectedTreeItem,
treeViewOpen,
} from './utils/utils';

import './VirtualMachineTreeView.scss';

type VirtualMachineTreeViewProps = {
onFilterChange?: (type: string, value: FilterValue) => void;
};

const VirtualMachineTreeView: FC<VirtualMachineTreeViewProps> = ({ children, onFilterChange }) => {
const [activeNamespace] = useActiveNamespace();
const navigate = useNavigate();
const location = useLocation();

const { setOrRemoveQueryArgument } = useQueryParamsMethods();
const { isAdmin, loaded, loadError, projectNames, vms } = useTreeViewData();
const vmsMapper = useMemo(() => convertResourceArrayToMap(vms, true), [vms]);

const treeData = useMemo(
() => createTreeViewData(projectNames, vms, activeNamespace, isAdmin, location.pathname),
[projectNames, vms, activeNamespace, isAdmin, location.pathname],
);

const { filteredItems, onSearch, setShowAll, showAll } = useTreeViewSearch(treeData);

useSyncClicksEffects(activeNamespace, loaded, location);
useHideNamespaceBar();

if (loadError) return <>{children}</>;

const onSelect = (_event: MouseEvent, treeViewItem: TreeViewDataItem) => {
setSelectedTreeItem(treeViewItem);
onFilterChange?.(TEXT_FILTER_LABELS_ID, null);

const treeItemName = treeViewItem.name as string;
if (treeViewItem.id.startsWith(FOLDER_SELECTOR_PREFIX)) {
const [_, folderNamespace] = treeViewItem.id.split('/');
navigate(
getResourceUrl({
activeNamespace: folderNamespace,
model: VirtualMachineModel,
}),
);
setOrRemoveQueryArgument(TEXT_FILTER_LABELS_ID, `${VM_FOLDER_LABEL}=${treeItemName}`);
return onFilterChange?.(TEXT_FILTER_LABELS_ID, {
all: [`${VM_FOLDER_LABEL}=${treeItemName}`],
});
}

if (treeViewItem.id.startsWith(PROJECT_SELECTOR_PREFIX)) {
return navigate(
getResourceUrl({
activeNamespace: treeItemName,
model: VirtualMachineModel,
}),
);
}

const [vmNamespace, vmName] = treeViewItem.id.split('/');
return navigate(
getResourceUrl({
activeNamespace: vmNamespace,
model: VirtualMachineModel,
resource: vmsMapper?.[vmNamespace]?.[vmName],
}),
);
};

return (
<Drawer isExpanded isInline position="start">
<DrawerContent
panelContent={
<DrawerPanelContent
style={{
height: getContentScrollableElement().offsetHeight || 0,
}}
className="vms-tree-view"
id={TREE_VIEW_PANEL_ID}
isResizable={treeViewOpen.value}
>
<DrawerPanelBody className="vms-tree-view-body">
<TreeView
toolbar={
<TreeViewToolbar onSearch={onSearch} setShowAll={setShowAll} showAll={showAll} />
}
activeItems={selectedTreeItem.value}
allExpanded={showAll}
data={!treeViewOpen.value ? [] : filteredItems ?? treeData}
hasBadges={loaded}
hasSelectableNodes
onSelect={onSelect}
/>
</DrawerPanelBody>
</DrawerPanelContent>
}
>
<DrawerContentBody>{children}</DrawerContentBody>
</DrawerContent>
</Drawer>
);
};

export default VirtualMachineTreeView;
80 changes: 80 additions & 0 deletions src/views/virtualmachines/tree/components/TreeViewToolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React, { ChangeEvent, Dispatch, FC, SetStateAction } from 'react';

import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation';
import {
Button,
ButtonVariant,
Toolbar,
ToolbarContent,
ToolbarItem,
Tooltip,
TreeViewSearch,
} from '@patternfly/react-core';
import { PanelCloseIcon, PanelOpenIcon } from '@patternfly/react-icons';

import {
CLOSED_DRAWER_SIZE,
OPEN_DRAWER_SIZE,
PANEL_WIDTH_PROPERTY,
TREE_VIEW_PANEL_ID,
TREE_VIEW_SEARCH_ID,
} from '../utils/constants';
import { treeViewOpen } from '../utils/utils';

type TreeViewToolbarProps = {
onSearch: (event: ChangeEvent<HTMLInputElement>) => void;
setShowAll: Dispatch<SetStateAction<boolean>>;
showAll: boolean;
};

const TreeViewToolbar: FC<TreeViewToolbarProps> = ({ onSearch, setShowAll, showAll }) => {
const { t } = useKubevirtTranslation();

const toggleDrawer = () => {
treeViewOpen.value = !treeViewOpen.value;
const panel = document.getElementById(TREE_VIEW_PANEL_ID);
panel.style.setProperty(
PANEL_WIDTH_PROPERTY,
treeViewOpen.value ? OPEN_DRAWER_SIZE : CLOSED_DRAWER_SIZE,
);
};

return (
<Toolbar className="vms-tree-view-toolbar">
<ToolbarContent className="vms-tree-view-toolbar-content">
<ToolbarItem>
{treeViewOpen.value && (
<TreeViewSearch
id={TREE_VIEW_SEARCH_ID}
name={TREE_VIEW_SEARCH_ID}
onSearch={onSearch}
placeholder={t('Search')}
/>
)}
</ToolbarItem>
<ToolbarItem className="vms-tree-view-toolbar-buttons">
<Tooltip content={treeViewOpen.value ? t('Close') : t('Open')}>
<Button
className="vms-tree-view-toolbar-buttons__padding"
onClick={toggleDrawer}
variant={ButtonVariant.plain}
>
{treeViewOpen.value ? <PanelCloseIcon /> : <PanelOpenIcon />}
</Button>
</Tooltip>
{treeViewOpen.value && (
<Button
className="vms-tree-view-toolbar-buttons__padding"
onClick={() => setShowAll((prev) => !prev)}
variant={ButtonVariant.link}
>
{showAll ? t('Collapse all') : t('Expand all')}
</Button>
)}
</ToolbarItem>
</ToolbarContent>
</Toolbar>
);
};

export default TreeViewToolbar;
Loading

0 comments on commit 656f212

Please sign in to comment.