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
129 changes: 120 additions & 9 deletions frontend/packages/console-app/locales/en/console-app.json
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,112 @@
"Unpin": "Unpin",
"Remove from navigation?": "Remove from navigation?",
"Remove": "Remove",
"auto-reboot": "auto-reboot",
"machine replacement": "machine replacement",
"template remediation": "template remediation",
"{{prefix}}: {{remediation}}; Drain: {{timeout}} timeout": "{{prefix}}: {{remediation}}; Drain: {{timeout}} timeout",
"{{prefix}}: {{remediation}}": "{{prefix}}: {{remediation}}",
"{{minMinutes}}-{{maxMinutes}} min": "{{minMinutes}}-{{maxMinutes}} min",
"Details": "Details",
"Unable to load high availability details": "Unable to load high availability details",
"Ready": "Ready",
"Unavailable": "Unavailable",
"Remediation": "Remediation",
"Estimated recovery time": "Estimated recovery time",
"Machine/Node health checks": "Machine/Node health checks",
"Unable to load health checks": "Unable to load health checks",
"Scope": "Scope",
"Selector": "Selector",
"Unhealthy conditions": "Unhealthy conditions",
"Last triggered": "Last triggered",
"No matching MachineHealthChecks or NodeHealthChecks": "No matching MachineHealthChecks or NodeHealthChecks",
"High availability": "High availability",
"SNR - Self Node Remediation": "SNR - Self Node Remediation",
"FAR - Fence Agent Remediation": "FAR - Fence Agent Remediation",
"MDR - Metal3-driven Remediation": "MDR - Metal3-driven Remediation",
"Unknown remediation": "Unknown remediation",
"Node remediation agents": "Node remediation agents",
"Unable to load remediation agents": "Unable to load remediation agents",
"Triggered by": "Triggered by",
"Config object": "Config object",
"Last action": "Last action",
"No matching remediation actions": "No matching remediation actions",
"Host addresses": "Host addresses",
"Management": "Management",
"NICs": "NICs",
"Boot interface MAC": "Boot interface MAC",
"Detached": "Detached",
"No power management": "No power management",
"Restart pending": "Restart pending",
"On": "On",
"Powering off": "Powering off",
"Powering on": "Powering on",
"Off": "Off",
"Address": "Address",
"Firmware": "Firmware",
"Power state": "Power state",
"Credentials": "Credentials",
"namespace {{namespace}}": "namespace {{namespace}}",
"BMC Configuration": "BMC Configuration",
"Unable to load BMC configuration": "Unable to load BMC configuration",
"There is no BMC configuration associated with this node": "There is no BMC configuration associated with this node",
"Machine details": "Machine details",
"Machine is not available": "Machine is not available",
"Unable to load Machine resources": "Unable to load Machine resources",
"There is no machine associated with this node": "There is no machine associated with this node",
"Phase": "Phase",
"Provider state": "Provider state",
"Machine role": "Machine role",
"Instance type": "Instance type",
"Region": "Region",
"Machine addresses": "Machine addresses",
"Rotational": "Rotational",
"SSD": "SSD",
"Local disks": "Local disks",
"Unable to load local disks": "Unable to load local disks",
"Unable to load BareMetalHost": "Unable to load BareMetalHost",
"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",
"Capacity": "Capacity",
"Pod": "Pod",
"No persistent volumes found": "No persistent volumes found",
"Operating system": "Operating system",
"Machine": "Machine",
"MachineConfigPool": "MachineConfigPool",
"MachineConfigPools are not available": "MachineConfigPools are not available",
"Unable to load MachineConfigPool resources": "Unable to load MachineConfigPool resources",
"There is no MachineConfigPool associated with this node": "There is no MachineConfigPool associated with this node",
"Max unavailable machines": "Max unavailable machines",
"Paused": "Paused",
"True": "True",
"False": "False",
"Node selector": "Node selector",
"MachineConfig selector": "MachineConfig selector",
"Current configuration": "Current configuration",
"Current configuration source": "Current configuration source",
"Performance": "Performance",
"Logs": "Logs",
"CPU": "CPU",
"CPU Utilisation": "CPU Utilisation",
"CPU Saturation (Load per CPU)": "CPU Saturation (Load per CPU)",
"Memory": "Memory",
"Memory Utilisation": "Memory Utilisation",
"Memory Saturation (Major Page Faults)": "Memory Saturation (Major Page Faults)",
"Network": "Network",
"Network Utilisation (Bytes Receive/Transmit)": "Network Utilisation (Bytes Receive/Transmit)",
"Network Saturation (Drops Receive/Transmit)": "Network Saturation (Drops Receive/Transmit)",
"Disk IO": "Disk IO",
"Disk IO Utilisation": "Disk IO Utilisation",
"Disk IO Saturation": "Disk IO Saturation",
"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 @@ -381,10 +487,7 @@
"Activity": "Activity",
"Disk": "Disk",
"Network interface": "Network interface",
"CPU": "CPU",
"Details": "Details",
"Node name": "Node name",
"Instance type": "Instance type",
"Not available": "Not available",
"Node addresses": "Node addresses",
"Uptime": "Uptime",
Expand All @@ -406,7 +509,6 @@
"Only one {{ machineHealthCheckLabel }} resource should match this node.": "Only one {{ machineHealthCheckLabel }} resource should match this node.",
"Not configured": "Not configured",
"No conditions": "No conditions",
"Memory": "Memory",
"Network in": "Network in",
"Network out": "Network out",
"Utilization": "Utilization",
Expand All @@ -425,24 +527,23 @@
"Annotations": "Annotations",
"Annotation_one": "Annotation",
"Annotation_other": "Annotations",
"Machine": "Machine",
"Provider ID": "Provider ID",
"Unschedulable": "Unschedulable",
"Created": "Created",
"Operating system": "Operating system",
"OS image": "OS image",
"Architecture": "Architecture",
"Kernel version": "Kernel version",
"Boot ID": "Boot ID",
"Container runtime": "Container runtime",
"Kubelet version": "Kubelet version",
"Kube-Proxy version": "Kube-Proxy version",
"Configuration": "Configuration",
"Health": "Health",
"Workload": "Workload",
"Machine set": "Machine set",
"This count is based on your access permissions and might not include all virtual machines.": "This count is based on your access permissions and might not include all virtual machines.",
"MachineConfigPool": "MachineConfigPool",
"{{formattedCores}} cores / {{totalCores}} cores": "{{formattedCores}} cores / {{totalCores}} cores",
"Node": "Node",
"Ready": "Ready",
"Not Ready": "Not Ready",
"Discovered": "Discovered",
"control-plane": "control-plane",
Expand All @@ -465,6 +566,17 @@
"Certificate approval required": "Certificate approval required",
"An error occurred. Please try again": "An error occurred. Please try again",
"No new Pods or workloads will be placed on this Node until it's marked as schedulable.": "No new Pods or workloads will be placed on this Node until it's marked as schedulable.",
"NodeHealthCheck": "NodeHealthCheck",
"NodeHealthChecks": "NodeHealthChecks",
"All machines": "All machines",
"MachineSet {{name}}": "MachineSet {{name}}",
"Machine role {{role}}": "Machine role {{role}}",
"Selected machines": "Selected machines",
"Cluster-wide": "Cluster-wide",
"Node role {{role}}": "Node role {{role}}",
"Node roles {{roles}}": "Node roles {{roles}}",
"Selected nodes": "Selected nodes",
"Unable to load VirtualMachines": "Unable to load VirtualMachines",
"Identity providers": "Identity providers",
"Mapping method": "Mapping method",
"Remove identity provider": "Remove identity provider",
Expand Down Expand Up @@ -508,7 +620,6 @@
"Min available": "Min available",
"Max unavailable": "Max unavailable",
"Allowed disruption": "Allowed disruption",
"Selector": "Selector",
"Label query over pods whose evictions are managed by the disruption budget. Anull selector will match no pods, while an empty ({}) selector will select all pods within the namespace.": "Label query over pods whose evictions are managed by the disruption budget. Anull selector will match no pods, while an empty ({}) selector will select all pods within the namespace.",
"Resource is already covered by another PodDisruptionBudget": "Resource is already covered by another PodDisruptionBudget",
"Availability requirement value": "Availability requirement value",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { FC, ComponentProps } from 'react';
import { useCallback } from 'react';
import { FLAG_NODE_MGMT_V1 } from '@console/app/src/consts';
import { ResourceEventStream } from '@console/internal/components/events';
import { DetailsPage } from '@console/internal/components/factory';
import { PodsPage } from '@console/internal/components/pod-list';
Expand All @@ -12,12 +13,16 @@ import {
ActionMenuVariant,
ActionServiceProvider,
} from '@console/shared/src/components/actions';
import { useFlag } from '@console/shared/src/hooks/useFlag';
import { isWindowsNode } from '@console/shared/src/selectors/node';
import { nodeStatus } from '../../status/node';
import { NodeConfiguration } from './configuration/NodeConfiguration';
import { NodeHealth } from './health/NodeHealth';
import NodeDashboard from './node-dashboard/NodeDashboard';
import NodeDetails from './NodeDetails';
import NodeLogs from './NodeLogs';
import NodeTerminal from './NodeTerminal';
import { NodeWorkload } from './NodeWorkload';

