From 458cd28cc2b136ca8b60d883d28b4a1f80ace934 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Wed, 8 Jan 2025 16:13:54 +0530 Subject: [PATCH] feat: pods and nodes implementation for k8s infra monitoring (#6558) --- frontend/jest.config.ts | 2 +- frontend/public/locales/en-GB/titles.json | 3 +- frontend/public/locales/en/titles.json | 3 +- frontend/src/AppRoutes/routes.ts | 7 + .../src/api/infra/getHostAttributeKeys.ts | 9 +- .../getInfraAttributeValues.ts | 2 + .../api/infraMonitoring/getK8sNodesList.ts | 65 + .../src/api/infraMonitoring/getK8sPodsList.ts | 93 + .../components/HostMetricsDetail/constants.ts | 2 + .../Checkbox/Checkbox.styles.scss | 2 +- .../FilterRenderers/Checkbox/Checkbox.tsx | 38 +- .../QuickFilters/QuickFilters.styles.scss | 4 + .../components/QuickFilters/QuickFilters.tsx | 72 +- frontend/src/constants/reactQueryKeys.ts | 1 + frontend/src/constants/routes.ts | 1 + frontend/src/container/AppLayout/index.tsx | 7 +- .../InfraMonitoringHosts/HostsList.tsx | 3 +- .../HostsListControls.tsx | 2 + .../InfraMonitoring.styles.scss | 5 +- .../container/InfraMonitoringHosts/utils.tsx | 1 + .../InfraMonitoringK8s.styles.scss | 834 +++++ .../InfraMonitoringK8s/InfraMonitoringK8s.tsx | 319 ++ .../K8sEmptyOrIncorrectMetrics.tsx | 32 + .../K8sFiltersSidePanel.styles.scss | 114 + .../K8sFiltersSidePanel.tsx | 129 + .../InfraMonitoringK8s/K8sHeader.tsx | 165 + .../InfraMonitoringK8s/LoadingContainer.tsx | 30 + .../Nodes/K8sNodesList.styles.scss | 17 + .../InfraMonitoringK8s/Nodes/K8sNodesList.tsx | 502 ++++ .../NodeDetails/Events/NoEventsContainer.tsx | 16 + .../NodeDetails/Events/NodeEvents.styles.scss | 289 ++ .../Nodes/NodeDetails/Events/NodeEvents.tsx | 357 +++ .../Nodes/NodeDetails/Events/constants.ts | 65 + .../Nodes/NodeDetails/Events/index.ts | 3 + .../NodeDetails/Logs/NoLogsContainer.tsx | 16 + .../NodeDetails/Logs/NodeLogs.styles.scss | 133 + .../Nodes/NodeDetails/Logs/NodeLogs.tsx | 216 ++ .../NodeDetails/Logs/NodeLogsDetailedView.tsx | 99 + .../Nodes/NodeDetails/Logs/constants.ts | 65 + .../Nodes/NodeDetails/Logs/index.ts | 3 + .../Metrics/NodeMetrics.styles.scss | 45 + .../Nodes/NodeDetails/Metrics/NodeMetrics.tsx | 140 + .../Nodes/NodeDetails/Metrics/constants.ts | 1634 ++++++++++ .../Nodes/NodeDetails/Metrics/index.ts | 3 + .../NodeDetails/NodeDetails.interfaces.ts | 7 + .../Nodes/NodeDetails/NodeDetails.styles.scss | 247 ++ .../Nodes/NodeDetails/NodeDetails.tsx | 555 ++++ .../NodeDetails/Traces/NodeTraces.styles.scss | 193 ++ .../Nodes/NodeDetails/Traces/NodeTraces.tsx | 199 ++ .../Nodes/NodeDetails/Traces/constants.ts | 200 ++ .../Nodes/NodeDetails/Traces/index.ts | 3 + .../Nodes/NodeDetails/constants.ts | 6 + .../Nodes/NodeDetails/index.ts | 3 + .../InfraMonitoringK8s/Nodes/utils.tsx | 212 ++ .../InfraMonitoringK8s/Pods/K8sPodLists.tsx | 568 ++++ .../Pods/PodDetails/Events/Events.styles.scss | 322 ++ .../Pods/PodDetails/Events/Events.tsx | 357 +++ .../PodDetails/Events/NoEventsContainer.tsx | 16 + .../Pods/PodDetails/Events/constants.ts | 65 + .../PodDetails/Metrics/Metrics.styles.scss | 45 + .../Pods/PodDetails/Metrics/Metrics.tsx | 140 + .../Pods/PodDetails/PodDetail.interfaces.ts | 7 + .../Pods/PodDetails/PodDetails.styles.scss | 247 ++ .../Pods/PodDetails/PodDetails.tsx | 598 ++++ .../PodDetails/PodLogs/NoLogsContainer.tsx | 16 + .../PodDetails/PodLogs/PodLogs.styles.scss | 133 + .../Pods/PodDetails/PodLogs/PodLogs.tsx | 218 ++ .../PodLogs/PodLogsDetailedView.tsx | 99 + .../Pods/PodDetails/PodLogs/constants.ts | 65 + .../PodTraces/PodTraces.styles.scss | 193 ++ .../Pods/PodDetails/PodTraces/PodTraces.tsx | 201 ++ .../Pods/PodDetails/PodTraces/constants.ts | 200 ++ .../Pods/PodDetails/constants.ts | 7 + .../Pods/PodDetails/index.tsx | 3 + .../InfraMonitoringK8s/Pods/constants.ts | 2672 +++++++++++++++++ .../InfraMonitoringK8s/commonUtils.tsx | 165 + .../container/InfraMonitoringK8s/constants.ts | 329 ++ .../container/InfraMonitoringK8s/index.tsx | 3 + .../container/InfraMonitoringK8s/utils.tsx | 403 +++ .../ListOfDashboard/DashboardList.styles.scss | 2 +- .../QueryBuilderSearch/OptionRenderer.tsx | 2 +- .../filters/QueryBuilderSearch/index.tsx | 9 + frontend/src/container/SideNav/config.ts | 4 + .../TopNav/DateTimeSelectionV2/config.ts | 1 + .../TopNav/DateTimeSelectionV2/index.tsx | 8 +- .../infraMonitoring/useGetK8sNodesList.ts | 45 + .../infraMonitoring/useGetK8sPodsList.ts | 45 + .../src/hooks/queryBuilder/useAutoComplete.ts | 3 + .../queryBuilder/useFetchKeysAndValues.ts | 27 +- .../hooks/queryBuilder/useGetAggregateKeys.ts | 19 +- .../InfrastructureMonitoring.styles.scss | 5 + .../InfrastructureMonitoringPage.tsx | 4 +- .../InfrastructureMonitoring/constants.tsx | 12 + frontend/src/periscope.scss | 7 + .../queryBuilder/queryAutocompleteResponse.ts | 4 +- frontend/src/utils/permission/index.ts | 1 + 96 files changed, 14376 insertions(+), 67 deletions(-) create mode 100644 frontend/src/api/infraMonitoring/getK8sNodesList.ts create mode 100644 frontend/src/api/infraMonitoring/getK8sPodsList.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.styles.scss create mode 100644 frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/K8sEmptyOrIncorrectMetrics.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/K8sFiltersSidePanel/K8sFiltersSidePanel.styles.scss create mode 100644 frontend/src/container/InfraMonitoringK8s/K8sFiltersSidePanel/K8sFiltersSidePanel.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/K8sHeader.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/LoadingContainer.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/K8sNodesList.styles.scss create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/K8sNodesList.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/Events/NoEventsContainer.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/Events/NodeEvents.styles.scss create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/Events/NodeEvents.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/Events/constants.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/Events/index.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/Logs/NoLogsContainer.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/Logs/NodeLogs.styles.scss create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/Logs/NodeLogs.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/Logs/NodeLogsDetailedView.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/Logs/constants.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/Logs/index.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/Metrics/NodeMetrics.styles.scss create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/Metrics/NodeMetrics.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/Metrics/constants.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/Metrics/index.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/NodeDetails.interfaces.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/NodeDetails.styles.scss create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/NodeDetails.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/Traces/NodeTraces.styles.scss create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/Traces/NodeTraces.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/Traces/constants.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/Traces/index.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/constants.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/index.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Nodes/utils.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Pods/K8sPodLists.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/Events.styles.scss create mode 100644 frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/Events.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/NoEventsContainer.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/constants.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Metrics/Metrics.styles.scss create mode 100644 frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Metrics/Metrics.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetail.interfaces.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetails.styles.scss create mode 100644 frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetails.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/NoLogsContainer.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/PodLogs.styles.scss create mode 100644 frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/PodLogs.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/PodLogsDetailedView.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/constants.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodTraces/PodTraces.styles.scss create mode 100644 frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodTraces/PodTraces.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodTraces/constants.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/constants.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/index.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Pods/constants.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/commonUtils.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/constants.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/index.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/utils.tsx create mode 100644 frontend/src/hooks/infraMonitoring/useGetK8sNodesList.ts create mode 100644 frontend/src/hooks/infraMonitoring/useGetK8sPodsList.ts diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts index 122b309dae..c96c94ef63 100644 --- a/frontend/jest.config.ts +++ b/frontend/jest.config.ts @@ -24,7 +24,7 @@ const config: Config.InitialOptions = { '^.+\\.(js|jsx)$': 'babel-jest', }, transformIgnorePatterns: [ - 'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|d3-interpolate|d3-color)/)', + 'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|d3-interpolate|d3-color|api)/)', ], setupFilesAfterEnv: ['jest.setup.ts'], testPathIgnorePatterns: ['/node_modules/', '/public/'], diff --git a/frontend/public/locales/en-GB/titles.json b/frontend/public/locales/en-GB/titles.json index bdee0edae8..d7d9fe57cb 100644 --- a/frontend/public/locales/en-GB/titles.json +++ b/frontend/public/locales/en-GB/titles.json @@ -43,5 +43,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 e2adcac351..766352a44a 100644 --- a/frontend/public/locales/en/titles.json +++ b/frontend/public/locales/en/titles.json @@ -56,5 +56,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 0b4899c4a1..dda546167f 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -415,6 +415,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/infra/getHostAttributeKeys.ts b/frontend/src/api/infra/getHostAttributeKeys.ts index 4ab03ea8eb..1a34cd4479 100644 --- a/frontend/src/api/infra/getHostAttributeKeys.ts +++ b/frontend/src/api/infra/getHostAttributeKeys.ts @@ -2,6 +2,7 @@ import { ApiBaseInstance } from 'api'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { AxiosError, AxiosResponse } from 'axios'; import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder'; +import { K8sCategory } from 'container/InfraMonitoringK8s/constants'; import { createIdFromObjectFields } from 'lib/createIdFromObjectFields'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { @@ -11,12 +12,18 @@ import { export const getHostAttributeKeys = async ( searchText = '', + entity: K8sCategory, ): Promise | ErrorResponse> => { try { const response: AxiosResponse<{ data: IQueryAutocompleteResponse; }> = await ApiBaseInstance.get( - `/hosts/attribute_keys?dataSource=metrics&searchText=${searchText}`, + `/${entity}/attribute_keys?dataSource=metrics&searchText=${searchText}`, + { + params: { + limit: 500, + }, + }, ); const payload: BaseAutocompleteData[] = diff --git a/frontend/src/api/infraMonitoring/getInfraAttributeValues.ts b/frontend/src/api/infraMonitoring/getInfraAttributeValues.ts index 10488af132..c1a9531b84 100644 --- a/frontend/src/api/infraMonitoring/getInfraAttributeValues.ts +++ b/frontend/src/api/infraMonitoring/getInfraAttributeValues.ts @@ -14,6 +14,7 @@ export const getInfraAttributesValues = async ({ filterAttributeKeyDataType, tagType, searchText, + aggregateAttribute, }: IGetAttributeValuesPayload): Promise< SuccessResponse | ErrorResponse > => { @@ -23,6 +24,7 @@ export const getInfraAttributesValues = async ({ dataSource, attributeKey, searchText, + aggregateAttribute, })}&filterAttributeKeyDataType=${filterAttributeKeyDataType}&tagType=${tagType}`, ); diff --git a/frontend/src/api/infraMonitoring/getK8sNodesList.ts b/frontend/src/api/infraMonitoring/getK8sNodesList.ts new file mode 100644 index 0000000000..e3c411b1d0 --- /dev/null +++ b/frontend/src/api/infraMonitoring/getK8sNodesList.ts @@ -0,0 +1,65 @@ +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; + k8s_cluster_name: 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/HostMetricsDetail/constants.ts b/frontend/src/components/HostMetricsDetail/constants.ts index 490ecbb4d3..e024fe5b55 100644 --- a/frontend/src/components/HostMetricsDetail/constants.ts +++ b/frontend/src/components/HostMetricsDetail/constants.ts @@ -4,6 +4,7 @@ export enum VIEWS { TRACES = 'traces', CONTAINERS = 'containers', PROCESSES = 'processes', + EVENTS = 'events', } export const VIEW_TYPES = { @@ -12,4 +13,5 @@ export const VIEW_TYPES = { TRACES: VIEWS.TRACES, CONTAINERS: VIEWS.CONTAINERS, PROCESSES: VIEWS.PROCESSES, + EVENTS: VIEWS.EVENTS, }; 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/FilterRenderers/Checkbox/Checkbox.tsx b/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.tsx index bd8b9b1d38..8b48a55dde 100644 --- a/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.tsx +++ b/frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.tsx @@ -8,10 +8,12 @@ import { Button, Checkbox, Input, Skeleton, Typography } from 'antd'; import cx from 'classnames'; import { IQuickFiltersConfig } from 'components/QuickFilters/QuickFilters'; import { OPERATORS } from 'constants/queryBuilder'; +import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig'; import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils'; import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; -import { cloneDeep, isArray, isEmpty, isEqual } from 'lodash-es'; +import useDebouncedFn from 'hooks/useDebouncedFunction'; +import { cloneDeep, isArray, isEmpty, isEqual, isFunction } from 'lodash-es'; import { ChevronDown, ChevronRight } from 'lucide-react'; import { useMemo, useState } from 'react'; import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; @@ -34,10 +36,11 @@ function setDefaultValues( } interface ICheckboxProps { filter: IQuickFiltersConfig; + onFilterChange?: (query: Query) => void; } export default function CheckboxFilter(props: ICheckboxProps): JSX.Element { - const { filter } = props; + const { filter, onFilterChange } = props; const [searchText, setSearchText] = useState(''); const [isOpen, setIsOpen] = useState(filter.defaultOpen); const [visibleItemsCount, setVisibleItemsCount] = useState(10); @@ -50,9 +53,9 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element { const { data, isLoading } = useGetAggregateValues( { - aggregateOperator: 'noop', - dataSource: DataSource.LOGS, - aggregateAttribute: '', + aggregateOperator: filter.aggregateOperator || 'noop', + dataSource: filter.dataSource || DataSource.LOGS, + aggregateAttribute: filter.aggregateAttribute || '', attributeKey: filter.attributeKey.key, filterAttributeKeyDataType: filter.attributeKey.dataType || DataTypes.EMPTY, tagType: filter.attributeKey.type || '', @@ -72,7 +75,11 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element { ); const currentAttributeKeys = attributeValues.slice(0, visibleItemsCount); - // derive the state of each filter key here in the renderer itself and keep it in sync with staged query + const setSearchTextDebounced = useDebouncedFn((...args) => { + setSearchText(args[0] as string); + }, DEBOUNCE_DELAY); + + // derive the state of each filter key here in the renderer itself and keep it in sync with current query // also we need to keep a note of last focussed query. // eslint-disable-next-line sonarjs/cognitive-complexity const currentFilterState = useMemo(() => { @@ -159,7 +166,12 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element { })), }, }; - redirectWithQueryBuilderData(preparedQuery); + + if (onFilterChange && isFunction(onFilterChange)) { + onFilterChange(preparedQuery); + } else { + redirectWithQueryBuilderData(preparedQuery); + } }; const isSomeFilterPresentForCurrentAttribute = currentQuery.builder.queryData?.[ @@ -391,7 +403,11 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element { }, }; - redirectWithQueryBuilderData(finalQuery); + if (onFilterChange && isFunction(onFilterChange)) { + onFilterChange(finalQuery); + } else { + redirectWithQueryBuilderData(finalQuery); + } }; return ( @@ -440,7 +456,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
setSearchText(e.target.value)} + onChange={(e): void => setSearchTextDebounced(e.target.value)} disabled={isFilterDisabled} />
@@ -511,3 +527,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element { ); } + +CheckboxFilter.defaultProps = { + onFilterChange: null, +}; 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..bc367b58fc 100644 --- a/frontend/src/components/QuickFilters/QuickFilters.tsx +++ b/frontend/src/components/QuickFilters/QuickFilters.tsx @@ -7,9 +7,10 @@ import { } from '@ant-design/icons'; import { Tooltip, Typography } from 'antd'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; -import { cloneDeep } from 'lodash-es'; +import { cloneDeep, isFunction } from 'lodash-es'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { DataSource } from 'types/common/queryBuilder'; import Checkbox from './FilterRenderers/Checkbox/Checkbox'; import Slider from './FilterRenderers/Slider/Slider'; @@ -33,6 +34,9 @@ export interface IQuickFiltersConfig { type: FiltersType; title: string; attributeKey: BaseAutocompleteData; + aggregateOperator?: string; + aggregateAttribute?: string; + dataSource?: DataSource; customRendererForValue?: (value: string) => JSX.Element; defaultOpen: boolean; } @@ -40,10 +44,12 @@ export interface IQuickFiltersConfig { interface IQuickFiltersProps { config: IQuickFiltersConfig[]; handleFilterVisibilityChange: () => void; + source?: string | null; + onFilterChange?: (query: Query) => void; } export default function QuickFilters(props: IQuickFiltersProps): JSX.Element { - const { config, handleFilterVisibilityChange } = props; + const { config, handleFilterVisibilityChange, source, onFilterChange } = props; const { currentQuery, @@ -78,47 +84,63 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element { })), }, }; - redirectWithQueryBuilderData(preparedQuery); + + if (onFilterChange && isFunction(onFilterChange)) { + onFilterChange(preparedQuery); + } else { + redirectWithQueryBuilderData(preparedQuery); + } }; const lastQueryName = currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName; + + const isInfraMonitoring = source === 'infra-monitoring'; + return (
-
-
- - Filters for - - {lastQueryName} - + {!isInfraMonitoring && ( +
+
+ + Filters for + + {lastQueryName} + +
+ +
+ + + +
+ + + +
-
- - - -
- - - -
-
+ )}
{config.map((filter) => { switch (filter.type) { case FiltersType.CHECKBOX: - return ; + return ; case FiltersType.SLIDER: return ; default: - return ; + return ; } })}
); } + +QuickFilters.defaultProps = { + source: null, + onFilterChange: null, +}; diff --git a/frontend/src/constants/reactQueryKeys.ts b/frontend/src/constants/reactQueryKeys.ts index 36cf99157e..5520872201 100644 --- a/frontend/src/constants/reactQueryKeys.ts +++ b/frontend/src/constants/reactQueryKeys.ts @@ -21,4 +21,5 @@ export const REACT_QUERY_KEY = { GET_HOST_LIST: 'GET_HOST_LIST', UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE', GET_ACTIVE_LICENSE_V3: 'GET_ACTIVE_LICENSE_V3', + GET_POD_LIST: 'GET_POD_LIST', }; diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index b31accd613..a5a5212eb7 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -62,6 +62,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 64140567ff..8bb91d48c3 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -292,8 +292,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 => @@ -422,7 +423,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/HostsListControls.tsx b/frontend/src/container/InfraMonitoringHosts/HostsListControls.tsx index 359248bb2a..ad07d1f2d7 100644 --- a/frontend/src/container/InfraMonitoringHosts/HostsListControls.tsx +++ b/frontend/src/container/InfraMonitoringHosts/HostsListControls.tsx @@ -1,5 +1,6 @@ import './InfraMonitoring.styles.scss'; +import { K8sCategory } from 'container/InfraMonitoringK8s/constants'; import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch'; import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; @@ -47,6 +48,7 @@ function HostsListControls({ onChange={handleChangeTagFilters} isInfraMonitoring disableNavigationShortcuts + entity={K8sCategory.HOSTS} />
diff --git a/frontend/src/container/InfraMonitoringHosts/InfraMonitoring.styles.scss b/frontend/src/container/InfraMonitoringHosts/InfraMonitoring.styles.scss index c1594d4929..8066481a4f 100644 --- a/frontend/src/container/InfraMonitoringHosts/InfraMonitoring.styles.scss +++ b/frontend/src/container/InfraMonitoringHosts/InfraMonitoring.styles.scss @@ -93,7 +93,7 @@ } .hostname-column-value { - color: var(--Vanilla-100, #fff); + color: var(--bg-vanilla-100); font-family: 'Geist Mono'; font-style: normal; font-weight: 600; @@ -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..26867ba094 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.styles.scss @@ -0,0 +1,834 @@ +.k8s-container { + display: flex; + flex-direction: row; + height: calc(100vh - 45px); + width: 100%; + + .k8s-quick-filters-container { + width: 280px; + min-width: 280px; + border-right: 1px solid var(--bg-slate-400); + overflow-y: auto; + + .k8s-quick-filters-container-header { + padding: 8px; + border-bottom: 1px solid var(--bg-slate-400); + + display: flex; + align-items: center; + justify-content: space-between; + } + + &::-webkit-scrollbar { + width: 0.1rem; + height: 0.1rem; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: var(--bg-ink-200); + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--bg-ink-100); + } + + .ant-collapse-header { + border-bottom: 1px solid var(--bg-slate-400); + padding: 12px 8px; + + &[aria-expanded='true'] { + background: var(--bg-ink-400); + } + } + + .ant-collapse-content-box { + padding: 0px; + padding-block: 0px !important; + + .quick-filters { + .checkbox-filter { + padding-left: 18px; + } + } + } + + .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-quick-filters-category-label { + display: flex; + align-items: center; + gap: 4px; + + .k8s-quick-filters-category-label-icon { + margin-right: 8px; + } + + .k8s-quick-filters-category-label-container { + display: flex; + align-items: center; + gap: 4px; + } + } + } + + .k8s-list-container { + flex: 1; + + max-width: 100%; + + &.k8s-list-container-filters-visible { + max-width: calc(100% - 280px); + } + } + + .periscope-btn { + &.ghost:not(:disabled) { + border: none; + background: transparent; + + &:hover { + background: transparent; + color: var(--bg-robin-500) !important; + font-weight: 500; + } + } + } + + .column-header { + display: flex; + align-items: center; + gap: 16px; + font-size: 11px; + + &.pod-group-header { + padding-left: 12px; + } + } +} + +.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 { + .column { + min-width: 180px; + max-width: 180px; + + font-size: 12px !important; + } + + .column-pod-name { + min-width: 200px; + max-width: 200px; + } + + .column-pod-group { + min-width: 200px; + max-width: 200px; + } + + .column-progress-bar { + min-width: 180px; + max-width: 180px; + } + + .column-dummy { + min-width: 32px; + max-width: 32px; + } + + .pod-group { + display: flex; + align-items: center; + gap: 4px; + flex-wrap: wrap; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .pod-group-tag-item { + border-radius: 2px; + font-size: 12px; + font-weight: 400; + background: var(--bg-slate-400); + color: var(--bg-vanilla-400); + + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .pod-name { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .ant-table-container { + .ant-table-row-expand-icon-cell { + padding: 0px; + } + + .ant-table-content { + &::-webkit-scrollbar { + width: 0.1rem; + height: 0.1rem; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: var(--bg-robin-500); + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--bg-robin-400); + } + } + } + } + + .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; + flex-wrap: wrap; + gap: 8px; + + .k8s-qb-search-container { + flex: 1; + min-width: 240px; + max-width: 60%; + } + + .k8s-attribute-search-container { + flex: 1; + min-width: 240px; + max-width: 40%; + display: flex; + align-items: center; + + .group-by-label { + min-width: max-content; + + color: var(--bg-vanilla-100, #c0c1c3); + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + + border-radius: 2px 0px 0px 2px; + border: 1px solid var(--bg-slate-400, #1d212d); + border-right: none; + background: var(--bg-ink-100, #16181d); + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + + display: flex; + height: 32px; + padding: 6px 6px 6px 8px; + justify-content: center; + align-items: center; + gap: 4px; + } + + .group-by-select { + .ant-select-selector { + border-left: none; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + } + } + } + } + + .k8s-list-controls-right { + min-width: 240px; + + display: flex; + align-items: center; + gap: 4px; + } + + .periscope-btn { + padding: 4px 8px; + + &.ghost:not(:disabled) { + border: none; + background: transparent; + } + } + } + + .progress-container { + display: flex; + align-items: center; + justify-content: center; + } + + .entity-progress-bar { + display: flex; + align-items: center; + } + + .progress-bar { + flex: 1; + margin-right: 8px; + margin-bottom: 0px; + min-width: 100px; + } + + .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); + border-bottom: none; + } + + .ant-table-cell:has(.hostname-column-value) { + background: var(--bg-ink-400); + } + + .hostname-column-value { + color: var(--bg-vanilla-100); + 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; + } + } + } + } + + .ant-table-expanded-row { + &:hover { + background: var(--bg-ink-500); + } + + .ant-table-cell { + background: var(--bg-ink-500) !important; + } + + .ant-table .ant-table-thead > tr > th { + padding: 4px 16px !important; + } + } + + .expanded-table-container { + border: 1px solid var(--bg-ink-400); + overflow-x: auto; + padding-left: 16px; + + &::-webkit-scrollbar { + width: 0.1rem; + height: 0.1rem; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: var(--bg-ink-200); + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--bg-ink-100); + } + + .ant-table-expanded-row { + background: var(--bg-ink-500); + + &:hover { + background: var(--bg-ink-500); + } + + .ant-table-cell { + background: var(--bg-ink-500); + } + } + + .expanded-table-footer { + display: flex; + justify-content: flex-start; + gap: 8px; + padding: 8px; + padding-left: 42px; + margin-top: 8px; + + .periscope-btn { + font-size: 10px; + + display: flex; + align-items: center; + gap: 4px; + } + + .view-all-text { + font-size: 10px; + color: var(--bg-vanilla-400); + } + } + } + } + + .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; + } + } + } + } + } +} + +.ant-table-cell { + min-width: 170px !important; + max-width: 170px !important; +} + +.ant-table-row-expand-icon-cell { + min-width: 30px !important; + max-width: 30px !important; +} + +.event-content-container { + .ant-table { + background: var(--bg-ink-400); + + .ant-table-row:hover { + .ant-table-cell { + .value-field { + .action-btn { + display: flex; + position: absolute; + top: 50%; + right: 16px; + transform: translateY(-50%); + gap: 4px; + } + } + } + } + + .ant-table-cell { + border: 1px solid var(--bg-slate-500); + } + + .attribute-name { + .ant-btn { + &:hover { + background-color: none !important; + } + } + } + + .attribute-pin { + cursor: pointer; + + padding: 0; + vertical-align: middle; + text-align: center; + + .log-attribute-pin { + padding: 8px; + + display: flex; + justify-content: center; + align-items: center; + + .pin-attribute-icon { + border: none; + + &.pinned svg { + fill: var(--bg-robin-500); + } + } + } + } + + .value-field-container { + background: rgba(22, 25, 34, 0.4); + + .value-field { + font-family: 'Geist Mono'; + + position: relative; + } + + .action-btn { + display: none; + width: max-content; + position: absolute; + // padding: 0 16px; + right: 0; + + .filter-btn { + display: flex; + align-items: center; + border: none; + box-shadow: none; + border-radius: 2px; + background: var(--bg-slate-400); + padding: 2px 3px; + gap: 3px; + height: 18px; + width: 20px; + } + } + } + } +} + +.lightMode { + .event-content-container { + .ant-table { + background: var(--bg-vanilla-100); + } + + .ant-table-cell { + border: 1px solid var(--bg-vanilla-200); + } + + .value-field-container { + background: var(--bg-vanilla-300); + + &.attribute-pin { + background: var(--bg-vanilla-100); + } + + .action-btn { + .filter-btn { + background: var(--bg-vanilla-300); + } + } + } + } +} diff --git a/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx b/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx new file mode 100644 index 0000000000..f152182ca8 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx @@ -0,0 +1,319 @@ +import './InfraMonitoringK8s.styles.scss'; + +import { VerticalAlignTopOutlined } from '@ant-design/icons'; +import * as Sentry from '@sentry/react'; +import type { CollapseProps } from 'antd'; +import { Collapse, Tooltip, Typography } from 'antd'; +import QuickFilters from 'components/QuickFilters/QuickFilters'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations'; +import { Container, Workflow } from 'lucide-react'; +import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'; +import { useCallback, useState } from 'react'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; + +import { + K8sCategories, + NodesQuickFiltersConfig, + PodsQuickFiltersConfig, +} from './constants'; +import K8sNodesList from './Nodes/K8sNodesList'; +import K8sPodLists from './Pods/K8sPodLists'; + +export default function InfraMonitoringK8s(): JSX.Element { + const [showFilters, setShowFilters] = useState(true); + + const [selectedCategory, setSelectedCategory] = useState(K8sCategories.PODS); + + const { currentQuery } = useQueryBuilder(); + + const handleFilterVisibilityChange = (): void => { + setShowFilters(!showFilters); + }; + + const { handleChangeQueryData } = useQueryOperations({ + index: 0, + query: currentQuery.builder.queryData[0], + entityVersion: '', + }); + + const handleFilterChange = useCallback( + (query: Query): void => { + // update the current query with the new filters + // in infra monitoring k8s, we are using only one query, hence updating the 0th index of queryData + handleChangeQueryData('filters', query.builder.queryData[0].filters); + }, + [handleChangeQueryData], + ); + + const items: CollapseProps['items'] = [ + { + label: ( +
+
+ + Pods +
+
+ ), + key: K8sCategories.PODS, + showArrow: false, + children: ( + + ), + }, + { + label: ( +
+
+ + Nodes +
+
+ ), + key: K8sCategories.NODES, + showArrow: false, + children: ( + + ), + }, + // NOTE - Enabled these as we release new entities + // { + // label: ( + //
+ //
+ // + // Namespace + //
+ //
+ // ), + // key: K8sCategories.NAMESPACES, + // showArrow: false, + // children: ( + // + // ), + // }, + // { + // label: ( + //
+ //
+ // + // Clusters + //
+ //
+ // ), + // key: K8sCategories.CLUSTERS, + // showArrow: false, + // children: ( + // + // ), + // }, + // { + // label: ( + //
+ //
+ // + // Containers + //
+ //
+ // ), + // key: K8sCategories.CONTAINERS, + // showArrow: false, + // children: ( + // + // ), + // }, + // { + // label: ( + //
+ //
+ // + // Volumes + //
+ //
+ // ), + // key: K8sCategories.VOLUMES, + // showArrow: false, + // children: ( + // + // ), + // }, + // { + // label: ( + //
+ //
+ // + // Deployments + //
+ //
+ // ), + // key: K8sCategories.DEPLOYMENTS, + // showArrow: false, + // children: ( + // + // ), + // }, + // { + // label: ( + //
+ //
+ // + // Jobs + //
+ //
+ // ), + // key: K8sCategories.JOBS, + // showArrow: false, + // children: ( + // + // ), + // }, + // { + // label: ( + //
+ //
+ // + // DaemonSets + //
+ //
+ // ), + // key: K8sCategories.DAEMONSETS, + // showArrow: false, + // children: ( + // + // ), + // }, + // { + // label: ( + //
+ //
+ // + // StatefulSets + //
+ //
+ // ), + // key: K8sCategories.STATEFULSETS, + // showArrow: false, + // children: ( + // + // ), + // }, + ]; + + const handleCategoryChange = (key: string | string[]): void => { + if (Array.isArray(key) && key.length > 0) { + setSelectedCategory(key[0] as string); + } + }; + + return ( + }> +
+
+ {showFilters && ( +
+
+ Filters + + + + +
+ +
+ )} + +
+ {selectedCategory === K8sCategories.PODS && ( + + )} + + {selectedCategory === K8sCategories.NODES && ( + + )} +
+
+
+
+ ); +} 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..8062f338b0 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/K8sFiltersSidePanel/K8sFiltersSidePanel.tsx @@ -0,0 +1,129 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* 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'; + +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} +
+
+ ))} +
+
+
+
+
+
+ ); +} + +K8sFiltersSidePanel.defaultProps = { + addedColumns: [], + availableColumns: [], + onAddColumn: () => {}, + onRemoveColumn: () => {}, +}; + +export default K8sFiltersSidePanel; diff --git a/frontend/src/container/InfraMonitoringK8s/K8sHeader.tsx b/frontend/src/container/InfraMonitoringK8s/K8sHeader.tsx new file mode 100644 index 0000000000..748cb205b3 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/K8sHeader.tsx @@ -0,0 +1,165 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import './InfraMonitoringK8s.styles.scss'; + +import { Button, Select } 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 { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; + +import { K8sCategory } from './constants'; +import K8sFiltersSidePanel from './K8sFiltersSidePanel/K8sFiltersSidePanel'; +import { IPodColumn } from './utils'; + +interface K8sHeaderProps { + selectedGroupBy: BaseAutocompleteData[]; + groupByOptions: { value: string; label: string }[]; + isLoadingGroupByFilters: boolean; + handleFiltersChange: (value: IBuilderQuery['filters']) => void; + handleGroupByChange: (value: IBuilderQuery['groupBy']) => void; + defaultAddedColumns: IPodColumn[]; + addedColumns?: IPodColumn[]; + availableColumns?: IPodColumn[]; + onAddColumn?: (column: IPodColumn) => void; + onRemoveColumn?: (column: IPodColumn) => void; + handleFilterVisibilityChange: () => void; + isFiltersVisible: boolean; + entity: K8sCategory; +} + +function K8sHeader({ + selectedGroupBy, + defaultAddedColumns, + groupByOptions, + isLoadingGroupByFilters, + addedColumns, + availableColumns, + handleFiltersChange, + handleGroupByChange, + onAddColumn, + onRemoveColumn, + handleFilterVisibilityChange, + isFiltersVisible, + entity, +}: K8sHeaderProps): 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
+