Compare commits

...

33 Commits

Author SHA1 Message Date
amlannandy
e8cea7ec77 feat: implement deployment details page 2024-12-22 23:02:55 +05:30
amlannandy
1798c0402f feat: implement quick filters and group by 2024-12-22 20:37:17 +05:30
amlannandy
0d79ce8b9a feat: remove nodes data 2024-12-22 19:11:16 +05:30
amlannandy
0dd674b45c chore: remove comments 2024-12-22 19:07:22 +05:30
amlannandy
56995145cc feat: implement deployments list table in infra monitoring 2024-12-22 19:07:22 +05:30
amlannandy
a7010b9237 feat: implement nodes list table in infra-monitoring 2024-12-22 19:06:57 +05:30
amlannandy
b5d57e1115 chore: fix expand issues 2024-12-19 18:14:48 +05:30
amlannandy
7f95f65f86 chore: styling changes 2024-12-19 18:14:48 +05:30
amlannandy
60e98fd9c9 feat: implement nested view 2024-12-19 18:14:48 +05:30
amlannandy
49a381a5c1 feat: implement group-by and quick select in node list table 2024-12-19 18:14:48 +05:30
amlannandy
82c657d63c chore: update props 2024-12-19 18:14:48 +05:30
amlannandy
1120fad7bc chore: add cluster name column 2024-12-19 18:14:48 +05:30
amlannandy
4317ebe975 chore: update columns 2024-12-19 18:14:48 +05:30
amlannandy
ebf5630f2d chore: update header props 2024-12-19 18:14:48 +05:30
amlannandy
ad5aa1aa7c feat: implement nodes list table in infra-monitoring 2024-12-19 18:14:48 +05:30
Srikanth Chekuri
172d69376e Merge branch 'main' into feat/infra-monitoring-k8s 2024-12-19 18:13:08 +05:30
Yunus M
c19ca091a8 feat: handle only one expanded row at a time 2024-12-19 13:10:11 +05:30
Yunus M
939103b098 feat: set size for nested table 2024-12-18 19:49:16 +05:30
Yunus M
791b530a85 feat: update query for metrics 2024-12-18 19:46:30 +05:30
Yunus M
4dfcd0ca72 feat: group by attribute - show nested table and handle view all 2024-12-18 19:45:53 +05:30
Yunus M
14eb19aa31 feat: ui fixes 2024-12-18 15:00:00 +05:30
Yunus M
c044f3126c feat: do not update url on filters change 2024-12-18 14:34:53 +05:30
Yunus M
e6ff2f18e6 feat: handle quick filters and qb search sync 2024-12-18 13:58:07 +05:30
Yunus M
f1be4d68e8 feat: handle groupby 2024-12-17 19:07:45 +05:30
Yunus M
39d9888bfe feat: handle category change 2024-12-11 20:19:16 +05:30
Yunus M
3878212300 feat: handle quick filter categorization 2024-12-11 20:00:43 +05:30
Yunus M
086416cdd2 feat: update query for quick filters 2024-12-11 17:13:38 +05:30
Yunus M
84a4ac4cb2 feat: update query to fetch logs and traces 2024-12-11 15:49:38 +05:30
Yunus M
a4a1d85b40 feat: integrate queries for the metrics tab 2024-12-10 19:35:20 +05:30
Yunus M
ab2ad8e308 feat: events tab integration 2024-12-10 19:35:20 +05:30
Yunus M
e90ae39003 chore: move pods related files in pods folder 2024-12-10 19:35:20 +05:30
Yunus M
974ec05f40 feat: setup k8s pods components 2024-12-10 19:35:20 +05:30
Yunus M
e8d39c3ff9 feat: base setup for k8s infra monitoring 2024-12-10 19:35:20 +05:30
86 changed files with 12676 additions and 46 deletions

View File

@@ -42,5 +42,6 @@
"DEFAULT": "Open source Observability Platform | SigNoz",
"ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring"
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring"
}

View File

@@ -55,5 +55,6 @@
"ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"MESSAGING_QUEUES": "SigNoz | Messaging Queues",
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring"
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring"
}

View File

@@ -407,6 +407,13 @@ const routes: AppRoutes[] = [
key: 'INFRASTRUCTURE_MONITORING_HOSTS',
isPrivate: true,
},
{
path: ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES,
exact: true,
component: InfrastructureMonitoring,
key: 'INFRASTRUCTURE_MONITORING_KUBERNETES',
isPrivate: true,
},
];
export const SUPPORT_ROUTE: AppRoutes = {

View File

@@ -0,0 +1,70 @@
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 K8sDeploymentsListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: {
columnName: string;
order: 'asc' | 'desc';
};
}
export interface K8sDeploymentsData {
deploymentName: string;
cpuUsage: number;
memoryUsage: number;
desiredPods: number;
availablePods: number;
cpuRequest: number;
memoryRequest: number;
cpuLimit: number;
memoryLimit: number;
restarts: number;
meta: {
k8s_cluster_name: string;
k8s_deployment_name: string;
k8s_namespace_name: string;
};
}
export interface K8sDeploymentsListResponse {
status: string;
data: {
type: string;
records: K8sDeploymentsData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
export const getK8sDeploymentsList = async (
props: K8sDeploymentsListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<K8sDeploymentsListResponse> | ErrorResponse> => {
try {
const response = await ApiBaseInstance.post('/deployments/list', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -0,0 +1,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);
}
};

View File

@@ -53,7 +53,7 @@
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
width: calc(100% - 24px);
cursor: pointer;
&.filter-disabled {

View File

@@ -11,7 +11,7 @@ import { OPERATORS } from 'constants/queryBuilder';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep, isArray, isEmpty, isEqual } from 'lodash-es';
import { cloneDeep, isArray, isEmpty, isEqual, isFunction } from 'lodash-es';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useMemo, useState } from 'react';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
@@ -34,10 +34,11 @@ function setDefaultValues(
}
interface ICheckboxProps {
filter: IQuickFiltersConfig;
onFilterChange?: (query: Query) => void;
}
export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
const { filter } = props;
const { filter, onFilterChange } = props;
const [searchText, setSearchText] = useState<string>('');
const [isOpen, setIsOpen] = useState<boolean>(filter.defaultOpen);
const [visibleItemsCount, setVisibleItemsCount] = useState<number>(10);
@@ -50,9 +51,9 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
const { data, isLoading } = useGetAggregateValues(
{
aggregateOperator: 'noop',
dataSource: DataSource.LOGS,
aggregateAttribute: '',
aggregateOperator: filter.aggregateOperator || 'noop',
dataSource: filter.dataSource || DataSource.LOGS,
aggregateAttribute: filter.aggregateAttribute || '',
attributeKey: filter.attributeKey.key,
filterAttributeKeyDataType: filter.attributeKey.dataType || DataTypes.EMPTY,
tagType: filter.attributeKey.type || '',
@@ -72,7 +73,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
);
const currentAttributeKeys = attributeValues.slice(0, visibleItemsCount);
// derive the state of each filter key here in the renderer itself and keep it in sync with staged query
// derive the state of each filter key here in the renderer itself and keep it in sync with current query
// also we need to keep a note of last focussed query.
// eslint-disable-next-line sonarjs/cognitive-complexity
const currentFilterState = useMemo(() => {
@@ -159,7 +160,12 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
})),
},
};
redirectWithQueryBuilderData(preparedQuery);
if (onFilterChange && isFunction(onFilterChange)) {
onFilterChange(preparedQuery);
} else {
redirectWithQueryBuilderData(preparedQuery);
}
};
const isSomeFilterPresentForCurrentAttribute = currentQuery.builder.queryData?.[
@@ -391,7 +397,11 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
},
};
redirectWithQueryBuilderData(finalQuery);
if (onFilterChange && isFunction(onFilterChange)) {
onFilterChange(finalQuery);
} else {
redirectWithQueryBuilderData(finalQuery);
}
};
return (
@@ -511,3 +521,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
</div>
);
}
CheckboxFilter.defaultProps = {
onFilterChange: null,
};

View File

