Compare commits

..

7 Commits

Author SHA1 Message Date
Gaurav Tewari
5d661217e9 test: merge feat/gcp-cloud-integration + fix/gcp-list-accounts for QA 2026-07-22 10:19:31 +05:30
Gaurav Tewari
4f3fe7ec6e fix(gcp): treat GCP as a one-click integration for connection status
isOneClickIntegration only listed AWS and Azure, so IntegrationDetailPage
treated GCP as a legacy integration and polled
GET /integrations/gcp/connection_status every 5s via useGetIntegrationStatus.
GCP is a one-click cloud integration like AWS/Azure and has no such legacy
status endpoint, so add it to the allowlist to disable the poll.
2026-07-22 09:53:35 +05:30
Gaurav Tewari
4f45b3a691 feat(gcp): add account settings (edit) drawer
Adds the GCP edit-account drawer for managing monitored project ids
and wires it into AccountActions so 'Edit Account' works for GCP.
2026-07-22 09:17:28 +05:30
Gaurav Tewari
dc4b4c3d03 feat(gcp): add cloud account setup (add-account) drawer
Adds the GCP add-account drawer (flow selector, connection secret
fields with shared CopyButton, validators) and wires it into
AccountActions so 'Integrate Now' / 'Add New Account' work for GCP.

Extends the shared periscope CopyButton with an optional onCopy
callback so callers can show a toast on copy.
2026-07-22 09:17:01 +05:30
Gaurav Tewari
c5f550d88f feat(gcp): add GCP integration config, types, and page wiring
Adds the GCP integration tile, config types, region constants, account
DTO mapping, and routes the 'gcp' integration id to the shared
CloudIntegration page. Accounts list, service details, and account
removal work for GCP; the add-account and edit-account drawers land in
follow-up commits (their buttons are no-ops for GCP in this commit).

Also narrows Azure config checks by 'resource_groups' instead of
'deployment_region', since the GCP config in the widened
CloudAccount.config union also has a deployment_region field.
2026-07-22 09:16:30 +05:30
swapnil-signoz
f5f8da4226 refactor: updating cloudintegration integration tests 2026-07-20 22:45:58 +05:30
swapnil-signoz
e8fae56354 fix: list gcp accounts was missing config 2026-07-20 20:07:21 +05:30
316 changed files with 5448 additions and 14101 deletions

View File

@@ -58,7 +58,6 @@ jobs:
- role
- rootuser
- serviceaccount
- spanmapper
- querier_json_body
- querier_skip_resource_fingerprint
- ttl

View File

@@ -9,11 +9,6 @@ global:
# the path component (e.g. /signoz in https://example.com/signoz) is used
# as the base path for all HTTP routes (both API and web frontend).
external_url: <unset>
# origins (scheme://host[:port], no path) allowed as login redirect targets. include
# the origin the signoz ui is served on. when not configured, redirect targets are
# not validated.
# allowed_origins:
# - https://signoz.example.com
# the url where the SigNoz backend receives telemetry data (traces, metrics, logs) from instrumented applications.
ingestion_url: <unset>
# the url of the SigNoz MCP server. when unset, the MCP settings page is hidden in the frontend.

View File

@@ -1495,8 +1495,6 @@ components:
- cassandradb
- redis
- cloudsql_postgres
- memorystore_redis
- computeengine
type: string
CloudintegrationtypesServiceMetadata:
properties:
@@ -8086,19 +8084,6 @@ components:
required:
- items
type: object
SpantypesGettableSpanMapperTest:
properties:
collectorLogs:
items:
type: string
nullable: true
type: array
spans:
items:
$ref: '#/components/schemas/SpantypesSpanMapperTestSpan'
nullable: true
type: array
type: object
SpantypesGettableSpanMappers:
properties:
items:
@@ -8195,39 +8180,6 @@ components:
- name
- condition
type: object
SpantypesPostableSpanMapperTest:
properties:
groups:
items:
$ref: '#/components/schemas/SpantypesPostableSpanMapperTestGroup'
nullable: true
type: array
spans:
items:
$ref: '#/components/schemas/SpantypesSpanMapperTestSpan'
nullable: true
type: array
required:
- spans
- groups
type: object
SpantypesPostableSpanMapperTestGroup:
properties:
condition:
$ref: '#/components/schemas/SpantypesSpanMapperGroupCondition'
enabled:
type: boolean
mappers:
items:
$ref: '#/components/schemas/SpantypesPostableSpanMapper'
nullable: true
type: array
name:
type: string
required:
- name
- condition
type: object
SpantypesPostableTraceAggregations:
properties:
aggregations:
@@ -8389,17 +8341,6 @@ components:
- operation
- priority
type: object
SpantypesSpanMapperTestSpan:
properties:
attributes:
additionalProperties: {}
nullable: true
type: object
resource:
additionalProperties: {}
nullable: true
type: object
type: object
SpantypesUpdatableSpanMapper:
properties:
config:
@@ -8617,7 +8558,6 @@ components:
- resource
- attribute
- body
- ""
type: string
TelemetrytypesFieldDataType:
enum:
@@ -8626,7 +8566,6 @@ components:
- float64
- int64
- number
- ""
type: string
TelemetrytypesGettableFieldKeys:
properties:
@@ -8658,7 +8597,6 @@ components:
- traces
- logs
- metrics
- ""
type: string
TelemetrytypesSource:
enum:
@@ -14133,69 +14071,6 @@ paths:
summary: Update a span mapper
tags:
- spanmapper
/api/v1/span_mapper_groups/test:
post:
deprecated: false
description: Tests how span mappers would transform sample spans
operationId: TestSpanMappers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SpantypesPostableSpanMapperTest'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/SpantypesGettableSpanMapperTest'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Test span mappers against sample spans
tags:
- spanmapper
/api/v1/stats:
get:
deprecated: false

View File

@@ -223,12 +223,17 @@ func (provider *provider) Update(ctx context.Context, orgID valuer.UUID, updated
return err
}
desiredTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, updatedRole.TransactionGroups)
existingGroups := authtypes.MustNewTransactionGroupsFromTuples(existingTuples)
additions, deletions := existingGroups.Diff(updatedRole.TransactionGroups)
additionTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, additions)
if err != nil {
return err
}
additionTuples, deletionTuples := authtypes.DiffTuples(existingTuples, desiredTuples)
deletionTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, deletions)
if err != nil {
return err
}
err = provider.Write(ctx, additionTuples, deletionTuples)
if err != nil {

View File

@@ -240,17 +240,17 @@ func TestManager_TestNotification_SendUnmatched_PromRule(t *testing.T) {
mock := mockStore.Mock()
// Mock the fingerprint query (for Prometheus label matching)
// args: $1=metric_name (the __name__ matcher maps onto the column)
mock.ExpectQuery("SELECT fingerprint, any").
WithArgs("test_metric").
WithArgs("test_metric", "__name__", "test_metric").
WillReturnRows(fingerprintRows)
// Mock the samples query (for Prometheus metric data)
// args: metric_name IN (discovered names), subquery metric_name, start, end
mock.ExpectQuery("SELECT metric_name, fingerprint, unix_milli").
WithArgs(
"test_metric",
"test_metric",
"__name__",
"test_metric",
queryStart,
queryEnd,
).

View File

@@ -89,7 +89,7 @@
"lodash-es": "^4.17.21",
"motion": "12.4.13",
"nuqs": "2.8.8",
"overlayscrollbars": "^2.16.0",
"overlayscrollbars": "^2.8.1",
"overlayscrollbars-react": "^0.5.6",
"papaparse": "5.4.1",
"posthog-js": "1.298.0",
@@ -221,18 +221,5 @@
"*.(scss|css)": [
"stylelint"
]
},
"overrides": {
"@babel/core@<=7.29.0": ">=7.29.6 <8",
"@istanbuljs/load-nyc-config>js-yaml": ">=4.2.0 <5",
"cookie@<0.7.0": ">=0.7.1 <1",
"dompurify@<=3.4.10": ">=3.4.11 <4",
"esbuild@>=0.27.3 <0.28.1": ">=0.28.1 <0.29.0",
"js-cookie@<=3.0.5": ">=3.0.7 <4",
"js-yaml@>=4.0.0 <=4.1.1": ">=4.2.0 <5",
"prismjs@<1.30.0": ">=1.30.0 <2",
"react-router@>=6.7.0 <6.30.4": ">=6.30.4 <7",
"tmp@<0.2.6": ">=0.2.6 <0.3.0",
"yaml@>=1.0.0 <1.10.3": ">=1.10.3 <2"
}
}
}

View File

@@ -187,11 +187,11 @@ importers:
specifier: 2.8.8
version: 2.8.8(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0)
overlayscrollbars:
specifier: ^2.16.0
version: 2.16.0
specifier: ^2.8.1
version: 2.9.2
overlayscrollbars-react:
specifier: ^0.5.6
version: 0.5.6(overlayscrollbars@2.16.0)(react@18.2.0)
version: 0.5.6(overlayscrollbars@2.9.2)(react@18.2.0)
papaparse:
specifier: 5.4.1
version: 5.4.1
@@ -6941,8 +6941,8 @@ packages:
overlayscrollbars: ^2.0.0
react: '>=16.8.0'
overlayscrollbars@2.16.0:
resolution: {integrity: sha512-N03oje/q7j93D0aLZtoCdsDSYLmhheSsv8H7oSLE7HhdV9P/bmCURtLV/KbPye7P/bpfyt/obSfDpGUYoJ0OWg==}
overlayscrollbars@2.9.2:
resolution: {integrity: sha512-iDT84r39i7oWP72diZN2mbJUsn/taCq568aQaIrc84S87PunBT7qtsVltAF2esk7ORTRjQDnfjVYoqqTzgs8QA==}
oxfmt@0.54.0:
resolution: {integrity: sha512-DjnMwn7smSLF+Mc2+pRItnuPftm/dkUFpY/d4+33y9TfKrsHZo8GLhmUg9BrOIUEy94Rlom1Q11N6vuhE+e0oQ==}
@@ -16456,12 +16456,12 @@ snapshots:
outvariant@1.4.0: {}
overlayscrollbars-react@0.5.6(overlayscrollbars@2.16.0)(react@18.2.0):
overlayscrollbars-react@0.5.6(overlayscrollbars@2.9.2)(react@18.2.0):
dependencies:
overlayscrollbars: 2.16.0
overlayscrollbars: 2.9.2
react: 18.2.0
overlayscrollbars@2.16.0: {}
overlayscrollbars@2.9.2: {}
oxfmt@0.54.0:
dependencies:

View File

@@ -1,20 +0,0 @@
import { ENVIRONMENT } from 'constants/env';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
export function mockFieldsAPIsWithEmptyResponse(): void {
server.use(
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/keys`, (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({ status: 'success', data: { keys: {}, complete: true } }),
),
),
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/values`, (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({ status: 'success', data: { values: {}, complete: true } }),
),
),
);
}

View File

