UNPKG

@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
"use strict"; 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