@@ -15,6 +15,8 @@
display: flex;
align-items: center;
gap: 6px;
width: 100%;
justify-content: flex-start;
.text {
color: var(--bg-vanilla-400);
@@ -50,6 +52,8 @@
display: flex;
align-items: center;
gap: 12px;
width: 100%;
justify-content: flex-end;
.divider-filter {
width: 1px;

View File

@@ -7,9 +7,10 @@ import {
} from '@ant-design/icons';
import { Tooltip, Typography } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep } from 'lodash-es';
import { cloneDeep, isFunction } from 'lodash-es';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import Checkbox from './FilterRenderers/Checkbox/Checkbox';
import Slider from './FilterRenderers/Slider/Slider';
@@ -33,6 +34,9 @@ export interface IQuickFiltersConfig {
type: FiltersType;
title: string;
attributeKey: BaseAutocompleteData;
aggregateOperator?: string;
aggregateAttribute?: string;
dataSource?: DataSource;
customRendererForValue?: (value: string) => JSX.Element;
defaultOpen: boolean;
}
@@ -40,10 +44,12 @@ export interface IQuickFiltersConfig {
interface IQuickFiltersProps {
config: IQuickFiltersConfig[];
handleFilterVisibilityChange: () => void;
source?: string | null;
onFilterChange?: (query: Query) => void;
}
export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
const { config, handleFilterVisibilityChange } = props;
const { config, handleFilterVisibilityChange, source, onFilterChange } = props;
const {
currentQuery,
@@ -78,47 +84,63 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
})),
},
};
redirectWithQueryBuilderData(preparedQuery);
if (onFilterChange && isFunction(onFilterChange)) {
onFilterChange(preparedQuery);
} else {
redirectWithQueryBuilderData(preparedQuery);
}
};
const lastQueryName =
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
const isInfraMonitoring = source === 'infra-monitoring';
return (
<div className="quick-filters">
<section className="header">
<section className="left-actions">
<FilterOutlined />
<Typography.Text className="text">Filters for</Typography.Text>
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
</Tooltip>
{!isInfraMonitoring && (
<section className="header">
<section className="left-actions">
<FilterOutlined />
<Typography.Text className="text">Filters for</Typography.Text>
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
<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 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">
{config.map((filter) => {
switch (filter.type) {
case FiltersType.CHECKBOX:
return <Checkbox filter={filter} />;
return <Checkbox filter={filter} onFilterChange={onFilterChange} />;
case FiltersType.SLIDER:
return <Slider filter={filter} />;
default:
return <Checkbox filter={filter} />;
return <Checkbox filter={filter} onFilterChange={onFilterChange} />;
}
})}
</section>
</div>
);
}
QuickFilters.defaultProps = {
source: null,
onFilterChange: null,
};

View File

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

View File

@@ -304,8 +304,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY';
const isAlertOverview = (): boolean => routeKey === 'ALERT_OVERVIEW';
const isInfraMonitoringHosts = (): boolean =>
routeKey === 'INFRASTRUCTURE_MONITORING_HOSTS';
const isInfraMonitoring = (): boolean =>
routeKey === 'INFRASTRUCTURE_MONITORING_HOSTS' ||
routeKey === 'INFRASTRUCTURE_MONITORING_KUBERNETES';
const isPathMatch = (regex: RegExp): boolean => regex.test(pathname);
const isDashboardView = (): boolean =>
@@ -403,7 +404,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isAlertHistory() ||
isAlertOverview() ||
isMessagingQueues() ||
isInfraMonitoringHosts()
isInfraMonitoring()
? 0
: '0 1rem',

View File

@@ -168,7 +168,8 @@ function HostsList(): JSX.Element {
const showHostsEmptyState =
!isFetching &&
!isLoading &&
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics);
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics) &&
!filters.items.length;
return (
<div className="hosts-list">

View File

@@ -137,6 +137,9 @@
.column-header-right {
text-align: right;
}
.column-header-left {
text-align: left;
}
.ant-table-tbody > tr > td {
border-bottom: none;
}

View File

@@ -26,6 +26,7 @@ export const getHostListsQuery = (): HostListPayload => ({
groupBy: [],
orderBy: { columnName: 'cpu', order: 'desc' },
});
export const getTabsItems = (): TabsProps['items'] => [
{
label: <TabLabel label="List View" isDisabled={false} tooltipText="" />,

View File

@@ -0,0 +1,7 @@
import { K8sDeploymentsData } from 'api/infraMonitoring/getK8sDeploymentsList';
export type DeploymentDetailsProps = {
deployment: K8sDeploymentsData | null;
isModalTimeSelection: boolean;
onClose: () => void;
};

View File

@@ -0,0 +1,247 @@
.deployment-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);
}
.deployment-detail-drawer__deployment {
.deployment-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;
}
.deployment-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;
}
.deployment-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);
}
.deployment-detail-drawer {
.title {
color: var(--text-ink-300);
}
.deployment-detail-drawer__deployment {
.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);
}
}
}
}

View File

@@ -0,0 +1,553 @@
import './DeploymentDetails.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 { 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, VIEW_TYPES, VIEWS } from './constants';
import { DeploymentDetailsProps } from './DeploymentDetails.interfaces';
import DeploymentEvents from './Events';
import DeploymentLogs from './Logs';
import DeploymentMetrics from './Metrics';
import DeploymentTraces from './Traces';
function DeploymentDetails({
deployment,
onClose,
isModalTimeSelection,
}: DeploymentDetailsProps): 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: 'deployment.name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
id: 'deployment.name--string--resource--false',
},
op: '=',
value: deployment?.meta.k8s_deployment_name || '',
},
],
}),
[deployment?.meta.k8s_deployment_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: 'deployment',
},
{
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: deployment?.meta.k8s_deployment_name || '',
},
],
}),
[deployment?.meta.k8s_deployment_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', {
deployment: deployment?.deploymentName,
});
// 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: Deployment list details time updated', {
deployment: deployment?.deploymentName,
interval,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setLogFilters((prevFilters) => {
const hostNameFilter = prevFilters.items.find(
(item) => item.key?.key === 'host.name',
);
const paginationFilter = value.items.find((item) => item.key?.key === 'id');
const newFilters = value.items.filter(
(item) => item.key?.key !== 'id' && item.key?.key !== 'host.name',
);
logEvent(
'Infra Monitoring: Deployments list details logs filters applied',
{
deployment: deployment?.deploymentName,
},
);
return {
op: 'AND',
items: [
hostNameFilter,
...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 hostNameFilter = prevFilters.items.find(
(item) => item.key?.key === 'host.name',
);
logEvent(
'Infra Monitoring: Deployments list details traces filters applied',
{
deployment: deployment?.deploymentName,
},
);
return {
op: 'AND',
items: [
hostNameFilter,
...value.items.filter((item) => item.key?.key !== 'host.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 deploymentKindFilter = prevFilters.items.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
);
const deploymentNameFilter = prevFilters.items.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
);
logEvent(
'Infra Monitoring: Deployments list details events filters applied',
{
deployment: deployment?.deploymentName,
},
);
return {
op: 'AND',
items: [
deploymentKindFilter,
deploymentNameFilter,
...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: Deployments list details explore clicked', {
deployment: deployment?.deploymentName,
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();
};
console.log({
isModalTimeSelection,
eventsFilters,
handleTimeChange,
handleChangeLogFilters,
handleChangeTracesFilters,
handleChangeEventsFilters,
});
return (
<Drawer
width="70%"
title={
<>
<Divider type="vertical" />
<Typography.Text className="title">
{deployment?.meta.k8s_deployment_name}
</Typography.Text>
</>
}
placement="right"
onClose={handleClose}
open={!!deployment}
style={{
overscrollBehavior: 'contain',
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
}}
className="deployment-detail-drawer"
destroyOnClose
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
>
{deployment && (
<>
<div className="deployment-detail-drawer__deployment">
<div className="deployment-details-grid">
<div className="labels-row">
<Typography.Text
type="secondary"
className="deployment-details-metadata-label"
>
Deployment Name
</Typography.Text>
<Typography.Text
type="secondary"
className="deployment-details-metadata-label"
>
Cluster Name
</Typography.Text>
</div>
<div className="values-row">
<Typography.Text className="deployment-details-metadata-value">
<Tooltip title={deployment.meta.k8s_deployment_name}>
{deployment.meta.k8s_deployment_name}
</Tooltip>
</Typography.Text>
<Typography.Text className="deployment-details-metadata-value">
<Tooltip title="Cluster name">
{deployment.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 && (
<DeploymentMetrics
timeRange={modalTimeRange}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
selectedInterval={selectedInterval}
deployment={deployment}
/>
)}
{selectedView === VIEW_TYPES.LOGS && (
<DeploymentLogs
timeRange={modalTimeRange}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logFilters}
selectedInterval={selectedInterval}
/>
)}
{selectedView === VIEW_TYPES.TRACES && (
<DeploymentTraces
timeRange={modalTimeRange}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeTracesFilters={handleChangeTracesFilters}
tracesFilters={tracesFilters}
selectedInterval={selectedInterval}
/>
)}
{selectedView === VIEW_TYPES.EVENTS && (
<DeploymentEvents
timeRange={modalTimeRange}
handleChangeEventFilters={handleChangeEventsFilters}
filters={eventsFilters}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
selectedInterval={selectedInterval}
/>
)}
</>
)}
</Drawer>
);
}
export default DeploymentDetails;

View File

@@ -0,0 +1,289 @@
.deployment-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;
}
}
}
.deployment-events-header {
display: flex;
justify-content: space-between;
gap: 8px;
padding: 12px;
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
}
.deployment-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(.deploymentname-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(.deploymentname-column-value) {
background: var(--bg-ink-400);
}
.deploymentname-column-value {
color: var(--Vanilla-100, #fff);
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;
}
}
}
}
}
.deployment-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;
}
}
.deployment-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;
}

