diff --git a/frontend/src/components/QuickFilters/QuickFilters.styles.scss b/frontend/src/components/QuickFilters/QuickFilters.styles.scss index 9272876a8cd..4c7d7065b70 100644 --- a/frontend/src/components/QuickFilters/QuickFilters.styles.scss +++ b/frontend/src/components/QuickFilters/QuickFilters.styles.scss @@ -1,8 +1,6 @@ .quick-filters-container { display: flex; flex-direction: row; - height: 100%; - min-height: 0; position: relative; .quick-filters-settings-container { diff --git a/frontend/src/components/TagKeyValueInput/TagKeyValueInput.test.tsx b/frontend/src/components/TagKeyValueInput/TagKeyValueInput.test.tsx new file mode 100644 index 00000000000..75d09d46883 --- /dev/null +++ b/frontend/src/components/TagKeyValueInput/TagKeyValueInput.test.tsx @@ -0,0 +1,112 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import TagKeyValueInput from './TagKeyValueInput'; + +const TID = 'tag-key-value-input'; + +type User = ReturnType; + +const startEditingFirstChip = async (user: User): Promise => { + await user.dblClick(screen.getAllByTestId(`${TID}-chip`)[0]); + return screen.getByTestId(`${TID}-edit`); +}; + +describe('TagKeyValueInput — inline chip edit', () => { + it('shows an error and stays in edit mode when Enter commits an invalid value', async () => { + const user = userEvent.setup(); + const onTagsChange = jest.fn(); + render(); + + const input = await startEditingFirstChip(user); + await user.clear(input); + await user.type(input, 'novalue{Enter}'); + + expect(screen.getByTestId(`${TID}-error`)).toHaveTextContent( + 'key:value format', + ); + // Still editing (input present), and no change committed. + expect(screen.getByTestId(`${TID}-edit`)).toBeInTheDocument(); + expect(onTagsChange).not.toHaveBeenCalled(); + }); + + it('shows a duplicate error when Enter commits an existing tag', async () => { + const user = userEvent.setup(); + const onTagsChange = jest.fn(); + render( + , + ); + + const input = await startEditingFirstChip(user); + await user.clear(input); + await user.type(input, 'team:pulse{Enter}'); + + expect(screen.getByTestId(`${TID}-error`)).toHaveTextContent( + 'already exists', + ); + expect(onTagsChange).not.toHaveBeenCalled(); + }); + + it('commits a valid edit on Enter', async () => { + const user = userEvent.setup(); + const onTagsChange = jest.fn(); + render(); + + const input = await startEditingFirstChip(user); + await user.clear(input); + await user.type(input, 'env:staging{Enter}'); + + expect(onTagsChange).toHaveBeenCalledWith(['env:staging']); + expect(screen.queryByTestId(`${TID}-error`)).not.toBeInTheDocument(); + }); + + it('reverts silently (no error) when blurring an invalid edit', async () => { + const user = userEvent.setup(); + const onTagsChange = jest.fn(); + render(); + + const input = await startEditingFirstChip(user); + await user.clear(input); + await user.type(input, 'novalue'); + await user.tab(); // blur off the edit input + + expect(screen.queryByTestId(`${TID}-error`)).not.toBeInTheDocument(); + expect(screen.queryByTestId(`${TID}-edit`)).not.toBeInTheDocument(); + expect(onTagsChange).not.toHaveBeenCalled(); + }); +}); + +describe('TagKeyValueInput — backend-rule validation', () => { + it('rejects a key containing a space on inline edit', async () => { + const user = userEvent.setup(); + const onTagsChange = jest.fn(); + render(); + + const input = await startEditingFirstChip(user); + await user.clear(input); + await user.type(input, 'my key:prod{Enter}'); + + expect(screen.getByTestId(`${TID}-error`)).toHaveTextContent( + 'spaces or special characters', + ); + expect(screen.getByTestId(`${TID}-edit`)).toBeInTheDocument(); + expect(onTagsChange).not.toHaveBeenCalled(); + }); + + it('rejects a value containing a space in the new-tag input', async () => { + const user = userEvent.setup(); + const onTagsChange = jest.fn(); + render(); + + const input = screen.getByTestId(TID); + await user.type(input, 'env:pro d{Enter}'); + + expect(screen.getByTestId(`${TID}-error`)).toHaveTextContent( + 'spaces or special characters', + ); + expect(onTagsChange).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/src/components/TagKeyValueInput/TagKeyValueInput.tsx b/frontend/src/components/TagKeyValueInput/TagKeyValueInput.tsx index f2025733baf..873746be92b 100644 --- a/frontend/src/components/TagKeyValueInput/TagKeyValueInput.tsx +++ b/frontend/src/components/TagKeyValueInput/TagKeyValueInput.tsx @@ -5,7 +5,7 @@ import { Typography } from '@signozhq/ui/typography'; import cx from 'classnames'; import TagBadge from '../TagBadge/TagBadge'; -import { parseKeyValueTag } from './utils'; +import { validateTag } from './utils'; import styles from './TagKeyValueInput.module.scss'; @@ -43,16 +43,12 @@ function TagKeyValueInput({ if (!raw) { return; } - const normalized = parseKeyValueTag(raw); - if (!normalized) { - setError('Tags must be in key:value format (both sides required).'); + const result = validateTag(raw, tags); + if ('error' in result) { + setError(result.error); return; } - if (tags.includes(normalized)) { - setError('This tag already exists.'); - return; - } - onTagsChange([...tags, normalized]); + onTagsChange([...tags, result.tag]); setInputValue(''); setError(''); }; @@ -82,15 +78,20 @@ function TagKeyValueInput({ const cancelEdit = (): void => { setEditIndex(-1); setEditValue(''); + setError(''); }; - const commitEdit = (): void => { - const normalized = parseKeyValueTag(editValue); - // Drop into a no-op (revert) on invalid or duplicate edits rather than - // stranding the user in an un-exitable edit box. - if (normalized && !tags.some((t, i) => t === normalized && i !== editIndex)) { - onTagsChange(tags.map((t, i) => (i === editIndex ? normalized : t))); + const commitEdit = (revertOnInvalid = false): void => { + const result = validateTag(editValue, tags, editIndex); + if ('error' in result) { + if (revertOnInvalid) { + cancelEdit(); + } else { + setError(result.error); + } + return; } + onTagsChange(tags.map((t, i) => (i === editIndex ? result.tag : t))); cancelEdit(); }; @@ -121,11 +122,14 @@ function TagKeyValueInput({ value={editValue} autoFocus testId={`${testId}-edit`} - onChange={(e: ChangeEvent): void => - setEditValue(e.target.value) - } + onChange={(e: ChangeEvent): void => { + setEditValue(e.target.value); + if (error) { + setError(''); + } + }} onKeyDown={handleEditKeyDown} - onBlur={commitEdit} + onBlur={(): void => commitEdit(true)} /> ) : ( MAX_TAG_LEN || value.length > MAX_TAG_LEN) { + return { + error: `Tag key and value must each be ${MAX_TAG_LEN} characters or fewer.`, + }; + } + if ( + existingTags.some( + (tag, index) => tag === normalized && index !== excludeIndex, + ) + ) { + return { error: 'This tag already exists.' }; + } + return { tag: normalized }; +} diff --git a/frontend/src/components/TanStackTableView/TanStackCustomTableRow.tsx b/frontend/src/components/TanStackTableView/TanStackCustomTableRow.tsx index dd220a9372b..259cbce08db 100644 --- a/frontend/src/components/TanStackTableView/TanStackCustomTableRow.tsx +++ b/frontend/src/components/TanStackTableView/TanStackCustomTableRow.tsx @@ -1,7 +1,11 @@ -import { ComponentProps, memo } from 'react'; +import { ComponentProps, memo, useCallback, useLayoutEffect } from 'react'; import { TableComponents } from 'react-virtuoso'; import cx from 'classnames'; +import { + chromePerformanceTanstackTableBeginHover, + chromePerformanceMeasureTanstackTable, +} from './perfDevtools'; import TanStackRowCells from './TanStackRow'; import { useClearRowHovered, @@ -10,6 +14,7 @@ import { import { FlatItem, TableRowContext } from './types'; import tableStyles from './TanStackTable.module.scss'; +import { chromePerformanceNow } from 'lib/chromePerformanceDevTools'; type VirtuosoTableRowProps = ComponentProps< NonNullable< @@ -22,6 +27,7 @@ function TanStackCustomTableRow({ context, ...props }: VirtuosoTableRowProps): JSX.Element { + const renderStart = chromePerformanceNow(); const rowId = item.row.id; const rowData = item.row.original; @@ -29,6 +35,23 @@ function TanStackCustomTableRow({ const setHovered = useSetRowHovered(rowId); const clearHovered = useClearRowHovered(rowId); + const handleMouseEnter = useCallback((): void => { + chromePerformanceTanstackTableBeginHover(rowId); + setHovered(); + }, [rowId, setHovered]); + + useLayoutEffect(() => { + chromePerformanceMeasureTanstackTable('Row render', renderStart, { + track: 'Row render', + color: 'secondary', + tooltipText: 'Row render + commit', + properties: [ + ['rowId', rowId], + ['kind', item.kind], + ], + }); + }); + if (item.kind === 'expansion') { return ( @@ -65,7 +88,7 @@ function TanStackCustomTableRow({ {...props} className={rowClassName} style={rowStyle} - onMouseEnter={setHovered} + onMouseEnter={handleMouseEnter} onMouseLeave={clearHovered} > & { + rowId: string; + children: ReactNode; +}; + +export function TanStackHoverTooltip({ + rowId, + children, + ...tooltipProps +}: HoverTooltipProps): JSX.Element { + const isHovered = useIsRowHovered(rowId); + + useLayoutEffect(() => { + if (isHovered) { + chromePerformanceTanstackTableEndHover(rowId); + } + }, [isHovered, rowId]); + + if (!isHovered) { + return <>{children}; + } + + return {children}; +} diff --git a/frontend/src/components/TanStackTableView/TanStackTable.tsx b/frontend/src/components/TanStackTableView/TanStackTable.tsx index c6349050c4a..f4e685d624a 100644 --- a/frontend/src/components/TanStackTableView/TanStackTable.tsx +++ b/frontend/src/components/TanStackTableView/TanStackTable.tsx @@ -5,6 +5,7 @@ import { useCallback, useEffect, useImperativeHandle, + useLayoutEffect, useMemo, useRef, } from 'react'; @@ -44,6 +45,7 @@ import { TanStackTableHandle, TanStackTableProps, } from './types'; +import { chromePerformanceMeasureTanstackTable } from './perfDevtools'; import { useColumnDnd } from './useColumnDnd'; import { useColumnHandlers } from './useColumnHandlers'; import { useColumnState } from './useColumnState'; @@ -56,6 +58,7 @@ import { VirtuosoTableColGroup } from './VirtuosoTableColGroup'; import tableStyles from './TanStackTable.module.scss'; import viewStyles from './TanStackTableView.module.scss'; +import { chromePerformanceNow } from 'lib/chromePerformanceDevTools'; const COLUMN_DND_AUTO_SCROLL = { layoutShiftCompensation: false as const, @@ -116,6 +119,8 @@ function TanStackTableInner( ); } + const renderStart = chromePerformanceNow(); + const virtuosoRef = useRef(null); const isDarkMode = useIsDarkMode(); @@ -344,6 +349,19 @@ function TanStackTableInner( const visibleColumnsCount = table.getVisibleFlatColumns().length; + useLayoutEffect(() => { + chromePerformanceMeasureTanstackTable('Table render', renderStart, { + track: 'Table render', + color: 'primary', + tooltipText: 'TanStackTable render + commit', + properties: [ + ['rows', String(flatItems.length)], + ['columns', String(visibleColumnsCount)], + ['loading', String(isLoading)], + ], + }); + }); + const columnOrderKey = useMemo(() => columnIds.join(','), [columnIds]); const columnVisibilityKey = useMemo( () => diff --git a/frontend/src/components/TanStackTableView/index.tsx b/frontend/src/components/TanStackTableView/index.tsx index 2e04c3b3c09..ee659c37041 100644 --- a/frontend/src/components/TanStackTableView/index.tsx +++ b/frontend/src/components/TanStackTableView/index.tsx @@ -1,6 +1,8 @@ +import { TanStackHoverTooltip } from './TanStackHoverTooltip'; import { TanStackTableBase } from './TanStackTable'; import TanStackTableText from './TanStackTableText'; +export * from './TanStackHoverTooltip'; export * from './TanStackTableStateContext'; export * from './types'; export * from './useCalculatedPageSize'; @@ -274,6 +276,7 @@ export * from './useTableParams'; */ const TanStackTable = Object.assign(TanStackTableBase, { Text: TanStackTableText, + HoverTooltip: TanStackHoverTooltip, }); export default TanStackTable; diff --git a/frontend/src/components/TanStackTableView/perfDevtools.ts b/frontend/src/components/TanStackTableView/perfDevtools.ts new file mode 100644 index 00000000000..98b9f03382d --- /dev/null +++ b/frontend/src/components/TanStackTableView/perfDevtools.ts @@ -0,0 +1,35 @@ +import { + createChromePerformanceInteractionTracker, + chromePerformanceMeasure, + ChromePerformanceTrackEntryOptions, +} from 'lib/chromePerformanceDevTools'; + +const TABLE_TRACK_GROUP = 'SigNoz Table'; + +export function chromePerformanceMeasureTanstackTable( + name: string, + start: number, + { + track, + color, + tooltipText, + properties, + }: Omit, +): void { + chromePerformanceMeasure(name, start, { + trackGroup: TABLE_TRACK_GROUP, + track, + color, + tooltipText, + properties, + }); +} + +const hoverTracker = createChromePerformanceInteractionTracker( + TABLE_TRACK_GROUP, + 'Hover', + 'Row hover', +); + +export const chromePerformanceTanstackTableBeginHover = hoverTracker.begin; +export const chromePerformanceTanstackTableEndHover = hoverTracker.end; diff --git a/frontend/src/components/TanStackTableView/types.ts b/frontend/src/components/TanStackTableView/types.ts index 19f8bc90832..a03ee5c7bac 100644 --- a/frontend/src/components/TanStackTableView/types.ts +++ b/frontend/src/components/TanStackTableView/types.ts @@ -21,6 +21,7 @@ export type TableCellContext = { value: TValue; isActive: boolean; rowIndex: number; + rowId: string; isExpanded: boolean; canExpand: boolean; toggleExpanded: () => void; diff --git a/frontend/src/components/TanStackTableView/utils.ts b/frontend/src/components/TanStackTableView/utils.ts index d73d208f5dc..35d83d1354f 100644 --- a/frontend/src/components/TanStackTableView/utils.ts +++ b/frontend/src/components/TanStackTableView/utils.ts @@ -135,6 +135,7 @@ export function buildTanstackColumnDef( value: getValue() as TData[any], isActive: isRowActive?.(rowData) ?? false, rowIndex: row.index, + rowId: row.id, isExpanded: row.getIsExpanded(), canExpand: row.getCanExpand(), toggleExpanded: (): void => { diff --git a/frontend/src/container/InfraMonitoringHostsV2/Hosts.tsx b/frontend/src/container/InfraMonitoringHostsV2/Hosts.tsx index bd628ecc8fe..24d789116ff 100644 --- a/frontend/src/container/InfraMonitoringHostsV2/Hosts.tsx +++ b/frontend/src/container/InfraMonitoringHostsV2/Hosts.tsx @@ -51,6 +51,7 @@ import { getHostsQuickFiltersConfig } from './utils'; import styles from './InfraMonitoringHosts.module.scss'; import { ArrowUpToLine, Filter } from '@signozhq/icons'; import { NANO_SECOND_MULTIPLIER, useGlobalTimeStore } from 'store/globalTime'; +import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; function Hosts(): JSX.Element { const [showFilters, setShowFilters] = useState(true); @@ -211,27 +212,31 @@ function Hosts(): JSX.Element {
{showFilters && (
-
- Filters - - + <> +
+ Filters + + + +
+ -
-
- + +
)}
diff --git a/frontend/src/container/InfraMonitoringHostsV2/InfraMonitoringHosts.module.scss b/frontend/src/container/InfraMonitoringHostsV2/InfraMonitoringHosts.module.scss index 2c0da6eb182..6be087ec243 100644 --- a/frontend/src/container/InfraMonitoringHostsV2/InfraMonitoringHosts.module.scss +++ b/frontend/src/container/InfraMonitoringHostsV2/InfraMonitoringHosts.module.scss @@ -48,24 +48,6 @@ width: 280px; min-width: 280px; border-right: 1px solid var(--l1-border); - overflow-y: auto; - - &::-webkit-scrollbar { - width: 0.1rem; - height: 0.1rem; - } - - &::-webkit-scrollbar-track { - background: transparent; - } - - &::-webkit-scrollbar-thumb { - background: var(--accent-primary); - } - - &::-webkit-scrollbar-thumb:hover { - background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%); - } :global(.ant-collapse-header) { border-bottom: 1px solid var(--l1-border); @@ -80,28 +62,6 @@ padding-left: 18px; } } - - :global(.quick-filters) { - overflow-y: auto; - overflow-x: hidden; - - &::-webkit-scrollbar { - width: 0.1rem; - height: 0.1rem; - } - - &::-webkit-scrollbar-track { - background: transparent; - } - - &::-webkit-scrollbar-thumb { - background: var(--accent-primary); - } - - &::-webkit-scrollbar-thumb:hover { - background: var(--accent-primary); - } - } } .quickFiltersContainerHeader { diff --git a/frontend/src/container/InfraMonitoringHostsV2/table.config.tsx b/frontend/src/container/InfraMonitoringHostsV2/table.config.tsx index 95813d18289..5652b16081e 100644 --- a/frontend/src/container/InfraMonitoringHostsV2/table.config.tsx +++ b/frontend/src/container/InfraMonitoringHostsV2/table.config.tsx @@ -130,12 +130,13 @@ export const hostColumnsConfig: HostColumnConfigType[] = [ accessorFn: (row): string => row.status, width: { min: 140 }, enableSort: false, - cell: ({ value, groupMeta, row }): React.ReactNode => { + cell: ({ value, groupMeta, row, rowId }): React.ReactNode => { const status = value as InframonitoringtypesHostStatusDTO; if (groupMeta) { return ( - Memory Usage (WSS) + Memory Usage +
(WSS) ), accessorFn: (row): number => row.memory, - width: { min: 240 }, + width: { min: 200 }, enableSort: true, cell: ({ value }): React.ReactNode => { const memory = value as number; @@ -248,11 +249,12 @@ export const hostColumnsConfig: HostColumnConfigType[] = [ id: 'load15', header: (): React.ReactNode => ( - Load Avg (15min) + Load Avg +
(15min)
), accessorFn: (row): number => row.load15, - width: { min: 200 }, + width: { min: 160 }, enableSort: true, cell: ({ value }): React.ReactNode => { const load15 = Number(value); @@ -276,7 +278,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [ ), accessorFn: (row): number => row.diskUsage, - width: { min: 160 }, + width: { min: 200 }, enableSort: true, cell: ({ value }): React.ReactNode => { const diskUsage = value as number; diff --git a/frontend/src/container/InfraMonitoringHostsV2/table.module.scss b/frontend/src/container/InfraMonitoringHostsV2/table.module.scss index 5320372db20..802e3ec1a34 100644 --- a/frontend/src/container/InfraMonitoringHostsV2/table.module.scss +++ b/frontend/src/container/InfraMonitoringHostsV2/table.module.scss @@ -13,14 +13,6 @@ text-align: right; } -.memoryUsageHeader { - display: inline-flex; - align-items: center; - justify-content: flex-end; - gap: var(--spacing-2); - width: 100%; -} - .statusTag { width: fit-content; font-family: Inter, sans-serif; diff --git a/frontend/src/container/InfraMonitoringK8sV2/Base/K8sBaseDetails.tsx b/frontend/src/container/InfraMonitoringK8sV2/Base/K8sBaseDetails.tsx index 13db22f17b2..d11686452ad 100644 --- a/frontend/src/container/InfraMonitoringK8sV2/Base/K8sBaseDetails.tsx +++ b/frontend/src/container/InfraMonitoringK8sV2/Base/K8sBaseDetails.tsx @@ -163,8 +163,9 @@ export default function K8sBaseDetails({ destroyOnClose closeIcon={} > - {isEntityLoading && } - {(isEntityError || hasResponseError) && ( + {(isEntityLoading || !selectedItem) && } + + {selectedItem && (isEntityError || hasResponseError) && (
({ />
)} - {entity && !isEntityLoading && !hasResponseError && ( + {selectedItem && entity && !isEntityLoading && !hasResponseError && ( ; /** Function to get the unique key for a row. */ - getRowKey?: (record: T) => string; + getRowKey: (record: T) => string; /** Function to get the item key used for selection. Can return string or SelectedItemParams. */ - getItemKey?: (record: T) => TItemKey; + getItemKey: (record: T) => TItemKey; eventCategory: InfraMonitoringEvents; renderEmptyState?: ( context: K8sBaseListEmptyStateContext, ) => React.ReactNode | null; extraQueryKeyParts?: string[]; + detailsQueryKeyPrefix: string; }; export function K8sBaseList< @@ -98,6 +99,7 @@ export function K8sBaseList< eventCategory, renderEmptyState, extraQueryKeyParts = [], + detailsQueryKeyPrefix, }: K8sBaseListProps): JSX.Element { const { currentQuery } = useQueryBuilder(); const expression = currentQuery.builder.queryData[0]?.filter?.expression || ''; @@ -234,17 +236,29 @@ export function K8sBaseList< }, [eventCategory, totalCount]); const handleRowClick = useCallback( - (_record: T, itemKey: TItemKey): void => { + (record: T, itemKey: TItemKey): void => { if (groupBy.length === 0) { - if (typeof itemKey === 'object' && itemKey !== null) { - setSelectedItemParams(itemKey); - } else { - setSelectedItemParams({ - selectedItem: itemKey, - clusterName: null, - namespaceName: null, - }); + const params: SelectedItemParams = + typeof itemKey === 'object' + ? itemKey + : { + selectedItem: itemKey, + clusterName: null, + namespaceName: null, + }; + + if (detailsQueryKeyPrefix) { + const detailQueryKey = getAutoRefreshQueryKey( + selectedTime, + `${detailsQueryKeyPrefix}EntityDetails`, + params.selectedItem, + params.clusterName, + params.namespaceName, + ); + queryClient.setQueryData(detailQueryKey, { data: record }); } + + setSelectedItemParams(params); } void logEvent(InfraMonitoringEvents.ItemClicked, { @@ -253,7 +267,15 @@ export function K8sBaseList< category: eventCategory, }); }, - [eventCategory, groupBy.length, setSelectedItemParams], + [ + eventCategory, + groupBy.length, + setSelectedItemParams, + detailsQueryKeyPrefix, + getAutoRefreshQueryKey, + selectedTime, + queryClient, + ], ); const handleRowClickNewTab = useCallback( @@ -403,7 +425,7 @@ export function K8sBaseList< onRowClickNewTab={handleRowClickNewTab} renderExpandedRow={isGroupedByAttribute ? renderExpandedRow : undefined} getRowCanExpand={isGroupedByAttribute ? getRowCanExpand : undefined} - className={cx(styles.k8SListTable, expandedRowColumns)} + className={cx(styles.k8SListTable)} enableQueryParams={{ page: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE, limit: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE_SIZE, diff --git a/frontend/src/container/InfraMonitoringK8sV2/Base/K8sDynamicList.tsx b/frontend/src/container/InfraMonitoringK8sV2/Base/K8sDynamicList.tsx new file mode 100644 index 00000000000..2b534d26fca --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8sV2/Base/K8sDynamicList.tsx @@ -0,0 +1,43 @@ +import { useMemo } from 'react'; + +import { useInfraMonitoringCategory } from '../hooks'; +import { getEntityConfig } from './entity.registry'; +import { K8sBaseList } from './K8sBaseList'; +import K8sBaseDetails from './K8sBaseDetails'; + +export interface K8sDynamicListProps { + controlListPrefix?: React.ReactNode; + leftFilters?: React.ReactNode; +} + +export function K8sDynamicList({ + controlListPrefix, + leftFilters, +}: K8sDynamicListProps): JSX.Element | null { + const [selectedCategory] = useInfraMonitoringCategory(); + + const config = useMemo( + () => getEntityConfig(selectedCategory), + [selectedCategory], + ); + + if (!config) { + return null; + } + + const { list, details } = config; + + return ( + <> + + + + + ); +} + +export default K8sDynamicList; diff --git a/frontend/src/container/InfraMonitoringK8sV2/Base/__tests__/K8sBaseDetails.tabValidation.test.tsx b/frontend/src/container/InfraMonitoringK8sV2/Base/__tests__/K8sBaseDetails.tabValidation.test.tsx index 61fcc48f666..82fb29f25bf 100644 --- a/frontend/src/container/InfraMonitoringK8sV2/Base/__tests__/K8sBaseDetails.tabValidation.test.tsx +++ b/frontend/src/container/InfraMonitoringK8sV2/Base/__tests__/K8sBaseDetails.tabValidation.test.tsx @@ -1,5 +1,6 @@ import { Box } from '@signozhq/icons'; import { screen } from '@testing-library/react'; +import { InfraMonitoringEvents } from 'constants/events'; import userEvent from '@testing-library/user-event'; import { NuqsTestingAdapter } from 'nuqs/adapters/testing'; import { act, render, waitFor } from 'tests/test-utils'; @@ -31,7 +32,7 @@ const mockEntity: TestEntity = { function createBaseProps() { return { category: InfraMonitoringEntity.PODS, - eventCategory: 'Pod', + eventCategory: InfraMonitoringEvents.Pod, getSelectedItemExpression: (): string => 'k8s.pod.name = "test-pod"', fetchEntityData: jest .fn() diff --git a/frontend/src/container/InfraMonitoringK8sV2/Base/__tests__/K8sBaseList.test.tsx b/frontend/src/container/InfraMonitoringK8sV2/Base/__tests__/K8sBaseList.test.tsx index 9ab127ad69d..babee60b5e3 100644 --- a/frontend/src/container/InfraMonitoringK8sV2/Base/__tests__/K8sBaseList.test.tsx +++ b/frontend/src/container/InfraMonitoringK8sV2/Base/__tests__/K8sBaseList.test.tsx @@ -175,8 +175,10 @@ function renderComponent< >({ queryParams, onUrlUpdate, + detailsQueryKeyPrefix = 'testEntity', ...props -}: K8sBaseListProps & { +}: Omit, 'detailsQueryKeyPrefix'> & { + detailsQueryKeyPrefix?: string; queryParams?: Record; onUrlUpdate?: OnUrlUpdateFunction; }) { @@ -203,7 +205,10 @@ function renderComponent< value={{ viewportHeight: 800, itemHeight: 50 }} > - {...props} /> + + {...props} + detailsQueryKeyPrefix={detailsQueryKeyPrefix} + /> diff --git a/frontend/src/container/InfraMonitoringK8sV2/Base/entity.config.types.ts b/frontend/src/container/InfraMonitoringK8sV2/Base/entity.config.types.ts new file mode 100644 index 00000000000..7f2319dc76e --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8sV2/Base/entity.config.types.ts @@ -0,0 +1,21 @@ +import { SelectedItemParams } from '../hooks'; +import { K8sBaseListProps } from './K8sBaseList'; +import { K8sBaseDetailsProps } from './types'; + +export type K8sEntityData = { meta?: Record | null }; + +export type K8sEntityListConfig< + T extends K8sEntityData, + TItemKey extends string | SelectedItemParams = string, +> = Omit, 'controlListPrefix' | 'leftFilters'>; + +export type K8sEntityDetailsConfig = + K8sBaseDetailsProps; + +export interface K8sEntityConfig< + T extends K8sEntityData, + TItemKey extends string | SelectedItemParams = string, +> { + list: K8sEntityListConfig; + details: K8sEntityDetailsConfig; +} diff --git a/frontend/src/container/InfraMonitoringK8sV2/Base/entity.registry.ts b/frontend/src/container/InfraMonitoringK8sV2/Base/entity.registry.ts new file mode 100644 index 00000000000..9783aa3c150 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8sV2/Base/entity.registry.ts @@ -0,0 +1,41 @@ +import { K8sCategories } from '../constants'; +import { SelectedItemParams } from '../hooks'; +import { K8sEntityConfig, K8sEntityData } from './entity.config.types'; + +import { podEntityConfig } from '../Pods/entity.config'; +import { nodeEntityConfig } from '../Nodes/entity.config'; +import { clusterEntityConfig } from '../Clusters/entity.config'; +import { deploymentEntityConfig } from '../Deployments/entity.config'; +import { namespaceEntityConfig } from '../Namespaces/entity.config'; +import { jobEntityConfig } from '../Jobs/entity.config'; +import { daemonSetEntityConfig } from '../DaemonSets/entity.config'; +import { statefulSetEntityConfig } from '../StatefulSets/entity.config'; +import { volumeEntityConfig } from '../Volumes/entity.config'; + +type AnyEntityConfig = K8sEntityConfig< + K8sEntityData, + string | SelectedItemParams +>; + +function registerConfig< + T extends K8sEntityData, + TItemKey extends string | SelectedItemParams, +>(config: K8sEntityConfig): AnyEntityConfig { + return config as unknown as AnyEntityConfig; +} + +export const entityRegistry: Record = { + [K8sCategories.PODS]: registerConfig(podEntityConfig), + [K8sCategories.NODES]: registerConfig(nodeEntityConfig), + [K8sCategories.CLUSTERS]: registerConfig(clusterEntityConfig), + [K8sCategories.DEPLOYMENTS]: registerConfig(deploymentEntityConfig), + [K8sCategories.NAMESPACES]: registerConfig(namespaceEntityConfig), + [K8sCategories.JOBS]: registerConfig(jobEntityConfig), + [K8sCategories.DAEMONSETS]: registerConfig(daemonSetEntityConfig), + [K8sCategories.STATEFULSETS]: registerConfig(statefulSetEntityConfig), + [K8sCategories.VOLUMES]: registerConfig(volumeEntityConfig), +}; + +export function getEntityConfig(category: string): AnyEntityConfig | undefined { + return entityRegistry[category]; +} diff --git a/frontend/src/container/InfraMonitoringK8sV2/Base/types.ts b/frontend/src/container/InfraMonitoringK8sV2/Base/types.ts index 999e57d11c6..bf422875399 100644 --- a/frontend/src/container/InfraMonitoringK8sV2/Base/types.ts +++ b/frontend/src/container/InfraMonitoringK8sV2/Base/types.ts @@ -3,6 +3,7 @@ import { CustomTimeType, Time, } from 'container/TopNav/DateTimeSelectionV2/types'; +import { InfraMonitoringEvents } from 'constants/events'; import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; import APIError from 'types/api/error'; @@ -99,7 +100,7 @@ export interface K8sDetailsCustomTab { export interface K8sBaseDetailsProps { category: InfraMonitoringEntity; - eventCategory: string; + eventCategory: InfraMonitoringEvents; // Data fetching configuration getSelectedItemExpression: (params: SelectedItemParams) => string; fetchEntityData: ( @@ -125,7 +126,7 @@ export interface K8sBaseDetailsProps { export interface K8sBaseDetailsContentProps { entity: T; category: InfraMonitoringEntity; - eventCategory: string; + eventCategory: InfraMonitoringEvents; metadataConfig: K8sDetailsMetadataConfig[]; countsConfig?: K8sDetailsCountConfig[]; getCountsFilterExpression?: (entity: T) => string; diff --git a/frontend/src/container/InfraMonitoringK8sV2/Clusters/K8sClustersList.tsx b/frontend/src/container/InfraMonitoringK8sV2/Clusters/K8sClustersList.tsx deleted file mode 100644 index 5e4926255e2..00000000000 --- a/frontend/src/container/InfraMonitoringK8sV2/Clusters/K8sClustersList.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { useCallback } from 'react'; -import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs'; -import { listClusters } from 'api/generated/services/inframonitoring'; -import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas'; -import { AxiosError } from 'axios'; -import { - InframonitoringtypesClusterRecordDTO, - InframonitoringtypesResponseTypeDTO, - Querybuildertypesv5OrderDirectionDTO, -} from 'api/generated/services/sigNoz.schemas'; -import { InfraMonitoringEvents } from 'constants/events'; -import APIError from 'types/api/error'; - -import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails'; -import { K8sBaseList } from '../Base/K8sBaseList'; -import { K8sBaseFilters } from '../Base/types'; -import { InfraMonitoringEntity } from '../constants'; -import { - clusterWidgetInfo, - getClusterMetricsQueryPayload, - k8sClusterDetailsCountsConfig, - k8sClusterDetailsMetadataConfig, - k8sClusterGetCountsFilterExpression, - k8sClusterGetEntityName, - k8sClusterGetSelectedItemExpression, - k8sClusterInitialEventsExpression, - k8sClusterInitialLogTracesExpression, -} from './constants'; -import { - getK8sClusterItemKey, - getK8sClusterRowKey, - k8sClustersColumnsConfig, -} from './table.config'; - -function K8sClustersList({ - controlListPrefix, -}: { - controlListPrefix?: React.ReactNode; -}): JSX.Element { - const fetchListData = useCallback( - async (filters: K8sBaseFilters, signal?: AbortSignal) => { - try { - const response = await listClusters( - { - filter: { expression: filters.filter.expression }, - groupBy: filters.groupBy?.map((g) => ({ name: g.name })), - offset: filters.offset, - limit: filters.limit ?? 10, - start: filters.start, - end: filters.end, - orderBy: filters.orderBy - ? { - key: { name: filters.orderBy.key.name }, - direction: - filters.orderBy.direction === 'asc' - ? Querybuildertypesv5OrderDirectionDTO.asc - : Querybuildertypesv5OrderDirectionDTO.desc, - } - : undefined, - }, - signal, - ); - - const data = response.data; - return { - type: - data.type === InframonitoringtypesResponseTypeDTO.grouped_list - ? ('grouped_list' as const) - : ('list' as const), - records: data.records, - total: data.total, - endTimeBeforeRetention: data.endTimeBeforeRetention, - warning: data.warning, - }; - } catch (error) { - return { - type: 'list' as const, - records: [] as InframonitoringtypesClusterRecordDTO[], - total: 0, - error: - convertToApiError(error as AxiosError) ?? null, - }; - } - }, - [], - ); - - const fetchEntityData = useCallback( - async ( - filters: K8sDetailsFilters, - signal?: AbortSignal, - ): Promise<{ - data: InframonitoringtypesClusterRecordDTO | null; - error?: APIError | null; - }> => { - try { - const response = await listClusters( - { - filter: { expression: filters.filter.expression }, - start: filters.start, - end: filters.end, - limit: 1, - offset: 0, - }, - signal, - ); - - return { - data: response.data.records.length > 0 ? response.data.records[0] : null, - }; - } catch (error) { - return { - data: null, - error: - convertToApiError(error as AxiosError) ?? null, - }; - } - }, - [], - ); - - return ( - <> - - controlListPrefix={controlListPrefix} - entity={InfraMonitoringEntity.CLUSTERS} - tableColumns={k8sClustersColumnsConfig} - fetchListData={fetchListData} - getRowKey={getK8sClusterRowKey} - getItemKey={getK8sClusterItemKey} - eventCategory={InfraMonitoringEvents.Cluster} - /> - - - category={InfraMonitoringEntity.CLUSTERS} - eventCategory={InfraMonitoringEvents.Cluster} - getSelectedItemExpression={k8sClusterGetSelectedItemExpression} - fetchEntityData={fetchEntityData} - getEntityName={k8sClusterGetEntityName} - getInitialLogTracesExpression={k8sClusterInitialLogTracesExpression} - getInitialEventsExpression={k8sClusterInitialEventsExpression} - metadataConfig={k8sClusterDetailsMetadataConfig} - countsConfig={k8sClusterDetailsCountsConfig} - getCountsFilterExpression={k8sClusterGetCountsFilterExpression} - entityWidgetInfo={clusterWidgetInfo} - getEntityQueryPayload={getClusterMetricsQueryPayload} - queryKeyPrefix="cluster" - /> - - ); -} - -export default K8sClustersList; diff --git a/frontend/src/container/InfraMonitoringK8sV2/Clusters/entity.config.ts b/frontend/src/container/InfraMonitoringK8sV2/Clusters/entity.config.ts new file mode 100644 index 00000000000..b86a5b28889 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8sV2/Clusters/entity.config.ts @@ -0,0 +1,138 @@ +import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs'; +import { listClusters } from 'api/generated/services/inframonitoring'; +import { + InframonitoringtypesClusterRecordDTO, + InframonitoringtypesResponseTypeDTO, + Querybuildertypesv5OrderDirectionDTO, + RenderErrorResponseDTO, +} from 'api/generated/services/sigNoz.schemas'; +import { AxiosError } from 'axios'; +import { InfraMonitoringEvents } from 'constants/events'; + +import { K8sEntityConfig } from '../Base/entity.config.types'; +import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types'; +import { InfraMonitoringEntity } from '../constants'; +import { + clusterWidgetInfo, + getClusterMetricsQueryPayload, + k8sClusterDetailsCountsConfig, + k8sClusterDetailsMetadataConfig, + k8sClusterGetCountsFilterExpression, + k8sClusterGetEntityName, + k8sClusterGetSelectedItemExpression, + k8sClusterInitialEventsExpression, + k8sClusterInitialLogTracesExpression, +} from './constants'; +import { + getK8sClusterItemKey, + getK8sClusterRowKey, + k8sClustersColumnsConfig, +} from './table.config'; + +async function fetchListData( + filters: K8sBaseFilters, + signal?: AbortSignal, +): ReturnType< + K8sEntityConfig['list']['fetchListData'] +> { + try { + const response = await listClusters( + { + filter: { expression: filters.filter.expression }, + groupBy: filters.groupBy?.map((g) => ({ name: g.name })), + offset: filters.offset, + limit: filters.limit ?? 10, + start: filters.start, + end: filters.end, + orderBy: filters.orderBy + ? { + key: { name: filters.orderBy.key.name }, + direction: + filters.orderBy.direction === 'asc' + ? Querybuildertypesv5OrderDirectionDTO.asc + : Querybuildertypesv5OrderDirectionDTO.desc, + } + : undefined, + }, + signal, + ); + + const data = response.data; + return { + type: + data.type === InframonitoringtypesResponseTypeDTO.grouped_list + ? ('grouped_list' as const) + : ('list' as const), + records: data.records, + total: data.total, + endTimeBeforeRetention: data.endTimeBeforeRetention, + warning: data.warning, + }; + } catch (error) { + return { + type: 'list' as const, + records: [] as InframonitoringtypesClusterRecordDTO[], + total: 0, + error: + convertToApiError(error as AxiosError) ?? null, + }; + } +} + +async function fetchEntityData( + filters: K8sDetailsFilters, + signal?: AbortSignal, +): ReturnType< + K8sEntityConfig['details']['fetchEntityData'] +> { + try { + const response = await listClusters( + { + filter: { expression: filters.filter.expression }, + start: filters.start, + end: filters.end, + limit: 1, + offset: 0, + }, + signal, + ); + + return { + data: response.data.records.length > 0 ? response.data.records[0] : null, + }; + } catch (error) { + return { + data: null, + error: + convertToApiError(error as AxiosError) ?? null, + }; + } +} + +export const clusterEntityConfig: K8sEntityConfig = + { + list: { + entity: InfraMonitoringEntity.CLUSTERS, + eventCategory: InfraMonitoringEvents.Cluster, + tableColumns: k8sClustersColumnsConfig, + fetchListData, + getRowKey: getK8sClusterRowKey, + getItemKey: getK8sClusterItemKey, + detailsQueryKeyPrefix: 'cluster', + }, + details: { + category: InfraMonitoringEntity.CLUSTERS, + eventCategory: InfraMonitoringEvents.Cluster, + queryKeyPrefix: 'cluster', + getSelectedItemExpression: k8sClusterGetSelectedItemExpression, + fetchEntityData, + getEntityName: k8sClusterGetEntityName, + getInitialLogTracesExpression: k8sClusterInitialLogTracesExpression, + getInitialEventsExpression: k8sClusterInitialEventsExpression, + metadataConfig: k8sClusterDetailsMetadataConfig, + entityWidgetInfo: clusterWidgetInfo, + getEntityQueryPayload: getClusterMetricsQueryPayload, + countsConfig: k8sClusterDetailsCountsConfig, + getCountsFilterExpression: k8sClusterGetCountsFilterExpression, + }, + }; diff --git a/frontend/src/container/InfraMonitoringK8sV2/Clusters/table.config.tsx b/frontend/src/container/InfraMonitoringK8sV2/Clusters/table.config.tsx index 57f50d36c10..41ebf801130 100644 --- a/frontend/src/container/InfraMonitoringK8sV2/Clusters/table.config.tsx +++ b/frontend/src/container/InfraMonitoringK8sV2/Clusters/table.config.tsx @@ -93,13 +93,14 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [ row.nodeCountsByReadiness, width: { min: 180 }, enableSort: false, - cell: ({ row }): React.ReactNode => { + cell: ({ row, rowId }): React.ReactNode => { if (!row.nodeCountsByReadiness) { return -; } return ( { + cell: ({ row, rowId }): React.ReactNode => { const podCountsByStatus = row.podCountsByStatus; if (!podCountsByStatus) { return -; } return ( - + ); }, }, diff --git a/frontend/src/container/InfraMonitoringK8sV2/DaemonSets/K8sDaemonSetsList.tsx b/frontend/src/container/InfraMonitoringK8sV2/DaemonSets/K8sDaemonSetsList.tsx deleted file mode 100644 index faf8e7d5cd7..00000000000 --- a/frontend/src/container/InfraMonitoringK8sV2/DaemonSets/K8sDaemonSetsList.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { useCallback, useMemo } from 'react'; -import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs'; -import { listDaemonSets } from 'api/generated/services/inframonitoring'; -import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas'; -import { AxiosError } from 'axios'; -import { - InframonitoringtypesDaemonSetRecordDTO, - InframonitoringtypesResponseTypeDTO, - Querybuildertypesv5OrderDirectionDTO, -} from 'api/generated/services/sigNoz.schemas'; -import { InfraMonitoringEvents } from 'constants/events'; -import APIError from 'types/api/error'; - -import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails'; -import { K8sBaseList } from '../Base/K8sBaseList'; -import { K8sBaseFilters } from '../Base/types'; -import { InfraMonitoringEntity } from '../constants'; -import { SelectedItemParams } from '../hooks'; -import { - daemonSetWidgetInfo, - getDaemonSetMetricsQueryPayload, - getDaemonSetPodMetricsQueryPayload, - k8sDaemonSetDetailsMetadataConfig, - k8sDaemonSetGetEntityName, - k8sDaemonSetGetSelectedItemExpression, - k8sDaemonSetInitialEventsExpression, - k8sDaemonSetInitialLogTracesExpression, -} from './constants'; -import { - getK8sDaemonSetItemKey, - getK8sDaemonSetRowKey, - k8sDaemonSetsColumnsConfig, -} from './table.config'; -import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab'; - -function K8sDaemonSetsList({ - controlListPrefix, -}: { - controlListPrefix?: React.ReactNode; -}): JSX.Element { - const fetchListData = useCallback( - async (filters: K8sBaseFilters, signal?: AbortSignal) => { - try { - const response = await listDaemonSets( - { - filter: { expression: filters.filter.expression }, - groupBy: filters.groupBy?.map((g) => ({ name: g.name })), - offset: filters.offset, - limit: filters.limit ?? 10, - start: filters.start, - end: filters.end, - orderBy: filters.orderBy - ? { - key: { name: filters.orderBy.key.name }, - direction: - filters.orderBy.direction === 'asc' - ? Querybuildertypesv5OrderDirectionDTO.asc - : Querybuildertypesv5OrderDirectionDTO.desc, - } - : undefined, - }, - signal, - ); - const data = response.data; - return { - type: - data.type === InframonitoringtypesResponseTypeDTO.grouped_list - ? ('grouped_list' as const) - : ('list' as const), - records: data.records, - total: data.total, - endTimeBeforeRetention: data.endTimeBeforeRetention, - warning: data.warning, - }; - } catch (error) { - return { - type: 'list' as const, - records: [] as InframonitoringtypesDaemonSetRecordDTO[], - total: 0, - error: - convertToApiError(error as AxiosError) ?? null, - }; - } - }, - [], - ); - const fetchEntityData = useCallback( - async ( - filters: K8sDetailsFilters, - signal?: AbortSignal, - ): Promise<{ - data: InframonitoringtypesDaemonSetRecordDTO | null; - error?: APIError | null; - }> => { - try { - const response = await listDaemonSets( - { - filter: { expression: filters.filter.expression }, - start: filters.start, - end: filters.end, - limit: 1, - offset: 0, - }, - signal, - ); - return { - data: response.data.records.length > 0 ? response.data.records[0] : null, - }; - } catch (error) { - return { - data: null, - error: - convertToApiError(error as AxiosError) ?? null, - }; - } - }, - [], - ); - const customTabs = useMemo( - () => [ - createPodMetricsTab({ - getQueryPayload: getDaemonSetPodMetricsQueryPayload, - category: InfraMonitoringEntity.DAEMONSETS, - queryKey: 'daemonSetPodMetrics', - }), - ], - [], - ); - - return ( - <> - - controlListPrefix={controlListPrefix} - entity={InfraMonitoringEntity.DAEMONSETS} - tableColumns={k8sDaemonSetsColumnsConfig} - fetchListData={fetchListData} - getRowKey={getK8sDaemonSetRowKey} - getItemKey={getK8sDaemonSetItemKey} - eventCategory={InfraMonitoringEvents.DaemonSet} - /> - - category={InfraMonitoringEntity.DAEMONSETS} - eventCategory={InfraMonitoringEvents.DaemonSet} - getSelectedItemExpression={k8sDaemonSetGetSelectedItemExpression} - fetchEntityData={fetchEntityData} - getEntityName={k8sDaemonSetGetEntityName} - getInitialLogTracesExpression={k8sDaemonSetInitialLogTracesExpression} - getInitialEventsExpression={k8sDaemonSetInitialEventsExpression} - metadataConfig={k8sDaemonSetDetailsMetadataConfig} - entityWidgetInfo={daemonSetWidgetInfo} - getEntityQueryPayload={getDaemonSetMetricsQueryPayload} - queryKeyPrefix="daemonset" - customTabs={customTabs} - /> - - ); -} -export default K8sDaemonSetsList; diff --git a/frontend/src/container/InfraMonitoringK8sV2/DaemonSets/entity.config.ts b/frontend/src/container/InfraMonitoringK8sV2/DaemonSets/entity.config.ts new file mode 100644 index 00000000000..1114f6ec886 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8sV2/DaemonSets/entity.config.ts @@ -0,0 +1,153 @@ +import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs'; +import { listDaemonSets } from 'api/generated/services/inframonitoring'; +import { + InframonitoringtypesDaemonSetRecordDTO, + InframonitoringtypesResponseTypeDTO, + Querybuildertypesv5OrderDirectionDTO, + RenderErrorResponseDTO, +} from 'api/generated/services/sigNoz.schemas'; +import { AxiosError } from 'axios'; +import { InfraMonitoringEvents } from 'constants/events'; + +import { K8sEntityConfig } from '../Base/entity.config.types'; +import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types'; +import { InfraMonitoringEntity } from '../constants'; +import { createPodMetricsTab } from '../EntityDetailsUtils/createPodMetricsTab'; +import { SelectedItemParams } from '../hooks'; +import { + daemonSetWidgetInfo, + getDaemonSetMetricsQueryPayload, + getDaemonSetPodMetricsQueryPayload, + k8sDaemonSetDetailsMetadataConfig, + k8sDaemonSetGetEntityName, + k8sDaemonSetGetSelectedItemExpression, + k8sDaemonSetInitialEventsExpression, + k8sDaemonSetInitialLogTracesExpression, +} from './constants'; +import { + getK8sDaemonSetItemKey, + getK8sDaemonSetRowKey, + k8sDaemonSetsColumnsConfig, +} from './table.config'; + +async function fetchListData( + filters: K8sBaseFilters, + signal?: AbortSignal, +): ReturnType< + K8sEntityConfig< + InframonitoringtypesDaemonSetRecordDTO, + SelectedItemParams + >['list']['fetchListData'] +> { + try { + const response = await listDaemonSets( + { + filter: { expression: filters.filter.expression }, + groupBy: filters.groupBy?.map((g) => ({ name: g.name })), + offset: filters.offset, + limit: filters.limit ?? 10, + start: filters.start, + end: filters.end, + orderBy: filters.orderBy + ? { + key: { name: filters.orderBy.key.name }, + direction: + filters.orderBy.direction === 'asc' + ? Querybuildertypesv5OrderDirectionDTO.asc + : Querybuildertypesv5OrderDirectionDTO.desc, + } + : undefined, + }, + signal, + ); + + const data = response.data; + return { + type: + data.type === InframonitoringtypesResponseTypeDTO.grouped_list + ? ('grouped_list' as const) + : ('list' as const), + records: data.records, + total: data.total, + endTimeBeforeRetention: data.endTimeBeforeRetention, + warning: data.warning, + }; + } catch (error) { + return { + type: 'list' as const, + records: [] as InframonitoringtypesDaemonSetRecordDTO[], + total: 0, + error: + convertToApiError(error as AxiosError) ?? null, + }; + } +} + +async function fetchEntityData( + filters: K8sDetailsFilters, + signal?: AbortSignal, +): ReturnType< + K8sEntityConfig< + InframonitoringtypesDaemonSetRecordDTO, + SelectedItemParams + >['details']['fetchEntityData'] +> { + try { + const response = await listDaemonSets( + { + filter: { expression: filters.filter.expression }, + start: filters.start, + end: filters.end, + limit: 1, + offset: 0, + }, + signal, + ); + + return { + data: response.data.records.length > 0 ? response.data.records[0] : null, + }; + } catch (error) { + return { + data: null, + error: + convertToApiError(error as AxiosError) ?? null, + }; + } +} + +export const daemonSetEntityConfig: K8sEntityConfig< + InframonitoringtypesDaemonSetRecordDTO, + SelectedItemParams +> = { + list: { + entity: InfraMonitoringEntity.DAEMONSETS, + eventCategory: InfraMonitoringEvents.DaemonSet, + tableColumns: k8sDaemonSetsColumnsConfig, + fetchListData, + getRowKey: getK8sDaemonSetRowKey, + getItemKey: getK8sDaemonSetItemKey, + detailsQueryKeyPrefix: 'daemonset', + }, + details: { + category: InfraMonitoringEntity.DAEMONSETS, + eventCategory: InfraMonitoringEvents.DaemonSet, + queryKeyPrefix: 'daemonset', + getSelectedItemExpression: k8sDaemonSetGetSelectedItemExpression, + fetchEntityData, + getEntityName: k8sDaemonSetGetEntityName, + getInitialLogTracesExpression: k8sDaemonSetInitialLogTracesExpression, + getInitialEventsExpression: k8sDaemonSetInitialEventsExpression, + metadataConfig: k8sDaemonSetDetailsMetadataConfig, + entityWidgetInfo: daemonSetWidgetInfo, + getEntityQueryPayload: getDaemonSetMetricsQueryPayload, + customTabs: [ + createPodMetricsTab({ + getQueryPayload: getDaemonSetPodMetricsQueryPayload, + category: InfraMonitoringEntity.DAEMONSETS, + queryKey: 'daemonSetPodMetrics', + docBasePath: '/infrastructure-monitoring/kubernetes/daemonsets/', + }), + ], + }, +}; diff --git a/frontend/src/container/InfraMonitoringK8sV2/DaemonSets/table.config.tsx b/frontend/src/container/InfraMonitoringK8sV2/DaemonSets/table.config.tsx index 21ddab5485d..87bb5b4f327 100644 --- a/frontend/src/container/InfraMonitoringK8sV2/DaemonSets/table.config.tsx +++ b/frontend/src/container/InfraMonitoringK8sV2/DaemonSets/table.config.tsx @@ -121,12 +121,17 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [ width: { min: 250 }, enableSort: false, enableResize: true, - cell: ({ row }): React.ReactNode => { + cell: ({ row, rowId }): React.ReactNode => { const podCountsByStatus = row.podCountsByStatus; if (!podCountsByStatus) { return -; } - return ; + return ( + + ); }, }, { @@ -140,8 +145,9 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [ width: { min: 210 }, enableSort: false, enableResize: true, - cell: ({ row }): React.ReactNode => ( + cell: ({ row, rowId }): React.ReactNode => ( { - try { - const response = await listDeployments( - { - filter: { expression: filters.filter.expression }, - groupBy: filters.groupBy?.map((g) => ({ name: g.name })), - offset: filters.offset, - limit: filters.limit ?? 10, - start: filters.start, - end: filters.end, - orderBy: filters.orderBy - ? { - key: { name: filters.orderBy.key.name }, - direction: - filters.orderBy.direction === 'asc' - ? Querybuildertypesv5OrderDirectionDTO.asc - : Querybuildertypesv5OrderDirectionDTO.desc, - } - : undefined, - }, - signal, - ); - - const data = response.data; - return { - type: - data.type === InframonitoringtypesResponseTypeDTO.grouped_list - ? ('grouped_list' as const) - : ('list' as const), - records: data.records, - total: data.total, - endTimeBeforeRetention: data.endTimeBeforeRetention, - warning: data.warning, - }; - } catch (error) { - return { - type: 'list' as const, - records: [] as InframonitoringtypesDeploymentRecordDTO[], - total: 0, - error: - convertToApiError(error as AxiosError) ?? null, - }; - } - }, - [], - ); - - const fetchEntityData = useCallback( - async ( - filters: K8sDetailsFilters, - signal?: AbortSignal, - ): Promise<{ - data: InframonitoringtypesDeploymentRecordDTO | null; - error?: APIError | null; - }> => { - try { - const response = await listDeployments( - { - filter: { expression: filters.filter.expression }, - start: filters.start, - end: filters.end, - limit: 1, - offset: 0, - }, - signal, - ); - - return { - data: response.data.records.length > 0 ? response.data.records[0] : null, - }; - } catch (error) { - return { - data: null, - error: - convertToApiError(error as AxiosError) ?? null, - }; - } - }, - [], - ); - - const customTabs = useMemo( - () => [ - createPodMetricsTab({ - getQueryPayload: getDeploymentPodMetricsQueryPayload, - category: InfraMonitoringEntity.DEPLOYMENTS, - queryKey: 'deploymentPodMetrics', - }), - ], - [], - ); - - return ( - <> - - controlListPrefix={controlListPrefix} - entity={InfraMonitoringEntity.DEPLOYMENTS} - tableColumns={k8sDeploymentsColumnsConfig} - fetchListData={fetchListData} - getRowKey={getK8sDeploymentRowKey} - getItemKey={getK8sDeploymentItemKey} - eventCategory={InfraMonitoringEvents.Deployment} - /> - - - category={InfraMonitoringEntity.DEPLOYMENTS} - eventCategory={InfraMonitoringEvents.Deployment} - getSelectedItemExpression={k8sDeploymentGetSelectedItemExpression} - fetchEntityData={fetchEntityData} - getEntityName={k8sDeploymentGetEntityName} - getInitialLogTracesExpression={k8sDeploymentInitialLogTracesExpression} - getInitialEventsExpression={k8sDeploymentInitialEventsExpression} - metadataConfig={k8sDeploymentDetailsMetadataConfig} - entityWidgetInfo={deploymentWidgetInfo} - getEntityQueryPayload={getDeploymentMetricsQueryPayload} - queryKeyPrefix="deployment" - customTabs={customTabs} - /> - - ); -} - -export default K8sDeploymentsList; diff --git a/frontend/src/container/InfraMonitoringK8sV2/Deployments/entity.config.ts b/frontend/src/container/InfraMonitoringK8sV2/Deployments/entity.config.ts new file mode 100644 index 00000000000..e395aebf0d2 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8sV2/Deployments/entity.config.ts @@ -0,0 +1,161 @@ +import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs'; +import { listDeployments } from 'api/generated/services/inframonitoring'; +import { + InframonitoringtypesDeploymentRecordDTO, + InframonitoringtypesResponseTypeDTO, + Querybuildertypesv5OrderDirectionDTO, + RenderErrorResponseDTO, +} from 'api/generated/services/sigNoz.schemas'; +import { AxiosError } from 'axios'; +import { InfraMonitoringEvents } from 'constants/events'; + +import { K8sEntityConfig } from '../Base/entity.config.types'; +import { + K8sBaseFilters, + K8sDetailsCustomTab, + K8sDetailsFilters, +} from '../Base/types'; +import { InfraMonitoringEntity } from '../constants'; +import { createPodMetricsTab } from '../EntityDetailsUtils/createPodMetricsTab'; +import { SelectedItemParams } from '../hooks'; +import { + deploymentWidgetInfo, + getDeploymentMetricsQueryPayload, + getDeploymentPodMetricsQueryPayload, + k8sDeploymentDetailsMetadataConfig, + k8sDeploymentGetEntityName, + k8sDeploymentGetSelectedItemExpression, + k8sDeploymentInitialEventsExpression, + k8sDeploymentInitialLogTracesExpression, +} from './constants'; +import { + getK8sDeploymentItemKey, + getK8sDeploymentRowKey, + k8sDeploymentsColumnsConfig, +} from './table.config'; + +async function fetchListData( + filters: K8sBaseFilters, + signal?: AbortSignal, +): ReturnType< + K8sEntityConfig< + InframonitoringtypesDeploymentRecordDTO, + SelectedItemParams + >['list']['fetchListData'] +> { + try { + const response = await listDeployments( + { + filter: { expression: filters.filter.expression }, + groupBy: filters.groupBy?.map((g) => ({ name: g.name })), + offset: filters.offset, + limit: filters.limit ?? 10, + start: filters.start, + end: filters.end, + orderBy: filters.orderBy + ? { + key: { name: filters.orderBy.key.name }, + direction: + filters.orderBy.direction === 'asc' + ? Querybuildertypesv5OrderDirectionDTO.asc + : Querybuildertypesv5OrderDirectionDTO.desc, + } + : undefined, + }, + signal, + ); + + const data = response.data; + return { + type: + data.type === InframonitoringtypesResponseTypeDTO.grouped_list + ? ('grouped_list' as const) + : ('list' as const), + records: data.records, + total: data.total, + endTimeBeforeRetention: data.endTimeBeforeRetention, + warning: data.warning, + }; + } catch (error) { + return { + type: 'list' as const, + records: [] as InframonitoringtypesDeploymentRecordDTO[], + total: 0, + error: + convertToApiError(error as AxiosError) ?? null, + }; + } +} + +async function fetchEntityData( + filters: K8sDetailsFilters, + signal?: AbortSignal, +): ReturnType< + K8sEntityConfig< + InframonitoringtypesDeploymentRecordDTO, + SelectedItemParams + >['details']['fetchEntityData'] +> { + try { + const response = await listDeployments( + { + filter: { expression: filters.filter.expression }, + start: filters.start, + end: filters.end, + limit: 1, + offset: 0, + }, + signal, + ); + + return { + data: response.data.records.length > 0 ? response.data.records[0] : null, + }; + } catch (error) { + return { + data: null, + error: + convertToApiError(error as AxiosError) ?? null, + }; + } +} + +export function createCustomTabs(): K8sDetailsCustomTab[] { + return [ + createPodMetricsTab({ + getQueryPayload: getDeploymentPodMetricsQueryPayload, + category: InfraMonitoringEntity.DEPLOYMENTS, + queryKey: 'deploymentPodMetrics', + docBasePath: '/infrastructure-monitoring/kubernetes/deployments/', + }), + ]; +} + +export const deploymentEntityConfig: K8sEntityConfig< + InframonitoringtypesDeploymentRecordDTO, + SelectedItemParams +> = { + list: { + entity: InfraMonitoringEntity.DEPLOYMENTS, + eventCategory: InfraMonitoringEvents.Deployment, + tableColumns: k8sDeploymentsColumnsConfig, + fetchListData, + getRowKey: getK8sDeploymentRowKey, + getItemKey: getK8sDeploymentItemKey, + detailsQueryKeyPrefix: 'deployment', + }, + details: { + category: InfraMonitoringEntity.DEPLOYMENTS, + eventCategory: InfraMonitoringEvents.Deployment, + queryKeyPrefix: 'deployment', + getSelectedItemExpression: k8sDeploymentGetSelectedItemExpression, + fetchEntityData, + getEntityName: k8sDeploymentGetEntityName, + getInitialLogTracesExpression: k8sDeploymentInitialLogTracesExpression, + getInitialEventsExpression: k8sDeploymentInitialEventsExpression, + metadataConfig: k8sDeploymentDetailsMetadataConfig, + entityWidgetInfo: deploymentWidgetInfo, + getEntityQueryPayload: getDeploymentMetricsQueryPayload, + customTabs: createCustomTabs(), + }, +}; diff --git a/frontend/src/container/InfraMonitoringK8sV2/Deployments/table.config.tsx b/frontend/src/container/InfraMonitoringK8sV2/Deployments/table.config.tsx index 6c2b6638f25..514c46e52d7 100644 --- a/frontend/src/container/InfraMonitoringK8sV2/Deployments/table.config.tsx +++ b/frontend/src/container/InfraMonitoringK8sV2/Deployments/table.config.tsx @@ -118,12 +118,17 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef { + cell: ({ row, rowId }): React.ReactNode => { const podCountsByStatus = row.podCountsByStatus; if (!podCountsByStatus) { return -; } - return ; + return ( + + ); }, }, { @@ -137,8 +142,9 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef ( + cell: ({ row, rowId }): React.ReactNode => ( { ) => GetQueryResultsProps[]; queryKey: string; category: InfraMonitoringEntity; + docBasePath?: string; } export function createPodMetricsTab({ getQueryPayload, queryKey, category, + docBasePath, }: CreatePodMetricsTabParams): CustomTab { const eventEntity = categoryToEventEntity[category]; + const widgetInfo = docBasePath + ? podUtilizationByPodWidgetInfo.map((widget) => ({ + ...widget, + docPath: widget.docPath ? `${docBasePath}${widget.docPath}` : undefined, + })) + : podUtilizationByPodWidgetInfo; + return { key: VIEW_TYPES.POD_METRICS, label: 'Pod Metrics', @@ -51,7 +60,7 @@ export function createPodMetricsTab({ {showFilters && (
-
-
- - Viewing · Resource - -
- - - -
-
-
- {categories.map((category) => ( - - ))} + + <> +
+
+ + Viewing · Resource + +
+ + + +
+
+
+ {categories.map((category) => ( + + ))} +
+
-
-
-
-
- - Filter by - -
-
- {selectedCategoryConfig && ( - - )} -
+
+
+ + Filter by + +
+
+ {selectedCategoryConfig && ( + + )} +
+ +
)} @@ -312,41 +309,7 @@ export default function InfraMonitoringK8s(): JSX.Element { showFilters ? styles.listContainerFiltersVisible : '' }`} > - {selectedCategory === K8sCategories.PODS && ( - - )} - - {selectedCategory === K8sCategories.NODES && ( - - )} - - {selectedCategory === K8sCategories.CLUSTERS && ( - - )} - - {selectedCategory === K8sCategories.DEPLOYMENTS && ( - - )} - - {selectedCategory === K8sCategories.NAMESPACES && ( - - )} - - {selectedCategory === K8sCategories.STATEFULSETS && ( - - )} - - {selectedCategory === K8sCategories.JOBS && ( - - )} - - {selectedCategory === K8sCategories.DAEMONSETS && ( - - )} - - {selectedCategory === K8sCategories.VOLUMES && ( - - )} +
diff --git a/frontend/src/container/InfraMonitoringK8sV2/Jobs/K8sJobsList.tsx b/frontend/src/container/InfraMonitoringK8sV2/Jobs/K8sJobsList.tsx deleted file mode 100644 index 4668b93319f..00000000000 --- a/frontend/src/container/InfraMonitoringK8sV2/Jobs/K8sJobsList.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { useCallback, useMemo } from 'react'; -import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs'; -import { listJobs } from 'api/generated/services/inframonitoring'; -import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas'; -import { AxiosError } from 'axios'; -import { - InframonitoringtypesJobRecordDTO, - InframonitoringtypesResponseTypeDTO, - Querybuildertypesv5OrderDirectionDTO, -} from 'api/generated/services/sigNoz.schemas'; -import { InfraMonitoringEvents } from 'constants/events'; -import APIError from 'types/api/error'; - -import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails'; -import { K8sBaseList } from '../Base/K8sBaseList'; -import { K8sBaseFilters } from '../Base/types'; -import { InfraMonitoringEntity } from '../constants'; -import { SelectedItemParams } from '../hooks'; -import { - getJobMetricsQueryPayload, - getJobPodMetricsQueryPayload, - jobWidgetInfo, - k8sJobDetailsMetadataConfig, - k8sJobGetEntityName, - k8sJobGetSelectedItemExpression, - k8sJobInitialEventsExpression, - k8sJobInitialLogTracesExpression, -} from './constants'; -import { - getK8sJobItemKey, - getK8sJobRowKey, - k8sJobsColumnsConfig, -} from './table.config'; -import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab'; - -function K8sJobsList({ - controlListPrefix, -}: { - controlListPrefix?: React.ReactNode; -}): JSX.Element { - const fetchListData = useCallback( - async (filters: K8sBaseFilters, signal?: AbortSignal) => { - try { - const response = await listJobs( - { - filter: { expression: filters.filter.expression }, - groupBy: filters.groupBy?.map((g) => ({ name: g.name })), - offset: filters.offset, - limit: filters.limit ?? 10, - start: filters.start, - end: filters.end, - orderBy: filters.orderBy - ? { - key: { name: filters.orderBy.key.name }, - direction: - filters.orderBy.direction === 'asc' - ? Querybuildertypesv5OrderDirectionDTO.asc - : Querybuildertypesv5OrderDirectionDTO.desc, - } - : undefined, - }, - signal, - ); - - const data = response.data; - return { - type: - data.type === InframonitoringtypesResponseTypeDTO.grouped_list - ? ('grouped_list' as const) - : ('list' as const), - records: data.records, - total: data.total, - endTimeBeforeRetention: data.endTimeBeforeRetention, - warning: data.warning, - }; - } catch (error) { - return { - type: 'list' as const, - records: [] as InframonitoringtypesJobRecordDTO[], - total: 0, - error: - convertToApiError(error as AxiosError) ?? null, - }; - } - }, - [], - ); - - const fetchEntityData = useCallback( - async ( - filters: K8sDetailsFilters, - signal?: AbortSignal, - ): Promise<{ - data: InframonitoringtypesJobRecordDTO | null; - error?: APIError | null; - }> => { - try { - const response = await listJobs( - { - filter: { expression: filters.filter.expression }, - start: filters.start, - end: filters.end, - limit: 1, - offset: 0, - }, - signal, - ); - - return { - data: response.data.records.length > 0 ? response.data.records[0] : null, - }; - } catch (error) { - return { - data: null, - error: - convertToApiError(error as AxiosError) ?? null, - }; - } - }, - [], - ); - - const customTabs = useMemo( - () => [ - createPodMetricsTab({ - getQueryPayload: getJobPodMetricsQueryPayload, - category: InfraMonitoringEntity.JOBS, - queryKey: 'jobPodMetrics', - }), - ], - [], - ); - - return ( - <> - - controlListPrefix={controlListPrefix} - entity={InfraMonitoringEntity.JOBS} - tableColumns={k8sJobsColumnsConfig} - fetchListData={fetchListData} - getRowKey={getK8sJobRowKey} - getItemKey={getK8sJobItemKey} - eventCategory={InfraMonitoringEvents.Job} - /> - - - category={InfraMonitoringEntity.JOBS} - eventCategory={InfraMonitoringEvents.Job} - getSelectedItemExpression={k8sJobGetSelectedItemExpression} - fetchEntityData={fetchEntityData} - getEntityName={k8sJobGetEntityName} - getInitialLogTracesExpression={k8sJobInitialLogTracesExpression} - getInitialEventsExpression={k8sJobInitialEventsExpression} - metadataConfig={k8sJobDetailsMetadataConfig} - entityWidgetInfo={jobWidgetInfo} - getEntityQueryPayload={getJobMetricsQueryPayload} - queryKeyPrefix="job" - customTabs={customTabs} - /> - - ); -} - -export default K8sJobsList; diff --git a/frontend/src/container/InfraMonitoringK8sV2/Jobs/entity.config.ts b/frontend/src/container/InfraMonitoringK8sV2/Jobs/entity.config.ts new file mode 100644 index 00000000000..154978dfaea --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8sV2/Jobs/entity.config.ts @@ -0,0 +1,153 @@ +import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs'; +import { listJobs } from 'api/generated/services/inframonitoring'; +import { + InframonitoringtypesJobRecordDTO, + InframonitoringtypesResponseTypeDTO, + Querybuildertypesv5OrderDirectionDTO, + RenderErrorResponseDTO, +} from 'api/generated/services/sigNoz.schemas'; +import { AxiosError } from 'axios'; +import { InfraMonitoringEvents } from 'constants/events'; + +import { K8sEntityConfig } from '../Base/entity.config.types'; +import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types'; +import { InfraMonitoringEntity } from '../constants'; +import { createPodMetricsTab } from '../EntityDetailsUtils/createPodMetricsTab'; +import { SelectedItemParams } from '../hooks'; +import { + getJobMetricsQueryPayload, + getJobPodMetricsQueryPayload, + jobWidgetInfo, + k8sJobDetailsMetadataConfig, + k8sJobGetEntityName, + k8sJobGetSelectedItemExpression, + k8sJobInitialEventsExpression, + k8sJobInitialLogTracesExpression, +} from './constants'; +import { + getK8sJobItemKey, + getK8sJobRowKey, + k8sJobsColumnsConfig, +} from './table.config'; + +async function fetchListData( + filters: K8sBaseFilters, + signal?: AbortSignal, +): ReturnType< + K8sEntityConfig< + InframonitoringtypesJobRecordDTO, + SelectedItemParams + >['list']['fetchListData'] +> { + try { + const response = await listJobs( + { + filter: { expression: filters.filter.expression }, + groupBy: filters.groupBy?.map((g) => ({ name: g.name })), + offset: filters.offset, + limit: filters.limit ?? 10, + start: filters.start, + end: filters.end, + orderBy: filters.orderBy + ? { + key: { name: filters.orderBy.key.name }, + direction: + filters.orderBy.direction === 'asc' + ? Querybuildertypesv5OrderDirectionDTO.asc + : Querybuildertypesv5OrderDirectionDTO.desc, + } + : undefined, + }, + signal, + ); + + const data = response.data; + return { + type: + data.type === InframonitoringtypesResponseTypeDTO.grouped_list + ? ('grouped_list' as const) + : ('list' as const), + records: data.records, + total: data.total, + endTimeBeforeRetention: data.endTimeBeforeRetention, + warning: data.warning, + }; + } catch (error) { + return { + type: 'list' as const, + records: [] as InframonitoringtypesJobRecordDTO[], + total: 0, + error: + convertToApiError(error as AxiosError) ?? null, + }; + } +} + +async function fetchEntityData( + filters: K8sDetailsFilters, + signal?: AbortSignal, +): ReturnType< + K8sEntityConfig< + InframonitoringtypesJobRecordDTO, + SelectedItemParams + >['details']['fetchEntityData'] +> { + try { + const response = await listJobs( + { + filter: { expression: filters.filter.expression }, + start: filters.start, + end: filters.end, + limit: 1, + offset: 0, + }, + signal, + ); + + return { + data: response.data.records.length > 0 ? response.data.records[0] : null, + }; + } catch (error) { + return { + data: null, + error: + convertToApiError(error as AxiosError) ?? null, + }; + } +} + +export const jobEntityConfig: K8sEntityConfig< + InframonitoringtypesJobRecordDTO, + SelectedItemParams +> = { + list: { + entity: InfraMonitoringEntity.JOBS, + eventCategory: InfraMonitoringEvents.Job, + tableColumns: k8sJobsColumnsConfig, + fetchListData, + getRowKey: getK8sJobRowKey, + getItemKey: getK8sJobItemKey, + detailsQueryKeyPrefix: 'job', + }, + details: { + category: InfraMonitoringEntity.JOBS, + eventCategory: InfraMonitoringEvents.Job, + queryKeyPrefix: 'job', + getSelectedItemExpression: k8sJobGetSelectedItemExpression, + fetchEntityData, + getEntityName: k8sJobGetEntityName, + getInitialLogTracesExpression: k8sJobInitialLogTracesExpression, + getInitialEventsExpression: k8sJobInitialEventsExpression, + metadataConfig: k8sJobDetailsMetadataConfig, + entityWidgetInfo: jobWidgetInfo, + getEntityQueryPayload: getJobMetricsQueryPayload, + customTabs: [ + createPodMetricsTab({ + getQueryPayload: getJobPodMetricsQueryPayload, + category: InfraMonitoringEntity.JOBS, + queryKey: 'jobPodMetrics', + docBasePath: '/infrastructure-monitoring/kubernetes/jobs/', + }), + ], + }, +}; diff --git a/frontend/src/container/InfraMonitoringK8sV2/Jobs/table.config.tsx b/frontend/src/container/InfraMonitoringK8sV2/Jobs/table.config.tsx index 5d603f5f3c3..659a756205b 100644 --- a/frontend/src/container/InfraMonitoringK8sV2/Jobs/table.config.tsx +++ b/frontend/src/container/InfraMonitoringK8sV2/Jobs/table.config.tsx @@ -113,12 +113,17 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [ width: { min: 250 }, enableSort: false, enableResize: true, - cell: ({ row }): React.ReactNode => { + cell: ({ row, rowId }): React.ReactNode => { const podCountsByStatus = row.podCountsByStatus; if (!podCountsByStatus) { return -; } - return ; + return ( + + ); }, }, { @@ -132,7 +137,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [ width: { min: 210 }, enableSort: false, enableResize: true, - cell: ({ row }): React.ReactNode => ( + cell: ({ row, rowId }): React.ReactNode => ( ), }, diff --git a/frontend/src/container/InfraMonitoringK8sV2/Namespaces/K8sNamespacesList.tsx b/frontend/src/container/InfraMonitoringK8sV2/Namespaces/K8sNamespacesList.tsx deleted file mode 100644 index ff911eab2b4..00000000000 --- a/frontend/src/container/InfraMonitoringK8sV2/Namespaces/K8sNamespacesList.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { useCallback, useMemo } from 'react'; -import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs'; -import { listNamespaces } from 'api/generated/services/inframonitoring'; -import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas'; -import { AxiosError } from 'axios'; -import { - InframonitoringtypesNamespaceRecordDTO, - InframonitoringtypesResponseTypeDTO, - Querybuildertypesv5OrderDirectionDTO, -} from 'api/generated/services/sigNoz.schemas'; -import { InfraMonitoringEvents } from 'constants/events'; -import APIError from 'types/api/error'; - -import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails'; -import { K8sBaseList } from '../Base/K8sBaseList'; -import { K8sBaseFilters } from '../Base/types'; -import { InfraMonitoringEntity } from '../constants'; -import { SelectedItemParams } from '../hooks'; -import { - getNamespaceMetricsQueryPayload, - getNamespacePodMetricsQueryPayload, - k8sNamespaceDetailsCountsConfig, - k8sNamespaceDetailsMetadataConfig, - k8sNamespaceGetCountsFilterExpression, - k8sNamespaceGetEntityName, - k8sNamespaceGetSelectedItemExpression, - k8sNamespaceInitialEventsExpression, - k8sNamespaceInitialLogTracesExpression, - namespaceWidgetInfo, -} from './constants'; -import { - getK8sNamespaceItemKey, - getK8sNamespaceRowKey, - k8sNamespacesColumnsConfig, -} from './table.config'; -import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab'; - -function K8sNamespacesList({ - controlListPrefix, -}: { - controlListPrefix?: React.ReactNode; -}): JSX.Element { - const fetchListData = useCallback( - async (filters: K8sBaseFilters, signal?: AbortSignal) => { - try { - const response = await listNamespaces( - { - filter: { expression: filters.filter.expression }, - groupBy: filters.groupBy?.map((g) => ({ name: g.name })), - offset: filters.offset, - limit: filters.limit ?? 10, - start: filters.start, - end: filters.end, - orderBy: filters.orderBy - ? { - key: { name: filters.orderBy.key.name }, - direction: - filters.orderBy.direction === 'asc' - ? Querybuildertypesv5OrderDirectionDTO.asc - : Querybuildertypesv5OrderDirectionDTO.desc, - } - : undefined, - }, - signal, - ); - - const data = response.data; - return { - type: - data.type === InframonitoringtypesResponseTypeDTO.grouped_list - ? ('grouped_list' as const) - : ('list' as const), - records: data.records, - total: data.total, - endTimeBeforeRetention: data.endTimeBeforeRetention, - warning: data.warning, - }; - } catch (error) { - return { - type: 'list' as const, - records: [] as InframonitoringtypesNamespaceRecordDTO[], - total: 0, - error: - convertToApiError(error as AxiosError) ?? null, - }; - } - }, - [], - ); - - const fetchEntityData = useCallback( - async ( - filters: K8sDetailsFilters, - signal?: AbortSignal, - ): Promise<{ - data: InframonitoringtypesNamespaceRecordDTO | null; - error?: APIError | null; - }> => { - try { - const response = await listNamespaces( - { - filter: { expression: filters.filter.expression }, - start: filters.start, - end: filters.end, - limit: 1, - offset: 0, - }, - signal, - ); - - return { - data: response.data.records.length > 0 ? response.data.records[0] : null, - }; - } catch (error) { - return { - data: null, - error: - convertToApiError(error as AxiosError) ?? null, - }; - } - }, - [], - ); - - const customTabs = useMemo( - () => [ - createPodMetricsTab({ - getQueryPayload: getNamespacePodMetricsQueryPayload, - category: InfraMonitoringEntity.NAMESPACES, - queryKey: 'namespacePodMetrics', - }), - ], - [], - ); - - return ( - <> - - controlListPrefix={controlListPrefix} - entity={InfraMonitoringEntity.NAMESPACES} - tableColumns={k8sNamespacesColumnsConfig} - fetchListData={fetchListData} - getRowKey={getK8sNamespaceRowKey} - getItemKey={getK8sNamespaceItemKey} - eventCategory={InfraMonitoringEvents.Namespace} - /> - - - category={InfraMonitoringEntity.NAMESPACES} - eventCategory={InfraMonitoringEvents.Namespace} - getSelectedItemExpression={k8sNamespaceGetSelectedItemExpression} - fetchEntityData={fetchEntityData} - getEntityName={k8sNamespaceGetEntityName} - getInitialLogTracesExpression={k8sNamespaceInitialLogTracesExpression} - getInitialEventsExpression={k8sNamespaceInitialEventsExpression} - metadataConfig={k8sNamespaceDetailsMetadataConfig} - countsConfig={k8sNamespaceDetailsCountsConfig} - getCountsFilterExpression={k8sNamespaceGetCountsFilterExpression} - entityWidgetInfo={namespaceWidgetInfo} - getEntityQueryPayload={getNamespaceMetricsQueryPayload} - queryKeyPrefix="namespace" - customTabs={customTabs} - /> - - ); -} - -export default K8sNamespacesList; diff --git a/frontend/src/container/InfraMonitoringK8sV2/Namespaces/entity.config.ts b/frontend/src/container/InfraMonitoringK8sV2/Namespaces/entity.config.ts new file mode 100644 index 00000000000..a71e5976047 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8sV2/Namespaces/entity.config.ts @@ -0,0 +1,159 @@ +import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs'; +import { listNamespaces } from 'api/generated/services/inframonitoring'; +import { + InframonitoringtypesNamespaceRecordDTO, + InframonitoringtypesResponseTypeDTO, + Querybuildertypesv5OrderDirectionDTO, + RenderErrorResponseDTO, +} from 'api/generated/services/sigNoz.schemas'; +import { AxiosError } from 'axios'; +import { InfraMonitoringEvents } from 'constants/events'; +import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab'; + +import { K8sEntityConfig } from '../Base/entity.config.types'; +import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types'; +import { InfraMonitoringEntity } from '../constants'; +import { SelectedItemParams } from '../hooks'; +import { + getNamespaceMetricsQueryPayload, + getNamespacePodMetricsQueryPayload, + k8sNamespaceDetailsCountsConfig, + k8sNamespaceDetailsMetadataConfig, + k8sNamespaceGetCountsFilterExpression, + k8sNamespaceGetEntityName, + k8sNamespaceGetSelectedItemExpression, + k8sNamespaceInitialEventsExpression, + k8sNamespaceInitialLogTracesExpression, + namespaceWidgetInfo, +} from './constants'; +import { + getK8sNamespaceItemKey, + getK8sNamespaceRowKey, + k8sNamespacesColumnsConfig, +} from './table.config'; + +async function fetchListData( + filters: K8sBaseFilters, + signal?: AbortSignal, +): ReturnType< + K8sEntityConfig< + InframonitoringtypesNamespaceRecordDTO, + SelectedItemParams + >['list']['fetchListData'] +> { + try { + const response = await listNamespaces( + { + filter: { expression: filters.filter.expression }, + groupBy: filters.groupBy?.map((g) => ({ name: g.name })), + offset: filters.offset, + limit: filters.limit ?? 10, + start: filters.start, + end: filters.end, + orderBy: filters.orderBy + ? { + key: { name: filters.orderBy.key.name }, + direction: + filters.orderBy.direction === 'asc' + ? Querybuildertypesv5OrderDirectionDTO.asc + : Querybuildertypesv5OrderDirectionDTO.desc, + } + : undefined, + }, + signal, + ); + + const data = response.data; + return { + type: + data.type === InframonitoringtypesResponseTypeDTO.grouped_list + ? ('grouped_list' as const) + : ('list' as const), + records: data.records, + total: data.total, + endTimeBeforeRetention: data.endTimeBeforeRetention, + warning: data.warning, + }; + } catch (error) { + return { + type: 'list' as const, + records: [] as InframonitoringtypesNamespaceRecordDTO[], + total: 0, + error: + convertToApiError(error as AxiosError) ?? null, + }; + } +} + +async function fetchEntityData( + filters: K8sDetailsFilters, + signal?: AbortSignal, +): ReturnType< + K8sEntityConfig< + InframonitoringtypesNamespaceRecordDTO, + SelectedItemParams + >['details']['fetchEntityData'] +> { + try { + const response = await listNamespaces( + { + filter: { expression: filters.filter.expression }, + start: filters.start, + end: filters.end, + limit: 1, + offset: 0, + }, + signal, + ); + + return { + data: response.data.records.length > 0 ? response.data.records[0] : null, + }; + } catch (error) { + return { + data: null, + error: + convertToApiError(error as AxiosError) ?? null, + }; + } +} + +const namespaceCustomTabs = [ + createPodMetricsTab({ + getQueryPayload: getNamespacePodMetricsQueryPayload, + category: InfraMonitoringEntity.NAMESPACES, + queryKey: 'namespacePodMetrics', + docBasePath: '/infrastructure-monitoring/kubernetes/namespaces/', + }), +]; + +export const namespaceEntityConfig: K8sEntityConfig< + InframonitoringtypesNamespaceRecordDTO, + SelectedItemParams +> = { + list: { + entity: InfraMonitoringEntity.NAMESPACES, + eventCategory: InfraMonitoringEvents.Namespace, + tableColumns: k8sNamespacesColumnsConfig, + fetchListData, + getRowKey: getK8sNamespaceRowKey, + getItemKey: getK8sNamespaceItemKey, + detailsQueryKeyPrefix: 'namespace', + }, + details: { + category: InfraMonitoringEntity.NAMESPACES, + eventCategory: InfraMonitoringEvents.Namespace, + queryKeyPrefix: 'namespace', + getSelectedItemExpression: k8sNamespaceGetSelectedItemExpression, + fetchEntityData, + getEntityName: k8sNamespaceGetEntityName, + getInitialLogTracesExpression: k8sNamespaceInitialLogTracesExpression, + getInitialEventsExpression: k8sNamespaceInitialEventsExpression, + metadataConfig: k8sNamespaceDetailsMetadataConfig, + countsConfig: k8sNamespaceDetailsCountsConfig, + getCountsFilterExpression: k8sNamespaceGetCountsFilterExpression, + entityWidgetInfo: namespaceWidgetInfo, + getEntityQueryPayload: getNamespaceMetricsQueryPayload, + customTabs: namespaceCustomTabs, + }, +}; diff --git a/frontend/src/container/InfraMonitoringK8sV2/Namespaces/table.config.tsx b/frontend/src/container/InfraMonitoringK8sV2/Namespaces/table.config.tsx index 8cb27505387..ca39164ee25 100644 --- a/frontend/src/container/InfraMonitoringK8sV2/Namespaces/table.config.tsx +++ b/frontend/src/container/InfraMonitoringK8sV2/Namespaces/table.config.tsx @@ -114,13 +114,16 @@ export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [ row.podCountsByStatus, width: { min: 250 }, enableSort: false, - cell: ({ row }): React.ReactNode => { + cell: ({ row, rowId }): React.ReactNode => { const podCountsByStatus = row.podCountsByStatus; if (!podCountsByStatus) { return -; } return ( - + ); }, }, diff --git a/frontend/src/container/InfraMonitoringK8sV2/Nodes/K8sNodesList.tsx b/frontend/src/container/InfraMonitoringK8sV2/Nodes/K8sNodesList.tsx deleted file mode 100644 index cdfd0ae3492..00000000000 --- a/frontend/src/container/InfraMonitoringK8sV2/Nodes/K8sNodesList.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { useCallback } from 'react'; -import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs'; -import { listNodes } from 'api/generated/services/inframonitoring'; -import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas'; -import { AxiosError } from 'axios'; -import { - InframonitoringtypesNodeRecordDTO, - InframonitoringtypesResponseTypeDTO, - Querybuildertypesv5OrderDirectionDTO, -} from 'api/generated/services/sigNoz.schemas'; -import { InfraMonitoringEvents } from 'constants/events'; -import APIError from 'types/api/error'; - -import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails'; -import { K8sBaseList } from '../Base/K8sBaseList'; -import { K8sBaseFilters } from '../Base/types'; -import { InfraMonitoringEntity } from '../constants'; -import { - getNodeMetricsQueryPayload, - k8sNodeDetailsMetadataConfig, - k8sNodeGetEntityName, - k8sNodeGetSelectedItemExpression, - k8sNodeInitialEventsExpression, - k8sNodeInitialLogTracesExpression, - nodeWidgetInfo, -} from './constants'; -import { - getK8sNodeItemKey, - getK8sNodeRowKey, - k8sNodesColumnsConfig, -} from './table.config'; - -function K8sNodesList({ - controlListPrefix, -}: { - controlListPrefix?: React.ReactNode; -}): JSX.Element { - const fetchListData = useCallback( - async (filters: K8sBaseFilters, signal?: AbortSignal) => { - try { - const response = await listNodes( - { - filter: { expression: filters.filter.expression }, - groupBy: filters.groupBy?.map((g) => ({ name: g.name })), - offset: filters.offset, - limit: filters.limit ?? 10, - start: filters.start, - end: filters.end, - orderBy: filters.orderBy - ? { - key: { name: filters.orderBy.key.name }, - direction: - filters.orderBy.direction === 'asc' - ? Querybuildertypesv5OrderDirectionDTO.asc - : Querybuildertypesv5OrderDirectionDTO.desc, - } - : undefined, - }, - signal, - ); - - const data = response.data; - return { - type: - data.type === InframonitoringtypesResponseTypeDTO.grouped_list - ? ('grouped_list' as const) - : ('list' as const), - records: data.records, - total: data.total, - endTimeBeforeRetention: data.endTimeBeforeRetention, - warning: data.warning, - }; - } catch (error) { - return { - type: 'list' as const, - records: [] as InframonitoringtypesNodeRecordDTO[], - total: 0, - error: - convertToApiError(error as AxiosError) ?? null, - }; - } - }, - [], - ); - - const fetchEntityData = useCallback( - async ( - filters: K8sDetailsFilters, - signal?: AbortSignal, - ): Promise<{ - data: InframonitoringtypesNodeRecordDTO | null; - error?: APIError | null; - }> => { - try { - const response = await listNodes( - { - filter: { expression: filters.filter.expression }, - start: filters.start, - end: filters.end, - limit: 1, - offset: 0, - }, - signal, - ); - - return { - data: response.data.records.length > 0 ? response.data.records[0] : null, - }; - } catch (error) { - return { - data: null, - error: - convertToApiError(error as AxiosError) ?? null, - }; - } - }, - [], - ); - - return ( - <> - - controlListPrefix={controlListPrefix} - entity={InfraMonitoringEntity.NODES} - tableColumns={k8sNodesColumnsConfig} - fetchListData={fetchListData} - getRowKey={getK8sNodeRowKey} - getItemKey={getK8sNodeItemKey} - eventCategory={InfraMonitoringEvents.Node} - /> - - - category={InfraMonitoringEntity.NODES} - eventCategory={InfraMonitoringEvents.Node} - getSelectedItemExpression={k8sNodeGetSelectedItemExpression} - fetchEntityData={fetchEntityData} - getEntityName={k8sNodeGetEntityName} - getInitialLogTracesExpression={k8sNodeInitialLogTracesExpression} - getInitialEventsExpression={k8sNodeInitialEventsExpression} - metadataConfig={k8sNodeDetailsMetadataConfig} - entityWidgetInfo={nodeWidgetInfo} - getEntityQueryPayload={getNodeMetricsQueryPayload} - queryKeyPrefix="node" - /> - - ); -} - -export default K8sNodesList; diff --git a/frontend/src/container/InfraMonitoringK8sV2/Nodes/entity.config.ts b/frontend/src/container/InfraMonitoringK8sV2/Nodes/entity.config.ts new file mode 100644 index 00000000000..b41588656b8 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8sV2/Nodes/entity.config.ts @@ -0,0 +1,142 @@ +import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs'; +import { listNodes } from 'api/generated/services/inframonitoring'; +import { + InframonitoringtypesNodeRecordDTO, + InframonitoringtypesResponseTypeDTO, + Querybuildertypesv5OrderDirectionDTO, + RenderErrorResponseDTO, +} from 'api/generated/services/sigNoz.schemas'; +import { AxiosError } from 'axios'; +import { InfraMonitoringEvents } from 'constants/events'; + +import { K8sEntityConfig } from '../Base/entity.config.types'; +import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types'; +import { InfraMonitoringEntity } from '../constants'; +import { + getNodeMetricsQueryPayload, + k8sNodeDetailsMetadataConfig, + k8sNodeGetEntityName, + k8sNodeGetSelectedItemExpression, + k8sNodeInitialEventsExpression, + k8sNodeInitialLogTracesExpression, + nodeWidgetInfo, +} from './constants'; +import { + getK8sNodeItemKey, + getK8sNodeRowKey, + k8sNodesColumnsConfig, +} from './table.config'; + +async function fetchListData( + filters: K8sBaseFilters, + signal?: AbortSignal, +): ReturnType< + K8sEntityConfig< + InframonitoringtypesNodeRecordDTO, + string + >['list']['fetchListData'] +> { + try { + const response = await listNodes( + { + filter: { expression: filters.filter.expression }, + groupBy: filters.groupBy?.map((g) => ({ name: g.name })), + offset: filters.offset, + limit: filters.limit ?? 10, + start: filters.start, + end: filters.end, + orderBy: filters.orderBy + ? { + key: { name: filters.orderBy.key.name }, + direction: + filters.orderBy.direction === 'asc' + ? Querybuildertypesv5OrderDirectionDTO.asc + : Querybuildertypesv5OrderDirectionDTO.desc, + } + : undefined, + }, + signal, + ); + + const data = response.data; + return { + type: + data.type === InframonitoringtypesResponseTypeDTO.grouped_list + ? ('grouped_list' as const) + : ('list' as const), + records: data.records, + total: data.total, + endTimeBeforeRetention: data.endTimeBeforeRetention, + warning: data.warning, + }; + } catch (error) { + return { + type: 'list' as const, + records: [] as InframonitoringtypesNodeRecordDTO[], + total: 0, + error: + convertToApiError(error as AxiosError) ?? null, + }; + } +} + +async function fetchEntityData( + filters: K8sDetailsFilters, + signal?: AbortSignal, +): ReturnType< + K8sEntityConfig< + InframonitoringtypesNodeRecordDTO, + string + >['details']['fetchEntityData'] +> { + try { + const response = await listNodes( + { + filter: { expression: filters.filter.expression }, + start: filters.start, + end: filters.end, + limit: 1, + offset: 0, + }, + signal, + ); + + return { + data: response.data.records.length > 0 ? response.data.records[0] : null, + }; + } catch (error) { + return { + data: null, + error: + convertToApiError(error as AxiosError) ?? null, + }; + } +} + +export const nodeEntityConfig: K8sEntityConfig< + InframonitoringtypesNodeRecordDTO, + string +> = { + list: { + entity: InfraMonitoringEntity.NODES, + eventCategory: InfraMonitoringEvents.Node, + tableColumns: k8sNodesColumnsConfig, + fetchListData, + getRowKey: getK8sNodeRowKey, + getItemKey: getK8sNodeItemKey, + detailsQueryKeyPrefix: 'node', + }, + details: { + category: InfraMonitoringEntity.NODES, + eventCategory: InfraMonitoringEvents.Node, + queryKeyPrefix: 'node', + getSelectedItemExpression: k8sNodeGetSelectedItemExpression, + fetchEntityData, + getEntityName: k8sNodeGetEntityName, + getInitialLogTracesExpression: k8sNodeInitialLogTracesExpression, + getInitialEventsExpression: k8sNodeInitialEventsExpression, + metadataConfig: k8sNodeDetailsMetadataConfig, + entityWidgetInfo: nodeWidgetInfo, + getEntityQueryPayload: getNodeMetricsQueryPayload, + }, +}; diff --git a/frontend/src/container/InfraMonitoringK8sV2/Nodes/table.config.tsx b/frontend/src/container/InfraMonitoringK8sV2/Nodes/table.config.tsx index 65c1239ac8e..0f0408e8888 100644 --- a/frontend/src/container/InfraMonitoringK8sV2/Nodes/table.config.tsx +++ b/frontend/src/container/InfraMonitoringK8sV2/Nodes/table.config.tsx @@ -98,7 +98,7 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [ accessorFn: (row): string => row.condition, width: { min: 120 }, enableSort: false, - cell: ({ row, groupMeta }): React.ReactNode => { + cell: ({ row, groupMeta, rowId }): React.ReactNode => { if (!groupMeta) { const color = NODE_CONDITION_COLORS[row.condition] || NODE_CONDITION_COLORS.no_data; @@ -123,6 +123,7 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [ color: Color.BG_AMBER_500, }, ]} + rowId={rowId} /> ); }, @@ -138,13 +139,16 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [ row.podCountsByStatus, width: { min: 250 }, enableSort: false, - cell: ({ row }): React.ReactNode => { + cell: ({ row, rowId }): React.ReactNode => { const podCountsByStatus = row.podCountsByStatus; if (!podCountsByStatus) { return -; } return ( - + ); }, }, diff --git a/frontend/src/container/InfraMonitoringK8sV2/Pods/K8sPodLists.tsx b/frontend/src/container/InfraMonitoringK8sV2/Pods/K8sPodLists.tsx deleted file mode 100644 index d6c62b91165..00000000000 --- a/frontend/src/container/InfraMonitoringK8sV2/Pods/K8sPodLists.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { useCallback } from 'react'; -import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs'; -import { listPods } from 'api/generated/services/inframonitoring'; -import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas'; -import { AxiosError } from 'axios'; -import { - InframonitoringtypesPodRecordDTO, - InframonitoringtypesResponseTypeDTO, - Querybuildertypesv5OrderDirectionDTO, -} from 'api/generated/services/sigNoz.schemas'; -import { InfraMonitoringEvents } from 'constants/events'; -import APIError from 'types/api/error'; - -import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails'; -import { K8sBaseList } from '../Base/K8sBaseList'; -import { K8sBaseFilters } from '../Base/types'; -import { InfraMonitoringEntity } from '../constants'; -import { - getPodMetricsQueryPayload, - k8sPodDetailsMetadataConfig, - k8sPodGetEntityName, - k8sPodGetSelectedItemExpression, - k8sPodInitialEventsExpression, - k8sPodInitialLogTracesExpression, - podWidgetInfo, -} from './constants'; -import { - getK8sPodItemKey, - getK8sPodRowKey, - k8sPodColumnsConfig, -} from './table.config'; - -function K8sPodsList({ - controlListPrefix, -}: { - controlListPrefix?: React.ReactNode; -}): JSX.Element { - const fetchListData = useCallback( - async (filters: K8sBaseFilters, signal?: AbortSignal) => { - try { - const response = await listPods( - { - filter: { expression: filters.filter.expression }, - groupBy: filters.groupBy?.map((g) => ({ name: g.name })), - offset: filters.offset, - limit: filters.limit ?? 10, - start: filters.start, - end: filters.end, - orderBy: filters.orderBy - ? { - key: { name: filters.orderBy.key.name }, - direction: - filters.orderBy.direction === 'asc' - ? Querybuildertypesv5OrderDirectionDTO.asc - : Querybuildertypesv5OrderDirectionDTO.desc, - } - : undefined, - }, - signal, - ); - - const data = response.data; - return { - type: - data.type === InframonitoringtypesResponseTypeDTO.grouped_list - ? ('grouped_list' as const) - : ('list' as const), - records: data.records, - total: data.total, - endTimeBeforeRetention: data.endTimeBeforeRetention, - warning: data.warning, - }; - } catch (error) { - return { - type: 'list' as const, - records: [] as InframonitoringtypesPodRecordDTO[], - total: 0, - error: - convertToApiError(error as AxiosError) ?? null, - }; - } - }, - [], - ); - - const fetchEntityData = useCallback( - async ( - filters: K8sDetailsFilters, - signal?: AbortSignal, - ): Promise<{ - data: InframonitoringtypesPodRecordDTO | null; - error?: APIError | null; - }> => { - try { - const response = await listPods( - { - filter: { expression: filters.filter.expression }, - start: filters.start, - end: filters.end, - limit: 1, - offset: 0, - }, - signal, - ); - - return { - data: response.data.records.length > 0 ? response.data.records[0] : null, - }; - } catch (error) { - return { - data: null, - error: - convertToApiError(error as AxiosError) ?? null, - }; - } - }, - [], - ); - - return ( - <> - - controlListPrefix={controlListPrefix} - entity={InfraMonitoringEntity.PODS} - tableColumns={k8sPodColumnsConfig} - fetchListData={fetchListData} - getRowKey={getK8sPodRowKey} - getItemKey={getK8sPodItemKey} - eventCategory={InfraMonitoringEvents.Pod} - /> - - - category={InfraMonitoringEntity.PODS} - eventCategory={InfraMonitoringEvents.Pod} - getSelectedItemExpression={k8sPodGetSelectedItemExpression} - fetchEntityData={fetchEntityData} - getEntityName={k8sPodGetEntityName} - getInitialLogTracesExpression={k8sPodInitialLogTracesExpression} - getInitialEventsExpression={k8sPodInitialEventsExpression} - metadataConfig={k8sPodDetailsMetadataConfig} - entityWidgetInfo={podWidgetInfo} - getEntityQueryPayload={getPodMetricsQueryPayload} - queryKeyPrefix="pod" - /> - - ); -} - -export default K8sPodsList; diff --git a/frontend/src/container/InfraMonitoringK8sV2/Pods/entity.config.ts b/frontend/src/container/InfraMonitoringK8sV2/Pods/entity.config.ts new file mode 100644 index 00000000000..0161334a5d0 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8sV2/Pods/entity.config.ts @@ -0,0 +1,134 @@ +import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs'; +import { listPods } from 'api/generated/services/inframonitoring'; +import { + InframonitoringtypesPodRecordDTO, + InframonitoringtypesResponseTypeDTO, + Querybuildertypesv5OrderDirectionDTO, + RenderErrorResponseDTO, +} from 'api/generated/services/sigNoz.schemas'; +import { AxiosError } from 'axios'; +import { InfraMonitoringEvents } from 'constants/events'; + +import { K8sEntityConfig } from '../Base/entity.config.types'; +import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types'; +import { InfraMonitoringEntity } from '../constants'; +import { + getPodMetricsQueryPayload, + k8sPodDetailsMetadataConfig, + k8sPodGetEntityName, + k8sPodGetSelectedItemExpression, + k8sPodInitialEventsExpression, + k8sPodInitialLogTracesExpression, + podWidgetInfo, +} from './constants'; +import { + getK8sPodItemKey, + getK8sPodRowKey, + k8sPodColumnsConfig, +} from './table.config'; + +async function fetchListData( + filters: K8sBaseFilters, + signal?: AbortSignal, +): ReturnType< + K8sEntityConfig['list']['fetchListData'] +> { + try { + const response = await listPods( + { + filter: { expression: filters.filter.expression }, + groupBy: filters.groupBy?.map((g) => ({ name: g.name })), + offset: filters.offset, + limit: filters.limit ?? 10, + start: filters.start, + end: filters.end, + orderBy: filters.orderBy + ? { + key: { name: filters.orderBy.key.name }, + direction: + filters.orderBy.direction === 'asc' + ? Querybuildertypesv5OrderDirectionDTO.asc + : Querybuildertypesv5OrderDirectionDTO.desc, + } + : undefined, + }, + signal, + ); + + const data = response.data; + return { + type: + data.type === InframonitoringtypesResponseTypeDTO.grouped_list + ? ('grouped_list' as const) + : ('list' as const), + records: data.records, + total: data.total, + endTimeBeforeRetention: data.endTimeBeforeRetention, + warning: data.warning, + }; + } catch (error) { + return { + type: 'list' as const, + records: [] as InframonitoringtypesPodRecordDTO[], + total: 0, + error: + convertToApiError(error as AxiosError) ?? null, + }; + } +} + +async function fetchEntityData( + filters: K8sDetailsFilters, + signal?: AbortSignal, +): ReturnType< + K8sEntityConfig['details']['fetchEntityData'] +> { + try { + const response = await listPods( + { + filter: { expression: filters.filter.expression }, + start: filters.start, + end: filters.end, + limit: 1, + offset: 0, + }, + signal, + ); + + return { + data: response.data.records.length > 0 ? response.data.records[0] : null, + }; + } catch (error) { + return { + data: null, + error: + convertToApiError(error as AxiosError) ?? null, + }; + } +} + +export const podEntityConfig: K8sEntityConfig = + { + list: { + entity: InfraMonitoringEntity.PODS, + eventCategory: InfraMonitoringEvents.Pod, + tableColumns: k8sPodColumnsConfig, + fetchListData, + getRowKey: getK8sPodRowKey, + getItemKey: getK8sPodItemKey, + detailsQueryKeyPrefix: 'pod', + }, + details: { + category: InfraMonitoringEntity.PODS, + eventCategory: InfraMonitoringEvents.Pod, + queryKeyPrefix: 'pod', + getSelectedItemExpression: k8sPodGetSelectedItemExpression, + fetchEntityData, + getEntityName: k8sPodGetEntityName, + getInitialLogTracesExpression: k8sPodInitialLogTracesExpression, + getInitialEventsExpression: k8sPodInitialEventsExpression, + metadataConfig: k8sPodDetailsMetadataConfig, + entityWidgetInfo: podWidgetInfo, + getEntityQueryPayload: getPodMetricsQueryPayload, + }, + }; diff --git a/frontend/src/container/InfraMonitoringK8sV2/Pods/table.config.tsx b/frontend/src/container/InfraMonitoringK8sV2/Pods/table.config.tsx index 4f8d7214f5d..9c92fff35d3 100644 --- a/frontend/src/container/InfraMonitoringK8sV2/Pods/table.config.tsx +++ b/frontend/src/container/InfraMonitoringK8sV2/Pods/table.config.tsx @@ -131,13 +131,16 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [ width: { min: 250 }, enableSort: false, visibilityBehavior: 'hidden-on-collapse', - cell: ({ row }): React.ReactNode => { + cell: ({ row, rowId }): React.ReactNode => { const podCountsByStatus = row.podCountsByStatus; if (!podCountsByStatus) { return -; } return ( - + ); }, }, @@ -171,7 +174,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [ ), accessorFn: (row): number => row.podRestarts, - width: { min: 100 }, + width: { min: 140 }, enableSort: true, cell: ({ value }): React.ReactNode => { const restarts = value as number; diff --git a/frontend/src/container/InfraMonitoringK8sV2/StatefulSets/K8sStatefulSetsList.tsx b/frontend/src/container/InfraMonitoringK8sV2/StatefulSets/K8sStatefulSetsList.tsx deleted file mode 100644 index d8b2a0daa64..00000000000 --- a/frontend/src/container/InfraMonitoringK8sV2/StatefulSets/K8sStatefulSetsList.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { useCallback, useMemo } from 'react'; -import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs'; -import { listStatefulSets } from 'api/generated/services/inframonitoring'; -import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas'; -import { AxiosError } from 'axios'; -import { - InframonitoringtypesResponseTypeDTO, - InframonitoringtypesStatefulSetRecordDTO, - Querybuildertypesv5OrderDirectionDTO, -} from 'api/generated/services/sigNoz.schemas'; -import { InfraMonitoringEvents } from 'constants/events'; -import APIError from 'types/api/error'; - -import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails'; -import { K8sBaseList } from '../Base/K8sBaseList'; -import { K8sBaseFilters } from '../Base/types'; -import { InfraMonitoringEntity } from '../constants'; -import { SelectedItemParams } from '../hooks'; -import { - getStatefulSetMetricsQueryPayload, - getStatefulSetPodMetricsQueryPayload, - k8sStatefulSetDetailsMetadataConfig, - k8sStatefulSetGetEntityName, - k8sStatefulSetGetSelectedItemExpression, - k8sStatefulSetInitialEventsExpression, - k8sStatefulSetInitialLogTracesExpression, - statefulSetWidgetInfo, -} from './constants'; -import { - getK8sStatefulSetItemKey, - getK8sStatefulSetRowKey, - k8sStatefulSetsColumnsConfig, -} from './table.config'; -import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab'; - -function K8sStatefulSetsList({ - controlListPrefix, -}: { - controlListPrefix?: React.ReactNode; -}): JSX.Element { - const fetchListData = useCallback( - async (filters: K8sBaseFilters, signal?: AbortSignal) => { - try { - const response = await listStatefulSets( - { - filter: { expression: filters.filter.expression }, - groupBy: filters.groupBy?.map((g) => ({ name: g.name })), - offset: filters.offset, - limit: filters.limit ?? 10, - start: filters.start, - end: filters.end, - orderBy: filters.orderBy - ? { - key: { name: filters.orderBy.key.name }, - direction: - filters.orderBy.direction === 'asc' - ? Querybuildertypesv5OrderDirectionDTO.asc - : Querybuildertypesv5OrderDirectionDTO.desc, - } - : undefined, - }, - signal, - ); - - const data = response.data; - return { - type: - data.type === InframonitoringtypesResponseTypeDTO.grouped_list - ? ('grouped_list' as const) - : ('list' as const), - records: data.records, - total: data.total, - endTimeBeforeRetention: data.endTimeBeforeRetention, - warning: data.warning, - }; - } catch (error) { - return { - type: 'list' as const, - records: [] as InframonitoringtypesStatefulSetRecordDTO[], - total: 0, - error: - convertToApiError(error as AxiosError) ?? null, - }; - } - }, - [], - ); - - const fetchEntityData = useCallback( - async ( - filters: K8sDetailsFilters, - signal?: AbortSignal, - ): Promise<{ - data: InframonitoringtypesStatefulSetRecordDTO | null; - error?: APIError | null; - }> => { - try { - const response = await listStatefulSets( - { - filter: { expression: filters.filter.expression }, - start: filters.start, - end: filters.end, - limit: 1, - offset: 0, - }, - signal, - ); - - return { - data: response.data.records.length > 0 ? response.data.records[0] : null, - }; - } catch (error) { - return { - data: null, - error: - convertToApiError(error as AxiosError) ?? null, - }; - } - }, - [], - ); - - const customTabs = useMemo( - () => [ - createPodMetricsTab({ - getQueryPayload: getStatefulSetPodMetricsQueryPayload, - category: InfraMonitoringEntity.STATEFULSETS, - queryKey: 'statefulSetPodMetrics', - }), - ], - [], - ); - - return ( - <> - - controlListPrefix={controlListPrefix} - entity={InfraMonitoringEntity.STATEFULSETS} - tableColumns={k8sStatefulSetsColumnsConfig} - fetchListData={fetchListData} - getRowKey={getK8sStatefulSetRowKey} - getItemKey={getK8sStatefulSetItemKey} - eventCategory={InfraMonitoringEvents.StatefulSet} - /> - - - category={InfraMonitoringEntity.STATEFULSETS} - eventCategory={InfraMonitoringEvents.StatefulSet} - getSelectedItemExpression={k8sStatefulSetGetSelectedItemExpression} - fetchEntityData={fetchEntityData} - getEntityName={k8sStatefulSetGetEntityName} - getInitialLogTracesExpression={k8sStatefulSetInitialLogTracesExpression} - getInitialEventsExpression={k8sStatefulSetInitialEventsExpression} - metadataConfig={k8sStatefulSetDetailsMetadataConfig} - entityWidgetInfo={statefulSetWidgetInfo} - getEntityQueryPayload={getStatefulSetMetricsQueryPayload} - queryKeyPrefix="statefulSet" - customTabs={customTabs} - /> - - ); -} - -export default K8sStatefulSetsList; diff --git a/frontend/src/container/InfraMonitoringK8sV2/StatefulSets/entity.config.ts b/frontend/src/container/InfraMonitoringK8sV2/StatefulSets/entity.config.ts new file mode 100644 index 00000000000..2f91d3b8060 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8sV2/StatefulSets/entity.config.ts @@ -0,0 +1,153 @@ +import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs'; +import { listStatefulSets } from 'api/generated/services/inframonitoring'; +import { + InframonitoringtypesResponseTypeDTO, + InframonitoringtypesStatefulSetRecordDTO, + Querybuildertypesv5OrderDirectionDTO, + RenderErrorResponseDTO, +} from 'api/generated/services/sigNoz.schemas'; +import { AxiosError } from 'axios'; +import { InfraMonitoringEvents } from 'constants/events'; +import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab'; + +import { K8sEntityConfig } from '../Base/entity.config.types'; +import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types'; +import { InfraMonitoringEntity } from '../constants'; +import { SelectedItemParams } from '../hooks'; +import { + getStatefulSetMetricsQueryPayload, + getStatefulSetPodMetricsQueryPayload, + k8sStatefulSetDetailsMetadataConfig, + k8sStatefulSetGetEntityName, + k8sStatefulSetGetSelectedItemExpression, + k8sStatefulSetInitialEventsExpression, + k8sStatefulSetInitialLogTracesExpression, + statefulSetWidgetInfo, +} from './constants'; +import { + getK8sStatefulSetItemKey, + getK8sStatefulSetRowKey, + k8sStatefulSetsColumnsConfig, +} from './table.config'; + +async function fetchListData( + filters: K8sBaseFilters, + signal?: AbortSignal, +): ReturnType< + K8sEntityConfig< + InframonitoringtypesStatefulSetRecordDTO, + SelectedItemParams + >['list']['fetchListData'] +> { + try { + const response = await listStatefulSets( + { + filter: { expression: filters.filter.expression }, + groupBy: filters.groupBy?.map((g) => ({ name: g.name })), + offset: filters.offset, + limit: filters.limit ?? 10, + start: filters.start, + end: filters.end, + orderBy: filters.orderBy + ? { + key: { name: filters.orderBy.key.name }, + direction: + filters.orderBy.direction === 'asc' + ? Querybuildertypesv5OrderDirectionDTO.asc + : Querybuildertypesv5OrderDirectionDTO.desc, + } + : undefined, + }, + signal, + ); + + const data = response.data; + return { + type: + data.type === InframonitoringtypesResponseTypeDTO.grouped_list + ? ('grouped_list' as const) + : ('list' as const), + records: data.records, + total: data.total, + endTimeBeforeRetention: data.endTimeBeforeRetention, + warning: data.warning, + }; + } catch (error) { + return { + type: 'list' as const, + records: [] as InframonitoringtypesStatefulSetRecordDTO[], + total: 0, + error: + convertToApiError(error as AxiosError) ?? null, + }; + } +} + +async function fetchEntityData( + filters: K8sDetailsFilters, + signal?: AbortSignal, +): ReturnType< + K8sEntityConfig< + InframonitoringtypesStatefulSetRecordDTO, + SelectedItemParams + >['details']['fetchEntityData'] +> { + try { + const response = await listStatefulSets( + { + filter: { expression: filters.filter.expression }, + start: filters.start, + end: filters.end, + limit: 1, + offset: 0, + }, + signal, + ); + + return { + data: response.data.records.length > 0 ? response.data.records[0] : null, + }; + } catch (error) { + return { + data: null, + error: + convertToApiError(error as AxiosError) ?? null, + }; + } +} + +export const statefulSetEntityConfig: K8sEntityConfig< + InframonitoringtypesStatefulSetRecordDTO, + SelectedItemParams +> = { + list: { + entity: InfraMonitoringEntity.STATEFULSETS, + eventCategory: InfraMonitoringEvents.StatefulSet, + tableColumns: k8sStatefulSetsColumnsConfig, + fetchListData, + getRowKey: getK8sStatefulSetRowKey, + getItemKey: getK8sStatefulSetItemKey, + detailsQueryKeyPrefix: 'statefulSet', + }, + details: { + category: InfraMonitoringEntity.STATEFULSETS, + eventCategory: InfraMonitoringEvents.StatefulSet, + queryKeyPrefix: 'statefulSet', + getSelectedItemExpression: k8sStatefulSetGetSelectedItemExpression, + fetchEntityData, + getEntityName: k8sStatefulSetGetEntityName, + getInitialLogTracesExpression: k8sStatefulSetInitialLogTracesExpression, + getInitialEventsExpression: k8sStatefulSetInitialEventsExpression, + metadataConfig: k8sStatefulSetDetailsMetadataConfig, + entityWidgetInfo: statefulSetWidgetInfo, + getEntityQueryPayload: getStatefulSetMetricsQueryPayload, + customTabs: [ + createPodMetricsTab({ + getQueryPayload: getStatefulSetPodMetricsQueryPayload, + category: InfraMonitoringEntity.STATEFULSETS, + queryKey: 'statefulSetPodMetrics', + docBasePath: '/infrastructure-monitoring/kubernetes/statefulsets/', + }), + ], + }, +}; diff --git a/frontend/src/container/InfraMonitoringK8sV2/StatefulSets/table.config.tsx b/frontend/src/container/InfraMonitoringK8sV2/StatefulSets/table.config.tsx index e28efcfa9ce..ccbfe7a9bf6 100644 --- a/frontend/src/container/InfraMonitoringK8sV2/StatefulSets/table.config.tsx +++ b/frontend/src/container/InfraMonitoringK8sV2/StatefulSets/table.config.tsx @@ -122,12 +122,17 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef { + cell: ({ row, rowId }): React.ReactNode => { const podCountsByStatus = row.podCountsByStatus; if (!podCountsByStatus) { return -; } - return ; + return ( + + ); }, }, { @@ -141,7 +146,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef ( + cell: ({ row, rowId }): React.ReactNode => ( ), }, diff --git a/frontend/src/container/InfraMonitoringK8sV2/Volumes/K8sVolumesList.tsx b/frontend/src/container/InfraMonitoringK8sV2/Volumes/K8sVolumesList.tsx deleted file mode 100644 index c6ce0f18c6c..00000000000 --- a/frontend/src/container/InfraMonitoringK8sV2/Volumes/K8sVolumesList.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { useCallback } from 'react'; -import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs'; -import { listVolumes } from 'api/generated/services/inframonitoring'; -import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas'; -import { AxiosError } from 'axios'; -import { - InframonitoringtypesResponseTypeDTO, - InframonitoringtypesVolumeRecordDTO, - Querybuildertypesv5OrderDirectionDTO, -} from 'api/generated/services/sigNoz.schemas'; -import { InfraMonitoringEvents } from 'constants/events'; -import APIError from 'types/api/error'; - -import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails'; -import { K8sBaseList } from '../Base/K8sBaseList'; -import { K8sBaseFilters } from '../Base/types'; -import { InfraMonitoringEntity } from '../constants'; -import { SelectedItemParams } from '../hooks'; -import { - getVolumeMetricsQueryPayload, - k8sVolumeDetailsMetadataConfig, - k8sVolumeGetEntityName, - k8sVolumeGetSelectedItemExpression, - k8sVolumeInitialEventsExpression, - k8sVolumeInitialLogTracesExpression, - volumeWidgetInfo, -} from './constants'; -import { - getK8sVolumeItemKey, - getK8sVolumeRowKey, - k8sVolumesColumnsConfig, -} from './table.config'; - -function K8sVolumesList({ - controlListPrefix, -}: { - controlListPrefix?: React.ReactNode; -}): JSX.Element { - const fetchListData = useCallback( - async (filters: K8sBaseFilters, signal?: AbortSignal) => { - try { - const response = await listVolumes( - { - filter: { expression: filters.filter.expression }, - groupBy: filters.groupBy?.map((g) => ({ name: g.name })), - offset: filters.offset, - limit: filters.limit ?? 10, - start: filters.start, - end: filters.end, - orderBy: filters.orderBy - ? { - key: { name: filters.orderBy.key.name }, - direction: - filters.orderBy.direction === 'asc' - ? Querybuildertypesv5OrderDirectionDTO.asc - : Querybuildertypesv5OrderDirectionDTO.desc, - } - : undefined, - }, - signal, - ); - - const data = response.data; - return { - type: - data.type === InframonitoringtypesResponseTypeDTO.grouped_list - ? ('grouped_list' as const) - : ('list' as const), - records: data.records, - total: data.total, - endTimeBeforeRetention: data.endTimeBeforeRetention, - warning: data.warning, - }; - } catch (error) { - return { - type: 'list' as const, - records: [] as InframonitoringtypesVolumeRecordDTO[], - total: 0, - error: - convertToApiError(error as AxiosError) ?? null, - }; - } - }, - [], - ); - - const fetchEntityData = useCallback( - async ( - filters: K8sDetailsFilters, - signal?: AbortSignal, - ): Promise<{ - data: InframonitoringtypesVolumeRecordDTO | null; - error?: APIError | null; - }> => { - try { - const response = await listVolumes( - { - filter: { expression: filters.filter.expression }, - start: filters.start, - end: filters.end, - limit: 1, - offset: 0, - }, - signal, - ); - - return { - data: response.data.records.length > 0 ? response.data.records[0] : null, - }; - } catch (error) { - return { - data: null, - error: - convertToApiError(error as AxiosError) ?? null, - }; - } - }, - [], - ); - - return ( - <> - - controlListPrefix={controlListPrefix} - entity={InfraMonitoringEntity.VOLUMES} - tableColumns={k8sVolumesColumnsConfig} - fetchListData={fetchListData} - getRowKey={getK8sVolumeRowKey} - getItemKey={getK8sVolumeItemKey} - eventCategory={InfraMonitoringEvents.Volumes} - /> - - - category={InfraMonitoringEntity.VOLUMES} - eventCategory={InfraMonitoringEvents.Volume} - getSelectedItemExpression={k8sVolumeGetSelectedItemExpression} - fetchEntityData={fetchEntityData} - getEntityName={k8sVolumeGetEntityName} - getInitialLogTracesExpression={k8sVolumeInitialLogTracesExpression} - getInitialEventsExpression={k8sVolumeInitialEventsExpression} - metadataConfig={k8sVolumeDetailsMetadataConfig} - entityWidgetInfo={volumeWidgetInfo} - getEntityQueryPayload={getVolumeMetricsQueryPayload} - queryKeyPrefix="volume" - hideDetailViewTabs - /> - - ); -} - -export default K8sVolumesList; diff --git a/frontend/src/container/InfraMonitoringK8sV2/Volumes/entity.config.ts b/frontend/src/container/InfraMonitoringK8sV2/Volumes/entity.config.ts new file mode 100644 index 00000000000..e0800f4f5bf --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8sV2/Volumes/entity.config.ts @@ -0,0 +1,141 @@ +import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs'; +import { listVolumes } from 'api/generated/services/inframonitoring'; +import { + InframonitoringtypesResponseTypeDTO, + InframonitoringtypesVolumeRecordDTO, + Querybuildertypesv5OrderDirectionDTO, + RenderErrorResponseDTO, +} from 'api/generated/services/sigNoz.schemas'; +import { AxiosError } from 'axios'; +import { InfraMonitoringEvents } from 'constants/events'; + +import { K8sEntityConfig } from '../Base/entity.config.types'; +import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types'; +import { InfraMonitoringEntity } from '../constants'; +import { SelectedItemParams } from '../hooks'; +import { + getVolumeMetricsQueryPayload, + k8sVolumeDetailsMetadataConfig, + k8sVolumeGetEntityName, + k8sVolumeGetSelectedItemExpression, + k8sVolumeInitialEventsExpression, + k8sVolumeInitialLogTracesExpression, + volumeWidgetInfo, +} from './constants'; +import { + getK8sVolumeItemKey, + getK8sVolumeRowKey, + k8sVolumesColumnsConfig, +} from './table.config'; + +async function fetchListData( + filters: K8sBaseFilters, + signal?: AbortSignal, +): ReturnType< + K8sEntityConfig< + InframonitoringtypesVolumeRecordDTO, + SelectedItemParams + >['list']['fetchListData'] +> { + try { + const response = await listVolumes( + { + filter: { expression: filters.filter.expression }, + groupBy: filters.groupBy?.map((g) => ({ name: g.name })), + offset: filters.offset, + limit: filters.limit ?? 10, + start: filters.start, + end: filters.end, + orderBy: filters.orderBy + ? { + key: { name: filters.orderBy.key.name }, + direction: + filters.orderBy.direction === 'asc' + ? Querybuildertypesv5OrderDirectionDTO.asc + : Querybuildertypesv5OrderDirectionDTO.desc, + } + : undefined, + }, + signal, + ); + + const data = response.data; + return { + type: + data.type === InframonitoringtypesResponseTypeDTO.grouped_list + ? ('grouped_list' as const) + : ('list' as const), + records: data.records, + total: data.total, + endTimeBeforeRetention: data.endTimeBeforeRetention, + warning: data.warning, + }; + } catch (error) { + return { + type: 'list' as const, + records: [] as InframonitoringtypesVolumeRecordDTO[], + total: 0, + error: + convertToApiError(error as AxiosError) ?? null, + }; + } +} + +async function fetchEntityData( + filters: K8sDetailsFilters, + signal?: AbortSignal, +): ReturnType< + K8sEntityConfig['details']['fetchEntityData'] +> { + try { + const response = await listVolumes( + { + filter: { expression: filters.filter.expression }, + start: filters.start, + end: filters.end, + limit: 1, + offset: 0, + }, + signal, + ); + + return { + data: response.data.records.length > 0 ? response.data.records[0] : null, + }; + } catch (error) { + return { + data: null, + error: + convertToApiError(error as AxiosError) ?? null, + }; + } +} + +export const volumeEntityConfig: K8sEntityConfig< + InframonitoringtypesVolumeRecordDTO, + SelectedItemParams +> = { + list: { + entity: InfraMonitoringEntity.VOLUMES, + eventCategory: InfraMonitoringEvents.Volumes, + tableColumns: k8sVolumesColumnsConfig, + fetchListData, + getRowKey: getK8sVolumeRowKey, + getItemKey: getK8sVolumeItemKey, + detailsQueryKeyPrefix: 'volume', + }, + details: { + category: InfraMonitoringEntity.VOLUMES, + eventCategory: InfraMonitoringEvents.Volume, + queryKeyPrefix: 'volume', + getSelectedItemExpression: k8sVolumeGetSelectedItemExpression, + fetchEntityData, + getEntityName: k8sVolumeGetEntityName, + getInitialLogTracesExpression: k8sVolumeInitialLogTracesExpression, + getInitialEventsExpression: k8sVolumeInitialEventsExpression, + metadataConfig: k8sVolumeDetailsMetadataConfig, + entityWidgetInfo: volumeWidgetInfo, + getEntityQueryPayload: getVolumeMetricsQueryPayload, + hideDetailViewTabs: true, + }, +}; diff --git a/frontend/src/container/InfraMonitoringK8sV2/components/GroupedStatusCounts.module.scss b/frontend/src/container/InfraMonitoringK8sV2/components/GroupedStatusCounts.module.scss index e9f2f66cfe3..d1b0c165bff 100644 --- a/frontend/src/container/InfraMonitoringK8sV2/components/GroupedStatusCounts.module.scss +++ b/frontend/src/container/InfraMonitoringK8sV2/components/GroupedStatusCounts.module.scss @@ -4,33 +4,24 @@ gap: 4px; } -.itemWrapper { - display: flex; +.item { + display: inline-flex; align-items: center; gap: 4px; + // 4ch value slot (old .valueWrapper) + 3px color bar + 4px gap + min-width: calc(4ch + 7px); + font-variant-numeric: tabular-nums; + text-align: left; + cursor: default; } -.separator { +.item::before { + content: ''; width: 3px; height: 14px; border-radius: 1px; flex-shrink: 0; -} - -.valueWrapper { - min-width: 4ch; -} - -.valueWrapperTooltip { - display: block; - width: fit-content; -} - -.value { - font-variant-numeric: tabular-nums; - text-align: left; - cursor: default; - min-width: min-content; + background-color: var(--gsc-color); } .tooltipContent { diff --git a/frontend/src/container/InfraMonitoringK8sV2/components/GroupedStatusCounts.tsx b/frontend/src/container/InfraMonitoringK8sV2/components/GroupedStatusCounts.tsx index dc9c12faa97..0f9911dee4f 100644 --- a/frontend/src/container/InfraMonitoringK8sV2/components/GroupedStatusCounts.tsx +++ b/frontend/src/container/InfraMonitoringK8sV2/components/GroupedStatusCounts.tsx @@ -1,5 +1,3 @@ -import { TooltipSimple } from '@signozhq/ui/tooltip'; - import styles from './GroupedStatusCounts.module.scss'; import TanStackTable from 'components/TanStackTableView'; import { Typography } from '@signozhq/ui/typography'; @@ -18,6 +16,7 @@ export interface StatusCountItem { interface GroupedStatusCountsProps { items: StatusCountItem[]; + rowId: string; showZeroValues?: boolean; } @@ -62,6 +61,7 @@ function buildTooltipContent(item: StatusCountItem): React.ReactNode { export function GroupedStatusCounts({ items, + rowId, showZeroValues = true, }: GroupedStatusCountsProps): JSX.Element { const visibleItems = @@ -74,21 +74,20 @@ export function GroupedStatusCounts({ return (
{visibleItems.map((item) => ( -
-
-
- - - - {item.value || '-'} - - - -
-
+ + + {item.value || '-'} + + ))}
); diff --git a/frontend/src/container/InfraMonitoringK8sV2/constants.ts b/frontend/src/container/InfraMonitoringK8sV2/constants.ts index fdad4ae64ef..4cf7cf5fae1 100644 --- a/frontend/src/container/InfraMonitoringK8sV2/constants.ts +++ b/frontend/src/container/InfraMonitoringK8sV2/constants.ts @@ -937,22 +937,27 @@ export const podUtilizationByPodWidgetInfo = [ { title: 'CPU Limit Utilization By Pod Name', yAxisUnit: 'percentunit', + docPath: '#cpu-limit-utilization-by-pod-name', }, { title: 'CPU Request Utilization By Pod Name', yAxisUnit: 'percentunit', + docPath: '#cpu-request-utilization-by-pod-name', }, { title: 'Memory Limit Utilization By Pod Name', yAxisUnit: 'percentunit', + docPath: '#memory-limit-utilization-by-pod-name', }, { title: 'Memory Request Utilization By Pod Name', yAxisUnit: 'percentunit', + docPath: '#memory-request-utilization-by-pod-name', }, { title: 'FileSystem Usage Percentage By Pod Name', yAxisUnit: 'percentunit', + docPath: '#filesystem-usage-percentage-by-pod-name', }, ]; diff --git a/frontend/src/lib/chromePerformanceDevTools.ts b/frontend/src/lib/chromePerformanceDevTools.ts new file mode 100644 index 00000000000..ed63e16ecd1 --- /dev/null +++ b/frontend/src/lib/chromePerformanceDevTools.ts @@ -0,0 +1,99 @@ +import { IS_DEV } from 'lib/env'; + +const CHROME_DEVTOOLS_TRACK_PERFORMANCE_ENABLED = + IS_DEV && + typeof performance !== 'undefined' && + typeof performance.measure === 'function' && + typeof performance.now === 'function'; + +export function chromePerformanceNow(): number { + return CHROME_DEVTOOLS_TRACK_PERFORMANCE_ENABLED ? performance.now() : 0; +} + +export interface ChromePerformanceTrackEntryOptions { + trackGroup: string; + track: string; + color?: + | 'primary' + | 'primary-light' + | 'primary-dark' + | 'secondary' + | 'secondary-light' + | 'secondary-dark' + | 'tertiary' + | 'tertiary-light' + | 'tertiary-dark' + | 'warning' + | 'error'; + tooltipText?: string; + properties?: [string, string][]; +} + +export function chromePerformanceMeasure( + name: string, + start: number, + { + trackGroup, + track, + color = 'primary', + tooltipText, + properties, + }: ChromePerformanceTrackEntryOptions, +): void { + if (!CHROME_DEVTOOLS_TRACK_PERFORMANCE_ENABLED) { + return; + } + try { + performance.measure(name, { + start, + detail: { + devtools: { + dataType: 'track-entry', + trackGroup, + track, + color, + tooltipText, + properties, + }, + }, + }); + } catch { + // User Timing rejected detail shape — ignore + } +} + +export function createChromePerformanceInteractionTracker( + trackGroup: string, + track: string, + name: string, +): { + begin: (id: string) => void; + end: (id: string) => void; +} { + const startById = new Map(); + + return { + begin(id: string): void { + if (!CHROME_DEVTOOLS_TRACK_PERFORMANCE_ENABLED) { + return; + } + startById.set(id, performance.now()); + }, + end(id: string): void { + if (!CHROME_DEVTOOLS_TRACK_PERFORMANCE_ENABLED) { + return; + } + const start = startById.get(id); + if (start === undefined) { + return; + } + startById.delete(id); + chromePerformanceMeasure(name, start, { + trackGroup, + track, + color: 'tertiary', + properties: [['id', id]], + }); + }, + }; +} diff --git a/frontend/src/providers/ErrorModalProvider.test.tsx b/frontend/src/providers/ErrorModalProvider.test.tsx new file mode 100644 index 00000000000..5dddf5e6d76 --- /dev/null +++ b/frontend/src/providers/ErrorModalProvider.test.tsx @@ -0,0 +1,62 @@ +import { render, screen } from '@testing-library/react'; +import { useEffect } from 'react'; +import APIError from 'types/api/error'; + +import { ErrorModalProvider, useErrorModal } from './ErrorModalProvider'; + +// Mock the heavy modal so the test only asserts which error it receives. +jest.mock('components/ErrorModal/ErrorModal', () => ({ + __esModule: true, + default: ({ error }: { error: APIError }): JSX.Element => ( +
{error.getErrorMessage()}
+ ), +})); + +function Trigger({ error }: { error: unknown }): null { + const { showErrorModal } = useErrorModal(); + useEffect(() => { + showErrorModal(error); + }, [error, showErrorModal]); + return null; +} + +describe('ErrorModalProvider — showErrorModal', () => { + it('surfaces the backend error message from a raw AxiosError', () => { + const axiosError = { + isAxiosError: true, + response: { + status: 400, + data: { + error: { + code: 'tag_invalid_key', + message: 'tag key contains disallowed characters', + errors: [], + }, + }, + }, + }; + render( + + + , + ); + expect(screen.getByTestId('error-message')).toHaveTextContent( + 'tag key contains disallowed characters', + ); + }); + + it('passes an already-constructed APIError through unchanged', () => { + const apiError = new APIError({ + httpStatusCode: 409, + error: { code: 'conflict', message: 'already exists', url: '', errors: [] }, + }); + render( + + + , + ); + expect(screen.getByTestId('error-message')).toHaveTextContent( + 'already exists', + ); + }); +}); diff --git a/frontend/src/providers/ErrorModalProvider.tsx b/frontend/src/providers/ErrorModalProvider.tsx index ca7f39dde74..e1b19c4fc71 100644 --- a/frontend/src/providers/ErrorModalProvider.tsx +++ b/frontend/src/providers/ErrorModalProvider.tsx @@ -8,11 +8,17 @@ import { useMemo, useState, } from 'react'; +import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs'; import ErrorModal from 'components/ErrorModal/ErrorModal'; import APIError from 'types/api/error'; interface ErrorModalContextType { - showErrorModal: (error: APIError) => void; + /** + * Accepts any thrown value. Generated-client calls reject with a raw + * `AxiosError`, so we normalize to `APIError` here — otherwise the modal would + * show a generic "status 400" instead of the backend's `error.message`. + */ + showErrorModal: (error: unknown) => void; hideErrorModal: () => void; isErrorModalVisible: boolean; } @@ -29,8 +35,15 @@ export function ErrorModalProvider({ const [error, setError] = useState(null); const [isVisible, setIsVisible] = useState(false); - const showErrorModal = useCallback((error: APIError): void => { - setError(error); + const showErrorModal = useCallback((rawError: unknown): void => { + const apiError = + rawError instanceof APIError + ? rawError + : convertToApiError(rawError as Parameters[0]); + if (!apiError) { + return; + } + setError(apiError); setIsVisible(true); }, []); diff --git a/tests/e2e/fixtures/auth.ts b/tests/e2e/fixtures/auth.ts index acac8c8ffda..8b066e5a842 100644 --- a/tests/e2e/fixtures/auth.ts +++ b/tests/e2e/fixtures/auth.ts @@ -30,6 +30,7 @@ async function storageFor(browser: Browser, user: User): Promise { const ctx = await browser.newContext(); const page = await ctx.newPage(); await login(page, user); + await pinSidenav(page); const state = await ctx.storageState(); await ctx.close(); return state; @@ -58,6 +59,24 @@ async function login(page: Page, user: User): Promise { await page.waitForURL((url) => !url.pathname.startsWith('/login')); } +// Pin the nav suite-wide: unpinned it flies out on hover and overlays content. +// Server-side pref, so set once per user at login. +async function pinSidenav(page: Page): Promise { + const token = await page.evaluate( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + () => (globalThis as any).localStorage.getItem('AUTH_TOKEN') || '', + ); + const res = await page.request.put('/api/v1/user/preferences/sidenav_pinned', { + data: { value: true }, + headers: { Authorization: `Bearer ${token}` }, + }); + if (!res.ok()) { + throw new Error( + `PUT /api/v1/user/preferences/sidenav_pinned ${res.status()}: ${await res.text()}`, + ); + } +} + export const test = base.extend<{ /** * User identity for this test. Override with `test.use({ user: ... })` at diff --git a/tests/e2e/playwright.config.ts b/tests/e2e/playwright.config.ts index 344406551c2..ce993c4c9ee 100644 --- a/tests/e2e/playwright.config.ts +++ b/tests/e2e/playwright.config.ts @@ -31,6 +31,10 @@ export default defineConfig({ // Workers workers: process.env.CI ? 2 : undefined, + // The SPA hydrates slowly on CI, so the 5s expect default fires mid-load. + expect: { timeout: 15_000 }, + timeout: process.env.CI ? 60_000 : 30_000, + // Reporter reporter: [ ['html', { outputFolder: 'artifacts/html', open: 'never' }], diff --git a/tests/e2e/tests/dashboards/details/12-sections.spec.ts b/tests/e2e/tests/dashboards/details/12-sections.spec.ts index cfe666d10ae..31025011b68 100644 --- a/tests/e2e/tests/dashboards/details/12-sections.spec.ts +++ b/tests/e2e/tests/dashboards/details/12-sections.spec.ts @@ -137,6 +137,26 @@ async function toggleSection(row: ReturnType): Promise { }); } +// Poll a section to the target collapsed/expanded state, re-clicking if a +// toggle is dropped under CI load. `name` has no regex metacharacters. +async function setSectionCollapsed( + page: Page, + name: string, + collapsed: boolean, +): Promise { + const collapsedTitle = new RegExp(`^${name} \\(\\d+ widgets?\\)$`); + await expect(async () => { + const alreadyCollapsed = (await page.getByText(collapsedTitle).count()) > 0; + if (alreadyCollapsed === collapsed) { + return; + } + await toggleSection( + sectionRow(page, alreadyCollapsed ? collapsedTitle : name), + ); + expect((await page.getByText(collapsedTitle).count()) > 0).toBe(collapsed); + }).toPass({ timeout: 30_000 }); +} + /** * Click the settings (⋮) icon on a section header, bypassing the sidenav's * pointer-event interception via `dispatchEvent('click')` (same root cause @@ -475,19 +495,19 @@ test.describe('Dashboard Detail — Sections', () => { }) => { await gotoApmDashboard(page); - await toggleSection(sectionRow(page, 'DB Metrics')); + await setSectionCollapsed(page, 'DB Metrics', true); await expect( page.getByText(/^DB Metrics \(\d+ widgets?\)$/).first(), ).toBeVisible(); - await toggleSection(sectionRow(page, 'External calls')); + await setSectionCollapsed(page, 'External calls', true); await expect( page.getByText(/^External calls \(\d+ widgets?\)$/).first(), ).toBeVisible(); // Restore both so the test leaves no state behind. - await toggleSection(sectionRow(page, /^DB Metrics \(\d+ widgets?\)$/)); - await toggleSection(sectionRow(page, /^External calls \(\d+ widgets?\)$/)); + await setSectionCollapsed(page, 'DB Metrics', false); + await setSectionCollapsed(page, 'External calls', false); await expect(page.getByText(/^DB Metrics \(\d+ widgets?\)$/)).toHaveCount(0); await expect(page.getByText(/^External calls \(\d+ widgets?\)$/)).toHaveCount( 0,