Compare commits

...

11 Commits

Author SHA1 Message Date
rahulkeswani101
b33be9c7f9 style: removed unused css 2024-11-05 18:55:34 +05:30
rahulkeswani101
d1c85361e9 style: removed unnecessary styles for host tabs 2024-11-05 18:47:27 +05:30
rahulkeswani101
f5ec5b2b05 style: added padding to date time selector 2024-11-05 17:20:18 +05:30
rahulkeswani101
ecf897f769 style: added new style changes for date time selection in host lists view 2024-11-05 17:15:45 +05:30
rahulkeswani101
6648e841eb refactor: removed inline styles 2024-10-22 11:25:22 +05:30
rahulkeswani101
403fe9d55b feat: added order by and color codes for cpu and memory usage progress bar 2024-10-21 21:31:44 +05:30
rahulkeswani101
689440bcfb feat: added global time range and order by for cpu,memory,iowait,load 2024-10-21 15:56:52 +05:30
rahulkeswani101
3d57dde02a feat: pass updated filters to api to get filtered data in the list 2024-10-21 11:06:27 +05:30
rahulkeswani101
08512b9392 feat: updated the table view and added the pagination 2024-10-19 16:42:16 +05:30
rahulkeswani101
ae014d1ead feat: removed group by filter and added autocomplete for where clause 2024-10-19 11:13:23 +05:30
rahulkeswani101
f8eeec62ad feat: added the host list view and filters 2024-10-17 18:35:00 +05:30
31 changed files with 849 additions and 33 deletions

View File

@@ -224,3 +224,10 @@ export const MQDetailPage = Loadable(
/* webpackChunkName: "MQDetailPage" */ 'pages/MessagingQueues/MQDetailPage' /* webpackChunkName: "MQDetailPage" */ 'pages/MessagingQueues/MQDetailPage'
), ),
); );
export const InfrastructureMonitoring = Loadable(
() =>
import(
/* webpackChunkName: "InfrastructureMonitoring" */ 'pages/InfrastructureMonitoring'
),
);

View File

@@ -15,6 +15,7 @@ import {
EditAlertChannelsAlerts, EditAlertChannelsAlerts,
EditRulesPage, EditRulesPage,
ErrorDetails, ErrorDetails,
InfrastructureMonitoring,
IngestionSettings, IngestionSettings,
InstalledIntegrations, InstalledIntegrations,
LicensePage, LicensePage,
@@ -383,6 +384,13 @@ const routes: AppRoutes[] = [
key: 'MESSAGING_QUEUES_DETAIL', key: 'MESSAGING_QUEUES_DETAIL',
isPrivate: true, isPrivate: true,
}, },
{
path: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
exact: true,
component: InfrastructureMonitoring,
key: 'INFRASTRUCTURE_MONITORING_HOSTS',
isPrivate: true,
},
]; ];
export const SUPPORT_ROUTE: AppRoutes = { export const SUPPORT_ROUTE: AppRoutes = {

View File

@@ -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<string, string>;
labelsArray: Array<Record<string, string>>;
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<string, string>,
): Promise<SuccessResponse<HostListResponse> | 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);
}
};

View File

@@ -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<IAttributeValuesResponse> | 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);
}
};

View File