View File

@@ -0,0 +1,264 @@
import './DeploymentEvents.styles.scss';
import { Table, TableColumnsType, Typography } from 'antd';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
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 { ChevronDown, ChevronRight } from 'lucide-react';
import { useMemo } from 'react';
import { useQuery } from 'react-query';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { getDeploymentsEventsQueryPayload } 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;
}
interface IDeploymentEventsProps {
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;
}
export default function Events({
timeRange,
handleChangeEventFilters,
filters,
isModalTimeSelection,
handleTimeChange,
selectedInterval,
}: IDeploymentEventsProps): 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,
},
},
],
},
}),
[currentQuery],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
// const [restFilters, setRestFilters] = useState<TagFilterItem[]>([]);
// const [resetLogsList, setResetLogsList] = useState<boolean>(false);
// useEffect(() => {
// const newRestFilters = filters?.items?.filter(
// (item) =>
// item.key?.key !== 'id' &&
// item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME &&
// item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND,
// );
// 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 = getDeploymentsEventsQueryPayload(
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 { data, isLoading, isError } = useQuery({
queryKey: [
'deploymentEvents',
timeRange.startTime,
timeRange.endTime,
filters,
],
queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION),
enabled: !!queryPayload,
});
const columns: TableColumnsType<EventDataType> = [
{ title: 'Severity', dataIndex: 'severity', key: 'severity' },
{ title: 'Timestamp', dataIndex: 'timestamp', key: 'timestamp' },
{ title: 'Body', dataIndex: 'body', key: 'body' },
];
const formattedDeploymentEvents = useMemo(() => {
const responsePayload =
data?.payload.data.newResult.data.result[0].list || [];
const formattedData = responsePayload?.map((event) => ({
timestamp: event.timestamp,
severity: event.data.severity_text,
body: event.data.body,
id: event.data.id,
key: event.data.id,
}));
return formattedData || [];
}, [data]);
const handleExpandRow = (record: EventDataType): JSX.Element => {
console.log('record', record);
return <p style={{ margin: 0 }}>{record.body}</p>;
};
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="deployment-events-container">
<div className="deployment-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 && (
<div className="loading-logs">
<div className="loading-logs-content">
<img
className="loading-gif"
src="/Icons/loading-plane.gif"
alt="wait-icon"
/>
<Typography>Loading Events. Please wait...</Typography>
</div>
</div>
)}
{!isLoading && !isError && formattedDeploymentEvents.length === 0 && (
<NoEventsContainer />
)}
{isError && !isLoading && <LogsError />}
{!isLoading && !isError && formattedDeploymentEvents.length > 0 && (
<div className="deployment-events-list-container">
<div className="deployment-events-list-card">
<Table<EventDataType>
columns={columns}
expandable={{
expandedRowRender: handleExpandRow,
rowExpandable: (record): boolean => record.body !== 'Not Expandable',
expandIcon: handleExpandRowIcon,
}}
dataSource={formattedDeploymentEvents}
pagination={false}
/>
</div>
</div>
)}
</div>
);
}

View 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
deployment in the selected time range.
</Text>
</div>
);
}

View 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 getDeploymentsEventsQueryPayload = (
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,
});

View File

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

View File

@@ -0,0 +1,133 @@
.deployment-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;
}
}
}
.deployment-logs-header {
display: flex;
justify-content: space-between;
gap: 8px;
padding: 12px;
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
}
.deployment-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;
}
}
}
.deployment-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;
}
}
.deployment-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);
}
}
}

View File

@@ -0,0 +1,211 @@
/* eslint-disable no-nested-ternary */
import './DeploymentLogs.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 { getDeploymentLogsQueryPayload } 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' && item.key?.key !== 'deployment.name',
);
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 = getDeploymentLogsQueryPayload(
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: ['deploymentLogs', 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="deployment-logs-list-card">
<OverlayScrollbar isVirtuoso>
<Virtuoso
className="deployment-logs-virtuoso"
key="deployment-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="deployment-logs">
{isLoading && <LogsLoading />}
{!isLoading && !isError && logs.length === 0 && <NoLogsContainer />}
{isError && !isLoading && <LogsError />}
{!isLoading && !isError && logs.length > 0 && (
<div className="deployment-logs-list-container">{renderContent}</div>
)}
</div>
);
}
export default PodLogs;

View File

@@ -0,0 +1,95 @@
import './DeploymentLogs.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 DeploymentLogs from './DeploymentLogs';
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 DeploymentLogsDetailedView({
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,
},
},
],
},
}),
[currentQuery],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
return (
<div className="deployment-logs-container">
<div className="deployment-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>
<DeploymentLogs
timeRange={timeRange}
handleChangeLogFilters={handleChangeLogFilters}
filters={logFilters}
/>
</div>
);
}
export default DeploymentLogsDetailedView;

View 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
deployment in the selected time range.
</Text>
</div>
);
}

View 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 getDeploymentLogsQueryPayload = (
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,
});

View File

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

View File

@@ -0,0 +1,45 @@
.empty-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.deployment-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);
}
.deployment-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%);
}
}

View File

@@ -0,0 +1,145 @@
import './DeploymentMetrics.styles.scss';
import { Card, Col, Row, Skeleton, Typography } from 'antd';
import { K8sDeploymentsData } from 'api/infraMonitoring/getK8sDeploymentsList';
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 { deploymentWidgetInfo, getDeploymentQueryPayload } from './constants';
interface DeploymentMetricsProps {
timeRange: {
startTime: number;
endTime: number;
};
isModalTimeSelection: boolean;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
selectedInterval: Time;
deployment: K8sDeploymentsData;
}
function DeploymentMetrics({
selectedInterval,
deployment,
timeRange,
handleTimeChange,
isModalTimeSelection,
}: DeploymentMetricsProps): JSX.Element {
const queryPayloads = useMemo(
() =>
getDeploymentQueryPayload(
deployment,
timeRange.startTime,
timeRange.endTime,
),
[deployment, timeRange.startTime, timeRange.endTime],
);
const queries = useQueries(
queryPayloads.map((payload) => ({
queryKey: ['deployment-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: deploymentWidgetInfo[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="deployment-metrics-container">
{queries.map((query, idx) => (
<Col span={12} key={deploymentWidgetInfo[idx].title}>
<Typography.Text>{deploymentWidgetInfo[idx].title}</Typography.Text>
<Card bordered className="deployment-metrics-card" ref={graphRef}>
{renderCardContent(query, idx)}
</Card>
</Col>
))}
</Row>
</>
);
}
export default DeploymentMetrics;

View File

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

View File

@@ -0,0 +1,193 @@
.deployment-metric-traces {
margin-top: 1rem;
.deployment-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;
}
}
}
}
.deployment-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);
}
}
}
}

View File

@@ -0,0 +1,195 @@
import './DeploymentTraces.styles.scss';
import { ResizeTable } from 'components/ResizeTable';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { QueryParams } from 'constants/query';
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
import NoLogs from 'container/NoLogs/NoLogs';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { ErrorText } from 'container/TimeSeriesView/styles';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import TraceExplorerControls from 'container/TracesExplorer/Controls';
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Pagination } from 'hooks/queryPagination';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { getDeploymentTracesQueryPayload, selectedColumns } from './constants';
import { getListColumns } from './utils';
interface Props {
timeRange: {
startTime: number;
endTime: number;
};
isModalTimeSelection: boolean;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
handleChangeTracesFilters: (value: IBuilderQuery['filters']) => void;
tracesFilters: IBuilderQuery['filters'];
selectedInterval: Time;
}
function DeploymentTraces({
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,
},
},
],
},
}),
[currentQuery],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
QueryParams.pagination,
);
const queryPayload = useMemo(
() =>
getDeploymentTracesQueryPayload(
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="deployment-metric-traces">
<div className="deployment-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="deployment-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 DeploymentTraces;

View 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 getDeploymentTracesQueryPayload = (
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,
},
],
},
});

