Compare commits

..

34 Commits

Author SHA1 Message Date
Vinícius Lourenço
c51ae68a37 fix(dashboards): open view name popover from tooltip-wrapped trigger 2026-09-24 19:32:13 -03:00
Vinícius Lourenço
b71aa13a55 test(storybook): add interaction stories for menus, drawers and selects 2026-09-24 18:52:22 -03:00
Vinícius Lourenço
042e5f064b test: align tests with reworked @signozhq/ui 2026-09-24 18:32:08 -03:00
Vinícius Lourenço
dd6cc7d047 test(alerts): build evaluation mock dates in local time 2026-09-24 18:32:05 -03:00
Vinícius Lourenço
2ae649355f fix(roles): render item selector chips with Pill.Closeable 2026-09-24 18:32:01 -03:00
Vinícius Lourenço
1abe0bcd2e fix(pipelines): keep JsonFlattening switch controlled 2026-09-24 18:31:58 -03:00
Vinícius Lourenço
6203ea68b4 fix(tooltip): set tooltip z-index once on :root 2026-09-24 18:31:55 -03:00
Vinícius Lourenço
c456b0c0ee refactor(authz): build AuthZButton on Button disabled and loading tooltips 2026-09-24 18:31:51 -03:00
Vinícius Lourenço
6634de6c64 fixup! feat(switch): adopt the reworked Switch API 2026-09-24 18:31:47 -03:00
Vinícius Lourenço
300b460bab fixup! feat(checkbox): adopt the reworked Checkbox API 2026-09-24 18:31:43 -03:00
Vinícius Lourenço
693dd225bb fixup! feat(radio-group): adopt the items API 2026-09-24 18:31:39 -03:00
Vinícius Lourenço
ed7872e072 fixup! feat(toggle-group): adopt the items API 2026-09-24 18:31:35 -03:00
Vinícius Lourenço
78f20afbb9 fixup! feat(dropdown): adopt Dropdown in place of DropdownMenu 2026-09-24 18:31:31 -03:00
Vinícius Lourenço
1ce4bd425b fixup! feat(tooltip): adopt the single Tooltip API 2026-09-24 18:31:27 -03:00
Vinícius Lourenço
31be5eb20d fixup! feat(badge): adopt the reworked Badge palette 2026-09-24 18:31:23 -03:00
Vinícius Lourenço
4b7837c4b0 fixup! feat(button): adopt the reworked Button API 2026-09-24 18:31:15 -03:00
Vinícius Lourenço
20f25c0732 fixup! feat(button): adopt the reworked Button API 2026-09-24 16:28:30 -03:00
Vinícius Lourenço
3a407f8945 feat(button-group): adopt the items API 2026-09-24 16:28:26 -03:00
Vinícius Lourenço
c04dfbd593 fixup! feat(switch): adopt the reworked Switch API 2026-09-24 16:28:18 -03:00
Vinícius Lourenço
90b505e95e feat(dashboards): move row actions to AuthZDropdown 2026-09-24 14:18:27 -03:00
Vinícius Lourenço
22c757ff91 fix(button): pass icons through prefix, suffix and icon 2026-09-24 14:12:57 -03:00
Vinícius Lourenço
ffdd6d0343 fixup! feat(button): adopt the reworked Button API 2026-09-24 13:52:48 -03:00
Vinícius Lourenço
9cb760905d fixup! feat(tooltip): adopt the single Tooltip API 2026-09-24 13:36:41 -03:00
Manika Malhotra
911ee2912d feat(calendar): select the calendar through data-slot 2026-09-23 21:57:22 +05:30
Manika Malhotra
d4a4217865 feat(dropdown): adopt Dropdown in place of DropdownMenu 2026-09-23 21:56:48 +05:30
Manika Malhotra
925cbafde6 feat(tabs): adopt the items API 2026-09-23 21:56:30 +05:30
Manika Malhotra
1f57634472 feat(radio-group): adopt the items API 2026-09-23 21:56:09 +05:30
Manika Malhotra
662c91884a feat(toggle-group): adopt the items API 2026-09-23 21:55:50 +05:30
Manika Malhotra
95bbd1b501 feat(tooltip): adopt the single Tooltip API 2026-09-23 21:55:23 +05:30
Manika Malhotra
f0037ae4bd feat(checkbox): adopt the reworked Checkbox API 2026-09-23 21:55:00 +05:30
Manika Malhotra
8c3688131e feat(switch): adopt the reworked Switch API 2026-09-23 21:54:38 +05:30
Manika Malhotra
db286c4f92 feat(badge): adopt the reworked Badge palette 2026-09-23 21:54:16 +05:30
Manika Malhotra
e14a6add4e feat(button-group): import ButtonGroup from its own entry 2026-09-23 21:53:56 +05:30
Manika Malhotra
3e2caa8110 feat(button): adopt the reworked Button API 2026-09-23 21:53:31 +05:30
963 changed files with 8968 additions and 15761 deletions

11
.github/CODEOWNERS vendored
View File

@@ -200,15 +200,6 @@ go.mod @therealpandey
/frontend/src/container/ListAlertRules/ @SigNoz/pulse-frontend
/frontend/src/container/TriggeredAlerts/ @SigNoz/pulse-frontend
/frontend/src/container/AnomalyAlertEvaluationView/ @SigNoz/pulse-frontend
/frontend/src/container/RoutingPolicies/ @SigNoz/pulse-frontend
/frontend/src/components/AlertBreadcrumb/ @SigNoz/pulse-frontend
/frontend/src/container/EditRules/ @SigNoz/pulse-frontend
/frontend/src/components/AlertDetailsFilters/ @SigNoz/pulse-frontend
/frontend/src/components/Alerts/ @SigNoz/pulse-frontend
/frontend/src/hooks/routingPolicies/ @SigNoz/pulse-frontend
/frontend/src/types/api/alerts/ @SigNoz/pulse-frontend
/frontend/src/providers/Alert.tsx @SigNoz/pulse-frontend
/frontend/src/constants/alerts.ts @SigNoz/pulse-frontend
## Notification Channels
/frontend/src/pages/ChannelsEdit/ @SigNoz/pulse-frontend
@@ -216,8 +207,6 @@ go.mod @therealpandey
/frontend/src/container/AllAlertChannels/ @SigNoz/pulse-frontend
/frontend/src/container/CreateAlertChannels/ @SigNoz/pulse-frontend
/frontend/src/container/EditAlertChannels/ @SigNoz/pulse-frontend
/frontend/src/container/FormAlertChannels/ @SigNoz/pulse-frontend
/frontend/src/hooks/notificationChannels/ @SigNoz/pulse-frontend
## OpenAPI Schema - Generated
/frontend/src/api/generated/services/ @therealpandey @vikrantgupta25 @srikanthccv

View File

@@ -38,6 +38,7 @@ jobs:
fail-fast: false
matrix:
suite:
- alerts
- alertmanager
- alertmanagerrotation
- basepath
@@ -63,7 +64,6 @@ jobs:
- querierauthz
- role
- rootuser
- ruler
- savedview
- semconvfamilies
- serviceaccount

File diff suppressed because it is too large Load Diff

View File

