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
25 changes: 24 additions & 1 deletion frontend/packages/console-app/locales/en/console-app.json
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,30 @@
"Unpin": "Unpin",
"Remove from navigation?": "Remove from navigation?",
"Remove": "Remove",
"Rotational": "Rotational",
"SSD": "SSD",
"Local disks": "Local disks",
"Unable to load local disks": "Unable to load local disks",
"Model": "Model",
"Serial number": "Serial number",
"Vendor": "Vendor",
"HCTL": "HCTL",
"No local disks found": "No local disks found",
"Node storage": "Node storage",
"No claim": "No claim",
"None": "None",
"Mounted persistent volumes": "Mounted persistent volumes",
"Unable to load persistent volumes": "Unable to load persistent volumes",
"PVC": "PVC",
"Pod": "Pod",
"Capacity": "Capacity",
"No persistent volumes found": "No persistent volumes found",
"Machine": "Machine",
"Error loading machine": "Error loading machine",
"Machine not found": "Machine not found",
"Error loading machine config pool": "Error loading machine config pool",
"Configuration": "Configuration",
"Machine config pool not found": "Machine config pool not found",
"This action cannot be undone. Deleting a node will instruct Kubernetes that the node is down or unrecoverable and delete all pods scheduled to that node. If the node is still running but unresponsive and the node is deleted, stateful workloads and persistent volumes may suffer corruption or data loss. Only delete a node that you have confirmed is completely stopped and cannot be restored.": "This action cannot be undone. Deleting a node will instruct Kubernetes that the node is down or unrecoverable and delete all pods scheduled to that node. If the node is still running but unresponsive and the node is deleted, stateful workloads and persistent volumes may suffer corruption or data loss. Only delete a node that you have confirmed is completely stopped and cannot be restored.",
"Mark as schedulable": "Mark as schedulable",
"Mark as unschedulable": "Mark as unschedulable",
Expand Down Expand Up @@ -403,7 +427,6 @@
"Annotations": "Annotations",
"Annotation_one": "Annotation",
"Annotation_other": "Annotations",
"Machine": "Machine",
"Provider ID": "Provider ID",
"Unschedulable": "Unschedulable",
"Created": "Created",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '@console/shared/src/components/actions';
import { isWindowsNode } from '@console/shared/src/selectors/node';
import { nodeStatus } from '../../status/node';
import { NodeConfiguration } from './configuration/NodeConfiguration';
import NodeDashboard from './node-dashboard/NodeDashboard';
import NodeDetails from './NodeDetails';
import NodeLogs from './NodeLogs';
Expand Down Expand Up @@ -43,6 +44,12 @@ export const NodeDetailsPage: FC<ComponentProps<typeof DetailsPage>> = (props) =
component: NodeDetails,
},
navFactory.editYaml(),
{
href: 'configuration',
// t('console-app~Configuration')
nameKey: 'console-app~Configuration',
component: NodeConfiguration,
},
navFactory.pods(NodePodsPage),
navFactory.logs(NodeLogs),
navFactory.events(ResourceEventStream),
Expand Down
109 changes: 109 additions & 0 deletions frontend/packages/console-app/src/components/nodes/NodeSubNavPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type { ComponentType, FC } from 'react';
import { useMemo } from 'react';
import { useResolvedExtensions } from '@openshift/dynamic-plugin-sdk';
import { Bullseye, Flex, FlexItem, Spinner, Tab, Tabs, TabTitleText } from '@patternfly/react-core';
import { useTranslation } from 'react-i18next';
import type { K8sResourceCommon, NodeKind, NodeSubNavTab } from '@console/dynamic-plugin-sdk/src';
import { isNodeSubNavTab } from '@console/dynamic-plugin-sdk/src';
import type { PageComponentProps } from '@console/internal/components/utils';
import { useQueryParamsMutator } from '@console/internal/components/utils';
import { useTranslatedExtensions } from '@console/plugin-sdk/src/utils/useTranslatedExtensions';
import { useQueryParams } from '@console/shared/src';

export type SubPageType = {
component: ComponentType<PageComponentProps<K8sResourceCommon>>;
tabId: string;
name?: string;
nameKey?: string;
priority: number;
};

type NodeSubNavPageProps = {
obj: NodeKind;
pageId: string;
standardPages: SubPageType[];
};