View File

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

View File

@@ -0,0 +1,84 @@
import { Tag, Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import {
BlockLink,
getTraceLink,
} from 'container/TracesExplorer/ListView/utils';
import dayjs from 'dayjs';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
const keyToLabelMap: Record<string, string> = {
timestamp: 'Timestamp',
serviceName: 'Service Name',
name: 'Name',
durationNano: 'Duration',
httpMethod: 'HTTP Method',
responseStatusCode: 'Status Code',
};
export const getListColumns = (
selectedColumns: BaseAutocompleteData[],
): ColumnsType<RowData> => {
const columns: ColumnsType<RowData> =
selectedColumns.map(({ dataType, key, type }) => ({
title: keyToLabelMap[key],
dataIndex: key,
key: `${key}-${dataType}-${type}`,
width: 145,
render: (value, item): JSX.Element => {
const itemData = item.data as any;
if (key === 'timestamp') {
const date =
typeof value === 'string'
? dayjs(value).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(value / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
return (
<BlockLink to={getTraceLink(item)} openInNewTab>
<Typography.Text>{date}</Typography.Text>
</BlockLink>
);
}
if (value === '') {
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Typography data-testid={key}>N/A</Typography>
</BlockLink>
);
}
if (key === 'httpMethod' || key === 'responseStatusCode') {
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Tag data-testid={key} color="magenta">
{itemData[key]}
</Tag>
</BlockLink>
);
}
if (key === 'durationNano') {
const durationNano = itemData[key];
return (
<BlockLink to={getTraceLink(item)} openInNewTab>
<Typography data-testid={key}>{getMs(durationNano)}ms</Typography>
</BlockLink>
);
}
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Typography data-testid={key}>{itemData[key]}</Typography>
</BlockLink>
);
},
responsive: ['md'],
})) || [];
return columns;
};

View File

@@ -0,0 +1,18 @@
export enum VIEWS {
METRICS = 'metrics',
LOGS = 'logs',
TRACES = 'traces',
EVENTS = 'events',
}
export const VIEW_TYPES = {
METRICS: VIEWS.METRICS,
LOGS: VIEWS.LOGS,
TRACES: VIEWS.TRACES,
EVENTS: VIEWS.EVENTS,
};
export const QUERY_KEYS = {
K8S_OBJECT_KIND: 'k8s.object.kind',
K8S_OBJECT_NAME: 'k8s.object.name',
};

View File

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

View File

@@ -0,0 +1,500 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
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 logEvent from 'api/common/logEvent';
import { K8sDeploymentsListPayload } from 'api/infraMonitoring/getK8sDeploymentsList';
import { useGetK8sDeploymentsList } from 'hooks/infraMonitoring/useGetK8sDeploymentsList';
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 K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import { dummyColumnConfig } from '../utils';
import {
defaultAddedColumns,
formatDataForTable,
getK8sDeploymentsListColumns,
getK8sDeploymentsListQuery,
K8sDeploymentsRowData,
} from './utils';
// eslint-disable-next-line sonarjs/cognitive-complexity
function K8sDeploymentsList({
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 [selectedDeploymentUID, setselectedDeploymentUID] = useState<string | null>(null);
const pageSize = 10;
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>([]);
const [
selectedRowData,
setSelectedRowData,
] = useState<K8sDeploymentsRowData | null>(null);
const [groupByOptions, setGroupByOptions] = useState<
{ value: string; label: string }[]
>([]);
const createFiltersForSelectedRowData = (
selectedRowData: K8sDeploymentsRowData,
): 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,
},
op: '=',
value: groupedByMeta[key],
});
}
return baseFilters;
};
const fetchGroupedByRowDataQuery = useMemo(() => {
if (!selectedRowData) return null;
const baseQuery = getK8sDeploymentsListQuery();
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,
} = useGetK8sDeploymentsList(
fetchGroupedByRowDataQuery as K8sDeploymentsListPayload,
{
queryKey: ['deploymentList', fetchGroupedByRowDataQuery],
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
},
);
const { currentQuery } = useQueryBuilder();
const {
data: groupByFiltersData,
isLoading: isLoadingGroupByFilters,
} = useGetAggregateKeys(
{
dataSource: currentQuery.builder.queryData[0].dataSource,
aggregateAttribute: '',
aggregateOperator: 'noop',
searchText: '',
tagType: '',
},
{
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
},
true,
);
const queryFilters = useMemo(
() =>
currentQuery?.builder?.queryData[0]?.filters || {
items: [],
op: 'and',
},
[currentQuery?.builder?.queryData],
);
const query = useMemo(() => {
const baseQuery = getK8sDeploymentsListQuery();
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 formattedGroupedByDeploymentsData = useMemo(
() =>
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
[groupedByRowData, groupBy],
);
const { data, isFetching, isLoading, isError } = useGetK8sDeploymentsList(
query as K8sDeploymentsListPayload,
{
queryKey: ['deploymentList', query],
enabled: !!query,
},
);
const deploymentsData = useMemo(() => data?.payload?.data?.records || [], [
data,
]);
const totalCount = data?.payload?.data?.total || 0;
const formattedDeploymentsData = useMemo(
() => formatDataForTable(deploymentsData, groupBy),
[deploymentsData, groupBy],
);
const columns = useMemo(() => getK8sDeploymentsListColumns(groupBy), [
groupBy,
]);
const handleGroupByRowClick = (record: K8sDeploymentsRowData): 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<K8sDeploymentsRowData>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
_filters: Record<string, (string | number | boolean)[] | null>,
sorter:
| SorterResult<K8sDeploymentsRowData>
| SorterResult<K8sDeploymentsRowData>[],
): 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 selectedDeploymentData = useMemo(() => {
// if (!selectedDeploymentUID) return null;
// return deploymentsData.find((deployment) => deployment.deploymentUID === selectedDeploymentUID) || null;
// }, [selectedDeploymentUID, deploymentsData]);
const handleRowClick = (record: K8sDeploymentsRowData): void => {
if (groupBy.length === 0) {
setSelectedRowData(null);
// setselectedDeploymentUID(record.deploymentUID);
} else {
handleGroupByRowClick(record);
}
logEvent('Infra Monitoring: K8s deployment list item clicked', {
deploymentUID: record.deploymentName,
});
};
const nestedColumns = useMemo(() => {
const nestedColumns = getK8sDeploymentsListColumns([]);
return [dummyColumnConfig, ...nestedColumns];
}, []);
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<K8sDeploymentsRowData>[]}
dataSource={formattedGroupedByDeploymentsData}
pagination={false}
scroll={{ x: true }}
tableLayout="fixed"
size="small"
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}
>
View All
</Button>
</div>
) : null}
</div>
)}
</div>
);
const expandRowIconRenderer = ({
expanded,
onExpand,
record,
}: {
expanded: boolean;
onExpand: (
record: K8sDeploymentsRowData,
e: React.MouseEvent<HTMLButtonElement>,
) => void;
record: K8sDeploymentsRowData;
}): 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 handleCloseDeploymentDetail = (): void => {
// setselectedDeploymentUID(null);
// };
const showsDeploymentsTable =
!isError &&
!isLoading &&
!isFetching &&
!(formattedDeploymentsData.length === 0 && queryFilters.items.length > 0);
const showNoFilteredDeploymentsMessage =
!isFetching &&
!isLoading &&
formattedDeploymentsData.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}
/>
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
{showNoFilteredDeploymentsMessage && (
<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 />}
{showsDeploymentsTable && (
<Table
className="k8s-list-table"
dataSource={isFetching || isLoading ? [] : formattedDeploymentsData}
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,
}}
/>
)}
{/* TODO - Handle Deployment Details flow */}
</div>
);
}
export default K8sDeploymentsList;

View File

