mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-29 16:14:42 +00:00
feat: pods and nodes implementation for k8s infra monitoring (#6558)
This commit is contained in:
parent
64a4606275
commit
458cd28cc2
@ -24,7 +24,7 @@ const config: Config.InitialOptions = {
|
|||||||
'^.+\\.(js|jsx)$': 'babel-jest',
|
'^.+\\.(js|jsx)$': 'babel-jest',
|
||||||
},
|
},
|
||||||
transformIgnorePatterns: [
|
transformIgnorePatterns: [
|
||||||
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|d3-interpolate|d3-color)/)',
|
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|d3-interpolate|d3-color|api)/)',
|
||||||
],
|
],
|
||||||
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
|
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
|
||||||
testPathIgnorePatterns: ['/node_modules/', '/public/'],
|
testPathIgnorePatterns: ['/node_modules/', '/public/'],
|
||||||
|
|||||||
@ -43,5 +43,6 @@
|
|||||||
"DEFAULT": "Open source Observability Platform | SigNoz",
|
"DEFAULT": "Open source Observability Platform | SigNoz",
|
||||||
"ALERT_HISTORY": "SigNoz | Alert Rule History",
|
"ALERT_HISTORY": "SigNoz | Alert Rule History",
|
||||||
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
|
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
|
||||||
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring"
|
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
|
||||||
|
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,5 +56,6 @@
|
|||||||
"ALERT_HISTORY": "SigNoz | Alert Rule History",
|
"ALERT_HISTORY": "SigNoz | Alert Rule History",
|
||||||
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
|
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
|
||||||
"MESSAGING_QUEUES": "SigNoz | Messaging Queues",
|
"MESSAGING_QUEUES": "SigNoz | Messaging Queues",
|
||||||
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring"
|
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
|
||||||
|
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -415,6 +415,13 @@ const routes: AppRoutes[] = [
|
|||||||
key: 'INFRASTRUCTURE_MONITORING_HOSTS',
|
key: 'INFRASTRUCTURE_MONITORING_HOSTS',
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES,
|
||||||
|
exact: true,
|
||||||
|
component: InfrastructureMonitoring,
|
||||||
|
key: 'INFRASTRUCTURE_MONITORING_KUBERNETES',
|
||||||
|
isPrivate: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const SUPPORT_ROUTE: AppRoutes = {
|
export const SUPPORT_ROUTE: AppRoutes = {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { ApiBaseInstance } 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';
|
||||||
|
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||||
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import {
|
import {
|
||||||
@ -11,12 +12,18 @@ import {
|
|||||||
|
|
||||||
export const getHostAttributeKeys = async (
|
export const getHostAttributeKeys = async (
|
||||||
searchText = '',
|
searchText = '',
|
||||||
|
entity: K8sCategory,
|
||||||
): Promise<SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse> => {
|
): Promise<SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const response: AxiosResponse<{
|
const response: AxiosResponse<{
|
||||||
data: IQueryAutocompleteResponse;
|
data: IQueryAutocompleteResponse;
|
||||||
}> = await ApiBaseInstance.get(
|
}> = await ApiBaseInstance.get(
|
||||||
`/hosts/attribute_keys?dataSource=metrics&searchText=${searchText}`,
|
`/${entity}/attribute_keys?dataSource=metrics&searchText=${searchText}`,
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
limit: 500,
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const payload: BaseAutocompleteData[] =
|
const payload: BaseAutocompleteData[] =
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export const getInfraAttributesValues = async ({
|
|||||||
filterAttributeKeyDataType,
|
filterAttributeKeyDataType,
|
||||||
tagType,
|
tagType,
|
||||||
searchText,
|
searchText,
|
||||||
|
aggregateAttribute,
|
||||||
}: IGetAttributeValuesPayload): Promise<
|
}: IGetAttributeValuesPayload): Promise<
|
||||||
SuccessResponse<IAttributeValuesResponse> | ErrorResponse
|
SuccessResponse<IAttributeValuesResponse> | ErrorResponse
|
||||||
> => {
|
> => {
|
||||||
@ -23,6 +24,7 @@ export const getInfraAttributesValues = async ({
|
|||||||
dataSource,
|
dataSource,
|
||||||
attributeKey,
|
attributeKey,
|
||||||
searchText,
|
searchText,
|
||||||
|
aggregateAttribute,
|
||||||
})}&filterAttributeKeyDataType=${filterAttributeKeyDataType}&tagType=${tagType}`,
|
})}&filterAttributeKeyDataType=${filterAttributeKeyDataType}&tagType=${tagType}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
65
frontend/src/api/infraMonitoring/getK8sNodesList.ts
Normal file
65
frontend/src/api/infraMonitoring/getK8sNodesList.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { ApiBaseInstance } from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
export interface K8sNodesListPayload {
|
||||||
|
filters: TagFilter;
|
||||||
|
groupBy?: BaseAutocompleteData[];
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
orderBy?: {
|
||||||
|
columnName: string;
|
||||||
|
order: 'asc' | 'desc';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface K8sNodesData {
|
||||||
|
nodeUID: string;
|
||||||
|
nodeCPUUsage: number;
|
||||||
|
nodeCPUAllocatable: number;
|
||||||
|
nodeMemoryUsage: number;
|
||||||
|
nodeMemoryAllocatable: number;
|
||||||
|
meta: {
|
||||||
|
k8s_node_name: string;
|
||||||
|
k8s_node_uid: string;
|
||||||
|
k8s_cluster_name: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface K8sNodesListResponse {
|
||||||
|
status: string;
|
||||||
|
data: {
|
||||||
|
type: string;
|
||||||
|
records: K8sNodesData[];
|
||||||
|
groups: null;
|
||||||
|
total: number;
|
||||||
|
sentAnyHostMetricsData: boolean;
|
||||||
|
isSendingK8SAgentMetrics: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getK8sNodesList = async (
|
||||||
|
props: K8sNodesListPayload,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
headers?: Record<string, string>,
|
||||||
|
): Promise<SuccessResponse<K8sNodesListResponse> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await ApiBaseInstance.post('/nodes/list', props, {
|
||||||
|
signal,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: 'Success',
|
||||||
|
payload: response.data,
|
||||||
|
params: props,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
93
frontend/src/api/infraMonitoring/getK8sPodsList.ts
Normal file
93
frontend/src/api/infraMonitoring/getK8sPodsList.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { ApiBaseInstance } from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
export interface K8sPodsListPayload {
|
||||||
|
filters: TagFilter;
|
||||||
|
groupBy?: BaseAutocompleteData[];
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
orderBy?: {
|
||||||
|
columnName: string;
|
||||||
|
order: 'asc' | 'desc';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimeSeriesValue {
|
||||||
|
timestamp: number;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimeSeries {
|
||||||
|
labels: Record<string, string>;
|
||||||
|
labelsArray: Array<Record<string, string>>;
|
||||||
|
values: TimeSeriesValue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface K8sPodsData {
|
||||||
|
podUID: string;
|
||||||
|
podCPU: number;
|
||||||
|
podCPURequest: number;
|
||||||
|
podCPULimit: number;
|
||||||
|
podMemory: number;
|
||||||
|
podMemoryRequest: number;
|
||||||
|
podMemoryLimit: number;
|
||||||
|
restartCount: number;
|
||||||
|
meta: {
|
||||||
|
k8s_cronjob_name: string;
|
||||||
|
k8s_daemonset_name: string;
|
||||||
|
k8s_deployment_name: string;
|
||||||
|
k8s_job_name: string;
|
||||||
|
k8s_namespace_name: string;
|
||||||
|
k8s_node_name: string;
|
||||||
|
k8s_pod_name: string;
|
||||||
|
k8s_pod_uid: string;
|
||||||
|
k8s_statefulset_name: string;
|
||||||
|
k8s_cluster_name: string;
|
||||||
|
};
|
||||||
|
countByPhase: {
|
||||||
|
pending: number;
|
||||||
|
running: number;
|
||||||
|
succeeded: number;
|
||||||
|
failed: number;
|
||||||
|
unknown: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface K8sPodsListResponse {
|
||||||
|
status: string;
|
||||||
|
data: {
|
||||||
|
type: string;
|
||||||
|
records: K8sPodsData[];
|
||||||
|
groups: null;
|
||||||
|
total: number;
|
||||||
|
sentAnyHostMetricsData: boolean;
|
||||||
|
isSendingK8SAgentMetrics: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getK8sPodsList = async (
|
||||||
|
props: K8sPodsListPayload,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
headers?: Record<string, string>,
|
||||||
|
): Promise<SuccessResponse<K8sPodsListResponse> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await ApiBaseInstance.post('/pods/list', props, {
|
||||||
|
signal,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: 'Success',
|
||||||
|
payload: response.data,
|
||||||
|
params: props,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -4,6 +4,7 @@ export enum VIEWS {
|
|||||||
TRACES = 'traces',
|
TRACES = 'traces',
|
||||||
CONTAINERS = 'containers',
|
CONTAINERS = 'containers',
|
||||||
PROCESSES = 'processes',
|
PROCESSES = 'processes',
|
||||||
|
EVENTS = 'events',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VIEW_TYPES = {
|
export const VIEW_TYPES = {
|
||||||
@ -12,4 +13,5 @@ export const VIEW_TYPES = {
|
|||||||
TRACES: VIEWS.TRACES,
|
TRACES: VIEWS.TRACES,
|
||||||
CONTAINERS: VIEWS.CONTAINERS,
|
CONTAINERS: VIEWS.CONTAINERS,
|
||||||
PROCESSES: VIEWS.PROCESSES,
|
PROCESSES: VIEWS.PROCESSES,
|
||||||
|
EVENTS: VIEWS.EVENTS,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -53,7 +53,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
width: 100%;
|
width: calc(100% - 24px);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&.filter-disabled {
|
&.filter-disabled {
|
||||||
|
|||||||
@ -8,10 +8,12 @@ import { Button, Checkbox, Input, Skeleton, Typography } from 'antd';
|
|||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { IQuickFiltersConfig } from 'components/QuickFilters/QuickFilters';
|
import { IQuickFiltersConfig } from 'components/QuickFilters/QuickFilters';
|
||||||
import { OPERATORS } from 'constants/queryBuilder';
|
import { OPERATORS } from 'constants/queryBuilder';
|
||||||
|
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
||||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||||
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
|
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { cloneDeep, isArray, isEmpty, isEqual } from 'lodash-es';
|
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||||
|
import { cloneDeep, isArray, isEmpty, isEqual, isFunction } from 'lodash-es';
|
||||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
@ -34,10 +36,11 @@ function setDefaultValues(
|
|||||||
}
|
}
|
||||||
interface ICheckboxProps {
|
interface ICheckboxProps {
|
||||||
filter: IQuickFiltersConfig;
|
filter: IQuickFiltersConfig;
|
||||||
|
onFilterChange?: (query: Query) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||||
const { filter } = props;
|
const { filter, onFilterChange } = props;
|
||||||
const [searchText, setSearchText] = useState<string>('');
|
const [searchText, setSearchText] = useState<string>('');
|
||||||
const [isOpen, setIsOpen] = useState<boolean>(filter.defaultOpen);
|
const [isOpen, setIsOpen] = useState<boolean>(filter.defaultOpen);
|
||||||
const [visibleItemsCount, setVisibleItemsCount] = useState<number>(10);
|
const [visibleItemsCount, setVisibleItemsCount] = useState<number>(10);
|
||||||
@ -50,9 +53,9 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
|||||||
|
|
||||||
const { data, isLoading } = useGetAggregateValues(
|
const { data, isLoading } = useGetAggregateValues(
|
||||||
{
|
{
|
||||||
aggregateOperator: 'noop',
|
aggregateOperator: filter.aggregateOperator || 'noop',
|
||||||
dataSource: DataSource.LOGS,
|
dataSource: filter.dataSource || DataSource.LOGS,
|
||||||
aggregateAttribute: '',
|
aggregateAttribute: filter.aggregateAttribute || '',
|
||||||
attributeKey: filter.attributeKey.key,
|
attributeKey: filter.attributeKey.key,
|
||||||
filterAttributeKeyDataType: filter.attributeKey.dataType || DataTypes.EMPTY,
|
filterAttributeKeyDataType: filter.attributeKey.dataType || DataTypes.EMPTY,
|
||||||
tagType: filter.attributeKey.type || '',
|
tagType: filter.attributeKey.type || '',
|
||||||
@ -72,7 +75,11 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
|||||||
);
|
);
|
||||||
const currentAttributeKeys = attributeValues.slice(0, visibleItemsCount);
|
const currentAttributeKeys = attributeValues.slice(0, visibleItemsCount);
|
||||||
|
|
||||||
// derive the state of each filter key here in the renderer itself and keep it in sync with staged query
|
const setSearchTextDebounced = useDebouncedFn((...args) => {
|
||||||
|
setSearchText(args[0] as string);
|
||||||
|
}, DEBOUNCE_DELAY);
|
||||||
|
|
||||||
|
// derive the state of each filter key here in the renderer itself and keep it in sync with current query
|
||||||
// also we need to keep a note of last focussed query.
|
// also we need to keep a note of last focussed query.
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
const currentFilterState = useMemo(() => {
|
const currentFilterState = useMemo(() => {
|
||||||
@ -159,7 +166,12 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
redirectWithQueryBuilderData(preparedQuery);
|
|
||||||
|
if (onFilterChange && isFunction(onFilterChange)) {
|
||||||
|
onFilterChange(preparedQuery);
|
||||||
|
} else {
|
||||||
|
redirectWithQueryBuilderData(preparedQuery);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSomeFilterPresentForCurrentAttribute = currentQuery.builder.queryData?.[
|
const isSomeFilterPresentForCurrentAttribute = currentQuery.builder.queryData?.[
|
||||||
@ -391,7 +403,11 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
redirectWithQueryBuilderData(finalQuery);
|
if (onFilterChange && isFunction(onFilterChange)) {
|
||||||
|
onFilterChange(finalQuery);
|
||||||
|
} else {
|
||||||
|
redirectWithQueryBuilderData(finalQuery);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -440,7 +456,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
|||||||
<section className="search">
|
<section className="search">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Filter values"
|
placeholder="Filter values"
|
||||||
onChange={(e): void => setSearchText(e.target.value)}
|
onChange={(e): void => setSearchTextDebounced(e.target.value)}
|
||||||
disabled={isFilterDisabled}
|
disabled={isFilterDisabled}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
@ -511,3 +527,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CheckboxFilter.defaultProps = {
|
||||||
|
onFilterChange: null,
|
||||||
|
};
|
||||||
|
|||||||
@ -15,6 +15,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
@ -50,6 +52,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
.divider-filter {
|
.divider-filter {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
|
|||||||
@ -7,9 +7,10 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { Tooltip, Typography } from 'antd';
|
import { Tooltip, Typography } from 'antd';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { cloneDeep } from 'lodash-es';
|
import { cloneDeep, isFunction } from 'lodash-es';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import Checkbox from './FilterRenderers/Checkbox/Checkbox';
|
import Checkbox from './FilterRenderers/Checkbox/Checkbox';
|
||||||
import Slider from './FilterRenderers/Slider/Slider';
|
import Slider from './FilterRenderers/Slider/Slider';
|
||||||
@ -33,6 +34,9 @@ export interface IQuickFiltersConfig {
|
|||||||
type: FiltersType;
|
type: FiltersType;
|
||||||
title: string;
|
title: string;
|
||||||
attributeKey: BaseAutocompleteData;
|
attributeKey: BaseAutocompleteData;
|
||||||
|
aggregateOperator?: string;
|
||||||
|
aggregateAttribute?: string;
|
||||||
|
dataSource?: DataSource;
|
||||||
customRendererForValue?: (value: string) => JSX.Element;
|
customRendererForValue?: (value: string) => JSX.Element;
|
||||||
defaultOpen: boolean;
|
defaultOpen: boolean;
|
||||||
}
|
}
|
||||||
@ -40,10 +44,12 @@ export interface IQuickFiltersConfig {
|
|||||||
interface IQuickFiltersProps {
|
interface IQuickFiltersProps {
|
||||||
config: IQuickFiltersConfig[];
|
config: IQuickFiltersConfig[];
|
||||||
handleFilterVisibilityChange: () => void;
|
handleFilterVisibilityChange: () => void;
|
||||||
|
source?: string | null;
|
||||||
|
onFilterChange?: (query: Query) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||||
const { config, handleFilterVisibilityChange } = props;
|
const { config, handleFilterVisibilityChange, source, onFilterChange } = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
currentQuery,
|
currentQuery,
|
||||||
@ -78,47 +84,63 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
redirectWithQueryBuilderData(preparedQuery);
|
|
||||||
|
if (onFilterChange && isFunction(onFilterChange)) {
|
||||||
|
onFilterChange(preparedQuery);
|
||||||
|
} else {
|
||||||
|
redirectWithQueryBuilderData(preparedQuery);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const lastQueryName =
|
const lastQueryName =
|
||||||
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
|
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
|
||||||
|
|
||||||
|
const isInfraMonitoring = source === 'infra-monitoring';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="quick-filters">
|
<div className="quick-filters">
|
||||||
<section className="header">
|
{!isInfraMonitoring && (
|
||||||
<section className="left-actions">
|
<section className="header">
|
||||||
<FilterOutlined />
|
<section className="left-actions">
|
||||||
<Typography.Text className="text">Filters for</Typography.Text>
|
<FilterOutlined />
|
||||||
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
|
<Typography.Text className="text">Filters for</Typography.Text>
|
||||||
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
|
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
|
||||||
</Tooltip>
|
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
|
||||||
|
</Tooltip>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="right-actions">
|
||||||
|
<Tooltip title="Reset All">
|
||||||
|
<SyncOutlined className="sync-icon" onClick={handleReset} />
|
||||||
|
</Tooltip>
|
||||||
|
<div className="divider-filter" />
|
||||||
|
<Tooltip title="Collapse Filters">
|
||||||
|
<VerticalAlignTopOutlined
|
||||||
|
rotate={270}
|
||||||
|
onClick={handleFilterVisibilityChange}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</section>
|
||||||
</section>
|
</section>
|
||||||
<section className="right-actions">
|
)}
|
||||||
<Tooltip title="Reset All">
|
|
||||||
<SyncOutlined className="sync-icon" onClick={handleReset} />
|
|
||||||
</Tooltip>
|
|
||||||
<div className="divider-filter" />
|
|
||||||
<Tooltip title="Collapse Filters">
|
|
||||||
<VerticalAlignTopOutlined
|
|
||||||
rotate={270}
|
|
||||||
onClick={handleFilterVisibilityChange}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="filters">
|
<section className="filters">
|
||||||
{config.map((filter) => {
|
{config.map((filter) => {
|
||||||
switch (filter.type) {
|
switch (filter.type) {
|
||||||
case FiltersType.CHECKBOX:
|
case FiltersType.CHECKBOX:
|
||||||
return <Checkbox filter={filter} />;
|
return <Checkbox filter={filter} onFilterChange={onFilterChange} />;
|
||||||
case FiltersType.SLIDER:
|
case FiltersType.SLIDER:
|
||||||
return <Slider filter={filter} />;
|
return <Slider filter={filter} />;
|
||||||
default:
|
default:
|
||||||
return <Checkbox filter={filter} />;
|
return <Checkbox filter={filter} onFilterChange={onFilterChange} />;
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QuickFilters.defaultProps = {
|
||||||
|
source: null,
|
||||||
|
onFilterChange: null,
|
||||||
|
};
|
||||||
|
|||||||
@ -21,4 +21,5 @@ export const REACT_QUERY_KEY = {
|
|||||||
GET_HOST_LIST: 'GET_HOST_LIST',
|
GET_HOST_LIST: 'GET_HOST_LIST',
|
||||||
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
|
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
|
||||||
GET_ACTIVE_LICENSE_V3: 'GET_ACTIVE_LICENSE_V3',
|
GET_ACTIVE_LICENSE_V3: 'GET_ACTIVE_LICENSE_V3',
|
||||||
|
GET_POD_LIST: 'GET_POD_LIST',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -62,6 +62,7 @@ const ROUTES = {
|
|||||||
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',
|
INFRASTRUCTURE_MONITORING_HOSTS: '/infrastructure-monitoring/hosts',
|
||||||
|
INFRASTRUCTURE_MONITORING_KUBERNETES: '/infrastructure-monitoring/kubernetes',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default ROUTES;
|
export default ROUTES;
|
||||||
|
|||||||
@ -292,8 +292,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
|
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
|
||||||
const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY';
|
const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY';
|
||||||
const isAlertOverview = (): boolean => routeKey === 'ALERT_OVERVIEW';
|
const isAlertOverview = (): boolean => routeKey === 'ALERT_OVERVIEW';
|
||||||
const isInfraMonitoringHosts = (): boolean =>
|
const isInfraMonitoring = (): boolean =>
|
||||||
routeKey === 'INFRASTRUCTURE_MONITORING_HOSTS';
|
routeKey === 'INFRASTRUCTURE_MONITORING_HOSTS' ||
|
||||||
|
routeKey === 'INFRASTRUCTURE_MONITORING_KUBERNETES';
|
||||||
const isPathMatch = (regex: RegExp): boolean => regex.test(pathname);
|
const isPathMatch = (regex: RegExp): boolean => regex.test(pathname);
|
||||||
|
|
||||||
const isDashboardView = (): boolean =>
|
const isDashboardView = (): boolean =>
|
||||||
@ -422,7 +423,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
isAlertHistory() ||
|
isAlertHistory() ||
|
||||||
isAlertOverview() ||
|
isAlertOverview() ||
|
||||||
isMessagingQueues() ||
|
isMessagingQueues() ||
|
||||||
isInfraMonitoringHosts()
|
isInfraMonitoring()
|
||||||
? 0
|
? 0
|
||||||
: '0 1rem',
|
: '0 1rem',
|
||||||
|
|
||||||
|
|||||||
@ -168,7 +168,8 @@ function HostsList(): JSX.Element {
|
|||||||
const showHostsEmptyState =
|
const showHostsEmptyState =
|
||||||
!isFetching &&
|
!isFetching &&
|
||||||
!isLoading &&
|
!isLoading &&
|
||||||
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics);
|
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics) &&
|
||||||
|
!filters.items.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="hosts-list">
|
<div className="hosts-list">
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import './InfraMonitoring.styles.scss';
|
import './InfraMonitoring.styles.scss';
|
||||||
|
|
||||||
|
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
@ -47,6 +48,7 @@ function HostsListControls({
|
|||||||
onChange={handleChangeTagFilters}
|
onChange={handleChangeTagFilters}
|
||||||
isInfraMonitoring
|
isInfraMonitoring
|
||||||
disableNavigationShortcuts
|
disableNavigationShortcuts
|
||||||
|
entity={K8sCategory.HOSTS}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -93,7 +93,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hostname-column-value {
|
.hostname-column-value {
|
||||||
color: var(--Vanilla-100, #fff);
|
color: var(--bg-vanilla-100);
|
||||||
font-family: 'Geist Mono';
|
font-family: 'Geist Mono';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@ -137,6 +137,9 @@
|
|||||||
.column-header-right {
|
.column-header-right {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
.column-header-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
.ant-table-tbody > tr > td {
|
.ant-table-tbody > tr > td {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ export const getHostListsQuery = (): HostListPayload => ({
|
|||||||
groupBy: [],
|
groupBy: [],
|
||||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getTabsItems = (): TabsProps['items'] => [
|
export const getTabsItems = (): TabsProps['items'] => [
|
||||||
{
|
{
|
||||||
label: <TabLabel label="List View" isDisabled={false} tooltipText="" />,
|
label: <TabLabel label="List View" isDisabled={false} tooltipText="" />,
|
||||||
|
|||||||
@ -0,0 +1,834 @@
|
|||||||
|
.k8s-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: calc(100vh - 45px);
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.k8s-quick-filters-container {
|
||||||
|
width: 280px;
|
||||||
|
min-width: 280px;
|
||||||
|
border-right: 1px solid var(--bg-slate-400);
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.k8s-quick-filters-container-header {
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0.1rem;
|
||||||
|
height: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--bg-ink-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-collapse-header {
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400);
|
||||||
|
padding: 12px 8px;
|
||||||
|
|
||||||
|
&[aria-expanded='true'] {
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-collapse-content-box {
|
||||||
|
padding: 0px;
|
||||||
|
padding-block: 0px !important;
|
||||||
|
|
||||||
|
.quick-filters {
|
||||||
|
.checkbox-filter {
|
||||||
|
padding-left: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-filters {
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0.1rem;
|
||||||
|
height: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--bg-slate-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--bg-slate-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.k8s-quick-filters-category-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.k8s-quick-filters-category-label-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.k8s-quick-filters-category-label-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.k8s-list-container {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
&.k8s-list-container-filters-visible {
|
||||||
|
max-width: calc(100% - 280px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.periscope-btn {
|
||||||
|
&.ghost:not(:disabled) {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--bg-robin-500) !important;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
font-size: 11px;
|
||||||
|
|
||||||
|
&.pod-group-header {
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.infra-monitoring-container {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.infra-monitoring-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.k8s-list {
|
||||||
|
.column {
|
||||||
|
min-width: 180px;
|
||||||
|
max-width: 180px;
|
||||||
|
|
||||||
|
font-size: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-pod-name {
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-pod-group {
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-progress-bar {
|
||||||
|
min-width: 180px;
|
||||||
|
max-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-dummy {
|
||||||
|
min-width: 32px;
|
||||||
|
max-width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pod-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pod-group-tag-item {
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
background: var(--bg-slate-400);
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pod-name {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-container {
|
||||||
|
.ant-table-row-expand-icon-cell {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-content {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0.1rem;
|
||||||
|
height: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--bg-robin-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--bg-robin-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.k8s-list-controls {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.ant-select-selector {
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--bg-slate-400) !important;
|
||||||
|
background-color: var(--bg-ink-300) !important;
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tag .ant-typography {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.k8s-list-controls-left {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.k8s-qb-search-container {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 240px;
|
||||||
|
max-width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.k8s-attribute-search-container {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 240px;
|
||||||
|
max-width: 40%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.group-by-label {
|
||||||
|
min-width: max-content;
|
||||||
|
|
||||||
|
color: var(--bg-vanilla-100, #c0c1c3);
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
|
||||||
|
border-radius: 2px 0px 0px 2px;
|
||||||
|
border: 1px solid var(--bg-slate-400, #1d212d);
|
||||||
|
border-right: none;
|
||||||
|
background: var(--bg-ink-100, #16181d);
|
||||||
|
border-top-right-radius: 0px;
|
||||||
|
border-bottom-right-radius: 0px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
height: 32px;
|
||||||
|
padding: 6px 6px 6px 8px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-by-select {
|
||||||
|
.ant-select-selector {
|
||||||
|
border-left: none;
|
||||||
|
border-top-left-radius: 0px;
|
||||||
|
border-bottom-left-radius: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.k8s-list-controls-right {
|
||||||
|
min-width: 240px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.periscope-btn {
|
||||||
|
padding: 4px 8px;
|
||||||
|
|
||||||
|
&.ghost:not(:disabled) {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-progress-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 8px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable-row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.k8s-list-table {
|
||||||
|
.ant-table {
|
||||||
|
.ant-table-thead > tr > th {
|
||||||
|
padding: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
border-bottom: none;
|
||||||
|
|
||||||
|
color: var(--Vanilla-400, #c0c1c3);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 18px; /* 163.636% */
|
||||||
|
letter-spacing: 0.44px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-thead > tr > th:has(.hostname-column-header) {
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell {
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:has(.hostname-column-value) {
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hostname-column-value {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cell {
|
||||||
|
.active-tag {
|
||||||
|
color: var(--bg-forest-500);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
.ant-progress-bg {
|
||||||
|
height: 8px !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr:hover > td {
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:first-child {
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:nth-child(2) {
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:nth-child(n + 3) {
|
||||||
|
padding-right: 24px;
|
||||||
|
}
|
||||||
|
.column-header-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.ant-table-tbody > tr > td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-thead
|
||||||
|
> tr
|
||||||
|
> th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-empty-normal {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-pagination {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
width: calc(100% - 64px);
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
padding: 16px;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
// this is to offset intercom icon till we improve the design
|
||||||
|
padding-right: 72px;
|
||||||
|
|
||||||
|
.ant-pagination-item {
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&-active {
|
||||||
|
background: var(--bg-robin-500);
|
||||||
|
border-color: var(--bg-robin-500);
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--bg-ink-500) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-expanded-row {
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell {
|
||||||
|
background: var(--bg-ink-500) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table .ant-table-thead > tr > th {
|
||||||
|
padding: 4px 16px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expanded-table-container {
|
||||||
|
border: 1px solid var(--bg-ink-400);
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-left: 16px;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0.1rem;
|
||||||
|
height: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--bg-ink-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-expanded-row {
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell {
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expanded-table-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
padding-left: 42px;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
.periscope-btn {
|
||||||
|
font-size: 10px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-all-text {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.k8s-list-container-filters-visible {
|
||||||
|
.k8s-list-table {
|
||||||
|
.ant-pagination {
|
||||||
|
width: calc(100% - 340px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.infra-monitoring-tags {
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 18px; /* 163.636% */
|
||||||
|
letter-spacing: 0.44px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
border-radius: 50px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--Forest-500, #25e192);
|
||||||
|
border: 1px solid rgba(37, 225, 146, 0.2);
|
||||||
|
background: rgba(37, 225, 146, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.inactive {
|
||||||
|
color: var(--Slate-50, #62687c);
|
||||||
|
border: 1px solid rgba(98, 104, 124, 0.2);
|
||||||
|
background: rgba(98, 104, 124, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.k8s-list-loading-state {
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
|
||||||
|
.k8s-list-loading-state-item {
|
||||||
|
height: 48px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-filtered-hosts-message-container {
|
||||||
|
height: 30vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.no-filtered-hosts-message-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: fit-content;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-filtered-hosts-message {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hosts-empty-state-container {
|
||||||
|
padding: 16px;
|
||||||
|
height: 40vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.hosts-empty-state-container-content {
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
.no-hosts-message {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.no-hosts-message-title {
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.infra-monitoring-container {
|
||||||
|
.ant-table-thead > tr > th {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
color: var(--bg-ink-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell {
|
||||||
|
color: var(--bg-ink-500);
|
||||||
|
}
|
||||||
|
.k8s-list-controls {
|
||||||
|
border-top: 1px solid var(--bg-vanilla-300);
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.ant-select-selector {
|
||||||
|
border-color: var(--bg-vanilla-300) !important;
|
||||||
|
background-color: var(--bg-vanilla-100) !important;
|
||||||
|
color: var(--bg-ink-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.k8s-list-table {
|
||||||
|
.ant-table {
|
||||||
|
.ant-table-thead > tr > th {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
color: var(--text-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-thead > tr > th:has(.hostname-column-header) {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
color: var(--bg-ink-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:has(.hostname-column-value) {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hostname-column-value {
|
||||||
|
color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr:hover > td {
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-pagination {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
|
||||||
|
.ant-pagination-item {
|
||||||
|
&-active {
|
||||||
|
background: var(--bg-robin-500);
|
||||||
|
border-color: var(--bg-robin-500);
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--bg-vanilla-100) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell {
|
||||||
|
min-width: 170px !important;
|
||||||
|
max-width: 170px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-row-expand-icon-cell {
|
||||||
|
min-width: 30px !important;
|
||||||
|
max-width: 30px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-content-container {
|
||||||
|
.ant-table {
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
|
||||||
|
.ant-table-row:hover {
|
||||||
|
.ant-table-cell {
|
||||||
|
.value-field {
|
||||||
|
.action-btn {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 16px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell {
|
||||||
|
border: 1px solid var(--bg-slate-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute-name {
|
||||||
|
.ant-btn {
|
||||||
|
&:hover {
|
||||||
|
background-color: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute-pin {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
padding: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.log-attribute-pin {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.pin-attribute-icon {
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&.pinned svg {
|
||||||
|
fill: var(--bg-robin-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-field-container {
|
||||||
|
background: rgba(22, 25, 34, 0.4);
|
||||||
|
|
||||||
|
.value-field {
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
display: none;
|
||||||
|
width: max-content;
|
||||||
|
position: absolute;
|
||||||
|
// padding: 0 16px;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
.filter-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--bg-slate-400);
|
||||||
|
padding: 2px 3px;
|
||||||
|
gap: 3px;
|
||||||
|
height: 18px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.event-content-container {
|
||||||
|
.ant-table {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell {
|
||||||
|
border: 1px solid var(--bg-vanilla-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-field-container {
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
&.attribute-pin {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
.filter-btn {
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
319
frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx
Normal file
319
frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
import './InfraMonitoringK8s.styles.scss';
|
||||||
|
|
||||||
|
import { VerticalAlignTopOutlined } from '@ant-design/icons';
|
||||||
|
import * as Sentry from '@sentry/react';
|
||||||
|
import type { CollapseProps } from 'antd';
|
||||||
|
import { Collapse, Tooltip, Typography } from 'antd';
|
||||||
|
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||||
|
import { Container, Workflow } from 'lucide-react';
|
||||||
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
import {
|
||||||
|
K8sCategories,
|
||||||
|
NodesQuickFiltersConfig,
|
||||||
|
PodsQuickFiltersConfig,
|
||||||
|
} from './constants';
|
||||||
|
import K8sNodesList from './Nodes/K8sNodesList';
|
||||||
|
import K8sPodLists from './Pods/K8sPodLists';
|
||||||
|
|
||||||
|
export default function InfraMonitoringK8s(): JSX.Element {
|
||||||
|
const [showFilters, setShowFilters] = useState(true);
|
||||||
|
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState(K8sCategories.PODS);
|
||||||
|
|
||||||
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
|
const handleFilterVisibilityChange = (): void => {
|
||||||
|
setShowFilters(!showFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { handleChangeQueryData } = useQueryOperations({
|
||||||
|
index: 0,
|
||||||
|
query: currentQuery.builder.queryData[0],
|
||||||
|
entityVersion: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFilterChange = useCallback(
|
||||||
|
(query: Query): void => {
|
||||||
|
// update the current query with the new filters
|
||||||
|
// in infra monitoring k8s, we are using only one query, hence updating the 0th index of queryData
|
||||||
|
handleChangeQueryData('filters', query.builder.queryData[0].filters);
|
||||||
|
},
|
||||||
|
[handleChangeQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const items: CollapseProps['items'] = [
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<div className="k8s-quick-filters-category-label">
|
||||||
|
<div className="k8s-quick-filters-category-label-container">
|
||||||
|
<Container size={14} className="k8s-quick-filters-category-label-icon" />
|
||||||
|
<Typography.Text>Pods</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
key: K8sCategories.PODS,
|
||||||
|
showArrow: false,
|
||||||
|
children: (
|
||||||
|
<QuickFilters
|
||||||
|
source="infra-monitoring"
|
||||||
|
config={PodsQuickFiltersConfig}
|
||||||
|
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||||
|
onFilterChange={handleFilterChange}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<div className="k8s-quick-filters-category-label">
|
||||||
|
<div className="k8s-quick-filters-category-label-container">
|
||||||
|
<Workflow size={14} className="k8s-quick-filters-category-label-icon" />
|
||||||
|
<Typography.Text>Nodes</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
key: K8sCategories.NODES,
|
||||||
|
showArrow: false,
|
||||||
|
children: (
|
||||||
|
<QuickFilters
|
||||||
|
source="infra-monitoring"
|
||||||
|
config={NodesQuickFiltersConfig}
|
||||||
|
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||||
|
onFilterChange={handleFilterChange}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
// NOTE - Enabled these as we release new entities
|
||||||
|
// {
|
||||||
|
// label: (
|
||||||
|
// <div className="k8s-quick-filters-category-label">
|
||||||
|
// <div className="k8s-quick-filters-category-label-container">
|
||||||
|
// <FilePenLine
|
||||||
|
// size={14}
|
||||||
|
// className="k8s-quick-filters-category-label-icon"
|
||||||
|
// />
|
||||||
|
// <Typography.Text>Namespace</Typography.Text>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// ),
|
||||||
|
// key: K8sCategories.NAMESPACES,
|
||||||
|
// showArrow: false,
|
||||||
|
// children: (
|
||||||
|
// <QuickFilters
|
||||||
|
// source="infra-monitoring"
|
||||||
|
// config={NamespaceQuickFiltersConfig}
|
||||||
|
// handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||||
|
// onFilterChange={handleFilterChange}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: (
|
||||||
|
// <div className="k8s-quick-filters-category-label">
|
||||||
|
// <div className="k8s-quick-filters-category-label-container">
|
||||||
|
// <Boxes size={14} className="k8s-quick-filters-category-label-icon" />
|
||||||
|
// <Typography.Text>Clusters</Typography.Text>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// ),
|
||||||
|
// key: K8sCategories.CLUSTERS,
|
||||||
|
// showArrow: false,
|
||||||
|
// children: (
|
||||||
|
// <QuickFilters
|
||||||
|
// source="infra-monitoring"
|
||||||
|
// config={ClustersQuickFiltersConfig}
|
||||||
|
// handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||||
|
// onFilterChange={handleFilterChange}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: (
|
||||||
|
// <div className="k8s-quick-filters-category-label">
|
||||||
|
// <div className="k8s-quick-filters-category-label-container">
|
||||||
|
// <PackageOpen
|
||||||
|
// size={14}
|
||||||
|
// className="k8s-quick-filters-category-label-icon"
|
||||||
|
// />
|
||||||
|
// <Typography.Text>Containers</Typography.Text>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// ),
|
||||||
|
// key: K8sCategories.CONTAINERS,
|
||||||
|
// showArrow: false,
|
||||||
|
// children: (
|
||||||
|
// <QuickFilters
|
||||||
|
// source="infra-monitoring"
|
||||||
|
// config={ContainersQuickFiltersConfig}
|
||||||
|
// handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||||
|
// onFilterChange={handleFilterChange}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: (
|
||||||
|
// <div className="k8s-quick-filters-category-label">
|
||||||
|
// <div className="k8s-quick-filters-category-label-container">
|
||||||
|
// <HardDrive size={14} className="k8s-quick-filters-category-label-icon" />
|
||||||
|
// <Typography.Text>Volumes</Typography.Text>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// ),
|
||||||
|
// key: K8sCategories.VOLUMES,
|
||||||
|
// showArrow: false,
|
||||||
|
// children: (
|
||||||
|
// <QuickFilters
|
||||||
|
// source="infra-monitoring"
|
||||||
|
// config={VolumesQuickFiltersConfig}
|
||||||
|
// handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||||
|
// onFilterChange={handleFilterChange}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: (
|
||||||
|
// <div className="k8s-quick-filters-category-label">
|
||||||
|
// <div className="k8s-quick-filters-category-label-container">
|
||||||
|
// <Computer size={14} className="k8s-quick-filters-category-label-icon" />
|
||||||
|
// <Typography.Text>Deployments</Typography.Text>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// ),
|
||||||
|
// key: K8sCategories.DEPLOYMENTS,
|
||||||
|
// showArrow: false,
|
||||||
|
// children: (
|
||||||
|
// <QuickFilters
|
||||||
|
// source="infra-monitoring"
|
||||||
|
// config={DeploymentsQuickFiltersConfig}
|
||||||
|
// handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||||
|
// onFilterChange={handleFilterChange}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: (
|
||||||
|
// <div className="k8s-quick-filters-category-label">
|
||||||
|
// <div className="k8s-quick-filters-category-label-container">
|
||||||
|
// <Bolt size={14} className="k8s-quick-filters-category-label-icon" />
|
||||||
|
// <Typography.Text>Jobs</Typography.Text>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// ),
|
||||||
|
// key: K8sCategories.JOBS,
|
||||||
|
// showArrow: false,
|
||||||
|
// children: (
|
||||||
|
// <QuickFilters
|
||||||
|
// source="infra-monitoring"
|
||||||
|
// config={JobsQuickFiltersConfig}
|
||||||
|
// handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||||
|
// onFilterChange={handleFilterChange}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: (
|
||||||
|
// <div className="k8s-quick-filters-category-label">
|
||||||
|
// <div className="k8s-quick-filters-category-label-container">
|
||||||
|
// <Group size={14} className="k8s-quick-filters-category-label-icon" />
|
||||||
|
// <Typography.Text>DaemonSets</Typography.Text>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// ),
|
||||||
|
// key: K8sCategories.DAEMONSETS,
|
||||||
|
// showArrow: false,
|
||||||
|
// children: (
|
||||||
|
// <QuickFilters
|
||||||
|
// source="infra-monitoring"
|
||||||
|
// config={DaemonSetsQuickFiltersConfig}
|
||||||
|
// handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||||
|
// onFilterChange={handleFilterChange}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: (
|
||||||
|
// <div className="k8s-quick-filters-category-label">
|
||||||
|
// <div className="k8s-quick-filters-category-label-container">
|
||||||
|
// <ArrowUpDown
|
||||||
|
// size={14}
|
||||||
|
// className="k8s-quick-filters-category-label-icon"
|
||||||
|
// />
|
||||||
|
// <Typography.Text>StatefulSets</Typography.Text>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// ),
|
||||||
|
// key: K8sCategories.STATEFULSETS,
|
||||||
|
// showArrow: false,
|
||||||
|
// children: (
|
||||||
|
// <QuickFilters
|
||||||
|
// source="infra-monitoring"
|
||||||
|
// config={StatefulsetsQuickFiltersConfig}
|
||||||
|
// handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||||
|
// onFilterChange={handleFilterChange}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleCategoryChange = (key: string | string[]): void => {
|
||||||
|
if (Array.isArray(key) && key.length > 0) {
|
||||||
|
setSelectedCategory(key[0] as string);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||||
|
<div className="infra-monitoring-container">
|
||||||
|
<div className="k8s-container">
|
||||||
|
{showFilters && (
|
||||||
|
<div className="k8s-quick-filters-container">
|
||||||
|
<div className="k8s-quick-filters-container-header">
|
||||||
|
<Typography.Text>Filters</Typography.Text>
|
||||||
|
|
||||||
|
<Tooltip title="Collapse Filters">
|
||||||
|
<VerticalAlignTopOutlined
|
||||||
|
rotate={270}
|
||||||
|
onClick={handleFilterVisibilityChange}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<Collapse
|
||||||
|
onChange={handleCategoryChange}
|
||||||
|
items={items}
|
||||||
|
defaultActiveKey={[selectedCategory]}
|
||||||
|
activeKey={[selectedCategory]}
|
||||||
|
accordion
|
||||||
|
bordered={false}
|
||||||
|
ghost
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`k8s-list-container ${
|
||||||
|
showFilters ? 'k8s-list-container-filters-visible' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{selectedCategory === K8sCategories.PODS && (
|
||||||
|
<K8sPodLists
|
||||||
|
isFiltersVisible={showFilters}
|
||||||
|
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedCategory === K8sCategories.NODES && (
|
||||||
|
<K8sNodesList
|
||||||
|
isFiltersVisible={showFilters}
|
||||||
|
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Sentry.ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { Typography } from 'antd';
|
||||||
|
|
||||||
|
export default function HostsEmptyOrIncorrectMetrics({
|
||||||
|
noData,
|
||||||
|
incorrectData,
|
||||||
|
}: {
|
||||||
|
noData: boolean;
|
||||||
|
incorrectData: boolean;
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="hosts-empty-state-container">
|
||||||
|
<div className="hosts-empty-state-container-content">
|
||||||
|
<img className="eyes-emoji" src="/Images/eyesEmoji.svg" alt="eyes emoji" />
|
||||||
|
|
||||||
|
{noData && (
|
||||||
|
<div className="no-hosts-message">
|
||||||
|
<Typography.Title level={5} className="no-hosts-message-title">
|
||||||
|
No data received yet.
|
||||||
|
</Typography.Title>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{incorrectData && (
|
||||||
|
<Typography.Text className="incorrect-metrics-message">
|
||||||
|
To see data, upgrade to the latest version of SigNoz k8s-infra chart.
|
||||||
|
Please contact support if you need help.
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,114 @@
|
|||||||
|
.k8s-filters-side-panel-container {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.k8s-filters-side-panel {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--bg-slate-500);
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
height: 88vh;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
width: 320px;
|
||||||
|
right: 4px;
|
||||||
|
top: 48px;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
.k8s-filters-side-panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px;
|
||||||
|
height: 40px;
|
||||||
|
|
||||||
|
.k8s-filters-side-panel-header-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.k8s-filters-side-panel-body {
|
||||||
|
height: calc(100% - 40px);
|
||||||
|
|
||||||
|
.k8s-filters-side-panel-body-header {
|
||||||
|
border: 1px solid var(--bg-ink-300);
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
|
||||||
|
.ant-input {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.k8s-filters-side-panel-body-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.added-columns,
|
||||||
|
.available-columns {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.filter-columns-title {
|
||||||
|
color: var(--text-slate-50);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 18px; /* 163.636% */
|
||||||
|
letter-spacing: 0.88px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.added-columns-list,
|
||||||
|
.available-columns-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.added-column-item,
|
||||||
|
.available-column-item {
|
||||||
|
color: var(--text-vanilla-100);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
|
||||||
|
padding: 4px 0px 4px 12px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.added-column-item-content,
|
||||||
|
.available-column-item-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal-divider {
|
||||||
|
border-top: 1px solid var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,129 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
|
import './K8sFiltersSidePanel.styles.scss';
|
||||||
|
|
||||||
|
import { Button, Input } from 'antd';
|
||||||
|
import { GripVertical, TableColumnsSplit, X } from 'lucide-react';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { IPodColumn } from '../utils';
|
||||||
|
|
||||||
|
function K8sFiltersSidePanel({
|
||||||
|
defaultAddedColumns,
|
||||||
|
onClose,
|
||||||
|
addedColumns = [],
|
||||||
|
availableColumns = [],
|
||||||
|
onAddColumn = () => {},
|
||||||
|
onRemoveColumn = () => {},
|
||||||
|
}: {
|
||||||
|
defaultAddedColumns: IPodColumn[];
|
||||||
|
onClose: () => void;
|
||||||
|
addedColumns?: IPodColumn[];
|
||||||
|
availableColumns?: IPodColumn[];
|
||||||
|
onAddColumn?: (column: IPodColumn) => void;
|
||||||
|
onRemoveColumn?: (column: IPodColumn) => void;
|
||||||
|
}): JSX.Element {
|
||||||
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
const sidePanelRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
setSearchValue(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sidePanelRef.current) {
|
||||||
|
sidePanelRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [searchValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="k8s-filters-side-panel-container">
|
||||||
|
<div className="k8s-filters-side-panel" ref={sidePanelRef}>
|
||||||
|
<div className="k8s-filters-side-panel-header">
|
||||||
|
<span className="k8s-filters-side-panel-header-title">
|
||||||
|
<TableColumnsSplit size={16} /> Columns
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="periscope-btn ghost"
|
||||||
|
icon={<X size={14} strokeWidth={1.5} onClick={onClose} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="k8s-filters-side-panel-body">
|
||||||
|
<div className="k8s-filters-side-panel-body-header">
|
||||||
|
<Input
|
||||||
|
autoFocus
|
||||||
|
className="periscope-input borderless"
|
||||||
|
placeholder="Search for a column ..."
|
||||||
|
value={searchValue}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="k8s-filters-side-panel-body-content">
|
||||||
|
<div className="added-columns">
|
||||||
|
<div className="filter-columns-title">Added Columns</div>
|
||||||
|
|
||||||
|
<div className="added-columns-list">
|
||||||
|
{[...defaultAddedColumns, ...addedColumns]
|
||||||
|
.filter((column) =>
|
||||||
|
column.label.toLowerCase().includes(searchValue.toLowerCase()),
|
||||||
|
)
|
||||||
|
.map((column) => (
|
||||||
|
<div className="added-column-item" key={column.value}>
|
||||||
|
<div className="added-column-item-content">
|
||||||
|
<GripVertical size={16} /> {column.label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{column.canRemove && (
|
||||||
|
<X
|
||||||
|
size={14}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
onClick={(): void => onRemoveColumn(column)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="horizontal-divider" />
|
||||||
|
|
||||||
|
<div className="available-columns">
|
||||||
|
<div className="filter-columns-title">Other Columns</div>
|
||||||
|
|
||||||
|
<div className="available-columns-list">
|
||||||
|
{availableColumns
|
||||||
|
.filter((column) =>
|
||||||
|
column.label.toLowerCase().includes(searchValue.toLowerCase()),
|
||||||
|
)
|
||||||
|
.map((column) => (
|
||||||
|
<div
|
||||||
|
className="available-column-item"
|
||||||
|
key={column.value}
|
||||||
|
onClick={(): void => onAddColumn(column)}
|
||||||
|
>
|
||||||
|
<div className="available-column-item-content">
|
||||||
|
<GripVertical size={16} /> {column.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
K8sFiltersSidePanel.defaultProps = {
|
||||||
|
addedColumns: [],
|
||||||
|
availableColumns: [],
|
||||||
|
onAddColumn: () => {},
|
||||||
|
onRemoveColumn: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default K8sFiltersSidePanel;
|
||||||
165
frontend/src/container/InfraMonitoringK8s/K8sHeader.tsx
Normal file
165
frontend/src/container/InfraMonitoringK8s/K8sHeader.tsx
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
import './InfraMonitoringK8s.styles.scss';
|
||||||
|
|
||||||
|
import { Button, Select } from 'antd';
|
||||||
|
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||||
|
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { Filter, SlidersHorizontal } from 'lucide-react';
|
||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
import { K8sCategory } from './constants';
|
||||||
|
import K8sFiltersSidePanel from './K8sFiltersSidePanel/K8sFiltersSidePanel';
|
||||||
|
import { IPodColumn } from './utils';
|
||||||
|
|
||||||
|
interface K8sHeaderProps {
|
||||||
|
selectedGroupBy: BaseAutocompleteData[];
|
||||||
|
groupByOptions: { value: string; label: string }[];
|
||||||
|
isLoadingGroupByFilters: boolean;
|
||||||
|
handleFiltersChange: (value: IBuilderQuery['filters']) => void;
|
||||||
|
handleGroupByChange: (value: IBuilderQuery['groupBy']) => void;
|
||||||
|
defaultAddedColumns: IPodColumn[];
|
||||||
|
addedColumns?: IPodColumn[];
|
||||||
|
availableColumns?: IPodColumn[];
|
||||||
|
onAddColumn?: (column: IPodColumn) => void;
|
||||||
|
onRemoveColumn?: (column: IPodColumn) => void;
|
||||||
|
handleFilterVisibilityChange: () => void;
|
||||||
|
isFiltersVisible: boolean;
|
||||||
|
entity: K8sCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
function K8sHeader({
|
||||||
|
selectedGroupBy,
|
||||||
|
defaultAddedColumns,
|
||||||
|
groupByOptions,
|
||||||
|
isLoadingGroupByFilters,
|
||||||
|
addedColumns,
|
||||||
|
availableColumns,
|
||||||
|
handleFiltersChange,
|
||||||
|
handleGroupByChange,
|
||||||
|
onAddColumn,
|
||||||
|
onRemoveColumn,
|
||||||
|
handleFilterVisibilityChange,
|
||||||
|
isFiltersVisible,
|
||||||
|
entity,
|
||||||
|
}: K8sHeaderProps): JSX.Element {
|
||||||
|
const [isFiltersSidePanelOpen, setIsFiltersSidePanelOpen] = useState(false);
|
||||||
|
|
||||||
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
|
const updatedCurrentQuery = useMemo(
|
||||||
|
() => ({
|
||||||
|
...currentQuery,
|
||||||
|
builder: {
|
||||||
|
...currentQuery.builder,
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
...currentQuery.builder.queryData[0],
|
||||||
|
aggregateOperator: 'noop',
|
||||||
|
aggregateAttribute: {
|
||||||
|
...currentQuery.builder.queryData[0].aggregateAttribute,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[currentQuery],
|
||||||
|
);
|
||||||
|
|
||||||
|
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||||
|
|
||||||
|
const handleChangeTagFilters = useCallback(
|
||||||
|
(value: IBuilderQuery['filters']) => {
|
||||||
|
handleFiltersChange(value);
|
||||||
|
},
|
||||||
|
[handleFiltersChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="k8s-list-controls">
|
||||||
|
<div className="k8s-list-controls-left">
|
||||||
|
{!isFiltersVisible && (
|
||||||
|
<div className="quick-filters-toggle-container">
|
||||||
|
<Button
|
||||||
|
className="periscope-btn ghost"
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
onClick={handleFilterVisibilityChange}
|
||||||
|
>
|
||||||
|
<Filter size={14} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="k8s-qb-search-container">
|
||||||
|
<QueryBuilderSearch
|
||||||
|
query={query}
|
||||||
|
onChange={handleChangeTagFilters}
|
||||||
|
isInfraMonitoring
|
||||||
|
disableNavigationShortcuts
|
||||||
|
entity={entity}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="k8s-attribute-search-container">
|
||||||
|
<div className="group-by-label"> Group by </div>
|
||||||
|
<Select
|
||||||
|
className="group-by-select"
|
||||||
|
loading={isLoadingGroupByFilters}
|
||||||
|
mode="multiple"
|
||||||
|
value={selectedGroupBy}
|
||||||
|
allowClear
|
||||||
|
maxTagCount="responsive"
|
||||||
|
placeholder="Search for attribute"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
options={groupByOptions}
|
||||||
|
onChange={handleGroupByChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="k8s-list-controls-right">
|
||||||
|
<DateTimeSelectionV2
|
||||||
|
showAutoRefresh={false}
|
||||||
|
showRefreshText={false}
|
||||||
|
hideShareModal
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
className="periscope-btn ghost"
|
||||||
|
disabled={selectedGroupBy?.length > 0}
|
||||||
|
onClick={(): void => setIsFiltersSidePanelOpen(true)}
|
||||||
|
>
|
||||||
|
<SlidersHorizontal size={14} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isFiltersSidePanelOpen && (
|
||||||
|
<K8sFiltersSidePanel
|
||||||
|
defaultAddedColumns={defaultAddedColumns}
|
||||||
|
addedColumns={addedColumns}
|
||||||
|
availableColumns={availableColumns}
|
||||||
|
onClose={(): void => {
|
||||||
|
if (isFiltersSidePanelOpen) {
|
||||||
|
setIsFiltersSidePanelOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onAddColumn={onAddColumn}
|
||||||
|
onRemoveColumn={onRemoveColumn}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
K8sHeader.defaultProps = {
|
||||||
|
addedColumns: [],
|
||||||
|
availableColumns: [],
|
||||||
|
onAddColumn: () => {},
|
||||||
|
onRemoveColumn: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default K8sHeader;
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import './InfraMonitoringK8s.styles.scss';
|
||||||
|
|
||||||
|
import { Skeleton } from 'antd';
|
||||||
|
|
||||||
|
function LoadingContainer(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="k8s-list-loading-state">
|
||||||
|
<Skeleton.Input
|
||||||
|
className="k8s-list-loading-state-item"
|
||||||
|
size="large"
|
||||||
|
block
|
||||||
|
active
|
||||||
|
/>
|
||||||
|
<Skeleton.Input
|
||||||
|
className="k8s-list-loading-state-item"
|
||||||
|
size="large"
|
||||||
|
block
|
||||||
|
active
|
||||||
|
/>
|
||||||
|
<Skeleton.Input
|
||||||
|
className="k8s-list-loading-state-item"
|
||||||
|
size="large"
|
||||||
|
block
|
||||||
|
active
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoadingContainer;
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
.infra-monitoring-container {
|
||||||
|
.nodes-list-table {
|
||||||
|
.expanded-table-container {
|
||||||
|
padding-left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell {
|
||||||
|
min-width: 223px !important;
|
||||||
|
max-width: 223px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-row-expand-icon-cell {
|
||||||
|
min-width: 30px !important;
|
||||||
|
max-width: 30px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
502
frontend/src/container/InfraMonitoringK8s/Nodes/K8sNodesList.tsx
Normal file
502
frontend/src/container/InfraMonitoringK8s/Nodes/K8sNodesList.tsx
Normal file
@ -0,0 +1,502 @@
|
|||||||
|
/* eslint-disable no-restricted-syntax */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
import '../InfraMonitoringK8s.styles.scss';
|
||||||
|
import './K8sNodesList.styles.scss';
|
||||||
|
|
||||||
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Spin,
|
||||||
|
Table,
|
||||||
|
TablePaginationConfig,
|
||||||
|
TableProps,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
|
import { ColumnType, SorterResult } from 'antd/es/table/interface';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import { K8sNodesListPayload } from 'api/infraMonitoring/getK8sNodesList';
|
||||||
|
import { useGetK8sNodesList } from 'hooks/infraMonitoring/useGetK8sNodesList';
|
||||||
|
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||||
|
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
|
import {
|
||||||
|
K8sCategory,
|
||||||
|
K8sEntityToAggregateAttributeMapping,
|
||||||
|
} from '../constants';
|
||||||
|
import K8sHeader from '../K8sHeader';
|
||||||
|
import LoadingContainer from '../LoadingContainer';
|
||||||
|
import NodeDetails from './NodeDetails';
|
||||||
|
import {
|
||||||
|
defaultAddedColumns,
|
||||||
|
formatDataForTable,
|
||||||
|
getK8sNodesListColumns,
|
||||||
|
getK8sNodesListQuery,
|
||||||
|
K8sNodesRowData,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
function K8sNodesList({
|
||||||
|
isFiltersVisible,
|
||||||
|
handleFilterVisibilityChange,
|
||||||
|
}: {
|
||||||
|
isFiltersVisible: boolean;
|
||||||
|
handleFilterVisibilityChange: () => void;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
|
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const [orderBy, setOrderBy] = useState<{
|
||||||
|
columnName: string;
|
||||||
|
order: 'asc' | 'desc';
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const [selectedNodeUID, setselectedNodeUID] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const pageSize = 10;
|
||||||
|
|
||||||
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>([]);
|
||||||
|
|
||||||
|
const [selectedRowData, setSelectedRowData] = useState<K8sNodesRowData | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [groupByOptions, setGroupByOptions] = useState<
|
||||||
|
{ value: string; label: string }[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const createFiltersForSelectedRowData = (
|
||||||
|
selectedRowData: K8sNodesRowData,
|
||||||
|
groupBy: IBuilderQuery['groupBy'],
|
||||||
|
): IBuilderQuery['filters'] => {
|
||||||
|
const baseFilters: IBuilderQuery['filters'] = {
|
||||||
|
items: [],
|
||||||
|
op: 'and',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!selectedRowData) return baseFilters;
|
||||||
|
|
||||||
|
const { groupedByMeta } = selectedRowData;
|
||||||
|
|
||||||
|
for (const key of groupBy) {
|
||||||
|
baseFilters.items.push({
|
||||||
|
key: {
|
||||||
|
key: key.key,
|
||||||
|
type: null,
|
||||||
|
},
|
||||||
|
op: '=',
|
||||||
|
value: groupedByMeta[key.key],
|
||||||
|
id: key.key,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseFilters;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchGroupedByRowDataQuery = useMemo(() => {
|
||||||
|
if (!selectedRowData) return null;
|
||||||
|
|
||||||
|
const baseQuery = getK8sNodesListQuery();
|
||||||
|
|
||||||
|
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseQuery,
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
filters,
|
||||||
|
start: Math.floor(minTime / 1000000),
|
||||||
|
end: Math.floor(maxTime / 1000000),
|
||||||
|
orderBy,
|
||||||
|
};
|
||||||
|
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: groupedByRowData,
|
||||||
|
isFetching: isFetchingGroupedByRowData,
|
||||||
|
isLoading: isLoadingGroupedByRowData,
|
||||||
|
isError: isErrorGroupedByRowData,
|
||||||
|
refetch: fetchGroupedByRowData,
|
||||||
|
} = useGetK8sNodesList(fetchGroupedByRowDataQuery as K8sNodesListPayload, {
|
||||||
|
queryKey: ['nodeList', fetchGroupedByRowDataQuery],
|
||||||
|
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: groupByFiltersData,
|
||||||
|
isLoading: isLoadingGroupByFilters,
|
||||||
|
} = useGetAggregateKeys(
|
||||||
|
{
|
||||||
|
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||||
|
aggregateAttribute: K8sEntityToAggregateAttributeMapping[K8sCategory.NODES],
|
||||||
|
aggregateOperator: 'noop',
|
||||||
|
searchText: '',
|
||||||
|
tagType: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
K8sCategory.NODES,
|
||||||
|
);
|
||||||
|
|
||||||
|
const queryFilters = useMemo(
|
||||||
|
() =>
|
||||||
|
currentQuery?.builder?.queryData[0]?.filters || {
|
||||||
|
items: [],
|
||||||
|
op: 'and',
|
||||||
|
},
|
||||||
|
[currentQuery?.builder?.queryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const query = useMemo(() => {
|
||||||
|
const baseQuery = getK8sNodesListQuery();
|
||||||
|
const queryPayload = {
|
||||||
|
...baseQuery,
|
||||||
|
limit: pageSize,
|
||||||
|
offset: (currentPage - 1) * pageSize,
|
||||||
|
filters: queryFilters,
|
||||||
|
start: Math.floor(minTime / 1000000),
|
||||||
|
end: Math.floor(maxTime / 1000000),
|
||||||
|
orderBy,
|
||||||
|
};
|
||||||
|
if (groupBy.length > 0) {
|
||||||
|
queryPayload.groupBy = groupBy;
|
||||||
|
}
|
||||||
|
return queryPayload;
|
||||||
|
}, [currentPage, minTime, maxTime, orderBy, groupBy, queryFilters]);
|
||||||
|
|
||||||
|
const formattedGroupedByNodesData = useMemo(
|
||||||
|
() =>
|
||||||
|
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
|
||||||
|
[groupedByRowData, groupBy],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data, isFetching, isLoading, isError } = useGetK8sNodesList(
|
||||||
|
query as K8sNodesListPayload,
|
||||||
|
{
|
||||||
|
queryKey: ['nodeList', query],
|
||||||
|
enabled: !!query,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const nodesData = useMemo(() => data?.payload?.data?.records || [], [data]);
|
||||||
|
const totalCount = data?.payload?.data?.total || 0;
|
||||||
|
|
||||||
|
const formattedNodesData = useMemo(
|
||||||
|
() => formatDataForTable(nodesData, groupBy),
|
||||||
|
[nodesData, groupBy],
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = useMemo(() => getK8sNodesListColumns(groupBy), [groupBy]);
|
||||||
|
|
||||||
|
const handleGroupByRowClick = (record: K8sNodesRowData): void => {
|
||||||
|
setSelectedRowData(record);
|
||||||
|
|
||||||
|
if (expandedRowKeys.includes(record.key)) {
|
||||||
|
setExpandedRowKeys(expandedRowKeys.filter((key) => key !== record.key));
|
||||||
|
} else {
|
||||||
|
setExpandedRowKeys([record.key]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedRowData) {
|
||||||
|
fetchGroupedByRowData();
|
||||||
|
}
|
||||||
|
}, [selectedRowData, fetchGroupedByRowData]);
|
||||||
|
|
||||||
|
const handleTableChange: TableProps<K8sNodesRowData>['onChange'] = useCallback(
|
||||||
|
(
|
||||||
|
pagination: TablePaginationConfig,
|
||||||
|
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||||
|
sorter: SorterResult<K8sNodesRowData> | SorterResult<K8sNodesRowData>[],
|
||||||
|
): 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 { handleChangeQueryData } = useQueryOperations({
|
||||||
|
index: 0,
|
||||||
|
query: currentQuery.builder.queryData[0],
|
||||||
|
entityVersion: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFiltersChange = useCallback(
|
||||||
|
(value: IBuilderQuery['filters']): void => {
|
||||||
|
handleChangeQueryData('filters', value);
|
||||||
|
setCurrentPage(1);
|
||||||
|
|
||||||
|
logEvent('Infra Monitoring: K8s list filters applied', {
|
||||||
|
filters: value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[handleChangeQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
logEvent('Infra Monitoring: K8s list page visited', {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const selectedNodeData = useMemo(() => {
|
||||||
|
if (!selectedNodeUID) return null;
|
||||||
|
return nodesData.find((node) => node.nodeUID === selectedNodeUID) || null;
|
||||||
|
}, [selectedNodeUID, nodesData]);
|
||||||
|
|
||||||
|
const handleRowClick = (record: K8sNodesRowData): void => {
|
||||||
|
if (groupBy.length === 0) {
|
||||||
|
setSelectedRowData(null);
|
||||||
|
setselectedNodeUID(record.nodeUID);
|
||||||
|
} else {
|
||||||
|
handleGroupByRowClick(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
logEvent('Infra Monitoring: K8s node list item clicked', {
|
||||||
|
nodeUID: record.nodeUID,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const nestedColumns = useMemo(() => getK8sNodesListColumns([]), []);
|
||||||
|
|
||||||
|
const isGroupedByAttribute = groupBy.length > 0;
|
||||||
|
|
||||||
|
const handleExpandedRowViewAllClick = (): void => {
|
||||||
|
if (!selectedRowData) return;
|
||||||
|
|
||||||
|
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
|
||||||
|
|
||||||
|
handleFiltersChange(filters);
|
||||||
|
|
||||||
|
setCurrentPage(1);
|
||||||
|
setSelectedRowData(null);
|
||||||
|
setGroupBy([]);
|
||||||
|
setOrderBy(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const expandedRowRender = (): JSX.Element => (
|
||||||
|
<div className="expanded-table-container">
|
||||||
|
{isErrorGroupedByRowData && (
|
||||||
|
<Typography>{groupedByRowData?.error || 'Something went wrong'}</Typography>
|
||||||
|
)}
|
||||||
|
{isFetchingGroupedByRowData || isLoadingGroupedByRowData ? (
|
||||||
|
<LoadingContainer />
|
||||||
|
) : (
|
||||||
|
<div className="expanded-table">
|
||||||
|
<Table
|
||||||
|
columns={nestedColumns as ColumnType<K8sNodesRowData>[]}
|
||||||
|
dataSource={formattedGroupedByNodesData}
|
||||||
|
pagination={false}
|
||||||
|
scroll={{ x: true }}
|
||||||
|
tableLayout="fixed"
|
||||||
|
size="small"
|
||||||
|
loading={{
|
||||||
|
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
|
||||||
|
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||||
|
}}
|
||||||
|
showHeader={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{groupedByRowData?.payload?.data?.total &&
|
||||||
|
groupedByRowData?.payload?.data?.total > 10 ? (
|
||||||
|
<div className="expanded-table-footer">
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
size="small"
|
||||||
|
className="periscope-btn secondary"
|
||||||
|
onClick={handleExpandedRowViewAllClick}
|
||||||
|
>
|
||||||
|
View All
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const expandRowIconRenderer = ({
|
||||||
|
expanded,
|
||||||
|
onExpand,
|
||||||
|
record,
|
||||||
|
}: {
|
||||||
|
expanded: boolean;
|
||||||
|
onExpand: (
|
||||||
|
record: K8sNodesRowData,
|
||||||
|
e: React.MouseEvent<HTMLButtonElement>,
|
||||||
|
) => void;
|
||||||
|
record: K8sNodesRowData;
|
||||||
|
}): JSX.Element | null => {
|
||||||
|
if (!isGroupedByAttribute) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return expanded ? (
|
||||||
|
<Button
|
||||||
|
className="periscope-btn ghost"
|
||||||
|
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
|
||||||
|
onExpand(record, e)
|
||||||
|
}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<ChevronDown size={14} />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
className="periscope-btn ghost"
|
||||||
|
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
|
||||||
|
onExpand(record, e)
|
||||||
|
}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<ChevronRight size={14} />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseNodeDetail = (): void => {
|
||||||
|
setselectedNodeUID(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showsNodesTable =
|
||||||
|
!isError &&
|
||||||
|
!isLoading &&
|
||||||
|
!isFetching &&
|
||||||
|
!(formattedNodesData.length === 0 && queryFilters.items.length > 0);
|
||||||
|
|
||||||
|
const showNoFilteredNodesMessage =
|
||||||
|
!isFetching &&
|
||||||
|
!isLoading &&
|
||||||
|
formattedNodesData.length === 0 &&
|
||||||
|
queryFilters.items.length > 0;
|
||||||
|
|
||||||
|
const handleGroupByChange = useCallback(
|
||||||
|
(value: IBuilderQuery['groupBy']) => {
|
||||||
|
const groupBy = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < value.length; index++) {
|
||||||
|
const element = (value[index] as unknown) as string;
|
||||||
|
|
||||||
|
const key = groupByFiltersData?.payload?.attributeKeys?.find(
|
||||||
|
(key) => key.key === element,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (key) {
|
||||||
|
groupBy.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setGroupBy(groupBy);
|
||||||
|
setExpandedRowKeys([]);
|
||||||
|
},
|
||||||
|
[groupByFiltersData],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (groupByFiltersData?.payload) {
|
||||||
|
setGroupByOptions(
|
||||||
|
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
|
||||||
|
value: filter.key,
|
||||||
|
label: filter.key,
|
||||||
|
})) || [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [groupByFiltersData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="k8s-list">
|
||||||
|
<K8sHeader
|
||||||
|
isFiltersVisible={isFiltersVisible}
|
||||||
|
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||||
|
defaultAddedColumns={defaultAddedColumns}
|
||||||
|
handleFiltersChange={handleFiltersChange}
|
||||||
|
groupByOptions={groupByOptions}
|
||||||
|
isLoadingGroupByFilters={isLoadingGroupByFilters}
|
||||||
|
handleGroupByChange={handleGroupByChange}
|
||||||
|
selectedGroupBy={groupBy}
|
||||||
|
entity={K8sCategory.NODES}
|
||||||
|
/>
|
||||||
|
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||||
|
|
||||||
|
{showNoFilteredNodesMessage && (
|
||||||
|
<div className="no-filtered-hosts-message-container">
|
||||||
|
<div className="no-filtered-hosts-message-content">
|
||||||
|
<img
|
||||||
|
src="/Icons/emptyState.svg"
|
||||||
|
alt="thinking-emoji"
|
||||||
|
className="empty-state-svg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography.Text className="no-filtered-hosts-message">
|
||||||
|
This query had no results. Edit your query and try again!
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(isFetching || isLoading) && <LoadingContainer />}
|
||||||
|
|
||||||
|
{showsNodesTable && (
|
||||||
|
<Table
|
||||||
|
className="k8s-list-table nodes-list-table"
|
||||||
|
dataSource={isFetching || isLoading ? [] : formattedNodesData}
|
||||||
|
columns={columns}
|
||||||
|
pagination={{
|
||||||
|
current: currentPage,
|
||||||
|
pageSize,
|
||||||
|
total: totalCount,
|
||||||
|
showSizeChanger: false,
|
||||||
|
hideOnSinglePage: true,
|
||||||
|
}}
|
||||||
|
scroll={{ x: true }}
|
||||||
|
loading={{
|
||||||
|
spinning: isFetching || isLoading,
|
||||||
|
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||||
|
}}
|
||||||
|
tableLayout="fixed"
|
||||||
|
onChange={handleTableChange}
|
||||||
|
onRow={(record): { onClick: () => void; className: string } => ({
|
||||||
|
onClick: (): void => handleRowClick(record),
|
||||||
|
className: 'clickable-row',
|
||||||
|
})}
|
||||||
|
expandable={{
|
||||||
|
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||||
|
expandIcon: expandRowIconRenderer,
|
||||||
|
expandedRowKeys,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<NodeDetails
|
||||||
|
node={selectedNodeData}
|
||||||
|
isModalTimeSelection
|
||||||
|
onClose={handleCloseNodeDetail}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default K8sNodesList;
|
||||||
@ -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 (
|
||||||
|
<div className="no-logs-found">
|
||||||
|
<Text type="secondary">
|
||||||
|
<Ghost size={24} color={Color.BG_AMBER_500} /> No events found for this node
|
||||||
|
in the selected time range.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,289 @@
|
|||||||
|
.node-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-events-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid var(--bg-slate-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-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(.nodename-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(.nodename-column-value) {
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodename-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-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;
|
||||||
|
}
|
||||||
@ -0,0 +1,357 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
|
import './NodeEvents.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 { getNodesEventsQueryPayload } from './constants';
|
||||||
|
import NoEventsContainer from './NoEventsContainer';
|
||||||
|
|
||||||
|
interface EventDataType {
|
||||||
|
key: string;
|
||||||
|
timestamp: string;
|
||||||
|
body: string;
|
||||||
|
id: string;
|
||||||
|
attributes_bool?: Record<string, boolean>;
|
||||||
|
attributes_number?: Record<string, number>;
|
||||||
|
attributes_string?: Record<string, string>;
|
||||||
|
resources_string?: Record<string, string>;
|
||||||
|
scope_name?: string;
|
||||||
|
scope_string?: Record<string, string>;
|
||||||
|
scope_version?: string;
|
||||||
|
severity_number?: number;
|
||||||
|
severity_text?: string;
|
||||||
|
span_id?: string;
|
||||||
|
trace_flags?: number;
|
||||||
|
trace_id?: string;
|
||||||
|
severity?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface INodeEventsProps {
|
||||||
|
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,
|
||||||
|
}: INodeEventsProps): JSX.Element {
|
||||||
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
|
const [formattedNodeEvents, setFormattedNodeEvents] = useState<
|
||||||
|
EventDataType[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
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 = getNodesEventsQueryPayload(
|
||||||
|
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: ['nodeEvents', timeRange.startTime, timeRange.endTime, filters],
|
||||||
|
queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION),
|
||||||
|
enabled: !!queryPayload,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns: TableColumnsType<EventDataType> = [
|
||||||
|
{ 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,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
setFormattedNodeEvents(formattedData);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!responsePayload ||
|
||||||
|
(responsePayload &&
|
||||||
|
isArray(responsePayload) &&
|
||||||
|
responsePayload.length < EventsPageSize)
|
||||||
|
) {
|
||||||
|
setHasReachedEndOfEvents(true);
|
||||||
|
} else {
|
||||||
|
setHasReachedEndOfEvents(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [eventsData]);
|
||||||
|
|
||||||
|
const handleExpandRow = (record: EventDataType): JSX.Element => (
|
||||||
|
<EventContents data={record.resources_string} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePrev = (): void => {
|
||||||
|
if (!formattedNodeEvents.length) return;
|
||||||
|
|
||||||
|
setPage(page - 1);
|
||||||
|
|
||||||
|
const firstEvent = formattedNodeEvents[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 (!formattedNodeEvents.length) return;
|
||||||
|
|
||||||
|
setPage(page + 1);
|
||||||
|
const lastEvent = formattedNodeEvents[formattedNodeEvents.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<HTMLElement, MouseEvent>,
|
||||||
|
) => void;
|
||||||
|
record: EventDataType;
|
||||||
|
}): JSX.Element =>
|
||||||
|
expanded ? (
|
||||||
|
<ChevronDown
|
||||||
|
className="periscope-btn-icon"
|
||||||
|
size={14}
|
||||||
|
onClick={(e): void =>
|
||||||
|
onExpand(
|
||||||
|
record,
|
||||||
|
(e as unknown) as React.MouseEvent<HTMLElement, MouseEvent>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ChevronRight
|
||||||
|
className="periscope-btn-icon"
|
||||||
|
size={14}
|
||||||
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||||
|
onClick={(e): void =>
|
||||||
|
onExpand(
|
||||||
|
record,
|
||||||
|
(e as unknown) as React.MouseEvent<HTMLElement, MouseEvent>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="node-events-container">
|
||||||
|
<div className="node-events-header">
|
||||||
|
<div className="filter-section">
|
||||||
|
{query && (
|
||||||
|
<QueryBuilderSearch
|
||||||
|
query={query}
|
||||||
|
onChange={handleChangeEventFilters}
|
||||||
|
disableNavigationShortcuts
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="datetime-section">
|
||||||
|
<DateTimeSelectionV2
|
||||||
|
showAutoRefresh={false}
|
||||||
|
showRefreshText={false}
|
||||||
|
hideShareModal
|
||||||
|
isModalTimeSelection={isModalTimeSelection}
|
||||||
|
onTimeChange={handleTimeChange}
|
||||||
|
defaultRelativeTime="5m"
|
||||||
|
modalSelectedInterval={selectedInterval}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading && <LoadingContainer />}
|
||||||
|
|
||||||
|
{!isLoading && !isError && formattedNodeEvents.length === 0 && (
|
||||||
|
<NoEventsContainer />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isError && !isLoading && <LogsError />}
|
||||||
|
|
||||||
|
{!isLoading && !isError && formattedNodeEvents.length > 0 && (
|
||||||
|
<div className="node-events-list-container">
|
||||||
|
<div className="node-events-list-card">
|
||||||
|
<Table<EventDataType>
|
||||||
|
loading={isLoading && page > 1}
|
||||||
|
columns={columns}
|
||||||
|
expandable={{
|
||||||
|
expandedRowRender: handleExpandRow,
|
||||||
|
rowExpandable: (record): boolean => record.body !== 'Not Expandable',
|
||||||
|
expandIcon: handleExpandRowIcon,
|
||||||
|
}}
|
||||||
|
dataSource={formattedNodeEvents}
|
||||||
|
pagination={false}
|
||||||
|
rowKey={(record): string => record.id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isError && formattedNodeEvents.length > 0 && (
|
||||||
|
<div className="node-events-footer">
|
||||||
|
<Button
|
||||||
|
className="node-events-footer-button periscope-btn ghost"
|
||||||
|
type="link"
|
||||||
|
onClick={handlePrev}
|
||||||
|
disabled={page === 1 || isFetching || isLoading}
|
||||||
|
>
|
||||||
|
{!isFetching && <ChevronLeft size={14} />}
|
||||||
|
Prev
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="node-events-footer-button periscope-btn ghost"
|
||||||
|
type="link"
|
||||||
|
onClick={handleNext}
|
||||||
|
disabled={hasReachedEndOfEvents || isFetching || isLoading}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
{!isFetching && <ChevronRight size={14} />}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{(isFetching || isLoading) && (
|
||||||
|
<Loader2 className="animate-spin" size={16} color={Color.BG_ROBIN_500} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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 getNodesEventsQueryPayload = (
|
||||||
|
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,
|
||||||
|
});
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import NodeEvents from './NodeEvents';
|
||||||
|
|
||||||
|
export default NodeEvents;
|
||||||
@ -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 NoLogsContainer(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<div className="no-logs-found">
|
||||||
|
<Text type="secondary">
|
||||||
|
<Ghost size={24} color={Color.BG_AMBER_500} /> No logs found for this node
|
||||||
|
in the selected time range.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,133 @@
|
|||||||
|
.node-logs-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-logs-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid var(--bg-slate-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-logs {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-logs-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-logs-list-card {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 12px;
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,216 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
|
import './NodeLogs.styles.scss';
|
||||||
|
|
||||||
|
import { Card } from 'antd';
|
||||||
|
import RawLogView from 'components/Logs/RawLogView';
|
||||||
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
|
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||||
|
import LogsError from 'container/LogsError/LogsError';
|
||||||
|
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||||
|
import { FontSize } from 'container/OptionsMenu/types';
|
||||||
|
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
||||||
|
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import {
|
||||||
|
IBuilderQuery,
|
||||||
|
TagFilterItem,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
import { QUERY_KEYS } from '../constants';
|
||||||
|
import { getNodeLogsQueryPayload } from './constants';
|
||||||
|
import NoLogsContainer from './NoLogsContainer';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
timeRange: {
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
};
|
||||||
|
handleChangeLogFilters: (filters: IBuilderQuery['filters']) => void;
|
||||||
|
filters: IBuilderQuery['filters'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function PodLogs({
|
||||||
|
timeRange,
|
||||||
|
handleChangeLogFilters,
|
||||||
|
filters,
|
||||||
|
}: Props): JSX.Element {
|
||||||
|
const [logs, setLogs] = useState<ILog[]>([]);
|
||||||
|
const [hasReachedEndOfLogs, setHasReachedEndOfLogs] = useState(false);
|
||||||
|
const [restFilters, setRestFilters] = useState<TagFilterItem[]>([]);
|
||||||
|
const [resetLogsList, setResetLogsList] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newRestFilters = filters.items.filter(
|
||||||
|
(item) =>
|
||||||
|
item.key?.key !== 'id' &&
|
||||||
|
![QUERY_KEYS.K8S_NODE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME].includes(
|
||||||
|
item.key?.key ?? '',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const areFiltersSame = isEqual(restFilters, newRestFilters);
|
||||||
|
|
||||||
|
if (!areFiltersSame) {
|
||||||
|
setResetLogsList(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRestFilters(newRestFilters);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [filters]);
|
||||||
|
|
||||||
|
const queryPayload = useMemo(() => {
|
||||||
|
const basePayload = getNodeLogsQueryPayload(
|
||||||
|
timeRange.startTime,
|
||||||
|
timeRange.endTime,
|
||||||
|
filters,
|
||||||
|
);
|
||||||
|
|
||||||
|
basePayload.query.builder.queryData[0].pageSize = 100;
|
||||||
|
basePayload.query.builder.queryData[0].orderBy = [
|
||||||
|
{ columnName: 'timestamp', order: ORDERBY_FILTERS.DESC },
|
||||||
|
];
|
||||||
|
|
||||||
|
return basePayload;
|
||||||
|
}, [timeRange.startTime, timeRange.endTime, filters]);
|
||||||
|
|
||||||
|
const [isPaginating, setIsPaginating] = useState(false);
|
||||||
|
|
||||||
|
const { data, isLoading, isFetching, isError } = useQuery({
|
||||||
|
queryKey: ['nodeLogs', timeRange.startTime, timeRange.endTime, filters],
|
||||||
|
queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION),
|
||||||
|
enabled: !!queryPayload,
|
||||||
|
keepPreviousData: isPaginating,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.payload?.data?.newResult?.data?.result) {
|
||||||
|
const currentData = data.payload.data.newResult.data.result;
|
||||||
|
|
||||||
|
if (resetLogsList) {
|
||||||
|
const currentLogs: ILog[] =
|
||||||
|
currentData[0].list?.map((item) => ({
|
||||||
|
...item.data,
|
||||||
|
timestamp: item.timestamp,
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
setLogs(currentLogs);
|
||||||
|
|
||||||
|
setResetLogsList(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentData.length > 0 && currentData[0].list) {
|
||||||
|
const currentLogs: ILog[] =
|
||||||
|
currentData[0].list.map((item) => ({
|
||||||
|
...item.data,
|
||||||
|
timestamp: item.timestamp,
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
setLogs((prev) => [...prev, ...currentLogs]);
|
||||||
|
} else {
|
||||||
|
setHasReachedEndOfLogs(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [data, restFilters, isPaginating, resetLogsList]);
|
||||||
|
|
||||||
|
const getItemContent = useCallback(
|
||||||
|
(_: number, logToRender: ILog): JSX.Element => (
|
||||||
|
<RawLogView
|
||||||
|
isReadOnly
|
||||||
|
isTextOverflowEllipsisDisabled
|
||||||
|
key={logToRender.id}
|
||||||
|
data={logToRender}
|
||||||
|
linesPerRow={5}
|
||||||
|
fontSize={FontSize.MEDIUM}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadMoreLogs = useCallback(() => {
|
||||||
|
if (!logs.length) return;
|
||||||
|
|
||||||
|
setIsPaginating(true);
|
||||||
|
const lastLog = logs[logs.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: lastLog.id,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const newFilters = {
|
||||||
|
op: 'AND',
|
||||||
|
items: newItems,
|
||||||
|
} as IBuilderQuery['filters'];
|
||||||
|
|
||||||
|
handleChangeLogFilters(newFilters);
|
||||||
|
}, [logs, filters, handleChangeLogFilters]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsPaginating(false);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const renderFooter = useCallback(
|
||||||
|
(): JSX.Element | null => (
|
||||||
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
|
<>
|
||||||
|
{isFetching ? (
|
||||||
|
<div className="logs-loading-skeleton"> Loading more logs ... </div>
|
||||||
|
) : hasReachedEndOfLogs ? (
|
||||||
|
<div className="logs-loading-skeleton"> *** End *** </div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
[isFetching, hasReachedEndOfLogs],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderContent = useMemo(
|
||||||
|
() => (
|
||||||
|
<Card bordered={false} className="node-logs-list-card">
|
||||||
|
<OverlayScrollbar isVirtuoso>
|
||||||
|
<Virtuoso
|
||||||
|
className="node-logs-virtuoso"
|
||||||
|
key="node-logs-virtuoso"
|
||||||
|
data={logs}
|
||||||
|
endReached={loadMoreLogs}
|
||||||
|
totalCount={logs.length}
|
||||||
|
itemContent={getItemContent}
|
||||||
|
overscan={200}
|
||||||
|
components={{
|
||||||
|
Footer: renderFooter,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</OverlayScrollbar>
|
||||||
|
</Card>
|
||||||
|
),
|
||||||
|
[logs, loadMoreLogs, getItemContent, renderFooter],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="node-logs">
|
||||||
|
{isLoading && <LogsLoading />}
|
||||||
|
{!isLoading && !isError && logs.length === 0 && <NoLogsContainer />}
|
||||||
|
{isError && !isLoading && <LogsError />}
|
||||||
|
{!isLoading && !isError && logs.length > 0 && (
|
||||||
|
<div className="node-logs-list-container">{renderContent}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PodLogs;
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
import './NodeLogs.styles.scss';
|
||||||
|
|
||||||
|
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 { useMemo } from 'react';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import NodeLogs from './NodeLogs';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
timeRange: {
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
};
|
||||||
|
isModalTimeSelection: boolean;
|
||||||
|
handleTimeChange: (
|
||||||
|
interval: Time | CustomTimeType,
|
||||||
|
dateTimeRange?: [number, number],
|
||||||
|
) => void;
|
||||||
|
handleChangeLogFilters: (value: IBuilderQuery['filters']) => void;
|
||||||
|
logFilters: IBuilderQuery['filters'];
|
||||||
|
selectedInterval: Time;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NodeLogsDetailedView({
|
||||||
|
timeRange,
|
||||||
|
isModalTimeSelection,
|
||||||
|
handleTimeChange,
|
||||||
|
handleChangeLogFilters,
|
||||||
|
logFilters,
|
||||||
|
selectedInterval,
|
||||||
|
}: Props): JSX.Element {
|
||||||
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
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;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="node-logs-container">
|
||||||
|
<div className="node-logs-header">
|
||||||
|
<div className="filter-section">
|
||||||
|
{query && (
|
||||||
|
<QueryBuilderSearch
|
||||||
|
query={query}
|
||||||
|
onChange={handleChangeLogFilters}
|
||||||
|
disableNavigationShortcuts
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="datetime-section">
|
||||||
|
<DateTimeSelectionV2
|
||||||
|
showAutoRefresh={false}
|
||||||
|
showRefreshText={false}
|
||||||
|
hideShareModal
|
||||||
|
isModalTimeSelection={isModalTimeSelection}
|
||||||
|
onTimeChange={handleTimeChange}
|
||||||
|
defaultRelativeTime="5m"
|
||||||
|
modalSelectedInterval={selectedInterval}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<NodeLogs
|
||||||
|
timeRange={timeRange}
|
||||||
|
handleChangeLogFilters={handleChangeLogFilters}
|
||||||
|
filters={logFilters}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NodeLogsDetailedView;
|
||||||
@ -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 getNodeLogsQueryPayload = (
|
||||||
|
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,
|
||||||
|
});
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import NodeLogs from './NodeLogsDetailedView';
|
||||||
|
|
||||||
|
export default NodeLogs;
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
.empty-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-metrics-container {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid var(--bg-slate-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-metrics-card {
|
||||||
|
margin: 8px 0 1rem 0;
|
||||||
|
height: 300px;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
border: 1px solid var(--bg-slate-500);
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,140 @@
|
|||||||
|
import './NodeMetrics.styles.scss';
|
||||||
|
|
||||||
|
import { Card, Col, Row, Skeleton, Typography } from 'antd';
|
||||||
|
import { K8sNodesData } from 'api/infraMonitoring/getK8sNodesList';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import Uplot from 'components/Uplot';
|
||||||
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
|
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||||
|
import {
|
||||||
|
CustomTimeType,
|
||||||
|
Time,
|
||||||
|
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
|
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||||
|
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||||
|
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
|
import { useMemo, useRef } from 'react';
|
||||||
|
import { useQueries, UseQueryResult } from 'react-query';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
|
||||||
|
import { getNodeQueryPayload, nodeWidgetInfo } from './constants';
|
||||||
|
|
||||||
|
interface NodeMetricsProps {
|
||||||
|
timeRange: {
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
};
|
||||||
|
isModalTimeSelection: boolean;
|
||||||
|
handleTimeChange: (
|
||||||
|
interval: Time | CustomTimeType,
|
||||||
|
dateTimeRange?: [number, number],
|
||||||
|
) => void;
|
||||||
|
selectedInterval: Time;
|
||||||
|
node: K8sNodesData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NodeMetrics({
|
||||||
|
selectedInterval,
|
||||||
|
node,
|
||||||
|
timeRange,
|
||||||
|
handleTimeChange,
|
||||||
|
isModalTimeSelection,
|
||||||
|
}: NodeMetricsProps): JSX.Element {
|
||||||
|
const queryPayloads = useMemo(
|
||||||
|
() => getNodeQueryPayload(node, timeRange.startTime, timeRange.endTime),
|
||||||
|
[node, timeRange.startTime, timeRange.endTime],
|
||||||
|
);
|
||||||
|
|
||||||
|
const queries = useQueries(
|
||||||
|
queryPayloads.map((payload) => ({
|
||||||
|
queryKey: ['node-metrics', payload, ENTITY_VERSION_V4, 'NODE'],
|
||||||
|
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||||
|
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||||
|
enabled: !!payload,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
|
const dimensions = useResizeObserver(graphRef);
|
||||||
|
|
||||||
|
const chartData = useMemo(
|
||||||
|
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
|
||||||
|
[queries],
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() =>
|
||||||
|
queries.map(({ data }, idx) =>
|
||||||
|
getUPlotChartOptions({
|
||||||
|
apiResponse: data?.payload,
|
||||||
|
isDarkMode,
|
||||||
|
dimensions,
|
||||||
|
yAxisUnit: nodeWidgetInfo[idx].yAxisUnit,
|
||||||
|
softMax: null,
|
||||||
|
softMin: null,
|
||||||
|
minTimeScale: timeRange.startTime,
|
||||||
|
maxTimeScale: timeRange.endTime,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
[queries, isDarkMode, dimensions, timeRange.startTime, timeRange.endTime],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderCardContent = (
|
||||||
|
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
|
||||||
|
idx: number,
|
||||||
|
): JSX.Element => {
|
||||||
|
if (query.isLoading) {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.error) {
|
||||||
|
const errorMessage =
|
||||||
|
(query.error as Error)?.message || 'Something went wrong';
|
||||||
|
return <div>{errorMessage}</div>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx('chart-container', {
|
||||||
|
'no-data-container':
|
||||||
|
!query.isLoading && !query?.data?.payload?.data?.result?.length,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Uplot options={options[idx]} data={chartData[idx]} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="metrics-header">
|
||||||
|
<div className="metrics-datetime-section">
|
||||||
|
<DateTimeSelectionV2
|
||||||
|
showAutoRefresh={false}
|
||||||
|
showRefreshText={false}
|
||||||
|
hideShareModal
|
||||||
|
onTimeChange={handleTimeChange}
|
||||||
|
defaultRelativeTime="5m"
|
||||||
|
isModalTimeSelection={isModalTimeSelection}
|
||||||
|
modalSelectedInterval={selectedInterval}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Row gutter={24} className="node-metrics-container">
|
||||||
|
{queries.map((query, idx) => (
|
||||||
|
<Col span={12} key={nodeWidgetInfo[idx].title}>
|
||||||
|
<Typography.Text>{nodeWidgetInfo[idx].title}</Typography.Text>
|
||||||
|
<Card bordered className="node-metrics-card" ref={graphRef}>
|
||||||
|
{renderCardContent(query, idx)}
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NodeMetrics;
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,3 @@
|
|||||||
|
import NodeMetrics from './NodeMetrics';
|
||||||
|
|
||||||
|
export default NodeMetrics;
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { K8sNodesData } from 'api/infraMonitoring/getK8sNodesList';
|
||||||
|
|
||||||
|
export type NodeDetailsProps = {
|
||||||
|
node: K8sNodesData | null;
|
||||||
|
isModalTimeSelection: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
@ -0,0 +1,247 @@
|
|||||||
|
.node-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-detail-drawer__node {
|
||||||
|
.node-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-detail-drawer {
|
||||||
|
.title {
|
||||||
|
color: var(--text-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-detail-drawer__node {
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,555 @@
|
|||||||
|
/* eslint-disable sonarjs/no-identical-functions */
|
||||||
|
import './NodeDetails.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 NodeEvents from './Events';
|
||||||
|
import NodeLogs from './Logs';
|
||||||
|
import NodeMetrics from './Metrics';
|
||||||
|
import { NodeDetailsProps } from './NodeDetails.interfaces';
|
||||||
|
import NodeTraces from './Traces';
|
||||||
|
|
||||||
|
function NodeDetails({
|
||||||
|
node,
|
||||||
|
onClose,
|
||||||
|
isModalTimeSelection,
|
||||||
|
}: NodeDetailsProps): 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<Time>(
|
||||||
|
selectedTime as Time,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.METRICS);
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
const initialFilters = useMemo(
|
||||||
|
() => ({
|
||||||
|
op: 'AND',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
key: {
|
||||||
|
key: QUERY_KEYS.K8S_NODE_NAME,
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'k8s_node_name--string--resource--false',
|
||||||
|
},
|
||||||
|
op: '=',
|
||||||
|
value: node?.meta.k8s_node_name || '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
key: {
|
||||||
|
key: QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'k8s_node_name--string--resource--false',
|
||||||
|
},
|
||||||
|
op: '=',
|
||||||
|
value: node?.meta.k8s_cluster_name || '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[node?.meta.k8s_node_name, node?.meta.k8s_cluster_name],
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialEventsFilters = useMemo(
|
||||||
|
() => ({
|
||||||
|
op: 'AND',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
key: {
|
||||||
|
key: QUERY_KEYS.K8S_OBJECT_KIND,
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'k8s.object.kind--string--resource--false',
|
||||||
|
},
|
||||||
|
op: '=',
|
||||||
|
value: 'Node',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
key: {
|
||||||
|
key: QUERY_KEYS.K8S_OBJECT_NAME,
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'k8s.object.name--string--resource--false',
|
||||||
|
},
|
||||||
|
op: '=',
|
||||||
|
value: node?.meta.k8s_node_name || '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[node?.meta.k8s_node_name],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [logFilters, setLogFilters] = useState<IBuilderQuery['filters']>(
|
||||||
|
initialFilters,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [tracesFilters, setTracesFilters] = useState<IBuilderQuery['filters']>(
|
||||||
|
initialFilters,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
|
||||||
|
initialEventsFilters,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
logEvent('Infra Monitoring: Nodes list details page visited', {
|
||||||
|
node: node?.nodeUID,
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLogFilters(initialFilters);
|
||||||
|
setTracesFilters(initialFilters);
|
||||||
|
setEventsFilters(initialEventsFilters);
|
||||||
|
}, [initialFilters, initialEventsFilters]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedInterval(selectedTime as Time);
|
||||||
|
|
||||||
|
if (selectedTime !== 'custom') {
|
||||||
|
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||||
|
|
||||||
|
setModalTimeRange({
|
||||||
|
startTime: Math.floor(minTime / 1000000000),
|
||||||
|
endTime: Math.floor(maxTime / 1000000000),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [selectedTime, minTime, maxTime]);
|
||||||
|
|
||||||
|
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||||
|
setSelectedView(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimeChange = useCallback(
|
||||||
|
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||||
|
setSelectedInterval(interval as Time);
|
||||||
|
|
||||||
|
if (interval === 'custom' && dateTimeRange) {
|
||||||
|
setModalTimeRange({
|
||||||
|
startTime: Math.floor(dateTimeRange[0] / 1000),
|
||||||
|
endTime: Math.floor(dateTimeRange[1] / 1000),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const { maxTime, minTime } = GetMinMax(interval);
|
||||||
|
|
||||||
|
setModalTimeRange({
|
||||||
|
startTime: Math.floor(minTime / 1000000000),
|
||||||
|
endTime: Math.floor(maxTime / 1000000000),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logEvent('Infra Monitoring: Nodes list details time updated', {
|
||||||
|
node: node?.nodeUID,
|
||||||
|
interval,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeLogFilters = useCallback(
|
||||||
|
(value: IBuilderQuery['filters']) => {
|
||||||
|
setLogFilters((prevFilters) => {
|
||||||
|
const primaryFilters = prevFilters.items.filter((item) =>
|
||||||
|
[QUERY_KEYS.K8S_NODE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME].includes(
|
||||||
|
item.key?.key ?? '',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const paginationFilter = value.items.find((item) => item.key?.key === 'id');
|
||||||
|
const newFilters = value.items.filter(
|
||||||
|
(item) =>
|
||||||
|
item.key?.key !== 'id' && item.key?.key !== QUERY_KEYS.K8S_NODE_NAME,
|
||||||
|
);
|
||||||
|
|
||||||
|
logEvent('Infra Monitoring: Nodes list details logs filters applied', {
|
||||||
|
node: node?.nodeUID,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
op: 'AND',
|
||||||
|
items: [
|
||||||
|
...primaryFilters,
|
||||||
|
...newFilters,
|
||||||
|
...(paginationFilter ? [paginationFilter] : []),
|
||||||
|
].filter((item): item is TagFilterItem => item !== undefined),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeTracesFilters = useCallback(
|
||||||
|
(value: IBuilderQuery['filters']) => {
|
||||||
|
setTracesFilters((prevFilters) => {
|
||||||
|
const primaryFilters = prevFilters.items.filter((item) =>
|
||||||
|
[QUERY_KEYS.K8S_NODE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME].includes(
|
||||||
|
item.key?.key ?? '',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
logEvent('Infra Monitoring: Nodes list details traces filters applied', {
|
||||||
|
node: node?.nodeUID,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
op: 'AND',
|
||||||
|
items: [
|
||||||
|
...primaryFilters,
|
||||||
|
...value.items.filter(
|
||||||
|
(item) => item.key?.key !== QUERY_KEYS.K8S_NODE_NAME,
|
||||||
|
),
|
||||||
|
].filter((item): item is TagFilterItem => item !== undefined),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeEventsFilters = useCallback(
|
||||||
|
(value: IBuilderQuery['filters']) => {
|
||||||
|
setEventsFilters((prevFilters) => {
|
||||||
|
const nodeKindFilter = prevFilters.items.find(
|
||||||
|
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
|
||||||
|
);
|
||||||
|
const nodeNameFilter = prevFilters.items.find(
|
||||||
|
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
|
||||||
|
);
|
||||||
|
|
||||||
|
logEvent('Infra Monitoring: Nodes list details events filters applied', {
|
||||||
|
node: node?.nodeUID,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
op: 'AND',
|
||||||
|
items: [
|
||||||
|
nodeKindFilter,
|
||||||
|
nodeNameFilter,
|
||||||
|
...value.items.filter(
|
||||||
|
(item) =>
|
||||||
|
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
|
||||||
|
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
|
||||||
|
),
|
||||||
|
].filter((item): item is TagFilterItem => item !== undefined),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleExplorePagesRedirect = (): void => {
|
||||||
|
if (selectedInterval !== 'custom') {
|
||||||
|
urlQuery.set(QueryParams.relativeTime, selectedInterval);
|
||||||
|
} else {
|
||||||
|
urlQuery.delete(QueryParams.relativeTime);
|
||||||
|
urlQuery.set(QueryParams.startTime, modalTimeRange.startTime.toString());
|
||||||
|
urlQuery.set(QueryParams.endTime, modalTimeRange.endTime.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
logEvent('Infra Monitoring: Nodes list details explore clicked', {
|
||||||
|
node: node?.nodeUID,
|
||||||
|
view: selectedView,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selectedView === VIEW_TYPES.LOGS) {
|
||||||
|
const filtersWithoutPagination = {
|
||||||
|
...logFilters,
|
||||||
|
items: logFilters.items.filter((item) => item.key?.key !== 'id'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const compositeQuery = {
|
||||||
|
...initialQueryState,
|
||||||
|
queryType: 'builder',
|
||||||
|
builder: {
|
||||||
|
...initialQueryState.builder,
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
...initialQueryBuilderFormValuesMap.logs,
|
||||||
|
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||||
|
filters: filtersWithoutPagination,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||||
|
|
||||||
|
window.open(
|
||||||
|
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
|
||||||
|
'_blank',
|
||||||
|
);
|
||||||
|
} else if (selectedView === VIEW_TYPES.TRACES) {
|
||||||
|
const compositeQuery = {
|
||||||
|
...initialQueryState,
|
||||||
|
queryType: 'builder',
|
||||||
|
builder: {
|
||||||
|
...initialQueryState.builder,
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
...initialQueryBuilderFormValuesMap.traces,
|
||||||
|
aggregateOperator: TracesAggregatorOperator.NOOP,
|
||||||
|
filters: tracesFilters,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||||
|
|
||||||
|
window.open(
|
||||||
|
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
|
||||||
|
'_blank',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (): void => {
|
||||||
|
setSelectedInterval(selectedTime as Time);
|
||||||
|
|
||||||
|
if (selectedTime !== 'custom') {
|
||||||
|
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||||
|
|
||||||
|
setModalTimeRange({
|
||||||
|
startTime: Math.floor(minTime / 1000000000),
|
||||||
|
endTime: Math.floor(maxTime / 1000000000),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setSelectedView(VIEW_TYPES.METRICS);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
width="70%"
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
<Divider type="vertical" />
|
||||||
|
<Typography.Text className="title">
|
||||||
|
{node?.meta.k8s_node_name}
|
||||||
|
</Typography.Text>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
placement="right"
|
||||||
|
onClose={handleClose}
|
||||||
|
open={!!node}
|
||||||
|
style={{
|
||||||
|
overscrollBehavior: 'contain',
|
||||||
|
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||||
|
}}
|
||||||
|
className="node-detail-drawer"
|
||||||
|
destroyOnClose
|
||||||
|
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||||
|
>
|
||||||
|
{node && (
|
||||||
|
<>
|
||||||
|
<div className="node-detail-drawer__node">
|
||||||
|
<div className="node-details-grid">
|
||||||
|
<div className="labels-row">
|
||||||
|
<Typography.Text
|
||||||
|
type="secondary"
|
||||||
|
className="node-details-metadata-label"
|
||||||
|
>
|
||||||
|
Node Name
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text
|
||||||
|
type="secondary"
|
||||||
|
className="node-details-metadata-label"
|
||||||
|
>
|
||||||
|
Cluster Name
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<div className="values-row">
|
||||||
|
<Typography.Text className="node-details-metadata-value">
|
||||||
|
<Tooltip title={node.meta.k8s_node_name}>
|
||||||
|
{node.meta.k8s_node_name}
|
||||||
|
</Tooltip>
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text className="node-details-metadata-value">
|
||||||
|
<Tooltip title="Cluster name">{node.meta.k8s_cluster_name}</Tooltip>
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="views-tabs-container">
|
||||||
|
<Radio.Group
|
||||||
|
className="views-tabs"
|
||||||
|
onChange={handleTabChange}
|
||||||
|
value={selectedView}
|
||||||
|
>
|
||||||
|
<Radio.Button
|
||||||
|
className={
|
||||||
|
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||||
|
selectedView === VIEW_TYPES.METRICS ? 'selected_view tab' : 'tab'
|
||||||
|
}
|
||||||
|
value={VIEW_TYPES.METRICS}
|
||||||
|
>
|
||||||
|
<div className="view-title">
|
||||||
|
<BarChart2 size={14} />
|
||||||
|
Metrics
|
||||||
|
</div>
|
||||||
|
</Radio.Button>
|
||||||
|
<Radio.Button
|
||||||
|
className={
|
||||||
|
selectedView === VIEW_TYPES.LOGS ? 'selected_view tab' : 'tab'
|
||||||
|
}
|
||||||
|
value={VIEW_TYPES.LOGS}
|
||||||
|
>
|
||||||
|
<div className="view-title">
|
||||||
|
<ScrollText size={14} />
|
||||||
|
Logs
|
||||||
|
</div>
|
||||||
|
</Radio.Button>
|
||||||
|
<Radio.Button
|
||||||
|
className={
|
||||||
|
selectedView === VIEW_TYPES.TRACES ? 'selected_view tab' : 'tab'
|
||||||
|
}
|
||||||
|
value={VIEW_TYPES.TRACES}
|
||||||
|
>
|
||||||
|
<div className="view-title">
|
||||||
|
<DraftingCompass size={14} />
|
||||||
|
Traces
|
||||||
|
</div>
|
||||||
|
</Radio.Button>
|
||||||
|
<Radio.Button
|
||||||
|
className={
|
||||||
|
selectedView === VIEW_TYPES.EVENTS ? 'selected_view tab' : 'tab'
|
||||||
|
}
|
||||||
|
value={VIEW_TYPES.EVENTS}
|
||||||
|
>
|
||||||
|
<div className="view-title">
|
||||||
|
<ChevronsLeftRight size={14} />
|
||||||
|
Events
|
||||||
|
</div>
|
||||||
|
</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
|
||||||
|
{(selectedView === VIEW_TYPES.LOGS ||
|
||||||
|
selectedView === VIEW_TYPES.TRACES) && (
|
||||||
|
<Button
|
||||||
|
icon={<Compass size={18} />}
|
||||||
|
className="compass-button"
|
||||||
|
onClick={handleExplorePagesRedirect}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{selectedView === VIEW_TYPES.METRICS && (
|
||||||
|
<NodeMetrics
|
||||||
|
timeRange={modalTimeRange}
|
||||||
|
isModalTimeSelection={isModalTimeSelection}
|
||||||
|
handleTimeChange={handleTimeChange}
|
||||||
|
selectedInterval={selectedInterval}
|
||||||
|
node={node}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedView === VIEW_TYPES.LOGS && (
|
||||||
|
<NodeLogs
|
||||||
|
timeRange={modalTimeRange}
|
||||||
|
isModalTimeSelection={isModalTimeSelection}
|
||||||
|
handleTimeChange={handleTimeChange}
|
||||||
|
handleChangeLogFilters={handleChangeLogFilters}
|
||||||
|
logFilters={logFilters}
|
||||||
|
selectedInterval={selectedInterval}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedView === VIEW_TYPES.TRACES && (
|
||||||
|
<NodeTraces
|
||||||
|
timeRange={modalTimeRange}
|
||||||
|
isModalTimeSelection={isModalTimeSelection}
|
||||||
|
handleTimeChange={handleTimeChange}
|
||||||
|
handleChangeTracesFilters={handleChangeTracesFilters}
|
||||||
|
tracesFilters={tracesFilters}
|
||||||
|
selectedInterval={selectedInterval}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedView === VIEW_TYPES.EVENTS && (
|
||||||
|
<NodeEvents
|
||||||
|
timeRange={modalTimeRange}
|
||||||
|
handleChangeEventFilters={handleChangeEventsFilters}
|
||||||
|
filters={eventsFilters}
|
||||||
|
isModalTimeSelection={isModalTimeSelection}
|
||||||
|
handleTimeChange={handleTimeChange}
|
||||||
|
selectedInterval={selectedInterval}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NodeDetails;
|
||||||
@ -0,0 +1,193 @@
|
|||||||
|
.node-metric-traces {
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
.node-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,199 @@
|
|||||||
|
import './NodeTraces.styles.scss';
|
||||||
|
|
||||||
|
import { getListColumns } from 'components/HostMetricsDetail/HostMetricTraces/utils';
|
||||||
|
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 { getNodeTracesQueryPayload, selectedColumns } from './constants';
|
||||||
|
|
||||||
|
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 NodeTraces({
|
||||||
|
timeRange,
|
||||||
|
isModalTimeSelection,
|
||||||
|
handleTimeChange,
|
||||||
|
handleChangeTracesFilters,
|
||||||
|
tracesFilters,
|
||||||
|
selectedInterval,
|
||||||
|
}: Props): JSX.Element {
|
||||||
|
const [traces, setTraces] = useState<any[]>([]);
|
||||||
|
const [offset] = useState<number>(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,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[currentQuery],
|
||||||
|
);
|
||||||
|
|
||||||
|
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||||
|
|
||||||
|
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
|
||||||
|
QueryParams.pagination,
|
||||||
|
);
|
||||||
|
|
||||||
|
const queryPayload = useMemo(
|
||||||
|
() =>
|
||||||
|
getNodeTracesQueryPayload(
|
||||||
|
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 (
|
||||||
|
<div className="node-metric-traces">
|
||||||
|
<div className="node-metric-traces-header">
|
||||||
|
<div className="filter-section">
|
||||||
|
{query && (
|
||||||
|
<QueryBuilderSearch
|
||||||
|
query={query}
|
||||||
|
onChange={handleChangeTracesFilters}
|
||||||
|
disableNavigationShortcuts
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="datetime-section">
|
||||||
|
<DateTimeSelectionV2
|
||||||
|
showAutoRefresh={false}
|
||||||
|
showRefreshText={false}
|
||||||
|
hideShareModal
|
||||||
|
isModalTimeSelection={isModalTimeSelection}
|
||||||
|
onTimeChange={handleTimeChange}
|
||||||
|
defaultRelativeTime="5m"
|
||||||
|
modalSelectedInterval={selectedInterval}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isError && <ErrorText>{data?.error || 'Something went wrong'}</ErrorText>}
|
||||||
|
|
||||||
|
{isLoading && traces.length === 0 && <TracesLoading />}
|
||||||
|
|
||||||
|
{isDataEmpty && !hasAdditionalFilters && (
|
||||||
|
<NoLogs dataSource={DataSource.TRACES} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isDataEmpty && hasAdditionalFilters && (
|
||||||
|
<EmptyLogsSearch dataSource={DataSource.TRACES} panelType="LIST" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isError && traces.length > 0 && (
|
||||||
|
<div className="node-traces-table">
|
||||||
|
<TraceExplorerControls
|
||||||
|
isLoading={isFetching}
|
||||||
|
totalCount={totalCount}
|
||||||
|
perPageOptions={PER_PAGE_OPTIONS}
|
||||||
|
showSizeChanger={false}
|
||||||
|
/>
|
||||||
|
<ResizeTable
|
||||||
|
tableLayout="fixed"
|
||||||
|
pagination={false}
|
||||||
|
scroll={{ x: true }}
|
||||||
|
loading={isFetching}
|
||||||
|
dataSource={traces}
|
||||||
|
columns={traceListColumns}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NodeTraces;
|
||||||
@ -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 getNodeTracesQueryPayload = (
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import NodeTraces from './NodeTraces';
|
||||||
|
|
||||||
|
export default NodeTraces;
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
export const QUERY_KEYS = {
|
||||||
|
K8S_OBJECT_KIND: 'k8s.object.kind',
|
||||||
|
K8S_OBJECT_NAME: 'k8s.object.name',
|
||||||
|
K8S_NODE_NAME: 'k8s.node.name',
|
||||||
|
K8S_CLUSTER_NAME: 'k8s.cluster.name',
|
||||||
|
};
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import NodeDetails from './NodeDetails';
|
||||||
|
|
||||||
|
export default NodeDetails;
|
||||||
212
frontend/src/container/InfraMonitoringK8s/Nodes/utils.tsx
Normal file
212
frontend/src/container/InfraMonitoringK8s/Nodes/utils.tsx
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Tag, Tooltip } from 'antd';
|
||||||
|
import { ColumnType } from 'antd/es/table';
|
||||||
|
import {
|
||||||
|
K8sNodesData,
|
||||||
|
K8sNodesListPayload,
|
||||||
|
} from 'api/infraMonitoring/getK8sNodesList';
|
||||||
|
import { Group } from 'lucide-react';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
import { formatBytes, ValidateColumnValueWrapper } from '../commonUtils';
|
||||||
|
import { IEntityColumn } from '../utils';
|
||||||
|
|
||||||
|
export const defaultAddedColumns: IEntityColumn[] = [
|
||||||
|
{
|
||||||
|
label: 'Node Name',
|
||||||
|
value: 'nodeName',
|
||||||
|
id: 'nodeName',
|
||||||
|
canRemove: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cluster Name',
|
||||||
|
value: 'clusterStatus',
|
||||||
|
id: 'clusterStatus',
|
||||||
|
canRemove: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'CPU Usage (cores)',
|
||||||
|
value: 'cpu',
|
||||||
|
id: 'cpu',
|
||||||
|
canRemove: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'CPU Alloc (cores)',
|
||||||
|
value: 'cpu_allocatable',
|
||||||
|
id: 'cpu_allocatable',
|
||||||
|
canRemove: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Memory Usage (bytes)',
|
||||||
|
value: 'memory',
|
||||||
|
id: 'memory',
|
||||||
|
canRemove: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Memory Alloc (bytes)',
|
||||||
|
value: 'memory_allocatable',
|
||||||
|
id: 'memory_allocatable',
|
||||||
|
canRemove: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface K8sNodesRowData {
|
||||||
|
key: string;
|
||||||
|
nodeUID: string;
|
||||||
|
nodeName: React.ReactNode;
|
||||||
|
clusterName: string;
|
||||||
|
cpu: React.ReactNode;
|
||||||
|
cpu_allocatable: React.ReactNode;
|
||||||
|
memory: React.ReactNode;
|
||||||
|
memory_allocatable: React.ReactNode;
|
||||||
|
groupedByMeta?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeGroupColumnConfig = {
|
||||||
|
title: (
|
||||||
|
<div className="column-header node-group-header">
|
||||||
|
<Group size={14} /> NODE GROUP
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
dataIndex: 'nodeGroup',
|
||||||
|
key: 'nodeGroup',
|
||||||
|
ellipsis: true,
|
||||||
|
width: 150,
|
||||||
|
align: 'left',
|
||||||
|
sorter: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getK8sNodesListQuery = (): K8sNodesListPayload => ({
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'and',
|
||||||
|
},
|
||||||
|
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const columnsConfig = [
|
||||||
|
{
|
||||||
|
title: <div className="column-header-left">Node Name</div>,
|
||||||
|
dataIndex: 'nodeName',
|
||||||
|
key: 'nodeName',
|
||||||
|
ellipsis: true,
|
||||||
|
width: 80,
|
||||||
|
sorter: false,
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <div className="column-header-left">Cluster Name</div>,
|
||||||
|
dataIndex: 'clusterName',
|
||||||
|
key: 'clusterName',
|
||||||
|
ellipsis: true,
|
||||||
|
width: 80,
|
||||||
|
sorter: false,
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <div className="column-header-left">CPU Usage (cores)</div>,
|
||||||
|
dataIndex: 'cpu',
|
||||||
|
key: 'cpu',
|
||||||
|
width: 80,
|
||||||
|
sorter: true,
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <div className="column-header-left">CPU Alloc (cores)</div>,
|
||||||
|
dataIndex: 'cpu_allocatable',
|
||||||
|
key: 'cpu_allocatable',
|
||||||
|
width: 80,
|
||||||
|
sorter: true,
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <div className="column-header-left">Memory Usage (bytes)</div>,
|
||||||
|
dataIndex: 'memory',
|
||||||
|
key: 'memory',
|
||||||
|
width: 80,
|
||||||
|
sorter: true,
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <div className="column-header-left">Memory Alloc (bytes)</div>,
|
||||||
|
dataIndex: 'memory_allocatable',
|
||||||
|
key: 'memory_allocatable',
|
||||||
|
width: 80,
|
||||||
|
sorter: true,
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getK8sNodesListColumns = (
|
||||||
|
groupBy: IBuilderQuery['groupBy'],
|
||||||
|
): ColumnType<K8sNodesRowData>[] => {
|
||||||
|
if (groupBy.length > 0) {
|
||||||
|
const filteredColumns = [...columnsConfig].filter(
|
||||||
|
(column) => column.key !== 'nodeName',
|
||||||
|
);
|
||||||
|
filteredColumns.unshift(nodeGroupColumnConfig);
|
||||||
|
return filteredColumns as ColumnType<K8sNodesRowData>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
return columnsConfig as ColumnType<K8sNodesRowData>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGroupByEle = (
|
||||||
|
node: K8sNodesData,
|
||||||
|
groupBy: IBuilderQuery['groupBy'],
|
||||||
|
): React.ReactNode => {
|
||||||
|
const groupByValues: string[] = [];
|
||||||
|
|
||||||
|
groupBy.forEach((group) => {
|
||||||
|
groupByValues.push(node.meta[group.key as keyof typeof node.meta]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pod-group">
|
||||||
|
{groupByValues.map((value) => (
|
||||||
|
<Tag key={value} color={Color.BG_SLATE_400} className="pod-group-tag-item">
|
||||||
|
{value === '' ? '<no-value>' : value}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatDataForTable = (
|
||||||
|
data: K8sNodesData[],
|
||||||
|
groupBy: IBuilderQuery['groupBy'],
|
||||||
|
): K8sNodesRowData[] =>
|
||||||
|
data.map((node, index) => ({
|
||||||
|
key: `${node.nodeUID}-${index}`,
|
||||||
|
nodeUID: node.nodeUID || '',
|
||||||
|
nodeName: (
|
||||||
|
<Tooltip title={node.meta.k8s_node_name}>
|
||||||
|
{node.meta.k8s_node_name || ''}
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
clusterName: node.meta.k8s_cluster_name,
|
||||||
|
cpu: (
|
||||||
|
<ValidateColumnValueWrapper value={node.nodeCPUUsage}>
|
||||||
|
{node.nodeCPUUsage}
|
||||||
|
</ValidateColumnValueWrapper>
|
||||||
|
),
|
||||||
|
memory: (
|
||||||
|
<ValidateColumnValueWrapper value={node.nodeMemoryUsage}>
|
||||||
|
{formatBytes(node.nodeMemoryUsage)}
|
||||||
|
</ValidateColumnValueWrapper>
|
||||||
|
),
|
||||||
|
cpu_allocatable: (
|
||||||
|
<ValidateColumnValueWrapper value={node.nodeCPUAllocatable}>
|
||||||
|
{node.nodeCPUAllocatable}
|
||||||
|
</ValidateColumnValueWrapper>
|
||||||
|
),
|
||||||
|
memory_allocatable: (
|
||||||
|
<ValidateColumnValueWrapper value={node.nodeMemoryAllocatable}>
|
||||||
|
{formatBytes(node.nodeMemoryAllocatable)}
|
||||||
|
</ValidateColumnValueWrapper>
|
||||||
|
),
|
||||||
|
nodeGroup: getGroupByEle(node, groupBy),
|
||||||
|
meta: node.meta,
|
||||||
|
...node.meta,
|
||||||
|
groupedByMeta: node.meta,
|
||||||
|
}));
|
||||||
568
frontend/src/container/InfraMonitoringK8s/Pods/K8sPodLists.tsx
generated
Normal file
568
frontend/src/container/InfraMonitoringK8s/Pods/K8sPodLists.tsx
generated
Normal file
@ -0,0 +1,568 @@
|
|||||||
|
/* eslint-disable no-restricted-syntax */
|
||||||
|
import '../InfraMonitoringK8s.styles.scss';
|
||||||
|
|
||||||
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Spin,
|
||||||
|
Table,
|
||||||
|
TablePaginationConfig,
|
||||||
|
TableProps,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
|
import { ColumnType, SorterResult } from 'antd/es/table/interface';
|
||||||
|
import get from 'api/browser/localstorage/get';
|
||||||
|
import set from 'api/browser/localstorage/set';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import { K8sPodsListPayload } from 'api/infraMonitoring/getK8sPodsList';
|
||||||
|
import { useGetK8sPodsList } from 'hooks/infraMonitoring/useGetK8sPodsList';
|
||||||
|
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||||
|
import { ChevronDown, ChevronRight, CornerDownRight } from 'lucide-react';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
|
import {
|
||||||
|
K8sCategory,
|
||||||
|
K8sEntityToAggregateAttributeMapping,
|
||||||
|
} from '../constants';
|
||||||
|
import K8sHeader from '../K8sHeader';
|
||||||
|
import LoadingContainer from '../LoadingContainer';
|
||||||
|
import {
|
||||||
|
defaultAddedColumns,
|
||||||
|
defaultAvailableColumns,
|
||||||
|
formatDataForTable,
|
||||||
|
getK8sPodsListColumns,
|
||||||
|
getK8sPodsListQuery,
|
||||||
|
IPodColumn,
|
||||||
|
K8sPodsRowData,
|
||||||
|
} from '../utils';
|
||||||
|
import PodDetails from './PodDetails/PodDetails';
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
function K8sPodsList({
|
||||||
|
isFiltersVisible,
|
||||||
|
handleFilterVisibilityChange,
|
||||||
|
}: {
|
||||||
|
isFiltersVisible: boolean;
|
||||||
|
handleFilterVisibilityChange: () => void;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
|
const [addedColumns, setAddedColumns] = useState<IPodColumn[]>([]);
|
||||||
|
|
||||||
|
const [availableColumns, setAvailableColumns] = useState<IPodColumn[]>(
|
||||||
|
defaultAvailableColumns,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>([]);
|
||||||
|
|
||||||
|
const [selectedRowData, setSelectedRowData] = useState<K8sPodsRowData | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const [groupByOptions, setGroupByOptions] = useState<
|
||||||
|
{ value: string; label: string }[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
|
const queryFilters = useMemo(
|
||||||
|
() =>
|
||||||
|
currentQuery?.builder?.queryData[0]?.filters || {
|
||||||
|
items: [],
|
||||||
|
op: 'and',
|
||||||
|
},
|
||||||
|
[currentQuery?.builder?.queryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: groupByFiltersData,
|
||||||
|
isLoading: isLoadingGroupByFilters,
|
||||||
|
} = useGetAggregateKeys(
|
||||||
|
{
|
||||||
|
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||||
|
aggregateAttribute: K8sEntityToAggregateAttributeMapping[K8sCategory.PODS],
|
||||||
|
aggregateOperator: 'noop',
|
||||||
|
searchText: '',
|
||||||
|
tagType: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
|
||||||
|
},
|
||||||
|
true, // isInfraMonitoring
|
||||||
|
K8sCategory.PODS, // infraMonitoringEntity
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const addedColumns = JSON.parse(get('k8sPodsAddedColumns') ?? '');
|
||||||
|
|
||||||
|
if (addedColumns && addedColumns.length > 0) {
|
||||||
|
const availableColumns = defaultAvailableColumns.filter(
|
||||||
|
(column) => !addedColumns.includes(column.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
const newAddedColumns = defaultAvailableColumns.filter((column) =>
|
||||||
|
addedColumns.includes(column.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
setAvailableColumns(availableColumns);
|
||||||
|
setAddedColumns(newAddedColumns);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [orderBy, setOrderBy] = useState<{
|
||||||
|
columnName: string;
|
||||||
|
order: 'asc' | 'desc';
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const [selectedPodUID, setSelectedPodUID] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const pageSize = 10;
|
||||||
|
|
||||||
|
const query = useMemo(() => {
|
||||||
|
const baseQuery = getK8sPodsListQuery();
|
||||||
|
|
||||||
|
const queryPayload = {
|
||||||
|
...baseQuery,
|
||||||
|
limit: pageSize,
|
||||||
|
offset: (currentPage - 1) * pageSize,
|
||||||
|
filters: queryFilters,
|
||||||
|
start: Math.floor(minTime / 1000000),
|
||||||
|
end: Math.floor(maxTime / 1000000),
|
||||||
|
orderBy,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (groupBy.length > 0) {
|
||||||
|
queryPayload.groupBy = groupBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryPayload;
|
||||||
|
}, [currentPage, minTime, maxTime, orderBy, groupBy, queryFilters]);
|
||||||
|
|
||||||
|
const { data, isFetching, isLoading, isError } = useGetK8sPodsList(
|
||||||
|
query as K8sPodsListPayload,
|
||||||
|
{
|
||||||
|
queryKey: ['hostList', query],
|
||||||
|
enabled: !!query,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const createFiltersForSelectedRowData = (
|
||||||
|
selectedRowData: K8sPodsRowData,
|
||||||
|
): IBuilderQuery['filters'] => {
|
||||||
|
const baseFilters: IBuilderQuery['filters'] = {
|
||||||
|
items: [],
|
||||||
|
op: 'and',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!selectedRowData) return baseFilters;
|
||||||
|
|
||||||
|
const { groupedByMeta } = selectedRowData;
|
||||||
|
|
||||||
|
for (const key of Object.keys(groupedByMeta)) {
|
||||||
|
baseFilters.items.push({
|
||||||
|
key: {
|
||||||
|
key,
|
||||||
|
type: null,
|
||||||
|
},
|
||||||
|
op: '=',
|
||||||
|
value: groupedByMeta[key],
|
||||||
|
id: key,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseFilters;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchGroupedByRowDataQuery = useMemo(() => {
|
||||||
|
if (!selectedRowData) return null;
|
||||||
|
|
||||||
|
const baseQuery = getK8sPodsListQuery();
|
||||||
|
|
||||||
|
const filters = createFiltersForSelectedRowData(selectedRowData);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseQuery,
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
filters,
|
||||||
|
start: Math.floor(minTime / 1000000),
|
||||||
|
end: Math.floor(maxTime / 1000000),
|
||||||
|
orderBy,
|
||||||
|
};
|
||||||
|
}, [minTime, maxTime, orderBy, selectedRowData]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: groupedByRowData,
|
||||||
|
isFetching: isFetchingGroupedByRowData,
|
||||||
|
isLoading: isLoadingGroupedByRowData,
|
||||||
|
isError: isErrorGroupedByRowData,
|
||||||
|
refetch: fetchGroupedByRowData,
|
||||||
|
} = useGetK8sPodsList(fetchGroupedByRowDataQuery as K8sPodsListPayload, {
|
||||||
|
queryKey: ['hostList', fetchGroupedByRowDataQuery],
|
||||||
|
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const podsData = useMemo(() => data?.payload?.data?.records || [], [data]);
|
||||||
|
const totalCount = data?.payload?.data?.total || 0;
|
||||||
|
|
||||||
|
const formattedPodsData = useMemo(
|
||||||
|
() => formatDataForTable(podsData, groupBy),
|
||||||
|
[podsData, groupBy],
|
||||||
|
);
|
||||||
|
|
||||||
|
const formattedGroupedByPodsData = useMemo(
|
||||||
|
() =>
|
||||||
|
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
|
||||||
|
[groupedByRowData, groupBy],
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = useMemo(() => getK8sPodsListColumns(addedColumns, groupBy), [
|
||||||
|
addedColumns,
|
||||||
|
groupBy,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleTableChange: TableProps<K8sPodsRowData>['onChange'] = useCallback(
|
||||||
|
(
|
||||||
|
pagination: TablePaginationConfig,
|
||||||
|
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||||
|
sorter: SorterResult<K8sPodsRowData> | SorterResult<K8sPodsRowData>[],
|
||||||
|
): 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 { handleChangeQueryData } = useQueryOperations({
|
||||||
|
index: 0,
|
||||||
|
query: currentQuery.builder.queryData[0],
|
||||||
|
entityVersion: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFiltersChange = useCallback(
|
||||||
|
(value: IBuilderQuery['filters']): void => {
|
||||||
|
handleChangeQueryData('filters', value);
|
||||||
|
setCurrentPage(1);
|
||||||
|
|
||||||
|
logEvent('Infra Monitoring: K8s list filters applied', {
|
||||||
|
filters: value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[handleChangeQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleGroupByChange = useCallback(
|
||||||
|
(value: IBuilderQuery['groupBy']) => {
|
||||||
|
const groupBy = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < value.length; index++) {
|
||||||
|
const element = (value[index] as unknown) as string;
|
||||||
|
|
||||||
|
const key = groupByFiltersData?.payload?.attributeKeys?.find(
|
||||||
|
(key) => key.key === element,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (key) {
|
||||||
|
groupBy.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setGroupBy(groupBy);
|
||||||
|
|
||||||
|
setExpandedRowKeys([]);
|
||||||
|
},
|
||||||
|
[groupByFiltersData],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
logEvent('Infra Monitoring: K8s list page visited', {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const selectedPodData = useMemo(() => {
|
||||||
|
if (!selectedPodUID) return null;
|
||||||
|
return podsData.find((pod) => pod.podUID === selectedPodUID) || null;
|
||||||
|
}, [selectedPodUID, podsData]);
|
||||||
|
|
||||||
|
const handleGroupByRowClick = (record: K8sPodsRowData): void => {
|
||||||
|
setSelectedRowData(record);
|
||||||
|
|
||||||
|
if (expandedRowKeys.includes(record.key)) {
|
||||||
|
setExpandedRowKeys(expandedRowKeys.filter((key) => key !== record.key));
|
||||||
|
} else {
|
||||||
|
setExpandedRowKeys([record.key]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedRowData) {
|
||||||
|
fetchGroupedByRowData();
|
||||||
|
}
|
||||||
|
}, [selectedRowData, fetchGroupedByRowData]);
|
||||||
|
|
||||||
|
const handleRowClick = (record: K8sPodsRowData): void => {
|
||||||
|
if (groupBy.length === 0) {
|
||||||
|
setSelectedPodUID(record.podUID);
|
||||||
|
setSelectedRowData(null);
|
||||||
|
} else {
|
||||||
|
handleGroupByRowClick(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
logEvent('Infra Monitoring: K8s list item clicked', {
|
||||||
|
podUID: record.podUID,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClosePodDetail = (): void => {
|
||||||
|
setSelectedPodUID(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showPodsTable =
|
||||||
|
!isError &&
|
||||||
|
!isLoading &&
|
||||||
|
!isFetching &&
|
||||||
|
!(formattedPodsData.length === 0 && queryFilters.items.length > 0);
|
||||||
|
|
||||||
|
const showNoFilteredPodsMessage =
|
||||||
|
!isFetching &&
|
||||||
|
!isLoading &&
|
||||||
|
formattedPodsData.length === 0 &&
|
||||||
|
queryFilters.items.length > 0;
|
||||||
|
|
||||||
|
const handleAddColumn = useCallback(
|
||||||
|
(column: IPodColumn): void => {
|
||||||
|
setAddedColumns((prev) => [...prev, column]);
|
||||||
|
|
||||||
|
setAvailableColumns((prev) => prev.filter((c) => c.value !== column.value));
|
||||||
|
},
|
||||||
|
[setAddedColumns, setAvailableColumns],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update local storage when added columns updated
|
||||||
|
useEffect(() => {
|
||||||
|
const addedColumnIDs = addedColumns.map((column) => column.id);
|
||||||
|
|
||||||
|
set('k8sPodsAddedColumns', JSON.stringify(addedColumnIDs));
|
||||||
|
}, [addedColumns]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (groupByFiltersData?.payload) {
|
||||||
|
setGroupByOptions(
|
||||||
|
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
|
||||||
|
value: filter.key,
|
||||||
|
label: filter.key,
|
||||||
|
})) || [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [groupByFiltersData]);
|
||||||
|
|
||||||
|
const handleRemoveColumn = useCallback(
|
||||||
|
(column: IPodColumn): void => {
|
||||||
|
setAddedColumns((prev) => prev.filter((c) => c.value !== column.value));
|
||||||
|
|
||||||
|
setAvailableColumns((prev) => [...prev, column]);
|
||||||
|
},
|
||||||
|
[setAddedColumns, setAvailableColumns],
|
||||||
|
);
|
||||||
|
|
||||||
|
const nestedColumns = useMemo(() => getK8sPodsListColumns(addedColumns, []), [
|
||||||
|
addedColumns,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const isGroupedByAttribute = groupBy.length > 0;
|
||||||
|
|
||||||
|
const handleExpandedRowViewAllClick = (): void => {
|
||||||
|
if (!selectedRowData) return;
|
||||||
|
|
||||||
|
const filters = createFiltersForSelectedRowData(selectedRowData);
|
||||||
|
|
||||||
|
handleFiltersChange(filters);
|
||||||
|
|
||||||
|
setCurrentPage(1);
|
||||||
|
setSelectedRowData(null);
|
||||||
|
setGroupBy([]);
|
||||||
|
setOrderBy(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const expandedRowRender = (): JSX.Element => (
|
||||||
|
<div className="expanded-table-container">
|
||||||
|
{isErrorGroupedByRowData && (
|
||||||
|
<Typography>{groupedByRowData?.error || 'Something went wrong'}</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isFetchingGroupedByRowData || isLoadingGroupedByRowData ? (
|
||||||
|
<LoadingContainer />
|
||||||
|
) : (
|
||||||
|
<div className="expanded-table">
|
||||||
|
<Table
|
||||||
|
columns={nestedColumns as ColumnType<K8sPodsRowData>[]}
|
||||||
|
dataSource={formattedGroupedByPodsData}
|
||||||
|
pagination={false}
|
||||||
|
scroll={{ x: true }}
|
||||||
|
tableLayout="fixed"
|
||||||
|
showHeader={false}
|
||||||
|
loading={{
|
||||||
|
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
|
||||||
|
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{groupedByRowData?.payload?.data?.total &&
|
||||||
|
groupedByRowData?.payload?.data?.total > 10 && (
|
||||||
|
<div className="expanded-table-footer">
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
size="small"
|
||||||
|
className="periscope-btn secondary"
|
||||||
|
onClick={handleExpandedRowViewAllClick}
|
||||||
|
>
|
||||||
|
<CornerDownRight size={14} />
|
||||||
|
View All
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const expandRowIconRenderer = ({
|
||||||
|
expanded,
|
||||||
|
onExpand,
|
||||||
|
record,
|
||||||
|
}: {
|
||||||
|
expanded: boolean;
|
||||||
|
onExpand: (
|
||||||
|
record: K8sPodsRowData,
|
||||||
|
e: React.MouseEvent<HTMLButtonElement>,
|
||||||
|
) => void;
|
||||||
|
record: K8sPodsRowData;
|
||||||
|
}): JSX.Element | null => {
|
||||||
|
if (!isGroupedByAttribute) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return expanded ? (
|
||||||
|
<Button
|
||||||
|
className="periscope-btn ghost"
|
||||||
|
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
|
||||||
|
onExpand(record, e)
|
||||||
|
}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<ChevronDown size={14} />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
className="periscope-btn ghost"
|
||||||
|
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
|
||||||
|
onExpand(record, e)
|
||||||
|
}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<ChevronRight size={14} />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="k8s-list">
|
||||||
|
<K8sHeader
|
||||||
|
selectedGroupBy={groupBy}
|
||||||
|
groupByOptions={groupByOptions}
|
||||||
|
isLoadingGroupByFilters={isLoadingGroupByFilters}
|
||||||
|
isFiltersVisible={isFiltersVisible}
|
||||||
|
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||||
|
defaultAddedColumns={defaultAddedColumns}
|
||||||
|
addedColumns={addedColumns}
|
||||||
|
availableColumns={availableColumns}
|
||||||
|
handleFiltersChange={handleFiltersChange}
|
||||||
|
handleGroupByChange={handleGroupByChange}
|
||||||
|
onAddColumn={handleAddColumn}
|
||||||
|
onRemoveColumn={handleRemoveColumn}
|
||||||
|
entity={K8sCategory.PODS}
|
||||||
|
/>
|
||||||
|
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||||
|
|
||||||
|
{showNoFilteredPodsMessage && (
|
||||||
|
<div className="no-filtered-hosts-message-container">
|
||||||
|
<div className="no-filtered-hosts-message-content">
|
||||||
|
<img
|
||||||
|
src="/Icons/emptyState.svg"
|
||||||
|
alt="thinking-emoji"
|
||||||
|
className="empty-state-svg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography.Text className="no-filtered-hosts-message">
|
||||||
|
This query had no results. Edit your query and try again!
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(isFetching || isLoading) && <LoadingContainer />}
|
||||||
|
|
||||||
|
{showPodsTable && (
|
||||||
|
<Table
|
||||||
|
className="k8s-list-table"
|
||||||
|
dataSource={isFetching || isLoading ? [] : formattedPodsData}
|
||||||
|
columns={columns}
|
||||||
|
pagination={{
|
||||||
|
current: currentPage,
|
||||||
|
pageSize,
|
||||||
|
total: totalCount,
|
||||||
|
showSizeChanger: false,
|
||||||
|
hideOnSinglePage: true,
|
||||||
|
}}
|
||||||
|
loading={{
|
||||||
|
spinning: isFetching || isLoading,
|
||||||
|
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||||
|
}}
|
||||||
|
scroll={{ x: true }}
|
||||||
|
tableLayout="fixed"
|
||||||
|
onChange={handleTableChange}
|
||||||
|
onRow={(record): { onClick: () => void; className: string } => ({
|
||||||
|
onClick: (): void => handleRowClick(record),
|
||||||
|
className: 'clickable-row',
|
||||||
|
})}
|
||||||
|
expandable={{
|
||||||
|
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||||
|
expandIcon: expandRowIconRenderer,
|
||||||
|
expandedRowKeys,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedPodData && (
|
||||||
|
<PodDetails
|
||||||
|
pod={selectedPodData}
|
||||||
|
isModalTimeSelection
|
||||||
|
onClose={handleClosePodDetail}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default K8sPodsList;
|
||||||
322
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/Events.styles.scss
generated
Normal file
322
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/Events.styles.scss
generated
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
.pod-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pod-events-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid var(--bg-slate-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pod-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(.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: rgb(18, 19, 23);
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:has(.hostname-column-value) {
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hostname-column-value {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cell {
|
||||||
|
.active-tag {
|
||||||
|
color: var(--bg-forest-500);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
.ant-progress-bg {
|
||||||
|
height: 8px !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr:hover > td {
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:first-child {
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:nth-child(2) {
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:nth-child(n + 3) {
|
||||||
|
padding-right: 24px;
|
||||||
|
}
|
||||||
|
.column-header-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.ant-table-tbody > tr > td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-thead
|
||||||
|
> tr
|
||||||
|
> th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-empty-normal {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-pagination {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
width: calc(100% - 64px);
|
||||||
|
background: 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pod-events-list-container {
|
||||||
|
flex: 1;
|
||||||
|
height: calc(100vh - 300px) !important;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.raw-log-content {
|
||||||
|
width: 100%;
|
||||||
|
text-wrap: inherit;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pod-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pod-events-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 16px 8px 16px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
border-radius: 0px 0px 3px 3px;
|
||||||
|
border-top: 1px solid var(--bg-slate-500);
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.periscope-btn {
|
||||||
|
&.ghost {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
357
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/Events.tsx
generated
Normal file
357
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/Events.tsx
generated
Normal file
@ -0,0 +1,357 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
|
import './Events.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 { getPodsEventsQueryPayload } from './constants';
|
||||||
|
import NoEventsContainer from './NoEventsContainer';
|
||||||
|
|
||||||
|
interface EventDataType {
|
||||||
|
key: string;
|
||||||
|
timestamp: string;
|
||||||
|
body: string;
|
||||||
|
id: string;
|
||||||
|
attributes_bool?: Record<string, boolean>;
|
||||||
|
attributes_number?: Record<string, number>;
|
||||||
|
attributes_string?: Record<string, string>;
|
||||||
|
resources_string?: Record<string, string>;
|
||||||
|
scope_name?: string;
|
||||||
|
scope_string?: Record<string, string>;
|
||||||
|
scope_version?: string;
|
||||||
|
severity_number?: number;
|
||||||
|
severity_text?: string;
|
||||||
|
span_id?: string;
|
||||||
|
trace_flags?: number;
|
||||||
|
trace_id?: string;
|
||||||
|
severity?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPodEventsProps {
|
||||||
|
timeRange: {
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
};
|
||||||
|
handleChangeLogFilters: (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,
|
||||||
|
handleChangeLogFilters,
|
||||||
|
filters,
|
||||||
|
isModalTimeSelection,
|
||||||
|
handleTimeChange,
|
||||||
|
selectedInterval,
|
||||||
|
}: IPodEventsProps): JSX.Element {
|
||||||
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
|
const [formattedPodEvents, setFormattedPodEvents] = useState<EventDataType[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
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 = getPodsEventsQueryPayload(
|
||||||
|
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: ['podEvents', timeRange.startTime, timeRange.endTime, filters],
|
||||||
|
queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION),
|
||||||
|
enabled: !!queryPayload,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns: TableColumnsType<EventDataType> = [
|
||||||
|
{ 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,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
setFormattedPodEvents(formattedData);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!responsePayload ||
|
||||||
|
(responsePayload &&
|
||||||
|
isArray(responsePayload) &&
|
||||||
|
responsePayload.length < EventsPageSize)
|
||||||
|
) {
|
||||||
|
setHasReachedEndOfEvents(true);
|
||||||
|
} else {
|
||||||
|
setHasReachedEndOfEvents(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [eventsData]);
|
||||||
|
|
||||||
|
const handleExpandRow = (record: EventDataType): JSX.Element => (
|
||||||
|
<EventContents data={record.resources_string} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePrev = (): void => {
|
||||||
|
if (!formattedPodEvents.length) return;
|
||||||
|
|
||||||
|
setPage(page - 1);
|
||||||
|
|
||||||
|
const firstEvent = formattedPodEvents[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'];
|
||||||
|
|
||||||
|
handleChangeLogFilters(newFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = (): void => {
|
||||||
|
if (!formattedPodEvents.length) return;
|
||||||
|
|
||||||
|
setPage(page + 1);
|
||||||
|
const lastEvent = formattedPodEvents[formattedPodEvents.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'];
|
||||||
|
|
||||||
|
handleChangeLogFilters(newFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExpandRowIcon = ({
|
||||||
|
expanded,
|
||||||
|
onExpand,
|
||||||
|
record,
|
||||||
|
}: {
|
||||||
|
expanded: boolean;
|
||||||
|
onExpand: (
|
||||||
|
record: EventDataType,
|
||||||
|
e: React.MouseEvent<HTMLElement, MouseEvent>,
|
||||||
|
) => void;
|
||||||
|
record: EventDataType;
|
||||||
|
}): JSX.Element =>
|
||||||
|
expanded ? (
|
||||||
|
<ChevronDown
|
||||||
|
className="periscope-btn-icon"
|
||||||
|
size={14}
|
||||||
|
onClick={(e): void =>
|
||||||
|
onExpand(
|
||||||
|
record,
|
||||||
|
(e as unknown) as React.MouseEvent<HTMLElement, MouseEvent>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ChevronRight
|
||||||
|
className="periscope-btn-icon"
|
||||||
|
size={14}
|
||||||
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||||
|
onClick={(e): void =>
|
||||||
|
onExpand(
|
||||||
|
record,
|
||||||
|
(e as unknown) as React.MouseEvent<HTMLElement, MouseEvent>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pod-events-container">
|
||||||
|
<div className="pod-events-header">
|
||||||
|
<div className="filter-section">
|
||||||
|
{query && (
|
||||||
|
<QueryBuilderSearch
|
||||||
|
query={query}
|
||||||
|
onChange={handleChangeLogFilters}
|
||||||
|
disableNavigationShortcuts
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="datetime-section">
|
||||||
|
<DateTimeSelectionV2
|
||||||
|
showAutoRefresh={false}
|
||||||
|
showRefreshText={false}
|
||||||
|
hideShareModal
|
||||||
|
isModalTimeSelection={isModalTimeSelection}
|
||||||
|
onTimeChange={handleTimeChange}
|
||||||
|
defaultRelativeTime="5m"
|
||||||
|
modalSelectedInterval={selectedInterval}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading && <LoadingContainer />}
|
||||||
|
|
||||||
|
{!isLoading && !isError && formattedPodEvents.length === 0 && (
|
||||||
|
<NoEventsContainer />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isError && !isLoading && <LogsError />}
|
||||||
|
|
||||||
|
{!isLoading && !isError && formattedPodEvents.length > 0 && (
|
||||||
|
<div className="pod-events-list-container">
|
||||||
|
<div className="pod-events-list-card">
|
||||||
|
<Table<EventDataType>
|
||||||
|
loading={isLoading && page > 1}
|
||||||
|
columns={columns}
|
||||||
|
expandable={{
|
||||||
|
expandedRowRender: handleExpandRow,
|
||||||
|
rowExpandable: (record): boolean => record.body !== 'Not Expandable',
|
||||||
|
expandIcon: handleExpandRowIcon,
|
||||||
|
}}
|
||||||
|
dataSource={formattedPodEvents}
|
||||||
|
pagination={false}
|
||||||
|
rowKey={(record): string => record.id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isError && formattedPodEvents.length > 0 && (
|
||||||
|
<div className="pod-events-footer">
|
||||||
|
<Button
|
||||||
|
className="pod-events-footer-button periscope-btn ghost"
|
||||||
|
type="link"
|
||||||
|
onClick={handlePrev}
|
||||||
|
disabled={page === 1 || isFetching || isLoading}
|
||||||
|
>
|
||||||
|
{!isFetching && <ChevronLeft size={14} />}
|
||||||
|
Prev
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="pod-events-footer-button periscope-btn ghost"
|
||||||
|
type="link"
|
||||||
|
onClick={handleNext}
|
||||||
|
disabled={hasReachedEndOfEvents || isFetching || isLoading}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
{!isFetching && <ChevronRight size={14} />}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{(isFetching || isLoading) && (
|
||||||
|
<Loader2 className="animate-spin" size={16} color={Color.BG_ROBIN_500} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/NoEventsContainer.tsx
generated
Normal file
16
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/NoEventsContainer.tsx
generated
Normal file
@ -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 (
|
||||||
|
<div className="no-logs-found">
|
||||||
|
<Text type="secondary">
|
||||||
|
<Ghost size={24} color={Color.BG_AMBER_500} /> No events found for this pod
|
||||||
|
in the selected time range.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
65
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/constants.ts
generated
Normal file
65
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Events/constants.ts
generated
Normal file
@ -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 getPodsEventsQueryPayload = (
|
||||||
|
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,
|
||||||
|
});
|
||||||
45
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Metrics/Metrics.styles.scss
generated
Normal file
45
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Metrics/Metrics.styles.scss
generated
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
.empty-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.host-metrics-container {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid var(--bg-slate-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.host-metrics-card {
|
||||||
|
margin: 8px 0 1rem 0;
|
||||||
|
height: 300px;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
border: 1px solid var(--bg-slate-500);
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
140
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Metrics/Metrics.tsx
generated
Normal file
140
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/Metrics/Metrics.tsx
generated
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import './Metrics.styles.scss';
|
||||||
|
|
||||||
|
import { Card, Col, Row, Skeleton, Typography } from 'antd';
|
||||||
|
import { K8sPodsData } from 'api/infraMonitoring/getK8sPodsList';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import Uplot from 'components/Uplot';
|
||||||
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
|
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||||
|
import {
|
||||||
|
CustomTimeType,
|
||||||
|
Time,
|
||||||
|
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
|
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||||
|
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||||
|
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
|
import { useMemo, useRef } from 'react';
|
||||||
|
import { useQueries, UseQueryResult } from 'react-query';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
|
||||||
|
import { getPodQueryPayload, podWidgetInfo } from '../../constants';
|
||||||
|
|
||||||
|
interface MetricsTabProps {
|
||||||
|
timeRange: {
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
};
|
||||||
|
isModalTimeSelection: boolean;
|
||||||
|
handleTimeChange: (
|
||||||
|
interval: Time | CustomTimeType,
|
||||||
|
dateTimeRange?: [number, number],
|
||||||
|
) => void;
|
||||||
|
selectedInterval: Time;
|
||||||
|
pod: K8sPodsData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Metrics({
|
||||||
|
selectedInterval,
|
||||||
|
pod,
|
||||||
|
timeRange,
|
||||||
|
handleTimeChange,
|
||||||
|
isModalTimeSelection,
|
||||||
|
}: MetricsTabProps): JSX.Element {
|
||||||
|
const queryPayloads = useMemo(
|
||||||
|
() => getPodQueryPayload(pod, timeRange.startTime, timeRange.endTime),
|
||||||
|
[pod, timeRange.startTime, timeRange.endTime],
|
||||||
|
);
|
||||||
|
|
||||||
|
const queries = useQueries(
|
||||||
|
queryPayloads.map((payload) => ({
|
||||||
|
queryKey: ['pod-metrics', payload, ENTITY_VERSION_V4, 'POD'],
|
||||||
|
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||||
|
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||||
|
enabled: !!payload,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
|
const dimensions = useResizeObserver(graphRef);
|
||||||
|
|
||||||
|
const chartData = useMemo(
|
||||||
|
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
|
||||||
|
[queries],
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() =>
|
||||||
|
queries.map(({ data }, idx) =>
|
||||||
|
getUPlotChartOptions({
|
||||||
|
apiResponse: data?.payload,
|
||||||
|
isDarkMode,
|
||||||
|
dimensions,
|
||||||
|
yAxisUnit: podWidgetInfo[idx].yAxisUnit,
|
||||||
|
softMax: null,
|
||||||
|
softMin: null,
|
||||||
|
minTimeScale: timeRange.startTime,
|
||||||
|
maxTimeScale: timeRange.endTime,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
[queries, isDarkMode, dimensions, timeRange.startTime, timeRange.endTime],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderCardContent = (
|
||||||
|
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
|
||||||
|
idx: number,
|
||||||
|
): JSX.Element => {
|
||||||
|
if (query.isLoading) {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.error) {
|
||||||
|
const errorMessage =
|
||||||
|
(query.error as Error)?.message || 'Something went wrong';
|
||||||
|
return <div>{errorMessage}</div>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx('chart-container', {
|
||||||
|
'no-data-container':
|
||||||
|
!query.isLoading && !query?.data?.payload?.data?.result?.length,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Uplot options={options[idx]} data={chartData[idx]} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="metrics-header">
|
||||||
|
<div className="metrics-datetime-section">
|
||||||
|
<DateTimeSelectionV2
|
||||||
|
showAutoRefresh={false}
|
||||||
|
showRefreshText={false}
|
||||||
|
hideShareModal
|
||||||
|
onTimeChange={handleTimeChange}
|
||||||
|
defaultRelativeTime="5m"
|
||||||
|
isModalTimeSelection={isModalTimeSelection}
|
||||||
|
modalSelectedInterval={selectedInterval}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Row gutter={24} className="host-metrics-container">
|
||||||
|
{queries.map((query, idx) => (
|
||||||
|
<Col span={12} key={podWidgetInfo[idx].title}>
|
||||||
|
<Typography.Text>{podWidgetInfo[idx].title}</Typography.Text>
|
||||||
|
<Card bordered className="host-metrics-card" ref={graphRef}>
|
||||||
|
{renderCardContent(query, idx)}
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Metrics;
|
||||||
7
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetail.interfaces.ts
generated
Normal file
7
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetail.interfaces.ts
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { K8sPodsData } from 'api/infraMonitoring/getK8sPodsList';
|
||||||
|
|
||||||
|
export type PodDetailProps = {
|
||||||
|
pod: K8sPodsData | null;
|
||||||
|
isModalTimeSelection: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
247
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetails.styles.scss
generated
Normal file
247
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetails.styles.scss
generated
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
.pod-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pod-detail-drawer__pod {
|
||||||
|
.pod-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pod-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pod-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pod-detail-drawer {
|
||||||
|
.title {
|
||||||
|
color: var(--text-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pod-detail-drawer__pod {
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
598
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetails.tsx
generated
Normal file
598
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetails.tsx
generated
Normal file
@ -0,0 +1,598 @@
|
|||||||
|
/* eslint-disable sonarjs/no-identical-functions */
|
||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
import './PodDetails.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 Events from './Events/Events';
|
||||||
|
import Metrics from './Metrics/Metrics';
|
||||||
|
import { PodDetailProps } from './PodDetail.interfaces';
|
||||||
|
import PodLogsDetailedView from './PodLogs/PodLogsDetailedView';
|
||||||
|
import PodTraces from './PodTraces/PodTraces';
|
||||||
|
|
||||||
|
const TimeRangeOffset = 1000000;
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
function PodDetails({
|
||||||
|
pod,
|
||||||
|
onClose,
|
||||||
|
isModalTimeSelection,
|
||||||
|
}: PodDetailProps): JSX.Element {
|
||||||
|
const { maxTime, minTime, selectedTime } = useSelector<
|
||||||
|
AppState,
|
||||||
|
GlobalReducer
|
||||||
|
>((state) => state.globalTime);
|
||||||
|
|
||||||
|
const startMs = useMemo(() => Math.floor(Number(minTime) / TimeRangeOffset), [
|
||||||
|
minTime,
|
||||||
|
]);
|
||||||
|
const endMs = useMemo(() => Math.floor(Number(maxTime) / TimeRangeOffset), [
|
||||||
|
maxTime,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const urlQuery = useUrlQuery();
|
||||||
|
|
||||||
|
const [modalTimeRange, setModalTimeRange] = useState(() => ({
|
||||||
|
startTime: startMs,
|
||||||
|
endTime: endMs,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||||
|
selectedTime as Time,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.METRICS);
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
const initialFilters = useMemo(
|
||||||
|
() => ({
|
||||||
|
op: 'AND',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
key: {
|
||||||
|
key: QUERY_KEYS.K8S_POD_NAME,
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'k8s_pod_name--string--resource--false',
|
||||||
|
},
|
||||||
|
op: '=',
|
||||||
|
value: pod?.meta.k8s_pod_name || '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
key: {
|
||||||
|
key: QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'k8s_pod_name--string--resource--false',
|
||||||
|
},
|
||||||
|
op: '=',
|
||||||
|
value: pod?.meta.k8s_cluster_name || '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
key: {
|
||||||
|
key: QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'k8s_pod_name--string--resource--false',
|
||||||
|
},
|
||||||
|
op: '=',
|
||||||
|
value: pod?.meta.k8s_namespace_name || '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
pod?.meta.k8s_cluster_name,
|
||||||
|
pod?.meta.k8s_namespace_name,
|
||||||
|
pod?.meta.k8s_pod_name,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialEventsFilters = useMemo(
|
||||||
|
() => ({
|
||||||
|
op: 'AND',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
key: {
|
||||||
|
key: QUERY_KEYS.K8S_OBJECT_KIND,
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'k8s.object.kind--string--resource--false',
|
||||||
|
},
|
||||||
|
op: '=',
|
||||||
|
value: 'Pod',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
key: {
|
||||||
|
key: QUERY_KEYS.K8S_OBJECT_NAME,
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'k8s.object.name--string--resource--false',
|
||||||
|
},
|
||||||
|
op: '=',
|
||||||
|
value: pod?.meta.k8s_pod_name || '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[pod?.meta.k8s_pod_name],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [logFilters, setLogFilters] = useState<IBuilderQuery['filters']>(
|
||||||
|
initialFilters,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [tracesFilters, setTracesFilters] = useState<IBuilderQuery['filters']>(
|
||||||
|
initialFilters,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
|
||||||
|
initialEventsFilters,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
logEvent('Infra Monitoring: Pods list details page visited', {
|
||||||
|
pod: pod?.podUID,
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLogFilters(initialFilters);
|
||||||
|
setTracesFilters(initialFilters);
|
||||||
|
setEventsFilters(initialEventsFilters);
|
||||||
|
}, [initialFilters, initialEventsFilters]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedInterval(selectedTime as Time);
|
||||||
|
|
||||||
|
if (selectedTime !== 'custom') {
|
||||||
|
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||||
|
|
||||||
|
setModalTimeRange({
|
||||||
|
startTime: Math.floor(minTime / TimeRangeOffset),
|
||||||
|
endTime: Math.floor(maxTime / TimeRangeOffset),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [selectedTime, minTime, maxTime]);
|
||||||
|
|
||||||
|
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||||
|
setSelectedView(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimeChange = useCallback(
|
||||||
|
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||||
|
setSelectedInterval(interval as Time);
|
||||||
|
|
||||||
|
if (interval === 'custom' && dateTimeRange) {
|
||||||
|
setModalTimeRange({
|
||||||
|
startTime: Math.floor(dateTimeRange[0] / 1000),
|
||||||
|
endTime: Math.floor(dateTimeRange[1] / 1000),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const { maxTime, minTime } = GetMinMax(interval);
|
||||||
|
|
||||||
|
setModalTimeRange({
|
||||||
|
startTime: Math.floor(minTime / TimeRangeOffset),
|
||||||
|
endTime: Math.floor(maxTime / TimeRangeOffset),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logEvent('Infra Monitoring: Pods list details time updated', {
|
||||||
|
pod: pod?.podUID,
|
||||||
|
interval,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeLogFilters = useCallback(
|
||||||
|
(value: IBuilderQuery['filters']) => {
|
||||||
|
setLogFilters((prevFilters) => {
|
||||||
|
const primaryFilters = prevFilters.items.filter((item) =>
|
||||||
|
[
|
||||||
|
QUERY_KEYS.K8S_POD_NAME,
|
||||||
|
QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||||
|
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||||
|
].includes(item.key?.key ?? ''),
|
||||||
|
);
|
||||||
|
const paginationFilter = value.items.find((item) => item.key?.key === 'id');
|
||||||
|
const newFilters = value.items.filter(
|
||||||
|
(item) =>
|
||||||
|
item.key?.key !== 'id' && item.key?.key !== QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||||
|
);
|
||||||
|
|
||||||
|
logEvent('Infra Monitoring: Pods list details logs filters applied', {
|
||||||
|
pod: pod?.podUID,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
op: 'AND',
|
||||||
|
items: [
|
||||||
|
...primaryFilters,
|
||||||
|
...newFilters,
|
||||||
|
...(paginationFilter ? [paginationFilter] : []),
|
||||||
|
].filter((item): item is TagFilterItem => item !== undefined),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeTracesFilters = useCallback(
|
||||||
|
(value: IBuilderQuery['filters']) => {
|
||||||
|
setTracesFilters((prevFilters) => {
|
||||||
|
const primaryFilters = prevFilters.items.filter((item) =>
|
||||||
|
[
|
||||||
|
QUERY_KEYS.K8S_POD_NAME,
|
||||||
|
QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||||
|
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||||
|
].includes(item.key?.key ?? ''),
|
||||||
|
);
|
||||||
|
|
||||||
|
logEvent('Infra Monitoring: Pods list details traces filters applied', {
|
||||||
|
pod: pod?.podUID,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
op: 'AND',
|
||||||
|
items: [
|
||||||
|
...primaryFilters,
|
||||||
|
...value.items.filter(
|
||||||
|
(item) => item.key?.key !== QUERY_KEYS.K8S_POD_NAME,
|
||||||
|
),
|
||||||
|
].filter((item): item is TagFilterItem => item !== undefined),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeEventsFilters = useCallback(
|
||||||
|
(value: IBuilderQuery['filters']) => {
|
||||||
|
setEventsFilters((prevFilters) => {
|
||||||
|
const podKindFilter = prevFilters.items.find(
|
||||||
|
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
|
||||||
|
);
|
||||||
|
const podNameFilter = prevFilters.items.find(
|
||||||
|
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
|
||||||
|
);
|
||||||
|
|
||||||
|
logEvent('Infra Monitoring: Pods list details events filters applied', {
|
||||||
|
pod: pod?.podUID,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
op: 'AND',
|
||||||
|
items: [
|
||||||
|
podKindFilter,
|
||||||
|
podNameFilter,
|
||||||
|
...value.items.filter(
|
||||||
|
(item) =>
|
||||||
|
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
|
||||||
|
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
|
||||||
|
),
|
||||||
|
].filter((item): item is TagFilterItem => item !== undefined),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleExplorePagesRedirect = (): void => {
|
||||||
|
if (selectedInterval !== 'custom') {
|
||||||
|
urlQuery.set(QueryParams.relativeTime, selectedInterval);
|
||||||
|
} else {
|
||||||
|
urlQuery.delete(QueryParams.relativeTime);
|
||||||
|
urlQuery.set(QueryParams.startTime, modalTimeRange.startTime.toString());
|
||||||
|
urlQuery.set(QueryParams.endTime, modalTimeRange.endTime.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
logEvent('Infra Monitoring: Pods list details explore clicked', {
|
||||||
|
pod: pod?.podUID,
|
||||||
|
view: selectedView,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selectedView === VIEW_TYPES.LOGS) {
|
||||||
|
const filtersWithoutPagination = {
|
||||||
|
...logFilters,
|
||||||
|
items: logFilters.items.filter((item) => item.key?.key !== 'id'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const compositeQuery = {
|
||||||
|
...initialQueryState,
|
||||||
|
queryType: 'builder',
|
||||||
|
builder: {
|
||||||
|
...initialQueryState.builder,
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
...initialQueryBuilderFormValuesMap.logs,
|
||||||
|
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||||
|
filters: filtersWithoutPagination,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||||
|
|
||||||
|
window.open(
|
||||||
|
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
|
||||||
|
'_blank',
|
||||||
|
);
|
||||||
|
} else if (selectedView === VIEW_TYPES.TRACES) {
|
||||||
|
const compositeQuery = {
|
||||||
|
...initialQueryState,
|
||||||
|
queryType: 'builder',
|
||||||
|
builder: {
|
||||||
|
...initialQueryState.builder,
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
...initialQueryBuilderFormValuesMap.traces,
|
||||||
|
aggregateOperator: TracesAggregatorOperator.NOOP,
|
||||||
|
filters: tracesFilters,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||||
|
|
||||||
|
window.open(
|
||||||
|
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
|
||||||
|
'_blank',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (): void => {
|
||||||
|
setSelectedInterval(selectedTime as Time);
|
||||||
|
|
||||||
|
if (selectedTime !== 'custom') {
|
||||||
|
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||||
|
|
||||||
|
setModalTimeRange({
|
||||||
|
startTime: Math.floor(minTime / TimeRangeOffset),
|
||||||
|
endTime: Math.floor(maxTime / TimeRangeOffset),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setSelectedView(VIEW_TYPES.METRICS);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
width="70%"
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
<Divider type="vertical" />
|
||||||
|
<Typography.Text className="title">
|
||||||
|
{pod?.meta.k8s_pod_name}
|
||||||
|
</Typography.Text>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
placement="right"
|
||||||
|
onClose={handleClose}
|
||||||
|
open={!!pod}
|
||||||
|
style={{
|
||||||
|
overscrollBehavior: 'contain',
|
||||||
|
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||||
|
}}
|
||||||
|
className="pod-detail-drawer"
|
||||||
|
destroyOnClose
|
||||||
|
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||||
|
>
|
||||||
|
{pod && (
|
||||||
|
<>
|
||||||
|
<div className="pod-detail-drawer__pod">
|
||||||
|
<div className="pod-details-grid">
|
||||||
|
<div className="labels-row">
|
||||||
|
<Typography.Text
|
||||||
|
type="secondary"
|
||||||
|
className="pod-details-metadata-label"
|
||||||
|
>
|
||||||
|
NAMESPACE
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text
|
||||||
|
type="secondary"
|
||||||
|
className="pod-details-metadata-label"
|
||||||
|
>
|
||||||
|
Cluster Name
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text
|
||||||
|
type="secondary"
|
||||||
|
className="pod-details-metadata-label"
|
||||||
|
>
|
||||||
|
Node
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="values-row">
|
||||||
|
<Typography.Text className="pod-details-metadata-value">
|
||||||
|
<Tooltip title={pod.meta.k8s_namespace_name}>
|
||||||
|
{pod.meta.k8s_namespace_name}
|
||||||
|
</Tooltip>
|
||||||
|
</Typography.Text>
|
||||||
|
|
||||||
|
<Typography.Text className="pod-details-metadata-value">
|
||||||
|
<Tooltip title={pod.meta.k8s_cluster_name}>
|
||||||
|
{pod.meta.k8s_cluster_name}
|
||||||
|
</Tooltip>
|
||||||
|
</Typography.Text>
|
||||||
|
|
||||||
|
<Typography.Text className="pod-details-metadata-value">
|
||||||
|
<Tooltip title={pod.meta.k8s_node_name}>
|
||||||
|
{pod.meta.k8s_node_name}
|
||||||
|
</Tooltip>
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="views-tabs-container">
|
||||||
|
<Radio.Group
|
||||||
|
className="views-tabs"
|
||||||
|
onChange={handleTabChange}
|
||||||
|
value={selectedView}
|
||||||
|
>
|
||||||
|
<Radio.Button
|
||||||
|
className={
|
||||||
|
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||||
|
selectedView === VIEW_TYPES.METRICS ? 'selected_view tab' : 'tab'
|
||||||
|
}
|
||||||
|
value={VIEW_TYPES.METRICS}
|
||||||
|
>
|
||||||
|
<div className="view-title">
|
||||||
|
<BarChart2 size={14} />
|
||||||
|
Metrics
|
||||||
|
</div>
|
||||||
|
</Radio.Button>
|
||||||
|
<Radio.Button
|
||||||
|
className={
|
||||||
|
selectedView === VIEW_TYPES.LOGS ? 'selected_view tab' : 'tab'
|
||||||
|
}
|
||||||
|
value={VIEW_TYPES.LOGS}
|
||||||
|
>
|
||||||
|
<div className="view-title">
|
||||||
|
<ScrollText size={14} />
|
||||||
|
Logs
|
||||||
|
</div>
|
||||||
|
</Radio.Button>
|
||||||
|
<Radio.Button
|
||||||
|
className={
|
||||||
|
selectedView === VIEW_TYPES.TRACES ? 'selected_view tab' : 'tab'
|
||||||
|
}
|
||||||
|
value={VIEW_TYPES.TRACES}
|
||||||
|
>
|
||||||
|
<div className="view-title">
|
||||||
|
<DraftingCompass size={14} />
|
||||||
|
Traces
|
||||||
|
</div>
|
||||||
|
</Radio.Button>
|
||||||
|
<Radio.Button
|
||||||
|
className={
|
||||||
|
selectedView === VIEW_TYPES.EVENTS ? 'selected_view tab' : 'tab'
|
||||||
|
}
|
||||||
|
value={VIEW_TYPES.EVENTS}
|
||||||
|
>
|
||||||
|
<div className="view-title">
|
||||||
|
<ChevronsLeftRight size={14} />
|
||||||
|
Events
|
||||||
|
</div>
|
||||||
|
</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
|
||||||
|
{(selectedView === VIEW_TYPES.LOGS ||
|
||||||
|
selectedView === VIEW_TYPES.TRACES) && (
|
||||||
|
<Button
|
||||||
|
icon={<Compass size={18} />}
|
||||||
|
className="compass-button"
|
||||||
|
onClick={handleExplorePagesRedirect}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedView === VIEW_TYPES.METRICS && (
|
||||||
|
<Metrics
|
||||||
|
pod={pod}
|
||||||
|
selectedInterval={selectedInterval}
|
||||||
|
timeRange={modalTimeRange}
|
||||||
|
handleTimeChange={handleTimeChange}
|
||||||
|
isModalTimeSelection={isModalTimeSelection}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedView === VIEW_TYPES.LOGS && (
|
||||||
|
<PodLogsDetailedView
|
||||||
|
timeRange={modalTimeRange}
|
||||||
|
isModalTimeSelection={isModalTimeSelection}
|
||||||
|
handleTimeChange={handleTimeChange}
|
||||||
|
handleChangeLogFilters={handleChangeLogFilters}
|
||||||
|
logFilters={logFilters}
|
||||||
|
selectedInterval={selectedInterval}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedView === VIEW_TYPES.TRACES && (
|
||||||
|
<PodTraces
|
||||||
|
timeRange={modalTimeRange}
|
||||||
|
isModalTimeSelection={isModalTimeSelection}
|
||||||
|
handleTimeChange={handleTimeChange}
|
||||||
|
handleChangeTracesFilters={handleChangeTracesFilters}
|
||||||
|
tracesFilters={tracesFilters}
|
||||||
|
selectedInterval={selectedInterval}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedView === VIEW_TYPES.EVENTS && (
|
||||||
|
<Events
|
||||||
|
timeRange={modalTimeRange}
|
||||||
|
isModalTimeSelection={isModalTimeSelection}
|
||||||
|
handleTimeChange={handleTimeChange}
|
||||||
|
handleChangeLogFilters={handleChangeEventsFilters}
|
||||||
|
filters={eventsFilters}
|
||||||
|
selectedInterval={selectedInterval}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PodDetails;
|
||||||
16
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/NoLogsContainer.tsx
generated
Normal file
16
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/NoLogsContainer.tsx
generated
Normal file
@ -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 NoLogsContainer(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<div className="no-logs-found">
|
||||||
|
<Text type="secondary">
|
||||||
|
<Ghost size={24} color={Color.BG_AMBER_500} /> No logs found for this pod in
|
||||||
|
the selected time range.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
133
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/PodLogs.styles.scss
generated
Normal file
133
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/PodLogs.styles.scss
generated
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
.pod-logs-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pod-logs-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid var(--bg-slate-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pod-logs {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pod-logs-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pod-logs-list-card {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 12px;
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
218
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/PodLogs.tsx
generated
Normal file
218
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/PodLogs.tsx
generated
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
|
import './PodLogs.styles.scss';
|
||||||
|
|
||||||
|
import { Card } from 'antd';
|
||||||
|
import RawLogView from 'components/Logs/RawLogView';
|
||||||
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
|
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||||
|
import LogsError from 'container/LogsError/LogsError';
|
||||||
|
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||||
|
import { FontSize } from 'container/OptionsMenu/types';
|
||||||
|
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
||||||
|
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import {
|
||||||
|
IBuilderQuery,
|
||||||
|
TagFilterItem,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
import { QUERY_KEYS } from '../constants';
|
||||||
|
import { getPodLogsQueryPayload } from './constants';
|
||||||
|
import NoLogsContainer from './NoLogsContainer';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
timeRange: {
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
};
|
||||||
|
handleChangeLogFilters: (filters: IBuilderQuery['filters']) => void;
|
||||||
|
filters: IBuilderQuery['filters'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function PodLogs({
|
||||||
|
timeRange,
|
||||||
|
handleChangeLogFilters,
|
||||||
|
filters,
|
||||||
|
}: Props): JSX.Element {
|
||||||
|
const [logs, setLogs] = useState<ILog[]>([]);
|
||||||
|
const [hasReachedEndOfLogs, setHasReachedEndOfLogs] = useState(false);
|
||||||
|
const [restFilters, setRestFilters] = useState<TagFilterItem[]>([]);
|
||||||
|
const [resetLogsList, setResetLogsList] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newRestFilters = filters.items.filter(
|
||||||
|
(item) =>
|
||||||
|
item.key?.key !== 'id' &&
|
||||||
|
![
|
||||||
|
QUERY_KEYS.K8S_POD_NAME,
|
||||||
|
QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||||
|
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||||
|
].includes(item.key?.key || ''),
|
||||||
|
);
|
||||||
|
|
||||||
|
const areFiltersSame = isEqual(restFilters, newRestFilters);
|
||||||
|
|
||||||
|
if (!areFiltersSame) {
|
||||||
|
setResetLogsList(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRestFilters(newRestFilters);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [filters]);
|
||||||
|
|
||||||
|
const queryPayload = useMemo(() => {
|
||||||
|
const basePayload = getPodLogsQueryPayload(
|
||||||
|
timeRange.startTime,
|
||||||
|
timeRange.endTime,
|
||||||
|
filters,
|
||||||
|
);
|
||||||
|
|
||||||
|
basePayload.query.builder.queryData[0].pageSize = 100;
|
||||||
|
basePayload.query.builder.queryData[0].orderBy = [
|
||||||
|
{ columnName: 'timestamp', order: ORDERBY_FILTERS.DESC },
|
||||||
|
];
|
||||||
|
|
||||||
|
return basePayload;
|
||||||
|
}, [timeRange.startTime, timeRange.endTime, filters]);
|
||||||
|
|
||||||
|
const [isPaginating, setIsPaginating] = useState(false);
|
||||||
|
|
||||||
|
const { data, isLoading, isFetching, isError } = useQuery({
|
||||||
|
queryKey: ['podLogs', timeRange.startTime, timeRange.endTime, filters],
|
||||||
|
queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION),
|
||||||
|
enabled: !!queryPayload,
|
||||||
|
keepPreviousData: isPaginating,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.payload?.data?.newResult?.data?.result) {
|
||||||
|
const currentData = data.payload.data.newResult.data.result;
|
||||||
|
|
||||||
|
if (resetLogsList) {
|
||||||
|
const currentLogs: ILog[] =
|
||||||
|
currentData[0].list?.map((item) => ({
|
||||||
|
...item.data,
|
||||||
|
timestamp: item.timestamp,
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
setLogs(currentLogs);
|
||||||
|
|
||||||
|
setResetLogsList(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentData.length > 0 && currentData[0].list) {
|
||||||
|
const currentLogs: ILog[] =
|
||||||
|
currentData[0].list.map((item) => ({
|
||||||
|
...item.data,
|
||||||
|
timestamp: item.timestamp,
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
setLogs((prev) => [...prev, ...currentLogs]);
|
||||||
|
} else {
|
||||||
|
setHasReachedEndOfLogs(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [data, restFilters, isPaginating, resetLogsList]);
|
||||||
|
|
||||||
|
const getItemContent = useCallback(
|
||||||
|
(_: number, logToRender: ILog): JSX.Element => (
|
||||||
|
<RawLogView
|
||||||
|
isReadOnly
|
||||||
|
isTextOverflowEllipsisDisabled
|
||||||
|
key={logToRender.id}
|
||||||
|
data={logToRender}
|
||||||
|
linesPerRow={5}
|
||||||
|
fontSize={FontSize.MEDIUM}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadMoreLogs = useCallback(() => {
|
||||||
|
if (!logs.length) return;
|
||||||
|
|
||||||
|
setIsPaginating(true);
|
||||||
|
const lastLog = logs[logs.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: lastLog.id,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const newFilters = {
|
||||||
|
op: 'AND',
|
||||||
|
items: newItems,
|
||||||
|
} as IBuilderQuery['filters'];
|
||||||
|
|
||||||
|
handleChangeLogFilters(newFilters);
|
||||||
|
}, [logs, filters, handleChangeLogFilters]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsPaginating(false);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const renderFooter = useCallback(
|
||||||
|
(): JSX.Element | null => (
|
||||||
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
|
<>
|
||||||
|
{isFetching ? (
|
||||||
|
<div className="logs-loading-skeleton"> Loading more logs ... </div>
|
||||||
|
) : hasReachedEndOfLogs ? (
|
||||||
|
<div className="logs-loading-skeleton"> *** End *** </div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
[isFetching, hasReachedEndOfLogs],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderContent = useMemo(
|
||||||
|
() => (
|
||||||
|
<Card bordered={false} className="pod-logs-list-card">
|
||||||
|
<OverlayScrollbar isVirtuoso>
|
||||||
|
<Virtuoso
|
||||||
|
className="pod-logs-virtuoso"
|
||||||
|
key="pod-logs-virtuoso"
|
||||||
|
data={logs}
|
||||||
|
endReached={loadMoreLogs}
|
||||||
|
totalCount={logs.length}
|
||||||
|
itemContent={getItemContent}
|
||||||
|
overscan={200}
|
||||||
|
components={{
|
||||||
|
Footer: renderFooter,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</OverlayScrollbar>
|
||||||
|
</Card>
|
||||||
|
),
|
||||||
|
[logs, loadMoreLogs, getItemContent, renderFooter],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pod-logs">
|
||||||
|
{isLoading && <LogsLoading />}
|
||||||
|
{!isLoading && !isError && logs.length === 0 && <NoLogsContainer />}
|
||||||
|
{isError && !isLoading && <LogsError />}
|
||||||
|
{!isLoading && !isError && logs.length > 0 && (
|
||||||
|
<div className="pod-logs-list-container">{renderContent}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PodLogs;
|
||||||
99
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/PodLogsDetailedView.tsx
generated
Normal file
99
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/PodLogsDetailedView.tsx
generated
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import './PodLogs.styles.scss';
|
||||||
|
|
||||||
|
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 { useMemo } from 'react';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import PodLogs from './PodLogs';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
timeRange: {
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
};
|
||||||
|
isModalTimeSelection: boolean;
|
||||||
|
handleTimeChange: (
|
||||||
|
interval: Time | CustomTimeType,
|
||||||
|
dateTimeRange?: [number, number],
|
||||||
|
) => void;
|
||||||
|
handleChangeLogFilters: (value: IBuilderQuery['filters']) => void;
|
||||||
|
logFilters: IBuilderQuery['filters'];
|
||||||
|
selectedInterval: Time;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PodLogsDetailedView({
|
||||||
|
timeRange,
|
||||||
|
isModalTimeSelection,
|
||||||
|
handleTimeChange,
|
||||||
|
handleChangeLogFilters,
|
||||||
|
logFilters,
|
||||||
|
selectedInterval,
|
||||||
|
}: Props): JSX.Element {
|
||||||
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
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;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="host-metrics-logs-container">
|
||||||
|
<div className="host-metrics-logs-header">
|
||||||
|
<div className="filter-section">
|
||||||
|
{query && (
|
||||||
|
<QueryBuilderSearch
|
||||||
|
query={query}
|
||||||
|
onChange={handleChangeLogFilters}
|
||||||
|
disableNavigationShortcuts
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="datetime-section">
|
||||||
|
<DateTimeSelectionV2
|
||||||
|
showAutoRefresh={false}
|
||||||
|
showRefreshText={false}
|
||||||
|
hideShareModal
|
||||||
|
isModalTimeSelection={isModalTimeSelection}
|
||||||
|
onTimeChange={handleTimeChange}
|
||||||
|
defaultRelativeTime="5m"
|
||||||
|
modalSelectedInterval={selectedInterval}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PodLogs
|
||||||
|
timeRange={timeRange}
|
||||||
|
handleChangeLogFilters={handleChangeLogFilters}
|
||||||
|
filters={logFilters}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PodLogsDetailedView;
|
||||||
65
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/constants.ts
generated
Normal file
65
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodLogs/constants.ts
generated
Normal file
@ -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 getPodLogsQueryPayload = (
|
||||||
|
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,
|
||||||
|
});
|
||||||
193
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodTraces/PodTraces.styles.scss
generated
Normal file
193
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodTraces/PodTraces.styles.scss
generated
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
.pod-metric-traces {
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
.pod-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pod-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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
201
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodTraces/PodTraces.tsx
generated
Normal file
201
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodTraces/PodTraces.tsx
generated
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import './PodTraces.styles.scss';
|
||||||
|
|
||||||
|
import { getListColumns } from 'components/HostMetricsDetail/HostMetricTraces/utils';
|
||||||
|
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 { getPodTracesQueryPayload, selectedColumns } from './constants';
|
||||||
|
|
||||||
|
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 PodTraces({
|
||||||
|
timeRange,
|
||||||
|
isModalTimeSelection,
|
||||||
|
handleTimeChange,
|
||||||
|
handleChangeTracesFilters,
|
||||||
|
tracesFilters,
|
||||||
|
selectedInterval,
|
||||||
|
}: Props): JSX.Element {
|
||||||
|
const [traces, setTraces] = useState<any[]>([]);
|
||||||
|
const [offset] = useState<number>(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,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[currentQuery],
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log({ updatedCurrentQuery });
|
||||||
|
|
||||||
|
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||||
|
|
||||||
|
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
|
||||||
|
QueryParams.pagination,
|
||||||
|
);
|
||||||
|
|
||||||
|
const queryPayload = useMemo(
|
||||||
|
() =>
|
||||||
|
getPodTracesQueryPayload(
|
||||||
|
timeRange.startTime,
|
||||||
|
timeRange.endTime,
|
||||||
|
paginationQueryData?.offset || offset,
|
||||||
|
tracesFilters,
|
||||||
|
),
|
||||||
|
[
|
||||||
|
timeRange.startTime,
|
||||||
|
timeRange.endTime,
|
||||||
|
offset,
|
||||||
|
tracesFilters,
|
||||||
|
paginationQueryData,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data, isLoading, isFetching, isError } = useQuery({
|
||||||
|
queryKey: [
|
||||||
|
'podTraces',
|
||||||
|
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 (
|
||||||
|
<div className="host-metric-traces">
|
||||||
|
<div className="host-metric-traces-header">
|
||||||
|
<div className="filter-section">
|
||||||
|
{query && (
|
||||||
|
<QueryBuilderSearch
|
||||||
|
query={query}
|
||||||
|
onChange={handleChangeTracesFilters}
|
||||||
|
disableNavigationShortcuts
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="datetime-section">
|
||||||
|
<DateTimeSelectionV2
|
||||||
|
showAutoRefresh={false}
|
||||||
|
showRefreshText={false}
|
||||||
|
hideShareModal
|
||||||
|
isModalTimeSelection={isModalTimeSelection}
|
||||||
|
onTimeChange={handleTimeChange}
|
||||||
|
defaultRelativeTime="5m"
|
||||||
|
modalSelectedInterval={selectedInterval}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isError && <ErrorText>{data?.error || 'Something went wrong'}</ErrorText>}
|
||||||
|
|
||||||
|
{isLoading && traces.length === 0 && <TracesLoading />}
|
||||||
|
|
||||||
|
{isDataEmpty && !hasAdditionalFilters && (
|
||||||
|
<NoLogs dataSource={DataSource.TRACES} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isDataEmpty && hasAdditionalFilters && (
|
||||||
|
<EmptyLogsSearch dataSource={DataSource.TRACES} panelType="LIST" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isError && traces.length > 0 && (
|
||||||
|
<div className="pod-traces-table">
|
||||||
|
<TraceExplorerControls
|
||||||
|
isLoading={isFetching}
|
||||||
|
totalCount={totalCount}
|
||||||
|
perPageOptions={PER_PAGE_OPTIONS}
|
||||||
|
showSizeChanger={false}
|
||||||
|
/>
|
||||||
|
<ResizeTable
|
||||||
|
tableLayout="fixed"
|
||||||
|
pagination={false}
|
||||||
|
scroll={{ x: true }}
|
||||||
|
loading={isFetching}
|
||||||
|
dataSource={traces}
|
||||||
|
columns={traceListColumns}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PodTraces;
|
||||||
200
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodTraces/constants.ts
generated
Normal file
200
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodTraces/constants.ts
generated
Normal file
@ -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 getPodTracesQueryPayload = (
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
7
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/constants.ts
generated
Normal file
7
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/constants.ts
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export const QUERY_KEYS = {
|
||||||
|
K8S_OBJECT_KIND: 'k8s.object.kind',
|
||||||
|
K8S_OBJECT_NAME: 'k8s.object.name',
|
||||||
|
K8S_POD_NAME: 'k8s.pod.name',
|
||||||
|
K8S_NAMESPACE_NAME: 'k8s.namespace.name',
|
||||||
|
K8S_CLUSTER_NAME: 'k8s.cluster.name',
|
||||||
|
};
|
||||||
3
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/index.tsx
generated
Normal file
3
frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/index.tsx
generated
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import PodDetails from './PodDetails';
|
||||||
|
|
||||||
|
export default PodDetails;
|
||||||
2672
frontend/src/container/InfraMonitoringK8s/Pods/constants.ts
generated
Normal file
2672
frontend/src/container/InfraMonitoringK8s/Pods/constants.ts
generated
Normal file
File diff suppressed because it is too large
Load Diff
165
frontend/src/container/InfraMonitoringK8s/commonUtils.tsx
Normal file
165
frontend/src/container/InfraMonitoringK8s/commonUtils.tsx
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
/* eslint-disable react/require-default-props */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Tooltip, Typography } from 'antd';
|
||||||
|
import { ColumnsType } from 'antd/es/table';
|
||||||
|
import { Progress } from 'antd/lib';
|
||||||
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
|
import FieldRenderer from 'container/LogDetailedView/FieldRenderer';
|
||||||
|
import { DataType } from 'container/LogDetailedView/TableView';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { getInvalidValueTooltipText, K8sCategory } from './constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts size in bytes to a human-readable string with appropriate units
|
||||||
|
*/
|
||||||
|
export function formatBytes(bytes: number, decimals = 2): string {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
|
return `${parseFloat((bytes / k ** i).toFixed(decimals))} ${sizes[i]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper component that renders its children for valid values or renders '-' for invalid values (-1)
|
||||||
|
*/
|
||||||
|
export function ValidateColumnValueWrapper({
|
||||||
|
children,
|
||||||
|
value,
|
||||||
|
entity,
|
||||||
|
attribute,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
value: number;
|
||||||
|
entity?: K8sCategory;
|
||||||
|
attribute?: string;
|
||||||
|
}): JSX.Element {
|
||||||
|
if (value === -1) {
|
||||||
|
let element = <div>-</div>;
|
||||||
|
if (entity && attribute) {
|
||||||
|
element = (
|
||||||
|
<Tooltip title={getInvalidValueTooltipText(entity, attribute)}>
|
||||||
|
{element}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns stroke color for request utilization parameters according to current value
|
||||||
|
*/
|
||||||
|
export function getStrokeColorForRequestUtilization(value: number): string {
|
||||||
|
const percent = Number((value * 100).toFixed(1));
|
||||||
|
// Orange
|
||||||
|
if (percent <= 50) {
|
||||||
|
return Color.BG_AMBER_500;
|
||||||
|
}
|
||||||
|
// Green
|
||||||
|
if (percent > 50 && percent <= 100) {
|
||||||
|
return Color.BG_FOREST_500;
|
||||||
|
}
|
||||||
|
// Regular Red
|
||||||
|
if (percent > 100 && percent <= 150) {
|
||||||
|
return Color.BG_SAKURA_500;
|
||||||
|
}
|
||||||
|
// Dark Red
|
||||||
|
return Color.BG_CHERRY_600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns stroke color for limit utilization parameters according to current value
|
||||||
|
*/
|
||||||
|
export function getStrokeColorForLimitUtilization(value: number): string {
|
||||||
|
const percent = Number((value * 100).toFixed(1));
|
||||||
|
// Green
|
||||||
|
if (percent <= 60) {
|
||||||
|
return Color.BG_FOREST_500;
|
||||||
|
}
|
||||||
|
// Yellow
|
||||||
|
if (percent > 60 && percent <= 80) {
|
||||||
|
return Color.BG_AMBER_200;
|
||||||
|
}
|
||||||
|
// Orange
|
||||||
|
if (percent > 80 && percent <= 95) {
|
||||||
|
return Color.BG_AMBER_500;
|
||||||
|
}
|
||||||
|
// Red
|
||||||
|
return Color.BG_SAKURA_500;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getProgressBarText = (percent: number): React.ReactNode =>
|
||||||
|
`${percent}%`;
|
||||||
|
|
||||||
|
export function EntityProgressBar({ value }: { value: number }): JSX.Element {
|
||||||
|
const percentage = Number((value * 100).toFixed(1));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="entity-progress-bar">
|
||||||
|
<Progress
|
||||||
|
percent={percentage}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
size="small"
|
||||||
|
status="normal"
|
||||||
|
strokeColor={getStrokeColorForLimitUtilization(value)}
|
||||||
|
className="progress-bar"
|
||||||
|
showInfo={false}
|
||||||
|
/>
|
||||||
|
<Typography.Text style={{ fontSize: '10px' }}>{percentage}%</Typography.Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventContents({
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
data: Record<string, string> | undefined;
|
||||||
|
}): JSX.Element {
|
||||||
|
const tableData = useMemo(
|
||||||
|
() =>
|
||||||
|
data ? Object.keys(data).map((key) => ({ key, value: data[key] })) : [],
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns: ColumnsType<DataType> = [
|
||||||
|
{
|
||||||
|
title: 'Key',
|
||||||
|
dataIndex: 'key',
|
||||||
|
key: 'key',
|
||||||
|
width: 50,
|
||||||
|
align: 'left',
|
||||||
|
className: 'attribute-pin value-field-container',
|
||||||
|
render: (field: string): JSX.Element => <FieldRenderer field={field} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Value',
|
||||||
|
dataIndex: 'value',
|
||||||
|
key: 'value',
|
||||||
|
width: 50,
|
||||||
|
align: 'left',
|
||||||
|
ellipsis: true,
|
||||||
|
className: 'attribute-name',
|
||||||
|
render: (field: string): JSX.Element => <FieldRenderer field={field} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResizeTable
|
||||||
|
columns={columns}
|
||||||
|
tableLayout="fixed"
|
||||||
|
dataSource={tableData}
|
||||||
|
pagination={false}
|
||||||
|
showHeader={false}
|
||||||
|
className="event-content-container"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
329
frontend/src/container/InfraMonitoringK8s/constants.ts
Normal file
329
frontend/src/container/InfraMonitoringK8s/constants.ts
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
import {
|
||||||
|
FiltersType,
|
||||||
|
IQuickFiltersConfig,
|
||||||
|
} from 'components/QuickFilters/QuickFilters';
|
||||||
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
export enum K8sCategory {
|
||||||
|
HOSTS = 'hosts',
|
||||||
|
PODS = 'pods',
|
||||||
|
NODES = 'nodes',
|
||||||
|
NAMESPACES = 'namespaces',
|
||||||
|
CLUSTERS = 'clusters',
|
||||||
|
DEPLOYMENTS = 'deployments',
|
||||||
|
STATEFULSETS = 'statefulsets',
|
||||||
|
DAEMONSETS = 'daemonsets',
|
||||||
|
CONTAINERS = 'containers',
|
||||||
|
JOBS = 'jobs',
|
||||||
|
VOLUMES = 'volumes',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const K8sCategories = {
|
||||||
|
HOSTS: 'hosts',
|
||||||
|
PODS: 'pods',
|
||||||
|
NODES: 'nodes',
|
||||||
|
NAMESPACES: 'namespaces',
|
||||||
|
CLUSTERS: 'clusters',
|
||||||
|
DEPLOYMENTS: 'deployments',
|
||||||
|
STATEFULSETS: 'statefulsets',
|
||||||
|
DAEMONSETS: 'daemonsets',
|
||||||
|
CONTAINERS: 'containers',
|
||||||
|
JOBS: 'jobs',
|
||||||
|
VOLUMES: 'volumes',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const K8sEntityToAggregateAttributeMapping = {
|
||||||
|
[K8sCategory.HOSTS]: 'system_cpu_load_average_15m',
|
||||||
|
[K8sCategory.PODS]: 'k8s_pod_cpu_utilization',
|
||||||
|
[K8sCategory.NODES]: 'k8s_node_cpu_utilization',
|
||||||
|
[K8sCategory.NAMESPACES]: 'k8s_pod_cpu_utilization',
|
||||||
|
[K8sCategory.CLUSTERS]: 'k8s_node_cpu_utilization',
|
||||||
|
[K8sCategory.DEPLOYMENTS]: 'k8s_pod_cpu_utilization',
|
||||||
|
[K8sCategory.STATEFULSETS]: 'k8s_pod_cpu_utilization',
|
||||||
|
[K8sCategory.DAEMONSETS]: 'k8s_pod_cpu_utilization',
|
||||||
|
[K8sCategory.CONTAINERS]: 'k8s_pod_cpu_utilization',
|
||||||
|
[K8sCategory.JOBS]: 'k8s_pod_cpu_utilization',
|
||||||
|
[K8sCategory.VOLUMES]: 'k8s_pod_cpu_utilization',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PodsQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Pod',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'k8s_pod_name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'k8s_pod_name--string--tag--true',
|
||||||
|
},
|
||||||
|
aggregateOperator: 'noop',
|
||||||
|
aggregateAttribute: 'k8s_pod_cpu_utilization',
|
||||||
|
dataSource: DataSource.METRICS,
|
||||||
|
defaultOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Namespace',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'k8s_namespace_name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
aggregateOperator: 'noop',
|
||||||
|
aggregateAttribute: 'k8s_pod_cpu_utilization',
|
||||||
|
dataSource: DataSource.METRICS,
|
||||||
|
defaultOpen: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Node',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'k8s_node_name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'k8s.node.name--string--resource--true',
|
||||||
|
},
|
||||||
|
aggregateOperator: 'noop',
|
||||||
|
aggregateAttribute: 'k8s_pod_cpu_utilization',
|
||||||
|
dataSource: DataSource.METRICS,
|
||||||
|
defaultOpen: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Cluster',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'k8s_cluster_name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
aggregateOperator: 'noop',
|
||||||
|
aggregateAttribute: 'k8s_pod_cpu_utilization',
|
||||||
|
dataSource: DataSource.METRICS,
|
||||||
|
defaultOpen: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Deployment',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'k8s_deployment_name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
aggregateOperator: 'noop',
|
||||||
|
aggregateAttribute: 'k8s_pod_cpu_utilization',
|
||||||
|
dataSource: DataSource.METRICS,
|
||||||
|
defaultOpen: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Statefulset',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'k8s_statefulset_name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
aggregateOperator: 'noop',
|
||||||
|
aggregateAttribute: 'k8s_pod_cpu_utilization',
|
||||||
|
dataSource: DataSource.METRICS,
|
||||||
|
defaultOpen: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'DaemonSet',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'k8s_daemonset_name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
dataSource: DataSource.METRICS,
|
||||||
|
defaultOpen: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Job',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'k8s_job_name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
aggregateOperator: 'noop',
|
||||||
|
aggregateAttribute: 'k8s_pod_cpu_utilization',
|
||||||
|
dataSource: DataSource.METRICS,
|
||||||
|
defaultOpen: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const NodesQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Node Name',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'k8s_node_name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
aggregateOperator: 'noop',
|
||||||
|
aggregateAttribute: 'k8s_pod_cpu_utilization',
|
||||||
|
dataSource: DataSource.METRICS,
|
||||||
|
defaultOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Cluster Name',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'k8s_cluster_name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
aggregateOperator: 'noop',
|
||||||
|
aggregateAttribute: 'k8s_pod_cpu_utilization',
|
||||||
|
dataSource: DataSource.METRICS,
|
||||||
|
defaultOpen: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const NamespaceQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Namespace',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'k8s_namespace_name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
defaultOpen: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ClustersQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Cluster',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'k8s.cluster.name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
defaultOpen: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ContainersQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Container',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'k8s_container_name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
defaultOpen: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const VolumesQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Volume',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'k8s_volume_name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
defaultOpen: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DeploymentsQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Deployment',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'k8s_deployment_name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
defaultOpen: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const StatefulsetsQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Statefulset',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'k8s_statefulset_name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
defaultOpen: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DaemonSetsQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'DaemonSet',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'k8s_daemonset_name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
defaultOpen: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const JobsQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Job',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'k8s_job_name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
defaultOpen: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getInvalidValueTooltipText = (
|
||||||
|
entity: K8sCategory,
|
||||||
|
attribute: string,
|
||||||
|
): string => `Some ${entity} do not have ${attribute}s.`;
|
||||||
3
frontend/src/container/InfraMonitoringK8s/index.tsx
Normal file
3
frontend/src/container/InfraMonitoringK8s/index.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import InfraMonitoringK8s from './InfraMonitoringK8s';
|
||||||
|
|
||||||
|
export default InfraMonitoringK8s;
|
||||||
403
frontend/src/container/InfraMonitoringK8s/utils.tsx
Normal file
403
frontend/src/container/InfraMonitoringK8s/utils.tsx
Normal file
@ -0,0 +1,403 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
|
import './InfraMonitoringK8s.styles.scss';
|
||||||
|
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Tag, Tooltip } from 'antd';
|
||||||
|
import { ColumnType } from 'antd/es/table';
|
||||||
|
import {
|
||||||
|
K8sPodsData,
|
||||||
|
K8sPodsListPayload,
|
||||||
|
} from 'api/infraMonitoring/getK8sPodsList';
|
||||||
|
import { Group } from 'lucide-react';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
import {
|
||||||
|
EntityProgressBar,
|
||||||
|
formatBytes,
|
||||||
|
ValidateColumnValueWrapper,
|
||||||
|
} from './commonUtils';
|
||||||
|
import { K8sCategory } from './constants';
|
||||||
|
|
||||||
|
export interface IEntityColumn {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
id: string;
|
||||||
|
canRemove: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPodColumn {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
id: string;
|
||||||
|
canRemove: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnProgressBarClassName = 'column-progress-bar';
|
||||||
|
|
||||||
|
export const defaultAddedColumns: IPodColumn[] = [
|
||||||
|
{
|
||||||
|
label: 'Pod name',
|
||||||
|
value: 'podName',
|
||||||
|
id: 'podName',
|
||||||
|
canRemove: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'CPU Req Usage (%)',
|
||||||
|
value: 'cpu_request',
|
||||||
|
id: 'cpu_request',
|
||||||
|
canRemove: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'CPU Limit Usage (%)',
|
||||||
|
value: 'cpu_limit',
|
||||||
|
id: 'cpu_limit',
|
||||||
|
canRemove: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'CPU Usage (cores)',
|
||||||
|
value: 'cpu',
|
||||||
|
id: 'cpu',
|
||||||
|
canRemove: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Mem Req Usage (%)',
|
||||||
|
value: 'memory_request',
|
||||||
|
id: 'memory_request',
|
||||||
|
canRemove: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Mem Limit Usage (%)',
|
||||||
|
value: 'memory_limit',
|
||||||
|
id: 'memory_limit',
|
||||||
|
canRemove: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Mem Usage',
|
||||||
|
value: 'memory',
|
||||||
|
id: 'memory',
|
||||||
|
canRemove: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Restarts',
|
||||||
|
value: 'restarts',
|
||||||
|
id: 'restarts',
|
||||||
|
canRemove: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const defaultAvailableColumns = [
|
||||||
|
{
|
||||||
|
label: 'Namespace name',
|
||||||
|
value: 'namespace',
|
||||||
|
id: 'namespace',
|
||||||
|
canRemove: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Node name',
|
||||||
|
value: 'node',
|
||||||
|
id: 'node',
|
||||||
|
canRemove: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cluster name',
|
||||||
|
value: 'cluster',
|
||||||
|
id: 'cluster',
|
||||||
|
canRemove: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface K8sPodsRowData {
|
||||||
|
key: string;
|
||||||
|
podName: React.ReactNode;
|
||||||
|
podUID: string;
|
||||||
|
cpu_request: React.ReactNode;
|
||||||
|
cpu_limit: React.ReactNode;
|
||||||
|
cpu: React.ReactNode;
|
||||||
|
memory_request: React.ReactNode;
|
||||||
|
memory_limit: React.ReactNode;
|
||||||
|
memory: React.ReactNode;
|
||||||
|
restarts: React.ReactNode;
|
||||||
|
groupedByMeta?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getK8sPodsListQuery = (): K8sPodsListPayload => ({
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'and',
|
||||||
|
},
|
||||||
|
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const podGroupColumnConfig = {
|
||||||
|
title: (
|
||||||
|
<div className="column-header pod-group-header">
|
||||||
|
<Group size={14} /> POD GROUP
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
dataIndex: 'podGroup',
|
||||||
|
key: 'podGroup',
|
||||||
|
ellipsis: true,
|
||||||
|
width: 180,
|
||||||
|
sorter: false,
|
||||||
|
className: 'column column-pod-group',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dummyColumnConfig = {
|
||||||
|
title: <div className="column-header dummy-column"> </div>,
|
||||||
|
dataIndex: 'dummy',
|
||||||
|
key: 'dummy',
|
||||||
|
width: 40,
|
||||||
|
sorter: false,
|
||||||
|
align: 'left',
|
||||||
|
className: 'column column-dummy',
|
||||||
|
};
|
||||||
|
|
||||||
|
const columnsConfig = [
|
||||||
|
{
|
||||||
|
title: <div className="column-header pod-name-header">Pod Name</div>,
|
||||||
|
dataIndex: 'podName',
|
||||||
|
key: 'podName',
|
||||||
|
width: 180,
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: true,
|
||||||
|
className: 'column column-pod-name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <div className="column-header">CPU Req Usage (%)</div>,
|
||||||
|
dataIndex: 'cpu_request',
|
||||||
|
key: 'cpu_request',
|
||||||
|
width: 180,
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: true,
|
||||||
|
align: 'left',
|
||||||
|
className: `column ${columnProgressBarClassName}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <div className="column-header">CPU Limit Usage (%)</div>,
|
||||||
|
dataIndex: 'cpu_limit',
|
||||||
|
key: 'cpu_limit',
|
||||||
|
width: 120,
|
||||||
|
sorter: true,
|
||||||
|
align: 'left',
|
||||||
|
className: `column ${columnProgressBarClassName}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <div className="column-header">CPU Usage (cores)</div>,
|
||||||
|
dataIndex: 'cpu',
|
||||||
|
key: 'cpu',
|
||||||
|
width: 80,
|
||||||
|
sorter: true,
|
||||||
|
align: 'left',
|
||||||
|
className: `column ${columnProgressBarClassName}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <div className="column-header">Mem Req Usage (%)</div>,
|
||||||
|
dataIndex: 'memory_request',
|
||||||
|
key: 'memory_request',
|
||||||
|
width: 120,
|
||||||
|
sorter: true,
|
||||||
|
align: 'left',
|
||||||
|
className: `column ${columnProgressBarClassName}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <div className="column-header">Mem Limit Usage (%)</div>,
|
||||||
|
dataIndex: 'memory_limit',
|
||||||
|
key: 'memory_limit',
|
||||||
|
width: 120,
|
||||||
|
sorter: true,
|
||||||
|
align: 'left',
|
||||||
|
className: `column ${columnProgressBarClassName}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <div className="column-header">Mem Usage</div>,
|
||||||
|
dataIndex: 'memory',
|
||||||
|
key: 'memory',
|
||||||
|
width: 80,
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: true,
|
||||||
|
align: 'left',
|
||||||
|
className: `column ${columnProgressBarClassName}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: (
|
||||||
|
<div className="column-header">
|
||||||
|
<Tooltip title="Container Restarts">Restarts</Tooltip>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
dataIndex: 'restarts',
|
||||||
|
key: 'restarts',
|
||||||
|
width: 40,
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: true,
|
||||||
|
align: 'left',
|
||||||
|
className: `column ${columnProgressBarClassName}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const namespaceColumnConfig = {
|
||||||
|
title: <div className="column-header">Namespace</div>,
|
||||||
|
dataIndex: 'namespace',
|
||||||
|
key: 'namespace',
|
||||||
|
width: 100,
|
||||||
|
sorter: false,
|
||||||
|
ellipsis: true,
|
||||||
|
align: 'left',
|
||||||
|
className: 'column column-namespace',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const nodeColumnConfig = {
|
||||||
|
title: <div className="column-header">Node</div>,
|
||||||
|
dataIndex: 'node',
|
||||||
|
key: 'node',
|
||||||
|
width: 100,
|
||||||
|
sorter: true,
|
||||||
|
ellipsis: true,
|
||||||
|
align: 'left',
|
||||||
|
className: 'column column-node',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clusterColumnConfig = {
|
||||||
|
title: <div className="column-header">Cluster</div>,
|
||||||
|
dataIndex: 'cluster',
|
||||||
|
key: 'cluster',
|
||||||
|
width: 100,
|
||||||
|
sorter: true,
|
||||||
|
ellipsis: true,
|
||||||
|
align: 'left',
|
||||||
|
className: 'column column-cluster',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const columnConfigMap = {
|
||||||
|
namespace: namespaceColumnConfig,
|
||||||
|
node: nodeColumnConfig,
|
||||||
|
cluster: clusterColumnConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getK8sPodsListColumns = (
|
||||||
|
addedColumns: IPodColumn[],
|
||||||
|
groupBy: IBuilderQuery['groupBy'],
|
||||||
|
): ColumnType<K8sPodsRowData>[] => {
|
||||||
|
const updatedColumnsConfig = [...columnsConfig];
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const column of addedColumns) {
|
||||||
|
const config = columnConfigMap[column.id as keyof typeof columnConfigMap];
|
||||||
|
if (config) {
|
||||||
|
updatedColumnsConfig.push(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupBy.length > 0) {
|
||||||
|
const filteredColumns = [...updatedColumnsConfig].filter(
|
||||||
|
(column) => column.key !== 'podName',
|
||||||
|
);
|
||||||
|
|
||||||
|
filteredColumns.unshift(podGroupColumnConfig);
|
||||||
|
|
||||||
|
return filteredColumns as ColumnType<K8sPodsRowData>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedColumnsConfig as ColumnType<K8sPodsRowData>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGroupByEle = (
|
||||||
|
pod: K8sPodsData,
|
||||||
|
groupBy: IBuilderQuery['groupBy'],
|
||||||
|
): React.ReactNode => {
|
||||||
|
const groupByValues: string[] = [];
|
||||||
|
|
||||||
|
groupBy.forEach((group) => {
|
||||||
|
groupByValues.push(pod.meta[group.key as keyof typeof pod.meta]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pod-group">
|
||||||
|
{groupByValues.map((value) => (
|
||||||
|
<Tag key={value} color={Color.BG_SLATE_400} className="pod-group-tag-item">
|
||||||
|
{value === '' ? '<no-value>' : value}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatDataForTable = (
|
||||||
|
data: K8sPodsData[],
|
||||||
|
groupBy: IBuilderQuery['groupBy'],
|
||||||
|
): K8sPodsRowData[] =>
|
||||||
|
data.map((pod, index) => ({
|
||||||
|
key: `${pod.podUID}-${index}`,
|
||||||
|
podName: (
|
||||||
|
<Tooltip title={pod.meta.k8s_pod_name || ''}>
|
||||||
|
{pod.meta.k8s_pod_name || ''}
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
podUID: pod.podUID || '',
|
||||||
|
cpu_request: (
|
||||||
|
<ValidateColumnValueWrapper
|
||||||
|
value={pod.podCPURequest}
|
||||||
|
entity={K8sCategory.PODS}
|
||||||
|
attribute="CPU Request"
|
||||||
|
>
|
||||||
|
<div className="progress-container">
|
||||||
|
<EntityProgressBar value={pod.podCPURequest} />
|
||||||
|
</div>
|
||||||
|
</ValidateColumnValueWrapper>
|
||||||
|
),
|
||||||
|
cpu_limit: (
|
||||||
|
<ValidateColumnValueWrapper
|
||||||
|
value={pod.podCPULimit}
|
||||||
|
entity={K8sCategory.PODS}
|
||||||
|
attribute="CPU Limit"
|
||||||
|
>
|
||||||
|
<div className="progress-container">
|
||||||
|
<EntityProgressBar value={pod.podCPULimit} />
|
||||||
|
</div>
|
||||||
|
</ValidateColumnValueWrapper>
|
||||||
|
),
|
||||||
|
cpu: (
|
||||||
|
<ValidateColumnValueWrapper value={pod.podCPU}>
|
||||||
|
{pod.podCPU}
|
||||||
|
</ValidateColumnValueWrapper>
|
||||||
|
),
|
||||||
|
memory_request: (
|
||||||
|
<ValidateColumnValueWrapper
|
||||||
|
value={pod.podMemoryRequest}
|
||||||
|
entity={K8sCategory.PODS}
|
||||||
|
attribute="Memory Request"
|
||||||
|
>
|
||||||
|
<div className="progress-container">
|
||||||
|
<EntityProgressBar value={pod.podMemoryRequest} />
|
||||||
|
</div>
|
||||||
|
</ValidateColumnValueWrapper>
|
||||||
|
),
|
||||||
|
memory_limit: (
|
||||||
|
<ValidateColumnValueWrapper
|
||||||
|
value={pod.podMemoryLimit}
|
||||||
|
entity={K8sCategory.PODS}
|
||||||
|
attribute="Memory Limit"
|
||||||
|
>
|
||||||
|
<div className="progress-container">
|
||||||
|
<EntityProgressBar value={pod.podMemoryLimit} />
|
||||||
|
</div>
|
||||||
|
</ValidateColumnValueWrapper>
|
||||||
|
),
|
||||||
|
memory: (
|
||||||
|
<ValidateColumnValueWrapper value={pod.podMemory}>
|
||||||
|
{formatBytes(pod.podMemory)}
|
||||||
|
</ValidateColumnValueWrapper>
|
||||||
|
),
|
||||||
|
restarts: (
|
||||||
|
<ValidateColumnValueWrapper value={pod.restartCount}>
|
||||||
|
{pod.restartCount}
|
||||||
|
</ValidateColumnValueWrapper>
|
||||||
|
),
|
||||||
|
namespace: pod.meta.k8s_namespace_name,
|
||||||
|
node: pod.meta.k8s_node_name,
|
||||||
|
cluster: pod.meta.k8s_job_name,
|
||||||
|
meta: pod.meta,
|
||||||
|
podGroup: getGroupByEle(pod, groupBy),
|
||||||
|
...pod.meta,
|
||||||
|
groupedByMeta: pod.meta,
|
||||||
|
}));
|
||||||
@ -905,7 +905,7 @@
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
color: var(--Vanilla-100, #fff);
|
color: var(--bg-vanilla-100);
|
||||||
font-family: Inter;
|
font-family: Inter;
|
||||||
font-size: 12.805px;
|
font-size: 12.805px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
|||||||
@ -40,7 +40,7 @@ function OptionRenderer({
|
|||||||
interface OptionRendererProps {
|
interface OptionRendererProps {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
dataType: string;
|
dataType: string | undefined;
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import cx from 'classnames';
|
|||||||
import { OPERATORS } from 'constants/queryBuilder';
|
import { OPERATORS } from 'constants/queryBuilder';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
|
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
|
||||||
|
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||||
import { getDataTypes } from 'container/LogDetailedView/utils';
|
import { getDataTypes } from 'container/LogDetailedView/utils';
|
||||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||||
import {
|
import {
|
||||||
@ -74,11 +75,13 @@ function QueryBuilderSearch({
|
|||||||
suffixIcon,
|
suffixIcon,
|
||||||
isInfraMonitoring,
|
isInfraMonitoring,
|
||||||
disableNavigationShortcuts,
|
disableNavigationShortcuts,
|
||||||
|
entity,
|
||||||
}: 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, [
|
||||||
pathname,
|
pathname,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
updateTag,
|
updateTag,
|
||||||
handleClearTag,
|
handleClearTag,
|
||||||
@ -100,19 +103,23 @@ function QueryBuilderSearch({
|
|||||||
whereClauseConfig,
|
whereClauseConfig,
|
||||||
isLogsExplorerPage,
|
isLogsExplorerPage,
|
||||||
isInfraMonitoring,
|
isInfraMonitoring,
|
||||||
|
entity,
|
||||||
);
|
);
|
||||||
|
|
||||||
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>(
|
||||||
placeholder || '',
|
placeholder || '',
|
||||||
);
|
);
|
||||||
const selectRef = useRef<BaseSelectRef>(null);
|
const selectRef = useRef<BaseSelectRef>(null);
|
||||||
|
|
||||||
const { sourceKeys, handleRemoveSourceKey } = useFetchKeysAndValues(
|
const { sourceKeys, handleRemoveSourceKey } = useFetchKeysAndValues(
|
||||||
searchValue,
|
searchValue,
|
||||||
query,
|
query,
|
||||||
searchKey,
|
searchKey,
|
||||||
isLogsExplorerPage,
|
isLogsExplorerPage,
|
||||||
isInfraMonitoring,
|
isInfraMonitoring,
|
||||||
|
entity,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||||
@ -450,6 +457,7 @@ interface QueryBuilderSearchProps {
|
|||||||
suffixIcon?: React.ReactNode;
|
suffixIcon?: React.ReactNode;
|
||||||
isInfraMonitoring?: boolean;
|
isInfraMonitoring?: boolean;
|
||||||
disableNavigationShortcuts?: boolean;
|
disableNavigationShortcuts?: boolean;
|
||||||
|
entity?: K8sCategory | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
QueryBuilderSearch.defaultProps = {
|
QueryBuilderSearch.defaultProps = {
|
||||||
@ -459,6 +467,7 @@ QueryBuilderSearch.defaultProps = {
|
|||||||
suffixIcon: undefined,
|
suffixIcon: undefined,
|
||||||
isInfraMonitoring: false,
|
isInfraMonitoring: false,
|
||||||
disableNavigationShortcuts: false,
|
disableNavigationShortcuts: false,
|
||||||
|
entity: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface CustomTagProps {
|
export interface CustomTagProps {
|
||||||
|
|||||||
@ -51,4 +51,8 @@ export const routeConfig: Record<string, QueryParams[]> = {
|
|||||||
[ROUTES.WORKSPACE_LOCKED]: [QueryParams.resourceAttributes],
|
[ROUTES.WORKSPACE_LOCKED]: [QueryParams.resourceAttributes],
|
||||||
[ROUTES.MESSAGING_QUEUES]: [QueryParams.resourceAttributes],
|
[ROUTES.MESSAGING_QUEUES]: [QueryParams.resourceAttributes],
|
||||||
[ROUTES.MESSAGING_QUEUES_DETAIL]: [QueryParams.resourceAttributes],
|
[ROUTES.MESSAGING_QUEUES_DETAIL]: [QueryParams.resourceAttributes],
|
||||||
|
[ROUTES.INFRASTRUCTURE_MONITORING_HOSTS]: [QueryParams.resourceAttributes],
|
||||||
|
[ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES]: [
|
||||||
|
QueryParams.resourceAttributes,
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -216,6 +216,7 @@ export const routesToSkip = [
|
|||||||
ROUTES.MESSAGING_QUEUES_DETAIL,
|
ROUTES.MESSAGING_QUEUES_DETAIL,
|
||||||
ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
|
ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
|
||||||
ROUTES.SOMETHING_WENT_WRONG,
|
ROUTES.SOMETHING_WENT_WRONG,
|
||||||
|
ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
||||||
|
|||||||
@ -384,8 +384,12 @@ function DateTimeSelection({
|
|||||||
}
|
}
|
||||||
}, [defaultRelativeTime, onSelectHandler]);
|
}, [defaultRelativeTime, onSelectHandler]);
|
||||||
|
|
||||||
const [modalStartTime, setModalStartTime] = useState<number>(0);
|
const [modalStartTime, setModalStartTime] = useState<number>(
|
||||||
const [modalEndTime, setModalEndTime] = useState<number>(0);
|
searchStartTime ? parseInt(searchStartTime, 10) : 0,
|
||||||
|
);
|
||||||
|
const [modalEndTime, setModalEndTime] = useState<number>(
|
||||||
|
searchEndTime ? parseInt(searchEndTime, 10) : 0,
|
||||||
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
const onCustomDateHandler = (dateTimeRange: DateTimeRangeType): void => {
|
const onCustomDateHandler = (dateTimeRange: DateTimeRangeType): void => {
|
||||||
|
|||||||
45
frontend/src/hooks/infraMonitoring/useGetK8sNodesList.ts
Normal file
45
frontend/src/hooks/infraMonitoring/useGetK8sNodesList.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
getK8sNodesList,
|
||||||
|
K8sNodesListPayload,
|
||||||
|
K8sNodesListResponse,
|
||||||
|
} from 'api/infraMonitoring/getK8sNodesList';
|
||||||
|
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 UseGetK8sNodesList = (
|
||||||
|
requestData: K8sNodesListPayload,
|
||||||
|
options?: UseQueryOptions<
|
||||||
|
SuccessResponse<K8sNodesListResponse> | ErrorResponse,
|
||||||
|
Error
|
||||||
|
>,
|
||||||
|
headers?: Record<string, string>,
|
||||||
|
) => UseQueryResult<
|
||||||
|
SuccessResponse<K8sNodesListResponse> | ErrorResponse,
|
||||||
|
Error
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const useGetK8sNodesList: UseGetK8sNodesList = (
|
||||||
|
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<K8sNodesListResponse> | ErrorResponse, Error>({
|
||||||
|
queryFn: ({ signal }) => getK8sNodesList(requestData, signal, headers),
|
||||||
|
...options,
|
||||||
|
queryKey,
|
||||||
|
});
|
||||||
|
};
|
||||||
45
frontend/src/hooks/infraMonitoring/useGetK8sPodsList.ts
Normal file
45
frontend/src/hooks/infraMonitoring/useGetK8sPodsList.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
getK8sPodsList,
|
||||||
|
K8sPodsListPayload,
|
||||||
|
K8sPodsListResponse,
|
||||||
|
} from 'api/infraMonitoring/getK8sPodsList';
|
||||||
|
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 UseGetK8sPodsList = (
|
||||||
|
requestData: K8sPodsListPayload,
|
||||||
|
options?: UseQueryOptions<
|
||||||
|
SuccessResponse<K8sPodsListResponse> | ErrorResponse,
|
||||||
|
Error
|
||||||
|
>,
|
||||||
|
headers?: Record<string, string>,
|
||||||
|
) => UseQueryResult<
|
||||||
|
SuccessResponse<K8sPodsListResponse> | ErrorResponse,
|
||||||
|
Error
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const useGetK8sPodsList: UseGetK8sPodsList = (
|
||||||
|
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_POD_LIST, requestData];
|
||||||
|
}, [options?.queryKey, requestData]);
|
||||||
|
|
||||||
|
return useQuery<SuccessResponse<K8sPodsListResponse> | ErrorResponse, Error>({
|
||||||
|
queryFn: ({ signal }) => getK8sPodsList(requestData, signal, headers),
|
||||||
|
...options,
|
||||||
|
queryKey,
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { OPERATORS } from 'constants/queryBuilder';
|
import { OPERATORS } from 'constants/queryBuilder';
|
||||||
|
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||||
import {
|
import {
|
||||||
getRemovePrefixFromKey,
|
getRemovePrefixFromKey,
|
||||||
getTagToken,
|
getTagToken,
|
||||||
@ -29,6 +30,7 @@ export const useAutoComplete = (
|
|||||||
whereClauseConfig?: WhereClauseConfig,
|
whereClauseConfig?: WhereClauseConfig,
|
||||||
shouldUseSuggestions?: boolean,
|
shouldUseSuggestions?: boolean,
|
||||||
isInfraMonitoring?: boolean,
|
isInfraMonitoring?: boolean,
|
||||||
|
entity?: K8sCategory | null,
|
||||||
): IAutoComplete => {
|
): IAutoComplete => {
|
||||||
const [searchValue, setSearchValue] = useState<string>('');
|
const [searchValue, setSearchValue] = useState<string>('');
|
||||||
const [searchKey, setSearchKey] = useState<string>('');
|
const [searchKey, setSearchKey] = useState<string>('');
|
||||||
@ -39,6 +41,7 @@ export const useAutoComplete = (
|
|||||||
searchKey,
|
searchKey,
|
||||||
shouldUseSuggestions,
|
shouldUseSuggestions,
|
||||||
isInfraMonitoring,
|
isInfraMonitoring,
|
||||||
|
entity,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys);
|
const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys);
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import { getInfraAttributesValues } from 'api/infraMonitoring/getInfraAttributeValues';
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
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 {
|
||||||
|
K8sCategory,
|
||||||
|
K8sEntityToAggregateAttributeMapping,
|
||||||
|
} from 'container/InfraMonitoringK8s/constants';
|
||||||
import {
|
import {
|
||||||
getRemovePrefixFromKey,
|
getRemovePrefixFromKey,
|
||||||
getTagToken,
|
getTagToken,
|
||||||
@ -45,6 +49,7 @@ export const useFetchKeysAndValues = (
|
|||||||
searchKey: string,
|
searchKey: string,
|
||||||
shouldUseSuggestions?: boolean,
|
shouldUseSuggestions?: boolean,
|
||||||
isInfraMonitoring?: boolean,
|
isInfraMonitoring?: boolean,
|
||||||
|
entity?: K8sCategory | null,
|
||||||
): IuseFetchKeysAndValues => {
|
): IuseFetchKeysAndValues => {
|
||||||
const [keys, setKeys] = useState<BaseAutocompleteData[]>([]);
|
const [keys, setKeys] = useState<BaseAutocompleteData[]>([]);
|
||||||
const [exampleQueries, setExampleQueries] = useState<TagFilter[]>([]);
|
const [exampleQueries, setExampleQueries] = useState<TagFilter[]>([]);
|
||||||
@ -104,14 +109,18 @@ export const useFetchKeysAndValues = (
|
|||||||
searchText: searchKey,
|
searchText: searchKey,
|
||||||
dataSource: query.dataSource,
|
dataSource: query.dataSource,
|
||||||
aggregateOperator: query.aggregateOperator,
|
aggregateOperator: query.aggregateOperator,
|
||||||
aggregateAttribute: query.aggregateAttribute.key,
|
aggregateAttribute:
|
||||||
|
isInfraMonitoring && entity
|
||||||
|
? K8sEntityToAggregateAttributeMapping[entity]
|
||||||
|
: query.aggregateAttribute.key,
|
||||||
tagType: query.aggregateAttribute.type ?? null,
|
tagType: query.aggregateAttribute.type ?? null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
queryKey: [searchParams],
|
queryKey: [searchParams],
|
||||||
enabled: isQueryEnabled && !shouldUseSuggestions,
|
enabled: isQueryEnabled && !shouldUseSuggestions,
|
||||||
},
|
},
|
||||||
isInfraMonitoring,
|
isInfraMonitoring, // isInfraMonitoring
|
||||||
|
entity, // infraMonitoringEntity
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -157,9 +166,13 @@ export const useFetchKeysAndValues = (
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let payload;
|
let payload;
|
||||||
if (isInfraMonitoring) {
|
if (isInfraMonitoring && entity) {
|
||||||
const response = await getInfraAttributesValues({
|
const response = await getAttributesValues({
|
||||||
dataSource: DataSource.METRICS,
|
aggregateOperator: 'noop',
|
||||||
|
dataSource: query.dataSource,
|
||||||
|
aggregateAttribute:
|
||||||
|
K8sEntityToAggregateAttributeMapping[entity] ||
|
||||||
|
query.aggregateAttribute.key,
|
||||||
attributeKey: filterAttributeKey?.key ?? tagKey,
|
attributeKey: filterAttributeKey?.key ?? tagKey,
|
||||||
filterAttributeKeyDataType:
|
filterAttributeKeyDataType:
|
||||||
filterAttributeKey?.dataType ?? DataTypes.EMPTY,
|
filterAttributeKey?.dataType ?? DataTypes.EMPTY,
|
||||||
@ -167,8 +180,6 @@ export const useFetchKeysAndValues = (
|
|||||||
searchText: isInNInOperator(tagOperator)
|
searchText: isInNInOperator(tagOperator)
|
||||||
? tagValue[tagValue.length - 1]?.toString() ?? ''
|
? tagValue[tagValue.length - 1]?.toString() ?? ''
|
||||||
: tagValue?.toString() ?? '',
|
: tagValue?.toString() ?? '',
|
||||||
aggregateOperator: query.aggregateOperator,
|
|
||||||
aggregateAttribute: query.aggregateAttribute.key,
|
|
||||||
});
|
});
|
||||||
payload = response.payload;
|
payload = response.payload;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { getHostAttributeKeys } from 'api/infra/getHostAttributeKeys';
|
|
||||||
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
||||||
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
||||||
|
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
|
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
@ -13,6 +13,7 @@ type UseGetAttributeKeys = (
|
|||||||
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
|
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
|
||||||
>,
|
>,
|
||||||
isInfraMonitoring?: boolean,
|
isInfraMonitoring?: boolean,
|
||||||
|
infraMonitoringEntity?: K8sCategory | null,
|
||||||
) => UseQueryResult<
|
) => UseQueryResult<
|
||||||
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
|
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
|
||||||
>;
|
>;
|
||||||
@ -21,6 +22,7 @@ export const useGetAggregateKeys: UseGetAttributeKeys = (
|
|||||||
requestData,
|
requestData,
|
||||||
options,
|
options,
|
||||||
isInfraMonitoring,
|
isInfraMonitoring,
|
||||||
|
infraMonitoringEntity,
|
||||||
) => {
|
) => {
|
||||||
const queryKey = useMemo(() => {
|
const queryKey = useMemo(() => {
|
||||||
if (options?.queryKey && Array.isArray(options.queryKey)) {
|
if (options?.queryKey && Array.isArray(options.queryKey)) {
|
||||||
@ -28,17 +30,20 @@ export const useGetAggregateKeys: UseGetAttributeKeys = (
|
|||||||
QueryBuilderKeys.GET_AGGREGATE_KEYS,
|
QueryBuilderKeys.GET_AGGREGATE_KEYS,
|
||||||
...options.queryKey,
|
...options.queryKey,
|
||||||
isInfraMonitoring,
|
isInfraMonitoring,
|
||||||
|
infraMonitoringEntity,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return [QueryBuilderKeys.GET_AGGREGATE_KEYS, requestData, isInfraMonitoring];
|
return [
|
||||||
}, [options?.queryKey, requestData, isInfraMonitoring]);
|
QueryBuilderKeys.GET_AGGREGATE_KEYS,
|
||||||
|
requestData,
|
||||||
|
isInfraMonitoring,
|
||||||
|
infraMonitoringEntity,
|
||||||
|
];
|
||||||
|
}, [options?.queryKey, requestData, isInfraMonitoring, infraMonitoringEntity]);
|
||||||
|
|
||||||
return useQuery<SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse>({
|
return useQuery<SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse>({
|
||||||
queryKey,
|
queryKey,
|
||||||
queryFn: () =>
|
queryFn: () => getAggregateKeys(requestData),
|
||||||
isInfraMonitoring
|
|
||||||
? getHostAttributeKeys(requestData.searchText)
|
|
||||||
: getAggregateKeys(requestData),
|
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,6 +2,11 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.ant-tabs {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.ant-tabs-nav {
|
.ant-tabs-nav {
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
|
|||||||
@ -5,12 +5,12 @@ import { TabRoutes } from 'components/RouteTab/types';
|
|||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { useLocation } from 'react-use';
|
import { useLocation } from 'react-use';
|
||||||
|
|
||||||
import { Hosts } from './constants';
|
import { Hosts, Kubernetes } from './constants';
|
||||||
|
|
||||||
export default function InfrastructureMonitoringPage(): JSX.Element {
|
export default function InfrastructureMonitoringPage(): JSX.Element {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
const routes: TabRoutes[] = [Hosts];
|
const routes: TabRoutes[] = [Hosts, Kubernetes];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="infra-monitoring-module-container">
|
<div className="infra-monitoring-module-container">
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { TabRoutes } from 'components/RouteTab/types';
|
import { TabRoutes } from 'components/RouteTab/types';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import InfraMonitoringHosts from 'container/InfraMonitoringHosts';
|
import InfraMonitoringHosts from 'container/InfraMonitoringHosts';
|
||||||
|
import InfraMonitoringK8s from 'container/InfraMonitoringK8s';
|
||||||
import { Inbox } from 'lucide-react';
|
import { Inbox } from 'lucide-react';
|
||||||
|
|
||||||
export const Hosts: TabRoutes = {
|
export const Hosts: TabRoutes = {
|
||||||
@ -13,3 +14,14 @@ export const Hosts: TabRoutes = {
|
|||||||
route: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
|
route: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
|
||||||
key: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
|
key: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Kubernetes: TabRoutes = {
|
||||||
|
Component: InfraMonitoringK8s,
|
||||||
|
name: (
|
||||||
|
<div className="tab-item">
|
||||||
|
<Inbox size={16} /> Kubernetes
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
route: ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES,
|
||||||
|
key: ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES,
|
||||||
|
};
|
||||||
|
|||||||
@ -44,6 +44,13 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.periscope-input {
|
||||||
|
&.borderless {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
.periscope-btn {
|
.periscope-btn {
|
||||||
border-color: var(--bg-vanilla-300);
|
border-color: var(--bg-vanilla-300);
|
||||||
|
|||||||
@ -16,8 +16,8 @@ export type AutocompleteType = 'tag' | 'resource' | '';
|
|||||||
|
|
||||||
export interface BaseAutocompleteData {
|
export interface BaseAutocompleteData {
|
||||||
id?: string;
|
id?: string;
|
||||||
dataType: DataTypes;
|
dataType?: DataTypes;
|
||||||
isColumn: boolean;
|
isColumn?: boolean;
|
||||||
key: string;
|
key: string;
|
||||||
type: AutocompleteType | string | null;
|
type: AutocompleteType | string | null;
|
||||||
isJSON?: boolean;
|
isJSON?: boolean;
|
||||||
|
|||||||
@ -107,4 +107,5 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
|||||||
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'],
|
INFRASTRUCTURE_MONITORING_HOSTS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
|
INFRASTRUCTURE_MONITORING_KUBERNETES: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user