diff --git a/frontend/public/locales/en-GB/titles.json b/frontend/public/locales/en-GB/titles.json index c74d82f028..12f7fa8ab0 100644 --- a/frontend/public/locales/en-GB/titles.json +++ b/frontend/public/locales/en-GB/titles.json @@ -42,5 +42,6 @@ "DEFAULT": "Open source Observability Platform | SigNoz", "ALERT_HISTORY": "SigNoz | Alert Rule History", "ALERT_OVERVIEW": "SigNoz | Alert Rule Overview", - "INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring" + "INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring", + "INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring" } diff --git a/frontend/public/locales/en/titles.json b/frontend/public/locales/en/titles.json index 4d903b7a40..254cbde262 100644 --- a/frontend/public/locales/en/titles.json +++ b/frontend/public/locales/en/titles.json @@ -55,5 +55,6 @@ "ALERT_HISTORY": "SigNoz | Alert Rule History", "ALERT_OVERVIEW": "SigNoz | Alert Rule Overview", "MESSAGING_QUEUES": "SigNoz | Messaging Queues", - "INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring" + "INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring", + "INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring" } diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index 480d03561b..789627ca2c 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -407,6 +407,13 @@ const routes: AppRoutes[] = [ key: 'INFRASTRUCTURE_MONITORING_HOSTS', isPrivate: true, }, + { + path: ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES, + exact: true, + component: InfrastructureMonitoring, + key: 'INFRASTRUCTURE_MONITORING_KUBERNETES', + isPrivate: true, + }, ]; export const SUPPORT_ROUTE: AppRoutes = { diff --git a/frontend/src/api/infraMonitoring/getK8sJobsList.ts b/frontend/src/api/infraMonitoring/getK8sJobsList.ts new file mode 100644 index 0000000000..6a2d43e97c --- /dev/null +++ b/frontend/src/api/infraMonitoring/getK8sJobsList.ts @@ -0,0 +1,69 @@ +import { ApiBaseInstance } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; + +export interface K8sJobsListPayload { + filters: TagFilter; + groupBy?: BaseAutocompleteData[]; + offset?: number; + limit?: number; + orderBy?: { + columnName: string; + order: 'asc' | 'desc'; + }; +} + +export interface K8sJobsData { + jobName: string; + cpuUsage: number; + memoryUsage: number; + desiredPods: number; + availablePods: number; + cpuRequest: number; + memoryRequest: number; + cpuLimit: number; + memoryLimit: number; + restarts: number; + meta: { + k8s_job_name: string; + k8s_namespace_name: string; + }; +} + +export interface K8sJobsListResponse { + status: string; + data: { + type: string; + records: K8sJobsData[]; + groups: null; + total: number; + sentAnyHostMetricsData: boolean; + isSendingK8SAgentMetrics: boolean; + }; +} + +export const getK8sJobsList = async ( + props: K8sJobsListPayload, + signal?: AbortSignal, + headers?: Record, +): Promise | ErrorResponse> => { + try { + const response = await ApiBaseInstance.post('/jobs/list', props, { + signal, + headers, + }); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data, + params: props, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; diff --git a/frontend/src/api/infraMonitoring/getK8sNodesList.ts b/frontend/src/api/infraMonitoring/getK8sNodesList.ts new file mode 100644 index 0000000000..18fc3ce42a --- /dev/null +++ b/frontend/src/api/infraMonitoring/getK8sNodesList.ts @@ -0,0 +1,64 @@ +import { ApiBaseInstance } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; + +export interface K8sNodesListPayload { + filters: TagFilter; + groupBy?: BaseAutocompleteData[]; + offset?: number; + limit?: number; + orderBy?: { + columnName: string; + order: 'asc' | 'desc'; + }; +} + +export interface K8sNodesData { + nodeUID: string; + nodeCPUUsage: number; + nodeCPUAllocatable: number; + nodeMemoryUsage: number; + nodeMemoryAllocatable: number; + meta: { + k8s_node_name: string; + k8s_node_uid: string; + }; +} + +export interface K8sNodesListResponse { + status: string; + data: { + type: string; + records: K8sNodesData[]; + groups: null; + total: number; + sentAnyHostMetricsData: boolean; + isSendingK8SAgentMetrics: boolean; + }; +} + +export const getK8sNodesList = async ( + props: K8sNodesListPayload, + signal?: AbortSignal, + headers?: Record, +): Promise | ErrorResponse> => { + try { + const response = await ApiBaseInstance.post('/nodes/list', props, { + signal, + headers, + }); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data, + params: props, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; diff --git a/frontend/src/api/infraMonitoring/getK8sPodsList.ts b/frontend/src/api/infraMonitoring/getK8sPodsList.ts new file mode 100644 index 0000000000..d1aa8bd1a7 --- /dev/null +++ b/frontend/src/api/infraMonitoring/getK8sPodsList.ts @@ -0,0 +1,93 @@ +import { ApiBaseInstance } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; + +export interface K8sPodsListPayload { + filters: TagFilter; + groupBy?: BaseAutocompleteData[]; + offset?: number; + limit?: number; + orderBy?: { + columnName: string; + order: 'asc' | 'desc'; + }; +} + +export interface TimeSeriesValue { + timestamp: number; + value: string; +} + +export interface TimeSeries { + labels: Record; + labelsArray: Array>; + values: TimeSeriesValue[]; +} + +export interface K8sPodsData { + podUID: string; + podCPU: number; + podCPURequest: number; + podCPULimit: number; + podMemory: number; + podMemoryRequest: number; + podMemoryLimit: number; + restartCount: number; + meta: { + k8s_cronjob_name: string; + k8s_daemonset_name: string; + k8s_deployment_name: string; + k8s_job_name: string; + k8s_namespace_name: string; + k8s_node_name: string; + k8s_pod_name: string; + k8s_pod_uid: string; + k8s_statefulset_name: string; + k8s_cluster_name: string; + }; + countByPhase: { + pending: number; + running: number; + succeeded: number; + failed: number; + unknown: number; + }; +} + +export interface K8sPodsListResponse { + status: string; + data: { + type: string; + records: K8sPodsData[]; + groups: null; + total: number; + sentAnyHostMetricsData: boolean; + isSendingK8SAgentMetrics: boolean; + }; +} + +export const getK8sPodsList = async ( + props: K8sPodsListPayload, + signal?: AbortSignal, + headers?: Record, +): Promise | ErrorResponse> => { + try { + const response = await ApiBaseInstance.post('/pods/list', props, { + signal, + headers, + }); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data, + params: props, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; diff --git a/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.styles.scss b/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.styles.scss index 34bdd0508e..72fc1b44b2 100644 --- a/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.styles.scss +++ b/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.styles.scss @@ -53,7 +53,7 @@ display: flex; align-items: center; justify-content: space-between; - width: 100%; + width: calc(100% - 24px); cursor: pointer; &.filter-disabled { diff --git a/frontend/src/components/QuickFilters/QuickFilters.styles.scss b/frontend/src/components/QuickFilters/QuickFilters.styles.scss index d5c3460891..01a17d83f1 100644 --- a/frontend/src/components/QuickFilters/QuickFilters.styles.scss +++ b/frontend/src/components/QuickFilters/QuickFilters.styles.scss @@ -15,6 +15,8 @@ display: flex; align-items: center; gap: 6px; + width: 100%; + justify-content: flex-start; .text { color: var(--bg-vanilla-400); @@ -50,6 +52,8 @@ display: flex; align-items: center; gap: 12px; + width: 100%; + justify-content: flex-end; .divider-filter { width: 1px; diff --git a/frontend/src/components/QuickFilters/QuickFilters.tsx b/frontend/src/components/QuickFilters/QuickFilters.tsx index a706e35aef..f673cba9e4 100644 --- a/frontend/src/components/QuickFilters/QuickFilters.tsx +++ b/frontend/src/components/QuickFilters/QuickFilters.tsx @@ -40,10 +40,11 @@ export interface IQuickFiltersConfig { interface IQuickFiltersProps { config: IQuickFiltersConfig[]; handleFilterVisibilityChange: () => void; + source?: string | null; } export default function QuickFilters(props: IQuickFiltersProps): JSX.Element { - const { config, handleFilterVisibilityChange } = props; + const { config, handleFilterVisibilityChange, source } = props; const { currentQuery, @@ -83,16 +84,22 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element { const lastQueryName = currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName; + + const isInfraMonitoring = source === 'infra-monitoring'; + return (
-
- - Filters for - - {lastQueryName} - -
+ {!isInfraMonitoring && ( +
+ + Filters for + + {lastQueryName} + +
+ )} +
@@ -122,3 +129,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
); } + +QuickFilters.defaultProps = { + source: null, +}; diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index 7b2911dbd6..46c7aa169d 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -61,6 +61,7 @@ const ROUTES = { MESSAGING_QUEUES: '/messaging-queues', MESSAGING_QUEUES_DETAIL: '/messaging-queues/detail', INFRASTRUCTURE_MONITORING_HOSTS: '/infrastructure-monitoring/hosts', + INFRASTRUCTURE_MONITORING_KUBERNETES: '/infrastructure-monitoring/kubernetes', } as const; export default ROUTES; diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index 264213b180..00d4c883c6 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -304,8 +304,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element { const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD'; const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY'; const isAlertOverview = (): boolean => routeKey === 'ALERT_OVERVIEW'; - const isInfraMonitoringHosts = (): boolean => - routeKey === 'INFRASTRUCTURE_MONITORING_HOSTS'; + const isInfraMonitoring = (): boolean => + routeKey === 'INFRASTRUCTURE_MONITORING_HOSTS' || + routeKey === 'INFRASTRUCTURE_MONITORING_KUBERNETES'; const isPathMatch = (regex: RegExp): boolean => regex.test(pathname); const isDashboardView = (): boolean => @@ -403,7 +404,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element { isAlertHistory() || isAlertOverview() || isMessagingQueues() || - isInfraMonitoringHosts() + isInfraMonitoring() ? 0 : '0 1rem', diff --git a/frontend/src/container/InfraMonitoringHosts/HostsList.tsx b/frontend/src/container/InfraMonitoringHosts/HostsList.tsx index 2dc9b26662..cfa11a5cbf 100644 --- a/frontend/src/container/InfraMonitoringHosts/HostsList.tsx +++ b/frontend/src/container/InfraMonitoringHosts/HostsList.tsx @@ -168,7 +168,8 @@ function HostsList(): JSX.Element { const showHostsEmptyState = !isFetching && !isLoading && - (!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics); + (!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics) && + !filters.items.length; return (
diff --git a/frontend/src/container/InfraMonitoringHosts/InfraMonitoring.styles.scss b/frontend/src/container/InfraMonitoringHosts/InfraMonitoring.styles.scss index c1594d4929..78c6409e5e 100644 --- a/frontend/src/container/InfraMonitoringHosts/InfraMonitoring.styles.scss +++ b/frontend/src/container/InfraMonitoringHosts/InfraMonitoring.styles.scss @@ -137,6 +137,9 @@ .column-header-right { text-align: right; } + .column-header-left { + text-align: left; + } .ant-table-tbody > tr > td { border-bottom: none; } diff --git a/frontend/src/container/InfraMonitoringHosts/utils.tsx b/frontend/src/container/InfraMonitoringHosts/utils.tsx index e25b4d4185..8e3dc59e7b 100644 --- a/frontend/src/container/InfraMonitoringHosts/utils.tsx +++ b/frontend/src/container/InfraMonitoringHosts/utils.tsx @@ -26,6 +26,7 @@ export const getHostListsQuery = (): HostListPayload => ({ groupBy: [], orderBy: { columnName: 'cpu', order: 'desc' }, }); + export const getTabsItems = (): TabsProps['items'] => [ { label: , diff --git a/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.styles.scss b/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.styles.scss new file mode 100644 index 0000000000..401c96e25a --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.styles.scss @@ -0,0 +1,414 @@ +.k8s-container { + display: flex; + flex-direction: row; + height: calc(100vh - 45px); + + .k8s-quick-filters-container { + width: 280px; + + .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(--bg-slate-300); + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--bg-slate-200); + } + } + } + + .k8s-list-container { + flex: 1; + } +} + +.infra-monitoring-container { + display: flex; + height: 100%; + flex-direction: column; + + .infra-monitoring-header { + display: flex; + justify-content: space-between; + width: 100%; + margin-bottom: 16px; + } + + .k8s-list-controls { + padding: 8px; + + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + + .ant-select-selector { + border-radius: 2px; + border: 1px solid var(--bg-slate-400) !important; + background-color: var(--bg-ink-300) !important; + + input { + font-size: 12px; + } + + .ant-tag .ant-typography { + font-size: 12px; + } + } + + .k8s-list-controls-left { + flex: 1; + + display: flex; + align-items: center; + gap: 8px; + + .k8s-qb-search-container { + width: 60%; + } + + .k8s-attribute-search-container { + width: 40%; + } + } + + .k8s-list-controls-right { + width: 240px; + + display: flex; + align-items: center; + gap: 4px; + } + + .periscope-btn { + padding: 4px 8px; + + &.ghost { + border: none; + background: transparent; + + &:hover { + color: var(--bg-vanilla-100); + } + } + } + } + + .progress-container { + display: flex; + align-items: center; + } + + .progress-bar { + flex: 1; + margin-right: 8px; + } + + .clickable-row { + cursor: pointer; + } + + .k8s-list-table { + .ant-table { + .ant-table-thead > tr > th { + padding: 12px; + font-weight: 500; + font-size: 12px; + line-height: 18px; + + background: var(--bg-ink-500); + border-bottom: none; + + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 600; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.44px; + text-transform: uppercase; + + &::before { + background-color: transparent; + } + } + + .ant-table-thead > tr > th:has(.hostname-column-header) { + background: var(--bg-ink-400); + } + + .ant-table-cell { + padding: 12px; + font-size: 13px; + line-height: 20px; + color: var(--bg-vanilla-100); + background: var(--bg-ink-500); + } + + .ant-table-cell:has(.hostname-column-value) { + background: var(--bg-ink-400); + } + + .hostname-column-value { + color: var(--Vanilla-100, #fff); + font-family: 'Geist Mono'; + font-style: normal; + font-weight: 600; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .status-cell { + .active-tag { + color: var(--bg-forest-500); + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + } + } + + .progress-container { + .ant-progress-bg { + height: 8px !important; + border-radius: 4px; + } + } + + .ant-table-tbody > tr:hover > td { + background: rgba(255, 255, 255, 0.04); + } + + .ant-table-cell:first-child { + text-align: justify; + } + + .ant-table-cell:nth-child(2) { + padding-left: 16px; + padding-right: 16px; + } + + .ant-table-cell:nth-child(n + 3) { + padding-right: 24px; + } + .column-header-right { + text-align: right; + } + .ant-table-tbody > tr > td { + border-bottom: none; + } + + .ant-table-thead + > tr + > th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before { + background-color: transparent; + } + + .ant-empty-normal { + visibility: hidden; + } + } + + .ant-pagination { + position: fixed; + bottom: 0; + width: calc(100% - 64px); + background: var(--bg-ink-500); + padding: 16px; + margin: 0; + + // this is to offset intercom icon till we improve the design + padding-right: 72px; + + .ant-pagination-item { + border-radius: 4px; + + &-active { + background: var(--bg-robin-500); + border-color: var(--bg-robin-500); + + a { + color: var(--bg-ink-500) !important; + } + } + } + } + } + + .k8s-list-container-filters-visible { + .k8s-list-table { + .ant-pagination { + width: calc(100% - 340px); + } + } + } +} + +.infra-monitoring-tags { + width: fit-content; + + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.44px; + text-transform: uppercase; + + border-radius: 50px; + padding: 2px 8px; + + &.active { + color: var(--Forest-500, #25e192); + border: 1px solid rgba(37, 225, 146, 0.2); + background: rgba(37, 225, 146, 0.1); + } + + &.inactive { + color: var(--Slate-50, #62687c); + border: 1px solid rgba(98, 104, 124, 0.2); + background: rgba(98, 104, 124, 0.1); + } +} + +.k8s-list-loading-state { + padding: 8px; + display: flex; + flex-direction: column; + gap: 2px; + + .k8s-list-loading-state-item { + height: 48px; + width: 100%; + } +} + +.no-filtered-hosts-message-container { + height: 30vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + .no-filtered-hosts-message-content { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + + width: fit-content; + padding: 24px; + } + + .no-filtered-hosts-message { + margin-top: 8px; + } +} + +.hosts-empty-state-container { + padding: 16px; + height: 40vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + .hosts-empty-state-container-content { + padding: 16px; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + + width: fit-content; + + .no-hosts-message { + margin-bottom: 16px; + + .no-hosts-message-title { + margin-top: 8px; + margin-bottom: 4px; + } + } + } +} + +.lightMode { + .infra-monitoring-container { + .ant-table-thead > tr > th { + background: var(--bg-vanilla-100); + color: var(--bg-ink-500); + } + + .ant-table-cell { + color: var(--bg-ink-500); + } + .k8s-list-controls { + border-top: 1px solid var(--bg-vanilla-300); + border-bottom: 1px solid var(--bg-vanilla-300); + + .ant-select-selector { + border-color: var(--bg-vanilla-300) !important; + background-color: var(--bg-vanilla-100) !important; + color: var(--bg-ink-200); + } + } + } + + .k8s-list-table { + .ant-table { + .ant-table-thead > tr > th { + background: var(--bg-vanilla-100); + color: var(--text-ink-300); + } + + .ant-table-thead > tr > th:has(.hostname-column-header) { + background: var(--bg-vanilla-100); + } + + .ant-table-cell { + background: var(--bg-vanilla-100); + color: var(--bg-ink-500); + } + + .ant-table-cell:has(.hostname-column-value) { + background: var(--bg-vanilla-100); + } + + .hostname-column-value { + color: var(--bg-ink-300); + } + + .ant-table-tbody > tr:hover > td { + background: rgba(0, 0, 0, 0.04); + } + } + + .ant-pagination { + background: var(--bg-vanilla-100); + + .ant-pagination-item { + &-active { + background: var(--bg-robin-500); + border-color: var(--bg-robin-500); + + a { + color: var(--bg-vanilla-100) !important; + } + } + } + } + } +} diff --git a/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx b/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx new file mode 100644 index 0000000000..83da0c97b2 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx @@ -0,0 +1,46 @@ +import './InfraMonitoringK8s.styles.scss'; + +import * as Sentry from '@sentry/react'; +import QuickFilters from 'components/QuickFilters/QuickFilters'; +import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'; +import { useState } from 'react'; + +import K8sPodLists from './Pods/K8sPodLists'; +import { K8sQuickFiltersConfig } from './utils'; + +export default function InfraMonitoringK8s(): JSX.Element { + const [showFilters, setShowFilters] = useState(true); + + const handleFilterVisibilityChange = (): void => { + setShowFilters(!showFilters); + }; + + return ( + }> +
+
+ {showFilters && ( +
+ +
+ )} + +
+ +
+
+
+
+ ); +} diff --git a/frontend/src/container/InfraMonitoringK8s/Jobs/K8sJobsList.tsx b/frontend/src/container/InfraMonitoringK8s/Jobs/K8sJobsList.tsx new file mode 100644 index 0000000000..ddf24a901d --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Jobs/K8sJobsList.tsx @@ -0,0 +1,243 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import '../InfraMonitoringK8s.styles.scss'; + +import { LoadingOutlined } from '@ant-design/icons'; +import { + Skeleton, + Spin, + Table, + TablePaginationConfig, + TableProps, + Typography, +} from 'antd'; +import { SorterResult } from 'antd/es/table/interface'; +import logEvent from 'api/common/logEvent'; +import { K8sJobsListPayload } from 'api/infraMonitoring/getK8sJobsList'; +import { useGetK8sJobsList } from 'hooks/infraMonitoring/useGetK8sJobsList'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +import K8sHeader from '../K8sHeader'; +import { + defaultAddedColumns, + formatDataForTable, + getK8sJobsListColumns, + getK8sJobsListQuery, + K8sJobsRowData, +} from './utils'; + +function K8sJobsList({ + isFiltersVisible, + handleFilterVisibilityChange, +}: { + isFiltersVisible: boolean; + handleFilterVisibilityChange: () => void; +}): JSX.Element { + const { maxTime, minTime } = useSelector( + (state) => state.globalTime, + ); + + const [currentPage, setCurrentPage] = useState(1); + + const [filters, setFilters] = useState({ + items: [], + op: 'and', + }); + + const [orderBy, setOrderBy] = useState<{ + columnName: string; + order: 'asc' | 'desc'; + } | null>(null); + + // const [selectedJobUID, setselectedJobUID] = useState(null); + + const pageSize = 10; + + const query = useMemo(() => { + const baseQuery = getK8sJobsListQuery(); + return { + ...baseQuery, + limit: pageSize, + offset: (currentPage - 1) * pageSize, + filters, + start: Math.floor(minTime / 1000000), + end: Math.floor(maxTime / 1000000), + orderBy, + }; + }, [currentPage, filters, minTime, maxTime, orderBy]); + + const { data, isFetching, isLoading, isError } = useGetK8sJobsList( + query as K8sJobsListPayload, + { + queryKey: ['hostList', query], + enabled: !!query, + }, + ); + + const JobsData = useMemo(() => data?.payload?.data?.records || [], [data]); + const totalCount = data?.payload?.data?.total || 0; + + const formattedJobsData = useMemo(() => formatDataForTable(JobsData), [ + JobsData, + ]); + + const columns = useMemo(() => getK8sJobsListColumns(), []); + + const handleTableChange: TableProps['onChange'] = useCallback( + ( + pagination: TablePaginationConfig, + _filters: Record, + sorter: SorterResult | SorterResult[], + ): void => { + if (pagination.current) { + setCurrentPage(pagination.current); + } + + if ('field' in sorter && sorter.order) { + setOrderBy({ + columnName: sorter.field as string, + order: sorter.order === 'ascend' ? 'asc' : 'desc', + }); + } else { + setOrderBy(null); + } + }, + [], + ); + + const handleFiltersChange = useCallback( + (value: IBuilderQuery['filters']): void => { + const isNewFilterAdded = value.items.length !== filters.items.length; + if (isNewFilterAdded) { + setFilters(value); + setCurrentPage(1); + + logEvent('Infra Monitoring: K8s list filters applied', { + filters: value, + }); + } + }, + [filters], + ); + + useEffect(() => { + logEvent('Infra Monitoring: K8s list page visited', {}); + }, []); + + // const selectedJobData = useMemo(() => { + // if (!selectedJobUID) return null; + // return JobsData.find((job) => job.JobUID === selectedJobUID) || null; + // }, [selectedJobUID, JobsData]); + + const handleRowClick = (record: K8sJobsRowData): void => { + // setselectedJobUID(record.JobUID); + + logEvent('Infra Monitoring: K8s job list item clicked', { + jobName: record.jobName, + }); + }; + + // const handleCloseJobDetail = (): void => { + // setselectedJobUID(null); + // }; + + const showsJobsTable = + !isError && + !isLoading && + !isFetching && + !(formattedJobsData.length === 0 && filters.items.length > 0); + + const showNoFilteredJobsMessage = + !isFetching && + !isLoading && + formattedJobsData.length === 0 && + filters.items.length > 0; + + return ( +
+ {}} + onRemoveColumn={() => {}} + /> + {isError && {data?.error || 'Something went wrong'}} + + {showNoFilteredJobsMessage && ( +
+
+ thinking-emoji + + + This query had no results. Edit your query and try again! + +
+
+ )} + + {(isFetching || isLoading) && ( +
+ + + +
+ )} + + {showsJobsTable && ( + } />, + }} + tableLayout="fixed" + rowKey={(record): string => record.jobName} + onChange={handleTableChange} + onRow={(record): { onClick: () => void; className: string } => ({ + onClick: (): void => handleRowClick(record), + className: 'clickable-row', + })} + /> + )} + {/* TODO - Handle Job Details flow */} + + ); +} + +export default K8sJobsList; diff --git a/frontend/src/container/InfraMonitoringK8s/Jobs/utils.tsx b/frontend/src/container/InfraMonitoringK8s/Jobs/utils.tsx new file mode 100644 index 0000000000..874d81a77f --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Jobs/utils.tsx @@ -0,0 +1,236 @@ +import { Color } from '@signozhq/design-tokens'; +import { Progress } from 'antd'; +import { ColumnType } from 'antd/es/table'; +import { + K8sJobsData, + K8sJobsListPayload, +} from 'api/infraMonitoring/getK8sJobsList'; + +import { IEntityColumn } from '../utils'; + +export const defaultAddedColumns: IEntityColumn[] = [ + { + label: 'Namespace Status', + value: 'NamespaceStatus', + id: 'NamespaceStatus', + canRemove: false, + }, + { + label: 'CPU Utilization (cores)', + value: 'cpuUsage', + id: 'cpuUsage', + canRemove: false, + }, + { + label: 'CPU Allocatable (cores)', + value: 'cpuAllocatable', + id: 'cpuAllocatable', + canRemove: false, + }, + { + label: 'Memory Allocatable (bytes)', + value: 'memoryAllocatable', + id: 'memoryAllocatable', + canRemove: false, + }, + { + label: 'Pods count by phase', + value: 'podsCount', + id: 'podsCount', + canRemove: false, + }, +]; + +export interface K8sJobsRowData { + key: string; + jobName: string; + availableReplicas: number; + desiredReplicas: number; + cpuRequestUtilization: React.ReactNode; + cpuLimitUtilization: React.ReactNode; + cpuUtilization: number; + memoryRequestUtilization: React.ReactNode; + memoryLimitUtilization: React.ReactNode; + memoryUtilization: number; + jobRestarts: number; +} + +export const getK8sJobsListQuery = (): K8sJobsListPayload => ({ + filters: { + items: [], + op: 'and', + }, + orderBy: { columnName: 'cpu', order: 'desc' }, +}); + +const columnsConfig = [ + { + title:
Job
, + dataIndex: 'jobName', + key: 'jobName', + ellipsis: true, + width: 150, + sorter: true, + align: 'left', + }, + { + title:
Available Replicas
, + dataIndex: 'availableReplicas', + key: 'availableReplicas', + width: 100, + sorter: true, + align: 'left', + }, + { + title:
Desired Replicas
, + dataIndex: 'desiredReplicas', + key: 'desiredReplicas', + width: 80, + sorter: true, + align: 'left', + }, + { + title: ( +
+ CPU Request Utilization (% of limit) +
+ ), + dataIndex: 'cpuRequestUtilization', + key: 'cpuRequestUtilization', + width: 80, + sorter: true, + align: 'left', + }, + { + title: ( +
+ CPU Limit Utilization (% of request) +
+ ), + dataIndex: 'cpuLimitUtilization', + key: 'cpuLimitUtilization', + width: 50, + sorter: true, + align: 'left', + }, + { + title:
CPU Utilization (cores)
, + dataIndex: 'cpuUtilization', + key: 'cpuUtilization', + width: 80, + sorter: true, + align: 'left', + }, + { + title: ( +
+ Memory Request Utilization (% of limit) +
+ ), + dataIndex: 'memoryRequestUtilization', + key: 'memoryRequestUtilization', + width: 50, + sorter: true, + align: 'left', + }, + { + title: ( +
+ Memory Limit Utilization (% of request) +
+ ), + dataIndex: 'memoryLimitUtilization', + key: 'memoryLimitUtilization', + width: 80, + sorter: true, + align: 'left', + }, + { + title:
Job Restarts
, + dataIndex: 'jobRestarts', + key: 'jobRestarts', + width: 50, + sorter: true, + align: 'left', + }, +]; + +export const getK8sJobsListColumns = (): ColumnType[] => + columnsConfig as ColumnType[]; + +const getStrokeColorForProgressBar = (value: number): string => { + if (value >= 90) return Color.BG_SAKURA_500; + if (value >= 60) return Color.BG_AMBER_500; + return Color.BG_FOREST_500; +}; + +export const formatDataForTable = (data: K8sJobsData[]): K8sJobsRowData[] => + data.map((job, index) => ({ + key: `${job.meta.k8s_job_name}-${index}`, + jobName: job.meta.k8s_job_name, + availableReplicas: job.availablePods, + desiredReplicas: job.desiredPods, + jobRestarts: job.restarts, + cpuUtilization: job.cpuUsage, + cpuRequestUtilization: ( +
+ { + const cpuPercent = Number((job.cpuRequest * 100).toFixed(1)); + return getStrokeColorForProgressBar(cpuPercent); + })()} + className="progress-bar" + /> +
+ ), + cpuLimitUtilization: ( +
+ { + const cpuPercent = Number((job.cpuLimit * 100).toFixed(1)); + return getStrokeColorForProgressBar(cpuPercent); + })()} + className="progress-bar" + /> +
+ ), + memoryUtilization: job.memoryUsage, + memoryRequestUtilization: ( +
+ { + const memoryPercent = Number((job.memoryRequest * 100).toFixed(1)); + return getStrokeColorForProgressBar(memoryPercent); + })()} + className="progress-bar" + /> +
+ ), + memoryLimitUtilization: ( +
+ { + const memoryPercent = Number((job.memoryLimit * 100).toFixed(1)); + return getStrokeColorForProgressBar(memoryPercent); + })()} + className="progress-bar" + /> +
+ ), + })); diff --git a/frontend/src/container/InfraMonitoringK8s/K8sEmptyOrIncorrectMetrics.tsx b/frontend/src/container/InfraMonitoringK8s/K8sEmptyOrIncorrectMetrics.tsx new file mode 100644 index 0000000000..e84695c8f2 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/K8sEmptyOrIncorrectMetrics.tsx @@ -0,0 +1,32 @@ +import { Typography } from 'antd'; + +export default function HostsEmptyOrIncorrectMetrics({ + noData, + incorrectData, +}: { + noData: boolean; + incorrectData: boolean; +}): JSX.Element { + return ( +
+
+ eyes emoji + + {noData && ( +
+ + No data received yet. + +
+ )} + + {incorrectData && ( + + To see data, upgrade to the latest version of SigNoz k8s-infra chart. + Please contact support if you need help. + + )} +
+
+ ); +} diff --git a/frontend/src/container/InfraMonitoringK8s/K8sFiltersSidePanel/K8sFiltersSidePanel.styles.scss b/frontend/src/container/InfraMonitoringK8s/K8sFiltersSidePanel/K8sFiltersSidePanel.styles.scss new file mode 100644 index 0000000000..addc3fb2d4 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/K8sFiltersSidePanel/K8sFiltersSidePanel.styles.scss @@ -0,0 +1,114 @@ +.k8s-filters-side-panel-container { + position: absolute; + width: 100%; + height: 100vh; + background-color: rgba(0, 0, 0, 0.2); + top: 0; + left: 0; + z-index: 10; +} + +.k8s-filters-side-panel { + border-radius: 4px; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2); + + height: 88vh; + + position: absolute; + width: 320px; + right: 4px; + top: 48px; + z-index: 2; + + .k8s-filters-side-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px; + height: 40px; + + .k8s-filters-side-panel-header-title { + display: flex; + align-items: center; + gap: 8px; + } + } + + .k8s-filters-side-panel-body { + height: calc(100% - 40px); + + .k8s-filters-side-panel-body-header { + border: 1px solid var(--bg-ink-300); + border-left: none; + border-right: none; + + .ant-input { + height: 40px; + } + } + + .k8s-filters-side-panel-body-content { + display: flex; + flex-direction: column; + + .added-columns, + .available-columns { + padding: 8px; + + .filter-columns-title { + color: var(--text-slate-50); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.88px; + text-transform: uppercase; + + padding: 8px 12px; + margin-bottom: 8px; + } + + .added-columns-list, + .available-columns-list { + display: flex; + flex-direction: column; + gap: 8px; + + .added-column-item, + .available-column-item { + color: var(--text-vanilla-100); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + + padding: 4px 0px 4px 12px; + + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + + cursor: pointer; + } + + .added-column-item-content, + .available-column-item-content { + display: flex; + align-items: center; + gap: 8px; + } + } + } + + .horizontal-divider { + border-top: 1px solid var(--bg-ink-300); + } + } + } +} diff --git a/frontend/src/container/InfraMonitoringK8s/K8sFiltersSidePanel/K8sFiltersSidePanel.tsx b/frontend/src/container/InfraMonitoringK8s/K8sFiltersSidePanel/K8sFiltersSidePanel.tsx new file mode 100644 index 0000000000..3b8beabb69 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/K8sFiltersSidePanel/K8sFiltersSidePanel.tsx @@ -0,0 +1,119 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +import './K8sFiltersSidePanel.styles.scss'; + +import { Button, Input } from 'antd'; +import { GripVertical, TableColumnsSplit, X } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; + +import { IPodColumn } from '../utils'; + +export default function K8sFiltersSidePanel({ + defaultAddedColumns, + onClose, + addedColumns, + availableColumns, + onAddColumn, + onRemoveColumn, +}: { + defaultAddedColumns: IPodColumn[]; + onClose: () => void; + addedColumns: IPodColumn[]; + availableColumns: IPodColumn[]; + onAddColumn: (column: IPodColumn) => void; + onRemoveColumn: (column: IPodColumn) => void; +}): JSX.Element { + const [searchValue, setSearchValue] = useState(''); + const sidePanelRef = useRef(null); + + const handleSearchChange = (e: React.ChangeEvent): void => { + setSearchValue(e.target.value); + }; + + useEffect(() => { + if (sidePanelRef.current) { + sidePanelRef.current.focus(); + } + }, [searchValue]); + + return ( +
+
+
+ + Columns + + +
+ +
+
+ +
+ +
+
+
Added Columns
+ +
+ {[...defaultAddedColumns, ...addedColumns] + .filter((column) => + column.label.toLowerCase().includes(searchValue.toLowerCase()), + ) + .map((column) => ( +
+
+ {column.label} +
+ + {column.canRemove && ( + onRemoveColumn(column)} + /> + )} +
+ ))} +
+
+ +
+ +
+
Other Columns
+ +
+ {availableColumns + .filter((column) => + column.label.toLowerCase().includes(searchValue.toLowerCase()), + ) + .map((column) => ( +
onAddColumn(column)} + > +
+ {column.label} +
+
+ ))} +
+
+
+
+
+
+ ); +} diff --git a/frontend/src/container/InfraMonitoringK8s/K8sHeader.tsx b/frontend/src/container/InfraMonitoringK8s/K8sHeader.tsx new file mode 100644 index 0000000000..c04151b4fc --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/K8sHeader.tsx @@ -0,0 +1,138 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import './InfraMonitoringK8s.styles.scss'; + +import { Button, Input } from 'antd'; +import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch'; +import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { Filter, SlidersHorizontal } from 'lucide-react'; +import { useCallback, useMemo, useState } from 'react'; +import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; + +import K8sFiltersSidePanel from './K8sFiltersSidePanel/K8sFiltersSidePanel'; +import { IPodColumn } from './utils'; + +function K8sHeader({ + defaultAddedColumns, + addedColumns = [], + availableColumns = [], + handleFiltersChange, + onAddColumn = () => {}, + onRemoveColumn = () => {}, + handleFilterVisibilityChange, + isFiltersVisible, +}: { + defaultAddedColumns: IPodColumn[]; + addedColumns?: IPodColumn[]; + availableColumns?: IPodColumn[]; + handleFiltersChange: (value: IBuilderQuery['filters']) => void; + onAddColumn?: (column: IPodColumn) => void; + onRemoveColumn?: (column: IPodColumn) => void; + handleFilterVisibilityChange: () => void; + isFiltersVisible: boolean; +}): JSX.Element { + const [isFiltersSidePanelOpen, setIsFiltersSidePanelOpen] = useState(false); + + const { currentQuery } = useQueryBuilder(); + const updatedCurrentQuery = useMemo( + () => ({ + ...currentQuery, + builder: { + ...currentQuery.builder, + queryData: [ + { + ...currentQuery.builder.queryData[0], + aggregateOperator: 'noop', + aggregateAttribute: { + ...currentQuery.builder.queryData[0].aggregateAttribute, + }, + }, + ], + }, + }), + [currentQuery], + ); + const query = updatedCurrentQuery?.builder?.queryData[0] || null; + + const handleChangeTagFilters = useCallback( + (value: IBuilderQuery['filters']) => { + handleFiltersChange(value); + }, + [handleFiltersChange], + ); + + return ( +
+
+ {!isFiltersVisible && ( +
+ +
+ )} + +
+ +
+ +
+ Group by
} + placeholder="Search for attribute" + /> +
+
+ +
+ + + +
+ + {isFiltersSidePanelOpen && ( + { + if (isFiltersSidePanelOpen) { + setIsFiltersSidePanelOpen(false); + } + }} + onAddColumn={onAddColumn} + onRemoveColumn={onRemoveColumn} + /> + )} +
+ ); +} + +K8sHeader.defaultProps = { + addedColumns: [], + availableColumns: [], + onAddColumn: () => {}, + onRemoveColumn: () => {}, +}; + +export default K8sHeader; diff --git a/frontend/src/container/InfraMonitoringK8s/Nodes/K8sNodesList.tsx b/frontend/src/container/InfraMonitoringK8s/Nodes/K8sNodesList.tsx new file mode 100644 index 0000000000..ae75ea16dd --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Nodes/K8sNodesList.tsx @@ -0,0 +1,240 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import '../InfraMonitoringK8s.styles.scss'; + +import { LoadingOutlined } from '@ant-design/icons'; +import { + Skeleton, + Spin, + Table, + TablePaginationConfig, + TableProps, + Typography, +} from 'antd'; +import { SorterResult } from 'antd/es/table/interface'; +import logEvent from 'api/common/logEvent'; +import { K8sNodesListPayload } from 'api/infraMonitoring/getK8sNodesList'; +import { useGetK8sNodesList } from 'hooks/infraMonitoring/useGetK8sNodesList'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +import K8sHeader from '../K8sHeader'; +import { + defaultAddedColumns, + formatDataForTable, + getK8sNodesListColumns, + getK8sNodesListQuery, + K8sNodesRowData, +} from './utils'; + +// eslint-disable-next-line sonarjs/cognitive-complexity +function K8sNodesList({ + isFiltersVisible, + handleFilterVisibilityChange, +}: { + isFiltersVisible: boolean; + handleFilterVisibilityChange: () => void; +}): JSX.Element { + const { maxTime, minTime } = useSelector( + (state) => state.globalTime, + ); + + const [currentPage, setCurrentPage] = useState(1); + + const [filters, setFilters] = useState({ + items: [], + op: 'and', + }); + + const [orderBy, setOrderBy] = useState<{ + columnName: string; + order: 'asc' | 'desc'; + } | null>(null); + + // const [selectedNodeUID, setselectedNodeUID] = useState(null); + + const pageSize = 10; + + const query = useMemo(() => { + const baseQuery = getK8sNodesListQuery(); + return { + ...baseQuery, + limit: pageSize, + offset: (currentPage - 1) * pageSize, + filters, + start: Math.floor(minTime / 1000000), + end: Math.floor(maxTime / 1000000), + orderBy, + }; + }, [currentPage, filters, minTime, maxTime, orderBy]); + + const { data, isFetching, isLoading, isError } = useGetK8sNodesList( + query as K8sNodesListPayload, + { + queryKey: ['hostList', query], + enabled: !!query, + }, + ); + + const nodesData = useMemo(() => data?.payload?.data?.records || [], [data]); + const totalCount = data?.payload?.data?.total || 0; + + const formattedNodesData = useMemo(() => formatDataForTable(nodesData), [ + nodesData, + ]); + + const columns = useMemo(() => getK8sNodesListColumns(), []); + + const handleTableChange: TableProps['onChange'] = useCallback( + ( + pagination: TablePaginationConfig, + _filters: Record, + sorter: SorterResult | SorterResult[], + ): void => { + if (pagination.current) { + setCurrentPage(pagination.current); + } + + if ('field' in sorter && sorter.order) { + setOrderBy({ + columnName: sorter.field as string, + order: sorter.order === 'ascend' ? 'asc' : 'desc', + }); + } else { + setOrderBy(null); + } + }, + [], + ); + + const handleFiltersChange = useCallback( + (value: IBuilderQuery['filters']): void => { + const isNewFilterAdded = value.items.length !== filters.items.length; + if (isNewFilterAdded) { + setFilters(value); + setCurrentPage(1); + + logEvent('Infra Monitoring: K8s list filters applied', { + filters: value, + }); + } + }, + [filters], + ); + + useEffect(() => { + logEvent('Infra Monitoring: K8s list page visited', {}); + }, []); + + // const selectedNodeData = useMemo(() => { + // if (!selectedNodeUID) return null; + // return nodesData.find((node) => node.nodeUID === selectedNodeUID) || null; + // }, [selectedNodeUID, nodesData]); + + const handleRowClick = (record: K8sNodesRowData): void => { + // setselectedNodeUID(record.nodeUID); + + logEvent('Infra Monitoring: K8s node list item clicked', { + nodeUID: record.nodeUID, + }); + }; + + // const handleCloseNodeDetail = (): void => { + // setselectedNodeUID(null); + // }; + + const showsNodesTable = + !isError && + !isLoading && + !isFetching && + !(formattedNodesData.length === 0 && filters.items.length > 0); + + const showNoFilteredNodesMessage = + !isFetching && + !isLoading && + formattedNodesData.length === 0 && + filters.items.length > 0; + + return ( +
+ + {isError && {data?.error || 'Something went wrong'}} + + {showNoFilteredNodesMessage && ( +
+
+ thinking-emoji + + + This query had no results. Edit your query and try again! + +
+
+ )} + + {(isFetching || isLoading) && ( +
+ + + +
+ )} + + {showsNodesTable && ( +
} />, + }} + tableLayout="fixed" + rowKey={(record): string => record.nodeUID} + onChange={handleTableChange} + onRow={(record): { onClick: () => void; className: string } => ({ + onClick: (): void => handleRowClick(record), + className: 'clickable-row', + })} + /> + )} + {/* TODO - Handle Node Details flow */} + + ); +} + +export default K8sNodesList; diff --git a/frontend/src/container/InfraMonitoringK8s/Nodes/utils.tsx b/frontend/src/container/InfraMonitoringK8s/Nodes/utils.tsx new file mode 100644 index 0000000000..b11633d9cf --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Nodes/utils.tsx @@ -0,0 +1,126 @@ +import { ColumnType } from 'antd/es/table'; +import { + K8sNodesData, + K8sNodesListPayload, +} from 'api/infraMonitoring/getK8sNodesList'; + +import { IEntityColumn } from '../utils'; + +export const defaultAddedColumns: IEntityColumn[] = [ + { + label: 'Node Status', + value: 'nodeStatus', + id: 'nodeStatus', + canRemove: false, + }, + { + label: 'CPU Utilization (cores)', + value: 'cpuUtilization', + id: 'cpuUtilization', + canRemove: false, + }, + { + label: 'CPU Allocatable (cores)', + value: 'cpuAllocatable', + id: 'cpuAllocatable', + canRemove: false, + }, + { + label: 'Memory Allocatable (bytes)', + value: 'memoryAllocatable', + id: 'memoryAllocatable', + canRemove: false, + }, + { + label: 'Pods count by phase', + value: 'podsCount', + id: 'podsCount', + canRemove: false, + }, +]; + +export interface K8sNodesRowData { + key: string; + nodeUID: string; + nodeStatus: string; + cpuUtilization: React.ReactNode; + cpuAllocatable: React.ReactNode; + memoryUtilization: React.ReactNode; + memoryAllocatable: React.ReactNode; + podsCount: number; +} + +export const getK8sNodesListQuery = (): K8sNodesListPayload => ({ + filters: { + items: [], + op: 'and', + }, + orderBy: { columnName: 'cpu', order: 'desc' }, +}); + +const columnsConfig = [ + { + title:
Node Status
, + dataIndex: 'nodeStatus', + key: 'nodeStatus', + ellipsis: true, + width: 150, + sorter: true, + align: 'left', + }, + { + title:
CPU Utilization (cores)
, + dataIndex: 'cpuUtilization', + key: 'cpuUtilization', + width: 100, + sorter: true, + align: 'left', + }, + { + title:
CPU Allocatable (cores)
, + dataIndex: 'cpuAllocatable', + key: 'cpuAllocatable', + width: 100, + sorter: true, + align: 'left', + }, + { + title:
Memory Utilization (bytes)
, + dataIndex: 'memoryUtilization', + key: 'memoryUtilization', + width: 80, + sorter: true, + align: 'left', + }, + { + title:
Memory Allocatable (bytes)
, + dataIndex: 'memoryAllocatable', + key: 'memoryAllocatable', + width: 80, + sorter: true, + align: 'left', + }, + { + title:
Pods count by phase
, + dataIndex: 'containerRestarts', + key: 'containerRestarts', + width: 50, + sorter: true, + align: 'left', + }, +]; + +export const getK8sNodesListColumns = (): ColumnType[] => + columnsConfig as ColumnType[]; + +export const formatDataForTable = (data: K8sNodesData[]): K8sNodesRowData[] => + data.map((node, index) => ({ + key: `${node.nodeUID}-${index}`, + nodeUID: node.nodeUID || '', + cpuUtilization: node.nodeCPUUsage, + memoryUtilization: node.nodeMemoryUsage, + cpuAllocatable: node.nodeCPUAllocatable, + memoryAllocatable: node.nodeMemoryAllocatable, + nodeStatus: node.meta.k8s_node_name, + podsCount: node.nodeCPUAllocatable, + })); diff --git a/frontend/src/container/InfraMonitoringK8s/Pods/K8sPodLists.tsx b/frontend/src/container/InfraMonitoringK8s/Pods/K8sPodLists.tsx new file mode 100644 index 0000000000..e502f6c824 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Pods/K8sPodLists.tsx @@ -0,0 +1,305 @@ +import '../InfraMonitoringK8s.styles.scss'; + +import { LoadingOutlined } from '@ant-design/icons'; +import { + Skeleton, + Spin, + Table, + TablePaginationConfig, + TableProps, + Typography, +} from 'antd'; +import { SorterResult } from 'antd/es/table/interface'; +import logEvent from 'api/common/logEvent'; +import { K8sPodsListPayload } from 'api/infraMonitoring/getK8sPodsList'; +import { useGetK8sPodsList } from 'hooks/infraMonitoring/useGetK8sPodsList'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; +import { GlobalReducer } from 'types/reducer/globalTime'; +import { + getFromLocalStorage, + updateLocalStorage, +} from 'utils/localStorageReadWrite'; + +import K8sHeader from '../K8sHeader'; +import { + defaultAddedColumns, + defaultAvailableColumns, + formatDataForTable, + getK8sPodsListColumns, + getK8sPodsListQuery, + IPodColumn, + K8sPodsRowData, +} from '../utils'; +import PodDetails from './PodDetails/PodDetails'; + +// eslint-disable-next-line sonarjs/cognitive-complexity +function K8sPodsList({ + isFiltersVisible, + handleFilterVisibilityChange, +}: { + isFiltersVisible: boolean; + handleFilterVisibilityChange: () => void; +}): JSX.Element { + const { maxTime, minTime } = useSelector( + (state) => state.globalTime, + ); + + const [currentPage, setCurrentPage] = useState(1); + + const [addedColumns, setAddedColumns] = useState([]); + + const [availableColumns, setAvailableColumns] = useState( + defaultAvailableColumns, + ); + + const [filters, setFilters] = useState({ + items: [], + op: 'and', + }); + + useEffect(() => { + const addedColumns = getFromLocalStorage('k8sPodsAddedColumns'); + + if (addedColumns && addedColumns.length > 0) { + const availableColumns = defaultAvailableColumns.filter( + (column) => !addedColumns.includes(column.id), + ); + + const newAddedColumns = defaultAvailableColumns.filter((column) => + addedColumns.includes(column.id), + ); + + setAvailableColumns(availableColumns); + setAddedColumns(newAddedColumns); + } + }, []); + + const [orderBy, setOrderBy] = useState<{ + columnName: string; + order: 'asc' | 'desc'; + } | null>(null); + + const [selectedPodUID, setSelectedPodUID] = useState(null); + + const pageSize = 10; + + const query = useMemo(() => { + const baseQuery = getK8sPodsListQuery(); + return { + ...baseQuery, + limit: pageSize, + offset: (currentPage - 1) * pageSize, + filters, + start: Math.floor(minTime / 1000000), + end: Math.floor(maxTime / 1000000), + orderBy, + }; + }, [currentPage, filters, minTime, maxTime, orderBy]); + + const { data, isFetching, isLoading, isError } = useGetK8sPodsList( + query as K8sPodsListPayload, + { + queryKey: ['hostList', query], + enabled: !!query, + }, + ); + + const podsData = useMemo(() => data?.payload?.data?.records || [], [data]); + const totalCount = data?.payload?.data?.total || 0; + + const formattedPodsData = useMemo(() => formatDataForTable(podsData), [ + podsData, + ]); + + const columns = useMemo(() => getK8sPodsListColumns(addedColumns), [ + addedColumns, + ]); + + const handleTableChange: TableProps['onChange'] = useCallback( + ( + pagination: TablePaginationConfig, + _filters: Record, + sorter: SorterResult | SorterResult[], + ): void => { + if (pagination.current) { + setCurrentPage(pagination.current); + } + + if ('field' in sorter && sorter.order) { + setOrderBy({ + columnName: sorter.field as string, + order: sorter.order === 'ascend' ? 'asc' : 'desc', + }); + } else { + setOrderBy(null); + } + }, + [], + ); + + const handleFiltersChange = useCallback( + (value: IBuilderQuery['filters']): void => { + const isNewFilterAdded = value.items.length !== filters.items.length; + if (isNewFilterAdded) { + setFilters(value); + setCurrentPage(1); + + logEvent('Infra Monitoring: K8s list filters applied', { + filters: value, + }); + } + }, + [filters], + ); + + useEffect(() => { + logEvent('Infra Monitoring: K8s list page visited', {}); + }, []); + + const selectedPodData = useMemo(() => { + if (!selectedPodUID) return null; + return podsData.find((pod) => pod.podUID === selectedPodUID) || null; + }, [selectedPodUID, podsData]); + + const handleRowClick = (record: K8sPodsRowData): void => { + setSelectedPodUID(record.podUID); + + logEvent('Infra Monitoring: K8s list item clicked', { + podUID: record.podUID, + }); + }; + + const handleClosePodDetail = (): void => { + setSelectedPodUID(null); + }; + + const showPodsTable = + !isError && + !isLoading && + !isFetching && + !(formattedPodsData.length === 0 && filters.items.length > 0); + + const showNoFilteredPodsMessage = + !isFetching && + !isLoading && + formattedPodsData.length === 0 && + filters.items.length > 0; + + const handleAddColumn = useCallback( + (column: IPodColumn): void => { + setAddedColumns((prev) => [...prev, column]); + + setAvailableColumns((prev) => prev.filter((c) => c.value !== column.value)); + }, + [setAddedColumns, setAvailableColumns], + ); + + // Update local storage when added columns updated + useEffect(() => { + const addedColumnIDs = addedColumns.map((column) => column.id); + + updateLocalStorage('k8sPodsAddedColumns', addedColumnIDs); + }, [addedColumns]); + + const handleRemoveColumn = useCallback( + (column: IPodColumn): void => { + setAddedColumns((prev) => prev.filter((c) => c.value !== column.value)); + + setAvailableColumns((prev) => [...prev, column]); + }, + [setAddedColumns, setAvailableColumns], + ); + + return ( +
+ + {isError && {data?.error || 'Something went wrong'}} + + {showNoFilteredPodsMessage && ( +
+
+ thinking-emoji + + + This query had no results. Edit your query and try again! + +
+
+ )} + + {(isFetching || isLoading) && ( +
+ + + +
+ )} + + {showPodsTable && ( +
} />, + }} + tableLayout="fixed" + rowKey={(record): string => record.podUID} + onChange={handleTableChange} + onRow={(record): { onClick: () => void; className: string } => ({ + onClick: (): void => handleRowClick(record), + className: 'clickable-row', + })} + /> + )} + + + + ); +} + +export default K8sPodsList; diff --git a/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/Events.styles.scss b/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/Events.styles.scss new file mode 100644 index 0000000000..9619fd6b20 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/Events.styles.scss @@ -0,0 +1,289 @@ +.pod-events-container { + margin-top: 1rem; + + .filter-section { + flex: 1; + + .ant-select-selector { + border-radius: 2px; + border: 1px solid var(--bg-slate-400) !important; + background-color: var(--bg-ink-300) !important; + + input { + font-size: 12px; + } + + .ant-tag .ant-typography { + font-size: 12px; + } + } + } + + .pod-events-header { + display: flex; + justify-content: space-between; + gap: 8px; + + padding: 12px; + border-radius: 3px; + border: 1px solid var(--bg-slate-500); + } + + .pod-events { + margin-top: 1rem; + + .virtuoso-list { + overflow-y: hidden !important; + + &::-webkit-scrollbar { + width: 0.3rem; + height: 0.3rem; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: var(--bg-slate-300); + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--bg-slate-200); + } + + .ant-row { + width: fit-content; + } + } + + .skeleton-container { + height: 100%; + padding: 16px; + } + } + + .ant-table { + .ant-table-thead > tr > th { + padding: 12px; + font-weight: 500; + font-size: 12px; + line-height: 18px; + + background: rgb(18, 19, 23); + border-bottom: none; + + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 600; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.44px; + text-transform: uppercase; + + &::before { + background-color: transparent; + } + } + + .ant-table-thead > tr > th:has(.hostname-column-header) { + background: var(--bg-ink-400); + } + + .ant-table-cell { + padding: 12px; + font-size: 13px; + line-height: 20px; + color: var(--bg-vanilla-100); + background: rgb(18, 19, 23); + border-bottom: none; + } + + .ant-table-cell:has(.hostname-column-value) { + background: var(--bg-ink-400); + } + + .hostname-column-value { + color: var(--Vanilla-100, #fff); + font-family: 'Geist Mono'; + font-style: normal; + font-weight: 600; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .status-cell { + .active-tag { + color: var(--bg-forest-500); + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + } + } + + .progress-container { + .ant-progress-bg { + height: 8px !important; + border-radius: 4px; + } + } + + .ant-table-tbody > tr:hover > td { + background: rgba(255, 255, 255, 0.04); + } + + .ant-table-cell:first-child { + text-align: justify; + } + + .ant-table-cell:nth-child(2) { + padding-left: 16px; + padding-right: 16px; + } + + .ant-table-cell:nth-child(n + 3) { + padding-right: 24px; + } + .column-header-right { + text-align: right; + } + .ant-table-tbody > tr > td { + border-bottom: none; + } + + .ant-table-thead + > tr + > th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before { + background-color: transparent; + } + + .ant-empty-normal { + visibility: hidden; + } + } + + .ant-pagination { + position: fixed; + bottom: 0; + width: calc(100% - 64px); + background: rgb(18, 19, 23); + padding: 16px; + margin: 0; + + // this is to offset intercom icon till we improve the design + padding-right: 72px; + + .ant-pagination-item { + border-radius: 4px; + + &-active { + background: var(--bg-robin-500); + border-color: var(--bg-robin-500); + + a { + color: var(--bg-ink-500) !important; + } + } + } + } +} + +.pod-events-list-container { + flex: 1; + height: calc(100vh - 272px) !important; + display: flex; + height: 100%; + + .raw-log-content { + width: 100%; + text-wrap: inherit; + word-wrap: break-word; + } +} + +.pod-events-list-card { + width: 100%; + margin-top: 12px; + + .ant-table-wrapper { + height: 100%; + overflow-y: auto; + + &::-webkit-scrollbar { + width: 0.3rem; + height: 0.3rem; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: var(--bg-slate-300); + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--bg-slate-200); + } + + .ant-row { + width: fit-content; + } + } + + .ant-card-body { + padding: 0; + + height: 100%; + width: 100%; + } +} + +.logs-loading-skeleton { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + padding: 8px 0; + + .ant-skeleton-input-sm { + height: 18px; + } +} + +.no-logs-found { + height: 50vh; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + + padding: 24px; + box-sizing: border-box; + + .ant-typography { + display: flex; + align-items: center; + gap: 16px; + } +} + +.lightMode { + .filter-section { + border-top: 1px solid var(--bg-vanilla-300); + border-bottom: 1px solid var(--bg-vanilla-300); + + .ant-select-selector { + border-color: var(--bg-vanilla-300) !important; + background-color: var(--bg-vanilla-100) !important; + color: var(--bg-ink-200); + } + } +} + +.periscope-btn-icon { + cursor: pointer; +} diff --git a/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/Events.tsx b/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/Events.tsx new file mode 100644 index 0000000000..ceb0c9da06 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/Events.tsx @@ -0,0 +1,259 @@ +import './Events.styles.scss'; + +import { Table, TableColumnsType, Typography } from 'antd'; +import { DEFAULT_ENTITY_VERSION } from 'constants/app'; +import LogsError from 'container/LogsError/LogsError'; +import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config'; +import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch'; +import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; +import { + CustomTimeType, + Time, +} from 'container/TopNav/DateTimeSelectionV2/config'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults'; +import { ChevronDown, ChevronRight } from 'lucide-react'; +import { useMemo } from 'react'; +import { useQuery } from 'react-query'; +import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; +import { DataSource } from 'types/common/queryBuilder'; + +import NoLogsContainer from '../PodLogs/NoLogsContainer'; +import { getPodsEventsQueryPayload } from './constants'; + +interface EventDataType { + key: string; + timestamp: string; + body: string; + id: string; + attributes_bool?: Record; + attributes_number?: Record; + attributes_string?: Record; + resources_string?: Record; + scope_name?: string; + scope_string?: Record; + scope_version?: string; + severity_number?: number; + severity_text?: string; + span_id?: string; + trace_flags?: number; + trace_id?: string; +} + +interface IPodEventsProps { + timeRange: { + startTime: number; + endTime: number; + }; + handleChangeLogFilters: (filters: IBuilderQuery['filters']) => void; + filters: IBuilderQuery['filters']; + isModalTimeSelection: boolean; + handleTimeChange: ( + interval: Time | CustomTimeType, + dateTimeRange?: [number, number], + ) => void; + selectedInterval: Time; +} + +export default function Events({ + timeRange, + handleChangeLogFilters, + filters, + isModalTimeSelection, + handleTimeChange, + selectedInterval, +}: IPodEventsProps): JSX.Element { + const { currentQuery } = useQueryBuilder(); + const updatedCurrentQuery = useMemo( + () => ({ + ...currentQuery, + builder: { + ...currentQuery.builder, + queryData: [ + { + ...currentQuery.builder.queryData[0], + dataSource: DataSource.LOGS, + aggregateOperator: 'noop', + aggregateAttribute: { + ...currentQuery.builder.queryData[0].aggregateAttribute, + }, + }, + ], + }, + }), + [currentQuery], + ); + + const query = updatedCurrentQuery?.builder?.queryData[0] || null; + + // const [restFilters, setRestFilters] = useState([]); + + // const [resetLogsList, setResetLogsList] = useState(false); + + // useEffect(() => { + // const newRestFilters = filters?.items?.filter( + // (item) => + // item.key?.key !== 'id' && + // item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME && + // item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND, + // ); + + // const areFiltersSame = isEqual(restFilters, newRestFilters); + + // if (!areFiltersSame) { + // setResetLogsList(true); + // } + + // setRestFilters(newRestFilters); + // // eslint-disable-next-line react-hooks/exhaustive-deps + // }, [filters]); + + const queryPayload = useMemo(() => { + const basePayload = getPodsEventsQueryPayload( + timeRange.startTime, + timeRange.endTime, + filters, + ); + + basePayload.query.builder.queryData[0].pageSize = 100; + basePayload.query.builder.queryData[0].orderBy = [ + { columnName: 'timestamp', order: ORDERBY_FILTERS.DESC }, + ]; + + return basePayload; + }, [timeRange.startTime, timeRange.endTime, filters]); + + const { data, isLoading, isError } = useQuery({ + queryKey: ['podEvents', timeRange.startTime, timeRange.endTime, filters], + queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION), + enabled: !!queryPayload, + }); + + const columns: TableColumnsType = [ + { title: 'Severity', dataIndex: 'severity', key: 'severity' }, + { title: 'Timestamp', dataIndex: 'timestamp', key: 'timestamp' }, + { title: 'Body', dataIndex: 'body', key: 'body' }, + ]; + + const formattedPodEvents = useMemo(() => { + const responsePayload = + data?.payload.data.newResult.data.result[0].list || []; + + const formattedData = responsePayload?.map((event) => ({ + timestamp: event.timestamp, + severity: event.data.severity_text, + body: event.data.body, + id: event.data.id, + key: event.data.id, + })); + + return formattedData || []; + }, [data]); + + const handleExpandRow = (record: EventDataType): JSX.Element => { + console.log('record', record); + + return

{record.body}

; + }; + + const handleExpandRowIcon = ({ + expanded, + onExpand, + record, + }: { + expanded: boolean; + onExpand: ( + record: EventDataType, + e: React.MouseEvent, + ) => void; + record: EventDataType; + }): JSX.Element => + expanded ? ( + + onExpand( + record, + (e as unknown) as React.MouseEvent, + ) + } + /> + ) : ( + + onExpand( + record, + (e as unknown) as React.MouseEvent, + ) + } + /> + ); + + return ( +
+
+
+ {query && ( + + )} +
+
+ +
+
+ + {isLoading && ( +
+
+ wait-icon + + Loading Events. Please wait... +
+
+ )} + + {!isLoading && !isError && formattedPodEvents.length === 0 && ( + + )} + + {isError && !isLoading && } + + {!isLoading && !isError && formattedPodEvents.length > 0 && ( +
+
+ + columns={columns} + expandable={{ + expandedRowRender: handleExpandRow, + rowExpandable: (record): boolean => record.body !== 'Not Expandable', + expandIcon: handleExpandRowIcon, + }} + dataSource={formattedPodEvents} + pagination={false} + /> +
+
+ )} +
+ ); +} diff --git a/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/constants.ts b/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/constants.ts new file mode 100644 index 0000000000..7b434effa9 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/constants.ts @@ -0,0 +1,65 @@ +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; +import { EQueryType } from 'types/common/dashboard'; +import { DataSource } from 'types/common/queryBuilder'; +import { v4 as uuidv4 } from 'uuid'; + +export const getPodsEventsQueryPayload = ( + start: number, + end: number, + filters: IBuilderQuery['filters'], +): GetQueryResultsProps => ({ + graphType: PANEL_TYPES.LIST, + selectedTime: 'GLOBAL_TIME', + query: { + clickhouse_sql: [], + promql: [], + builder: { + queryData: [ + { + dataSource: DataSource.LOGS, + queryName: 'A', + aggregateOperator: 'noop', + aggregateAttribute: { + id: '------false', + dataType: DataTypes.String, + key: '', + isColumn: false, + type: '', + isJSON: false, + }, + timeAggregation: 'rate', + spaceAggregation: 'sum', + functions: [], + filters, + expression: 'A', + disabled: false, + stepInterval: 60, + having: [], + limit: null, + orderBy: [ + { + columnName: 'timestamp', + order: 'desc', + }, + ], + groupBy: [], + legend: '', + reduceTo: 'avg', + offset: 0, + pageSize: 100, + }, + ], + queryFormulas: [], + }, + id: uuidv4(), + queryType: EQueryType.QUERY_BUILDER, + }, + params: { + lastLogLineTimestamp: null, + }, + start, + end, +}); diff --git a/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Metrics/Metrics.styles.scss b/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Metrics/Metrics.styles.scss new file mode 100644 index 0000000000..3978680c09 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Metrics/Metrics.styles.scss @@ -0,0 +1,45 @@ +.empty-container { + display: flex; + justify-content: center; + align-items: center; + height: 100%; +} + +.host-metrics-container { + margin-top: 1rem; +} + +.metrics-header { + display: flex; + justify-content: flex-end; + margin-top: 1rem; + + gap: 8px; + padding: 12px; + border-radius: 3px; + border: 1px solid var(--bg-slate-500); +} + +.host-metrics-card { + margin: 8px 0 1rem 0; + height: 300px; + padding: 10px; + + border: 1px solid var(--bg-slate-500); + + .ant-card-body { + padding: 0; + } + + .chart-container { + width: 100%; + height: 100%; + } + + .no-data-container { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } +} diff --git a/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Metrics/Metrics.tsx b/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Metrics/Metrics.tsx new file mode 100644 index 0000000000..2135e7b610 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Metrics/Metrics.tsx @@ -0,0 +1,140 @@ +import './Metrics.styles.scss'; + +import { Card, Col, Row, Skeleton, Typography } from 'antd'; +import { K8sPodsData } from 'api/infraMonitoring/getK8sPodsList'; +import cx from 'classnames'; +import Uplot from 'components/Uplot'; +import { ENTITY_VERSION_V4 } from 'constants/app'; +import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; +import { + CustomTimeType, + Time, +} from 'container/TopNav/DateTimeSelectionV2/config'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import { useResizeObserver } from 'hooks/useDimensions'; +import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults'; +import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions'; +import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; +import { useMemo, useRef } from 'react'; +import { useQueries, UseQueryResult } from 'react-query'; +import { SuccessResponse } from 'types/api'; +import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; + +import { getPodQueryPayload, podWidgetInfo } from '../../constants'; + +interface MetricsTabProps { + timeRange: { + startTime: number; + endTime: number; + }; + isModalTimeSelection: boolean; + handleTimeChange: ( + interval: Time | CustomTimeType, + dateTimeRange?: [number, number], + ) => void; + selectedInterval: Time; + pod: K8sPodsData; +} + +function Metrics({ + selectedInterval, + pod, + timeRange, + handleTimeChange, + isModalTimeSelection, +}: MetricsTabProps): JSX.Element { + const queryPayloads = useMemo( + () => getPodQueryPayload(pod, timeRange.startTime, timeRange.endTime), + [pod, timeRange.startTime, timeRange.endTime], + ); + + const queries = useQueries( + queryPayloads.map((payload) => ({ + queryKey: ['pod-metrics', payload, ENTITY_VERSION_V4, 'POD'], + queryFn: (): Promise> => + GetMetricQueryRange(payload, ENTITY_VERSION_V4), + enabled: !!payload, + })), + ); + + const isDarkMode = useIsDarkMode(); + const graphRef = useRef(null); + const dimensions = useResizeObserver(graphRef); + + const chartData = useMemo( + () => queries.map(({ data }) => getUPlotChartData(data?.payload)), + [queries], + ); + + const options = useMemo( + () => + queries.map(({ data }, idx) => + getUPlotChartOptions({ + apiResponse: data?.payload, + isDarkMode, + dimensions, + yAxisUnit: podWidgetInfo[idx].yAxisUnit, + softMax: null, + softMin: null, + minTimeScale: timeRange.startTime, + maxTimeScale: timeRange.endTime, + }), + ), + [queries, isDarkMode, dimensions, timeRange.startTime, timeRange.endTime], + ); + + const renderCardContent = ( + query: UseQueryResult, unknown>, + idx: number, + ): JSX.Element => { + if (query.isLoading) { + return ; + } + + if (query.error) { + const errorMessage = + (query.error as Error)?.message || 'Something went wrong'; + return
{errorMessage}
; + } + return ( +
+ +
+ ); + }; + + return ( + <> +
+
+ +
+
+ + {queries.map((query, idx) => ( +
+ {podWidgetInfo[idx].title} + + {renderCardContent(query, idx)} + + + ))} + + + ); +} + +export default Metrics; diff --git a/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetail.interfaces.ts b/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetail.interfaces.ts new file mode 100644 index 0000000000..7f07be841e --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetail.interfaces.ts @@ -0,0 +1,7 @@ +import { K8sPodsData } from 'api/infraMonitoring/getK8sPodsList'; + +export type PodDetailProps = { + pod: K8sPodsData | null; + isModalTimeSelection: boolean; + onClose: () => void; +}; diff --git a/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetails.styles.scss b/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetails.styles.scss new file mode 100644 index 0000000000..5c2446014e --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetails.styles.scss @@ -0,0 +1,247 @@ +.pod-detail-drawer { + border-left: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2); + + .ant-drawer-header { + padding: 8px 16px; + border-bottom: none; + + align-items: stretch; + + border-bottom: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + } + + .ant-drawer-close { + margin-inline-end: 0px; + } + + .ant-drawer-body { + display: flex; + flex-direction: column; + padding: 16px; + } + + .title { + color: var(--text-vanilla-400); + font-family: 'Geist Mono'; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .radio-button { + display: flex; + align-items: center; + justify-content: center; + padding-top: var(--padding-1); + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); + } + + .pod-detail-drawer__pod { + .pod-details-grid { + .labels-row, + .values-row { + display: grid; + grid-template-columns: 1fr 1.5fr 1.5fr 1.5fr; + gap: 30px; + align-items: center; + } + + .labels-row { + margin-bottom: 8px; + } + + .pod-details-metadata-label { + color: var(--text-vanilla-400); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.44px; + text-transform: uppercase; + } + + .pod-details-metadata-value { + color: var(--text-vanilla-400); + font-family: 'Geist Mono'; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .status-tag { + margin: 0; + + &.active { + color: var(--success-500); + background: var(--success-100); + border-color: var(--success-500); + } + + &.inactive { + color: var(--error-500); + background: var(--error-100); + border-color: var(--error-500); + } + } + + .progress-container { + width: 158px; + .ant-progress { + margin: 0; + + .ant-progress-text { + font-weight: 600; + } + } + } + + .ant-card { + &.ant-card-bordered { + border: 1px solid var(--bg-slate-500) !important; + } + } + } + } + + .tabs-and-search { + display: flex; + justify-content: space-between; + align-items: center; + margin: 16px 0; + + .action-btn { + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); + display: flex; + align-items: center; + justify-content: center; + } + } + + .views-tabs-container { + margin-top: 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; + + .views-tabs { + color: var(--text-vanilla-400); + + .view-title { + display: flex; + gap: var(--margin-2); + align-items: center; + justify-content: center; + font-size: var(--font-size-xs); + font-style: normal; + font-weight: var(--font-weight-normal); + } + + .tab { + border: 1px solid var(--bg-slate-400); + width: 114px; + } + + .tab::before { + background: var(--bg-slate-400); + } + + .selected_view { + background: var(--bg-slate-300); + color: var(--text-vanilla-100); + border: 1px solid var(--bg-slate-400); + } + + .selected_view::before { + background: var(--bg-slate-400); + } + } + + .compass-button { + width: 30px; + height: 30px; + + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); + } + } + .ant-drawer-close { + padding: 0px; + } +} + +.lightMode { + .ant-drawer-header { + border-bottom: 1px solid var(--bg-vanilla-400); + background: var(--bg-vanilla-100); + } + + .pod-detail-drawer { + .title { + color: var(--text-ink-300); + } + + .pod-detail-drawer__pod { + .ant-typography { + color: var(--text-ink-300); + background: transparent; + } + } + + .radio-button { + border: 1px solid var(--bg-vanilla-400); + background: var(--bg-vanilla-100); + color: var(--text-ink-300); + } + + .views-tabs { + .tab { + background: var(--bg-vanilla-100); + } + + .selected_view { + background: var(--bg-vanilla-300); + border: 1px solid var(--bg-slate-300); + color: var(--text-ink-400); + } + + .selected_view::before { + background: var(--bg-vanilla-300); + border-left: 1px solid var(--bg-slate-300); + } + } + + .compass-button { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); + } + + .tabs-and-search { + .action-btn { + border: 1px solid var(--bg-vanilla-400); + background: var(--bg-vanilla-100); + color: var(--text-ink-300); + } + } + } +} diff --git a/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetails.tsx b/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetails.tsx new file mode 100644 index 0000000000..79c65f6654 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetails.tsx @@ -0,0 +1,552 @@ +import './PodDetails.styles.scss'; + +import { Color, Spacing } from '@signozhq/design-tokens'; +import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd'; +import { RadioChangeEvent } from 'antd/lib'; +import logEvent from 'api/common/logEvent'; +import { QueryParams } from 'constants/query'; +import { + initialQueryBuilderFormValuesMap, + initialQueryState, +} from 'constants/queryBuilder'; +import ROUTES from 'constants/routes'; +import { + CustomTimeType, + Time, +} from 'container/TopNav/DateTimeSelectionV2/config'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import useUrlQuery from 'hooks/useUrlQuery'; +import GetMinMax from 'lib/getMinMax'; +import { + BarChart2, + ChevronsLeftRight, + Compass, + DraftingCompass, + ScrollText, + X, +} from 'lucide-react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { + IBuilderQuery, + TagFilterItem, +} from 'types/api/queryBuilder/queryBuilderData'; +import { + LogsAggregatorOperator, + TracesAggregatorOperator, +} from 'types/common/queryBuilder'; +import { GlobalReducer } from 'types/reducer/globalTime'; +import { v4 as uuidv4 } from 'uuid'; + +import { QUERY_KEYS, VIEW_TYPES, VIEWS } from './constants'; +import Events from './Events/Events'; +import Metrics from './Metrics/Metrics'; +import { PodDetailProps } from './PodDetail.interfaces'; +import PodLogsDetailedView from './PodLogs/PodLogsDetailedView'; +import PodTraces from './PodTraces/PodTraces'; + +// eslint-disable-next-line sonarjs/cognitive-complexity +function PodDetails({ + pod, + onClose, + isModalTimeSelection, +}: PodDetailProps): JSX.Element { + const { maxTime, minTime, selectedTime } = useSelector< + AppState, + GlobalReducer + >((state) => state.globalTime); + + const startMs = useMemo(() => Math.floor(Number(minTime) / 1000000000), [ + minTime, + ]); + const endMs = useMemo(() => Math.floor(Number(maxTime) / 1000000000), [ + maxTime, + ]); + + const urlQuery = useUrlQuery(); + + const [modalTimeRange, setModalTimeRange] = useState(() => ({ + startTime: startMs, + endTime: endMs, + })); + + const [selectedInterval, setSelectedInterval] = useState