Skip to content

Commit

Permalink
Node dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
rawagner committed Apr 3, 2020
1 parent 18c7703 commit 30c9dbc
Show file tree
Hide file tree
Showing 38 changed files with 1,023 additions and 226 deletions.
Expand Up @@ -75,11 +75,10 @@ export const StatusCard: React.FC<DashboardItemProps> = ({
]) as PrometheusResponse;
const resiliencyProgressError = prometheusResults.getIn([resiliencyProgressQuery, 'loadError']);

const cephHealthState = getCephHealthState([cephStatus], [cephStatusError]);
const dataResiliencyState = getDataResiliencyState(
[resiliencyProgress],
[resiliencyProgressError],
);
const cephHealthState = getCephHealthState([{ response: cephStatus, error: cephStatusError }]);
const dataResiliencyState = getDataResiliencyState([
{ response: resiliencyProgress, error: resiliencyProgressError },
]);

return (
<DashboardCard gradient>
Expand Down
Expand Up @@ -18,24 +18,24 @@ const CephHealthStatus = [
},
];

export const getCephHealthState: PrometheusHealthHandler = (responses = [], errors = []) => {
if (errors[0]) {
export const getCephHealthState: PrometheusHealthHandler = (responses) => {
if (responses[0].error) {
return CephHealthStatus[3];
}
if (!responses[0]) {
if (!responses[0].response) {
return { state: HealthState.LOADING };
}

const value = _.get(responses[0], 'data.result[0].value[1]');
const value = _.get(responses[0].response, 'data.result[0].value[1]');
return CephHealthStatus[value] || CephHealthStatus[3];
};

export const getDataResiliencyState: PrometheusHealthHandler = (responses = [], errors = []) => {
const progress: number = getResiliencyProgress(responses[0]);
if (errors[0]) {
export const getDataResiliencyState: PrometheusHealthHandler = (responses) => {
const progress: number = getResiliencyProgress(responses[0].response);
if (responses[0].error) {
return { state: HealthState.NOT_AVAILABLE };
}
if (!responses[0]) {
if (!responses[0].response) {
return { state: HealthState.LOADING };
}
if (Number.isNaN(progress)) {
Expand Down
Expand Up @@ -3,62 +3,31 @@ import {
HealthState,
healthStateMapping,
} from '@console/shared/src/components/dashboard/status-card/states';
import { PrometheusResponse } from '@console/internal/components/graphs';
import { getControlPlaneComponentHealth } from './status';
import Status, {
StatusPopupSection,
} from '@console/shared/src/components/dashboard/status-card/StatusPopup';
import { PrometheusHealthPopupProps } from '@console/plugin-sdk';

const ResponseRate: React.FC<ResponseRateProps> = ({ response, children, error }) => {
const health = getControlPlaneComponentHealth(response, error);
let icon: React.ReactNode;
if (health.state === HealthState.LOADING) {
icon = <div className="skeleton-health" />;
} else if (health.state !== HealthState.NOT_AVAILABLE) {
icon = healthStateMapping[health.state].icon;
}
return (
<div className="co-overview-status__row">
<div>{children}</div>
<div className="co-overview-status__response-rate">
<div className="text-secondary">{health.message}</div>
{icon && <div className="co-overview-status__response-rate-icon">{icon}</div>}
</div>
</div>
);
};
const titles = ['API Servers', 'Controller Managers', 'Schedulers', 'API Request Success Rate'];

const ControlPlanePopup: React.FC<ControlPlanePopupProps> = ({ results, errors }) => (
const ControlPlanePopup: React.FC<PrometheusHealthPopupProps> = ({ responses }) => (
<>
<div className="co-overview-status__control-plane-description">
Components of the Control Plane are responsible for maintaining and reconciling the state of
the cluster.
</div>
<div className="co-overview-status__row">
<div className="co-overview-status__text--bold">Components</div>
<div className="text-secondary">Response rate</div>
</div>
<ResponseRate response={results[0]} error={errors[0]}>
API Servers
</ResponseRate>
<ResponseRate response={results[1]} error={errors[1]}>
Controller Managers
</ResponseRate>
<ResponseRate response={results[2]} error={errors[2]}>
Schedulers
</ResponseRate>
<ResponseRate response={results[3]} error={errors[3]}>
API Request Success Rate
</ResponseRate>
Components of the Control Plane are responsible for maintaining and reconciling the state of the
cluster.
<StatusPopupSection firstColumn="Components" secondColumn="Response rate">
{responses.map(({ response, error }, index) => {
const health = getControlPlaneComponentHealth(response, error);
let icon: React.ReactNode;
if (health.state === HealthState.LOADING) {
icon = <div className="skeleton-health" />;
} else if (health.state !== HealthState.NOT_AVAILABLE) {
icon = healthStateMapping[health.state].icon;
}
return <Status title={titles[index]} value={health.message} icon={icon} />;
})}
</StatusPopupSection>
</>
);

export default ControlPlanePopup;

type ControlPlanePopupProps = {
results: PrometheusResponse[];
errors: any[];
};

type ResponseRateProps = {
response: PrometheusResponse;
children: React.ReactNode;
error: any;
};
Expand Up @@ -3,21 +3,19 @@ import { referenceForModel, ClusterOperator } from '@console/internal/module/k8s
import { ClusterOperatorModel } from '@console/internal/models';
import { ResourceLink } from '@console/internal/components/utils/resource-link';
import { OperatorRowProps } from '@console/plugin-sdk';
import { OperatorStatusRow } from '@console/shared/src/components/dashboard/status-card/OperatorStatusBody';

import './operator-status.scss';
import Status from '@console/shared/src/components/dashboard/status-card/StatusPopup';

const ClusterOperatorStatusRow: React.FC<OperatorRowProps<ClusterOperator>> = ({
operatorStatus,
}) => (
<OperatorStatusRow title={operatorStatus.status.title} icon={operatorStatus.status.icon}>
<Status value={operatorStatus.status.title} icon={operatorStatus.status.icon}>
<ResourceLink
kind={referenceForModel(ClusterOperatorModel)}
name={operatorStatus.operators[0].metadata.name}
hideIcon
className="co-operator-status__title"
className="co-status-popup__title"
/>
</OperatorStatusRow>
</Status>
);

export default ClusterOperatorStatusRow;

This file was deleted.

Expand Up @@ -71,9 +71,9 @@ export const getControlPlaneComponentHealth = (

const errorStates = [HealthState.WARNING, HealthState.ERROR, HealthState.NOT_AVAILABLE];

export const getControlPlaneHealth: PrometheusHealthHandler = (responses = [], errors = []) => {
const componentsHealth = responses.map((r, index) =>
getControlPlaneComponentHealth(r, errors[index]),
export const getControlPlaneHealth: PrometheusHealthHandler = (responses) => {
const componentsHealth = responses.map(({ response, error }) =>
getControlPlaneComponentHealth(response, error),
);
if (componentsHealth.some((c) => c.state === HealthState.LOADING)) {
return { state: HealthState.LOADING };
Expand Down
Expand Up @@ -7,11 +7,21 @@ import { nodeStatus } from '../../status/node';
import NodeDetails from './NodeDetails';
import NodeTerminal from './NodeTerminal';
import { menuActions } from './menu-actions';
import NodeDashboard from './node-dashboard/NodeDashboard';

const { details, editYaml, events, pods } = navFactory;
const { editYaml, events, pods } = navFactory;

const pages = [
details(NodeDetails),
{
href: '',
name: 'Overview',
component: NodeDashboard,
},
{
href: 'details',
name: 'Details',
component: NodeDetails,
},
editYaml(),
pods(({ obj }) => (
<PodsPage showTitle={false} fieldSelector={`spec.nodeName=${obj.metadata.name}`} />
Expand Down
18 changes: 5 additions & 13 deletions frontend/packages/console-app/src/components/nodes/NodeStatus.tsx
Expand Up @@ -4,8 +4,8 @@ import ConsumerPopover from '@console/shared/src/components/dashboard/utilizatio
import { Status, SecondaryStatus, getNodeSecondaryStatus } from '@console/shared';
import { NodeKind } from '@console/internal/module/k8s';
import { humanizeBinaryBytes, humanizeNumber } from '@console/internal/components/utils';
import { nodeStatus } from '../../status/node';
import { PressureQueries, Condition } from '../../queries';
import { nodeStatus, Condition, getDegradedStates } from '../../status/node';
import { PressureQueries } from '../../queries';

const conditionDescriptionMap = Object.freeze({
[Condition.DISK_PRESSURE]: 'available memory is low',
Expand All @@ -19,20 +19,11 @@ const humanizeMap = Object.freeze({
[Condition.PID_PRESSURE]: humanizeNumber,
});

const isMonitoredCondition = (condition: Condition): boolean =>
[Condition.DISK_PRESSURE, Condition.MEM_PRESSURE, Condition.PID_PRESSURE].includes(condition);

const getDegradedStates = (node: NodeKind): Condition[] => {
return node.status.conditions
.filter(({ status, type }) => status === 'True' && isMonitoredCondition(type as Condition))
.map(({ type }) => type as Condition);
};

const NodeStatus: React.FC<NodeStatusProps> = ({ node, showPopovers = false }) => {
const NodeStatus: React.FC<NodeStatusProps> = ({ node, showPopovers = false, className }) => {
const status = showPopovers ? getDegradedStates(node) : [];
return (
<>
<Status status={nodeStatus(node)} />
<Status status={nodeStatus(node)} className={className} />
<SecondaryStatus status={getNodeSecondaryStatus(node)} />
{status.length > 0 &&
status.map((item) => (
Expand All @@ -53,6 +44,7 @@ const NodeStatus: React.FC<NodeStatusProps> = ({ node, showPopovers = false }) =
type NodeStatusProps = {
node: NodeKind;
showPopovers?: boolean;
className?: string;
};

export default NodeStatus;
@@ -0,0 +1,61 @@
import * as React from 'react';
import DashboardCard from '@console/shared/src/components/dashboard/dashboard-card/DashboardCard';
import DashboardCardHeader from '@console/shared/src/components/dashboard/dashboard-card/DashboardCardHeader';
import DashboardCardTitle from '@console/shared/src/components/dashboard/dashboard-card/DashboardCardTitle';
import DashboardCardLink from '@console/shared/src/components/dashboard/dashboard-card/DashboardCardLink';
import DashboardCardBody from '@console/shared/src/components/dashboard/dashboard-card/DashboardCardBody';
import ActivityBody, {
RecentEventsBody,
OngoingActivityBody,
} from '@console/shared/src/components/dashboard/activity-card/ActivityBody';
import { EventModel, NodeModel } from '@console/internal/models';
import { EventKind, NodeKind } from '@console/internal/module/k8s';
import { resourcePathFromModel } from '@console/internal/components/utils';
import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook';

import { NodeDashboardContext } from './NodeDashboardContext';

const eventsResource = {
isList: true,
kind: EventModel.kind,
};

const nodeEventsFilter = (event: EventKind, uid: string, kind: string, name: string): boolean => {
const { uid: objectUID, kind: objectKind, name: objectName } = event?.involvedObject || {};
return objectUID === uid && objectKind === kind && objectName === name;
};

const RecentEvent: React.FC<RecentEventProps> = ({ node }) => {
const [data, loaded, loadError] = useK8sWatchResource<EventKind[]>(eventsResource);
const { uid, name } = node.metadata;
const eventsFilter = React.useCallback(
(event) => nodeEventsFilter(event, uid, NodeModel.kind, name),
[uid, name],
);
return <RecentEventsBody events={{ data, loaded, loadError }} filter={eventsFilter} />;
};

const ActivityCard: React.FC = () => {
const { obj } = React.useContext(NodeDashboardContext);
const eventsLink = `${resourcePathFromModel(NodeModel, obj.metadata.name)}/events`;
return (
<DashboardCard gradient data-test-id="activity-card">
<DashboardCardHeader>
<DashboardCardTitle>Activity</DashboardCardTitle>
<DashboardCardLink to={eventsLink}>View events</DashboardCardLink>
</DashboardCardHeader>
<DashboardCardBody>
<ActivityBody className="co-project-dashboard__activity-body">
<OngoingActivityBody loaded />
<RecentEvent node={obj} />
</ActivityBody>
</DashboardCardBody>
</DashboardCard>
);
};

type RecentEventProps = {
node: NodeKind;
};

export default ActivityCard;
@@ -0,0 +1,51 @@
import * as React from 'react';
import DashboardCard from '@console/shared/src/components/dashboard/dashboard-card/DashboardCard';
import DashboardCardHeader from '@console/shared/src/components/dashboard/dashboard-card/DashboardCardHeader';
import DashboardCardTitle from '@console/shared/src/components/dashboard/dashboard-card/DashboardCardTitle';
import DashboardCardBody from '@console/shared/src/components/dashboard/dashboard-card/DashboardCardBody';
import DetailsBody from '@console/shared/src/components/dashboard/details-card/DetailsBody';
import DetailItem from '@console/shared/src/components/dashboard/details-card/DetailItem';
import DashboardCardLink from '@console/shared/src/components/dashboard/dashboard-card/DashboardCardLink';
import { getNodeAddresses } from '@console/shared/src/selectors/node';
import { resourcePathFromModel } from '@console/internal/components/utils';
import { NodeModel } from '@console/internal/models';

import NodeIPList from '../NodeIPList';
import NodeRoles from '../NodeRoles';
import { NodeDashboardContext } from './NodeDashboardContext';

const DetailsCard: React.FC = () => {
const { obj } = React.useContext(NodeDashboardContext);
const detailsLink = `${resourcePathFromModel(NodeModel, obj.metadata.name)}/details`;
const instanceType = obj.metadata.labels?.['beta.kubernetes.io/instance-type'];
const zone = obj.metadata.labels?.['topology.kubernetes.io/region'];
return (
<DashboardCard data-test-id="details-card">
<DashboardCardHeader>
<DashboardCardTitle>Details</DashboardCardTitle>
<DashboardCardLink to={detailsLink}>View all</DashboardCardLink>
</DashboardCardHeader>
<DashboardCardBody>
<DetailsBody>
<DetailItem isLoading={!obj} title="Node Name">
{obj.metadata.name}
</DetailItem>
<DetailItem isLoading={!obj} title="Role">
<NodeRoles node={obj} />
</DetailItem>
<DetailItem isLoading={!obj} title="Instance Type" error={!instanceType}>
{instanceType}
</DetailItem>
<DetailItem isLoading={!obj} title="Zone" error={!zone}>
{zone}
</DetailItem>
<DetailItem isLoading={!obj} title="Node Addresses">
<NodeIPList ips={getNodeAddresses(obj)} expand />
</DetailItem>
</DetailsBody>
</DashboardCardBody>
</DashboardCard>
);
};

export default DetailsCard;

0 comments on commit 30c9dbc

Please sign in to comment.