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 11, 2024
1 parent a1c6775 commit 7d84398
Show file tree
Hide file tree
Showing 10 changed files with 386 additions and 18 deletions.
27 changes: 15 additions & 12 deletions src/views/virtualmachines/details/VirtualMachineNavPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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 VirtualMachineTreeView from '@virtualmachines/tree/VirtualMachineTreeView';

import { useVirtualMachineTabs } from './hooks/useVirtualMachineTabs';
import VirtualMachineNavPageTitle from './VirtualMachineNavPageTitle';
Expand Down Expand Up @@ -34,19 +35,21 @@ const VirtualMachineNavPage: React.FC<VirtualMachineDetailsPageProps> = ({
const pages = useVirtualMachineTabs();

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 @@ -33,6 +33,7 @@ import {
} from '@openshift-console/dynamic-plugin-sdk';
import { Pagination } from '@patternfly/react-core';
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 @@ -153,12 +154,16 @@ const VirtualMachinesList: FC<VirtualMachinesListProps> = ({ kind, namespace })
const noVMs = isEmpty(unfilteredData) && !vmsFilteredWithProxy;

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')}>
<VirtualMachinesCreateButton namespace={namespace} />
</ListPageHeader>
Expand Down Expand Up @@ -224,7 +229,7 @@ const VirtualMachinesList: FC<VirtualMachinesListProps> = ({ kind, namespace })
unfilteredData={unfilteredData}
/>
</ListPageBody>
</>
</VirtualMachineTreeView>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getName, getNamespace } from '@kubevirt-utils/resources/shared';
import { ResourceLink, RowProps, TableData } from '@openshift-console/dynamic-plugin-sdk';
import VirtualMachineActions from '@virtualmachines/actions/components/VirtualMachineActions/VirtualMachineActions';
import useVirtualMachineActionsProvider from '@virtualmachines/actions/hooks/useVirtualMachineActionsProvider';
import { setSelectedTreeItem, treeDataMap } from '@virtualmachines/tree/utils/utils';

import VirtualMachineStatus from '../VirtualMachineStatus/VirtualMachineStatus';
import { VMStatusConditionLabelList } from '../VMStatusConditionLabel';
Expand All @@ -32,6 +33,9 @@ const VirtualMachineRowLayout: React.FC<
<>
<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
6 changes: 6 additions & 0 deletions src/views/virtualmachines/tree/VirtualMachineTreeView.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.pf-v5-l-grid {
.tree-view-grid-item {
overflow-y: auto;
border-right: var(--pf-v5-global--BorderWidth--sm) solid var(--pf-v5-global--BorderColor--100);
}
}
112 changes: 112 additions & 0 deletions src/views/virtualmachines/tree/VirtualMachineTreeView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React, { FC, MouseEvent, useMemo } from 'react';
import { useLocation, useNavigate } from 'react-router-dom-v5-compat';
import { VirtualMachineModel } from 'src/views/dashboard-extensions/utils';

import { useQueryParamsMethods } from '@kubevirt-utils/components/ListPageFilter/hooks/useQueryParamsMethods';
import { convertResourceArrayToMap, getResourceUrl } from '@kubevirt-utils/resources/shared';
import { FilterValue, useActiveNamespace } from '@openshift-console/dynamic-plugin-sdk';
import { Grid, GridItem, TreeView, TreeViewDataItem } from '@patternfly/react-core';
import { TEXT_FILTER_LABELS_ID } from '@virtualmachines/list/hooks/constants';

import { useSyncClicksEffects } from './hooks/useSyncClicksEffects';
import { useTreeViewData } from './hooks/useTreeViewData';
import {
FOLDER_SELECTOR_PREFIX,
PROJECT_SELECTOR_PREFIX,
VM_FOLDER_LABEL,
} from './utils/constants';
import {
createTreeViewData,
selectedTreeItem,
setSelectedTreeItem,
treeDataMap,
} from './utils/utils';

import './VirtualMachineTreeView.scss';

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

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

const { isAdmin, loaded, loadError, projectNames, vms } = useTreeViewData();

const treeData = useMemo(() => {
const [data, dataMap] = createTreeViewData(
projectNames,
vms,
activeNamespace,
isAdmin,
pathname,
);
treeDataMap.value = dataMap;
return data;
}, [projectNames, vms, activeNamespace, isAdmin]);

useSyncClicksEffects(activeNamespace, loaded, location);

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)) {
setActiveNamespace(treeItemName);
return navigate(
getResourceUrl({
activeNamespace: treeItemName,
model: VirtualMachineModel,
}),
);
}

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