@@ -1,4 +1,4 @@
import { ApiV3Instance } from 'api'; import { ApiBaseInstance, ApiV3Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError, AxiosResponse } from 'axios'; import { AxiosError, AxiosResponse } from 'axios';
import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder'; import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder';
@@ -18,20 +18,25 @@ export const getAggregateKeys = async ({
dataSource, dataSource,
aggregateAttribute, aggregateAttribute,
tagType, tagType,
isInfraMonitoring,
}: IGetAttributeKeysPayload): Promise< }: IGetAttributeKeysPayload): Promise<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
> => { > => {
try { try {
const endpoint = isInfraMonitoring
? `/hosts/attribute_keys?dataSource=metrics&searchText=${searchText || ''}`
: `/autocomplete/attribute_keys?${createQueryParams({
aggregateOperator,
searchText,
dataSource,
aggregateAttribute,
})}&tagType=${tagType}`;
const apiInstance = isInfraMonitoring ? ApiBaseInstance : ApiV3Instance;
const response: AxiosResponse<{ const response: AxiosResponse<{
data: IQueryAutocompleteResponse; data: IQueryAutocompleteResponse;
}> = await ApiV3Instance.get( }> = await apiInstance.get(endpoint);
`/autocomplete/attribute_keys?${createQueryParams({
aggregateOperator,
searchText,
dataSource,
aggregateAttribute,
})}&tagType=${tagType}`,
);
const payload: BaseAutocompleteData[] = const payload: BaseAutocompleteData[] =
response.data.data.attributeKeys?.map(({ id: _, ...item }) => ({ response.data.data.attributeKeys?.map(({ id: _, ...item }) => ({

View File

@@ -18,4 +18,5 @@ export const REACT_QUERY_KEY = {
GET_ALL_ALLERTS: 'GET_ALL_ALLERTS', GET_ALL_ALLERTS: 'GET_ALL_ALLERTS',
REMOVE_ALERT_RULE: 'REMOVE_ALERT_RULE', REMOVE_ALERT_RULE: 'REMOVE_ALERT_RULE',
DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE', DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE',
GET_HOST_LIST: 'GET_HOST_LIST',
}; };

View File

@@ -58,6 +58,7 @@ const ROUTES = {
INTEGRATIONS: '/integrations', INTEGRATIONS: '/integrations',
MESSAGING_QUEUES: '/messaging-queues', MESSAGING_QUEUES: '/messaging-queues',
MESSAGING_QUEUES_DETAIL: '/messaging-queues/detail', MESSAGING_QUEUES_DETAIL: '/messaging-queues/detail',
INFRASTRUCTURE_MONITORING_HOSTS: '/infrastructure-monitoring/hosts',
} as const; } as const;
export default ROUTES; export default ROUTES;

View File

@@ -0,0 +1,19 @@
.loading-host-metrics {
padding: 24px 0;
height: 600px;
display: flex;
justify-content: center;
align-items: center;
.loading-host-metrics-content {
display: flex;
align-items: center;
flex-direction: column;
.loading-gif {
height: 72px;
margin-left: -24px;
}
}
}

View File

@@ -0,0 +1,26 @@
import './HostMetricsLoading.styles.scss';
import { Typography } from 'antd';
import { useTranslation } from 'react-i18next';
import { DataSource } from 'types/common/queryBuilder';
export function HostMetricsLoading(): JSX.Element {
const { t } = useTranslation('common');
return (
<div className="loading-host-metrics">
<div className="loading-host-metrics-content">
<img
className="loading-gif"
src="/Icons/loading-plane.gif"
alt="wait-icon"
/>
<Typography>
{t('pending_data_placeholder', {
dataSource: `host ${DataSource.METRICS}`,
})}
</Typography>
</div>
</div>
);
}

View File

@@ -4,5 +4,6 @@ import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export type GroupByFilterProps = { export type GroupByFilterProps = {
query: IBuilderQuery; query: IBuilderQuery;
onChange: (values: BaseAutocompleteData[]) => void; onChange: (values: BaseAutocompleteData[]) => void;
disabled: boolean; disabled?: boolean;
isInfraMonitoring?: boolean;
}; };

View File

@@ -25,6 +25,7 @@ export const GroupByFilter = memo(function GroupByFilter({
query, query,
onChange, onChange,
disabled, disabled,
isInfraMonitoring,
}: GroupByFilterProps): JSX.Element { }: GroupByFilterProps): JSX.Element {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [searchText, setSearchText] = useState<string>(''); const [searchText, setSearchText] = useState<string>('');
@@ -85,6 +86,7 @@ export const GroupByFilter = memo(function GroupByFilter({
setOptionsData(options); setOptionsData(options);
}, },
}, },
isInfraMonitoring,
); );
const getAttributeKeys = useCallback(async () => { const getAttributeKeys = useCallback(async () => {
@@ -96,6 +98,7 @@ export const GroupByFilter = memo(function GroupByFilter({
dataSource: query.dataSource, dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator, aggregateOperator: query.aggregateOperator,
searchText, searchText,
isInfraMonitoring,
}), }),
); );
@@ -107,6 +110,7 @@ export const GroupByFilter = memo(function GroupByFilter({
query.dataSource, query.dataSource,
queryClient, queryClient,
searchText, searchText,
isInfraMonitoring,
]); ]);
const handleSearchKeys = (searchText: string): void => { const handleSearchKeys = (searchText: string): void => {

View File

@@ -72,6 +72,7 @@ function QueryBuilderSearch({
className, className,
placeholder, placeholder,
suffixIcon, suffixIcon,
isInfraMonitoring,
}: QueryBuilderSearchProps): JSX.Element { }: QueryBuilderSearchProps): JSX.Element {
const { pathname } = useLocation(); const { pathname } = useLocation();
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [ const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
@@ -93,7 +94,12 @@ function QueryBuilderSearch({
searchKey, searchKey,
key, key,
exampleQueries, exampleQueries,
} = useAutoComplete(query, whereClauseConfig, isLogsExplorerPage); } = useAutoComplete(
query,
whereClauseConfig,
isLogsExplorerPage,
isInfraMonitoring,
);
const [isOpen, setIsOpen] = useState<boolean>(false); const [isOpen, setIsOpen] = useState<boolean>(false);
const [showAllFilters, setShowAllFilters] = useState<boolean>(false); const [showAllFilters, setShowAllFilters] = useState<boolean>(false);
const [dynamicPlacholder, setDynamicPlaceholder] = useState<string>( const [dynamicPlacholder, setDynamicPlaceholder] = useState<string>(
@@ -105,6 +111,7 @@ function QueryBuilderSearch({
query, query,
searchKey, searchKey,
isLogsExplorerPage, isLogsExplorerPage,
isInfraMonitoring,
); );
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys(); const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
@@ -185,8 +192,8 @@ function QueryBuilderSearch({
); );
const isMetricsDataSource = useMemo( const isMetricsDataSource = useMemo(
() => query.dataSource === DataSource.METRICS, () => query.dataSource === DataSource.METRICS && !isInfraMonitoring,
[query.dataSource], [query.dataSource, isInfraMonitoring],
); );
const fetchValueDataType = (value: unknown, operator: string): DataTypes => { const fetchValueDataType = (value: unknown, operator: string): DataTypes => {
@@ -426,6 +433,7 @@ interface QueryBuilderSearchProps {
className?: string; className?: string;
placeholder?: string; placeholder?: string;
suffixIcon?: React.ReactNode; suffixIcon?: React.ReactNode;
isInfraMonitoring?: boolean;
} }
QueryBuilderSearch.defaultProps = { QueryBuilderSearch.defaultProps = {
@@ -433,6 +441,7 @@ QueryBuilderSearch.defaultProps = {
className: '', className: '',
placeholder: PLACEHOLDER, placeholder: PLACEHOLDER,
suffixIcon: undefined, suffixIcon: undefined,
isInfraMonitoring: false,
}; };
export interface CustomTagProps { export interface CustomTagProps {

View File

@@ -11,6 +11,7 @@ import {
LayoutGrid, LayoutGrid,
ListMinus, ListMinus,
MessageSquare, MessageSquare,
PackagePlus,
Receipt, Receipt,
Route, Route,
ScrollText, ScrollText,
@@ -118,6 +119,11 @@ const menuItems: SidebarItem[] = [
label: 'Billing', label: 'Billing',
icon: <Receipt size={16} />, icon: <Receipt size={16} />,
}, },
{
key: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
label: 'Infrastructure Monitoring',
icon: <PackagePlus size={16} />,
},
{ {
key: ROUTES.SETTINGS, key: ROUTES.SETTINGS,
label: 'Settings', label: 'Settings',

View File

@@ -212,6 +212,7 @@ export const routesToSkip = [
ROUTES.ALERT_OVERVIEW, ROUTES.ALERT_OVERVIEW,
ROUTES.MESSAGING_QUEUES, ROUTES.MESSAGING_QUEUES,
ROUTES.MESSAGING_QUEUES_DETAIL, ROUTES.MESSAGING_QUEUES_DETAIL,
ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
]; ];
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS]; export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];

View File

@@ -0,0 +1,34 @@
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { IGetAttributeKeysPayload } from 'types/api/queryBuilder/getAttributeKeys';
import { IQueryAutocompleteResponse } from 'types/api/queryBuilder/queryAutocompleteResponse';
type UseGetAttributeKeys = (
requestData: IGetAttributeKeysPayload,
options?: UseQueryOptions<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
>,
) => UseQueryResult<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
>;
export const useGetAggregateKeys: UseGetAttributeKeys = (
requestData,
options,
) => {
const queryKey = useMemo(() => {
if (options?.queryKey && Array.isArray(options.queryKey)) {
return [QueryBuilderKeys.GET_AGGREGATE_KEYS, ...options.queryKey];
}
return [QueryBuilderKeys.GET_AGGREGATE_KEYS, requestData];
}, [options?.queryKey, requestData]);
return useQuery<SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse>({
queryKey,
queryFn: () => getAggregateKeys(requestData),
...options,
});
};

View File

@@ -0,0 +1,42 @@
import {
getHostLists,
HostListPayload,
HostListResponse,
} from 'api/infraMonitoring/getHostLists';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseGetHostList = (
requestData: HostListPayload,
options?: UseQueryOptions<
SuccessResponse<HostListResponse> | ErrorResponse,
Error
>,
headers?: Record<string, string>,
) => UseQueryResult<SuccessResponse<HostListResponse> | ErrorResponse, Error>;
export const useGetHostList: UseGetHostList = (
requestData,
options,
headers,
) => {
const queryKey = useMemo(() => {
if (options?.queryKey && Array.isArray(options.queryKey)) {
return [...options.queryKey];
}
if (options?.queryKey && typeof options.queryKey === 'string') {
return options.queryKey;
}
return [REACT_QUERY_KEY.GET_HOST_LIST, requestData];
}, [options?.queryKey, requestData]);
return useQuery<SuccessResponse<HostListResponse> | ErrorResponse, Error>({
queryFn: ({ signal }) => getHostLists(requestData, signal, headers),
...options,
queryKey,
});
};

View File

@@ -28,6 +28,7 @@ export const useAutoComplete = (
query: IBuilderQuery, query: IBuilderQuery,
whereClauseConfig?: WhereClauseConfig, whereClauseConfig?: WhereClauseConfig,
shouldUseSuggestions?: boolean, shouldUseSuggestions?: boolean,
isInfraMonitoring?: boolean,
): IAutoComplete => { ): IAutoComplete => {
const [searchValue, setSearchValue] = useState<string>(''); const [searchValue, setSearchValue] = useState<string>('');
const [searchKey, setSearchKey] = useState<string>(''); const [searchKey, setSearchKey] = useState<string>('');
@@ -37,6 +38,7 @@ export const useAutoComplete = (
query, query,
searchKey, searchKey,
shouldUseSuggestions, shouldUseSuggestions,
isInfraMonitoring,
); );
const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys); const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys);
@@ -170,4 +172,5 @@ interface IAutoComplete {
searchKey: string; searchKey: string;
key: string; key: string;
exampleQueries: TagFilter[]; exampleQueries: TagFilter[];
isInfraMonitoring?: boolean;
} }

View File

@@ -1,3 +1,4 @@
import { getInfraAttributesValues } from 'api/infraMonitoring/getInfraAttributeValues';
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues'; import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig'; import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { import {
@@ -43,6 +44,7 @@ export const useFetchKeysAndValues = (
query: IBuilderQuery, query: IBuilderQuery,
searchKey: string, searchKey: string,
shouldUseSuggestions?: boolean, shouldUseSuggestions?: boolean,
isInfraMonitoring?: boolean,
): IuseFetchKeysAndValues => { ): IuseFetchKeysAndValues => {
const [keys, setKeys] = useState<BaseAutocompleteData[]>([]); const [keys, setKeys] = useState<BaseAutocompleteData[]>([]);
const [exampleQueries, setExampleQueries] = useState<TagFilter[]>([]); const [exampleQueries, setExampleQueries] = useState<TagFilter[]>([]);
@@ -91,10 +93,10 @@ export const useFetchKeysAndValues = (
const isQueryEnabled = useMemo( const isQueryEnabled = useMemo(
() => () =>
query.dataSource === DataSource.METRICS query.dataSource === DataSource.METRICS && !isInfraMonitoring
? !!query.dataSource && !!query.aggregateAttribute.dataType ? !!query.dataSource && !!query.aggregateAttribute.dataType
: true, : true,
[query.aggregateAttribute.dataType, query.dataSource], [isInfraMonitoring, query.aggregateAttribute.dataType, query.dataSource],
); );
const { data, isFetching, status } = useGetAggregateKeys( const { data, isFetching, status } = useGetAggregateKeys(
@@ -109,6 +111,7 @@ export const useFetchKeysAndValues = (
queryKey: [searchParams], queryKey: [searchParams],
enabled: isQueryEnabled && !shouldUseSuggestions, enabled: isQueryEnabled && !shouldUseSuggestions,
}, },
isInfraMonitoring,
); );
const { const {
@@ -136,6 +139,7 @@ export const useFetchKeysAndValues = (
value: string, value: string,
query: IBuilderQuery, query: IBuilderQuery,
keys: BaseAutocompleteData[], keys: BaseAutocompleteData[],
// eslint-disable-next-line sonarjs/cognitive-complexity
): Promise<void> => { ): Promise<void> => {
if (!value) { if (!value) {
return; return;
@@ -152,17 +156,36 @@ export const useFetchKeysAndValues = (
setAggregateFetching(true); setAggregateFetching(true);
try { try {
const { payload } = await getAttributesValues({ let payload;
aggregateOperator: query.aggregateOperator, if (isInfraMonitoring) {
dataSource: query.dataSource, const response = await getInfraAttributesValues({
aggregateAttribute: query.aggregateAttribute.key, dataSource: query.dataSource,
attributeKey: filterAttributeKey?.key ?? tagKey, attributeKey: filterAttributeKey?.key ?? tagKey,
filterAttributeKeyDataType: filterAttributeKey?.dataType ?? DataTypes.EMPTY, filterAttributeKeyDataType:
tagType: filterAttributeKey?.type ?? '', filterAttributeKey?.dataType ?? DataTypes.EMPTY,
searchText: isInNInOperator(tagOperator) tagType: filterAttributeKey?.type ?? '',
? tagValue[tagValue.length - 1]?.toString() ?? '' // last element of tagvalue will be always user search value searchText: isInNInOperator(tagOperator)
: tagValue?.toString() ?? '', ? tagValue[tagValue.length - 1]?.toString() ?? ''
}); : tagValue?.toString() ?? '',
aggregateOperator: query.aggregateOperator,
aggregateAttribute: query.aggregateAttribute.key,
});
payload = response.payload;
} else {
const response = await getAttributesValues({
aggregateOperator: query.aggregateOperator,
dataSource: query.dataSource,
aggregateAttribute: query.aggregateAttribute.key,
attributeKey: filterAttributeKey?.key ?? tagKey,
filterAttributeKeyDataType:
filterAttributeKey?.dataType ?? DataTypes.EMPTY,
tagType: filterAttributeKey?.type ?? '',
searchText: isInNInOperator(tagOperator)
? tagValue[tagValue.length - 1]?.toString() ?? ''
: tagValue?.toString() ?? '',
});
payload = response.payload;
}
if (payload) { if (payload) {
const values = Object.values(payload).find((el) => !!el) || []; const values = Object.values(payload).find((el) => !!el) || [];

View File

@@ -11,6 +11,7 @@ type UseGetAttributeKeys = (
options?: UseQueryOptions< options?: UseQueryOptions<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
>, >,
isInfraMonitoring?: boolean,
) => UseQueryResult< ) => UseQueryResult<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
>; >;
@@ -18,17 +19,22 @@ type UseGetAttributeKeys = (
export const useGetAggregateKeys: UseGetAttributeKeys = ( export const useGetAggregateKeys: UseGetAttributeKeys = (
requestData, requestData,
options, options,
isInfraMonitoring,
) => { ) => {
const queryKey = useMemo(() => { const queryKey = useMemo(() => {
if (options?.queryKey && Array.isArray(options.queryKey)) { if (options?.queryKey && Array.isArray(options.queryKey)) {
return [QueryBuilderKeys.GET_AGGREGATE_KEYS, ...options.queryKey]; return [
QueryBuilderKeys.GET_AGGREGATE_KEYS,
...options.queryKey,
isInfraMonitoring,
];
} }
return [QueryBuilderKeys.GET_AGGREGATE_KEYS, requestData]; return [QueryBuilderKeys.GET_AGGREGATE_KEYS, requestData, isInfraMonitoring];
}, [options?.queryKey, requestData]); }, [options?.queryKey, requestData, isInfraMonitoring]);
return useQuery<SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse>({ return useQuery<SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse>({
queryKey, queryKey,
queryFn: () => getAggregateKeys(requestData), queryFn: () => getAggregateKeys({ ...requestData, isInfraMonitoring }),
...options, ...options,
}); });
}; };

View File

@@ -12,7 +12,7 @@ import {
} from 'container/TopNav/DateTimeSelectionV2/config'; } from 'container/TopNav/DateTimeSelectionV2/config';
import { Pagination } from 'hooks/queryPagination'; import { Pagination } from 'hooks/queryPagination';
import { convertNewDataToOld } from 'lib/newQueryBuilder/convertNewDataToOld'; import { convertNewDataToOld } from 'lib/newQueryBuilder/convertNewDataToOld';
import { isEmpty, cloneDeep } from 'lodash-es'; import { isEmpty } from 'lodash-es';
import { SuccessResponse } from 'types/api'; import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -24,6 +24,7 @@ export async function GetMetricQueryRange(
version: string, version: string,
signal?: AbortSignal, signal?: AbortSignal,
headers?: Record<string, string>, headers?: Record<string, string>,
isInfraMonitoring?: boolean,
): Promise<SuccessResponse<MetricRangePayloadProps>> { ): Promise<SuccessResponse<MetricRangePayloadProps>> {
const { legendMap, queryPayload } = prepareQueryRangePayload(props); const { legendMap, queryPayload } = prepareQueryRangePayload(props);
const response = await getMetricsQueryRange( const response = await getMetricsQueryRange(

View File

@@ -0,0 +1,141 @@
import { Table, TablePaginationConfig, TableProps, Typography } from 'antd';
import { SorterResult } from 'antd/es/table/interface';
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
import { HostMetricsLoading } from 'container/HostMetricsLoading/HostMetricsLoading';
import NoLogs from 'container/NoLogs/NoLogs';
import { useGetHostList } from 'hooks/infraMonitoring/useGetHostList';
import { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import HostsListControls from './HostsListControls';
import {
formatDataForTable,
getHostListsQuery,
getHostsListColumns,
HostRowData,
} from './utils';
function HostsList(): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilters] = useState<IBuilderQuery['filters']>({
items: [],
op: 'and',
});
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(null);
const pageSize = 10;
const query = useMemo(() => {
const baseQuery = getHostListsQuery();
return {
...baseQuery,
limit: pageSize,
offset: (currentPage - 1) * pageSize,
filters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
};
}, [currentPage, filters, minTime, maxTime, orderBy]);
const { data, isFetching, isLoading, isError } = useGetHostList(
query as HostListPayload,
{
queryKey: ['hostList', query],
enabled: !!query,
},
);
const hostMetricsData = useMemo(() => data?.payload?.data?.records || [], [
data,
]);
const totalCount = data?.payload?.data?.total || 0;
const formattedHostMetricsData = useMemo(
() => formatDataForTable(hostMetricsData),
[hostMetricsData],
);
const columns = useMemo(() => getHostsListColumns(), []);
const isDataPresent =
!isLoading && !isFetching && !isError && hostMetricsData.length === 0;
const handleTableChange: TableProps<HostRowData>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
_filters: Record<string, (string | number | boolean)[] | null>,
sorter: SorterResult<HostRowData> | SorterResult<HostRowData>[],
): void => {
if (pagination.current) {
setCurrentPage(pagination.current);
}
if ('field' in sorter && sorter.order) {
setOrderBy({
columnName: sorter.field as string,
order: sorter.order === 'ascend' ? 'asc' : 'desc',
});
} else {
setOrderBy(null);
}
},
[],
);
const handleFiltersChange = useCallback(
(value: IBuilderQuery['filters']): void => {
setFilters(value);
},
[],
);
return (
<div>
<HostsListControls handleFiltersChange={handleFiltersChange} />
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
{isLoading && <HostMetricsLoading />}
{isDataPresent && filters.items.length === 0 && (
<NoLogs dataSource={DataSource.METRICS} />
)}
{isDataPresent && filters.items.length > 0 && (
<div>No hosts match the applied filters.</div>
)}
{!isError && formattedHostMetricsData.length > 0 && (
<Table
dataSource={formattedHostMetricsData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: false,
hideOnSinglePage: true,
}}
scroll={{ x: true }}
loading={isFetching}
tableLayout="fixed"
rowKey={(record): string => record.hostName}
onChange={handleTableChange}
/>
)}
</div>
);
}
export default HostsList;

View File

@@ -0,0 +1,58 @@
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { useCallback, useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
function HostsListControls({
handleFiltersChange,
}: {
handleFiltersChange: (value: IBuilderQuery['filters']) => void;
}): JSX.Element {
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 { handleChangeQueryData } = useQueryOperations({
index: 0,
query,
isListViewPanel: true,
entityVersion: '',
});
const handleChangeTagFilters = useCallback(
(value: IBuilderQuery['filters']) => {
handleChangeQueryData('filters', value);
handleFiltersChange(value);
},
[handleChangeQueryData, handleFiltersChange],
);
return (
<div className="hosts-list-controls">
<QueryBuilderSearch
query={query}
onChange={handleChangeTagFilters}
isInfraMonitoring
/>
</div>
);
}
export default HostsListControls;

View File

@@ -0,0 +1,59 @@
.infra-monitoring-container {
display: flex;
height: 100%;
margin-top: 1rem;
.time-selector {
position: absolute;
top: 9px;
right: 0;
padding: 0 1.5rem;
}
.infra-monitoring-header {
display: flex;
justify-content: space-between;
width: 100%;
margin-bottom: 16px;
.tabs-wrapper {
flex: 1;
margin-right: 24px;
.infra-monitoring-tabs {
width: 100%;
:global(.ant-tabs-nav) {
margin: 0;
}
}
}
.time-selector {
flex-shrink: 0;
}
}
.hosts-list-controls {
margin: 1rem 0.5rem;
}
.progress-container {
display: flex;
align-items: center;
}
.progress-bar {
flex: 1;
margin-right: 8px;
}
.clickable-row {
cursor: pointer;
}
.infra-monitoring-tags {
border-radius: 10px;
width: fit-content;
}
}

View File

@@ -0,0 +1,36 @@
import './InfraMonitoring.styles.scss';
import * as Sentry from '@sentry/react';
import { Tabs } from 'antd';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { getTabsItems } from './utils';
function InfraMonitoringHosts(): JSX.Element {
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="infra-monitoring-container">
<div className="infra-monitoring-header">
<div className="tabs-wrapper">
<Tabs
defaultActiveKey="list"
items={getTabsItems()}
className="infra-monitoring-tabs"
type="card"
/>
</div>
</div>
<div className="time-selector">
<DateTimeSelectionV2
showAutoRefresh={false}
showRefreshText={false}
hideShareModal
/>
</div>
</div>
</Sentry.ErrorBoundary>
);
}
export default InfraMonitoringHosts;

View File

@@ -0,0 +1,121 @@
import './InfraMonitoring.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Progress, TabsProps, Tag } from 'antd';
import { ColumnType } from 'antd/es/table';
import { HostData, HostListPayload } from 'api/infraMonitoring/getHostLists';
import TabLabel from 'components/TabLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import HostsList from './HostsList';
export interface HostRowData {
hostName: string;
cpu: React.ReactNode;
memory: React.ReactNode;
ioWait: number;
load15: number;
active: React.ReactNode;
}
export const getHostListsQuery = (): HostListPayload => ({
filters: {
items: [],
op: 'and',
},
groupBy: [],
orderBy: { columnName: '', order: 'asc' },
});
export const getTabsItems = (): TabsProps['items'] => [
{
label: <TabLabel label="List View" isDisabled={false} tooltipText="" />,
key: PANEL_TYPES.LIST,
children: <HostsList />,
},
];
export const getHostsListColumns = (): ColumnType<HostRowData>[] => [
{
title: 'Hostname',
dataIndex: 'hostName',
key: 'hostName',
width: 150,
},
{
title: 'Status',
dataIndex: 'active',
key: 'active',
width: 100,
},
{
title: 'CPU Usage',
dataIndex: 'cpu',
key: 'cpu',
width: 100,
sorter: true,
},
{
title: 'Memory Usage',
dataIndex: 'memory',
key: 'memory',
width: 100,
sorter: true,
},
{
title: 'IOWait',
dataIndex: 'wait',
key: 'wait',
width: 100,
sorter: true,
},
{
title: 'Load Avg',
dataIndex: 'load15',
key: 'load15',
width: 100,
sorter: true,
},
];
export const formatDataForTable = (data: HostData[]): HostRowData[] =>
data.map((host, index) => ({
key: `${host.hostName}-${index}`,
hostName: host.hostName || '',
active: (
<Tag color={host.active ? 'success' : 'default'} bordered>
{host.active ? 'ACTIVE' : 'INACTIVE'}
</Tag>
),
cpu: (
<div className="progress-container">
<Progress
percent={Number((host.cpu * 100).toFixed(1))}
size="small"
strokeColor={((): string => {
const cpuPercent = Number((host.cpu * 100).toFixed(1));
if (cpuPercent >= 90) return Color.BG_SAKURA_500;
if (cpuPercent >= 60) return Color.BG_AMBER_500;
return Color.BG_FOREST_500;
})()}
className="progress-bar"
/>
</div>
),
memory: (
<div className="progress-container">
<Progress
percent={Number((host.memory * 100).toFixed(1))}
size="small"
strokeColor={((): string => {
const memoryPercent = Number((host.memory * 100).toFixed(1));
if (memoryPercent >= 90) return Color.BG_CHERRY_500;
if (memoryPercent >= 60) return Color.BG_AMBER_500;
return Color.BG_FOREST_500;
})()}
className="progress-bar"
/>
</div>
),
ioWait: host.wait,
load15: host.load15,
}));

View File

@@ -0,0 +1,51 @@
.infra-monitoring-module-container {
flex: 1;
display: flex;
flex-direction: column;
.ant-tabs {
flex: 1;
}
.ant-tabs-nav {
padding: 0;
margin-bottom: 0px;
&::before {
border-bottom: 1px solid var(--bg-slate-400) !important;
}
}
.ant-tabs-content-holder {
display: flex;
.ant-tabs-content {
flex: 1;
display: flex;
flex-direction: column;
.ant-tabs-tabpane {
flex: 1;
display: flex;
flex-direction: column;
}
}
}
.tab-item {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
}
.lightMode {
.infra-monitoring-module-container {
.ant-tabs-nav {
&::before {
border-bottom: 1px solid var(--bg-vanilla-300) !important;
}
}
}
}

View File

@@ -0,0 +1,20 @@
import './InfrastructureMonitoring.styles.scss';
import RouteTab from 'components/RouteTab';
import { TabRoutes } from 'components/RouteTab/types';
import history from 'lib/history';
import { useLocation } from 'react-use';
import { Hosts } from './constants';
export default function InfrastructureMonitoringPage(): JSX.Element {
const { pathname } = useLocation();
const routes: TabRoutes[] = [Hosts];
return (
<div className="infra-monitoring-module-container">
<RouteTab routes={routes} activeKey={pathname} history={history} />
</div>
);
}

View File

@@ -0,0 +1,15 @@
import { TabRoutes } from 'components/RouteTab/types';
import ROUTES from 'constants/routes';
import { Inbox } from 'lucide-react';
import InfraMonitoringHosts from 'pages/InfraMonitoringHosts';
export const Hosts: TabRoutes = {
Component: InfraMonitoringHosts,
name: (
<div className="tab-item">
<Inbox size={16} /> Hosts
</div>
),
route: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
key: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
};

View File

@@ -0,0 +1,3 @@
import InfrastructureMonitoringPage from './InfrastructureMonitoringPage';
export default InfrastructureMonitoringPage;

View File

@@ -3,9 +3,10 @@ import { DataSource } from 'types/common/queryBuilder';
import { BaseAutocompleteData } from './queryAutocompleteResponse'; import { BaseAutocompleteData } from './queryAutocompleteResponse';
export interface IGetAttributeKeysPayload { export interface IGetAttributeKeysPayload {
aggregateOperator: string; aggregateOperator?: string;
dataSource: DataSource; dataSource: DataSource;
searchText: string; searchText: string;
aggregateAttribute: string; aggregateAttribute?: string;
tagType?: BaseAutocompleteData['type']; tagType?: BaseAutocompleteData['type'];
isInfraMonitoring?: boolean;
} }

View File

@@ -103,4 +103,5 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
SHORTCUTS: ['ADMIN', 'EDITOR', 'VIEWER'], SHORTCUTS: ['ADMIN', 'EDITOR', 'VIEWER'],
INTEGRATIONS: ['ADMIN', 'EDITOR', 'VIEWER'], INTEGRATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
SERVICE_TOP_LEVEL_OPERATIONS: ['ADMIN', 'EDITOR', 'VIEWER'], SERVICE_TOP_LEVEL_OPERATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
INFRASTRUCTURE_MONITORING_HOSTS: ['ADMIN', 'EDITOR', 'VIEWER'],
}; };