diff --git a/frontend/public/Icons/broom.svg b/frontend/public/Icons/broom.svg new file mode 100644 index 0000000000..afb21213a4 --- /dev/null +++ b/frontend/public/Icons/broom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/Icons/infraContainers.svg b/frontend/public/Icons/infraContainers.svg new file mode 100644 index 0000000000..8dc7ad0de4 --- /dev/null +++ b/frontend/public/Icons/infraContainers.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/locales/en-GB/infraMonitoring.json b/frontend/public/locales/en-GB/infraMonitoring.json new file mode 100644 index 0000000000..edf1a5f8c8 --- /dev/null +++ b/frontend/public/locales/en-GB/infraMonitoring.json @@ -0,0 +1,7 @@ +{ + "containers_visualization_message": "The ability to visualise containers is in active development and should be available to you soon.", + "processes_visualization_message": "The ability to visualise processes is in active development and should be available to you soon.", + "working_message": "We're working to extend infrastructure monitoring to take care of a bunch of different cases. Thank you for your patience.", + "waitlist_message": "Join the waitlist for early access or contact support.", + "contact_support": "Contact Support" +} diff --git a/frontend/public/locales/en-GB/titles.json b/frontend/public/locales/en-GB/titles.json index 6cfe6e0238..c7e8736aba 100644 --- a/frontend/public/locales/en-GB/titles.json +++ b/frontend/public/locales/en-GB/titles.json @@ -40,5 +40,6 @@ "SUPPORT": "SigNoz | Support", "DEFAULT": "Open source Observability Platform | SigNoz", "ALERT_HISTORY": "SigNoz | Alert Rule History", - "ALERT_OVERVIEW": "SigNoz | Alert Rule Overview" + "ALERT_OVERVIEW": "SigNoz | Alert Rule Overview", + "INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring" } diff --git a/frontend/public/locales/en/infraMonitoring.json b/frontend/public/locales/en/infraMonitoring.json new file mode 100644 index 0000000000..edf1a5f8c8 --- /dev/null +++ b/frontend/public/locales/en/infraMonitoring.json @@ -0,0 +1,7 @@ +{ + "containers_visualization_message": "The ability to visualise containers is in active development and should be available to you soon.", + "processes_visualization_message": "The ability to visualise processes is in active development and should be available to you soon.", + "working_message": "We're working to extend infrastructure monitoring to take care of a bunch of different cases. Thank you for your patience.", + "waitlist_message": "Join the waitlist for early access or contact support.", + "contact_support": "Contact Support" +} diff --git a/frontend/public/locales/en/titles.json b/frontend/public/locales/en/titles.json index 126b8a7ac1..9d0a7cd650 100644 --- a/frontend/public/locales/en/titles.json +++ b/frontend/public/locales/en/titles.json @@ -52,5 +52,6 @@ "INTEGRATIONS": "SigNoz | Integrations", "ALERT_HISTORY": "SigNoz | Alert Rule History", "ALERT_OVERVIEW": "SigNoz | Alert Rule Overview", - "MESSAGING_QUEUES": "SigNoz | Messaging Queues" + "MESSAGING_QUEUES": "SigNoz | Messaging Queues", + "INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring" } diff --git a/frontend/src/AppRoutes/index.tsx b/frontend/src/AppRoutes/index.tsx index 8cb2bf0a8b..195873b2d0 100644 --- a/frontend/src/AppRoutes/index.tsx +++ b/frontend/src/AppRoutes/index.tsx @@ -85,6 +85,18 @@ function App(): JSX.Element { setRoutes(newRoutes); } + + const isInfraMonitoringEnabled = + allFlags.find((flag) => flag.name === FeatureKeys.HOSTS_INFRA_MONITORING) + ?.active || false; + + if (!isInfraMonitoringEnabled) { + const newRoutes = routes.filter( + (route) => route?.path !== ROUTES.INFRASTRUCTURE_MONITORING_HOSTS, + ); + + setRoutes(newRoutes); + } }); const isOnBasicPlan = diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index 0a7764149b..1a6478218a 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -224,3 +224,10 @@ export const MQDetailPage = Loadable( /* webpackChunkName: "MQDetailPage" */ 'pages/MessagingQueues/MQDetailPage' ), ); + +export const InfrastructureMonitoring = Loadable( + () => + import( + /* webpackChunkName: "InfrastructureMonitoring" */ 'pages/InfrastructureMonitoring' + ), +); diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index 42ce00c0fb..4565d76d20 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -15,6 +15,7 @@ import { EditAlertChannelsAlerts, EditRulesPage, ErrorDetails, + InfrastructureMonitoring, IngestionSettings, InstalledIntegrations, LicensePage, @@ -383,6 +384,13 @@ const routes: AppRoutes[] = [ key: 'MESSAGING_QUEUES_DETAIL', isPrivate: true, }, + { + path: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS, + exact: true, + component: InfrastructureMonitoring, + key: 'INFRASTRUCTURE_MONITORING_HOSTS', + isPrivate: true, + }, ]; export const SUPPORT_ROUTE: AppRoutes = { diff --git a/frontend/src/api/infra/getHostAttributeKeys.ts b/frontend/src/api/infra/getHostAttributeKeys.ts new file mode 100644 index 0000000000..4ab03ea8eb --- /dev/null +++ b/frontend/src/api/infra/getHostAttributeKeys.ts @@ -0,0 +1,37 @@ +import { ApiBaseInstance } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError, AxiosResponse } from 'axios'; +import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder'; +import { createIdFromObjectFields } from 'lib/createIdFromObjectFields'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { + BaseAutocompleteData, + IQueryAutocompleteResponse, +} from 'types/api/queryBuilder/queryAutocompleteResponse'; + +export const getHostAttributeKeys = async ( + searchText = '', +): Promise | ErrorResponse> => { + try { + const response: AxiosResponse<{ + data: IQueryAutocompleteResponse; + }> = await ApiBaseInstance.get( + `/hosts/attribute_keys?dataSource=metrics&searchText=${searchText}`, + ); + + const payload: BaseAutocompleteData[] = + response.data.data.attributeKeys?.map(({ id: _, ...item }) => ({ + ...item, + id: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder), + })) || []; + + return { + statusCode: 200, + error: null, + message: response.statusText, + payload: { attributeKeys: payload }, + }; + } catch (e) { + return ErrorResponseHandler(e as AxiosError); + } +}; diff --git a/frontend/src/api/infraMonitoring/getHostLists.ts b/frontend/src/api/infraMonitoring/getHostLists.ts new file mode 100644 index 0000000000..ce2ef9b544 --- /dev/null +++ b/frontend/src/api/infraMonitoring/getHostLists.ts @@ -0,0 +1,75 @@ +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 HostListPayload { + 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 HostData { + hostName: string; + active: boolean; + os: string; + cpu: number; + cpuTimeSeries: TimeSeries; + memory: number; + memoryTimeSeries: TimeSeries; + wait: number; + waitTimeSeries: TimeSeries; + load15: number; + load15TimeSeries: TimeSeries; +} + +export interface HostListResponse { + status: string; + data: { + type: string; + records: HostData[]; + groups: null; + total: number; + }; +} + +export const getHostLists = async ( + props: HostListPayload, + signal?: AbortSignal, + headers?: Record, +): Promise | ErrorResponse> => { + try { + const response = await ApiBaseInstance.post('/hosts/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/getInfraAttributeValues.ts b/frontend/src/api/infraMonitoring/getInfraAttributeValues.ts new file mode 100644 index 0000000000..10488af132 --- /dev/null +++ b/frontend/src/api/infraMonitoring/getInfraAttributeValues.ts @@ -0,0 +1,38 @@ +import { ApiBaseInstance } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import createQueryParams from 'lib/createQueryParams'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { + IAttributeValuesResponse, + IGetAttributeValuesPayload, +} from 'types/api/queryBuilder/getAttributesValues'; + +export const getInfraAttributesValues = async ({ + dataSource, + attributeKey, + filterAttributeKeyDataType, + tagType, + searchText, +}: IGetAttributeValuesPayload): Promise< + SuccessResponse | ErrorResponse +> => { + try { + const response = await ApiBaseInstance.get( + `/hosts/attribute_values?${createQueryParams({ + dataSource, + attributeKey, + searchText, + })}&filterAttributeKeyDataType=${filterAttributeKeyDataType}&tagType=${tagType}`, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; diff --git a/frontend/src/api/queryBuilder/getAttributeKeys.ts b/frontend/src/api/queryBuilder/getAttributeKeys.ts index 9cc127bb71..f9b84ea2a9 100644 --- a/frontend/src/api/queryBuilder/getAttributeKeys.ts +++ b/frontend/src/api/queryBuilder/getAttributeKeys.ts @@ -5,7 +5,6 @@ import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder'; import { createIdFromObjectFields } from 'lib/createIdFromObjectFields'; import createQueryParams from 'lib/createQueryParams'; import { ErrorResponse, SuccessResponse } from 'types/api'; -// ** Types import { IGetAttributeKeysPayload } from 'types/api/queryBuilder/getAttributeKeys'; import { BaseAutocompleteData, diff --git a/frontend/src/components/CustomTimePicker/CustomTimePicker.tsx b/frontend/src/components/CustomTimePicker/CustomTimePicker.tsx index a3bb980175..47ee89c880 100644 --- a/frontend/src/components/CustomTimePicker/CustomTimePicker.tsx +++ b/frontend/src/components/CustomTimePicker/CustomTimePicker.tsx @@ -120,7 +120,6 @@ function CustomTimePicker({ useEffect(() => { const value = getSelectedTimeRangeLabel(selectedTime, selectedValue); - setSelectedTimePlaceholderValue(value); }, [selectedTime, selectedValue]); diff --git a/frontend/src/components/HostMetricsDetail/Containers/Containers.styles.scss b/frontend/src/components/HostMetricsDetail/Containers/Containers.styles.scss new file mode 100644 index 0000000000..6542d748ea --- /dev/null +++ b/frontend/src/components/HostMetricsDetail/Containers/Containers.styles.scss @@ -0,0 +1,48 @@ +.host-containers { + max-width: 600px; + margin: 150px auto; + padding: 0 16px; + + .infra-container-card { + display: flex; + flex-direction: column; + justify-content: center; + } + + .infra-container-card-text { + font-size: var(--font-size-sm); + color: var(--text-vanilla-400); + line-height: 20px; + letter-spacing: -0.07px; + width: 400px; + font-family: 'Inter'; + margin-top: 12px; + } + + .infra-container-working-msg { + display: flex; + width: 400px; + padding: 12px; + align-items: flex-start; + gap: 12px; + border-radius: 4px; + background: rgba(171, 189, 255, 0.04); + + .ant-space { + align-items: flex-start; + } + } + + .infra-container-contact-support-btn { + display: flex; + align-items: center; + justify-content: center; + margin: auto; + } +} + +.lightMode { + .infra-container-card-text { + color: var(--text-ink-200); + } +} diff --git a/frontend/src/components/HostMetricsDetail/Containers/Containers.tsx b/frontend/src/components/HostMetricsDetail/Containers/Containers.tsx new file mode 100644 index 0000000000..e838b4aa42 --- /dev/null +++ b/frontend/src/components/HostMetricsDetail/Containers/Containers.tsx @@ -0,0 +1,36 @@ +import './Containers.styles.scss'; + +import { Space, Typography } from 'antd'; +import { useTranslation } from 'react-i18next'; + +const { Text } = Typography; + +function Containers(): JSX.Element { + const { t } = useTranslation(['infraMonitoring']); + + return ( + +
+ infra-container + + + {t('containers_visualization_message')} + +
+ +
+ + broom + {t('working_message')} + +
+
+ ); +} + +export default Containers; diff --git a/frontend/src/components/HostMetricsDetail/HostMetricDetail.interfaces.ts b/frontend/src/components/HostMetricsDetail/HostMetricDetail.interfaces.ts new file mode 100644 index 0000000000..65d3bf44d8 --- /dev/null +++ b/frontend/src/components/HostMetricsDetail/HostMetricDetail.interfaces.ts @@ -0,0 +1,7 @@ +import { HostData } from 'api/infraMonitoring/getHostLists'; + +export type HostDetailProps = { + host: HostData | null; + isModalTimeSelection: boolean; + onClose: () => void; +}; diff --git a/frontend/src/components/HostMetricsDetail/HostMetricTraces/HostMetricTraces.styles.scss b/frontend/src/components/HostMetricsDetail/HostMetricTraces/HostMetricTraces.styles.scss new file mode 100644 index 0000000000..2bcabd1b30 --- /dev/null +++ b/frontend/src/components/HostMetricsDetail/HostMetricTraces/HostMetricTraces.styles.scss @@ -0,0 +1,193 @@ +.host-metric-traces { + margin-top: 1rem; + + .host-metric-traces-header { + display: flex; + justify-content: space-between; + margin-bottom: 1rem; + + gap: 8px; + padding: 12px; + border-radius: 3px; + border: 1px solid var(--bg-slate-500); + + .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; + } + } + } + } + + .host-metric-traces-table { + .ant-table-content { + overflow: hidden !important; + } + + .ant-table { + border-radius: 3px; + border: 1px solid var(--bg-slate-500); + + .ant-table-thead > tr > th { + padding: 12px; + font-weight: 500; + font-size: 12px; + line-height: 18px; + + background: rgba(171, 189, 255, 0.01); + 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: rgba(171, 189, 255, 0.01); + } + + .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-table-container::after { + content: none; + } + } +} + +.lightMode { + .host-metric-traces-header { + .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); + } + } + } + + .host-metric-traces-table { + .ant-table { + border-radius: 3px; + border: 1px solid var(--bg-vanilla-300); + + .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); + } + } + } +} diff --git a/frontend/src/components/HostMetricsDetail/HostMetricTraces/HostMetricTraces.tsx b/frontend/src/components/HostMetricsDetail/HostMetricTraces/HostMetricTraces.tsx new file mode 100644 index 0000000000..2d4b7af903 --- /dev/null +++ b/frontend/src/components/HostMetricsDetail/HostMetricTraces/HostMetricTraces.tsx @@ -0,0 +1,195 @@ +import './HostMetricTraces.styles.scss'; + +import { ResizeTable } from 'components/ResizeTable'; +import { DEFAULT_ENTITY_VERSION } from 'constants/app'; +import { QueryParams } from 'constants/query'; +import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch'; +import NoLogs from 'container/NoLogs/NoLogs'; +import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch'; +import { ErrorText } from 'container/TimeSeriesView/styles'; +import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; +import { + CustomTimeType, + Time, +} from 'container/TopNav/DateTimeSelectionV2/config'; +import TraceExplorerControls from 'container/TracesExplorer/Controls'; +import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs'; +import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { Pagination } from 'hooks/queryPagination'; +import useUrlQueryData from 'hooks/useUrlQueryData'; +import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults'; +import { useEffect, useMemo, useState } from 'react'; +import { useQuery } from 'react-query'; +import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; +import { DataSource } from 'types/common/queryBuilder'; + +import { getHostTracesQueryPayload, selectedColumns } from './constants'; +import { getListColumns } from './utils'; + +interface Props { + timeRange: { + startTime: number; + endTime: number; + }; + isModalTimeSelection: boolean; + handleTimeChange: ( + interval: Time | CustomTimeType, + dateTimeRange?: [number, number], + ) => void; + handleChangeTracesFilters: (value: IBuilderQuery['filters']) => void; + tracesFilters: IBuilderQuery['filters']; + selectedInterval: Time; +} + +function HostMetricTraces({ + timeRange, + isModalTimeSelection, + handleTimeChange, + handleChangeTracesFilters, + tracesFilters, + selectedInterval, +}: Props): JSX.Element { + const [traces, setTraces] = useState([]); + const [offset] = useState(0); + + const { currentQuery } = useQueryBuilder(); + const updatedCurrentQuery = useMemo( + () => ({ + ...currentQuery, + builder: { + ...currentQuery.builder, + queryData: [ + { + ...currentQuery.builder.queryData[0], + dataSource: DataSource.TRACES, + aggregateOperator: 'noop', + aggregateAttribute: { + ...currentQuery.builder.queryData[0].aggregateAttribute, + }, + }, + ], + }, + }), + [currentQuery], + ); + + const query = updatedCurrentQuery?.builder?.queryData[0] || null; + + const { queryData: paginationQueryData } = useUrlQueryData( + QueryParams.pagination, + ); + + const queryPayload = useMemo( + () => + getHostTracesQueryPayload( + timeRange.startTime, + timeRange.endTime, + paginationQueryData?.offset || offset, + tracesFilters, + ), + [ + timeRange.startTime, + timeRange.endTime, + offset, + tracesFilters, + paginationQueryData, + ], + ); + + const { data, isLoading, isFetching, isError } = useQuery({ + queryKey: [ + 'hostMetricTraces', + timeRange.startTime, + timeRange.endTime, + offset, + tracesFilters, + DEFAULT_ENTITY_VERSION, + paginationQueryData, + ], + queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION), + enabled: !!queryPayload, + }); + + const traceListColumns = getListColumns(selectedColumns); + + useEffect(() => { + if (data?.payload?.data?.newResult?.data?.result) { + const currentData = data.payload.data.newResult.data.result; + if (currentData.length > 0 && currentData[0].list) { + if (offset === 0) { + setTraces(currentData[0].list ?? []); + } else { + setTraces((prev) => [...prev, ...(currentData[0].list ?? [])]); + } + } + } + }, [data, offset]); + + const isDataEmpty = + !isLoading && !isFetching && !isError && traces.length === 0; + const hasAdditionalFilters = tracesFilters.items.length > 1; + + const totalCount = + data?.payload?.data?.newResult?.data?.result?.[0]?.list?.length || 0; + + return ( +
+
+
+ {query && ( + + )} +
+
+ +
+
+ + {isError && {data?.error || 'Something went wrong'}} + + {isLoading && traces.length === 0 && } + + {isDataEmpty && !hasAdditionalFilters && ( + + )} + + {isDataEmpty && hasAdditionalFilters && ( + + )} + + {!isError && traces.length > 0 && ( +
+ + +
+ )} +
+ ); +} + +export default HostMetricTraces; diff --git a/frontend/src/components/HostMetricsDetail/HostMetricTraces/constants.ts b/frontend/src/components/HostMetricsDetail/HostMetricTraces/constants.ts new file mode 100644 index 0000000000..8e2a3d6238 --- /dev/null +++ b/frontend/src/components/HostMetricsDetail/HostMetricTraces/constants.ts @@ -0,0 +1,200 @@ +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; +import { + BaseAutocompleteData, + 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 { nanoToMilli } from 'utils/timeUtils'; + +export const columns = [ + { + dataIndex: 'timestamp', + key: 'timestamp', + title: 'Timestamp', + width: 200, + render: (timestamp: string): string => new Date(timestamp).toLocaleString(), + }, + { + title: 'Service Name', + dataIndex: ['data', 'serviceName'], + key: 'serviceName-string-tag', + width: 150, + }, + { + title: 'Name', + dataIndex: ['data', 'name'], + key: 'name-string-tag', + width: 145, + }, + { + title: 'Duration', + dataIndex: ['data', 'durationNano'], + key: 'durationNano-float64-tag', + width: 145, + render: (duration: number): string => `${nanoToMilli(duration)}ms`, + }, + { + title: 'HTTP Method', + dataIndex: ['data', 'httpMethod'], + key: 'httpMethod-string-tag', + width: 145, + }, + { + title: 'Status Code', + dataIndex: ['data', 'responseStatusCode'], + key: 'responseStatusCode-string-tag', + width: 145, + }, +]; + +export const selectedColumns: BaseAutocompleteData[] = [ + { + key: 'timestamp', + dataType: DataTypes.String, + type: 'tag', + isColumn: true, + }, + { + key: 'serviceName', + dataType: DataTypes.String, + type: 'tag', + isColumn: true, + }, + { + key: 'name', + dataType: DataTypes.String, + type: 'tag', + isColumn: true, + }, + { + key: 'durationNano', + dataType: DataTypes.Float64, + type: 'tag', + isColumn: true, + }, + { + key: 'httpMethod', + dataType: DataTypes.String, + type: 'tag', + isColumn: true, + }, + { + key: 'responseStatusCode', + dataType: DataTypes.String, + type: 'tag', + isColumn: true, + }, +]; + +export const getHostTracesQueryPayload = ( + start: number, + end: number, + offset = 0, + filters: IBuilderQuery['filters'], +): GetQueryResultsProps => ({ + query: { + promql: [], + clickhouse_sql: [], + builder: { + queryData: [ + { + dataSource: DataSource.TRACES, + queryName: 'A', + aggregateOperator: 'noop', + aggregateAttribute: { + id: '------false', + dataType: DataTypes.EMPTY, + 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', + }, + ], + queryFormulas: [], + }, + id: '572f1d91-6ac0-46c0-b726-c21488b34434', + queryType: EQueryType.QUERY_BUILDER, + }, + graphType: PANEL_TYPES.LIST, + selectedTime: 'GLOBAL_TIME', + start, + end, + params: { + dataSource: DataSource.TRACES, + }, + tableParams: { + pagination: { + limit: 10, + offset, + }, + selectColumns: [ + { + key: 'serviceName', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'serviceName--string--tag--true', + isIndexed: false, + }, + { + key: 'name', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'name--string--tag--true', + isIndexed: false, + }, + { + key: 'durationNano', + dataType: 'float64', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'durationNano--float64--tag--true', + isIndexed: false, + }, + { + key: 'httpMethod', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'httpMethod--string--tag--true', + isIndexed: false, + }, + { + key: 'responseStatusCode', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'responseStatusCode--string--tag--true', + isIndexed: false, + }, + ], + }, +}); diff --git a/frontend/src/components/HostMetricsDetail/HostMetricTraces/utils.tsx b/frontend/src/components/HostMetricsDetail/HostMetricTraces/utils.tsx new file mode 100644 index 0000000000..aba284586a --- /dev/null +++ b/frontend/src/components/HostMetricsDetail/HostMetricTraces/utils.tsx @@ -0,0 +1,84 @@ +import { Tag, Typography } from 'antd'; +import { ColumnsType } from 'antd/es/table'; +import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util'; +import { + BlockLink, + getTraceLink, +} from 'container/TracesExplorer/ListView/utils'; +import dayjs from 'dayjs'; +import { RowData } from 'lib/query/createTableColumnsFromQuery'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; + +const keyToLabelMap: Record = { + timestamp: 'Timestamp', + serviceName: 'Service Name', + name: 'Name', + durationNano: 'Duration', + httpMethod: 'HTTP Method', + responseStatusCode: 'Status Code', +}; + +export const getListColumns = ( + selectedColumns: BaseAutocompleteData[], +): ColumnsType => { + const columns: ColumnsType = + selectedColumns.map(({ dataType, key, type }) => ({ + title: keyToLabelMap[key], + dataIndex: key, + key: `${key}-${dataType}-${type}`, + width: 145, + render: (value, item): JSX.Element => { + const itemData = item.data as any; + + if (key === 'timestamp') { + const date = + typeof value === 'string' + ? dayjs(value).format('YYYY-MM-DD HH:mm:ss.SSS') + : dayjs(value / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'); + + return ( + + {date} + + ); + } + + if (value === '') { + return ( + + N/A + + ); + } + + if (key === 'httpMethod' || key === 'responseStatusCode') { + return ( + + + {itemData[key]} + + + ); + } + + if (key === 'durationNano') { + const durationNano = itemData[key]; + + return ( + + {getMs(durationNano)}ms + + ); + } + + return ( + + {itemData[key]} + + ); + }, + responsive: ['md'], + })) || []; + + return columns; +}; diff --git a/frontend/src/components/HostMetricsDetail/HostMetricsDetail.styles.scss b/frontend/src/components/HostMetricsDetail/HostMetricsDetail.styles.scss new file mode 100644 index 0000000000..511348c463 --- /dev/null +++ b/frontend/src/components/HostMetricsDetail/HostMetricsDetail.styles.scss @@ -0,0 +1,232 @@ +.host-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); + } + + .host-detail-drawer__host { + .host-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; + } + + .host-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; + } + + .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); + } + + .host-detail-drawer { + .title { + color: var(--text-ink-300); + } + + .host-detail-drawer__host { + .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/components/HostMetricsDetail/HostMetricsDetails.tsx b/frontend/src/components/HostMetricsDetail/HostMetricsDetails.tsx new file mode 100644 index 0000000000..7a3ab8c0ab --- /dev/null +++ b/frontend/src/components/HostMetricsDetail/HostMetricsDetails.tsx @@ -0,0 +1,486 @@ +import './HostMetricsDetail.styles.scss'; + +import { Color, Spacing } from '@signozhq/design-tokens'; +import { + Button, + Divider, + Drawer, + Progress, + Radio, + Tag, + Typography, +} from 'antd'; +import { RadioChangeEvent } from 'antd/lib'; +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, + Package2, + 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 { VIEW_TYPES, VIEWS } from './constants'; +import Containers from './Containers/Containers'; +import { HostDetailProps } from './HostMetricDetail.interfaces'; +import HostMetricLogsDetailedView from './HostMetricsLogs/HostMetricLogsDetailedView'; +import HostMetricTraces from './HostMetricTraces/HostMetricTraces'; +import Metrics from './Metrics/Metrics'; +import Processes from './Processes/Processes'; + +// eslint-disable-next-line sonarjs/cognitive-complexity +function HostMetricsDetails({ + host, + onClose, + isModalTimeSelection, +}: HostDetailProps): 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