events: times in period; links; empty view

This commit is contained in:
typescreep
2025-10-31 16:12:03 +03:00
parent a416ee43f7
commit dfbc8b55ec
12 changed files with 262 additions and 33 deletions

6
.env
View File

@@ -33,3 +33,9 @@ VITE_REMOVE_BACKLINK_TEXT=true
VITE_DOCS_URL=https://in-cloud.io/docs/tech-docs/introduction/
VITE_SEARCH_TABLE_CUSTOMIZATION_PREFIX=stock-
VITE_BASE_FACTORY_NAMESPACED_API_KEY=base-factory-namespaced-api
VITE_BASE_FACTORY_CLUSTERSCOPED_API_KEY=base-factory-clusterscoped-api
VITE_BASE_FACTORY_NAMESPACED_BUILTIN_KEY=base-factory-namespaced-builtin
VITE_BASE_FACTORY_CLUSTERSCOPED_BUILTIN_KEY=base-factory-clusterscoped-builtin
VITE_BASE_NAMESPACE_FACTORY_KEY=base-factory-clusterscoped-builtin

View File

@@ -35,3 +35,9 @@ REMOVE_BACKLINK_TEXT=
DOCS_URL=
SEARCH_TABLE_CUSTOMIZATION_PREFIX=
BASE_FACTORY_NAMESPACED_API_KEY=
BASE_FACTORY_CLUSTERSCOPED_API_KEY=
BASE_FACTORY_NAMESPACED_BUILTIN_KEY=
BASE_FACTORY_CLUSTERSCOPED_BUILTIN_KEY=
BASE_NAMESPACE_FACTORY_KEY=

8
package-lock.json generated
View File