@@ -179,7 +179,6 @@ The `handler.New` function ties the HTTP handler to OpenAPI metadata via `OpenAP
- **SuccessStatusCode**: The HTTP status for successful responses (for example, `http.StatusOK`, `http.StatusCreated`, `http.StatusNoContent`).
- **ErrorStatusCodes**: Additional error status codes beyond the standard ones automatically added by `handler.New`.
- **SecuritySchemes**: Auth mechanisms and scopes required by the operation.
- **Stability**: Maturity marker (`handler.StabilityDevelopment`, `handler.StabilityAlpha`, `handler.StabilityBeta`, `handler.StabilityStable`, the OpenTelemetry Collector levels) emitted as the `x-signoz-stability` extension on every operation. Unset is emitted as `alpha`.
The generic handler:

View File

@@ -23,15 +23,6 @@ func (f *formatter) JSONExtractString(column, path string) []byte {
return append(f.TextToJsonColumn(column), ops...)
}
func (f *formatter) JSONExtractMapValue(column, mapField, key string) []byte {
sql := f.TextToJsonColumn(column)
sql = append(sql, "->"...)
sql = schema.Append(f.bunf, sql, mapField)
sql = append(sql, "->>"...)
sql = schema.Append(f.bunf, sql, key)
return sql
}
func (f *formatter) JSONType(column, path string) []byte {
var sql []byte
sql = append(sql, "jsonb_typeof("...)

View File

@@ -55,67 +55,6 @@ func TestJSONExtractString(t *testing.T) {
}
}
func TestJSONExtractMapValue(t *testing.T) {
tests := []struct {
name string
column string
mapField string
key string
expected string
}{
{
name: "PlainKey",
column: "data",
mapField: "labels",
key: "team",
expected: `"data"::jsonb->'labels'->>'team'`,
},
{
name: "DottedKey_OneMapEntry",
column: "data",
mapField: "labels",
key: "k8s.cluster",
expected: `"data"::jsonb->'labels'->>'k8s.cluster'`,
},
{
name: "SingleQuoteInKey_Doubled",
column: "data",
mapField: "labels",
key: "o'brien",
expected: `"data"::jsonb->'labels'->>'o''brien'`,
},
{
name: "BackslashInKey_Literal",
column: "data",
mapField: "labels",
key: `a\b`,
expected: `"data"::jsonb->'labels'->>'a\b'`,
},
{
name: "DoubleQuoteInKey_Literal",
column: "data",
mapField: "labels",
key: `a"b`,
expected: `"data"::jsonb->'labels'->>'a"b'`,
},
{
name: "QualifiedColumn",
column: "rule.data",
mapField: "labels",
key: "severity",
expected: `"rule"."data"::jsonb->'labels'->>'severity'`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New())
got := string(f.JSONExtractMapValue(tt.column, tt.mapField, tt.key))
assert.Equal(t, tt.expected, got)
})
}
}
func TestJSONType(t *testing.T) {
tests := []struct {
name string

View File

@@ -41,6 +41,20 @@ if (!HTMLElement.prototype.releasePointerCapture) {
HTMLElement.prototype.releasePointerCapture = function (): void {};
}
// jsdom has no PointerEvent; Base UI Switch constructs one on click.
if (typeof window.PointerEvent === 'undefined') {
class PointerEventMock extends MouseEvent {
pointerId: number;
pointerType: string;
constructor(type: string, init: PointerEventInit = {}) {
super(type, init);
this.pointerId = init.pointerId ?? 0;
this.pointerType = init.pointerType ?? '';
}
}
(window as any).PointerEvent = PointerEventMock;
}
if (typeof window.IntersectionObserver === 'undefined') {
class IntersectionObserverMock {
observe(): void {}

View File

@@ -12,14 +12,13 @@ import { CmdKPalette } from 'components/cmdKPalette/cmdKPalette';
import NotFound from 'components/NotFound';
import { ShiftHoldOverlayController } from 'components/ShiftOverlay/ShiftHoldOverlayController';
import Spinner from 'components/Spinner';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
import AppLayout from 'container/AppLayout';
import Hex from 'crypto-js/enc-hex';
import HmacSHA256 from 'crypto-js/hmac-sha256';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
import { ChatSupportState, useChatSupport } from 'hooks/useChatSupport';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { StatusCodes } from 'http-status-codes';
@@ -59,6 +58,7 @@ function App(): JSX.Element {
isFetchingActiveLicense,
activeLicenseFetchError,
userFetchError,
featureFlagsFetchError,
isLoggedIn: isLoggedInState,
featureFlags,
org,
@@ -66,8 +66,6 @@ function App(): JSX.Element {
} = useAppContext();
const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
const isAIAssistantEnabled = useIsAIAssistantEnabled();
const isSavedViewEnabled = useSavedViewEnabled();
const chatSupport = useChatSupport();
const { hostname } = window.location;
const [pathname, setPathname] = useState(history.location.pathname);
@@ -255,9 +253,7 @@ function App(): JSX.Element {
}, [isDarkMode]);
useEffect(() => {
// The bottom strip carries Support, so the floating bubble goes entirely.
if (
isSavedViewEnabled ||
pathname === ROUTES.ONBOARDING ||
pathname.startsWith('/public/dashboard/') ||
pathname === '/ai-assistant' ||
@@ -267,32 +263,71 @@ function App(): JSX.Element {
} else {
window.Pylon?.('showChatBubble');
}
}, [pathname, isSavedViewEnabled]);
}, [pathname]);
// Identity for the Pylon widget. Whether this user gets Pylon at all is
// `useChatSupport`'s call — this only fills in who they are.
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
if (chatSupport !== ChatSupportState.Pylon) {
return;
// feature flag shouldn't be loading and featureFlags or fetchError any one of this should be true indicating that req is complete
// licenses should also be present. there is no check for licenses for loading and error as that is mandatory if not present then routing
// to something went wrong which would ideally need a reload.
if (
!isFetchingFeatureFlags &&
(featureFlags || featureFlagsFetchError) &&
activeLicense &&
trialInfo
) {
let isChatSupportEnabled = false;
let isPremiumSupportEnabled = false;
if (featureFlags && featureFlags.length > 0) {
isChatSupportEnabled =
featureFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)
?.active || false;
isPremiumSupportEnabled =
featureFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT)
?.active || false;
}
const showAddCreditCardModal =
!isPremiumSupportEnabled && !trialInfo?.trialConvertedToSubscription;
if (
isLoggedInState &&
isChatSupportEnabled &&
!showAddCreditCardModal &&
(isCloudUser || isEnterpriseSelfHostedUser) &&
window.signozBootData?.settings?.pylon?.enabled
) {
const email = user.email || '';
const secret = window.signozBootData?.settings?.pylon?.identitySecret || '';
let emailHash = '';
if (email && secret) {
emailHash = HmacSHA256(email, Hex.parse(secret)).toString(Hex);
}
window.pylon = {
chat_settings: {
app_id: window.signozBootData?.settings?.pylon?.appId,
email: user.email,
name: user.displayName || user.email,
email_hash: emailHash,
},
};
}
}
const email = user.email || '';
const secret = window.signozBootData?.settings?.pylon?.identitySecret || '';
let emailHash = '';
if (email && secret) {
emailHash = HmacSHA256(email, Hex.parse(secret)).toString(Hex);
}
window.pylon = {
chat_settings: {
app_id: window.signozBootData?.settings?.pylon?.appId,
email: user.email,
name: user.displayName || user.email,
email_hash: emailHash,
},
};
}, [chatSupport, user]);
}, [
isLoggedInState,
user,
pathname,
trialInfo?.trialConvertedToSubscription,
featureFlags,
isFetchingFeatureFlags,
featureFlagsFetchError,
activeLicense,
trialInfo,
isCloudUser,
isEnterpriseSelfHostedUser,
]);
useEffect(() => {
if (!isFetchingUser && isCloudUser && user && user.email) {

View File

@@ -41,8 +41,6 @@ import type {
GetRuleHistoryTopContributorsParams,
GetRuleHistoryTopContributorsPathParameters,
ListRules200,
ListRulesV3200,
ListRulesV3Params,
PatchRuleByID200,
PatchRuleByIDPathParameters,
RenderErrorResponseDTO,
@@ -75,8 +73,7 @@ const withQueryKey = <T extends object, K>(
};
/**
* This endpoint lists all alert rules with their current evaluation state. Deprecated: use ListRulesV3, which supports filtering, sorting and pagination.
* @deprecated
* This endpoint lists all alert rules with their current evaluation state
* @summary List alert rules
*/
export const listRules = (signal?: AbortSignal) => {
@@ -118,7 +115,6 @@ export type ListRulesQueryResult = NonNullable<
export type ListRulesQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary List alert rules
*/
@@ -138,7 +134,6 @@ export function useListRules<
}
/**
* @deprecated
* @summary List alert rules
*/
export const invalidateListRules = async (
@@ -1393,97 +1388,3 @@ export const useTestRule = <
> => {
return useMutation(getTestRuleMutationOptions(options));
};
/**
* Returns a page of alert rules with their current evaluation state, trimmed to the fields the list page renders. Supports a filter DSL (`query`), a repeated `states` filter applied after the state overlay, sort (`updated_at`/`created_at`/`name`/`state`/`severity`), order (`asc`/`desc`), and offset-based pagination (`limit`/`offset`). In the filter DSL, a non-reserved key is matched as a rule label directly (`team = infra`); a key that collides with a reserved keyword matches either interpretation (negative operators exclude both), and `labels.<key>` targets only the label. The response also carries the org's label pairs and the reserved filter keys for building filter suggestions.
* @summary List alert rules (v3)
*/
export const listRulesV3 = (
params?: ListRulesV3Params,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListRulesV3200>({
url: `/api/v3/rules`,
method: 'GET',
params,
signal,
});
};
export const getListRulesV3QueryKey = (params?: ListRulesV3Params) => {
return [`/api/v3/rules`, ...(params ? [params] : [])] as const;
};
export const getListRulesV3QueryOptions = <
TData = Awaited<ReturnType<typeof listRulesV3>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params?: ListRulesV3Params,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listRulesV3>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListRulesV3QueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listRulesV3>>> = ({
signal,
}) => listRulesV3(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listRulesV3>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListRulesV3QueryResult = NonNullable<
Awaited<ReturnType<typeof listRulesV3>>
>;
export type ListRulesV3QueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List alert rules (v3)
*/
export function useListRulesV3<
TData = Awaited<ReturnType<typeof listRulesV3>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params?: ListRulesV3Params,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listRulesV3>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListRulesV3QueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
}
/**
* @summary List alert rules (v3)
*/
export const invalidateListRulesV3 = async (
queryClient: QueryClient,
params?: ListRulesV3Params,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListRulesV3QueryKey(params) },
options,
);
return queryClient;
};

View File

@@ -10188,99 +10188,6 @@ export interface RuletypesGettableTestRuleDTO {
message?: string;
}
export interface RuletypesLabelPairDTO {
/**
* @type string
*/
key: string;
/**
* @type string
*/
value: string;
}
export enum RuletypesListOrderDTO {
asc = 'asc',
desc = 'desc',
}
export enum RuletypesListSortDTO {
updated_at = 'updated_at',
created_at = 'created_at',
name = 'name',
state = 'state',
severity = 'severity',
}
export type RuletypesListableRuleDTOLabels = { [key: string]: string };
export enum RuletypesRuleTypeDTO {
threshold_rule = 'threshold_rule',
promql_rule = 'promql_rule',
anomaly_rule = 'anomaly_rule',
}
export interface RuletypesListableRuleDTO {
/**
* @type string
*/
alert: string;
alertType: RuletypesAlertTypeDTO;
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
/**
* @type string
*/
description?: string;
/**
* @type boolean
*/
disabled?: boolean;
/**
* @type string
*/
id: string;
/**
* @type object
*/
labels?: RuletypesListableRuleDTOLabels;
ruleType: RuletypesRuleTypeDTO;
state: RuletypesAlertStateDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface RuletypesListableRulesDTO {
/**
* @type array
*/
labels: RuletypesLabelPairDTO[];
/**
* @type array
*/
reservedKeywords: string[];
/**
* @type array
*/
rules: RuletypesListableRuleDTO[];
/**
* @type integer
* @format int64
*/
total: number;
}
export interface RuletypesRenotifyDTO {
/**
* @type array,null
@@ -10377,6 +10284,11 @@ export interface RuletypesRuleConditionDTO {
thresholds?: RuletypesRuleThresholdDataDTO;
}
export enum RuletypesRuleTypeDTO {
threshold_rule = 'threshold_rule',
promql_rule = 'promql_rule',
anomaly_rule = 'anomaly_rule',
}
export interface RuletypesPostableRuleDTO {
/**
* @type string
@@ -14277,45 +14189,6 @@ export type GetMetricDashboardsV2200 = {
status: string;
};
export type ListRulesV3Params = {
/**
* @type string
* @description undefined
*/
query?: string;
/**
* @type array
* @description undefined
*/
states?: string[];
/**
* @description undefined
*/
sort?: RuletypesListSortDTO;
/**
* @description undefined
*/
order?: RuletypesListOrderDTO;
/**
* @type integer
* @description undefined
*/
limit?: number;
/**
* @type integer
* @description undefined
*/
offset?: number;
};
export type ListRulesV3200 = {
data: RuletypesListableRulesDTO;
/**
* @type string
*/
status: string;
};
export type GetFlamegraphPathParameters = {
traceID: string;
};

View File

@@ -1,103 +0,0 @@
import { useMutation } from 'react-query';
import { Button, Modal } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { createSubscription } from 'api/generated/services/subscriptions';
import type { CreateSubscription201 } from 'api/generated/services/sigNoz.schemas';
import { useNotifications } from 'hooks/useNotifications';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import { SubscriptionCreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/subscription.permissions';
import { CreditCard, X } from '@signozhq/icons';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
interface AddCreditCardModalProps {
open: boolean;
onClose: () => void;
onAddCreditCard?: () => void;
}
/**
* Shown to trial users who have not added a card, in place of chat support.
* Submitting creates the subscription and opens the returned billing URL.
*/
function AddCreditCardModal({
open,
onClose,
onAddCreditCard,
}: AddCreditCardModalProps): JSX.Element {
const { notifications } = useNotifications();
const handleBillingOnSuccess = (data: CreateSubscription201): void => {
if (data?.data?.redirectURL) {
const newTab = document.createElement('a');
newTab.href = data.data.redirectURL;
newTab.target = '_blank';
newTab.rel = 'noopener noreferrer';
newTab.click();
}
};
const handleBillingOnError = (error: APIError): void => {
notifications.error({
message: error.getErrorCode(),
description: error.getErrorMessage(),
});
};
const { mutate: updateCreditCard, isLoading: isLoadingBilling } = useMutation(
createSubscription,
{ onSuccess: handleBillingOnSuccess, onError: handleBillingOnError },
);
const handleAddCreditCard = (): void => {
onAddCreditCard?.();
updateCreditCard({ url: getBaseUrl() });
};
return (
<Modal
className="add-credit-card-modal"
title={<span className="title">Add Credit Card for Chat Support</span>}
open={open}
closable
onCancel={onClose}
destroyOnClose
footer={[
<Button
key="cancel"
onClick={onClose}
className="cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<AuthZTooltip
key="submit"
checks={[SubscriptionCreatePermission]}
withPortal={false}
>
<Button
type="primary"
icon={<CreditCard size={16} />}
size="middle"
loading={isLoadingBilling}
disabled={isLoadingBilling}
onClick={handleAddCreditCard}
className="add-credit-card-btn"
>
Add Credit Card
</Button>
</AuthZTooltip>,
]}
>
<Typography.Text className="add-credit-card-text">
You&apos;re currently on <span className="highlight-text">Trial plan</span>.
Add a credit card to access SigNoz chat support to your workspace.
</Typography.Text>
</Modal>
);
}
AddCreditCardModal.defaultProps = { onAddCreditCard: undefined };
export default AddCreditCardModal;

View File

@@ -1,8 +1,3 @@
.item {
--button-padding: 0;
--button-font-size: var(--periscope-font-size-base);
}
.itemLast {
color: var(--muted-foreground);
font-size: var(--periscope-font-size-base);

View File

@@ -26,9 +26,9 @@ function BreadcrumbItem({
return (
<Button
size="md"
variant="ghost"
color="secondary"
className={styles.item}
onClick={(e: React.MouseEvent): void => {
if (!('route' in props) || !props.route) {
return;

View File

@@ -34,21 +34,23 @@ function ErrorEmptyState({
</div>
<div className={styles.actions}>
<Button
size="md"
variant="solid"
color="secondary"
prefix={<LifeBuoy size={14} />}
onClick={onContactSupport}
data-testid="error-contact-support-button"
testId="error-contact-support-button"
>
Contact Support
</Button>
{onRefresh && (
<Button
size="md"
variant="outlined"
color="secondary"
prefix={<RefreshCw size={14} />}
onClick={onRefresh}
data-testid="error-refresh-button"
testId="error-refresh-button"
>
Refresh
</Button>

View File

@@ -7,26 +7,11 @@
width: 100%;
}
.labelBadge {
cursor: default;
font-size: 12px;
--badge-display: inline;
max-width: 180px;
text-overflow: ellipsis;
}
.overflowTrigger {
all: unset;
cursor: pointer;
}
.overflowBadge {
cursor: pointer;
font-size: 12px;
}
.labelPopover {
display: flex;
flex-direction: column;

View File

@@ -1,4 +1,3 @@
import { TooltipProvider } from '@signozhq/ui/tooltip';
import { act, render, screen } from '@testing-library/react';
import LabelColumn from './LabelColumn';
@@ -37,7 +36,7 @@ afterEach(() => {
function renderWithProviders(
ui: React.ReactElement,
): ReturnType<typeof render> {
return render(<TooltipProvider>{ui}</TooltipProvider>);
return render(ui);
}
describe('LabelColumn', () => {

View File

@@ -1,11 +1,7 @@
import { Copy } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Badge, type BadgeColorType } from '@signozhq/ui/badge';
import { toast } from '@signozhq/ui/sonner';
import {
TooltipContent,
TooltipRoot,
TooltipTrigger,
} from '@signozhq/ui/tooltip';
import { Tooltip } from '@signozhq/ui/tooltip';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
@@ -16,20 +12,7 @@ import { BADGE_GAP, estimateBadgeWidth, OVERFLOW_BADGE_WIDTH } from './utils';
export interface LabelColumnProps {
labels: string[];
color?:
| 'primary'
| 'secondary'
| 'success'
| 'error'
| 'warning'
| 'robin'
| 'forest'
| 'amber'
| 'sienna'
| 'cherry'
| 'sakura'
| 'aqua'
| 'vanilla';
color?: BadgeColorType;
value?: { [key: string]: string };
}
@@ -104,20 +87,10 @@ function LabelColumn({
<LabelTag key={label} label={label} color={color} value={value?.[label]} />
))}
{remainingLabels.length > 0 && (
<TooltipRoot>
<TooltipTrigger asChild>
<span>
<Badge
color={color}
className={styles.overflowBadge}
variant="outline"
data-testid="label-overflow-badge"
>
+{remainingLabels.length}
</Badge>
</span>
</TooltipTrigger>
<TooltipContent side="bottom" align="end">
<Tooltip
side="bottom"
align="end"
title={
<div className={styles.tooltipContent}>
<span>
{remainingLabels
@@ -140,8 +113,14 @@ function LabelColumn({
<Copy size={12} />
</button>
</div>
</TooltipContent>
</TooltipRoot>
}
>
<span>
<Badge color={color} variant="outlined" testId="label-overflow-badge">
+{remainingLabels.length}
</Badge>
</span>
</Tooltip>
)}
</div>
);

View File

@@ -1,11 +1,3 @@
.labelBadge {
cursor: default;
font-size: 12px;
max-width: 180px;
text-overflow: ellipsis;
}
.labelValue {
text-overflow: ellipsis;
overflow: hidden;

View File

@@ -1,31 +1,14 @@
import { Copy } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Badge, type BadgeColorType } from '@signozhq/ui/badge';
import { toast } from '@signozhq/ui/sonner';
import {
TooltipContent,
TooltipRoot,
TooltipTrigger,
} from '@signozhq/ui/tooltip';
import { Tooltip } from '@signozhq/ui/tooltip';
import { useCopyToClipboard } from 'react-use';
import styles from './LabelTag.module.scss';
export interface LabelTagProps {
label: string;
color?:
| 'primary'
| 'secondary'
| 'success'
| 'error'
| 'warning'
| 'robin'
| 'forest'
| 'amber'
| 'sienna'
| 'cherry'
| 'sakura'
| 'aqua'
| 'vanilla';
color?: BadgeColorType;
value?: string;
}
@@ -41,20 +24,8 @@ function LabelTag({ label, value, color }: LabelTagProps): JSX.Element {
};
return (
<TooltipRoot>
<TooltipTrigger asChild>
<span>
<Badge
color={color}
className={styles.labelBadge}
variant="outline"
data-testid={`label-tag-${label}`}
>
<span className={styles.labelValue}>{displayText}</span>
</Badge>
</span>
</TooltipTrigger>
<TooltipContent>
<Tooltip
title={
<div className={styles.tooltipContent}>
<span>{displayText}</span>
<button
@@ -66,8 +37,19 @@ function LabelTag({ label, value, color }: LabelTagProps): JSX.Element {
<Copy size={12} />
</button>
</div>
</TooltipContent>
</TooltipRoot>
}
>
<span>
<Badge
color={color ?? 'secondary'}
maxWidth={180}
variant="outlined"
testId={`label-tag-${label}`}
>
<span className={styles.labelValue}>{displayText}</span>
</Badge>
</span>
</Tooltip>
);
}

View File

@@ -30,21 +30,23 @@ function NoResultsEmptyState({
<div className={styles.actions}>
{onClear && (
<Button
size="md"
variant="outlined"
color="secondary"
onClick={onClear}
data-testid="no-results-clear-button"
testId="no-results-clear-button"
>
{clearButtonText}
</Button>
)}
{onRefresh && (
<Button
size="md"
variant="outlined"
color="secondary"
prefix={<RefreshCw size={14} />}
onClick={onRefresh}
data-testid="no-results-refresh-button"
testId="no-results-refresh-button"
>
Refresh
</Button>

View File

@@ -1,4 +1,4 @@
import type { BadgeColor } from '@signozhq/ui/badge';
import type { BadgeColorType } from '@signozhq/ui/badge';
export const STATE_ORDER = ['firing', 'pending', 'inactive', 'disabled'];
export const SEVERITY_ORDER = ['critical', 'error', 'warning', 'info'];
@@ -24,9 +24,9 @@ export const SEVERITY_COLORS: Record<string, string> = {
info: 'var(--bg-robin-500)',
};
export const SEVERITY_BADGE_COLORS: Record<string, BadgeColor> = {
critical: 'error',
error: 'error',
export const SEVERITY_BADGE_COLORS: Record<string, BadgeColorType> = {
critical: 'danger',
error: 'danger',
warning: 'warning',
info: 'primary',
};

View File

@@ -33,36 +33,3 @@
color: var(--l1-foreground);
white-space: nowrap;
}
.auth-header-help-button {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 32px;
padding: 10px 16px;
background: var(--l2-background);
color: var(--l2-foreground);
border: none;
border-radius: 2px;
cursor: pointer;
transition: opacity 0.2s ease;
span {
font-family: var(--font-family-inter, Inter, sans-serif);
font-size: 11px;
font-weight: 500;
line-height: 1;
color: var(--l2-foreground);
text-align: center;
}
svg {
flex-shrink: 0;
color: var(--l2-foreground);
}
&:hover {
opacity: 0.8;
}
}

View File

@@ -22,11 +22,11 @@ function AuthHeader(): JSX.Element {
<span className="auth-header-logo-text">SigNoz</span>
</div>
<Button
className="auth-header-help-button"
size="md"
prefix={<LifeBuoy size={12} />}
onClick={handleGetHelp}
variant="solid"
color="none"
color="secondary"
>
Get Help
</Button>

View File

@@ -48,14 +48,21 @@ function Badges({ tags, setTags }: AddTagsProps): JSX.Element {
<div className="tags-container">
{tags.map<React.ReactNode>((tag) => (
<Badge
variant="solid"
key={tag}
color="vanilla"
style={{ userSelect: 'none' }}
closable
onClose={(e): void => {
e.preventDefault();
handleClose(tag);
}}
color="secondary"
suffix={
<button
type="button"
aria-label={`Remove ${tag}`}
onClick={(e): void => {
e.preventDefault();
handleClose(tag);
}}
>
<X size={12} />
</button>
}
>
{tag}
</Badge>

View File

@@ -1,15 +1,63 @@
import { useState } from 'react';
import { useMutation } from 'react-query';
import { useLocation } from 'react-router-dom';
import { Button } from 'antd';
import AddCreditCardModal from 'components/AddCreditCardModal/AddCreditCardModal';
import { Button, Modal } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { MessageSquareText } from '@signozhq/icons';
import { createSubscription } from 'api/generated/services/subscriptions';
import type { CreateSubscription201 } from 'api/generated/services/sigNoz.schemas';
import { useNotifications } from 'hooks/useNotifications';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import { SubscriptionCreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/subscription.permissions';
import { CreditCard, MessageSquareText, X } from '@signozhq/icons';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
export default function ChatSupportGateway(): JSX.Element {
const { notifications } = useNotifications();
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] =
useState(false);
const handleBillingOnSuccess = (data: CreateSubscription201): void => {
if (data?.data?.redirectURL) {
const newTab = document.createElement('a');
newTab.href = data.data.redirectURL;
newTab.target = '_blank';
newTab.rel = 'noopener noreferrer';
newTab.click();
}
};
const handleBillingOnError = (error: APIError): void => {
notifications.error({
message: error.getErrorCode(),
description: error.getErrorMessage(),
});
};
const { mutate: updateCreditCard, isLoading: isLoadingBilling } = useMutation(
createSubscription,
{
onSuccess: (data) => {
handleBillingOnSuccess(data);
},
onError: handleBillingOnError,
},
);
const { pathname } = useLocation();
const handleAddCreditCard = (): void => {
logEvent('Add Credit card modal: Clicked', {
source: `chat support icon`,
page: pathname,
});
updateCreditCard({
url: getBaseUrl(),
});
};
return (
<>
<div className="chat-support-gateway">
@@ -28,16 +76,47 @@ export default function ChatSupportGateway(): JSX.Element {
</Button>
</div>
<AddCreditCardModal
{/* Add Credit Card Modal */}
<Modal
className="add-credit-card-modal"
title={<span className="title">Add Credit Card for Chat Support</span>}
open={isAddCreditCardModalOpen}
onClose={(): void => setIsAddCreditCardModalOpen(false)}
onAddCreditCard={(): void => {
logEvent('Add Credit card modal: Clicked', {
source: `chat support icon`,
page: pathname,
});
}}
/>
closable
onCancel={(): void => setIsAddCreditCardModalOpen(false)}
destroyOnClose
footer={[
<Button
key="cancel"
onClick={(): void => setIsAddCreditCardModalOpen(false)}
className="cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<AuthZTooltip
key="submit"
checks={[SubscriptionCreatePermission]}
withPortal={false}
>
<Button
type="primary"
icon={<CreditCard size={16} />}
size="middle"
loading={isLoadingBilling}
disabled={isLoadingBilling}
onClick={handleAddCreditCard}
className="add-credit-card-btn"
>
Add Credit Card
</Button>
</AuthZTooltip>,
]}
>
<Typography.Text className="add-credit-card-text">
You&apos;re currently on <span className="highlight-text">Trial plan</span>
. Add a credit card to access SigNoz chat support to your workspace.
</Typography.Text>
</Modal>
</>
);
}

View File

@@ -39,9 +39,3 @@
}
}
}
.cloud-service-data-collected-table-tooltip {
max-width: 280px;
white-space: normal;
word-break: break-word;
}

View File

@@ -4,7 +4,7 @@ import {
CloudintegrationtypesCollectedMetricDTO,
} from 'api/generated/services/sigNoz.schemas';
import { BarChart, Info, ScrollText } from '@signozhq/icons';
import { TooltipProvider, TooltipSimple } from '@signozhq/ui/tooltip';
import { Tooltip } from '@signozhq/ui/tooltip';
import './CloudServiceDataCollected.styles.scss';
@@ -88,23 +88,15 @@ function CloudServiceDataCollected({
<BarChart size={14} />
Metrics
{metricsInfoTooltip && (
<TooltipProvider>
<TooltipSimple
title={metricsInfoTooltip}
side="top"
tooltipContentProps={{
className: 'cloud-service-data-collected-table-tooltip',
}}
<Tooltip title={metricsInfoTooltip} side="top">
<span
className="cloud-service-data-collected-table-heading-info"
aria-label="About the metrics listed below"
data-testid="data-collected-metrics-info"
>
<span
className="cloud-service-data-collected-table-heading-info"
aria-label="About the metrics listed below"
data-testid="data-collected-metrics-info"
>
<Info size={12} />
</span>
</TooltipSimple>
</TooltipProvider>
<Info size={12} />
</span>
</Tooltip>
)}
</div>
<Table

View File

@@ -2,6 +2,13 @@
position: relative;
}
.copyButton {
position: absolute;
right: 8px;
top: 8px;
z-index: 1;
}
.codeBlockSyntaxHighlighter {
background-color: var(--l2-background) !important;
border-radius: 4px !important;

View File

@@ -2,6 +2,7 @@ import { useMemo, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Check, Copy } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Tooltip } from '@signozhq/ui/tooltip';
import SyntaxHighlighter, {
a11yDark,
} from 'components/MarkdownRenderer/syntaxHighlighter';
@@ -52,16 +53,20 @@ function CodeBlock({
data-testid="code-block-container"
>
{showCopyButton ? (
<Button
variant="ghost"
color="secondary"
size="sm"
onClick={handleCopy}
prefix={isCopied ? <Check size={14} /> : <Copy size={14} />}
aria-label="Copy code"
title={isCopied ? 'Copied' : 'Copy'}
style={{ position: 'absolute', right: 8, top: 8, zIndex: 1 }}
/>
<div className={styles.copyButton}>
<Tooltip title={isCopied ? 'Copied' : 'Copy'}>
<Button
variant="ghost"
color="secondary"
size="sm"
icon
onClick={handleCopy}
aria-label="Copy code"
>
{isCopied ? <Check size={14} /> : <Copy size={14} />}
</Button>
</Tooltip>
</div>
) : null}
<SyntaxHighlighter
style={a11yDark}

View File

@@ -134,26 +134,32 @@ function CreateServiceAccountModal(): JSX.Element {
<DialogFooter className="create-sa-modal__footer">
<Button
size="md"
type="button"
variant="solid"
color="secondary"
onClick={handleClose}
data-testid="create-sa-cancel-btn"
testId="create-sa-cancel-btn"
prefix={<X size={12} />}
>
<X size={12} />
Cancel
</Button>
<AuthZButton
size="md"
checks={[SACreatePermission]}
withPortal={false}
type="submit"
form="create-sa-form"
type="button"
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
data-testid="create-sa-submit-btn"
testId="create-sa-submit-btn"
onClick={(): void => {
const form = document.getElementById('create-sa-form');
if (form instanceof HTMLFormElement) {
form.requestSubmit();
}
}}
>
Create Service Account
</AuthZButton>

View File

@@ -60,7 +60,10 @@ describe('CreateServiceAccountModal', () => {
await screen.findByTestId('create-sa-name-input');
await waitFor(() =>
expect(screen.getByTestId('create-sa-submit-btn')).toBeDisabled(),
expect(screen.getByTestId('create-sa-submit-btn')).toHaveAttribute(
'aria-disabled',
'true',
),
);
});
@@ -72,10 +75,14 @@ describe('CreateServiceAccountModal', () => {
const submitBtn = await screen.findByTestId('create-sa-submit-btn');
await user.type(nameInput, 'test');
await waitFor(() => expect(submitBtn).not.toBeDisabled());
await waitFor(() =>
expect(submitBtn).not.toHaveAttribute('aria-disabled', 'true'),
);
await user.clear(nameInput);
await waitFor(() => expect(submitBtn).toBeDisabled());
await waitFor(() =>
expect(submitBtn).toHaveAttribute('aria-disabled', 'true'),
);
});
it('successful submit shows toast.success and closes modal', async () => {
@@ -86,7 +93,9 @@ describe('CreateServiceAccountModal', () => {
await user.type(nameInput, 'Deploy Bot');
const submitBtn = screen.getByTestId('create-sa-submit-btn');
await waitFor(() => expect(submitBtn).not.toBeDisabled());
await waitFor(() =>
expect(submitBtn).not.toHaveAttribute('aria-disabled', 'true'),
);
await user.click(submitBtn);
await waitFor(() => {
@@ -120,7 +129,9 @@ describe('CreateServiceAccountModal', () => {
await user.type(nameInput, 'Dupe Bot');
const submitBtn = screen.getByTestId('create-sa-submit-btn');
await waitFor(() => expect(submitBtn).not.toBeDisabled());
await waitFor(() =>
expect(submitBtn).not.toHaveAttribute('aria-disabled', 'true'),
);
await user.click(submitBtn);
await waitFor(() => {
@@ -180,7 +191,10 @@ describe('CreateServiceAccountModal', () => {
).resolves.toBeInTheDocument();
// The footer lives outside the guard: submit is gated, Cancel still works.
expect(screen.getByTestId('create-sa-submit-btn')).toBeDisabled();
expect(screen.getByTestId('create-sa-submit-btn')).toHaveAttribute(
'aria-disabled',
'true',
);
await user.click(screen.getByTestId('create-sa-cancel-btn'));

View File

@@ -4,30 +4,6 @@
align-items: center;
gap: 4px;
.zoom-out-btn {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--secondary-foreground);
background-color: var(--secondary-background);
border: 1px solid var(--secondary-border);
border-radius: 2px;
box-shadow: none;
padding: 10px;
height: 33px;
&:hover:not(:disabled) {
color: var(--primary-foreground);
background: var(--primary-background);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.timeSelection-input {
&:hover {
border-color: var(--l1-border) !important;

View File

@@ -650,20 +650,20 @@ function CustomTimePicker({
</Popover>
</Tooltip>
{!showLiveLogs && !isModalTimeSelection && (
<Tooltip
title={
zoomOutDisabled ? 'Zoom out time range is limited to 1 month' : 'Zoom out'
}
>
<Tooltip title={zoomOutDisabled ? undefined : 'Zoom out'}>
<Button
className="zoom-out-btn"
disabledTooltip="Zoom out time range is limited to 1 month"
size="md"
onClick={handleZoomOut}
disabled={zoomOutDisabled}
data-testid="zoom-out-btn"
prefix={<ZoomOut size={14} />}
testId="zoom-out-btn"
icon
aria-label="Zoom out"
variant="solid"
color="none"
/>
color="secondary"
>
<ZoomOut size={14} />
</Button>
</Tooltip>
)}
</div>

View File

@@ -164,6 +164,6 @@ describe('CustomTimePicker - zoom out button', () => {
);
const zoomOutBtn = screen.getByTestId('zoom-out-btn');
expect(zoomOutBtn).toBeDisabled();
expect(zoomOutBtn).toHaveAttribute('aria-disabled', 'true');
});
});

View File

@@ -27,12 +27,14 @@ function DetailsHeader({
const closeButton = (
<Button
variant="ghost"
size="icon"
size="sm"
icon
color="secondary"
onClick={onClose}
aria-label="Close"
prefix={<X size={14} />}
></Button>
>
<X size={14} />
</Button>
);
return (

View File

@@ -1,6 +1,6 @@
import { useCallback, useMemo, useState } from 'react';
import { Button, Popover, Tooltip } from 'antd';
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
import { RadioGroup } from '@signozhq/ui/radio-group';
import { Typography } from '@signozhq/ui/typography';
import { TelemetryFieldKey } from 'api/v5/v5';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -68,10 +68,15 @@ export default function DownloadOptionsMenu({
>
<div className="export-format">
<Typography.Text className="title">FORMAT</Typography.Text>
<RadioGroup value={exportFormat} onChange={setExportFormat}>
<RadioGroupItem value={DownloadFormats.CSV}>csv</RadioGroupItem>
<RadioGroupItem value={DownloadFormats.JSONL}>jsonl</RadioGroupItem>
</RadioGroup>
<RadioGroup
color="primary"
value={exportFormat}
onChange={setExportFormat}
items={[
{ value: DownloadFormats.CSV, label: 'csv' },
{ value: DownloadFormats.JSONL, label: 'jsonl' },
]}
/>
</div>
<div className="horizontal-line" />
@@ -79,19 +84,15 @@ export default function DownloadOptionsMenu({
<div className="row-limit">
<Typography.Text className="title">Number of Rows</Typography.Text>
<RadioGroup
color="primary"
value={String(rowLimit)}
onChange={(value): void => setRowLimit(Number(value))}
>
<RadioGroupItem value={String(DownloadRowCounts.TEN_K)}>
10k
</RadioGroupItem>
<RadioGroupItem value={String(DownloadRowCounts.THIRTY_K)}>
30k
</RadioGroupItem>
<RadioGroupItem value={String(DownloadRowCounts.FIFTY_K)}>
50k
</RadioGroupItem>
</RadioGroup>
items={[
{ value: String(DownloadRowCounts.TEN_K), label: '10k' },
{ value: String(DownloadRowCounts.THIRTY_K), label: '30k' },
{ value: String(DownloadRowCounts.FIFTY_K), label: '50k' },
]}
/>
</div>
{dataSource !== DataSource.TRACES && (
@@ -100,12 +101,15 @@ export default function DownloadOptionsMenu({
<div className="columns-scope">
<Typography.Text className="title">Columns</Typography.Text>
<RadioGroup value={columnsScope} onChange={setColumnsScope}>
<RadioGroupItem value={DownloadColumnsScopes.ALL}>All</RadioGroupItem>
<RadioGroupItem value={DownloadColumnsScopes.SELECTED}>
Selected
</RadioGroupItem>
</RadioGroup>
<RadioGroup
color="primary"
value={columnsScope}
onChange={setColumnsScope}
items={[
{ value: DownloadColumnsScopes.ALL, label: 'All' },
{ value: DownloadColumnsScopes.SELECTED, label: 'Selected' },
]}
/>
</div>
</>
)}

View File

@@ -0,0 +1,227 @@
import { isValidElement, type ReactElement, type ReactNode } from 'react';
import {
Dropdown,
type DropdownItemType,
type DropdownProps,
} from '@signozhq/ui/dropdown';
/**
* The menu-item shape SigNoz built against `@signozhq/ui/dropdown-menu`.
* `Dropdown` only accepts its own `items` array, so this module maps the old
* rows onto that array and renders them.
*/
export type BaseMenuItem = {
key?: string;
label?: ReactNode;
disabled?: boolean;
disabledTooltip?: ReactNode;
icon?: ReactNode;
rightIcon?: ReactNode;
shortcut?: ReactNode;
onClick?: (info: { key: string; keyPath: string[] }) => void;
danger?: boolean;
className?: string;
};
export type MenuGroup = BaseMenuItem & {
type: 'group';
label: string;
children: MenuItem[];
};
export type MenuDivider = {
type: 'divider';
key?: string;
};
export type SubMenuItem = BaseMenuItem & {
children: MenuItem[];
};
export type CheckboxMenuItem = BaseMenuItem & {
type: 'checkbox';
key: string;
label: ReactNode;
checked?: boolean;
onCheckedChange?: (checked: boolean) => void;
};
export type RadioMenuItem = {
type: 'radio';
key: string;
label: ReactNode;
value: string;
disabled?: boolean;
className?: string;
};
export type RadioGroupMenuItem = {
type: 'radio-group';
key?: string;
value?: string;
onChange?: (value: string) => void;
children: RadioMenuItem[];
};
export type MenuItem =
| MenuGroup
| MenuDivider
| CheckboxMenuItem
| RadioGroupMenuItem
| (SubMenuItem & { type?: never })
| (BaseMenuItem & { type?: never; children?: never });
export type MenuProps = {
items: MenuItem[];
search?: {
placeholder?: string;
onSearchChange?: (value: string) => void;
};
loading?: boolean | { text?: string };
};
type Align = DropdownProps['align'];
type Side = DropdownProps['side'];
function elementOf(node: ReactNode): ReactElement | undefined {
return isValidElement(node) ? node : undefined;
}
function disabledFields(item: {
disabled?: boolean;
disabledTooltip?: ReactNode;
}): { disabled: boolean; disabledTooltip: ReactNode } | Record<string, never> {
if (item.disabled === undefined && item.disabledTooltip === undefined) {
return {};
}
return {
disabled: Boolean(item.disabled),
disabledTooltip: item.disabledTooltip,
};
}
function mapItem(item: MenuItem, index: number): DropdownItemType {
if ('type' in item && item.type === 'divider') {
return { type: 'separator', value: item.key ?? `separator-${index}` };
}
if ('type' in item && item.type === 'group') {
return {
type: 'group',
value: item.key ?? `group-${index}`,
label: item.label,
items: item.children.map((child, childIndex) => mapItem(child, childIndex)),
} as DropdownItemType;
}
if ('type' in item && item.type === 'checkbox') {
return {
type: 'checkbox',
name: item.key,
label: item.label,
value: item.checked,
onChange: item.onCheckedChange,
prefix: elementOf(item.icon),
...disabledFields(item),
};
}
if ('type' in item && item.type === 'radio-group') {
return {
type: 'radio-group',
name: item.key ?? `radio-${index}`,
value: item.value,
onChange: item.onChange,
items: item.children.map((child) => ({
value: child.value,
label: child.label,
...disabledFields(child),
})),
};
}
if ('children' in item && item.children) {
const key = item.key ?? `submenu-${index}`;
return {
type: 'submenu',
value: key,
label: item.label ?? '',
prefix: elementOf(item.icon),
items: item.children.map((child, childIndex) => mapItem(child, childIndex)),
...disabledFields(item),
} as DropdownItemType;
}
const key = item.key ?? `item-${index}`;
const shortcut = 'shortcut' in item ? item.shortcut : undefined;
const suffix = elementOf('rightIcon' in item ? item.rightIcon : undefined);
return {
type: 'item',
value: key,
label: item.label ?? '',
danger: 'danger' in item ? item.danger : undefined,
prefix: elementOf('icon' in item ? item.icon : undefined),
...(shortcut != null ? { shortcut } : { suffix }),
onClick:
'onClick' in item && item.onClick
? (): void => {
item.onClick?.({ key, keyPath: [key] });
}
: undefined,
...disabledFields(item),
};
}
interface DropdownMenuSimpleProps {
menu: MenuProps;
children: ReactNode;
contentMaxWidth?: number | string;
align?: Align;
side?: Side;
testId?: string;
nativeButton?: boolean;
disabled?: boolean;
disabledTooltip?: ReactNode;
}
export function DropdownMenuSimple({
menu,
children,
contentMaxWidth,
align = 'end',
side = 'bottom',
testId,
nativeButton = true,
disabled,
disabledTooltip,
}: DropdownMenuSimpleProps): JSX.Element {
const loading = menu.loading;
const loadingText = typeof loading === 'object' ? loading.text : undefined;
return (
<Dropdown
items={menu.items.map(mapItem) as DropdownItemType[]}
nativeButton={nativeButton}
align={align}
side={side}
contentMaxWidth={contentMaxWidth}
testId={testId}
loading={Boolean(loading)}
disabled={disabled}
disabledTooltip={disabledTooltip}
loadingContent={loadingText}
searchInputProps={
menu.search
? {
placeholder: menu.search.placeholder,
onChange: menu.search.onSearchChange,
}
: undefined
}
>
{children}
</Dropdown>
);
}
export default DropdownMenuSimple;

View File

@@ -38,18 +38,23 @@ function DeleteMemberDialog({
const footer = (
<>
<Button variant="solid" color="secondary" onClick={onClose}>
<X size={12} />
<Button
size="md"
variant="solid"
color="secondary"
onClick={onClose}
prefix={<X size={12} />}
>
Cancel
</Button>
<Button
size="md"
variant="solid"
color="destructive"
disabled={isDeleting}
color="danger"
onClick={onConfirm}
loading={isDeleting}
prefix={<Trash2 size={12} />}
>
<Trash2 size={12} />
{isDeleting ? 'Processing...' : title}
</Button>
</>

View File

@@ -128,10 +128,6 @@
flex-shrink: 0;
}
&__tooltip-wrapper {
display: inline-flex;
}
&__footer-btn {
display: inline-flex;
align-items: center;
@@ -220,8 +216,4 @@
line-height: var(--line-height-18);
letter-spacing: -0.07px;
}
&__copy-btn {
border-left: 1px solid var(--l1-border);
}
}

View File

@@ -519,7 +519,7 @@ function EditMemberDrawer({
localRoles.map((roleId) => {
const role = availableRoles.find((r) => r.id === roleId);
return (
<Badge key={roleId} color="vanilla">
<Badge variant="solid" key={roleId} color="secondary">
{role?.name ?? roleId}
</Badge>
);
@@ -559,15 +559,15 @@ function EditMemberDrawer({
<div className="edit-member-drawer__meta-item">
<span className="edit-member-drawer__meta-label">Status</span>
{member?.status === MemberStatus.Active ? (
<Badge color="forest" variant="outline">
<Badge color="success" variant="outlined">
ACTIVE
</Badge>
) : member?.status === MemberStatus.Deleted ? (
<Badge color="cherry" variant="outline">
<Badge color="danger" variant="outlined">
DELETED
</Badge>
) : (
<Badge color="amber" variant="outline">
<Badge color="warning" variant="outlined">
INVITED
</Badge>
)}
@@ -575,12 +575,16 @@ function EditMemberDrawer({
<div className="edit-member-drawer__meta-item">
<span className="edit-member-drawer__meta-label">{joinedOnLabel}</span>
<Badge color="vanilla">{formatTimestamp(member?.joinedOn)}</Badge>
<Badge variant="solid" color="secondary">
{formatTimestamp(member?.joinedOn)}
</Badge>
</div>
{!isInvited && (
<div className="edit-member-drawer__meta-item">
<span className="edit-member-drawer__meta-label">Last Modified</span>
<Badge color="vanilla">{formatTimestamp(member?.updatedAt)}</Badge>
<Badge variant="solid" color="secondary">
{formatTimestamp(member?.updatedAt)}
</Badge>
</div>
)}
</div>
@@ -611,55 +615,59 @@ function EditMemberDrawer({
{!isDeleted && (
<>
<div className="edit-member-drawer__footer-left">
<Tooltip title={getDeleteTooltip(isRootUser, isSelf)}>
<span className="edit-member-drawer__tooltip-wrapper">
<Button
onClick={(): void => setShowDeleteConfirm(true)}
disabled={isRootUser || isSelf}
variant="link"
color="destructive"
>
<Trash2 size={12} />
{isInvited ? 'Revoke Invite' : 'Delete Member'}
</Button>
</span>
</Tooltip>
<Button
disabledTooltip={getDeleteTooltip(isRootUser, isSelf)}
size="md"
onClick={(): void => setShowDeleteConfirm(true)}
disabled={isRootUser || isSelf}
variant="link"
color="danger"
prefix={<Trash2 size={12} />}
>
{isInvited ? 'Revoke Invite' : 'Delete Member'}
</Button>
<div className="edit-member-drawer__footer-divider" />
<Tooltip title={isRootUser ? ROOT_USER_TOOLTIP : undefined}>
<span className="edit-member-drawer__tooltip-wrapper">
<Button
onClick={handleGenerateResetLink}
disabled={isGeneratingLink || isRootUser || isLoadingTokenStatus}
variant="link"
color="warning"
>
<RefreshCw size={12} />
{isGeneratingLink
? 'Generating...'
: isInvited
? getInviteButtonLabel(
isLoadingTokenStatus,
existingToken,
isTokenExpired,
tokenNotFound,
)
: 'Generate Password Reset Link'}
</Button>
</span>
</Tooltip>
<Button
disabledTooltip={ROOT_USER_TOOLTIP}
size="md"
onClick={handleGenerateResetLink}
disabled={isRootUser}
loading={isGeneratingLink || isLoadingTokenStatus}
variant="link"
color="warning"
prefix={<RefreshCw size={12} />}
>
{isGeneratingLink
? 'Generating...'
: isInvited
? getInviteButtonLabel(
isLoadingTokenStatus,
existingToken,
isTokenExpired,
tokenNotFound,
)
: 'Generate Password Reset Link'}
</Button>
</div>
<div className="edit-member-drawer__footer-right">
<Button variant="outlined" color="secondary" onClick={handleClose}>
<X size={14} />
<Button
size="md"
variant="outlined"
color="secondary"
onClick={handleClose}
prefix={<X size={14} />}
>
Cancel
</Button>
<Button
disabledTooltip={isRootUser ? ROOT_USER_TOOLTIP : 'No changes to save'}
size="md"
variant="solid"
color="primary"
disabled={!isDirty || isSaving || isRootUser}
disabled={!isDirty || isRootUser}
onClick={handleSave}
loading={isSaving}
>

View File

@@ -45,11 +45,11 @@ function ResetLinkDialog({
<span className="reset-link-dialog__link-text">{resetLink}</span>
</div>
<Button
size="md"
variant="link"
color="secondary"
onClick={onCopy}
prefix={hasCopied ? <Check size={12} /> : <Copy size={12} />}
className="reset-link-dialog__copy-btn"
>
{hasCopied ? 'Copied!' : 'Copy'}
</Button>

View File

@@ -251,7 +251,7 @@ describe('EditMemberDrawer', () => {
expect(screen.getByText('ACTIVE')).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /save member details/i }),
).toBeDisabled();
).toHaveAttribute('aria-disabled', 'true');
});
it('enables Save after editing name and calls updateUser on confirm', async () => {
@@ -271,7 +271,9 @@ describe('EditMemberDrawer', () => {
await user.type(nameInput, 'Alice Updated');
const saveBtn = screen.getByRole('button', { name: /save member details/i });
await waitFor(() => expect(saveBtn).not.toBeDisabled());
await waitFor(() =>
expect(saveBtn).not.toHaveAttribute('aria-disabled', 'true'),
);
await user.click(saveBtn);
@@ -295,7 +297,9 @@ describe('EditMemberDrawer', () => {
await user.type(nameInput, 'Alice Updated');
const saveBtn = screen.getByRole('button', { name: /save member details/i });
await waitFor(() => expect(saveBtn).not.toBeDisabled());
await waitFor(() =>
expect(saveBtn).not.toHaveAttribute('aria-disabled', 'true'),
);
await user.click(saveBtn);
await waitFor(() => {
@@ -323,7 +327,9 @@ describe('EditMemberDrawer', () => {
await user.click(await screen.findByTitle('signoz-editor'));
const saveBtn = screen.getByRole('button', { name: /save member details/i });
await waitFor(() => expect(saveBtn).not.toBeDisabled());
await waitFor(() =>
expect(saveBtn).not.toHaveAttribute('aria-disabled', 'true'),
);
await user.click(saveBtn);
await waitFor(() => {
@@ -349,7 +355,9 @@ describe('EditMemberDrawer', () => {
await user.click(removeBtn);
const saveBtn = screen.getByRole('button', { name: /save member details/i });
await waitFor(() => expect(saveBtn).not.toBeDisabled());
await waitFor(() =>
expect(saveBtn).not.toHaveAttribute('aria-disabled', 'true'),
);
await user.click(saveBtn);
await waitFor(() => {
@@ -505,7 +513,9 @@ describe('EditMemberDrawer', () => {
await user.type(nameInput, 'Bob Updated');
const saveBtn = screen.getByRole('button', { name: /save member details/i });
await waitFor(() => expect(saveBtn).not.toBeDisabled());
await waitFor(() =>
expect(saveBtn).not.toHaveAttribute('aria-disabled', 'true'),
);
await user.click(saveBtn);
await waitFor(() => {
@@ -541,7 +551,9 @@ describe('EditMemberDrawer', () => {
await user.type(nameInput, 'Alice Updated');
const saveBtn = screen.getByRole('button', { name: /save member details/i });
await waitFor(() => expect(saveBtn).not.toBeDisabled());
await waitFor(() =>
expect(saveBtn).not.toHaveAttribute('aria-disabled', 'true'),
);
await user.click(saveBtn);
await waitFor(() => {
@@ -619,7 +631,7 @@ describe('EditMemberDrawer', () => {
renderDrawer({ member: selfMember });
expect(
screen.getByRole('button', { name: /delete member/i }),
).toBeDisabled();
).toHaveAttribute('aria-disabled', 'true');
});
it('does not open delete confirm dialog when Delete is clicked while disabled (isSelf)', async () => {
@@ -642,7 +654,7 @@ describe('EditMemberDrawer', () => {
renderDrawer({ member: selfMember });
expect(
screen.getByRole('button', { name: /generate password reset link/i }),
).not.toBeDisabled();
).not.toHaveAttribute('aria-disabled', 'true');
});
});
@@ -664,21 +676,21 @@ describe('EditMemberDrawer', () => {
renderDrawer();
expect(
screen.getByRole('button', { name: /delete member/i }),
).toBeDisabled();
).toHaveAttribute('aria-disabled', 'true');
});
it('disables Reset Link button for root user', () => {
renderDrawer();
expect(
screen.getByRole('button', { name: /generate password reset link/i }),
).toBeDisabled();
).toHaveAttribute('aria-disabled', 'true');
});
it('disables Save button for root user', () => {
renderDrawer();
expect(
screen.getByRole('button', { name: /save member details/i }),
).toBeDisabled();
).toHaveAttribute('aria-disabled', 'true');
});
it('does not open delete confirm dialog when Delete is clicked while disabled (root)', async () => {

View File

@@ -53,7 +53,7 @@ function ErrorModal({
onClick={(): void => setVisible(true)}
onKeyDown={undefined}
>
<Badge color="error">
<Badge variant="solid" color="danger">
<CircleAlert size={14} color={Color.BG_CHERRY_500} /> error
</Badge>
</span>

View File

@@ -1,7 +1,10 @@
import { useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Button, Col, Popover, Row, Select, Space } from 'antd';
import { DropdownMenuSimple, type MenuProps } from '@signozhq/ui/dropdown-menu';
import {
DropdownMenuSimple,
type MenuProps,
} from 'components/DropdownMenu/DropdownMenuSimple';
import { Typography } from '@signozhq/ui/typography';
import axios from 'axios';
import TextToolTip from 'components/TextToolTip';

View File

@@ -26,8 +26,4 @@
font-size: var(--periscope-font-size-base);
}
}
.export-button {
width: 100%;
}
}

View File

@@ -1,8 +1,8 @@
import { Download } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui/popover';
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { RadioGroup } from '@signozhq/ui/radio-group';
import { Tooltip } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import {
ClientExportData,
@@ -51,36 +51,41 @@ export default function ExportMenu({
return (
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<TooltipSimple title="Download">
<Tooltip title="Download">
<PopoverTrigger asChild>
<Button
variant="ghost"
color="secondary"
size="icon"
size="sm"
icon
aria-label="Download"
data-testid={`export-menu-${dataSource}`}
disabled={isExporting}
testId={`export-menu-${dataSource}`}
loading={isExporting}
>
<Download size={14} />
</Button>
</PopoverTrigger>
</TooltipSimple>
</Tooltip>
<PopoverContent align="end" className="export-menu-popover">
<div className="export-format">
<Typography.Text className="title">FORMAT</Typography.Text>
<RadioGroup value={exportFormat} onChange={setExportFormat}>
<RadioGroupItem value={ExportFormat.Csv}>csv</RadioGroupItem>
<RadioGroupItem value={ExportFormat.Jsonl}>jsonl</RadioGroupItem>
</RadioGroup>
<RadioGroup
color="primary"
value={exportFormat}
onChange={setExportFormat}
items={[
{ value: ExportFormat.Csv, label: 'csv' },
{ value: ExportFormat.Jsonl, label: 'jsonl' },
]}
/>
</div>
<Button
size="md"
variant="solid"
color="primary"
className="export-button"
width="100%"
onClick={handleExport}
disabled={isExporting}
loading={isExporting}
prefix={<Download size={16} />}
>

View File

@@ -80,6 +80,6 @@ describe('ExportMenu', () => {
mockIsExporting = true;
renderMenu();
expect(screen.getByTestId(TEST_ID)).toBeDisabled();
expect(screen.getByTestId(TEST_ID)).toHaveAttribute('aria-disabled', 'true');
});
});

View File

@@ -57,9 +57,8 @@ function SortableField({
</div>
{!isRequired && (
<Button
className={cx(styles.removeBtn, 'periscope-btn')}
variant="outlined"
color="destructive"
variant="solid"
color="danger"
size="sm"
onClick={(): void => onRemove(field)}
>

View File

@@ -98,11 +98,15 @@
user-select: none;
font-size: 13px;
> [data-slot='button'] {
opacity: 0;
transition: opacity 0.15s ease-in-out;
}
&:hover {
background-color: var(--l2-background);
.removeBtn,
.addBtn {
> [data-slot='button'] {
opacity: 1;
}
}
@@ -137,14 +141,6 @@
height: 32px;
}
.removeBtn,
.addBtn {
padding: 4px 10px;
opacity: 0;
transition: opacity 0.15s ease-in-out;
flex-shrink: 0;
}
.footer {
display: flex;
gap: 12px;

View File

@@ -173,6 +173,7 @@ function FieldsSelectorContent({
{hasUnsavedChanges && (
<div className={styles.footer}>
<Button
size="md"
variant="outlined"
color="secondary"
onClick={handleDiscard}
@@ -181,6 +182,7 @@ function FieldsSelectorContent({
Discard
</Button>
<Button
size="md"
variant="solid"
color="primary"
onClick={handleSave}

View File

@@ -138,7 +138,6 @@ function OtherFields({
<span className={styles.fieldKey}>{attr.name}</span>
{!isAtLimit && (
<Button
className={cx(styles.addBtn, 'periscope-btn')}
variant="outlined"
color="secondary"
size="sm"

View File

@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { toast } from '@signozhq/ui/sonner';
import { Button, Input } from 'antd';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { ToggleGroup } from '@signozhq/ui/toggle-group';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { handleContactSupport } from 'container/Integrations/utils';
@@ -102,10 +102,13 @@ function FeedbackModal({ onClose }: { onClose: () => void }): JSX.Element {
return (
<div className="feedback-modal-container">
<div className="feedback-modal-header">
<ToggleGroupSimple
<ToggleGroup
variant="outlined"
color="secondary"
size="sm"
type="single"
value={activeTab}
className="feedback-modal-tabs"
width="100%"
onChange={setActiveTab}
items={items}
/>

View File

@@ -121,78 +121,26 @@
}
.feedback-modal-container {
.feedback-modal-tabs {
width: 100%;
.feedback-modal-tab-label {
display: flex;
align-items: center;
gap: 8px;
.ant-radio-button-wrapper {
flex: 1;
margin: 0px !important;
border: 1px solid var(--l1-border);
&:before {
display: none;
}
.ant-radio-button-checked {
background-color: var(--l3-background);
}
.tab-icon {
width: 6px;
height: 6px;
}
.feedback-modal-tab-label {
display: flex;
align-items: center;
gap: 8px;
.tab-icon {
width: 6px;
height: 6px;
}
.feedback-tab {
background-color: var(--danger-background);
}
.bug-tab {
background-color: var(--warning-background);
}
.feature-tab {
background-color: var(--primary-background);
}
.feedback-tab {
background-color: var(--danger-background);
}
.ant-tabs-nav-list {
.ant-tabs-tab {
padding: 6px 16px;
.bug-tab {
background-color: var(--warning-background);
}
border-radius: 2px;
background: var(--l2-background);
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.1);
border: 1px solid var(--l1-border);
margin: 0 !important;
.ant-tabs-tab-btn {
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 166.667% */
letter-spacing: -0.06px;
}
&-active {
background: var(--l3-background);
color: var(--l1-foreground);
border-bottom: none !important;
.ant-tabs-tab-btn {
color: var(--l1-foreground);
}
}
}
.feature-tab {
background-color: var(--primary-background);
}
}

View File

@@ -2,7 +2,7 @@ import { useCallback, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { Dot } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Tooltip } from '@signozhq/ui/tooltip';
import Noz from 'components/Noz/Noz';
import { NOZ_TOOLTIP_TITLE } from 'components/Noz/Noz.constants';
import { Popover } from 'antd';
@@ -113,24 +113,26 @@ function HeaderRightSection({
</span>
) : null}
<TooltipSimple title={NOZ_TOOLTIP_TITLE}>
<Button
variant="solid"
color="secondary"
className="noz-wave"
onClick={handleOpenAIAssistant}
aria-label={
showHeaderPendingBadge
? pendingUserInputCount === 1
? 'Open Noz, 1 action needs your response'
: `Open Noz, ${pendingUserInputCount} actions need your response`
: 'Open Noz'
}
prefix={<Noz size={20} />}
>
<Typography.Text>Noz</Typography.Text>
</Button>
</TooltipSimple>
<span className="noz-wave">
<Tooltip title={NOZ_TOOLTIP_TITLE}>
<Button
size="md"
variant="solid"
color="secondary"
onClick={handleOpenAIAssistant}
aria-label={
showHeaderPendingBadge
? pendingUserInputCount === 1
? 'Open Noz, 1 action needs your response'
: `Open Noz, ${pendingUserInputCount} actions need your response`
: 'Open Noz'
}
prefix={<Noz size={20} />}
>
<Typography.Text>Noz</Typography.Text>
</Button>
</Tooltip>
</span>
</div>
)}
@@ -147,13 +149,15 @@ function HeaderRightSection({
onOpenChange={handleOpenFeedbackModalChange}
>
<Button
color="secondary"
variant="ghost"
size="icon"
className="share-feedback-btn"
size="sm"
icon
aria-label="Feedback"
prefix={<SquarePen size={14} />}
onClick={handleOpenFeedbackModal}
/>
>
<SquarePen size={14} />
</Button>
</Popover>
)}
@@ -170,16 +174,19 @@ function HeaderRightSection({
onOpenChange={handleOpenAnnouncementsModalChange}
>
<Button
color="secondary"
variant="ghost"
size="icon"
size="sm"
icon
aria-label="Announcements"
prefix={<Inbox size={14} />}
onClick={(): void => {
logEvent('Announcements: Clicked', {
page: location.pathname,
});
}}
/>
>
<Inbox size={14} />
</Button>
</Popover>
)}
@@ -196,12 +203,15 @@ function HeaderRightSection({
onOpenChange={handleOpenShareURLModalChange}
>
<Button
color="secondary"
variant="ghost"
size="icon"
size="sm"
icon
aria-label="Share"
prefix={<Globe size={14} />}
onClick={handleOpenShareURLModal}
/>
>
<Globe size={14} />
</Button>
</Popover>
)}
</div>

View File

@@ -149,6 +149,9 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
<Info size={14} color={Color.BG_AMBER_600} />
)}
<Switch
color="primary"
textPlacement="right"
disabledTooltip="Please select / enter valid relative time to toggle."
value={enableAbsoluteTime}
disabled={!isValidateRelativeTime}
onChange={(): void => {
@@ -173,6 +176,8 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
</Typography.Text>
<div className="absolute-relative-time-toggler">
<Switch
color="primary"
textPlacement="right"
value={enableExtraOption}
onChange={(): void => setEnableExtraOption((prev) => !prev)}
/>

View File

@@ -69,23 +69,23 @@ describe('FeedbackModal', () => {
const user = userEvent.setup();
render(<FeedbackModal onClose={mockOnClose} />);
// Initially, feedback radio should be active
const feedbackRadio = screen.getByRole('radio', { name: 'Feedback' });
expect(feedbackRadio).toBeChecked();
// Initially, feedback button should be active
const feedbackButton = screen.getByRole('button', { name: 'Feedback' });
expect(feedbackButton).toHaveAttribute('aria-pressed', 'true');
const bugTab = screen.getByText('Report a bug');
await user.click(bugTab);
// Bug radio should now be active
const bugRadio = screen.getByRole('radio', { name: 'Report a bug' });
expect(bugRadio).toBeChecked();
// Bug button should now be active
const bugButton = screen.getByRole('button', { name: 'Report a bug' });
expect(bugButton).toHaveAttribute('aria-pressed', 'true');
const featureTab = screen.getByText('Feature request');
await user.click(featureTab);
// Feature radio should now be active
const featureRadio = screen.getByRole('radio', { name: 'Feature request' });
expect(featureRadio).toBeChecked();
// Feature button should now be active
const featureButton = screen.getByRole('button', { name: 'Feature request' });
expect(featureButton).toHaveAttribute('aria-pressed', 'true');
});
it('should update feedback text when typing in textarea', async () => {
@@ -133,9 +133,9 @@ describe('FeedbackModal', () => {
const bugTab = screen.getByText('Report a bug');
await user.click(bugTab);
// Verify bug report radio is now active
const bugRadio = screen.getByRole('radio', { name: 'Report a bug' });
expect(bugRadio).toBeChecked();
// Verify bug report button is now active
const bugButton = screen.getByRole('button', { name: 'Report a bug' });
expect(bugButton).toHaveAttribute('aria-pressed', 'true');
const textarea = screen.getByPlaceholderText('Write your feedback here...');
const submitButton = screen.getByRole('button', { name: /submit/i });
@@ -166,9 +166,9 @@ describe('FeedbackModal', () => {
const featureTab = screen.getByText('Feature request');
await user.click(featureTab);
// Verify feature request radio is now active
const featureRadio = screen.getByRole('radio', { name: 'Feature request' });
expect(featureRadio).toBeChecked();
// Verify feature request button is now active
const featureButton = screen.getByRole('button', { name: 'Feature request' });
expect(featureButton).toHaveAttribute('aria-pressed', 'true');
const textarea = screen.getByPlaceholderText('Write your feedback here...');
const submitButton = screen.getByRole('button', { name: /submit/i });
@@ -262,8 +262,8 @@ describe('FeedbackModal', () => {
);
expect(newTextArea).toHaveValue(''); // Should be empty
// Verify active radio is reset to default (Feedback radio)
const feedbackRadio = screen.getByRole('radio', { name: 'Feedback' });
expect(feedbackRadio).toBeChecked();
// Verify active button is reset to default (Feedback button)
const feedbackButton = screen.getByRole('button', { name: 'Feedback' });
expect(feedbackButton).toHaveAttribute('aria-pressed', 'true');
});
});

View File

@@ -176,7 +176,7 @@ describe('ShareURLModal', () => {
expect(
screen.getByText('Please select / enter valid relative time to toggle.'),
).toBeInTheDocument();
expect(screen.getByRole('switch')).toBeDisabled();
expect(screen.getByRole('switch')).toHaveAttribute('aria-disabled', 'true');
});
it('should process URL with absolute time for non-custom time', async () => {

View File

@@ -1,44 +1,32 @@
import { Badge } from '@signozhq/ui/badge';
import { Badge, type BadgeColorType } from '@signozhq/ui/badge';
type BadgeColor =
| 'vanilla'
| 'robin'
| 'forest'
| 'amber'
| 'sienna'
| 'cherry'
| 'sakura'
| 'aqua';
function getStatusCodeColor(statusCode: number): BadgeColorType {
if (statusCode >= 200 && statusCode < 300) {
return 'success';
}
if (statusCode >= 300 && statusCode < 400) {
return 'primary';
}
if (statusCode >= 400 && statusCode < 500) {
return 'warning';
}
if (statusCode >= 500) {
return 'danger';
}
if (statusCode >= 100 && statusCode < 200) {
return 'secondary';
}
return 'primary';
}
interface HttpStatusBadgeProps {
statusCode: string | number;
testId?: string;
className?: string;
}
function getStatusCodeColor(statusCode: number): BadgeColor {
if (statusCode >= 200 && statusCode < 300) {
return 'forest'; // Success - green
}
if (statusCode >= 300 && statusCode < 400) {
return 'robin'; // Redirect - blue
}
if (statusCode >= 400 && statusCode < 500) {
return 'amber'; // Client error - amber
}
if (statusCode >= 500) {
return 'cherry'; // Server error - red
}
if (statusCode >= 100 && statusCode < 200) {
return 'vanilla'; // Informational - neutral
}
return 'robin'; // Default fallback
}
function HttpStatusBadge({
statusCode,
testId,
className,
}: HttpStatusBadgeProps): JSX.Element | null {
const numericStatusCode = Number(statusCode);
@@ -49,12 +37,7 @@ function HttpStatusBadge({
const color = getStatusCodeColor(numericStatusCode);
return (
<Badge
color={color}
variant="outline"
data-testid={testId}
className={className}
>
<Badge color={color} variant="outlined" testId={testId}>
{statusCode}
</Badge>
);

View File

@@ -119,11 +119,13 @@ function InviteMembers({
<div className={styles.cellAction}>
{canRemoveRow && (
<Button
variant="ghost"
color="destructive"
size="md"
variant="solid"
color="danger"
onClick={(): void => removeRow(row.id)}
aria-label="Remove row"
data-testid={`invite-remove-${row.id}`}
testId={`invite-remove-${row.id}`}
icon
>
<Trash2 size={12} />
</Button>
@@ -136,11 +138,12 @@ function InviteMembers({
{showAddButton && (
<div className={styles.addRow}>
<Button
size="md"
variant="dashed"
color="secondary"
prefix={<Plus size={12} />}
onClick={addRow}
data-testid="invite-add-row"
testId="invite-add-row"
>
Add another
</Button>

View File

@@ -1,14 +1,22 @@
import { useMemo, useState } from 'react';
import { useMutation } from 'react-query';
import { useLocation } from 'react-router-dom';
import { Button, Tooltip } from 'antd';
import AddCreditCardModal from 'components/AddCreditCardModal/AddCreditCardModal';
import { Button, Modal, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { createSubscription } from 'api/generated/services/subscriptions';
import type { CreateSubscription201 } from 'api/generated/services/sigNoz.schemas';
import cx from 'classnames';
import { FeatureKeys } from 'constants/features';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useNotifications } from 'hooks/useNotifications';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import { SubscriptionCreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/subscription.permissions';
import { defaultTo } from 'lodash-es';
import { CircleHelp } from '@signozhq/icons';
import { CircleHelp, CreditCard, X } from '@signozhq/icons';
import { useAppContext } from 'providers/App/App';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
import './LaunchChatSupport.styles.scss';
@@ -33,6 +41,7 @@ function LaunchChatSupport({
chatMessageDisabled = false,
}: LaunchChatSupportProps): JSX.Element | null {
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const { notifications } = useNotifications();
const {
trialInfo,
featureFlags,
@@ -110,12 +119,43 @@ function LaunchChatSupport({
}
};
const handleBillingOnSuccess = (data: CreateSubscription201): void => {
if (data?.data?.redirectURL) {
const newTab = document.createElement('a');
newTab.href = data.data.redirectURL;
newTab.target = '_blank';
newTab.rel = 'noopener noreferrer';
newTab.click();
}
};
const handleBillingOnError = (error: APIError): void => {
notifications.error({
message: error.getErrorCode(),
description: error.getErrorMessage(),
});
};
const { mutate: updateCreditCard, isLoading: isLoadingBilling } = useMutation(
createSubscription,
{
onSuccess: (data) => {
handleBillingOnSuccess(data);
},
onError: handleBillingOnError,
},
);
const handleAddCreditCard = (): void => {
logEvent('Add Credit card modal: Clicked', {
source: `facing issues button`,
page: pathname,
...attributes,
});
updateCreditCard({
url: getBaseUrl(),
});
};
return isCloudUserVal && isChatSupportEnabled ? ( // Note: we would need to move this condition to license based in future
@@ -135,11 +175,47 @@ function LaunchChatSupport({
</Button>
</Tooltip>
<AddCreditCardModal
{/* Add Credit Card Modal */}
<Modal
className="add-credit-card-modal"
title={<span className="title">Add Credit Card for Chat Support</span>}
open={isAddCreditCardModalOpen}
onClose={(): void => setIsAddCreditCardModalOpen(false)}
onAddCreditCard={handleAddCreditCard}
/>
closable
onCancel={(): void => setIsAddCreditCardModalOpen(false)}
destroyOnClose
footer={[
<Button
key="cancel"
onClick={(): void => setIsAddCreditCardModalOpen(false)}
className="cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<AuthZTooltip
key="submit"
checks={[SubscriptionCreatePermission]}
withPortal={false}
>
<Button
type="primary"
icon={<CreditCard size={16} />}
size="middle"
loading={isLoadingBilling}
disabled={isLoadingBilling}
onClick={handleAddCreditCard}
className="add-credit-card-btn"
>
Add Credit Card
</Button>
</AuthZTooltip>,
]}
>
<Typography.Text className="add-credit-card-text">
You&apos;re currently on <span className="highlight-text">Trial plan</span>
. Add a credit card to access SigNoz chat support to your workspace.
</Typography.Text>
</Modal>
</div>
) : null;
}

View File

@@ -158,37 +158,14 @@
}
}
.views-tabs {
color: var(--l2-foreground);
.view-title {
display: flex;
gap: var(--margin-2);
align-items: center;
justify-content: center;
font-size: var(--font-size-xs);
font-style: normal;
font-weight: var(--font-weight-normal);
}
> button {
border: 1px solid var(--l1-border);
width: 114px;
&::before {
background: var(--l1-border);
}
&[data-state='on'] {
background: var(--l3-background);
color: var(--l1-foreground);
border: 1px solid var(--l1-border);
&::before {
background: var(--l1-border);
}
}
}
.view-title {
display: flex;
gap: var(--margin-2);
align-items: center;
justify-content: center;
font-size: var(--font-size-xs);
font-style: normal;
font-weight: var(--font-weight-normal);
}
.search-input {
@@ -239,42 +216,4 @@
align-items: center;
margin-left: 8px;
}
.log-arrow-btn {
padding: 0;
min-width: 28px;
height: 28px;
border-radius: 4px;
background: var(--l2-background);
color: var(--l2-foreground);
border: 1px solid var(--l1-border);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease-in-out;
}
.log-arrow-btn-up,
.log-arrow-btn-down {
background: var(--l2-background);
}
.log-arrow-btn:active,
.log-arrow-btn:focus {
background: var(--l3-background);
color: var(--l1-foreground);
}
.log-arrow-btn[disabled] {
opacity: 0.5;
cursor: not-allowed;
background: var(--l1-background);
color: var(--l3-foreground);
.log-arrow-btn:hover:not([disabled]) {
background: var(--l3-background);
color: var(--l1-foreground);
}
}
}

View File

@@ -6,14 +6,6 @@
gap: 8px;
}
.tooltipContent {
--tooltip-z-index: 2100;
}
.dropdownContent {
--dropdown-menu-content-z-index: 2100;
}
.leftSection {
display: flex;
align-items: center;

View File

@@ -1,8 +1,8 @@
import { Button } from '@signozhq/ui/button';
import { Divider } from '@signozhq/ui/divider';
import { DropdownMenuSimple as Dropdown } from '@signozhq/ui/dropdown-menu';
import { DropdownMenuSimple as Dropdown } from 'components/DropdownMenu/DropdownMenuSimple';
import { Typography } from '@signozhq/ui/typography';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Tooltip } from '@signozhq/ui/tooltip';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils';
import { toast } from '@signozhq/ui/sonner';
@@ -23,8 +23,6 @@ import { useCopyToClipboard } from 'react-use';
import styles from './LogDetailsHeader.module.scss';
const TOOLTIP_CONTENT_PROPS = { className: styles.tooltipContent };
interface LogDetailsHeaderProps {
log: ILog;
onNavigatePrev: () => void;
@@ -91,6 +89,7 @@ function LogDetailsHeader({
<div className={styles.actions}>
{showOpenInExplorer && (
<Button
size="md"
variant="outlined"
color="secondary"
prefix={<Compass size={16} />}
@@ -100,51 +99,57 @@ function LogDetailsHeader({
</Button>
)}
<Dropdown
menu={{ items: menuItems }}
align="end"
className={styles.dropdownContent}
onClick={(e: MouseEvent): void => e.stopPropagation()}
>
<Dropdown menu={{ items: menuItems }} align="end">
<Button
size="md"
variant="link"
color="secondary"
prefix={<Ellipsis size={16} />}
data-testid="log-details-header-menu"
/>
icon
aria-label="Log actions"
testId="log-details-header-menu"
onClick={(e: MouseEvent): void => e.stopPropagation()}
>
<Ellipsis size={16} />
</Button>
</Dropdown>
<div className={styles.arrows}>
<TooltipSimple
title="Move to previous log"
<Tooltip
title={isPrevDisabled ? undefined : 'Move to previous log'}
side="top"
open={isPrevDisabled ? false : undefined}
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
>
<Button
disabledTooltip="No previous log"
size="md"
variant="outlined"
color="secondary"
prefix={<ChevronUp size={14} />}
icon
aria-label="Move to previous log"
disabled={isPrevDisabled}
onClick={onNavigatePrev}
data-testid="log-details-header-prev"
/>
</TooltipSimple>
<TooltipSimple
title="Move to next log"
testId="log-details-header-prev"
>
<ChevronUp size={14} />
</Button>
</Tooltip>
<Tooltip
title={isNextDisabled ? undefined : 'Move to next log'}
side="top"
open={isNextDisabled ? false : undefined}
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
>
<Button
disabledTooltip="No next log"
size="md"
variant="outlined"
color="secondary"
prefix={<ChevronDown size={14} />}
icon
aria-label="Move to next log"
disabled={isNextDisabled}
onClick={onNavigateNext}
data-testid="log-details-header-next"
/>
</TooltipSimple>
testId="log-details-header-next"
>
<ChevronDown size={14} />
</Button>
</Tooltip>
</div>
</div>
</div>

View File

@@ -12,13 +12,6 @@
}
}
.valueBadge {
--badge-font-size: 13px;
box-sizing: border-box;
max-width: 100%;
min-width: 0;
}
// Truncating text inside a badge
.badgeText {
min-width: 0;

View File

@@ -1,5 +1,5 @@
import { ReactNode } from 'react';
import { Badge, BadgeColor } from '@signozhq/ui/badge';
import { Badge, type BadgeColorType } from '@signozhq/ui/badge';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import { getLogIndicatorType } from 'components/Logs/LogStateIndicator/utils';
import { ILog } from 'types/api/logs/log';
@@ -8,13 +8,13 @@ import styles from './LogHighlights.module.scss';
import TraceIdField from './TraceIdField';
// Severity badge color mirrors the LogStateIndicator bar
const SEVERITY_COLOR: Record<string, BadgeColor> = {
[LogType.TRACE]: 'forest',
[LogType.DEBUG]: 'aqua',
[LogType.INFO]: 'robin',
[LogType.WARN]: 'amber',
[LogType.ERROR]: 'cherry',
[LogType.FATAL]: 'sakura',
const SEVERITY_COLOR: Record<string, BadgeColorType> = {
[LogType.TRACE]: 'success',
[LogType.DEBUG]: 'info',
[LogType.INFO]: 'primary',
[LogType.WARN]: 'warning',
[LogType.ERROR]: 'danger',
[LogType.FATAL]: 'highlight-danger',
};
export interface LogHighlightConfig {
@@ -32,9 +32,9 @@ const getAttr = (log: ILog, key: string): string =>
const valueBadge = (
value: string,
options?: { prefix?: ReactNode; color?: BadgeColor },
options?: { prefix?: ReactNode; color?: BadgeColorType },
): ReactNode => (
<Badge color={options?.color ?? 'vanilla'} className={styles.valueBadge}>
<Badge variant="solid" color={options?.color ?? 'secondary'}>
{options?.prefix}
<span className={styles.badgeText} title={value}>
{value}

View File

@@ -221,8 +221,14 @@ describe('LogDetail drawer — header (isLogDetailsV2)', () => {
// Active log is the first one.
renderDrawer({ log: logs[0], logs, onNavigateLog });
expect(screen.getByTestId('log-details-header-prev')).toBeDisabled();
expect(screen.getByTestId('log-details-header-next')).toBeEnabled();
expect(screen.getByTestId('log-details-header-prev')).toHaveAttribute(
'aria-disabled',
'true',
);
expect(screen.getByTestId('log-details-header-next')).not.toHaveAttribute(
'aria-disabled',
'true',
);
await user.click(screen.getByTestId('log-details-header-next'));
expect(onNavigateLog).toHaveBeenLastCalledWith(logs[1]);

View File

@@ -4,7 +4,7 @@ import { useCopyToClipboard } from 'react-use';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button } from '@signozhq/ui/button';
import { Drawer, Tooltip } from 'antd';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { ToggleGroup } from '@signozhq/ui/toggle-group';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
@@ -323,41 +323,49 @@ function LogDetailInner({
<div className="log-detail-drawer__title-right">
<div className="log-arrows">
<Tooltip
title={isPrevDisabled ? '' : 'Move to previous log'}
title={isPrevDisabled ? undefined : 'Move to previous log'}
placement="top"
mouseLeaveDelay={0}
>
<Button
disabledTooltip="No previous log"
size="md"
variant="outlined"
color="secondary"
prefix={<ChevronUp size={14} />}
className="log-arrow-btn log-arrow-btn-up"
icon
aria-label="Move to previous log"
disabled={isPrevDisabled}
onClick={goToPrev}
/>
>
<ChevronUp size={14} />
</Button>
</Tooltip>
<Tooltip
title={isNextDisabled ? '' : 'Move to next log'}
title={isNextDisabled ? undefined : 'Move to next log'}
placement="top"
mouseLeaveDelay={0}
>
<Button
disabledTooltip="No next log"
size="md"
variant="outlined"
color="secondary"
prefix={<ChevronDown size={14} />}
className="log-arrow-btn log-arrow-btn-down"
icon
aria-label="Move to next log"
disabled={isNextDisabled}
onClick={goToNext}
/>
>
<ChevronDown size={14} />
</Button>
</Tooltip>
</div>
{handleOpenInExplorer && (
<div>
<Button
size="md"
variant="outlined"
color="secondary"
prefix={<Compass size={16} />}
className="open-in-explorer-btn"
onClick={handleOpenInExplorer}
>
Open in Explorer
@@ -410,9 +418,12 @@ function LogDetailInner({
{isLogDetailsV2 && <div className="log-detail-drawer__section-divider" />}
<div className="tabs-and-search">
<ToggleGroupSimple
<ToggleGroup
variant="outlined"
color="secondary"
size="sm"
type="single"
className="views-tabs"
testId="log-detail-views-tabs"
onChange={handleModeChange}
value={selectedView}
items={[
@@ -473,9 +484,12 @@ function LogDetailInner({
variant="link"
color="secondary"
size="sm"
prefix={<Filter size="lg" />}
icon
aria-label="Show Filters"
onClick={handleFilterVisible}
/>
>
<Filter size="lg" />
</Button>
</Tooltip>
)}
@@ -493,9 +507,14 @@ function LogDetailInner({
variant="link"
color="secondary"
size="sm"
prefix={<Copy size={12} />}
icon
aria-label={
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
}
onClick={selectedView === VIEW_TYPES.JSON ? handleJSONCopy : onLogCopy}
/>
>
<Copy size={12} />
</Button>
</Tooltip>
)}
</div>

View File

@@ -1,4 +1,4 @@
import type { ReactNode } from 'react';
import type { ReactElement, ReactNode } from 'react';
import {
Bold,
CodeXml,
@@ -11,16 +11,17 @@ import {
Type,
} from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Tooltip } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import { READ_ONLY_TOOLTIP } from './constants';
import InsertVariableMenu from './InsertVariableMenu';
import MarkdownHelp from './MarkdownHelp';
import type { EditorCommand, EditorVariable } from './types';
import styles from './MarkdownEditor.module.scss';
const COMMAND_ICONS: Record<string, ReactNode> = {
const COMMAND_ICONS: Record<string, ReactElement> = {
heading: <Heading size={14} />,
bold: <Bold size={14} />,
italic: <Italic size={14} />,
@@ -61,20 +62,22 @@ function EditorToolbar({
<span className={styles.toolbarDivider} />
<div className={styles.commands}>
{commands.map((command) => (
<TooltipSimple key={command.id} title={command.label}>
<Tooltip key={command.id} title={disabled ? undefined : command.label}>
<Button
disabledTooltip={READ_ONLY_TOOLTIP}
type="button"
variant="ghost"
color="secondary"
size="icon"
size="sm"
icon
disabled={disabled}
aria-label={command.label}
data-testid={`markdown-command-${command.id}`}
testId={`markdown-command-${command.id}`}
onClick={(): void => onRunCommand(command)}
>
{COMMAND_ICONS[command.id]}
</Button>
</TooltipSimple>
</Tooltip>
))}
</div>
<div className={styles.toolbarEnd}>

View File

@@ -1,8 +1,12 @@
import { useMemo, useState } from 'react';
import { ChevronDown, DollarSign, Search } from '@signozhq/icons';
import { ChevronDown, DollarSign } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import {
DropdownMenuSimple,
type MenuItem,
} from 'components/DropdownMenu/DropdownMenuSimple';
import { READ_ONLY_TOOLTIP } from './constants';
import type { EditorVariable } from './types';
import styles from './MarkdownEditor.module.scss';
@@ -61,12 +65,13 @@ function InsertVariableMenu({
return (
<DropdownMenuSimple
className={styles.variableMenu}
contentMaxWidth={320}
disabled={disabled}
disabledTooltip={READ_ONLY_TOOLTIP}
menu={{
items,
search: {
placeholder: 'Search variables',
searchIcon: <Search size={14} />,
onSearchChange: setSearch,
},
}}
@@ -76,11 +81,9 @@ function InsertVariableMenu({
variant="outlined"
color="secondary"
size="sm"
disabled={disabled}
prefix={<DollarSign size={14} className={styles.insertVariableIcon} />}
suffix={<ChevronDown size={14} />}
className={styles.insertVariable}
data-testid="markdown-insert-variable"
testId="markdown-insert-variable"
>
Insert variable
</Button>

View File

@@ -74,27 +74,10 @@
margin-left: auto;
}
.insertVariable {
white-space: nowrap;
}
.insertVariableIcon {
color: var(--text-amber-400);
}
// The ui library's dropdown assumes a global border-box reset this app doesn't
// have (`box-sizing` is set on `body` only and doesn't inherit): its items are
// `width: 100%` + padding, so in the portal they lay out content-box and
// overflow the popup by the padding — clipping the flush-right badge.
.variableMenu,
.variableMenu * {
box-sizing: border-box;
}
.variableMenu {
width: 320px;
}
// Shrinkable, so a clamped popup truncates the name instead of clipping the
// badge at the content's `overflow: hidden` edge.
.variableRow {

View File

@@ -15,9 +15,10 @@ function MarkdownHelp(): JSX.Element {
type="button"
variant="ghost"
color="secondary"
size="icon"
size="sm"
icon
aria-label="Markdown syntax help"
data-testid="markdown-help-trigger"
testId="markdown-help-trigger"
>
<CircleHelp size={14} />
</Button>

View File

@@ -256,8 +256,14 @@ describe('MarkdownEditor', () => {
/>,
);
expect(screen.getByTestId('markdown-command-bold')).toBeDisabled();
expect(screen.getByTestId('markdown-insert-variable')).toBeDisabled();
expect(screen.getByTestId('markdown-command-bold')).toHaveAttribute(
'aria-disabled',
'true',
);
expect(screen.getByTestId('markdown-insert-variable')).toHaveAttribute(
'aria-disabled',
'true',
);
});
it('hides the insert-variable control when none are available', () => {

View File

@@ -1,6 +1,8 @@
// The body is persisted inline in the dashboard JSON, so its length is capped.
export const MARKDOWN_MAX_LENGTH = 16000;
export const READ_ONLY_TOOLTIP = 'The editor is read-only';
/** The canonical syntax; the renderer resolves the other three too. */
export const formatVariableToken = (name: string): string => `$${name}`;

View File

@@ -55,14 +55,14 @@ function NameEmailCell({
function StatusBadge({ status }: { status: MemberRow['status'] }): JSX.Element {
if (status === MemberStatus.Active) {
return (
<Badge color="forest" variant="outline">
<Badge color="success" variant="outlined">
ACTIVE
</Badge>
);
}
if (status === MemberStatus.Deleted) {
return (
<Badge color="cherry" variant="outline">
<Badge color="danger" variant="outlined">
DELETED
</Badge>
);
@@ -70,13 +70,17 @@ function StatusBadge({ status }: { status: MemberRow['status'] }): JSX.Element {
if (status === MemberStatus.Invited) {
return (
<Badge color="amber" variant="outline">
<Badge color="warning" variant="outlined">
INVITED
</Badge>
);
}
return <Badge color="vanilla">⎯</Badge>;
return (
<Badge variant="solid" color="secondary">
⎯
</Badge>
);
}
function MembersEmptyState({

View File

@@ -20,7 +20,7 @@ import {
import { Color } from '@signozhq/design-tokens';
import { Button, Select } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { TooltipProvider, TooltipSimple } from '@signozhq/ui/tooltip';
import { Tooltip } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import TextToolTip from 'components/TextToolTip/TextToolTip';
@@ -758,9 +758,14 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
tabIndex={isActive ? 0 : -1}
>
<Checkbox
color="primary"
value={isSelected}
className="option-checkbox"
onClick={(e): void => selectFromButton(e, 'checkbox')}
width="100%"
onChange={(): void => {
handleItemSelection('checkbox');
setActiveChipIndex(-1);
setActiveIndex(-1);
}}
>
<div className="option-content">
<Typography.Text truncate={1} className="option-label-text">
@@ -1595,7 +1600,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
}}
>
<div style={{ display: 'flex', alignItems: 'center', width: '100%' }}>
<Checkbox value={allOptionsSelected} className="option-checkbox">
<Checkbox color="primary" value={allOptionsSelected} width="100%">
<div className="option-content">
<div className="all-option-text">ALL</div>
</div>
@@ -1973,13 +1978,9 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
// `label` arrives already cut to maxTagTextLength, so the reveal reads the
// option's own text (falling back to the raw value for freeform tags).
return (
<TooltipSimple
side="top"
delayDuration={300}
title={findOptionLabelText(options, value)}
>
<Tooltip side="top" title={findOptionLabelText(options, value)}>
{tag}
</TooltipSimple>
</Tooltip>
);
}
@@ -2015,56 +2016,51 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
return (
// Self-provided so the per-tag tooltips work wherever this select is rendered,
// without every consumer having to sit under an app-level provider.
<TooltipProvider>
<div
className={cx('custom-multiselect-wrapper', {
'all-selected': allOptionShown || isAllSelected,
<div
className={cx('custom-multiselect-wrapper', {
'all-selected': allOptionShown || isAllSelected,
})}
>
{(allOptionShown || isAllSelected) && !searchText && (
<div className="all-text">ALL</div>
)}
<Select
ref={selectRef}
className={cx('custom-multiselect', className, {
'has-selection': selectedChips.length > 0 && !isAllSelected,
'is-all-selected': isAllSelected,
})}
>
{(allOptionShown || isAllSelected) && !searchText && (
<div className="all-text">ALL</div>
)}
<Select
ref={selectRef}
className={cx('custom-multiselect', className, {
'has-selection': selectedChips.length > 0 && !isAllSelected,
'is-all-selected': isAllSelected,
})}
placeholder={placeholder}
mode="multiple"
showSearch
filterOption={false}
onSearch={handleSearch}
value={displayValue}
onChange={(newValue): void => {
handleInternalChange(newValue, false);
}}
onClear={onClearHandler}
onDropdownVisibleChange={handleDropdownVisibleChange}
open={isOpen}
defaultActiveFirstOption={defaultActiveFirstOption}
popupMatchSelectWidth={dropdownMatchSelectWidth}
allowClear={allowClear}
getPopupContainer={getPopupContainer ?? popupContainer}
suffixIcon={<ChevronDown style={{ cursor: 'default' }} size="md" />}
dropdownRender={customDropdownRender}
menuItemSelectedIcon={null}
popupClassName={cx(
'custom-multiselect-dropdown-container',
popupClassName,
)}
notFoundContent={<div className="empty-message">{noDataMessage}</div>}
onKeyDown={handleKeyDown}
tagRender={tagRender as any}
placement={placement}
listHeight={300}
searchValue={searchText}
maxTagTextLength={maxTagTextLength}
maxTagCount={isAllSelected ? undefined : maxTagCount}
{...rest}
/>
</div>
</TooltipProvider>
placeholder={placeholder}
mode="multiple"
showSearch
filterOption={false}
onSearch={handleSearch}
value={displayValue}
onChange={(newValue): void => {
handleInternalChange(newValue, false);
}}
onClear={onClearHandler}
onDropdownVisibleChange={handleDropdownVisibleChange}
open={isOpen}
defaultActiveFirstOption={defaultActiveFirstOption}
popupMatchSelectWidth={dropdownMatchSelectWidth}
allowClear={allowClear}
getPopupContainer={getPopupContainer ?? popupContainer}
suffixIcon={<ChevronDown style={{ cursor: 'default' }} size="md" />}
dropdownRender={customDropdownRender}
menuItemSelectedIcon={null}
popupClassName={cx('custom-multiselect-dropdown-container', popupClassName)}
notFoundContent={<div className="empty-message">{noDataMessage}</div>}
onKeyDown={handleKeyDown}
tagRender={tagRender as any}
placement={placement}
listHeight={300}
searchValue={searchText}
maxTagTextLength={maxTagTextLength}
maxTagCount={isAllSelected ? undefined : maxTagCount}
{...rest}
/>
</div>
);
};

View File

@@ -1,6 +1,5 @@
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TooltipProvider } from '@signozhq/ui/tooltip';
import CustomMultiSelect from '../CustomMultiSelect';
@@ -14,15 +13,13 @@ const SELECTED = ['checkout-service-prod', 'payments-service-prod'];
function renderSelect(): void {
render(
<TooltipProvider>
<CustomMultiSelect
options={OPTIONS}
value={SELECTED}
maxTagCount={1}
maxTagTextLength={10}
maxTagPlaceholder={(omitted): string => `+${omitted.length}`}
/>
</TooltipProvider>,
<CustomMultiSelect
options={OPTIONS}
value={SELECTED}
maxTagCount={1}
maxTagTextLength={10}
maxTagPlaceholder={(omitted): string => `+${omitted.length}`}
/>,
);
}

View File

@@ -498,114 +498,95 @@ $custom-border-color: #2c3044;
margin-bottom: 8px;
}
.option-checkbox {
.all-option-text {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
cursor: default;
}
// The checkbox button is the only pointer target on the left; the label
// still toggles on click but keeps a default cursor.
> button {
cursor: pointer;
}
.option-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
// @signozhq/ui Checkbox renders children inside a <label> that is
// content-sized by default. Make it fill the row (min-width: 0 lets it
// shrink) so the option text below can truncate instead of overflowing.
> label {
.option-label-text {
flex: 1 1 auto;
min-width: 0;
margin-bottom: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.all-option-text {
.option-badge {
font-size: 12px;
padding: 2px 6px;
border-radius: 4px;
background-color: $custom-border-color;
color: var(--l2-foreground);
margin-left: 8px;
}
}
// "Only"/"All" is the primary action — a filled pill that reads as a
// button; "Toggle" is a secondary hint in plain text. Sized to the row's
// resting height so revealing them on hover never shifts it.
.only-btn,
.toggle-btn {
display: none;
align-items: center;
justify-content: center;
height: 18px;
min-height: 0;
font-size: 12px;
line-height: 1;
box-shadow: none;
}
.only-btn {
padding: 4px 8px;
// Black interior + a visible border so the pill stands out clearly
// against the near-black row when revealed on hover.
border: 1px solid var(--l3-border);
border-radius: 3px;
background-color: var(--bg-ink-500, #0b0c0e);
color: var(--l1-foreground);
cursor: pointer;
}
.toggle-btn {
padding: 0 6px;
border: none;
background-color: transparent;
color: var(--l2-foreground);
cursor: pointer;
}
// Toggle appears over the checkbox area; "Only/All" takes over the row
// content and hides Toggle there (higher specificity wins).
&:hover {
.toggle-btn {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.option-content {
.option-badge {
display: none;
}
}
.option-content:hover {
.only-btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
.option-label-text {
flex: 1 1 auto;
min-width: 0;
margin-bottom: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.option-badge {
font-size: 12px;
padding: 2px 6px;
border-radius: 4px;
background-color: $custom-border-color;
color: var(--l2-foreground);
margin-left: 8px;
}
}
// "Only"/"All" is the primary action — a filled pill that reads as a
// button; "Toggle" is a secondary hint in plain text. Sized to the row's
// resting height so revealing them on hover never shifts it.
.only-btn,
.toggle-btn {
display: none;
align-items: center;
justify-content: center;
height: 18px;
min-height: 0;
font-size: 12px;
line-height: 1;
box-shadow: none;
}
.only-btn {
padding: 4px 8px;
// Black interior + a visible border so the pill stands out clearly
// against the near-black row when revealed on hover.
border: 1px solid var(--l3-border);
border-radius: 3px;
background-color: var(--bg-ink-500, #0b0c0e);
color: var(--l1-foreground);
cursor: pointer;
}
.toggle-btn {
padding: 0 6px;
border: none;
background-color: transparent;
color: var(--l2-foreground);
cursor: pointer;
}
// Toggle appears over the checkbox area; "Only/All" takes over the row
// content and hides Toggle there (higher specificity wins).
&:hover {
.toggle-btn {
display: flex;
}
.option-badge {
display: none;
}
}
.option-content:hover {
.only-btn {
display: flex;
}
.toggle-btn {
display: none;
}
.option-badge {
display: none;
}
.option-badge {
display: none;
}
}
}

View File

@@ -513,8 +513,4 @@
color: var(--l2-foreground) !important;
}
}
.query-actions-dropdown {
cursor: pointer;
}
}

View File

@@ -36,51 +36,6 @@
align-items: center;
gap: 16px;
.add-ons-tabs {
display: flex;
flex-wrap: wrap;
.add-on-tab-title {
display: flex;
gap: var(--margin-2);
align-items: center;
justify-content: center;
font-size: var(--font-size-xs);
font-style: normal;
font-weight: var(--font-weight-normal);
color: var(--query-builder-v2-color, var(--l2-foreground));
}
> button {
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border-left: none;
min-width: 120px;
height: 36px;
line-height: 36px;
&:first-child {
border-left: 1px solid
var(--query-builder-v2-border-color, var(--l2-border));
}
&::before {
background: var(--query-builder-v2-border-color, var(--l2-border));
}
&[data-state='on'] {
color: var(--text-robin-500);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
display: none;
&::before {
background: var(--query-builder-v2-border-color, var(--l2-border));
}
}
}
}
.compass-button {
width: 30px;
height: 30px;

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Button, Tooltip } from 'antd';
import cx from 'classnames';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { ToggleGroup } from '@signozhq/ui/toggle-group';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GroupByFilter } from 'container/QueryBuilder/filters/GroupByFilter/GroupByFilter';
@@ -562,9 +562,11 @@ function QueryAddOns({
</div>
)}
<ToggleGroupSimple
<ToggleGroup
variant="outlined"
color="secondary"
size="sm"
type="multiple"
className="add-ons-tabs"
value={selectedViews.map((view) => view.key)}
onChange={(newKeys: string[]): void => {
const oldKeys: string[] = selectedViews.map((view) => view.key);

View File

@@ -724,26 +724,62 @@ function QuerySearch({
// Helper function to render a badge for the current context mode
const renderContextBadge = (): JSX.Element => {
if (!editingMode) {
return <Badge color="vanilla">Unknown</Badge>;
return (
<Badge variant="solid" color="secondary">
Unknown
</Badge>
);
}
switch (editingMode) {
case 'key':
return <Badge color="robin">Key</Badge>;
return (
<Badge variant="solid" color="primary">
Key
</Badge>
);
case 'operator':
return <Badge color="sakura">Operator</Badge>;
return (
<Badge variant="solid" color="highlight-danger">
Operator
</Badge>
);
case 'value':
return <Badge color="forest">Value</Badge>;
return (
<Badge variant="solid" color="success">
Value
</Badge>
);
case 'conjunction':
return <Badge color="amber">Conjunction</Badge>;
return (
<Badge variant="solid" color="warning">
Conjunction
</Badge>
);
case 'function':
return <Badge color="aqua">Function</Badge>;
return (
<Badge variant="solid" color="info">
Function
</Badge>
);
case 'parenthesis':
return <Badge color="sakura">Parenthesis</Badge>;
return (
<Badge variant="solid" color="highlight-danger">
Parenthesis
</Badge>
);
case 'bracketList':
return <Badge color="cherry">Bracket List</Badge>;
return (
<Badge variant="solid" color="danger">
Bracket List
</Badge>
);
default:
return <Badge color="vanilla">Unknown</Badge>;
return (
<Badge variant="solid" color="secondary">
Unknown
</Badge>
);
}
};
@@ -1465,27 +1501,44 @@ function QuerySearch({
Currently editing: {renderContextBadge()}
{queryContext?.keyToken && (
<span className="triplet-info">
Key: <Badge color="vanilla">{queryContext.keyToken}</Badge>
Key:{' '}
<Badge variant="solid" color="secondary">
{queryContext.keyToken}
</Badge>
</span>
)}
{queryContext?.operatorToken && (
<span className="triplet-info">
Operator: <Badge color="vanilla">{queryContext.operatorToken}</Badge>
Operator:{' '}
<Badge variant="solid" color="secondary">
{queryContext.operatorToken}
</Badge>
</span>
)}
{queryContext?.valueToken && (
<span className="triplet-info">
Value: <Badge color="vanilla">{queryContext.valueToken}</Badge>
Value:{' '}
<Badge variant="solid" color="secondary">
{queryContext.valueToken}
</Badge>
</span>
)}
{queryContext?.currentPair && (
<span className="triplet-info query-pair-info">
Current pair: <Badge color="robin">{queryContext.currentPair.key}</Badge>
<Badge color="sakura">{queryContext.currentPair.operator}</Badge>
Current pair:{' '}
<Badge variant="solid" color="primary">
{queryContext.currentPair.key}
</Badge>
<Badge variant="solid" color="highlight-danger">
{queryContext.currentPair.operator}
</Badge>
{queryContext.currentPair.value && (
<Badge color="forest">{queryContext.currentPair.value}</Badge>
<Badge variant="solid" color="success">
{queryContext.currentPair.value}
</Badge>
)}
<Badge
variant="solid"
color={queryContext.currentPair.isComplete ? 'success' : 'warning'}
>
{queryContext.currentPair.isComplete ? 'Complete' : 'Incomplete'}
@@ -1495,7 +1548,9 @@ function QuerySearch({
{queryContext?.queryPairs && queryContext.queryPairs.length > 0 && (
<span className="triplet-info">
Total pairs:{' '}
<Badge color="robin">{queryContext.queryPairs.length}</Badge>
<Badge variant="solid" color="primary">
{queryContext.queryPairs.length}
</Badge>
</span>
)}
</div>

View File

@@ -6,7 +6,7 @@ import {
useMemo,
useState,
} from 'react';
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
import { DropdownMenuSimple } from 'components/DropdownMenu/DropdownMenuSimple';
import cx from 'classnames';
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -225,7 +225,6 @@ export const QueryV2 = forwardRef(function QueryV2(
{isMultiQueryAllowed && (
<DropdownMenuSimple
className="query-actions-dropdown"
menu={{
items: [
{

View File

@@ -146,7 +146,7 @@
}
// Hovering the checkbox reveals the "Toggle" action.
.check-box:hover ~ .checkbox-value-section .toggle-btn {
[data-slot='checkbox']:hover ~ .checkbox-value-section .toggle-btn {
display: flex;
opacity: 1;
transform: translateX(0);

View File

@@ -18,7 +18,9 @@ import LogsQuickFilterEmptyState from './LogsQuickFilterEmptyState';
import useActiveQueryIndex from 'components/QuickFilters/hooks/useActiveQueryIndex';
import useCheckboxDisclosure from './useCheckboxDisclosure';
import useCheckboxFilterActions from './useCheckboxFilterActions';
import useCheckboxFilterState from './useCheckboxFilterState';
import useCheckboxFilterState, {
FILTER_DISABLED_REASON,
} from './useCheckboxFilterState';
import useCheckboxFilterValues from './useCheckboxFilterValues';
import './Checkbox.styles.scss';
@@ -132,6 +134,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
value={value}
checked={currentFilterState[value]}
disabled={isFilterDisabled}
disabledTooltip={FILTER_DISABLED_REASON}
title={filter.title}
onlyButtonLabel={
isSomeFilterPresentForCurrentAttribute

View File

@@ -2,12 +2,13 @@ import { Button } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Tooltip } from '@signozhq/ui/tooltip';
interface CheckboxValueRowProps {
value: string;
checked: boolean;
disabled: boolean;
disabledTooltip?: string;
title: string;
onlyButtonLabel: string;
customRendererForValue?: (value: string) => JSX.Element;
@@ -19,6 +20,7 @@ function CheckboxValueRow({
value,
checked,
disabled,
disabledTooltip,
title,
onlyButtonLabel,
customRendererForValue,
@@ -28,10 +30,11 @@ function CheckboxValueRow({
return (
<div className="value">
<Checkbox
color="primary"
disabledTooltip={disabledTooltip}
onChange={(isChecked): void => onCheckboxChange(isChecked === true)}
value={checked}
disabled={disabled}
className="check-box"
/>
<div
@@ -47,11 +50,11 @@ function CheckboxValueRow({
{customRendererForValue ? (
customRendererForValue(value)
) : (
<TooltipSimple title={String(value)} side="top" align="start">
<Tooltip title={String(value)} side="top" align="start">
<Typography.Text className="value-string" truncate={1}>
{String(value)}
</Typography.Text>
</TooltipSimple>
</Tooltip>
)}
<div className="value-actions">
<Button type="text" className="only-btn">
@@ -67,6 +70,7 @@ function CheckboxValueRow({
}
CheckboxValueRow.defaultProps = {
disabledTooltip: undefined,
customRendererForValue: undefined,
};

View File

@@ -17,6 +17,9 @@ interface UseCheckboxFilterStateReturn {
isMultipleValuesTrueForTheKey: boolean;
}
export const FILTER_DISABLED_REASON =
'This attribute is used more than once in the filter bar';
/**
* Reads the active query and derives the per-value checked state for this
* attribute, whether the filter is disabled (same key used more than once in

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Tooltip } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import { ChevronDown, ChevronRight, Search, Undo2 } from '@signozhq/icons';
@@ -62,13 +62,7 @@ export function CheckboxFilterV2Header({
) : (
<ChevronRight size={13} cursor="pointer" />
)}
{isTitleTruncated ? (
<TooltipSimple title={title} delayDuration={400}>
{titleText}
</TooltipSimple>
) : (
titleText
)}
<Tooltip title={isTitleTruncated ? title : undefined}>{titleText}</Tooltip>
</section>
{isOpen && (
<section className={classNames(styles.rightAction, actionsClassName)}>

View File

@@ -3,6 +3,7 @@ import {
CheckedState,
} from 'components/QuickFilters/types';
import { FILTER_DISABLED_REASON } from '../useCheckboxFilterState';
import { CheckboxFilterV2ValueRow } from './CheckboxFilterV2ValueRow';
import { SectionDivider } from './SectionDivider';
import { Section } from './useSectionedValues';
@@ -84,6 +85,7 @@ export function CheckboxFilterV2Section(
value={value}
checkedState={checkedState}
disabled={isFilterDisabled}
disabledTooltip={FILTER_DISABLED_REASON}
title={filter.title}
badge={badge}
onlyButtonLabel={

View File

@@ -37,53 +37,31 @@
align-items: center;
justify-items: end;
--button-height: 21px;
--button-padding: var(--spacing-5);
// Stack badge / only / toggle in a single cell so the crossfade overlaps
// instead of laying them side-by-side mid-transition.
> * {
grid-area: 1 / 1;
}
}
.badge {
opacity: 1;
transition:
opacity 0.16s ease,
display 0.16s allow-discrete;
}
.onlyButton {
display: none;
align-items: center;
justify-content: center;
opacity: 0;
transform: translateX(4px);
transition:
opacity 0.16s ease,
transform 0.16s ease,
display 0.16s allow-discrete;
--button-height: 21px;
--button-padding: var(--spacing-5);
&:hover {
background-color: unset;
> [data-action='badge'] {
opacity: 1;
transition:
opacity 0.16s ease,
display 0.16s allow-discrete;
}
}
.toggleButton {
display: none;
align-items: center;
justify-content: center;
opacity: 0;
transform: translateX(4px);
transition:
opacity 0.16s ease,
transform 0.16s ease,
display 0.16s allow-discrete;
--button-height: 21px;
--button-padding: var(--spacing-5);
&:hover {
background-color: unset;
> [data-action='only'],
> [data-action='toggle'] {
display: none;
opacity: 0;
transform: translateX(4px);
transition:
opacity 0.16s ease,
transform 0.16s ease,
display 0.16s allow-discrete;
}
}
@@ -94,19 +72,15 @@
color: var(--l3-foreground);
}
.onlyButton {
cursor: not-allowed;
color: var(--l3-foreground);
}
.toggleButton {
[data-action='only'],
[data-action='toggle'] {
cursor: not-allowed;
color: var(--l3-foreground);
}
}
.valueButton:hover {
.onlyButton {
[data-action='only'] {
display: flex;
opacity: 1;
transform: translateX(0);
@@ -117,14 +91,14 @@
}
}
.badge {
[data-action='badge'] {
display: none;
opacity: 0;
}
}
.checkbox:hover ~ .valueButton {
.toggleButton {
[data-action='toggle'] {
display: flex;
opacity: 1;
transform: translateX(0);
@@ -135,16 +109,14 @@
}
}
.badge {
[data-action='badge'] {
display: none;
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.badge,
.onlyButton,
.toggleButton {
.actions > * {
transition: none;
}
}

View File

@@ -12,6 +12,7 @@ interface ValueRowProps {
value: string;
checkedState: CheckedState;
disabled: boolean;
disabledTooltip?: string;
title: string;
onlyButtonLabel: string;
customRendererForValue?: (value: string) => JSX.Element;
@@ -36,6 +37,7 @@ export function CheckboxFilterV2ValueRow({
value,
checkedState,
disabled,
disabledTooltip,
title,
onlyButtonLabel,
customRendererForValue,
@@ -54,6 +56,7 @@ export function CheckboxFilterV2ValueRow({
>
<div className={styles.checkbox}>
<Checkbox
disabledTooltip={disabledTooltip}
onChange={(isChecked): void =>
onCheckboxChange(isChecked === true, checkedState)
}
@@ -97,18 +100,18 @@ export function CheckboxFilterV2ValueRow({
<div className={styles.actions}>
{badge && (
<Badge
variant="outline"
variant="outlined"
color={badge.color}
className={styles.badge}
data-action="badge"
testId={`badge-${badge.key}`}
>
{badge.label}
</Badge>
)}
<Button variant="ghost" color="secondary" className={styles.onlyButton}>
<Button size="md" variant="ghost" color="secondary" data-action="only">
{onlyButtonLabel}
</Button>
<Button variant="ghost" color="secondary" className={styles.toggleButton}>
<Button size="md" variant="ghost" color="secondary" data-action="toggle">
Toggle
</Button>
</div>

View File

@@ -1,6 +1,5 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TooltipProvider } from '@signozhq/ui/tooltip';
import { CheckboxFilterV2Header } from '../CheckboxFilterV2Header';
@@ -158,11 +157,7 @@ describe('CheckboxFilterV2Header', () => {
it('shows the full name on hover when the title is truncated', async () => {
mockTitleWidths(200, 100);
const user = userEvent.setup();
render(
<TooltipProvider>
<CheckboxFilterV2Header {...defaultProps} />
</TooltipProvider>,
);
render(<CheckboxFilterV2Header {...defaultProps} />);
await user.hover(screen.getByText(defaultProps.title));
@@ -174,11 +169,7 @@ describe('CheckboxFilterV2Header', () => {
it('shows no tooltip when the title fits', async () => {
mockTitleWidths(100, 100);
const user = userEvent.setup();
render(
<TooltipProvider>
<CheckboxFilterV2Header {...defaultProps} />
</TooltipProvider>,
);
render(<CheckboxFilterV2Header {...defaultProps} />);
await user.hover(screen.getByText(defaultProps.title));

View File

@@ -64,7 +64,7 @@ describe('CheckboxFilterV2ValueRow', () => {
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
badge={{ key: 'related', label: 'Related', color: 'robin' }}
badge={{ key: 'related', label: 'Related', color: 'primary' }}
/>,
);

View File

@@ -9,7 +9,7 @@ export enum SectionType {
export interface BadgeConfig {
key: string;
label: string;
color: 'robin' | 'warning' | 'secondary';
color: 'primary' | 'warning' | 'secondary';
}
export interface ItemConfig {

View File

@@ -1,8 +0,0 @@
.iconBtn {
display: flex;
align-items: center;
justify-content: center;
padding: 2px;
min-width: 24px;
height: 24px;
}

View File

@@ -2,10 +2,6 @@ import { ReactNode } from 'react';
import { Button } from '@signozhq/ui/button';
import { Tooltip } from 'antd';
import classNames from 'classnames';
import styles from './SectionActionButton.module.scss';
interface SectionActionButtonProps {
icon: ReactNode;
tooltip: string;
@@ -23,21 +19,21 @@ export function SectionActionButton({
}: SectionActionButtonProps): JSX.Element {
return (
<Tooltip title={tooltip}>
<Button
variant="link"
color="secondary"
size="sm"
className={classNames(styles.iconBtn, className)}
onMouseDown={(e): void => e.preventDefault()}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
onClick();
}}
data-testid={testId}
>
{icon}
</Button>
<span className={className} onMouseDown={(e): void => e.preventDefault()}>
<Button
variant="link"
color="secondary"
size="sm"
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
onClick();
}}
testId={testId}
>
{icon}
</Button>
</span>
</Tooltip>
);
}

View File

@@ -2,8 +2,6 @@
display: flex;
flex-direction: row;
position: relative;
flex: 1;
min-height: 0;
.quick-filters-settings-container {
flex: 0 0 0;
@@ -108,24 +106,6 @@
.sync-icon {
cursor: pointer;
}
.right-action-icon-container {
position: relative;
display: flex;
padding: 2px;
background-color: var(--l1-background);
.settings-icon {
height: 14px;
width: 14px;
cursor: pointer;
}
&.active,
&:hover {
background: var(--l2-background);
}
}
}
}

View File

@@ -232,48 +232,50 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
<section className="right-actions">
<Tooltip title="Reset All">
<Button
size="md"
variant="link"
color="secondary"
icon
aria-label="Reset All"
className="right-action-icon-container"
onClick={handleReset}
prefix={<RefreshCw className="sync-icon" size="md" />}
/>
>
<RefreshCw className="sync-icon" size="md" />
</Button>
</Tooltip>
{showFilterCollapse && (
<Tooltip title="Collapse Filters">
<Button
size="md"
variant="link"
color="secondary"
icon
aria-label="Collapse Filters"
className="right-action-icon-container"
onClick={handleFilterVisibilityChange}
prefix={<ArrowUpToLine style={{ rotate: '270deg' }} size="md" />}
/>
>
<ArrowUpToLine style={{ rotate: '270deg' }} size="md" />
</Button>
</Tooltip>
)}
{isDynamicFilters && (
<AuthZButton
size="md"
checks={QuickFilterManagePermissions}
variant="link"
color="secondary"
icon
aria-label="Settings"
className={classNames('right-action-icon-container', {
active: isSettingsOpen,
})}
onClick={(): void => setIsSettingsOpen(true)}
testId="settings-icon-container"
prefix={
<Tooltip title="Settings" open={isSettingsDisabled ? false : undefined}>
<SettingsIcon
className="settings-icon"
data-testid="settings-icon"
width={14}
height={14}
/>
</Tooltip>
}
/>
>
<Tooltip title="Settings" open={isSettingsDisabled ? false : undefined}>
<SettingsIcon
className="settings-icon"
data-testid="settings-icon"
width={14}
height={14}
/>
</Tooltip>
</AuthZButton>
)}
</section>
);
@@ -284,7 +286,8 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
<div className="api-quick-filters-header">
<Typography.Text>Show IP addresses</Typography.Text>
<Switch
style={{ marginLeft: 'auto' }}
color="primary"
textPlacement="right"
value={showIP ?? true}
onChange={(checked): void => {
logEvent('API Monitoring: Show IP addresses clicked', {

View File

@@ -1,33 +0,0 @@
// The one `overflow: hidden` in the chain. Ancestors (RouteTab, AppLayout)
// only hand height down; each pane below owns its own scroll.
.layout {
display: flex;
flex: 1;
height: 100%;
min-height: 0;
overflow: hidden;
}
// Positioned so overlays (settings drawer) paint above the content pane
// without changing this pane's layout width.
.filters {
width: 280px;
flex-shrink: 0;
display: flex;
flex-direction: column;
min-height: 0;
position: relative;
overflow: visible;
z-index: 2;
}
// Bounded box for the OverlayScrollbar inside it (`.overlay-scrollbar` is
// `height: 100%`), which owns the scrolling.
.content {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}

View File

@@ -1,54 +0,0 @@
import { ComponentProps, ReactNode } from 'react';
import cx from 'classnames';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import QuickFilters from '../QuickFilters';
import styles from './QuickFiltersLayout.module.scss';
// Same optionality as `<QuickFilters />` in JSX (honours its defaultProps).
type QuickFiltersElementProps = JSX.LibraryManagedAttributes<
typeof QuickFilters,
ComponentProps<typeof QuickFilters>
>;
export interface QuickFiltersLayoutProps {
quickFilterProps: QuickFiltersElementProps;
showFilters: boolean;
className?: string;
contentClassName?: string;
testId?: string;
children: ReactNode;
}
function QuickFiltersLayout({
quickFilterProps,
showFilters,
className,
contentClassName,
testId,
children,
}: QuickFiltersLayoutProps): JSX.Element {
return (
<div className={cx(styles.layout, className)} data-testid={testId}>
{showFilters && (
<aside
className={styles.filters}
data-testid="quick-filters-layout-filters"
>
<QuickFilters {...quickFilterProps} />
</aside>
)}
<section
className={cx(styles.content, contentClassName)}
data-testid="quick-filters-layout-content"
>
<OverlayScrollbar>
<div>{children}</div>
</OverlayScrollbar>
</section>
</div>
);
}
export default QuickFiltersLayout;

View File

@@ -1,79 +0,0 @@
import { render, screen } from 'tests/test-utils';
import { QuickFiltersSource } from '../../types';
import QuickFiltersLayout from '../QuickFiltersLayout';
jest.mock('../QuickFiltersLayout.module.scss', () => ({
__esModule: true,
default: {
layout: 'layout',
filters: 'filters',
content: 'content',
},
}));
jest.mock('../../QuickFilters', () => ({
__esModule: true,
default: ({ source }: { source: string }): JSX.Element => (
<div data-testid="quick-filters">{source}</div>
),
}));
const quickFilterProps = {
source: QuickFiltersSource.TRACES_EXPLORER,
handleFilterVisibilityChange: jest.fn(),
};
describe('QuickFiltersLayout', () => {
it('renders QuickFilters with the given props inside the filters pane', () => {
render(
<QuickFiltersLayout showFilters quickFilterProps={quickFilterProps}>
<div>content</div>
</QuickFiltersLayout>,
);
const filtersPane = screen.getByTestId('quick-filters-layout-filters');
expect(filtersPane).toContainElement(screen.getByTestId('quick-filters'));
expect(screen.getByTestId('quick-filters')).toHaveTextContent(
QuickFiltersSource.TRACES_EXPLORER,
);
expect(screen.getByTestId('quick-filters-layout-content')).toHaveTextContent(
'content',
);
});
it('does not render the filters pane when showFilters is false', () => {
render(
<QuickFiltersLayout showFilters={false} quickFilterProps={quickFilterProps}>
<div>content</div>
</QuickFiltersLayout>,
);
expect(
screen.queryByTestId('quick-filters-layout-filters'),
).not.toBeInTheDocument();
expect(screen.queryByTestId('quick-filters')).not.toBeInTheDocument();
expect(screen.getByText('content')).toBeInTheDocument();
});
it('merges classNames onto the root and content panes', () => {
render(
<QuickFiltersLayout
showFilters
quickFilterProps={quickFilterProps}
className="page-root"
contentClassName="page-content"
testId="page"
>
<div>content</div>
</QuickFiltersLayout>,
);
const root = screen.getByTestId('page');
expect(root).toHaveClass('layout', 'page-root');
expect(screen.getByTestId('quick-filters-layout-content')).toHaveClass(
'content',
'page-content',
);
});
});

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