return (
<Grid style={{ height: document.getElementById('content-scrollable')?.offsetHeight || 0 }}>
<GridItem className="tree-view-grid-item" span={3}>
<TreeView
activeItems={selectedTreeItem.value}
data={treeData}
hasBadges={loaded}
hasSelectableNodes
onSelect={onSelect}
/>
</GridItem>
<GridItem span={9}>{children}</GridItem>
</Grid>
);
};

export default VirtualMachineTreeView;
52 changes: 52 additions & 0 deletions src/views/virtualmachines/tree/hooks/useSyncClicksEffects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useEffect } from 'react';
import { Location } from 'react-router-dom-v5-compat';

import { ALL_NAMESPACES, ALL_NAMESPACES_SESSION_KEY } from '@kubevirt-utils/hooks/constants';
import { VirtualMachineModelRef } from '@kubevirt-utils/models';

import { FOLDER_SELECTOR_PREFIX, PROJECT_SELECTOR_PREFIX } from '../utils/constants';
import { setSelectedTreeItem, treeDataMap } from '../utils/utils';

export const useSyncClicksEffects = (
activeNamespace: string,
loaded: boolean,
location: Location<any>,
) => {
useEffect(() => {
const queryParams = new URLSearchParams(location.search);

const folderFilterName = queryParams.get('labels')?.split('=')?.[1];
const pathname = location.pathname;
if (loaded) {
const dataMap = treeDataMap.value;
if (pathname.startsWith(`/k8s/${ALL_NAMESPACES}`)) {
setSelectedTreeItem(dataMap[ALL_NAMESPACES_SESSION_KEY]);
return;
}

if (
pathname.startsWith('/k8s/ns/') &&
(pathname.endsWith(VirtualMachineModelRef) ||
pathname.endsWith(`${VirtualMachineModelRef}/`))
) {
if (folderFilterName) {
setSelectedTreeItem(
dataMap[`${FOLDER_SELECTOR_PREFIX}/${activeNamespace}/${folderFilterName}`],
);
return;
}

setSelectedTreeItem(dataMap[`${PROJECT_SELECTOR_PREFIX}/${activeNamespace}`]);
return;
}

const vmName = pathname.split('/')[5];
if (vmName && pathname.includes(vmName)) {
setSelectedTreeItem(dataMap[`${activeNamespace}/${vmName}`]);
return;
}

return;
}
}, [activeNamespace, loaded, location.search, location.pathname]);
};
58 changes: 58 additions & 0 deletions src/views/virtualmachines/tree/hooks/useTreeViewData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useMemo } from 'react';

import { VirtualMachineModelGroupVersionKind } from '@kubevirt-ui/kubevirt-api/console';
import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt';
import { useIsAdmin } from '@kubevirt-utils/hooks/useIsAdmin';
import useProjects from '@kubevirt-utils/hooks/useProjects';
import { isEmpty } from '@kubevirt-utils/utils/utils';
import { useK8sWatchResource, useK8sWatchResources } from '@openshift-console/dynamic-plugin-sdk';
import { OBJECTS_FETCHING_LIMIT } from '@virtualmachines/utils';

type UseTreeViewData = {
isAdmin: boolean;
loaded: boolean;
loadError: any;
projectNames: string[];
vms: V1VirtualMachine[];
};

export const useTreeViewData = (): UseTreeViewData => {
const isAdmin = useIsAdmin();

const [projectNames, projectNamesLoaded, projectNamesError] = useProjects();

const [allVMs, allVMsLoaded] = useK8sWatchResource<V1VirtualMachine[]>({
groupVersionKind: VirtualMachineModelGroupVersionKind,
isList: true,
limit: OBJECTS_FETCHING_LIMIT,
});

// user has limited access, so we can only get vms from allowed namespaces
const allowedResources = useK8sWatchResources<{ [key: string]: V1VirtualMachine[] }>(
Object.fromEntries(
projectNamesLoaded && !isAdmin
? (projectNames || []).map((namespace) => [
namespace,
{
groupVersionKind: VirtualMachineModelGroupVersionKind,
isList: true,
namespace,
},
])
: [],
),
);

const memoizedVMs = useMemo(
() => (isAdmin ? allVMs : Object.values(allowedResources).flatMap((resource) => resource.data)),
[allVMs, allowedResources, isAdmin],
);

return {
isAdmin,
loaded: projectNamesLoaded && (isAdmin ? allVMsLoaded : !isEmpty(memoizedVMs)),
loadError: projectNamesError,
projectNames,
vms: memoizedVMs,
};
};
3 changes: 3 additions & 0 deletions src/views/virtualmachines/tree/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const VM_FOLDER_LABEL = 'vm.kubevirt.io/folder';
export const PROJECT_SELECTOR_PREFIX = 'projectSelector';
export const FOLDER_SELECTOR_PREFIX = 'folderSelector';
Loading

0 comments on commit 7d84398

Please sign in to comment.