diff --git a/package-lock.json b/package-lock.json index a69a72d..ae7697c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@ant-design/icons": "5.6.0", "@monaco-editor/react": "4.6.0", "@originjs/vite-plugin-federation": "1.3.6", - "@prorobotech/openapi-k8s-toolkit": "^0.0.1-alpha.151", + "@prorobotech/openapi-k8s-toolkit": "^0.0.1-alpha.152", "@readme/openapi-parser": "4.0.0", "@reduxjs/toolkit": "2.2.5", "@tanstack/react-query": "5.62.2", @@ -2804,9 +2804,9 @@ } }, "node_modules/@prorobotech/openapi-k8s-toolkit": { - "version": "0.0.1-alpha.151", - "resolved": "https://registry.npmjs.org/@prorobotech/openapi-k8s-toolkit/-/openapi-k8s-toolkit-0.0.1-alpha.151.tgz", - "integrity": "sha512-AV+6muJNp75WLUPGuLi3JIH8j3P1sBgodHFNfebJ25CdfzJQeYUVkRs0yqWQoNFnw1lG92W4i7fIToYmk1WzJQ==", + "version": "0.0.1-alpha.152", + "resolved": "https://registry.npmjs.org/@prorobotech/openapi-k8s-toolkit/-/openapi-k8s-toolkit-0.0.1-alpha.152.tgz", + "integrity": "sha512-bfOD3cTkfqc5C+4FR1LNvB7I7JzXdOUf9BhoEb1QD9bGH4jel89ZAScvqRhB1HichogXO21NnZNGSmco2oYOfQ==", "license": "MIT", "dependencies": { "@monaco-editor/react": "4.6.0", diff --git a/package.json b/package.json index eb29204..e599830 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@ant-design/icons": "5.6.0", "@monaco-editor/react": "4.6.0", "@originjs/vite-plugin-federation": "1.3.6", - "@prorobotech/openapi-k8s-toolkit": "0.0.1-alpha.151", + "@prorobotech/openapi-k8s-toolkit": "0.0.1-alpha.152", "@readme/openapi-parser": "4.0.0", "@reduxjs/toolkit": "2.2.5", "@tanstack/react-query": "5.62.2", diff --git a/src/App.tsx b/src/App.tsx index 34dedbc..6711b2b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -27,7 +27,6 @@ import { FactoryPage, FactoryAdminPage, SearchPage, - EventsPage, } from 'pages' import { getBasePrefix } from 'utils/getBaseprefix' import { colorsLight, colorsDark, sizes } from 'constants/colors' @@ -125,7 +124,6 @@ export const App: FC = ({ isFederation, forcedTheme }) => { element={} /> } /> - } /> } /> diff --git a/src/components/organisms/Events/Events.tsx b/src/components/organisms/Events/Events.tsx deleted file mode 100644 index 497c80d..0000000 --- a/src/components/organisms/Events/Events.tsx +++ /dev/null @@ -1,417 +0,0 @@ -/* eslint-disable max-lines-per-function */ -// ------------------------------------------------------------ -// Simple, self-contained React component implementing: -// - WebSocket connection to your events endpoint -// - Handling of INITIAL, PAGE, ADDED, MODIFIED, DELETED, PAGE_ERROR -// - Infinite scroll via IntersectionObserver (sends { type: "SCROLL" }) -// - Lightweight CSS-in-JS styling -// - Minimal reconnection logic (bounded exponential backoff) -// - Small initials avatar (derived from a name/kind) -// ------------------------------------------------------------ - -import React, { FC, useCallback, useEffect, useReducer, useRef, useState } from 'react' -import { theme as antdtheme, Flex, Tooltip, Empty } from 'antd' -import { - // TRequestError, - TKindIndex, - TKindWithVersion, - getKinds, - getSortedKindsAll, - pluralByKind, - ResumeCircleIcon, - PauseCircleIcon, - LockedIcon, - UnlockedIcon, -} from '@prorobotech/openapi-k8s-toolkit' -import { TScrollMsg, TServerFrame } from './types' -import { eventKey, compareRV, getRV, getMaxRV } from './utils' -import { reducer } from './reducer' -import { EventRow } from './molecules' -import { Styled } from './styled' - -type TEventsProps = { - baseprefix?: string - cluster: string - wsUrl: string // e.g. ws://localhost:3000/api/events?namespace=default&limit=40 - pageSize?: number // SCROLL page size (optional) - height?: number // optional override - title?: string -} - -export const Events: FC = ({ baseprefix, cluster, wsUrl, pageSize = 50, height }) => { - const { token } = antdtheme.useToken() - - // const [error, setError] = useState() - // const [isLoading, setIsLoading] = useState(false) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [kindIndex, setKindIndex] = useState() - const [kindsWithVersion, setKindWithVersion] = useState() - - useEffect(() => { - // setIsLoading(true) - // setError(undefined) - getKinds({ clusterName: cluster }) - .then(data => { - setKindIndex(data) - setKindWithVersion(getSortedKindsAll(data)) - // setIsLoading(false) - // setError(undefined) - }) - .catch(error => { - // setIsLoading(false) - // setError(error) - // eslint-disable-next-line no-console - console.error(error) - }) - }, [cluster]) - - // pause behaviour - const [isPaused, setIsPaused] = useState(false) - const pausedRef = useRef(isPaused) - - useEffect(() => { - pausedRef.current = isPaused - }, [isPaused]) - - // ignore REMOVE signal - const [isRemoveIgnored, setIsRemoveIgnored] = useState(true) - const removeIgnoredRef = useRef(isRemoveIgnored) - - useEffect(() => { - removeIgnoredRef.current = isRemoveIgnored - }, [isRemoveIgnored]) - - // track latest resourceVersion we have processed - const latestRVRef = useRef(undefined) - - // Reducer-backed store of events - const [state, dispatch] = useReducer(reducer, { order: [], byKey: {} }) - - // Pagination/bookmarking state returned by server - const [contToken, setContToken] = useState(undefined) - const [hasMore, setHasMore] = useState(false) - - // Connection state & errors for small status UI - const [connStatus, setConnStatus] = useState<'connecting' | 'open' | 'closed'>('connecting') - const [lastError, setLastError] = useState(undefined) - - // ------------------ Refs (mutable, do not trigger render) ------------------ - const wsRef = useRef(null) // current WebSocket instance - const listRef = useRef(null) // scrollable list element - const sentinelRef = useRef(null) // bottom sentinel for IO - const wantMoreRef = useRef(false) // whether sentinel is currently visible - const fetchingRef = useRef(false) // guard: avoid parallel PAGE requests - const backoffRef = useRef(750) // ms; increases on failures up to a cap - const urlRef = useRef(wsUrl) // latest wsUrl (stable inside callbacks) - - // Guards for unmount & reconnect timer - const mountedRef = useRef(true) - const reconnectTimerRef = useRef(null) - const onMessageRef = useRef<(ev: MessageEvent) => void>(() => {}) - const startedRef = useRef(false) - const connectingRef = useRef(false) - const haveAnchorRef = useRef(false) - - // Keep urlRef in sync so connect() uses the latest wsUrl - useEffect(() => { - urlRef.current = wsUrl - }, [wsUrl]) - - // Close current WS safely - const closeWS = useCallback(() => { - try { - wsRef.current?.close() - } catch (e) { - // eslint-disable-next-line no-console - console.error(e) - } - wsRef.current = null - }, []) - - // Attempt to request the next page of older events - const sendScroll = useCallback(() => { - const token = contToken - if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return - if (!token || fetchingRef.current) return - fetchingRef.current = true - const msg: TScrollMsg = { type: 'SCROLL', continue: token, limit: pageSize } - wsRef.current.send(JSON.stringify(msg)) - }, [contToken, pageSize]) - - const maybeAutoScroll = useCallback(() => { - if (wantMoreRef.current && hasMore) sendScroll() - }, [hasMore, sendScroll]) - - // Handle all incoming frames from the server - useEffect(() => { - onMessageRef.current = (ev: MessageEvent) => { - let frame: TServerFrame | undefined - try { - frame = JSON.parse(String(ev.data)) as TServerFrame - } catch { - return - } - if (!frame) return - - if (frame.type === 'INITIAL') { - dispatch({ type: 'RESET', items: frame.items }) - setContToken(frame.continue) - setHasMore(Boolean(frame.continue)) - setLastError(undefined) - fetchingRef.current = false - - const snapshotRV = frame.resourceVersion || getMaxRV(frame.items) - if (snapshotRV) { - latestRVRef.current = snapshotRV - haveAnchorRef.current = true // NEW: we now have a safe anchor - } - return - } - - if (frame.type === 'PAGE') { - dispatch({ type: 'APPEND_PAGE', items: frame.items }) - setContToken(frame.continue) - setHasMore(Boolean(frame.continue)) - fetchingRef.current = false - - const batchRV = getMaxRV(frame.items) - if (batchRV && (!latestRVRef.current || compareRV(batchRV, latestRVRef.current) > 0)) { - latestRVRef.current = batchRV - } - maybeAutoScroll() - return - } - - if (frame.type === 'PAGE_ERROR') { - setLastError(frame.error || 'Failed to load next page') - fetchingRef.current = false - return - } - - if (frame.type === 'ADDED' || frame.type === 'MODIFIED' || frame.type === 'DELETED') { - const rv = getRV(frame.item) - if (rv && (!latestRVRef.current || compareRV(rv, latestRVRef.current) > 0)) { - latestRVRef.current = rv - } - } - - if (!pausedRef.current) { - if (frame.type === 'ADDED' || frame.type === 'MODIFIED') { - dispatch({ type: 'UPSERT', item: frame.item }) - return - } - - if (!removeIgnoredRef.current && frame.type === 'DELETED') { - dispatch({ type: 'REMOVE', key: eventKey(frame.item) }) - } - } - } - }, [maybeAutoScroll]) - - const buildWsUrl = useCallback((raw: string) => { - try { - const hasScheme = /^[a-z]+:/i.test(raw) - const base = window.location.origin - let u = hasScheme ? new URL(raw) : new URL(raw.startsWith('/') ? raw : `/${raw}`, base) - if (u.protocol === 'http:') u.protocol = 'ws:' - if (u.protocol === 'https:') u.protocol = 'wss:' - if (u.protocol !== 'ws:' && u.protocol !== 'wss:') { - u = new URL(u.pathname + u.search + u.hash, base) - u.protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' - } - if (haveAnchorRef.current && latestRVRef.current) { - u.searchParams.set('sinceRV', latestRVRef.current) - } else { - u.searchParams.delete('sinceRV') - } - return u.toString() - } catch { - const origin = window.location.origin.replace(/^http/, 'ws') - const prefix = raw.startsWith('/') ? '' : '/' - const rv = haveAnchorRef.current ? latestRVRef.current : undefined - const sep = raw.includes('?') ? '&' : '?' - return `${origin}${prefix}${raw}${rv ? `${sep}sinceRV=${encodeURIComponent(rv)}` : ''}` - } - }, []) - - // Establish and maintain the WebSocket connection with bounded backoff - const connect = useCallback(() => { - if (!mountedRef.current) return - // Prevent duplicate opens - if (connectingRef.current) return - if ( - wsRef.current && - (wsRef.current.readyState === WebSocket.OPEN || wsRef.current.readyState === WebSocket.CONNECTING) - ) { - return - } - connectingRef.current = true - - setConnStatus('connecting') - setLastError(undefined) - - const url = buildWsUrl(urlRef.current) - const ws = new WebSocket(url) - wsRef.current = ws - - ws.addEventListener('open', () => { - if (!mountedRef.current) return - backoffRef.current = 750 - fetchingRef.current = false - setConnStatus('open') - connectingRef.current = false - }) - - ws.addEventListener('message', ev => onMessageRef.current(ev)) - - const scheduleReconnect = () => { - if (wsRef.current === ws) wsRef.current = null - setConnStatus('closed') - connectingRef.current = false - // Bounded exponential backoff with jitter to avoid herding - const base = Math.min(backoffRef.current, 8000) - const jitter = Math.random() * 0.4 + 0.8 // 0.8x–1.2x - const wait = Math.floor(base * jitter) - const next = Math.min(base * 2, 12000) - backoffRef.current = next - if (reconnectTimerRef.current) { - window.clearTimeout(reconnectTimerRef.current) - reconnectTimerRef.current = null - } - reconnectTimerRef.current = window.setTimeout(() => { - if (!mountedRef.current) return - connect() - }, wait) - } - - ws.addEventListener('close', scheduleReconnect) - ws.addEventListener('error', () => { - setLastError('WebSocket error') - scheduleReconnect() - }) - }, [buildWsUrl]) - - // Kick off initial connection on mount; clean up on unmount - useEffect(() => { - if (startedRef.current) return undefined // StrictMode double-invoke guard - startedRef.current = true - - mountedRef.current = true - connect() - - return () => { - mountedRef.current = false - startedRef.current = false - if (reconnectTimerRef.current) { - window.clearTimeout(reconnectTimerRef.current) - reconnectTimerRef.current = null - } - closeWS() - wsRef.current = null - connectingRef.current = false - } - // INTENTIONALLY EMPTY DEPS – do not reopen on state changes - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - // IntersectionObserver to trigger SCROLL when sentinel becomes visible - useEffect(() => { - // Get the current DOM element referenced by sentinelRef - const el = sentinelRef.current - - // If the sentinel element is not mounted yet, exit early - if (!el) return undefined - - // Create a new IntersectionObserver to watch visibility changes of the sentinel - const io = new IntersectionObserver(entries => { - // Determine if any observed element is currently visible in the viewport - const visible = entries.some(e => e.isIntersecting) - - // Store the current visibility status in a ref (no re-render triggered) - wantMoreRef.current = visible - - // If sentinel is visible and there are more pages available, request the next page - if (visible && hasMore) sendScroll() - }) - - // Start observing the sentinel element for intersection events - io.observe(el) - - // Cleanup: disconnect the observer when component unmounts or dependencies change - return () => io.disconnect() - - // Dependencies: re-run this effect if hasMore or sendScroll changes - }, [hasMore, sendScroll]) - - // Fallback: if user scrolls near bottom manually, also try to fetch - const onScroll = useCallback(() => { - if (!listRef.current) return - const nearBottom = listRef.current.scrollTop + listRef.current.clientHeight >= listRef.current.scrollHeight - 24 - if (nearBottom && hasMore) sendScroll() - }, [hasMore, sendScroll]) - - const total = state.order.length - - const getPlural = kindsWithVersion ? pluralByKind(kindsWithVersion) : undefined - - return ( - - - - - { - if (isPaused) { - setIsPaused(false) - } else { - setIsPaused(true) - } - }} - > - {isPaused ? : } - - - {isPaused && 'Streaming paused'} - {!isPaused && connStatus === 'connecting' && 'Connecting…'} - {!isPaused && connStatus === 'open' && 'Streaming events...'} - {!isPaused && connStatus === 'closed' && 'Reconnecting…'} - - - - - {!hasMore &&
No more events ·
} - {typeof total === 'number' ?
Loaded {total} events
: ''} - {lastError && · {lastError}} - -
{isRemoveIgnored ? 'Handle REMOVE signals' : 'Ignore REMOVE signals'}
- Locked means ignore - - } - placement="left" - > - setIsRemoveIgnored(!isRemoveIgnored)}> - {isRemoveIgnored ? : } - -
-
-
- - {/* Scrollable list of event rows */} - - {state.order.length > 0 ? ( - state.order.map(k => ( - - )) - ) : ( - - )} - {/* Infinite scroll sentinel */} - - - - {state.order.length > 0 && } -
- ) -} diff --git a/src/components/organisms/Events/index.ts b/src/components/organisms/Events/index.ts deleted file mode 100644 index b4d4184..0000000 --- a/src/components/organisms/Events/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Events' diff --git a/src/components/organisms/Events/molecules/EventRow/EventRow.tsx b/src/components/organisms/Events/molecules/EventRow/EventRow.tsx deleted file mode 100644 index 824c191..0000000 --- a/src/components/organisms/Events/molecules/EventRow/EventRow.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React, { FC } from 'react' -import { useNavigate } from 'react-router-dom' -import { theme as antdtheme, Flex, Typography } from 'antd' -import { EarthIcon, getUppercase, hslFromString, Spacer } from '@prorobotech/openapi-k8s-toolkit' -import { useSelector } from 'react-redux' -import { RootState } from 'store/store' -import { TEventsV1Event } from '../../types' -import { eventText, timeAgo, getResourceLink, getNamespaceLink, formatEventSummary } from './utils' -import { Styled } from './styled' - -type TEventRowProps = { - e: TEventsV1Event - baseprefix?: string - cluster: string - getPlural?: (kind: string, apiVersion?: string) => string | undefined -} - -export const EventRow: FC = ({ e, baseprefix, cluster, getPlural }) => { - const { token } = antdtheme.useToken() - const navigate = useNavigate() - const theme = useSelector((state: RootState) => state.openapiTheme.theme) - - const abbr = e.regarding?.kind ? getUppercase(e.regarding.kind) : undefined - const bgColor = e.regarding?.kind && abbr ? hslFromString(e.regarding?.kind, theme) : 'initial' - const bgColorNamespace = hslFromString('Namespace', theme) - - const regardingKind: string | undefined = e.regarding?.kind - const regardingApiVersion: string = e.regarding?.apiVersion || 'v1' - const pluralName: string | undefined = - regardingKind && regardingApiVersion ? getPlural?.(regardingKind, regardingApiVersion) : undefined - const resourceLink: string | undefined = getResourceLink({ - baseprefix, - cluster, - namespace: e.regarding?.namespace, - apiGroupVersion: regardingApiVersion, - pluralName, - name: e.regarding?.name, - }) - const namespaceLink: string | undefined = getNamespaceLink({ - baseprefix, - cluster, - apiGroupVersion: 'v1', - pluralName: 'namespaces', - namespace: e.regarding?.namespace, - }) - - return ( - - - - - {abbr} - {resourceLink ? ( - { - e.preventDefault() - navigate(resourceLink) - }} - > - {e.regarding?.name} - - ) : ( - {e.regarding?.name} - )} - - {e.regarding?.namespace && ( - - NS - {namespaceLink ? ( - { - e.preventDefault() - navigate(namespaceLink) - }} - > - {e.regarding?.namespace} - - ) : ( - {e.regarding?.namespace} - )} - - )} - - {e.metadata?.creationTimestamp && ( - -
- -
- {timeAgo(e.metadata?.creationTimestamp)} -
- )} -
- - - -
- {e.deprecatedSource?.component && ( - - - Generated by - {e.deprecatedSource?.component} - -
- -
-
- )} -
- {e.reason || e.action || 'Event'} -
- - {formatEventSummary(e)} - -
- - {eventText(e) &&
{eventText(e)}
} -
- ) -} diff --git a/src/components/organisms/Events/molecules/EventRow/index.ts b/src/components/organisms/Events/molecules/EventRow/index.ts deleted file mode 100644 index 1f3ecc6..0000000 --- a/src/components/organisms/Events/molecules/EventRow/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { EventRow } from './EventRow' diff --git a/src/components/organisms/Events/molecules/EventRow/styled.ts b/src/components/organisms/Events/molecules/EventRow/styled.ts deleted file mode 100644 index 315b71e..0000000 --- a/src/components/organisms/Events/molecules/EventRow/styled.ts +++ /dev/null @@ -1,73 +0,0 @@ -import styled from 'styled-components' - -type TCardProps = { - $mainColor: string - $bigBorder?: boolean -} - -const Card = styled.div` - border-radius: 6px; - padding: 16px 8px; - border: ${({ $bigBorder }) => ($bigBorder ? 2 : 1)}px solid ${({ $mainColor }) => $mainColor}; - gap: 12px; - margin-bottom: 16px; - position: relative; - - &:before { - position: absolute; - content: ''; - width: 36px; - height: ${({ $bigBorder }) => ($bigBorder ? 2 : 1)}px; - background: ${({ $mainColor }) => $mainColor}; - left: -37px; - top: 50%; /* halfway down parent */ - transform: translateY(-50%); /* center vertically */ - } - - &:after { - position: absolute; - content: ''; - width: ${({ $bigBorder }) => ($bigBorder ? 7 : 6)}px; - height: ${({ $bigBorder }) => ($bigBorder ? 7 : 6)}px; - border-radius: 50%; - background: ${({ $mainColor }) => $mainColor}; - left: ${({ $bigBorder }) => ($bigBorder ? -41 : -39)}px; - top: 50%; - transform: translateY(-50%); - } -` - -type TAbbrProps = { - $bgColor: string -} - -const Abbr = styled.span` - background-color: ${({ $bgColor }) => $bgColor}; - border-radius: 13px; - padding: 1px 5px; - font-size: 13px; - height: min-content; - margin-right: 4px; -` - -const TimeStamp = styled.div` - font-weight: 400; - font-size: 12px; - line-height: 20px; -` - -const Title = styled.div` - font-weight: 700; -` - -const TimesInPeriod = styled.div` - margin-top: -16px; -` - -export const Styled = { - Card, - Abbr, - TimeStamp, - Title, - TimesInPeriod, -} diff --git a/src/components/organisms/Events/molecules/EventRow/utils.ts b/src/components/organisms/Events/molecules/EventRow/utils.ts deleted file mode 100644 index 739bff9..0000000 --- a/src/components/organisms/Events/molecules/EventRow/utils.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { - BASE_FACTORY_NAMESPACED_API_KEY, - BASE_FACTORY_CLUSTERSCOPED_API_KEY, - BASE_FACTORY_NAMESPACED_BUILTIN_KEY, - BASE_FACTORY_CLUSTERSCOPED_BUILTIN_KEY, - BASE_NAMESPACE_FACTORY_KEY, -} from 'constants/customizationApiGroupAndVersion' -import { TEventsV1Event } from '../../types' - -// Prefer modern `note`, fallback to legacy `message` -export const eventText = (e: TEventsV1Event) => e.note || e.message || '' - -// Friendly relative time formatter; returns locale string for >24h -export const timeAgo = (iso?: string) => { - if (!iso) { - return '' - } - const dt = new Date(iso).getTime() - - const diff = Date.now() - dt - - if (diff < 60_000) { - return `${Math.max(0, Math.floor(diff / 1000))}s ago` - } - if (diff < 3_600_000) { - return `${Math.floor(diff / 60_000)}m ago` - } - if (diff < 86_400_000) { - return `${Math.floor(diff / 3_600_000)}h ago` - } - - return new Date(iso).toLocaleString() -} - -export const getResourceLink = ({ - baseprefix, - cluster, - namespace, - apiGroupVersion, - pluralName, - name, -}: { - baseprefix?: string - cluster: string - namespace?: string - apiGroupVersion: string - pluralName?: string - name?: string -}): string | undefined => { - if (!pluralName || !name) { - return undefined - } - - if (apiGroupVersion === 'v1') { - return `${baseprefix}/${cluster}${namespace ? `/${namespace}` : ''}/factory/${ - namespace ? BASE_FACTORY_NAMESPACED_BUILTIN_KEY : BASE_FACTORY_CLUSTERSCOPED_BUILTIN_KEY - }/${apiGroupVersion}/${pluralName}/${name}` - } - - return `${baseprefix}/${cluster}${namespace ? `/${namespace}` : ''}/factory/${ - namespace ? BASE_FACTORY_NAMESPACED_API_KEY : BASE_FACTORY_CLUSTERSCOPED_API_KEY - }/${apiGroupVersion}/${pluralName}/${name}` -} - -export const getNamespaceLink = ({ - baseprefix, - cluster, - apiGroupVersion, - pluralName, - namespace, -}: { - baseprefix?: string - cluster: string - pluralName: string - apiGroupVersion: string - namespace?: string -}): string | undefined => { - if (!namespace) { - return undefined - } - - return `${baseprefix}/${cluster}/factory/${BASE_NAMESPACE_FACTORY_KEY}/${apiGroupVersion}/${pluralName}/${namespace}` -} - -export const formatEventSummary = (event: TEventsV1Event): string | undefined => { - if (!event.deprecatedCount || !event.deprecatedFirstTimestamp) { - return undefined - } - - const now = new Date() - const first = new Date(event.deprecatedFirstTimestamp) - const days = Math.floor((now.getTime() - first.getTime()) / (1000 * 60 * 60 * 24)) - - return `${event.deprecatedCount} times ${days === 0 ? 'today' : `in the last ${days} days`}` -} diff --git a/src/components/organisms/Events/molecules/index.ts b/src/components/organisms/Events/molecules/index.ts deleted file mode 100644 index 1f3ecc6..0000000 --- a/src/components/organisms/Events/molecules/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { EventRow } from './EventRow' diff --git a/src/components/organisms/Events/reducer.ts b/src/components/organisms/Events/reducer.ts deleted file mode 100644 index e30c701..0000000 --- a/src/components/organisms/Events/reducer.ts +++ /dev/null @@ -1,60 +0,0 @@ -// ------------------------------------------------------------ -// Reducer to maintain a keyed list of events, supporting ADDED/MODIFIED/DELETED -// ------------------------------------------------------------ -// We keep an `order` array for display order (newest first) and a `byKey` map -// for O(1) updates/reads. Pages append to the END (older items), while live -// UPSERTs (ADDED/MODIFIED) unshift to the START if new. -import { TEventsV1Event } from './types' -import { eventKey } from './utils' - -type TState = { - order: string[] // list of keys (newest first) - byKey: Record -} - -type TAction = - | { type: 'RESET'; items: TEventsV1Event[] } - | { type: 'APPEND_PAGE'; items: TEventsV1Event[] } // for older pages (append to end) - | { type: 'UPSERT'; item: TEventsV1Event } // ADDED/MODIFIED - | { type: 'REMOVE'; key: string } // DELETED - -export const reducer = (state: TState, action: TAction): TState => { - switch (action.type) { - case 'RESET': { - // Replace everything with the initial payload (usually newest N) - const order = action.items.map(eventKey) - const byKey: TState['byKey'] = {} - // eslint-disable-next-line no-return-assign - action.items.forEach(it => (byKey[eventKey(it)] = it)) - return { order, byKey } - } - case 'APPEND_PAGE': { - // Append only truly new keys to the end; update any items that already exist - const next = { ...state.byKey } - const addKeys: string[] = [] - action.items.forEach(it => { - const k = eventKey(it) - if (!next[k]) addKeys.push(k) - next[k] = it - }) - return { order: [...state.order, ...addKeys], byKey: next } - } - case 'UPSERT': { - // Insert new items at the front; replace existing in-place - const k = eventKey(action.item) - const exists = Boolean(state.byKey[k]) - const byKey = { ...state.byKey, [k]: action.item } - const order = exists ? state.order : [k, ...state.order] - return { order, byKey } - } - case 'REMOVE': { - // Remove from map and order if present - if (!state.byKey[action.key]) return state - const byKey = { ...state.byKey } - delete byKey[action.key] - return { order: state.order.filter(k => k !== action.key), byKey } - } - default: - return state - } -} diff --git a/src/components/organisms/Events/styled.ts b/src/components/organisms/Events/styled.ts deleted file mode 100644 index 1ac1e29..0000000 --- a/src/components/organisms/Events/styled.ts +++ /dev/null @@ -1,100 +0,0 @@ -import styled from 'styled-components' - -type TRootProps = { - $maxHeight: number -} - -const Root = styled.div` - display: flex; - flex-direction: column; - width: 100%; - height: 100%; - max-height: ${({ $maxHeight }) => $maxHeight}px; - border-radius: 12px; - overflow: hidden; - position: relative; -` - -const Header = styled.div` - display: flex; - align-items: center; - gap: 16px; - align-self: stretch; - margin-bottom: 16px; - padding-left: 19px; -` - -const HeaderLeftSide = styled.div` - display: flex; - align-items: center; - gap: 10px; - flex: 1 0 0; -` - -const CursorPointerDiv = styled.div` - cursor: pointer; - user-select: none; -` - -const StatusText = styled.div` - font-size: 16px; - line-height: 24px; /* 150% */ -` - -type THeaderRightSideProps = { - $colorTextDescription: string -} - -const HeaderRightSide = styled.div` - display: flex; - gap: 4px; - text-align: right; - color: ${({ $colorTextDescription }) => $colorTextDescription}; -` - -const List = styled.div` - flex: 1; - overflow-y: auto; - padding: 8px 8px 8px 72px; - z-index: 2; -` - -type TTimelineProps = { - $colorText: string - $maxHeight: number -} - -const Timeline = styled.div` - width: 100%; - height: ${({ $maxHeight }) => $maxHeight}px; - position: absolute; - top: 40px; - left: 36px; - z-index: 1; - - &:before { - content: ''; - position: absolute; - top: -2px; - width: 1px; - background: ${({ $colorText }) => $colorText}; - pointer-events: none; - height: 100%; - } -` - -const Sentinel = styled.div` - height: 1px; -` - -export const Styled = { - Root, - Header, - HeaderLeftSide, - CursorPointerDiv, - StatusText, - HeaderRightSide, - Timeline, - List, - Sentinel, -} diff --git a/src/components/organisms/Events/types.ts b/src/components/organisms/Events/types.ts deleted file mode 100644 index 15008de..0000000 --- a/src/components/organisms/Events/types.ts +++ /dev/null @@ -1,80 +0,0 @@ -// ========================= Types ============================ -// Messages are intentionally permissive (no k8s deps). Adjust to your API as needed. - -type TWatchPhase = 'ADDED' | 'MODIFIED' | 'DELETED' | 'BOOKMARK' - -// Shape of an events.k8s.io/v1 Event (subset) -// Note: Both modern `note` and legacy `message` are supported for text. -// Only the fields we render / key on are listed here. - -export type TEventsV1Event = { - metadata?: { - name?: string - namespace?: string - resourceVersion?: string - creationTimestamp?: string - } - type?: string // Normal | Warning - reason?: string - note?: string // message text in events.k8s.io/v1 - message?: string // legacy fallback - reportingController?: string - reportingInstance?: string - deprecatedCount?: number - deprecatedFirstTimestamp?: Date - action?: string - eventTime?: string - regarding?: { - apiVersion?: string - kind?: string - name?: string - namespace?: string - } - deprecatedSource?: { - component?: string - host?: string - } -} - -// ====================== Server Frames ======================= -// Incoming frames from the server. Your backend should emit one of these. -// INITIAL: first page (newest events) + a `continue` token -// PAGE: older page fetched via SCROLL -// PAGE_ERROR: pagination failed (keep live stream running) -// ADDED/MODIFIED/DELETED: watch-style deltas for live updates - -type TInitialFrame = { - type: 'INITIAL' - items: TEventsV1Event[] - continue?: string - remainingItemCount?: number - resourceVersion?: string -} - -type TPageFrame = { - type: 'PAGE' - items: TEventsV1Event[] - continue?: string - remainingItemCount?: number -} - -type TPageErrorFrame = { - type: 'PAGE_ERROR' - error: string -} - -type TDeltaFrame = { - type: TWatchPhase // ADDED | MODIFIED | DELETED - item: TEventsV1Event -} - -export type TServerFrame = TInitialFrame | TPageFrame | TPageErrorFrame | TDeltaFrame - -// Outgoing scroll request to server -// Sent when the bottom sentinel intersects view and `continue` exists. - -export type TScrollMsg = { - type: 'SCROLL' - continue: string - limit?: number -} diff --git a/src/components/organisms/Events/utils.ts b/src/components/organisms/Events/utils.ts deleted file mode 100644 index 59db057..0000000 --- a/src/components/organisms/Events/utils.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { TEventsV1Event } from './types' - -// Unique key per event for stable list rendering and updates -export const eventKey = (e: TEventsV1Event) => { - const n = e.metadata?.name ?? '' - const ns = e.metadata?.namespace ?? '' - return `${ns}/${n}` -} - -// Compare resourceVersions safely (string-based) -export const compareRV = (a: string, b: string): number => { - if (a.length !== b.length) return a.length > b.length ? 1 : -1 - // eslint-disable-next-line no-nested-ternary - return a > b ? 1 : a < b ? -1 : 0 -} - -type WithRV = { metadata?: { resourceVersion?: string } } - -export const getRV = (item: WithRV): string | undefined => item?.metadata?.resourceVersion - -// ✅ Pure functional + no restricted syntax -export const getMaxRV = (items: ReadonlyArray): string | undefined => { - const rvs = items - .map(getRV) - .filter((v): v is string => Boolean(v)) - .sort(compareRV) - return rvs.length ? rvs[rvs.length - 1] : undefined -} diff --git a/src/components/organisms/index.ts b/src/components/organisms/index.ts index 7042caa..e79f7bc 100644 --- a/src/components/organisms/index.ts +++ b/src/components/organisms/index.ts @@ -10,4 +10,3 @@ export * from './HeaderSecond' export * from './Sidebar' export * from './Footer' export * from './Search' -export * from './Events' diff --git a/src/pages/EventsPage/EventsPage.tsx b/src/pages/EventsPage/EventsPage.tsx deleted file mode 100644 index 2264b65..0000000 --- a/src/pages/EventsPage/EventsPage.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { FC } from 'react' -import { useParams } from 'react-router-dom' -import { ManageableBreadcrumbs, ManageableSidebar, Events, NavigationContainer } from 'components' -import { getBreadcrumbsIdPrefix } from 'utils/getBreadcrumbsIdPrefix' -import { getSidebarIdPrefix } from 'utils/getSidebarIdPrefix' -import { BaseTemplate } from 'templates' - -export const EventsPage: FC = () => { - const { clusterName, namespace, syntheticProject, key } = useParams() - - const possibleProject = syntheticProject && namespace ? syntheticProject : namespace - const possibleInstance = syntheticProject && namespace ? namespace : undefined - - const breadcrumbsId = `${getBreadcrumbsIdPrefix({ - instance: !!syntheticProject, - project: !!namespace, - })}factory-${key}` - - const sidebarId = `${getSidebarIdPrefix({ - instance: !!syntheticProject, - project: !!namespace, - })}factory-${key}` - - return ( - - } - // withNoCluster - > - - - - {clusterName && ( - - )} - - ) -} diff --git a/src/pages/EventsPage/index.ts b/src/pages/EventsPage/index.ts deleted file mode 100644 index 1fe46c6..0000000 --- a/src/pages/EventsPage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { EventsPage } from './EventsPage' diff --git a/src/pages/index.ts b/src/pages/index.ts index a1b7138..82d8723 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -19,5 +19,3 @@ export { FactoryPage } from './FactoryPage' export { FactoryAdminPage } from './FactoryAdminPage' /* search */ export { SearchPage } from './SearchPage' -/* events */ -export { EventsPage } from './EventsPage'