@@ -2814,8 +2814,6 @@ export enum CloudintegrationtypesServiceIDDTO {
cassandradb = 'cassandradb',
redis = 'redis',
cloudsql_postgres = 'cloudsql_postgres',
memorystore_redis = 'memorystore_redis',
computeengine = 'computeengine',
}
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
/**
@@ -3481,7 +3479,6 @@ export enum TelemetrytypesFieldContextDTO {
resource = 'resource',
attribute = 'attribute',
body = 'body',
'' = '',
}
export enum TelemetrytypesFieldDataTypeDTO {
string = 'string',
@@ -3489,13 +3486,11 @@ export enum TelemetrytypesFieldDataTypeDTO {
float64 = 'float64',
int64 = 'int64',
number = 'number',
'' = '',
}
export enum TelemetrytypesSignalDTO {
traces = 'traces',
logs = 'logs',
metrics = 'metrics',
'' = '',
}
export interface Querybuildertypesv5GroupByKeyDTO {
/**
@@ -9272,48 +9267,6 @@ export interface SpantypesGettableSpanMapperGroupsDTO {
items: SpantypesSpanMapperGroupDTO[];
}
export type SpantypesSpanMapperTestSpanDTOAttributesAnyOf = {
[key: string]: unknown;
};
/**
* @nullable
*/
export type SpantypesSpanMapperTestSpanDTOAttributes =
SpantypesSpanMapperTestSpanDTOAttributesAnyOf | null;
export type SpantypesSpanMapperTestSpanDTOResourceAnyOf = {
[key: string]: unknown;
};
/**
* @nullable
*/
export type SpantypesSpanMapperTestSpanDTOResource =
SpantypesSpanMapperTestSpanDTOResourceAnyOf | null;
export interface SpantypesSpanMapperTestSpanDTO {
/**
* @type object,null
*/
attributes?: SpantypesSpanMapperTestSpanDTOAttributes;
/**
* @type object,null
*/
resource?: SpantypesSpanMapperTestSpanDTOResource;
}
export interface SpantypesGettableSpanMapperTestDTO {
/**
* @type array,null
*/
collectorLogs?: string[] | null;
/**
* @type array,null
*/
spans?: SpantypesSpanMapperTestSpanDTO[] | null;
}
export enum SpantypesSpanMapperOperationDTO {
move = 'move',
copy = 'copy',
@@ -9655,33 +9608,6 @@ export interface SpantypesPostableSpanMapperGroupDTO {
name: string;
}
export interface SpantypesPostableSpanMapperTestGroupDTO {
condition: SpantypesSpanMapperGroupConditionDTO | null;
/**
* @type boolean
*/
enabled?: boolean;
/**
* @type array,null
*/
mappers?: SpantypesPostableSpanMapperDTO[] | null;
/**
* @type string
*/
name: string;
}
export interface SpantypesPostableSpanMapperTestDTO {
/**
* @type array,null
*/
groups: SpantypesPostableSpanMapperTestGroupDTO[] | null;
/**
* @type array,null
*/
spans: SpantypesSpanMapperTestSpanDTO[] | null;
}
export interface SpantypesSpanAggregationDTO {
aggregation: SpantypesSpanAggregationTypeDTO;
field: TelemetrytypesTelemetryFieldKeyDTO;
@@ -11032,14 +10958,6 @@ export type UpdateSpanMapperPathParameters = {
groupId: string;
mapperId: string;
};
export type TestSpanMappers200 = {
data: SpantypesGettableSpanMapperTestDTO;
/**
* @type string
*/
status: string;
};
export type GetStats200Data = { [key: string]: unknown };
export type GetStats200 = {

View File

@@ -30,10 +30,8 @@ import type {
RenderErrorResponseDTO,
SpantypesPostableSpanMapperDTO,
SpantypesPostableSpanMapperGroupDTO,
SpantypesPostableSpanMapperTestDTO,
SpantypesUpdatableSpanMapperDTO,
SpantypesUpdatableSpanMapperGroupDTO,
TestSpanMappers200,
UpdateSpanMapperGroupPathParameters,
UpdateSpanMapperPathParameters,
} from '../sigNoz.schemas';
@@ -782,86 +780,3 @@ export const useUpdateSpanMapper = <
> => {
return useMutation(getUpdateSpanMapperMutationOptions(options));
};
/**
* Tests how span mappers would transform sample spans
* @summary Test span mappers against sample spans
*/
export const testSpanMappers = (
spantypesPostableSpanMapperTestDTO?: BodyType<SpantypesPostableSpanMapperTestDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<TestSpanMappers200>({
url: `/api/v1/span_mapper_groups/test`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: spantypesPostableSpanMapperTestDTO,
signal,
});
};
export const getTestSpanMappersMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testSpanMappers>>,
TError,
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof testSpanMappers>>,
TError,
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
TContext
> => {
const mutationKey = ['testSpanMappers'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof testSpanMappers>>,
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> }
> = (props) => {
const { data } = props ?? {};
return testSpanMappers(data);
};
return { mutationFn, ...mutationOptions };
};
export type TestSpanMappersMutationResult = NonNullable<
Awaited<ReturnType<typeof testSpanMappers>>
>;
export type TestSpanMappersMutationBody =
| BodyType<SpantypesPostableSpanMapperTestDTO>
| undefined;
export type TestSpanMappersMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Test span mappers against sample spans
*/
export const useTestSpanMappers = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testSpanMappers>>,
TError,
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof testSpanMappers>>,
TError,
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
TContext
> => {
return useMutation(getTestSpanMappersMutationOptions(options));
};

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 128 128"><path fill="#ea4535" d="M80.6 40.3h.4l-.2-.2 14-14v-.3c-11.8-10.4-28.1-14-43.2-9.5C36.5 20.8 24.9 32.8 20.7 48c.2-.1.5-.2.8-.2 5.2-3.4 11.4-5.4 17.9-5.4 2.2 0 4.3.2 6.4.6.1-.1.2-.1.3-.1 9-9.9 24.2-11.1 34.6-2.6h-.1z"/><path fill="#557ebf" d="M108.1 47.8c-2.3-8.5-7.1-16.2-13.8-22.1L80 39.9c6 4.9 9.5 12.3 9.3 20v2.5c16.9 0 16.9 25.2 0 25.2H63.9v20h-.1l.1.2h25.4c14.6.1 27.5-9.3 31.8-23.1 4.3-13.8-1-28.8-13-36.9z"/><path fill="#36a852" d="M39 107.9h26.3V87.7H39c-1.9 0-3.7-.4-5.4-1.1l-15.2 14.6v.2c6 4.3 13.2 6.6 20.7 6.6z"/><path fill="#f9bc15" d="M40.2 41.9c-14.9.1-28.1 9.3-32.9 22.8-4.8 13.6 0 28.5 11.8 37.3l15.6-14.9c-8.6-3.7-10.6-14.5-4-20.8 6.6-6.4 17.8-4.4 21.7 3.8L68 55.2C61.4 46.9 51.1 42 40.2 42.1z"/></svg>

After

Width:  |  Height:  |  Size: 805 B

View File

@@ -20,7 +20,7 @@ import { Globe, Inbox, SquarePen } from '@signozhq/icons';
import AnnouncementsModal from './AnnouncementsModal';
import FeedbackModal from './FeedbackModal';
import ShareURLModal, { type ShareURLExtraOption } from './ShareURLModal';
import ShareURLModal from './ShareURLModal';
import './HeaderRightSection.styles.scss';
import { Typography } from '@signozhq/ui/typography';
@@ -29,15 +29,12 @@ interface HeaderRightSectionProps {
enableAnnouncements: boolean;
enableShare: boolean;
enableFeedback: boolean;
/** Optional page-specific toggle for the share dialog (e.g. "Include variables"). */
shareModalExtraOption?: ShareURLExtraOption;
}
function HeaderRightSection({
enableAnnouncements,
enableShare,
enableFeedback,
shareModalExtraOption,
}: HeaderRightSectionProps): JSX.Element | null {
const location = useLocation();
@@ -188,7 +185,7 @@ function HeaderRightSection({
rootClassName="header-section-popover-root"
className="shareable-link-popover"
placement="bottomRight"
content={<ShareURLModal extraOption={shareModalExtraOption} />}
content={<ShareURLModal />}
open={openShareURLModal}
destroyTooltipOnHide
arrow={false}

View File

@@ -24,22 +24,7 @@ const routesToBeSharedWithTime = [
ROUTES.METER_EXPLORER,
];
/**
* An optional, page-specific toggle in the share dialog (e.g. a dashboard's
* "Include variables"). When enabled, `apply` mutates the URL params that go into
* the shared link. Keeps this shared modal generic — the page owns what it adds.
*/
export interface ShareURLExtraOption {
label: string;
defaultEnabled?: boolean;
apply: (params: URLSearchParams) => void;
}
interface ShareURLModalProps {
extraOption?: ShareURLExtraOption;
}
function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
function ShareURLModal(): JSX.Element {
const urlQuery = useUrlQuery();
const location = useLocation();
const { selectedTime } = useSelector<AppState, GlobalReducer>(
@@ -49,9 +34,6 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
const [enableAbsoluteTime, setEnableAbsoluteTime] = useState(
selectedTime !== 'custom',
);
const [enableExtraOption, setEnableExtraOption] = useState(
extraOption?.defaultEnabled ?? false,
);
const startTime = urlQuery.get(QueryParams.startTime);
const endTime = urlQuery.get(QueryParams.endTime);
@@ -111,11 +93,6 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
}
}
if (extraOption && enableExtraOption) {
extraOption.apply(urlQuery);
currentUrl = getAbsoluteUrl(`${location.pathname}?${urlQuery.toString()}`);
}
return currentUrl;
};
@@ -166,20 +143,6 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
</>
)}
{extraOption && (
<div className="absolute-relative-time-toggler-container">
<Typography.Text className="absolute-relative-time-toggler-label">
{extraOption.label}
</Typography.Text>
<div className="absolute-relative-time-toggler">
<Switch
value={enableExtraOption}
onChange={(): void => setEnableExtraOption((prev) => !prev)}
/>
</div>
</div>
)}
<div className="share-link">
<div className="url-share-container">
<div className="url-share-container-header">

View File

@@ -1,6 +1,8 @@
.quick-filters-container {
display: flex;
flex-direction: row;
height: 100%;
min-height: 0;
position: relative;
.quick-filters-settings-container {

View File

@@ -1,112 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import TagKeyValueInput from './TagKeyValueInput';
const TID = 'tag-key-value-input';
type User = ReturnType<typeof userEvent.setup>;
const startEditingFirstChip = async (user: User): Promise<HTMLElement> => {
await user.dblClick(screen.getAllByTestId(`${TID}-chip`)[0]);
return screen.getByTestId(`${TID}-edit`);
};
describe('TagKeyValueInput — inline chip edit', () => {
it('shows an error and stays in edit mode when Enter commits an invalid value', async () => {
const user = userEvent.setup();
const onTagsChange = jest.fn();
render(<TagKeyValueInput tags={['env:prod']} onTagsChange={onTagsChange} />);
const input = await startEditingFirstChip(user);
await user.clear(input);
await user.type(input, 'novalue{Enter}');
expect(screen.getByTestId(`${TID}-error`)).toHaveTextContent(
'key:value format',
);
// Still editing (input present), and no change committed.
expect(screen.getByTestId(`${TID}-edit`)).toBeInTheDocument();
expect(onTagsChange).not.toHaveBeenCalled();
});
it('shows a duplicate error when Enter commits an existing tag', async () => {
const user = userEvent.setup();
const onTagsChange = jest.fn();
render(
<TagKeyValueInput
tags={['env:prod', 'team:pulse']}
onTagsChange={onTagsChange}
/>,
);
const input = await startEditingFirstChip(user);
await user.clear(input);
await user.type(input, 'team:pulse{Enter}');
expect(screen.getByTestId(`${TID}-error`)).toHaveTextContent(
'already exists',
);
expect(onTagsChange).not.toHaveBeenCalled();
});
it('commits a valid edit on Enter', async () => {
const user = userEvent.setup();
const onTagsChange = jest.fn();
render(<TagKeyValueInput tags={['env:prod']} onTagsChange={onTagsChange} />);
const input = await startEditingFirstChip(user);
await user.clear(input);
await user.type(input, 'env:staging{Enter}');
expect(onTagsChange).toHaveBeenCalledWith(['env:staging']);
expect(screen.queryByTestId(`${TID}-error`)).not.toBeInTheDocument();
});
it('reverts silently (no error) when blurring an invalid edit', async () => {
const user = userEvent.setup();
const onTagsChange = jest.fn();
render(<TagKeyValueInput tags={['env:prod']} onTagsChange={onTagsChange} />);
const input = await startEditingFirstChip(user);
await user.clear(input);
await user.type(input, 'novalue');
await user.tab(); // blur off the edit input
expect(screen.queryByTestId(`${TID}-error`)).not.toBeInTheDocument();
expect(screen.queryByTestId(`${TID}-edit`)).not.toBeInTheDocument();
expect(onTagsChange).not.toHaveBeenCalled();
});
});
describe('TagKeyValueInput — backend-rule validation', () => {
it('rejects a key containing a space on inline edit', async () => {
const user = userEvent.setup();
const onTagsChange = jest.fn();
render(<TagKeyValueInput tags={['env:prod']} onTagsChange={onTagsChange} />);
const input = await startEditingFirstChip(user);
await user.clear(input);
await user.type(input, 'my key:prod{Enter}');
expect(screen.getByTestId(`${TID}-error`)).toHaveTextContent(
'spaces or special characters',
);
expect(screen.getByTestId(`${TID}-edit`)).toBeInTheDocument();
expect(onTagsChange).not.toHaveBeenCalled();
});
it('rejects a value containing a space in the new-tag input', async () => {
const user = userEvent.setup();
const onTagsChange = jest.fn();
render(<TagKeyValueInput tags={[]} onTagsChange={onTagsChange} />);
const input = screen.getByTestId(TID);
await user.type(input, 'env:pro d{Enter}');
expect(screen.getByTestId(`${TID}-error`)).toHaveTextContent(
'spaces or special characters',
);
expect(onTagsChange).not.toHaveBeenCalled();
});
});

View File

@@ -5,7 +5,7 @@ import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import TagBadge from '../TagBadge/TagBadge';
import { validateTag } from './utils';
import { parseKeyValueTag } from './utils';
import styles from './TagKeyValueInput.module.scss';
@@ -43,12 +43,16 @@ function TagKeyValueInput({
if (!raw) {
return;
}
const result = validateTag(raw, tags);
if ('error' in result) {
setError(result.error);
const normalized = parseKeyValueTag(raw);
if (!normalized) {
setError('Tags must be in key:value format (both sides required).');
return;
}
onTagsChange([...tags, result.tag]);
if (tags.includes(normalized)) {
setError('This tag already exists.');
return;
}
onTagsChange([...tags, normalized]);
setInputValue('');
setError('');
};
@@ -78,20 +82,15 @@ function TagKeyValueInput({
const cancelEdit = (): void => {
setEditIndex(-1);
setEditValue('');
setError('');
};
const commitEdit = (revertOnInvalid = false): void => {
const result = validateTag(editValue, tags, editIndex);
if ('error' in result) {
if (revertOnInvalid) {
cancelEdit();
} else {
setError(result.error);
}
return;
const commitEdit = (): void => {
const normalized = parseKeyValueTag(editValue);
// Drop into a no-op (revert) on invalid or duplicate edits rather than
// stranding the user in an un-exitable edit box.
if (normalized && !tags.some((t, i) => t === normalized && i !== editIndex)) {
onTagsChange(tags.map((t, i) => (i === editIndex ? normalized : t)));
}
onTagsChange(tags.map((t, i) => (i === editIndex ? result.tag : t)));
cancelEdit();
};
@@ -122,14 +121,11 @@ function TagKeyValueInput({
value={editValue}
autoFocus
testId={`${testId}-edit`}
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
setEditValue(e.target.value);
if (error) {
setError('');
}
}}
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
setEditValue(e.target.value)
}
onKeyDown={handleEditKeyDown}
onBlur={(): void => commitEdit(true)}
onBlur={commitEdit}
/>
) : (
<TagBadge

View File

@@ -15,42 +15,3 @@ export function parseKeyValueTag(raw: string): string | null {
}
return `${key}:${value}`;
}
const TAG_KEY_REGEX = new RegExp('^[a-zA-Z$_@{#][a-zA-Z0-9$_@#{}:/-]*$');
const TAG_VALUE_REGEX = new RegExp('^[a-zA-Z0-9$_@#{}:.+=/-]*$');
const MAX_TAG_LEN = 32;
export type TagValidation = { tag: string } | { error: string };
export function validateTag(
raw: string,
existingTags: string[],
excludeIndex = -1,
): TagValidation {
const normalized = parseKeyValueTag(raw);
if (!normalized) {
return { error: 'Tags must be in key:value format (both sides required).' };
}
const separator = normalized.indexOf(':');
const key = normalized.slice(0, separator);
const value = normalized.slice(separator + 1);
if (!TAG_KEY_REGEX.test(key)) {
return { error: 'Tag keys cannot contain spaces or special characters.' };
}
if (!TAG_VALUE_REGEX.test(value)) {
return { error: 'Tag values cannot contain spaces or special characters.' };
}
if (key.length > MAX_TAG_LEN || value.length > MAX_TAG_LEN) {
return {
error: `Tag key and value must each be ${MAX_TAG_LEN} characters or fewer.`,
};
}
if (
existingTags.some(
(tag, index) => tag === normalized && index !== excludeIndex,
)
) {
return { error: 'This tag already exists.' };
}
return { tag: normalized };
}

View File

@@ -1,11 +1,7 @@
import { ComponentProps, memo, useCallback, useLayoutEffect } from 'react';
import { ComponentProps, memo } from 'react';
import { TableComponents } from 'react-virtuoso';
import cx from 'classnames';
import {
chromePerformanceTanstackTableBeginHover,
chromePerformanceMeasureTanstackTable,
} from './perfDevtools';
import TanStackRowCells from './TanStackRow';
import {
useClearRowHovered,
@@ -14,7 +10,6 @@ import {
import { FlatItem, TableRowContext } from './types';
import tableStyles from './TanStackTable.module.scss';
import { chromePerformanceNow } from 'lib/chromePerformanceDevTools';
type VirtuosoTableRowProps<TData, TItemKey = string> = ComponentProps<
NonNullable<
@@ -27,7 +22,6 @@ function TanStackCustomTableRow<TData, TItemKey = string>({
context,
...props
}: VirtuosoTableRowProps<TData, TItemKey>): JSX.Element {
const renderStart = chromePerformanceNow();
const rowId = item.row.id;
const rowData = item.row.original;
@@ -35,23 +29,6 @@ function TanStackCustomTableRow<TData, TItemKey = string>({
const setHovered = useSetRowHovered(rowId);
const clearHovered = useClearRowHovered(rowId);
const handleMouseEnter = useCallback((): void => {
chromePerformanceTanstackTableBeginHover(rowId);
setHovered();
}, [rowId, setHovered]);
useLayoutEffect(() => {
chromePerformanceMeasureTanstackTable('Row render', renderStart, {
track: 'Row render',
color: 'secondary',
tooltipText: 'Row render + commit',
properties: [
['rowId', rowId],
['kind', item.kind],
],
});
});
if (item.kind === 'expansion') {
return (
<tr {...props} className={tableStyles.tableRowExpansion}>
@@ -88,7 +65,7 @@ function TanStackCustomTableRow<TData, TItemKey = string>({
{...props}
className={rowClassName}
style={rowStyle}
onMouseEnter={handleMouseEnter}
onMouseEnter={setHovered}
onMouseLeave={clearHovered}
>
<TanStackRowCells

View File

@@ -1,30 +0,0 @@
import { useLayoutEffect, type ReactNode } from 'react';
import { chromePerformanceTanstackTableEndHover } from './perfDevtools';
import { useIsRowHovered } from './TanStackTableStateContext';
import { TooltipSimple, TooltipSimpleProps } from '@signozhq/ui/tooltip';
export type HoverTooltipProps = Omit<TooltipSimpleProps, 'open'> & {
rowId: string;
children: ReactNode;
};
export function TanStackHoverTooltip({
rowId,
children,
...tooltipProps
}: HoverTooltipProps): JSX.Element {
const isHovered = useIsRowHovered(rowId);
useLayoutEffect(() => {
if (isHovered) {
chromePerformanceTanstackTableEndHover(rowId);
}
}, [isHovered, rowId]);
if (!isHovered) {
return <>{children}</>;
}
return <TooltipSimple {...tooltipProps}>{children}</TooltipSimple>;
}

View File

@@ -5,7 +5,6 @@ import {
useCallback,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
} from 'react';
@@ -45,7 +44,6 @@ import {
TanStackTableHandle,
TanStackTableProps,
} from './types';
import { chromePerformanceMeasureTanstackTable } from './perfDevtools';
import { useColumnDnd } from './useColumnDnd';
import { useColumnHandlers } from './useColumnHandlers';
import { useColumnState } from './useColumnState';
@@ -58,7 +56,6 @@ import { VirtuosoTableColGroup } from './VirtuosoTableColGroup';
import tableStyles from './TanStackTable.module.scss';
import viewStyles from './TanStackTableView.module.scss';
import { chromePerformanceNow } from 'lib/chromePerformanceDevTools';
const COLUMN_DND_AUTO_SCROLL = {
layoutShiftCompensation: false as const,
@@ -119,8 +116,6 @@ function TanStackTableInner<TData, TItemKey = string>(
);
}
const renderStart = chromePerformanceNow();
const virtuosoRef = useRef<TableVirtuosoHandle | null>(null);
const isDarkMode = useIsDarkMode();
@@ -349,19 +344,6 @@ function TanStackTableInner<TData, TItemKey = string>(
const visibleColumnsCount = table.getVisibleFlatColumns().length;
useLayoutEffect(() => {
chromePerformanceMeasureTanstackTable('Table render', renderStart, {
track: 'Table render',
color: 'primary',
tooltipText: 'TanStackTable render + commit',
properties: [
['rows', String(flatItems.length)],
['columns', String(visibleColumnsCount)],
['loading', String(isLoading)],
],
});
});
const columnOrderKey = useMemo(() => columnIds.join(','), [columnIds]);
const columnVisibilityKey = useMemo(
() =>

View File

@@ -1,8 +1,6 @@
import { TanStackHoverTooltip } from './TanStackHoverTooltip';
import { TanStackTableBase } from './TanStackTable';
import TanStackTableText from './TanStackTableText';
export * from './TanStackHoverTooltip';
export * from './TanStackTableStateContext';
export * from './types';
export * from './useCalculatedPageSize';
@@ -276,7 +274,6 @@ export * from './useTableParams';
*/
const TanStackTable = Object.assign(TanStackTableBase, {
Text: TanStackTableText,
HoverTooltip: TanStackHoverTooltip,
});
export default TanStackTable;

View File

@@ -1,35 +0,0 @@
import {
createChromePerformanceInteractionTracker,
chromePerformanceMeasure,
ChromePerformanceTrackEntryOptions,
} from 'lib/chromePerformanceDevTools';
const TABLE_TRACK_GROUP = 'SigNoz Table';
export function chromePerformanceMeasureTanstackTable(
name: string,
start: number,
{
track,
color,
tooltipText,
properties,
}: Omit<ChromePerformanceTrackEntryOptions, 'trackGroup'>,
): void {
chromePerformanceMeasure(name, start, {
trackGroup: TABLE_TRACK_GROUP,
track,
color,
tooltipText,
properties,
});
}
const hoverTracker = createChromePerformanceInteractionTracker(
TABLE_TRACK_GROUP,
'Hover',
'Row hover',
);
export const chromePerformanceTanstackTableBeginHover = hoverTracker.begin;
export const chromePerformanceTanstackTableEndHover = hoverTracker.end;

View File

@@ -21,7 +21,6 @@ export type TableCellContext<TData, TValue> = {
value: TValue;
isActive: boolean;
rowIndex: number;
rowId: string;
isExpanded: boolean;
canExpand: boolean;
toggleExpanded: () => void;

View File

@@ -135,7 +135,6 @@ export function buildTanstackColumnDef<TData, TItemKey = string>(
value: getValue() as TData[any],
isActive: isRowActive?.(rowData) ?? false,
rowIndex: row.index,
rowId: row.id,
isExpanded: row.getIsExpanded(),
canExpand: row.getCanExpand(),
toggleExpanded: (): void => {

View File

@@ -1,11 +1,9 @@
import { QueryClient, QueryClientProvider } from 'react-query';
import { MemoryRouter, useHistory } from 'react-router-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { INITIAL_CREATE_ALERT_STATE } from '../constants';
import { CreateAlertProvider, useCreateAlertState } from '../index';
import { AlertThresholdMatchType } from '../types';
// The provider only needs a query with a unit + empty builder for these assertions.
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
@@ -110,67 +108,3 @@ describe('CreateAlertProvider — URL-declared prefill (issue #5291)', () => {
expect(screen.getByTestId('window-type')).toHaveTextContent('cumulative');
});
});
// Reproduces the #12137 regression: on the alert overview page the query builder
// rewrites location.search after the alert loads, which used to re-run the prefill
// effect and RESET the loaded threshold back to 0.
function SearchMutator(): JSX.Element {
const routerHistory = useHistory();
return (
<button
type="button"
data-testid="mutate-search"
onClick={(): void =>
routerHistory.replace(
'/alerts/overview?compositeQuery=normalized&ruleId=r1',
)
}
>
change search
</button>
);
}
describe('CreateAlertProvider — edit mode ignores URL prefill', () => {
it('keeps loaded thresholds when the query builder rewrites location.search', () => {
const initialAlertState = {
...INITIAL_CREATE_ALERT_STATE,
threshold: {
...INITIAL_CREATE_ALERT_STATE.threshold,
matchType: AlertThresholdMatchType.AT_LEAST_ONCE,
thresholds: [
{
...INITIAL_CREATE_ALERT_STATE.threshold.thresholds[0],
thresholdValue: 245,
},
],
},
};
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
render(
<MemoryRouter initialEntries={['/alerts/overview?ruleId=r1']}>
<QueryClientProvider client={queryClient}>
<CreateAlertProvider
initialAlertType={AlertTypes.METRICS_BASED_ALERT}
isEditMode
ruleId="r1"
initialAlertState={initialAlertState}
>
<Probe />
<SearchMutator />
</CreateAlertProvider>
</QueryClientProvider>
</MemoryRouter>,
);
expect(screen.getByTestId('threshold-value')).toHaveTextContent('245');
// Simulate the query builder normalizing the query into the URL post-load.
fireEvent.click(screen.getByTestId('mutate-search'));
expect(screen.getByTestId('threshold-value')).toHaveTextContent('245');
});
});

View File

@@ -168,14 +168,6 @@ export function CreateAlertProvider(
const yAxisUnitAppliedRef = useRef(false);
useEffect(() => {
// URL-declared prefill is a create-flow concern. In edit mode the alert
// state is owned by SET_INITIAL_STATE; running the RESET below would wipe
// the loaded thresholds each time the query builder rewrites location.search
// (which yields a fresh urlPrefill object and re-triggers this effect).
if (isEditMode) {
return;
}
setCreateAlertState({
slice: CreateAlertSlice.THRESHOLD,
action: {
@@ -243,7 +235,7 @@ export function CreateAlertProvider(
},
});
}
}, [alertType, urlPrefill, ruleNameFromURL, yAxisUnitFromURL, isEditMode]);
}, [alertType, urlPrefill, ruleNameFromURL, yAxisUnitFromURL]);
useEffect(() => {
if (isEditMode && initialAlertState) {

View File

@@ -51,7 +51,6 @@ import { getHostsQuickFiltersConfig } from './utils';
import styles from './InfraMonitoringHosts.module.scss';
import { ArrowUpToLine, Filter } from '@signozhq/icons';
import { NANO_SECOND_MULTIPLIER, useGlobalTimeStore } from 'store/globalTime';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
function Hosts(): JSX.Element {
const [showFilters, setShowFilters] = useState(true);
@@ -212,31 +211,27 @@ function Hosts(): JSX.Element {
<div className={styles.infraContentRow}>
{showFilters && (
<div className={styles.quickFiltersContainer}>
<OverlayScrollbar>
<>
<div className={styles.quickFiltersContainerHeader}>
<Typography.Text>Filters</Typography.Text>
<Tooltip title="Collapse Filters">
<ArrowUpToLine
style={{ rotate: '270deg', cursor: 'pointer' }}
onClick={handleFilterVisibilityChange}
size="md"
/>
</Tooltip>
</div>
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={getHostsQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
useFieldApis={{
metricNamespace:
METRIC_NAMESPACE_BY_ENTITY[InfraMonitoringEntity.HOSTS],
startUnixMilli,
endUnixMilli,
}}
<div className={styles.quickFiltersContainerHeader}>
<Typography.Text>Filters</Typography.Text>
<Tooltip title="Collapse Filters">
<ArrowUpToLine
style={{ rotate: '270deg', cursor: 'pointer' }}
onClick={handleFilterVisibilityChange}
size="md"
/>
</>
</OverlayScrollbar>
</Tooltip>
</div>
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={getHostsQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
useFieldApis={{
metricNamespace:
METRIC_NAMESPACE_BY_ENTITY[InfraMonitoringEntity.HOSTS],
startUnixMilli,
endUnixMilli,
}}
/>
</div>
)}
<div
@@ -253,7 +248,6 @@ function Hosts(): JSX.Element {
getRowKey={getHostRowKey}
getItemKey={getHostItemKey}
eventCategory={InfraMonitoringEvents.HostEntity}
detailsQueryKeyPrefix="hosts"
/>
</div>
</div>

View File

@@ -48,6 +48,24 @@
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);
@@ -62,6 +80,28 @@
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: var(--accent-primary);
}
}
}
.quickFiltersContainerHeader {

View File

@@ -130,13 +130,12 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
accessorFn: (row): string => row.status,
width: { min: 140 },
enableSort: false,
cell: ({ value, groupMeta, row, rowId }): React.ReactNode => {
cell: ({ value, groupMeta, row }): React.ReactNode => {
const status = value as InframonitoringtypesHostStatusDTO;
if (groupMeta) {
return (
<GroupedStatusCounts
rowId={rowId}
items={[
{
value: row.activeHostCount,
@@ -196,13 +195,13 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
<ColumnHeader
tooltip="Excluding cache memory."
docPath="/infrastructure-monitoring/host-monitoring#memory-usage"
className={styles.memoryUsageHeader}
>
Memory Usage
<br /> (WSS)
Memory Usage (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.memory,
width: { min: 200 },
width: { min: 240 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
@@ -249,12 +248,11 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
id: 'load15',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#load-avg">
Load Avg
<br /> (15min)
Load Avg (15min)
</ColumnHeader>
),
accessorFn: (row): number => row.load15,
width: { min: 160 },
width: { min: 200 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const load15 = Number(value);
@@ -278,7 +276,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
</ColumnHeader>
),
accessorFn: (row): number => row.diskUsage,
width: { min: 200 },
width: { min: 160 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const diskUsage = value as number;

View File

@@ -13,6 +13,14 @@
text-align: right;
}
.memoryUsageHeader {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: var(--spacing-2);
width: 100%;
}
.statusTag {
width: fit-content;
font-family: Inter, sans-serif;

View File

@@ -1,39 +1,148 @@
import { useCallback, useEffect, useMemo } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useQuery } from 'react-query';
import { Color, Spacing } from '@signozhq/design-tokens';
import { X } from '@signozhq/icons';
import { Button, Drawer, Tooltip } from 'antd';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import { Drawer } from 'antd';
import logEvent from 'api/common/logEvent';
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import APIError from 'types/api/error';
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
import { InfraMonitoringEvents } from 'constants/events';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { QueryParams } from 'constants/query';
import {
GlobalTimeProvider,
NANO_SECOND_MULTIPLIER,
useGlobalTimeStore,
} from 'store/globalTime';
initialQueryBuilderFormValuesMap,
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import GetMinMax from 'lib/getMinMax';
import {
BarChart,
ChevronsLeftRight,
Compass,
DraftingCompass,
ScrollText,
X,
} from '@signozhq/icons';
import { isCustomTimeRange, useGlobalTimeStore } from 'store/globalTime';
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
import {
LogsAggregatorOperator,
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { openInNewTab } from 'utils/navigation';
import { INFRA_MONITORING_K8S_PARAMS_KEYS } from '../constants';
import { useInfraMonitoringSelectedItemParams } from '../hooks';
import { InfraMonitoringEntity, VIEW_TYPES } from '../constants';
import EntityEvents from '../EntityDetailsUtils/EntityEvents';
import EntityLogs from '../EntityDetailsUtils/EntityLogs';
import { K8S_ENTITY_LOGS_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityLogs/hooks';
import EntityMetrics from '../EntityDetailsUtils/EntityMetrics';
import EntityTraces from '../EntityDetailsUtils/EntityTraces';
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityTraces/hooks';
import {
SelectedItemParams,
useInfraMonitoringEventsFilters,
useInfraMonitoringLogFilters,
useInfraMonitoringSelectedItemParams,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from '../hooks';
import LoadingContainer from '../LoadingContainer';
import K8sBaseDetailsContent from './K8sBaseDetailsContent';
import { K8sBaseDetailsProps } from './types';
import '../EntityDetailsUtils/entityDetails.styles.scss';
import { parseAsString, useQueryState } from 'nuqs';
import {
EntityCountConfig,
EntityCountsSection,
} from './components/EntityCountsSection/EntityCountsSection';
export type {
CustomTab,
CustomTabRenderProps,
K8sBaseDetailsProps,
K8sDetailsCountConfig,
K8sDetailsFilters,
K8sDetailsMetadataConfig,
} from './types';
const TimeRangeOffset = 1000000000;
export interface K8sDetailsMetadataConfig<T> {
label: string;
getValue: (entity: T) => string | number;
render?: (value: string | number, entity: T) => React.ReactNode;
}
export type K8sDetailsCountConfig<T> = EntityCountConfig<T>;
export interface K8sDetailsFilters {
filter: { expression: string };
start: number;
end: number;
}
export interface CustomTabRenderProps<T> {
entity: T;
timeRange: { startTime: number; endTime: number };
selectedInterval: Time;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
}
export interface CustomTab<T> {
key: string;
label: string;
icon: React.ReactNode;
render: (props: CustomTabRenderProps<T>) => React.ReactNode;
}
export interface K8sBaseDetailsProps<T> {
category: InfraMonitoringEntity;
eventCategory: string;
// Data fetching configuration
getSelectedItemExpression: (params: SelectedItemParams) => string;
fetchEntityData: (
filters: K8sDetailsFilters,
signal?: AbortSignal,
) => Promise<{ data: T | null; error?: APIError | null }>;
// Entity configuration
getEntityName: (entity: T) => string;
getInitialLogTracesExpression: (entity: T) => string;
getInitialEventsExpression: (entity: T) => string;
metadataConfig: K8sDetailsMetadataConfig<T>[];
countsConfig?: K8sDetailsCountConfig<T>[];
getCountsFilterExpression?: (entity: T) => string;
entityWidgetInfo: {
title: string;
yAxisUnit: string;
docPath?: string;
}[];
getEntityQueryPayload: (
entity: T,
start: number,
end: number,
dotMetricsEnabled: boolean,
) => GetQueryResultsProps[];
queryKeyPrefix: string;
/** When true, only metrics are shown and the Metrics/Logs/Traces/Events tab bar is hidden. */
hideDetailViewTabs?: boolean;
tabsConfig?: {
showMetrics?: boolean;
showLogs?: boolean;
showTraces?: boolean;
showEvents?: boolean;
};
customTabs?: Array<CustomTab<T>>;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function K8sBaseDetails<T>({
category,
eventCategory,
@@ -54,6 +163,7 @@ export default function K8sBaseDetails<T>({
}: K8sBaseDetailsProps<T>): JSX.Element {
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
const lastComputedMinMax = useGlobalTimeStore((s) => s.lastComputedMinMax);
const getAutoRefreshQueryKey = useGlobalTimeStore(
(s) => s.getAutoRefreshQueryKey,
);
@@ -127,9 +237,86 @@ export default function K8sBaseDetails<T>({
const entityName = entity ? getEntityName(entity) : '';
// Content state (previously in K8sBaseDetailsContent)
const tabVisibility = useMemo(
() => ({
showMetrics: true,
showLogs: true,
showTraces: true,
showEvents: true,
...tabsConfig,
}),
[tabsConfig],
);
const { startMs, endMs } = useMemo(
() => ({
startMs: Math.floor(lastComputedMinMax.minTime / NANO_SECOND_MULTIPLIER),
endMs: Math.floor(lastComputedMinMax.maxTime / NANO_SECOND_MULTIPLIER),
}),
[lastComputedMinMax],
);
const [modalTimeRange, setModalTimeRange] = useState(() => ({
startTime: startMs,
endTime: endMs,
}));
// TODO(h4ad): Remove this and use context/zustand
const lastSelectedInterval = useRef<Time | null>(null);
const [selectedInterval, setSelectedInterval] = useState<Time>(
lastSelectedInterval.current
? lastSelectedInterval.current
: isCustomTimeRange(selectedTime)
? DEFAULT_TIME_RANGE
: selectedTime,
);
const [selectedView, setSelectedView] = useInfraMonitoringView();
const effectiveView = hideDetailViewTabs ? VIEW_TYPES.METRICS : selectedView;
const validTabs = useMemo(() => {
const tabs: string[] = [];
if (tabVisibility.showMetrics) {
tabs.push(VIEW_TYPES.METRICS);
}
if (tabVisibility.showLogs) {
tabs.push(VIEW_TYPES.LOGS);
}
if (tabVisibility.showTraces) {
tabs.push(VIEW_TYPES.TRACES);
}
if (tabVisibility.showEvents) {
tabs.push(VIEW_TYPES.EVENTS);
}
if (customTabs) {
tabs.push(...customTabs.map((t) => t.key));
}
return tabs;
}, [tabVisibility, customTabs]);
useEffect(() => {
if (!hideDetailViewTabs && !validTabs.includes(selectedView)) {
const firstValid = validTabs[0] || VIEW_TYPES.METRICS;
void setSelectedView(firstValid);
}
}, [hideDetailViewTabs, selectedView, validTabs, setSelectedView]);
const [, setLogFiltersParam] = useInfraMonitoringLogFilters();
const [, setTracesFiltersParam] = useInfraMonitoringTracesFilters();
const [, setEventsFiltersParam] = useInfraMonitoringEventsFilters();
const [userLogsExpression] = useQueryState(
K8S_ENTITY_LOGS_EXPRESSION_KEY,
parseAsString,
);
const [userTracesExpression] = useQueryState(
K8S_ENTITY_TRACES_EXPRESSION_KEY,
parseAsString,
);
useEffect(() => {
if (entity) {
void logEvent(InfraMonitoringEvents.PageVisited, {
logEvent(InfraMonitoringEvents.PageVisited, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: eventCategory,
@@ -137,6 +324,136 @@ export default function K8sBaseDetails<T>({
}
}, [entity, eventCategory]);
useEffect(() => {
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
if (!isCustomTimeRange(currentSelectedInterval)) {
setSelectedInterval(currentSelectedInterval);
const { minTime, maxTime } = getMinMaxTime();
setModalTimeRange({
startTime: Math.floor(minTime / TimeRangeOffset),
endTime: Math.floor(maxTime / TimeRangeOffset),
});
}
}, [getMinMaxTime, selectedTime]);
const handleTabChange = (value: string | null): void => {
if (!value) {
return;
}
setSelectedView(value);
setLogFiltersParam(null);
setTracesFiltersParam(null);
setEventsFiltersParam(null);
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: eventCategory,
view: value,
});
};
const handleTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
lastSelectedInterval.current = interval as Time;
setSelectedInterval(interval as Time);
if (interval === 'custom' && dateTimeRange) {
setModalTimeRange({
startTime: Math.floor(dateTimeRange[0] / 1000),
endTime: Math.floor(dateTimeRange[1] / 1000),
});
} else {
const { maxTime, minTime } = GetMinMax(interval);
setModalTimeRange({
startTime: Math.floor(minTime / TimeRangeOffset),
endTime: Math.floor(maxTime / TimeRangeOffset),
});
}
logEvent(InfraMonitoringEvents.TimeUpdated, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: eventCategory,
interval,
view: effectiveView,
});
},
[eventCategory, effectiveView],
);
const handleExplorePagesRedirect = (): void => {
const urlQuery = new URLSearchParams();
if (selectedInterval !== 'custom') {
urlQuery.set(QueryParams.relativeTime, selectedInterval);
} else {
urlQuery.delete(QueryParams.relativeTime);
urlQuery.set(QueryParams.startTime, modalTimeRange.startTime.toString());
urlQuery.set(QueryParams.endTime, modalTimeRange.endTime.toString());
}
logEvent(InfraMonitoringEvents.ExploreClicked, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: eventCategory,
view: selectedView,
});
if (selectedView === VIEW_TYPES.LOGS) {
const fullExpression = combineInitialAndUserExpression(
logsAndTracesInitialExpression,
userLogsExpression || '',
);
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.logs,
aggregateOperator: LogsAggregatorOperator.NOOP,
expression: fullExpression,
filter: { expression: fullExpression },
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const fullExpression = combineInitialAndUserExpression(
logsAndTracesInitialExpression,
userTracesExpression || '',
);
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
expression: fullExpression,
filter: { expression: fullExpression },
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};
return (
<Drawer
width="70%"
@@ -163,9 +480,8 @@ export default function K8sBaseDetails<T>({
destroyOnClose
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
>
{(isEntityLoading || !selectedItem) && <LoadingContainer />}
{selectedItem && (isEntityError || hasResponseError) && (
{isEntityLoading && <LoadingContainer />}
{(isEntityError || hasResponseError) && (
<div className="entity-error-container">
<ErrorContent
error={
@@ -181,34 +497,208 @@ export default function K8sBaseDetails<T>({
/>
</div>
)}
{selectedItem && entity && !isEntityLoading && !hasResponseError && (
<GlobalTimeProvider
inheritGlobalTime
enableUrlParams={{
relativeTimeKey: INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_RELATIVE_TIME,
startTimeKey: INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_START_TIME,
endTimeKey: INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_END_TIME,
}}
>
<K8sBaseDetailsContent<T>
entity={entity}
category={category}
eventCategory={eventCategory}
metadataConfig={metadataConfig}
countsConfig={countsConfig}
getCountsFilterExpression={getCountsFilterExpression}
selectedItem={selectedItem}
handleClose={handleClose}
entityWidgetInfo={entityWidgetInfo}
getEntityQueryPayload={getEntityQueryPayload}
queryKeyPrefix={queryKeyPrefix}
hideDetailViewTabs={hideDetailViewTabs}
tabsConfig={tabsConfig}
customTabs={customTabs}
logsAndTracesInitialExpression={logsAndTracesInitialExpression}
eventsInitialExpression={eventsInitialExpression}
/>
</GlobalTimeProvider>
{entity && !isEntityLoading && !hasResponseError && (
<>
<div className="entity-detail-drawer__entity">
<div className="entity-details-grid">
<div className="labels-row">
{metadataConfig.map((config) => (
<Typography.Text
key={config.label}
color="muted"
className="entity-details-metadata-label"
>
{config.label}
</Typography.Text>
))}
</div>
<div className="values-row">
{metadataConfig.map((config) => {
const value = config.getValue(entity);
const displayValue = String(value);
return (
<Typography.Text
key={config.label}
className="entity-details-metadata-value"
>
{config.render ? (
config.render(value, entity)
) : (
<Tooltip title={displayValue}>{displayValue}</Tooltip>
)}
</Typography.Text>
);
})}
</div>
</div>
{countsConfig &&
countsConfig.length > 0 &&
selectedItem &&
getCountsFilterExpression && (
<EntityCountsSection
entity={entity}
countsConfig={countsConfig}
selectedItem={selectedItem}
filterExpression={getCountsFilterExpression(entity)}
closeDrawer={handleClose}
/>
)}
</div>
{!hideDetailViewTabs && (
<div className="views-tabs-container">
<ToggleGroupSimple
type="single"
className="views-tabs"
onChange={handleTabChange}
value={selectedView}
items={[
...(tabVisibility.showMetrics
? [
{
value: VIEW_TYPES.METRICS,
label: (
<div className="view-title">
<BarChart size={14} />
Metrics
</div>
),
},
]
: []),
...(tabVisibility.showLogs
? [
{
value: VIEW_TYPES.LOGS,
label: (
<div className="view-title">
<ScrollText size={14} />
Logs
</div>
),
},
]
: []),
...(tabVisibility.showTraces
? [
{
value: VIEW_TYPES.TRACES,
label: (
<div className="view-title">
<DraftingCompass size={14} />
Traces
</div>
),
},
]
: []),
...(tabVisibility.showEvents
? [
{
value: VIEW_TYPES.EVENTS,
label: (
<div className="view-title">
<ChevronsLeftRight size={14} />
Events
</div>
),
},
]
: []),
...(customTabs?.map((tab) => ({
value: tab.key,
label: (
<div className="view-title">
{tab.icon}
{tab.label}
</div>
),
})) ?? []),
]}
/>
{selectedView === VIEW_TYPES.LOGS && (
<Tooltip title="Go to Logs Explorer" placement="left">
<Button
icon={<Compass size={18} />}
className="compass-button"
onClick={handleExplorePagesRedirect}
/>
</Tooltip>
)}
{selectedView === VIEW_TYPES.TRACES && (
<Tooltip title="Go to Traces Explorer" placement="left">
<Button
icon={<Compass size={18} />}
className="compass-button"
onClick={handleExplorePagesRedirect}
/>
</Tooltip>
)}
</div>
)}
{effectiveView === VIEW_TYPES.METRICS && (
<EntityMetrics<T>
entity={entity}
selectedInterval={selectedInterval}
timeRange={modalTimeRange}
handleTimeChange={handleTimeChange}
isModalTimeSelection
entityWidgetInfo={entityWidgetInfo}
getEntityQueryPayload={getEntityQueryPayload}
category={category}
queryKey={`${queryKeyPrefix}Metrics`}
/>
)}
{effectiveView === VIEW_TYPES.LOGS && (
<EntityLogs
timeRange={modalTimeRange}
isModalTimeSelection
handleTimeChange={handleTimeChange}
selectedInterval={selectedInterval}
queryKey={`${queryKeyPrefix}Logs`}
category={category}
initialExpression={logsAndTracesInitialExpression}
/>
)}
{effectiveView === VIEW_TYPES.TRACES && (
<EntityTraces
timeRange={modalTimeRange}
isModalTimeSelection
handleTimeChange={handleTimeChange}
selectedInterval={selectedInterval}
queryKey={`${queryKeyPrefix}Traces`}
category={category}
initialExpression={logsAndTracesInitialExpression}
/>
)}
{effectiveView === VIEW_TYPES.EVENTS && tabVisibility.showEvents && (
<EntityEvents
timeRange={modalTimeRange}
isModalTimeSelection
handleTimeChange={handleTimeChange}
selectedInterval={selectedInterval}
category={category}
queryKey={`${queryKeyPrefix}Events`}
initialExpression={eventsInitialExpression}
/>
)}
{customTabs?.map((tab) =>
selectedView === tab.key ? (
<React.Fragment key={tab.key}>
{tab.render({
entity,
timeRange: modalTimeRange,
selectedInterval,
handleTimeChange,
})}
</React.Fragment>
) : null,
)}
</>
)}
</Drawer>
);

View File

@@ -1,401 +0,0 @@
import { Fragment, useEffect, useMemo } from 'react';
import {
BarChart,
ChevronsLeftRight,
Compass,
DraftingCompass,
ScrollText,
} from '@signozhq/icons';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Typography } from '@signozhq/ui/typography';
import { Button, Tooltip } from 'antd';
import logEvent from 'api/common/logEvent';
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
import { InfraMonitoringEvents } from 'constants/events';
import { QueryParams } from 'constants/query';
import {
initialQueryBuilderFormValuesMap,
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { parseAsString, useQueryState } from 'nuqs';
import {
LogsAggregatorOperator,
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { openInNewTab } from 'utils/navigation';
import { VIEW_TYPES } from '../constants';
import { useEntityDetailsTime } from '../EntityDetailsUtils/EntityDateTimeSelector/useEntityDetailsTime';
import EntityEvents from '../EntityDetailsUtils/EntityEvents';
import EntityLogs from '../EntityDetailsUtils/EntityLogs';
import { K8S_ENTITY_LOGS_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityLogs/hooks';
import EntityMetrics from '../EntityDetailsUtils/EntityMetrics';
import EntityTraces from '../EntityDetailsUtils/EntityTraces';
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityTraces/hooks';
import {
useInfraMonitoringEventsFilters,
useInfraMonitoringLogFilters,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from '../hooks';
import { EntityCountsSection } from './components/EntityCountsSection/EntityCountsSection';
import { K8sBaseDetailsContentProps } from './types';
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function K8sBaseDetailsContent<T>({
entity,
category,
eventCategory,
metadataConfig,
countsConfig,
getCountsFilterExpression,
selectedItem,
handleClose,
entityWidgetInfo,
getEntityQueryPayload,
queryKeyPrefix,
hideDetailViewTabs,
tabsConfig,
customTabs,
logsAndTracesInitialExpression,
eventsInitialExpression,
}: K8sBaseDetailsContentProps<T>): JSX.Element {
const { timeRange, selectedInterval, handleTimeChange } =
useEntityDetailsTime();
const tabVisibility = useMemo(
() => ({
showMetrics: true,
showLogs: true,
showTraces: true,
showEvents: true,
...tabsConfig,
}),
[tabsConfig],
);
const [selectedView, setSelectedView] = useInfraMonitoringView();
const validTabs = useMemo(() => {
const tabs: string[] = [];
if (tabVisibility.showMetrics) {
tabs.push(VIEW_TYPES.METRICS);
}
if (tabVisibility.showLogs) {
tabs.push(VIEW_TYPES.LOGS);
}
if (tabVisibility.showTraces) {
tabs.push(VIEW_TYPES.TRACES);
}
if (tabVisibility.showEvents) {
tabs.push(VIEW_TYPES.EVENTS);
}
if (customTabs) {
tabs.push(...customTabs.map((t) => t.key));
}
return tabs;
}, [tabVisibility, customTabs]);
useEffect(() => {
if (!hideDetailViewTabs && !validTabs.includes(selectedView)) {
const firstValid = validTabs[0] || VIEW_TYPES.METRICS;
void setSelectedView(firstValid);
}
}, [hideDetailViewTabs, selectedView, validTabs, setSelectedView]);
const effectiveView = hideDetailViewTabs ? VIEW_TYPES.METRICS : selectedView;
const [, setLogFiltersParam] = useInfraMonitoringLogFilters();
const [, setTracesFiltersParam] = useInfraMonitoringTracesFilters();
const [, setEventsFiltersParam] = useInfraMonitoringEventsFilters();
const [userLogsExpression] = useQueryState(
K8S_ENTITY_LOGS_EXPRESSION_KEY,
parseAsString,
);
const [userTracesExpression] = useQueryState(
K8S_ENTITY_TRACES_EXPRESSION_KEY,
parseAsString,
);
const handleTabChange = (value: string | null): void => {
if (!value) {
return;
}
void setSelectedView(value);
void setLogFiltersParam(null);
void setTracesFiltersParam(null);
void setEventsFiltersParam(null);
void logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: eventCategory,
view: value,
});
};
const handleExplorePagesRedirect = (): void => {
const urlQuery = new URLSearchParams();
if (selectedInterval !== 'custom') {
urlQuery.set(QueryParams.relativeTime, selectedInterval);
} else {
urlQuery.delete(QueryParams.relativeTime);
// Explorer URL params are in milliseconds, timeRange is in seconds
urlQuery.set(QueryParams.startTime, (timeRange.startTime * 1000).toString());
urlQuery.set(QueryParams.endTime, (timeRange.endTime * 1000).toString());
}
void logEvent(InfraMonitoringEvents.ExploreClicked, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: eventCategory,
view: selectedView,
});
if (selectedView === VIEW_TYPES.LOGS) {
const fullExpression = combineInitialAndUserExpression(
logsAndTracesInitialExpression,
userLogsExpression || '',
);
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.logs,
aggregateOperator: LogsAggregatorOperator.NOOP,
expression: fullExpression,
filter: { expression: fullExpression },
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const fullExpression = combineInitialAndUserExpression(
logsAndTracesInitialExpression,
userTracesExpression || '',
);
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
expression: fullExpression,
filter: { expression: fullExpression },
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};
return (
<>
<div className="entity-detail-drawer__entity">
<div className="entity-details-grid">
<div className="labels-row">
{metadataConfig.map((config) => (
<Typography.Text
key={config.label}
color="muted"
className="entity-details-metadata-label"
>
{config.label}
</Typography.Text>
))}
</div>
<div className="values-row">
{metadataConfig.map((config) => {
const value = config.getValue(entity);
const displayValue = String(value);
return (
<Typography.Text
key={config.label}
className="entity-details-metadata-value"
>
{config.render ? (
config.render(value, entity)
) : (
<Tooltip title={displayValue}>{displayValue}</Tooltip>
)}
</Typography.Text>
);
})}
</div>
</div>
{countsConfig &&
countsConfig.length > 0 &&
selectedItem &&
getCountsFilterExpression && (
<EntityCountsSection
entity={entity}
countsConfig={countsConfig}
selectedItem={selectedItem}
filterExpression={getCountsFilterExpression(entity)}
closeDrawer={handleClose}
/>
)}
</div>
{!hideDetailViewTabs && (
<div className="views-tabs-container">
<ToggleGroupSimple
type="single"
className="views-tabs"
onChange={handleTabChange}
value={selectedView}
items={[
...(tabVisibility.showMetrics
? [
{
value: VIEW_TYPES.METRICS,
label: (
<div className="view-title">
<BarChart size={14} />
Metrics
</div>
),
},
]
: []),
...(tabVisibility.showLogs
? [
{
value: VIEW_TYPES.LOGS,
label: (
<div className="view-title">
<ScrollText size={14} />
Logs
</div>
),
},
]
: []),
...(tabVisibility.showTraces
? [
{
value: VIEW_TYPES.TRACES,
label: (
<div className="view-title">
<DraftingCompass size={14} />
Traces
</div>
),
},
]
: []),
...(tabVisibility.showEvents
? [
{
value: VIEW_TYPES.EVENTS,
label: (
<div className="view-title">
<ChevronsLeftRight size={14} />
Events
</div>
),
},
]
: []),
...(customTabs?.map((tab) => ({
value: tab.key,
label: (
<div className="view-title">
{tab.icon}
{tab.label}
</div>
),
})) ?? []),
]}
/>
{selectedView === VIEW_TYPES.LOGS && (
<Tooltip title="Go to Logs Explorer" placement="left">
<Button
icon={<Compass size={18} />}
className="compass-button"
onClick={handleExplorePagesRedirect}
/>
</Tooltip>
)}
{selectedView === VIEW_TYPES.TRACES && (
<Tooltip title="Go to Traces Explorer" placement="left">
<Button
icon={<Compass size={18} />}
className="compass-button"
onClick={handleExplorePagesRedirect}
/>
</Tooltip>
)}
</div>
)}
{effectiveView === VIEW_TYPES.METRICS && (
<EntityMetrics<T>
entity={entity}
eventEntity={eventCategory}
entityWidgetInfo={entityWidgetInfo}
getEntityQueryPayload={getEntityQueryPayload}
category={category}
queryKey={`${queryKeyPrefix}Metrics`}
/>
)}
{effectiveView === VIEW_TYPES.LOGS && (
<EntityLogs
eventEntity={eventCategory}
queryKey={`${queryKeyPrefix}Logs`}
category={category}
initialExpression={logsAndTracesInitialExpression}
/>
)}
{effectiveView === VIEW_TYPES.TRACES && (
<EntityTraces
eventEntity={eventCategory}
queryKey={`${queryKeyPrefix}Traces`}
category={category}
initialExpression={logsAndTracesInitialExpression}
/>
)}
{effectiveView === VIEW_TYPES.EVENTS && tabVisibility.showEvents && (
<EntityEvents
eventEntity={eventCategory}
queryKey={`${queryKeyPrefix}Events`}
initialExpression={eventsInitialExpression}
category={category}
/>
)}
{customTabs?.map((tab) =>
selectedView === tab.key ? (
<Fragment key={tab.key}>
{tab.render({
entity,
timeRange,
selectedInterval,
handleTimeChange,
})}
</Fragment>
) : null,
)}
</>
);
}

View File

@@ -74,15 +74,14 @@ export type K8sBaseListProps<
warning?: Querybuildertypesv5QueryWarnDataDTO | null;
}>;
/** Function to get the unique key for a row. */
getRowKey: (record: T) => string;
getRowKey?: (record: T) => string;
/** Function to get the item key used for selection. Can return string or SelectedItemParams. */
getItemKey: (record: T) => TItemKey;
getItemKey?: (record: T) => TItemKey;
eventCategory: InfraMonitoringEvents;
renderEmptyState?: (
context: K8sBaseListEmptyStateContext,
) => React.ReactNode | null;
extraQueryKeyParts?: string[];
detailsQueryKeyPrefix: string;
};
export function K8sBaseList<
@@ -99,7 +98,6 @@ export function K8sBaseList<
eventCategory,
renderEmptyState,
extraQueryKeyParts = [],
detailsQueryKeyPrefix,
}: K8sBaseListProps<T, TItemKey>): JSX.Element {
const { currentQuery } = useQueryBuilder();
const expression = currentQuery.builder.queryData[0]?.filter?.expression || '';
@@ -236,29 +234,17 @@ export function K8sBaseList<
}, [eventCategory, totalCount]);
const handleRowClick = useCallback(
(record: T, itemKey: TItemKey): void => {
(_record: T, itemKey: TItemKey): void => {
if (groupBy.length === 0) {
const params: SelectedItemParams =
typeof itemKey === 'object'
? itemKey
: {
selectedItem: itemKey,
clusterName: null,
namespaceName: null,
};
if (detailsQueryKeyPrefix) {
const detailQueryKey = getAutoRefreshQueryKey(
selectedTime,
`${detailsQueryKeyPrefix}EntityDetails`,
params.selectedItem,
params.clusterName,
params.namespaceName,
);
queryClient.setQueryData(detailQueryKey, { data: record });
if (typeof itemKey === 'object' && itemKey !== null) {
setSelectedItemParams(itemKey);
} else {
setSelectedItemParams({
selectedItem: itemKey,
clusterName: null,
namespaceName: null,
});
}
setSelectedItemParams(params);
}
void logEvent(InfraMonitoringEvents.ItemClicked, {
@@ -267,15 +253,7 @@ export function K8sBaseList<
category: eventCategory,
});
},
[
eventCategory,
groupBy.length,
setSelectedItemParams,
detailsQueryKeyPrefix,
getAutoRefreshQueryKey,
selectedTime,
queryClient,
],
[eventCategory, groupBy.length, setSelectedItemParams],
);
const handleRowClickNewTab = useCallback(
@@ -425,7 +403,7 @@ export function K8sBaseList<
onRowClickNewTab={handleRowClickNewTab}
renderExpandedRow={isGroupedByAttribute ? renderExpandedRow : undefined}
getRowCanExpand={isGroupedByAttribute ? getRowCanExpand : undefined}
className={cx(styles.k8SListTable)}
className={cx(styles.k8SListTable, expandedRowColumns)}
enableQueryParams={{
page: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE,
limit: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE_SIZE,

View File

@@ -1,43 +0,0 @@
import { useMemo } from 'react';
import { useInfraMonitoringCategory } from '../hooks';
import { getEntityConfig } from './entity.registry';
import { K8sBaseList } from './K8sBaseList';
import K8sBaseDetails from './K8sBaseDetails';
export interface K8sDynamicListProps {
controlListPrefix?: React.ReactNode;
leftFilters?: React.ReactNode;
}
export function K8sDynamicList({
controlListPrefix,
leftFilters,
}: K8sDynamicListProps): JSX.Element | null {
const [selectedCategory] = useInfraMonitoringCategory();
const config = useMemo(
() => getEntityConfig(selectedCategory),
[selectedCategory],
);
if (!config) {
return null;
}
const { list, details } = config;
return (
<>
<K8sBaseList
{...list}
controlListPrefix={controlListPrefix}
leftFilters={leftFilters}
/>
<K8sBaseDetails {...details} />
</>
);
}
export default K8sDynamicList;

View File

@@ -245,7 +245,6 @@ function K8sHeader<TData>({
showAutoRefresh={showAutoRefresh}
showRefreshText={false}
hideShareModal
defaultRelativeTime="30m"
/>
</div>

View File

@@ -1,6 +1,5 @@
import { Box } from '@signozhq/icons';
import { screen } from '@testing-library/react';
import { InfraMonitoringEvents } from 'constants/events';
import userEvent from '@testing-library/user-event';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { act, render, waitFor } from 'tests/test-utils';
@@ -32,7 +31,7 @@ const mockEntity: TestEntity = {
function createBaseProps() {
return {
category: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
eventCategory: 'Pod',
getSelectedItemExpression: (): string => 'k8s.pod.name = "test-pod"',
fetchEntityData: jest
.fn()

View File

@@ -175,10 +175,8 @@ function renderComponent<
>({
queryParams,
onUrlUpdate,
detailsQueryKeyPrefix = 'testEntity',
...props
}: Omit<K8sBaseListProps<T, TItemKey>, 'detailsQueryKeyPrefix'> & {
detailsQueryKeyPrefix?: string;
}: K8sBaseListProps<T, TItemKey> & {
queryParams?: Record<string, string>;
onUrlUpdate?: OnUrlUpdateFunction;
}) {
@@ -205,10 +203,7 @@ function renderComponent<
value={{ viewportHeight: 800, itemHeight: 50 }}
>
<TooltipProvider>
<K8sBaseList<T, TItemKey>
{...props}
detailsQueryKeyPrefix={detailsQueryKeyPrefix}
/>
<K8sBaseList<T, TItemKey> {...props} />
</TooltipProvider>
</VirtuosoMockContext.Provider>
</NuqsTestingAdapter>

View File

@@ -52,6 +52,7 @@ export function EntityCountsSection<T>({
},
};
// TODO(H4ad): After https://github.com/SigNoz/signoz/pull/12038, inherit custom time of drawer to list
const urlParams = new URLSearchParams();
urlParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.CATEGORY, targetCategory);
urlParams.set(
@@ -59,44 +60,6 @@ export function EntityCountsSection<T>({
encodeURIComponent(JSON.stringify(compositeQuery)),
);
const currentSearchParams = new URLSearchParams(window.location.search);
const detailRelativeTime = currentSearchParams.get(
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_RELATIVE_TIME,
);
const detailStartTime = currentSearchParams.get(
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_START_TIME,
);
const detailEndTime = currentSearchParams.get(
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_END_TIME,
);
const listRelativeTime = currentSearchParams.get(QueryParams.relativeTime);
const listStartTime = currentSearchParams.get(QueryParams.startTime);
const listEndTime = currentSearchParams.get(QueryParams.endTime);
if (listRelativeTime) {
urlParams.set(QueryParams.relativeTime, listRelativeTime);
} else if (listStartTime && listEndTime) {
urlParams.set(QueryParams.startTime, listStartTime);
urlParams.set(QueryParams.endTime, listEndTime);
}
if (detailRelativeTime) {
urlParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_RELATIVE_TIME,
detailRelativeTime,
);
} else if (detailStartTime && detailEndTime) {
urlParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_START_TIME,
detailStartTime,
);
urlParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_END_TIME,
detailEndTime,
);
}
return `${ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES}?${urlParams.toString()}`;
};

View File

@@ -1,21 +0,0 @@
import { SelectedItemParams } from '../hooks';
import { K8sBaseListProps } from './K8sBaseList';
import { K8sBaseDetailsProps } from './types';
export type K8sEntityData = { meta?: Record<string, string> | null };
export type K8sEntityListConfig<
T extends K8sEntityData,
TItemKey extends string | SelectedItemParams = string,
> = Omit<K8sBaseListProps<T, TItemKey>, 'controlListPrefix' | 'leftFilters'>;
export type K8sEntityDetailsConfig<T extends K8sEntityData> =
K8sBaseDetailsProps<T>;
export interface K8sEntityConfig<
T extends K8sEntityData,
TItemKey extends string | SelectedItemParams = string,
> {
list: K8sEntityListConfig<T, TItemKey>;
details: K8sEntityDetailsConfig<T>;
}

View File

@@ -1,41 +0,0 @@
import { K8sCategories } from '../constants';
import { SelectedItemParams } from '../hooks';
import { K8sEntityConfig, K8sEntityData } from './entity.config.types';
import { podEntityConfig } from '../Pods/entity.config';
import { nodeEntityConfig } from '../Nodes/entity.config';
import { clusterEntityConfig } from '../Clusters/entity.config';
import { deploymentEntityConfig } from '../Deployments/entity.config';
import { namespaceEntityConfig } from '../Namespaces/entity.config';
import { jobEntityConfig } from '../Jobs/entity.config';
import { daemonSetEntityConfig } from '../DaemonSets/entity.config';
import { statefulSetEntityConfig } from '../StatefulSets/entity.config';
import { volumeEntityConfig } from '../Volumes/entity.config';
type AnyEntityConfig = K8sEntityConfig<
K8sEntityData,
string | SelectedItemParams
>;
function registerConfig<
T extends K8sEntityData,
TItemKey extends string | SelectedItemParams,
>(config: K8sEntityConfig<T, TItemKey>): AnyEntityConfig {
return config as unknown as AnyEntityConfig;
}
export const entityRegistry: Record<string, AnyEntityConfig> = {
[K8sCategories.PODS]: registerConfig(podEntityConfig),
[K8sCategories.NODES]: registerConfig(nodeEntityConfig),
[K8sCategories.CLUSTERS]: registerConfig(clusterEntityConfig),
[K8sCategories.DEPLOYMENTS]: registerConfig(deploymentEntityConfig),
[K8sCategories.NAMESPACES]: registerConfig(namespaceEntityConfig),
[K8sCategories.JOBS]: registerConfig(jobEntityConfig),
[K8sCategories.DAEMONSETS]: registerConfig(daemonSetEntityConfig),
[K8sCategories.STATEFULSETS]: registerConfig(statefulSetEntityConfig),
[K8sCategories.VOLUMES]: registerConfig(volumeEntityConfig),
};
export function getEntityConfig(category: string): AnyEntityConfig | undefined {
return entityRegistry[category];
}

View File

@@ -1,18 +1,3 @@
import { ReactNode } from 'react';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/types';
import { InfraMonitoringEvents } from 'constants/events';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import APIError from 'types/api/error';
import { EntityCountConfig } from './components/EntityCountsSection/EntityCountsSection';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
export type K8sDetailsCountConfig<T> = EntityCountConfig<T>;
export type K8sBaseFilters = {
filter: {
expression: string;
@@ -48,100 +33,3 @@ export type K8sTableRowData<T> = T & {
/** Metadata about which attributes were used for grouping */
groupedByMeta?: Record<string, string>;
};
export interface K8sDetailsMetadataConfig<T> {
label: string;
getValue: (entity: T) => string | number;
render?: (value: string | number, entity: T) => ReactNode;
}
export interface K8sDetailsFilters {
filter: { expression: string };
start: number;
end: number;
}
export interface K8sDetailsWidgetInfo {
title: string;
yAxisUnit: string;
}
export type GetEntityQueryPayload<T> = (
entity: T,
start: number,
end: number,
dotMetricsEnabled: boolean,
) => GetQueryResultsProps[];
export interface K8sDetailsTabsConfig {
showMetrics?: boolean;
showLogs?: boolean;
showTraces?: boolean;
showEvents?: boolean;
}
export interface K8sDetailsCustomTabRenderProps<T> {
entity: T;
/** Time range in seconds — see useEntityDetailsTime */
timeRange: { startTime: number; endTime: number };
selectedInterval: Time;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
}
export interface K8sDetailsCustomTab<T> {
key: string;
label: string;
icon: ReactNode;
render: (props: K8sDetailsCustomTabRenderProps<T>) => ReactNode;
}
export interface K8sBaseDetailsProps<T> {
category: InfraMonitoringEntity;
eventCategory: InfraMonitoringEvents;
// Data fetching configuration
getSelectedItemExpression: (params: SelectedItemParams) => string;
fetchEntityData: (
filters: K8sDetailsFilters,
signal?: AbortSignal,
) => Promise<{ data: T | null; error?: APIError | null }>;
// Entity configuration
getEntityName: (entity: T) => string;
getInitialLogTracesExpression: (entity: T) => string;
getInitialEventsExpression: (entity: T) => string;
metadataConfig: K8sDetailsMetadataConfig<T>[];
countsConfig?: K8sDetailsCountConfig<T>[];
getCountsFilterExpression?: (entity: T) => string;
entityWidgetInfo: K8sDetailsWidgetInfo[];
getEntityQueryPayload: GetEntityQueryPayload<T>;
queryKeyPrefix: string;
/** When true, only metrics are shown and the Metrics/Logs/Traces/Events tab bar is hidden. */
hideDetailViewTabs?: boolean;
tabsConfig?: K8sDetailsTabsConfig;
customTabs?: K8sDetailsCustomTab<T>[];
}
export interface K8sBaseDetailsContentProps<T> {
entity: T;
category: InfraMonitoringEntity;
eventCategory: InfraMonitoringEvents;
metadataConfig: K8sDetailsMetadataConfig<T>[];
countsConfig?: K8sDetailsCountConfig<T>[];
getCountsFilterExpression?: (entity: T) => string;
selectedItem: string | null;
handleClose: () => void;
entityWidgetInfo: K8sDetailsWidgetInfo[];
getEntityQueryPayload: GetEntityQueryPayload<T>;
queryKeyPrefix: string;
hideDetailViewTabs: boolean;
tabsConfig?: K8sDetailsTabsConfig;
customTabs?: K8sDetailsCustomTab<T>[];
logsAndTracesInitialExpression: string;
eventsInitialExpression: string;
}
// Aliases for backward compatibility
export type CustomTab<T> = K8sDetailsCustomTab<T>;
export type CustomTabRenderProps<T> = K8sDetailsCustomTabRenderProps<T>;

View File

@@ -0,0 +1,153 @@
import { useCallback } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listClusters } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesClusterRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import {
clusterWidgetInfo,
getClusterMetricsQueryPayload,
k8sClusterDetailsCountsConfig,
k8sClusterDetailsMetadataConfig,
k8sClusterGetCountsFilterExpression,
k8sClusterGetEntityName,
k8sClusterGetSelectedItemExpression,
k8sClusterInitialEventsExpression,
k8sClusterInitialLogTracesExpression,
} from './constants';
import {
getK8sClusterItemKey,
getK8sClusterRowKey,
k8sClustersColumnsConfig,
} from './table.config';
function K8sClustersList({
controlListPrefix,
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
try {
const response = await listClusters(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
warning: data.warning,
};
} catch (error) {
return {
type: 'list' as const,
records: [] as InframonitoringtypesClusterRecordDTO[],
total: 0,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesClusterRecordDTO | null;
error?: APIError | null;
}> => {
try {
const response = await listClusters(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
return {
data: null,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
return (
<>
<K8sBaseList<InframonitoringtypesClusterRecordDTO>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.CLUSTERS}
tableColumns={k8sClustersColumnsConfig}
fetchListData={fetchListData}
getRowKey={getK8sClusterRowKey}
getItemKey={getK8sClusterItemKey}
eventCategory={InfraMonitoringEvents.Cluster}
/>
<K8sBaseDetails<InframonitoringtypesClusterRecordDTO>
category={InfraMonitoringEntity.CLUSTERS}
eventCategory={InfraMonitoringEvents.Cluster}
getSelectedItemExpression={k8sClusterGetSelectedItemExpression}
fetchEntityData={fetchEntityData}
getEntityName={k8sClusterGetEntityName}
getInitialLogTracesExpression={k8sClusterInitialLogTracesExpression}
getInitialEventsExpression={k8sClusterInitialEventsExpression}
metadataConfig={k8sClusterDetailsMetadataConfig}
countsConfig={k8sClusterDetailsCountsConfig}
getCountsFilterExpression={k8sClusterGetCountsFilterExpression}
entityWidgetInfo={clusterWidgetInfo}
getEntityQueryPayload={getClusterMetricsQueryPayload}
queryKeyPrefix="cluster"
/>
</>
);
}
export default K8sClustersList;

View File

@@ -1,138 +0,0 @@
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listClusters } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesClusterRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
RenderErrorResponseDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import { InfraMonitoringEvents } from 'constants/events';
import { K8sEntityConfig } from '../Base/entity.config.types';
import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import {
clusterWidgetInfo,
getClusterMetricsQueryPayload,
k8sClusterDetailsCountsConfig,
k8sClusterDetailsMetadataConfig,
k8sClusterGetCountsFilterExpression,
k8sClusterGetEntityName,
k8sClusterGetSelectedItemExpression,
k8sClusterInitialEventsExpression,
k8sClusterInitialLogTracesExpression,
} from './constants';
import {
getK8sClusterItemKey,
getK8sClusterRowKey,
k8sClustersColumnsConfig,
} from './table.config';
async function fetchListData(
filters: K8sBaseFilters,
signal?: AbortSignal,
): ReturnType<
K8sEntityConfig<InframonitoringtypesClusterRecordDTO>['list']['fetchListData']
> {
try {
const response = await listClusters(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
warning: data.warning,
};
} catch (error) {
return {
type: 'list' as const,
records: [] as InframonitoringtypesClusterRecordDTO[],
total: 0,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
}
async function fetchEntityData(
filters: K8sDetailsFilters,
signal?: AbortSignal,
): ReturnType<
K8sEntityConfig<InframonitoringtypesClusterRecordDTO>['details']['fetchEntityData']
> {
try {
const response = await listClusters(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
return {
data: null,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
}
export const clusterEntityConfig: K8sEntityConfig<InframonitoringtypesClusterRecordDTO> =
{
list: {
entity: InfraMonitoringEntity.CLUSTERS,
eventCategory: InfraMonitoringEvents.Cluster,
tableColumns: k8sClustersColumnsConfig,
fetchListData,
getRowKey: getK8sClusterRowKey,
getItemKey: getK8sClusterItemKey,
detailsQueryKeyPrefix: 'cluster',
},
details: {
category: InfraMonitoringEntity.CLUSTERS,
eventCategory: InfraMonitoringEvents.Cluster,
queryKeyPrefix: 'cluster',
getSelectedItemExpression: k8sClusterGetSelectedItemExpression,
fetchEntityData,
getEntityName: k8sClusterGetEntityName,
getInitialLogTracesExpression: k8sClusterInitialLogTracesExpression,
getInitialEventsExpression: k8sClusterInitialEventsExpression,
metadataConfig: k8sClusterDetailsMetadataConfig,
entityWidgetInfo: clusterWidgetInfo,
getEntityQueryPayload: getClusterMetricsQueryPayload,
countsConfig: k8sClusterDetailsCountsConfig,
getCountsFilterExpression: k8sClusterGetCountsFilterExpression,
},
};

View File

@@ -93,14 +93,13 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
row.nodeCountsByReadiness,
width: { min: 180 },
enableSort: false,
cell: ({ row, rowId }): React.ReactNode => {
cell: ({ row }): React.ReactNode => {
if (!row.nodeCountsByReadiness) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts
rowId={rowId}
items={[
{
value: row.nodeCountsByReadiness.ready,
@@ -130,16 +129,13 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
row.podCountsByStatus,
width: { min: 250 },
enableSort: false,
cell: ({ row, rowId }): React.ReactNode => {
cell: ({ row }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts
rowId={rowId}
items={getPodStatusItems(row.podCountsByStatus)}
/>
<GroupedStatusCounts items={getPodStatusItems(row.podCountsByStatus)} />
);
},
},

View File

@@ -0,0 +1,158 @@
import { useCallback, useMemo } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listDaemonSets } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesDaemonSetRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
daemonSetWidgetInfo,
getDaemonSetMetricsQueryPayload,
getDaemonSetPodMetricsQueryPayload,
k8sDaemonSetDetailsMetadataConfig,
k8sDaemonSetGetEntityName,
k8sDaemonSetGetSelectedItemExpression,
k8sDaemonSetInitialEventsExpression,
k8sDaemonSetInitialLogTracesExpression,
} from './constants';
import {
getK8sDaemonSetItemKey,
getK8sDaemonSetRowKey,
k8sDaemonSetsColumnsConfig,
} from './table.config';
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
function K8sDaemonSetsList({
controlListPrefix,
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
try {
const response = await listDaemonSets(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
warning: data.warning,
};
} catch (error) {
return {
type: 'list' as const,
records: [] as InframonitoringtypesDaemonSetRecordDTO[],
total: 0,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesDaemonSetRecordDTO | null;
error?: APIError | null;
}> => {
try {
const response = await listDaemonSets(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
return {
data: null,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
const customTabs = useMemo(
() => [
createPodMetricsTab<InframonitoringtypesDaemonSetRecordDTO>({
getQueryPayload: getDaemonSetPodMetricsQueryPayload,
category: InfraMonitoringEntity.DAEMONSETS,
queryKey: 'daemonSetPodMetrics',
}),
],
[],
);
return (
<>
<K8sBaseList<InframonitoringtypesDaemonSetRecordDTO, SelectedItemParams>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.DAEMONSETS}
tableColumns={k8sDaemonSetsColumnsConfig}
fetchListData={fetchListData}
getRowKey={getK8sDaemonSetRowKey}
getItemKey={getK8sDaemonSetItemKey}
eventCategory={InfraMonitoringEvents.DaemonSet}
/>
<K8sBaseDetails<InframonitoringtypesDaemonSetRecordDTO>
category={InfraMonitoringEntity.DAEMONSETS}
eventCategory={InfraMonitoringEvents.DaemonSet}
getSelectedItemExpression={k8sDaemonSetGetSelectedItemExpression}
fetchEntityData={fetchEntityData}
getEntityName={k8sDaemonSetGetEntityName}
getInitialLogTracesExpression={k8sDaemonSetInitialLogTracesExpression}
getInitialEventsExpression={k8sDaemonSetInitialEventsExpression}
metadataConfig={k8sDaemonSetDetailsMetadataConfig}
entityWidgetInfo={daemonSetWidgetInfo}
getEntityQueryPayload={getDaemonSetMetricsQueryPayload}
queryKeyPrefix="daemonset"
customTabs={customTabs}
/>
</>
);
}
export default K8sDaemonSetsList;

View File

@@ -1,153 +0,0 @@
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listDaemonSets } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesDaemonSetRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
RenderErrorResponseDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import { InfraMonitoringEvents } from 'constants/events';
import { K8sEntityConfig } from '../Base/entity.config.types';
import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { createPodMetricsTab } from '../EntityDetailsUtils/createPodMetricsTab';
import { SelectedItemParams } from '../hooks';
import {
daemonSetWidgetInfo,
getDaemonSetMetricsQueryPayload,
getDaemonSetPodMetricsQueryPayload,
k8sDaemonSetDetailsMetadataConfig,
k8sDaemonSetGetEntityName,
k8sDaemonSetGetSelectedItemExpression,
k8sDaemonSetInitialEventsExpression,
k8sDaemonSetInitialLogTracesExpression,
} from './constants';
import {
getK8sDaemonSetItemKey,
getK8sDaemonSetRowKey,
k8sDaemonSetsColumnsConfig,
} from './table.config';
async function fetchListData(
filters: K8sBaseFilters,
signal?: AbortSignal,
): ReturnType<
K8sEntityConfig<
InframonitoringtypesDaemonSetRecordDTO,
SelectedItemParams
>['list']['fetchListData']
> {
try {
const response = await listDaemonSets(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
warning: data.warning,
};
} catch (error) {
return {
type: 'list' as const,
records: [] as InframonitoringtypesDaemonSetRecordDTO[],
total: 0,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
}
async function fetchEntityData(
filters: K8sDetailsFilters,
signal?: AbortSignal,
): ReturnType<
K8sEntityConfig<
InframonitoringtypesDaemonSetRecordDTO,
SelectedItemParams
>['details']['fetchEntityData']
> {
try {
const response = await listDaemonSets(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
return {
data: null,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
}
export const daemonSetEntityConfig: K8sEntityConfig<
InframonitoringtypesDaemonSetRecordDTO,
SelectedItemParams
> = {
list: {
entity: InfraMonitoringEntity.DAEMONSETS,
eventCategory: InfraMonitoringEvents.DaemonSet,
tableColumns: k8sDaemonSetsColumnsConfig,
fetchListData,
getRowKey: getK8sDaemonSetRowKey,
getItemKey: getK8sDaemonSetItemKey,
detailsQueryKeyPrefix: 'daemonset',
},
details: {
category: InfraMonitoringEntity.DAEMONSETS,
eventCategory: InfraMonitoringEvents.DaemonSet,
queryKeyPrefix: 'daemonset',
getSelectedItemExpression: k8sDaemonSetGetSelectedItemExpression,
fetchEntityData,
getEntityName: k8sDaemonSetGetEntityName,
getInitialLogTracesExpression: k8sDaemonSetInitialLogTracesExpression,
getInitialEventsExpression: k8sDaemonSetInitialEventsExpression,
metadataConfig: k8sDaemonSetDetailsMetadataConfig,
entityWidgetInfo: daemonSetWidgetInfo,
getEntityQueryPayload: getDaemonSetMetricsQueryPayload,
customTabs: [
createPodMetricsTab<InframonitoringtypesDaemonSetRecordDTO>({
getQueryPayload: getDaemonSetPodMetricsQueryPayload,
category: InfraMonitoringEntity.DAEMONSETS,
queryKey: 'daemonSetPodMetrics',
docBasePath: '/infrastructure-monitoring/kubernetes/daemonsets/',
}),
],
},
};

View File

@@ -121,17 +121,12 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
width: { min: 250 },
enableSort: false,
enableResize: true,
cell: ({ row, rowId }): React.ReactNode => {
cell: ({ row }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts
rowId={rowId}
items={getPodStatusItems(podCountsByStatus)}
/>
);
return <GroupedStatusCounts items={getPodStatusItems(podCountsByStatus)} />;
},
},
{
@@ -145,9 +140,8 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
width: { min: 210 },
enableSort: false,
enableResize: true,
cell: ({ row, rowId }): React.ReactNode => (
cell: ({ row }): React.ReactNode => (
<GroupedStatusCounts
rowId={rowId}
items={[
{
value: row.readyNodes,

View File

@@ -0,0 +1,164 @@
import { useCallback, useMemo } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listDeployments } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesDeploymentRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
deploymentWidgetInfo,
getDeploymentMetricsQueryPayload,
getDeploymentPodMetricsQueryPayload,
k8sDeploymentDetailsMetadataConfig,
k8sDeploymentGetEntityName,
k8sDeploymentGetSelectedItemExpression,
k8sDeploymentInitialEventsExpression,
k8sDeploymentInitialLogTracesExpression,
} from './constants';
import {
getK8sDeploymentItemKey,
getK8sDeploymentRowKey,
k8sDeploymentsColumnsConfig,
} from './table.config';
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
function K8sDeploymentsList({
controlListPrefix,
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
try {
const response = await listDeployments(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
warning: data.warning,
};
} catch (error) {
return {
type: 'list' as const,
records: [] as InframonitoringtypesDeploymentRecordDTO[],
total: 0,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesDeploymentRecordDTO | null;
error?: APIError | null;
}> => {
try {
const response = await listDeployments(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
return {
data: null,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
const customTabs = useMemo(
() => [
createPodMetricsTab<InframonitoringtypesDeploymentRecordDTO>({
getQueryPayload: getDeploymentPodMetricsQueryPayload,
category: InfraMonitoringEntity.DEPLOYMENTS,
queryKey: 'deploymentPodMetrics',
}),
],
[],
);
return (
<>
<K8sBaseList<InframonitoringtypesDeploymentRecordDTO, SelectedItemParams>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.DEPLOYMENTS}
tableColumns={k8sDeploymentsColumnsConfig}
fetchListData={fetchListData}
getRowKey={getK8sDeploymentRowKey}
getItemKey={getK8sDeploymentItemKey}
eventCategory={InfraMonitoringEvents.Deployment}
/>
<K8sBaseDetails<InframonitoringtypesDeploymentRecordDTO>
category={InfraMonitoringEntity.DEPLOYMENTS}
eventCategory={InfraMonitoringEvents.Deployment}
getSelectedItemExpression={k8sDeploymentGetSelectedItemExpression}
fetchEntityData={fetchEntityData}
getEntityName={k8sDeploymentGetEntityName}
getInitialLogTracesExpression={k8sDeploymentInitialLogTracesExpression}
getInitialEventsExpression={k8sDeploymentInitialEventsExpression}
metadataConfig={k8sDeploymentDetailsMetadataConfig}
entityWidgetInfo={deploymentWidgetInfo}
getEntityQueryPayload={getDeploymentMetricsQueryPayload}
queryKeyPrefix="deployment"
customTabs={customTabs}
/>
</>
);
}
export default K8sDeploymentsList;

View File

@@ -1,161 +0,0 @@
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listDeployments } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesDeploymentRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
RenderErrorResponseDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import { InfraMonitoringEvents } from 'constants/events';
import { K8sEntityConfig } from '../Base/entity.config.types';
import {
K8sBaseFilters,
K8sDetailsCustomTab,
K8sDetailsFilters,
} from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { createPodMetricsTab } from '../EntityDetailsUtils/createPodMetricsTab';
import { SelectedItemParams } from '../hooks';
import {
deploymentWidgetInfo,
getDeploymentMetricsQueryPayload,
getDeploymentPodMetricsQueryPayload,
k8sDeploymentDetailsMetadataConfig,
k8sDeploymentGetEntityName,
k8sDeploymentGetSelectedItemExpression,
k8sDeploymentInitialEventsExpression,
k8sDeploymentInitialLogTracesExpression,
} from './constants';
import {
getK8sDeploymentItemKey,
getK8sDeploymentRowKey,
k8sDeploymentsColumnsConfig,
} from './table.config';
async function fetchListData(
filters: K8sBaseFilters,
signal?: AbortSignal,
): ReturnType<
K8sEntityConfig<
InframonitoringtypesDeploymentRecordDTO,
SelectedItemParams
>['list']['fetchListData']
> {
try {
const response = await listDeployments(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
warning: data.warning,
};
} catch (error) {
return {
type: 'list' as const,
records: [] as InframonitoringtypesDeploymentRecordDTO[],
total: 0,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
}
async function fetchEntityData(
filters: K8sDetailsFilters,
signal?: AbortSignal,
): ReturnType<
K8sEntityConfig<
InframonitoringtypesDeploymentRecordDTO,
SelectedItemParams
>['details']['fetchEntityData']
> {
try {
const response = await listDeployments(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
return {
data: null,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
}
export function createCustomTabs(): K8sDetailsCustomTab<InframonitoringtypesDeploymentRecordDTO>[] {
return [
createPodMetricsTab<InframonitoringtypesDeploymentRecordDTO>({
getQueryPayload: getDeploymentPodMetricsQueryPayload,
category: InfraMonitoringEntity.DEPLOYMENTS,
queryKey: 'deploymentPodMetrics',
docBasePath: '/infrastructure-monitoring/kubernetes/deployments/',
}),
];
}
export const deploymentEntityConfig: K8sEntityConfig<
InframonitoringtypesDeploymentRecordDTO,
SelectedItemParams
> = {
list: {
entity: InfraMonitoringEntity.DEPLOYMENTS,
eventCategory: InfraMonitoringEvents.Deployment,
tableColumns: k8sDeploymentsColumnsConfig,
fetchListData,
getRowKey: getK8sDeploymentRowKey,
getItemKey: getK8sDeploymentItemKey,
detailsQueryKeyPrefix: 'deployment',
},
details: {
category: InfraMonitoringEntity.DEPLOYMENTS,
eventCategory: InfraMonitoringEvents.Deployment,
queryKeyPrefix: 'deployment',
getSelectedItemExpression: k8sDeploymentGetSelectedItemExpression,
fetchEntityData,
getEntityName: k8sDeploymentGetEntityName,
getInitialLogTracesExpression: k8sDeploymentInitialLogTracesExpression,
getInitialEventsExpression: k8sDeploymentInitialEventsExpression,
metadataConfig: k8sDeploymentDetailsMetadataConfig,
entityWidgetInfo: deploymentWidgetInfo,
getEntityQueryPayload: getDeploymentMetricsQueryPayload,
customTabs: createCustomTabs(),
},
};

View File

@@ -118,17 +118,12 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
width: { min: 250 },
enableSort: false,
enableResize: true,
cell: ({ row, rowId }): React.ReactNode => {
cell: ({ row }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts
rowId={rowId}
items={getPodStatusItems(podCountsByStatus)}
/>
);
return <GroupedStatusCounts items={getPodStatusItems(podCountsByStatus)} />;
},
},
{
@@ -142,9 +137,8 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
width: { min: 180 },
enableSort: false,
enableResize: true,
cell: ({ row, rowId }): React.ReactNode => (
cell: ({ row }): React.ReactNode => (
<GroupedStatusCounts
rowId={rowId}
items={[
{
value: row.availablePods,

View File

@@ -1,78 +0,0 @@
import { useCallback } from 'react';
import { Undo } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import logEvent from 'api/common/logEvent';
import { InfraMonitoringEvents } from 'constants/events';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useEntityDetailsTime } from './useEntityDetailsTime';
interface EntityDateTimeSelectorProps {
eventEntity: string;
category: InfraMonitoringEntity;
view: string;
}
function EntityDateTimeSelector({
eventEntity,
category,
view,
}: EntityDateTimeSelectorProps): JSX.Element {
const {
timeRange,
selectedInterval,
handleTimeChange,
handleResetToParentTime,
hasTimeChanged,
} = useEntityDetailsTime();
const onTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
void logEvent(InfraMonitoringEvents.TimeUpdated, {
entity: eventEntity,
page: InfraMonitoringEvents.DetailedPage,
category,
view,
interval,
});
handleTimeChange(interval, dateTimeRange);
},
[category, view, eventEntity, handleTimeChange],
);
return (
<div className="entity-date-time-selector">
{hasTimeChanged && (
<TooltipSimple title="Reset to list time" side="bottom">
<Button
variant="outlined"
color="secondary"
onClick={handleResetToParentTime}
data-testid="reset-to-list-time-button"
prefix={<Undo size={14} />}
>
Reset
</Button>
</TooltipSimple>
)}
<DateTimeSelectionV2
showAutoRefresh
showRefreshText={false}
hideShareModal
isModalTimeSelection
onTimeChange={onTimeChange}
modalSelectedInterval={selectedInterval}
modalInitialStartTime={timeRange.startTime * 1000}
modalInitialEndTime={timeRange.endTime * 1000}
/>
</div>
);
}
export default EntityDateTimeSelector;

View File

@@ -1,95 +0,0 @@
import { useCallback, useMemo } from 'react';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/types';
import {
createCustomTimeRange,
isCustomTimeRange,
NANO_SECOND_MULTIPLIER,
useGlobalTime,
} from 'store/globalTime';
export interface EntityDetailsTimeRange {
/** Unix timestamp in seconds — the unit API payload builders expect. Multiply by 1000 for ms-based UI (modals, URL params). */
startTime: number;
/** Unix timestamp in seconds — the unit API payload builders expect. Multiply by 1000 for ms-based UI (modals, URL params). */
endTime: number;
}
export interface UseEntityDetailsTimeResult {
/** Time range in seconds */
timeRange: EntityDetailsTimeRange;
selectedInterval: Time;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
handleResetToParentTime: () => boolean;
canResetToParent: boolean;
hasTimeChanged: boolean;
}
export function useEntityDetailsTime(): UseEntityDetailsTimeResult {
const selectedTime = useGlobalTime((s) => s.selectedTime);
const lastComputedMinMax = useGlobalTime((s) => s.lastComputedMinMax);
const setSelectedTime = useGlobalTime((s) => s.setSelectedTime);
const handleResetToParentTime = useGlobalTime((s) => s.resetToParentTime);
const parentStore = useGlobalTime((s) => s.parentStore);
const canResetToParent = !!parentStore;
const hasTimeChanged = useMemo(() => {
if (!parentStore) {
return false;
}
return selectedTime !== parentStore.getState().selectedTime;
}, [parentStore, selectedTime]);
const timeRange = useMemo<EntityDetailsTimeRange>(
() => ({
startTime: Math.floor(
lastComputedMinMax.minTime / NANO_SECOND_MULTIPLIER / 1000,
),
endTime: Math.floor(
lastComputedMinMax.maxTime / NANO_SECOND_MULTIPLIER / 1000,
),
}),
[lastComputedMinMax],
);
const selectedInterval = useMemo<Time>(
() =>
isCustomTimeRange(selectedTime)
? ('custom' as Time)
: (selectedTime as Time),
[selectedTime],
);
const handleTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
if (interval === 'custom' && dateTimeRange) {
// DateTimeSelector delivers milliseconds; the store keys custom
// ranges by nanoseconds.
setSelectedTime(
createCustomTimeRange(
dateTimeRange[0] * NANO_SECOND_MULTIPLIER,
dateTimeRange[1] * NANO_SECOND_MULTIPLIER,
),
);
} else {
setSelectedTime(interval as Time);
}
},
[setSelectedTime],
);
return {
timeRange,
selectedInterval,
handleTimeChange,
handleResetToParentTime,
canResetToParent,
hasTimeChanged,
};
}

View File

@@ -21,14 +21,17 @@ import Controls from 'container/Controls';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import LoadingContainer from 'container/InfraMonitoringK8sV2/LoadingContainer';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/types';
import { ChevronDown, ChevronRight } from '@signozhq/icons';
import { useQueryState } from 'nuqs';
import { DataSource } from 'types/common/queryBuilder';
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
import { validateQuery } from 'utils/queryValidationUtils';
import EntityDateTimeSelector from '../EntityDateTimeSelector/EntityDateTimeSelector';
import { useEntityDetailsTime } from '../EntityDateTimeSelector/useEntityDetailsTime';
import EntityEmptyState from '../EntityEmptyState/EntityEmptyState';
import EntityError from '../EntityError/EntityError';
import { EventContents } from './EventsContent';
@@ -50,7 +53,16 @@ interface EventDataType {
}
interface Props {
eventEntity: string;
timeRange: {
startTime: number;
endTime: number;
};
isModalTimeSelection: boolean;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
selectedInterval: Time;
queryKey: string;
category: InfraMonitoringEntity;
initialExpression: string;
@@ -65,11 +77,13 @@ const handleExpandRow = (record: EventDataType): JSX.Element => (
);
function EntityEventsContent({
eventEntity,
timeRange,
isModalTimeSelection,
handleTimeChange,
selectedInterval,
queryKey,
category,
}: Omit<Props, 'initialExpression'>): JSX.Element {
const { timeRange } = useEntityDetailsTime();
const expression = useExpression();
const inputExpression = useInputExpression();
const userExpression = useUserExpression();
@@ -117,8 +131,8 @@ function EntityEventsContent({
if (validation.isValid) {
querySearchOnRun(newUserExpression || '');
void logEvent(InfraMonitoringEvents.FilterApplied, {
entity: eventEntity,
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category,
view: InfraMonitoringEvents.EventsView,
@@ -127,14 +141,7 @@ function EntityEventsContent({
refetch();
}
},
[
inputExpression,
initialExpression,
refetch,
querySearchOnRun,
category,
eventEntity,
],
[inputExpression, initialExpression, refetch, querySearchOnRun, category],
);
const queryData = useMemo(
@@ -222,10 +229,16 @@ function EntityEventsContent({
<div className={styles.container}>
<div className={styles.filterContainer}>
<div className={styles.filterContainerTime}>
<EntityDateTimeSelector
eventEntity={eventEntity}
category={category}
view={InfraMonitoringEvents.EventsView}
<DateTimeSelectionV2
showAutoRefresh
showRefreshText={false}
hideShareModal
isModalTimeSelection={isModalTimeSelection}
onTimeChange={handleTimeChange}
defaultRelativeTime="5m"
modalSelectedInterval={selectedInterval}
modalInitialStartTime={timeRange.startTime * 1000}
modalInitialEndTime={timeRange.endTime * 1000}
/>
<RunQueryBtn

View File

@@ -29,25 +29,25 @@ function verifyEntityEventsV5Request({
expect(orderKeys).toContain('timestamp');
}
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
jest.mock('container/TopNav/DateTimeSelectionV2/index.tsx', () => ({
__esModule: true,
default: (): JSX.Element => (
<div data-testid="mock-datetime-selection">Date Time</div>
default: ({
onTimeChange,
}: {
onTimeChange?: (interval: string, dateTimeRange?: [number, number]) => void;
}): JSX.Element => (
<button
type="button"
data-testid="mock-datetime-selection"
onClick={(): void => {
onTimeChange?.('5m');
}}
>
Select Time
</button>
),
}));
jest.mock('../../EntityDateTimeSelector/useEntityDetailsTime', () => ({
useEntityDetailsTime: (): {
timeRange: { startTime: number; endTime: number };
selectedInterval: string;
handleTimeChange: jest.Mock;
} => ({
timeRange: { startTime: 1, endTime: 2 },
selectedInterval: '5m',
handleTimeChange: jest.fn(),
}),
}));
describe('EntityEvents', () => {
let capturedQueryRangePayloads: QueryRangePayloadV5[] = [];
@@ -69,7 +69,10 @@ describe('EntityEvents', () => {
searchParams={`${K8S_ENTITY_EVENTS_EXPRESSION_KEY}=k8s.pod.name+%3D+%22x%22`}
>
<EntityEvents
eventEntity="test"
timeRange={{ startTime: 1, endTime: 2 }}
isModalTimeSelection={false}
handleTimeChange={jest.fn()}
selectedInterval="5m"
queryKey="test"
category={InfraMonitoringEntity.PODS}
initialExpression='k8s.pod.name = "x"'

View File

@@ -1,81 +0,0 @@
import { mockFieldsAPIsWithEmptyResponse } from '__tests__/fields_api.util';
import { mockQueryRangeV5WithEventsResponse } from '__tests__/query_range_v5.util';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import {
createCustomTimeRange,
createGlobalTimeStore,
GlobalTimeContext,
NANO_SECOND_MULTIPLIER,
} from 'store/globalTime';
import { act, render, waitFor } from 'tests/test-utils';
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
import EntityEvents from '../EntityEvents';
import { K8S_ENTITY_EVENTS_EXPRESSION_KEY } from '../hooks';
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
__esModule: true,
default: (): JSX.Element => (
<div data-testid="mock-datetime-selection">Date Time</div>
),
}));
const START_MS = 1705315200000; // 2024-01-15T10:40:00Z
const END_MS = 1705318800000; // 2024-01-15T11:40:00Z
describe('EntityEvents time range wiring', () => {
let capturedPayloads: QueryRangePayloadV5[] = [];
beforeEach(() => {
capturedPayloads = [];
mockQueryRangeV5WithEventsResponse({
onReceiveRequest: async (req) => {
const body = (await req.json()) as QueryRangePayloadV5;
capturedPayloads.push(body);
return {};
},
});
mockFieldsAPIsWithEmptyResponse();
});
it('should query the V5 API with the global time range converted to milliseconds', async () => {
const store = createGlobalTimeStore();
store
.getState()
.setSelectedTime(
createCustomTimeRange(
START_MS * NANO_SECOND_MULTIPLIER,
END_MS * NANO_SECOND_MULTIPLIER,
),
);
const expression = 'k8s.pod.name = "test-pod"';
act(() => {
render(
<GlobalTimeContext.Provider value={store}>
<NuqsTestingAdapter
searchParams={`${K8S_ENTITY_EVENTS_EXPRESSION_KEY}=${encodeURIComponent(
expression,
)}`}
>
<EntityEvents
eventEntity="test"
queryKey="test"
category={InfraMonitoringEntity.PODS}
initialExpression={expression}
/>
</NuqsTestingAdapter>
</GlobalTimeContext.Provider>,
);
});
await waitFor(() => {
expect(capturedPayloads).toHaveLength(1);
});
expect(capturedPayloads[0].start).toBe(START_MS);
expect(capturedPayloads[0].end).toBe(END_MS);
});
});

View File

@@ -25,6 +25,11 @@ import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants'
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { FontSize } from 'container/OptionsMenu/types';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/types';
import { getOldLogsOperatorFromNew } from 'hooks/logs/useActiveLog';
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
import useScrollToLog from 'hooks/logs/useScrollToLog';
@@ -33,8 +38,6 @@ import { ILog } from 'types/api/logs/log';
import { DataSource } from 'types/common/queryBuilder';
import { validateQuery } from 'utils/queryValidationUtils';
import EntityDateTimeSelector from '../EntityDateTimeSelector/EntityDateTimeSelector';
import { useEntityDetailsTime } from '../EntityDateTimeSelector/useEntityDetailsTime';
import EntityEmptyState from '../EntityEmptyState/EntityEmptyState';
import EntityError from '../EntityError/EntityError';
import { isKeyNotFoundError } from '../utils';
@@ -44,18 +47,29 @@ import { getEntityLogsQueryPayload } from './utils';
import styles from './EntityLogs.module.scss';
interface Props {
eventEntity: string;
timeRange: {
startTime: number;
endTime: number;
};
isModalTimeSelection: boolean;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
selectedInterval: Time;
queryKey: string;
category: InfraMonitoringEntity;
initialExpression: string;
}
function EntityLogsContent({
eventEntity,
timeRange,
isModalTimeSelection,
handleTimeChange,
selectedInterval,
queryKey,
category,
}: Omit<Props, 'initialExpression'>): JSX.Element {
const { timeRange } = useEntityDetailsTime();
const virtuosoRef = useRef<VirtuosoHandle>(null);
const expression = useExpression();
@@ -120,7 +134,7 @@ function EntityLogsContent({
if (validation.isValid) {
querySearchOnRun(newUserExpression);
void logEvent(InfraMonitoringEvents.FilterApplied, {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category,
@@ -225,10 +239,16 @@ function EntityLogsContent({
<div className={styles.container}>
<div className={styles.filterContainer}>
<div className={styles.filterContainerTime}>
<EntityDateTimeSelector
eventEntity={eventEntity}
category={category}
view={InfraMonitoringEvents.LogsView}
<DateTimeSelectionV2
showAutoRefresh
showRefreshText={false}
hideShareModal
isModalTimeSelection={isModalTimeSelection}
onTimeChange={handleTimeChange}
defaultRelativeTime="5m"
modalSelectedInterval={selectedInterval}
modalInitialStartTime={timeRange.startTime * 1000}
modalInitialEndTime={timeRange.endTime * 1000}
/>
<RunQueryBtn

View File

@@ -50,25 +50,25 @@ jest.mock(
},
);
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
jest.mock('container/TopNav/DateTimeSelectionV2/index.tsx', () => ({
__esModule: true,
default: (): JSX.Element => (
<div data-testid="mock-datetime-selection">Date Time</div>
default: ({
onTimeChange,
}: {
onTimeChange?: (interval: string, dateTimeRange?: [number, number]) => void;
}): JSX.Element => (
<button
type="button"
data-testid="mock-datetime-selection"
onClick={(): void => {
onTimeChange?.('5m');
}}
>
Select Time
</button>
),
}));
jest.mock('../../EntityDateTimeSelector/useEntityDetailsTime', () => ({
useEntityDetailsTime: (): {
timeRange: { startTime: number; endTime: number };
selectedInterval: string;
handleTimeChange: jest.Mock;
} => ({
timeRange: { startTime: 1, endTime: 2 },
selectedInterval: '5m',
handleTimeChange: jest.fn(),
}),
}));
describe('EntityLogs', () => {
let capturedQueryRangePayloads: QueryRangePayloadV5[] = [];
const itemHeight = 100;
@@ -94,7 +94,10 @@ describe('EntityLogs', () => {
>
<VirtuosoMockContext.Provider value={{ viewportHeight: 500, itemHeight }}>
<EntityLogs
eventEntity="test"
timeRange={{ startTime: 1, endTime: 2 }}
isModalTimeSelection={false}
handleTimeChange={jest.fn()}
selectedInterval="5m"
queryKey="test"
category={InfraMonitoringEntity.PODS}
initialExpression='k8s.pod.name = "x"'

View File

@@ -1,93 +0,0 @@
import { mockFieldsAPIsWithEmptyResponse } from '__tests__/fields_api.util';
import { mockQueryRangeV5WithLogsResponse } from '__tests__/query_range_v5.util';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import {
createCustomTimeRange,
createGlobalTimeStore,
GlobalTimeContext,
NANO_SECOND_MULTIPLIER,
} from 'store/globalTime';
import { act, render, waitFor } from 'tests/test-utils';
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
import EntityLogs from '../EntityLogs';
import { K8S_ENTITY_LOGS_EXPRESSION_KEY } from '../hooks';
jest.mock(
'components/OverlayScrollbar/OverlayScrollbar',
() =>
function MockOverlayScrollbar({
children,
}: {
children: React.ReactNode;
}): JSX.Element {
return <div>{children}</div>;
},
);
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
__esModule: true,
default: (): JSX.Element => (
<div data-testid="mock-datetime-selection">Date Time</div>
),
}));
const START_MS = 1705315200000; // 2024-01-15T10:40:00Z
const END_MS = 1705318800000; // 2024-01-15T11:40:00Z
describe('EntityLogs time range wiring', () => {
let capturedPayloads: QueryRangePayloadV5[] = [];
beforeEach(() => {
capturedPayloads = [];
mockQueryRangeV5WithLogsResponse({
onReceiveRequest: async (req) => {
const body = (await req.json()) as QueryRangePayloadV5;
capturedPayloads.push(body);
return {};
},
});
mockFieldsAPIsWithEmptyResponse();
});
it('should query the V5 API with the global time range converted to milliseconds', async () => {
const store = createGlobalTimeStore();
store
.getState()
.setSelectedTime(
createCustomTimeRange(
START_MS * NANO_SECOND_MULTIPLIER,
END_MS * NANO_SECOND_MULTIPLIER,
),
);
const expression = 'k8s.pod.name = "test-pod"';
act(() => {
render(
<GlobalTimeContext.Provider value={store}>
<NuqsTestingAdapter
searchParams={`${K8S_ENTITY_LOGS_EXPRESSION_KEY}=${encodeURIComponent(
expression,
)}`}
>
<EntityLogs
eventEntity="test"
queryKey="test"
category={InfraMonitoringEntity.PODS}
initialExpression={expression}
/>
</NuqsTestingAdapter>
</GlobalTimeContext.Provider>,
);
});
await waitFor(() => {
expect(capturedPayloads.length).toBeGreaterThanOrEqual(1);
});
expect(capturedPayloads[0].start).toBe(START_MS);
expect(capturedPayloads[0].end).toBe(END_MS);
});
});

View File

@@ -2,11 +2,15 @@ import { useCallback, useMemo, useRef } from 'react';
import { UseQueryResult } from 'react-query';
import { Skeleton } from 'antd';
import cx from 'classnames';
import { InfraMonitoringEvents } from 'constants/events';
import { PANEL_TYPES } from 'constants/queryBuilder';
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/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';
@@ -20,17 +24,25 @@ import { getMetricsExplorerUrl } from 'utils/explorerUtils';
import { buildEntityMetricsChartConfig } from './configBuilder';
import ChartHeader from './ChartHeader';
import EntityDateTimeSelector from '../EntityDateTimeSelector/EntityDateTimeSelector';
import { useEntityDetailsTime } from '../EntityDateTimeSelector/useEntityDetailsTime';
import { useEntityMetrics } from './hooks';
import { isKeyNotFoundError } from '../utils';
import styles from './EntityMetrics.module.scss';
import { MetricsTable } from './MetricsTable';
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
interface EntityMetricsProps<T> {
timeRange: {
startTime: number;
endTime: number;
};
isModalTimeSelection: boolean;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
selectedInterval: Time;
entity: T;
eventEntity: string;
entityWidgetInfo: {
title: string;
yAxisUnit: string;
@@ -47,16 +59,16 @@ interface EntityMetricsProps<T> {
}
function EntityMetrics<T>({
selectedInterval,
entity,
eventEntity,
timeRange,
handleTimeChange,
isModalTimeSelection,
entityWidgetInfo,
getEntityQueryPayload,
queryKey,
category,
}: EntityMetricsProps<T>): JSX.Element {
const { timeRange, selectedInterval, handleTimeChange } =
useEntityDetailsTime();
const { visibilities, setElement } = useMultiIntersectionObserver(
entityWidgetInfo.length,
{ threshold: 0.1 },
@@ -79,8 +91,10 @@ function EntityMetrics<T>({
const onDragSelect = useCallback(
(start: number, end: number): void => {
// UPlotConfigBuilder's setSelect hook already delivers milliseconds
handleTimeChange('custom', [Math.trunc(start), Math.trunc(end)]);
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
handleTimeChange('custom', [startTimestamp, endTimestamp]);
},
[handleTimeChange],
);
@@ -174,10 +188,16 @@ function EntityMetrics<T>({
return (
<>
<div className={styles.metricsHeader}>
<EntityDateTimeSelector
eventEntity={eventEntity}
category={category}
view={InfraMonitoringEvents.MetricsView}
<DateTimeSelectionV2
showAutoRefresh
showRefreshText={false}
hideShareModal
onTimeChange={handleTimeChange}
defaultRelativeTime={DEFAULT_TIME_RANGE}
isModalTimeSelection={isModalTimeSelection}
modalSelectedInterval={selectedInterval}
modalInitialStartTime={timeRange.startTime * 1000}
modalInitialEndTime={timeRange.endTime * 1000}
/>
</div>
<div className={styles.entityMetricsContainer}>

View File

@@ -1,6 +1,7 @@
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
import * as appContextHooks from 'providers/App/App';
import { LicenseEvent } from 'types/api/licensesV3/getActive';
import uPlot from 'uplot';
@@ -27,28 +28,13 @@ jest.mock('lib/uPlotV2/utils/dataUtils', () => ({
hasSingleVisiblePoint: jest.fn().mockReturnValue(false),
}));
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
__esModule: true,
default: (): JSX.Element => (
<div data-testid="date-time-selection">Date Time</div>
),
}));
const mockTimeRange = { startTime: 1705315200, endTime: 1705318800 };
let mockSelectedInterval = '5m';
jest.mock('../../EntityDateTimeSelector/useEntityDetailsTime', () => ({
useEntityDetailsTime: (): {
timeRange: { startTime: number; endTime: number };
selectedInterval: string;
handleTimeChange: jest.Mock;
} => ({
timeRange: mockTimeRange,
selectedInterval: mockSelectedInterval,
handleTimeChange: jest.fn(),
}),
}));
jest.mock(
'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries',
() => ({
@@ -158,6 +144,13 @@ const mockGetEntityQueryPayload = jest.fn().mockReturnValue([
},
]);
const mockTimeRange = {
startTime: 1705315200,
endTime: 1705318800,
};
const mockHandleTimeChange = jest.fn();
const mockQueries = [
{
data: {
@@ -290,6 +283,10 @@ const mockEmptyQueries = [
const renderEntityMetrics = (overrides = {}): any => {
const defaultProps = {
timeRange: mockTimeRange,
isModalTimeSelection: false,
handleTimeChange: mockHandleTimeChange,
selectedInterval: '5m' as Time,
entity: mockEntity,
entityWidgetInfo: mockEntityWidgetInfo,
getEntityQueryPayload: mockGetEntityQueryPayload,
@@ -301,8 +298,11 @@ const renderEntityMetrics = (overrides = {}): any => {
return render(
<MemoryRouter>
<EntityMetrics
timeRange={defaultProps.timeRange}
isModalTimeSelection={defaultProps.isModalTimeSelection}
handleTimeChange={defaultProps.handleTimeChange}
selectedInterval={defaultProps.selectedInterval}
entity={defaultProps.entity}
eventEntity="test"
entityWidgetInfo={defaultProps.entityWidgetInfo}
getEntityQueryPayload={defaultProps.getEntityQueryPayload}
queryKey={defaultProps.queryKey}
@@ -344,7 +344,6 @@ const mockQueryPayloads = [
describe('EntityMetrics', () => {
beforeEach(() => {
jest.clearAllMocks();
mockSelectedInterval = '5m';
mockUseEntityMetrics.mockReturnValue({
queries: mockQueries as any,
chartData: mockChartData,
@@ -455,8 +454,7 @@ describe('EntityMetrics', () => {
});
it('builds metrics explorer link with relativeTime when a relative interval is selected', () => {
mockSelectedInterval = '5m';
renderEntityMetrics();
renderEntityMetrics({ selectedInterval: '5m' as Time });
const href = screen
.getByTestId('open-metrics-explorer-0')
.getAttribute('href');
@@ -466,8 +464,7 @@ describe('EntityMetrics', () => {
});
it('builds metrics explorer link with absolute time range in milliseconds for custom interval', () => {
mockSelectedInterval = 'custom';
renderEntityMetrics();
renderEntityMetrics({ selectedInterval: 'custom' as Time });
const href = screen
.getByTestId('open-metrics-explorer-0')
.getAttribute('href');
@@ -481,6 +478,7 @@ describe('EntityMetrics', () => {
expect(mockUseEntityMetrics).toHaveBeenCalledWith(
expect.objectContaining({
queryKey: 'test-query-key',
timeRange: mockTimeRange,
entity: mockEntity,
category: InfraMonitoringEntity.PODS,
}),

View File

@@ -1,119 +0,0 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import {
createCustomTimeRange,
createGlobalTimeStore,
GlobalTimeContext,
GlobalTimeStoreApi,
NANO_SECOND_MULTIPLIER,
} from 'store/globalTime';
import { act, render } from 'tests/test-utils';
import { buildEntityMetricsChartConfig } from '../configBuilder';
import EntityMetrics from '../EntityMetrics';
jest.mock('../configBuilder', () => ({
buildEntityMetricsChartConfig: jest.fn().mockReturnValue({
getId: jest.fn().mockReturnValue('mock-id'),
}),
}));
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
__esModule: true,
default: (): JSX.Element => (
<div data-testid="date-time-selection">Date Time</div>
),
}));
const mockBuildChartConfig = buildEntityMetricsChartConfig as jest.Mock;
const START_MS = 1705315200000; // 2024-01-15T10:40:00Z
const END_MS = 1705318800000; // 2024-01-15T11:40:00Z
const START_SECONDS = 1705315200;
const END_SECONDS = 1705318800;
const entity = { id: 'test-entity-1' };
// The jsdom IntersectionObserver polyfill never reports visibility, so the
// queries stay disabled and no network request is issued.
const queryPayload = {
graphType: PANEL_TYPES.TIME_SERIES,
} as unknown as GetQueryResultsProps;
function createStoreWithCustomRange(): GlobalTimeStoreApi {
const store = createGlobalTimeStore();
store
.getState()
.setSelectedTime(
createCustomTimeRange(
START_MS * NANO_SECOND_MULTIPLIER,
END_MS * NANO_SECOND_MULTIPLIER,
),
);
return store;
}
function renderEntityMetrics(store: GlobalTimeStoreApi): jest.Mock {
const getEntityQueryPayload = jest.fn().mockReturnValue([queryPayload]);
render(
<GlobalTimeContext.Provider value={store}>
<EntityMetrics
entity={entity}
eventEntity="test"
entityWidgetInfo={[{ title: 'CPU Usage', yAxisUnit: 'percentage' }]}
getEntityQueryPayload={getEntityQueryPayload}
queryKey="test-query-key"
category={InfraMonitoringEntity.PODS}
/>
</GlobalTimeContext.Provider>,
);
return getEntityQueryPayload;
}
describe('EntityMetrics time range wiring', () => {
beforeEach(() => {
mockBuildChartConfig.mockClear();
});
it('should build the metrics query payloads and chart config from the global time range in seconds', () => {
const store = createStoreWithCustomRange();
const getEntityQueryPayload = renderEntityMetrics(store);
expect(getEntityQueryPayload).toHaveBeenCalledWith(
entity,
START_SECONDS,
END_SECONDS,
false,
);
expect(mockBuildChartConfig).toHaveBeenCalledWith(
expect.objectContaining({
minTimeScale: START_SECONDS,
maxTimeScale: END_SECONDS,
}),
);
});
it('should store a drag selection (received in milliseconds) as the equivalent custom range', () => {
const store = createStoreWithCustomRange();
renderEntityMetrics(store);
const { onDragSelect } = mockBuildChartConfig.mock.calls[0][0];
// UPlotConfigBuilder's setSelect hook calls onDragSelect with milliseconds
const dragStartMs = 1705316000000;
const dragEndMs = 1705317000000;
act(() => {
onDragSelect(dragStartMs, dragEndMs);
});
expect(store.getState().selectedTime).toBe(
createCustomTimeRange(
dragStartMs * NANO_SECOND_MULTIPLIER,
dragEndMs * NANO_SECOND_MULTIPLIER,
),
);
});
});

View File

@@ -20,6 +20,11 @@ import { InfraMonitoringEvents } from 'constants/events';
import Controls from 'container/Controls';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/types';
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
import { useQueryState } from 'nuqs';
@@ -27,11 +32,10 @@ import { DataSource } from 'types/common/queryBuilder';
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
import { validateQuery } from 'utils/queryValidationUtils';
import EntityDateTimeSelector from '../EntityDateTimeSelector/EntityDateTimeSelector';
import { useEntityDetailsTime } from '../EntityDateTimeSelector/useEntityDetailsTime';
import EntityEmptyState from '../EntityEmptyState/EntityEmptyState';
import EntityError from '../EntityError/EntityError';
import { isKeyNotFoundError, selectedEntityTracesColumns } from '../utils';
import { selectedEntityTracesColumns } from '../utils';
import { isKeyNotFoundError } from '../utils';
import { K8S_ENTITY_TRACES_EXPRESSION_KEY, useEntityTraces } from './hooks';
import { getTraceListColumns } from './traceListColumns';
import { getEntityTracesQueryPayload } from './utils';
@@ -40,18 +44,29 @@ import styles from './EntityTraces.module.scss';
import { useTimezone } from 'providers/Timezone';
interface Props {
eventEntity: string;
timeRange: {
startTime: number;
endTime: number;
};
isModalTimeSelection: boolean;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
selectedInterval: Time;
queryKey: string;
category: InfraMonitoringEntity;
initialExpression: string;
}
function EntityTracesContent({
eventEntity,
timeRange,
isModalTimeSelection,
handleTimeChange,
selectedInterval,
queryKey,
category,
}: Omit<Props, 'initialExpression'>): JSX.Element {
const { timeRange } = useEntityDetailsTime();
const expression = useExpression();
const inputExpression = useInputExpression();
const userExpression = useUserExpression();
@@ -99,8 +114,8 @@ function EntityTracesContent({
if (validation.isValid) {
querySearchOnRun(newUserExpression || '');
void logEvent(InfraMonitoringEvents.FilterApplied, {
entity: eventEntity,
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category,
view: InfraMonitoringEvents.TracesView,
@@ -109,14 +124,7 @@ function EntityTracesContent({
refetch();
}
},
[
inputExpression,
initialExpression,
refetch,
querySearchOnRun,
category,
eventEntity,
],
[inputExpression, initialExpression, refetch, querySearchOnRun, category],
);
const queryData = useMemo(
@@ -144,7 +152,7 @@ function EntityTracesContent({
const hasAdditionalFilters = !!userExpression?.trim();
const handleRowClick = useCallback(() => {
void logEvent(InfraMonitoringEvents.ItemClicked, {
logEvent(InfraMonitoringEvents.ItemClicked, {
entity: InfraMonitoringEvents.K8sEntity,
category,
view: InfraMonitoringEvents.TracesView,
@@ -161,10 +169,16 @@ function EntityTracesContent({
<div className={styles.container}>
<div className={styles.filterContainer}>
<div className={styles.filterContainerTime}>
<EntityDateTimeSelector
eventEntity={eventEntity}
category={category}
view={InfraMonitoringEvents.TracesView}
<DateTimeSelectionV2
showAutoRefresh
showRefreshText={false}
hideShareModal
isModalTimeSelection={isModalTimeSelection}
onTimeChange={handleTimeChange}
defaultRelativeTime="5m"
modalSelectedInterval={selectedInterval}
modalInitialStartTime={timeRange.startTime * 1000}
modalInitialEndTime={timeRange.endTime * 1000}
/>
<RunQueryBtn

View File

@@ -34,8 +34,10 @@ describe('EntityTraces - Default Behavior', () => {
});
it('should pass time range to API (converted to milliseconds)', async () => {
const timeRange = { startTime: 1000, endTime: 2000 };
act(() => {
renderEntityTraces();
renderEntityTraces({ timeRange });
});
await waitFor(() => {
@@ -45,8 +47,8 @@ describe('EntityTraces - Default Behavior', () => {
verifyQueryPayload({
payload: capturedPayloads[0],
expectedTimeRange: {
start: 1 * 1000,
end: 2 * 1000,
start: timeRange.startTime * 1000,
end: timeRange.endTime * 1000,
},
});
});

View File

@@ -1,81 +0,0 @@
import { mockFieldsAPIsWithEmptyResponse } from '__tests__/fields_api.util';
import { mockQueryRangeV5WithTracesResponse } from '__tests__/query_range_v5.util';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import {
createCustomTimeRange,
createGlobalTimeStore,
GlobalTimeContext,
NANO_SECOND_MULTIPLIER,
} from 'store/globalTime';
import { act, render, waitFor } from 'tests/test-utils';
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
import EntityTraces from '../EntityTraces';
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../hooks';
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
__esModule: true,
default: (): JSX.Element => (
<div data-testid="mock-datetime-selection">Date Time</div>
),
}));
const START_MS = 1705315200000; // 2024-01-15T10:40:00Z
const END_MS = 1705318800000; // 2024-01-15T11:40:00Z
describe('EntityTraces time range wiring', () => {
let capturedPayloads: QueryRangePayloadV5[] = [];
beforeEach(() => {
capturedPayloads = [];
mockQueryRangeV5WithTracesResponse({
onReceiveRequest: async (req) => {
const body = (await req.json()) as QueryRangePayloadV5;
capturedPayloads.push(body);
return {};
},
});
mockFieldsAPIsWithEmptyResponse();
});
it('should query the V5 API with the global time range converted to milliseconds', async () => {
const store = createGlobalTimeStore();
store
.getState()
.setSelectedTime(
createCustomTimeRange(
START_MS * NANO_SECOND_MULTIPLIER,
END_MS * NANO_SECOND_MULTIPLIER,
),
);
const expression = 'k8s.pod.name = "test-pod"';
act(() => {
render(
<GlobalTimeContext.Provider value={store}>
<NuqsTestingAdapter
searchParams={`${K8S_ENTITY_TRACES_EXPRESSION_KEY}=${encodeURIComponent(
expression,
)}`}
>
<EntityTraces
eventEntity="test"
queryKey="test"
category={InfraMonitoringEntity.PODS}
initialExpression={expression}
/>
</NuqsTestingAdapter>
</GlobalTimeContext.Provider>,
);
});
await waitFor(() => {
expect(capturedPayloads).toHaveLength(1);
});
expect(capturedPayloads[0].start).toBe(START_MS);
expect(capturedPayloads[0].end).toBe(END_MS);
});
});

View File

@@ -1,5 +1,7 @@
import { mockFieldsAPIsWithEmptyResponse } from '__tests__/fields_api.util';
import { ENVIRONMENT } from 'constants/env';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, RenderResult } from 'tests/test-utils';
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
@@ -7,19 +9,39 @@ import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
import EntityTraces from '../EntityTraces';
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../hooks';
// QuerySearch fires autocomplete requests on mount; without handlers MSW
// passes them through to the real network and the resulting AxiosError fails
// whichever test happens to be running.
beforeEach(() => {
mockFieldsAPIsWithEmptyResponse();
server.use(
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/keys`, (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({ status: 'success', data: { keys: {}, complete: true } }),
),
),
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/values`, (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({ status: 'success', data: { values: {}, complete: true } }),
),
),
);
});
export interface RenderEntityTracesOptions {
expression?: string;
timeRange?: { startTime: number; endTime: number };
category?: InfraMonitoringEntity;
selectedInterval?: '5m' | '15m' | '30m' | '1h';
pagination?: { offset: number; limit: number };
}
export function renderEntityTraces({
expression = 'k8s.pod.name = "test-pod"',
timeRange = { startTime: 1, endTime: 2 },
category = InfraMonitoringEntity.PODS,
selectedInterval = '5m',
pagination,
}: RenderEntityTracesOptions = {}): RenderResult {
const encodedExpression = encodeURIComponent(expression);
@@ -33,7 +55,10 @@ export function renderEntityTraces({
return render(
<NuqsTestingAdapter searchParams={searchParams}>
<EntityTraces
eventEntity="test"
timeRange={timeRange}
isModalTimeSelection={false}
handleTimeChange={jest.fn()}
selectedInterval={selectedInterval}
queryKey="test"
category={category}
initialExpression={expression}
@@ -76,21 +101,21 @@ export function verifyQueryPayload({
expect(orderKeys).toContain('timestamp');
}
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
jest.mock('container/TopNav/DateTimeSelectionV2/index.tsx', () => ({
__esModule: true,
default: (): JSX.Element => (
<div data-testid="mock-datetime-selection">Date Time</div>
default: ({
onTimeChange,
}: {
onTimeChange?: (interval: string, dateTimeRange?: [number, number]) => void;
}): JSX.Element => (
<button
type="button"
data-testid="mock-datetime-selection"
onClick={(): void => {
onTimeChange?.('5m');
}}
>
Select Time
</button>
),
}));
jest.mock('../../EntityDateTimeSelector/useEntityDetailsTime', () => ({
useEntityDetailsTime: (): {
timeRange: { startTime: number; endTime: number };
selectedInterval: string;
handleTimeChange: jest.Mock;
} => ({
timeRange: { startTime: 1, endTime: 2 },
selectedInterval: '5m',
handleTimeChange: jest.fn(),
}),
}));

View File

@@ -1,5 +1,4 @@
import { Container } from '@signozhq/icons';
import { InfraMonitoringEvents } from 'constants/events';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { CustomTab } from '../Base/K8sBaseDetails';
@@ -11,20 +10,6 @@ import {
import EntityMetrics from './EntityMetrics';
const categoryToEventEntity: Record<InfraMonitoringEntity, string> = {
[InfraMonitoringEntity.DAEMONSETS]: InfraMonitoringEvents.DaemonSet,
[InfraMonitoringEntity.DEPLOYMENTS]: InfraMonitoringEvents.Deployment,
[InfraMonitoringEntity.JOBS]: InfraMonitoringEvents.Job,
[InfraMonitoringEntity.NAMESPACES]: InfraMonitoringEvents.Namespace,
[InfraMonitoringEntity.STATEFULSETS]: InfraMonitoringEvents.StatefulSet,
[InfraMonitoringEntity.PODS]: InfraMonitoringEvents.Pod,
[InfraMonitoringEntity.NODES]: InfraMonitoringEvents.Node,
[InfraMonitoringEntity.CLUSTERS]: InfraMonitoringEvents.Cluster,
[InfraMonitoringEntity.VOLUMES]: InfraMonitoringEvents.Volume,
[InfraMonitoringEntity.HOSTS]: InfraMonitoringEvents.HostEntity,
[InfraMonitoringEntity.CONTAINERS]: 'container',
};
interface CreatePodMetricsTabParams<T> {
getQueryPayload: (
entity: T,
@@ -32,38 +17,30 @@ interface CreatePodMetricsTabParams<T> {
end: number,
dotMetricsEnabled: boolean,
) => GetQueryResultsProps[];
queryKey: string;
category: InfraMonitoringEntity;
docBasePath?: string;
queryKey: string;
}
export function createPodMetricsTab<T>({
getQueryPayload,
queryKey,
category,
docBasePath,
queryKey,
}: CreatePodMetricsTabParams<T>): CustomTab<T> {
const eventEntity = categoryToEventEntity[category];
const widgetInfo = docBasePath
? podUtilizationByPodWidgetInfo.map((widget) => ({
...widget,
docPath: widget.docPath ? `${docBasePath}${widget.docPath}` : undefined,
}))
: podUtilizationByPodWidgetInfo;
return {
key: VIEW_TYPES.POD_METRICS,
label: 'Pod Metrics',
icon: <Container size={14} />,
render: ({ entity }) => (
render: ({ entity, timeRange, selectedInterval, handleTimeChange }) => (
<EntityMetrics
entity={entity}
eventEntity={eventEntity}
entityWidgetInfo={widgetInfo}
selectedInterval={selectedInterval}
timeRange={timeRange}
handleTimeChange={handleTimeChange}
isModalTimeSelection
entityWidgetInfo={podUtilizationByPodWidgetInfo}
getEntityQueryPayload={getQueryPayload}
queryKey={queryKey}
category={category}
queryKey={queryKey}
/>
),
};

View File

@@ -454,9 +454,3 @@
gap: 16px;
}
}
.entity-date-time-selector {
display: flex;
align-items: center;
gap: 8px;
}

View File

@@ -48,6 +48,46 @@
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(.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: var(--accent-primary);
}
}
}
.quickFiltersContainerHeader {

View File

@@ -28,7 +28,7 @@ import { DataSource } from 'types/common/queryBuilder';
import { FeatureKeys } from '../../constants/features';
import { useAppContext } from '../../providers/App/App';
import { K8sDynamicList } from './Base/K8sDynamicList';
import K8sClustersList from './Clusters/K8sClustersList';
import {
GetClustersQuickFiltersConfig,
GetDaemonsetsQuickFiltersConfig,
@@ -43,18 +43,25 @@ import {
K8sCategories,
METRIC_NAMESPACE_BY_ENTITY,
} from './constants';
import K8sDaemonSetsList from './DaemonSets/K8sDaemonSetsList';
import K8sDeploymentsList from './Deployments/K8sDeploymentsList';
import {
useInfraMonitoringCategory,
useInfraMonitoringGroupBy,
useInfraMonitoringOrderBy,
useInfraMonitoringSelectedItemParams,
} from './hooks';
import K8sJobsList from './Jobs/K8sJobsList';
import K8sNamespacesList from './Namespaces/K8sNamespacesList';
import K8sNodesList from './Nodes/K8sNodesList';
import K8sPodLists from './Pods/K8sPodLists';
import K8sStatefulSetsList from './StatefulSets/K8sStatefulSetsList';
import K8sVolumesList from './Volumes/K8sVolumesList';
import styles from './InfraMonitoringK8s.module.scss';
import { InfraMonitoringEvents } from 'constants/events';
import logEvent from 'api/common/logEvent';
import { NANO_SECOND_MULTIPLIER, useGlobalTimeStore } from 'store/globalTime';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
export default function InfraMonitoringK8s(): JSX.Element {
const [showFilters, setShowFilters] = useState(true);
@@ -245,62 +252,58 @@ export default function InfraMonitoringK8s(): JSX.Element {
<div className={styles.infraContentRow}>
{showFilters && (
<div className={styles.quickFiltersContainer}>
<OverlayScrollbar>
<>
<div className={styles.categorySelectorSection}>
<div className={styles.sectionHeader} data-type="resource">
<Typography.Text className={styles.sectionLabel}>
Viewing · Resource
</Typography.Text>
<div className={styles.sectionLine} />
<Tooltip title="Collapse Filters">
<ArrowUpToLine
style={{ transform: 'rotate(270deg)' }}
onClick={handleFilterVisibilityChange}
size="md"
/>
</Tooltip>
</div>
<div className={styles.categoryCard}>
<div className={styles.categoryList}>
{categories.map((category) => (
<button
key={category.key}
type="button"
className={`${styles.categoryItem} ${
selectedCategory === category.key
? styles.categoryItemSelected
: ''
}`}
onClick={(): void => handleCategorySelect(category.key)}
data-testid={`category-${category.key}`}
>
{category.icon}
<Typography.Text>{category.label}</Typography.Text>
</button>
))}
</div>
</div>
<div className={styles.categorySelectorSection}>
<div className={styles.sectionHeader} data-type="resource">
<Typography.Text className={styles.sectionLabel}>
Viewing · Resource
</Typography.Text>
<div className={styles.sectionLine} />
<Tooltip title="Collapse Filters">
<ArrowUpToLine
style={{ transform: 'rotate(270deg)' }}
onClick={handleFilterVisibilityChange}
size="md"
/>
</Tooltip>
</div>
<div className={styles.categoryCard}>
<div className={styles.categoryList}>
{categories.map((category) => (
<button
key={category.key}
type="button"
className={`${styles.categoryItem} ${
selectedCategory === category.key
? styles.categoryItemSelected
: ''
}`}
onClick={(): void => handleCategorySelect(category.key)}
data-testid={`category-${category.key}`}
>
{category.icon}
<Typography.Text>{category.label}</Typography.Text>
</button>
))}
</div>
</div>
</div>
<div className={styles.quickFiltersSection}>
<div className={styles.sectionHeader} data-type="filter">
<Typography.Text className={styles.sectionLabel}>
Filter by
</Typography.Text>
<div className={styles.sectionLine} />
</div>
{selectedCategoryConfig && (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={selectedCategoryConfig}
handleFilterVisibilityChange={handleFilterVisibilityChange}
useFieldApis={selectedCategoryUseFieldApis}
/>
)}
</div>
</>
</OverlayScrollbar>
<div className={styles.quickFiltersSection}>
<div className={styles.sectionHeader} data-type="filter">
<Typography.Text className={styles.sectionLabel}>
Filter by
</Typography.Text>
<div className={styles.sectionLine} />
</div>
{selectedCategoryConfig && (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={selectedCategoryConfig}
handleFilterVisibilityChange={handleFilterVisibilityChange}
useFieldApis={selectedCategoryUseFieldApis}
/>
)}
</div>
</div>
)}
@@ -309,7 +312,41 @@ export default function InfraMonitoringK8s(): JSX.Element {
showFilters ? styles.listContainerFiltersVisible : ''
}`}
>
<K8sDynamicList controlListPrefix={showFiltersComp} />
{selectedCategory === K8sCategories.PODS && (
<K8sPodLists controlListPrefix={showFiltersComp} />
)}
{selectedCategory === K8sCategories.NODES && (
<K8sNodesList controlListPrefix={showFiltersComp} />
)}
{selectedCategory === K8sCategories.CLUSTERS && (
<K8sClustersList controlListPrefix={showFiltersComp} />
)}
{selectedCategory === K8sCategories.DEPLOYMENTS && (
<K8sDeploymentsList controlListPrefix={showFiltersComp} />
)}
{selectedCategory === K8sCategories.NAMESPACES && (
<K8sNamespacesList controlListPrefix={showFiltersComp} />
)}
{selectedCategory === K8sCategories.STATEFULSETS && (
<K8sStatefulSetsList controlListPrefix={showFiltersComp} />
)}
{selectedCategory === K8sCategories.JOBS && (
<K8sJobsList controlListPrefix={showFiltersComp} />
)}
{selectedCategory === K8sCategories.DAEMONSETS && (
<K8sDaemonSetsList controlListPrefix={showFiltersComp} />
)}
{selectedCategory === K8sCategories.VOLUMES && (
<K8sVolumesList controlListPrefix={showFiltersComp} />
)}
</div>
</div>
</div>

View File

@@ -0,0 +1,164 @@
import { useCallback, useMemo } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listJobs } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesJobRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
getJobMetricsQueryPayload,
getJobPodMetricsQueryPayload,
jobWidgetInfo,
k8sJobDetailsMetadataConfig,
k8sJobGetEntityName,
k8sJobGetSelectedItemExpression,
k8sJobInitialEventsExpression,
k8sJobInitialLogTracesExpression,
} from './constants';
import {
getK8sJobItemKey,
getK8sJobRowKey,
k8sJobsColumnsConfig,
} from './table.config';
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
function K8sJobsList({
controlListPrefix,
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
try {
const response = await listJobs(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
warning: data.warning,
};
} catch (error) {
return {
type: 'list' as const,
records: [] as InframonitoringtypesJobRecordDTO[],
total: 0,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesJobRecordDTO | null;
error?: APIError | null;
}> => {
try {
const response = await listJobs(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
return {
data: null,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
const customTabs = useMemo(
() => [
createPodMetricsTab<InframonitoringtypesJobRecordDTO>({
getQueryPayload: getJobPodMetricsQueryPayload,
category: InfraMonitoringEntity.JOBS,
queryKey: 'jobPodMetrics',
}),
],
[],
);
return (
<>
<K8sBaseList<InframonitoringtypesJobRecordDTO, SelectedItemParams>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.JOBS}
tableColumns={k8sJobsColumnsConfig}
fetchListData={fetchListData}
getRowKey={getK8sJobRowKey}
getItemKey={getK8sJobItemKey}
eventCategory={InfraMonitoringEvents.Job}
/>
<K8sBaseDetails<InframonitoringtypesJobRecordDTO>
category={InfraMonitoringEntity.JOBS}
eventCategory={InfraMonitoringEvents.Job}
getSelectedItemExpression={k8sJobGetSelectedItemExpression}
fetchEntityData={fetchEntityData}
getEntityName={k8sJobGetEntityName}
getInitialLogTracesExpression={k8sJobInitialLogTracesExpression}
getInitialEventsExpression={k8sJobInitialEventsExpression}
metadataConfig={k8sJobDetailsMetadataConfig}
entityWidgetInfo={jobWidgetInfo}
getEntityQueryPayload={getJobMetricsQueryPayload}
queryKeyPrefix="job"
customTabs={customTabs}
/>
</>
);
}
export default K8sJobsList;

View File

@@ -1,153 +0,0 @@
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listJobs } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesJobRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
RenderErrorResponseDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import { InfraMonitoringEvents } from 'constants/events';
import { K8sEntityConfig } from '../Base/entity.config.types';
import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { createPodMetricsTab } from '../EntityDetailsUtils/createPodMetricsTab';
import { SelectedItemParams } from '../hooks';
import {
getJobMetricsQueryPayload,
getJobPodMetricsQueryPayload,
jobWidgetInfo,
k8sJobDetailsMetadataConfig,
k8sJobGetEntityName,
k8sJobGetSelectedItemExpression,
k8sJobInitialEventsExpression,
k8sJobInitialLogTracesExpression,
} from './constants';
import {
getK8sJobItemKey,
getK8sJobRowKey,
k8sJobsColumnsConfig,
} from './table.config';
async function fetchListData(
filters: K8sBaseFilters,
signal?: AbortSignal,
): ReturnType<
K8sEntityConfig<
InframonitoringtypesJobRecordDTO,
SelectedItemParams
>['list']['fetchListData']
> {
try {
const response = await listJobs(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
warning: data.warning,
};
} catch (error) {
return {
type: 'list' as const,
records: [] as InframonitoringtypesJobRecordDTO[],
total: 0,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
}
async function fetchEntityData(
filters: K8sDetailsFilters,
signal?: AbortSignal,
): ReturnType<
K8sEntityConfig<
InframonitoringtypesJobRecordDTO,
SelectedItemParams
>['details']['fetchEntityData']
> {
try {
const response = await listJobs(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
return {
data: null,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
}
export const jobEntityConfig: K8sEntityConfig<
InframonitoringtypesJobRecordDTO,
SelectedItemParams
> = {
list: {
entity: InfraMonitoringEntity.JOBS,
eventCategory: InfraMonitoringEvents.Job,
tableColumns: k8sJobsColumnsConfig,
fetchListData,
getRowKey: getK8sJobRowKey,
getItemKey: getK8sJobItemKey,
detailsQueryKeyPrefix: 'job',
},
details: {
category: InfraMonitoringEntity.JOBS,
eventCategory: InfraMonitoringEvents.Job,
queryKeyPrefix: 'job',
getSelectedItemExpression: k8sJobGetSelectedItemExpression,
fetchEntityData,
getEntityName: k8sJobGetEntityName,
getInitialLogTracesExpression: k8sJobInitialLogTracesExpression,
getInitialEventsExpression: k8sJobInitialEventsExpression,
metadataConfig: k8sJobDetailsMetadataConfig,
entityWidgetInfo: jobWidgetInfo,
getEntityQueryPayload: getJobMetricsQueryPayload,
customTabs: [
createPodMetricsTab<InframonitoringtypesJobRecordDTO>({
getQueryPayload: getJobPodMetricsQueryPayload,
category: InfraMonitoringEntity.JOBS,
queryKey: 'jobPodMetrics',
docBasePath: '/infrastructure-monitoring/kubernetes/jobs/',
}),
],
},
};

View File

@@ -113,17 +113,12 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
width: { min: 250 },
enableSort: false,
enableResize: true,
cell: ({ row, rowId }): React.ReactNode => {
cell: ({ row }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts
items={getPodStatusItems(podCountsByStatus)}
rowId={rowId}
/>
);
return <GroupedStatusCounts items={getPodStatusItems(podCountsByStatus)} />;
},
},
{
@@ -137,7 +132,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
width: { min: 210 },
enableSort: false,
enableResize: true,
cell: ({ row, rowId }): React.ReactNode => (
cell: ({ row }): React.ReactNode => (
<GroupedStatusCounts
items={[
{ value: row.activePods, label: 'Active', color: Color.BG_ROBIN_500 },
@@ -153,7 +148,6 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
color: Color.BG_AMBER_500,
},
]}
rowId={rowId}
/>
),
},

View File

@@ -0,0 +1,168 @@
import { useCallback, useMemo } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listNamespaces } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesNamespaceRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
getNamespaceMetricsQueryPayload,
getNamespacePodMetricsQueryPayload,
k8sNamespaceDetailsCountsConfig,
k8sNamespaceDetailsMetadataConfig,
k8sNamespaceGetCountsFilterExpression,
k8sNamespaceGetEntityName,
k8sNamespaceGetSelectedItemExpression,
k8sNamespaceInitialEventsExpression,
k8sNamespaceInitialLogTracesExpression,
namespaceWidgetInfo,
} from './constants';
import {
getK8sNamespaceItemKey,
getK8sNamespaceRowKey,
k8sNamespacesColumnsConfig,
} from './table.config';
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
function K8sNamespacesList({
controlListPrefix,
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
try {
const response = await listNamespaces(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
warning: data.warning,
};
} catch (error) {
return {
type: 'list' as const,
records: [] as InframonitoringtypesNamespaceRecordDTO[],
total: 0,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesNamespaceRecordDTO | null;
error?: APIError | null;
}> => {
try {
const response = await listNamespaces(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
return {
data: null,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
const customTabs = useMemo(
() => [
createPodMetricsTab<InframonitoringtypesNamespaceRecordDTO>({
getQueryPayload: getNamespacePodMetricsQueryPayload,
category: InfraMonitoringEntity.NAMESPACES,
queryKey: 'namespacePodMetrics',
}),
],
[],
);
return (
<>
<K8sBaseList<InframonitoringtypesNamespaceRecordDTO, SelectedItemParams>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.NAMESPACES}
tableColumns={k8sNamespacesColumnsConfig}
fetchListData={fetchListData}
getRowKey={getK8sNamespaceRowKey}
getItemKey={getK8sNamespaceItemKey}
eventCategory={InfraMonitoringEvents.Namespace}
/>
<K8sBaseDetails<InframonitoringtypesNamespaceRecordDTO>
category={InfraMonitoringEntity.NAMESPACES}
eventCategory={InfraMonitoringEvents.Namespace}
getSelectedItemExpression={k8sNamespaceGetSelectedItemExpression}
fetchEntityData={fetchEntityData}
getEntityName={k8sNamespaceGetEntityName}
getInitialLogTracesExpression={k8sNamespaceInitialLogTracesExpression}
getInitialEventsExpression={k8sNamespaceInitialEventsExpression}
metadataConfig={k8sNamespaceDetailsMetadataConfig}
countsConfig={k8sNamespaceDetailsCountsConfig}
getCountsFilterExpression={k8sNamespaceGetCountsFilterExpression}
entityWidgetInfo={namespaceWidgetInfo}
getEntityQueryPayload={getNamespaceMetricsQueryPayload}
queryKeyPrefix="namespace"
customTabs={customTabs}
/>
</>
);
}
export default K8sNamespacesList;

View File

@@ -1,159 +0,0 @@
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listNamespaces } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesNamespaceRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
RenderErrorResponseDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import { InfraMonitoringEvents } from 'constants/events';
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
import { K8sEntityConfig } from '../Base/entity.config.types';
import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
getNamespaceMetricsQueryPayload,
getNamespacePodMetricsQueryPayload,
k8sNamespaceDetailsCountsConfig,
k8sNamespaceDetailsMetadataConfig,
k8sNamespaceGetCountsFilterExpression,
k8sNamespaceGetEntityName,
k8sNamespaceGetSelectedItemExpression,
k8sNamespaceInitialEventsExpression,
k8sNamespaceInitialLogTracesExpression,
namespaceWidgetInfo,
} from './constants';
import {
getK8sNamespaceItemKey,
getK8sNamespaceRowKey,
k8sNamespacesColumnsConfig,
} from './table.config';
async function fetchListData(
filters: K8sBaseFilters,
signal?: AbortSignal,
): ReturnType<
K8sEntityConfig<
InframonitoringtypesNamespaceRecordDTO,
SelectedItemParams
>['list']['fetchListData']
> {
try {
const response = await listNamespaces(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
warning: data.warning,
};
} catch (error) {
return {
type: 'list' as const,
records: [] as InframonitoringtypesNamespaceRecordDTO[],
total: 0,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
}
async function fetchEntityData(
filters: K8sDetailsFilters,
signal?: AbortSignal,
): ReturnType<
K8sEntityConfig<
InframonitoringtypesNamespaceRecordDTO,
SelectedItemParams
>['details']['fetchEntityData']
> {
try {
const response = await listNamespaces(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
return {
data: null,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
}
const namespaceCustomTabs = [
createPodMetricsTab<InframonitoringtypesNamespaceRecordDTO>({
getQueryPayload: getNamespacePodMetricsQueryPayload,
category: InfraMonitoringEntity.NAMESPACES,
queryKey: 'namespacePodMetrics',
docBasePath: '/infrastructure-monitoring/kubernetes/namespaces/',
}),
];
export const namespaceEntityConfig: K8sEntityConfig<
InframonitoringtypesNamespaceRecordDTO,
SelectedItemParams
> = {
list: {
entity: InfraMonitoringEntity.NAMESPACES,
eventCategory: InfraMonitoringEvents.Namespace,
tableColumns: k8sNamespacesColumnsConfig,
fetchListData,
getRowKey: getK8sNamespaceRowKey,
getItemKey: getK8sNamespaceItemKey,
detailsQueryKeyPrefix: 'namespace',
},
details: {
category: InfraMonitoringEntity.NAMESPACES,
eventCategory: InfraMonitoringEvents.Namespace,
queryKeyPrefix: 'namespace',
getSelectedItemExpression: k8sNamespaceGetSelectedItemExpression,
fetchEntityData,
getEntityName: k8sNamespaceGetEntityName,
getInitialLogTracesExpression: k8sNamespaceInitialLogTracesExpression,
getInitialEventsExpression: k8sNamespaceInitialEventsExpression,
metadataConfig: k8sNamespaceDetailsMetadataConfig,
countsConfig: k8sNamespaceDetailsCountsConfig,
getCountsFilterExpression: k8sNamespaceGetCountsFilterExpression,
entityWidgetInfo: namespaceWidgetInfo,
getEntityQueryPayload: getNamespaceMetricsQueryPayload,
customTabs: namespaceCustomTabs,
},
};

View File

@@ -114,16 +114,13 @@ export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
row.podCountsByStatus,
width: { min: 250 },
enableSort: false,
cell: ({ row, rowId }): React.ReactNode => {
cell: ({ row }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts
items={getPodStatusItems(row.podCountsByStatus)}
rowId={rowId}
/>
<GroupedStatusCounts items={getPodStatusItems(row.podCountsByStatus)} />
);
},
},

View File

@@ -0,0 +1,149 @@
import { useCallback } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listNodes } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesNodeRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import {
getNodeMetricsQueryPayload,
k8sNodeDetailsMetadataConfig,
k8sNodeGetEntityName,
k8sNodeGetSelectedItemExpression,
k8sNodeInitialEventsExpression,
k8sNodeInitialLogTracesExpression,
nodeWidgetInfo,
} from './constants';
import {
getK8sNodeItemKey,
getK8sNodeRowKey,
k8sNodesColumnsConfig,
} from './table.config';
function K8sNodesList({
controlListPrefix,
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
try {
const response = await listNodes(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
warning: data.warning,
};
} catch (error) {
return {
type: 'list' as const,
records: [] as InframonitoringtypesNodeRecordDTO[],
total: 0,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesNodeRecordDTO | null;
error?: APIError | null;
}> => {
try {
const response = await listNodes(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
return {
data: null,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
return (
<>
<K8sBaseList<InframonitoringtypesNodeRecordDTO>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.NODES}
tableColumns={k8sNodesColumnsConfig}
fetchListData={fetchListData}
getRowKey={getK8sNodeRowKey}
getItemKey={getK8sNodeItemKey}
eventCategory={InfraMonitoringEvents.Node}
/>
<K8sBaseDetails<InframonitoringtypesNodeRecordDTO>
category={InfraMonitoringEntity.NODES}
eventCategory={InfraMonitoringEvents.Node}
getSelectedItemExpression={k8sNodeGetSelectedItemExpression}
fetchEntityData={fetchEntityData}
getEntityName={k8sNodeGetEntityName}
getInitialLogTracesExpression={k8sNodeInitialLogTracesExpression}
getInitialEventsExpression={k8sNodeInitialEventsExpression}
metadataConfig={k8sNodeDetailsMetadataConfig}
entityWidgetInfo={nodeWidgetInfo}
getEntityQueryPayload={getNodeMetricsQueryPayload}
queryKeyPrefix="node"
/>
</>
);
}
export default K8sNodesList;

View File

@@ -1,142 +0,0 @@
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listNodes } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesNodeRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
RenderErrorResponseDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import { InfraMonitoringEvents } from 'constants/events';
import { K8sEntityConfig } from '../Base/entity.config.types';
import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import {
getNodeMetricsQueryPayload,
k8sNodeDetailsMetadataConfig,
k8sNodeGetEntityName,
k8sNodeGetSelectedItemExpression,
k8sNodeInitialEventsExpression,
k8sNodeInitialLogTracesExpression,
nodeWidgetInfo,
} from './constants';
import {
getK8sNodeItemKey,
getK8sNodeRowKey,
k8sNodesColumnsConfig,
} from './table.config';
async function fetchListData(
filters: K8sBaseFilters,
signal?: AbortSignal,
): ReturnType<
K8sEntityConfig<
InframonitoringtypesNodeRecordDTO,
string
>['list']['fetchListData']
> {
try {
const response = await listNodes(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
warning: data.warning,
};
} catch (error) {
return {
type: 'list' as const,
records: [] as InframonitoringtypesNodeRecordDTO[],
total: 0,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
}
async function fetchEntityData(
filters: K8sDetailsFilters,
signal?: AbortSignal,
): ReturnType<
K8sEntityConfig<
InframonitoringtypesNodeRecordDTO,
string
>['details']['fetchEntityData']
> {
try {
const response = await listNodes(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
return {
data: null,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
}
export const nodeEntityConfig: K8sEntityConfig<
InframonitoringtypesNodeRecordDTO,
string
> = {
list: {
entity: InfraMonitoringEntity.NODES,
eventCategory: InfraMonitoringEvents.Node,
tableColumns: k8sNodesColumnsConfig,
fetchListData,
getRowKey: getK8sNodeRowKey,
getItemKey: getK8sNodeItemKey,
detailsQueryKeyPrefix: 'node',
},
details: {
category: InfraMonitoringEntity.NODES,
eventCategory: InfraMonitoringEvents.Node,
queryKeyPrefix: 'node',
getSelectedItemExpression: k8sNodeGetSelectedItemExpression,
fetchEntityData,
getEntityName: k8sNodeGetEntityName,
getInitialLogTracesExpression: k8sNodeInitialLogTracesExpression,
getInitialEventsExpression: k8sNodeInitialEventsExpression,
metadataConfig: k8sNodeDetailsMetadataConfig,
entityWidgetInfo: nodeWidgetInfo,
getEntityQueryPayload: getNodeMetricsQueryPayload,
},
};

View File

@@ -98,7 +98,7 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
accessorFn: (row): string => row.condition,
width: { min: 120 },
enableSort: false,
cell: ({ row, groupMeta, rowId }): React.ReactNode => {
cell: ({ row, groupMeta }): React.ReactNode => {
if (!groupMeta) {
const color =
NODE_CONDITION_COLORS[row.condition] || NODE_CONDITION_COLORS.no_data;
@@ -123,7 +123,6 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
color: Color.BG_AMBER_500,
},
]}
rowId={rowId}
/>
);
},
@@ -139,16 +138,13 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
row.podCountsByStatus,
width: { min: 250 },
enableSort: false,
cell: ({ row, rowId }): React.ReactNode => {
cell: ({ row }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts
items={getPodStatusItems(row.podCountsByStatus)}
rowId={rowId}
/>
<GroupedStatusCounts items={getPodStatusItems(row.podCountsByStatus)} />
);
},
},

View File

@@ -0,0 +1,149 @@
import { useCallback } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listPods } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesPodRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import {
getPodMetricsQueryPayload,
k8sPodDetailsMetadataConfig,
k8sPodGetEntityName,
k8sPodGetSelectedItemExpression,
k8sPodInitialEventsExpression,
k8sPodInitialLogTracesExpression,
podWidgetInfo,
} from './constants';
import {
getK8sPodItemKey,
getK8sPodRowKey,
k8sPodColumnsConfig,
} from './table.config';
function K8sPodsList({
controlListPrefix,
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
try {
const response = await listPods(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
warning: data.warning,
};
} catch (error) {
return {
type: 'list' as const,
records: [] as InframonitoringtypesPodRecordDTO[],
total: 0,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesPodRecordDTO | null;
error?: APIError | null;
}> => {
try {
const response = await listPods(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
return {
data: null,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
return (
<>
<K8sBaseList<InframonitoringtypesPodRecordDTO>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.PODS}
tableColumns={k8sPodColumnsConfig}
fetchListData={fetchListData}
getRowKey={getK8sPodRowKey}
getItemKey={getK8sPodItemKey}
eventCategory={InfraMonitoringEvents.Pod}
/>
<K8sBaseDetails<InframonitoringtypesPodRecordDTO>
category={InfraMonitoringEntity.PODS}
eventCategory={InfraMonitoringEvents.Pod}
getSelectedItemExpression={k8sPodGetSelectedItemExpression}
fetchEntityData={fetchEntityData}
getEntityName={k8sPodGetEntityName}
getInitialLogTracesExpression={k8sPodInitialLogTracesExpression}
getInitialEventsExpression={k8sPodInitialEventsExpression}
metadataConfig={k8sPodDetailsMetadataConfig}
entityWidgetInfo={podWidgetInfo}
getEntityQueryPayload={getPodMetricsQueryPayload}
queryKeyPrefix="pod"
/>
</>
);
}
export default K8sPodsList;

View File

@@ -1,134 +0,0 @@
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listPods } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesPodRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
RenderErrorResponseDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import { InfraMonitoringEvents } from 'constants/events';
import { K8sEntityConfig } from '../Base/entity.config.types';
import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import {
getPodMetricsQueryPayload,
k8sPodDetailsMetadataConfig,
k8sPodGetEntityName,
k8sPodGetSelectedItemExpression,
k8sPodInitialEventsExpression,
k8sPodInitialLogTracesExpression,
podWidgetInfo,
} from './constants';
import {
getK8sPodItemKey,
getK8sPodRowKey,
k8sPodColumnsConfig,
} from './table.config';
async function fetchListData(
filters: K8sBaseFilters,
signal?: AbortSignal,
): ReturnType<
K8sEntityConfig<InframonitoringtypesPodRecordDTO>['list']['fetchListData']
> {
try {
const response = await listPods(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
warning: data.warning,
};
} catch (error) {
return {
type: 'list' as const,
records: [] as InframonitoringtypesPodRecordDTO[],
total: 0,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
}
async function fetchEntityData(
filters: K8sDetailsFilters,
signal?: AbortSignal,
): ReturnType<
K8sEntityConfig<InframonitoringtypesPodRecordDTO>['details']['fetchEntityData']
> {
try {
const response = await listPods(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
return {
data: null,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
}
export const podEntityConfig: K8sEntityConfig<InframonitoringtypesPodRecordDTO> =
{
list: {
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
tableColumns: k8sPodColumnsConfig,
fetchListData,
getRowKey: getK8sPodRowKey,
getItemKey: getK8sPodItemKey,
detailsQueryKeyPrefix: 'pod',
},
details: {
category: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
queryKeyPrefix: 'pod',
getSelectedItemExpression: k8sPodGetSelectedItemExpression,
fetchEntityData,
getEntityName: k8sPodGetEntityName,
getInitialLogTracesExpression: k8sPodInitialLogTracesExpression,
getInitialEventsExpression: k8sPodInitialEventsExpression,
metadataConfig: k8sPodDetailsMetadataConfig,
entityWidgetInfo: podWidgetInfo,
getEntityQueryPayload: getPodMetricsQueryPayload,
},
};

View File

@@ -131,16 +131,13 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
width: { min: 250 },
enableSort: false,
visibilityBehavior: 'hidden-on-collapse',
cell: ({ row, rowId }): React.ReactNode => {
cell: ({ row }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts
items={getPodStatusItems(row.podCountsByStatus)}
rowId={rowId}
/>
<GroupedStatusCounts items={getPodStatusItems(row.podCountsByStatus)} />
);
},
},
@@ -174,7 +171,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
</ColumnHeader>
),
accessorFn: (row): number => row.podRestarts,
width: { min: 140 },
width: { min: 100 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const restarts = value as number;

View File

@@ -0,0 +1,164 @@
import { useCallback, useMemo } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listStatefulSets } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesResponseTypeDTO,
InframonitoringtypesStatefulSetRecordDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
getStatefulSetMetricsQueryPayload,
getStatefulSetPodMetricsQueryPayload,
k8sStatefulSetDetailsMetadataConfig,
k8sStatefulSetGetEntityName,
k8sStatefulSetGetSelectedItemExpression,
k8sStatefulSetInitialEventsExpression,
k8sStatefulSetInitialLogTracesExpression,
statefulSetWidgetInfo,
} from './constants';
import {
getK8sStatefulSetItemKey,
getK8sStatefulSetRowKey,
k8sStatefulSetsColumnsConfig,
} from './table.config';
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
function K8sStatefulSetsList({
controlListPrefix,
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
try {
const response = await listStatefulSets(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
warning: data.warning,
};
} catch (error) {
return {
type: 'list' as const,
records: [] as InframonitoringtypesStatefulSetRecordDTO[],
total: 0,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesStatefulSetRecordDTO | null;
error?: APIError | null;
}> => {
try {
const response = await listStatefulSets(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
return {
data: null,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
const customTabs = useMemo(
() => [
createPodMetricsTab<InframonitoringtypesStatefulSetRecordDTO>({
getQueryPayload: getStatefulSetPodMetricsQueryPayload,
category: InfraMonitoringEntity.STATEFULSETS,
queryKey: 'statefulSetPodMetrics',
}),
],
[],
);
return (
<>
<K8sBaseList<InframonitoringtypesStatefulSetRecordDTO, SelectedItemParams>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.STATEFULSETS}
tableColumns={k8sStatefulSetsColumnsConfig}
fetchListData={fetchListData}
getRowKey={getK8sStatefulSetRowKey}
getItemKey={getK8sStatefulSetItemKey}
eventCategory={InfraMonitoringEvents.StatefulSet}
/>
<K8sBaseDetails<InframonitoringtypesStatefulSetRecordDTO>
category={InfraMonitoringEntity.STATEFULSETS}
eventCategory={InfraMonitoringEvents.StatefulSet}
getSelectedItemExpression={k8sStatefulSetGetSelectedItemExpression}
fetchEntityData={fetchEntityData}
getEntityName={k8sStatefulSetGetEntityName}
getInitialLogTracesExpression={k8sStatefulSetInitialLogTracesExpression}
getInitialEventsExpression={k8sStatefulSetInitialEventsExpression}
metadataConfig={k8sStatefulSetDetailsMetadataConfig}
entityWidgetInfo={statefulSetWidgetInfo}
getEntityQueryPayload={getStatefulSetMetricsQueryPayload}
queryKeyPrefix="statefulSet"
customTabs={customTabs}
/>
</>
);
}
export default K8sStatefulSetsList;

View File

@@ -1,153 +0,0 @@
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listStatefulSets } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesResponseTypeDTO,
InframonitoringtypesStatefulSetRecordDTO,
Querybuildertypesv5OrderDirectionDTO,
RenderErrorResponseDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import { InfraMonitoringEvents } from 'constants/events';
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
import { K8sEntityConfig } from '../Base/entity.config.types';
import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
getStatefulSetMetricsQueryPayload,
getStatefulSetPodMetricsQueryPayload,
k8sStatefulSetDetailsMetadataConfig,
k8sStatefulSetGetEntityName,
k8sStatefulSetGetSelectedItemExpression,
k8sStatefulSetInitialEventsExpression,
k8sStatefulSetInitialLogTracesExpression,
statefulSetWidgetInfo,
} from './constants';
import {
getK8sStatefulSetItemKey,
getK8sStatefulSetRowKey,
k8sStatefulSetsColumnsConfig,
} from './table.config';
async function fetchListData(
filters: K8sBaseFilters,
signal?: AbortSignal,
): ReturnType<
K8sEntityConfig<
InframonitoringtypesStatefulSetRecordDTO,
SelectedItemParams
>['list']['fetchListData']
> {
try {
const response = await listStatefulSets(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
warning: data.warning,
};
} catch (error) {
return {
type: 'list' as const,
records: [] as InframonitoringtypesStatefulSetRecordDTO[],
total: 0,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
}
async function fetchEntityData(
filters: K8sDetailsFilters,
signal?: AbortSignal,
): ReturnType<
K8sEntityConfig<
InframonitoringtypesStatefulSetRecordDTO,
SelectedItemParams
>['details']['fetchEntityData']
> {
try {
const response = await listStatefulSets(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
return {
data: null,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
}
export const statefulSetEntityConfig: K8sEntityConfig<
InframonitoringtypesStatefulSetRecordDTO,
SelectedItemParams
> = {
list: {
entity: InfraMonitoringEntity.STATEFULSETS,
eventCategory: InfraMonitoringEvents.StatefulSet,
tableColumns: k8sStatefulSetsColumnsConfig,
fetchListData,
getRowKey: getK8sStatefulSetRowKey,
getItemKey: getK8sStatefulSetItemKey,
detailsQueryKeyPrefix: 'statefulSet',
},
details: {
category: InfraMonitoringEntity.STATEFULSETS,
eventCategory: InfraMonitoringEvents.StatefulSet,
queryKeyPrefix: 'statefulSet',
getSelectedItemExpression: k8sStatefulSetGetSelectedItemExpression,
fetchEntityData,
getEntityName: k8sStatefulSetGetEntityName,
getInitialLogTracesExpression: k8sStatefulSetInitialLogTracesExpression,
getInitialEventsExpression: k8sStatefulSetInitialEventsExpression,
metadataConfig: k8sStatefulSetDetailsMetadataConfig,
entityWidgetInfo: statefulSetWidgetInfo,
getEntityQueryPayload: getStatefulSetMetricsQueryPayload,
customTabs: [
createPodMetricsTab<InframonitoringtypesStatefulSetRecordDTO>({
getQueryPayload: getStatefulSetPodMetricsQueryPayload,
category: InfraMonitoringEntity.STATEFULSETS,
queryKey: 'statefulSetPodMetrics',
docBasePath: '/infrastructure-monitoring/kubernetes/statefulsets/',
}),
],
},
};

View File

@@ -122,17 +122,12 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
width: { min: 250 },
enableSort: false,
enableResize: true,
cell: ({ row, rowId }): React.ReactNode => {
cell: ({ row }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts
items={getPodStatusItems(podCountsByStatus)}
rowId={rowId}
/>
);
return <GroupedStatusCounts items={getPodStatusItems(podCountsByStatus)} />;
},
},
{
@@ -146,7 +141,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
width: { min: 140 },
enableSort: false,
enableResize: true,
cell: ({ row, rowId }): React.ReactNode => (
cell: ({ row }): React.ReactNode => (
<GroupedStatusCounts
items={[
{
@@ -160,7 +155,6 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
color: Color.BG_ROBIN_500,
},
]}
rowId={rowId}
/>
),
},

View File

@@ -0,0 +1,151 @@
import { useCallback } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listVolumes } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesResponseTypeDTO,
InframonitoringtypesVolumeRecordDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
getVolumeMetricsQueryPayload,
k8sVolumeDetailsMetadataConfig,
k8sVolumeGetEntityName,
k8sVolumeGetSelectedItemExpression,
k8sVolumeInitialEventsExpression,
k8sVolumeInitialLogTracesExpression,
volumeWidgetInfo,
} from './constants';
import {
getK8sVolumeItemKey,
getK8sVolumeRowKey,
k8sVolumesColumnsConfig,
} from './table.config';
function K8sVolumesList({
controlListPrefix,
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
try {
const response = await listVolumes(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
warning: data.warning,
};
} catch (error) {
return {
type: 'list' as const,
records: [] as InframonitoringtypesVolumeRecordDTO[],
total: 0,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesVolumeRecordDTO | null;
error?: APIError | null;
}> => {
try {
const response = await listVolumes(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
return {
data: null,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
[],
);
return (
<>
<K8sBaseList<InframonitoringtypesVolumeRecordDTO, SelectedItemParams>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.VOLUMES}
tableColumns={k8sVolumesColumnsConfig}
fetchListData={fetchListData}
getRowKey={getK8sVolumeRowKey}
getItemKey={getK8sVolumeItemKey}
eventCategory={InfraMonitoringEvents.Volumes}
/>
<K8sBaseDetails<InframonitoringtypesVolumeRecordDTO>
category={InfraMonitoringEntity.VOLUMES}
eventCategory={InfraMonitoringEvents.Volume}
getSelectedItemExpression={k8sVolumeGetSelectedItemExpression}
fetchEntityData={fetchEntityData}
getEntityName={k8sVolumeGetEntityName}
getInitialLogTracesExpression={k8sVolumeInitialLogTracesExpression}
getInitialEventsExpression={k8sVolumeInitialEventsExpression}
metadataConfig={k8sVolumeDetailsMetadataConfig}
entityWidgetInfo={volumeWidgetInfo}
getEntityQueryPayload={getVolumeMetricsQueryPayload}
queryKeyPrefix="volume"
hideDetailViewTabs
/>
</>
);
}
export default K8sVolumesList;

View File

@@ -1,141 +0,0 @@
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listVolumes } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesResponseTypeDTO,
InframonitoringtypesVolumeRecordDTO,
Querybuildertypesv5OrderDirectionDTO,
RenderErrorResponseDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import { InfraMonitoringEvents } from 'constants/events';
import { K8sEntityConfig } from '../Base/entity.config.types';
import { K8sBaseFilters, K8sDetailsFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
getVolumeMetricsQueryPayload,
k8sVolumeDetailsMetadataConfig,
k8sVolumeGetEntityName,
k8sVolumeGetSelectedItemExpression,
k8sVolumeInitialEventsExpression,
k8sVolumeInitialLogTracesExpression,
volumeWidgetInfo,
} from './constants';
import {
getK8sVolumeItemKey,
getK8sVolumeRowKey,
k8sVolumesColumnsConfig,
} from './table.config';
async function fetchListData(
filters: K8sBaseFilters,
signal?: AbortSignal,
): ReturnType<
K8sEntityConfig<
InframonitoringtypesVolumeRecordDTO,
SelectedItemParams
>['list']['fetchListData']
> {
try {
const response = await listVolumes(
{
filter: { expression: filters.filter.expression },
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
warning: data.warning,
};
} catch (error) {
return {
type: 'list' as const,
records: [] as InframonitoringtypesVolumeRecordDTO[],
total: 0,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
}
async function fetchEntityData(
filters: K8sDetailsFilters,
signal?: AbortSignal,
): ReturnType<
K8sEntityConfig<InframonitoringtypesVolumeRecordDTO>['details']['fetchEntityData']
> {
try {
const response = await listVolumes(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
return {
data: null,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
}
export const volumeEntityConfig: K8sEntityConfig<
InframonitoringtypesVolumeRecordDTO,
SelectedItemParams
> = {
list: {
entity: InfraMonitoringEntity.VOLUMES,
eventCategory: InfraMonitoringEvents.Volumes,
tableColumns: k8sVolumesColumnsConfig,
fetchListData,
getRowKey: getK8sVolumeRowKey,
getItemKey: getK8sVolumeItemKey,
detailsQueryKeyPrefix: 'volume',
},
details: {
category: InfraMonitoringEntity.VOLUMES,
eventCategory: InfraMonitoringEvents.Volume,
queryKeyPrefix: 'volume',
getSelectedItemExpression: k8sVolumeGetSelectedItemExpression,
fetchEntityData,
getEntityName: k8sVolumeGetEntityName,
getInitialLogTracesExpression: k8sVolumeInitialLogTracesExpression,
getInitialEventsExpression: k8sVolumeInitialEventsExpression,
metadataConfig: k8sVolumeDetailsMetadataConfig,
entityWidgetInfo: volumeWidgetInfo,
getEntityQueryPayload: getVolumeMetricsQueryPayload,
hideDetailViewTabs: true,
},
};

View File

@@ -4,24 +4,33 @@
gap: 4px;
}
.item {
display: inline-flex;
.itemWrapper {
display: flex;
align-items: center;
gap: 4px;
// 4ch value slot (old .valueWrapper) + 3px color bar + 4px gap
min-width: calc(4ch + 7px);
font-variant-numeric: tabular-nums;
text-align: left;
cursor: default;
}
.item::before {
content: '';
.separator {
width: 3px;
height: 14px;
border-radius: 1px;
flex-shrink: 0;
background-color: var(--gsc-color);
}
.valueWrapper {
min-width: 4ch;
}
.valueWrapperTooltip {
display: block;
width: fit-content;
}
.value {
font-variant-numeric: tabular-nums;
text-align: left;
cursor: default;
min-width: min-content;
}
.tooltipContent {

View File

@@ -1,3 +1,5 @@
import { TooltipSimple } from '@signozhq/ui/tooltip';
import styles from './GroupedStatusCounts.module.scss';
import TanStackTable from 'components/TanStackTableView';
import { Typography } from '@signozhq/ui/typography';
@@ -16,7 +18,6 @@ export interface StatusCountItem {
interface GroupedStatusCountsProps {
items: StatusCountItem[];
rowId: string;
showZeroValues?: boolean;
}
@@ -61,7 +62,6 @@ function buildTooltipContent(item: StatusCountItem): React.ReactNode {
export function GroupedStatusCounts({
items,
rowId,
showZeroValues = true,
}: GroupedStatusCountsProps): JSX.Element {
const visibleItems =
@@ -74,20 +74,21 @@ export function GroupedStatusCounts({
return (
<div className={styles.container}>
{visibleItems.map((item) => (
<TanStackTable.HoverTooltip
key={item.label}
rowId={rowId}
title={buildTooltipContent(item)}
arrow
align="start"
>
<TanStackTable.Text
className={styles.item}
style={{ '--gsc-color': item.color } as React.CSSProperties}
>
{item.value || '-'}
</TanStackTable.Text>
</TanStackTable.HoverTooltip>
<div key={item.label} className={styles.itemWrapper}>
<div
className={styles.separator}
style={{ backgroundColor: item.color }}
/>
<div className={styles.valueWrapper}>
<TooltipSimple title={buildTooltipContent(item)} arrow align="start">
<span className={styles.valueWrapperTooltip}>
<TanStackTable.Text className={styles.value}>
{item.value || '-'}
</TanStackTable.Text>
</span>
</TooltipSimple>
</div>
</div>
))}
</div>
);

View File

@@ -905,9 +905,6 @@ export const INFRA_MONITORING_K8S_PARAMS_KEYS = {
SELECTED_ITEM: 'selectedItem',
SELECTED_ITEM_CLUSTER_NAME: 'selectedItemClusterName',
SELECTED_ITEM_NAMESPACE_NAME: 'selectedItemNamespaceName',
DETAIL_RELATIVE_TIME: 'detailRelativeTime',
DETAIL_START_TIME: 'detailStartTime',
DETAIL_END_TIME: 'detailEndTime',
};
/** Metric namespace prefixes for /fields/keys and /fields/values APIs */
@@ -937,27 +934,22 @@ export const podUtilizationByPodWidgetInfo = [
{
title: 'CPU Limit Utilization By Pod Name',
yAxisUnit: 'percentunit',
docPath: '#cpu-limit-utilization-by-pod-name',
},
{
title: 'CPU Request Utilization By Pod Name',
yAxisUnit: 'percentunit',
docPath: '#cpu-request-utilization-by-pod-name',
},
{
title: 'Memory Limit Utilization By Pod Name',
yAxisUnit: 'percentunit',
docPath: '#memory-limit-utilization-by-pod-name',
},
{
title: 'Memory Request Utilization By Pod Name',
yAxisUnit: 'percentunit',
docPath: '#memory-request-utilization-by-pod-name',
},
{
title: 'FileSystem Usage Percentage By Pod Name',
yAxisUnit: 'percentunit',
docPath: '#filesystem-usage-percentage-by-pod-name',
},
];

View File

@@ -17,9 +17,12 @@ import { ChevronDown, Dot, PencilLine, Plug, Plus } from '@signozhq/icons';
import AzureCloudAccountSetupModal from '../../AzureCloudServices/AddNewAccount/CloudAccountSetupModal';
import AzureAccountSettingsModal from '../../AzureCloudServices/EditAccount/AccountSettingsModal';
import GcpCloudAccountSetupDrawer from '../../GoogleCloudPlatform/AddNewAccount/CloudAccountSetupDrawer';
import GcpAccountSettingsDrawer from '../../GoogleCloudPlatform/EditAccount/AccountSettingsDrawer';
import {
mapAccountDtoToAwsCloudAccount,
mapAccountDtoToAzureCloudAccount,
mapAccountDtoToGcpCloudAccount,
} from '../../mapCloudAccountFromDto';
import AwsCloudAccountSetupModal from '../AddNewAccount/CloudAccountSetupModal';
import AwsAccountSettingsModal from '../EditAccount/AccountSettingsModal';
@@ -156,6 +159,18 @@ function AccountActions({ type }: { type: IntegrationType }): JSX.Element {
});
}
if (type === IntegrationType.GCP_SERVICES) {
raw.forEach((account) => {
if (!account) {
return;
}
const mapped = mapAccountDtoToGcpCloudAccount(account);
if (mapped) {
mappedAccounts.push(mapped);
}
});
}
return mappedAccounts;
}, [listAccountsResponse, type]);
@@ -207,13 +222,23 @@ function AccountActions({ type }: { type: IntegrationType }): JSX.Element {
// log telemetry event when an account is viewed.
useEffect(() => {
if (activeAccount) {
const { config } = activeAccount;
let enabledRegions: string[];
if ('regions' in config) {
// AWS
enabledRegions = config.regions;
} else if ('resource_groups' in config) {
// Azure
enabledRegions = config.resource_groups;
} else {
// GCP
enabledRegions = config.project_ids;
}
logEvent(`${type} Integration: Account viewed`, {
cloudAccountId: activeAccount?.cloud_account_id,
status: activeAccount?.status,
enabledRegions:
'regions' in activeAccount.config
? activeAccount.config.regions
: activeAccount.config.resource_groups,
enabledRegions,
});
}
}, [activeAccount, type]);
@@ -260,6 +285,11 @@ function AccountActions({ type }: { type: IntegrationType }): JSX.Element {
onClose={(): void => setIsIntegrationModalOpen(false)}
/>
)}
{type === IntegrationType.GCP_SERVICES && (
<GcpCloudAccountSetupDrawer
onClose={(): void => setIsIntegrationModalOpen(false)}
/>
)}
</>
)}
@@ -281,6 +311,13 @@ function AccountActions({ type }: { type: IntegrationType }): JSX.Element {
setActiveAccount={setActiveAccount}
/>
)}
{type === IntegrationType.GCP_SERVICES && (
<GcpAccountSettingsDrawer
onClose={(): void => setIsAccountSettingsModalOpen(false)}
account={activeAccount}
setActiveAccount={setActiveAccount}
/>
)}
</>
)}
</div>

View File

@@ -46,14 +46,31 @@ const EMPTY_FORM_VALUES: ServiceConfigFormValues = {
s3BucketsByRegion: {},
};
function getIntegrationServiceConfig(
type: IntegrationType,
serviceDetailsData?: ServiceDetailsData,
):
| { logs?: { enabled?: boolean }; metrics?: { enabled?: boolean } }
| undefined {
const config = serviceDetailsData?.cloudIntegrationService?.config;
if (type === IntegrationType.AWS_SERVICES) {
return config?.aws;
}
if (type === IntegrationType.GCP_SERVICES) {
return config?.gcp;
}
return config?.azure;
}
function getInitialFormValues(
type: IntegrationType,
serviceDetailsData?: ServiceDetailsData,
): ServiceConfigFormValues {
const integrationConfig =
type === IntegrationType.AWS_SERVICES
? serviceDetailsData?.cloudIntegrationService?.config?.aws
: serviceDetailsData?.cloudIntegrationService?.config?.azure;
const integrationConfig = getIntegrationServiceConfig(
type,
serviceDetailsData,
);
return {
logsEnabled: integrationConfig?.logs?.enabled || false,
@@ -98,16 +115,21 @@ function getServiceConfigPayload({
};
}
return {
azure: {
logs: {
enabled: isLogsSupported ? logsEnabled : false,
},
metrics: {
enabled: isMetricsSupported ? metricsEnabled : false,
},
// Azure and GCP share the same simple logs/metrics enable-flag shape.
const signalConfig = {
logs: {
enabled: isLogsSupported ? logsEnabled : false,
},
metrics: {
enabled: isMetricsSupported ? metricsEnabled : false,
},
};
if (type === IntegrationType.GCP_SERVICES) {
return { gcp: signalConfig };
}
return { azure: signalConfig };
}
function ServiceDetails({
@@ -163,10 +185,10 @@ function ServiceDetails({
? isAccountServiceLoading
: isReadOnlyServiceLoading;
const integrationConfig =
type === IntegrationType.AWS_SERVICES
? serviceDetailsData?.cloudIntegrationService?.config?.aws
: serviceDetailsData?.cloudIntegrationService?.config?.azure;
const integrationConfig = getIntegrationServiceConfig(
type,
serviceDetailsData,
);
const isServiceEnabledInPersistedConfig =
Boolean(integrationConfig?.logs?.enabled) ||
Boolean(integrationConfig?.metrics?.enabled);

View File

@@ -36,8 +36,12 @@ function AccountSettingsModal({
const queryClient = useQueryClient();
// `account.config` is the shared per-provider union (Azure | AWS | GCP).
// Narrow to Azure by `resource_groups` (Azure-only) rather than
// `deployment_region`, which GCP also has — so it no longer identifies
// Azure uniquely.
const azureConfig = useMemo(
() => ('deployment_region' in account.config ? account.config : null),
() => ('resource_groups' in account.config ? account.config : null),
[account.config],
);

View File

@@ -0,0 +1,150 @@
.setupDrawer {
--dialog-header-padding: var(--spacing-10) var(--spacing-12);
--dialog-footer-padding: var(--spacing-8) var(--spacing-12);
// Fill every field with --l2-background (Input/ComboboxSimple default to transparent).
--input-background: var(--l2-background);
--input-hover-background: var(--l2-background);
--input-focus-background: var(--l2-background);
--combobox-trigger-background-color: var(--l2-background);
// Text uses the brighter --l1-foreground, placeholders the duller --l3-foreground.
--input-foreground: var(--l1-foreground);
--input-placeholder-color: var(--l3-foreground);
--combobox-trigger-placeholder-color: var(--l3-foreground);
// Shared resting border (--l2-border) and text size; focus borders stay per-component.
--input-border-color: var(--l2-border);
--input-hover-border-color: var(--l2-border);
--combobox-trigger-border-color: var(--l2-border);
--input-font-size: var(--periscope-font-size-base);
--combobox-trigger-font-size: var(--periscope-font-size-base);
--combobox-trigger-value-font-size: var(--periscope-font-size-base);
// Bounded flex column so the header/footer stay put and only the body
// scrolls when content overflows.
display: flex;
flex-direction: column;
overflow: hidden;
[data-slot='drawer-header'],
[data-slot='drawer-footer'] {
flex-shrink: 0;
}
// The drawer body renders inside [data-slot='drawer-description'] — this is
// the only region allowed to scroll.
[data-slot='drawer-description'] {
display: flex;
flex: 1 1 auto;
flex-direction: column;
gap: var(--spacing-10);
min-height: 0;
padding: var(--spacing-10) var(--spacing-12);
overflow-y: auto;
}
[data-slot='select-content'] {
width: var(--radix-select-trigger-width);
}
[data-slot='combobox-content'] {
z-index: 5;
background: var(--l1-background);
border: 1px solid var(--l2-border);
border-radius: var(--radius-2);
}
// Selected region value: bright, like every other field's text.
[data-slot='combobox-value'] {
color: var(--l1-foreground);
}
// Empty trigger (.regionEmpty, set from the RHF value): dull the placeholder text.
.regionEmpty [data-slot='combobox-value'] {
color: var(--l3-foreground);
}
}
.title {
h3 {
margin: 0;
font-size: var(--periscope-font-size-medium);
font-weight: var(--font-weight-semibold);
}
}
.footerContainer {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
width: 100%;
}
.footer {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0;
}
.drawerSection {
composes: drawerSection from './shared.module.scss';
}
.mono {
composes: mono from './shared.module.scss';
}
.fullWidth {
width: 100%;
}
.fieldError {
composes: fieldError from './shared.module.scss';
}
.projectIdsSelect {
:global(.ant-select-selector) {
min-height: 36px;
background: var(--l2-background);
border: 1px solid var(--l2-border) !important;
}
&:hover :global(.ant-select-selector),
&:global(.ant-select-focused) :global(.ant-select-selector) {
border-color: var(--l2-border);
box-shadow: none;
}
// antd defaults to 14px; pin to 13px to line up with the Input/Combobox fields.
:global(.ant-select-selection-placeholder),
:global(.ant-select-selection-search-input),
:global(.ant-select-selection-item) {
font-size: var(--periscope-font-size-base);
}
:global(.ant-select-selection-placeholder) {
color: var(--l3-foreground);
}
:global(.ant-select-selection-search-input) {
color: var(--l1-foreground);
}
:global(.ant-select-selection-item) {
color: var(--l1-foreground);
background: var(--l2-background);
border: 1px solid var(--l2-border);
border-radius: var(--radius-2);
}
:global(.ant-select-selection-item-remove) {
color: var(--l3-foreground);
&:hover {
color: var(--l1-foreground);
}
}
}

View File

@@ -0,0 +1,286 @@
import { useEffect, useState } from 'react';
import { Button } from '@signozhq/ui/button';
import { Callout } from '@signozhq/ui/callout';
import { ComboboxSimple } from '@signozhq/ui/combobox';
import { DrawerWrapper } from '@signozhq/ui/drawer';
import { Input } from '@signozhq/ui/input';
import { Typography } from '@signozhq/ui/typography';
import { Select } from 'antd';
import cx from 'classnames';
import { GCP_REGIONS } from 'container/Integrations/constants';
import { IntegrationModalProps } from 'container/Integrations/HeroSection/types';
import { useCloudAccountSetupDrawer } from 'hooks/integration/gcp/useCloudAccountSetupDrawer';
import { Controller, useForm } from 'react-hook-form';
import { popupContainer } from 'utils/selectPopupContainer';
import ConnectionSecretsFields from './ConnectionSecretsFields';
import FieldLabel from './FieldLabel';
import FlowSelector from './FlowSelector';
import { GcpSetupFormValues, SetupFlow } from './types';
import styles from './CloudAccountSetupDrawer.module.scss';
const REGION_ITEMS = GCP_REGIONS.map((region) => ({
value: region.value,
label: `${region.label} (${region.value})`,
}));
const DEFAULT_VALUES: GcpSetupFormValues = {
accountName: '',
deploymentProjectId: '',
deploymentRegion: '',
projectIds: [],
sigNozApiUrl: '',
sigNozApiKey: '',
ingestionUrl: '',
ingestionKey: '',
};
function CloudAccountSetupDrawer({
onClose,
}: IntegrationModalProps): JSX.Element {
const {
isLoading,
connectAccount,
handleClose,
connectionParams,
isConnectionParamsLoading,
submitError,
clearSubmitError,
} = useCloudAccountSetupDrawer({ onClose });
const { control, handleSubmit, setValue } = useForm<GcpSetupFormValues>({
defaultValues: DEFAULT_VALUES,
});
const [flow, setFlow] = useState<SetupFlow>('manual');
// Pre-fill the deployment/ingestion fields with the fetched credentials.
useEffect(() => {
if (!connectionParams) {
return;
}
setValue('sigNozApiUrl', connectionParams.sigNozApiUrl);
setValue('sigNozApiKey', connectionParams.sigNozApiKey);
setValue('ingestionUrl', connectionParams.ingestionUrl);
setValue('ingestionKey', connectionParams.ingestionKey);
}, [connectionParams, setValue]);
const footer = (
<div className={styles.footerContainer}>
{submitError && (
<Callout
type="error"
size="small"
showIcon
action="dismissible"
onClick={clearSubmitError}
title="Failed to connect GCP account"
testId="gcp-connect-error"
>
{submitError}
</Callout>
)}
<div className={styles.footer}>
<Button
variant="outlined"
color="secondary"
onClick={handleClose}
testId="gcp-cancel-btn"
>
Cancel
</Button>
<Button
variant="solid"
color="primary"
onClick={handleSubmit(connectAccount)}
loading={isLoading}
disabled={isConnectionParamsLoading}
testId="gcp-connect-account-btn"
>
Connect Account
</Button>
</div>
</div>
);
return (
<DrawerWrapper
open={true}
className={styles.setupDrawer}
onOpenChange={(open): void => {
if (!open) {
handleClose();
}
}}
direction="right"
showCloseButton
title="Connect Google Cloud Platform"
width="base"
footer={footer}
drawerHeaderProps={{ className: styles.title }}
>
<FlowSelector value={flow} onChange={setFlow} />
<div className={styles.drawerSection}>
<FieldLabel
htmlFor="gcp-account-name-input"
label="Account Name"
tooltip="A label to identify this group of GCP projects (org ID, billing email, or any descriptive name)"
required
/>
<Controller
name="accountName"
control={control}
rules={{ required: 'Please enter an account name' }}
render={({ field, fieldState }): JSX.Element => (
<>
<Input
id="gcp-account-name-input"
className={styles.fullWidth}
placeholder="e.g. my-org or billing@company.com"
value={field.value}
onChange={(e): void => field.onChange(e.target.value)}
testId="gcp-account-name-input"
/>
{fieldState.error && (
<Typography.Text
as="span"
size="small"
role="alert"
className={styles.fieldError}
>
{fieldState.error.message}
</Typography.Text>
)}
</>
)}
/>
</div>
<div className={styles.drawerSection}>
<FieldLabel
htmlFor="gcp-deployment-project-id-input"
label="Deployment Project ID"
tooltip="The GCP project that hosts your OTel Collector deployment — often separate from the projects you actually monitor"
required
/>
<Controller
name="deploymentProjectId"
control={control}
rules={{ required: 'Please enter the deployment project ID' }}
render={({ field, fieldState }): JSX.Element => (
<>
<Input
id="gcp-deployment-project-id-input"
className={cx(styles.fullWidth, styles.mono)}
placeholder="e.g. my-deployment-project-123"
value={field.value}
onChange={(e): void => field.onChange(e.target.value)}
testId="gcp-deployment-project-id-input"
/>
{fieldState.error && (
<Typography.Text
as="span"
size="small"
role="alert"
className={styles.fieldError}
>
{fieldState.error.message}
</Typography.Text>
)}
</>
)}
/>
</div>
<div className={styles.drawerSection}>
<FieldLabel
htmlFor="gcp-deployment-region-select"
label="Deployment Region"
tooltip="The GCP region where your OTel Collector will be deployed"
required
/>
<Controller
name="deploymentRegion"
control={control}
rules={{ required: 'Please select a region' }}
render={({ field, fieldState }): JSX.Element => (
<>
<ComboboxSimple
id="gcp-deployment-region-select"
className={cx(styles.fullWidth, {
[styles.regionEmpty]: !field.value,
})}
items={REGION_ITEMS}
value={field.value}
onChange={(value): void => field.onChange(value as string)}
placeholder="Select a region..."
inputPlaceholder="Search regions…"
withPortal={false}
testId="gcp-deployment-region-select"
/>
{fieldState.error && (
<Typography.Text
as="span"
size="small"
role="alert"
className={styles.fieldError}
>
{fieldState.error.message}
</Typography.Text>
)}
</>
)}
/>
</div>
<div className={styles.drawerSection}>
<FieldLabel
htmlFor="gcp-project-ids-select"
label="Projects to Monitor"
tooltip="Enter each GCP project ID then press Enter"
required
/>
<Controller
name="projectIds"
control={control}
rules={{
validate: (value): true | string =>
value.length > 0 || 'Please add at least one project ID',
}}
render={({ field, fieldState }): JSX.Element => (
<>
<Select
id="gcp-project-ids-select"
className={cx(styles.fullWidth, styles.projectIdsSelect)}
mode="tags"
value={field.value}
onChange={(value): void => field.onChange(value)}
placeholder="Add project IDs…"
tokenSeparators={[',', ' ']}
notFoundContent={null}
suffixIcon={null}
getPopupContainer={popupContainer}
data-testid="gcp-project-ids-select"
/>
{fieldState.error && (
<Typography.Text
as="span"
size="small"
role="alert"
className={styles.fieldError}
>
{fieldState.error.message}
</Typography.Text>
)}
</>
)}
/>
</div>
<ConnectionSecretsFields
control={control}
isLoading={isConnectionParamsLoading}
connectionParams={connectionParams}
/>
</DrawerWrapper>
);
}
export default CloudAccountSetupDrawer;

View File

@@ -0,0 +1,71 @@
.drawerSurface {
composes: drawerSurface from './shared.module.scss';
}
.drawerSurfaceHead {
composes: drawerSurfaceHead from './shared.module.scss';
}
.drawerSection {
composes: drawerSection from './shared.module.scss';
}
.headLabel {
display: flex;
justify-content: center;
align-items: center;
gap: var(--spacing-2);
}
.mono {
composes: mono from './shared.module.scss';
}
.fieldError {
composes: fieldError from './shared.module.scss';
}
.fullWidth {
width: 100%;
}
.secretsBody {
display: flex;
flex-direction: column;
gap: var(--spacing-6);
}
.skeletonLabel :global(.ant-skeleton-input) {
width: 120px;
min-width: 120px;
height: 14px;
}
.skeletonInput :global(.ant-skeleton-input) {
width: 100%;
min-width: 100%;
height: 36px;
}
.readonlyField {
display: flex;
gap: var(--spacing-2);
align-items: center;
height: 36px;
padding: 0 var(--spacing-2) 0 var(--spacing-4);
background: var(--l2-background);
border: 1px solid var(--l2-border);
border-radius: var(--radius-2);
}
.readonlyValue {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
// Match the other fields' value text: 13px and the brighter --l1-foreground.
font-size: var(--periscope-font-size-base);
color: var(--l2-foreground);
text-overflow: ellipsis;
white-space: nowrap;
cursor: not-allowed;
}

View File

@@ -0,0 +1,193 @@
import { Lock } from '@signozhq/icons';
import { Input } from '@signozhq/ui/input';
import { toast } from '@signozhq/ui/sonner';
import { Typography } from '@signozhq/ui/typography';
import { Skeleton } from 'antd';
import { CloudintegrationtypesCredentialsDTO } from 'api/generated/services/sigNoz.schemas';
import cx from 'classnames';
import CopyButton from 'periscope/components/CopyButton/CopyButton';
import { Control, Controller } from 'react-hook-form';
import FieldLabel from './FieldLabel';
import { GcpSetupFormValues } from './types';
import { SecretFieldType, validateSecretValue } from './validators';
import styles from './ConnectionSecretsFields.module.scss';
type CredentialField = keyof CloudintegrationtypesCredentialsDTO;
interface FieldConfig {
name: CredentialField;
label: string;
tooltip: string;
placeholder: string;
testId: string;
type: SecretFieldType;
}
const FIELDS: FieldConfig[] = [
{
name: 'sigNozApiUrl',
label: 'SigNoz API URL',
tooltip: 'Base URL of your SigNoz instance the collector reports to',
placeholder: 'https://<tenant>.signoz.cloud',
testId: 'gcp-signoz-api-url-input',
type: 'url',
},
{
name: 'sigNozApiKey',
label: 'SigNoz API Key',
tooltip: 'API key used to authenticate with your SigNoz instance',
placeholder: 'Enter SigNoz API key',
testId: 'gcp-signoz-api-key-input',
type: 'text',
},
{
name: 'ingestionUrl',
label: 'Ingestion URL',
tooltip: 'OTLP ingestion endpoint your OTel Collector sends telemetry to',
placeholder: 'https://ingest.<region>.signoz.cloud',
testId: 'gcp-ingestion-url-input',
type: 'url',
},
{
name: 'ingestionKey',
label: 'Ingestion Key',
tooltip: 'Ingestion key that authorizes telemetry sent to SigNoz',
placeholder: 'Enter ingestion key',
testId: 'gcp-ingestion-key-input',
type: 'text',
},
];
interface ConnectionSecretsFieldsProps {
control: Control<GcpSetupFormValues>;
isLoading: boolean;
connectionParams?: CloudintegrationtypesCredentialsDTO;
}
function ConnectionSecretsFields({
control,
isLoading,
connectionParams,
}: ConnectionSecretsFieldsProps): JSX.Element {
const hasMissingValue = FIELDS.some(
(field) => !connectionParams?.[field.name],
);
return (
<div className={styles.drawerSurface}>
<div className={styles.drawerSurfaceHead}>
<Typography.Text weight="bold" size="base">
Deployment details &amp; ingestion secrets
</Typography.Text>
{!hasMissingValue && (
<div className={styles.headLabel}>
<Lock size={12} />
<Typography.Text as="span" size="small" className={styles.headLabel}>
Auto-filled by SigNoz
</Typography.Text>
</div>
)}
</div>
{isLoading ? (
<div className={styles.secretsBody} data-testid="gcp-secrets-skeleton">
{FIELDS.map((field) => (
<div key={field.name} className={styles.drawerSection}>
<Skeleton.Input active size="small" className={styles.skeletonLabel} />
<Skeleton.Input active block className={styles.skeletonInput} />
</div>
))}
</div>
) : (
<div className={styles.secretsBody}>
{FIELDS.map((field) => {
// Backend-provided values are read-only — the user can't edit them, so
// show a truncated value with a copy button. Missing values (enterprise)
// stay editable inputs with no copy button.
const providedValue = connectionParams?.[field.name];
if (providedValue) {
return (
<div key={field.name} className={styles.drawerSection}>
<FieldLabel
htmlFor={field.testId}
label={field.label}
tooltip={field.tooltip}
/>
<div className={styles.readonlyField}>
<Typography.Text
as="span"
id={field.testId}
className={cx(styles.readonlyValue, styles.mono)}
title={providedValue}
testId={field.testId}
>
{providedValue}
</Typography.Text>
<CopyButton
value={providedValue}
size={12}
ariaLabel={`Copy ${field.label}`}
testId={`${field.testId}-copy`}
onCopy={(): void => {
toast.success(`${field.label} copied to clipboard`, {
position: 'bottom-right',
});
}}
/>
</div>
</div>
);
}
return (
<div key={field.name} className={styles.drawerSection}>
<FieldLabel
htmlFor={field.testId}
label={field.label}
tooltip={field.tooltip}
/>
<Controller
name={field.name}
control={control}
rules={{
validate: (value): true | string =>
validateSecretValue(field.label, field.type, value),
}}
render={({ field: rhfField, fieldState }): JSX.Element => (
<>
<Input
id={field.testId}
className={cx(styles.fullWidth, styles.mono)}
placeholder={field.placeholder}
value={rhfField.value}
onChange={(e): void => rhfField.onChange(e.target.value)}
testId={field.testId}
/>
{fieldState.error && (
<Typography.Text
as="span"
size="small"
role="alert"
className={styles.fieldError}
>
{fieldState.error.message}
</Typography.Text>
)}
</>
)}
/>
</div>
);
})}
</div>
)}
</div>
);
}
ConnectionSecretsFields.defaultProps = {
connectionParams: undefined,
};
export default ConnectionSecretsFields;

View File

@@ -0,0 +1,22 @@
.fieldLabel {
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
}
.required {
composes: required from './shared.module.scss';
}
.infoTrigger {
display: inline-flex;
align-items: center;
color: var(--l3-foreground);
cursor: help;
}
.tooltipContent {
max-width: 240px;
white-space: normal;
word-break: break-word;
}

View File

@@ -0,0 +1,49 @@
import { Info } from '@signozhq/icons';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import styles from './FieldLabel.module.scss';
interface FieldLabelProps {
htmlFor: string;
label: string;
tooltip: string;
required?: boolean;
}
function FieldLabel({
htmlFor,
label,
tooltip,
required,
}: FieldLabelProps): JSX.Element {
return (
<label className={styles.fieldLabel} htmlFor={htmlFor}>
{label}
<TooltipSimple
title={tooltip}
side="top"
tooltipContentProps={{ className: styles.tooltipContent }}
>
<span
className={styles.infoTrigger}
aria-label={`${label} help`}
data-testid={`${htmlFor}-tooltip`}
>
<Info size={12} />
</span>
</TooltipSimple>
{required && (
<span className={styles.required} aria-hidden="true">
*
</span>
)}
</label>
);
}
FieldLabel.defaultProps = {
required: false,
};
export default FieldLabel;

View File

@@ -0,0 +1,84 @@
.drawerSection {
composes: drawerSection from './shared.module.scss';
}
.drawerSurface {
composes: drawerSurface from './shared.module.scss';
}
.drawerSurfaceHead {
composes: drawerSurfaceHead from './shared.module.scss';
}
.flowRadioGroup {
--radio-group-item-border-color: var(--l2-border);
display: flex;
flex-direction: column;
gap: var(--spacing-4);
width: 100%;
.flowRadio {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: flex-start;
gap: var(--spacing-5);
width: 100%;
padding: var(--spacing-5) var(--spacing-6);
margin: 0;
border: 1px solid transparent;
border-radius: var(--radius-2);
box-sizing: border-box;
cursor: pointer;
transition:
background-color 0.12s ease,
border-color 0.12s ease;
> button[role='radio'] {
flex: 0 0 16px;
width: 16px;
height: 16px;
margin-top: 3px;
}
> label {
flex: 1 1 auto;
min-width: 0;
display: block;
text-align: left;
cursor: pointer;
font-size: inherit;
font-weight: inherit;
color: inherit;
}
&.flowRadioManual:has(button[data-state='checked']) {
background: color-mix(in srgb, var(--accent-primary) 10%, transparent);
border-color: color-mix(in srgb, var(--accent-primary) 30%, transparent);
}
&:hover {
background: var(--l3-background-hover);
}
&:has(button[disabled]) {
cursor: not-allowed;
opacity: 0.55;
&:hover {
background: var(--l3-background);
}
}
}
}
.flowRadioTitle {
display: flex;
gap: var(--spacing-2);
align-items: center;
}
.flowRadioDesc {
margin-top: var(--spacing-2);
}

View File

@@ -0,0 +1,76 @@
import { Badge } from '@signozhq/ui/badge';
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { SetupFlow } from './types';
import styles from './FlowSelector.module.scss';
interface FlowSelectorProps {
value: SetupFlow;
onChange: (flow: SetupFlow) => void;
}
function FlowSelector({ value, onChange }: FlowSelectorProps): JSX.Element {
return (
<div className={cx(styles.drawerSection, styles.drawerSurface)}>
<div className={styles.drawerSurfaceHead}>
<Typography.Text weight="bold" size="base">
Connection method
</Typography.Text>
</div>
<RadioGroup
value={value}
onChange={(next): void => onChange(next as SetupFlow)}
className={styles.flowRadioGroup}
>
<RadioGroupItem
value="manual"
containerClassName={cx(styles.flowRadio, styles.flowRadioManual)}
testId="gcp-flow-manual"
>
<div className={styles.flowRadioTitle}>
<Typography.Text weight="semibold" size="base">
Connect Manually
</Typography.Text>
</div>
<Typography.Text
as="p"
size="small"
color="muted"
className={styles.flowRadioDesc}
>
Deploy your own OTel Collector and configure log sinks.
</Typography.Text>
</RadioGroupItem>
<RadioGroupItem
value="agent"
containerClassName={styles.flowRadio}
testId="gcp-flow-agent"
disabled
>
<div className={styles.flowRadioTitle}>
<Typography.Text weight="semibold" size="base">
Connect via Agent
</Typography.Text>
<Badge color="robin" variant="default">
Soon
</Badge>
</div>
<Typography.Text
as="p"
size="small"
color="muted"
className={styles.flowRadioDesc}
>
SigNoz deploys and manages the collector for you.
</Typography.Text>
</RadioGroupItem>
</RadioGroup>
</div>
);
}
export default FlowSelector;

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