const NodePodsPage: FC<PageComponentProps<NodeKind>> = ({ obj }) => (
<PodsPage
Expand All @@ -28,6 +33,8 @@ const NodePodsPage: FC<PageComponentProps<NodeKind>> = ({ obj }) => (
);

export const NodeDetailsPage: FC<ComponentProps<typeof DetailsPage>> = (props) => {
const nodeMgmtV1Enabled = useFlag(FLAG_NODE_MGMT_V1);

const pagesFor = useCallback(
(node: NodeKind) => [
{
Expand All @@ -42,13 +49,37 @@ export const NodeDetailsPage: FC<ComponentProps<typeof DetailsPage>> = (props) =
nameKey: 'console-app~Details',
component: NodeDetails,
},
navFactory.editYaml(),
navFactory.pods(NodePodsPage),
navFactory.logs(NodeLogs),
navFactory.events(ResourceEventStream),
...(nodeMgmtV1Enabled
? [
{
href: 'configuration',
// t('console-app~Configuration')
nameKey: 'console-app~Configuration',
component: NodeConfiguration,
},
{
href: 'health',
// t('console-app~Health')
nameKey: 'console-app~Health',
component: NodeHealth,
},
{
href: 'workload',
// t('console-app~Workload')
nameKey: 'console-app~Workload',
component: NodeWorkload,
},
navFactory.editYaml(),
]
: [
navFactory.editYaml(),
navFactory.pods(NodePodsPage),
navFactory.logs(NodeLogs),
navFactory.events(ResourceEventStream),
]),
...(!isWindowsNode(node) ? [navFactory.terminal(NodeTerminal)] : []),
],
[],
[nodeMgmtV1Enabled],
);

const customActionMenu = (kindObj: K8sModel, obj: NodeKind) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ import {
import { LogViewer, LogViewerSearch } from '@patternfly/react-log-viewer';
import { css } from '@patternfly/react-styles';
import { Trans, useTranslation } from 'react-i18next';
import { FLAG_NODE_MGMT_V1 } from '@console/app/src/consts';
import { useFlag } from '@console/dynamic-plugin-sdk/src/utils/flags';
import { coFetch } from '@console/internal/co-fetch';
import { useTheme } from '@console/internal/components/ThemeProvider';
import { SectionHeading } from '@console/internal/components/utils';
import { LoadingBox, LoadingInline } from '@console/internal/components/utils/status-box';
import type { NodeKind } from '@console/internal/module/k8s';
import { modelFor, resourceURL } from '@console/internal/module/k8s';
Expand Down Expand Up @@ -176,6 +179,7 @@ const HeaderBanner: FC<{ lineCount: number }> = ({ lineCount }) => {

const NodeLogs: FC<NodeLogsProps> = ({ obj: node }) => {
const { getQueryArgument, setQueryArgument, removeQueryArgument } = useQueryParamsMutator();
const nodeMgmtV1Enabled = useFlag(FLAG_NODE_MGMT_V1);

const {
kind,
Expand Down Expand Up @@ -369,6 +373,7 @@ const NodeLogs: FC<NodeLogsProps> = ({ obj: node }) => {

return (
<PaneBody fullHeight>
{nodeMgmtV1Enabled ? <SectionHeading text={t('console-app~Logs')} /> : null}
<div className="log-window-wrapper">
{(isLoadingLog || errorExists) && logControls}
{(lineCount >= MAX_LINE_COUNT || trimmedContent?.length > 0) && !isLoadingLog && (
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 { useTranslatedExtensions } from '@console/plugin-sdk/src/utils/useTranslatedExtensions';
import { useQueryParams } from '@console/shared/src/hooks/useQueryParams';
import { useQueryParamsMutator } from '@console/shared/src/hooks/useQueryParamsMutator';

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>
);
};
Loading