Compare commits

...

13 Commits

Author SHA1 Message Date
Yunus M
17a921f49c chore: use Button from @signozhq/ui 2026-04-20 23:57:27 +05:30
Yunus M
ca8c58e21f Merge branch 'main' into chore/use-new-components 2026-04-20 23:56:07 +05:30
Yunus M
4f28748c65 fix: add missing semicolon 2026-04-20 23:54:40 +05:30
Yunus M
fabce46b59 fix: add missing semicolon 2026-04-20 22:52:42 +05:30
Yunus M
605e2d1b24 refactor: use correct variables and latest components 2026-04-20 22:47:24 +05:30
Vinicius Lourenço
c1a35808d9 feat(infrastructure-monitoring-k8s-and-hosts): add shared component for list/details (#10800)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
* feat(infra-monitoring): single component to be reused across pages

* feat(pod-details): extract to a common generic component

* refactor(k8s-filters-panel): use our components & css modules

* refactor(k8s-pods): split pod new components into more config files

* refactor(list): split expanded row into own component & change how the selected item is rendered

* refactor(infra-monitoring): add tests & migrate all css to css modules

* fix(empty-state): rendering svg too big

* fix(css): use more semantic tokens

* fix(prettify): ensure all components are correct formatted

* fix(dead-code): remove unused code

* fix(table.tsx): ensure all classes are on css modules

* test(k8s-base-list): add more details why the test fails

* fix(k8s-base-details): remove eslint ignore

* chore(k8s-base-list): cleaning up comments

* refactor(infra-monitoring): create hook for selected item

* refactor(base-details): move close to inside component

* chore(table.scss): fix prettify

* fix(k8s-base-details): auto-refresh causing the entire modal to refresh

* feat(global-time-adapter): adopt new global time adapter

* fix(css): table with wrong background color

* test(k8s-base-list): fix count of fetch times

* fix(table-module): css of the group header not correct

* refactor(utils): better organization for common code

* fix(k8s-base): not showing ellipsis

* fix(k8s-base-list): ensure loading always appear when loading data

* fix(k8s-base-details): issue with floating division

* refactor(infra-k8s): use constant for NANO_SECOND_MULTIPLIER

* fix(k8s): fallback to rawKey when meta via dot to under is not found

* fix(k8s-base-list): not rendering correctly when keys are duplicated

* chore(utils): change no value label to be consistent with previous behavior

* fix(k8s-base-list): add min size for columns

* fix(k8s-base-list): adjust size of first cell on group

* refactor(k8s-base-list): create const for max items to fetch on group by

* feat(infra-monitoring): migrate nodes to shared component (#10850)

* feat(infra-monitoring): migrate clusters to shared component (#10851)

* feat(infra-monitoring): migrate namespaces to shared component (#10852)

* feat(infra-monitoring): migrate deployments to shared component (#10854)

* feat(infra-monitoring): migrate deployments to shared component

* fix(table): ensure widths do not wrap text

* feat(infra-monitoring): migrate jobs to shared component (#10857)

* feat(infra-monitoring): migrate daemonsets to shared component (#10858)

* feat(infra-monitoring): migrate daemonsets to shared component

* fix(infra-monitoring-k8s): included more code change than needed

* feat(infra-monitoring): migrate volumes to shared component (#10859)

* feat(infra-monitoring): migrate volumes to shared component

* fix(infra-monitoring-k8s): missing code change than needed

* feat(infra-monitoring): migrate statefulsets to shared component (#10853)

* refactor(infra-monitoring): remove dead-code (#10865)

* refactor(infra-monitoring-k8s): code cleanup (#10867)

* refactor(expanded-row): move to own component file

* refactor(filters): use correct type for filters

* refactor(api): add deprecate comments

* refactor(table): rename to config

* refactor(k8s-empty): remove unused file

* fix(k8s-expanded-row): fix prettify

* fix(css): broken css due to update on css modules config

* feat(infra-monitoring-hosts): migrate to use shared component (#10874)

* feat(infra-monitoring-hosts): use shared component on hosts container

* fix(infra-monitoring): migrate to css modules

* refactor(k8s-base-list): extract empty state component

* fix(img): to correct import

* fix(infra-monitoring): state + url desync on quick filters (#10930)

* test(k8s-base-list): fix flaky test

* refactor(infra-monitoring): better error feedback and fix custom range (#10991)

* fix(infra-monitoring): not being able to set custom date

* refactor(infra-monitoring): ensure error message is displayed correctly & empty states

* fix(infra-monitoring): ensure we can close modal when custom date is defined by parent

* refactor(k8s-empty-state): add better error message

* fix(k8s-empty-state): try fix issue with import
2026-04-20 15:13:20 +00:00
Yunus M
d3fdacb388 refactor: update button imports and adjust prefixIcon to prefix in AWS integration components 2026-04-20 18:49:20 +05:30
Yunus M
486112185f Merge branch 'main' into chore/use-new-components 2026-04-20 18:20:33 +05:30
Yunus M
b36b1cb827 fix: failing test cases 2026-04-20 18:14:26 +05:30
Yunus M
5f4e0c6026 chore: add assignee in todo items 2026-04-20 18:04:19 +05:30
Yunus M
f5e49d1947 refactor: update test imports and improve dialog handling 2026-04-20 14:06:02 +05:30
Yunus M
c9637583ef chore: remove migration doc 2026-04-20 09:58:59 +05:30
Yunus M
cab9db6b74 refactor: use new components from signozhq/ui 2026-04-20 09:48:49 +05:30
338 changed files with 10276 additions and 24949 deletions

View File

@@ -45,7 +45,8 @@ const config: Config.InitialOptions = {
'^.+\\.(js|jsx)$': 'babel-jest',
},
transformIgnorePatterns: [
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs)/)',
// @chenglou/pretext is ESM-only; @signozhq/ui pulls it in via text-ellipsis.
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou/pretext|@signozhq/design-tokens|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs)/)',
],
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],

View File

@@ -24,6 +24,10 @@ window.matchMedia =
};
};
if (!HTMLElement.prototype.scrollIntoView) {
HTMLElement.prototype.scrollIntoView = function (): void {};
}
// Patch getComputedStyle to handle CSS parsing errors from @signozhq/* packages.
// These packages inject CSS at import time via style-inject / vite-plugin-css-injected-by-js.
// jsdom's nwsapi cannot parse some of the injected selectors (e.g. Tailwind's :animate-in),

View File

@@ -48,24 +48,9 @@
"@radix-ui/react-tooltip": "1.0.7",
"@sentry/react": "8.41.0",
"@sentry/vite-plugin": "2.22.6",
"@signozhq/button": "0.0.5",
"@signozhq/calendar": "0.1.1",
"@signozhq/callout": "0.0.4",
"@signozhq/checkbox": "0.0.4",
"@signozhq/combobox": "0.0.4",
"@signozhq/command": "0.0.2",
"@signozhq/design-tokens": "2.1.4",
"@signozhq/dialog": "0.0.4",
"@signozhq/drawer": "0.0.6",
"@signozhq/icons": "0.1.0",
"@signozhq/input": "0.0.4",
"@signozhq/popover": "0.1.2",
"@signozhq/radio-group": "0.0.4",
"@signozhq/resizable": "0.0.2",
"@signozhq/tabs": "0.0.11",
"@signozhq/table": "0.3.8",
"@signozhq/toggle-group": "0.0.3",
"@signozhq/ui": "0.0.5",
"@signozhq/ui": "0.0.9",
"@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "3.13.22",
"@uiw/codemirror-theme-copilot": "4.23.11",

View File

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

View File

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

View File

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

View File

@@ -10,21 +10,6 @@
// PR for reference: https://github.com/SigNoz/signoz/pull/9694
// -------------------------------------------------------------------------
import '@signozhq/button';
import '@signozhq/calendar';
import '@signozhq/callout';
import '@signozhq/checkbox';
import '@signozhq/combobox';
import '@signozhq/command';
import '@signozhq/design-tokens';
import '@signozhq/dialog';
import '@signozhq/drawer';
import '@signozhq/icons';
import '@signozhq/input';
import '@signozhq/popover';
import '@signozhq/radio-group';
import '@signozhq/resizable';
import '@signozhq/tabs';
import '@signozhq/table';
import '@signozhq/toggle-group';
import '@signozhq/ui';

View File

@@ -80,12 +80,12 @@
mask-image: radial-gradient(
circle at 50% 0,
color-mix(in srgb, var(--background) 10%, transparent) 0,
color-mix(in srgb, var(--l1-background) 10%, transparent) 0,
transparent 100%
);
-webkit-mask-image: radial-gradient(
circle at 50% 0,
color-mix(in srgb, var(--background) 10%, transparent) 0,
color-mix(in srgb, var(--l1-background) 10%, transparent) 0,
transparent 100%
);
}

View File

@@ -1,5 +1,5 @@
import { useCallback } from 'react';
import { Button } from '@signozhq/button';
import { Button } from '@signozhq/ui';
import { LifeBuoy } from 'lucide-react';
import signozBrandLogoUrl from '@/assets/Logos/signoz-brand-logo.svg';
@@ -23,8 +23,10 @@ function AuthHeader(): JSX.Element {
</div>
<Button
className="auth-header-help-button"
prefixIcon={<LifeBuoy size={12} />}
prefix={<LifeBuoy size={12} />}
onClick={handleGetHelp}
variant="solid"
color="none"
>
Get Help
</Button>

View File

@@ -43,12 +43,12 @@
.masked-dots {
mask-image: radial-gradient(
circle at 50% 0%,
color-mix(in srgb, var(--background) 10%, transparent) 0%,
color-mix(in srgb, var(--l1-background) 10%, transparent) 0%,
transparent 56.77%
);
-webkit-mask-image: radial-gradient(
circle at 50% 0%,
color-mix(in srgb, var(--background) 10%, transparent) 0%,
color-mix(in srgb, var(--l1-background) 10%, transparent) 0%,
transparent 56.77%
);
}

View File

@@ -1,10 +1,13 @@
import { Controller, useForm } from 'react-hook-form';
import { useQueryClient } from 'react-query';
import { Button } from '@signozhq/button';
import { DialogFooter, DialogWrapper } from '@signozhq/dialog';
import { X } from '@signozhq/icons';
import { Input } from '@signozhq/input';
import { toast } from '@signozhq/ui';
import {
Button,
DialogFooter,
DialogWrapper,
Input,
toast,
} from '@signozhq/ui';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
invalidateListServiceAccounts,
@@ -137,6 +140,7 @@ function CreateServiceAccountModal(): JSX.Element {
<Button
type="submit"
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
form="create-sa-form"
variant="solid"
color="primary"

View File

@@ -1,7 +1,13 @@
import { toast } from '@signozhq/ui';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import {
render,
screen,
userEvent,
waitFor,
waitForElementToBeRemoved,
} from 'tests/test-utils';
import CreateServiceAccountModal from '../CreateServiceAccountModal';
@@ -121,12 +127,12 @@ describe('CreateServiceAccountModal', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
await screen.findByRole('dialog', { name: /New Service Account/i });
const dialog = await screen.findByRole('dialog', {
name: /New Service Account/i,
});
await user.click(screen.getByRole('button', { name: /Cancel/i }));
expect(
screen.queryByRole('dialog', { name: /New Service Account/i }),
).not.toBeInTheDocument();
await waitForElementToBeRemoved(dialog);
});
it('shows "Name is required" after clearing the name field', async () => {

View File

@@ -1,4 +1,4 @@
import { Calendar } from '@signozhq/calendar';
import { Calendar } from '@signozhq/ui';
import { Button } from 'antd';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import dayjs from 'dayjs';

View File

@@ -7,7 +7,7 @@ import {
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
import { Button } from '@signozhq/button';
import { Button } from '@signozhq/ui';
import { Input, InputRef, Popover, Tooltip } from 'antd';
import cx from 'classnames';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
@@ -661,7 +661,9 @@ function CustomTimePicker({
onClick={handleZoomOut}
disabled={zoomOutDisabled}
data-testid="zoom-out-btn"
prefixIcon={<ZoomOut size={14} />}
prefix={<ZoomOut size={14} />}
variant="solid"
color="none"
/>
</Tooltip>
)}

View File

@@ -1,6 +1,5 @@
import { Button } from '@signozhq/button';
import { DialogFooter, DialogWrapper } from '@signozhq/dialog';
import { Trash2, X } from '@signozhq/icons';
import { Button, DialogFooter, DialogWrapper } from '@signozhq/ui';
import { MemberRow } from 'components/MembersTable/MembersTable';
interface DeleteMemberDialogProps {

View File

@@ -1,17 +1,10 @@
.edit-member-drawer {
&__layout {
display: flex;
flex-direction: column;
height: calc(100vh - 48px);
}
&__body {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--spacing-8);
padding: var(--padding-5) var(--padding-4);
}
&__field {
@@ -120,11 +113,6 @@
align-items: center;
justify-content: space-between;
width: 100%;
height: 56px;
padding: 0 var(--padding-4);
border-top: 1px solid var(--l1-border);
flex-shrink: 0;
background: var(--card);
}
&__footer-left {

View File

@@ -1,10 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Button } from '@signozhq/button';
import { DrawerWrapper } from '@signozhq/drawer';
import { LockKeyhole, RefreshCw, Trash2, X } from '@signozhq/icons';
import { Input } from '@signozhq/input';
import { Badge, toast } from '@signozhq/ui';
import { Badge, Button, DrawerWrapper, Input, toast } from '@signozhq/ui';
import { Skeleton, Tooltip } from 'antd';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
@@ -596,65 +593,69 @@ function EditMemberDrawer({
const drawerContent = (
<div className="edit-member-drawer__layout">
<div className="edit-member-drawer__body">{drawerBody}</div>
</div>
);
{!isDeleted && (
<div className="edit-member-drawer__footer">
<div className="edit-member-drawer__footer-left">
<Tooltip title={getDeleteTooltip(isRootUser, isSelf)}>
<span className="edit-member-drawer__tooltip-wrapper">
<Button
className="edit-member-drawer__footer-btn edit-member-drawer__footer-btn--danger"
onClick={(): void => setShowDeleteConfirm(true)}
disabled={isRootUser || isSelf}
>
<Trash2 size={12} />
{isInvited ? 'Revoke Invite' : 'Delete Member'}
</Button>
</span>
</Tooltip>
<div className="edit-member-drawer__footer-divider" />
<Tooltip title={isRootUser ? ROOT_USER_TOOLTIP : undefined}>
<span className="edit-member-drawer__tooltip-wrapper">
<Button
className="edit-member-drawer__footer-btn edit-member-drawer__footer-btn--warning"
onClick={handleGenerateResetLink}
disabled={isGeneratingLink || isRootUser || isLoadingTokenStatus}
>
<RefreshCw size={12} />
{isGeneratingLink
? 'Generating...'
: isInvited
? getInviteButtonLabel(
isLoadingTokenStatus,
existingToken,
isTokenExpired,
tokenNotFound,
)
: 'Generate Password Reset Link'}
</Button>
</span>
</Tooltip>
</div>
<div className="edit-member-drawer__footer-right">
<Button variant="solid" color="secondary" size="sm" onClick={handleClose}>
<X size={14} />
Cancel
</Button>
const footer = (
<div className="edit-member-drawer__footer">
<div className="edit-member-drawer__footer-left">
<Tooltip title={getDeleteTooltip(isRootUser, isSelf)}>
<span className="edit-member-drawer__tooltip-wrapper">
<Button
className="edit-member-drawer__footer-btn edit-member-drawer__footer-btn--danger"
onClick={(): void => setShowDeleteConfirm(true)}
disabled={isRootUser || isSelf}
variant="solid"
color="primary"
size="sm"
disabled={!isDirty || isSaving || isRootUser}
onClick={handleSave}
color="none"
>
{isSaving ? 'Saving...' : 'Save Member Details'}
<Trash2 size={12} />
{isInvited ? 'Revoke Invite' : 'Delete Member'}
</Button>
</div>
</div>
)}
</span>
</Tooltip>
<div className="edit-member-drawer__footer-divider" />
<Tooltip title={isRootUser ? ROOT_USER_TOOLTIP : undefined}>
<span className="edit-member-drawer__tooltip-wrapper">
<Button
className="edit-member-drawer__footer-btn edit-member-drawer__footer-btn--warning"
onClick={handleGenerateResetLink}
disabled={isGeneratingLink || isRootUser || isLoadingTokenStatus}
variant="solid"
color="none"
>
<RefreshCw size={12} />
{isGeneratingLink
? 'Generating...'
: isInvited
? getInviteButtonLabel(
isLoadingTokenStatus,
existingToken,
isTokenExpired,
tokenNotFound,
)
: 'Generate Password Reset Link'}
</Button>
</span>
</Tooltip>
</div>
<div className="edit-member-drawer__footer-right">
<Button variant="solid" color="secondary" size="sm" onClick={handleClose}>
<X size={14} />
Cancel
</Button>
<Button
variant="solid"
color="primary"
size="sm"
disabled={!isDirty || isSaving || isRootUser}
onClick={handleSave}
>
{isSaving ? 'Saving...' : 'Save Member Details'}
</Button>
</div>
</div>
);
@@ -668,14 +669,14 @@ function EditMemberDrawer({
}
}}
direction="right"
type="panel"
showCloseButton
showOverlay={false}
allowOutsideClick
header={{ title: 'Member Details' }}
content={drawerContent}
className="edit-member-drawer"
/>
title="Member Details"
footer={footer}
width="wide"
>
{drawerContent}
</DrawerWrapper>
<ResetLinkDialog
open={showResetLinkDialog}

View File

@@ -1,6 +1,5 @@
import { Button } from '@signozhq/button';
import { DialogWrapper } from '@signozhq/dialog';
import { Check, Copy } from '@signozhq/icons';
import { Button, DialogWrapper } from '@signozhq/ui';
interface ResetLinkDialogProps {
open: boolean;
@@ -49,7 +48,7 @@ function ResetLinkDialog({
color="secondary"
size="sm"
onClick={onCopy}
prefixIcon={hasCopied ? <Check size={12} /> : <Copy size={12} />}
prefix={hasCopied ? <Check size={12} /> : <Copy size={12} />}
className="reset-link-dialog__copy-btn"
>
{hasCopied ? 'Copied!' : 'Copy'}

View File

@@ -20,17 +20,29 @@ import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import EditMemberDrawer, { EditMemberDrawerProps } from '../EditMemberDrawer';
jest.mock('@signozhq/drawer', () => ({
DrawerWrapper: ({
content,
open,
}: {
content?: ReactNode;
open: boolean;
}): JSX.Element | null => (open ? <div>{content}</div> : null),
jest.mock('api/generated/services/users', () => ({
useDeleteUser: jest.fn(),
useGetUser: jest.fn(),
useUpdateUser: jest.fn(),
useUpdateMyUserV2: jest.fn(),
useSetRoleByUserID: jest.fn(),
useGetResetPasswordToken: jest.fn(),
useCreateResetPasswordToken: jest.fn(),
}));
jest.mock('@signozhq/dialog', () => ({
jest.mock('api/ErrorResponseHandlerForGeneratedAPIs', () => ({
convertToApiError: jest.fn(),
}));
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
DrawerWrapper: ({
children,
open,
}: {
children?: ReactNode;
open: boolean;
}): JSX.Element | null => (open ? <div>{children}</div> : null),
DialogWrapper: ({
children,
open,
@@ -48,24 +60,6 @@ jest.mock('@signozhq/dialog', () => ({
DialogFooter: ({ children }: { children?: ReactNode }): JSX.Element => (
<div>{children}</div>
),
}));
jest.mock('api/generated/services/users', () => ({
useDeleteUser: jest.fn(),
useGetUser: jest.fn(),
useUpdateUser: jest.fn(),
useUpdateMyUserV2: jest.fn(),
useSetRoleByUserID: jest.fn(),
useGetResetPasswordToken: jest.fn(),
useCreateResetPasswordToken: jest.fn(),
}));
jest.mock('api/ErrorResponseHandlerForGeneratedAPIs', () => ({
convertToApiError: jest.fn(),
}));
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
toast: {
success: jest.fn(),
error: jest.fn(),

View File

@@ -21,7 +21,7 @@
&__wrap {
background: linear-gradient(
180deg,
color-mix(in srgb, var(--background) 12%, transparent) 0.07%,
color-mix(in srgb, var(--l1-background) 12%, transparent) 0.07%,
color-mix(in srgb, var(--bg-sakura-950) 24%, transparent) 50.04%,
color-mix(in srgb, var(--bg-sakura-800) 36%, transparent) 75.02%,
color-mix(in srgb, var(--bg-sakura-600) 48%, transparent) 87.51%,
@@ -40,15 +40,17 @@
margin: auto;
}
}
&__body {
padding: 0;
background: var(--l2-background);
overflow: hidden;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
&__header {
background: none !important;
.ant-modal-title {
display: flex;
justify-content: space-between;
@@ -80,6 +82,7 @@
pointer-events: none;
}
}
.close-button {
padding: 3px 7px;
background: var(--l2-background);
@@ -90,15 +93,15 @@
box-shadow: none;
}
}
&__footer {
margin: 0 !important;
height: 6px;
background: var(--bg-sakura-500);
}
&__content {
padding: 0 !important;
border-radius: 4px;
overflow: hidden;
background: none !important;
}
}

View File

@@ -5,7 +5,6 @@
&__summary-section {
display: flex;
flex-direction: column;
border-bottom: 1px solid var(--l1-border);
}
&__summary {

View File

@@ -1,7 +0,0 @@
import { HostData } from 'api/infraMonitoring/getHostLists';
export type HostDetailProps = {
host: HostData | null;
isModalTimeSelection: boolean;
onClose: () => void;
};

View File

@@ -1,145 +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(--l1-border);
.filter-section {
flex: 1;
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--l1-border) !important;
background-color: var(--l3-background) !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(--l1-border);
.ant-table-thead > tr > th {
padding: 12px;
font-weight: 500;
font-size: 12px;
line-height: 18px;
background: color-mix(in srgb, var(--bg-robin-200) 1%, transparent);
border-bottom: none;
color: var(--l2-foreground);
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(--l2-background);
}
.ant-table-cell {
padding: 12px;
font-size: 13px;
line-height: 20px;
color: var(--l1-foreground);
background: color-mix(in srgb, var(--bg-robin-200) 1%, transparent);
}
.ant-table-cell:has(.hostname-column-value) {
background: var(--l2-background);
}
.hostname-column-value {
color: var(--l1-foreground);
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: color-mix(in srgb, var(--l1-foreground) 4%, transparent);
}
.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;
}
}
}

View File

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

View File

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

View File

@@ -1,176 +0,0 @@
.host-detail-drawer {
border-left: 1px solid var(--l1-border);
background: var(--l2-background);
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(--l1-border);
background: var(--l2-background);
}
.ant-drawer-close {
margin-inline-end: 0px;
}
.ant-drawer-body {
display: flex;
flex-direction: column;
padding: 16px;
}
.title {
color: var(--l2-foreground);
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(--l1-border);
background: var(--l3-background);
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(--l2-foreground);
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(--l1-border) !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(--l1-border);
background: var(--l3-background);
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(--l2-foreground);
.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(--l1-border);
width: 114px;
}
.tab::before {
background: var(--l1-border);
}
.selected_view {
background: var(--l3-background);
color: var(--l1-foreground);
border: 1px solid var(--l1-border);
}
.selected_view::before {
background: var(--l1-border);
}
}
.compass-button {
width: 30px;
height: 30px;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
}
.ant-drawer-close {
padding: 0px;
}
}

View File

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

View File

@@ -1,119 +0,0 @@
.host-metrics-logs-container {
margin-top: 1rem;
.filter-section {
flex: 1;
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--l1-border) !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(--l1-border);
}
.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(--l1-border);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--l1-border);
}
.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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(--l1-border);
}
.host-metrics-card {
margin: 8px 0 1rem 0;
height: 300px;
padding: 10px;
border: 1px solid var(--l1-border);
.ant-card-body {
padding: 0;
}
.chart-container {
width: 100%;
height: 100%;
}
.no-data-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,6 @@
border-radius: 2px 0px 0px 2px;
.label {
color: var(--l2-foreground);
font-size: 12px;
font-style: normal;
font-weight: 500;
@@ -21,8 +20,9 @@
padding: 0px 8px;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
border: 1px solid var(--l2-border);
background: var(--l2-background);
color: var(--l2-foreground);
display: flex;
justify-content: flex-start;
@@ -37,6 +37,7 @@
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
border-right: none;
border-left: none;
@@ -46,6 +47,7 @@
border-bottom-left-radius: 0px;
font-size: 12px !important;
line-height: 27px;
&::placeholder {
color: var(--l2-foreground) !important;
font-size: 12px !important;
@@ -61,8 +63,8 @@
.close-btn {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
border: 1px solid var(--l2-border);
background: var(--l2-background);
height: 38px;
width: 38px;
}
@@ -71,7 +73,7 @@
.input {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
background: var(--l2-background);
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
}

View File

@@ -1,11 +1,14 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Button } from '@signozhq/button';
import { Callout } from '@signozhq/callout';
import { Style } from '@signozhq/design-tokens';
import { DialogFooter, DialogWrapper } from '@signozhq/dialog';
import { ChevronDown, CircleAlert, Plus, Trash2, X } from '@signozhq/icons';
import { Input } from '@signozhq/input';
import { toast } from '@signozhq/ui';
import {
Button,
Callout,
DialogFooter,
DialogWrapper,
Input,
toast,
} from '@signozhq/ui';
import { Select } from 'antd';
import inviteUsers from 'api/v1/invite/bulk/create';
import sendInvite from 'api/v1/invite/create';
@@ -295,8 +298,9 @@ function InviteMembersModal({
showIcon
icon={<CircleAlert size={12} />}
className="invite-team-members-error-callout"
description={getValidationErrorMessage()}
/>
>
{getValidationErrorMessage()}
</Callout>
)}
</div>
@@ -306,7 +310,7 @@ function InviteMembersModal({
color="secondary"
size="sm"
className="add-another-member-button"
prefixIcon={<Plus size={12} color={Style.L1_FOREGROUND} />}
prefix={<Plus size={12} color={Style.L1_FOREGROUND} />}
onClick={addRow}
>
Add another

View File

@@ -21,7 +21,7 @@
.ant-table-thead {
> tr > th,
> tr > td {
background: var(--background);
background: var(--l1-background);
font-size: var(--paragraph-small-600-font-size);
font-weight: var(--paragraph-small-600-font-weight);
line-height: var(--paragraph-small-600-line-height);

View File

@@ -1,3 +1,5 @@
// TODO: Improve the styling of the query aggregation container and its components. - @YounixM , @H4ad
.query-builder-v2 {
display: flex;
flex-direction: row;
@@ -274,7 +276,7 @@
.ant-input-group-addon {
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
background: var(--l3-background);
background: var(--l2-background);
color: var(--l2-foreground);
font-size: 12px;
font-weight: 300;

View File

@@ -1,3 +1,5 @@
// TODO: Improve the styling of the query aggregation container and its components. - @YounixM , @H4ad
.query-add-ons {
width: 100%;
}
@@ -102,7 +104,7 @@
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
padding: 0px !important;
background-color: var(--card) !important;
background-color: var(--l2-background) !important;
&:focus-within {
border-color: var(--l1-border);
@@ -211,7 +213,7 @@
.cm-line {
line-height: 36px !important;
font-family: 'Space Mono', monospace !important;
background-color: var(--card) !important;
background-color: var(--l2-background) !important;
::-moz-selection {
background: var(--l3-background) !important;
@@ -249,8 +251,8 @@
.close-btn {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
border: 1px solid var(--l2-border);
background: var(--l2-background);
height: 38px;
width: 38px;
@@ -284,108 +286,3 @@
}
}
}
.lightMode {
.add-ons-list {
.add-ons-tabs {
.add-on-tab-title {
color: var(--l1-foreground) !important;
}
.tab {
border: 1px solid var(--l1-border) !important;
background: var(--l1-background) !important;
&:first-child {
border-left: 1px solid var(--l1-border) !important;
}
}
.tab::before {
background: var(--l3-background) !important;
}
.selected-view {
color: var(--primary-background) !important;
border: 1px solid var(--l1-border) !important;
}
.selected-view::before {
background: var(--l3-background) !important;
}
}
.compass-button {
border: 1px solid var(--l1-border) !important;
background: var(--l1-background) !important;
}
}
.having-filter-container {
.having-filter-select-container {
.having-filter-select-editor {
.cm-editor {
&:focus-within {
border-color: var(--l1-border) !important;
}
.cm-content {
border: 1px solid var(--l1-border) !important;
background: var(--l1-background) !important;
&:focus-within {
border-color: var(--l1-border) !important;
}
}
.cm-tooltip-autocomplete {
background: var(--l1-background) !important;
border: 1px solid var(--l1-border) !important;
color: var(--l1-foreground) !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
ul {
li {
color: var(--l1-foreground) !important;
&:hover {
background: var(--l3-background) !important;
}
&[aria-selected='true'] {
background: var(--l3-background) !important;
font-weight: 600 !important;
}
}
}
}
.cm-line {
background-color: var(--l1-background) !important;
::-moz-selection {
background: var(--l1-background) !important;
}
::selection {
background: var(--l3-background) !important;
}
.chip-decorator {
background: var(--l3-background) !important;
color: var(--l1-foreground) !important;
}
}
.cm-selectionBackground {
background: var(--l1-background) !important;
}
}
}
.close-btn {
border: 1px solid var(--l1-border) !important;
background: var(--l1-background) !important;
}
}
}
}

View File

@@ -1,3 +1,5 @@
// TODO: Improve the styling of the query aggregation container and its components. - @YounixM , @H4ad
.query-aggregation-container {
display: block;
@@ -140,7 +142,7 @@
.cm-line {
line-height: 36px !important;
font-family: 'Space Mono', monospace !important;
background-color: var(--l1-background) !important;
background-color: var(--l2-background) !important;
::-moz-selection {
background: var(--l3-background) !important;
@@ -196,6 +198,7 @@
min-width: auto;
}
}
.close-btn {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--l1-border);
@@ -270,7 +273,7 @@
.cm-line {
::-moz-selection {
background: var(--l1-background) !important;
background: var(--l2-background) !important;
opacity: 0.5 !important;
}

View File

@@ -1,3 +1,5 @@
// TODO: Improve the styling of the query aggregation container and its components. - @YounixM , @H4ad
.code-mirror-where-clause {
width: 100%;
display: flex;
@@ -156,7 +158,7 @@
.cm-line {
line-height: 34px !important;
font-family: 'Space Mono', monospace !important;
background-color: var(--l1-background) !important;
background-color: var(--l2-background) !important;
::-moz-selection {
background: var(--l3-background) !important;
@@ -454,30 +456,3 @@
margin-top: -6px !important;
}
}
.lightMode {
.code-mirror-where-clause {
.cm-editor {
.cm-tooltip-autocomplete {
background: var(--l1-background) !important;
border: 1px solid var(--l1-border);
backdrop-filter: blur(20px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
}
.cm-line {
::-moz-selection {
background: var(--bg-robin-200) !important;
}
::selection {
background: var(--bg-robin-200) !important;
}
}
.cm-selectionBackground {
background: var(--bg-robin-200) !important;
}
}
}
}

View File

@@ -158,12 +158,12 @@
mask-image: radial-gradient(
circle at 50% 0,
color-mix(in srgb, var(--background) 10%, transparent) 0,
color-mix(in srgb, var(--l1-background) 10%, transparent) 0,
transparent 100%
);
-webkit-mask-image: radial-gradient(
circle at 50% 0,
color-mix(in srgb, var(--background) 10%, transparent) 0,
color-mix(in srgb, var(--l1-background) 10%, transparent) 0,
transparent 100%
);
}

View File

@@ -11,7 +11,7 @@ import {
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from '@signozhq/combobox';
} from '@signozhq/ui';
import { Skeleton, Switch, Tooltip, Typography } from 'antd';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
@@ -200,7 +200,6 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
setOpen(false);
}}
isSelected={validQueryIndex === option.value}
showCheck={false}
>
{option.label}
</ComboboxItem>

View File

@@ -1,7 +1,5 @@
import { Button } from '@signozhq/button';
import { Callout } from '@signozhq/callout';
import { Check, Copy } from '@signozhq/icons';
import { Badge } from '@signozhq/ui';
import { Badge, Button, Callout } from '@signozhq/ui';
import type { ServiceaccounttypesGettableFactorAPIKeyWithKeyDTO } from 'api/generated/services/sigNoz.schemas';
export interface KeyCreatedPhaseProps {
@@ -43,7 +41,7 @@ function KeyCreatedPhase({
<Callout
type="info"
showIcon
message="Store the key securely. This is the only time it will be displayed."
title="Store the key securely. This is the only time it will be displayed."
/>
</div>
);

View File

@@ -1,8 +1,6 @@
import type { Control, UseFormRegister } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import { Button } from '@signozhq/button';
import { Input } from '@signozhq/input';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
import { Button, Input, ToggleGroup, ToggleGroupItem } from '@signozhq/ui';
import { DatePicker } from 'antd';
import { popupContainer } from 'utils/selectPopupContainer';
@@ -56,7 +54,7 @@ function KeyFormPhase({
<ToggleGroup
type="single"
value={field.value}
onValueChange={(val): void => {
onChange={(val): void => {
if (val) {
field.onChange(val);
}
@@ -112,6 +110,7 @@ function KeyFormPhase({
</Button>
<Button
type="submit"
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
form={FORM_ID}
variant="solid"
color="primary"

View File

@@ -2,8 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useQueryClient } from 'react-query';
import { useCopyToClipboard } from 'react-use';
import { DialogWrapper } from '@signozhq/dialog';
import { toast } from '@signozhq/ui';
import { DialogWrapper, toast } from '@signozhq/ui';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
invalidateListServiceAccountKeys,

View File

@@ -1,8 +1,6 @@
import { useQueryClient } from 'react-query';
import { Button } from '@signozhq/button';
import { DialogFooter, DialogWrapper } from '@signozhq/dialog';
import { Trash2, X } from '@signozhq/icons';
import { toast } from '@signozhq/ui';
import { Button, DialogFooter, DialogWrapper, toast } from '@signozhq/ui';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
getGetServiceAccountQueryKey,

View File

@@ -1,10 +1,13 @@
import type { Control, UseFormRegister } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import { Button } from '@signozhq/button';
import { LockKeyhole, Trash2, X } from '@signozhq/icons';
import { Input } from '@signozhq/input';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
import { Badge } from '@signozhq/ui';
import {
Badge,
Button,
Input,
ToggleGroup,
ToggleGroupItem,
} from '@signozhq/ui';
import { DatePicker } from 'antd';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import { popupContainer } from 'utils/selectPopupContainer';
@@ -72,7 +75,7 @@ function EditKeyForm({
<ToggleGroup
type="single"
value={field.value}
onValueChange={(val): void => {
onChange={(val): void => {
if (val) {
field.onChange(val);
}
@@ -147,6 +150,7 @@ function EditKeyForm({
</Button>
<Button
type="submit"
// @ts-expect-error -- form prop not in @signozhq/ui Button type - TODO: Fix this - @SagarRajput
form={FORM_ID}
variant="solid"
color="primary"

View File

@@ -1,8 +1,7 @@
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useQueryClient } from 'react-query';
import { DialogWrapper } from '@signozhq/dialog';
import { toast } from '@signozhq/ui';
import { DialogWrapper, toast } from '@signozhq/ui';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
invalidateListServiceAccountKeys,

View File

@@ -1,6 +1,6 @@
import { useCallback, useMemo } from 'react';
import { Button } from '@signozhq/button';
import { KeyRound, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui';
import { Skeleton, Table, Tooltip } from 'antd';
import type { ColumnsType } from 'antd/es/table/interface';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
@@ -96,7 +96,7 @@ function buildColumns({
<Tooltip title={isDisabled ? 'Service account disabled' : 'Revoke Key'}>
<Button
variant="ghost"
size="xs"
size="sm"
color="destructive"
disabled={isDisabled}
onClick={(e): void => {

View File

@@ -1,7 +1,6 @@
import { useCallback } from 'react';
import { LockKeyhole } from '@signozhq/icons';
import { Input } from '@signozhq/input';
import { Badge } from '@signozhq/ui';
import { Badge, Input } from '@signozhq/ui';
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import RolesSelect from 'components/RolesSelect';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';

View File

@@ -1,8 +1,6 @@
import { useQueryClient } from 'react-query';
import { Button } from '@signozhq/button';
import { DialogFooter, DialogWrapper } from '@signozhq/dialog';
import { Trash2, X } from '@signozhq/icons';
import { toast } from '@signozhq/ui';
import { Button, DialogFooter, DialogWrapper, toast } from '@signozhq/ui';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
getListServiceAccountKeysQueryKey,

View File

@@ -1,7 +1,7 @@
import { useState } from 'react';
import { Button } from '@signozhq/button';
import { Color } from '@signozhq/design-tokens';
import { ChevronDown, ChevronUp, CircleAlert, RotateCw } from '@signozhq/icons';
import { Button } from '@signozhq/ui';
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import APIError from 'types/api/error';
@@ -42,7 +42,7 @@ function SaveErrorItem({
<Button
type="button"
aria-label="Retry"
size="xs"
size="sm"
onClick={async (e): Promise<void> => {
e.stopPropagation();
setIsRetrying(true);

View File

@@ -5,31 +5,21 @@
margin-left: var(--margin-2);
}
&__layout {
display: flex;
flex-direction: column;
height: calc(100vh - 48px);
}
&__tabs {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--padding-3) var(--padding-4) var(--padding-2) var(--padding-4);
flex-shrink: 0;
}
&__tab-group {
[data-slot='toggle-group'] {
height: 32px;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
gap: 0;
}
[data-slot='toggle-group-item'] {
height: 32px;
border-radius: 0;
border-left: 1px solid var(--l1-border);
background: transparent;
@@ -40,6 +30,7 @@
padding: 0 var(--padding-7);
gap: var(--spacing-3);
box-shadow: none;
border: none;
&:first-child {
border-left: none;
@@ -88,7 +79,7 @@
&__body {
flex: 1;
overflow-y: auto;
padding: var(--padding-5) var(--padding-4);
padding-top: var(--padding-5);
display: flex;
flex-direction: column;
gap: var(--spacing-8);
@@ -112,14 +103,11 @@
}
&__footer {
height: 56px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--padding-4);
border-top: 1px solid var(--secondary);
background: var(--card);
width: 100%;
}
&__keys-pagination {

View File

@@ -1,10 +1,13 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQueryClient } from 'react-query';
import { Button } from '@signozhq/button';
import { DrawerWrapper } from '@signozhq/drawer';
import { Key, LayoutGrid, Plus, Trash2, X } from '@signozhq/icons';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
import { toast } from '@signozhq/ui';
import {
Button,
DrawerWrapper,
toast,
ToggleGroup,
ToggleGroupItem,
} from '@signozhq/ui';
import { Pagination, Skeleton } from 'antd';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
@@ -379,7 +382,7 @@ function ServiceAccountDrawer({
<ToggleGroup
type="single"
value={activeTab}
onValueChange={(val): void => {
onChange={(val): void => {
if (val) {
setActiveTab(val as ServiceAccountDrawerTab);
if (val !== ServiceAccountDrawerTab.Keys) {
@@ -471,69 +474,71 @@ function ServiceAccountDrawer({
</>
)}
</div>
</div>
);
<div className="sa-drawer__footer">
{activeTab === ServiceAccountDrawerTab.Keys ? (
<Pagination
current={keysPage}
pageSize={PAGE_SIZE}
total={keys.length}
showTotal={(total: number, range: number[]): JSX.Element => (
<>
<span className="sa-drawer__pagination-range">
{range[0]} &#8212; {range[1]}
</span>
<span className="sa-drawer__pagination-total"> of {total}</span>
</>
)}
showSizeChanger={false}
hideOnSinglePage
onChange={(page): void => {
void setKeysPage(page);
}}
className="sa-drawer__keys-pagination"
/>
) : (
<>
{!isDeleted && (
const footer = (
<div className="sa-drawer__footer">
{activeTab === ServiceAccountDrawerTab.Keys ? (
<Pagination
current={keysPage}
pageSize={PAGE_SIZE}
total={keys.length}
showTotal={(total: number, range: number[]): JSX.Element => (
<>
<span className="sa-drawer__pagination-range">
{range[0]} &#8212; {range[1]}
</span>
<span className="sa-drawer__pagination-total"> of {total}</span>
</>
)}
showSizeChanger={false}
hideOnSinglePage
onChange={(page): void => {
void setKeysPage(page);
}}
className="sa-drawer__keys-pagination"
/>
) : (
<>
{!isDeleted && (
<Button
variant="ghost"
color="destructive"
className="sa-drawer__footer-btn"
onClick={(): void => {
setIsDeleteOpen(true);
}}
>
<Trash2 size={12} />
Delete Service Account
</Button>
)}
{!isDeleted && (
<div className="sa-drawer__footer-right">
<Button
variant="ghost"
color="destructive"
className="sa-drawer__footer-btn"
onClick={(): void => {
setIsDeleteOpen(true);
}}
variant="solid"
color="secondary"
size="sm"
onClick={handleClose}
>
<Trash2 size={12} />
Delete Service Account
<X size={14} />
Cancel
</Button>
)}
{!isDeleted && (
<div className="sa-drawer__footer-right">
<Button
variant="solid"
color="secondary"
size="sm"
onClick={handleClose}
>
<X size={14} />
Cancel
</Button>
<Button
variant="solid"
color="primary"
size="sm"
loading={isSaving}
disabled={!isDirty}
onClick={handleSave}
>
Save Changes
</Button>
</div>
)}
</>
)}
</div>
<Button
variant="solid"
color="primary"
size="sm"
loading={isSaving}
disabled={!isDirty}
onClick={handleSave}
>
Save Changes
</Button>
</div>
)}
</>
)}
</div>
);
@@ -547,14 +552,15 @@ function ServiceAccountDrawer({
}
}}
direction="right"
type="panel"
showCloseButton
showOverlay={false}
allowOutsideClick
header={{ title: 'Service Account Details' }}
content={drawerContent}
title="Service Account Details"
className="sa-drawer"
/>
width="wide"
footer={footer}
>
{drawerContent}
</DrawerWrapper>
<DeleteAccountModal />

View File

@@ -1,7 +1,13 @@
import { toast } from '@signozhq/ui';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import {
render,
screen,
userEvent,
waitFor,
waitForElementToBeRemoved,
} from 'tests/test-utils';
import AddKeyModal from '../AddKeyModal';
@@ -128,11 +134,9 @@ describe('AddKeyModal', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
await screen.findByRole('dialog', { name: /Add a New Key/i });
const dialog = await screen.findByRole('dialog', { name: /Add a New Key/i });
await user.click(screen.getByRole('button', { name: /Cancel/i }));
expect(
screen.queryByRole('dialog', { name: /Add a New Key/i }),
).not.toBeInTheDocument();
await waitForElementToBeRemoved(dialog);
});
});

View File

@@ -29,9 +29,14 @@ function renderModal(
account: 'sa-1',
'edit-key': 'key-1',
},
onUrlUpdate?: jest.Mock,
): ReturnType<typeof render> {
return render(
<NuqsTestingAdapter searchParams={searchParams} hasMemory>
<NuqsTestingAdapter
searchParams={searchParams}
hasMemory
onUrlUpdate={onUrlUpdate}
>
<EditKeyModal keyItem={keyItem} />
</NuqsTestingAdapter>,
);
@@ -97,14 +102,31 @@ describe('EditKeyModal (URL-controlled)', () => {
it('cancel clears edit-key param and closes modal', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
const onUrlUpdate = jest.fn();
renderModal(mockKey, undefined, onUrlUpdate);
await screen.findByDisplayValue('Original Key Name');
await user.click(screen.getByRole('button', { name: /Cancel/i }));
expect(
screen.queryByRole('dialog', { name: /Edit Key Details/i }),
).not.toBeInTheDocument();
await waitFor(() => {
expect(onUrlUpdate).toHaveBeenCalled();
});
const latestUrlUpdate =
onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]?.[0];
expect(latestUrlUpdate).toEqual(
expect.objectContaining({
queryString: expect.any(String),
}),
);
expect(latestUrlUpdate.queryString).toContain('account=sa-1');
expect(latestUrlUpdate.queryString).not.toContain('edit-key=');
await waitFor(() => {
expect(
screen.queryByRole('dialog', { name: /Edit Key Details/i }),
).not.toBeInTheDocument();
});
});
it('revoke flow: clicking Revoke Key shows confirmation inside same dialog', async () => {

View File

@@ -6,18 +6,15 @@ import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import ServiceAccountDrawer from '../ServiceAccountDrawer';
jest.mock('@signozhq/drawer', () => ({
DrawerWrapper: ({
content,
open,
}: {
content?: ReactNode;
open: boolean;
}): JSX.Element | null => (open ? <div>{content}</div> : null),
}));
jest.mock('@signozhq/ui', () => ({
...jest.requireActual('@signozhq/ui'),
DrawerWrapper: ({
children,
open,
}: {
children?: ReactNode;
open: boolean;
}): JSX.Element | null => (open ? <div>{children}</div> : null),
toast: { success: jest.fn(), error: jest.fn() },
}));

View File

@@ -21,7 +21,7 @@
.ant-table-thead {
> tr > th,
> tr > td {
background: var(--background);
background: var(--l1-background);
font-size: var(--paragraph-small-600-font-size);
font-weight: var(--paragraph-small-600-font-weight);
line-height: var(--paragraph-small-600-line-height);

View File

@@ -11,7 +11,7 @@
gap: 20px;
padding: 8px 12px;
background: var(--background);
background: var(--l1-background);
color: var(--l2-foreground);
border-radius: 8px;

View File

@@ -1,11 +1,11 @@
.warning-content {
display: flex;
flex-direction: column;
// === SECTION: Summary (Top)
&__summary-section {
display: flex;
flex-direction: column;
border-bottom: 1px solid var(--l1-border);
}
&__summary {

View File

@@ -161,10 +161,11 @@ describe('CmdKPalette', () => {
});
test('clicking a navigation item calls history.push with correct route', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CmdKPalette userRole="ADMIN" />);
const homeItem = screen.getByText(HOME_LABEL);
await userEvent.click(homeItem);
await user.click(homeItem);
expect(history.push).toHaveBeenCalledWith(ROUTES.HOME);
});
@@ -194,10 +195,11 @@ describe('CmdKPalette', () => {
});
test('closing the palette via handleInvoke sets open to false', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CmdKPalette userRole="ADMIN" />);
const dashItem = screen.getByText('Go to Dashboards');
await userEvent.click(dashItem);
await user.click(dashItem);
// last call from handleInvoke should set open to false
expect(mockSetOpen).toHaveBeenCalledWith(false);

View File

@@ -41,7 +41,7 @@
.cmdk-item-light:hover {
cursor: pointer;
background-color: var(--background) !important;
background-color: var(--l1-background) !important;
}
.cmdk-item-light[data-selected='true'] {

View File

@@ -7,7 +7,7 @@ import {
CommandItem,
CommandList,
CommandShortcut,
} from '@signozhq/command';
} from '@signozhq/ui';
import logEvent from 'api/common/logEvent';
import { useThemeMode } from 'hooks/useDarkMode';
import history from 'lib/history';

View File

@@ -50,6 +50,7 @@
.app-content {
width: calc(100% - 54px); // width of the sidebar
z-index: 0;
background: var(--l1-background);
margin: 0 auto;
@@ -139,22 +140,6 @@
}
}
.isDarkMode {
.app-layout {
.app-content {
background: var(--background);
}
}
}
.isLightMode {
.app-layout {
.app-content {
background: var(--card);
}
}
}
.trial-expiry-banner,
.slow-api-warning-banner,
.workspace-restricted-banner {

View File

@@ -82,7 +82,7 @@
.sentence-text {
color: var(--l2-foreground);
font-size: 14px;
font-size: 13px;
line-height: 1.5;
display: flex;
align-items: center;

View File

@@ -1,5 +1,5 @@
import { useCallback, useMemo } from 'react';
import { Button } from 'antd';
import { Button, Input } from '@signozhq/ui';
import logEvent from 'api/common/logEvent';
import classNames from 'classnames';
import { QueryParams } from 'constants/query';
@@ -59,15 +59,18 @@ function CreateAlertHeader(): JSX.Element {
<div className="alert-header__tab-bar">
<div className="alert-header__tab">New Alert Rule</div>
<Button
icon={<RotateCcw size={16} />}
prefix={<RotateCcw size={12} />}
onClick={handleSwitchToClassicExperience}
variant="solid"
color="secondary"
size="sm"
>
Switch to Classic Experience
</Button>
</div>
)}
<div className="alert-header__content">
<input
<Input
type="text"
value={alertState.name}
onChange={(e): void =>

View File

@@ -2,6 +2,7 @@
background-color: var(--l1-background);
font-family: inherit;
color: var(--l1-foreground);
padding: 12px 16px;
&__tab-bar {
display: flex;
@@ -14,23 +15,20 @@
display: flex;
align-items: center;
background-color: var(--l1-background);
padding: 0 12px;
height: 32px;
font-size: 13px;
color: var(--l1-foreground);
margin-left: 12px;
margin-top: 12px;
}
&__tab::before {
content: '';
margin-right: 6px;
font-size: 14px;
font-size: 13px;
color: var(--l3-foreground);
}
&__content {
padding: 16px;
padding: 8px 0;
background: var(--l1-background);
display: flex;
flex-direction: column;
@@ -40,22 +38,14 @@
}
&__input.title {
font-size: 18px;
font-weight: 500;
background-color: transparent;
color: var(--l1-foreground);
width: 100%;
min-width: 300px;
}
&__input:focus,
&__input:active {
border: none;
outline: none;
}
&__input.description {
font-size: 14px;
font-size: 13px;
background-color: transparent;
color: var(--l2-foreground);
}

View File

@@ -13,7 +13,7 @@
.advanced-option-item-title {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-size: 13px;
font-weight: 500;
display: flex;
align-items: center;
@@ -23,7 +23,7 @@
.advanced-option-item-description {
color: var(--muted-foreground);
font-family: Inter;
font-size: 14px;
font-size: 13px;
font-weight: 400;
}

View File

@@ -22,7 +22,7 @@
color: var(--l2-foreground);
font-weight: 500;
margin: 0 4px;
font-size: 14px;
font-size: 13px;
}
}
@@ -30,7 +30,7 @@
.ant-btn {
border: 1px solid var(--l1-border);
color: var(--l2-foreground);
font-size: 14px;
font-size: 13px;
display: flex;
align-items: center;
gap: 8px;
@@ -48,7 +48,7 @@
.evaluation-cadence-details-title {
color: var(--l1-foreground);
font-size: 14px;
font-size: 13px;
font-weight: 500;
padding-left: 16px;
padding-top: 16px;
@@ -138,7 +138,7 @@
border-radius: 4px;
color: var(--l2-foreground) !important;
font-family: 'Space Mono';
font-size: 14px;
font-size: 13px;
&::placeholder {
font-family: 'Space Mono';
@@ -202,7 +202,7 @@
gap: 8px;
height: 100%;
color: var(--l1-foreground);
font-size: 14px;
font-size: 13px;
}
.schedule-preview {
@@ -225,7 +225,7 @@
.schedule-preview-title {
color: var(--l2-foreground);
font-size: 14px;
font-size: 13px;
font-weight: 500;
}
}
@@ -281,7 +281,7 @@
.schedule-preview-date {
color: var(--l2-foreground);
font-size: 14px;
font-size: 13px;
font-weight: 400;
white-space: nowrap;
}
@@ -396,7 +396,7 @@
.schedule-preview-title {
color: var(--l1-foreground);
font-size: 14px;
font-size: 13px;
font-weight: 500;
}
}
@@ -422,7 +422,7 @@
.schedule-preview-content {
.schedule-preview-date {
color: var(--l1-foreground);
font-size: 14px;
font-size: 13px;
font-weight: 500;
}

View File

@@ -10,7 +10,7 @@
border: 1px solid var(--l1-border);
color: var(--l1-foreground);
font-family: 'Space Mono', monospace;
font-size: 14px;
font-size: 13px;
font-weight: 600;
text-align: center;
border-radius: 4px;
@@ -43,7 +43,7 @@
.time-input-separator {
color: var(--l2-foreground);
font-size: 14px;
font-size: 13px;
font-weight: 600;
margin: 0 4px;
user-select: none;

View File

@@ -13,7 +13,7 @@
.ant-typography {
color: var(--l2-foreground);
font-size: 14px;
font-size: 13px;
}
.evaluate-alert-conditions-separator {
@@ -175,7 +175,7 @@
background-color: var(--l3-background);
border: 1px solid var(--l1-border);
color: var(--l2-foreground);
font-size: 14px;
font-size: 13px;
}
}
}

View File

@@ -1,6 +1,6 @@
import { useCallback, useMemo } from 'react';
import { toast } from '@signozhq/ui';
import { Button, Tooltip, Typography } from 'antd';
import { Button, toast } from '@signozhq/ui';
import { Tooltip } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { Check, Loader, Send, X } from 'lucide-react';
@@ -147,7 +147,8 @@ function Footer(): JSX.Element {
const saveAlertButton = useMemo(() => {
let button = (
<Button
type="primary"
variant="solid"
color="primary"
onClick={handleSaveAlert}
disabled={disableButtons || Boolean(alertValidationMessage)}
>
@@ -156,7 +157,7 @@ function Footer(): JSX.Element {
) : (
<Check size={14} />
)}
<Typography.Text>Save Alert Rule</Typography.Text>
Save Alert Rule
</Button>
);
if (alertValidationMessage) {
@@ -174,12 +175,13 @@ function Footer(): JSX.Element {
const testAlertButton = useMemo(() => {
let button = (
<Button
type="default"
variant="solid"
color="secondary"
onClick={handleTestNotification}
disabled={disableButtons || Boolean(alertValidationMessage)}
>
{isTestingAlertRule ? <Loader size={14} /> : <Send size={14} />}
<Typography.Text>Test Notification</Typography.Text>
Test Notification
</Button>
);
if (alertValidationMessage) {
@@ -195,7 +197,12 @@ function Footer(): JSX.Element {
return (
<div className="create-alert-v2-footer">
<Button type="default" onClick={handleDiscard} disabled={disableButtons}>
<Button
variant="solid"
color="secondary"
onClick={handleDiscard}
disabled={disableButtons}
>
<X size={14} /> Discard
</Button>
<div className="button-group">

View File

@@ -4,9 +4,8 @@
left: 63px;
right: 0;
background-color: var(--l1-background);
height: 70px;
border-top: 1px solid var(--l1-border);
padding: 16px 24px;
padding: 12px;
z-index: 1000;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
display: flex;

View File

@@ -29,14 +29,14 @@
align-items: center;
color: var(--l1-foreground);
font-family: Inter;
font-size: 14px;
font-size: 13px;
font-weight: 500;
}
.notification-message-header-description {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-size: 13px;
font-weight: 400;
}
}
@@ -59,7 +59,7 @@
border-radius: 4px;
color: var(--l2-foreground) !important;
font-family: Inter;
font-size: 14px;
font-size: 13px;
}
}
@@ -130,7 +130,7 @@
.multiple-notifications-header-title {
color: var(--l1-foreground);
font-family: Inter;
font-size: 14px;
font-size: 13px;
font-weight: 500;
display: flex;
align-items: center;
@@ -140,7 +140,7 @@
.multiple-notifications-header-description {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-size: 13px;
font-weight: 400;
}
}
@@ -179,14 +179,14 @@
.advanced-option-item-title {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-size: 13px;
font-weight: 500;
}
.advanced-option-item-description {
color: var(--muted-foreground);
font-family: Inter;
font-size: 14px;
font-size: 13px;
font-weight: 400;
}
}
@@ -206,7 +206,7 @@
flex-wrap: nowrap;
.ant-typography {
font-size: 14px;
font-size: 13px;
font-weight: 400;
color: var(--l2-foreground);
white-space: nowrap;

View File

@@ -16,8 +16,8 @@
margin-bottom: -1px;
.prom-ql-icon {
height: 14px;
width: 14px;
height: 13px;
width: 13px;
}
.explorer-view-option {

View File

@@ -17,7 +17,7 @@
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
font-size: 13px;
flex-shrink: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

View File

@@ -1,9 +1,7 @@
import { useEffect, useState } from 'react';
import { Button } from '@signozhq/button';
import { Color } from '@signozhq/design-tokens';
import { DialogWrapper } from '@signozhq/dialog';
import { CircleAlert, CircleCheck, LoaderCircle } from '@signozhq/icons';
import { Input } from '@signozhq/input';
import { Button, DialogWrapper, Input } from '@signozhq/ui';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';

View File

@@ -1,6 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { Button } from '@signozhq/button';
import { Callout } from '@signozhq/callout';
import {
Check,
ChevronDown,
@@ -11,7 +9,7 @@ import {
SolidAlertCircle,
X,
} from '@signozhq/icons';
import { toast } from '@signozhq/ui';
import { Button, Callout, toast } from '@signozhq/ui';
import { Dropdown, Skeleton } from 'antd';
import {
RenderErrorResponseDTO,
@@ -44,9 +42,9 @@ function DomainUpdateToast({
<div className="custom-domain-toast-actions">
<Button
variant="ghost"
size="xs"
size="sm"
className="custom-domain-toast-visit-btn"
suffixIcon={<ExternalLink size={12} />}
suffix={<ExternalLink size={12} />}
onClick={(): void => {
window.open(url, '_blank', 'noopener,noreferrer');
}}
@@ -61,7 +59,7 @@ function DomainUpdateToast({
toast.dismiss(toastId);
}}
aria-label="Dismiss"
prefixIcon={<X size={14} />}
prefix={<X size={14} />}
/>
</div>
</div>
@@ -246,7 +244,7 @@ export default function CustomDomainSettings(): JSX.Element {
>
<Button
type="button"
size="xs"
size="sm"
className="workspace-url-trigger"
disabled={isFetchingHosts}
>
@@ -266,7 +264,7 @@ export default function CustomDomainSettings(): JSX.Element {
variant="solid"
size="sm"
className="custom-domain-edit-button"
prefixIcon={<FilePenLine size={12} />}
prefix={<FilePenLine size={12} />}
disabled={isFetchingHosts || isPollingEnabled}
onClick={(): void => setIsEditModalOpen(true)}
>
@@ -281,7 +279,7 @@ export default function CustomDomainSettings(): JSX.Element {
className="custom-domain-callout"
size="small"
icon={<SolidAlertCircle size={13} color="primary" />}
message={`Updating your URL to ⎯ ${customDomainSubdomain}.${dnsSuffix}. This may take a few mins.`}
title={`Updating your URL to ⎯ ${customDomainSubdomain}.${dnsSuffix}. This may take a few mins.`}
/>
)}

View File

@@ -1,8 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useMutation } from 'react-query';
import { useCopyToClipboard } from 'react-use';
import { Checkbox } from '@signozhq/checkbox';
import { toast } from '@signozhq/ui';
import { Checkbox, toast } from '@signozhq/ui';
import { Button, Select, Typography } from 'antd';
import createPublicDashboardAPI from 'api/dashboard/public/createPublicDashboard';
import revokePublicDashboardAccessAPI from 'api/dashboard/public/revokePublicDashboardAccess';
@@ -247,10 +246,11 @@ function PublicDashboardSetting(): JSX.Element {
<div className="timerange-enabled-checkbox">
<Checkbox
id="enable-time-range"
checked={timeRangeEnabled}
onCheckedChange={handleTimeRangeEnabled}
labelName="Enable time range"
/>
value={timeRangeEnabled}
onChange={handleTimeRangeEnabled}
>
Enable time range
</Checkbox>
</div>
<div className="default-time-range-select">

View File

@@ -1,5 +1,5 @@
import { Button } from '@signozhq/button';
import { ArrowLeft, Mail } from '@signozhq/icons';
import { Button } from '@signozhq/ui';
interface SuccessScreenProps {
onBackToLogin: () => void;
@@ -28,7 +28,7 @@ function SuccessScreen({ onBackToLogin }: SuccessScreenProps): JSX.Element {
data-testid="back-to-login"
className="login-submit-btn"
onClick={onBackToLogin}
prefixIcon={<ArrowLeft size={12} />}
prefix={<ArrowLeft size={12} />}
>
Back to login
</Button>

View File

@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo } from 'react';
import { Button } from '@signozhq/button';
import { ArrowLeft, ArrowRight } from '@signozhq/icons';
import { Input } from '@signozhq/input';
import { Button, Input } from '@signozhq/ui';
import { Form, Select } from 'antd';
import { ErrorResponseHandlerForGeneratedAPIs } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useForgotPassword } from 'api/generated/services/users';
@@ -191,7 +190,7 @@ function ForgotPassword({
data-testid="forgot-password-back"
className="forgot-password-back-button"
onClick={handleBackToLogin}
prefixIcon={<ArrowLeft size={12} />}
prefix={<ArrowLeft size={12} />}
>
Back to login
</Button>
@@ -204,7 +203,7 @@ function ForgotPassword({
type="submit"
data-testid="forgot-password-submit"
className="login-submit-btn"
suffixIcon={<ArrowRight size={12} />}
suffix={<ArrowRight size={12} />}
>
{isLoading ? 'Sending...' : 'Send reset link'}
</Button>

View File

@@ -3,8 +3,8 @@ import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
import { useInterval } from 'react-use';
import { LoadingOutlined } from '@ant-design/icons';
import { Button } from '@signozhq/button';
import { Compass, ScrollText } from '@signozhq/icons';
import { Button } from '@signozhq/ui';
import { Modal, Spin } from 'antd';
import setRetentionApi from 'api/settings/setRetention';
import setRetentionApiV2 from 'api/settings/setRetentionV2';

View File

@@ -1,7 +1,6 @@
import { useCopyToClipboard } from 'react-use';
import { Button } from '@signozhq/button';
import { Copy, KeyRound } from '@signozhq/icons';
import { toast } from '@signozhq/ui';
import { Button, toast } from '@signozhq/ui';
import { useAppContext } from 'providers/App/App';
import { getMaskedKey } from 'utils/maskedKey';
@@ -32,7 +31,7 @@ function LicenseKeyRow(): JSX.Element | null {
</code>
<Button
type="button"
size="xs"
size="sm"
aria-label="Copy license key"
data-testid="license-key-row-copy-btn"
className="license-key-row__copy-btn"

View File

@@ -1,6 +1,6 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { Callout } from '@signozhq/callout';
import { Callout } from '@signozhq/ui';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import { FeatureKeys } from 'constants/features';
@@ -44,39 +44,38 @@ function LicenseRowDismissibleCallout(): JSX.Element | null {
type="info"
size="small"
showIcon
dismissable
onClose={handleDismissCallout}
action="dismissible"
onClick={handleDismissCallout}
className="license-key-callout"
description={
<div className="license-key-callout__description">
This is <strong>NOT</strong> your ingestion or Service account key.
{(hasServiceAccountsAccess || hasIngestionAccess) && (
<>
{' '}
Find your{' '}
{hasServiceAccountsAccess && (
<Link
to={ROUTES.SERVICE_ACCOUNTS_SETTINGS}
className="license-key-callout__link"
>
Service account here
</Link>
)}
{hasServiceAccountsAccess && hasIngestionAccess && ' and '}
{hasIngestionAccess && (
<Link
to={ROUTES.INGESTION_SETTINGS}
className="license-key-callout__link"
>
Ingestion key here
</Link>
)}
.
</>
)}
</div>
}
/>
>
<div className="license-key-callout__description">
This is <strong>NOT</strong> your ingestion or Service account key.
{(hasServiceAccountsAccess || hasIngestionAccess) && (
<>
{' '}
Find your{' '}
{hasServiceAccountsAccess && (
<Link
to={ROUTES.SERVICE_ACCOUNTS_SETTINGS}
className="license-key-callout__link"
>
Service account here
</Link>
)}
{hasServiceAccountsAccess && hasIngestionAccess && ' and '}
{hasIngestionAccess && (
<Link
to={ROUTES.INGESTION_SETTINGS}
className="license-key-callout__link"
>
Ingestion key here
</Link>
)}
.
</>
)}
</div>
</Callout>
) : null;
}

View File

@@ -7,7 +7,7 @@ import {
useRef,
useState,
} from 'react';
import { Input as SignozInput } from '@signozhq/input';
import { Input as SignozInput } from '@signozhq/ui';
import { Col, Row, Select } from 'antd';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { find } from 'lodash-es';

View File

@@ -68,8 +68,8 @@
gap: 4px;
border-radius: 4px 0px 0px 0px;
background: var(--bg-sakura-500);
color: var(--l1-foreground);
backdrop-filter: blur(6px);
color: var(--foreground);
font-family: 'Space Mono';
font-size: 12px;
font-style: normal;

View File

@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import { Button } from '@signozhq/button';
import { Button } from '@signozhq/ui';
import { Skeleton } from 'antd';
import logEvent from 'api/common/logEvent';
import { useGetHosts } from 'api/generated/services/zeus';
@@ -81,15 +81,9 @@ function DataSourceInfo({
color="primary"
size="sm"
className="periscope-btn primary"
prefixIcon={<img src={containerPlusUrl} alt="plus" />}
prefix={<img src={containerPlusUrl} alt="plus" />}
onClick={handleConnect}
role="button"
tabIndex={0}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
handleConnect();
}
}}
// TODO - Support tabindex, keyboard events - @H4ad
>
Connect Data Source
</Button>

View File

@@ -6,7 +6,7 @@
height: 100%;
width: 100%;
background: color-mix(in srgb, var(--background) 90%, transparent);
background: color-mix(in srgb, var(--l1-background) 90%, transparent);
&::-webkit-scrollbar {
width: 0.3rem;
@@ -759,12 +759,11 @@
}
.welcome-checklist-popover {
padding: 1rem 1.5rem;
background-color: var(--l2-background);
padding: 12px 24px;
border-radius: 2px;
border: 1px solid var(--l2-border);
background: var(--l2-background);
color: var(--l1-foreground);
color: var(--l2-foreground);
.ant-popover-content {
border: none;
@@ -777,7 +776,7 @@
.home-checklist-container {
background-color: transparent !important;
width: 400px;
width: 420px;
}
}

View File

@@ -3,8 +3,8 @@ import React, { useCallback, useEffect, useState } from 'react';
import { useMutation, useQuery } from 'react-query';
import { Color } from '@signozhq/design-tokens';
import { Compass, Dot, House, Plus, Wrench } from '@signozhq/icons';
import { PersistedAnnouncementBanner } from '@signozhq/ui';
import { Button, Popover } from 'antd';
import { Button, PersistedAnnouncementBanner } from '@signozhq/ui';
import { Popover } from 'antd';
import logEvent from 'api/common/logEvent';
import { useGetMetricsOnboardingStatus } from 'api/generated/services/metrics';
import listUserPreferences from 'api/v1/user/preferences/list';
@@ -317,8 +317,9 @@ export default function Home(): JSX.Element {
rootClassName="welcome-checklist-popover"
>
<Button
type="default"
size="small"
variant="solid"
color="secondary"
size="sm"
className="periscope-btn secondary welcome-checklist-btn"
>
<img
@@ -511,9 +512,10 @@ export default function Home(): JSX.Element {
<div className="section-actions">
<Button
type="default"
variant="solid"
color="secondary"
className="periscope-btn secondary"
icon={<Wrench size={14} />}
prefix={<Wrench size={14} />}
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Logs',
@@ -527,9 +529,10 @@ export default function Home(): JSX.Element {
</Button>
<Button
type="default"
variant="solid"
color="secondary"
className="periscope-btn secondary"
icon={<Wrench size={14} />}
prefix={<Wrench size={14} />}
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Traces',
@@ -543,9 +546,10 @@ export default function Home(): JSX.Element {
</Button>
<Button
type="default"
variant="solid"
color="secondary"
className="periscope-btn secondary"
icon={<Wrench size={14} />}
prefix={<Wrench size={14} />}
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Metrics',
@@ -581,9 +585,10 @@ export default function Home(): JSX.Element {
<div className="section-actions">
<Button
type="default"
variant="solid"
color="secondary"
className="periscope-btn secondary"
icon={<Plus size={14} />}
prefix={<Plus size={14} />}
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Dashboards',
@@ -625,9 +630,10 @@ export default function Home(): JSX.Element {
<div className="section-actions">
<Button
type="default"
variant="solid"
color="secondary"
className="periscope-btn secondary"
icon={<Plus size={14} />}
prefix={<Plus size={14} />}
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Alerts',
@@ -706,7 +712,8 @@ export default function Home(): JSX.Element {
<Card.Footer>
<div className="checklist-footer-container">
<Button
type="link"
variant="link"
color="secondary"
onClick={handleWillDoThisLater}
loading={updatingUserPreferences}
>

View File

@@ -130,7 +130,7 @@
height: 18px;
border-radius: 50%;
background-color: var(--l2-background);
border: 1px dashed var(--l1-border);
border: 1px dashed var(--l2-border);
color: var(--l2-background);
display: flex;
align-items: center;

View File

@@ -0,0 +1,205 @@
import { useCallback, useEffect, useMemo, useRef, 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,
useInfraMonitoringFilters,
} from 'container/InfraMonitoringK8s/hooks';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { Filter } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import {
fetchHostEntityData,
fetchHostListData,
getHostMetricsQueryPayload,
hostDetailsMetadataConfig,
hostGetEntityName,
hostGetSelectedItemFilters,
hostInitialEventsFilter,
hostInitialLogTracesFilter,
hostWidgetInfo,
} from './constants';
import {
hostColumns,
hostColumnsConfig,
hostRenderRowData,
} from './table.config';
import { getHostsQuickFiltersConfig } from './utils';
import styles from './InfraMonitoringHosts.module.scss';
function Hosts(): JSX.Element {
const [showFilters, setShowFilters] = useState(true);
const [, setCurrentPage] = useInfraMonitoringCurrentPage();
const [urlFilters, setUrlFilters] = useInfraMonitoringFilters();
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: '',
});
// Track previous urlFilters to only sync when the value actually changes
// (not when handleChangeQueryData changes due to query updates)
const prevUrlFiltersRef = useRef<string | null>(null);
useEffect(() => {
const currentFiltersJson = urlFilters ? JSON.stringify(urlFilters) : null;
// Only sync if urlFilters value has actually changed
if (prevUrlFiltersRef.current !== currentFiltersJson) {
prevUrlFiltersRef.current = currentFiltersJson;
// Sync filters to query builder, using empty filter when urlFilters is null
handleChangeQueryData('filters', urlFilters || { items: [], op: 'and' });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [urlFilters]); // handleChangeQueryData intentionally omitted - we call the current version but don't re-run when it changes
const handleFilterVisibilityChange = (): void => {
setShowFilters(!showFilters);
};
const handleQuickFiltersChange = (query: Query): void => {
const filters = query.builder.queryData[0].filters;
// Nuqs batches these calls into a single URL update
// The useEffect will sync filters to query builder
setUrlFilters(filters || null);
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={styles.quickFiltersToggleContainer}>
<Button
className="periscope-btn ghost"
type="text"
size="small"
onClick={handleFilterVisibilityChange}
>
<Filter size={14} />
</Button>
</div>
) : undefined;
return (
<>
<div className={styles.infraMonitoringContainer}>
<div className={styles.infraContentRow}>
{showFilters && (
<div className={styles.quickFiltersContainer}>
<div className={styles.quickFiltersContainerHeader}>
<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={`${styles.listContainer}${
showFilters ? ` ${styles.listContainerFiltersVisible}` : ''
}`}
>
<K8sBaseList
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.HOSTS}
tableColumnsDefinitions={hostColumns}
tableColumns={hostColumnsConfig}
fetchListData={fetchListData}
renderRowData={hostRenderRowData}
eventCategory={InfraMonitoringEvents.HostEntity}
/>
</div>
</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,
}}
/>
</>
);
}
export default Hosts;

View File

@@ -1,54 +0,0 @@
import { Typography } from 'antd';
import eyesEmojiUrl from '@/assets/Images/eyesEmoji.svg';
export default function HostsEmptyOrIncorrectMetrics({
noData,
incorrectData,
}: {
noData: boolean;
incorrectData: boolean;
}): JSX.Element {
return (
<div className="hosts-empty-state-container">
<div className="hosts-empty-state-container-content">
<img className="eyes-emoji" src={eyesEmojiUrl} alt="eyes emoji" />
{noData && (
<div className="no-hosts-message">
<Typography.Title level={5} className="no-hosts-message-title">
No host metrics data received yet.
</Typography.Title>
<Typography.Text className="no-hosts-message-text">
Infrastructure monitoring requires the{' '}
<a
href="https://github.com/open-telemetry/semantic-conventions/blob/main/docs/system/system-metrics.md"
target="_blank"
rel="noreferrer"
>
OpenTelemetry system metrics
</a>
. Please refer to{' '}
<a
href="https://signoz.io/docs/userguide/hostmetrics"
target="_blank"
rel="noreferrer"
>
this
</a>{' '}
to learn how to send host metrics to SigNoz.
</Typography.Text>
</div>
)}
{incorrectData && (
<Typography.Text className="incorrect-metrics-message">
To see host metrics, upgrade to the latest version of SigNoz k8s-infra
chart. Please contact support if you need help.
</Typography.Text>
)}
</div>
</div>
);
}

View File

@@ -1,253 +0,0 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { VerticalAlignTopOutlined } from '@ant-design/icons';
import { Button, Tooltip, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import {
getHostLists,
HostListPayload,
HostListResponse,
} 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 { FeatureKeys } from 'constants/features';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
useInfraMonitoringCurrentPage,
useInfraMonitoringFiltersHosts,
useInfraMonitoringOrderByHosts,
} from 'container/InfraMonitoringK8s/hooks';
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
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 { useGlobalTimeStore } from 'store/globalTime';
import {
getAutoRefreshQueryKey,
NANO_SECOND_MULTIPLIER,
} from 'store/globalTime/utils';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
IBuilderQuery,
Query,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import HostsListControls from './HostsListControls';
import HostsListTable from './HostsListTable';
import { getHostListsQuery, GetHostsQuickFiltersConfig } from './utils';
import './InfraMonitoring.styles.scss';
const defaultFilters: TagFilter = { items: [], op: 'and' };
const baseQuery = getHostListsQuery();
function HostsList(): JSX.Element {
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 selectedTime = useGlobalTimeStore((s) => s.selectedTime);
const isRefreshEnabled = useGlobalTimeStore((s) => s.isRefreshEnabled);
const refreshInterval = useGlobalTimeStore((s) => s.refreshInterval);
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
const queryKey = useMemo(
() =>
getAutoRefreshQueryKey(
selectedTime,
REACT_QUERY_KEY.GET_HOST_LIST,
String(pageSize),
String(currentPage),
JSON.stringify(filters),
JSON.stringify(orderBy),
),
[pageSize, currentPage, filters, orderBy, selectedTime],
);
const { data, isFetching, isLoading, isError } = useQuery<
SuccessResponse<HostListResponse> | ErrorResponse,
Error
>({
queryKey,
queryFn: ({ signal }) => {
const { minTime, maxTime } = getMinMaxTime();
const payload: HostListPayload = {
...baseQuery,
limit: pageSize,
offset: (currentPage - 1) * pageSize,
filters: filters ?? defaultFilters,
orderBy,
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
};
return getHostLists(payload, signal);
},
enabled: true,
keepPreviousData: true,
refetchInterval: isRefreshEnabled ? refreshInterval : false,
});
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 ?? defaultFilters}
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;

View File

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

View File

@@ -1,271 +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 ErrorContent from 'components/ErrorModal/components/ErrorContent';
import { InfraMonitoringEvents } from 'constants/events';
import { isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';
import emptyStateUrl from '@/assets/Icons/emptyState.svg';
import eyesEmojiUrl from '@/assets/Images/eyesEmoji.svg';
import HostsEmptyOrIncorrectMetrics from './HostsEmptyOrIncorrectMetrics';
import {
EmptyOrLoadingViewProps,
formatDataForTable,
getHostsListColumns,
HostRowData,
HostsListTableProps,
} from './utils';
function EmptyOrLoadingView(
viewState: EmptyOrLoadingViewProps,
): React.ReactNode {
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>
);
}
const { isError, data } = viewState;
if (isError || data?.error || (data?.statusCode || 0) >= 300) {
return (
<ErrorContent
error={{
code: data?.statusCode || 500,
message: data?.error || 'Something went wrong',
}}
/>
);
}
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={eyesEmojiUrl} 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={emptyStateUrl}
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>
);
}
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 &&
!endTimeBeforeRetention;
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,
data,
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',
})}
/>
);
}

View File

@@ -1,416 +0,0 @@
.infra-monitoring-container {
display: flex;
height: 100%;
flex-direction: column;
.infra-monitoring-header {
display: flex;
justify-content: space-between;
width: 100%;
margin-bottom: 16px;
}
.hosts-list-controls {
flex: 1;
padding: 8px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--l1-border) !important;
background-color: var(--l3-background) !important;
input {
font-size: 12px;
}
.ant-tag .ant-typography {
font-size: 12px;
}
}
.hosts-list-controls-left {
flex: 1;
}
}
.progress-container {
display: flex;
align-items: center;
}
.progress-bar {
flex: 1;
margin-right: 8px;
}
.clickable-row {
cursor: pointer;
}
.hosts-list-content {
display: flex;
flex-direction: row;
align-items: stretch;
height: calc(100vh - 110px);
.hosts-quick-filters-container {
width: 280px;
min-width: 280px;
border-right: 1px solid var(--l1-border);
height: 100%;
.hosts-quick-filters-container-header {
padding: 8px;
border-bottom: 1px solid var(--l1-border);
display: flex;
align-items: center;
justify-content: space-between;
}
}
}
.hosts-list-table-container {
flex: 1;
.hosts-list-table-header {
display: flex;
justify-content: space-between;
align-items: center;
.quick-filters-toggle-container {
padding: 0 8px;
}
}
}
.hosts-list-table {
.ant-table {
.ant-table-thead > tr > th {
padding: 12px;
font-weight: 500;
font-size: 12px;
line-height: 18px;
background: var(--l1-background);
border-bottom: none;
color: var(--muted-foreground);
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(--l2-background);
}
.ant-table-cell {
padding: 12px;
font-size: 13px;
line-height: 20px;
color: var(--l1-foreground);
background: var(--l1-background);
}
.ant-table-cell:has(.hostname-column-value),
.ant-table-cell:has(.hostname-cell-missing) {
background: var(--l2-background);
}
.hostname-column-value {
color: var(--l1-foreground);
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(--muted-foreground);
}
.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: color-mix(in srgb, var(--l1-foreground) 4%, transparent);
}
.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(--l1-background);
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(--primary-background);
border-color: var(--primary-background);
a {
color: var(--l1-foreground) !important;
}
}
}
}
}
}
.infra-monitoring-tags {
width: fit-content;
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
border-radius: 50px;
padding: 2px 8px;
&.active {
color: var(--bg-forest-500);
border: 1px solid color-mix(in srgb, var(--bg-forest-500) 20%, transparent);
background: color-mix(in srgb, var(--bg-forest-500) 10%, transparent);
}
&.inactive {
color: var(--l3-foreground);
border: 1px solid color-mix(in srgb, var(--bg-slate-50) 20%, transparent);
background: color-mix(in srgb, var(--bg-slate-50) 10%, transparent);
}
}
.hosts-list-loading-state {
padding: 8px;
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
.hosts-list-loading-state-item {
height: 48px;
width: 100%;
}
}
.no-filtered-hosts-message-container {
flex: 1;
height: 30vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.no-filtered-hosts-message-content {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
width: fit-content;
padding: 24px;
}
.no-filtered-hosts-message {
margin-top: 8px;
}
}
.hosts-empty-state-container {
padding: 16px;
height: 40vh;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.hosts-empty-state-container-content {
padding: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
width: fit-content;
.no-hosts-message {
margin-bottom: 16px;
.no-hosts-message-title {
margin-top: 8px;
margin-bottom: 4px;
}
}
}
}
.lightMode {
.infra-monitoring-container {
.ant-table-thead > tr > th {
background: var(--l1-background);
color: var(--l1-foreground);
}
.ant-table-cell {
color: var(--l1-foreground);
}
.hosts-list-controls {
border-top: 1px solid var(--l1-border);
border-bottom: 1px solid var(--l1-border);
.ant-select-selector {
border-color: var(--l1-border) !important;
background-color: var(--l1-background) !important;
color: var(--l2-foreground);
}
}
}
.hosts-list-table {
.ant-table {
.ant-table-thead > tr > th {
background: var(--l1-background);
color: var(--l2-foreground);
}
.ant-table-thead > tr > th:has(.hostname-column-header) {
background: var(--l1-background);
}
.ant-table-cell {
background: var(--l1-background);
color: var(--l1-foreground);
}
.ant-table-cell:has(.hostname-column-value),
.ant-table-cell:has(.hostname-cell-missing) {
background: var(--l1-background);
}
.hostname-column-value {
color: var(--l1-foreground);
}
.hostname-cell-placeholder {
color: var(--l2-foreground);
}
.ant-table-tbody > tr:hover > td {
background: rgba(0, 0, 0, 0.04);
}
}
.ant-pagination {
background: var(--l1-background);
.ant-pagination-item {
&-active {
background: var(--primary-background);
border-color: var(--primary-background);
a {
color: var(--l1-background) !important;
}
}
}
}
}
}

View File

@@ -0,0 +1,175 @@
.infraMonitoringContainer {
display: flex;
height: 100%;
flex-direction: column;
}
.infraContentRow {
display: flex;
flex-direction: row;
height: calc(100vh - 45px);
width: 100%;
position: relative;
overflow-y: auto;
:global(.periscope-btn) {
&.ghost:not(:disabled) {
border: none;
background: transparent;
&:hover {
background: transparent;
color: var(--bg-robin-500) !important;
font-weight: 500;
}
}
}
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--accent-primary);
}
&::-webkit-scrollbar-thumb:hover {
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
}
}
.quickFiltersContainer {
width: 280px;
min-width: 280px;
border-right: 1px solid var(--l1-border);
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--accent-primary);
}
&::-webkit-scrollbar-thumb:hover {
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
}
:global(.ant-collapse-header) {
border-bottom: 1px solid var(--l1-border);
padding: 12px 8px;
}
:global(.ant-collapse-content-box) {
padding: 0 !important;
padding-block: 0 !important;
:global(.quick-filters .checkbox-filter) {
padding-left: 18px;
}
}
:global(.quick-filters) {
overflow-y: auto;
overflow-x: hidden;
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--accent-primary);
}
&::-webkit-scrollbar-thumb:hover {
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
}
}
}
.quickFiltersContainerHeader {
padding: 8px;
border-bottom: 1px solid var(--l1-border);
display: flex;
align-items: center;
justify-content: space-between;
}
.listContainer {
flex: 1;
min-width: 0;
max-width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
> * {
min-width: 0;
max-width: 100%;
box-sizing: border-box;
}
> :global(.ant-table-wrapper) {
width: 100%;
max-width: 100%;
:global(.ant-spin-container) {
display: flex !important;
flex-direction: column;
}
:global(.ant-table),
:global(.ant-table-container) {
max-width: 100%;
}
}
}
.listContainerFiltersVisible {
max-width: calc(100% - 280px);
}
.quickFiltersToggleContainer {
padding: 0 8px;
}
.infraMonitoringTags {
width: fit-content;
font-family: Inter;
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;
}
.tagsActive {
color: var(--bg-forest-500, #25e192);
border: 1px solid rgba(37, 225, 146, 0.2);
background: rgba(37, 225, 146, 0.1);
}
.tagsInactive {
color: var(--bg-slate-50, #62687c);
border: 1px solid rgba(98, 104, 124, 0.2);
background: rgba(98, 104, 124, 0.1);
}

Some files were not shown because too many files have changed in this diff Show More