@@ -0,0 +1,337 @@
import { Color } from '@signozhq/design-tokens';
import { Progress, Tag } from 'antd';
import { ColumnType } from 'antd/es/table';
import {
K8sDeploymentsData,
K8sDeploymentsListPayload,
} from 'api/infraMonitoring/getK8sDeploymentsList';
import { Group } from 'lucide-react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { IEntityColumn } from '../utils';
export const defaultAddedColumns: IEntityColumn[] = [
{
label: 'Deployment Name',
value: 'deploymentName',
id: 'deploymentName',
canRemove: false,
},
{
label: 'Namespace Name',
value: 'namespaceName',
id: 'namespaceName',
canRemove: false,
},
{
label: 'Available',
value: 'available',
id: 'available',
canRemove: false,
},
{
label: 'Desired',
value: 'desired',
id: 'desired',
canRemove: false,
},
{
label: 'CPU Request Utilization (% of limit)',
value: 'cpuRequestUtilization',
id: 'cpuRequestUtilization',
canRemove: false,
},
{
label: 'CPU Limit Utilization (% of request)',
value: 'cpuLimitUtilization',
id: 'cpuLimitUtilization',
canRemove: false,
},
{
label: 'CPU Utilization (cores)',
value: 'cpuUtilization',
id: 'cpuUtilization',
canRemove: false,
},
{
label: 'Memory Request Utilization (% of limit)',
value: 'memoryRequestUtilization',
id: 'memoryRequestUtilization',
canRemove: false,
},
{
label: 'Memory Limit Utilization (% of request)',
value: 'memoryLimitUtilization',
id: 'memoryLimitUtilization',
canRemove: false,
},
{
label: 'Memory Utilization (bytes)',
value: 'memoryUtilization',
id: 'memoryUtilization',
canRemove: false,
},
];
export interface K8sDeploymentsRowData {
key: string;
deploymentName: string;
availableReplicas: number;
desiredReplicas: number;
cpuRequestUtilization: React.ReactNode;
cpuLimitUtilization: React.ReactNode;
cpuUtilization: number;
memoryRequestUtilization: React.ReactNode;
memoryLimitUtilization: React.ReactNode;
memoryUtilization: number;
containerRestarts: number;
clusterName: string;
namespaceName: string;
groupedByMeta?: any;
}
const deploymentGroupColumnConfig = {
title: (
<div className="column-header pod-group-header">
<Group size={14} /> DEPLOYMENT GROUP
</div>
),
dataIndex: 'deploymentGroup',
key: 'deploymentGroup',
ellipsis: true,
width: 150,
align: 'left',
sorter: false,
};
export const getK8sDeploymentsListQuery = (): K8sDeploymentsListPayload => ({
filters: {
items: [],
op: 'and',
},
orderBy: { columnName: 'cpu', order: 'desc' },
});
const columnsConfig = [
{
title: <div className="column-header-left">Deployment Name</div>,
dataIndex: 'deploymentName',
key: 'deploymentName',
ellipsis: true,
width: 150,
sorter: true,
align: 'left',
},
{
title: <div className="column-header-left">Namespace Name</div>,
dataIndex: 'namespaceName',
key: 'namespaceName',
ellipsis: true,
width: 150,
sorter: true,
align: 'left',
},
{
title: <div className="column-header-left">Available</div>,
dataIndex: 'availableReplicas',
key: 'availableReplicas',
width: 100,
sorter: true,
align: 'left',
},
{
title: <div className="column-header-left">Desired</div>,
dataIndex: 'desiredReplicas',
key: 'desiredReplicas',
width: 80,
sorter: true,
align: 'left',
},
{
title: (
<div className="column-header-left">
CPU Request Utilization (% of limit)
</div>
),
dataIndex: 'cpuRequestUtilization',
key: 'cpuRequestUtilization',
width: 80,
sorter: true,
align: 'left',
},
{
title: (
<div className="column-header-left">
CPU Limit Utilization (% of request)
</div>
),
dataIndex: 'cpuLimitUtilization',
key: 'cpuLimitUtilization',
width: 50,
sorter: true,
align: 'left',
},
{
title: <div className="column-header-left">CPU Utilization (cores)</div>,
dataIndex: 'cpuUtilization',
key: 'cpuUtilization',
width: 80,
sorter: true,
align: 'left',
},
{
title: (
<div className="column-header-left">
Memory Request Utilization (% of limit)
</div>
),
dataIndex: 'memoryRequestUtilization',
key: 'memoryRequestUtilization',
width: 50,
sorter: true,
align: 'left',
},
{
title: (
<div className="column-header-left">
Memory Limit Utilization (% of request)
</div>
),
dataIndex: 'memoryLimitUtilization',
key: 'memoryLimitUtilization',
width: 80,
sorter: true,
align: 'left',
},
{
title: <div className="column-header-left">Memory Utilization (bytes)</div>,
dataIndex: 'memoryUtilization',
key: 'memoryUtilization',
width: 80,
sorter: true,
align: 'left',
},
];
export const getK8sDeploymentsListColumns = (
groupBy: IBuilderQuery['groupBy'],
): ColumnType<K8sDeploymentsRowData>[] => {
if (groupBy.length > 0) {
const filteredColumns = [...columnsConfig].filter(
(column) => column.key !== 'deploymentName',
);
filteredColumns.unshift(deploymentGroupColumnConfig);
return filteredColumns as ColumnType<K8sDeploymentsRowData>[];
}
return columnsConfig as ColumnType<K8sDeploymentsRowData>[];
};
const getGroupByEle = (
deployment: K8sDeploymentsData,
groupBy: IBuilderQuery['groupBy'],
): React.ReactNode => {
const groupByValues: string[] = [];
groupBy.forEach((group) => {
groupByValues.push(
deployment.meta[group.key as keyof typeof deployment.meta],
);
});
return (
<div className="pod-group">
{groupByValues.map((value) => (
<Tag key={value} color="#1D212D" className="pod-group-tag-item">
{value === '' ? '<no-value>' : value}
</Tag>
))}
</div>
);
};
const getStrokeColorForProgressBar = (value: number): string => {
if (value >= 90) return Color.BG_SAKURA_500;
if (value >= 60) return Color.BG_AMBER_500;
return Color.BG_FOREST_500;
};
export const formatDataForTable = (
data: K8sDeploymentsData[],
groupBy: IBuilderQuery['groupBy'],
): K8sDeploymentsRowData[] =>
data.map((deployment, index) => ({
key: `${deployment.meta.k8s_deployment_name}-${index}`,
deploymentName: deployment.meta.k8s_deployment_name,
availableReplicas: deployment.availablePods,
desiredReplicas: deployment.desiredPods,
containerRestarts: deployment.restarts,
cpuUtilization: deployment.cpuUsage,
cpuRequestUtilization: (
<div className="progress-container">
<Progress
percent={Number((deployment.cpuRequest * 100).toFixed(1))}
strokeLinecap="butt"
size="small"
status="active"
strokeColor={((): string => {
const cpuPercent = Number((deployment.cpuRequest * 100).toFixed(1));
return getStrokeColorForProgressBar(cpuPercent);
})()}
className="progress-bar"
/>
</div>
),
cpuLimitUtilization: (
<div className="progress-container">
<Progress
percent={Number((deployment.cpuLimit * 100).toFixed(1))}
strokeLinecap="butt"
size="small"
status="active"
strokeColor={((): string => {
const cpuPercent = Number((deployment.cpuLimit * 100).toFixed(1));
return getStrokeColorForProgressBar(cpuPercent);
})()}
className="progress-bar"
/>
</div>
),
memoryUtilization: deployment.memoryUsage,
memoryRequestUtilization: (
<div className="progress-container">
<Progress
percent={Number((deployment.memoryRequest * 100).toFixed(1))}
strokeLinecap="butt"
size="small"
status="active"
strokeColor={((): string => {
const memoryPercent = Number((deployment.memoryRequest * 100).toFixed(1));
return getStrokeColorForProgressBar(memoryPercent);
})()}
className="progress-bar"
/>
</div>
),
memoryLimitUtilization: (
<div className="progress-container">
<Progress
percent={Number((deployment.memoryLimit * 100).toFixed(1))}
strokeLinecap="butt"
size="small"
status="active"
strokeColor={((): string => {
const memoryPercent = Number((deployment.memoryLimit * 100).toFixed(1));
return getStrokeColorForProgressBar(memoryPercent);
})()}
className="progress-bar"
/>
</div>
),
clusterName: deployment.meta.k8s_cluster_name,
namespaceName: deployment.meta.k8s_namespace_name,
deploymentGroup: getGroupByEle(deployment, groupBy),
meta: deployment.meta,
...deployment.meta,
groupedByMeta: deployment.meta,
}));

View File

@@ -0,0 +1,673 @@
.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;
&.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 {
.pod-group {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
min-width: 120px;
max-width: 120px;
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 {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
min-width: 120px;
max-width: 120px;
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(--Vanilla-400, #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(--Slate-400, #1d212d);
border-right: none;
background: var(--Ink-300, #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 {
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;
}
.progress-bar {
flex: 1;
margin-right: 8px;
margin-bottom: 0px;
}
.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(--Vanilla-100, #fff);
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: 8px;
&::-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;
padding: 8px;
padding-left: 56px;
margin-top: 8px;
.periscope-btn {
font-size: 11px;
}
}
}
}
.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;
}
}
}
}
}
}

