From 28cf4271f56fa70c0066b11e6c993436a5a2cf8a Mon Sep 17 00:00:00 2001 From: amlannandy Date: Tue, 14 Jan 2025 22:58:32 +0530 Subject: [PATCH 01/32] feat: jobs implementation in k8s infra monitoring --- .../src/api/infraMonitoring/getK8sJobsList.ts | 72 +++ frontend/src/constants/reactQueryKeys.ts | 1 + .../InfraMonitoringK8s/InfraMonitoringK8s.tsx | 38 +- .../JobDetails/Events/JobEvents.styles.scss | 289 +++++++++ .../Jobs/JobDetails/Events/JobEvents.tsx | 360 +++++++++++ .../JobDetails/Events/NoEventsContainer.tsx | 16 + .../Jobs/JobDetails/Events/constants.ts | 65 ++ .../Jobs/JobDetails/Events/index.ts | 3 + .../Jobs/JobDetails/JobDetails.interfaces.ts | 7 + .../Jobs/JobDetails/JobDetails.styles.scss | 247 ++++++++ .../Jobs/JobDetails/JobDetails.tsx | 558 ++++++++++++++++++ .../Jobs/JobDetails/Logs/JobLogs.styles.scss | 133 +++++ .../Jobs/JobDetails/Logs/JobLogs.tsx | 216 +++++++ .../JobDetails/Logs/JobLogsDetailedView.tsx | 99 ++++ .../Jobs/JobDetails/Logs/NoLogsContainer.tsx | 16 + .../Jobs/JobDetails/Logs/constants.ts | 65 ++ .../Jobs/JobDetails/Logs/index.ts | 3 + .../JobDetails/Metrics/JobMetrics.styles.scss | 45 ++ .../Jobs/JobDetails/Metrics/JobMetrics.tsx | 140 +++++ .../Jobs/JobDetails/Metrics/constants.ts | 372 ++++++++++++ .../Jobs/JobDetails/Metrics/index.ts | 3 + .../JobDetails/Traces/JobTraces.styles.scss | 193 ++++++ .../Jobs/JobDetails/Traces/JobTraces.tsx | 199 +++++++ .../Jobs/JobDetails/Traces/constants.ts | 200 +++++++ .../Jobs/JobDetails/Traces/index.ts | 3 + .../Jobs/JobDetails/constants.ts | 6 + .../Jobs/JobDetails/index.ts | 3 + .../Jobs/K8sJobsList.styles.scss | 27 + .../InfraMonitoringK8s/Jobs/K8sJobsList.tsx | 502 ++++++++++++++++ .../InfraMonitoringK8s/Jobs/utils.tsx | 377 ++++++++++++ .../container/InfraMonitoringK8s/constants.ts | 35 +- .../infraMonitoring/useGetK8sJobsList.ts | 51 ++ 32 files changed, 4339 insertions(+), 5 deletions(-) create mode 100644 frontend/src/api/infraMonitoring/getK8sJobsList.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Events/JobEvents.styles.scss create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Events/JobEvents.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Events/NoEventsContainer.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Events/constants.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Events/index.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails.interfaces.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails.styles.scss create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Logs/JobLogs.styles.scss create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Logs/JobLogs.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Logs/JobLogsDetailedView.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Logs/NoLogsContainer.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Logs/constants.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Logs/index.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Metrics/JobMetrics.styles.scss create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Metrics/JobMetrics.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Metrics/constants.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Metrics/index.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Traces/JobTraces.styles.scss create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Traces/JobTraces.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Traces/constants.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Traces/index.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/constants.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/index.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/K8sJobsList.styles.scss create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/K8sJobsList.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/Jobs/utils.tsx create mode 100644 frontend/src/hooks/infraMonitoring/useGetK8sJobsList.ts diff --git a/frontend/src/api/infraMonitoring/getK8sJobsList.ts b/frontend/src/api/infraMonitoring/getK8sJobsList.ts new file mode 100644 index 0000000000..19c0b4463a --- /dev/null +++ b/frontend/src/api/infraMonitoring/getK8sJobsList.ts @@ -0,0 +1,72 @@ +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; + cpuRequest: number; + memoryRequest: number; + cpuLimit: number; + memoryLimit: number; + restarts: number; + desiredSuccessfulPods: number; + activePods: number; + failedPods: number; + successfulPods: number; + meta: { + k8s_cluster_name: string; + 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/constants/reactQueryKeys.ts b/frontend/src/constants/reactQueryKeys.ts index 30da4e1362..6296d600df 100644 --- a/frontend/src/constants/reactQueryKeys.ts +++ b/frontend/src/constants/reactQueryKeys.ts @@ -26,4 +26,5 @@ export const REACT_QUERY_KEY = { GET_DEPLOYMENT_LIST: 'GET_DEPLOYMENT_LIST', GET_CLUSTER_LIST: 'GET_CLUSTER_LIST', GET_NAMESPACE_LIST: 'GET_NAMESPACE_LIST', + GET_JOB_LIST: 'GET_JOB_LIST', }; diff --git a/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx b/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx index 5ba6995673..b89e3c0805 100644 --- a/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx +++ b/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx @@ -8,6 +8,7 @@ import QuickFilters from 'components/QuickFilters/QuickFilters'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations'; import { + Bolt, Boxes, Computer, Container, @@ -22,12 +23,14 @@ import K8sClustersList from './Clusters/K8sClustersList'; import { ClustersQuickFiltersConfig, DeploymentsQuickFiltersConfig, + JobsQuickFiltersConfig, K8sCategories, NamespaceQuickFiltersConfig, NodesQuickFiltersConfig, PodsQuickFiltersConfig, } from './constants'; import K8sDeploymentsList from './Deployments/K8sDeploymentsList'; +import K8sJobsList from './Jobs/K8sJobsList'; import K8sNamespacesList from './Namespaces/K8sNamespacesList'; import K8sNodesList from './Nodes/K8sNodesList'; import K8sPodLists from './Pods/K8sPodLists'; @@ -209,22 +212,42 @@ export default function InfraMonitoringK8s(): JSX.Element { // label: ( //
//
- // - // Jobs + // + // Deployments //
//
// ), - // key: K8sCategories.JOBS, + // key: K8sCategories.DEPLOYMENTS, // showArrow: false, // children: ( // // ), // }, + { + label: ( +
+
+ + Jobs +
+
+ ), + key: K8sCategories.JOBS, + showArrow: false, + children: ( + + ), + }, // { // label: ( //
@@ -350,6 +373,13 @@ export default function InfraMonitoringK8s(): JSX.Element { quickFiltersLastUpdated={quickFiltersLastUpdated} /> )} + + {selectedCategory === K8sCategories.JOBS && ( + + )}
diff --git a/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Events/JobEvents.styles.scss b/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Events/JobEvents.styles.scss new file mode 100644 index 0000000000..8ce4a77e4f --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Events/JobEvents.styles.scss @@ -0,0 +1,289 @@ +.job-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; + } + } + } + + .job-events-header { + display: flex; + justify-content: space-between; + gap: 8px; + + padding: 12px; + border-radius: 3px; + border: 1px solid var(--bg-slate-500); + } + + .job-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(.jobname-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(.jobname-column-value) { + background: var(--bg-ink-400); + } + + .jobname-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: 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; + } + } + } + } +} + +.job-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; + } +} + +.job-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/Jobs/JobDetails/Events/JobEvents.tsx b/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Events/JobEvents.tsx new file mode 100644 index 0000000000..77ffddc43a --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Events/JobEvents.tsx @@ -0,0 +1,360 @@ +/* eslint-disable no-nested-ternary */ +import './JobEvents.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { Button, Table, TableColumnsType } from 'antd'; +import { DEFAULT_ENTITY_VERSION } from 'constants/app'; +import { EventContents } from 'container/InfraMonitoringK8s/commonUtils'; +import LoadingContainer from 'container/InfraMonitoringK8s/LoadingContainer'; +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 { isArray } from 'lodash-es'; +import { ChevronDown, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react'; +import { useEffect, useMemo, useState } from 'react'; +import { useQuery } from 'react-query'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; +import { DataSource } from 'types/common/queryBuilder'; +import { v4 } from 'uuid'; + +import { getJobsEventsQueryPayload } from './constants'; +import NoEventsContainer from './NoEventsContainer'; + +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; + severity?: string; +} + +interface IJobEventsProps { + timeRange: { + startTime: number; + endTime: number; + }; + handleChangeEventFilters: (filters: IBuilderQuery['filters']) => void; + filters: IBuilderQuery['filters']; + isModalTimeSelection: boolean; + handleTimeChange: ( + interval: Time | CustomTimeType, + dateTimeRange?: [number, number], + ) => void; + selectedInterval: Time; +} + +const EventsPageSize = 10; + +export default function Events({ + timeRange, + handleChangeEventFilters, + filters, + isModalTimeSelection, + handleTimeChange, + selectedInterval, +}: IJobEventsProps): JSX.Element { + const { currentQuery } = useQueryBuilder(); + + const [formattedJobEvents, setFormattedJobEvents] = useState( + [], + ); + + const [hasReachedEndOfEvents, setHasReachedEndOfEvents] = useState(false); + + const [page, setPage] = useState(1); + + const updatedCurrentQuery = useMemo( + () => ({ + ...currentQuery, + builder: { + ...currentQuery.builder, + queryData: [ + { + ...currentQuery.builder.queryData[0], + dataSource: DataSource.LOGS, + aggregateOperator: 'noop', + aggregateAttribute: { + ...currentQuery.builder.queryData[0].aggregateAttribute, + }, + filters: { + items: [], + op: 'AND', + }, + }, + ], + }, + }), + [currentQuery], + ); + + const query = updatedCurrentQuery?.builder?.queryData[0] || null; + + const queryPayload = useMemo(() => { + const basePayload = getJobsEventsQueryPayload( + timeRange.startTime, + timeRange.endTime, + filters, + ); + + basePayload.query.builder.queryData[0].pageSize = 10; + basePayload.query.builder.queryData[0].orderBy = [ + { columnName: 'timestamp', order: ORDERBY_FILTERS.DESC }, + ]; + + return basePayload; + }, [timeRange.startTime, timeRange.endTime, filters]); + + const { data: eventsData, isLoading, isFetching, isError } = useQuery({ + queryKey: ['jobEvents', timeRange.startTime, timeRange.endTime, filters], + queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION), + enabled: !!queryPayload, + }); + + const columns: TableColumnsType = [ + { title: 'Severity', dataIndex: 'severity', key: 'severity', width: 100 }, + { + title: 'Timestamp', + dataIndex: 'timestamp', + width: 200, + ellipsis: true, + key: 'timestamp', + }, + { title: 'Body', dataIndex: 'body', key: 'body' }, + ]; + + useEffect(() => { + if (eventsData?.payload?.data?.newResult?.data?.result) { + const responsePayload = + eventsData?.payload.data.newResult.data.result[0].list || []; + + const formattedData = responsePayload?.map( + (event): EventDataType => ({ + timestamp: event.timestamp, + severity: event.data.severity_text, + body: event.data.body, + id: event.data.id, + key: event.data.id, + resources_string: event.data.resources_string, + attributes_string: event.data.attributes_string, + }), + ); + + setFormattedJobEvents(formattedData); + + if ( + !responsePayload || + (responsePayload && + isArray(responsePayload) && + responsePayload.length < EventsPageSize) + ) { + setHasReachedEndOfEvents(true); + } else { + setHasReachedEndOfEvents(false); + } + } + }, [eventsData]); + + const handleExpandRow = (record: EventDataType): JSX.Element => ( + + ); + + const handlePrev = (): void => { + if (!formattedJobEvents.length) return; + + setPage(page - 1); + + const firstEvent = formattedJobEvents[0]; + + const newItems = [ + ...filters.items.filter((item) => item.key?.key !== 'id'), + { + id: v4(), + key: { + key: 'id', + type: '', + dataType: DataTypes.String, + isColumn: true, + }, + op: '>', + value: firstEvent.id, + }, + ]; + + const newFilters = { + op: 'AND', + items: newItems, + } as IBuilderQuery['filters']; + + handleChangeEventFilters(newFilters); + }; + + const handleNext = (): void => { + if (!formattedJobEvents.length) return; + + setPage(page + 1); + const lastEvent = formattedJobEvents[formattedJobEvents.length - 1]; + + const newItems = [ + ...filters.items.filter((item) => item.key?.key !== 'id'), + { + id: v4(), + key: { + key: 'id', + type: '', + dataType: DataTypes.String, + isColumn: true, + }, + op: '<', + value: lastEvent.id, + }, + ]; + + const newFilters = { + op: 'AND', + items: newItems, + } as IBuilderQuery['filters']; + + handleChangeEventFilters(newFilters); + }; + + 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 && } + + {!isLoading && !isError && formattedJobEvents.length === 0 && ( + + )} + + {isError && !isLoading && } + + {!isLoading && !isError && formattedJobEvents.length > 0 && ( +
+
+ + loading={isLoading && page > 1} + columns={columns} + expandable={{ + expandedRowRender: handleExpandRow, + rowExpandable: (record): boolean => record.body !== 'Not Expandable', + expandIcon: handleExpandRowIcon, + }} + dataSource={formattedJobEvents} + pagination={false} + rowKey={(record): string => record.id} + /> +
+
+ )} + + {!isError && formattedJobEvents.length > 0 && ( +
+ + + + + {(isFetching || isLoading) && ( + + )} +
+ )} +
+ ); +} diff --git a/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Events/NoEventsContainer.tsx b/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Events/NoEventsContainer.tsx new file mode 100644 index 0000000000..4c799b30da --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Events/NoEventsContainer.tsx @@ -0,0 +1,16 @@ +import { Color } from '@signozhq/design-tokens'; +import { Typography } from 'antd'; +import { Ghost } from 'lucide-react'; + +const { Text } = Typography; + +export default function NoEventsContainer(): React.ReactElement { + return ( +
+ + No events found for this job + in the selected time range. + +
+ ); +} diff --git a/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Events/constants.ts b/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Events/constants.ts new file mode 100644 index 0000000000..5e3a959c85 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/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 getJobsEventsQueryPayload = ( + 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/Jobs/JobDetails/Events/index.ts b/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Events/index.ts new file mode 100644 index 0000000000..ee232136a7 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/Events/index.ts @@ -0,0 +1,3 @@ +import JobEvents from './JobEvents'; + +export default JobEvents; diff --git a/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails.interfaces.ts b/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails.interfaces.ts new file mode 100644 index 0000000000..c86821258f --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails.interfaces.ts @@ -0,0 +1,7 @@ +import { K8sJobsData } from 'api/infraMonitoring/getK8sJobsList'; + +export type JobDetailsProps = { + job: K8sJobsData | null; + isModalTimeSelection: boolean; + onClose: () => void; +}; diff --git a/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails.styles.scss b/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails.styles.scss new file mode 100644 index 0000000000..7bde889eb1 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails.styles.scss @@ -0,0 +1,247 @@ +.job-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); + } + + .job-detail-drawer__job { + .job-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; + } + + .job-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; + } + + .job-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); + } + + .job-detail-drawer { + .title { + color: var(--text-ink-300); + } + + .job-detail-drawer__job { + .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/Jobs/JobDetails/JobDetails.tsx b/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails.tsx new file mode 100644 index 0000000000..880bc10686 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails.tsx @@ -0,0 +1,558 @@ +/* eslint-disable sonarjs/no-identical-functions */ +import './JobDetails.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 { VIEW_TYPES, VIEWS } from 'components/HostMetricsDetail/constants'; +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 } from './constants'; +import JobEvents from './Events'; +import { JobDetailsProps } from './JobDetails.interfaces'; +import JobLogs from './Logs'; +import JobMetrics from './Metrics'; +import JobTraces from './Traces'; + +function JobDetails({ + job, + onClose, + isModalTimeSelection, +}: JobDetailsProps): 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