diff --git a/locales/en/plugin__kubevirt-plugin.json b/locales/en/plugin__kubevirt-plugin.json index edfbb440f..d7283b0de 100644 --- a/locales/en/plugin__kubevirt-plugin.json +++ b/locales/en/plugin__kubevirt-plugin.json @@ -957,6 +957,7 @@ "Project labels": "Project labels", "Project name to clone the template to": "Project name to clone the template to", "Project selector": "Project selector", + "Projects": "Projects", "Provider": "Provider", "Provisioning": "Provisioning", "Public SSH key": "Public SSH key", @@ -1132,6 +1133,7 @@ "Show all": "Show all", "Show all alerts": "Show all alerts", "Show less": "Show less", + "Show OCP default projects": "Show OCP default projects", "Show top 10": "Show top 10", "Show top 5": "Show top 5", "Show uncategorized VirtualMachines": "Show uncategorized VirtualMachines", diff --git a/src/views/virtualmachines/tree/VirtualMachineTreeView.scss b/src/views/virtualmachines/tree/VirtualMachineTreeView.scss index 388b05ab4..d8c2258c1 100644 --- a/src/views/virtualmachines/tree/VirtualMachineTreeView.scss +++ b/src/views/virtualmachines/tree/VirtualMachineTreeView.scss @@ -2,24 +2,46 @@ overflow-x: hidden; } -.vms-tree-view-body, -.vms-tree-view-toolbar, -.vms-tree-view-toolbar-content { +.pf-v5-c-drawer__body.vms-tree-view-body, +.pf-v5-c-toolbar.vms-tree-view-toolbar, +.pf-v5-c-toolbar__content.vms-tree-view-toolbar-content, +.pf-v5-c-drawer__body.vms-tree-view-actions { padding: 0; } -.pf-v5-c-tree-view__node .pf-v5-c-tree-view__node-count, -.vms-tree-view-toolbar-buttons { +.pf-v5-c-drawer__panel-main { + .pf-v5-c-drawer__body { + padding: 0 var(--pf-global--spacer--sm); + height: fit-content; + } +} +.pf-v5-c-toolbar__content.vms-tree-view-toolbar-content, +.vms-tree-view-toolbar-section, +.vms-tree-view-search-input { + width: 100%; +} +.vms-tree-view-search-input { + margin: var(--pf-global--spacer--sm) var(--pf-global--spacer--sm) 0 var(--pf-global--spacer--sm); +} +.pf-v5-c-toolbar.vms-tree-view-toolbar { + margin-bottom: var(--pf-global--spacer--sm); +} +.pf-v5-c-tree-view__node .pf-v5-c-tree-view__node-count { display: flex; flex-grow: 1; flex-direction: row-reverse; } -.vms-tree-view-toolbar-buttons { - align-self: center; - margin-right: var(--pf-global--spacer--sm); +.pf-v5-c-tree-view { + padding: 0; +} - &__padding { - padding: var(--pf-global--spacer--sm); - } +.vms-tree-view-toolbar-action { + padding: var(--pf-global--spacer--sm); +} + +.vms-tree-view-toolbar-default-project-switch { + margin-left: var(--pf-global--spacer--sm); + margin-bottom: 0; + font-weight: var(--pf-global--FontWeight--light); } diff --git a/src/views/virtualmachines/tree/VirtualMachineTreeView.tsx b/src/views/virtualmachines/tree/VirtualMachineTreeView.tsx index 82e0587cd..0639a4308 100644 --- a/src/views/virtualmachines/tree/VirtualMachineTreeView.tsx +++ b/src/views/virtualmachines/tree/VirtualMachineTreeView.tsx @@ -1,20 +1,29 @@ -import React, { FC, MouseEvent, useMemo } from 'react'; +import React, { FC, MouseEvent, useEffect, 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 { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation'; +import useLocalStorage from '@kubevirt-utils/hooks/useLocalStorage'; 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 { + Button, + ButtonVariant, Drawer, + DrawerActions, DrawerContent, DrawerContentBody, + DrawerHead, DrawerPanelBody, DrawerPanelContent, + Title, + Tooltip, TreeView, TreeViewDataItem, } from '@patternfly/react-core'; +// import { PanelCloseIcon, PanelOpenIcon } from '@patternfly/react-icons'; import { TEXT_FILTER_LABELS_ID } from '@virtualmachines/list/hooks/constants'; import TreeViewToolbar from './components/TreeViewToolbar'; @@ -22,9 +31,17 @@ import { useHideNamespaceBar } from './hooks/useHideNamespaceBar'; import { useSyncClicksEffects } from './hooks/useSyncClicksEffects'; import { useTreeViewData } from './hooks/useTreeViewData'; import { useTreeViewSearch } from './hooks/useTreeViewSearch'; +import ClosePanelIcon from './icons/ClosePanelIcon'; +import CollapseAllIcon from './icons/CollapseAllIcon'; +import ExpandAllIcon from './icons/ExpandAllIcon'; +import OpenPanelIcon from './icons/OpenPanelIcon'; import { + CLOSED_DRAWER_SIZE, FOLDER_SELECTOR_PREFIX, + OPEN_DRAWER_SIZE, + PANEL_WIDTH_PROPERTY, PROJECT_SELECTOR_PREFIX, + TREE_VIEW_LAST_WIDTH, TREE_VIEW_PANEL_ID, VM_FOLDER_LABEL, } from './utils/constants'; @@ -42,12 +59,23 @@ type VirtualMachineTreeViewProps = { }; const VirtualMachineTreeView: FC = ({ children, onFilterChange }) => { + const { t } = useKubevirtTranslation(); const [activeNamespace] = useActiveNamespace(); const navigate = useNavigate(); const location = useLocation(); - const { setOrRemoveQueryArgument } = useQueryParamsMethods(); - const { isAdmin, loaded, loadError, projectNames, vms } = useTreeViewData(); + const [drawerWidth, setDrawerWidth] = useLocalStorage(TREE_VIEW_LAST_WIDTH, OPEN_DRAWER_SIZE); + + const { + isAdmin, + loaded, + loadError, + projectNames, + setShowDefaultProjects, + showDefaultProjects, + vms, + } = useTreeViewData(); + const vmsMapper = useMemo(() => convertResourceArrayToMap(vms, true), [vms]); const treeData = useMemo( @@ -57,8 +85,13 @@ const VirtualMachineTreeView: FC = ({ children, onF const { filteredItems, onSearch, setShowAll, showAll } = useTreeViewSearch(treeData); + const drawerPanel = document.getElementById(TREE_VIEW_PANEL_ID); + useSyncClicksEffects(activeNamespace, loaded, location); useHideNamespaceBar(); + useEffect(() => { + drawerPanel?.style?.setProperty(PANEL_WIDTH_PROPERTY, drawerWidth); + }, [drawerPanel, drawerWidth]); if (loadError) return <>{children}; @@ -100,6 +133,32 @@ const VirtualMachineTreeView: FC = ({ children, onF ); }; + const toggleDrawer = () => { + treeViewOpen.value = !treeViewOpen.value; + + const size = treeViewOpen.value ? OPEN_DRAWER_SIZE : CLOSED_DRAWER_SIZE; + drawerPanel.style.setProperty(PANEL_WIDTH_PROPERTY, size); + setDrawerWidth(size); + }; + + const onResize = ( + _e: globalThis.MouseEvent | React.KeyboardEvent | TouchEvent, + width: number, + ) => { + setDrawerWidth(`${String(width)}px`); + }; + const togglePanelButton = ( + + + + ); + return ( = ({ children, onF height: getContentScrollableElement().offsetHeight || 0, }} className="vms-tree-view" + defaultSize={drawerWidth} id={TREE_VIEW_PANEL_ID} isResizable={treeViewOpen.value} + onResize={onResize} > - - - } - activeItems={selectedTreeItem.value} - allExpanded={showAll} - data={!treeViewOpen.value ? [] : filteredItems ?? treeData} - hasBadges={loaded} - hasSelectableNodes - onSelect={onSelect} - /> - + {!treeViewOpen.value ? ( + togglePanelButton + ) : ( + <> + + + {t('Projects')} + + + + + {togglePanelButton} + + + + + + + )} } > diff --git a/src/views/virtualmachines/tree/components/TreeViewToolbar.tsx b/src/views/virtualmachines/tree/components/TreeViewToolbar.tsx index 2e583072c..e3cf80d53 100644 --- a/src/views/virtualmachines/tree/components/TreeViewToolbar.tsx +++ b/src/views/virtualmachines/tree/components/TreeViewToolbar.tsx @@ -1,77 +1,63 @@ -import React, { ChangeEvent, Dispatch, FC, SetStateAction } from 'react'; +import React, { ChangeEvent, FC } from 'react'; import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation'; import { - Button, - ButtonVariant, + Divider, + Stack, + StackItem, + Switch, 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 { HIDE, SHOW, TREE_VIEW_SEARCH_ID } from '../utils/constants'; import { treeViewOpen } from '../utils/utils'; type TreeViewToolbarProps = { onSearch: (event: ChangeEvent) => void; - setShowAll: Dispatch>; - showAll: boolean; + setShowDefaultProjects: (show: string) => void; + showDefaultProjects: string; }; -const TreeViewToolbar: FC = ({ onSearch, setShowAll, showAll }) => { +const TreeViewToolbar: FC = ({ + onSearch, + setShowDefaultProjects, + showDefaultProjects, +}) => { 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 ( - + - - {treeViewOpen.value && ( - - )} - - - - - - {treeViewOpen.value && ( - - )} - + + + + {treeViewOpen.value && ( + + )} + + + + + {treeViewOpen.value && ( + setShowDefaultProjects(checked ? SHOW : HIDE)} + /> + )} + + + ); diff --git a/src/views/virtualmachines/tree/hooks/useTreeViewData.ts b/src/views/virtualmachines/tree/hooks/useTreeViewData.ts index 9b66c3b10..ce6dac2a8 100644 --- a/src/views/virtualmachines/tree/hooks/useTreeViewData.ts +++ b/src/views/virtualmachines/tree/hooks/useTreeViewData.ts @@ -3,20 +3,30 @@ 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 useLocalStorage from '@kubevirt-utils/hooks/useLocalStorage'; import useProjects from '@kubevirt-utils/hooks/useProjects'; import { useK8sWatchResource, useK8sWatchResources } from '@openshift-console/dynamic-plugin-sdk'; import { OBJECTS_FETCHING_LIMIT } from '@virtualmachines/utils'; +import { HIDE, SHOW_DEFAULT_PROJECTS_KEY } from '../utils/constants'; +import { isSystemNamespace } from '../utils/utils'; + type UseTreeViewData = { isAdmin: boolean; loaded: boolean; loadError: any; projectNames: string[]; + setShowDefaultProjects: (newValue: string) => void; + showDefaultProjects: string; vms: V1VirtualMachine[]; }; export const useTreeViewData = (): UseTreeViewData => { const isAdmin = useIsAdmin(); + const [showDefaultProjects, setShowDefaultProjects] = useLocalStorage( + SHOW_DEFAULT_PROJECTS_KEY, + HIDE, + ); const [projectNames, projectNamesLoaded, projectNamesError] = useProjects(); @@ -55,7 +65,12 @@ export const useTreeViewData = (): UseTreeViewData => { ? allVMsLoaded : Object.values(allowedResources).some((resource) => resource.loaded)), loadError: projectNamesError, - projectNames, + projectNames: + showDefaultProjects === HIDE + ? projectNames?.filter((ns) => !isSystemNamespace(ns)) + : projectNames, + setShowDefaultProjects, + showDefaultProjects, vms: memoizedVMs, }; }; diff --git a/src/views/virtualmachines/tree/icons/ClosePanelIcon.tsx b/src/views/virtualmachines/tree/icons/ClosePanelIcon.tsx new file mode 100644 index 000000000..d69e9668b --- /dev/null +++ b/src/views/virtualmachines/tree/icons/ClosePanelIcon.tsx @@ -0,0 +1,28 @@ +import React, { FC } from 'react'; + +const ClosePanelIcon: FC = () => ( + + + + + + + + + + +); + +export default ClosePanelIcon; diff --git a/src/views/virtualmachines/tree/icons/CollapseAllIcon.tsx b/src/views/virtualmachines/tree/icons/CollapseAllIcon.tsx new file mode 100644 index 000000000..ca5cc94af --- /dev/null +++ b/src/views/virtualmachines/tree/icons/CollapseAllIcon.tsx @@ -0,0 +1,34 @@ +import React, { FC } from 'react'; + +const CollapseAllIcon: FC = () => ( + + + + + + + + + + + +); + +export default CollapseAllIcon; diff --git a/src/views/virtualmachines/tree/icons/ExpandAllIcon.tsx b/src/views/virtualmachines/tree/icons/ExpandAllIcon.tsx new file mode 100644 index 000000000..10f2ea21d --- /dev/null +++ b/src/views/virtualmachines/tree/icons/ExpandAllIcon.tsx @@ -0,0 +1,34 @@ +import React, { FC } from 'react'; + +const ExpandAllIcon: FC = () => ( + + + + + + + + + + + +); + +export default ExpandAllIcon; diff --git a/src/views/virtualmachines/tree/icons/OpenPanelIcon.tsx b/src/views/virtualmachines/tree/icons/OpenPanelIcon.tsx new file mode 100644 index 000000000..7e28f2e59 --- /dev/null +++ b/src/views/virtualmachines/tree/icons/OpenPanelIcon.tsx @@ -0,0 +1,28 @@ +import React, { FC } from 'react'; + +const OpenPanelIcon: FC = () => ( + + + + + + + + + + +); + +export default OpenPanelIcon; diff --git a/src/views/virtualmachines/tree/icons/PausedVirtualMachineIcon.tsx b/src/views/virtualmachines/tree/icons/PausedVirtualMachineIcon.tsx index c8311f02a..03e53c2e3 100644 --- a/src/views/virtualmachines/tree/icons/PausedVirtualMachineIcon.tsx +++ b/src/views/virtualmachines/tree/icons/PausedVirtualMachineIcon.tsx @@ -1,29 +1,27 @@ import React, { FC } from 'react'; -const PausedVirtualMachineIcon: FC = () => { - return ( - - - - - - ); -}; +const PausedVirtualMachineIcon: FC = () => ( + + + + + +); export default PausedVirtualMachineIcon; diff --git a/src/views/virtualmachines/tree/icons/RunningVirtualMachineIcon.tsx b/src/views/virtualmachines/tree/icons/RunningVirtualMachineIcon.tsx index 540c47715..ebc70ae2c 100644 --- a/src/views/virtualmachines/tree/icons/RunningVirtualMachineIcon.tsx +++ b/src/views/virtualmachines/tree/icons/RunningVirtualMachineIcon.tsx @@ -1,29 +1,27 @@ import React, { FC } from 'react'; -const RunningVirtualMachineIcon: FC = () => { - return ( - - - - - - ); -}; +const RunningVirtualMachineIcon: FC = () => ( + + + + + +); export default RunningVirtualMachineIcon; diff --git a/src/views/virtualmachines/tree/icons/StoppedVirtualMachineIcon.tsx b/src/views/virtualmachines/tree/icons/StoppedVirtualMachineIcon.tsx index 7f8500f4f..2ad753a2d 100644 --- a/src/views/virtualmachines/tree/icons/StoppedVirtualMachineIcon.tsx +++ b/src/views/virtualmachines/tree/icons/StoppedVirtualMachineIcon.tsx @@ -1,36 +1,34 @@ import React, { FC } from 'react'; -const StoppedVirtualMachineIcon: FC = () => { - return ( - - +const StoppedVirtualMachineIcon: FC = () => ( + + + + - - - - - - - - - - ); -}; + + + + + + + +); export default StoppedVirtualMachineIcon; diff --git a/src/views/virtualmachines/tree/utils/constants.ts b/src/views/virtualmachines/tree/utils/constants.ts index 973f61248..afdb5e75e 100644 --- a/src/views/virtualmachines/tree/utils/constants.ts +++ b/src/views/virtualmachines/tree/utils/constants.ts @@ -7,3 +7,10 @@ export const TREE_VIEW_SEARCH_ID = 'vms-tree-view-search-input'; export const OPEN_DRAWER_SIZE = '400px'; export const CLOSED_DRAWER_SIZE = '30px'; export const PANEL_WIDTH_PROPERTY = '--pf-v5-c-drawer__panel--md--FlexBasis'; + +export const SYSTEM_NAMESPACES_PREFIX = ['kube-', 'openshift-', 'kubernetes-']; +export const SYSTEM_NAMESPACES = ['default', 'openshift']; +export const SHOW_DEFAULT_PROJECTS_KEY = 'showDefaultProjects'; +export const TREE_VIEW_LAST_WIDTH = 'treeViewLastWidth'; +export const SHOW = 'show'; +export const HIDE = 'hide'; diff --git a/src/views/virtualmachines/tree/utils/utils.tsx b/src/views/virtualmachines/tree/utils/utils.tsx index 074197f61..32d9a1300 100644 --- a/src/views/virtualmachines/tree/utils/utils.tsx +++ b/src/views/virtualmachines/tree/utils/utils.tsx @@ -9,7 +9,13 @@ import { signal } from '@preact/signals-react'; import { statusIcon } from '../icons/utils'; -import { FOLDER_SELECTOR_PREFIX, PROJECT_SELECTOR_PREFIX, VM_FOLDER_LABEL } from './constants'; +import { + FOLDER_SELECTOR_PREFIX, + PROJECT_SELECTOR_PREFIX, + SYSTEM_NAMESPACES, + SYSTEM_NAMESPACES_PREFIX, + VM_FOLDER_LABEL, +} from './constants'; export const treeViewOpen = signal(true); export const treeDataMap = signal>(null); @@ -175,3 +181,10 @@ export const filterItems = (item: TreeViewDataItem, input: string) => { ); } }; + +export const isSystemNamespace = (projectName: string) => { + const startsWithNamespace = SYSTEM_NAMESPACES_PREFIX.some((ns) => projectName.startsWith(ns)); + const isNamespace = SYSTEM_NAMESPACES.includes(projectName); + + return startsWithNamespace || isNamespace; +};