View File

@@ -0,0 +1,375 @@
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 {
ArrowUpDown,
Bolt,
Boxes,
Computer,
Container,
FilePenLine,
Group,
HardDrive,
PackageOpen,
Workflow,
} from 'lucide-react';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useCallback, useState } from 'react';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import {
ClustersQuickFiltersConfig,
ContainersQuickFiltersConfig,
DaemonSetsQuickFiltersConfig,
DeploymentsQuickFiltersConfig,
JobsQuickFiltersConfig,
K8sCategories,
NamespaceQuickFiltersConfig,
NodesQuickFiltersConfig,
PodsQuickFiltersConfig,
StatefulsetsQuickFiltersConfig,
VolumesQuickFiltersConfig,
} from './constants';
import K8sDeploymentsList from './Deployments/K8sDeploymentsList';
import K8sPodLists from './Pods/K8sPodLists';
import Volumes from './Volumes/Volumes';
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>
<Typography.Text> ({PodsQuickFiltersConfig.length}) </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>
<Typography.Text> ({NodesQuickFiltersConfig.length}) </Typography.Text>
</div>
</div>
),
key: K8sCategories.NODES,
showArrow: false,
children: (
<QuickFilters
source="infra-monitoring"
config={NodesQuickFiltersConfig}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
{
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>
<Typography.Text>
{' '}
({NamespaceQuickFiltersConfig.length}){' '}
</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>
<Typography.Text> ({ClustersQuickFiltersConfig.length}) </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>
<Typography.Text>
{' '}
({ContainersQuickFiltersConfig.length}){' '}
</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>
<Typography.Text> ({VolumesQuickFiltersConfig.length}) </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>
<Typography.Text>
{' '}
({DeploymentsQuickFiltersConfig.length}){' '}
</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>
<Typography.Text> ({JobsQuickFiltersConfig.length}) </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>
<Typography.Text>
{' '}
({DaemonSetsQuickFiltersConfig.length}){' '}
</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>
<Typography.Text>
{' '}
({StatefulsetsQuickFiltersConfig.length}){' '}
</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.DEPLOYMENTS && (
<K8sDeploymentsList
isFiltersVisible={showFilters}
handleFilterVisibilityChange={handleFilterVisibilityChange}
/>
)}
{selectedCategory === K8sCategories.VOLUMES && <Volumes />}
</div>
</div>
</div>
</Sentry.ErrorBoundary>
);
}

View File

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

View File

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

View File

@@ -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;

View File