export const NodeSubNavPage: FC<NodeSubNavPageProps> = ({ obj, pageId, standardPages }) => {
const { t } = useTranslation();
const queryParams = useQueryParams();
const { setAllQueryArguments } = useQueryParamsMutator();
const activeTabKey = queryParams.get('activeTab');

const setActiveTabKey = (key: string) => {
setAllQueryArguments({ activeTab: key });
};

const [subTabExtensions, extensionsResolved] = useResolvedExtensions<NodeSubNavTab>(
isNodeSubNavTab,
);
const nodeSubTabExtensions = useTranslatedExtensions(subTabExtensions ?? []);

const pages: SubPageType[] = useMemo(() => {
if (!extensionsResolved) {
return standardPages;
}
return [
...standardPages,
...nodeSubTabExtensions
.filter((ext) => ext.properties.parentTab === pageId)
.map((ext) => ({
...ext.properties.page,
component: ext.properties.component,
})),
].sort((a, b) => b.priority - a.priority);
}, [pageId, standardPages, nodeSubTabExtensions, extensionsResolved]);

const activePage = pages.find((page) => page.tabId === activeTabKey) ?? (pages[0] || null);
const Component = activePage?.component;

return (
<Flex
className="pf-v6-u-h-100 pf-v6-u-ml-md"
flexWrap={{ default: 'nowrap' }}
spaceItems={{ default: 'spaceItemsMd' }}
alignItems={{ default: 'alignItemsFlexStart' }}
>
{!extensionsResolved ? (
<FlexItem className="pf-v6-u-h-100" flex={{ default: 'flex_1' }}>
<Bullseye>
<Spinner />
</Bullseye>
</FlexItem>
) : (
<>
<FlexItem className="pf-v6-u-h-100">
<Tabs
className="pf-v6-u-pt-md"
activeKey={activeTabKey || pages[0].tabId}
component="nav"
isVertical
usePageInsets
isSubtab
onSelect={(_e, tabId) => {
setActiveTabKey(String(tabId));
}}
>
{pages.map(({ nameKey, name, tabId }) => {
return (
<Tab
key={tabId}
eventKey={tabId}
data-test-id={`subnav-${tabId}`}
title={<TabTitleText>{nameKey ? t(nameKey) : name}</TabTitleText>}
aria-controls={undefined} // there is no corresponding tab content to control, so this ID is invalid
/>
);
})}
</Tabs>
</FlexItem>
{Component ? (
<FlexItem flex={{ default: 'flex_1' }} className="pf-v6-u-h-100">
<Component obj={obj} />
</FlexItem>
) : null}
</>
)}
</Flex>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
filterVirtualMachineInstancesByNode,
useIsKubevirtPluginActive,
useWatchVirtualMachineInstances,
} from '@console/app/src/components/nodes/NodeVmUtils';
} from '@console/app/src/components/nodes/utils/NodeVmUtils';
import type { K8sModel } from '@console/dynamic-plugin-sdk/src/api/dynamic-core-api';
import {
getGroupVersionKindForResource,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { FC } from 'react';
import type { NodeKind } from '@console/dynamic-plugin-sdk/src';
import { NodeSubNavPage } from '../NodeSubNavPage';
import NodeStorage from './node-storage/NodeStorage';
import NodeMachine from './NodeMachine';

type NodeConfigurationProps = {
obj: NodeKind;
};

const standardPages = [
{
tabId: 'storage',
// t('console-app~Storage')
nameKey: 'console-app~Storage',
component: NodeStorage,
priority: 70,
},
{
tabId: 'machine',
// t('console-app~Machine')
nameKey: 'console-app~Machine',
component: NodeMachine,
priority: 50,
},
];

export const NodeConfiguration: FC<NodeConfigurationProps> = ({ obj }) => (
<NodeSubNavPage obj={obj} pageId="configuration" standardPages={standardPages} />
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import type { ComponentType, FC } from 'react';
import { useMemo } from 'react';
import { Grid, GridItem } from '@patternfly/react-core';
import { useTranslation } from 'react-i18next';
import { useK8sWatchResource } from '@console/dynamic-plugin-sdk/src/utils/k8s/hooks';
import { MachineDetails } from '@console/internal/components/machine';
import {
MachineConfigPoolCharacteristics,
MachineConfigPoolSummary,
} from '@console/internal/components/machine-config-pool';
import type { PageComponentProps } from '@console/internal/components/utils';
import { SectionHeading, WorkloadPausedAlert } from '@console/internal/components/utils';
import { MachineConfigPoolModel, MachineModel } from '@console/internal/models';
import type { MachineConfigPoolKind, MachineKind, NodeKind } from '@console/internal/module/k8s';
import { LabelSelector } from '@console/internal/module/k8s/label-selector';
import { getNodeMachineNameAndNamespace } from '@console/shared/src';
import PaneBody from '@console/shared/src/components/layout/PaneBody';

const SkeletonDetails: FC = () => (
<div data-test="skeleton-detail-view" className="skeleton-detail-view">
<div className="skeleton-detail-view--head" />
<div className="skeleton-detail-view--grid">
<div className="skeleton-detail-view--column">
<div className="skeleton-detail-view--tile skeleton-detail-view--tile-plain" />
<div className="skeleton-detail-view--tile skeleton-detail-view--tile-resource" />
<div className="skeleton-detail-view--tile skeleton-detail-view--tile-labels" />
<div className="skeleton-detail-view--tile skeleton-detail-view--tile-resource" />
</div>
<div className="skeleton-detail-view--column">
<div className="skeleton-detail-view--tile skeleton-detail-view--tile-plain" />
<div className="skeleton-detail-view--tile skeleton-detail-view--tile-plain" />
<div className="skeleton-detail-view--tile skeleton-detail-view--tile-resource" />
<div className="skeleton-detail-view--tile skeleton-detail-view--tile-plain" />
</div>
</div>
</div>
);

const NodeMachine: ComponentType<PageComponentProps<NodeKind>> = ({ obj }) => {
const { t } = useTranslation();
const [machineName, machineNamespace] = getNodeMachineNameAndNamespace(obj);
const [machine, machineLoaded, machineLoadError] = useK8sWatchResource<MachineKind>(
machineName && machineNamespace
? {
groupVersionKind: {
kind: MachineModel.kind,
group: MachineModel.apiGroup,
version: MachineModel.apiVersion,
},
name: machineName,
namespace: machineNamespace,
}
: null,
);

const [
machineConfigPools,
machineConfigPoolsLoaded,
machineConfigPoolsLoadError,
] = useK8sWatchResource<MachineConfigPoolKind[]>({
groupVersionKind: {
kind: MachineConfigPoolModel.kind,
group: MachineConfigPoolModel.apiGroup,
version: MachineConfigPoolModel.apiVersion,
},
isList: true,
});

const machineConfigPool = useMemo(() => {
if (!machineConfigPoolsLoaded || !machineConfigPools?.length) {
return undefined;
}
return machineConfigPools.find((mcp) => {
if (!mcp.spec?.nodeSelector) {
return false;
}
const labelSelector = new LabelSelector(mcp.spec.nodeSelector);
return labelSelector.matches(obj);
});
}, [machineConfigPools, machineConfigPoolsLoaded, obj]);

const paused = machineConfigPool?.spec?.paused;

return (
<>
{machineLoadError ? (
<div>{t('console-app~Error loading machine')}</div>
) : machineLoaded ? (
machine ? (
<MachineDetails obj={machine} hideConditions />
) : (
<div>{t('console-app~Machine not found')}</div>
)
) : (
<SkeletonDetails />
)}
{machineConfigPoolsLoadError ? (
<div>{t('console-app~Error loading machine config pool')}</div>
) : machineConfigPoolsLoaded ? (
machineConfigPool ? (
<PaneBody>
{paused && (
<WorkloadPausedAlert model={MachineConfigPoolModel} obj={machineConfigPool} />
)}
<Grid hasGutter>
<GridItem sm={6}>
<SectionHeading text={t('console-app~Configuration')} />
<MachineConfigPoolSummary obj={machineConfigPool} />
</GridItem>
<GridItem sm={6}>
<SectionHeading text={t('console-app~MachineConfigs')} />
<MachineConfigPoolCharacteristics obj={machineConfigPool} />
</GridItem>
</Grid>
</PaneBody>
) : (
<div>{t('console-app~Machine config pool not found')}</div>
)
) : (
<SkeletonDetails />
)}
</>
);
};

export default NodeMachine;
Loading