@@ -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.150",
"@prorobotech/openapi-k8s-toolkit": "^0.0.1-alpha.151",
"@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.150",
"resolved": "https://registry.npmjs.org/@prorobotech/openapi-k8s-toolkit/-/openapi-k8s-toolkit-0.0.1-alpha.150.tgz",
"integrity": "sha512-WkrZDN4XNHA5p/Vtcj4vE+yNHu1vyG6ezX2QCrE77TxmCTRfTSJM69Pvh6AllqWwoL7kDeUKG66Zbh+TiDm+vg==",
"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==",
"license": "MIT",
"dependencies": {
"@monaco-editor/react": "4.6.0",

View File

@@ -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.150",
"@prorobotech/openapi-k8s-toolkit": "0.0.1-alpha.151",
"@readme/openapi-parser": "4.0.0",
"@reduxjs/toolkit": "2.2.5",
"@tanstack/react-query": "5.62.2",

View File

@@ -73,6 +73,23 @@ const SEARCH_TABLE_CUSTOMIZATION_PREFIX =
? options?.SEARCH_TABLE_CUSTOMIZATION_PREFIX
: process.env.SEARCH_TABLE_CUSTOMIZATION_PREFIX
const BASE_FACTORY_NAMESPACED_API_KEY =
process.env.LOCAL === 'true' ? options?.BASE_FACTORY_NAMESPACED_API_KEY : process.env.BASE_FACTORY_NAMESPACED_API_KEY
const BASE_FACTORY_CLUSTERSCOPED_API_KEY =
process.env.LOCAL === 'true'
? options?.BASE_FACTORY_CLUSTERSCOPED_API_KEY
: process.env.BASE_FACTORY_CLUSTERSCOPED_API_KEY
const BASE_FACTORY_NAMESPACED_BUILTIN_KEY =
process.env.LOCAL === 'true'
? options?.BASE_FACTORY_NAMESPACED_BUILTIN_KEY
: process.env.BASE_FACTORY_NAMESPACED_BUILTIN_KEY
const BASE_FACTORY_CLUSTERSCOPED_BUILTIN_KEY =
process.env.LOCAL === 'true'
? options?.BASE_FACTORY_CLUSTERSCOPED_BUILTIN_KEY
: process.env.BASE_FACTORY_CLUSTERSCOPED_BUILTIN_KEY
const BASE_NAMESPACE_FACTORY_KEY =
process.env.LOCAL === 'true' ? options?.BASE_NAMESPACE_FACTORY_KEY : process.env.BASE_NAMESPACE_FACTORY_KEY
const healthcheck = require('express-healthcheck')
const promBundle = require('express-prom-bundle')
@@ -199,7 +216,14 @@ app.get(`${basePrefix ? basePrefix : ''}/env.js`, (_, res) => {
DOCS_URL: ${JSON.stringify(DOCS_URL) || '"/docs"'},
SEARCH_TABLE_CUSTOMIZATION_PREFIX: ${JSON.stringify(SEARCH_TABLE_CUSTOMIZATION_PREFIX) || '"search-"'},
REMOVE_BACKLINK: ${!!REMOVE_BACKLINK ? JSON.stringify(REMOVE_BACKLINK).toLowerCase() : '"false"'},
REMOVE_BACKLINK_TEXT: ${!!REMOVE_BACKLINK_TEXT ? JSON.stringify(REMOVE_BACKLINK_TEXT).toLowerCase() : '"false"'}
REMOVE_BACKLINK_TEXT: ${!!REMOVE_BACKLINK_TEXT ? JSON.stringify(REMOVE_BACKLINK_TEXT).toLowerCase() : '"false"'},
BASE_FACTORY_NAMESPACED_API_KEY: ${JSON.stringify(BASE_FACTORY_NAMESPACED_API_KEY) || '"check envs"'},
BASE_FACTORY_CLUSTERSCOPED_API_KEY: ${JSON.stringify(BASE_FACTORY_CLUSTERSCOPED_API_KEY) || '"check envs"'},
BASE_FACTORY_NAMESPACED_BUILTIN_KEY: ${JSON.stringify(BASE_FACTORY_NAMESPACED_BUILTIN_KEY) || '"check envs"'},
BASE_FACTORY_CLUSTERSCOPED_BUILTIN_KEY: ${
JSON.stringify(BASE_FACTORY_CLUSTERSCOPED_BUILTIN_KEY) || '"check envs"'
},
BASE_NAMESPACE_FACTORY_KEY: ${JSON.stringify(BASE_NAMESPACE_FACTORY_KEY) || '"check envs"'}
}
`,
)

View File

@@ -10,8 +10,19 @@
// ------------------------------------------------------------
import React, { FC, useCallback, useEffect, useReducer, useRef, useState } from 'react'
import { theme as antdtheme, Flex, Tooltip } from 'antd'
import { ResumeCircleIcon, PauseCircleIcon, LockedIcon, UnlockedIcon } from '@prorobotech/openapi-k8s-toolkit'
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'
@@ -19,15 +30,41 @@ 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<TEventsProps> = ({ wsUrl, pageSize = 50, height }) => {
export const Events: FC<TEventsProps> = ({ baseprefix, cluster, wsUrl, pageSize = 50, height }) => {
const { token } = antdtheme.useToken()
// const [error, setError] = useState<TRequestError | undefined>()
// const [isLoading, setIsLoading] = useState<boolean>(false)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [kindIndex, setKindIndex] = useState<TKindIndex>()
const [kindsWithVersion, setKindWithVersion] = useState<TKindWithVersion[]>()
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)
@@ -315,6 +352,8 @@ export const Events: FC<TEventsProps> = ({ wsUrl, pageSize = 50, height }) => {
const total = state.order.length
const getPlural = kindsWithVersion ? pluralByKind(kindsWithVersion) : undefined
return (
<Styled.Root $maxHeight={height || 640}>
<Styled.Header>
@@ -361,14 +400,18 @@ export const Events: FC<TEventsProps> = ({ wsUrl, pageSize = 50, height }) => {
{/* Scrollable list of event rows */}
<Styled.List ref={listRef} onScroll={onScroll}>
{state.order.map(k => (
<EventRow key={k} e={state.byKey[k]} />
))}
{state.order.length > 0 ? (
state.order.map(k => (
<EventRow key={k} e={state.byKey[k]} baseprefix={baseprefix} cluster={cluster} getPlural={getPlural} />
))
) : (
<Empty />
)}
{/* Infinite scroll sentinel */}
<Styled.Sentinel ref={sentinelRef} />
</Styled.List>
<Styled.Timeline $colorText={token.colorText} $maxHeight={height || 640} />
{state.order.length > 0 && <Styled.Timeline $colorText={token.colorText} $maxHeight={height || 640} />}
</Styled.Root>
)
}

View File

@@ -1,24 +1,49 @@
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 } from './utils'
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<TEventRowProps> = ({ e }) => {
export const EventRow: FC<TEventRowProps> = ({ 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 (
<Styled.Card
$bigBorder={e.type === 'Warning'}
@@ -28,12 +53,34 @@ export const EventRow: FC<TEventRowProps> = ({ e }) => {
<Flex align="center" gap={16}>
<Flex align="center" gap={8}>
<Styled.Abbr $bgColor={bgColor}>{abbr}</Styled.Abbr>
{e.regarding?.name}
{resourceLink ? (
<Typography.Link
onClick={e => {
e.preventDefault()
navigate(resourceLink)
}}
>
{e.regarding?.name}
</Typography.Link>
) : (
<Typography.Text>{e.regarding?.name}</Typography.Text>
)}
</Flex>
{e.metadata?.namespace && (
{e.regarding?.namespace && (
<Flex align="center" gap={8}>
<Styled.Abbr $bgColor={bgColorNamespace}>NS</Styled.Abbr>
{e.metadata?.namespace}
{namespaceLink ? (
<Typography.Link
onClick={e => {
e.preventDefault()
navigate(namespaceLink)
}}
>
{e.regarding?.namespace}
</Typography.Link>
) : (
<Typography.Text>{e.regarding?.namespace}</Typography.Text>
)}
</Flex>
)}
</Flex>
@@ -47,21 +94,26 @@ export const EventRow: FC<TEventRowProps> = ({ e }) => {
)}
</Flex>
<Spacer $space={16} $samespace />
<Flex gap={8} align="center" wrap>
<div>
{e.deprecatedSource?.component && (
<Flex gap={8} align="center" wrap>
<Flex gap={6} align="center" wrap>
<Typography.Text type="secondary">Generated by</Typography.Text>
<Styled.Title>{e.deprecatedSource?.component}</Styled.Title>
<Flex justify="space-between">
<Flex gap={8} align="center" wrap>
<div>
{e.deprecatedSource?.component && (
<Flex gap={8} align="center" wrap>
<Flex gap={6} align="center" wrap>
<Typography.Text type="secondary">Generated by</Typography.Text>
<Styled.Title>{e.deprecatedSource?.component}</Styled.Title>
</Flex>
<div>
<Typography.Text type="secondary"></Typography.Text>
</div>
</Flex>
<div>
<Typography.Text type="secondary"></Typography.Text>
</div>
</Flex>
)}
</div>
<Styled.Title>{e.reason || e.action || 'Event'}</Styled.Title>
)}
</div>
<Styled.Title>{e.reason || e.action || 'Event'}</Styled.Title>
</Flex>
<Styled.TimesInPeriod>
<Typography.Text type="secondary">{formatEventSummary(e)}</Typography.Text>
</Styled.TimesInPeriod>
</Flex>
<Spacer $space={16} $samespace />
{eventText(e) && <div>{eventText(e)}</div>}

View File

@@ -60,9 +60,14 @@ const Title = styled.div`
font-weight: 700;
`
const TimesInPeriod = styled.div`
margin-top: -16px;
`
export const Styled = {
Card,
Abbr,
TimeStamp,
Title,
TimesInPeriod,
}

View File

@@ -1,3 +1,10 @@
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`
@@ -24,3 +31,65 @@ export const timeAgo = (iso?: string) => {
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`}`
}

View File

@@ -21,9 +21,11 @@ export type TEventsV1Event = {
reportingController?: string
reportingInstance?: string
deprecatedCount?: number
deprecatedFirstTimestamp?: Date
action?: string
eventTime?: string
regarding?: {
apiVersion?: string
kind?: string
name?: string
namespace?: string

View File

@@ -77,3 +77,19 @@ export const BASE_REMOVE_BACKLINK_TEXT = import.meta.env.DEV
? window._env_.REMOVE_BACKLINK_TEXT === 'true' ||
import.meta.env.VITE_REMOVE_BACKLINK_TEXT?.toString().toLowerCase() === 'true'
: window._env_.REMOVE_BACKLINK_TEXT === 'true'
export const BASE_FACTORY_NAMESPACED_API_KEY = import.meta.env.DEV
? window._env_.BASE_FACTORY_NAMESPACED_API_KEY || import.meta.env.VITE_BASE_FACTORY_NAMESPACED_API_KEY
: window._env_.BASE_FACTORY_NAMESPACED_API_KEY
export const BASE_FACTORY_CLUSTERSCOPED_API_KEY = import.meta.env.DEV
? window._env_.BASE_FACTORY_CLUSTERSCOPED_API_KEY || import.meta.env.VITE_BASE_FACTORY_CLUSTERSCOPED_API_KEY
: window._env_.BASE_FACTORY_CLUSTERSCOPED_API_KEY
export const BASE_FACTORY_NAMESPACED_BUILTIN_KEY = import.meta.env.DEV
? window._env_.BASE_FACTORY_NAMESPACED_BUILTIN_KEY || import.meta.env.VITE_BASE_FACTORY_NAMESPACED_BUILTIN_KEY
: window._env_.BASE_FACTORY_NAMESPACED_BUILTIN_KEY
export const BASE_FACTORY_CLUSTERSCOPED_BUILTIN_KEY = import.meta.env.DEV
? window._env_.BASE_FACTORY_CLUSTERSCOPED_BUILTIN_KEY || import.meta.env.VITE_BASE_FACTORY_CLUSTERSCOPED_BUILTIN_KEY
: window._env_.BASE_FACTORY_CLUSTERSCOPED_BUILTIN_KEY
export const BASE_NAMESPACE_FACTORY_KEY = import.meta.env.DEV
? window._env_.BASE_NAMESPACE_FACTORY_KEY || import.meta.env.VITE_BASE_NAMESPACE_FACTORY_KEY
: window._env_.BASE_NAMESPACE_FACTORY_KEY

View File

@@ -36,7 +36,13 @@ export const EventsPage: FC = () => {
<NavigationContainer>
<ManageableBreadcrumbs idToCompare={breadcrumbsId} />
</NavigationContainer>
<Events wsUrl={`/api/clusters/${clusterName}/openapi-bff-ws/events/eventsWs?limit=40`} />
{clusterName && (
<Events
baseprefix="/openapi-ui"
cluster={clusterName}
wsUrl={`/api/clusters/${clusterName}/openapi-bff-ws/events/eventsWs?limit=40`}
/>
)}
</BaseTemplate>
)
}