@@ -0,0 +1,161 @@
/* 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 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;
}
function K8sHeader({
selectedGroupBy,
defaultAddedColumns,
groupByOptions,
isLoadingGroupByFilters,
addedColumns,
availableColumns,
handleFiltersChange,
handleGroupByChange,
onAddColumn,
onRemoveColumn,
handleFilterVisibilityChange,
isFiltersVisible,
}: 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
/>
</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;

View File

@@ -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;

View File

@@ -0,0 +1,561 @@
/* 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 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 } 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 {
getFromLocalStorage,
updateLocalStorage,
} from 'utils/localStorageReadWrite';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import {
defaultAddedColumns,
defaultAvailableColumns,
dummyColumnConfig,
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: '',
aggregateOperator: 'noop',
searchText: '',
tagType: '',
},
{
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
},
true,
);
useEffect(() => {
const addedColumns = getFromLocalStorage('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,
},
op: '=',
value: groupedByMeta[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);
updateLocalStorage('k8sPodsAddedColumns', 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(() => {
const nestedColumns = getK8sPodsListColumns(addedColumns, []);
return [dummyColumnConfig, ...nestedColumns];
}, [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"
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}
>
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}
/>
{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,
}}
/>
)}
<PodDetails
pod={selectedPodData}
isModalTimeSelection
onClose={handleClosePodDetail}
/>
</div>
);
}
export default K8sPodsList;

View File

@@ -0,0 +1,289 @@
.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(--Vanilla-100, #fff);
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 - 272px) !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;
}
}
.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;
}

View File

@@ -0,0 +1,259 @@
import './Events.styles.scss';
import { Table, TableColumnsType, Typography } from 'antd';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
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 { ChevronDown, ChevronRight } from 'lucide-react';
import { useMemo } from 'react';
import { useQuery } from 'react-query';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
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;
}
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;
}
export default function Events({
timeRange,
handleChangeLogFilters,
filters,
isModalTimeSelection,
handleTimeChange,
selectedInterval,
}: IPodEventsProps): 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,
},
},
],
},
}),
[currentQuery],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
// const [restFilters, setRestFilters] = useState<TagFilterItem[]>([]);
// const [resetLogsList, setResetLogsList] = useState<boolean>(false);
// useEffect(() => {
// const newRestFilters = filters?.items?.filter(
// (item) =>
// item.key?.key !== 'id' &&
// item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME &&
// item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND,
// );
// 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 = getPodsEventsQueryPayload(
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 { data, isLoading, 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' },
{ title: 'Timestamp', dataIndex: 'timestamp', key: 'timestamp' },
{ title: 'Body', dataIndex: 'body', key: 'body' },
];
const formattedPodEvents = useMemo(() => {
const responsePayload =
data?.payload.data.newResult.data.result[0].list || [];
const formattedData = responsePayload?.map((event) => ({
timestamp: event.timestamp,
severity: event.data.severity_text,
body: event.data.body,
id: event.data.id,
key: event.data.id,
}));
return formattedData || [];
}, [data]);
const handleExpandRow = (record: EventDataType): JSX.Element => {
console.log('record', record);
return <p style={{ margin: 0 }}>{record.body}</p>;
};
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 && (
<div className="loading-logs">
<div className="loading-logs-content">
<img
className="loading-gif"
src="/Icons/loading-plane.gif"
alt="wait-icon"
/>
<Typography>Loading Events. Please wait...</Typography>
</div>
</div>
)}
{!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>
columns={columns}
expandable={{
expandedRowRender: handleExpandRow,
rowExpandable: (record): boolean => record.body !== 'Not Expandable',
expandIcon: handleExpandRowIcon,
}}
dataSource={formattedPodEvents}
pagination={false}
/>
</div>
</div>
)}
</div>
);
}

View 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>
);
}

View 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,
});

View 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%);
}
}

View 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;

View File

@@ -0,0 +1,7 @@
import { K8sPodsData } from 'api/infraMonitoring/getK8sPodsList';
export type PodDetailProps = {
pod: K8sPodsData | null;
isModalTimeSelection: boolean;
onClose: () => void;
};

View 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);
}
}
}
}

View File

@@ -0,0 +1,555 @@
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 { 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, VIEW_TYPES, VIEWS } 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';
// 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) / 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_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 || '',
},
],
}),
[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 / 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: 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 hostNameFilter = prevFilters.items.find(
(item) => item.key?.key === QUERY_KEYS.K8S_POD_NAME,
);
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_POD_NAME,
);
logEvent('Infra Monitoring: Pods list details logs filters applied', {
pod: pod?.podUID,
});
return {
op: 'AND',
items: [
hostNameFilter,
...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 hostNameFilter = prevFilters.items.find(
(item) => item.key?.key === QUERY_KEYS.K8S_POD_NAME,
);
logEvent('Infra Monitoring: Pods list details traces filters applied', {
pod: pod?.podUID,
});
return {
op: 'AND',
items: [
hostNameFilter,
...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 / 1000000000),
endTime: Math.floor(maxTime / 1000000000),
});
}
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;

View 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>
);
}

View 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);
}
}
}

View File

@@ -0,0 +1,213 @@
/* 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' && item.key?.key !== QUERY_KEYS.K8S_POD_NAME,
);
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;

View File

@@ -0,0 +1,95 @@
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,
},
},
],
},
}),
[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;

View 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,
});

View 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);
}
}
}
}

View File

@@ -0,0 +1,195 @@
import './PodTraces.styles.scss';
import { ResizeTable } from 'components/ResizeTable';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { QueryParams } from 'constants/query';
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
import NoLogs from 'container/NoLogs/NoLogs';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { ErrorText } from 'container/TimeSeriesView/styles';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import TraceExplorerControls from 'container/TracesExplorer/Controls';
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Pagination } from 'hooks/queryPagination';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { getPodTracesQueryPayload, selectedColumns } from './constants';
import { getListColumns } from './utils';
interface Props {
timeRange: {
startTime: number;
endTime: number;
};
isModalTimeSelection: boolean;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
handleChangeTracesFilters: (value: IBuilderQuery['filters']) => void;
tracesFilters: IBuilderQuery['filters'];
selectedInterval: Time;
}
function 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,
},
},
],
},
}),
[currentQuery],
);
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;

View 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,
},
],
},
});

View File

@@ -0,0 +1,84 @@
import { Tag, Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import {
BlockLink,
getTraceLink,
} from 'container/TracesExplorer/ListView/utils';
import dayjs from 'dayjs';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
const keyToLabelMap: Record<string, string> = {
timestamp: 'Timestamp',
serviceName: 'Service Name',
name: 'Name',
durationNano: 'Duration',
httpMethod: 'HTTP Method',
responseStatusCode: 'Status Code',
};
export const getListColumns = (
selectedColumns: BaseAutocompleteData[],
): ColumnsType<RowData> => {
const columns: ColumnsType<RowData> =
selectedColumns.map(({ dataType, key, type }) => ({
title: keyToLabelMap[key],
dataIndex: key,
key: `${key}-${dataType}-${type}`,
width: 145,
render: (value, item): JSX.Element => {
const itemData = item.data as any;
if (key === 'timestamp') {
const date =
typeof value === 'string'
? dayjs(value).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(value / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
return (
<BlockLink to={getTraceLink(item)} openInNewTab>
<Typography.Text>{date}</Typography.Text>
</BlockLink>
);
}
if (value === '') {
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Typography data-testid={key}>N/A</Typography>
</BlockLink>
);
}
if (key === 'httpMethod' || key === 'responseStatusCode') {
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Tag data-testid={key} color="magenta">
{itemData[key]}
</Tag>
</BlockLink>
);
}
if (key === 'durationNano') {
const durationNano = itemData[key];
return (
<BlockLink to={getTraceLink(item)} openInNewTab>
<Typography data-testid={key}>{getMs(durationNano)}ms</Typography>
</BlockLink>
);
}
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Typography data-testid={key}>{itemData[key]}</Typography>
</BlockLink>
);
},
responsive: ['md'],
})) || [];
return columns;
};

View File

@@ -0,0 +1,19 @@
export enum VIEWS {
METRICS = 'metrics',
LOGS = 'logs',
TRACES = 'traces',
EVENTS = 'events',
}
export const VIEW_TYPES = {
METRICS: VIEWS.METRICS,
LOGS: VIEWS.LOGS,
TRACES: VIEWS.TRACES,
EVENTS: VIEWS.EVENTS,
};
export const QUERY_KEYS = {
K8S_OBJECT_KIND: 'k8s.object.kind',
K8S_OBJECT_NAME: 'k8s.object.name',
K8S_POD_NAME: 'k8s.pod.name',
};

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
export default function Volumes(): JSX.Element {
return <div>Volumes</div>;
}

View File

@@ -0,0 +1,340 @@
import {
FiltersType,
IQuickFiltersConfig,
} from 'components/QuickFilters/QuickFilters';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
export const K8sCategories = {
PODS: 'pods',
NODES: 'nodes',
NAMESPACES: 'namespaces',
CLUSTERS: 'clusters',
DEPLOYMENTS: 'deployments',
STATEFULSETS: 'statefulsets',
DAEMONSETS: 'daemonsets',
CONTAINERS: 'containers',
JOBS: 'jobs',
VOLUMES: 'volumes',
};
export const PodsQuickFiltersConfig: IQuickFiltersConfig[] = [
{
type: FiltersType.CHECKBOX,
title: 'Pods',
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: 'Nodes',
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: 'Deployments',
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: 'Statefulsets',
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: 'DaemonSets',
attributeKey: {
key: 'k8s_daemonset_name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
defaultOpen: false,
},
{
type: FiltersType.CHECKBOX,
title: 'Jobs',
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,
},
{
type: FiltersType.CHECKBOX,
title: 'Volumes',
attributeKey: {
key: 'k8s_volume_name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
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,
// id: 'k8s_pod_name--string--tag--true',
},
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: 'Clusters',
attributeKey: {
key: 'k8s.cluster.name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
defaultOpen: true,
},
];
export const ContainersQuickFiltersConfig: IQuickFiltersConfig[] = [
{
type: FiltersType.CHECKBOX,
title: 'Containers',
attributeKey: {
key: 'k8s_container_name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
defaultOpen: true,
},
];
export const VolumesQuickFiltersConfig: IQuickFiltersConfig[] = [
{
type: FiltersType.CHECKBOX,
title: 'Volumes',
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 Name',
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: true,
},
{
type: FiltersType.CHECKBOX,
title: 'Namespace Name',
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: 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 StatefulsetsQuickFiltersConfig: IQuickFiltersConfig[] = [
{
type: FiltersType.CHECKBOX,
title: 'Statefulsets',
attributeKey: {
key: 'k8s_statefulset_name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
defaultOpen: true,
},
];
export const DaemonSetsQuickFiltersConfig: IQuickFiltersConfig[] = [
{
type: FiltersType.CHECKBOX,
title: 'DaemonSets',
attributeKey: {
key: 'k8s_daemonset_name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
defaultOpen: true,
},
];
export const JobsQuickFiltersConfig: IQuickFiltersConfig[] = [
{
type: FiltersType.CHECKBOX,
title: 'Jobs',
attributeKey: {
key: 'k8s_job_name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
defaultOpen: true,
},
];

View File

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

View File

@@ -0,0 +1,427 @@
/* eslint-disable sonarjs/cognitive-complexity */
import './InfraMonitoringK8s.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Progress, 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';
export interface IEntityColumn {
label: string;
value: string;
id: string;
canRemove: boolean;
}
export interface IPodColumn {
label: string;
value: string;
id: string;
canRemove: boolean;
}
export const defaultAddedColumns: IPodColumn[] = [
{
label: 'Pod name',
value: 'podName',
id: 'podName',
canRemove: false,
},
{
label: 'CPU Request Utilization (% of limit)',
value: 'cpuRequestUtilization',
id: 'cpuRequestUtilization',
canRemove: false,
},
{
label: 'CPU Limit Utilization (% of request)',
value: 'cpuLimitUtilization',
id: 'cpuLimitUtilization',
canRemove: false,
},
{
label: 'CPU Utilization (cores)',
value: 'cpuUtilization',
id: 'cpuUtilization',
canRemove: false,
},
{
label: 'Memory Request Utilization (% of limit)',
value: 'memoryRequestUtilization',
id: 'memoryRequestUtilization',
canRemove: false,
},
{
label: 'Memory Limit Utilization (% of request)',
value: 'memoryLimitUtilization',
id: 'memoryLimitUtilization',
canRemove: false,
},
{
label: 'Memory Utilization (bytes)',
value: 'memoryUtilization',
id: 'memoryUtilization',
canRemove: false,
},
{
label: 'Container Restarts',
value: 'containerRestarts',
id: 'containerRestarts',
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;
cpuRequestUtilization: React.ReactNode;
cpuLimitUtilization: React.ReactNode;
cpuUtilization: React.ReactNode;
memoryRequestUtilization: React.ReactNode;
memoryLimitUtilization: React.ReactNode;
memoryUtilization: React.ReactNode;
containerRestarts: number;
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: {
showTitle: false,
},
width: 120,
align: 'left',
sorter: false,
};
export const dummyColumnConfig = {
title: <div className="column-header dummy-column">&nbsp;</div>,
dataIndex: 'dummy',
key: 'dummy',
width: 25,
sorter: false,
align: 'left',
};
const columnsConfig = [
{
title: <div className="column-header pod-name-header">Pod Name</div>,
dataIndex: 'podName',
key: 'podName',
ellipsis: {
showTitle: false,
},
width: 120,
sorter: true,
align: 'left',
},
{
title: (
<div className="column-header">
<Tooltip title="CPU Request Utilization (% of limit)">CPU Req Util</Tooltip>
</div>
),
dataIndex: 'cpuRequestUtilization',
key: 'cpuRequestUtilization',
width: 150,
sorter: true,
align: 'left',
},
{
title: (
<div className="column-header">
<Tooltip title="CPU Limit Utilization (% of request)">
CPU Limit Util
</Tooltip>
</div>
),
dataIndex: 'cpuLimitUtilization',
key: 'cpuLimitUtilization',
width: 150,
sorter: true,
align: 'left',
},
{
title: (
<div className="column-header">
<Tooltip title="CPU Utilization (cores)">CPU Util (cores)</Tooltip>
</div>
),
dataIndex: 'cpuUtilization',
key: 'cpuUtilization',
width: 80,
sorter: true,
align: 'left',
},
{
title: (
<div className="column-header">
<Tooltip title="Memory Request Utilization (% of limit)">
Mem Req Util
</Tooltip>
</div>
),
dataIndex: 'memoryRequestUtilization',
key: 'memoryRequestUtilization',
width: 150,
sorter: true,
align: 'left',
},
{
title: (
<div className="column-header">
<Tooltip title="Memory Limit Utilization (% of request)">
Mem Limit Util
</Tooltip>
</div>
),
dataIndex: 'memoryLimitUtilization',
key: 'memoryLimitUtilization',
width: 150,
sorter: true,
align: 'left',
},
{
title: (
<div className="column-header">
<Tooltip title="Memory Utilization (bytes)">Mem Util (bytes)</Tooltip>
</div>
),
dataIndex: 'memoryUtilization',
key: 'memoryUtilization',
width: 100,
ellipsis: true,
sorter: true,
align: 'left',
},
{
title: (
<div className="column-header">
<Tooltip title="Container Restarts">Container Restarts</Tooltip>
</div>
),
dataIndex: 'containerRestarts',
key: 'containerRestarts',
width: 100,
ellipsis: true,
sorter: true,
align: 'left',
},
];
export const namespaceColumnConfig = {
title: <div className="column-header">Namespace</div>,
dataIndex: 'namespace',
key: 'namespace',
width: 100,
sorter: true,
ellipsis: true,
align: 'left',
};
export const nodeColumnConfig = {
title: <div className="column-header">Node</div>,
dataIndex: 'node',
key: 'node',
width: 100,
sorter: true,
ellipsis: true,
align: 'left',
};
export const clusterColumnConfig = {
title: <div className="column-header">Cluster</div>,
dataIndex: 'cluster',
key: 'cluster',
width: 100,
sorter: true,
ellipsis: true,
align: 'left',
};
export const columnConfigMap = {
namespace: namespaceColumnConfig,
node: nodeColumnConfig,
cluster: clusterColumnConfig,
};
export const getK8sPodsListColumns = (
addedColumns: IPodColumn[],
groupBy: IBuilderQuery['groupBy'],
): ColumnType<K8sPodsRowData>[] => {
if (groupBy.length > 0) {
const filteredColumns = [...columnsConfig].filter(
(column) => column.key !== 'podName',
);
filteredColumns.unshift(podGroupColumnConfig);
return filteredColumns as 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);
}
}
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="#1D212D" 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: (
<div className="pod-name-container">
<div className="pod-name">{pod.meta.k8s_pod_name || ''}</div>
</div>
),
podUID: pod.podUID || '',
cpuRequestUtilization: (
<div className="progress-container">
<Progress
percent={Number((pod.podCPURequest * 100).toFixed(1))}
strokeLinecap="butt"
size="small"
status="active"
strokeColor={((): string => {
const cpuPercent = Number((pod.podCPURequest * 100).toFixed(1));
if (cpuPercent >= 90) return Color.BG_SAKURA_500;
if (cpuPercent >= 60) return Color.BG_AMBER_500;
return Color.BG_FOREST_500;
})()}
className="progress-bar"
/>
</div>
),
cpuLimitUtilization: (
<div className="progress-container">
<Progress
percent={Number((pod.podCPULimit * 100).toFixed(1))}
strokeLinecap="butt"
size="small"
status="active"
strokeColor={((): string => {
const cpuPercent = Number((pod.podCPULimit * 100).toFixed(1));
if (cpuPercent >= 90) return Color.BG_SAKURA_500;
if (cpuPercent >= 60) return Color.BG_AMBER_500;
return Color.BG_FOREST_500;
})()}
className="progress-bar"
/>
</div>
),
cpuUtilization: pod.podCPU,
memoryRequestUtilization: (
<div className="progress-container">
<Progress
percent={Number((pod.podMemoryRequest * 100).toFixed(1))}
strokeLinecap="butt"
size="small"
status="active"
strokeColor={((): string => {
const memoryPercent = Number((pod.podMemoryRequest * 100).toFixed(1));
if (memoryPercent >= 90) return Color.BG_SAKURA_500;
if (memoryPercent >= 60) return Color.BG_AMBER_500;
return Color.BG_FOREST_500;
})()}
className="progress-bar"
/>
</div>
),
memoryLimitUtilization: (
<div className="progress-container">
<Progress
percent={Number((pod.podMemoryLimit * 100).toFixed(1))}
strokeLinecap="butt"
size="small"
status="active"
strokeColor={((): string => {
const memoryPercent = Number((pod.podMemoryLimit * 100).toFixed(1));
if (memoryPercent >= 90) return Color.BG_SAKURA_500;
if (memoryPercent >= 60) return Color.BG_AMBER_500;
return Color.BG_FOREST_500;
})()}
className="progress-bar"
/>
</div>
),
memoryUtilization: pod.podMemory,
containerRestarts: pod.restartCount,
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,
}));

