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
164 changes: 122 additions & 42 deletions web/src/components/forms/resource-status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,29 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from '../../utils/url';
import { ComponentStatus, ExporterStatus } from './pipeline';

/** `FlowCollector.status` fields used by this form (mirrors operator CRD shape). */
export type FlowCollectorStatus = {
conditions?: K8sResourceCondition[];
components?: {
agent?: ComponentStatus;
processor?: ComponentStatus;
plugin?: ComponentStatus;
};
integrations?: {
loki?: ComponentStatus;
monitoring?: ComponentStatus;
exporters?: ExporterStatus[];
};
};

function flowCollectorStatus(existing: K8sResourceKind | null): FlowCollectorStatus | undefined {
const raw = existing?.status;
if (raw == null || typeof raw !== 'object') {
return undefined;
}
return raw as FlowCollectorStatus;
}

export type ResourceStatusProps = {
group: string;
version: string;
Expand Down Expand Up @@ -46,17 +69,17 @@ const stateColor = (state: string | undefined): LabelColor => {
const StateIcon: FC<{ state: string | undefined }> = ({ state }) => {
switch (state) {
case 'Ready':
return <CheckCircleIcon color="var(--pf-v5-global--success-color--100)" />;
return <CheckCircleIcon color="var(--pf-t--global--icon--color--status--success--default)" />;
case 'Degraded':
return <ExclamationTriangleIcon color="var(--pf-v5-global--warning-color--100)" />;
return <ExclamationTriangleIcon color="var(--pf-t--global--icon--color--status--warning--default)" />;
case 'Failure':
return <ExclamationCircleIcon color="var(--pf-v5-global--danger-color--100)" />;
return <ExclamationCircleIcon color="var(--pf-t--global--icon--color--status--danger--default)" />;
case 'InProgress':
return <HourglassHalfIcon color="var(--pf-v5-global--info-color--100)" />;
return <HourglassHalfIcon color="var(--pf-t--global--icon--color--status--info--default)" />;
case 'Unused':
return <BanIcon color="var(--pf-v5-global--disabled-color--100)" />;
return <BanIcon color="var(--pf-t--global--icon--color--disabled)" />;
default:
return <UnknownIcon color="var(--pf-v5-global--disabled-color--100)" />;
return <UnknownIcon color="var(--pf-t--global--icon--color--disabled)" />;
}
};

Expand Down Expand Up @@ -135,7 +158,7 @@ const ComponentStatusTable: FC<{
))}
{unusedComponents.length > 0 && (
<Tr>
<Td colSpan={4} style={{ color: 'var(--pf-v5-global--disabled-color--100)', fontStyle: 'italic' }}>
<Td colSpan={4} style={{ color: 'var(--pf-t--global--text--color--disabled)', fontStyle: 'italic' }}>
{t('Unused: {{list}}', { list: unusedComponents.map(c => c.name).join(', ') })}
</Td>
</Tr>
Expand All @@ -146,6 +169,68 @@ const ComponentStatusTable: FC<{
);
};

/**
* `Waiting*` FlowCollector conditions use inverted polarity (operator `statuses.go`): `True` means not ready.
* Component state on the same status object is the source of truth for Failure / Degraded / InProgress.
*/
const WAITING_NO_STATUS_FIELD = new Set(['FlowCollectorController', 'StaticController', 'NetworkPolicy']);

type ConditionTone = 'error' | 'warning' | 'progress' | 'success' | 'unused' | 'unknown';

function waitingComponentState(st: FlowCollectorStatus | undefined, suffix: string): string | undefined {
if (!st) return undefined;
const { components, integrations } = st;
switch (suffix) {
case 'EBPFAgents':
return components?.agent?.state;
case 'WebConsole':
return components?.plugin?.state;
case 'FLPMonolith':
case 'FLPParent':
case 'FLPTransformer':
return components?.processor?.state;
case 'Monitoring':
return integrations?.monitoring?.state;
case 'LokiStack':
case 'DemoLoki':
return integrations?.loki?.state;
default:
return undefined;
}
}

/** One tone per row: drives icon and message color. */
function conditionTone(c: K8sResourceCondition, fcStatus: FlowCollectorStatus | undefined): ConditionTone {
const { type, status, reason } = c;

if (type === 'ConfigurationIssue') {
if (status === 'True' && reason === 'Error') return 'error';
if (status === 'True' && reason === 'Warnings') return 'warning';
return 'unknown';
}

if (type.startsWith('Waiting')) {
const suffix = type.slice('Waiting'.length);
if (status === 'False' && reason === 'Ready') return 'success';
// Operator `setUnused` sets reason `ComponentUnused`; `toCondition` default is `Unused`.
if (status === 'Unknown' && (reason === 'Unused' || reason === 'ComponentUnused')) return 'unused';
if (status !== 'True') return 'unknown';

const st = waitingComponentState(fcStatus, suffix);
if (st === 'Failure') return 'error';
if (st === 'Degraded') return 'warning';
if (st === 'InProgress') return 'progress';
if (st === 'Ready') return 'success';
return WAITING_NO_STATUS_FIELD.has(suffix) ? 'error' : 'progress';
}

if (type === 'Ready' && status === 'True' && reason === 'Ready,Degraded') return 'warning';
if (status === 'True') return 'success';
if (status === 'False' && reason === 'Pending') return 'progress';
if (status === 'False' && reason !== 'Valid') return 'error';
return 'unknown';
}

export const ResourceStatus: FC<ResourceStatusProps> = ({
group,
version,
Expand All @@ -172,30 +257,32 @@ export const ResourceStatus: FC<ResourceStatusProps> = ({
);
}

const fcStatus = flowCollectorStatus(existing);

const components: ComponentRowData[] = [];
if (existing.status?.components?.agent) {
components.push({ id: 'agent', name: t('eBPF Agent'), status: existing.status.components.agent });
if (fcStatus?.components?.agent) {
components.push({ id: 'agent', name: t('eBPF Agent'), status: fcStatus.components.agent });
}
if (existing.status?.components?.processor) {
components.push({ id: 'processor', name: t('Flowlogs Pipeline'), status: existing.status.components.processor });
if (fcStatus?.components?.processor) {
components.push({ id: 'processor', name: t('Flowlogs Pipeline'), status: fcStatus.components.processor });
}
if (existing.status?.components?.plugin) {
components.push({ id: 'plugin', name: t('Console Plugin'), status: existing.status.components.plugin });
if (fcStatus?.components?.plugin) {
components.push({ id: 'plugin', name: t('Console Plugin'), status: fcStatus.components.plugin });
}
if (existing.status?.integrations?.loki) {
components.push({ id: 'loki', name: 'Loki', status: existing.status.integrations.loki });
if (fcStatus?.integrations?.loki) {
components.push({ id: 'loki', name: 'Loki', status: fcStatus.integrations.loki });
}
if (existing.status?.integrations?.monitoring) {
components.push({ id: 'monitoring', name: t('Monitoring'), status: existing.status.integrations.monitoring });
if (fcStatus?.integrations?.monitoring) {
components.push({ id: 'monitoring', name: t('Monitoring'), status: fcStatus.integrations.monitoring });
}
const exporters: ExporterStatus[] = existing.status?.integrations?.exporters || [];
const exporters: ExporterStatus[] = fcStatus?.integrations?.exporters || [];

const sortConditions = [
(c: K8sResourceCondition) => c.type === 'Ready',
(c: K8sResourceCondition) => c.type === 'ConfigurationIssue',
(c: K8sResourceCondition) => c.type === 'KafkaReady'
];
const conditions = ((existing?.status?.conditions || []) as K8sResourceCondition[]).sort((a, b) => {
const conditions = (fcStatus?.conditions || []).sort((a, b) => {
for (const pred of sortConditions) {
if (pred(a) && pred(b)) {
return 0;
Expand Down Expand Up @@ -232,16 +319,7 @@ export const ResourceStatus: FC<ResourceStatusProps> = ({
</Thead>
<Tbody>
{conditions.map((condition, i) => {
const isWarning =
condition.type === 'ConfigurationIssue' && condition.status === 'True' && condition.reason === 'Warnings';
const isError =
(condition.type === 'ConfigurationIssue' &&
condition.status === 'True' &&
condition.reason === 'Error') ||
(condition.type !== 'ConfigurationIssue' &&
condition.status === 'False' &&
condition.reason !== 'Pending' &&
condition.reason !== 'Valid');
const tone = conditionTone(condition, fcStatus);
return (
<Tr
id={`${condition.type}-row`}
Expand All @@ -250,27 +328,29 @@ export const ResourceStatus: FC<ResourceStatusProps> = ({
key={i}
>
<Td>
{isError ? (
<ExclamationCircleIcon color="var(--pf-v5-global--danger-color--100)" />
) : isWarning ? (
<ExclamationTriangleIcon color="var(--pf-v5-global--warning-color--100)" />
) : condition.status === 'True' && condition.type !== 'ConfigurationIssue' ? (
<CheckCircleIcon color="var(--pf-v5-global--success-color--100)" />
) : condition.status === 'False' && condition.reason === 'Pending' ? (
<HourglassHalfIcon color="var(--pf-v5-global--info-color--100)" />
{tone === 'error' ? (
<ExclamationCircleIcon color="var(--pf-t--global--icon--color--status--danger--default)" />
) : tone === 'warning' ? (
<ExclamationTriangleIcon color="var(--pf-t--global--icon--color--status--warning--default)" />
) : tone === 'progress' ? (
<HourglassHalfIcon color="var(--pf-t--global--icon--color--status--info--default)" />
) : tone === 'unused' ? (
<BanIcon color="var(--pf-t--global--icon--color--disabled)" />
) : tone === 'success' ? (
<CheckCircleIcon color="var(--pf-t--global--icon--color--status--success--default)" />
) : (
<UnknownIcon color="var(--pf-v5-global--disabled-color--100)" />
<UnknownIcon color="var(--pf-t--global--icon--color--disabled)" />
)}{' '}
{condition.type}
</Td>
<Td>{condition.status}</Td>
<Td>{condition.reason}</Td>
<Td
style={
isWarning
? { color: 'var(--pf-v5-global--warning-color--200)' }
: isError
? { color: 'var(--pf-v5-global--danger-color--100)' }
tone === 'warning'
? { color: 'var(--pf-t--global--text--color--status--warning--default)' }
: tone === 'error'
? { color: 'var(--pf-t--global--text--color--status--danger--default)' }
: undefined
}
>
Expand Down
12 changes: 6 additions & 6 deletions web/src/components/status/flowcollector-status-icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,22 @@ export const FlowCollectorStatusIcon: React.FC<FlowCollectorStatusIconProps> = (
const icon = React.useMemo(() => {
switch (status) {
case 'ready':
return <ConnectedIcon color="var(--pf-v5-global--success-color--100)" />;
return <ConnectedIcon color="var(--pf-t--global--icon--color--status--success--default)" />;
case 'degraded':
return <ExclamationTriangleIcon color="var(--pf-v5-global--warning-color--100)" />;
return <ExclamationTriangleIcon color="var(--pf-t--global--icon--color--status--warning--default)" />;
case 'pending':
return <ExclamationTriangleIcon color="var(--pf-v5-global--warning-color--100)" />;
return <ExclamationTriangleIcon color="var(--pf-t--global--icon--color--status--warning--default)" />;
case 'error':
return <ExclamationCircleIcon color="var(--pf-v5-global--danger-color--100)" />;
return <ExclamationCircleIcon color="var(--pf-t--global--icon--color--status--danger--default)" />;
case 'onHold':
return <PauseCircleIcon color="var(--pf-v5-global--info-color--100)" />;
return <PauseCircleIcon color="var(--pf-t--global--icon--color--status--info--default)" />;
case 'loading':
return <Spinner size="md" />;
}
}, [status]);

return (
<Tooltip content={tooltipContent} position="bottom">
<Tooltip id="flowcollector-status-tooltip" content={tooltipContent} position="bottom">
<span style={{ display: 'inline-flex', verticalAlign: 'middle' }}>{icon}</span>
</Tooltip>
);
Expand Down
Loading