@stolostron/multicluster-sdk
Version:
Provides extensions and APIs that dynamic plugins can use to leverage multicluster capabilities provided by Red Hat Advanced Cluster Management.
260 lines • 13.7 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FleetResourceEventStream = void 0;
const jsx_runtime_1 = require("react/jsx-runtime");
/* Copyright Contributors to the Open Cluster Management project */
const react_1 = require("react");
// References translations directly from OpenShift console - not from plugins
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
const react_i18next_1 = require("react-i18next");
const dynamic_plugin_sdk_1 = require("@openshift-console/dynamic-plugin-sdk");
const _ = __importStar(require("lodash"));
const api_1 = require("../api");
const apiRequests_1 = require("../internal/apiRequests");
const utils_1 = require("../internal/FleetResourceEventStream/utils");
const constants_1 = require("../internal/FleetResourceEventStream/constants");
const react_core_1 = require("@patternfly/react-core");
const react_styles_1 = require("@patternfly/react-styles");
const TogglePlay_1 = __importDefault(require("../internal/FleetResourceEventStream/TogglePlay"));
const EventStreamList_1 = require("../internal/FleetResourceEventStream/EventStreamList");
const EventComponent_1 = __importDefault(require("../internal/FleetResourceEventStream/EventComponent"));
/**
* A multicluster-aware ResourceEventStream component that displays real-time Kubernetes events
* for resources on managed clusters. Provides equivalent functionality to the OpenShift console's
* ResourceEventStream for resources on managed clusters.
*
* For managed cluster resources, this component establishes a websocket connection to stream
* events from the specified cluster. For hub cluster resources or when no cluster is specified,
* it falls back to the standard OpenShift console ResourceEventStream component.
*
* @see {@link https://github.com/openshift/console/blob/main/frontend/packages/console-dynamic-plugin-sdk/docs/api.md#resourceeventstream} OpenShift Console Dynamic Plugin SDK ResourceEventStream
*
* @component
*
* @param {Object} props - Component properties
* @param {FleetK8sResourceCommon} props.resource - The Kubernetes resource to show events for.
* Must include standard K8s metadata (name, namespace, uid, kind) and an optional cluster property.
*
* @example
* // Display events for a resource on a managed cluster
* <FleetResourceEventStream
* resource={{
* metadata: { name: 'my-pod', namespace: 'default', uid: '123' },
* kind: 'Pod',
* cluster: 'managed-cluster-1'
* }}
* />
*
* @example
* // Display events for a hub cluster resource (falls back to OpenShift console component)
* <FleetResourceEventStream
* resource={{
* metadata: { name: 'my-deployment', namespace: 'openshift-gitops', uid: '456' },
* kind: 'Deployment'
* // No cluster property - uses hub cluster
* }}
* />
*
* @example
* // Display events for a cluster-scoped resource on a managed cluster
* <FleetResourceEventStream
* resource={{
* metadata: { name: 'my-node', uid: '789' },
* kind: 'Node',
* cluster: 'edge-cluster-2'
* }}
* />
*
* @returns {JSX.Element} A rendered event stream component showing real-time Kubernetes events
*
* @remarks
* **Behavior:**
* - When `resource.cluster` is set and differs from hub cluster: Uses fleet websocket connection
* - When `resource.cluster` is undefined or equals hub cluster: Falls back to OpenShift console ResourceEventStream
* - Automatically handles connection lifecycle (open, close, error, reconnect)
* - Supports both namespaced and cluster-scoped resources
* - Displays up to 50 most recent events with real-time streaming
* - Provides play/pause controls for event streaming
*
* **Event Filtering:**
* Events are filtered by `involvedObject.uid`, `involvedObject.name`, and `involvedObject.kind`
* to show only events related to the specified resource.
*
* **Error Handling:**
* - Shows loading spinner during initial connection
* - Displays error states for connection failures
* - Shows empty state when no events exist
* - Automatically attempts reconnection on websocket errors
*
* @see {@link FleetK8sResourceCommon} for resource type definition
* @see {@link https://github.com/openshift/console/tree/master/frontend/packages/console-dynamic-plugin-sdk} OpenShift Console Dynamic Plugin SDK
*/
const FleetResourceEventStream = ({ resource }) => {
const [active, setActive] = (0, react_1.useState)(true);
const [hubCluster] = (0, api_1.useHubClusterName)();
const { t } = (0, react_i18next_1.useTranslation)('public');
const [sortedEvents, setSortedEvents] = (0, react_1.useState)([]);
const [error, setError] = (0, react_1.useState)(false);
const [loading, setLoading] = (0, react_1.useState)(true);
const ws = (0, react_1.useRef)();
const [backendAPIPath, loaded] = (0, api_1.useFleetK8sAPIPath)(resource?.cluster);
const fieldSelector = `involvedObject.uid=${resource?.metadata?.uid},involvedObject.name=${resource?.metadata?.name},involvedObject.kind=${resource?.kind}`;
const namespace = resource?.metadata?.namespace;
// handle websocket setup and teardown when dependent props change
(0, react_1.useEffect)(() => {
if (!resource.cluster || resource.cluster === hubCluster || !loaded)
return;
const watchURLOptions = {
cluster: resource.cluster,
...(namespace ? { ns: namespace } : {}),
...(fieldSelector
? {
fieldSelector,
}
: {}),
};
if (!ws.current) {
// create new WebSocket connection
ws.current = (0, apiRequests_1.fleetWatch)(utils_1.EventModel, watchURLOptions, backendAPIPath);
if (ws.current === undefined)
return;
ws.current.onmessage = (message) => {
if (!active)
return;
try {
const eventdataParsed = JSON.parse(message.data);
if (!eventdataParsed)
return;
const eventType = eventdataParsed.type;
const object = eventdataParsed.object;
setSortedEvents((currentSortedEvents) => {
const topEvents = currentSortedEvents.slice(0, constants_1.MAX_MESSAGES);
const uid = object?.metadata?.uid || '';
const eventAlreadyExists = topEvents.find((e) => e?.metadata?.uid === uid);
switch (eventType) {
case 'ADDED':
case 'MODIFIED':
if (eventAlreadyExists &&
eventAlreadyExists?.count !== undefined &&
object?.count !== undefined &&
eventAlreadyExists.count > object.count) {
// We already have a more recent version of this message stored, so skip this one
return topEvents;
}
return (0, utils_1.sortEvents)([...topEvents, object]);
case 'DELETED':
return topEvents.filter((e) => e?.metadata?.uid !== uid);
default:
console.error(`UNHANDLED EVENT: ${eventType}`);
return topEvents;
}
});
}
catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
ws.current.onopen = () => {
setActive(true);
setError(false);
setLoading(false);
};
ws.current.onclose = (evt) => {
ws.current = undefined;
setActive(false);
if (evt?.wasClean === false) {
setError(evt.reason || t('public~Connection did not close cleanly.'));
}
};
ws.current.onerror = () => {
setActive(false);
setError(true);
};
}
}, [namespace, fieldSelector, active, t, resource.cluster, hubCluster, loaded, backendAPIPath]);
// return early after all hooks are called, otherwise the component will render twice
if (!resource.cluster || resource.cluster === hubCluster)
return (0, jsx_runtime_1.jsx)(dynamic_plugin_sdk_1.ResourceEventStream, { resource: resource });
const count = sortedEvents.length;
const noEvents = count === 0;
const noMatches = count > 0 && count === 0;
let sysEventStatus, statusBtnTxt;
if (noEvents || (noMatches && resource)) {
sysEventStatus = (0, jsx_runtime_1.jsx)(react_core_1.EmptyState, { title: t('public~No events') });
}
if (noMatches && !resource) {
sysEventStatus = ((0, jsx_runtime_1.jsx)(react_core_1.EmptyState, { title: t('public~No matching events'), children: count >= constants_1.MAX_MESSAGES
? t('public~{{count}}+ event exist, but none match the current filter', {
count: constants_1.MAX_MESSAGES,
})
: t('public~{{count}} event exist, but none match the current filter', {
count,
}) }));
}
if (error) {
statusBtnTxt = ((0, jsx_runtime_1.jsx)("span", { className: "co-sysevent-stream__connection-error", children: _.isString(error)
? t('public~Error connecting to event stream: {{ error }}', {
error,
})
: t('public~Error connecting to event stream') }));
sysEventStatus = ((0, jsx_runtime_1.jsx)(react_core_1.EmptyState, { title: t('public~Error loading events'), id: "empty-state", children: t('public~An error occurred during event retrieval. Attempting to reconnect...') }));
}
else if (loading) {
statusBtnTxt = (0, jsx_runtime_1.jsx)("span", { children: t('public~Loading events...') });
sysEventStatus = (0, jsx_runtime_1.jsx)(react_core_1.Spinner, { id: "spinner" });
}
else if (active) {
statusBtnTxt = (0, jsx_runtime_1.jsx)("span", { children: t('public~Streaming events...') });
}
else {
statusBtnTxt = (0, jsx_runtime_1.jsx)("span", { children: t('public~Event stream is paused.') });
}
const klass = (0, react_styles_1.css)('co-sysevent-stream__timeline', {
'co-sysevent-stream__timeline--empty': !count,
});
const messageCount = count < constants_1.MAX_MESSAGES
? t('public~Showing {{count}} event', { count })
: t('public~Showing most recent {{count}} event', { count });
const toggleStream = () => {
setActive((prev) => !prev);
};
return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsx)(react_core_1.PageSection, { children: (0, jsx_runtime_1.jsxs)("div", { className: "co-sysevent-stream", children: [(0, jsx_runtime_1.jsxs)("div", { className: "co-sysevent-stream__status", children: [(0, jsx_runtime_1.jsx)("div", { className: "co-sysevent-stream__timeline__btn-text", children: statusBtnTxt }), (0, jsx_runtime_1.jsx)("div", { className: "co-sysevent-stream__totals pf-v6-u-text-color-subtle", "data-test": "event-totals", children: messageCount })] }), (0, jsx_runtime_1.jsxs)("div", { className: klass, children: [(0, jsx_runtime_1.jsx)(TogglePlay_1.default, { active: active, onClick: toggleStream, className: "co-sysevent-stream__timeline__btn" }), (0, jsx_runtime_1.jsx)("div", { className: "co-sysevent-stream__timeline__end-message", children: t('public~Older events are not stored.') })] }), count > 0 && (0, jsx_runtime_1.jsx)(EventStreamList_1.EventStreamList, { events: sortedEvents, EventComponent: EventComponent_1.default }), sysEventStatus] }) }) }));
};
exports.FleetResourceEventStream = FleetResourceEventStream;
//# sourceMappingURL=FleetResourceEventStream.js.map