View File

@@ -51,4 +51,8 @@ export const routeConfig: Record<string, QueryParams[]> = {
[ROUTES.WORKSPACE_LOCKED]: [QueryParams.resourceAttributes],
[ROUTES.MESSAGING_QUEUES]: [QueryParams.resourceAttributes],
[ROUTES.MESSAGING_QUEUES_DETAIL]: [QueryParams.resourceAttributes],
[ROUTES.INFRASTRUCTURE_MONITORING_HOSTS]: [QueryParams.resourceAttributes],
[ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES]: [
QueryParams.resourceAttributes,
],
};

View File

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

View File

@@ -0,0 +1,48 @@
import {
getK8sDeploymentsList,
K8sDeploymentsListPayload,
K8sDeploymentsListResponse,
} from 'api/infraMonitoring/getK8sDeploymentsList';
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 UseGetK8sDeploymentsList = (
requestData: K8sDeploymentsListPayload,
options?: UseQueryOptions<
SuccessResponse<K8sDeploymentsListResponse> | ErrorResponse,
Error
>,
headers?: Record<string, string>,
) => UseQueryResult<
SuccessResponse<K8sDeploymentsListResponse> | ErrorResponse,
Error
>;
export const useGetK8sDeploymentsList: UseGetK8sDeploymentsList = (
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<K8sDeploymentsListResponse> | ErrorResponse,
Error
>({
queryFn: ({ signal }) => getK8sDeploymentsList(requestData, signal, headers),
...options,
queryKey,
});
};

View 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_HOST_LIST, requestData];
}, [options?.queryKey, requestData]);
return useQuery<SuccessResponse<K8sPodsListResponse> | ErrorResponse, Error>({
queryFn: ({ signal }) => getK8sPodsList(requestData, signal, headers),
...options,
queryKey,
});
};

View File

@@ -2,6 +2,11 @@
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
.ant-tabs {
height: 100%;
}
.ant-tabs-nav {
padding: 0 8px;

View File

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

View File

@@ -1,6 +1,7 @@
import { TabRoutes } from 'components/RouteTab/types';
import ROUTES from 'constants/routes';
import InfraMonitoringHosts from 'container/InfraMonitoringHosts';
import InfraMonitoringK8s from 'container/InfraMonitoringK8s';
import { Inbox } from 'lucide-react';
export const Hosts: TabRoutes = {
@@ -13,3 +14,14 @@ export const Hosts: TabRoutes = {
route: 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,
};

View File

@@ -44,6 +44,13 @@
gap: 8px;
}
.periscope-input {
&.borderless {
border: none;
border-radius: 0;
}
}
.lightMode {
.periscope-btn {
border-color: var(--bg-vanilla-300);

View File

@@ -16,10 +16,10 @@ export type AutocompleteType = 'tag' | 'resource' | '';
export interface BaseAutocompleteData {
id?: string;
dataType: DataTypes;
isColumn: boolean;
dataType?: DataTypes;
isColumn?: boolean;
key: string;
type: AutocompleteType | string | null;
type?: AutocompleteType | string | null;
isJSON?: boolean;
isIndexed?: boolean;
}

View File

@@ -21,7 +21,7 @@ export interface IBuilderFormula {
}
export interface TagFilterItem {
id: string;
id?: string;
key?: BaseAutocompleteData;
op: string;
value: string[] | string | number | boolean;

View File

@@ -0,0 +1,17 @@
export const updateLocalStorage = (key: string, value: any): void => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(`Failed to update ${key} in localStorage`, error);
}
};
export const getFromLocalStorage = (key: string): any => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (error) {
console.error(`Failed to retrieve ${key} from localStorage`, error);
return null;
}
};

View File

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