mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-07 20:50:26 +01:00
Compare commits
52 Commits
nv/4310
...
feat/migra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e32b9577d | ||
|
|
65e649b088 | ||
|
|
54a7e7aa82 | ||
|
|
72ce8768b3 | ||
|
|
cbfd8a111f | ||
|
|
910bb7e464 | ||
|
|
7c1fe82043 | ||
|
|
d4dbbceab7 | ||
|
|
4d6bc77773 | ||
|
|
47cabfbca1 | ||
|
|
e81e69bc39 | ||
|
|
9bdf2e166a | ||
|
|
bef6fb87b8 | ||
|
|
6150ac1b4c | ||
|
|
8e506a2f58 | ||
|
|
206775b5a3 | ||
|
|
918cf4dfe5 | ||
|
|
a91775b7cc | ||
|
|
09150e8b3c | ||
|
|
c729ed2637 | ||
|
|
2edde14f1c | ||
|
|
3aead98c9c | ||
|
|
7a9063b64f | ||
|
|
2be4e49215 | ||
|
|
1f50a3bd8b | ||
|
|
46a16a77ad | ||
|
|
43f7363e84 | ||
|
|
b4056956be | ||
|
|
8a8cc857f9 | ||
|
|
1ed468af21 | ||
|
|
dfb1dcd86a | ||
|
|
976d2d2f15 | ||
|
|
e8b5aef7c9 | ||
|
|
b0831ca7e2 | ||
|
|
4c12bebe00 | ||
|
|
b81217735e | ||
|
|
6a63fa5659 | ||
|
|
6fa1a4dd0d | ||
|
|
1d78dd9da0 | ||
|
|
f0193088d4 | ||
|
|
0e0febf9bb | ||
|
|
8247fcb4b9 | ||
|
|
d7f55f0950 | ||
|
|
8772b1808d | ||
|
|
db88491045 | ||
|
|
7ba97faf20 | ||
|
|
b5d46780d9 | ||
|
|
86e6e51c81 | ||
|
|
4ff7529856 | ||
|
|
eea9c0c586 | ||
|
|
d8273eab86 | ||
|
|
3a9c5daab8 |
@@ -54,7 +54,7 @@
|
||||
"@signozhq/checkbox": "0.0.2",
|
||||
"@signozhq/combobox": "0.0.2",
|
||||
"@signozhq/command": "0.0.0",
|
||||
"@signozhq/design-tokens": "2.1.1",
|
||||
"@signozhq/design-tokens": "2.1.4",
|
||||
"@signozhq/dialog": "^0.0.2",
|
||||
"@signozhq/drawer": "0.0.4",
|
||||
"@signozhq/icons": "0.1.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactChild, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ReactChild, useCallback, useMemo } from 'react';
|
||||
import { matchPath, Redirect, useLocation } from 'react-router-dom';
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
@@ -8,12 +8,10 @@ import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { ORG_PREFERENCES } from 'constants/orgPreferences';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import history from 'lib/history';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive';
|
||||
import { OrgPreference } from 'types/api/preferences/preference';
|
||||
import { Organization } from 'types/api/user/getOrganization';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { routePermission } from 'utils/permission';
|
||||
|
||||
@@ -25,6 +23,7 @@ import routes, {
|
||||
SUPPORT_ROUTE,
|
||||
} from './routes';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
const location = useLocation();
|
||||
const { pathname } = location;
|
||||
@@ -57,7 +56,12 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
const currentRoute = mapRoutes.get('current');
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const [orgData, setOrgData] = useState<Organization | undefined>(undefined);
|
||||
const orgData = useMemo(() => {
|
||||
if (org && org.length > 0 && org[0].id !== undefined) {
|
||||
return org[0];
|
||||
}
|
||||
return undefined;
|
||||
}, [org]);
|
||||
|
||||
const { data: usersData, isFetching: isFetchingUsers } = useListUsers({
|
||||
query: {
|
||||
@@ -75,214 +79,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
return remainingUsers.length === 1;
|
||||
}, [usersData?.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isCloudUserVal &&
|
||||
!isFetchingOrgPreferences &&
|
||||
orgPreferences &&
|
||||
!isFetchingUsers &&
|
||||
usersData &&
|
||||
usersData.data
|
||||
) {
|
||||
const isOnboardingComplete = orgPreferences?.find(
|
||||
(preference: OrgPreference) =>
|
||||
preference.name === ORG_PREFERENCES.ORG_ONBOARDING,
|
||||
)?.value;
|
||||
|
||||
// Don't redirect to onboarding if workspace has issues (blocked, suspended, or restricted)
|
||||
// User needs access to settings/billing to fix payment issues
|
||||
const isWorkspaceBlocked = trialInfo?.workSpaceBlock;
|
||||
const isWorkspaceSuspended = activeLicense?.state === LicenseState.DEFAULTED;
|
||||
const isWorkspaceAccessRestricted =
|
||||
activeLicense?.state === LicenseState.TERMINATED ||
|
||||
activeLicense?.state === LicenseState.EXPIRED ||
|
||||
activeLicense?.state === LicenseState.CANCELLED;
|
||||
|
||||
const hasWorkspaceIssue =
|
||||
isWorkspaceBlocked || isWorkspaceSuspended || isWorkspaceAccessRestricted;
|
||||
|
||||
if (hasWorkspaceIssue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isFirstUser = checkFirstTimeUser();
|
||||
if (
|
||||
isFirstUser &&
|
||||
!isOnboardingComplete &&
|
||||
// if the current route is allowed to be overriden by org onboarding then only do the same
|
||||
!ROUTES_NOT_TO_BE_OVERRIDEN.includes(pathname)
|
||||
) {
|
||||
history.push(ROUTES.ONBOARDING);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
checkFirstTimeUser,
|
||||
isCloudUserVal,
|
||||
isFetchingOrgPreferences,
|
||||
isFetchingUsers,
|
||||
orgPreferences,
|
||||
usersData,
|
||||
pathname,
|
||||
trialInfo?.workSpaceBlock,
|
||||
activeLicense?.state,
|
||||
]);
|
||||
|
||||
const navigateToWorkSpaceBlocked = useCallback((): void => {
|
||||
const isRouteEnabledForWorkspaceBlockedState =
|
||||
isAdmin &&
|
||||
(pathname === ROUTES.SETTINGS ||
|
||||
pathname === ROUTES.ORG_SETTINGS ||
|
||||
pathname === ROUTES.MEMBERS_SETTINGS ||
|
||||
pathname === ROUTES.BILLING ||
|
||||
pathname === ROUTES.MY_SETTINGS);
|
||||
|
||||
if (
|
||||
pathname &&
|
||||
pathname !== ROUTES.WORKSPACE_LOCKED &&
|
||||
!isRouteEnabledForWorkspaceBlockedState
|
||||
) {
|
||||
history.push(ROUTES.WORKSPACE_LOCKED);
|
||||
}
|
||||
}, [isAdmin, pathname]);
|
||||
|
||||
const navigateToWorkSpaceAccessRestricted = useCallback((): void => {
|
||||
if (pathname && pathname !== ROUTES.WORKSPACE_ACCESS_RESTRICTED) {
|
||||
history.push(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetchingActiveLicense && activeLicense) {
|
||||
const isTerminated = activeLicense.state === LicenseState.TERMINATED;
|
||||
const isExpired = activeLicense.state === LicenseState.EXPIRED;
|
||||
const isCancelled = activeLicense.state === LicenseState.CANCELLED;
|
||||
|
||||
const isWorkspaceAccessRestricted = isTerminated || isExpired || isCancelled;
|
||||
|
||||
const { platform } = activeLicense;
|
||||
|
||||
if (isWorkspaceAccessRestricted && platform === LicensePlatform.CLOUD) {
|
||||
navigateToWorkSpaceAccessRestricted();
|
||||
}
|
||||
}
|
||||
}, [
|
||||
isFetchingActiveLicense,
|
||||
activeLicense,
|
||||
navigateToWorkSpaceAccessRestricted,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetchingActiveLicense) {
|
||||
const shouldBlockWorkspace = trialInfo?.workSpaceBlock;
|
||||
|
||||
if (
|
||||
shouldBlockWorkspace &&
|
||||
activeLicense?.platform === LicensePlatform.CLOUD
|
||||
) {
|
||||
navigateToWorkSpaceBlocked();
|
||||
}
|
||||
}
|
||||
}, [
|
||||
isFetchingActiveLicense,
|
||||
trialInfo?.workSpaceBlock,
|
||||
activeLicense?.platform,
|
||||
navigateToWorkSpaceBlocked,
|
||||
]);
|
||||
|
||||
const navigateToWorkSpaceSuspended = useCallback((): void => {
|
||||
if (pathname && pathname !== ROUTES.WORKSPACE_SUSPENDED) {
|
||||
history.push(ROUTES.WORKSPACE_SUSPENDED);
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetchingActiveLicense && activeLicense) {
|
||||
const shouldSuspendWorkspace =
|
||||
activeLicense.state === LicenseState.DEFAULTED;
|
||||
|
||||
if (
|
||||
shouldSuspendWorkspace &&
|
||||
activeLicense.platform === LicensePlatform.CLOUD
|
||||
) {
|
||||
navigateToWorkSpaceSuspended();
|
||||
}
|
||||
}
|
||||
}, [isFetchingActiveLicense, activeLicense, navigateToWorkSpaceSuspended]);
|
||||
|
||||
useEffect(() => {
|
||||
if (org && org.length > 0 && org[0].id !== undefined) {
|
||||
setOrgData(org[0]);
|
||||
}
|
||||
}, [org]);
|
||||
|
||||
// if the feature flag is enabled and the current route is /get-started then redirect to /get-started-with-signoz-cloud
|
||||
useEffect(() => {
|
||||
if (
|
||||
currentRoute?.path === ROUTES.GET_STARTED &&
|
||||
featureFlags?.find((e) => e.name === FeatureKeys.ONBOARDING_V3)?.active
|
||||
) {
|
||||
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
|
||||
}
|
||||
}, [currentRoute, featureFlags]);
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
useEffect(() => {
|
||||
// if it is an old route navigate to the new route
|
||||
if (isOldRoute) {
|
||||
// this will be handled by the redirect component below
|
||||
return;
|
||||
}
|
||||
|
||||
// if the current route is public dashboard then don't redirect to login
|
||||
const isPublicDashboard = currentRoute?.path === ROUTES.PUBLIC_DASHBOARD;
|
||||
|
||||
if (isPublicDashboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the current route
|
||||
if (currentRoute) {
|
||||
const { isPrivate, key } = currentRoute;
|
||||
if (isPrivate) {
|
||||
if (isLoggedInState) {
|
||||
const route = routePermission[key];
|
||||
if (route && route.find((e) => e === user.role) === undefined) {
|
||||
history.push(ROUTES.UN_AUTHORIZED);
|
||||
}
|
||||
} else {
|
||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, pathname);
|
||||
history.push(ROUTES.LOGIN);
|
||||
}
|
||||
} else if (isLoggedInState) {
|
||||
const fromPathname = getLocalStorageApi(
|
||||
LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT,
|
||||
);
|
||||
if (fromPathname) {
|
||||
history.push(fromPathname);
|
||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, '');
|
||||
} else if (pathname !== ROUTES.SOMETHING_WENT_WRONG) {
|
||||
history.push(ROUTES.HOME);
|
||||
}
|
||||
} else {
|
||||
// do nothing as the unauthenticated routes are LOGIN and SIGNUP and the LOGIN container takes care of routing to signup if
|
||||
// setup is not completed
|
||||
}
|
||||
} else if (isLoggedInState) {
|
||||
const fromPathname = getLocalStorageApi(
|
||||
LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT,
|
||||
);
|
||||
if (fromPathname) {
|
||||
history.push(fromPathname);
|
||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, '');
|
||||
} else {
|
||||
history.push(ROUTES.HOME);
|
||||
}
|
||||
} else {
|
||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, pathname);
|
||||
history.push(ROUTES.LOGIN);
|
||||
}
|
||||
}, [isLoggedInState, pathname, user, isOldRoute, currentRoute, location]);
|
||||
|
||||
// Handle old routes - redirect to new routes
|
||||
if (isOldRoute) {
|
||||
const redirectUrl = oldNewRoutesMapping[pathname];
|
||||
return (
|
||||
@@ -296,7 +93,143 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: disabling this rule as there is no need to have div
|
||||
// Public dashboard - no redirect needed
|
||||
const isPublicDashboard = currentRoute?.path === ROUTES.PUBLIC_DASHBOARD;
|
||||
if (isPublicDashboard) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
// Check for workspace access restriction (cloud only)
|
||||
const isCloudPlatform = activeLicense?.platform === LicensePlatform.CLOUD;
|
||||
|
||||
if (!isFetchingActiveLicense && activeLicense && isCloudPlatform) {
|
||||
const isTerminated = activeLicense.state === LicenseState.TERMINATED;
|
||||
const isExpired = activeLicense.state === LicenseState.EXPIRED;
|
||||
const isCancelled = activeLicense.state === LicenseState.CANCELLED;
|
||||
const isWorkspaceAccessRestricted = isTerminated || isExpired || isCancelled;
|
||||
|
||||
if (
|
||||
isWorkspaceAccessRestricted &&
|
||||
pathname !== ROUTES.WORKSPACE_ACCESS_RESTRICTED
|
||||
) {
|
||||
return <Redirect to={ROUTES.WORKSPACE_ACCESS_RESTRICTED} />;
|
||||
}
|
||||
|
||||
// Check for workspace suspended (DEFAULTED)
|
||||
const shouldSuspendWorkspace = activeLicense.state === LicenseState.DEFAULTED;
|
||||
if (shouldSuspendWorkspace && pathname !== ROUTES.WORKSPACE_SUSPENDED) {
|
||||
return <Redirect to={ROUTES.WORKSPACE_SUSPENDED} />;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for workspace blocked (trial expired)
|
||||
if (!isFetchingActiveLicense && isCloudPlatform && trialInfo?.workSpaceBlock) {
|
||||
const isRouteEnabledForWorkspaceBlockedState =
|
||||
isAdmin &&
|
||||
(pathname === ROUTES.SETTINGS ||
|
||||
pathname === ROUTES.ORG_SETTINGS ||
|
||||
pathname === ROUTES.MEMBERS_SETTINGS ||
|
||||
pathname === ROUTES.BILLING ||
|
||||
pathname === ROUTES.MY_SETTINGS);
|
||||
|
||||
if (
|
||||
pathname !== ROUTES.WORKSPACE_LOCKED &&
|
||||
!isRouteEnabledForWorkspaceBlockedState
|
||||
) {
|
||||
return <Redirect to={ROUTES.WORKSPACE_LOCKED} />;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for onboarding redirect (cloud users, first user, onboarding not complete)
|
||||
if (
|
||||
isCloudUserVal &&
|
||||
!isFetchingOrgPreferences &&
|
||||
orgPreferences &&
|
||||
!isFetchingUsers &&
|
||||
usersData &&
|
||||
usersData.data
|
||||
) {
|
||||
const isOnboardingComplete = orgPreferences?.find(
|
||||
(preference: OrgPreference) =>
|
||||
preference.name === ORG_PREFERENCES.ORG_ONBOARDING,
|
||||
)?.value;
|
||||
|
||||
// Don't redirect to onboarding if workspace has issues
|
||||
const isWorkspaceBlocked = trialInfo?.workSpaceBlock;
|
||||
const isWorkspaceSuspended = activeLicense?.state === LicenseState.DEFAULTED;
|
||||
const isWorkspaceAccessRestricted =
|
||||
activeLicense?.state === LicenseState.TERMINATED ||
|
||||
activeLicense?.state === LicenseState.EXPIRED ||
|
||||
activeLicense?.state === LicenseState.CANCELLED;
|
||||
|
||||
const hasWorkspaceIssue =
|
||||
isWorkspaceBlocked || isWorkspaceSuspended || isWorkspaceAccessRestricted;
|
||||
|
||||
if (!hasWorkspaceIssue) {
|
||||
const isFirstUser = checkFirstTimeUser();
|
||||
if (
|
||||
isFirstUser &&
|
||||
!isOnboardingComplete &&
|
||||
!ROUTES_NOT_TO_BE_OVERRIDEN.includes(pathname) &&
|
||||
pathname !== ROUTES.ONBOARDING
|
||||
) {
|
||||
return <Redirect to={ROUTES.ONBOARDING} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for GET_STARTED → GET_STARTED_WITH_CLOUD redirect (feature flag)
|
||||
if (
|
||||
currentRoute?.path === ROUTES.GET_STARTED &&
|
||||
featureFlags?.find((e) => e.name === FeatureKeys.ONBOARDING_V3)?.active
|
||||
) {
|
||||
return <Redirect to={ROUTES.GET_STARTED_WITH_CLOUD} />;
|
||||
}
|
||||
|
||||
// Main routing logic
|
||||
if (currentRoute) {
|
||||
const { isPrivate, key } = currentRoute;
|
||||
if (isPrivate) {
|
||||
if (isLoggedInState) {
|
||||
const route = routePermission[key];
|
||||
if (route && route.find((e) => e === user.role) === undefined) {
|
||||
return <Redirect to={ROUTES.UN_AUTHORIZED} />;
|
||||
}
|
||||
} else {
|
||||
// Save current path and redirect to login
|
||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, pathname);
|
||||
return <Redirect to={ROUTES.LOGIN} />;
|
||||
}
|
||||
} else if (isLoggedInState) {
|
||||
// Non-private route, but user is logged in
|
||||
const fromPathname = getLocalStorageApi(
|
||||
LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT,
|
||||
);
|
||||
if (fromPathname) {
|
||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, '');
|
||||
return <Redirect to={fromPathname} />;
|
||||
}
|
||||
if (pathname !== ROUTES.SOMETHING_WENT_WRONG) {
|
||||
return <Redirect to={ROUTES.HOME} />;
|
||||
}
|
||||
}
|
||||
// Non-private route, user not logged in - let login/signup pages handle it
|
||||
} else if (isLoggedInState) {
|
||||
// Unknown route, logged in
|
||||
const fromPathname = getLocalStorageApi(
|
||||
LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT,
|
||||
);
|
||||
if (fromPathname) {
|
||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, '');
|
||||
return <Redirect to={fromPathname} />;
|
||||
}
|
||||
return <Redirect to={ROUTES.HOME} />;
|
||||
} else {
|
||||
// Unknown route, not logged in
|
||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, pathname);
|
||||
return <Redirect to={ROUTES.LOGIN} />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { FeatureKeys } from 'constants/features';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { ORG_PREFERENCES } from 'constants/orgPreferences';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { AppContext } from 'providers/App/App';
|
||||
import { IAppContext, IUser } from 'providers/App/types';
|
||||
import {
|
||||
@@ -22,19 +21,6 @@ import { ROLES, USER_ROLES } from 'types/roles';
|
||||
|
||||
import PrivateRoute from '../Private';
|
||||
|
||||
// Mock history module
|
||||
jest.mock('lib/history', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
push: jest.fn(),
|
||||
location: { pathname: '/', search: '', hash: '' },
|
||||
listen: jest.fn(),
|
||||
createHref: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockHistoryPush = history.push as jest.Mock;
|
||||
|
||||
// Mock localStorage APIs
|
||||
const mockLocalStorage: Record<string, string> = {};
|
||||
jest.mock('api/browser/localstorage/get', () => ({
|
||||
@@ -239,20 +225,18 @@ function renderPrivateRoute(options: RenderPrivateRouteOptions = {}): void {
|
||||
}
|
||||
|
||||
// Generic assertion helpers for navigation behavior
|
||||
// Using these allows easier refactoring when switching from history.push to Redirect component
|
||||
// Using location-based assertions since Private.tsx now uses Redirect component
|
||||
|
||||
async function assertRedirectsTo(targetRoute: string): Promise<void> {
|
||||
await waitFor(() => {
|
||||
expect(mockHistoryPush).toHaveBeenCalledWith(targetRoute);
|
||||
expect(screen.getByTestId('location-display')).toHaveTextContent(targetRoute);
|
||||
});
|
||||
}
|
||||
|
||||
function assertNoRedirect(): void {
|
||||
expect(mockHistoryPush).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
function assertDoesNotRedirectTo(targetRoute: string): void {
|
||||
expect(mockHistoryPush).not.toHaveBeenCalledWith(targetRoute);
|
||||
function assertStaysOnRoute(expectedRoute: string): void {
|
||||
expect(screen.getByTestId('location-display')).toHaveTextContent(
|
||||
expectedRoute,
|
||||
);
|
||||
}
|
||||
|
||||
function assertRendersChildren(): void {
|
||||
@@ -350,7 +334,7 @@ describe('PrivateRoute', () => {
|
||||
});
|
||||
|
||||
assertRendersChildren();
|
||||
assertNoRedirect();
|
||||
assertStaysOnRoute('/public/dashboard/abc123');
|
||||
});
|
||||
|
||||
it('should render children for public dashboard route when logged in without redirecting', () => {
|
||||
@@ -362,7 +346,7 @@ describe('PrivateRoute', () => {
|
||||
assertRendersChildren();
|
||||
// Critical: without the isPublicDashboard early return, logged-in users
|
||||
// would be redirected to HOME due to the non-private route handling
|
||||
assertNoRedirect();
|
||||
assertStaysOnRoute('/public/dashboard/abc123');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -420,7 +404,7 @@ describe('PrivateRoute', () => {
|
||||
});
|
||||
|
||||
assertRendersChildren();
|
||||
assertNoRedirect();
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
|
||||
it('should redirect to unauthorized when VIEWER tries to access admin-only route /alerts/new', async () => {
|
||||
@@ -529,7 +513,7 @@ describe('PrivateRoute', () => {
|
||||
appContext: { isLoggedIn: true },
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.HOME);
|
||||
assertStaysOnRoute(ROUTES.SOMETHING_WENT_WRONG);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -541,7 +525,7 @@ describe('PrivateRoute', () => {
|
||||
});
|
||||
|
||||
// Should not redirect - login page handles its own routing
|
||||
assertNoRedirect();
|
||||
assertStaysOnRoute(ROUTES.LOGIN);
|
||||
});
|
||||
|
||||
it('should not redirect when not logged in user visits signup page', () => {
|
||||
@@ -550,7 +534,7 @@ describe('PrivateRoute', () => {
|
||||
appContext: { isLoggedIn: false },
|
||||
});
|
||||
|
||||
assertNoRedirect();
|
||||
assertStaysOnRoute(ROUTES.SIGN_UP);
|
||||
});
|
||||
|
||||
it('should not redirect when not logged in user visits password reset page', () => {
|
||||
@@ -559,7 +543,7 @@ describe('PrivateRoute', () => {
|
||||
appContext: { isLoggedIn: false },
|
||||
});
|
||||
|
||||
assertNoRedirect();
|
||||
assertStaysOnRoute(ROUTES.PASSWORD_RESET);
|
||||
});
|
||||
|
||||
it('should not redirect when not logged in user visits forgot password page', () => {
|
||||
@@ -568,7 +552,7 @@ describe('PrivateRoute', () => {
|
||||
appContext: { isLoggedIn: false },
|
||||
});
|
||||
|
||||
assertNoRedirect();
|
||||
assertStaysOnRoute(ROUTES.FORGOT_PASSWORD);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -657,7 +641,7 @@ describe('PrivateRoute', () => {
|
||||
});
|
||||
|
||||
// Admin should be able to access settings even when workspace is blocked
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
|
||||
assertStaysOnRoute(ROUTES.SETTINGS);
|
||||
});
|
||||
|
||||
it('should allow ADMIN to access /settings/billing when workspace is blocked', () => {
|
||||
@@ -673,7 +657,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
|
||||
assertStaysOnRoute(ROUTES.BILLING);
|
||||
});
|
||||
|
||||
it('should allow ADMIN to access /settings/org-settings when workspace is blocked', () => {
|
||||
@@ -689,7 +673,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
|
||||
assertStaysOnRoute(ROUTES.ORG_SETTINGS);
|
||||
});
|
||||
|
||||
it('should allow ADMIN to access /settings/members when workspace is blocked', () => {
|
||||
@@ -705,7 +689,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
|
||||
assertStaysOnRoute(ROUTES.MEMBERS_SETTINGS);
|
||||
});
|
||||
|
||||
it('should allow ADMIN to access /settings/my-settings when workspace is blocked', () => {
|
||||
@@ -721,7 +705,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
|
||||
assertStaysOnRoute(ROUTES.MY_SETTINGS);
|
||||
});
|
||||
|
||||
it('should redirect VIEWER to workspace locked even when trying to access settings', async () => {
|
||||
@@ -832,7 +816,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
|
||||
assertStaysOnRoute(ROUTES.WORKSPACE_LOCKED);
|
||||
});
|
||||
|
||||
it('should not redirect self-hosted users to workspace locked even when workSpaceBlock is true', () => {
|
||||
@@ -849,7 +833,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: false,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -919,7 +903,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
|
||||
assertStaysOnRoute(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
|
||||
});
|
||||
|
||||
it('should not redirect self-hosted users to workspace access restricted when license is terminated', () => {
|
||||
@@ -936,7 +920,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: false,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
|
||||
it('should not redirect when license is ACTIVE', () => {
|
||||
@@ -953,7 +937,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
|
||||
it('should not redirect when license is EVALUATING', () => {
|
||||
@@ -970,7 +954,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1006,7 +990,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_SUSPENDED);
|
||||
assertStaysOnRoute(ROUTES.WORKSPACE_SUSPENDED);
|
||||
});
|
||||
|
||||
it('should not redirect self-hosted users to workspace suspended when license is defaulted', () => {
|
||||
@@ -1023,7 +1007,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: false,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_SUSPENDED);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1043,6 +1027,11 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
// Wait for the users query to complete and trigger re-render
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
await assertRedirectsTo(ROUTES.ONBOARDING);
|
||||
});
|
||||
|
||||
@@ -1058,7 +1047,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
|
||||
it('should not redirect to onboarding when onboarding is already complete', async () => {
|
||||
@@ -1084,7 +1073,7 @@ describe('PrivateRoute', () => {
|
||||
|
||||
// Critical: if isOnboardingComplete check is broken (always false),
|
||||
// this test would fail because all other conditions for redirect ARE met
|
||||
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
|
||||
it('should not redirect to onboarding for non-cloud users', () => {
|
||||
@@ -1099,7 +1088,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: false,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
|
||||
it('should not redirect to onboarding when on /workspace-locked route', () => {
|
||||
@@ -1114,7 +1103,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
|
||||
assertStaysOnRoute(ROUTES.WORKSPACE_LOCKED);
|
||||
});
|
||||
|
||||
it('should not redirect to onboarding when on /workspace-suspended route', () => {
|
||||
@@ -1129,7 +1118,7 @@ describe('PrivateRoute', () => {
|
||||
isCloudUser: true,
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
|
||||
assertStaysOnRoute(ROUTES.WORKSPACE_SUSPENDED);
|
||||
});
|
||||
|
||||
it('should not redirect to onboarding when workspace is blocked and accessing billing', async () => {
|
||||
@@ -1156,7 +1145,7 @@ describe('PrivateRoute', () => {
|
||||
});
|
||||
|
||||
// Should NOT redirect to onboarding - user needs to access billing to fix payment
|
||||
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
|
||||
assertStaysOnRoute(ROUTES.BILLING);
|
||||
});
|
||||
|
||||
it('should not redirect to onboarding when workspace is blocked and accessing settings', async () => {
|
||||
@@ -1180,7 +1169,7 @@ describe('PrivateRoute', () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
|
||||
assertStaysOnRoute(ROUTES.SETTINGS);
|
||||
});
|
||||
|
||||
it('should not redirect to onboarding when workspace is suspended (DEFAULTED)', async () => {
|
||||
@@ -1207,7 +1196,7 @@ describe('PrivateRoute', () => {
|
||||
});
|
||||
|
||||
// Should redirect to WORKSPACE_SUSPENDED, not ONBOARDING
|
||||
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
|
||||
await assertRedirectsTo(ROUTES.WORKSPACE_SUSPENDED);
|
||||
});
|
||||
|
||||
it('should not redirect to onboarding when workspace is access restricted (TERMINATED)', async () => {
|
||||
@@ -1234,7 +1223,7 @@ describe('PrivateRoute', () => {
|
||||
});
|
||||
|
||||
// Should redirect to WORKSPACE_ACCESS_RESTRICTED, not ONBOARDING
|
||||
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
|
||||
await assertRedirectsTo(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
|
||||
});
|
||||
|
||||
it('should not redirect to onboarding when workspace is access restricted (EXPIRED)', async () => {
|
||||
@@ -1260,7 +1249,7 @@ describe('PrivateRoute', () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
|
||||
await assertRedirectsTo(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1302,7 +1291,7 @@ describe('PrivateRoute', () => {
|
||||
},
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.GET_STARTED_WITH_CLOUD);
|
||||
assertStaysOnRoute(ROUTES.GET_STARTED);
|
||||
});
|
||||
|
||||
it('should not redirect when on GET_STARTED and ONBOARDING_V3 feature flag is not present', () => {
|
||||
@@ -1314,7 +1303,7 @@ describe('PrivateRoute', () => {
|
||||
},
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.GET_STARTED_WITH_CLOUD);
|
||||
assertStaysOnRoute(ROUTES.GET_STARTED);
|
||||
});
|
||||
|
||||
it('should not redirect when on different route even if ONBOARDING_V3 is active', () => {
|
||||
@@ -1334,7 +1323,7 @@ describe('PrivateRoute', () => {
|
||||
},
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.GET_STARTED_WITH_CLOUD);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1350,7 +1339,7 @@ describe('PrivateRoute', () => {
|
||||
},
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
|
||||
it('should not fetch users when org data is not available', () => {
|
||||
@@ -1393,9 +1382,7 @@ describe('PrivateRoute', () => {
|
||||
},
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_SUSPENDED);
|
||||
assertDoesNotRedirectTo(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
|
||||
assertStaysOnRoute(ROUTES.HOME);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1436,22 +1423,40 @@ describe('PrivateRoute', () => {
|
||||
await assertRedirectsTo(ROUTES.UN_AUTHORIZED);
|
||||
});
|
||||
|
||||
it('should allow all roles to access /services route', () => {
|
||||
const roles = [USER_ROLES.ADMIN, USER_ROLES.EDITOR, USER_ROLES.VIEWER];
|
||||
|
||||
roles.forEach((role) => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
renderPrivateRoute({
|
||||
initialRoute: ROUTES.APPLICATION,
|
||||
appContext: {
|
||||
isLoggedIn: true,
|
||||
user: createMockUser({ role: role as ROLES }),
|
||||
},
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.UN_AUTHORIZED);
|
||||
it('should allow ADMIN to access /services route', () => {
|
||||
renderPrivateRoute({
|
||||
initialRoute: ROUTES.APPLICATION,
|
||||
appContext: {
|
||||
isLoggedIn: true,
|
||||
user: createMockUser({ role: USER_ROLES.ADMIN as ROLES }),
|
||||
},
|
||||
});
|
||||
|
||||
assertStaysOnRoute(ROUTES.APPLICATION);
|
||||
});
|
||||
|
||||
it('should allow EDITOR to access /services route', () => {
|
||||
renderPrivateRoute({
|
||||
initialRoute: ROUTES.APPLICATION,
|
||||
appContext: {
|
||||
isLoggedIn: true,
|
||||
user: createMockUser({ role: USER_ROLES.EDITOR as ROLES }),
|
||||
},
|
||||
});
|
||||
|
||||
assertStaysOnRoute(ROUTES.APPLICATION);
|
||||
});
|
||||
|
||||
it('should allow VIEWER to access /services route', () => {
|
||||
renderPrivateRoute({
|
||||
initialRoute: ROUTES.APPLICATION,
|
||||
appContext: {
|
||||
isLoggedIn: true,
|
||||
user: createMockUser({ role: USER_ROLES.VIEWER as ROLES }),
|
||||
},
|
||||
});
|
||||
|
||||
assertStaysOnRoute(ROUTES.APPLICATION);
|
||||
});
|
||||
|
||||
it('should redirect VIEWER from /onboarding route (admin only)', async () => {
|
||||
@@ -1481,7 +1486,7 @@ describe('PrivateRoute', () => {
|
||||
});
|
||||
|
||||
assertRendersChildren();
|
||||
assertDoesNotRedirectTo(ROUTES.UN_AUTHORIZED);
|
||||
assertStaysOnRoute(ROUTES.CHANNELS_NEW);
|
||||
});
|
||||
|
||||
it('should allow EDITOR to access /get-started route', () => {
|
||||
@@ -1493,7 +1498,7 @@ describe('PrivateRoute', () => {
|
||||
},
|
||||
});
|
||||
|
||||
assertDoesNotRedirectTo(ROUTES.UN_AUTHORIZED);
|
||||
assertStaysOnRoute(ROUTES.GET_STARTED);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8s/constants';
|
||||
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
|
||||
export const getHostAttributeKeys = async (
|
||||
searchText = '',
|
||||
entity: K8sCategory,
|
||||
entity: InfraMonitoringEntity,
|
||||
): Promise<SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse> => {
|
||||
try {
|
||||
const response: AxiosResponse<{
|
||||
|
||||
@@ -13,7 +13,9 @@ export interface HostListPayload {
|
||||
orderBy?: {
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
};
|
||||
} | null;
|
||||
start?: number;
|
||||
end?: number;
|
||||
}
|
||||
|
||||
export interface TimeSeriesValue {
|
||||
@@ -31,6 +33,8 @@ export interface HostData {
|
||||
hostName: string;
|
||||
active: boolean;
|
||||
os: string;
|
||||
/** Present when the list API returns grouped rows or extra resource attributes. */
|
||||
meta?: Record<string, string>;
|
||||
cpu: number;
|
||||
cpuTimeSeries: TimeSeries;
|
||||
memory: number;
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
import axios 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';
|
||||
|
||||
import { UnderscoreToDotMap } from '../utils';
|
||||
|
||||
export interface K8sNodesListPayload {
|
||||
filters: TagFilter;
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: {
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sNodesData {
|
||||
nodeUID: string;
|
||||
nodeCPUUsage: number;
|
||||
nodeCPUAllocatable: number;
|
||||
nodeMemoryUsage: number;
|
||||
nodeMemoryAllocatable: number;
|
||||
meta: {
|
||||
k8s_node_name: string;
|
||||
k8s_node_uid: string;
|
||||
k8s_cluster_name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sNodesListResponse {
|
||||
status: string;
|
||||
data: {
|
||||
type: string;
|
||||
records: K8sNodesData[];
|
||||
groups: null;
|
||||
total: number;
|
||||
sentAnyHostMetricsData: boolean;
|
||||
isSendingK8SAgentMetrics: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const nodesMetaMap = [
|
||||
{ dot: 'k8s.node.name', under: 'k8s_node_name' },
|
||||
{ dot: 'k8s.node.uid', under: 'k8s_node_uid' },
|
||||
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
|
||||
] as const;
|
||||
|
||||
export function mapNodesMeta(
|
||||
raw: Record<string, unknown>,
|
||||
): K8sNodesData['meta'] {
|
||||
const out: Record<string, unknown> = { ...raw };
|
||||
nodesMetaMap.forEach(({ dot, under }) => {
|
||||
if (dot in raw) {
|
||||
const v = raw[dot];
|
||||
out[under] = typeof v === 'string' ? v : raw[under];
|
||||
}
|
||||
});
|
||||
return out as K8sNodesData['meta'];
|
||||
}
|
||||
|
||||
export const getK8sNodesList = async (
|
||||
props: K8sNodesListPayload,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
): Promise<SuccessResponse<K8sNodesListResponse> | ErrorResponse> => {
|
||||
try {
|
||||
const requestProps =
|
||||
dotMetricsEnabled && Array.isArray(props.filters?.items)
|
||||
? {
|
||||
...props,
|
||||
filters: {
|
||||
...props.filters,
|
||||
items: props.filters.items.reduce<typeof props.filters.items>(
|
||||
(acc, item) => {
|
||||
if (item.value === undefined) {
|
||||
return acc;
|
||||
}
|
||||
if (
|
||||
item.key &&
|
||||
typeof item.key === 'object' &&
|
||||
'key' in item.key &&
|
||||
typeof item.key.key === 'string'
|
||||
) {
|
||||
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
|
||||
acc.push({
|
||||
...item,
|
||||
key: { ...item.key, key: mappedKey },
|
||||
});
|
||||
} else {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as typeof props.filters.items,
|
||||
),
|
||||
},
|
||||
}
|
||||
: props;
|
||||
|
||||
const response = await axios.post('/nodes/list', requestProps, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
const payload: K8sNodesListResponse = response.data;
|
||||
|
||||
// one-liner to map dot→underscore
|
||||
payload.data.records = payload.data.records.map((record) => ({
|
||||
...record,
|
||||
meta: mapNodesMeta(record.meta as Record<string, unknown>),
|
||||
}));
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload,
|
||||
params: requestProps,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,7 +10,12 @@ import APIError from 'types/api/error';
|
||||
import './ErrorContent.styles.scss';
|
||||
|
||||
interface ErrorContentProps {
|
||||
error: APIError;
|
||||
error:
|
||||
| APIError
|
||||
| {
|
||||
code: number;
|
||||
message: string;
|
||||
};
|
||||
icon?: ReactNode;
|
||||
}
|
||||
|
||||
@@ -20,7 +25,15 @@ function ErrorContent({ error, icon }: ErrorContentProps): JSX.Element {
|
||||
errors: errorMessages,
|
||||
code: errorCode,
|
||||
message: errorMessage,
|
||||
} = error?.error?.error || {};
|
||||
} =
|
||||
error && 'error' in error
|
||||
? error?.error?.error || {}
|
||||
: {
|
||||
url: undefined,
|
||||
errors: [],
|
||||
code: error.code || 500,
|
||||
message: error.message || 'Something went wrong',
|
||||
};
|
||||
return (
|
||||
<section className="error-content">
|
||||
{/* Summary Header */}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { HostData } from 'api/infraMonitoring/getHostLists';
|
||||
|
||||
export type HostDetailProps = {
|
||||
host: HostData | null;
|
||||
isModalTimeSelection: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -1,193 +0,0 @@
|
||||
.host-metric-traces {
|
||||
margin-top: 1rem;
|
||||
|
||||
.host-metric-traces-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
|
||||
.filter-section {
|
||||
flex: 1;
|
||||
|
||||
.ant-select-selector {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400) !important;
|
||||
background-color: var(--bg-ink-300) !important;
|
||||
|
||||
input {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ant-tag .ant-typography {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.host-metric-traces-table {
|
||||
.ant-table-content {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.ant-table {
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
|
||||
.ant-table-thead > tr > th {
|
||||
padding: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
|
||||
background: rgba(171, 189, 255, 0.01);
|
||||
border-bottom: none;
|
||||
|
||||
color: var(--Vanilla-400, #c0c1c3);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.44px;
|
||||
text-transform: uppercase;
|
||||
|
||||
&::before {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th:has(.hostname-column-header) {
|
||||
background: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: var(--bg-vanilla-100);
|
||||
background: rgba(171, 189, 255, 0.01);
|
||||
}
|
||||
|
||||
.ant-table-cell:has(.hostname-column-value) {
|
||||
background: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.hostname-column-value {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-family: 'Geist Mono';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.status-cell {
|
||||
.active-tag {
|
||||
color: var(--bg-forest-500);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
.ant-progress-bg {
|
||||
height: 8px !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.ant-table-cell:first-child {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.ant-table-cell:nth-child(2) {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.ant-table-cell:nth-child(n + 3) {
|
||||
padding-right: 24px;
|
||||
}
|
||||
.column-header-right {
|
||||
text-align: right;
|
||||
}
|
||||
.ant-table-tbody > tr > td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ant-table-thead
|
||||
> tr
|
||||
> th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ant-empty-normal {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-container::after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.host-metric-traces-header {
|
||||
.filter-section {
|
||||
border-top: 1px solid var(--bg-vanilla-300);
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
.ant-select-selector {
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
background-color: var(--bg-vanilla-100) !important;
|
||||
color: var(--bg-ink-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.host-metric-traces-table {
|
||||
.ant-table {
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
.ant-table-thead > tr > th {
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th:has(.hostname-column-header) {
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.ant-table-cell:has(.hostname-column-value) {
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.hostname-column-value {
|
||||
color: var(--bg-ink-300);
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
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/types';
|
||||
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 { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { VIEWS } from '../constants';
|
||||
import { getHostTracesQueryPayload, selectedColumns } from './constants';
|
||||
import { getListColumns } from './utils';
|
||||
|
||||
import './HostMetricTraces.styles.scss';
|
||||
|
||||
interface Props {
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
isModalTimeSelection: boolean;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
handleChangeTracesFilters: (
|
||||
value: IBuilderQuery['filters'],
|
||||
view: VIEWS,
|
||||
) => void;
|
||||
tracesFilters: IBuilderQuery['filters'];
|
||||
selectedInterval: Time;
|
||||
}
|
||||
|
||||
function HostMetricTraces({
|
||||
timeRange,
|
||||
isModalTimeSelection,
|
||||
handleTimeChange,
|
||||
handleChangeTracesFilters,
|
||||
tracesFilters,
|
||||
selectedInterval,
|
||||
}: Props): JSX.Element {
|
||||
const [traces, setTraces] = useState<any[]>([]);
|
||||
const [offset] = useState<number>(0);
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const updatedCurrentQuery = useMemo(
|
||||
() => ({
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...currentQuery.builder.queryData[0],
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
...currentQuery.builder.queryData[0].aggregateAttribute,
|
||||
},
|
||||
filters: {
|
||||
items:
|
||||
tracesFilters?.items?.filter((item) => item.key?.key !== 'host.name') ||
|
||||
[],
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
[currentQuery, tracesFilters?.items],
|
||||
);
|
||||
|
||||
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||
|
||||
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
|
||||
QueryParams.pagination,
|
||||
);
|
||||
|
||||
const queryPayload = useMemo(
|
||||
() =>
|
||||
getHostTracesQueryPayload(
|
||||
timeRange.startTime,
|
||||
timeRange.endTime,
|
||||
paginationQueryData?.offset || offset,
|
||||
tracesFilters,
|
||||
),
|
||||
[
|
||||
timeRange.startTime,
|
||||
timeRange.endTime,
|
||||
offset,
|
||||
tracesFilters,
|
||||
paginationQueryData,
|
||||
],
|
||||
);
|
||||
|
||||
const { data, isLoading, isFetching, isError } = useQuery({
|
||||
queryKey: [
|
||||
'hostMetricTraces',
|
||||
timeRange.startTime,
|
||||
timeRange.endTime,
|
||||
offset,
|
||||
tracesFilters,
|
||||
DEFAULT_ENTITY_VERSION,
|
||||
paginationQueryData,
|
||||
],
|
||||
queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION),
|
||||
enabled: !!queryPayload,
|
||||
});
|
||||
|
||||
const traceListColumns = getListColumns(selectedColumns);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.payload?.data?.newResult?.data?.result) {
|
||||
const currentData = data.payload.data.newResult.data.result;
|
||||
if (currentData.length > 0 && currentData[0].list) {
|
||||
if (offset === 0) {
|
||||
setTraces(currentData[0].list ?? []);
|
||||
} else {
|
||||
setTraces((prev) => [...prev, ...(currentData[0].list ?? [])]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [data, offset]);
|
||||
|
||||
const isDataEmpty =
|
||||
!isLoading && !isFetching && !isError && traces.length === 0;
|
||||
const hasAdditionalFilters =
|
||||
tracesFilters?.items && tracesFilters?.items?.length > 1;
|
||||
|
||||
const totalCount =
|
||||
data?.payload?.data?.newResult?.data?.result?.[0]?.list?.length || 0;
|
||||
|
||||
const handleRowClick = useCallback(() => {
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.HostEntity,
|
||||
view: InfraMonitoringEvents.TracesView,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="host-metric-traces">
|
||||
<div className="host-metric-traces-header">
|
||||
<div className="filter-section">
|
||||
{query && (
|
||||
<QueryBuilderSearch
|
||||
query={query as IBuilderQuery}
|
||||
onChange={(value): void =>
|
||||
handleChangeTracesFilters(value, VIEWS.TRACES)
|
||||
}
|
||||
disableNavigationShortcuts
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="datetime-section">
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
</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="host-metric-traces-table">
|
||||
<TraceExplorerControls
|
||||
isLoading={isFetching && traces.length === 0}
|
||||
totalCount={totalCount}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
showSizeChanger={false}
|
||||
/>
|
||||
<ResizeTable
|
||||
tableLayout="fixed"
|
||||
pagination={false}
|
||||
scroll={{ x: true }}
|
||||
loading={isFetching && traces.length === 0}
|
||||
dataSource={traces}
|
||||
columns={traceListColumns}
|
||||
onRow={(): Record<string, unknown> => ({
|
||||
onClick: (): void => handleRowClick(),
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HostMetricTraces;
|
||||
@@ -1,183 +0,0 @@
|
||||
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, ReduceOperators } 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',
|
||||
},
|
||||
{
|
||||
key: 'serviceName',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
key: 'durationNano',
|
||||
dataType: DataTypes.Float64,
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
key: 'httpMethod',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
key: 'responseStatusCode',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
},
|
||||
];
|
||||
|
||||
export const getHostTracesQueryPayload = (
|
||||
start: number,
|
||||
end: number,
|
||||
offset = 0,
|
||||
filters: IBuilderQuery['filters'],
|
||||
): GetQueryResultsProps => ({
|
||||
query: {
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
dataSource: DataSource.TRACES,
|
||||
queryName: 'A',
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
id: '------false',
|
||||
dataType: DataTypes.EMPTY,
|
||||
key: '',
|
||||
type: '',
|
||||
},
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
functions: [],
|
||||
filters,
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
stepInterval: 60,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [
|
||||
{
|
||||
columnName: 'timestamp',
|
||||
order: 'desc',
|
||||
},
|
||||
],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
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',
|
||||
id: 'serviceName--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
id: 'name--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
{
|
||||
key: 'durationNano',
|
||||
dataType: 'float64',
|
||||
type: 'tag',
|
||||
id: 'durationNano--float64--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
{
|
||||
key: 'httpMethod',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
id: 'httpMethod--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
{
|
||||
key: 'responseStatusCode',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
id: 'responseStatusCode--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -1,233 +0,0 @@
|
||||
.host-detail-drawer {
|
||||
border-left: 1px solid var(--bg-slate-500);
|
||||
background: var(--bg-ink-400);
|
||||
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.ant-drawer-header {
|
||||
padding: 8px 16px;
|
||||
border-bottom: none;
|
||||
|
||||
align-items: stretch;
|
||||
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
background: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--text-vanilla-400);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: var(--padding-1);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.host-detail-drawer__host {
|
||||
.host-details-grid {
|
||||
.labels-row,
|
||||
.values-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.5fr 1.5fr 1.5fr;
|
||||
gap: 30px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.labels-row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.host-details-metadata-label {
|
||||
color: var(--text-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.44px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
margin: 0;
|
||||
|
||||
&.active {
|
||||
color: var(--success-500);
|
||||
background: var(--success-100);
|
||||
border-color: var(--success-500);
|
||||
}
|
||||
|
||||
&.inactive {
|
||||
color: var(--error-500);
|
||||
background: var(--error-100);
|
||||
border-color: var(--error-500);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
width: 158px;
|
||||
.ant-progress {
|
||||
margin: 0;
|
||||
|
||||
.ant-progress-text {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
&.ant-card-bordered {
|
||||
border: 1px solid var(--bg-slate-500) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-and-search {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 16px 0;
|
||||
|
||||
.action-btn {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.views-tabs-container {
|
||||
margin-top: 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.views-tabs {
|
||||
color: var(--text-vanilla-400);
|
||||
|
||||
.view-title {
|
||||
display: flex;
|
||||
gap: var(--margin-2);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-xs);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
|
||||
.tab {
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
width: 114px;
|
||||
}
|
||||
|
||||
.tab::before {
|
||||
background: var(--bg-slate-400);
|
||||
}
|
||||
|
||||
.selected_view {
|
||||
background: var(--bg-slate-300);
|
||||
color: var(--text-vanilla-100);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
}
|
||||
|
||||
.selected_view::before {
|
||||
background: var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
|
||||
.compass-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.ant-drawer-header {
|
||||
border-bottom: 1px solid var(--bg-vanilla-400);
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.host-detail-drawer {
|
||||
.title {
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
|
||||
.host-detail-drawer__host {
|
||||
.ant-typography {
|
||||
color: var(--text-ink-300);
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
border: 1px solid var(--bg-vanilla-400);
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
|
||||
.views-tabs {
|
||||
.tab {
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.selected_view {
|
||||
background: var(--bg-vanilla-300);
|
||||
border: 1px solid var(--bg-slate-300);
|
||||
color: var(--text-ink-400);
|
||||
}
|
||||
|
||||
.selected_view::before {
|
||||
background: var(--bg-vanilla-300);
|
||||
border-left: 1px solid var(--bg-slate-300);
|
||||
}
|
||||
}
|
||||
|
||||
.compass-button {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tabs-and-search {
|
||||
.action-btn {
|
||||
border: 1px solid var(--bg-vanilla-400);
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,595 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Drawer,
|
||||
Progress,
|
||||
Radio,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import type { RadioChangeEvent } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryState,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { getFiltersFromParams } from 'container/InfraMonitoringK8s/commonUtils';
|
||||
import { INFRA_MONITORING_K8S_PARAMS_KEYS } from 'container/InfraMonitoringK8s/constants';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import {
|
||||
BarChart2,
|
||||
ChevronsLeftRight,
|
||||
Compass,
|
||||
DraftingCompass,
|
||||
Package2,
|
||||
ScrollText,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
LogsAggregatorOperator,
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { VIEW_TYPES, VIEWS } from './constants';
|
||||
import Containers from './Containers/Containers';
|
||||
import { HostDetailProps } from './HostMetricDetail.interfaces';
|
||||
import HostMetricLogsDetailedView from './HostMetricsLogs/HostMetricLogsDetailedView';
|
||||
import HostMetricTraces from './HostMetricTraces/HostMetricTraces';
|
||||
import Metrics from './Metrics/Metrics';
|
||||
import Processes from './Processes/Processes';
|
||||
|
||||
import './HostMetricsDetail.styles.scss';
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function HostMetricsDetails({
|
||||
host,
|
||||
onClose,
|
||||
isModalTimeSelection,
|
||||
}: HostDetailProps): JSX.Element {
|
||||
const { maxTime, minTime, selectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
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 lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
);
|
||||
|
||||
const [selectedView, setSelectedView] = useState<VIEWS>(
|
||||
(searchParams.get('view') as VIEWS) || VIEWS.METRICS,
|
||||
);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const initialFilters = useMemo(() => {
|
||||
const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
|
||||
const queryKey =
|
||||
urlView === VIEW_TYPES.LOGS
|
||||
? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS
|
||||
: INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS;
|
||||
const filters = getFiltersFromParams(searchParams, queryKey);
|
||||
if (filters) {
|
||||
return filters;
|
||||
}
|
||||
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: 'host.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'host.name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: host?.hostName || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [host?.hostName, searchParams]);
|
||||
|
||||
const [logFilters, setLogFilters] = useState<IBuilderQuery['filters']>(
|
||||
initialFilters,
|
||||
);
|
||||
|
||||
const [tracesFilters, setTracesFilters] = useState<IBuilderQuery['filters']>(
|
||||
initialFilters,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (host) {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.HostEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
});
|
||||
}
|
||||
}, [host]);
|
||||
|
||||
useEffect(() => {
|
||||
setLogFilters(initialFilters);
|
||||
setTracesFilters(initialFilters);
|
||||
}, [initialFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
endTime: Math.floor(maxTime / 1000000000),
|
||||
});
|
||||
}
|
||||
}, [selectedTime, minTime, maxTime]);
|
||||
|
||||
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||
setSelectedView(e.target.value);
|
||||
if (host?.hostName) {
|
||||
setSelectedView(e.target.value);
|
||||
setSearchParams({
|
||||
...Object.fromEntries(searchParams.entries()),
|
||||
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value,
|
||||
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null),
|
||||
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null),
|
||||
});
|
||||
}
|
||||
logEvent(InfraMonitoringEvents.TabChanged, {
|
||||
entity: InfraMonitoringEvents.HostEntity,
|
||||
view: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
lastSelectedInterval.current = interval as Time;
|
||||
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(InfraMonitoringEvents.TimeUpdated, {
|
||||
entity: InfraMonitoringEvents.HostEntity,
|
||||
interval,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeLogFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
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',
|
||||
);
|
||||
|
||||
if (newFilters && newFilters?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.HostEntity,
|
||||
view: InfraMonitoringEvents.LogsView,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: [
|
||||
hostNameFilter,
|
||||
...(newFilters || []),
|
||||
...(paginationFilter ? [paginationFilter] : []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
};
|
||||
|
||||
setSearchParams({
|
||||
...Object.fromEntries(searchParams.entries()),
|
||||
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(
|
||||
updatedFilters,
|
||||
),
|
||||
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
|
||||
});
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeTracesFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setTracesFilters((prevFilters) => {
|
||||
const hostNameFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === 'host.name',
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.HostEntity,
|
||||
view: InfraMonitoringEvents.TracesView,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: [
|
||||
hostNameFilter,
|
||||
...(value?.items?.filter((item) => item.key?.key !== 'host.name') || []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
};
|
||||
|
||||
setSearchParams({
|
||||
...Object.fromEntries(searchParams.entries()),
|
||||
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(
|
||||
updatedFilters,
|
||||
),
|
||||
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
|
||||
});
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// 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(InfraMonitoringEvents.ExploreClicked, {
|
||||
view: selectedView,
|
||||
entity: InfraMonitoringEvents.HostEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
});
|
||||
|
||||
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);
|
||||
lastSelectedInterval.current = null;
|
||||
setSearchParams({});
|
||||
|
||||
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">{host?.hostName}</Typography.Text>
|
||||
</>
|
||||
}
|
||||
placement="right"
|
||||
onClose={handleClose}
|
||||
open={!!host}
|
||||
style={{
|
||||
overscrollBehavior: 'contain',
|
||||
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||
}}
|
||||
className="host-detail-drawer"
|
||||
destroyOnClose
|
||||
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||
>
|
||||
{host && (
|
||||
<>
|
||||
<div className="host-detail-drawer__host">
|
||||
<div className="host-details-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="host-details-metadata-label"
|
||||
>
|
||||
STATUS
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="host-details-metadata-label"
|
||||
>
|
||||
OPERATING SYSTEM
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="host-details-metadata-label"
|
||||
>
|
||||
CPU USAGE
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="host-details-metadata-label"
|
||||
>
|
||||
MEMORY USAGE
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className="values-row">
|
||||
<Tag
|
||||
bordered
|
||||
className={`infra-monitoring-tags ${
|
||||
host.active ? 'active' : 'inactive'
|
||||
}`}
|
||||
>
|
||||
{host.active ? 'ACTIVE' : 'INACTIVE'}
|
||||
</Tag>
|
||||
{host.os ? (
|
||||
<Tag className="infra-monitoring-tags" bordered>
|
||||
{host.os}
|
||||
</Tag>
|
||||
) : (
|
||||
<Typography.Text>-</Typography.Text>
|
||||
)}
|
||||
<div className="progress-container">
|
||||
<Progress
|
||||
percent={Number((host.cpu * 100).toFixed(1))}
|
||||
size="small"
|
||||
strokeColor={((): string => {
|
||||
const cpuPercent = Number((host.cpu * 100).toFixed(1));
|
||||
if (cpuPercent >= 90) {
|
||||
return Color.BG_SAKURA_500;
|
||||
}
|
||||
if (cpuPercent >= 60) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
return Color.BG_FOREST_500;
|
||||
})()}
|
||||
className="progress-bar"
|
||||
/>
|
||||
</div>
|
||||
<div className="progress-container">
|
||||
<Progress
|
||||
percent={Number((host.memory * 100).toFixed(1))}
|
||||
size="small"
|
||||
strokeColor={((): string => {
|
||||
const memoryPercent = Number((host.memory * 100).toFixed(1));
|
||||
if (memoryPercent >= 90) {
|
||||
return Color.BG_CHERRY_500;
|
||||
}
|
||||
if (memoryPercent >= 60) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
return Color.BG_FOREST_500;
|
||||
})()}
|
||||
className="progress-bar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="views-tabs-container">
|
||||
<Radio.Group
|
||||
className="views-tabs"
|
||||
onChange={handleTabChange}
|
||||
value={selectedView}
|
||||
>
|
||||
<Radio.Button
|
||||
className={
|
||||
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.CONTAINERS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.CONTAINERS}
|
||||
>
|
||||
<div className="view-title">
|
||||
<Package2 size={14} />
|
||||
Containers
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.PROCESSES ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.PROCESSES}
|
||||
>
|
||||
<div className="view-title">
|
||||
<ChevronsLeftRight size={14} />
|
||||
Processes
|
||||
</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
|
||||
selectedInterval={selectedInterval}
|
||||
hostName={host.hostName}
|
||||
timeRange={modalTimeRange}
|
||||
handleTimeChange={handleTimeChange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<HostMetricLogsDetailedView
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeLogFilters={handleChangeLogFilters}
|
||||
logFilters={logFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.TRACES && (
|
||||
<HostMetricTraces
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeTracesFilters={handleChangeTracesFilters}
|
||||
tracesFilters={tracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedView === VIEW_TYPES.CONTAINERS && <Containers />}
|
||||
{selectedView === VIEW_TYPES.PROCESSES && <Processes />}
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default HostMetricsDetails;
|
||||
@@ -1,133 +0,0 @@
|
||||
.host-metrics-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.host-metrics-logs-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
padding: 12px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
}
|
||||
|
||||
.host-metrics-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.host-metrics-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;
|
||||
}
|
||||
}
|
||||
|
||||
.host-metrics-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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { VIEWS } from '../constants';
|
||||
import HostMetricsLogs from './HostMetricsLogs';
|
||||
|
||||
import './HostMetricLogs.styles.scss';
|
||||
|
||||
interface Props {
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
isModalTimeSelection: boolean;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
handleChangeLogFilters: (value: IBuilderQuery['filters'], view: VIEWS) => void;
|
||||
logFilters: IBuilderQuery['filters'];
|
||||
selectedInterval: Time;
|
||||
}
|
||||
|
||||
function HostMetricLogsDetailedView({
|
||||
timeRange,
|
||||
isModalTimeSelection,
|
||||
handleTimeChange,
|
||||
handleChangeLogFilters,
|
||||
logFilters,
|
||||
selectedInterval,
|
||||
}: Props): JSX.Element {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const updatedCurrentQuery = useMemo(
|
||||
() => ({
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...currentQuery.builder.queryData[0],
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
...currentQuery.builder.queryData[0].aggregateAttribute,
|
||||
},
|
||||
filters: {
|
||||
items:
|
||||
logFilters?.items?.filter((item) => item.key?.key !== 'host.name') ||
|
||||
[],
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
[currentQuery, logFilters?.items],
|
||||
);
|
||||
|
||||
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 as IBuilderQuery}
|
||||
onChange={(value): void => handleChangeLogFilters(value, VIEWS.LOGS)}
|
||||
disableNavigationShortcuts
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="datetime-section">
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<HostMetricsLogs timeRange={timeRange} filters={logFilters} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HostMetricLogsDetailedView;
|
||||
@@ -1,187 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||
import { Card } from 'antd';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
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 { useHandleLogsPagination } from 'hooks/infraMonitoring/useHandleLogsPagination';
|
||||
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
|
||||
import useScrollToLog from 'hooks/logs/useScrollToLog';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { getHostLogsQueryPayload } from './constants';
|
||||
import NoLogsContainer from './NoLogsContainer';
|
||||
|
||||
import './HostMetricLogs.styles.scss';
|
||||
|
||||
interface Props {
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
filters: IBuilderQuery['filters'];
|
||||
}
|
||||
|
||||
function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
|
||||
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||
const {
|
||||
activeLog,
|
||||
onAddToQuery,
|
||||
selectedTab,
|
||||
handleSetActiveLog,
|
||||
handleCloseLogDetail,
|
||||
} = useLogDetailHandlers();
|
||||
|
||||
const basePayload = getHostLogsQueryPayload(
|
||||
timeRange.startTime,
|
||||
timeRange.endTime,
|
||||
filters,
|
||||
);
|
||||
const {
|
||||
logs,
|
||||
hasReachedEndOfLogs,
|
||||
isPaginating,
|
||||
currentPage,
|
||||
setIsPaginating,
|
||||
handleNewData,
|
||||
loadMoreLogs,
|
||||
queryPayload,
|
||||
} = useHandleLogsPagination({
|
||||
timeRange,
|
||||
filters,
|
||||
excludeFilterKeys: ['host.name'],
|
||||
basePayload,
|
||||
});
|
||||
|
||||
const { data, isLoading, isFetching, isError } = useQuery({
|
||||
queryKey: [
|
||||
'hostMetricsLogs',
|
||||
timeRange.startTime,
|
||||
timeRange.endTime,
|
||||
filters,
|
||||
currentPage,
|
||||
],
|
||||
queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION),
|
||||
enabled: !!queryPayload,
|
||||
keepPreviousData: isPaginating,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.payload?.data?.newResult?.data?.result) {
|
||||
handleNewData(data.payload.data.newResult.data.result);
|
||||
}
|
||||
}, [data, handleNewData]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsPaginating(false);
|
||||
}, [data, setIsPaginating]);
|
||||
|
||||
const handleScrollToLog = useScrollToLog({
|
||||
logs,
|
||||
virtuosoRef,
|
||||
});
|
||||
|
||||
const getItemContent = useCallback(
|
||||
(_: number, logToRender: ILog): JSX.Element => {
|
||||
return (
|
||||
<div key={logToRender.id}>
|
||||
<RawLogView
|
||||
isTextOverflowEllipsisDisabled
|
||||
data={logToRender}
|
||||
linesPerRow={5}
|
||||
fontSize={FontSize.MEDIUM}
|
||||
selectedFields={[
|
||||
{
|
||||
dataType: 'string',
|
||||
type: '',
|
||||
name: 'body',
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
type: '',
|
||||
name: 'timestamp',
|
||||
},
|
||||
]}
|
||||
onSetActiveLog={handleSetActiveLog}
|
||||
onClearActiveLog={handleCloseLogDetail}
|
||||
isActiveLog={activeLog?.id === logToRender.id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[activeLog, handleSetActiveLog, handleCloseLogDetail],
|
||||
);
|
||||
|
||||
const renderFooter = useCallback(
|
||||
(): JSX.Element | null => (
|
||||
<>
|
||||
{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="host-metrics-logs-list-card">
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso
|
||||
className="host-metrics-logs-virtuoso"
|
||||
key="host-metrics-logs-virtuoso"
|
||||
ref={virtuosoRef}
|
||||
data={logs}
|
||||
endReached={loadMoreLogs}
|
||||
totalCount={logs.length}
|
||||
itemContent={getItemContent}
|
||||
overscan={200}
|
||||
components={{
|
||||
Footer: renderFooter,
|
||||
}}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</Card>
|
||||
),
|
||||
[logs, loadMoreLogs, getItemContent, renderFooter],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="host-metrics-logs">
|
||||
{isLoading && <LogsLoading />}
|
||||
{!isLoading && !isError && logs.length === 0 && <NoLogsContainer />}
|
||||
{isError && !isLoading && <LogsError />}
|
||||
{!isLoading && !isError && logs.length > 0 && (
|
||||
<div
|
||||
className="host-metrics-logs-list-container"
|
||||
data-log-detail-ignore="true"
|
||||
>
|
||||
{renderContent}
|
||||
</div>
|
||||
)}
|
||||
{selectedTab && activeLog && (
|
||||
<LogDetail
|
||||
log={activeLog}
|
||||
onClose={handleCloseLogDetail}
|
||||
logs={logs}
|
||||
onNavigateLog={handleSetActiveLog}
|
||||
selectedTab={selectedTab}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
onScrollToLog={handleScrollToLog}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HostMetricsLogs;
|
||||
@@ -1,16 +0,0 @@
|
||||
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 host
|
||||
in the selected time range.
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
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, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export const getHostLogsQueryPayload = (
|
||||
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: '',
|
||||
type: '',
|
||||
},
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
functions: [],
|
||||
filters,
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
stepInterval: 60,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [
|
||||
{
|
||||
columnName: 'timestamp',
|
||||
order: 'desc',
|
||||
},
|
||||
],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
offset: 0,
|
||||
pageSize: 100,
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
id: uuidv4(),
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
},
|
||||
start,
|
||||
end,
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
.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%);
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { QueryFunctionContext, useQueries, UseQueryResult } from 'react-query';
|
||||
import { Card, Col, Row, Skeleton, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import Uplot from 'components/Uplot';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import {
|
||||
getHostQueryPayload,
|
||||
hostWidgetInfo,
|
||||
} from 'container/LogDetailedView/InfraMetrics/constants';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { useMultiIntersectionObserver } from 'hooks/useMultiIntersectionObserver';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
|
||||
import './Metrics.styles.scss';
|
||||
|
||||
interface MetricsTabProps {
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
isModalTimeSelection: boolean;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
selectedInterval: Time;
|
||||
|
||||
hostName: string;
|
||||
}
|
||||
|
||||
function Metrics({
|
||||
selectedInterval,
|
||||
hostName,
|
||||
timeRange,
|
||||
handleTimeChange,
|
||||
isModalTimeSelection,
|
||||
}: MetricsTabProps): JSX.Element {
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const {
|
||||
visibilities,
|
||||
setElement,
|
||||
} = useMultiIntersectionObserver(hostWidgetInfo.length, { threshold: 0.1 });
|
||||
|
||||
const legendScrollPositionRef = useRef<{
|
||||
scrollTop: number;
|
||||
scrollLeft: number;
|
||||
}>({
|
||||
scrollTop: 0,
|
||||
scrollLeft: 0,
|
||||
});
|
||||
|
||||
const queryPayloads = useMemo(
|
||||
() =>
|
||||
getHostQueryPayload(
|
||||
hostName,
|
||||
timeRange.startTime,
|
||||
timeRange.endTime,
|
||||
dotMetricsEnabled,
|
||||
),
|
||||
[hostName, timeRange.startTime, timeRange.endTime, dotMetricsEnabled],
|
||||
);
|
||||
|
||||
const queries = useQueries(
|
||||
queryPayloads.map((payload, index) => ({
|
||||
queryKey: ['host-metrics', payload, ENTITY_VERSION_V4, 'HOST'],
|
||||
queryFn: ({
|
||||
signal,
|
||||
}: QueryFunctionContext): Promise<
|
||||
SuccessResponse<MetricRangePayloadProps>
|
||||
> => GetMetricQueryRange(payload, ENTITY_VERSION_V4, undefined, signal),
|
||||
enabled: !!payload && visibilities[index],
|
||||
keepPreviousData: true,
|
||||
})),
|
||||
);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const dimensions = useResizeObserver(graphRef);
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const chartData = useMemo(
|
||||
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
|
||||
[queries],
|
||||
);
|
||||
|
||||
const [graphTimeIntervals, setGraphTimeIntervals] = useState<
|
||||
{
|
||||
start: number;
|
||||
end: number;
|
||||
}[]
|
||||
>(
|
||||
new Array(queries.length).fill({
|
||||
start: timeRange.startTime,
|
||||
end: timeRange.endTime,
|
||||
}),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setGraphTimeIntervals(
|
||||
new Array(queries.length).fill({
|
||||
start: timeRange.startTime,
|
||||
end: timeRange.endTime,
|
||||
}),
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [timeRange]);
|
||||
|
||||
const onDragSelect = useCallback(
|
||||
(start: number, end: number, graphIndex: number) => {
|
||||
const startTimestamp = Math.trunc(start);
|
||||
const endTimestamp = Math.trunc(end);
|
||||
|
||||
setGraphTimeIntervals((prev) => {
|
||||
const newIntervals = [...prev];
|
||||
newIntervals[graphIndex] = {
|
||||
start: Math.floor(startTimestamp / 1000),
|
||||
end: Math.floor(endTimestamp / 1000),
|
||||
};
|
||||
return newIntervals;
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
queries.map(({ data }, idx) =>
|
||||
getUPlotChartOptions({
|
||||
apiResponse: data?.payload,
|
||||
isDarkMode,
|
||||
dimensions,
|
||||
yAxisUnit: hostWidgetInfo[idx].yAxisUnit,
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
minTimeScale: graphTimeIntervals[idx].start,
|
||||
maxTimeScale: graphTimeIntervals[idx].end,
|
||||
onDragSelect: (start, end) => onDragSelect(start, end, idx),
|
||||
query: currentQuery,
|
||||
legendScrollPosition: legendScrollPositionRef.current,
|
||||
setLegendScrollPosition: (position: {
|
||||
scrollTop: number;
|
||||
scrollLeft: number;
|
||||
}) => {
|
||||
legendScrollPositionRef.current = position;
|
||||
},
|
||||
}),
|
||||
),
|
||||
[
|
||||
queries,
|
||||
isDarkMode,
|
||||
dimensions,
|
||||
graphTimeIntervals,
|
||||
onDragSelect,
|
||||
currentQuery,
|
||||
],
|
||||
);
|
||||
|
||||
const renderCardContent = (
|
||||
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
|
||||
idx: number,
|
||||
): JSX.Element => {
|
||||
if ((!query.data && query.isLoading) || !visibilities[idx]) {
|
||||
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
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Row gutter={24} className="host-metrics-container">
|
||||
{queries.map((query, idx) => (
|
||||
<Col ref={setElement(idx)} span={12} key={hostWidgetInfo[idx].title}>
|
||||
<Typography.Text>{hostWidgetInfo[idx].title}</Typography.Text>
|
||||
<Card bordered className="host-metrics-card" ref={graphRef}>
|
||||
{renderCardContent(query, idx)}
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Metrics;
|
||||
@@ -1,17 +0,0 @@
|
||||
export enum VIEWS {
|
||||
METRICS = 'metrics',
|
||||
LOGS = 'logs',
|
||||
TRACES = 'traces',
|
||||
CONTAINERS = 'containers',
|
||||
PROCESSES = 'processes',
|
||||
EVENTS = 'events',
|
||||
}
|
||||
|
||||
export const VIEW_TYPES = {
|
||||
METRICS: VIEWS.METRICS,
|
||||
LOGS: VIEWS.LOGS,
|
||||
TRACES: VIEWS.TRACES,
|
||||
CONTAINERS: VIEWS.CONTAINERS,
|
||||
PROCESSES: VIEWS.PROCESSES,
|
||||
EVENTS: VIEWS.EVENTS,
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
import HostMetricsDetails from './HostMetricsDetails';
|
||||
|
||||
export default HostMetricsDetails;
|
||||
@@ -13,7 +13,7 @@ import uPlot from 'uplot';
|
||||
|
||||
import { ChartProps } from '../types';
|
||||
|
||||
const TOOLTIP_WIDTH_PADDING = 60;
|
||||
const TOOLTIP_WIDTH_PADDING = 120;
|
||||
const TOOLTIP_MIN_WIDTH = 200;
|
||||
|
||||
export default function ChartWrapper({
|
||||
|
||||
194
frontend/src/container/InfraMonitoringHosts/Hosts.tsx
Normal file
194
frontend/src/container/InfraMonitoringHosts/Hosts.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { VerticalAlignTopOutlined } from '@ant-design/icons';
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import K8sBaseDetails from 'container/InfraMonitoringK8s/Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from 'container/InfraMonitoringK8s/Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from 'container/InfraMonitoringK8s/Base/types';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8s/constants';
|
||||
import {
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringSelectedItem,
|
||||
} from 'container/InfraMonitoringK8s/hooks';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { Filter } from 'lucide-react';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
fetchHostEntityData,
|
||||
fetchHostListData,
|
||||
getHostMetricsQueryPayload,
|
||||
hostDetailsMetadataConfig,
|
||||
hostGetEntityName,
|
||||
hostGetSelectedItemFilters,
|
||||
hostInitialEventsFilter,
|
||||
hostInitialLogTracesFilter,
|
||||
hostRenderEmptyState,
|
||||
hostWidgetInfo,
|
||||
} from './hostsData.utils';
|
||||
import {
|
||||
hostColumns,
|
||||
hostColumnsConfig,
|
||||
hostRenderRowData,
|
||||
} from './table.config';
|
||||
import { getHostsQuickFiltersConfig } from './utils';
|
||||
|
||||
import './InfraMonitoring.styles.scss';
|
||||
|
||||
function Hosts(): JSX.Element {
|
||||
const [showFilters, setShowFilters] = useState(true);
|
||||
const [, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [, setSelectedItem] = useInfraMonitoringSelectedItem();
|
||||
const [legacyHostName, setLegacyHostName] = useQueryState(
|
||||
'hostName',
|
||||
parseAsString,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (legacyHostName) {
|
||||
void setSelectedItem(legacyHostName);
|
||||
void setLegacyHostName(null);
|
||||
}
|
||||
}, [legacyHostName, setLegacyHostName, setSelectedItem]);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const handleFilterVisibilityChange = (): void => {
|
||||
setShowFilters(!showFilters);
|
||||
};
|
||||
|
||||
const handleQuickFiltersChange = (query: Query): void => {
|
||||
handleChangeQueryData('filters', query.builder.queryData[0].filters);
|
||||
setCurrentPage(1);
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.HostEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
});
|
||||
};
|
||||
|
||||
const fetchListData = useCallback(
|
||||
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
|
||||
filters.orderBy ||= {
|
||||
columnName: 'cpu',
|
||||
order: 'desc',
|
||||
};
|
||||
|
||||
return fetchHostListData(filters, signal);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: Parameters<typeof fetchHostEntityData>[0],
|
||||
signal?: AbortSignal,
|
||||
) => fetchHostEntityData(filters, signal),
|
||||
[],
|
||||
);
|
||||
|
||||
const getSelectedItemFilters = useCallback(
|
||||
(selectedItem: string) =>
|
||||
hostGetSelectedItemFilters(selectedItem, dotMetricsEnabled),
|
||||
[dotMetricsEnabled],
|
||||
);
|
||||
|
||||
const getInitialLogTracesFilters = useCallback(
|
||||
(host: import('api/infraMonitoring/getHostLists').HostData) =>
|
||||
hostInitialLogTracesFilter(host, dotMetricsEnabled),
|
||||
[dotMetricsEnabled],
|
||||
);
|
||||
|
||||
const primaryFilterKeys = useMemo(
|
||||
() => [dotMetricsEnabled ? 'host.name' : 'host_name'],
|
||||
[dotMetricsEnabled],
|
||||
);
|
||||
|
||||
const controlListPrefix = !showFilters ? (
|
||||
<div className="quick-filters-toggle-container">
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={handleFilterVisibilityChange}
|
||||
>
|
||||
<Filter size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<div className="hosts-list">
|
||||
<div className="hosts-list-content">
|
||||
{showFilters && (
|
||||
<div className="hosts-quick-filters-container">
|
||||
<div className="hosts-quick-filters-container-header">
|
||||
<Typography.Text>Filters</Typography.Text>
|
||||
<Tooltip title="Collapse Filters">
|
||||
<VerticalAlignTopOutlined
|
||||
rotate={270}
|
||||
onClick={handleFilterVisibilityChange}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={getHostsQuickFiltersConfig(dotMetricsEnabled)}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
onFilterChange={handleQuickFiltersChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="hosts-list-table-container">
|
||||
<K8sBaseList
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
tableColumnsDefinitions={hostColumns}
|
||||
tableColumns={hostColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={hostRenderRowData}
|
||||
renderEmptyState={hostRenderEmptyState}
|
||||
eventCategory={InfraMonitoringEvents.HostEntity}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<K8sBaseDetails
|
||||
category={InfraMonitoringEntity.HOSTS}
|
||||
eventCategory={InfraMonitoringEvents.HostEntity}
|
||||
getSelectedItemFilters={getSelectedItemFilters}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={hostGetEntityName}
|
||||
getInitialLogTracesFilters={getInitialLogTracesFilters}
|
||||
getInitialEventsFilters={hostInitialEventsFilter}
|
||||
primaryFilterKeys={primaryFilterKeys}
|
||||
metadataConfig={hostDetailsMetadataConfig}
|
||||
entityWidgetInfo={hostWidgetInfo}
|
||||
getEntityQueryPayload={getHostMetricsQueryPayload}
|
||||
queryKeyPrefix="hosts"
|
||||
tabsConfig={{
|
||||
showEvents: false,
|
||||
showContainers: true,
|
||||
showProcesses: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Hosts;
|
||||
@@ -1,250 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { VerticalAlignTopOutlined } from '@ant-design/icons';
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
|
||||
import HostMetricDetail from 'components/HostMetricsDetail';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import {
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringFiltersHosts,
|
||||
useInfraMonitoringOrderByHosts,
|
||||
} from 'container/InfraMonitoringK8s/hooks';
|
||||
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
|
||||
import { useGetHostList } from 'hooks/infraMonitoring/useGetHostList';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { Filter } from 'lucide-react';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { FeatureKeys } from '../../constants/features';
|
||||
import { useAppContext } from '../../providers/App/App';
|
||||
import HostsListControls from './HostsListControls';
|
||||
import HostsListTable from './HostsListTable';
|
||||
import { getHostListsQuery, GetHostsQuickFiltersConfig } from './utils';
|
||||
|
||||
import './InfraMonitoring.styles.scss';
|
||||
function HostsList(): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [filters, setFilters] = useInfraMonitoringFiltersHosts();
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderByHosts();
|
||||
|
||||
const [showFilters, setShowFilters] = useState<boolean>(true);
|
||||
|
||||
const [selectedHostName, setSelectedHostName] = useQueryState(
|
||||
'hostName',
|
||||
parseAsString.withDefault(''),
|
||||
);
|
||||
|
||||
const handleOrderByChange = (
|
||||
orderByValue: {
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
} | null,
|
||||
): void => {
|
||||
setOrderBy(orderByValue);
|
||||
};
|
||||
|
||||
const handleHostClick = (hostName: string): void => {
|
||||
setSelectedHostName(hostName);
|
||||
};
|
||||
|
||||
const { pageSize, setPageSize } = usePageSize('hosts');
|
||||
|
||||
const query = useMemo(() => {
|
||||
const baseQuery = getHostListsQuery();
|
||||
return {
|
||||
...baseQuery,
|
||||
limit: pageSize,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
filters,
|
||||
start: Math.floor(minTime / 1000000),
|
||||
end: Math.floor(maxTime / 1000000),
|
||||
orderBy,
|
||||
};
|
||||
}, [pageSize, currentPage, filters, minTime, maxTime, orderBy]);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedHostName) {
|
||||
return [
|
||||
'hostList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(filters),
|
||||
JSON.stringify(orderBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'hostList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(filters),
|
||||
JSON.stringify(orderBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
pageSize,
|
||||
currentPage,
|
||||
filters,
|
||||
orderBy,
|
||||
selectedHostName,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetHostList(
|
||||
query as HostListPayload,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
);
|
||||
|
||||
const hostMetricsData = useMemo(() => data?.payload?.data?.records || [], [
|
||||
data,
|
||||
]);
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const handleFiltersChange = useCallback(
|
||||
(value: IBuilderQuery['filters']): void => {
|
||||
const isNewFilterAdded = value?.items?.length !== filters?.items?.length;
|
||||
setFilters(value ?? null);
|
||||
handleChangeQueryData('filters', value);
|
||||
if (isNewFilterAdded) {
|
||||
setCurrentPage(1);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.HostEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[filters, setFilters, setCurrentPage, handleChangeQueryData],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
total: data?.payload?.data?.total,
|
||||
entity: InfraMonitoringEvents.HostEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
});
|
||||
}, [data?.payload?.data?.total]);
|
||||
|
||||
const selectedHostData = useMemo(() => {
|
||||
if (!selectedHostName?.trim()) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
hostMetricsData.find((host) => host.hostName === selectedHostName) || null
|
||||
);
|
||||
}, [selectedHostName, hostMetricsData]);
|
||||
|
||||
const handleCloseHostDetail = (): void => {
|
||||
setSelectedHostName(null);
|
||||
};
|
||||
|
||||
const handleFilterVisibilityChange = (): void => {
|
||||
setShowFilters(!showFilters);
|
||||
};
|
||||
|
||||
const handleQuickFiltersChange = (query: Query): void => {
|
||||
handleChangeQueryData('filters', query.builder.queryData[0].filters);
|
||||
handleFiltersChange(query.builder.queryData[0].filters);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="hosts-list">
|
||||
<div className="hosts-list-content">
|
||||
{showFilters && (
|
||||
<div className="hosts-quick-filters-container">
|
||||
<div className="hosts-quick-filters-container-header">
|
||||
<Typography.Text>Filters</Typography.Text>
|
||||
<Tooltip title="Collapse Filters">
|
||||
<VerticalAlignTopOutlined
|
||||
rotate={270}
|
||||
onClick={handleFilterVisibilityChange}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={GetHostsQuickFiltersConfig(dotMetricsEnabled)}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
onFilterChange={handleQuickFiltersChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="hosts-list-table-container">
|
||||
<div className="hosts-list-table-header">
|
||||
{!showFilters && (
|
||||
<div className="quick-filters-toggle-container">
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={handleFilterVisibilityChange}
|
||||
>
|
||||
<Filter size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<HostsListControls
|
||||
filters={filters}
|
||||
handleFiltersChange={handleFiltersChange}
|
||||
showAutoRefresh={!selectedHostData}
|
||||
/>
|
||||
</div>
|
||||
<HostsListTable
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
isError={isError}
|
||||
tableData={data}
|
||||
hostMetricsData={hostMetricsData}
|
||||
filters={filters || { items: [], op: 'AND' }}
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
onHostClick={handleHostClick}
|
||||
pageSize={pageSize}
|
||||
setPageSize={setPageSize}
|
||||
setOrderBy={handleOrderByChange}
|
||||
orderBy={orderBy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<HostMetricDetail
|
||||
host={selectedHostData}
|
||||
isModalTimeSelection
|
||||
onClose={handleCloseHostDetail}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HostsList;
|
||||
@@ -1,72 +0,0 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import './InfraMonitoring.styles.scss';
|
||||
|
||||
function HostsListControls({
|
||||
handleFiltersChange,
|
||||
filters,
|
||||
showAutoRefresh,
|
||||
}: {
|
||||
handleFiltersChange: (value: IBuilderQuery['filters']) => void;
|
||||
filters: IBuilderQuery['filters'] | null;
|
||||
showAutoRefresh: boolean;
|
||||
}): JSX.Element {
|
||||
const currentQuery = initialQueriesMap[DataSource.METRICS];
|
||||
const updatedCurrentQuery = useMemo(
|
||||
() => ({
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...currentQuery.builder.queryData[0],
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
...currentQuery.builder.queryData[0].aggregateAttribute,
|
||||
},
|
||||
filters,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
[currentQuery, filters],
|
||||
);
|
||||
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||
|
||||
const handleChangeTagFilters = useCallback(
|
||||
(value: IBuilderQuery['filters']) => {
|
||||
handleFiltersChange(value);
|
||||
},
|
||||
[handleFiltersChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="hosts-list-controls">
|
||||
<div className="hosts-list-controls-left">
|
||||
<QueryBuilderSearch
|
||||
query={query as IBuilderQuery}
|
||||
onChange={handleChangeTagFilters}
|
||||
isInfraMonitoring
|
||||
disableNavigationShortcuts
|
||||
entity={K8sCategory.HOSTS}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="time-selector">
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh={showAutoRefresh}
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HostsListControls;
|
||||
@@ -1,259 +0,0 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Skeleton,
|
||||
Spin,
|
||||
Table,
|
||||
TablePaginationConfig,
|
||||
TableProps,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import type { SorterResult } from 'antd/es/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import HostsEmptyOrIncorrectMetrics from './HostsEmptyOrIncorrectMetrics';
|
||||
import {
|
||||
EmptyOrLoadingViewProps,
|
||||
formatDataForTable,
|
||||
getHostsListColumns,
|
||||
HostRowData,
|
||||
HostsListTableProps,
|
||||
} from './utils';
|
||||
|
||||
function EmptyOrLoadingView(
|
||||
viewState: EmptyOrLoadingViewProps,
|
||||
): React.ReactNode {
|
||||
const { isError, errorMessage } = viewState;
|
||||
if (isError) {
|
||||
return <Typography>{errorMessage || 'Something went wrong'}</Typography>;
|
||||
}
|
||||
if (viewState.showHostsEmptyState) {
|
||||
return (
|
||||
<HostsEmptyOrIncorrectMetrics
|
||||
noData={!viewState.sentAnyHostMetricsData}
|
||||
incorrectData={viewState.isSendingIncorrectK8SAgentMetrics}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (viewState.showEndTimeBeforeRetentionMessage) {
|
||||
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" />
|
||||
<div className="no-hosts-message">
|
||||
<Typography.Title level={5} className="no-hosts-message-title">
|
||||
Queried time range is before earliest host metrics
|
||||
</Typography.Title>
|
||||
<Typography.Text className="no-hosts-message-text">
|
||||
Your requested end time is earlier than the earliest detected time of
|
||||
host metrics data, please adjust your end time.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (viewState.showNoRecordsInSelectedTimeRangeMessage) {
|
||||
return (
|
||||
<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.Title level={5} className="no-filtered-hosts-title">
|
||||
No host metrics found
|
||||
</Typography.Title>
|
||||
<Typography.Text className="no-filtered-hosts-message">
|
||||
No host metrics in the selected time range and filters. Please adjust your
|
||||
time range or filters.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (viewState.showTableLoadingState) {
|
||||
return (
|
||||
<div className="hosts-list-loading-state">
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function HostsListTable({
|
||||
isLoading,
|
||||
isFetching,
|
||||
isError,
|
||||
tableData: data,
|
||||
hostMetricsData,
|
||||
filters,
|
||||
onHostClick,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
pageSize,
|
||||
setOrderBy,
|
||||
setPageSize,
|
||||
orderBy,
|
||||
}: HostsListTableProps): JSX.Element {
|
||||
const [defaultOrderBy] = useState(orderBy);
|
||||
const columns = useMemo(() => getHostsListColumns(defaultOrderBy), [
|
||||
defaultOrderBy,
|
||||
]);
|
||||
|
||||
const sentAnyHostMetricsData = useMemo(
|
||||
() => data?.payload?.data?.sentAnyHostMetricsData || false,
|
||||
[data],
|
||||
);
|
||||
|
||||
const isSendingIncorrectK8SAgentMetrics = useMemo(
|
||||
() => data?.payload?.data?.isSendingK8SAgentMetrics || false,
|
||||
[data],
|
||||
);
|
||||
|
||||
const endTimeBeforeRetention = useMemo(
|
||||
() => data?.payload?.data?.endTimeBeforeRetention || false,
|
||||
[data],
|
||||
);
|
||||
|
||||
const formattedHostMetricsData = useMemo(
|
||||
() => formatDataForTable(hostMetricsData),
|
||||
[hostMetricsData],
|
||||
);
|
||||
|
||||
const totalCount = data?.payload?.data?.total || 0;
|
||||
|
||||
const handleTableChange: TableProps<HostRowData>['onChange'] = useCallback(
|
||||
(
|
||||
pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter: SorterResult<HostRowData> | SorterResult<HostRowData>[],
|
||||
): void => {
|
||||
if (pagination.current) {
|
||||
setCurrentPage(pagination.current);
|
||||
}
|
||||
|
||||
if ('field' in sorter && sorter.order) {
|
||||
setOrderBy({
|
||||
columnName: sorter.field as string,
|
||||
order: sorter.order === 'ascend' ? 'asc' : 'desc',
|
||||
});
|
||||
} else {
|
||||
setOrderBy(null);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleRowClick = (
|
||||
record: HostRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set('hostName', record.hostName);
|
||||
openInNewTab(`${window.location.pathname}?${params.toString()}`);
|
||||
return;
|
||||
}
|
||||
onHostClick(record.hostName);
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.HostEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
});
|
||||
};
|
||||
|
||||
const showHostsEmptyState =
|
||||
!isFetching &&
|
||||
!isLoading &&
|
||||
formattedHostMetricsData.length === 0 &&
|
||||
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics) &&
|
||||
!filters.items.length;
|
||||
|
||||
const showEndTimeBeforeRetentionMessage =
|
||||
!isFetching &&
|
||||
!isLoading &&
|
||||
formattedHostMetricsData.length === 0 &&
|
||||
endTimeBeforeRetention &&
|
||||
!filters.items.length;
|
||||
|
||||
const showNoRecordsInSelectedTimeRangeMessage =
|
||||
!isFetching &&
|
||||
!isLoading &&
|
||||
formattedHostMetricsData.length === 0 &&
|
||||
!showEndTimeBeforeRetentionMessage &&
|
||||
!showHostsEmptyState;
|
||||
|
||||
const showTableLoadingState =
|
||||
(isLoading || isFetching) && formattedHostMetricsData.length === 0;
|
||||
|
||||
const emptyOrLoadingView = EmptyOrLoadingView({
|
||||
isError,
|
||||
errorMessage: data?.error ?? '',
|
||||
showHostsEmptyState,
|
||||
sentAnyHostMetricsData,
|
||||
isSendingIncorrectK8SAgentMetrics,
|
||||
showEndTimeBeforeRetentionMessage,
|
||||
showNoRecordsInSelectedTimeRangeMessage,
|
||||
showTableLoadingState,
|
||||
});
|
||||
|
||||
if (emptyOrLoadingView) {
|
||||
return <>{emptyOrLoadingView}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
className="hosts-list-table"
|
||||
dataSource={showTableLoadingState ? [] : formattedHostMetricsData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
total: totalCount,
|
||||
showSizeChanger: true,
|
||||
hideOnSinglePage: false,
|
||||
onChange: (page, pageSize): void => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
loading={{
|
||||
spinning: showTableLoadingState,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
rowKey={(record): string =>
|
||||
(record as HostRowData & { key: string }).key ?? record.hostName
|
||||
}
|
||||
onChange={handleTableChange}
|
||||
onRow={(record: HostRowData): Record<string, unknown> => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -21,8 +21,8 @@
|
||||
|
||||
.ant-select-selector {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400) !important;
|
||||
background-color: var(--bg-ink-300) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
background-color: var(--l1-background) !important;
|
||||
|
||||
input {
|
||||
font-size: 12px;
|
||||
@@ -38,16 +38,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.clickable-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -61,12 +51,12 @@
|
||||
.hosts-quick-filters-container {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
border-right: 1px solid var(--bg-slate-400);
|
||||
border-right: 1px solid var(--border);
|
||||
height: 100%;
|
||||
|
||||
.hosts-quick-filters-container-header {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
border-bottom: 1px solid var(--border);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -88,166 +78,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hosts-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);
|
||||
}
|
||||
|
||||
.ant-table-cell:has(.hostname-column-value),
|
||||
.ant-table-cell:has(.hostname-cell-missing) {
|
||||
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;
|
||||
}
|
||||
|
||||
.hostname-cell-missing {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.hostname-cell-placeholder {
|
||||
color: var(--Vanilla-400, #c0c1c3);
|
||||
}
|
||||
|
||||
.hostname-cell-warning-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 4px;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
.status-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.memory-usage-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.column-header-right {
|
||||
text-align: right;
|
||||
}
|
||||
.column-header-left {
|
||||
text-align: left;
|
||||
}
|
||||
.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% - 364px);
|
||||
background: var(--bg-ink-500);
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
|
||||
// this is to offset chat support icon till we improve the design
|
||||
right: 20px;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.infra-monitoring-tags {
|
||||
@@ -341,76 +171,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
.hosts-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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hosts-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),
|
||||
.ant-table-cell:has(.hostname-cell-missing) {
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.hostname-column-value {
|
||||
color: var(--bg-ink-300);
|
||||
}
|
||||
|
||||
.hostname-cell-placeholder {
|
||||
color: var(--text-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,18 @@ import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { render } from '@testing-library/react';
|
||||
import * as useGetHostListHooks from 'hooks/infraMonitoring/useGetHostList';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import * as getHostListsApi from 'api/infraMonitoring/getHostLists';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import * as useQueryBuilderHooks from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import * as useQueryBuilderOperations from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { withNuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import * as appContextHooks from 'providers/App/App';
|
||||
import * as timezoneHooks from 'providers/Timezone';
|
||||
import store from 'store';
|
||||
import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
||||
|
||||
import HostsList from '../HostsList';
|
||||
import Hosts from '../Hosts';
|
||||
|
||||
jest.mock('lib/getMinMax', () => ({
|
||||
__esModule: true,
|
||||
@@ -19,6 +22,10 @@ jest.mock('lib/getMinMax', () => ({
|
||||
maxTime: 1713738000000,
|
||||
isValidShortHandDateTimeFormat: jest.fn().mockReturnValue(true),
|
||||
})),
|
||||
getMinMaxForSelectedTime: jest.fn().mockReturnValue({
|
||||
minTime: 1713734400000000000,
|
||||
maxTime: 1713738000000000000,
|
||||
}),
|
||||
}));
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
|
||||
__esModule: true,
|
||||
@@ -26,10 +33,6 @@ jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
|
||||
<div data-testid="date-time-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
jest.mock('components/HostMetricsDetail', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
__esModule: true,
|
||||
default: ({ onSelect, selectedTime, selectedValue }: any): JSX.Element => (
|
||||
@@ -41,21 +44,13 @@ jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: (): any => ({
|
||||
globalTime: {
|
||||
selectedTime: {
|
||||
startTime: 1713734400000,
|
||||
endTime: 1713738000000,
|
||||
},
|
||||
maxTime: 1713738000000,
|
||||
minTime: 1713734400000,
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const ROUTES = jest.requireActual('constants/routes').default;
|
||||
@@ -80,31 +75,45 @@ jest.spyOn(timezoneHooks, 'useTimezone').mockReturnValue({
|
||||
offset: 0,
|
||||
},
|
||||
} as any);
|
||||
jest.spyOn(useGetHostListHooks, 'useGetHostList').mockReturnValue({
|
||||
data: {
|
||||
payload: {
|
||||
data: {
|
||||
records: [
|
||||
{
|
||||
hostName: 'test-host',
|
||||
active: true,
|
||||
cpu: 0.75,
|
||||
memory: 0.65,
|
||||
wait: 0.03,
|
||||
},
|
||||
],
|
||||
isSendingK8SAgentMetrics: false,
|
||||
sentAnyHostMetricsData: true,
|
||||
},
|
||||
|
||||
jest.spyOn(getHostListsApi, 'getHostLists').mockResolvedValue({
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: {
|
||||
status: 'success',
|
||||
data: {
|
||||
type: 'list',
|
||||
records: [
|
||||
{
|
||||
hostName: 'test-host',
|
||||
active: true,
|
||||
os: 'linux',
|
||||
cpu: 0.75,
|
||||
cpuTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
memory: 0.65,
|
||||
memoryTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
wait: 0.03,
|
||||
waitTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
load15: 0.5,
|
||||
load15TimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
},
|
||||
],
|
||||
groups: null,
|
||||
total: 1,
|
||||
sentAnyHostMetricsData: true,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
endTimeBeforeRetention: false,
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
} as any);
|
||||
params: {} as any,
|
||||
});
|
||||
|
||||
jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
|
||||
user: {
|
||||
role: 'admin',
|
||||
},
|
||||
featureFlags: [],
|
||||
activeLicenseV3: {
|
||||
event_queue: {
|
||||
created_at: '0',
|
||||
@@ -125,36 +134,57 @@ jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
|
||||
},
|
||||
} as any);
|
||||
|
||||
jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue({
|
||||
currentQuery: initialQueriesMap.metrics,
|
||||
setSupersetQuery: jest.fn(),
|
||||
setLastUsedQuery: jest.fn(),
|
||||
handleSetConfig: jest.fn(),
|
||||
resetQuery: jest.fn(),
|
||||
updateAllQueriesOperators: jest.fn(),
|
||||
} as any);
|
||||
|
||||
jest.spyOn(useQueryBuilderOperations, 'useQueryOperations').mockReturnValue({
|
||||
handleChangeQueryData: jest.fn(),
|
||||
} as any);
|
||||
|
||||
const Wrapper = withNuqsTestingAdapter({ searchParams: {} });
|
||||
|
||||
describe('HostsList', () => {
|
||||
it('renders hosts list table', () => {
|
||||
const { container } = render(
|
||||
<Wrapper>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<HostsList />
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(container.querySelector('.hosts-list-table')).toBeInTheDocument();
|
||||
describe('Hosts', () => {
|
||||
beforeEach(() => {
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
it('renders filters', () => {
|
||||
it('renders hosts list table', async () => {
|
||||
const { container } = render(
|
||||
<Wrapper>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<HostsList />
|
||||
<Hosts />
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(container.querySelector('.filters')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('.ant-table')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders filters', async () => {
|
||||
const { container } = render(
|
||||
<Wrapper>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<Hosts />
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('.filters')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import HostsListControls from '../HostsListControls';
|
||||
|
||||
jest.mock('container/QueryBuilder/filters/QueryBuilderSearch', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="query-builder-search">Search</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="date-time-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('HostsListControls', () => {
|
||||
const mockHandleFiltersChange = jest.fn();
|
||||
const mockFilters = {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
it('renders search and date time filters', () => {
|
||||
render(
|
||||
<HostsListControls
|
||||
handleFiltersChange={mockHandleFiltersChange}
|
||||
filters={mockFilters}
|
||||
showAutoRefresh={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('query-builder-search')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('date-time-selection')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,276 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { HostData, HostListResponse } from 'api/infraMonitoring/getHostLists';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import HostsListTable from '../HostsListTable';
|
||||
import { HostsListTableProps } from '../utils';
|
||||
|
||||
const EMPTY_STATE_CONTAINER_CLASS = '.hosts-empty-state-container';
|
||||
|
||||
const createMockHost = (): HostData =>
|
||||
({
|
||||
hostName: 'test-host-1',
|
||||
active: true,
|
||||
cpu: 0.75,
|
||||
memory: 0.65,
|
||||
wait: 0.03,
|
||||
load15: 1.5,
|
||||
os: 'linux',
|
||||
cpuTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
memoryTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
waitTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
load15TimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
} as HostData);
|
||||
|
||||
const createMockTableData = (
|
||||
overrides: Partial<HostListResponse['data']> = {},
|
||||
): SuccessResponse<HostListResponse> => {
|
||||
const mockHost = createMockHost();
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: 'Success',
|
||||
error: null,
|
||||
payload: {
|
||||
status: 'success',
|
||||
data: {
|
||||
type: 'list',
|
||||
records: [mockHost],
|
||||
groups: null,
|
||||
total: 1,
|
||||
sentAnyHostMetricsData: true,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
endTimeBeforeRetention: false,
|
||||
...overrides,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('HostsListTable', () => {
|
||||
const mockHost = createMockHost();
|
||||
const mockTableData = createMockTableData();
|
||||
|
||||
const mockOnHostClick = jest.fn();
|
||||
const mockSetCurrentPage = jest.fn();
|
||||
const mockSetOrderBy = jest.fn();
|
||||
const mockSetPageSize = jest.fn();
|
||||
|
||||
const mockProps: HostsListTableProps = {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
isFetching: false,
|
||||
tableData: mockTableData,
|
||||
hostMetricsData: [mockHost],
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
onHostClick: mockOnHostClick,
|
||||
currentPage: 1,
|
||||
setCurrentPage: mockSetCurrentPage,
|
||||
pageSize: 10,
|
||||
setOrderBy: mockSetOrderBy,
|
||||
setPageSize: mockSetPageSize,
|
||||
orderBy: null,
|
||||
};
|
||||
|
||||
it('renders loading state if isLoading is true and tableData is empty', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
isLoading
|
||||
hostMetricsData={[]}
|
||||
tableData={createMockTableData({ records: [] })}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders loading state if isFetching is true and tableData is empty', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
isFetching
|
||||
hostMetricsData={[]}
|
||||
tableData={createMockTableData({ records: [] })}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders error state if isError is true', () => {
|
||||
render(<HostsListTable {...mockProps} isError />);
|
||||
expect(screen.getByText('Something went wrong')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders "Something went wrong" fallback when isError is true and error message is empty', () => {
|
||||
const tableDataWithEmptyError: ErrorResponse = {
|
||||
statusCode: 500,
|
||||
payload: null,
|
||||
error: '',
|
||||
message: null,
|
||||
};
|
||||
render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
isError
|
||||
hostMetricsData={[]}
|
||||
tableData={tableDataWithEmptyError}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders custom error message when isError is true and error message is provided', () => {
|
||||
const customErrorMessage = 'Failed to fetch host metrics';
|
||||
const tableDataWithError: ErrorResponse = {
|
||||
statusCode: 500,
|
||||
payload: null,
|
||||
error: customErrorMessage,
|
||||
message: null,
|
||||
};
|
||||
render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
isError
|
||||
hostMetricsData={[]}
|
||||
tableData={tableDataWithError}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText(customErrorMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders empty state if no hosts are found', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
tableData={createMockTableData({
|
||||
records: [],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
container.querySelector('.no-filtered-hosts-message-container'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders empty state if sentAnyHostMetricsData is false', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
tableData={createMockTableData({
|
||||
sentAnyHostMetricsData: false,
|
||||
records: [],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders empty state if isSendingK8SAgentMetrics is true', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
tableData={createMockTableData({
|
||||
isSendingK8SAgentMetrics: true,
|
||||
records: [],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders end time before retention message when endTimeBeforeRetention is true', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
tableData={createMockTableData({
|
||||
sentAnyHostMetricsData: true,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
endTimeBeforeRetention: true,
|
||||
records: [],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Your requested end time is earlier than the earliest detected time of host metrics data, please adjust your end time\./,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders no records message when noRecordsInSelectedTimeRangeAndFilters is true', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
tableData={createMockTableData({
|
||||
sentAnyHostMetricsData: true,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
records: [],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
container.querySelector('.no-filtered-hosts-message-container'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText(/No host metrics in the selected time range and filters/),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders no filtered hosts message when filters are present and no hosts are found', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
filters={{
|
||||
items: [
|
||||
{
|
||||
id: 'host_name',
|
||||
key: {
|
||||
key: 'host_name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isIndexed: true,
|
||||
},
|
||||
op: '=',
|
||||
value: 'unknown',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
}}
|
||||
tableData={createMockTableData({
|
||||
sentAnyHostMetricsData: true,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
records: [],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('.no-filtered-hosts-message')).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/No host metrics in the selected time range and filters\. Please adjust your time range or filters\./,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders table data', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
tableData={createMockTableData({
|
||||
isSendingK8SAgentMetrics: false,
|
||||
sentAnyHostMetricsData: true,
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('.hosts-list-table')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,8 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { HostData, TimeSeries } from 'api/infraMonitoring/getHostLists';
|
||||
|
||||
import {
|
||||
formatDataForTable,
|
||||
GetHostsQuickFiltersConfig,
|
||||
HostnameCell,
|
||||
} from '../utils';
|
||||
|
||||
const PROGRESS_BAR_CLASS = '.progress-bar';
|
||||
import { hostRenderRowData } from '../table.config';
|
||||
import { getHostsQuickFiltersConfig, HostnameCell } from '../utils';
|
||||
|
||||
const emptyTimeSeries: TimeSeries = {
|
||||
labels: {},
|
||||
@@ -16,94 +11,79 @@ const emptyTimeSeries: TimeSeries = {
|
||||
};
|
||||
|
||||
describe('InfraMonitoringHosts utils', () => {
|
||||
describe('formatDataForTable', () => {
|
||||
describe('hostRenderRowData', () => {
|
||||
it('should format host data correctly', () => {
|
||||
const mockData: HostData[] = [
|
||||
{
|
||||
hostName: 'test-host',
|
||||
active: true,
|
||||
cpu: 0.95,
|
||||
memory: 0.85,
|
||||
wait: 0.05,
|
||||
load15: 2.5,
|
||||
os: 'linux',
|
||||
cpuTimeSeries: emptyTimeSeries,
|
||||
memoryTimeSeries: emptyTimeSeries,
|
||||
waitTimeSeries: emptyTimeSeries,
|
||||
load15TimeSeries: emptyTimeSeries,
|
||||
},
|
||||
];
|
||||
const host: HostData = {
|
||||
hostName: 'test-host',
|
||||
active: true,
|
||||
cpu: 0.95,
|
||||
memory: 0.85,
|
||||
wait: 0.05,
|
||||
load15: 2.5,
|
||||
os: 'linux',
|
||||
cpuTimeSeries: emptyTimeSeries,
|
||||
memoryTimeSeries: emptyTimeSeries,
|
||||
waitTimeSeries: emptyTimeSeries,
|
||||
load15TimeSeries: emptyTimeSeries,
|
||||
};
|
||||
|
||||
const result = formatDataForTable(mockData);
|
||||
const result = hostRenderRowData(host, []);
|
||||
|
||||
expect(result[0].hostName).toBe('test-host');
|
||||
expect(result[0].wait).toBe('5%');
|
||||
expect(result[0].load15).toBe(2.5);
|
||||
expect(result.wait).toBe('5%');
|
||||
expect(result.load15).toBe(2.5);
|
||||
expect(result.itemKey).toBe('test-host');
|
||||
expect(result.hostName).toBe('test-host');
|
||||
|
||||
// Test active tag rendering
|
||||
const activeTag = render(result[0].active as JSX.Element);
|
||||
const activeTag = render(result.active as JSX.Element);
|
||||
expect(activeTag.container.textContent).toBe('ACTIVE');
|
||||
expect(activeTag.container.querySelector('.active')).toBeTruthy();
|
||||
expect(activeTag.getByText('ACTIVE')).toBeTruthy();
|
||||
|
||||
// Test CPU progress bar
|
||||
const cpuProgress = render(result[0].cpu as JSX.Element);
|
||||
const cpuProgressBar = cpuProgress.container.querySelector(
|
||||
PROGRESS_BAR_CLASS,
|
||||
);
|
||||
expect(cpuProgressBar).toBeTruthy();
|
||||
const cpuProgress = render(result.cpu as JSX.Element);
|
||||
expect(cpuProgress.container.querySelector('.ant-progress')).toBeTruthy();
|
||||
|
||||
// Test memory progress bar
|
||||
const memoryProgress = render(result[0].memory as JSX.Element);
|
||||
const memoryProgressBar = memoryProgress.container.querySelector(
|
||||
PROGRESS_BAR_CLASS,
|
||||
);
|
||||
expect(memoryProgressBar).toBeTruthy();
|
||||
const memoryProgress = render(result.memory as JSX.Element);
|
||||
expect(memoryProgress.container.querySelector('.ant-progress')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should handle inactive hosts', () => {
|
||||
const mockData: HostData[] = [
|
||||
{
|
||||
hostName: 'test-host',
|
||||
active: false,
|
||||
cpu: 0.3,
|
||||
memory: 0.4,
|
||||
wait: 0.02,
|
||||
load15: 1.2,
|
||||
os: 'linux',
|
||||
cpuTimeSeries: emptyTimeSeries,
|
||||
memoryTimeSeries: emptyTimeSeries,
|
||||
waitTimeSeries: emptyTimeSeries,
|
||||
load15TimeSeries: emptyTimeSeries,
|
||||
},
|
||||
];
|
||||
const host: HostData = {
|
||||
hostName: 'test-host',
|
||||
active: false,
|
||||
cpu: 0.3,
|
||||
memory: 0.4,
|
||||
wait: 0.02,
|
||||
load15: 1.2,
|
||||
os: 'linux',
|
||||
cpuTimeSeries: emptyTimeSeries,
|
||||
memoryTimeSeries: emptyTimeSeries,
|
||||
waitTimeSeries: emptyTimeSeries,
|
||||
load15TimeSeries: emptyTimeSeries,
|
||||
};
|
||||
|
||||
const result = formatDataForTable(mockData);
|
||||
const result = hostRenderRowData(host, []);
|
||||
|
||||
const inactiveTag = render(result[0].active as JSX.Element);
|
||||
const inactiveTag = render(result.active as JSX.Element);
|
||||
expect(inactiveTag.container.textContent).toBe('INACTIVE');
|
||||
expect(inactiveTag.container.querySelector('.inactive')).toBeTruthy();
|
||||
expect(inactiveTag.getByText('INACTIVE')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set hostName to empty string when host has no hostname', () => {
|
||||
const mockData: HostData[] = [
|
||||
{
|
||||
hostName: '',
|
||||
active: true,
|
||||
cpu: 0.5,
|
||||
memory: 0.4,
|
||||
wait: 0.01,
|
||||
load15: 1.0,
|
||||
os: 'linux',
|
||||
cpuTimeSeries: emptyTimeSeries,
|
||||
memoryTimeSeries: emptyTimeSeries,
|
||||
waitTimeSeries: emptyTimeSeries,
|
||||
load15TimeSeries: emptyTimeSeries,
|
||||
},
|
||||
];
|
||||
it('should use empty itemKey when host has no hostname', () => {
|
||||
const host: HostData = {
|
||||
hostName: '',
|
||||
active: true,
|
||||
cpu: 0.5,
|
||||
memory: 0.4,
|
||||
wait: 0.01,
|
||||
load15: 1.0,
|
||||
os: 'linux',
|
||||
cpuTimeSeries: emptyTimeSeries,
|
||||
memoryTimeSeries: emptyTimeSeries,
|
||||
waitTimeSeries: emptyTimeSeries,
|
||||
load15TimeSeries: emptyTimeSeries,
|
||||
};
|
||||
|
||||
const result = formatDataForTable(mockData);
|
||||
expect(result[0].hostName).toBe('');
|
||||
expect(result[0].key).toBe('-0');
|
||||
const result = hostRenderRowData(host, []);
|
||||
expect(result.itemKey).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -126,7 +106,6 @@ describe('InfraMonitoringHosts utils', () => {
|
||||
'Missing host.name metadata',
|
||||
);
|
||||
expect(iconWrapper?.getAttribute('tabindex')).toBe('0');
|
||||
// Tooltip with "Learn how to configure →" link is shown on hover/focus
|
||||
});
|
||||
|
||||
it('should render placeholder and icon when hostName is whitespace only (case C)', () => {
|
||||
@@ -144,9 +123,9 @@ describe('InfraMonitoringHosts utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetHostsQuickFiltersConfig', () => {
|
||||
describe('getHostsQuickFiltersConfig', () => {
|
||||
it('should return correct config when dotMetricsEnabled is true', () => {
|
||||
const result = GetHostsQuickFiltersConfig(true);
|
||||
const result = getHostsQuickFiltersConfig(true);
|
||||
|
||||
expect(result[0].attributeKey.key).toBe('host.name');
|
||||
expect(result[1].attributeKey.key).toBe('os.type');
|
||||
@@ -154,7 +133,7 @@ describe('InfraMonitoringHosts utils', () => {
|
||||
});
|
||||
|
||||
it('should return correct config when dotMetricsEnabled is false', () => {
|
||||
const result = GetHostsQuickFiltersConfig(false);
|
||||
const result = getHostsQuickFiltersConfig(false);
|
||||
|
||||
expect(result[0].attributeKey.key).toBe('host_name');
|
||||
expect(result[1].attributeKey.key).toBe('os_type');
|
||||
234
frontend/src/container/InfraMonitoringHosts/hostsData.utils.tsx
Normal file
234
frontend/src/container/InfraMonitoringHosts/hostsData.utils.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import React from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, Tag, Typography } from 'antd';
|
||||
import {
|
||||
getHostLists,
|
||||
HostData,
|
||||
HostListPayload,
|
||||
HostListResponse,
|
||||
} from 'api/infraMonitoring/getHostLists';
|
||||
import {
|
||||
createFilterItem,
|
||||
K8sDetailsFilters,
|
||||
K8sDetailsMetadataConfig,
|
||||
} from 'container/InfraMonitoringK8s/Base/K8sBaseDetails';
|
||||
import type { K8sBaseListEmptyStateContext } from 'container/InfraMonitoringK8s/Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from 'container/InfraMonitoringK8s/Base/types';
|
||||
import {
|
||||
getHostQueryPayload,
|
||||
hostWidgetInfo,
|
||||
} from 'container/LogDetailedView/InfraMetrics/constants';
|
||||
import {
|
||||
TagFilter,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import HostsEmptyOrIncorrectMetrics from './HostsEmptyOrIncorrectMetrics';
|
||||
import { getHostListsQuery } from './utils';
|
||||
|
||||
import './InfraMonitoring.styles.scss';
|
||||
|
||||
function getProgressColor(percent: number): string {
|
||||
if (percent >= 90) {
|
||||
return Color.BG_SAKURA_500;
|
||||
}
|
||||
if (percent >= 60) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
return Color.BG_FOREST_500;
|
||||
}
|
||||
|
||||
function getMemoryProgressColor(percent: number): string {
|
||||
if (percent >= 90) {
|
||||
return Color.BG_CHERRY_500;
|
||||
}
|
||||
if (percent >= 60) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
return Color.BG_FOREST_500;
|
||||
}
|
||||
|
||||
export const hostDetailsMetadataConfig: K8sDetailsMetadataConfig<HostData>[] = [
|
||||
{
|
||||
label: 'STATUS',
|
||||
getValue: (h): string => (h.active ? 'ACTIVE' : 'INACTIVE'),
|
||||
render: (value, h): React.ReactNode => (
|
||||
<Tag
|
||||
className={`infra-monitoring-tags ${h.active ? 'active' : 'inactive'}`}
|
||||
bordered
|
||||
>
|
||||
{value}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'OPERATING SYSTEM',
|
||||
getValue: (h): string => h.os || '-',
|
||||
render: (value): React.ReactNode =>
|
||||
value !== '-' ? (
|
||||
<Tag className="infra-monitoring-tags" bordered>
|
||||
{value}
|
||||
</Tag>
|
||||
) : (
|
||||
<Typography.Text>-</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'CPU USAGE',
|
||||
getValue: (h): number => h.cpu * 100,
|
||||
render: (value): React.ReactNode => (
|
||||
<Progress
|
||||
percent={Number(Number(value).toFixed(1))}
|
||||
size="small"
|
||||
strokeColor={getProgressColor(Number(value))}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'MEMORY USAGE',
|
||||
getValue: (h): number => h.memory * 100,
|
||||
render: (value): React.ReactNode => (
|
||||
<Progress
|
||||
percent={Number(Number(value).toFixed(1))}
|
||||
size="small"
|
||||
strokeColor={getMemoryProgressColor(Number(value))}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export function getHostMetricsQueryPayload(
|
||||
host: HostData,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
): ReturnType<typeof getHostQueryPayload> {
|
||||
return getHostQueryPayload(host.hostName, start, end, dotMetricsEnabled);
|
||||
}
|
||||
|
||||
export { hostWidgetInfo };
|
||||
|
||||
export function hostGetSelectedItemFilters(
|
||||
selectedItem: string,
|
||||
dotMetricsEnabled: boolean,
|
||||
): TagFilter {
|
||||
const hostKey = dotMetricsEnabled ? 'host.name' : 'host_name';
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [createFilterItem(hostKey, selectedItem)],
|
||||
};
|
||||
}
|
||||
|
||||
export function hostInitialLogTracesFilter(
|
||||
host: HostData,
|
||||
dotMetricsEnabled: boolean,
|
||||
): TagFilterItem[] {
|
||||
const hostKey = dotMetricsEnabled ? 'host.name' : 'host_name';
|
||||
return [createFilterItem(hostKey, host.hostName || '')];
|
||||
}
|
||||
|
||||
export function hostInitialEventsFilter(_host: HostData): TagFilterItem[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
export const hostGetEntityName = (host: HostData): string => host.hostName;
|
||||
|
||||
export async function fetchHostListData(
|
||||
filters: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{
|
||||
data: HostData[];
|
||||
total: number;
|
||||
error?: string | null;
|
||||
rawData?: unknown;
|
||||
}> {
|
||||
const baseQuery = getHostListsQuery();
|
||||
const payload: HostListPayload = {
|
||||
...baseQuery,
|
||||
limit: filters.limit,
|
||||
offset: filters.offset,
|
||||
filters: filters.filters ?? { items: [], op: 'and' },
|
||||
orderBy: filters.orderBy,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
groupBy: filters.groupBy ?? [],
|
||||
};
|
||||
|
||||
const response = await getHostLists(payload, signal);
|
||||
|
||||
return {
|
||||
data: response.payload?.data?.records || [],
|
||||
total: response.payload?.data?.total || 0,
|
||||
error: response.error,
|
||||
rawData: response.payload?.data,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchHostEntityData(
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ data: HostData | null; error?: string | null }> {
|
||||
const response = await getHostLists(
|
||||
{
|
||||
...getHostListsQuery(),
|
||||
filters: filters.filters,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
groupBy: [],
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const records = response.payload?.data?.records || [];
|
||||
|
||||
return {
|
||||
data: records.length > 0 ? records[0] : null,
|
||||
error: response.error,
|
||||
};
|
||||
}
|
||||
|
||||
function EndTimeBeforeRetentionMessage(): 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" />
|
||||
<div className="no-hosts-message">
|
||||
<Typography.Title level={5} className="no-hosts-message-title">
|
||||
Queried time range is before earliest host metrics
|
||||
</Typography.Title>
|
||||
<Typography.Text className="no-hosts-message-text">
|
||||
Your requested end time is earlier than the earliest detected time of host
|
||||
metrics data, please adjust your end time.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function hostRenderEmptyState(
|
||||
context: K8sBaseListEmptyStateContext,
|
||||
): React.ReactNode | null {
|
||||
if (context.isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawData = context.rawData as HostListResponse['data'] | undefined;
|
||||
|
||||
if (!rawData?.sentAnyHostMetricsData || rawData?.isSendingK8SAgentMetrics) {
|
||||
return (
|
||||
<HostsEmptyOrIncorrectMetrics
|
||||
noData={!rawData?.sentAnyHostMetricsData}
|
||||
incorrectData={!!rawData?.isSendingK8SAgentMetrics}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (rawData?.endTimeBeforeRetention) {
|
||||
return <EndTimeBeforeRetentionMessage />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import HostsList from './HostsList';
|
||||
import Hosts from './Hosts';
|
||||
|
||||
import './InfraMonitoring.styles.scss';
|
||||
|
||||
@@ -65,7 +65,7 @@ function InfraMonitoringHosts(): JSX.Element {
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className="infra-monitoring-container">
|
||||
<div className="hosts-list-container">
|
||||
<HostsList />
|
||||
<Hosts />
|
||||
</div>
|
||||
</div>
|
||||
</Sentry.ErrorBoundary>
|
||||
|
||||
254
frontend/src/container/InfraMonitoringHosts/table.config.tsx
Normal file
254
frontend/src/container/InfraMonitoringHosts/table.config.tsx
Normal file
@@ -0,0 +1,254 @@
|
||||
import React from 'react';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
|
||||
import { HostData } from 'api/infraMonitoring/getHostLists';
|
||||
import { K8sRenderedRowData } from 'container/InfraMonitoringK8s/Base/types';
|
||||
import { IEntityColumn } from 'container/InfraMonitoringK8s/Base/useInfraMonitoringTableColumnsStore';
|
||||
import {
|
||||
getGroupByEl,
|
||||
getGroupedByMeta,
|
||||
getRowKey,
|
||||
} from 'container/InfraMonitoringK8s/Base/utils';
|
||||
import { ValidateColumnValueWrapper } from 'container/InfraMonitoringK8s/commonUtils';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8s/constants';
|
||||
import { Group } from 'lucide-react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { HostnameCell } from './utils';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
|
||||
export const hostColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'Host group',
|
||||
value: 'hostGroup',
|
||||
id: 'hostGroup',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-collapse',
|
||||
},
|
||||
{
|
||||
label: 'Hostname',
|
||||
value: 'hostName',
|
||||
id: 'hostName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-expand',
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
value: 'active',
|
||||
id: 'active',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Memory Usage',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'IOWait',
|
||||
value: 'wait',
|
||||
id: 'wait',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Load Avg',
|
||||
value: 'load15',
|
||||
id: 'load15',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
];
|
||||
|
||||
export const hostColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
|
||||
{
|
||||
title: (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
<Group size={14} /> HOST GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'hostGroup',
|
||||
key: 'hostGroup',
|
||||
ellipsis: true,
|
||||
width: 180,
|
||||
sorter: false,
|
||||
},
|
||||
{
|
||||
title: <div className={styles.hostnameColumnHeader}>Hostname</div>,
|
||||
dataIndex: 'hostName',
|
||||
key: 'hostName',
|
||||
width: 250,
|
||||
render: (_value, record): React.ReactNode => (
|
||||
<HostnameCell
|
||||
hostName={typeof record.hostName === 'string' ? record.hostName : ''}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<div className={styles.statusHeader}>
|
||||
Status
|
||||
<Tooltip title="Sent system metrics in last 10 mins">
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'active',
|
||||
key: 'active',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: <div className={styles.columnHeaderRight}>CPU Usage</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<div className={`${styles.columnHeaderRight} ${styles.memoryUsageHeader}`}>
|
||||
Memory Usage
|
||||
<Tooltip title="Excluding cache memory">
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: <div className={styles.columnHeaderRight}>IOWait</div>,
|
||||
dataIndex: 'wait',
|
||||
key: 'wait',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: <div className={styles.columnHeaderRight}>Load Avg</div>,
|
||||
dataIndex: 'load15',
|
||||
key: 'load15',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
align: 'right',
|
||||
},
|
||||
];
|
||||
|
||||
function hostRowSource(host: HostData): { meta: Record<string, string> } {
|
||||
return {
|
||||
meta: {
|
||||
...(host.meta ?? {}),
|
||||
host_name: host.hostName ?? '',
|
||||
'host.name': host.hostName ?? '',
|
||||
os_type: host.os ?? '',
|
||||
'os.type': host.os ?? '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function progressStrokeColor(percent: number): string {
|
||||
if (percent >= 90) {
|
||||
return Color.BG_SAKURA_500;
|
||||
}
|
||||
if (percent >= 60) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
return Color.BG_FOREST_500;
|
||||
}
|
||||
|
||||
function memoryProgressStrokeColor(percent: number): string {
|
||||
if (percent >= 90) {
|
||||
return Color.BG_CHERRY_500;
|
||||
}
|
||||
if (percent >= 60) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
return Color.BG_FOREST_500;
|
||||
}
|
||||
|
||||
export const hostRenderRowData = (
|
||||
host: HostData,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): K8sRenderedRowData => {
|
||||
const synthetic = hostRowSource(host);
|
||||
const rowKey = getRowKey(synthetic, () => host.hostName || 'unknown', groupBy);
|
||||
const groupedByMeta = getGroupedByMeta(synthetic, groupBy);
|
||||
const cpuPercent = Number((host.cpu * 100).toFixed(1));
|
||||
const memoryPercent = Number((host.memory * 100).toFixed(1));
|
||||
|
||||
return {
|
||||
key: rowKey,
|
||||
itemKey: host.hostName ?? '',
|
||||
groupedByMeta,
|
||||
meta: synthetic.meta,
|
||||
hostGroup: getGroupByEl(synthetic, groupBy),
|
||||
...synthetic.meta,
|
||||
hostName: host.hostName ?? '',
|
||||
active: (
|
||||
<Tag
|
||||
bordered
|
||||
className={`${styles.statusTag} ${
|
||||
host.active ? styles.statusTagActive : styles.statusTagInactive
|
||||
}`}
|
||||
>
|
||||
{host.active ? 'ACTIVE' : 'INACTIVE'}
|
||||
</Tag>
|
||||
),
|
||||
cpu: (
|
||||
<div className={styles.progressContainer}>
|
||||
<ValidateColumnValueWrapper
|
||||
value={host.cpu}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
>
|
||||
<Progress
|
||||
percent={cpuPercent}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
strokeColor={progressStrokeColor(cpuPercent)}
|
||||
className={styles.progressBar}
|
||||
/>
|
||||
</ValidateColumnValueWrapper>
|
||||
</div>
|
||||
),
|
||||
memory: (
|
||||
<div className={styles.progressContainer}>
|
||||
<ValidateColumnValueWrapper
|
||||
value={host.memory}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
>
|
||||
<Progress
|
||||
percent={memoryPercent}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
strokeColor={memoryProgressStrokeColor(memoryPercent)}
|
||||
className={styles.progressBar}
|
||||
/>
|
||||
</ValidateColumnValueWrapper>
|
||||
</div>
|
||||
),
|
||||
wait: `${Number((host.wait * 100).toFixed(1))}%`,
|
||||
load15: host.load15,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
.entityGroupHeader {
|
||||
padding-left: var(--spacing-5);
|
||||
gap: var(--spacing-5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hostnameColumnHeader {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.statusHeader {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.columnHeaderRight {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.memoryUsageHeader {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-2);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.statusTag {
|
||||
width: fit-content;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.44px;
|
||||
text-transform: uppercase;
|
||||
border-radius: 50px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.statusTagActive {
|
||||
color: var(--Forest-500, #25e192);
|
||||
border: 1px solid rgba(37, 225, 146, 0.2);
|
||||
background: rgba(37, 225, 146, 0.1);
|
||||
}
|
||||
|
||||
.statusTagInactive {
|
||||
color: var(--Slate-50, #62687c);
|
||||
border: 1px solid rgba(98, 104, 124, 0.2);
|
||||
background: rgba(98, 104, 124, 0.1);
|
||||
}
|
||||
|
||||
.progressContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& > div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:global(.ant-progress-bg) {
|
||||
height: 8px !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
}
|
||||
@@ -1,41 +1,15 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import React from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, TabsProps, Tag, Tooltip, Typography } from 'antd';
|
||||
import { TableColumnType as ColumnType } from 'antd';
|
||||
import { SortOrder } from 'antd/lib/table/interface';
|
||||
import {
|
||||
HostData,
|
||||
HostListPayload,
|
||||
HostListResponse,
|
||||
} from 'api/infraMonitoring/getHostLists';
|
||||
import { Tooltip, Typography } from 'antd';
|
||||
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
|
||||
import {
|
||||
FiltersType,
|
||||
IQuickFiltersConfig,
|
||||
} from 'components/QuickFilters/types';
|
||||
import TabLabel from 'components/TabLabel';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { TriangleAlert } from 'lucide-react';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { OrderBySchemaType } from '../InfraMonitoringK8s/schemas';
|
||||
import HostsList from './HostsList';
|
||||
|
||||
import './InfraMonitoring.styles.scss';
|
||||
|
||||
export interface HostRowData {
|
||||
key?: string;
|
||||
hostName: string;
|
||||
cpu: React.ReactNode;
|
||||
memory: React.ReactNode;
|
||||
wait: string;
|
||||
load15: number;
|
||||
active: React.ReactNode;
|
||||
}
|
||||
|
||||
const HOSTNAME_DOCS_URL =
|
||||
'https://signoz.io/docs/infrastructure-monitoring/hostmetrics/#host-name-is-blankempty';
|
||||
|
||||
@@ -89,38 +63,6 @@ export function HostnameCell({
|
||||
);
|
||||
}
|
||||
|
||||
export interface HostsListTableProps {
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
isFetching: boolean;
|
||||
tableData:
|
||||
| SuccessResponse<HostListResponse, unknown>
|
||||
| ErrorResponse
|
||||
| undefined;
|
||||
hostMetricsData: HostData[];
|
||||
filters: TagFilter;
|
||||
onHostClick: (hostName: string) => void;
|
||||
currentPage: number;
|
||||
setCurrentPage: Dispatch<SetStateAction<number>>;
|
||||
pageSize: number;
|
||||
setOrderBy: (
|
||||
orderBy: { columnName: string; order: 'asc' | 'desc' } | null,
|
||||
) => void;
|
||||
setPageSize: (pageSize: number) => void;
|
||||
orderBy: OrderBySchemaType;
|
||||
}
|
||||
|
||||
export interface EmptyOrLoadingViewProps {
|
||||
isError: boolean;
|
||||
errorMessage: string;
|
||||
showHostsEmptyState: boolean;
|
||||
sentAnyHostMetricsData: boolean;
|
||||
isSendingIncorrectK8SAgentMetrics: boolean;
|
||||
showEndTimeBeforeRetentionMessage: boolean;
|
||||
showNoRecordsInSelectedTimeRangeMessage: boolean;
|
||||
showTableLoadingState: boolean;
|
||||
}
|
||||
|
||||
export const getHostListsQuery = (): HostListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
@@ -130,151 +72,6 @@ export const getHostListsQuery = (): HostListPayload => ({
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
function mapOrderByToSortOrder(
|
||||
column: string,
|
||||
orderBy: OrderBySchemaType,
|
||||
): SortOrder | undefined {
|
||||
return orderBy?.columnName === column
|
||||
? orderBy?.order === 'asc'
|
||||
? 'ascend'
|
||||
: 'descend'
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export const getTabsItems = (): TabsProps['items'] => [
|
||||
{
|
||||
label: <TabLabel label="List View" isDisabled={false} tooltipText="" />,
|
||||
key: PANEL_TYPES.LIST,
|
||||
children: <HostsList />,
|
||||
},
|
||||
];
|
||||
|
||||
export const getHostsListColumns = (
|
||||
orderBy: OrderBySchemaType,
|
||||
): ColumnType<HostRowData>[] => [
|
||||
{
|
||||
title: <div className="hostname-column-header">Hostname</div>,
|
||||
dataIndex: 'hostName',
|
||||
key: 'hostName',
|
||||
width: 250,
|
||||
render: (value: string | undefined): React.ReactNode => (
|
||||
<HostnameCell hostName={value ?? ''} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<div className="status-header">
|
||||
Status
|
||||
<Tooltip title="Sent system metrics in last 10 mins">
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'active',
|
||||
key: 'active',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-right">CPU Usage</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
defaultSortOrder: mapOrderByToSortOrder('cpu', orderBy),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<div className="column-header-right memory-usage-header">
|
||||
Memory Usage
|
||||
<Tooltip title="Excluding cache memory">
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
defaultSortOrder: mapOrderByToSortOrder('memory', orderBy),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-right">IOWait</div>,
|
||||
dataIndex: 'wait',
|
||||
key: 'wait',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
defaultSortOrder: mapOrderByToSortOrder('wait', orderBy),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-right">Load Avg</div>,
|
||||
dataIndex: 'load15',
|
||||
key: 'load15',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
defaultSortOrder: mapOrderByToSortOrder('load15', orderBy),
|
||||
align: 'right',
|
||||
},
|
||||
];
|
||||
|
||||
export const formatDataForTable = (data: HostData[]): HostRowData[] =>
|
||||
data.map((host, index) => ({
|
||||
key: `${host.hostName}-${index}`,
|
||||
hostName: host.hostName || '',
|
||||
active: (
|
||||
<Tag
|
||||
bordered
|
||||
className={`infra-monitoring-tags ${host.active ? 'active' : 'inactive'}`}
|
||||
>
|
||||
{host.active ? 'ACTIVE' : 'INACTIVE'}
|
||||
</Tag>
|
||||
),
|
||||
cpu: (
|
||||
<div className="progress-container">
|
||||
<Progress
|
||||
percent={Number((host.cpu * 100).toFixed(1))}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
strokeColor={((): string => {
|
||||
const cpuPercent = Number((host.cpu * 100).toFixed(1));
|
||||
if (cpuPercent >= 90) {
|
||||
return Color.BG_SAKURA_500;
|
||||
}
|
||||
if (cpuPercent >= 60) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
return Color.BG_FOREST_500;
|
||||
})()}
|
||||
className="progress-bar"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
memory: (
|
||||
<div className="progress-container">
|
||||
<Progress
|
||||
percent={Number((host.memory * 100).toFixed(1))}
|
||||
strokeLinecap="butt"
|
||||
size="small"
|
||||
strokeColor={((): string => {
|
||||
const memoryPercent = Number((host.memory * 100).toFixed(1));
|
||||
if (memoryPercent >= 90) {
|
||||
return Color.BG_CHERRY_500;
|
||||
}
|
||||
if (memoryPercent >= 60) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
return Color.BG_FOREST_500;
|
||||
})()}
|
||||
className="progress-bar"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
wait: `${Number((host.wait * 100).toFixed(1))}%`,
|
||||
load15: host.load15,
|
||||
}));
|
||||
|
||||
export const HostsQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
@@ -304,13 +101,11 @@ export const HostsQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export function GetHostsQuickFiltersConfig(
|
||||
export function getHostsQuickFiltersConfig(
|
||||
dotMetricsEnabled: boolean,
|
||||
): IQuickFiltersConfig[] {
|
||||
// These keys don’t change with dotMetricsEnabled
|
||||
const hostNameKey = dotMetricsEnabled ? 'host.name' : 'host_name';
|
||||
const osTypeKey = dotMetricsEnabled ? 'os.type' : 'os_type';
|
||||
// This metric stays the same regardless of notation
|
||||
const metricName = dotMetricsEnabled
|
||||
? 'system.cpu.load_average.15m'
|
||||
: 'system_cpu_load_average_15m';
|
||||
|
||||
@@ -0,0 +1,872 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
|
||||
import type { RadioChangeEvent } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryState,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import {
|
||||
BarChart2,
|
||||
ChevronsLeftRight,
|
||||
Compass,
|
||||
DraftingCompass,
|
||||
Package2,
|
||||
ScrollText,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilter,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
LogsAggregatorOperator,
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import {
|
||||
isCustomTimeRange,
|
||||
useGlobalTimeStore,
|
||||
} from '../../../store/globalTime';
|
||||
import {
|
||||
getAutoRefreshQueryKey,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from '../../../store/globalTime/utils';
|
||||
import { DEFAULT_TIME_RANGE } from '../../TopNav/DateTimeSelectionV2/constants';
|
||||
import { filterDuplicateFilters } from '../commonUtils';
|
||||
import { InfraMonitoringEntity, VIEW_TYPES, VIEWS } from '../constants';
|
||||
import EntityContainers from '../EntityDetailsUtils/EntityContainers';
|
||||
import EntityEvents from '../EntityDetailsUtils/EntityEvents';
|
||||
import EntityLogs from '../EntityDetailsUtils/EntityLogs';
|
||||
import EntityMetrics from '../EntityDetailsUtils/EntityMetrics';
|
||||
import EntityProcesses from '../EntityDetailsUtils/EntityProcesses';
|
||||
import EntityTraces from '../EntityDetailsUtils/EntityTraces';
|
||||
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
|
||||
import {
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringSelectedItem,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from '../hooks';
|
||||
import LoadingContainer from '../LoadingContainer';
|
||||
|
||||
import '../EntityDetailsUtils/entityDetails.styles.scss';
|
||||
|
||||
const TimeRangeOffset = 1000000000;
|
||||
|
||||
export interface K8sDetailsMetadataConfig<T> {
|
||||
label: string;
|
||||
getValue: (entity: T) => string | number;
|
||||
render?: (value: string | number, entity: T) => React.ReactNode;
|
||||
}
|
||||
|
||||
export interface K8sDetailsFilters {
|
||||
filters: TagFilter;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface K8sBaseDetailsProps<T> {
|
||||
category: InfraMonitoringEntity;
|
||||
eventCategory: string;
|
||||
// Data fetching configuration
|
||||
getSelectedItemFilters: (selectedItem: string) => TagFilter;
|
||||
fetchEntityData: (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<{ data: T | null; error?: string | null }>;
|
||||
// Entity configuration
|
||||
getEntityName: (entity: T) => string;
|
||||
getInitialLogTracesFilters: (entity: T) => TagFilterItem[];
|
||||
getInitialEventsFilters: (entity: T) => TagFilterItem[];
|
||||
primaryFilterKeys: string[];
|
||||
metadataConfig: K8sDetailsMetadataConfig<T>[];
|
||||
entityWidgetInfo: {
|
||||
title: string;
|
||||
yAxisUnit: string;
|
||||
}[];
|
||||
getEntityQueryPayload: (
|
||||
entity: T,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
) => GetQueryResultsProps[];
|
||||
queryKeyPrefix: string;
|
||||
/** When true, only metrics are shown and the Metrics/Logs/Traces/Events tab bar is hidden. */
|
||||
hideDetailViewTabs?: boolean;
|
||||
tabsConfig?: {
|
||||
showMetrics?: boolean;
|
||||
showLogs?: boolean;
|
||||
showTraces?: boolean;
|
||||
showEvents?: boolean;
|
||||
showContainers?: boolean;
|
||||
showProcesses?: boolean;
|
||||
};
|
||||
customTabs?: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
render: (props: {
|
||||
entity: T;
|
||||
timeRange: { startTime: number; endTime: number };
|
||||
selectedInterval: Time;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
}) => React.ReactNode;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function createFilterItem(
|
||||
key: string,
|
||||
value: string,
|
||||
dataType: DataTypes = DataTypes.String,
|
||||
): TagFilterItem {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key,
|
||||
dataType,
|
||||
type: 'resource',
|
||||
id: `${key}--string--resource--false`,
|
||||
},
|
||||
op: '=',
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function K8sBaseDetails<T>({
|
||||
category,
|
||||
eventCategory,
|
||||
getSelectedItemFilters,
|
||||
fetchEntityData,
|
||||
getEntityName,
|
||||
getInitialLogTracesFilters,
|
||||
getInitialEventsFilters,
|
||||
primaryFilterKeys,
|
||||
metadataConfig,
|
||||
entityWidgetInfo,
|
||||
getEntityQueryPayload,
|
||||
queryKeyPrefix,
|
||||
hideDetailViewTabs = false,
|
||||
tabsConfig,
|
||||
customTabs,
|
||||
}: K8sBaseDetailsProps<T>): JSX.Element {
|
||||
const tabVisibility = useMemo(
|
||||
() => ({
|
||||
showMetrics: true,
|
||||
showLogs: true,
|
||||
showTraces: true,
|
||||
showEvents: true,
|
||||
showContainers: false,
|
||||
showProcesses: false,
|
||||
...tabsConfig,
|
||||
}),
|
||||
[tabsConfig],
|
||||
);
|
||||
|
||||
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
|
||||
const { startMs, endMs } = useMemo(() => {
|
||||
const { minTime: startNs, maxTime: endNs } = getMinMaxTime(selectedTime);
|
||||
|
||||
return {
|
||||
startMs: Math.floor(startNs / NANO_SECOND_MULTIPLIER),
|
||||
endMs: Math.floor(endNs / NANO_SECOND_MULTIPLIER),
|
||||
};
|
||||
}, [getMinMaxTime, selectedTime]);
|
||||
|
||||
const [modalTimeRange, setModalTimeRange] = useState(() => ({
|
||||
startTime: startMs,
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: isCustomTimeRange(selectedTime)
|
||||
? DEFAULT_TIME_RANGE
|
||||
: selectedTime,
|
||||
);
|
||||
|
||||
const [selectedView, setSelectedView] = useInfraMonitoringView();
|
||||
const effectiveView = hideDetailViewTabs ? VIEW_TYPES.METRICS : selectedView;
|
||||
|
||||
const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters();
|
||||
const [
|
||||
tracesFiltersParam,
|
||||
setTracesFiltersParam,
|
||||
] = useInfraMonitoringTracesFilters();
|
||||
const [
|
||||
eventsFiltersParam,
|
||||
setEventsFiltersParam,
|
||||
] = useInfraMonitoringEventsFilters();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const [selectedItem, setSelectedItem] = useInfraMonitoringSelectedItem();
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
hideDetailViewTabs &&
|
||||
selectedItem &&
|
||||
selectedView !== VIEW_TYPES.METRICS
|
||||
) {
|
||||
setSelectedView(VIEW_TYPES.METRICS);
|
||||
}
|
||||
}, [hideDetailViewTabs, selectedItem, selectedView, setSelectedView]);
|
||||
|
||||
const entityQueryKey = useMemo(
|
||||
() =>
|
||||
getAutoRefreshQueryKey(
|
||||
selectedTime,
|
||||
`${queryKeyPrefix}EntityDetails`,
|
||||
selectedItem,
|
||||
),
|
||||
[queryKeyPrefix, selectedItem, selectedTime],
|
||||
);
|
||||
|
||||
const {
|
||||
data: entityResponse,
|
||||
isLoading: isEntityLoading,
|
||||
isError: isEntityError,
|
||||
} = useQuery({
|
||||
queryKey: entityQueryKey,
|
||||
queryFn: ({ signal }) => {
|
||||
if (!selectedItem) {
|
||||
return { data: null };
|
||||
}
|
||||
const filters = getSelectedItemFilters(selectedItem);
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
|
||||
return fetchEntityData(
|
||||
{
|
||||
filters,
|
||||
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
|
||||
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
|
||||
},
|
||||
signal,
|
||||
);
|
||||
},
|
||||
enabled: !!selectedItem,
|
||||
});
|
||||
|
||||
const entity = entityResponse?.data ?? null;
|
||||
|
||||
const initialFilters = useMemo(() => {
|
||||
const filters =
|
||||
effectiveView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam;
|
||||
if (filters) {
|
||||
return filters;
|
||||
}
|
||||
if (!entity) {
|
||||
return { op: 'AND', items: [] };
|
||||
}
|
||||
return {
|
||||
op: 'AND',
|
||||
items: getInitialLogTracesFilters(entity),
|
||||
};
|
||||
}, [
|
||||
entity,
|
||||
effectiveView,
|
||||
logFiltersParam,
|
||||
tracesFiltersParam,
|
||||
getInitialLogTracesFilters,
|
||||
]);
|
||||
|
||||
const initialEventsFilters = useMemo(() => {
|
||||
if (eventsFiltersParam) {
|
||||
return eventsFiltersParam;
|
||||
}
|
||||
if (!entity) {
|
||||
return { op: 'AND', items: [] };
|
||||
}
|
||||
return {
|
||||
op: 'AND',
|
||||
items: getInitialEventsFilters(entity),
|
||||
};
|
||||
}, [entity, eventsFiltersParam, getInitialEventsFilters]);
|
||||
|
||||
const [logsAndTracesFilters, setLogsAndTracesFilters] = useState<
|
||||
IBuilderQuery['filters']
|
||||
>(initialFilters);
|
||||
|
||||
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
|
||||
initialEventsFilters,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (entity) {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
});
|
||||
}
|
||||
}, [entity, eventCategory]);
|
||||
|
||||
useEffect(() => {
|
||||
setLogsAndTracesFilters(initialFilters);
|
||||
setEventsFilters(initialEventsFilters);
|
||||
}, [initialFilters, initialEventsFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
if (!isCustomTimeRange(currentSelectedInterval)) {
|
||||
setSelectedInterval(currentSelectedInterval);
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / TimeRangeOffset),
|
||||
endTime: Math.floor(maxTime / TimeRangeOffset),
|
||||
});
|
||||
}
|
||||
}, [getMinMaxTime, selectedTime]);
|
||||
|
||||
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||
setSelectedView(e.target.value);
|
||||
setLogFiltersParam(null);
|
||||
setTracesFiltersParam(null);
|
||||
setEventsFiltersParam(null);
|
||||
logEvent(InfraMonitoringEvents.TabChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
view: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
lastSelectedInterval.current = interval as Time;
|
||||
setSelectedInterval(interval as Time);
|
||||
|
||||
if (interval === 'custom' && dateTimeRange) {
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(dateTimeRange[0] / 1000),
|
||||
endTime: Math.floor(dateTimeRange[1] / 1000),
|
||||
});
|
||||
} else {
|
||||
const { maxTime, minTime } = GetMinMax(interval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / TimeRangeOffset),
|
||||
endTime: Math.floor(maxTime / TimeRangeOffset),
|
||||
});
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.TimeUpdated, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
interval,
|
||||
view: effectiveView,
|
||||
});
|
||||
},
|
||||
[eventCategory, effectiveView],
|
||||
);
|
||||
|
||||
const handleChangeLogFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setLogsAndTracesFilters((prevFilters) => {
|
||||
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||
primaryFilterKeys.includes(item.key?.key ?? ''),
|
||||
);
|
||||
const paginationFilter = value?.items?.find(
|
||||
(item) => item.key?.key === 'id',
|
||||
);
|
||||
const newFilters = value?.items?.filter(
|
||||
(item) =>
|
||||
item.key?.key !== 'id' &&
|
||||
!primaryFilterKeys.includes(item.key?.key ?? ''),
|
||||
);
|
||||
|
||||
if (newFilters && newFilters?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
view: selectedView,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...(primaryFilters || []),
|
||||
...(newFilters || []),
|
||||
...(paginationFilter ? [paginationFilter] : []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
setLogFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
[
|
||||
setLogFiltersParam,
|
||||
setSelectedView,
|
||||
primaryFilterKeys,
|
||||
eventCategory,
|
||||
selectedView,
|
||||
],
|
||||
);
|
||||
|
||||
const handleChangeTracesFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setLogsAndTracesFilters((prevFilters) => {
|
||||
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||
primaryFilterKeys.includes(item.key?.key ?? ''),
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
view: selectedView,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...(primaryFilters || []),
|
||||
...(value?.items?.filter(
|
||||
(item) => !primaryFilterKeys.includes(item.key?.key ?? ''),
|
||||
) || []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
setTracesFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
[
|
||||
setTracesFiltersParam,
|
||||
setSelectedView,
|
||||
primaryFilterKeys,
|
||||
eventCategory,
|
||||
selectedView,
|
||||
],
|
||||
);
|
||||
|
||||
const handleChangeEventsFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setEventsFilters((prevFilters) => {
|
||||
const kindFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
);
|
||||
const nameFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
view: selectedView,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
kindFilter,
|
||||
nameFilter,
|
||||
...(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),
|
||||
),
|
||||
};
|
||||
|
||||
setEventsFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
[eventCategory, selectedView, setEventsFiltersParam, setSelectedView],
|
||||
);
|
||||
|
||||
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(InfraMonitoringEvents.ExploreClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
view: selectedView,
|
||||
});
|
||||
|
||||
if (selectedView === VIEW_TYPES.LOGS) {
|
||||
const filtersWithoutPagination = {
|
||||
...logsAndTracesFilters,
|
||||
items:
|
||||
logsAndTracesFilters?.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: logsAndTracesFilters,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
lastSelectedInterval.current = null;
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / TimeRangeOffset),
|
||||
endTime: Math.floor(maxTime / TimeRangeOffset),
|
||||
});
|
||||
}
|
||||
|
||||
setSelectedItem(null);
|
||||
setSelectedView(null);
|
||||
setTracesFiltersParam(null);
|
||||
setEventsFiltersParam(null);
|
||||
setLogFiltersParam(null);
|
||||
};
|
||||
|
||||
const entityName = entity ? getEntityName(entity) : '';
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
width="70%"
|
||||
title={
|
||||
<>
|
||||
<Divider type="vertical" />
|
||||
<Typography.Text className="title">{entityName}</Typography.Text>
|
||||
</>
|
||||
}
|
||||
placement="right"
|
||||
onClose={handleClose}
|
||||
open={!!selectedItem}
|
||||
style={{
|
||||
overscrollBehavior: 'contain',
|
||||
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||
}}
|
||||
className="entity-detail-drawer"
|
||||
destroyOnClose
|
||||
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||
>
|
||||
{isEntityLoading && <LoadingContainer />}
|
||||
{isEntityError && (
|
||||
<Typography.Text type="danger">
|
||||
{entityResponse?.error || 'Failed to load entity details'}
|
||||
</Typography.Text>
|
||||
)}
|
||||
{entity && !isEntityLoading && (
|
||||
<>
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
{metadataConfig.map((config) => (
|
||||
<Typography.Text
|
||||
key={config.label}
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
{config.label}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="values-row">
|
||||
{metadataConfig.map((config) => {
|
||||
const value = config.getValue(entity);
|
||||
const displayValue = String(value);
|
||||
return (
|
||||
<Typography.Text
|
||||
key={config.label}
|
||||
className="entity-details-metadata-value"
|
||||
>
|
||||
{config.render ? (
|
||||
config.render(value, entity)
|
||||
) : (
|
||||
<Tooltip title={displayValue}>{displayValue}</Tooltip>
|
||||
)}
|
||||
</Typography.Text>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!hideDetailViewTabs && (
|
||||
<div className="views-tabs-container">
|
||||
<Radio.Group
|
||||
className="views-tabs"
|
||||
onChange={handleTabChange}
|
||||
value={selectedView}
|
||||
>
|
||||
{tabVisibility.showMetrics && (
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.METRICS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.METRICS}
|
||||
>
|
||||
<div className="view-title">
|
||||
<BarChart2 size={14} />
|
||||
Metrics
|
||||
</div>
|
||||
</Radio.Button>
|
||||
)}
|
||||
{tabVisibility.showLogs && (
|
||||
<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>
|
||||
)}
|
||||
{tabVisibility.showTraces && (
|
||||
<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>
|
||||
)}
|
||||
{tabVisibility.showEvents && (
|
||||
<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>
|
||||
)}
|
||||
{tabVisibility.showContainers && (
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.CONTAINERS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.CONTAINERS}
|
||||
>
|
||||
<div className="view-title">
|
||||
<Package2 size={14} />
|
||||
Containers
|
||||
</div>
|
||||
</Radio.Button>
|
||||
)}
|
||||
{tabVisibility.showProcesses && (
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.PROCESSES ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.PROCESSES}
|
||||
>
|
||||
<div className="view-title">
|
||||
<ChevronsLeftRight size={14} />
|
||||
Processes
|
||||
</div>
|
||||
</Radio.Button>
|
||||
)}
|
||||
{customTabs?.map((tab) => (
|
||||
<Radio.Button
|
||||
key={tab.key}
|
||||
className={selectedView === tab.key ? 'selected_view tab' : 'tab'}
|
||||
value={tab.key}
|
||||
>
|
||||
<div className="view-title">
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
</div>
|
||||
</Radio.Button>
|
||||
))}
|
||||
</Radio.Group>
|
||||
|
||||
{(selectedView === VIEW_TYPES.LOGS ||
|
||||
selectedView === VIEW_TYPES.TRACES) && (
|
||||
<Button
|
||||
icon={<Compass size={18} />}
|
||||
className="compass-button"
|
||||
onClick={handleExplorePagesRedirect}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{effectiveView === VIEW_TYPES.METRICS && (
|
||||
<EntityMetrics<T>
|
||||
entity={entity}
|
||||
selectedInterval={selectedInterval}
|
||||
timeRange={modalTimeRange}
|
||||
handleTimeChange={handleTimeChange}
|
||||
isModalTimeSelection
|
||||
entityWidgetInfo={entityWidgetInfo}
|
||||
getEntityQueryPayload={getEntityQueryPayload}
|
||||
category={category}
|
||||
queryKey={`${queryKeyPrefix}Metrics`}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.LOGS && (
|
||||
<EntityLogs
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeLogFilters={handleChangeLogFilters}
|
||||
logFilters={logsAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKeyFilters={primaryFilterKeys}
|
||||
queryKey={`${queryKeyPrefix}Logs`}
|
||||
category={category}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.TRACES && (
|
||||
<EntityTraces
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeTracesFilters={handleChangeTracesFilters}
|
||||
tracesFilters={logsAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey={`${queryKeyPrefix}Traces`}
|
||||
category={eventCategory}
|
||||
queryKeyFilters={primaryFilterKeys}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.EVENTS && tabVisibility.showEvents && (
|
||||
<EntityEvents
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeEventFilters={handleChangeEventsFilters}
|
||||
filters={eventsFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
category={category}
|
||||
queryKey={`${queryKeyPrefix}Events`}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.CONTAINERS &&
|
||||
tabVisibility.showContainers && <EntityContainers />}
|
||||
{effectiveView === VIEW_TYPES.PROCESSES && tabVisibility.showProcesses && (
|
||||
<EntityProcesses />
|
||||
)}
|
||||
{customTabs?.map((tab) =>
|
||||
selectedView === tab.key ? (
|
||||
<React.Fragment key={tab.key}>
|
||||
{tab.render({
|
||||
entity,
|
||||
timeRange: modalTimeRange,
|
||||
selectedInterval,
|
||||
handleTimeChange,
|
||||
})}
|
||||
</React.Fragment>
|
||||
) : null,
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sBaseDetails;
|
||||
@@ -0,0 +1,146 @@
|
||||
.clickableRow {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.k8SListTable {
|
||||
:global(.ant-table) {
|
||||
:global(.ant-table-thead > tr > th) {
|
||||
padding: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
|
||||
border-bottom: none;
|
||||
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.44px;
|
||||
text-transform: uppercase;
|
||||
background: var(--l1-background) !important;
|
||||
|
||||
&::before {
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-table-cell) {
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: var(--l1-foreground);
|
||||
background: var(--l1-background);
|
||||
border-bottom: none;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.hostname-column-value) {
|
||||
color: var(--l1-foreground);
|
||||
font-family: 'Geist Mono';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
:global(.progress-container) {
|
||||
:global(.ant-progress-bg) {
|
||||
height: 8px !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-table-tbody > tr:hover > td) {
|
||||
background: var(--l1-background-hover);
|
||||
}
|
||||
|
||||
:global(.ant-table-tbody > tr:not(.ant-table-expanded-row):hover > td) {
|
||||
background: var(--l1-background-hover) !important;
|
||||
}
|
||||
|
||||
:global(.ant-table-tbody > tr.ant-table-expanded-row:hover > td) {
|
||||
background: var(--l1-background);
|
||||
}
|
||||
|
||||
:global(.ant-table-tbody > tr > td) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:global(.ant-empty-normal) {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
:global(.ant-table-cell) {
|
||||
min-width: 180px !important;
|
||||
max-width: 180px !important;
|
||||
}
|
||||
|
||||
:global(.ant-table-cell:first-of-type):not(:global(.ant-table-row-expand-icon-cell)) {
|
||||
min-width: 250px !important;
|
||||
max-width: 250px !important;
|
||||
}
|
||||
|
||||
:global(.ant-table-row-expand-icon-cell) {
|
||||
min-width: 40px !important;
|
||||
max-width: 40px !important;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-pagination) {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: 16px;
|
||||
background-color: var(--l1-background);
|
||||
margin: 0 !important;
|
||||
padding-right: 72px;
|
||||
|
||||
:global(.ant-pagination-item) {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:global(.ant-pagination-item-active) {
|
||||
background: var(--l2-background);
|
||||
border-color: var(--l2-border);
|
||||
|
||||
a {
|
||||
color: var(--l1-foreground) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.noFilteredHostsMessageContainer {
|
||||
height: 30vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.noFilteredHostsMessageContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
|
||||
width: fit-content;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.noFilteredHostsMessage {
|
||||
margin-top: var(--spacing-4);
|
||||
}
|
||||
|
||||
.emptyStateSvg {
|
||||
width: 32px;
|
||||
max-width: 100%;
|
||||
}
|
||||
451
frontend/src/container/InfraMonitoringK8s/Base/K8sBaseList.tsx
Normal file
451
frontend/src/container/InfraMonitoringK8s/Base/K8sBaseList.tsx
Normal file
@@ -0,0 +1,451 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Spin,
|
||||
Table,
|
||||
TableColumnType as ColumnType,
|
||||
TablePaginationConfig,
|
||||
TableProps,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import type { SorterResult } from 'antd/es/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { useGlobalTimeStore } from 'store/globalTime';
|
||||
import {
|
||||
getAutoRefreshQueryKey,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime/utils';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import {
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringQueryFilters,
|
||||
} from '../hooks';
|
||||
import { usePageSize } from '../utils';
|
||||
import { K8sExpandedRow } from './K8sExpandedRow';
|
||||
import K8sHeader from './K8sHeader';
|
||||
import { K8sBaseFilters, K8sRenderedRowData } from './types';
|
||||
import {
|
||||
IEntityColumn,
|
||||
useInfraMonitoringTableColumnsForPage,
|
||||
useInfraMonitoringTableColumnsStore,
|
||||
} from './useInfraMonitoringTableColumnsStore';
|
||||
|
||||
import styles from './K8sBaseList.module.scss';
|
||||
|
||||
export type K8sBaseListEmptyStateContext = {
|
||||
isError: boolean;
|
||||
error?: string | null;
|
||||
totalCount: number;
|
||||
hasFilters: boolean;
|
||||
isLoading: boolean;
|
||||
rawData?: unknown;
|
||||
};
|
||||
|
||||
export type K8sBaseListProps<T = unknown> = {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
entity: InfraMonitoringEntity;
|
||||
tableColumnsDefinitions: IEntityColumn[];
|
||||
tableColumns: ColumnType<K8sRenderedRowData>[];
|
||||
fetchListData: (
|
||||
filters: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<{
|
||||
data: T[];
|
||||
total: number;
|
||||
error?: string | null;
|
||||
rawData?: unknown;
|
||||
}>;
|
||||
renderRowData: (
|
||||
record: T,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
) => K8sRenderedRowData;
|
||||
eventCategory: InfraMonitoringEvents;
|
||||
renderEmptyState?: (
|
||||
context: K8sBaseListEmptyStateContext,
|
||||
) => React.ReactNode | null;
|
||||
};
|
||||
|
||||
export function K8sBaseList<T>({
|
||||
controlListPrefix,
|
||||
entity,
|
||||
tableColumnsDefinitions,
|
||||
tableColumns,
|
||||
fetchListData,
|
||||
renderRowData,
|
||||
eventCategory,
|
||||
renderEmptyState,
|
||||
}: K8sBaseListProps<T>): JSX.Element {
|
||||
const queryFilters = useInfraMonitoringQueryFilters();
|
||||
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [groupBy] = useInfraMonitoringGroupBy();
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
const [initialOrderBy] = useState(orderBy);
|
||||
const [selectedItem, setSelectedItem] = useQueryState(
|
||||
'selectedItem',
|
||||
parseAsString,
|
||||
);
|
||||
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
||||
useEffect(() => {
|
||||
setExpandedRowKeys([]);
|
||||
}, [groupBy, currentPage]);
|
||||
const { pageSize, setPageSize } = usePageSize(entity);
|
||||
|
||||
const initializeTableColumns = useInfraMonitoringTableColumnsStore(
|
||||
(state) => state.initializePageColumns,
|
||||
);
|
||||
useEffect(() => {
|
||||
initializeTableColumns(entity, tableColumnsDefinitions);
|
||||
}, [initializeTableColumns, entity, tableColumnsDefinitions]);
|
||||
|
||||
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
|
||||
const refreshInterval = useGlobalTimeStore((s) => s.refreshInterval);
|
||||
const isRefreshEnabled = useGlobalTimeStore((s) => s.isRefreshEnabled);
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
return getAutoRefreshQueryKey(
|
||||
selectedTime,
|
||||
'k8sBaseList',
|
||||
entity,
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
);
|
||||
}, [
|
||||
selectedTime,
|
||||
entity,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
]);
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey,
|
||||
queryFn: ({ signal }) => {
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
|
||||
return fetchListData(
|
||||
{
|
||||
limit: pageSize,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
filters: queryFilters,
|
||||
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
|
||||
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
|
||||
orderBy: orderBy || undefined,
|
||||
groupBy: groupBy?.length > 0 ? groupBy : undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
},
|
||||
refetchInterval: isRefreshEnabled ? refreshInterval : false,
|
||||
});
|
||||
|
||||
const pageData = data?.data;
|
||||
const totalCount = data?.total || 0;
|
||||
const hasFilters = (queryFilters?.items?.length ?? 0) > 0;
|
||||
|
||||
const formattedItemsData = useMemo(() => {
|
||||
if (!pageData) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const rows = pageData.map((item) => renderRowData(item, groupBy));
|
||||
|
||||
// Without handling duplicated keys, the table became unpredictable/unstable
|
||||
const keyCount = new Map<string, number>();
|
||||
return rows.map(
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
(row): K8sRenderedRowData => {
|
||||
const count = keyCount.get(row.key) || 0;
|
||||
keyCount.set(row.key, count + 1);
|
||||
|
||||
if (count > 0) {
|
||||
return { ...row, key: `${row.key}-${count}` };
|
||||
}
|
||||
return row;
|
||||
},
|
||||
);
|
||||
}, [pageData, renderRowData, groupBy]);
|
||||
|
||||
const handleTableChange: TableProps<K8sRenderedRowData>['onChange'] = useCallback(
|
||||
(
|
||||
pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter:
|
||||
| SorterResult<K8sRenderedRowData>
|
||||
| SorterResult<K8sRenderedRowData>[],
|
||||
): void => {
|
||||
if (pagination.current) {
|
||||
setCurrentPage(pagination.current);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: eventCategory,
|
||||
});
|
||||
}
|
||||
|
||||
if ('field' in sorter && sorter.order) {
|
||||
setOrderBy({
|
||||
columnName: sorter.field as string,
|
||||
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
|
||||
});
|
||||
} else {
|
||||
setOrderBy(null);
|
||||
}
|
||||
},
|
||||
[eventCategory, setCurrentPage, setOrderBy],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: eventCategory,
|
||||
total: totalCount,
|
||||
});
|
||||
}, [eventCategory, totalCount]);
|
||||
|
||||
const handleGroupByRowClick = (record: K8sRenderedRowData): void => {
|
||||
if (expandedRowKeys.includes(record.key)) {
|
||||
setExpandedRowKeys(expandedRowKeys.filter((key) => key !== record.key));
|
||||
} else {
|
||||
setExpandedRowKeys([record.key]);
|
||||
}
|
||||
};
|
||||
|
||||
const openItemInNewTab = (record: K8sRenderedRowData): void => {
|
||||
const newParams = new URLSearchParams(document.location.search);
|
||||
newParams.set('selectedItem', record.itemKey);
|
||||
openInNewTab(
|
||||
buildAbsolutePath({
|
||||
relativePath: '',
|
||||
urlQueryString: newParams.toString(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sRenderedRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openItemInNewTab(record);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedItem(record.itemKey);
|
||||
} else {
|
||||
handleGroupByRowClick(record);
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: eventCategory,
|
||||
});
|
||||
};
|
||||
|
||||
const [
|
||||
columnsDefinitions,
|
||||
columnsHidden,
|
||||
] = useInfraMonitoringTableColumnsForPage(entity);
|
||||
|
||||
const hiddenColumnIdsOnList = useMemo(
|
||||
() =>
|
||||
columnsDefinitions
|
||||
.filter(
|
||||
(col) =>
|
||||
(groupBy?.length > 0 && col.behavior === 'hidden-on-expand') ||
|
||||
(!groupBy?.length && col.behavior === 'hidden-on-collapse'),
|
||||
)
|
||||
.map((col) => col.id),
|
||||
[columnsDefinitions, groupBy?.length],
|
||||
);
|
||||
|
||||
const mapDefaultSort = useCallback(
|
||||
(
|
||||
tableColumn: ColumnType<K8sRenderedRowData>,
|
||||
): ColumnType<K8sRenderedRowData> => {
|
||||
if (tableColumn.key === initialOrderBy?.columnName) {
|
||||
return {
|
||||
...tableColumn,
|
||||
defaultSortOrder: initialOrderBy?.order === 'asc' ? 'ascend' : 'descend',
|
||||
};
|
||||
}
|
||||
|
||||
return tableColumn;
|
||||
},
|
||||
[initialOrderBy?.columnName, initialOrderBy?.order],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
tableColumns
|
||||
.filter(
|
||||
(c) =>
|
||||
!hiddenColumnIdsOnList.includes(c.key?.toString() || '') &&
|
||||
!columnsHidden.includes(c.key?.toString() || ''),
|
||||
)
|
||||
.map(mapDefaultSort),
|
||||
[columnsHidden, hiddenColumnIdsOnList, mapDefaultSort, tableColumns],
|
||||
);
|
||||
|
||||
const isGroupedByAttribute = groupBy.length > 0;
|
||||
|
||||
const expandedRowRender = (record: K8sRenderedRowData): JSX.Element => (
|
||||
<K8sExpandedRow<T>
|
||||
record={record}
|
||||
entity={entity}
|
||||
tableColumns={tableColumns}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={renderRowData}
|
||||
/>
|
||||
);
|
||||
|
||||
const expandRowIconRenderer = ({
|
||||
expanded,
|
||||
onExpand,
|
||||
record,
|
||||
}: {
|
||||
expanded: boolean;
|
||||
onExpand: (
|
||||
record: K8sRenderedRowData,
|
||||
e: React.MouseEvent<HTMLButtonElement>,
|
||||
) => void;
|
||||
record: K8sRenderedRowData;
|
||||
}): 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 onPaginationChange = (page: number, pageSize: number): void => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(pageSize);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: eventCategory,
|
||||
});
|
||||
};
|
||||
|
||||
const showTableLoadingState = isLoading;
|
||||
|
||||
const defaultEmptyState = (
|
||||
<div className={styles.noFilteredHostsMessageContainer}>
|
||||
<div className={styles.noFilteredHostsMessageContent}>
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className={styles.emptyStateSvg}
|
||||
/>
|
||||
|
||||
<Typography.Text className={styles.noFilteredHostsMessage}>
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
let emptyTableMessage: React.ReactNode = defaultEmptyState;
|
||||
if (renderEmptyState) {
|
||||
const custom = renderEmptyState({
|
||||
isError,
|
||||
error: data?.error,
|
||||
totalCount,
|
||||
hasFilters,
|
||||
isLoading: showTableLoadingState,
|
||||
rawData: data?.rawData,
|
||||
});
|
||||
if (custom != null) {
|
||||
emptyTableMessage = custom;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sHeader
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={entity}
|
||||
showAutoRefresh={!selectedItem}
|
||||
/>
|
||||
{isError && (
|
||||
<Typography>{data?.error?.toString() || 'Something went wrong'}</Typography>
|
||||
)}
|
||||
|
||||
<Table
|
||||
className={styles.k8SListTable}
|
||||
dataSource={showTableLoadingState ? [] : formattedItemsData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
total: totalCount,
|
||||
showSizeChanger: true,
|
||||
hideOnSinglePage: false,
|
||||
onChange: onPaginationChange,
|
||||
}}
|
||||
loading={{
|
||||
spinning: showTableLoadingState,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
locale={{
|
||||
emptyText: showTableLoadingState ? null : emptyTableMessage,
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: styles.clickableRow,
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
expandIcon: expandRowIconRenderer,
|
||||
expandedRowKeys,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
.expandedClickableRow {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.expandedTableContainer {
|
||||
border: 1px solid var(--l1-border);
|
||||
overflow-x: auto;
|
||||
padding-left: 48px;
|
||||
|
||||
:global(.ant-table-tbody > tr:hover > td) {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.expandedTableFooter {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
padding-left: 42px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.viewAllButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Spin,
|
||||
Table,
|
||||
TableColumnType as ColumnType,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { CornerDownRight } from 'lucide-react';
|
||||
import { useGlobalTimeStore } from 'store/globalTime';
|
||||
import {
|
||||
getAutoRefreshQueryKey,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime/utils';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { BaseAutocompleteData } from '../../../types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import {
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringFilters,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringQueryFilters,
|
||||
useInfraMonitoringSelectedItem,
|
||||
} from '../hooks';
|
||||
import LoadingContainer from '../LoadingContainer';
|
||||
import { K8sBaseFilters, K8sRenderedRowData } from './types';
|
||||
import { useInfraMonitoringTableColumnsForPage } from './useInfraMonitoringTableColumnsStore';
|
||||
|
||||
import styles from './K8sExpandedRow.module.scss';
|
||||
|
||||
export type K8sExpandedRowProps<T> = {
|
||||
record: K8sRenderedRowData;
|
||||
entity: InfraMonitoringEntity;
|
||||
tableColumns: ColumnType<K8sRenderedRowData>[];
|
||||
fetchListData: (
|
||||
filters: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<{
|
||||
data: T[];
|
||||
total: number;
|
||||
error?: string | null;
|
||||
rawData?: unknown;
|
||||
}>;
|
||||
renderRowData: (
|
||||
record: T,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
) => K8sRenderedRowData;
|
||||
};
|
||||
|
||||
export const MAX_ITEMS_TO_FETCH_WHEN_GROUP_BY = 10;
|
||||
|
||||
export function K8sExpandedRow<T>({
|
||||
record,
|
||||
entity,
|
||||
tableColumns,
|
||||
fetchListData,
|
||||
renderRowData,
|
||||
}: K8sExpandedRowProps<T>): JSX.Element {
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
const [, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [, setFilters] = useInfraMonitoringFilters();
|
||||
const [, setSelectedItem] = useInfraMonitoringSelectedItem();
|
||||
|
||||
const queryFilters = useInfraMonitoringQueryFilters();
|
||||
|
||||
const [
|
||||
columnsDefinitions,
|
||||
columnsHidden,
|
||||
] = useInfraMonitoringTableColumnsForPage(entity);
|
||||
|
||||
const hiddenColumnIdsForNested = useMemo(
|
||||
() =>
|
||||
columnsDefinitions
|
||||
.filter((col) => col.behavior === 'hidden-on-collapse')
|
||||
.map((col) => col.id),
|
||||
[columnsDefinitions],
|
||||
);
|
||||
|
||||
const nestedColumns = useMemo(
|
||||
() =>
|
||||
tableColumns.filter(
|
||||
(c) =>
|
||||
!columnsHidden.includes(c.key?.toString() || '') &&
|
||||
!hiddenColumnIdsForNested.includes(c.key?.toString() || ''),
|
||||
),
|
||||
[tableColumns, columnsHidden, hiddenColumnIdsForNested],
|
||||
);
|
||||
|
||||
const createFiltersForRecord = useCallback((): NonNullable<
|
||||
IBuilderQuery['filters']
|
||||
> => {
|
||||
const baseFilters: IBuilderQuery['filters'] = {
|
||||
items: [...queryFilters.items],
|
||||
op: 'and',
|
||||
};
|
||||
|
||||
const { groupedByMeta } = record;
|
||||
|
||||
for (const key of Object.keys(groupedByMeta)) {
|
||||
baseFilters.items.push({
|
||||
key: {
|
||||
key,
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: groupedByMeta[key],
|
||||
id: key,
|
||||
});
|
||||
}
|
||||
|
||||
return baseFilters;
|
||||
}, [queryFilters.items, record]);
|
||||
|
||||
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
|
||||
const refreshInterval = useGlobalTimeStore((s) => s.refreshInterval);
|
||||
const isRefreshEnabled = useGlobalTimeStore((s) => s.isRefreshEnabled);
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
return getAutoRefreshQueryKey(selectedTime, [
|
||||
'k8sExpandedRow',
|
||||
record.key,
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
]);
|
||||
}, [selectedTime, record.key, queryFilters, orderBy]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useQuery({
|
||||
queryKey,
|
||||
queryFn: ({ signal }) => {
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
|
||||
return fetchListData(
|
||||
{
|
||||
limit: MAX_ITEMS_TO_FETCH_WHEN_GROUP_BY,
|
||||
offset: 0,
|
||||
filters: createFiltersForRecord(),
|
||||
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
|
||||
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
|
||||
orderBy: orderBy || undefined,
|
||||
groupBy: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
},
|
||||
refetchInterval: isRefreshEnabled ? refreshInterval : false,
|
||||
});
|
||||
|
||||
const formattedData = useMemo(() => {
|
||||
if (!data?.data) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const rows = data.data.map((item) => renderRowData(item, groupBy));
|
||||
|
||||
// Without handling duplicated keys, the table became unpredictable/unstable
|
||||
const keyCount = new Map<string, number>();
|
||||
return rows.map(
|
||||
(row): K8sRenderedRowData => {
|
||||
const count = keyCount.get(row.key) || 0;
|
||||
keyCount.set(row.key, count + 1);
|
||||
|
||||
if (count > 0) {
|
||||
return { ...row, key: `${row.key}-${count}` };
|
||||
}
|
||||
return row;
|
||||
},
|
||||
);
|
||||
}, [data?.data, renderRowData, groupBy]);
|
||||
|
||||
const openRecordInNewTab = (rowRecord: K8sRenderedRowData): void => {
|
||||
const newParams = new URLSearchParams(document.location.search);
|
||||
newParams.set('selectedItem', rowRecord.itemKey);
|
||||
openInNewTab(
|
||||
buildAbsolutePath({
|
||||
relativePath: '',
|
||||
urlQueryString: newParams.toString(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleViewAllClick = (): void => {
|
||||
const filters = createFiltersForRecord();
|
||||
setFilters(JSON.stringify(filters));
|
||||
setCurrentPage(1);
|
||||
setGroupBy([]);
|
||||
setOrderBy(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.expandedTableContainer}
|
||||
data-testid="expanded-table-container"
|
||||
>
|
||||
{isError && (
|
||||
<Typography>{data?.error?.toString() || 'Something went wrong'}</Typography>
|
||||
)}
|
||||
|
||||
{isFetching || isLoading ? (
|
||||
<LoadingContainer />
|
||||
) : (
|
||||
<div data-testid="expanded-table">
|
||||
<Table
|
||||
columns={nestedColumns}
|
||||
dataSource={formattedData}
|
||||
pagination={false}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
showHeader={false}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
onRow={(
|
||||
rowRecord: K8sRenderedRowData,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
openRecordInNewTab(rowRecord);
|
||||
return;
|
||||
}
|
||||
setSelectedItem(rowRecord.itemKey);
|
||||
},
|
||||
className: styles.expandedClickableRow,
|
||||
})}
|
||||
/>
|
||||
|
||||
{data?.total && data?.total > MAX_ITEMS_TO_FETCH_WHEN_GROUP_BY && (
|
||||
<div className={styles.expandedTableFooter}>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
className={styles.viewAllButton}
|
||||
onClick={handleViewAllClick}
|
||||
>
|
||||
<CornerDownRight size={14} />
|
||||
View All
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
.drawer {
|
||||
--dialog-description-padding: 0px 0px var(--spacing-8) 0px;
|
||||
}
|
||||
|
||||
.columnItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.columnsTitle {
|
||||
color: var(--l2-foreground);
|
||||
font-size: var(--periscope-font-size-small, 11px);
|
||||
font-weight: var(--periscope-font-weight-medium, 500);
|
||||
text-transform: uppercase;
|
||||
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
border-bottom: 1px solid var(--l2-border);
|
||||
|
||||
&:not(:first-of-type) {
|
||||
border-top: 1px solid var(--l2-border);
|
||||
}
|
||||
}
|
||||
|
||||
.columnsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.columnItem {
|
||||
justify-content: flex-start !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.horizontalDivider {
|
||||
border-top: 1px solid var(--l1-border);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Button, DrawerWrapper } from '@signozhq/ui';
|
||||
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import {
|
||||
useInfraMonitoringTableColumnsForPage,
|
||||
useInfraMonitoringTableColumnsStore,
|
||||
} from './useInfraMonitoringTableColumnsStore';
|
||||
|
||||
import styles from './K8sFiltersSidePanel.module.scss';
|
||||
|
||||
function K8sFiltersSidePanel({
|
||||
open,
|
||||
onClose,
|
||||
entity,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
entity: InfraMonitoringEntity;
|
||||
}): JSX.Element {
|
||||
const addColumn = useInfraMonitoringTableColumnsStore(
|
||||
(state) => state.addColumn,
|
||||
);
|
||||
const removeColumn = useInfraMonitoringTableColumnsStore(
|
||||
(state) => state.removeColumn,
|
||||
);
|
||||
|
||||
const [columns, columnsHidden] = useInfraMonitoringTableColumnsForPage(entity);
|
||||
|
||||
const drawerContent = (
|
||||
<>
|
||||
<div className={styles.columnsTitle}>Added Columns (Click to remove)</div>
|
||||
|
||||
<div className={styles.columnsList}>
|
||||
{columns
|
||||
.filter(
|
||||
(column) =>
|
||||
!columnsHidden.includes(column.id) &&
|
||||
column.behavior !== 'hidden-on-collapse',
|
||||
)
|
||||
.map((column) => (
|
||||
<div className={styles.columnItem} key={column.value}>
|
||||
{/*<GripVertical size={16} /> TODO: Add support back when update the table component */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="none"
|
||||
className={styles.columnItem}
|
||||
disabled={!column.canBeHidden}
|
||||
data-testid={`remove-column-${column.id}`}
|
||||
onClick={(): void => removeColumn(entity, column.id)}
|
||||
>
|
||||
{column.label}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles.horizontalDivider} />
|
||||
|
||||
<div className={styles.columnsTitle}>Other Columns (Click to add)</div>
|
||||
|
||||
<div className={styles.columnsList}>
|
||||
{columns
|
||||
.filter((column) => columnsHidden.includes(column.id))
|
||||
.map((column) => (
|
||||
<div className={styles.columnItem} key={column.value}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="none"
|
||||
className={styles.columnItem}
|
||||
data-can-be-added="true"
|
||||
data-testid={`add-column-${column.id}`}
|
||||
onClick={(): void => addColumn(entity, column.id)}
|
||||
tabIndex={0}
|
||||
>
|
||||
{column.label}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<DrawerWrapper
|
||||
open={open}
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
title="Columns"
|
||||
direction="right"
|
||||
showCloseButton
|
||||
showOverlay={false}
|
||||
className={styles.drawer}
|
||||
>
|
||||
{drawerContent}
|
||||
</DrawerWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sFiltersSidePanel;
|
||||
@@ -0,0 +1,80 @@
|
||||
.k8SListControls {
|
||||
padding: var(--spacing-4);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
|
||||
:global(.ant-select-selector) {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border) !important;
|
||||
background-color: var(--l2-background) !important;
|
||||
|
||||
input {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:global(.ant-tag .ant-typography) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.k8SListControlsLeft {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-4);
|
||||
|
||||
.k8SQbSearchContainer {
|
||||
flex: 1;
|
||||
min-width: 240px;
|
||||
max-width: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
.k8SAttributeSearchContainer {
|
||||
flex: 1;
|
||||
min-width: 240px;
|
||||
max-width: 40%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.groupByLabel {
|
||||
min-width: max-content;
|
||||
font-size: var(--periscope-font-size-base, 13px);
|
||||
font-weight: var(--periscope-font-weight-regular, 400);
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
border-radius: 2px 0px 0px 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-right: none;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
|
||||
display: flex;
|
||||
height: 32px;
|
||||
padding: var(--spacing-3) var(--spacing-3) var(--spacing-3) var(--spacing-4);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.groupBySelect {
|
||||
:global(.ant-select-selector) {
|
||||
border-left: none;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.k8SListControlsRight {
|
||||
min-width: 240px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
229
frontend/src/container/InfraMonitoringK8s/Base/K8sHeader.tsx
Normal file
229
frontend/src/container/InfraMonitoringK8s/Base/K8sHeader.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Select } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { SlidersHorizontal } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
InfraMonitoringEntity,
|
||||
} from '../constants';
|
||||
import {
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringFiltersK8s,
|
||||
useInfraMonitoringGroupBy,
|
||||
} from '../hooks';
|
||||
import K8sFiltersSidePanel from './K8sFiltersSidePanel';
|
||||
|
||||
import styles from './K8sHeader.module.scss';
|
||||
|
||||
interface K8sHeaderProps {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
entity: InfraMonitoringEntity;
|
||||
showAutoRefresh: boolean;
|
||||
}
|
||||
|
||||
function K8sHeader({
|
||||
controlListPrefix,
|
||||
entity,
|
||||
showAutoRefresh,
|
||||
}: K8sHeaderProps): JSX.Element {
|
||||
const [isFiltersSidePanelOpen, setIsFiltersSidePanelOpen] = useState(false);
|
||||
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
|
||||
|
||||
const currentQuery = initialQueriesMap[DataSource.METRICS];
|
||||
|
||||
const updatedCurrentQuery = useMemo(() => {
|
||||
let { filters } = currentQuery.builder.queryData[0];
|
||||
if (urlFilters) {
|
||||
filters = urlFilters;
|
||||
}
|
||||
return {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...currentQuery.builder.queryData[0],
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
...currentQuery.builder.queryData[0].aggregateAttribute,
|
||||
},
|
||||
filters,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}, [currentQuery, urlFilters]);
|
||||
|
||||
const query = useMemo(
|
||||
() => updatedCurrentQuery?.builder?.queryData[0] || null,
|
||||
[updatedCurrentQuery],
|
||||
);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const [, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const handleChangeTagFilters = useCallback(
|
||||
(value: IBuilderQuery['filters']) => {
|
||||
setUrlFilters(value || null);
|
||||
handleChangeQueryData('filters', value);
|
||||
setCurrentPage(1);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
});
|
||||
}
|
||||
},
|
||||
[handleChangeQueryData, setCurrentPage, setUrlFilters],
|
||||
);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const {
|
||||
data: groupByFiltersData,
|
||||
isLoading: isLoadingGroupByFilters,
|
||||
} = useGetAggregateKeys(
|
||||
{
|
||||
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
aggregateAttribute: GetK8sEntityToAggregateAttribute(
|
||||
entity,
|
||||
dotMetricsEnabled,
|
||||
),
|
||||
aggregateOperator: 'noop',
|
||||
searchText: '',
|
||||
tagType: '',
|
||||
},
|
||||
{
|
||||
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
|
||||
},
|
||||
true,
|
||||
entity,
|
||||
);
|
||||
|
||||
const groupByOptions = useMemo(
|
||||
() =>
|
||||
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
|
||||
value: filter.key,
|
||||
label: filter.key,
|
||||
})) || [],
|
||||
[groupByFiltersData],
|
||||
);
|
||||
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
|
||||
const handleGroupByChange = useCallback(
|
||||
(value: IBuilderQuery['groupBy']) => {
|
||||
const newGroupBy = [];
|
||||
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
const element = (value[index] as unknown) as string;
|
||||
|
||||
const key = groupByFiltersData?.payload?.attributeKeys?.find(
|
||||
(k) => k.key === element,
|
||||
);
|
||||
|
||||
if (key) {
|
||||
newGroupBy.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset pagination on switching to groupBy
|
||||
setCurrentPage(1);
|
||||
setGroupBy(newGroupBy);
|
||||
|
||||
logEvent(InfraMonitoringEvents.GroupByChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
});
|
||||
},
|
||||
[groupByFiltersData, setCurrentPage, setGroupBy],
|
||||
);
|
||||
|
||||
const onClickOutside = useCallback(() => {
|
||||
setIsFiltersSidePanelOpen(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.k8SListControls}>
|
||||
<div className={styles.k8SListControlsLeft}>
|
||||
{controlListPrefix}
|
||||
|
||||
<div className={styles.k8SQbSearchContainer}>
|
||||
<QueryBuilderSearch
|
||||
query={query as IBuilderQuery}
|
||||
onChange={handleChangeTagFilters}
|
||||
isInfraMonitoring
|
||||
disableNavigationShortcuts
|
||||
entity={entity}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.k8SAttributeSearchContainer}>
|
||||
<div className={styles.groupByLabel}> Group by </div>
|
||||
<Select
|
||||
className={styles.groupBySelect}
|
||||
loading={isLoadingGroupByFilters}
|
||||
mode="multiple"
|
||||
value={groupBy}
|
||||
allowClear
|
||||
maxTagCount="responsive"
|
||||
placeholder="Search for attribute"
|
||||
style={{ width: '100%' }}
|
||||
options={groupByOptions}
|
||||
onChange={handleGroupByChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.k8SListControlsRight}>
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh={showAutoRefresh}
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="none"
|
||||
disabled={groupBy?.length > 0}
|
||||
data-testid="k8s-list-filters-button"
|
||||
onClick={(): void => setIsFiltersSidePanelOpen(true)}
|
||||
>
|
||||
<SlidersHorizontal size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<K8sFiltersSidePanel
|
||||
open={isFiltersSidePanelOpen}
|
||||
entity={entity}
|
||||
onClose={onClickOutside}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sHeader;
|
||||
File diff suppressed because it is too large
Load Diff
27
frontend/src/container/InfraMonitoringK8s/Base/types.ts
Normal file
27
frontend/src/container/InfraMonitoringK8s/Base/types.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { OrderBySchemaType } from '../schemas';
|
||||
|
||||
export type K8sBaseFilters = {
|
||||
filters: TagFilter;
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
start: number;
|
||||
end: number;
|
||||
orderBy?: OrderBySchemaType;
|
||||
};
|
||||
|
||||
export type K8sRenderedRowData = {
|
||||
/**
|
||||
* The unique ID for the row
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* The ID to the selectedItem
|
||||
*/
|
||||
itemKey: string;
|
||||
groupedByMeta: Record<string, string>;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
export interface IEntityColumn {
|
||||
label: string;
|
||||
value: string;
|
||||
id: string;
|
||||
defaultVisibility: boolean;
|
||||
canBeHidden: boolean;
|
||||
behavior: 'hidden-on-expand' | 'hidden-on-collapse' | 'always-visible';
|
||||
}
|
||||
|
||||
export interface IInfraMonitoringTableColumnsStore {
|
||||
columns: Record<string, IEntityColumn[]>;
|
||||
columnsHidden: Record<string, string[]>;
|
||||
addColumn: (page: string, columnId: string) => void;
|
||||
removeColumn: (page: string, columnId: string) => void;
|
||||
initializePageColumns: (page: string, columns: IEntityColumn[]) => void;
|
||||
}
|
||||
|
||||
export const useInfraMonitoringTableColumnsStore = create<IInfraMonitoringTableColumnsStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
columns: {},
|
||||
columnsHidden: {},
|
||||
addColumn: (page, columnId): void => {
|
||||
const state = get();
|
||||
const columnDefinition = state.columns[page]?.find(
|
||||
(c) => c.id === columnId,
|
||||
);
|
||||
|
||||
if (!columnDefinition) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!columnDefinition.canBeHidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
const columnsHidden = state.columnsHidden[page];
|
||||
|
||||
if (columnsHidden.includes(columnId)) {
|
||||
set({
|
||||
columnsHidden: {
|
||||
...state.columnsHidden,
|
||||
[page]: columnsHidden.filter((id) => id !== columnId),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
removeColumn: (page, columnId): void => {
|
||||
const state = get();
|
||||
const columnDefinition = state.columns[page]?.find(
|
||||
(c) => c.id === columnId,
|
||||
);
|
||||
|
||||
if (!columnDefinition) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!columnDefinition.canBeHidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
const columnsHidden = state.columnsHidden[page];
|
||||
|
||||
if (!columnsHidden.includes(columnId)) {
|
||||
set({
|
||||
columnsHidden: {
|
||||
...state.columnsHidden,
|
||||
[page]: [...columnsHidden, columnId],
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
initializePageColumns: (page, columns): void => {
|
||||
const state = get();
|
||||
|
||||
set({
|
||||
columns: {
|
||||
...state.columns,
|
||||
[page]: columns,
|
||||
},
|
||||
});
|
||||
|
||||
if (state.columnsHidden[page] === undefined) {
|
||||
set({
|
||||
columnsHidden: {
|
||||
...state.columnsHidden,
|
||||
[page]: columns
|
||||
.filter((c) => c.defaultVisibility === false)
|
||||
.map((c) => c.id),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: '@signoz/infra-monitoring-columns',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export const useInfraMonitoringTableColumnsForPage = (
|
||||
page: string,
|
||||
): [columns: IEntityColumn[], columnsHidden: string[]] => {
|
||||
const state = useInfraMonitoringTableColumnsStore((s) => s.columns);
|
||||
const columnsHidden = useInfraMonitoringTableColumnsStore(
|
||||
(s) => s.columnsHidden,
|
||||
);
|
||||
|
||||
return [state[page] ?? [], columnsHidden[page] ?? []];
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
.itemDataGroup {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.itemDataGroupTagItem {
|
||||
display: block !important;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
96
frontend/src/container/InfraMonitoringK8s/Base/utils.tsx
Normal file
96
frontend/src/container/InfraMonitoringK8s/Base/utils.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Badge } from '@signozhq/ui';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import styles from './utils.module.scss';
|
||||
|
||||
const dotToUnder: Record<string, string> = {
|
||||
'os.type': 'os_type',
|
||||
'host.name': 'host_name',
|
||||
'deployment.environment': 'deployment_environment',
|
||||
'k8s.node.name': 'k8s_node_name',
|
||||
'k8s.cluster.name': 'k8s_cluster_name',
|
||||
'k8s.node.uid': 'k8s_node_uid',
|
||||
'k8s.cronjob.name': 'k8s_cronjob_name',
|
||||
'k8s.daemonset.name': 'k8s_daemonset_name',
|
||||
'k8s.deployment.name': 'k8s_deployment_name',
|
||||
'k8s.job.name': 'k8s_job_name',
|
||||
'k8s.namespace.name': 'k8s_namespace_name',
|
||||
'k8s.pod.name': 'k8s_pod_name',
|
||||
'k8s.pod.uid': 'k8s_pod_uid',
|
||||
'k8s.statefulset.name': 'k8s_statefulset_name',
|
||||
'k8s.persistentvolumeclaim.name': 'k8s_persistentvolumeclaim_name',
|
||||
};
|
||||
|
||||
export function getGroupedByMeta<T extends { meta: Record<string, string> }>(
|
||||
itemData: T,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
|
||||
groupBy.forEach((group) => {
|
||||
const rawKey = group.key as string;
|
||||
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof itemData.meta;
|
||||
result[rawKey] = (itemData.meta[metaKey] || itemData.meta[rawKey]) ?? '';
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getRowKey<T extends { meta: Record<string, string> }>(
|
||||
itemData: T,
|
||||
getItemIdentifier: () => string,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): string {
|
||||
const nodeIdentifier = getItemIdentifier();
|
||||
|
||||
if (groupBy.length === 0) {
|
||||
return nodeIdentifier || JSON.stringify(itemData.meta);
|
||||
}
|
||||
|
||||
const groupedMeta = getGroupedByMeta(itemData, groupBy);
|
||||
const groupKey = Object.values(groupedMeta).join('-');
|
||||
|
||||
if (groupKey && nodeIdentifier) {
|
||||
return `${groupKey}-${nodeIdentifier}`;
|
||||
}
|
||||
if (groupKey) {
|
||||
return groupKey;
|
||||
}
|
||||
if (nodeIdentifier) {
|
||||
return nodeIdentifier;
|
||||
}
|
||||
|
||||
return JSON.stringify(itemData.meta);
|
||||
}
|
||||
|
||||
export function getGroupByEl<T extends { meta: Record<string, string> }>(
|
||||
itemData: T,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): React.ReactNode {
|
||||
const groupByValues: string[] = [];
|
||||
|
||||
groupBy.forEach((group) => {
|
||||
const rawKey = group.key as string;
|
||||
|
||||
// Choose mapped key if present, otherwise use rawKey
|
||||
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof itemData.meta;
|
||||
const value = itemData.meta[metaKey] || itemData.meta[rawKey] || '<no-value>';
|
||||
|
||||
groupByValues.push(value);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.itemDataGroup}>
|
||||
{groupByValues.map((value) => (
|
||||
<Badge
|
||||
key={value}
|
||||
color="secondary"
|
||||
className={styles.itemDataGroupTagItem}
|
||||
>
|
||||
{value === '' ? '<no-value>' : value}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { K8sClustersData } from 'api/infraMonitoring/getK8sClustersList';
|
||||
|
||||
export type ClusterDetailsProps = {
|
||||
cluster: K8sClustersData | null;
|
||||
isModalTimeSelection: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -1,624 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
|
||||
import type { RadioChangeEvent } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { K8sClustersData } from 'api/infraMonitoring/getK8sClustersList';
|
||||
import { VIEW_TYPES, VIEWS } from 'components/HostMetricsDetail/constants';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryState,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils';
|
||||
import {
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from 'container/InfraMonitoringK8s/hooks';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
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 { 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 ClusterEvents from '../../EntityDetailsUtils/EntityEvents';
|
||||
import ClusterLogs from '../../EntityDetailsUtils/EntityLogs';
|
||||
import ClusterMetrics from '../../EntityDetailsUtils/EntityMetrics';
|
||||
import ClusterTraces from '../../EntityDetailsUtils/EntityTraces';
|
||||
import { ClusterDetailsProps } from './ClusterDetails.interfaces';
|
||||
import { clusterWidgetInfo, getClusterMetricsQueryPayload } from './constants';
|
||||
|
||||
import '../../EntityDetailsUtils/entityDetails.styles.scss';
|
||||
|
||||
function ClusterDetails({
|
||||
cluster,
|
||||
onClose,
|
||||
isModalTimeSelection,
|
||||
}: ClusterDetailsProps): 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 lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
);
|
||||
|
||||
const [selectedView, setSelectedView] = useInfraMonitoringView();
|
||||
const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters();
|
||||
const [
|
||||
tracesFiltersParam,
|
||||
setTracesFiltersParam,
|
||||
] = useInfraMonitoringTracesFilters();
|
||||
const [
|
||||
eventsFiltersParam,
|
||||
setEventsFiltersParam,
|
||||
] = useInfraMonitoringEventsFilters();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const initialFilters = useMemo(() => {
|
||||
const filters =
|
||||
selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam;
|
||||
if (filters) {
|
||||
return filters;
|
||||
}
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s_cluster_name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster?.meta.k8s_cluster_name || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [
|
||||
cluster?.meta.k8s_cluster_name,
|
||||
selectedView,
|
||||
logFiltersParam,
|
||||
tracesFiltersParam,
|
||||
]);
|
||||
|
||||
const initialEventsFilters = useMemo(() => {
|
||||
if (eventsFiltersParam) {
|
||||
return eventsFiltersParam;
|
||||
}
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s.object.kind--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: 'Cluster',
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s.object.name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster?.meta.k8s_cluster_name || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [cluster?.meta.k8s_cluster_name, eventsFiltersParam]);
|
||||
|
||||
const [logsAndTracesFilters, setLogsAndTracesFilters] = useState<
|
||||
IBuilderQuery['filters']
|
||||
>(initialFilters);
|
||||
|
||||
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
|
||||
initialEventsFilters,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (cluster) {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
});
|
||||
}
|
||||
}, [cluster]);
|
||||
|
||||
useEffect(() => {
|
||||
setLogsAndTracesFilters(initialFilters);
|
||||
setEventsFilters(initialEventsFilters);
|
||||
}, [initialFilters, initialEventsFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
endTime: Math.floor(maxTime / 1000000000),
|
||||
});
|
||||
}
|
||||
}, [selectedTime, minTime, maxTime]);
|
||||
|
||||
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||
setSelectedView(e.target.value);
|
||||
setLogFiltersParam(null);
|
||||
setTracesFiltersParam(null);
|
||||
setEventsFiltersParam(null);
|
||||
logEvent(InfraMonitoringEvents.TabChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
view: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
lastSelectedInterval.current = interval as Time;
|
||||
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(InfraMonitoringEvents.TimeUpdated, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
interval,
|
||||
view: selectedView,
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeLogFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setLogsAndTracesFilters((prevFilters) => {
|
||||
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||
[QUERY_KEYS.K8S_CLUSTER_NAME].includes(item.key?.key ?? ''),
|
||||
);
|
||||
const paginationFilter = value?.items?.find(
|
||||
(item) => item.key?.key === 'id',
|
||||
);
|
||||
const newFilters = value?.items?.filter(
|
||||
(item) =>
|
||||
item.key?.key !== 'id' && item.key?.key !== QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||
);
|
||||
|
||||
if (newFilters && newFilters?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
view: InfraMonitoringEvents.LogsView,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...(primaryFilters || []),
|
||||
...(newFilters || []),
|
||||
...(paginationFilter ? [paginationFilter] : []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
setLogFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeTracesFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setLogsAndTracesFilters((prevFilters) => {
|
||||
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||
[QUERY_KEYS.K8S_CLUSTER_NAME].includes(item.key?.key ?? ''),
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
view: InfraMonitoringEvents.TracesView,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...(primaryFilters || []),
|
||||
...(value?.items?.filter(
|
||||
(item) => item.key?.key !== QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||
) || []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
setTracesFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeEventsFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setEventsFilters((prevFilters) => {
|
||||
const clusterKindFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
);
|
||||
const clusterNameFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
view: InfraMonitoringEvents.EventsView,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
clusterKindFilter,
|
||||
clusterNameFilter,
|
||||
...(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),
|
||||
),
|
||||
};
|
||||
|
||||
setEventsFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// 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(InfraMonitoringEvents.ExploreClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
view: selectedView,
|
||||
});
|
||||
|
||||
if (selectedView === VIEW_TYPES.LOGS) {
|
||||
const filtersWithoutPagination = {
|
||||
...logsAndTracesFilters,
|
||||
items:
|
||||
logsAndTracesFilters?.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: logsAndTracesFilters,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
lastSelectedInterval.current = null;
|
||||
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">
|
||||
{cluster?.meta.k8s_cluster_name}
|
||||
</Typography.Text>
|
||||
</>
|
||||
}
|
||||
placement="right"
|
||||
onClose={handleClose}
|
||||
open={!!cluster}
|
||||
style={{
|
||||
overscrollBehavior: 'contain',
|
||||
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||
}}
|
||||
className="entity-detail-drawer"
|
||||
destroyOnClose
|
||||
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||
>
|
||||
{cluster && (
|
||||
<>
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Cluster Name
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="values-row">
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={cluster.meta.k8s_cluster_name}>
|
||||
{cluster.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={
|
||||
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 && (
|
||||
<ClusterMetrics<K8sClustersData>
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
entity={cluster}
|
||||
entityWidgetInfo={clusterWidgetInfo}
|
||||
getEntityQueryPayload={getClusterMetricsQueryPayload}
|
||||
category={K8sCategory.CLUSTERS}
|
||||
queryKey="clusterMetrics"
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<ClusterLogs
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeLogFilters={handleChangeLogFilters}
|
||||
logFilters={logsAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey="clusterLogs"
|
||||
category={K8sCategory.CLUSTERS}
|
||||
queryKeyFilters={[QUERY_KEYS.K8S_CLUSTER_NAME]}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.TRACES && (
|
||||
<ClusterTraces
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeTracesFilters={handleChangeTracesFilters}
|
||||
tracesFilters={logsAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey="clusterTraces"
|
||||
category={InfraMonitoringEvents.Cluster}
|
||||
queryKeyFilters={[QUERY_KEYS.K8S_CLUSTER_NAME]}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.EVENTS && (
|
||||
<ClusterEvents
|
||||
timeRange={modalTimeRange}
|
||||
handleChangeEventFilters={handleChangeEventsFilters}
|
||||
filters={eventsFilters}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
category={K8sCategory.CLUSTERS}
|
||||
queryKey="clusterEvents"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClusterDetails;
|
||||
@@ -1,3 +0,0 @@
|
||||
import ClusterDetails from './ClusterDetails';
|
||||
|
||||
export default ClusterDetails;
|
||||
@@ -1,17 +0,0 @@
|
||||
.infra-monitoring-container {
|
||||
.clusters-list-table {
|
||||
.expanded-table-container {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
min-width: 223px !important;
|
||||
max-width: 223px !important;
|
||||
}
|
||||
|
||||
.ant-table-row-expand-icon-cell {
|
||||
min-width: 30px !important;
|
||||
max-width: 30px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,693 +1,117 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Spin,
|
||||
Table,
|
||||
TableColumnType as ColumnType,
|
||||
TablePaginationConfig,
|
||||
TableProps,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import type { SorterResult } from 'antd/es/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { K8sClustersListPayload } from 'api/infraMonitoring/getK8sClustersList';
|
||||
import React, { useCallback } from 'react';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { useGetK8sClustersList } from 'hooks/infraMonitoring/useGetK8sClustersList';
|
||||
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 { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { getK8sClustersList, K8sClusterData } from './api';
|
||||
import {
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
K8sCategory,
|
||||
} from '../constants';
|
||||
clusterWidgetInfo,
|
||||
getClusterMetricsQueryPayload,
|
||||
k8sClusterDetailsMetadataConfig,
|
||||
k8sClusterGetEntityName,
|
||||
k8sClusterGetSelectedItemFilters,
|
||||
k8sClusterInitialEventsFilter,
|
||||
k8sClusterInitialFilters,
|
||||
k8sClusterInitialLogTracesFilter,
|
||||
} from './constants';
|
||||
import {
|
||||
useInfraMonitoringClusterName,
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringOrderBy,
|
||||
} from '../hooks';
|
||||
import K8sHeader from '../K8sHeader';
|
||||
import LoadingContainer from '../LoadingContainer';
|
||||
import { usePageSize } from '../utils';
|
||||
import ClusterDetails from './ClusterDetails';
|
||||
import {
|
||||
defaultAddedColumns,
|
||||
formatDataForTable,
|
||||
getK8sClustersListColumns,
|
||||
getK8sClustersListQuery,
|
||||
K8sClustersRowData,
|
||||
} from './utils';
|
||||
|
||||
import '../InfraMonitoringK8s.styles.scss';
|
||||
import './K8sClustersList.styles.scss';
|
||||
k8sClustersColumns,
|
||||
k8sClustersColumnsConfig,
|
||||
k8sClustersRenderRowData,
|
||||
} from './table.config';
|
||||
|
||||
function K8sClustersList({
|
||||
isFiltersVisible,
|
||||
handleFilterVisibilityChange,
|
||||
quickFiltersLastUpdated,
|
||||
controlListPrefix,
|
||||
}: {
|
||||
isFiltersVisible: boolean;
|
||||
handleFilterVisibilityChange: () => void;
|
||||
quickFiltersLastUpdated: number;
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
const [
|
||||
selectedClusterName,
|
||||
setselectedClusterName,
|
||||
] = useInfraMonitoringClusterName();
|
||||
|
||||
const [filtersInitialised, setFiltersInitialised] = useState(false);
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
||||
|
||||
const { pageSize, setPageSize } = usePageSize(K8sCategory.CLUSTERS);
|
||||
|
||||
const [
|
||||
selectedRowData,
|
||||
setSelectedRowData,
|
||||
] = useState<K8sClustersRowData | null>(null);
|
||||
|
||||
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],
|
||||
);
|
||||
|
||||
// Reset pagination every time quick filters are changed
|
||||
useEffect(() => {
|
||||
if (quickFiltersLastUpdated !== -1) {
|
||||
setCurrentPage(1);
|
||||
}
|
||||
}, [quickFiltersLastUpdated, setCurrentPage]);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const createFiltersForSelectedRowData = (
|
||||
selectedRowData: K8sClustersRowData,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): IBuilderQuery['filters'] => {
|
||||
const baseFilters: IBuilderQuery['filters'] = {
|
||||
items: [...queryFilters.items],
|
||||
op: 'and',
|
||||
};
|
||||
const fetchListData = useCallback(
|
||||
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
|
||||
filters.orderBy ||= {
|
||||
columnName: 'cpu',
|
||||
order: 'desc',
|
||||
};
|
||||
|
||||
if (!selectedRowData) {
|
||||
return baseFilters;
|
||||
}
|
||||
|
||||
const { groupedByMeta } = selectedRowData;
|
||||
|
||||
for (const key of groupBy) {
|
||||
baseFilters.items.push({
|
||||
key: {
|
||||
key: key.key,
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: groupedByMeta[key.key],
|
||||
id: key.key,
|
||||
});
|
||||
}
|
||||
|
||||
return baseFilters;
|
||||
};
|
||||
|
||||
const fetchGroupedByRowDataQuery = useMemo(() => {
|
||||
if (!selectedRowData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseQuery = getK8sClustersListQuery();
|
||||
|
||||
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
|
||||
|
||||
return {
|
||||
...baseQuery,
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
filters,
|
||||
start: Math.floor(minTime / 1000000),
|
||||
end: Math.floor(maxTime / 1000000),
|
||||
orderBy: orderBy || baseQuery.orderBy,
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
|
||||
|
||||
const groupedByRowDataQueryKey = useMemo(() => {
|
||||
// be careful with what you serialize from selectedRowData
|
||||
// since it's react node, it could contain circular references
|
||||
const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta);
|
||||
if (selectedClusterName) {
|
||||
return [
|
||||
'clusterList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
selectedRowDataKey,
|
||||
];
|
||||
}
|
||||
return [
|
||||
'clusterList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
selectedRowDataKey,
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
queryFilters,
|
||||
orderBy,
|
||||
selectedClusterName,
|
||||
minTime,
|
||||
maxTime,
|
||||
selectedRowData,
|
||||
]);
|
||||
|
||||
const {
|
||||
data: groupedByRowData,
|
||||
isFetching: isFetchingGroupedByRowData,
|
||||
isLoading: isLoadingGroupedByRowData,
|
||||
isError: isErrorGroupedByRowData,
|
||||
refetch: fetchGroupedByRowData,
|
||||
} = useGetK8sClustersList(
|
||||
fetchGroupedByRowDataQuery as K8sClustersListPayload,
|
||||
{
|
||||
queryKey: groupedByRowDataQueryKey,
|
||||
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const {
|
||||
data: groupByFiltersData,
|
||||
isLoading: isLoadingGroupByFilters,
|
||||
} = useGetAggregateKeys(
|
||||
{
|
||||
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
aggregateAttribute: GetK8sEntityToAggregateAttribute(
|
||||
K8sCategory.CLUSTERS,
|
||||
const response = await getK8sClustersList(
|
||||
filters,
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
),
|
||||
aggregateOperator: 'noop',
|
||||
searchText: '',
|
||||
tagType: '',
|
||||
},
|
||||
{
|
||||
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
|
||||
},
|
||||
true,
|
||||
K8sCategory.NODES,
|
||||
);
|
||||
|
||||
const query = useMemo(() => {
|
||||
const baseQuery = getK8sClustersListQuery();
|
||||
const queryPayload = {
|
||||
...baseQuery,
|
||||
limit: pageSize,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
filters: queryFilters,
|
||||
start: Math.floor(minTime / 1000000),
|
||||
end: Math.floor(maxTime / 1000000),
|
||||
orderBy: orderBy || baseQuery.orderBy,
|
||||
};
|
||||
if (groupBy.length > 0) {
|
||||
queryPayload.groupBy = groupBy;
|
||||
}
|
||||
return queryPayload;
|
||||
}, [pageSize, currentPage, queryFilters, minTime, maxTime, orderBy, groupBy]);
|
||||
|
||||
const formattedGroupedByClustersData = useMemo(
|
||||
() =>
|
||||
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
|
||||
[groupedByRowData, groupBy],
|
||||
);
|
||||
|
||||
const nestedClustersData = useMemo(() => {
|
||||
if (!selectedRowData || !groupedByRowData?.payload?.data.records) {
|
||||
return [];
|
||||
}
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedClusterName) {
|
||||
return [
|
||||
'clusterList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'clusterList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
selectedClusterName,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sClustersList(
|
||||
query as K8sClustersListPayload,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const clustersData = useMemo(() => data?.payload?.data?.records || [], [data]);
|
||||
const totalCount = data?.payload?.data?.total || 0;
|
||||
|
||||
const formattedClustersData = useMemo(
|
||||
() => formatDataForTable(clustersData, groupBy),
|
||||
[clustersData, groupBy],
|
||||
);
|
||||
|
||||
const columns = useMemo(() => getK8sClustersListColumns(groupBy), [groupBy]);
|
||||
|
||||
const handleGroupByRowClick = (record: K8sClustersRowData): 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<K8sClustersRowData>['onChange'] = useCallback(
|
||||
(
|
||||
pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter:
|
||||
| SorterResult<K8sClustersRowData>
|
||||
| SorterResult<K8sClustersRowData>[],
|
||||
): void => {
|
||||
if (pagination.current) {
|
||||
setCurrentPage(pagination.current);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
});
|
||||
}
|
||||
|
||||
if ('field' in sorter && sorter.order) {
|
||||
setOrderBy({
|
||||
columnName: sorter.field as string,
|
||||
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
|
||||
});
|
||||
} else {
|
||||
setOrderBy(null);
|
||||
}
|
||||
},
|
||||
[setCurrentPage, setOrderBy],
|
||||
);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const handleFiltersChange = useCallback(
|
||||
(value: IBuilderQuery['filters']): void => {
|
||||
handleChangeQueryData('filters', value);
|
||||
if (filtersInitialised) {
|
||||
setCurrentPage(1);
|
||||
} else {
|
||||
setFiltersInitialised(true);
|
||||
}
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
});
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
total: data?.payload?.data?.total,
|
||||
});
|
||||
}, [data?.payload?.data?.total]);
|
||||
|
||||
const selectedClusterData = useMemo(() => {
|
||||
if (!selectedClusterName) {
|
||||
return null;
|
||||
}
|
||||
if (groupBy.length > 0) {
|
||||
// If grouped by, return the cluster from the formatted grouped by clusters data
|
||||
return (
|
||||
nestedClustersData.find(
|
||||
(cluster) => cluster.meta.k8s_cluster_name === selectedClusterName,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
// If not grouped by, return the cluster from the clusters data
|
||||
return (
|
||||
clustersData.find(
|
||||
(cluster) => cluster.meta.k8s_cluster_name === selectedClusterName,
|
||||
) || null
|
||||
);
|
||||
}, [selectedClusterName, groupBy.length, clustersData, nestedClustersData]);
|
||||
|
||||
const openClusterInNewTab = (record: K8sClustersRowData): void => {
|
||||
const newParams = new URLSearchParams(document.location.search);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME,
|
||||
record.clusterUID,
|
||||
);
|
||||
openInNewTab(
|
||||
buildAbsolutePath({
|
||||
relativePath: '',
|
||||
urlQueryString: newParams.toString(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sClustersRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openClusterInNewTab(record);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedClusterName(record.clusterUID);
|
||||
} else {
|
||||
handleGroupByRowClick(record);
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
});
|
||||
};
|
||||
|
||||
const nestedColumns = useMemo(() => getK8sClustersListColumns([]), []);
|
||||
|
||||
const isGroupedByAttribute = groupBy.length > 0;
|
||||
|
||||
const handleExpandedRowViewAllClick = (): void => {
|
||||
if (!selectedRowData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
|
||||
|
||||
handleFiltersChange(filters);
|
||||
|
||||
setCurrentPage(1);
|
||||
setSelectedRowData(null);
|
||||
setGroupBy([]);
|
||||
setOrderBy(null);
|
||||
};
|
||||
|
||||
const expandedRowRender = (): JSX.Element => (
|
||||
<div className="expanded-table-container">
|
||||
{isErrorGroupedByRowData && (
|
||||
<Typography>{groupedByRowData?.error || 'Something went wrong'}</Typography>
|
||||
)}
|
||||
{isFetchingGroupedByRowData || isLoadingGroupedByRowData ? (
|
||||
<LoadingContainer />
|
||||
) : (
|
||||
<div className="expanded-table">
|
||||
<Table
|
||||
columns={nestedColumns as ColumnType<K8sClustersRowData>[]}
|
||||
dataSource={formattedGroupedByClustersData}
|
||||
pagination={false}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
size="small"
|
||||
loading={{
|
||||
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openClusterInNewTab(record);
|
||||
return;
|
||||
}
|
||||
setselectedClusterName(record.clusterUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
{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: K8sClustersRowData,
|
||||
e: React.MouseEvent<HTMLButtonElement>,
|
||||
) => void;
|
||||
record: K8sClustersRowData;
|
||||
}): 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 handleCloseClusterDetail = (): void => {
|
||||
setselectedClusterName(null);
|
||||
};
|
||||
|
||||
const handleGroupByChange = useCallback(
|
||||
(value: IBuilderQuery['groupBy']) => {
|
||||
const newGroupBy = [];
|
||||
|
||||
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) {
|
||||
newGroupBy.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset pagination on switching to groupBy
|
||||
setCurrentPage(1);
|
||||
setGroupBy(newGroupBy);
|
||||
setExpandedRowKeys([]);
|
||||
logEvent(InfraMonitoringEvents.GroupByChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
});
|
||||
return {
|
||||
data: response.payload?.data.records || [],
|
||||
total: response.payload?.data.total || 0,
|
||||
error: response.error,
|
||||
};
|
||||
},
|
||||
[groupByFiltersData, setCurrentPage, setGroupBy],
|
||||
[dotMetricsEnabled],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (groupByFiltersData?.payload) {
|
||||
setGroupByOptions(
|
||||
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
|
||||
value: filter.key,
|
||||
label: filter.key,
|
||||
})) || [],
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ data: K8sClusterData | null; error?: string | null }> => {
|
||||
const response = await getK8sClustersList(
|
||||
{
|
||||
filters: filters.filters,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
}
|
||||
}, [groupByFiltersData]);
|
||||
|
||||
const onPaginationChange = (page: number, pageSize: number): void => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(pageSize);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Cluster,
|
||||
});
|
||||
};
|
||||
const records = response.payload?.data.records || [];
|
||||
|
||||
const showTableLoadingState =
|
||||
(isFetching || isLoading) && formattedClustersData.length === 0;
|
||||
return {
|
||||
data: records.length > 0 ? records[0] : null,
|
||||
error: response.error,
|
||||
};
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="k8s-list">
|
||||
<K8sHeader
|
||||
isFiltersVisible={isFiltersVisible}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
defaultAddedColumns={defaultAddedColumns}
|
||||
handleFiltersChange={handleFiltersChange}
|
||||
groupByOptions={groupByOptions}
|
||||
isLoadingGroupByFilters={isLoadingGroupByFilters}
|
||||
handleGroupByChange={handleGroupByChange}
|
||||
selectedGroupBy={groupBy}
|
||||
entity={K8sCategory.NODES}
|
||||
showAutoRefresh={!selectedClusterData}
|
||||
/>
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
<Table
|
||||
className="k8s-list-table clusters-list-table"
|
||||
dataSource={showTableLoadingState ? [] : formattedClustersData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
total: totalCount,
|
||||
showSizeChanger: true,
|
||||
hideOnSinglePage: false,
|
||||
onChange: onPaginationChange,
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
loading={{
|
||||
spinning: showTableLoadingState,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
locale={{
|
||||
emptyText: showTableLoadingState ? null : (
|
||||
<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>
|
||||
),
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
expandIcon: expandRowIconRenderer,
|
||||
expandedRowKeys,
|
||||
}}
|
||||
<>
|
||||
<K8sBaseList<K8sClusterData>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.CLUSTERS}
|
||||
tableColumnsDefinitions={k8sClustersColumns}
|
||||
tableColumns={k8sClustersColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={k8sClustersRenderRowData}
|
||||
eventCategory={InfraMonitoringEvents.Cluster}
|
||||
/>
|
||||
|
||||
<ClusterDetails
|
||||
cluster={selectedClusterData}
|
||||
isModalTimeSelection
|
||||
onClose={handleCloseClusterDetail}
|
||||
<K8sBaseDetails<K8sClusterData>
|
||||
category={InfraMonitoringEntity.CLUSTERS}
|
||||
eventCategory={InfraMonitoringEvents.Cluster}
|
||||
getSelectedItemFilters={k8sClusterGetSelectedItemFilters}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sClusterGetEntityName}
|
||||
getInitialLogTracesFilters={k8sClusterInitialLogTracesFilter}
|
||||
getInitialEventsFilters={k8sClusterInitialEventsFilter}
|
||||
primaryFilterKeys={k8sClusterInitialFilters}
|
||||
metadataConfig={k8sClusterDetailsMetadataConfig}
|
||||
entityWidgetInfo={clusterWidgetInfo}
|
||||
getEntityQueryPayload={getClusterMetricsQueryPayload}
|
||||
queryKeyPrefix="cluster"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
125
frontend/src/container/InfraMonitoringK8s/Clusters/api.ts
Normal file
125
frontend/src/container/InfraMonitoringK8s/Clusters/api.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { UnderscoreToDotMap } from 'api/utils';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
|
||||
export interface K8sClustersListPayload {
|
||||
filters: TagFilter;
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: {
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sClusterData {
|
||||
clusterUID: string;
|
||||
cpuUsage: number;
|
||||
cpuAllocatable: number;
|
||||
memoryUsage: number;
|
||||
memoryAllocatable: number;
|
||||
meta: {
|
||||
k8s_cluster_name: string;
|
||||
k8s_cluster_uid: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sClustersListResponse {
|
||||
status: string;
|
||||
data: {
|
||||
type: string;
|
||||
records: K8sClusterData[];
|
||||
groups: null;
|
||||
total: number;
|
||||
sentAnyHostMetricsData: boolean;
|
||||
isSendingK8SAgentMetrics: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(H4ad): Erase this whole file when migrating to openapi
|
||||
export const clustersMetaMap = [
|
||||
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
|
||||
{ dot: 'k8s.cluster.uid', under: 'k8s_cluster_uid' },
|
||||
] as const;
|
||||
|
||||
export function mapClustersMeta(
|
||||
raw: Record<string, unknown>,
|
||||
): K8sClusterData['meta'] {
|
||||
const out: Record<string, unknown> = { ...raw };
|
||||
clustersMetaMap.forEach(({ dot, under }) => {
|
||||
if (dot in raw) {
|
||||
const v = raw[dot];
|
||||
out[under] = typeof v === 'string' ? v : raw[under];
|
||||
}
|
||||
});
|
||||
return out as K8sClusterData['meta'];
|
||||
}
|
||||
|
||||
export const getK8sClustersList = async (
|
||||
props: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
): Promise<SuccessResponse<K8sClustersListResponse> | ErrorResponse> => {
|
||||
try {
|
||||
const requestProps = dotMetricsEnabled
|
||||
? {
|
||||
...props,
|
||||
filters: {
|
||||
...props.filters,
|
||||
items: props.filters.items.reduce<typeof props.filters.items>(
|
||||
(acc, item) => {
|
||||
if (item.value === undefined) {
|
||||
return acc;
|
||||
}
|
||||
if (
|
||||
item.key &&
|
||||
typeof item.key === 'object' &&
|
||||
'key' in item.key &&
|
||||
typeof item.key.key === 'string'
|
||||
) {
|
||||
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
|
||||
acc.push({
|
||||
...item,
|
||||
key: { ...item.key, key: mappedKey },
|
||||
});
|
||||
} else {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as typeof props.filters.items,
|
||||
),
|
||||
},
|
||||
}
|
||||
: props;
|
||||
|
||||
const response = await axios.post('/clusters/list', requestProps, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
const payload: K8sClustersListResponse = response.data;
|
||||
|
||||
payload.data.records = payload.data.records.map((record) => ({
|
||||
...record,
|
||||
meta: mapClustersMeta(record.meta as Record<string, unknown>),
|
||||
}));
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload,
|
||||
params: requestProps,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
@@ -1,11 +1,57 @@
|
||||
import { K8sClustersData } from 'api/infraMonitoring/getK8sClustersList';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
createFilterItem,
|
||||
K8sDetailsMetadataConfig,
|
||||
} from '../Base/K8sBaseDetails';
|
||||
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
|
||||
import { K8sClusterData } from './api';
|
||||
|
||||
export const k8sClusterGetSelectedItemFilters = (
|
||||
selectedItemId: string,
|
||||
): TagFilter => ({
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: 'k8s_cluster_name',
|
||||
key: {
|
||||
key: 'k8s_cluster_name',
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: selectedItemId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const k8sClusterDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sClusterData>[] = [
|
||||
{ label: 'Cluster Name', getValue: (p): string => p.meta.k8s_cluster_name },
|
||||
];
|
||||
|
||||
export const k8sClusterInitialFilters = [QUERY_KEYS.K8S_CLUSTER_NAME];
|
||||
|
||||
export const k8sClusterInitialEventsFilter = (
|
||||
item: K8sClusterData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Cluster'),
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_cluster_name),
|
||||
];
|
||||
|
||||
export const k8sClusterInitialLogTracesFilter = (
|
||||
item: K8sClusterData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_CLUSTER_NAME, item.meta.k8s_cluster_name),
|
||||
];
|
||||
|
||||
export const k8sClusterGetEntityName = (item: K8sClusterData): string =>
|
||||
item.meta.k8s_cluster_name;
|
||||
|
||||
export const clusterWidgetInfo = [
|
||||
{
|
||||
title: 'CPU Usage, allocatable',
|
||||
@@ -42,7 +88,7 @@ export const clusterWidgetInfo = [
|
||||
];
|
||||
|
||||
export const getClusterMetricsQueryPayload = (
|
||||
cluster: K8sClustersData,
|
||||
cluster: K8sClusterData,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
@@ -0,0 +1,183 @@
|
||||
import { TableColumnType as ColumnType, Tooltip } from 'antd';
|
||||
import { Group } from 'lucide-react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { K8sRenderedRowData } from '../Base/types';
|
||||
import { IEntityColumn } from '../Base/useInfraMonitoringTableColumnsStore';
|
||||
import { getGroupByEl, getGroupedByMeta, getRowKey } from '../Base/utils';
|
||||
import { formatBytes, ValidateColumnValueWrapper } from '../commonUtils';
|
||||
import { K8sClusterData, K8sClustersListPayload } from './api';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
|
||||
export interface K8sClustersRowData {
|
||||
key: string;
|
||||
itemKey: string;
|
||||
clusterUID: string;
|
||||
clusterName: React.ReactNode;
|
||||
cpu: React.ReactNode;
|
||||
cpu_allocatable: React.ReactNode;
|
||||
memory: React.ReactNode;
|
||||
memory_allocatable: React.ReactNode;
|
||||
groupedByMeta?: Record<string, string>;
|
||||
}
|
||||
|
||||
export const k8sClustersColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'Cluster Group',
|
||||
value: 'clusterGroup',
|
||||
id: 'clusterGroup',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-collapse',
|
||||
},
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
value: 'clusterName',
|
||||
id: 'clusterName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-expand',
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Alloc (cores)',
|
||||
value: 'cpu_allocatable',
|
||||
id: 'cpu_allocatable',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Memory Usage (WSS)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Memory Alloc (bytes)',
|
||||
value: 'memory_allocatable',
|
||||
id: 'memory_allocatable',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
];
|
||||
|
||||
export const getK8sClustersListQuery = (): K8sClustersListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
export const k8sClustersColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
|
||||
{
|
||||
title: (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
<Group size={14} /> CLUSTER GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'clusterGroup',
|
||||
key: 'clusterGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
},
|
||||
{
|
||||
title: <div>Cluster Name</div>,
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Alloc (cores)</div>,
|
||||
dataIndex: 'cpu_allocatable',
|
||||
key: 'cpu_allocatable',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Memory Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Memory Allocatable</div>,
|
||||
dataIndex: 'memory_allocatable',
|
||||
key: 'memory_allocatable',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sClustersRenderRowData = (
|
||||
cluster: K8sClusterData,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): K8sRenderedRowData => ({
|
||||
key: getRowKey(
|
||||
cluster,
|
||||
() =>
|
||||
cluster.clusterUID ||
|
||||
cluster.meta.k8s_cluster_uid ||
|
||||
cluster.meta.k8s_cluster_name,
|
||||
groupBy,
|
||||
),
|
||||
itemKey: cluster.meta.k8s_cluster_name,
|
||||
clusterUID: cluster.clusterUID || cluster.meta.k8s_cluster_uid,
|
||||
clusterName: (
|
||||
<Tooltip title={cluster.meta.k8s_cluster_name}>
|
||||
{cluster.meta.k8s_cluster_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={cluster.cpuUsage}>
|
||||
{cluster.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={cluster.memoryUsage}>
|
||||
{formatBytes(cluster.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_allocatable: (
|
||||
<ValidateColumnValueWrapper value={cluster.cpuAllocatable}>
|
||||
{cluster.cpuAllocatable}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_allocatable: (
|
||||
<ValidateColumnValueWrapper value={cluster.memoryAllocatable}>
|
||||
{formatBytes(cluster.memoryAllocatable)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
clusterGroup: getGroupByEl(cluster, groupBy),
|
||||
...cluster.meta,
|
||||
groupedByMeta: getGroupedByMeta(cluster, groupBy),
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
.entityGroupHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: var(--spacing-5);
|
||||
gap: var(--spacing-5);
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
|
||||
import {
|
||||
K8sClustersData,
|
||||
K8sClustersListPayload,
|
||||
} from 'api/infraMonitoring/getK8sClustersList';
|
||||
import { Group } from 'lucide-react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { formatBytes, ValidateColumnValueWrapper } from '../commonUtils';
|
||||
import { IEntityColumn } from '../utils';
|
||||
|
||||
export const defaultAddedColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
value: 'clusterName',
|
||||
id: 'cluster',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Allocatable (cores)',
|
||||
value: 'cpu_allocatable',
|
||||
id: 'cpu_allocatable',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Mem Usage (WSS)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Mem Allocatable',
|
||||
value: 'memory_allocatable',
|
||||
id: 'memory_allocatable',
|
||||
canRemove: false,
|
||||
},
|
||||
];
|
||||
|
||||
export interface K8sClustersRowData {
|
||||
key: string;
|
||||
clusterUID: string;
|
||||
clusterName: React.ReactNode;
|
||||
cpu: React.ReactNode;
|
||||
memory: React.ReactNode;
|
||||
cpu_allocatable: React.ReactNode;
|
||||
memory_allocatable: React.ReactNode;
|
||||
groupedByMeta?: any;
|
||||
}
|
||||
|
||||
const clusterGroupColumnConfig = {
|
||||
title: (
|
||||
<div className="column-header entity-group-header">
|
||||
<Group size={14} /> CLUSTER GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'clusterGroup',
|
||||
key: 'clusterGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
className: 'column entity-group-header',
|
||||
};
|
||||
|
||||
export const getK8sClustersListQuery = (): K8sClustersListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
const columnsConfig = [
|
||||
{
|
||||
title: <div className="column-header-left">Cluster Name</div>,
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left">CPU Utilization (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left">CPU Allocatable (cores)</div>,
|
||||
dataIndex: 'cpu_allocatable',
|
||||
key: 'cpu_allocatable',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left">Memory Utilization (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left">Memory Allocatable</div>,
|
||||
dataIndex: 'memory_allocatable',
|
||||
key: 'memory_allocatable',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
|
||||
export const getK8sClustersListColumns = (
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): ColumnType<K8sClustersRowData>[] => {
|
||||
if (groupBy.length > 0) {
|
||||
const filteredColumns = [...columnsConfig].filter(
|
||||
(column) => column.key !== 'clusterName',
|
||||
);
|
||||
filteredColumns.unshift(clusterGroupColumnConfig);
|
||||
return filteredColumns as ColumnType<K8sClustersRowData>[];
|
||||
}
|
||||
|
||||
return columnsConfig as ColumnType<K8sClustersRowData>[];
|
||||
};
|
||||
|
||||
const dotToUnder: Record<string, keyof K8sClustersData['meta']> = {
|
||||
'k8s.cluster.name': 'k8s_cluster_name',
|
||||
'k8s.cluster.uid': 'k8s_cluster_uid',
|
||||
};
|
||||
|
||||
const getGroupByEle = (
|
||||
cluster: K8sClustersData,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): React.ReactNode => {
|
||||
const groupByValues: string[] = [];
|
||||
|
||||
groupBy.forEach((group) => {
|
||||
const rawKey = group.key as string;
|
||||
|
||||
// Choose mapped key if present, otherwise use rawKey
|
||||
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof cluster.meta;
|
||||
const value = cluster.meta[metaKey];
|
||||
|
||||
groupByValues.push(value);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="pod-group">
|
||||
{groupByValues.map((value) => (
|
||||
<Tag key={value} color={Color.BG_SLATE_400} className="pod-group-tag-item">
|
||||
{value === '' ? '<no-value>' : value}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const formatDataForTable = (
|
||||
data: K8sClustersData[],
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): K8sClustersRowData[] =>
|
||||
data.map((cluster, index) => ({
|
||||
key: index.toString(),
|
||||
clusterUID: cluster.meta.k8s_cluster_name,
|
||||
clusterName: (
|
||||
<Tooltip title={cluster.meta.k8s_cluster_name}>
|
||||
{cluster.meta.k8s_cluster_name}
|
||||
</Tooltip>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={cluster.cpuUsage}>
|
||||
{cluster.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={cluster.memoryUsage}>
|
||||
{formatBytes(cluster.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_allocatable: (
|
||||
<ValidateColumnValueWrapper value={cluster.cpuAllocatable}>
|
||||
{cluster.cpuAllocatable}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_allocatable: (
|
||||
<ValidateColumnValueWrapper value={cluster.memoryAllocatable}>
|
||||
{formatBytes(cluster.memoryAllocatable)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
clusterGroup: getGroupByEle(cluster, groupBy),
|
||||
meta: cluster.meta,
|
||||
...cluster.meta,
|
||||
groupedByMeta: cluster.meta,
|
||||
}));
|
||||
@@ -1,7 +0,0 @@
|
||||
import { K8sDaemonSetsData } from 'api/infraMonitoring/getK8sDaemonSetsList';
|
||||
|
||||
export type DaemonSetDetailsProps = {
|
||||
daemonSet: K8sDaemonSetsData | null;
|
||||
isModalTimeSelection: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -1,667 +0,0 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
|
||||
import type { RadioChangeEvent } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { VIEW_TYPES, VIEWS } from 'components/HostMetricsDetail/constants';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryState,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import {
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from 'container/InfraMonitoringK8s/hooks';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
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 { 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 DaemonSetEvents from '../../EntityDetailsUtils/EntityEvents';
|
||||
import DaemonSetLogs from '../../EntityDetailsUtils/EntityLogs';
|
||||
import DaemonSetMetrics from '../../EntityDetailsUtils/EntityMetrics';
|
||||
import DaemonSetTraces from '../../EntityDetailsUtils/EntityTraces';
|
||||
import { QUERY_KEYS } from '../../EntityDetailsUtils/utils';
|
||||
import {
|
||||
daemonSetWidgetInfo,
|
||||
getDaemonSetMetricsQueryPayload,
|
||||
} from './constants';
|
||||
import { DaemonSetDetailsProps } from './DaemonSetDetails.interfaces';
|
||||
|
||||
import '../../EntityDetailsUtils/entityDetails.styles.scss';
|
||||
|
||||
function DaemonSetDetails({
|
||||
daemonSet,
|
||||
onClose,
|
||||
isModalTimeSelection,
|
||||
}: DaemonSetDetailsProps): 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 lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
);
|
||||
|
||||
const [selectedView, setSelectedView] = useInfraMonitoringView();
|
||||
const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters();
|
||||
const [
|
||||
tracesFiltersParam,
|
||||
setTracesFiltersParam,
|
||||
] = useInfraMonitoringTracesFilters();
|
||||
const [
|
||||
eventsFiltersParam,
|
||||
setEventsFiltersParam,
|
||||
] = useInfraMonitoringEventsFilters();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const initialFilters = useMemo(() => {
|
||||
const filters =
|
||||
selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam;
|
||||
if (filters) {
|
||||
return filters;
|
||||
}
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_DAEMON_SET_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s_daemonSet_name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: daemonSet?.meta.k8s_daemonset_name || '',
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s_daemonSet_name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: daemonSet?.meta.k8s_namespace_name || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [
|
||||
daemonSet?.meta.k8s_daemonset_name,
|
||||
daemonSet?.meta.k8s_namespace_name,
|
||||
selectedView,
|
||||
logFiltersParam,
|
||||
tracesFiltersParam,
|
||||
]);
|
||||
|
||||
const initialEventsFilters = useMemo(() => {
|
||||
if (eventsFiltersParam) {
|
||||
return eventsFiltersParam;
|
||||
}
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s.object.kind--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: 'DaemonSet',
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s.object.name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: daemonSet?.meta.k8s_daemonset_name || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [daemonSet?.meta.k8s_daemonset_name, eventsFiltersParam]);
|
||||
|
||||
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
|
||||
IBuilderQuery['filters']
|
||||
>(initialFilters);
|
||||
|
||||
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
|
||||
initialEventsFilters,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (daemonSet) {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
});
|
||||
}
|
||||
}, [daemonSet]);
|
||||
|
||||
useEffect(() => {
|
||||
setLogAndTracesFilters(initialFilters);
|
||||
setEventsFilters(initialEventsFilters);
|
||||
}, [initialFilters, initialEventsFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
endTime: Math.floor(maxTime / 1000000000),
|
||||
});
|
||||
}
|
||||
}, [selectedTime, minTime, maxTime]);
|
||||
|
||||
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||
setSelectedView(e.target.value);
|
||||
setLogFiltersParam(null);
|
||||
setTracesFiltersParam(null);
|
||||
setEventsFiltersParam(null);
|
||||
logEvent(InfraMonitoringEvents.TabChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
view: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
lastSelectedInterval.current = interval as Time;
|
||||
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(InfraMonitoringEvents.TimeUpdated, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
interval,
|
||||
view: selectedView,
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeLogFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setLogAndTracesFilters((prevFilters) => {
|
||||
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||
[QUERY_KEYS.K8S_DAEMON_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
|
||||
item.key?.key ?? '',
|
||||
),
|
||||
);
|
||||
const paginationFilter = value?.items?.find(
|
||||
(item) => item.key?.key === 'id',
|
||||
);
|
||||
const newFilters = value?.items?.filter(
|
||||
(item) =>
|
||||
item.key?.key !== 'id' &&
|
||||
item.key?.key !== QUERY_KEYS.K8S_DAEMON_SET_NAME,
|
||||
);
|
||||
|
||||
if (newFilters && newFilters?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
view: InfraMonitoringEvents.LogsView,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...(primaryFilters || []),
|
||||
...(newFilters || []),
|
||||
...(paginationFilter ? [paginationFilter] : []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
setLogFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeTracesFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setLogAndTracesFilters((prevFilters) => {
|
||||
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||
[QUERY_KEYS.K8S_DAEMON_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
|
||||
item.key?.key ?? '',
|
||||
),
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
view: InfraMonitoringEvents.TracesView,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...(primaryFilters || []),
|
||||
...(value?.items?.filter(
|
||||
(item) => item.key?.key !== QUERY_KEYS.K8S_DAEMON_SET_NAME,
|
||||
) || []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
setTracesFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeEventsFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setEventsFilters((prevFilters) => {
|
||||
const daemonSetKindFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
);
|
||||
const daemonSetNameFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
view: InfraMonitoringEvents.EventsView,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: [
|
||||
daemonSetKindFilter,
|
||||
daemonSetNameFilter,
|
||||
...(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),
|
||||
};
|
||||
|
||||
setEventsFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// 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(InfraMonitoringEvents.ExploreClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
view: selectedView,
|
||||
});
|
||||
|
||||
if (selectedView === VIEW_TYPES.LOGS) {
|
||||
const filtersWithoutPagination = {
|
||||
...logAndTracesFilters,
|
||||
items: logAndTracesFilters?.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: logAndTracesFilters,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
lastSelectedInterval.current = null;
|
||||
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">
|
||||
{daemonSet?.meta.k8s_daemonset_name}
|
||||
</Typography.Text>
|
||||
</>
|
||||
}
|
||||
placement="right"
|
||||
onClose={handleClose}
|
||||
open={!!daemonSet}
|
||||
style={{
|
||||
overscrollBehavior: 'contain',
|
||||
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||
}}
|
||||
className="entity-detail-drawer"
|
||||
destroyOnClose
|
||||
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||
>
|
||||
{daemonSet && (
|
||||
<>
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Daemonset Name
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Cluster Name
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Namespace Name
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="values-row">
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={daemonSet.meta.k8s_daemonset_name}>
|
||||
{daemonSet.meta.k8s_daemonset_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={daemonSet.meta.k8s_cluster_name}>
|
||||
{daemonSet.meta.k8s_cluster_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={daemonSet.meta.k8s_namespace_name}>
|
||||
{daemonSet.meta.k8s_namespace_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="views-tabs-container">
|
||||
<Radio.Group
|
||||
className="views-tabs"
|
||||
onChange={handleTabChange}
|
||||
value={selectedView}
|
||||
>
|
||||
<Radio.Button
|
||||
className={
|
||||
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 && (
|
||||
<DaemonSetMetrics
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
entity={daemonSet}
|
||||
entityWidgetInfo={daemonSetWidgetInfo}
|
||||
getEntityQueryPayload={getDaemonSetMetricsQueryPayload}
|
||||
category={K8sCategory.DAEMONSETS}
|
||||
queryKey="daemonsetMetrics"
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<DaemonSetLogs
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeLogFilters={handleChangeLogFilters}
|
||||
logFilters={logAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
category={K8sCategory.DAEMONSETS}
|
||||
queryKey="daemonsetLogs"
|
||||
queryKeyFilters={[
|
||||
QUERY_KEYS.K8S_DAEMON_SET_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.TRACES && (
|
||||
<DaemonSetTraces
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeTracesFilters={handleChangeTracesFilters}
|
||||
tracesFilters={logAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey="daemonsetTraces"
|
||||
category={InfraMonitoringEvents.DaemonSet}
|
||||
queryKeyFilters={[
|
||||
QUERY_KEYS.K8S_DAEMON_SET_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.EVENTS && (
|
||||
<DaemonSetEvents
|
||||
timeRange={modalTimeRange}
|
||||
handleChangeEventFilters={handleChangeEventsFilters}
|
||||
filters={eventsFilters}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey="daemonsetEvents"
|
||||
category={K8sCategory.DAEMONSETS}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default DaemonSetDetails;
|
||||
@@ -1,3 +0,0 @@
|
||||
import DaemonSetDetails from './DaemonSetDetails';
|
||||
|
||||
export default DaemonSetDetails;
|
||||
@@ -1,62 +0,0 @@
|
||||
.infra-monitoring-container {
|
||||
.daemonSets-list-table {
|
||||
.expanded-table-container {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.ant-table-cell.column-progress-bar {
|
||||
min-width: 180px;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.ant-table-row-expand-icon-cell {
|
||||
min-width: 30px !important;
|
||||
max-width: 30px !important;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.daemonset-name-header) {
|
||||
min-width: 250px !important;
|
||||
max-width: 250px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.namespace-name-header) {
|
||||
min-width: 220px !important;
|
||||
max-width: 220px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.med-col) {
|
||||
min-width: 200px !important;
|
||||
max-width: 200px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.small-col) {
|
||||
min-width: 120px !important;
|
||||
max-width: 120px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.expanded-daemonsets-list-table {
|
||||
.ant-table-cell {
|
||||
min-width: 220px !important;
|
||||
max-width: 220px !important;
|
||||
}
|
||||
|
||||
.ant-table-row-expand-icon-cell {
|
||||
min-width: 30px !important;
|
||||
max-width: 30px !important;
|
||||
}
|
||||
}
|
||||
@@ -1,715 +1,117 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Spin,
|
||||
Table,
|
||||
TableColumnType as ColumnType,
|
||||
TablePaginationConfig,
|
||||
TableProps,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import type { SorterResult } from 'antd/es/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { K8sDaemonSetsListPayload } from 'api/infraMonitoring/getK8sDaemonSetsList';
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback } from 'react';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { useGetK8sDaemonSetsList } from 'hooks/infraMonitoring/useGetK8sDaemonSetsList';
|
||||
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 { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { getK8sDaemonSetsList, K8sDaemonSetsData } from './api';
|
||||
import {
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
K8sCategory,
|
||||
} from '../constants';
|
||||
daemonSetWidgetInfo,
|
||||
getDaemonSetMetricsQueryPayload,
|
||||
k8sDaemonSetDetailsMetadataConfig,
|
||||
k8sDaemonSetGetEntityName,
|
||||
k8sDaemonSetGetSelectedItemFilters,
|
||||
k8sDaemonSetInitialEventsFilter,
|
||||
k8sDaemonSetInitialFilters,
|
||||
k8sDaemonSetInitialLogTracesFilter,
|
||||
} from './constants';
|
||||
import {
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringDaemonSetUID,
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from '../hooks';
|
||||
import K8sHeader from '../K8sHeader';
|
||||
import LoadingContainer from '../LoadingContainer';
|
||||
import { usePageSize } from '../utils';
|
||||
import DaemonSetDetails from './DaemonSetDetails';
|
||||
import {
|
||||
defaultAddedColumns,
|
||||
formatDataForTable,
|
||||
getK8sDaemonSetsListColumns,
|
||||
getK8sDaemonSetsListQuery,
|
||||
K8sDaemonSetsRowData,
|
||||
} from './utils';
|
||||
|
||||
import '../InfraMonitoringK8s.styles.scss';
|
||||
import './K8sDaemonSetsList.styles.scss';
|
||||
k8sDaemonSetsColumns,
|
||||
k8sDaemonSetsColumnsConfig,
|
||||
k8sDaemonSetsRenderRowData,
|
||||
} from './table.config';
|
||||
|
||||
function K8sDaemonSetsList({
|
||||
isFiltersVisible,
|
||||
handleFilterVisibilityChange,
|
||||
quickFiltersLastUpdated,
|
||||
controlListPrefix,
|
||||
}: {
|
||||
isFiltersVisible: boolean;
|
||||
handleFilterVisibilityChange: () => void;
|
||||
quickFiltersLastUpdated: number;
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [filtersInitialised, setFiltersInitialised] = useState(false);
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
||||
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
|
||||
const [
|
||||
selectedDaemonSetUID,
|
||||
setSelectedDaemonSetUID,
|
||||
] = useInfraMonitoringDaemonSetUID();
|
||||
|
||||
const { pageSize, setPageSize } = usePageSize(K8sCategory.DAEMONSETS);
|
||||
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
|
||||
const [, setView] = useInfraMonitoringView();
|
||||
const [, setTracesFilters] = useInfraMonitoringTracesFilters();
|
||||
const [, setEventsFilters] = useInfraMonitoringEventsFilters();
|
||||
const [, setLogFilters] = useInfraMonitoringLogFilters();
|
||||
|
||||
const [
|
||||
selectedRowData,
|
||||
setSelectedRowData,
|
||||
] = useState<K8sDaemonSetsRowData | null>(null);
|
||||
|
||||
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],
|
||||
);
|
||||
|
||||
// Reset pagination every time quick filters are changed
|
||||
useEffect(() => {
|
||||
if (quickFiltersLastUpdated !== -1) {
|
||||
setCurrentPage(1);
|
||||
}
|
||||
}, [quickFiltersLastUpdated, setCurrentPage]);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const createFiltersForSelectedRowData = (
|
||||
selectedRowData: K8sDaemonSetsRowData,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): IBuilderQuery['filters'] => {
|
||||
const baseFilters: IBuilderQuery['filters'] = {
|
||||
items: [...queryFilters.items],
|
||||
op: 'and',
|
||||
};
|
||||
const fetchListData = useCallback(
|
||||
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
|
||||
filters.orderBy ||= {
|
||||
columnName: 'cpu',
|
||||
order: 'desc',
|
||||
};
|
||||
|
||||
if (!selectedRowData) {
|
||||
return baseFilters;
|
||||
}
|
||||
|
||||
const { groupedByMeta } = selectedRowData;
|
||||
|
||||
for (const key of groupBy) {
|
||||
baseFilters.items.push({
|
||||
key: {
|
||||
key: key.key,
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: groupedByMeta[key.key],
|
||||
id: key.key,
|
||||
});
|
||||
}
|
||||
|
||||
return baseFilters;
|
||||
};
|
||||
|
||||
const fetchGroupedByRowDataQuery = useMemo(() => {
|
||||
if (!selectedRowData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseQuery = getK8sDaemonSetsListQuery();
|
||||
|
||||
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
|
||||
|
||||
return {
|
||||
...baseQuery,
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
filters,
|
||||
start: Math.floor(minTime / 1000000),
|
||||
end: Math.floor(maxTime / 1000000),
|
||||
orderBy: orderBy || baseQuery.orderBy,
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
|
||||
|
||||
const groupedByRowDataQueryKey = useMemo(() => {
|
||||
// be careful with what you serialize from selectedRowData
|
||||
// since it's react node, it could contain circular references
|
||||
const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta);
|
||||
if (selectedDaemonSetUID) {
|
||||
return [
|
||||
'daemonSetList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
selectedRowDataKey,
|
||||
];
|
||||
}
|
||||
return [
|
||||
'daemonSetList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
selectedRowDataKey,
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
queryFilters,
|
||||
orderBy,
|
||||
selectedDaemonSetUID,
|
||||
minTime,
|
||||
maxTime,
|
||||
selectedRowData,
|
||||
]);
|
||||
|
||||
const {
|
||||
data: groupedByRowData,
|
||||
isFetching: isFetchingGroupedByRowData,
|
||||
isLoading: isLoadingGroupedByRowData,
|
||||
isError: isErrorGroupedByRowData,
|
||||
refetch: fetchGroupedByRowData,
|
||||
} = useGetK8sDaemonSetsList(
|
||||
fetchGroupedByRowDataQuery as K8sDaemonSetsListPayload,
|
||||
{
|
||||
queryKey: groupedByRowDataQueryKey,
|
||||
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const {
|
||||
data: groupByFiltersData,
|
||||
isLoading: isLoadingGroupByFilters,
|
||||
} = useGetAggregateKeys(
|
||||
{
|
||||
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
aggregateAttribute: GetK8sEntityToAggregateAttribute(
|
||||
K8sCategory.DAEMONSETS,
|
||||
const response = await getK8sDaemonSetsList(
|
||||
filters,
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
),
|
||||
aggregateOperator: 'noop',
|
||||
searchText: '',
|
||||
tagType: '',
|
||||
},
|
||||
{
|
||||
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
|
||||
},
|
||||
true,
|
||||
K8sCategory.DAEMONSETS,
|
||||
);
|
||||
|
||||
const query = useMemo(() => {
|
||||
const baseQuery = getK8sDaemonSetsListQuery();
|
||||
const queryPayload = {
|
||||
...baseQuery,
|
||||
limit: pageSize,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
filters: queryFilters,
|
||||
start: Math.floor(minTime / 1000000),
|
||||
end: Math.floor(maxTime / 1000000),
|
||||
orderBy: orderBy || baseQuery.orderBy,
|
||||
};
|
||||
if (groupBy.length > 0) {
|
||||
queryPayload.groupBy = groupBy;
|
||||
}
|
||||
return queryPayload;
|
||||
}, [pageSize, currentPage, queryFilters, minTime, maxTime, orderBy, groupBy]);
|
||||
|
||||
const formattedGroupedByDaemonSetsData = useMemo(
|
||||
() =>
|
||||
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
|
||||
[groupedByRowData, groupBy],
|
||||
);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedDaemonSetUID) {
|
||||
return [
|
||||
'daemonSetList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'daemonSetList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
selectedDaemonSetUID,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sDaemonSetsList(
|
||||
query as K8sDaemonSetsListPayload,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const daemonSetsData = useMemo(() => data?.payload?.data?.records || [], [
|
||||
data,
|
||||
]);
|
||||
const totalCount = data?.payload?.data?.total || 0;
|
||||
|
||||
const formattedDaemonSetsData = useMemo(
|
||||
() => formatDataForTable(daemonSetsData, groupBy),
|
||||
[daemonSetsData, groupBy],
|
||||
);
|
||||
|
||||
const nestedDaemonSetsData = useMemo(() => {
|
||||
if (!selectedRowData || !groupedByRowData?.payload?.data.records) {
|
||||
return [];
|
||||
}
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const columns = useMemo(() => getK8sDaemonSetsListColumns(groupBy), [groupBy]);
|
||||
|
||||
const handleGroupByRowClick = (record: K8sDaemonSetsRowData): 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<K8sDaemonSetsRowData>['onChange'] = useCallback(
|
||||
(
|
||||
pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter:
|
||||
| SorterResult<K8sDaemonSetsRowData>
|
||||
| SorterResult<K8sDaemonSetsRowData>[],
|
||||
): void => {
|
||||
if (pagination.current) {
|
||||
setCurrentPage(pagination.current);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
});
|
||||
}
|
||||
|
||||
if ('field' in sorter && sorter.order) {
|
||||
setOrderBy({
|
||||
columnName: sorter.field as string,
|
||||
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
|
||||
});
|
||||
} else {
|
||||
setOrderBy(null);
|
||||
}
|
||||
},
|
||||
[setCurrentPage, setOrderBy],
|
||||
);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const handleFiltersChange = useCallback(
|
||||
(value: IBuilderQuery['filters']): void => {
|
||||
handleChangeQueryData('filters', value);
|
||||
if (filtersInitialised) {
|
||||
setCurrentPage(1);
|
||||
} else {
|
||||
setFiltersInitialised(true);
|
||||
}
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
});
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
total: data?.payload?.data?.total,
|
||||
});
|
||||
}, [data?.payload?.data?.total]);
|
||||
|
||||
const selectedDaemonSetData = useMemo(() => {
|
||||
if (groupBy.length > 0) {
|
||||
// If grouped by, return the daemonset from the formatted grouped by data
|
||||
return (
|
||||
nestedDaemonSetsData.find(
|
||||
(daemonSet) => daemonSet.daemonSetName === selectedDaemonSetUID,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
// If not grouped by, return the daemonset from the daemonsets data
|
||||
return (
|
||||
daemonSetsData.find(
|
||||
(daemonSet) => daemonSet.daemonSetName === selectedDaemonSetUID,
|
||||
) || null
|
||||
);
|
||||
}, [
|
||||
selectedDaemonSetUID,
|
||||
groupBy.length,
|
||||
daemonSetsData,
|
||||
nestedDaemonSetsData,
|
||||
]);
|
||||
|
||||
const openDaemonSetInNewTab = (record: K8sDaemonSetsRowData): void => {
|
||||
const newParams = new URLSearchParams(document.location.search);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID,
|
||||
record.daemonsetUID,
|
||||
);
|
||||
openInNewTab(
|
||||
buildAbsolutePath({
|
||||
relativePath: '',
|
||||
urlQueryString: newParams.toString(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sDaemonSetsRowData,
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openDaemonSetInNewTab(record);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setSelectedDaemonSetUID(record.daemonsetUID);
|
||||
} else {
|
||||
handleGroupByRowClick(record);
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
});
|
||||
};
|
||||
|
||||
const nestedColumns = useMemo(() => getK8sDaemonSetsListColumns([]), []);
|
||||
|
||||
const isGroupedByAttribute = groupBy.length > 0;
|
||||
|
||||
const handleExpandedRowViewAllClick = (): void => {
|
||||
if (!selectedRowData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
|
||||
|
||||
handleFiltersChange(filters);
|
||||
|
||||
setCurrentPage(1);
|
||||
setSelectedRowData(null);
|
||||
setGroupBy([]);
|
||||
setOrderBy(null);
|
||||
};
|
||||
|
||||
const expandedRowRender = (): JSX.Element => (
|
||||
<div className="expanded-table-container">
|
||||
{isErrorGroupedByRowData && (
|
||||
<Typography>{groupedByRowData?.error || 'Something went wrong'}</Typography>
|
||||
)}
|
||||
{isFetchingGroupedByRowData || isLoadingGroupedByRowData ? (
|
||||
<LoadingContainer />
|
||||
) : (
|
||||
<div className="expanded-table">
|
||||
<Table
|
||||
columns={nestedColumns as ColumnType<K8sDaemonSetsRowData>[]}
|
||||
dataSource={formattedGroupedByDaemonSetsData}
|
||||
pagination={false}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
size="small"
|
||||
loading={{
|
||||
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openDaemonSetInNewTab(record);
|
||||
return;
|
||||
}
|
||||
setSelectedDaemonSetUID(record.daemonsetUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
{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: K8sDaemonSetsRowData,
|
||||
e: React.MouseEvent<HTMLButtonElement>,
|
||||
) => void;
|
||||
record: K8sDaemonSetsRowData;
|
||||
}): 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 handleCloseDaemonSetDetail = (): void => {
|
||||
setSelectedDaemonSetUID(null);
|
||||
setView(null);
|
||||
setTracesFilters(null);
|
||||
setEventsFilters(null);
|
||||
setLogFilters(null);
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentPage(1);
|
||||
setGroupBy(groupBy);
|
||||
setExpandedRowKeys([]);
|
||||
|
||||
logEvent(InfraMonitoringEvents.GroupByChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
});
|
||||
return {
|
||||
data: response.payload?.data.records || [],
|
||||
total: response.payload?.data.total || 0,
|
||||
error: response.error,
|
||||
};
|
||||
},
|
||||
[groupByFiltersData, setCurrentPage, setGroupBy],
|
||||
[dotMetricsEnabled],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (groupByFiltersData?.payload) {
|
||||
setGroupByOptions(
|
||||
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
|
||||
value: filter.key,
|
||||
label: filter.key,
|
||||
})) || [],
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ data: K8sDaemonSetsData | null; error?: string | null }> => {
|
||||
const response = await getK8sDaemonSetsList(
|
||||
{
|
||||
filters: filters.filters,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
}
|
||||
}, [groupByFiltersData]);
|
||||
|
||||
const onPaginationChange = (page: number, pageSize: number): void => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(pageSize);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.DaemonSet,
|
||||
});
|
||||
};
|
||||
const records = response.payload?.data.records || [];
|
||||
|
||||
const showTableLoadingState =
|
||||
(isFetching || isLoading) && formattedDaemonSetsData.length === 0;
|
||||
return {
|
||||
data: records.length > 0 ? records[0] : null,
|
||||
error: response.error,
|
||||
};
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="k8s-list">
|
||||
<K8sHeader
|
||||
isFiltersVisible={isFiltersVisible}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
defaultAddedColumns={defaultAddedColumns}
|
||||
handleFiltersChange={handleFiltersChange}
|
||||
groupByOptions={groupByOptions}
|
||||
isLoadingGroupByFilters={isLoadingGroupByFilters}
|
||||
handleGroupByChange={handleGroupByChange}
|
||||
selectedGroupBy={groupBy}
|
||||
entity={K8sCategory.DAEMONSETS}
|
||||
showAutoRefresh={!selectedDaemonSetData}
|
||||
/>
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
<Table
|
||||
className={classNames('k8s-list-table', 'daemonSets-list-table', {
|
||||
'expanded-daemonsets-list-table': isGroupedByAttribute,
|
||||
})}
|
||||
dataSource={showTableLoadingState ? [] : formattedDaemonSetsData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
total: totalCount,
|
||||
showSizeChanger: true,
|
||||
hideOnSinglePage: false,
|
||||
onChange: onPaginationChange,
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
loading={{
|
||||
spinning: showTableLoadingState,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
locale={{
|
||||
emptyText: showTableLoadingState ? null : (
|
||||
<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>
|
||||
),
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
expandIcon: expandRowIconRenderer,
|
||||
expandedRowKeys,
|
||||
}}
|
||||
<>
|
||||
<K8sBaseList<K8sDaemonSetsData>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
tableColumnsDefinitions={k8sDaemonSetsColumns}
|
||||
tableColumns={k8sDaemonSetsColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={k8sDaemonSetsRenderRowData}
|
||||
eventCategory={InfraMonitoringEvents.DaemonSet}
|
||||
/>
|
||||
|
||||
<DaemonSetDetails
|
||||
daemonSet={selectedDaemonSetData}
|
||||
isModalTimeSelection
|
||||
onClose={handleCloseDaemonSetDetail}
|
||||
<K8sBaseDetails<K8sDaemonSetsData>
|
||||
category={InfraMonitoringEntity.DAEMONSETS}
|
||||
eventCategory={InfraMonitoringEvents.DaemonSet}
|
||||
getSelectedItemFilters={k8sDaemonSetGetSelectedItemFilters}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sDaemonSetGetEntityName}
|
||||
getInitialLogTracesFilters={k8sDaemonSetInitialLogTracesFilter}
|
||||
getInitialEventsFilters={k8sDaemonSetInitialEventsFilter}
|
||||
primaryFilterKeys={k8sDaemonSetInitialFilters}
|
||||
metadataConfig={k8sDaemonSetDetailsMetadataConfig}
|
||||
entityWidgetInfo={daemonSetWidgetInfo}
|
||||
getEntityQueryPayload={getDaemonSetMetricsQueryPayload}
|
||||
queryKeyPrefix="daemonset"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { UnderscoreToDotMap } from 'api/utils';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { UnderscoreToDotMap } from '../utils';
|
||||
|
||||
export interface K8sDaemonSetsListPayload {
|
||||
filters: TagFilter;
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: {
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
};
|
||||
}
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
|
||||
export interface K8sDaemonSetsData {
|
||||
daemonSetName: string;
|
||||
@@ -48,6 +36,7 @@ export interface K8sDaemonSetsListResponse {
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(H4ad): Erase this whole file when migrating to openapi
|
||||
export const daemonSetsMetaMap = [
|
||||
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
|
||||
{ dot: 'k8s.daemonset.name', under: 'k8s_daemonset_name' },
|
||||
@@ -68,45 +57,43 @@ export function mapDaemonSetsMeta(
|
||||
}
|
||||
|
||||
export const getK8sDaemonSetsList = async (
|
||||
props: K8sDaemonSetsListPayload,
|
||||
props: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
): Promise<SuccessResponse<K8sDaemonSetsListResponse> | ErrorResponse> => {
|
||||
try {
|
||||
// filter prep (unchanged)…
|
||||
const requestProps =
|
||||
dotMetricsEnabled && Array.isArray(props.filters?.items)
|
||||
? {
|
||||
...props,
|
||||
filters: {
|
||||
...props.filters,
|
||||
items: props.filters.items.reduce<typeof props.filters.items>(
|
||||
(acc, item) => {
|
||||
if (item.value === undefined) {
|
||||
return acc;
|
||||
}
|
||||
if (
|
||||
item.key &&
|
||||
typeof item.key === 'object' &&
|
||||
'key' in item.key &&
|
||||
typeof item.key.key === 'string'
|
||||
) {
|
||||
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
|
||||
acc.push({
|
||||
...item,
|
||||
key: { ...item.key, key: mappedKey },
|
||||
});
|
||||
} else {
|
||||
acc.push(item);
|
||||
}
|
||||
const requestProps = dotMetricsEnabled
|
||||
? {
|
||||
...props,
|
||||
filters: {
|
||||
...props.filters,
|
||||
items: props.filters.items.reduce<typeof props.filters.items>(
|
||||
(acc, item) => {
|
||||
if (item.value === undefined) {
|
||||
return acc;
|
||||
},
|
||||
[] as typeof props.filters.items,
|
||||
),
|
||||
},
|
||||
}
|
||||
: props;
|
||||
}
|
||||
if (
|
||||
item.key &&
|
||||
typeof item.key === 'object' &&
|
||||
'key' in item.key &&
|
||||
typeof item.key.key === 'string'
|
||||
) {
|
||||
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
|
||||
acc.push({
|
||||
...item,
|
||||
key: { ...item.key, key: mappedKey },
|
||||
});
|
||||
} else {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as typeof props.filters.items,
|
||||
),
|
||||
},
|
||||
}
|
||||
: props;
|
||||
|
||||
const response = await axios.post('/daemonsets/list', requestProps, {
|
||||
signal,
|
||||
@@ -114,7 +101,6 @@ export const getK8sDaemonSetsList = async (
|
||||
});
|
||||
const payload: K8sDaemonSetsListResponse = response.data;
|
||||
|
||||
// single-line meta mapping
|
||||
payload.data.records = payload.data.records.map((record) => ({
|
||||
...record,
|
||||
meta: mapDaemonSetsMeta(record.meta as Record<string, unknown>),
|
||||
@@ -1,11 +1,72 @@
|
||||
import { K8sDaemonSetsData } from 'api/infraMonitoring/getK8sDaemonSetsList';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
createFilterItem,
|
||||
K8sDetailsMetadataConfig,
|
||||
} from '../Base/K8sBaseDetails';
|
||||
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
|
||||
import { K8sDaemonSetsData } from './api';
|
||||
|
||||
export const k8sDaemonSetGetSelectedItemFilters = (
|
||||
selectedItemId: string,
|
||||
): TagFilter => ({
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: 'k8s_daemonset_name',
|
||||
key: {
|
||||
key: 'k8s_daemonset_name',
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: selectedItemId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const k8sDaemonSetDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sDaemonSetsData>[] = [
|
||||
{
|
||||
label: 'Daemonset Name',
|
||||
getValue: (p): string => p.meta.k8s_daemonset_name,
|
||||
},
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
getValue: (p): string => p.meta.k8s_cluster_name,
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
getValue: (p): string => p.meta.k8s_namespace_name,
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sDaemonSetInitialFilters = [
|
||||
QUERY_KEYS.K8S_DAEMON_SET_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
];
|
||||
|
||||
export const k8sDaemonSetInitialEventsFilter = (
|
||||
item: K8sDaemonSetsData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'DaemonSet'),
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_daemonset_name),
|
||||
];
|
||||
|
||||
export const k8sDaemonSetInitialLogTracesFilter = (
|
||||
item: K8sDaemonSetsData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_DAEMON_SET_NAME, item.meta.k8s_daemonset_name),
|
||||
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.meta.k8s_namespace_name),
|
||||
];
|
||||
|
||||
export const k8sDaemonSetGetEntityName = (item: K8sDaemonSetsData): string =>
|
||||
item.meta.k8s_daemonset_name;
|
||||
|
||||
export const daemonSetWidgetInfo = [
|
||||
{
|
||||
title: 'CPU usage, request, limits',
|
||||
@@ -0,0 +1,297 @@
|
||||
import { TableColumnType as ColumnType, Tooltip } from 'antd';
|
||||
import { Group } from 'lucide-react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { K8sRenderedRowData } from '../Base/types';
|
||||
import { IEntityColumn } from '../Base/useInfraMonitoringTableColumnsStore';
|
||||
import { getGroupByEl, getGroupedByMeta, getRowKey } from '../Base/utils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
formatBytes,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../commonUtils';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { K8sDaemonSetsData } from './api';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
|
||||
export const k8sDaemonSetsColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'DaemonSet Group',
|
||||
value: 'daemonSetGroup',
|
||||
id: 'daemonSetGroup',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-collapse',
|
||||
},
|
||||
{
|
||||
label: 'DaemonSet Name',
|
||||
value: 'daemonsetName',
|
||||
id: 'daemonsetName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-expand',
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
value: 'namespaceName',
|
||||
id: 'namespaceName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Available',
|
||||
value: 'available_nodes',
|
||||
id: 'available_nodes',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Desired',
|
||||
value: 'desired_nodes',
|
||||
id: 'desired_nodes',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Req Usage (%)',
|
||||
value: 'cpu_request',
|
||||
id: 'cpu_request',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Limit Usage (%)',
|
||||
value: 'cpu_limit',
|
||||
id: 'cpu_limit',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Req Usage (%)',
|
||||
value: 'memory_request',
|
||||
id: 'memory_request',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Limit Usage (%)',
|
||||
value: 'memory_limit',
|
||||
id: 'memory_limit',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Usage (WSS)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sDaemonSetsColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
|
||||
{
|
||||
title: (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
<Group size={14} /> DAEMONSET GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'daemonSetGroup',
|
||||
key: 'daemonSetGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
},
|
||||
{
|
||||
title: <div>DaemonSet Name</div>,
|
||||
dataIndex: 'daemonsetName',
|
||||
key: 'daemonsetName',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Namespace Name</div>,
|
||||
dataIndex: 'namespaceName',
|
||||
key: 'namespaceName',
|
||||
ellipsis: true,
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Available</div>,
|
||||
dataIndex: 'available_nodes',
|
||||
key: 'available_nodes',
|
||||
width: 50,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Desired</div>,
|
||||
dataIndex: 'desired_nodes',
|
||||
key: 'desired_nodes',
|
||||
width: 50,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Req Usage (%)</div>,
|
||||
dataIndex: 'cpu_request',
|
||||
key: 'cpu_request',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Limit Usage (%)</div>,
|
||||
dataIndex: 'cpu_limit',
|
||||
key: 'cpu_limit',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Req Usage (%)</div>,
|
||||
dataIndex: 'memory_request',
|
||||
key: 'memory_request',
|
||||
width: 170,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Limit Usage (%)</div>,
|
||||
dataIndex: 'memory_limit',
|
||||
key: 'memory_limit',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sDaemonSetsRenderRowData = (
|
||||
entity: K8sDaemonSetsData,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): K8sRenderedRowData => ({
|
||||
key: getRowKey(
|
||||
entity,
|
||||
() => entity.daemonSetName || entity.meta.k8s_daemonset_name || '',
|
||||
groupBy,
|
||||
),
|
||||
itemKey: entity.meta.k8s_daemonset_name,
|
||||
daemonsetName: (
|
||||
<Tooltip title={entity.meta.k8s_daemonset_name}>
|
||||
{entity.meta.k8s_daemonset_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
namespaceName: (
|
||||
<Tooltip title={entity.meta.k8s_namespace_name}>
|
||||
{entity.meta.k8s_namespace_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
cpu_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={entity.cpuRequest}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={entity.cpuRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={entity.cpuLimit}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={entity.cpuLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={entity.cpuUsage}>
|
||||
{entity.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={entity.memoryRequest}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={entity.memoryRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={entity.memoryLimit}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={entity.memoryLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={entity.memoryUsage}>
|
||||
{formatBytes(entity.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
available_nodes: (
|
||||
<ValidateColumnValueWrapper value={entity.availableNodes}>
|
||||
{entity.availableNodes}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
desired_nodes: (
|
||||
<ValidateColumnValueWrapper value={entity.desiredNodes}>
|
||||
{entity.desiredNodes}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
daemonSetGroup: getGroupByEl(entity, groupBy),
|
||||
...entity.meta,
|
||||
groupedByMeta: getGroupedByMeta(entity, groupBy),
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
.entityGroupHeader {
|
||||
padding-left: var(--spacing-5);
|
||||
gap: var(--spacing-5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
}
|
||||
@@ -1,356 +0,0 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
|
||||
import {
|
||||
K8sDaemonSetsData,
|
||||
K8sDaemonSetsListPayload,
|
||||
} from 'api/infraMonitoring/getK8sDaemonSetsList';
|
||||
import { Group } from 'lucide-react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
EntityProgressBar,
|
||||
formatBytes,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../commonUtils';
|
||||
import { K8sCategory } from '../constants';
|
||||
import { IEntityColumn } from '../utils';
|
||||
|
||||
export const defaultAddedColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'DaemonSet Name',
|
||||
value: 'daemonsetName',
|
||||
id: 'daemonsetName',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
value: 'namespaceName',
|
||||
id: 'namespaceName',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Available',
|
||||
value: 'available_nodes',
|
||||
id: 'available_nodes',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Desired',
|
||||
value: 'desired_nodes',
|
||||
id: 'desired_nodes',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Req Usage (%)',
|
||||
value: 'cpu_request',
|
||||
id: 'cpu_request',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Limit Usage (%)',
|
||||
value: 'cpu_limit',
|
||||
id: 'cpu_limit',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Mem Req Usage (%)',
|
||||
value: 'memory_request',
|
||||
id: 'memory_request',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Mem Limit Usage (%)',
|
||||
value: 'memory_limit',
|
||||
id: 'memory_limit',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Mem Usage (WSS)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canRemove: false,
|
||||
},
|
||||
];
|
||||
|
||||
export interface K8sDaemonSetsRowData {
|
||||
key: string;
|
||||
daemonsetUID: string;
|
||||
daemonsetName: React.ReactNode;
|
||||
cpu_request: React.ReactNode;
|
||||
cpu_limit: React.ReactNode;
|
||||
cpu: React.ReactNode;
|
||||
memory_request: React.ReactNode;
|
||||
memory_limit: React.ReactNode;
|
||||
memory: React.ReactNode;
|
||||
desired_nodes: React.ReactNode;
|
||||
available_nodes: React.ReactNode;
|
||||
namespaceName: React.ReactNode;
|
||||
groupedByMeta?: any;
|
||||
}
|
||||
|
||||
const daemonSetGroupColumnConfig = {
|
||||
title: (
|
||||
<div className="column-header entity-group-header">
|
||||
<Group size={14} /> DAEMONSET GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'daemonSetGroup',
|
||||
key: 'daemonSetGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
className: 'column entity-group-header',
|
||||
};
|
||||
|
||||
export const getK8sDaemonSetsListQuery = (): K8sDaemonSetsListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
const columnProgressBarClassName = 'column-progress-bar';
|
||||
|
||||
const columnsConfig = [
|
||||
{
|
||||
title: (
|
||||
<div className="column-header-left daemonset-name-header">
|
||||
DaemonSet Name
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'daemonsetName',
|
||||
key: 'daemonsetName',
|
||||
ellipsis: true,
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<div className="column-header-left namespace-name-header">
|
||||
Namespace Name
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'namespaceName',
|
||||
key: 'namespaceName',
|
||||
ellipsis: true,
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header small-col">Available</div>,
|
||||
dataIndex: 'available_nodes',
|
||||
key: 'available_nodes',
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header small-col">Desired</div>,
|
||||
dataIndex: 'desired_nodes',
|
||||
key: 'desired_nodes',
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header med-col">CPU Req Usage (%)</div>,
|
||||
dataIndex: 'cpu_request',
|
||||
key: 'cpu_request',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header med-col">CPU Limit Usage (%)</div>,
|
||||
dataIndex: 'cpu_limit',
|
||||
key: 'cpu_limit',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header small-col">CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header med-col">Mem Req Usage (%)</div>,
|
||||
dataIndex: 'memory_request',
|
||||
key: 'memory_request',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header med-col">Mem Limit Usage (%)</div>,
|
||||
dataIndex: 'memory_limit',
|
||||
key: 'memory_limit',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header med-col">Mem Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
];
|
||||
|
||||
export const getK8sDaemonSetsListColumns = (
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): ColumnType<K8sDaemonSetsRowData>[] => {
|
||||
if (groupBy.length > 0) {
|
||||
const filteredColumns = [...columnsConfig].filter(
|
||||
(column) => column.key !== 'daemonsetName',
|
||||
);
|
||||
filteredColumns.unshift(daemonSetGroupColumnConfig);
|
||||
return filteredColumns as ColumnType<K8sDaemonSetsRowData>[];
|
||||
}
|
||||
|
||||
return columnsConfig as ColumnType<K8sDaemonSetsRowData>[];
|
||||
};
|
||||
|
||||
const dotToUnder: Record<string, keyof K8sDaemonSetsData['meta']> = {
|
||||
'k8s.daemonset.name': 'k8s_daemonset_name',
|
||||
'k8s.namespace.name': 'k8s_namespace_name',
|
||||
'k8s.cluster.name': 'k8s_cluster_name',
|
||||
};
|
||||
|
||||
const getGroupByEle = (
|
||||
daemonSet: K8sDaemonSetsData,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): React.ReactNode => {
|
||||
const groupByValues: string[] = [];
|
||||
|
||||
groupBy.forEach((group) => {
|
||||
const rawKey = group.key as string;
|
||||
|
||||
// Choose mapped key if present, otherwise use rawKey
|
||||
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof daemonSet.meta;
|
||||
const value = daemonSet.meta[metaKey];
|
||||
|
||||
groupByValues.push(value);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="pod-group">
|
||||
{groupByValues.map((value) => (
|
||||
<Tag key={value} color={Color.BG_SLATE_400} className="pod-group-tag-item">
|
||||
{value === '' ? '<no-value>' : value}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const formatDataForTable = (
|
||||
data: K8sDaemonSetsData[],
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): K8sDaemonSetsRowData[] =>
|
||||
data.map((daemonSet, index) => ({
|
||||
key: index.toString(),
|
||||
daemonsetUID: daemonSet.daemonSetName,
|
||||
daemonsetName: (
|
||||
<Tooltip title={daemonSet.meta.k8s_daemonset_name}>
|
||||
{daemonSet.meta.k8s_daemonset_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
namespaceName: (
|
||||
<Tooltip title={daemonSet.meta.k8s_namespace_name}>
|
||||
{daemonSet.meta.k8s_namespace_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
cpu_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={daemonSet.cpuRequest}
|
||||
entity={K8sCategory.DAEMONSETS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={daemonSet.cpuRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={daemonSet.cpuLimit}
|
||||
entity={K8sCategory.DAEMONSETS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={daemonSet.cpuLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={daemonSet.cpuUsage}>
|
||||
{daemonSet.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={daemonSet.memoryRequest}
|
||||
entity={K8sCategory.DAEMONSETS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={daemonSet.memoryRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={daemonSet.memoryLimit}
|
||||
entity={K8sCategory.DAEMONSETS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={daemonSet.memoryLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={daemonSet.memoryUsage}>
|
||||
{formatBytes(daemonSet.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
available_nodes: (
|
||||
<ValidateColumnValueWrapper value={daemonSet.availableNodes}>
|
||||
{daemonSet.availableNodes}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
desired_nodes: (
|
||||
<ValidateColumnValueWrapper value={daemonSet.desiredNodes}>
|
||||
{daemonSet.desiredNodes}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
daemonSetGroup: getGroupByEle(daemonSet, groupBy),
|
||||
meta: daemonSet.meta,
|
||||
...daemonSet.meta,
|
||||
groupedByMeta: daemonSet.meta,
|
||||
}));
|
||||
@@ -1,7 +0,0 @@
|
||||
import { K8sDeploymentsData } from 'api/infraMonitoring/getK8sDeploymentsList';
|
||||
|
||||
export type DeploymentDetailsProps = {
|
||||
deployment: K8sDeploymentsData | null;
|
||||
isModalTimeSelection: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -1,671 +0,0 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
|
||||
import type { RadioChangeEvent } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { K8sDeploymentsData } from 'api/infraMonitoring/getK8sDeploymentsList';
|
||||
import { VIEW_TYPES, VIEWS } from 'components/HostMetricsDetail/constants';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryState,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils';
|
||||
import {
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from 'container/InfraMonitoringK8s/hooks';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
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 { 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 DeploymentEvents from '../../EntityDetailsUtils/EntityEvents';
|
||||
import DeploymentLogs from '../../EntityDetailsUtils/EntityLogs';
|
||||
import DeploymentMetrics from '../../EntityDetailsUtils/EntityMetrics';
|
||||
import DeploymentTraces from '../../EntityDetailsUtils/EntityTraces';
|
||||
import {
|
||||
deploymentWidgetInfo,
|
||||
getDeploymentMetricsQueryPayload,
|
||||
} from './constants';
|
||||
import { DeploymentDetailsProps } from './DeploymentDetails.interfaces';
|
||||
|
||||
import '../../EntityDetailsUtils/entityDetails.styles.scss';
|
||||
|
||||
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 lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
);
|
||||
|
||||
const [selectedView, setSelectedView] = useInfraMonitoringView();
|
||||
const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters();
|
||||
const [
|
||||
tracesFiltersParam,
|
||||
setTracesFiltersParam,
|
||||
] = useInfraMonitoringTracesFilters();
|
||||
const [
|
||||
eventsFiltersParam,
|
||||
setEventsFiltersParam,
|
||||
] = useInfraMonitoringEventsFilters();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const initialFilters = useMemo(() => {
|
||||
const filters =
|
||||
selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam;
|
||||
if (filters) {
|
||||
return filters;
|
||||
}
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_DEPLOYMENT_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s_deployment_name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: deployment?.meta.k8s_deployment_name || '',
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s_deployment_name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: deployment?.meta.k8s_namespace_name || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [
|
||||
deployment?.meta.k8s_deployment_name,
|
||||
deployment?.meta.k8s_namespace_name,
|
||||
selectedView,
|
||||
logFiltersParam,
|
||||
tracesFiltersParam,
|
||||
]);
|
||||
|
||||
const initialEventsFilters = useMemo(() => {
|
||||
if (eventsFiltersParam) {
|
||||
return eventsFiltersParam;
|
||||
}
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s.object.kind--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: 'Deployment',
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s.object.name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: deployment?.meta.k8s_deployment_name || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [deployment?.meta.k8s_deployment_name, eventsFiltersParam]);
|
||||
|
||||
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
|
||||
IBuilderQuery['filters']
|
||||
>(initialFilters);
|
||||
|
||||
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
|
||||
initialEventsFilters,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (deployment) {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
});
|
||||
}
|
||||
}, [deployment]);
|
||||
|
||||
useEffect(() => {
|
||||
setLogAndTracesFilters(initialFilters);
|
||||
setEventsFilters(initialEventsFilters);
|
||||
}, [initialFilters, initialEventsFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
endTime: Math.floor(maxTime / 1000000000),
|
||||
});
|
||||
}
|
||||
}, [selectedTime, minTime, maxTime]);
|
||||
|
||||
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||
setSelectedView(e.target.value);
|
||||
setLogFiltersParam(null);
|
||||
setTracesFiltersParam(null);
|
||||
setEventsFiltersParam(null);
|
||||
logEvent(InfraMonitoringEvents.TabChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
view: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
lastSelectedInterval.current = interval as Time;
|
||||
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(InfraMonitoringEvents.TimeUpdated, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
interval,
|
||||
view: selectedView,
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeLogFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setLogAndTracesFilters((prevFilters) => {
|
||||
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||
[QUERY_KEYS.K8S_DEPLOYMENT_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
|
||||
item.key?.key ?? '',
|
||||
),
|
||||
);
|
||||
const paginationFilter = value?.items?.find(
|
||||
(item) => item.key?.key === 'id',
|
||||
);
|
||||
const newFilters = value?.items?.filter(
|
||||
(item) =>
|
||||
item.key?.key !== 'id' &&
|
||||
item.key?.key !== QUERY_KEYS.K8S_DEPLOYMENT_NAME,
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
view: InfraMonitoringEvents.LogsView,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...(primaryFilters || []),
|
||||
...(newFilters || []),
|
||||
...(paginationFilter ? [paginationFilter] : []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
setLogFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeTracesFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setLogAndTracesFilters((prevFilters) => {
|
||||
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||
[QUERY_KEYS.K8S_DEPLOYMENT_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
|
||||
item.key?.key ?? '',
|
||||
),
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
view: InfraMonitoringEvents.TracesView,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...(primaryFilters || []),
|
||||
...(value?.items?.filter(
|
||||
(item) => item.key?.key !== QUERY_KEYS.K8S_DEPLOYMENT_NAME,
|
||||
) || []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
setTracesFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeEventsFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
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,
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
view: InfraMonitoringEvents.EventsView,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
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),
|
||||
),
|
||||
};
|
||||
|
||||
setEventsFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// 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(InfraMonitoringEvents.ExploreClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
view: selectedView,
|
||||
});
|
||||
|
||||
if (selectedView === VIEW_TYPES.LOGS) {
|
||||
const filtersWithoutPagination = {
|
||||
...logAndTracesFilters,
|
||||
items: logAndTracesFilters?.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: logAndTracesFilters,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
lastSelectedInterval.current = null;
|
||||
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">
|
||||
{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="entity-detail-drawer"
|
||||
destroyOnClose
|
||||
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||
>
|
||||
{deployment && (
|
||||
<>
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Deployment Name
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Cluster Name
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Namespace Name
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="values-row">
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={deployment.meta.k8s_deployment_name}>
|
||||
{deployment.meta.k8s_deployment_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={deployment.meta.k8s_cluster_name}>
|
||||
{deployment.meta.k8s_cluster_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={deployment.meta.k8s_namespace_name}>
|
||||
{deployment.meta.k8s_namespace_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="views-tabs-container">
|
||||
<Radio.Group
|
||||
className="views-tabs"
|
||||
onChange={handleTabChange}
|
||||
value={selectedView}
|
||||
>
|
||||
<Radio.Button
|
||||
className={
|
||||
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<K8sDeploymentsData>
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
entity={deployment}
|
||||
entityWidgetInfo={deploymentWidgetInfo}
|
||||
getEntityQueryPayload={getDeploymentMetricsQueryPayload}
|
||||
category={K8sCategory.DEPLOYMENTS}
|
||||
queryKey="deploymentMetrics"
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<DeploymentLogs
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeLogFilters={handleChangeLogFilters}
|
||||
logFilters={logAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKeyFilters={[
|
||||
QUERY_KEYS.K8S_DEPLOYMENT_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
]}
|
||||
queryKey="deploymentLogs"
|
||||
category={K8sCategory.DEPLOYMENTS}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.TRACES && (
|
||||
<DeploymentTraces
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeTracesFilters={handleChangeTracesFilters}
|
||||
tracesFilters={logAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey="deploymentTraces"
|
||||
category={InfraMonitoringEvents.Deployment}
|
||||
queryKeyFilters={[
|
||||
QUERY_KEYS.K8S_DEPLOYMENT_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.EVENTS && (
|
||||
<DeploymentEvents
|
||||
timeRange={modalTimeRange}
|
||||
handleChangeEventFilters={handleChangeEventsFilters}
|
||||
filters={eventsFilters}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
category={K8sCategory.DEPLOYMENTS}
|
||||
queryKey="deploymentEvents"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeploymentDetails;
|
||||
@@ -1,3 +0,0 @@
|
||||
import DeploymentDetails from './DeploymentDetails';
|
||||
|
||||
export default DeploymentDetails;
|
||||
@@ -1,57 +0,0 @@
|
||||
.infra-monitoring-container {
|
||||
.deployments-list-table {
|
||||
.expanded-table-container {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.ant-table-row-expand-icon-cell {
|
||||
min-width: 30px !important;
|
||||
max-width: 30px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.deployment-name-header) {
|
||||
min-width: 250px !important;
|
||||
max-width: 250px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.namespace-name-header) {
|
||||
min-width: 220px !important;
|
||||
max-width: 220px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.med-col) {
|
||||
min-width: 200px !important;
|
||||
max-width: 200px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.small-col) {
|
||||
min-width: 120px !important;
|
||||
max-width: 120px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: flex !important;
|
||||
justify-content: start !important;
|
||||
}
|
||||
|
||||
.expanded-deployments-list-table {
|
||||
.ant-table-cell {
|
||||
min-width: 180px !important;
|
||||
max-width: 180px !important;
|
||||
}
|
||||
|
||||
.ant-table-row-expand-icon-cell {
|
||||
min-width: 30px !important;
|
||||
max-width: 30px !important;
|
||||
}
|
||||
}
|
||||
@@ -1,722 +1,117 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Spin,
|
||||
Table,
|
||||
TableColumnType as ColumnType,
|
||||
TablePaginationConfig,
|
||||
TableProps,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import type { SorterResult } from 'antd/es/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { K8sDeploymentsListPayload } from 'api/infraMonitoring/getK8sDeploymentsList';
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback } from 'react';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
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 { useAppContext } from 'providers/App/App';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { getK8sDeploymentsList, K8sDeploymentsData } from './api';
|
||||
import {
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
K8sCategory,
|
||||
} from '../constants';
|
||||
deploymentWidgetInfo,
|
||||
getDeploymentMetricsQueryPayload,
|
||||
k8sDeploymentDetailsMetadataConfig,
|
||||
k8sDeploymentGetEntityName,
|
||||
k8sDeploymentGetSelectedItemFilters,
|
||||
k8sDeploymentInitialEventsFilter,
|
||||
k8sDeploymentInitialFilters,
|
||||
k8sDeploymentInitialLogTracesFilter,
|
||||
} from './constants';
|
||||
import {
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringDeploymentUID,
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from '../hooks';
|
||||
import K8sHeader from '../K8sHeader';
|
||||
import LoadingContainer from '../LoadingContainer';
|
||||
import { usePageSize } from '../utils';
|
||||
import DeploymentDetails from './DeploymentDetails';
|
||||
import {
|
||||
defaultAddedColumns,
|
||||
formatDataForTable,
|
||||
getK8sDeploymentsListColumns,
|
||||
getK8sDeploymentsListQuery,
|
||||
K8sDeploymentsRowData,
|
||||
} from './utils';
|
||||
|
||||
import '../InfraMonitoringK8s.styles.scss';
|
||||
import './K8sDeploymentsList.styles.scss';
|
||||
k8sDeploymentsColumns,
|
||||
k8sDeploymentsColumnsConfig,
|
||||
k8sDeploymentsRenderRowData,
|
||||
} from './table.config';
|
||||
|
||||
function K8sDeploymentsList({
|
||||
isFiltersVisible,
|
||||
handleFilterVisibilityChange,
|
||||
quickFiltersLastUpdated,
|
||||
controlListPrefix,
|
||||
}: {
|
||||
isFiltersVisible: boolean;
|
||||
handleFilterVisibilityChange: () => void;
|
||||
quickFiltersLastUpdated: number;
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [filtersInitialised, setFiltersInitialised] = useState(false);
|
||||
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
||||
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
|
||||
const [
|
||||
selectedDeploymentUID,
|
||||
setselectedDeploymentUID,
|
||||
] = useInfraMonitoringDeploymentUID();
|
||||
|
||||
const { pageSize, setPageSize } = usePageSize(K8sCategory.DEPLOYMENTS);
|
||||
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
|
||||
const [, setView] = useInfraMonitoringView();
|
||||
const [, setTracesFilters] = useInfraMonitoringTracesFilters();
|
||||
const [, setEventsFilters] = useInfraMonitoringEventsFilters();
|
||||
const [, setLogFilters] = useInfraMonitoringLogFilters();
|
||||
|
||||
const [
|
||||
selectedRowData,
|
||||
setSelectedRowData,
|
||||
] = useState<K8sDeploymentsRowData | null>(null);
|
||||
|
||||
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],
|
||||
);
|
||||
|
||||
// Reset pagination every time quick filters are changed
|
||||
useEffect(() => {
|
||||
if (quickFiltersLastUpdated !== -1) {
|
||||
setCurrentPage(1);
|
||||
}
|
||||
}, [quickFiltersLastUpdated, setCurrentPage]);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const createFiltersForSelectedRowData = (
|
||||
selectedRowData: K8sDeploymentsRowData,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): IBuilderQuery['filters'] => {
|
||||
const baseFilters: IBuilderQuery['filters'] = {
|
||||
items: [...queryFilters.items],
|
||||
op: 'and',
|
||||
};
|
||||
const fetchListData = useCallback(
|
||||
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
|
||||
filters.orderBy ||= {
|
||||
columnName: 'cpu',
|
||||
order: 'desc',
|
||||
};
|
||||
|
||||
if (!selectedRowData) {
|
||||
return baseFilters;
|
||||
}
|
||||
|
||||
const { groupedByMeta } = selectedRowData;
|
||||
|
||||
for (const key of groupBy) {
|
||||
baseFilters.items.push({
|
||||
key: {
|
||||
key: key.key,
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: groupedByMeta[key.key],
|
||||
id: key.key,
|
||||
});
|
||||
}
|
||||
|
||||
return baseFilters;
|
||||
};
|
||||
|
||||
const fetchGroupedByRowDataQuery = useMemo(() => {
|
||||
if (!selectedRowData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseQuery = getK8sDeploymentsListQuery();
|
||||
|
||||
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
|
||||
|
||||
return {
|
||||
...baseQuery,
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
filters,
|
||||
start: Math.floor(minTime / 1000000),
|
||||
end: Math.floor(maxTime / 1000000),
|
||||
orderBy: orderBy || baseQuery.orderBy,
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
|
||||
|
||||
const groupedByRowDataQueryKey = useMemo(() => {
|
||||
// be careful with what you serialize from selectedRowData
|
||||
// since it's react node, it could contain circular references
|
||||
const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta);
|
||||
if (selectedDeploymentUID) {
|
||||
return [
|
||||
'deploymentList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
selectedRowDataKey,
|
||||
];
|
||||
}
|
||||
return [
|
||||
'deploymentList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
selectedRowDataKey,
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
queryFilters,
|
||||
orderBy,
|
||||
selectedDeploymentUID,
|
||||
minTime,
|
||||
maxTime,
|
||||
selectedRowData,
|
||||
]);
|
||||
|
||||
const {
|
||||
data: groupedByRowData,
|
||||
isFetching: isFetchingGroupedByRowData,
|
||||
isLoading: isLoadingGroupedByRowData,
|
||||
isError: isErrorGroupedByRowData,
|
||||
refetch: fetchGroupedByRowData,
|
||||
} = useGetK8sDeploymentsList(
|
||||
fetchGroupedByRowDataQuery as K8sDeploymentsListPayload,
|
||||
{
|
||||
queryKey: groupedByRowDataQueryKey,
|
||||
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const {
|
||||
data: groupByFiltersData,
|
||||
isLoading: isLoadingGroupByFilters,
|
||||
} = useGetAggregateKeys(
|
||||
{
|
||||
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
aggregateAttribute: GetK8sEntityToAggregateAttribute(
|
||||
K8sCategory.DEPLOYMENTS,
|
||||
const response = await getK8sDeploymentsList(
|
||||
filters,
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
),
|
||||
aggregateOperator: 'noop',
|
||||
searchText: '',
|
||||
tagType: '',
|
||||
},
|
||||
{
|
||||
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
|
||||
},
|
||||
true,
|
||||
K8sCategory.NODES,
|
||||
);
|
||||
|
||||
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: orderBy || baseQuery.orderBy,
|
||||
};
|
||||
if (groupBy.length > 0) {
|
||||
queryPayload.groupBy = groupBy;
|
||||
}
|
||||
return queryPayload;
|
||||
}, [pageSize, currentPage, queryFilters, minTime, maxTime, orderBy, groupBy]);
|
||||
|
||||
const formattedGroupedByDeploymentsData = useMemo(
|
||||
() =>
|
||||
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
|
||||
[groupedByRowData, groupBy],
|
||||
);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedDeploymentUID) {
|
||||
return [
|
||||
'deploymentList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'deploymentList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
selectedDeploymentUID,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sDeploymentsList(
|
||||
query as K8sDeploymentsListPayload,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const deploymentsData = useMemo(() => data?.payload?.data?.records || [], [
|
||||
data,
|
||||
]);
|
||||
const totalCount = data?.payload?.data?.total || 0;
|
||||
|
||||
const formattedDeploymentsData = useMemo(
|
||||
() => formatDataForTable(deploymentsData, groupBy),
|
||||
[deploymentsData, groupBy],
|
||||
);
|
||||
|
||||
const nestedDeploymentsData = useMemo(() => {
|
||||
if (!selectedRowData || !groupedByRowData?.payload?.data.records) {
|
||||
return [];
|
||||
}
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
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);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
});
|
||||
}
|
||||
|
||||
if ('field' in sorter && sorter.order) {
|
||||
setOrderBy({
|
||||
columnName: sorter.field as string,
|
||||
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
|
||||
});
|
||||
} else {
|
||||
setOrderBy(null);
|
||||
}
|
||||
},
|
||||
[setCurrentPage, setOrderBy],
|
||||
);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const handleFiltersChange = useCallback(
|
||||
(value: IBuilderQuery['filters']): void => {
|
||||
handleChangeQueryData('filters', value);
|
||||
if (filtersInitialised) {
|
||||
setCurrentPage(1);
|
||||
} else {
|
||||
setFiltersInitialised(true);
|
||||
}
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
});
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
total: data?.payload?.data?.total,
|
||||
});
|
||||
}, [data?.payload?.data?.total]);
|
||||
|
||||
const selectedDeploymentData = useMemo(() => {
|
||||
if (!selectedDeploymentUID) {
|
||||
return null;
|
||||
}
|
||||
if (groupBy.length > 0) {
|
||||
// If grouped by, return the deployment from the formatted grouped by deployments data
|
||||
return (
|
||||
nestedDeploymentsData.find(
|
||||
(deployment) => deployment.deploymentName === selectedDeploymentUID,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
// If not grouped by, return the deployment from the deployments data
|
||||
return (
|
||||
deploymentsData.find(
|
||||
(deployment) => deployment.deploymentName === selectedDeploymentUID,
|
||||
) || null
|
||||
);
|
||||
}, [
|
||||
selectedDeploymentUID,
|
||||
groupBy.length,
|
||||
deploymentsData,
|
||||
nestedDeploymentsData,
|
||||
]);
|
||||
|
||||
const openDeploymentInNewTab = (record: K8sDeploymentsRowData): void => {
|
||||
const newParams = new URLSearchParams(document.location.search);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID,
|
||||
record.deploymentUID,
|
||||
);
|
||||
openInNewTab(
|
||||
buildAbsolutePath({
|
||||
relativePath: '',
|
||||
urlQueryString: newParams.toString(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sDeploymentsRowData,
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openDeploymentInNewTab(record);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedDeploymentUID(record.deploymentUID);
|
||||
} else {
|
||||
handleGroupByRowClick(record);
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
});
|
||||
};
|
||||
|
||||
const nestedColumns = useMemo(() => getK8sDeploymentsListColumns([]), []);
|
||||
|
||||
const isGroupedByAttribute = groupBy.length > 0;
|
||||
|
||||
const handleExpandedRowViewAllClick = (): void => {
|
||||
if (!selectedRowData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
|
||||
|
||||
handleFiltersChange(filters);
|
||||
|
||||
setCurrentPage(1);
|
||||
setSelectedRowData(null);
|
||||
setGroupBy([]);
|
||||
setOrderBy(null);
|
||||
};
|
||||
|
||||
const expandedRowRender = (): JSX.Element => (
|
||||
<div className="expanded-table-container">
|
||||
{isErrorGroupedByRowData && (
|
||||
<Typography>{groupedByRowData?.error || 'Something went wrong'}</Typography>
|
||||
)}
|
||||
{isFetchingGroupedByRowData || isLoadingGroupedByRowData ? (
|
||||
<LoadingContainer />
|
||||
) : (
|
||||
<div className="expanded-table">
|
||||
<Table
|
||||
columns={nestedColumns as ColumnType<K8sDeploymentsRowData>[]}
|
||||
dataSource={formattedGroupedByDeploymentsData}
|
||||
pagination={false}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
size="small"
|
||||
loading={{
|
||||
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openDeploymentInNewTab(record);
|
||||
return;
|
||||
}
|
||||
setselectedDeploymentUID(record.deploymentUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
{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);
|
||||
setView(null);
|
||||
setTracesFilters(null);
|
||||
setEventsFilters(null);
|
||||
setLogFilters(null);
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset pagination on switching to groupBy
|
||||
setCurrentPage(1);
|
||||
setGroupBy(groupBy);
|
||||
setExpandedRowKeys([]);
|
||||
|
||||
logEvent(InfraMonitoringEvents.GroupByChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
});
|
||||
return {
|
||||
data: response.payload?.data.records || [],
|
||||
total: response.payload?.data.total || 0,
|
||||
error: response.error,
|
||||
};
|
||||
},
|
||||
[groupByFiltersData, setCurrentPage, setGroupBy],
|
||||
[dotMetricsEnabled],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (groupByFiltersData?.payload) {
|
||||
setGroupByOptions(
|
||||
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
|
||||
value: filter.key,
|
||||
label: filter.key,
|
||||
})) || [],
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ data: K8sDeploymentsData | null; error?: string | null }> => {
|
||||
const response = await getK8sDeploymentsList(
|
||||
{
|
||||
filters: filters.filters,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
}
|
||||
}, [groupByFiltersData]);
|
||||
|
||||
const onPaginationChange = (page: number, pageSize: number): void => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(pageSize);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Deployment,
|
||||
});
|
||||
};
|
||||
const records = response.payload?.data.records || [];
|
||||
|
||||
const showTableLoadingState =
|
||||
(isFetching || isLoading) && formattedDeploymentsData.length === 0;
|
||||
return {
|
||||
data: records.length > 0 ? records[0] : null,
|
||||
error: response.error,
|
||||
};
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="k8s-list">
|
||||
<K8sHeader
|
||||
isFiltersVisible={isFiltersVisible}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
defaultAddedColumns={defaultAddedColumns}
|
||||
handleFiltersChange={handleFiltersChange}
|
||||
groupByOptions={groupByOptions}
|
||||
isLoadingGroupByFilters={isLoadingGroupByFilters}
|
||||
handleGroupByChange={handleGroupByChange}
|
||||
selectedGroupBy={groupBy}
|
||||
entity={K8sCategory.NODES}
|
||||
showAutoRefresh={!selectedDeploymentData}
|
||||
/>
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
<Table
|
||||
className={classNames('k8s-list-table', 'deployments-list-table', {
|
||||
'expanded-deployments-list-table': isGroupedByAttribute,
|
||||
})}
|
||||
dataSource={showTableLoadingState ? [] : formattedDeploymentsData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
total: totalCount,
|
||||
showSizeChanger: true,
|
||||
hideOnSinglePage: false,
|
||||
onChange: onPaginationChange,
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
loading={{
|
||||
spinning: showTableLoadingState,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
locale={{
|
||||
emptyText: showTableLoadingState ? null : (
|
||||
<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>
|
||||
),
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
expandIcon: expandRowIconRenderer,
|
||||
expandedRowKeys,
|
||||
}}
|
||||
<>
|
||||
<K8sBaseList<K8sDeploymentsData>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
tableColumnsDefinitions={k8sDeploymentsColumns}
|
||||
tableColumns={k8sDeploymentsColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={k8sDeploymentsRenderRowData}
|
||||
eventCategory={InfraMonitoringEvents.Deployment}
|
||||
/>
|
||||
|
||||
<DeploymentDetails
|
||||
deployment={selectedDeploymentData}
|
||||
isModalTimeSelection
|
||||
onClose={handleCloseDeploymentDetail}
|
||||
<K8sBaseDetails<K8sDeploymentsData>
|
||||
category={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
eventCategory={InfraMonitoringEvents.Deployment}
|
||||
getSelectedItemFilters={k8sDeploymentGetSelectedItemFilters}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sDeploymentGetEntityName}
|
||||
getInitialLogTracesFilters={k8sDeploymentInitialLogTracesFilter}
|
||||
getInitialEventsFilters={k8sDeploymentInitialEventsFilter}
|
||||
primaryFilterKeys={k8sDeploymentInitialFilters}
|
||||
metadataConfig={k8sDeploymentDetailsMetadataConfig}
|
||||
entityWidgetInfo={deploymentWidgetInfo}
|
||||
getEntityQueryPayload={getDeploymentMetricsQueryPayload}
|
||||
queryKeyPrefix="deployment"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { UnderscoreToDotMap } from 'api/utils';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { UnderscoreToDotMap } from '../utils';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
|
||||
export interface K8sDeploymentsListPayload {
|
||||
filters: TagFilter;
|
||||
@@ -48,6 +49,7 @@ export interface K8sDeploymentsListResponse {
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(H4ad): Erase this whole file when migrating to openapi
|
||||
export const deploymentsMetaMap = [
|
||||
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
|
||||
{ dot: 'k8s.deployment.name', under: 'k8s_deployment_name' },
|
||||
@@ -68,44 +70,43 @@ export function mapDeploymentsMeta(
|
||||
}
|
||||
|
||||
export const getK8sDeploymentsList = async (
|
||||
props: K8sDeploymentsListPayload,
|
||||
props: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
): Promise<SuccessResponse<K8sDeploymentsListResponse> | ErrorResponse> => {
|
||||
try {
|
||||
const requestProps =
|
||||
dotMetricsEnabled && Array.isArray(props.filters?.items)
|
||||
? {
|
||||
...props,
|
||||
filters: {
|
||||
...props.filters,
|
||||
items: props.filters.items.reduce<typeof props.filters.items>(
|
||||
(acc, item) => {
|
||||
if (item.value === undefined) {
|
||||
return acc;
|
||||
}
|
||||
if (
|
||||
item.key &&
|
||||
typeof item.key === 'object' &&
|
||||
'key' in item.key &&
|
||||
typeof item.key.key === 'string'
|
||||
) {
|
||||
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
|
||||
acc.push({
|
||||
...item,
|
||||
key: { ...item.key, key: mappedKey },
|
||||
});
|
||||
} else {
|
||||
acc.push(item);
|
||||
}
|
||||
const requestProps = dotMetricsEnabled
|
||||
? {
|
||||
...props,
|
||||
filters: {
|
||||
...props.filters,
|
||||
items: props.filters.items.reduce<typeof props.filters.items>(
|
||||
(acc, item) => {
|
||||
if (item.value === undefined) {
|
||||
return acc;
|
||||
},
|
||||
[] as typeof props.filters.items,
|
||||
),
|
||||
},
|
||||
}
|
||||
: props;
|
||||
}
|
||||
if (
|
||||
item.key &&
|
||||
typeof item.key === 'object' &&
|
||||
'key' in item.key &&
|
||||
typeof item.key.key === 'string'
|
||||
) {
|
||||
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
|
||||
acc.push({
|
||||
...item,
|
||||
key: { ...item.key, key: mappedKey },
|
||||
});
|
||||
} else {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as typeof props.filters.items,
|
||||
),
|
||||
},
|
||||
}
|
||||
: props;
|
||||
|
||||
const response = await axios.post('/deployments/list', requestProps, {
|
||||
signal,
|
||||
@@ -113,7 +114,6 @@ export const getK8sDeploymentsList = async (
|
||||
});
|
||||
const payload: K8sDeploymentsListResponse = response.data;
|
||||
|
||||
// single-line mapping
|
||||
payload.data.records = payload.data.records.map((record) => ({
|
||||
...record,
|
||||
meta: mapDeploymentsMeta(record.meta as Record<string, unknown>),
|
||||
@@ -1,11 +1,75 @@
|
||||
import { K8sDeploymentsData } from 'api/infraMonitoring/getK8sDeploymentsList';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
createFilterItem,
|
||||
K8sDetailsMetadataConfig,
|
||||
} from '../Base/K8sBaseDetails';
|
||||
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
|
||||
import { K8sDeploymentsData } from './api';
|
||||
|
||||
export const k8sDeploymentGetSelectedItemFilters = (
|
||||
selectedItemId: string,
|
||||
): TagFilter => ({
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: 'k8s_deployment_name',
|
||||
key: {
|
||||
key: 'k8s_deployment_name',
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: selectedItemId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const k8sDeploymentDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sDeploymentsData>[] = [
|
||||
{
|
||||
label: 'Deployment Name',
|
||||
getValue: (p): string => p.meta.k8s_deployment_name,
|
||||
},
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
getValue: (p): string => p.meta.k8s_cluster_name,
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
getValue: (p): string => p.meta.k8s_namespace_name,
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sDeploymentInitialFilters = [
|
||||
QUERY_KEYS.K8S_DEPLOYMENT_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
];
|
||||
|
||||
export const k8sDeploymentInitialEventsFilter = (
|
||||
item: K8sDeploymentsData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Deployment'),
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_deployment_name),
|
||||
];
|
||||
|
||||
export const k8sDeploymentInitialLogTracesFilter = (
|
||||
item: K8sDeploymentsData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(
|
||||
QUERY_KEYS.K8S_DEPLOYMENT_NAME,
|
||||
item.meta.k8s_deployment_name,
|
||||
),
|
||||
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.meta.k8s_namespace_name),
|
||||
];
|
||||
|
||||
export const k8sDeploymentGetEntityName = (item: K8sDeploymentsData): string =>
|
||||
item.meta.k8s_deployment_name;
|
||||
|
||||
export const deploymentWidgetInfo = [
|
||||
{
|
||||
title: 'CPU usage, request, limits',
|
||||
@@ -0,0 +1,269 @@
|
||||
import { TableColumnType as ColumnType, Tooltip } from 'antd';
|
||||
import { Group } from 'lucide-react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { K8sRenderedRowData } from '../Base/types';
|
||||
import { IEntityColumn } from '../Base/useInfraMonitoringTableColumnsStore';
|
||||
import { getGroupByEl, getGroupedByMeta, getRowKey } from '../Base/utils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
formatBytes,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../commonUtils';
|
||||
import { K8sDeploymentsData } from './api';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
|
||||
export const k8sDeploymentsColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'Deployment Group',
|
||||
value: 'deploymentGroup',
|
||||
id: 'deploymentGroup',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-collapse',
|
||||
},
|
||||
{
|
||||
label: 'Deployment Name',
|
||||
value: 'deploymentName',
|
||||
id: 'deploymentName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-expand',
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
value: 'namespaceName',
|
||||
id: 'namespaceName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Available',
|
||||
value: 'available_pods',
|
||||
id: 'available_pods',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Desired',
|
||||
value: 'desired_pods',
|
||||
id: 'desired_pods',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Req Usage (%)',
|
||||
value: 'cpu_request',
|
||||
id: 'cpu_request',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Limit Usage (%)',
|
||||
value: 'cpu_limit',
|
||||
id: 'cpu_limit',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Req Usage (%)',
|
||||
value: 'memory_request',
|
||||
id: 'memory_request',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Limit Usage (%)',
|
||||
value: 'memory_limit',
|
||||
id: 'memory_limit',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Usage (WSS)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sDeploymentsColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
|
||||
{
|
||||
title: (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
<Group size={14} /> DEPLOYMENT GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'deploymentGroup',
|
||||
key: 'deploymentGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
},
|
||||
{
|
||||
title: <div>Deployment Name</div>,
|
||||
dataIndex: 'deploymentName',
|
||||
key: 'deploymentName',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Namespace Name</div>,
|
||||
dataIndex: 'namespaceName',
|
||||
key: 'namespaceName',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Available</div>,
|
||||
dataIndex: 'available_pods',
|
||||
key: 'available_pods',
|
||||
width: 100,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Desired</div>,
|
||||
dataIndex: 'desired_pods',
|
||||
key: 'desired_pods',
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Req Usage (%)</div>,
|
||||
dataIndex: 'cpu_request',
|
||||
key: 'cpu_request',
|
||||
width: 170,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Limit Usage (%)</div>,
|
||||
dataIndex: 'cpu_limit',
|
||||
key: 'cpu_limit',
|
||||
width: 170,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Req Usage (%)</div>,
|
||||
dataIndex: 'memory_request',
|
||||
key: 'memory_request',
|
||||
width: 170,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Limit Usage (%)</div>,
|
||||
dataIndex: 'memory_limit',
|
||||
key: 'memory_limit',
|
||||
width: 170,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sDeploymentsRenderRowData = (
|
||||
deployment: K8sDeploymentsData,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): K8sRenderedRowData => ({
|
||||
key: getRowKey(deployment, () => deployment.meta.k8s_deployment_name, groupBy),
|
||||
itemKey: deployment.meta.k8s_deployment_name,
|
||||
deploymentName: (
|
||||
<Tooltip title={deployment.meta.k8s_deployment_name}>
|
||||
{deployment.meta.k8s_deployment_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
namespaceName: deployment.meta.k8s_namespace_name,
|
||||
available_pods: (
|
||||
<ValidateColumnValueWrapper value={deployment.availablePods}>
|
||||
{deployment.availablePods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
desired_pods: (
|
||||
<ValidateColumnValueWrapper value={deployment.desiredPods}>
|
||||
{deployment.desiredPods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={deployment.cpuUsage}>
|
||||
{deployment.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_request: (
|
||||
<ValidateColumnValueWrapper value={deployment.cpuRequest}>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={deployment.cpuRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_limit: (
|
||||
<ValidateColumnValueWrapper value={deployment.cpuLimit}>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={deployment.cpuLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={deployment.memoryUsage}>
|
||||
{formatBytes(deployment.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_request: (
|
||||
<ValidateColumnValueWrapper value={deployment.memoryRequest}>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={deployment.memoryRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_limit: (
|
||||
<ValidateColumnValueWrapper value={deployment.memoryLimit}>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={deployment.memoryLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
deploymentGroup: getGroupByEl(deployment, groupBy),
|
||||
...deployment.meta,
|
||||
groupedByMeta: getGroupedByMeta(deployment, groupBy),
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
.entityGroupHeader {
|
||||
padding-left: var(--spacing-5);
|
||||
gap: var(--spacing-5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
}
|
||||
@@ -1,333 +0,0 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
|
||||
import {
|
||||
K8sDeploymentsData,
|
||||
K8sDeploymentsListPayload,
|
||||
} from 'api/infraMonitoring/getK8sDeploymentsList';
|
||||
import { Group } from 'lucide-react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
EntityProgressBar,
|
||||
formatBytes,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../commonUtils';
|
||||
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_pods',
|
||||
id: 'available_pods',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Desired',
|
||||
value: 'desired_pods',
|
||||
id: 'desired_pods',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Request Utilization (% of limit)',
|
||||
value: 'cpu_request',
|
||||
id: 'cpu_request',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Limit Utilization (% of request)',
|
||||
value: 'cpu_limit',
|
||||
id: 'cpu_limit',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Utilization (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Memory Request Utilization (% of limit)',
|
||||
value: 'memory_request',
|
||||
id: 'memory_request',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Memory Limit Utilization (% of request)',
|
||||
value: 'memory_limit',
|
||||
id: 'memory_limit',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Memory Utilization (bytes)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canRemove: false,
|
||||
},
|
||||
];
|
||||
|
||||
export interface K8sDeploymentsRowData {
|
||||
key: string;
|
||||
deploymentUID: string;
|
||||
deploymentName: React.ReactNode;
|
||||
available_pods: React.ReactNode;
|
||||
desired_pods: React.ReactNode;
|
||||
cpu_request: React.ReactNode;
|
||||
cpu_limit: React.ReactNode;
|
||||
cpu: React.ReactNode;
|
||||
memory_request: React.ReactNode;
|
||||
memory_limit: React.ReactNode;
|
||||
memory: React.ReactNode;
|
||||
restarts: React.ReactNode;
|
||||
clusterName: string;
|
||||
namespaceName: string;
|
||||
groupedByMeta?: any;
|
||||
}
|
||||
|
||||
const deploymentGroupColumnConfig = {
|
||||
title: (
|
||||
<div className="column-header entity-group-header">
|
||||
<Group size={14} /> DEPLOYMENT GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'deploymentGroup',
|
||||
key: 'deploymentGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
className: 'column entity-group-header',
|
||||
};
|
||||
|
||||
export const getK8sDeploymentsListQuery = (): K8sDeploymentsListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
const columnsConfig = [
|
||||
{
|
||||
title: (
|
||||
<div className="column-header-left deployment-name-header">
|
||||
Deployment Name
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'deploymentName',
|
||||
key: 'deploymentName',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<div className="column-header-left namespace-name-header">
|
||||
Namespace Name
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'namespaceName',
|
||||
key: 'namespaceName',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left small-col">Available</div>,
|
||||
dataIndex: 'available_pods',
|
||||
key: 'available_pods',
|
||||
width: 100,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left small-col">Desired</div>,
|
||||
dataIndex: 'desired_pods',
|
||||
key: 'desired_pods',
|
||||
width: 80,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left med-col">CPU Req Usage (%)</div>,
|
||||
dataIndex: 'cpu_request',
|
||||
key: 'cpu_request',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left med-col">CPU Limit Usage (%)</div>,
|
||||
dataIndex: 'cpu_limit',
|
||||
key: 'cpu_limit',
|
||||
width: 50,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header- small-col">CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left med-col">Mem Req Usage (%)</div>,
|
||||
dataIndex: 'memory_request',
|
||||
key: 'memory_request',
|
||||
width: 50,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left med-col">Mem Limit Usage (%)</div>,
|
||||
dataIndex: 'memory_limit',
|
||||
key: 'memory_limit',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left small-col">Mem Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 120,
|
||||
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 dotToUnder: Record<string, keyof K8sDeploymentsData['meta']> = {
|
||||
'k8s.deployment.name': 'k8s_deployment_name',
|
||||
'k8s.namespace.name': 'k8s_namespace_name',
|
||||
'k8s.cluster.name': 'k8s_cluster_name',
|
||||
};
|
||||
|
||||
const getGroupByEle = (
|
||||
deployment: K8sDeploymentsData,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): React.ReactNode => {
|
||||
const groupByValues: string[] = [];
|
||||
|
||||
groupBy.forEach((group) => {
|
||||
const rawKey = group.key as string;
|
||||
|
||||
// Choose mapped key if present, otherwise use rawKey
|
||||
const metaKey = (dotToUnder[rawKey] ??
|
||||
rawKey) as keyof typeof deployment.meta;
|
||||
const value = deployment.meta[metaKey];
|
||||
|
||||
groupByValues.push(value);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="pod-group">
|
||||
{groupByValues.map((value) => (
|
||||
<Tag key={value} color={Color.BG_SLATE_400} className="pod-group-tag-item">
|
||||
{value === '' ? '<no-value>' : value}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const formatDataForTable = (
|
||||
data: K8sDeploymentsData[],
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): K8sDeploymentsRowData[] =>
|
||||
data.map((deployment, index) => ({
|
||||
key: index.toString(),
|
||||
deploymentUID: deployment.meta.k8s_deployment_name,
|
||||
deploymentName: (
|
||||
<Tooltip title={deployment.meta.k8s_deployment_name}>
|
||||
{deployment.meta.k8s_deployment_name}
|
||||
</Tooltip>
|
||||
),
|
||||
available_pods: (
|
||||
<ValidateColumnValueWrapper value={deployment.availablePods}>
|
||||
{deployment.availablePods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
desired_pods: (
|
||||
<ValidateColumnValueWrapper value={deployment.desiredPods}>
|
||||
{deployment.desiredPods}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
restarts: (
|
||||
<ValidateColumnValueWrapper value={deployment.restarts}>
|
||||
{deployment.restarts}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={deployment.cpuUsage}>
|
||||
{deployment.cpuUsage}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_request: (
|
||||
<ValidateColumnValueWrapper value={deployment.cpuRequest}>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={deployment.cpuRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_limit: (
|
||||
<ValidateColumnValueWrapper value={deployment.cpuLimit}>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={deployment.cpuLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={deployment.memoryUsage}>
|
||||
{formatBytes(deployment.memoryUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_request: (
|
||||
<ValidateColumnValueWrapper value={deployment.memoryRequest}>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={deployment.memoryRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_limit: (
|
||||
<ValidateColumnValueWrapper value={deployment.memoryLimit}>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={deployment.memoryLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
clusterName: deployment.meta.k8s_cluster_name,
|
||||
namespaceName: deployment.meta.k8s_namespace_name,
|
||||
deploymentGroup: getGroupByEle(deployment, groupBy),
|
||||
meta: deployment.meta,
|
||||
...deployment.meta,
|
||||
groupedByMeta: deployment.meta,
|
||||
}));
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Space, Typography } from 'antd';
|
||||
import WaitlistFragment from 'components/HostMetricsDetail/WaitlistFragment/WaitlistFragment';
|
||||
|
||||
import WaitlistFragment from '../WaitlistFragment/WaitlistFragment';
|
||||
|
||||
import './Containers.styles.scss';
|
||||
import 'components/HostMetricsDetail/Containers/Containers.styles.scss';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
function Containers(): JSX.Element {
|
||||
function EntityContainers(): JSX.Element {
|
||||
const { t } = useTranslation(['infraMonitoring']);
|
||||
|
||||
return (
|
||||
@@ -41,4 +40,4 @@ function Containers(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
export default Containers;
|
||||
export default EntityContainers;
|
||||
@@ -2,10 +2,10 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Table, TableColumnsType } from 'antd';
|
||||
import { VIEWS } from 'components/HostMetricsDetail/constants';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { EventContents } from 'container/InfraMonitoringK8s/commonUtils';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import { VIEWS } from 'container/InfraMonitoringK8s/constants';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8s/constants';
|
||||
import LoadingContainer from 'container/InfraMonitoringK8s/LoadingContainer';
|
||||
import { INITIAL_PAGE_SIZE } from 'container/LogsContextList/configs';
|
||||
import LogsError from 'container/LogsError/LogsError';
|
||||
@@ -67,7 +67,7 @@ interface IEntityEventsProps {
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
selectedInterval: Time;
|
||||
category: K8sCategory;
|
||||
category: InfraMonitoringEntity;
|
||||
queryKey: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8s/constants';
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import * as useQueryBuilderHooks from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import * as appContextHooks from 'providers/App/App';
|
||||
@@ -106,7 +106,7 @@ const mockFilters: IBuilderQuery['filters'] = {
|
||||
const isModalTimeSelection = false;
|
||||
const mockHandleTimeChange = jest.fn();
|
||||
const selectedInterval: Time = '1m';
|
||||
const category = K8sCategory.PODS;
|
||||
const category = InfraMonitoringEntity.PODS;
|
||||
const queryKey = 'pod-events';
|
||||
|
||||
const mockEventsData = {
|
||||
|
||||
@@ -6,7 +6,7 @@ import LogDetail from 'components/LogDetail';
|
||||
import RawLogView from 'components/Logs/RawLogView';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8s/constants';
|
||||
import LogsError from 'container/LogsError/LogsError';
|
||||
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
@@ -31,7 +31,7 @@ interface Props {
|
||||
};
|
||||
filters: IBuilderQuery['filters'];
|
||||
queryKey: string;
|
||||
category: K8sCategory;
|
||||
category: InfraMonitoringEntity;
|
||||
queryKeyFilters: Array<string>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { VIEWS } from 'components/HostMetricsDetail/constants';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import { VIEWS } from 'container/InfraMonitoringK8s/constants';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8s/constants';
|
||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
@@ -30,7 +30,7 @@ interface Props {
|
||||
logFilters: IBuilderQuery['filters'];
|
||||
selectedInterval: Time;
|
||||
queryKey: string;
|
||||
category: K8sCategory;
|
||||
category: InfraMonitoringEntity;
|
||||
queryKeyFilters: Array<string>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8s/constants';
|
||||
import { verifyFiltersAndOrderBy } from 'container/LogsExplorerViews/tests/LogsExplorerPagination.test';
|
||||
import { logsPaginationQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
|
||||
import { server } from 'mocks-server/server';
|
||||
@@ -94,7 +94,7 @@ describe('EntityLogs', () => {
|
||||
timeRange={{ startTime: 1, endTime: 2 }}
|
||||
filters={{ items: [], op: 'AND' }}
|
||||
queryKey="test"
|
||||
category={K8sCategory.PODS}
|
||||
category={InfraMonitoringEntity.PODS}
|
||||
queryKeyFilters={[]}
|
||||
/>
|
||||
</VirtuosoMockContext.Provider>,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
getMetricsTableData,
|
||||
MetricsTable,
|
||||
} from 'container/InfraMonitoringK8s/commonUtils';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8s/constants';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
@@ -57,7 +57,7 @@ interface EntityMetricsProps<T> {
|
||||
dotMetricsEnabled: boolean,
|
||||
) => GetQueryResultsProps[];
|
||||
queryKey: string;
|
||||
category: K8sCategory;
|
||||
category: InfraMonitoringEntity;
|
||||
}
|
||||
|
||||
function EntityMetrics<T>({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8s/constants';
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import * as appContextHooks from 'providers/App/App';
|
||||
import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
||||
@@ -280,7 +280,7 @@ const renderEntityMetrics = (overrides = {}): any => {
|
||||
entityWidgetInfo: mockEntityWidgetInfo,
|
||||
getEntityQueryPayload: mockGetEntityQueryPayload,
|
||||
queryKey: 'test-query-key',
|
||||
category: K8sCategory.PODS,
|
||||
category: InfraMonitoringEntity.PODS,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Space, Typography } from 'antd';
|
||||
import WaitlistFragment from 'components/HostMetricsDetail/WaitlistFragment/WaitlistFragment';
|
||||
|
||||
import WaitlistFragment from '../WaitlistFragment/WaitlistFragment';
|
||||
|
||||
import './Processes.styles.scss';
|
||||
import 'components/HostMetricsDetail/Processes/Processes.styles.scss';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
function Processes(): JSX.Element {
|
||||
function EntityProcesses(): JSX.Element {
|
||||
const { t } = useTranslation(['infraMonitoring']);
|
||||
|
||||
return (
|
||||
@@ -40,4 +39,4 @@ function Processes(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
export default Processes;
|
||||
export default EntityProcesses;
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { VIEWS } from 'components/HostMetricsDetail/constants';
|
||||
import { getListColumns } from 'components/HostMetricsDetail/HostMetricTraces/utils';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
|
||||
import { VIEWS } from 'container/InfraMonitoringK8s/constants';
|
||||
import NoLogs from 'container/NoLogs/NoLogs';
|
||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||
import { ErrorText } from 'container/TimeSeriesView/styles';
|
||||
@@ -31,6 +30,7 @@ import {
|
||||
getEntityTracesQueryPayload,
|
||||
selectedEntityTracesColumns,
|
||||
} from '../utils';
|
||||
import { getTraceListColumns } from './traceListColumns';
|
||||
|
||||
import './entityTraces.styles.scss';
|
||||
|
||||
@@ -134,7 +134,7 @@ function EntityTraces({
|
||||
enabled: !!queryPayload,
|
||||
});
|
||||
|
||||
const traceListColumns = getListColumns(selectedEntityTracesColumns);
|
||||
const traceListColumns = getTraceListColumns(selectedEntityTracesColumns);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.payload?.data?.newResult?.data?.result) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8s/constants';
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import * as useQueryBuilderHooks from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import * as appContextHooks from 'providers/App/App';
|
||||
@@ -112,7 +112,7 @@ const mockTracesFilters: IBuilderQuery['filters'] = {
|
||||
const isModalTimeSelection = false;
|
||||
const mockHandleTimeChange = jest.fn();
|
||||
const selectedInterval: Time = '5m';
|
||||
const category = K8sCategory.PODS;
|
||||
const category = InfraMonitoringEntity.PODS;
|
||||
const queryKey = 'pod-traces';
|
||||
const queryKeyFilters = ['service.name'];
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const keyToLabelMap: Record<string, string> = {
|
||||
responseStatusCode: 'Status Code',
|
||||
};
|
||||
|
||||
export const getListColumns = (
|
||||
export const getTraceListColumns = (
|
||||
selectedColumns: BaseAutocompleteData[],
|
||||
): ColumnsType<RowData> => {
|
||||
const columns: ColumnsType<RowData> =
|
||||
@@ -16,7 +16,7 @@ import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { nanoToMilli } from 'utils/timeUtils';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { K8sCategory } from '../constants';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
|
||||
export const QUERY_KEYS = {
|
||||
K8S_OBJECT_KIND: 'k8s.object.kind',
|
||||
@@ -29,6 +29,7 @@ export const QUERY_KEYS = {
|
||||
K8S_STATEFUL_SET_NAME: 'k8s.statefulset.name',
|
||||
K8S_JOB_NAME: 'k8s.job.name',
|
||||
K8S_DAEMON_SET_NAME: 'k8s.daemonset.name',
|
||||
K8S_PERSISTENT_VOLUME_CLAIM_NAME: 'k8s.persistentvolumeclaim.name',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -96,7 +97,7 @@ export function EntityDetailsEmptyContainer({
|
||||
category,
|
||||
}: {
|
||||
view: 'logs' | 'traces' | 'events';
|
||||
category: K8sCategory;
|
||||
category: InfraMonitoringEntity;
|
||||
}): React.ReactElement {
|
||||
const label = category.slice(0, category.length);
|
||||
|
||||
|
||||
@@ -337,205 +337,6 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.k8s-list-table {
|
||||
.ant-table {
|
||||
.ant-table-thead > tr > th {
|
||||
padding: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
|
||||
background: var(--bg-ink-500);
|
||||
border-bottom: none;
|
||||
|
||||
color: var(--Vanilla-400, #c0c1c3);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.44px;
|
||||
text-transform: uppercase;
|
||||
|
||||
&::before {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th:has(.hostname-column-header) {
|
||||
background: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: var(--bg-vanilla-100);
|
||||
background: var(--bg-ink-500);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ant-table-cell:has(.hostname-column-value) {
|
||||
background: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.hostname-column-value {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-family: 'Geist Mono';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.status-cell {
|
||||
.active-tag {
|
||||
color: var(--bg-forest-500);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
.ant-progress-bg {
|
||||
height: 8px !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.ant-table-cell:first-child {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.ant-table-cell:nth-child(2) {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.ant-table-cell:nth-child(n + 3) {
|
||||
padding-right: 24px;
|
||||
}
|
||||
.column-header-right {
|
||||
text-align: right;
|
||||
}
|
||||
.ant-table-tbody > tr > td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ant-table-thead
|
||||
> tr
|
||||
> th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ant-empty-normal {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: calc(100% - 54px);
|
||||
background: var(--bg-ink-500);
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
|
||||
// this is to offset chat support icon till we improve the design
|
||||
padding-right: 72px;
|
||||
|
||||
.ant-pagination-item {
|
||||
border-radius: 4px;
|
||||
|
||||
&-active {
|
||||
background: var(--bg-robin-500);
|
||||
border-color: var(--bg-robin-500);
|
||||
|
||||
a {
|
||||
color: var(--bg-ink-500) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-expanded-row {
|
||||
&:hover {
|
||||
background: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
background: var(--bg-ink-500) !important;
|
||||
}
|
||||
|
||||
.ant-table .ant-table-thead > tr > th {
|
||||
padding: 4px 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.expanded-table-container {
|
||||
border: 1px solid var(--bg-ink-400);
|
||||
overflow-x: auto;
|
||||
padding-left: 48px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
height: 0.1rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-ink-200);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--bg-ink-100);
|
||||
}
|
||||
|
||||
.ant-table-expanded-row {
|
||||
background: var(--bg-ink-500);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
background: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
.expanded-table-footer {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
padding-left: 42px;
|
||||
margin-top: 8px;
|
||||
|
||||
.periscope-btn {
|
||||
font-size: 10px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.view-all-text {
|
||||
font-size: 10px;
|
||||
color: var(--bg-vanilla-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.k8s-list-container-filters-visible {
|
||||
.k8s-list-table {
|
||||
.ant-pagination {
|
||||
@@ -655,51 +456,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.infra-monitoring-container {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { VerticalAlignTopOutlined } from '@ant-design/icons';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import type { CollapseProps } from 'antd';
|
||||
import { Button, CollapseProps } from 'antd';
|
||||
import { Collapse, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Computer,
|
||||
Container,
|
||||
FilePenLine,
|
||||
Filter,
|
||||
Group,
|
||||
HardDrive,
|
||||
Workflow,
|
||||
@@ -63,13 +64,11 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
const [, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
const [, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
|
||||
const [quickFiltersLastUpdated, setQuickFiltersLastUpdated] = useState(-1);
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const handleFilterVisibilityChange = (): void => {
|
||||
setShowFilters(!showFilters);
|
||||
};
|
||||
const handleFilterVisibilityChange = useCallback((): void => {
|
||||
setShowFilters((show) => !show);
|
||||
}, []);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
@@ -86,7 +85,6 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
// 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);
|
||||
setQuickFiltersLastUpdated(Date.now());
|
||||
setFilters(JSON.stringify(query.builder.queryData[0].filters));
|
||||
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
@@ -321,6 +319,25 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
const showFiltersComp = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
{!showFilters && (
|
||||
<div className="quick-filters-toggle-container">
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={handleFilterVisibilityChange}
|
||||
>
|
||||
<Filter size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}, [handleFilterVisibilityChange, showFilters]);
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className="infra-monitoring-container">
|
||||
@@ -355,75 +372,39 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
}`}
|
||||
>
|
||||
{selectedCategory === K8sCategories.PODS && (
|
||||
<K8sPodLists
|
||||
isFiltersVisible={showFilters}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
quickFiltersLastUpdated={quickFiltersLastUpdated}
|
||||
/>
|
||||
<K8sPodLists controlListPrefix={showFiltersComp} />
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.NODES && (
|
||||
<K8sNodesList
|
||||
isFiltersVisible={showFilters}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
quickFiltersLastUpdated={quickFiltersLastUpdated}
|
||||
/>
|
||||
<K8sNodesList controlListPrefix={showFiltersComp} />
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.CLUSTERS && (
|
||||
<K8sClustersList
|
||||
isFiltersVisible={showFilters}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
quickFiltersLastUpdated={quickFiltersLastUpdated}
|
||||
/>
|
||||
<K8sClustersList controlListPrefix={showFiltersComp} />
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.DEPLOYMENTS && (
|
||||
<K8sDeploymentsList
|
||||
isFiltersVisible={showFilters}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
quickFiltersLastUpdated={quickFiltersLastUpdated}
|
||||
/>
|
||||
<K8sDeploymentsList controlListPrefix={showFiltersComp} />
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.NAMESPACES && (
|
||||
<K8sNamespacesList
|
||||
isFiltersVisible={showFilters}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
quickFiltersLastUpdated={quickFiltersLastUpdated}
|
||||
/>
|
||||
<K8sNamespacesList controlListPrefix={showFiltersComp} />
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.STATEFULSETS && (
|
||||
<K8sStatefulSetsList
|
||||
isFiltersVisible={showFilters}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
quickFiltersLastUpdated={quickFiltersLastUpdated}
|
||||
/>
|
||||
<K8sStatefulSetsList controlListPrefix={showFiltersComp} />
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.JOBS && (
|
||||
<K8sJobsList
|
||||
isFiltersVisible={showFilters}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
quickFiltersLastUpdated={quickFiltersLastUpdated}
|
||||
/>
|
||||
<K8sJobsList controlListPrefix={showFiltersComp} />
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.DAEMONSETS && (
|
||||
<K8sDaemonSetsList
|
||||
isFiltersVisible={showFilters}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
quickFiltersLastUpdated={quickFiltersLastUpdated}
|
||||
/>
|
||||
<K8sDaemonSetsList controlListPrefix={showFiltersComp} />
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.VOLUMES && (
|
||||
<K8sVolumesList
|
||||
isFiltersVisible={showFilters}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
quickFiltersLastUpdated={quickFiltersLastUpdated}
|
||||
/>
|
||||
<K8sVolumesList controlListPrefix={showFiltersComp} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { K8sJobsData } from 'api/infraMonitoring/getK8sJobsList';
|
||||
|
||||
export type JobDetailsProps = {
|
||||
job: K8sJobsData | null;
|
||||
isModalTimeSelection: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -1,654 +0,0 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
|
||||
import type { RadioChangeEvent } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { VIEW_TYPES, VIEWS } from 'components/HostMetricsDetail/constants';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryState,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import {
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from 'container/InfraMonitoringK8s/hooks';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
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 { 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 JobEvents from '../../EntityDetailsUtils/EntityEvents';
|
||||
import JobLogs from '../../EntityDetailsUtils/EntityLogs';
|
||||
import JobMetrics from '../../EntityDetailsUtils/EntityMetrics';
|
||||
import JobTraces from '../../EntityDetailsUtils/EntityTraces';
|
||||
import { QUERY_KEYS } from '../../EntityDetailsUtils/utils';
|
||||
import { getJobMetricsQueryPayload, jobWidgetInfo } from './constants';
|
||||
import { JobDetailsProps } from './JobDetails.interfaces';
|
||||
|
||||
import '../../EntityDetailsUtils/entityDetails.styles.scss';
|
||||
|
||||
function JobDetails({
|
||||
job,
|
||||
onClose,
|
||||
isModalTimeSelection,
|
||||
}: JobDetailsProps): JSX.Element {
|
||||
const { maxTime, minTime, selectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const startMs = useMemo(() => Math.floor(Number(minTime) / 1000000000), [
|
||||
minTime,
|
||||
]);
|
||||
const endMs = useMemo(() => Math.floor(Number(maxTime) / 1000000000), [
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const [modalTimeRange, setModalTimeRange] = useState(() => ({
|
||||
startTime: startMs,
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
);
|
||||
|
||||
const [selectedView, setSelectedView] = useInfraMonitoringView();
|
||||
const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters();
|
||||
const [
|
||||
tracesFiltersParam,
|
||||
setTracesFiltersParam,
|
||||
] = useInfraMonitoringTracesFilters();
|
||||
const [
|
||||
eventsFiltersParam,
|
||||
setEventsFiltersParam,
|
||||
] = useInfraMonitoringEventsFilters();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const initialFilters = useMemo(() => {
|
||||
const filters =
|
||||
selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam;
|
||||
if (filters) {
|
||||
return filters;
|
||||
}
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_JOB_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s_job_name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: job?.meta.k8s_job_name || '',
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s_job_name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: job?.meta.k8s_namespace_name || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [
|
||||
job?.meta.k8s_job_name,
|
||||
job?.meta.k8s_namespace_name,
|
||||
selectedView,
|
||||
logFiltersParam,
|
||||
tracesFiltersParam,
|
||||
]);
|
||||
|
||||
const initialEventsFilters = useMemo(() => {
|
||||
if (eventsFiltersParam) {
|
||||
return eventsFiltersParam;
|
||||
}
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s.object.kind--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: 'Job',
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s.object.name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: job?.meta.k8s_job_name || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [job?.meta.k8s_job_name, eventsFiltersParam]);
|
||||
|
||||
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
|
||||
IBuilderQuery['filters']
|
||||
>(initialFilters);
|
||||
|
||||
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
|
||||
initialEventsFilters,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (job) {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Job,
|
||||
});
|
||||
}
|
||||
}, [job]);
|
||||
|
||||
useEffect(() => {
|
||||
setLogAndTracesFilters(initialFilters);
|
||||
setEventsFilters(initialEventsFilters);
|
||||
}, [initialFilters, initialEventsFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
endTime: Math.floor(maxTime / 1000000000),
|
||||
});
|
||||
}
|
||||
}, [selectedTime, minTime, maxTime]);
|
||||
|
||||
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||
setSelectedView(e.target.value);
|
||||
setLogFiltersParam(null);
|
||||
setTracesFiltersParam(null);
|
||||
setEventsFiltersParam(null);
|
||||
logEvent(InfraMonitoringEvents.TabChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Job,
|
||||
view: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
lastSelectedInterval.current = interval as Time;
|
||||
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(InfraMonitoringEvents.TimeUpdated, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Job,
|
||||
interval,
|
||||
view: selectedView,
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeLogFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setLogAndTracesFilters((prevFilters) => {
|
||||
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||
[QUERY_KEYS.K8S_JOB_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
|
||||
item.key?.key ?? '',
|
||||
),
|
||||
);
|
||||
const paginationFilter = value?.items?.find(
|
||||
(item) => item.key?.key === 'id',
|
||||
);
|
||||
const newFilters = value?.items?.filter(
|
||||
(item) =>
|
||||
item.key?.key !== 'id' && item.key?.key !== QUERY_KEYS.K8S_JOB_NAME,
|
||||
);
|
||||
|
||||
if (newFilters && newFilters?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Job,
|
||||
view: 'logs',
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...(primaryFilters || []),
|
||||
...(newFilters || []),
|
||||
...(paginationFilter ? [paginationFilter] : []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
setLogFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeTracesFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setLogAndTracesFilters((prevFilters) => {
|
||||
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||
[QUERY_KEYS.K8S_JOB_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
|
||||
item.key?.key ?? '',
|
||||
),
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Job,
|
||||
view: 'traces',
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
...(primaryFilters || []),
|
||||
...(value?.items?.filter(
|
||||
(item) => item.key?.key !== QUERY_KEYS.K8S_JOB_NAME,
|
||||
) || []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
setTracesFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeEventsFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setEventsFilters((prevFilters) => {
|
||||
const jobKindFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
);
|
||||
const jobNameFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Job,
|
||||
view: 'events',
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: [
|
||||
jobKindFilter,
|
||||
jobNameFilter,
|
||||
...(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),
|
||||
};
|
||||
|
||||
setEventsFiltersParam(updatedFilters);
|
||||
setSelectedView(view);
|
||||
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// 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(InfraMonitoringEvents.ExploreClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Job,
|
||||
view: selectedView,
|
||||
});
|
||||
|
||||
if (selectedView === VIEW_TYPES.LOGS) {
|
||||
const filtersWithoutPagination = {
|
||||
...logAndTracesFilters,
|
||||
items:
|
||||
logAndTracesFilters?.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: logAndTracesFilters,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
lastSelectedInterval.current = null;
|
||||
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">
|
||||
{job?.meta.k8s_job_name}
|
||||
</Typography.Text>
|
||||
</>
|
||||
}
|
||||
placement="right"
|
||||
onClose={handleClose}
|
||||
open={!!job}
|
||||
style={{
|
||||
overscrollBehavior: 'contain',
|
||||
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||
}}
|
||||
className="entity-detail-drawer"
|
||||
destroyOnClose
|
||||
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||
>
|
||||
{job && (
|
||||
<>
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Job Name
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Namespace Name
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="values-row">
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={job.meta.k8s_job_name}>
|
||||
{job.meta.k8s_job_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={job.meta.k8s_namespace_name}>
|
||||
{job.meta.k8s_namespace_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="views-tabs-container">
|
||||
<Radio.Group
|
||||
className="views-tabs"
|
||||
onChange={handleTabChange}
|
||||
value={selectedView}
|
||||
>
|
||||
<Radio.Button
|
||||
className={
|
||||
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 && (
|
||||
<JobMetrics
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
entity={job}
|
||||
category={K8sCategory.JOBS}
|
||||
queryKey="jobMetrics"
|
||||
entityWidgetInfo={jobWidgetInfo}
|
||||
getEntityQueryPayload={getJobMetricsQueryPayload}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<JobLogs
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeLogFilters={handleChangeLogFilters}
|
||||
logFilters={logAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
category={K8sCategory.JOBS}
|
||||
queryKey="jobLogs"
|
||||
queryKeyFilters={[
|
||||
QUERY_KEYS.K8S_JOB_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.TRACES && (
|
||||
<JobTraces
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeTracesFilters={handleChangeTracesFilters}
|
||||
tracesFilters={logAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey="jobTraces"
|
||||
category={InfraMonitoringEvents.Job}
|
||||
queryKeyFilters={[
|
||||
QUERY_KEYS.K8S_JOB_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.EVENTS && (
|
||||
<JobEvents
|
||||
timeRange={modalTimeRange}
|
||||
handleChangeEventFilters={handleChangeEventsFilters}
|
||||
filters={eventsFilters}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
category={K8sCategory.JOBS}
|
||||
queryKey="jobEvents"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default JobDetails;
|
||||
@@ -1,3 +0,0 @@
|
||||
import JobDetails from './JobDetails';
|
||||
|
||||
export default JobDetails;
|
||||
@@ -1,62 +0,0 @@
|
||||
.infra-monitoring-container {
|
||||
.jobs-list-table {
|
||||
.expanded-table-container {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.ant-table-cell.column-progress-bar {
|
||||
min-width: 180px;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.ant-table-row-expand-icon-cell {
|
||||
min-width: 30px !important;
|
||||
max-width: 30px !important;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.job-name-header) {
|
||||
min-width: 250px !important;
|
||||
max-width: 250px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.namespace-name-header) {
|
||||
min-width: 220px !important;
|
||||
max-width: 220px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.med-col) {
|
||||
min-width: 200px !important;
|
||||
max-width: 200px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.small-col) {
|
||||
min-width: 120px !important;
|
||||
max-width: 120px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.expanded-jobs-list-table {
|
||||
.ant-table-cell {
|
||||
min-width: 200px !important;
|
||||
max-width: 200px !important;
|
||||
}
|
||||
|
||||
.ant-table-row-expand-icon-cell {
|
||||
min-width: 30px !important;
|
||||
max-width: 30px !important;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user