diff --git a/frontend/src/api/infraMonitoring/getK8sDeploymentsList.ts b/frontend/src/api/infraMonitoring/getK8sDeploymentsList.ts new file mode 100644 index 0000000000..9ce4643687 --- /dev/null +++ b/frontend/src/api/infraMonitoring/getK8sDeploymentsList.ts @@ -0,0 +1,70 @@ +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 K8sDeploymentsListPayload { + filters: TagFilter; + groupBy?: BaseAutocompleteData[]; + offset?: number; + limit?: number; + orderBy?: { + columnName: string; + order: 'asc' | 'desc'; + }; +} + +export interface K8sDeploymentsData { + deploymentName: string; + cpuUsage: number; + memoryUsage: number; + desiredPods: number; + availablePods: number; + cpuRequest: number; + memoryRequest: number; + cpuLimit: number; + memoryLimit: number; + restarts: number; + meta: { + k8s_cluster_name: string; + k8s_deployment_name: string; + k8s_namespace_name: string; + }; +} + +export interface K8sDeploymentsListResponse { + status: string; + data: { + type: string; + records: K8sDeploymentsData[]; + groups: null; + total: number; + sentAnyHostMetricsData: boolean; + isSendingK8SAgentMetrics: boolean; + }; +} + +export const getK8sDeploymentsList = async ( + props: K8sDeploymentsListPayload, + signal?: AbortSignal, + headers?: Record, +): Promise | ErrorResponse> => { + try { + const response = await ApiBaseInstance.post('/deployments/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/container/InfraMonitoringK8s/Deployments/K8sDeploymentsList.tsx b/frontend/src/container/InfraMonitoringK8s/Deployments/K8sDeploymentsList.tsx new file mode 100644 index 0000000000..15d6fda34c --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Deployments/K8sDeploymentsList.tsx @@ -0,0 +1,248 @@ +/* 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 { K8sDeploymentsListPayload } from 'api/infraMonitoring/getK8sDeploymentsList'; +import { useGetK8sDeploymentsList } from 'hooks/infraMonitoring/useGetK8sDeploymentsList'; +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, + getK8sDeploymentsListColumns, + getK8sDeploymentsListQuery, + K8sDeploymentsRowData, +} from './utils'; + +function K8sDeploymentsList({ + 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 [selectedDeploymentUID, setselectedDeploymentUID] = useState(null); + + const pageSize = 10; + + const query = useMemo(() => { + const baseQuery = getK8sDeploymentsListQuery(); + 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 } = useGetK8sDeploymentsList( + query as K8sDeploymentsListPayload, + { + queryKey: ['hostList', query], + enabled: !!query, + }, + ); + + const DeploymentsData = useMemo(() => data?.payload?.data?.records || [], [ + data, + ]); + const totalCount = data?.payload?.data?.total || 0; + + const formattedDeploymentsData = useMemo( + () => formatDataForTable(DeploymentsData), + [DeploymentsData], + ); + + const columns = useMemo(() => getK8sDeploymentsListColumns(), []); + + 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 selectedDeploymentData = useMemo(() => { + // if (!selectedDeploymentUID) return null; + // return DeploymentsData.find((deployment) => deployment.DeploymentUID === selectedDeploymentUID) || null; + // }, [selectedDeploymentUID, DeploymentsData]); + + const handleRowClick = (record: K8sDeploymentsRowData): void => { + // setselectedDeploymentUID(record.DeploymentUID); + + logEvent('Infra Monitoring: K8s deployment list item clicked', { + deploymentName: record.deploymentName, + }); + }; + + // const handleCloseDeploymentDetail = (): void => { + // setselectedDeploymentUID(null); + // }; + + const showsDeploymentsTable = + !isError && + !isLoading && + !isFetching && + !(formattedDeploymentsData.length === 0 && filters.items.length > 0); + + const showNoFilteredDeploymentsMessage = + !isFetching && + !isLoading && + formattedDeploymentsData.length === 0 && + filters.items.length > 0; + + return ( +
+ {}} + onRemoveColumn={() => {}} + /> + {isError && {data?.error || 'Something went wrong'}} + + {showNoFilteredDeploymentsMessage && ( +
+
+ thinking-emoji + + + This query had no results. Edit your query and try again! + +
+
+ )} + + {(isFetching || isLoading) && ( +
+ + + +
+ )} + + {showsDeploymentsTable && ( + } />, + }} + tableLayout="fixed" + rowKey={(record): string => record.deploymentName} + onChange={handleTableChange} + onRow={(record): { onClick: () => void; className: string } => ({ + onClick: (): void => handleRowClick(record), + className: 'clickable-row', + })} + /> + )} + {/* TODO - Handle Deployment Details flow */} + + ); +} + +export default K8sDeploymentsList; diff --git a/frontend/src/container/InfraMonitoringK8s/Deployments/utils.tsx b/frontend/src/container/InfraMonitoringK8s/Deployments/utils.tsx new file mode 100644 index 0000000000..347762d772 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Deployments/utils.tsx @@ -0,0 +1,248 @@ +import { Color } from '@signozhq/design-tokens'; +import { Progress } from 'antd'; +import { ColumnType } from 'antd/es/table'; +import { + K8sDeploymentsData, + K8sDeploymentsListPayload, +} from 'api/infraMonitoring/getK8sDeploymentsList'; + +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 K8sDeploymentsRowData { + key: string; + deploymentName: string; + availableReplicas: number; + desiredReplicas: number; + cpuRequestUtilization: React.ReactNode; + cpuLimitUtilization: React.ReactNode; + cpuUtilization: number; + memoryRequestUtilization: React.ReactNode; + memoryLimitUtilization: React.ReactNode; + memoryUtilization: number; + containerRestarts: number; +} + +export const getK8sDeploymentsListQuery = (): K8sDeploymentsListPayload => ({ + filters: { + items: [], + op: 'and', + }, + orderBy: { columnName: 'cpu', order: 'desc' }, +}); + +// - Available Replicas +// - Desired Replicas +// - CPU Request Utilization (% of limit) +// - CPU Limit Utilization (% of request) +// - CPU Utilization (cores) +// - Memory Request Utilization (% of limit) +// - Memory Limit Utilization (% of request) +// - Memory Utilization (bytes) +// - Container Restarts + +const columnsConfig = [ + { + title:
Deployment
, + dataIndex: 'deploymentName', + key: 'deploymentName', + 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:
Container Restarts
, + dataIndex: 'containerRestarts', + key: 'containerRestarts', + width: 50, + sorter: true, + align: 'left', + }, +]; + +export const getK8sDeploymentsListColumns = (): 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: K8sDeploymentsData[], +): K8sDeploymentsRowData[] => + data.map((deployment, index) => ({ + key: `${deployment.meta.k8s_deployment_name}-${index}`, + deploymentName: deployment.meta.k8s_deployment_name, + availableReplicas: deployment.availablePods, + desiredReplicas: deployment.desiredPods, + containerRestarts: deployment.restarts, + cpuUtilization: deployment.cpuUsage, + cpuRequestUtilization: ( +
+ { + const cpuPercent = Number((deployment.cpuRequest * 100).toFixed(1)); + return getStrokeColorForProgressBar(cpuPercent); + })()} + className="progress-bar" + /> +
+ ), + cpuLimitUtilization: ( +
+ { + const cpuPercent = Number((deployment.cpuLimit * 100).toFixed(1)); + return getStrokeColorForProgressBar(cpuPercent); + })()} + className="progress-bar" + /> +
+ ), + memoryUtilization: deployment.memoryUsage, + memoryRequestUtilization: ( +
+ { + const memoryPercent = Number((deployment.memoryRequest * 100).toFixed(1)); + return getStrokeColorForProgressBar(memoryPercent); + })()} + className="progress-bar" + /> +
+ ), + memoryLimitUtilization: ( +
+ { + const memoryPercent = Number((deployment.memoryLimit * 100).toFixed(1)); + return getStrokeColorForProgressBar(memoryPercent); + })()} + className="progress-bar" + /> +
+ ), + })); diff --git a/frontend/src/hooks/infraMonitoring/useGetK8sDeploymentsList.ts b/frontend/src/hooks/infraMonitoring/useGetK8sDeploymentsList.ts new file mode 100644 index 0000000000..8e4926b78a --- /dev/null +++ b/frontend/src/hooks/infraMonitoring/useGetK8sDeploymentsList.ts @@ -0,0 +1,48 @@ +import { + getK8sDeploymentsList, + K8sDeploymentsListPayload, + K8sDeploymentsListResponse, +} from 'api/infraMonitoring/getK8sDeploymentsList'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import { useMemo } from 'react'; +import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query'; +import { ErrorResponse, SuccessResponse } from 'types/api'; + +type UseGetK8sDeploymentsList = ( + requestData: K8sDeploymentsListPayload, + options?: UseQueryOptions< + SuccessResponse | ErrorResponse, + Error + >, + headers?: Record, +) => UseQueryResult< + SuccessResponse | ErrorResponse, + Error +>; + +export const useGetK8sDeploymentsList: UseGetK8sDeploymentsList = ( + requestData, + options, + headers, +) => { + const queryKey = useMemo(() => { + if (options?.queryKey && Array.isArray(options.queryKey)) { + return [...options.queryKey]; + } + + if (options?.queryKey && typeof options.queryKey === 'string') { + return options.queryKey; + } + + return [REACT_QUERY_KEY.GET_HOST_LIST, requestData]; + }, [options?.queryKey, requestData]); + + return useQuery< + SuccessResponse | ErrorResponse, + Error + >({ + queryFn: ({ signal }) => getK8sDeploymentsList(requestData, signal, headers), + ...options, + queryKey, + }); +};