Compare commits

...

3 Commits

Author SHA1 Message Date
SagarRajput-7
3d75731a39 feat: addressed comments 2026-04-15 16:16:19 +05:30
SagarRajput-7
43b6a14a1e Merge branch 'main' into dismissble-workspace-setting-callout 2026-04-15 15:39:48 +05:30
SagarRajput-7
09d2db297b feat: added info dismissible callout for the license row in workspace settings 2026-04-15 14:50:57 +05:30
5 changed files with 345 additions and 1 deletions

View File

@@ -37,4 +37,5 @@ export enum LOCALSTORAGE {
SHOW_FREQUENCY_CHART = 'SHOW_FREQUENCY_CHART',
DISSMISSED_COST_METER_INFO = 'DISMISSED_COST_METER_INFO',
DISMISSED_API_KEYS_DEPRECATION_BANNER = 'DISMISSED_API_KEYS_DEPRECATION_BANNER',
LICENSE_KEY_CALLOUT_DISMISSED = 'LICENSE_KEY_CALLOUT_DISMISSED',
}

View File

@@ -38,6 +38,7 @@ import {
} from 'types/api/settings/getRetention';
import { USER_ROLES } from 'types/roles';
import LicenseRowDismissibleCallout from './LicenseKeyRow/LicenseRowDismissibleCallout/LicenseRowDismissibleCallout';
import Retention from './Retention';
import StatusMessage from './StatusMessage';
import { ActionItemsContainer, ErrorText, ErrorTextContainer } from './styles';
@@ -683,7 +684,12 @@ function GeneralSettings({
{showCustomDomainSettings && activeLicense?.key && (
<div className="custom-domain-card-divider" />
)}
{activeLicense?.key && <LicenseKeyRow />}
{activeLicense?.key && (
<>
<LicenseKeyRow />
<LicenseRowDismissibleCallout />
</>
)}
</div>
)}

View File

@@ -0,0 +1,31 @@
.license-key-callout {
margin: var(--spacing-4) var(--spacing-6);
width: auto;
.license-key-callout__description {
display: flex;
align-items: baseline;
gap: var(--spacing-2);
min-width: 0;
flex-wrap: wrap;
font-size: 13px;
}
.license-key-callout__link {
display: inline-flex;
align-items: center;
padding: 1px 5px;
border-radius: 2px;
background: var(--callout-primary-background);
color: var(--callout-primary-description);
font-family: 'SF Mono', 'Fira Code', 'Fira Mono', monospace;
font-size: var(--paragraph-base-400-font-size);
text-decoration: none;
&:hover {
background: var(--callout-primary-border);
color: var(--callout-primary-icon);
text-decoration: none;
}
}
}

View File

@@ -0,0 +1,87 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { Callout } from '@signozhq/callout';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useAppContext } from 'providers/App/App';
import { USER_ROLES } from 'types/roles';
import './LicenseRowDismissible.styles.scss';
function LicenseRowDismissibleCallout(): JSX.Element | null {
const [isCalloutDismissed, setIsCalloutDismissed] = useState<boolean>(
() =>
getLocalStorageApi(LOCALSTORAGE.LICENSE_KEY_CALLOUT_DISMISSED) === 'true',
);
const { user, featureFlags } = useAppContext();
const { isCloudUser } = useGetTenantLicense();
const isAdmin = user.role === USER_ROLES.ADMIN;
const isEditor = user.role === USER_ROLES.EDITOR;
const isGatewayEnabled =
featureFlags?.find((feature) => feature.name === FeatureKeys.GATEWAY)
?.active || false;
// Service accounts are only accessible to admins
const hasServiceAccountsAccess = isAdmin;
// Ingestion settings are accessible to:
// - Cloud users when gateway is not enabled
// - Admin/Editor when gateway is enabled
const hasIngestionAccess =
(isCloudUser && !isGatewayEnabled) ||
(isGatewayEnabled && (isAdmin || isEditor));
const handleDismissCallout = (): void => {
setLocalStorageApi(LOCALSTORAGE.LICENSE_KEY_CALLOUT_DISMISSED, 'true');
setIsCalloutDismissed(true);
};
return !isCalloutDismissed ? (
<Callout
type="info"
size="small"
showIcon
dismissable
onClose={handleDismissCallout}
className="license-key-callout"
description={
<div className="license-key-callout__description">
This is <strong>NOT</strong> your ingestion or Service account key.
{(hasServiceAccountsAccess || hasIngestionAccess) && (
<>
{' '}
Find your{' '}
{hasServiceAccountsAccess && (
<Link
to={ROUTES.SERVICE_ACCOUNTS_SETTINGS}
className="license-key-callout__link"
>
Service account here
</Link>
)}
{hasServiceAccountsAccess && hasIngestionAccess && ' and '}
{hasIngestionAccess && (
<Link
to={ROUTES.INGESTION_SETTINGS}
className="license-key-callout__link"
>
Ingestion key here
</Link>
)}
.
</>
)}
</div>
}
/>
) : null;
}
export default LicenseRowDismissibleCallout;

View File

@@ -0,0 +1,219 @@
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { render, screen, userEvent } from 'tests/test-utils';
import { USER_ROLES } from 'types/roles';
import LicenseRowDismissibleCallout from '../LicenseRowDismissibleCallout';
const getDescription = (): HTMLElement =>
screen.getByText(
(_, el) =>
el?.classList?.contains('license-key-callout__description') ?? false,
);
const queryDescription = (): HTMLElement | null =>
screen.queryByText(
(_, el) =>
el?.classList?.contains('license-key-callout__description') ?? false,
);
jest.mock('hooks/useGetTenantLicense', () => ({
useGetTenantLicense: jest.fn(),
}));
const mockLicense = (isCloudUser: boolean): void => {
(useGetTenantLicense as jest.Mock).mockReturnValue({
isCloudUser,
isEnterpriseSelfHostedUser: !isCloudUser,
isCommunityUser: false,
isCommunityEnterpriseUser: false,
});
};
const renderCallout = (
role: string,
isCloudUser: boolean,
gatewayActive: boolean,
): void => {
mockLicense(isCloudUser);
render(
<LicenseRowDismissibleCallout />,
{},
{
role,
appContextOverrides: {
featureFlags: [
{
name: FeatureKeys.GATEWAY,
active: gatewayActive,
usage: 0,
usage_limit: -1,
route: '',
},
],
},
},
);
};
describe('LicenseRowDismissibleCallout', () => {
beforeEach(() => {
localStorage.clear();
jest.clearAllMocks();
});
describe('callout content per access level', () => {
it.each([
{
scenario: 'viewer, non-cloud, gateway off — base text only, no links',
role: USER_ROLES.VIEWER,
isCloudUser: false,
gatewayActive: false,
serviceAccountLink: false,
ingestionLink: false,
expectedText: 'This is NOT your ingestion or Service account key.',
},
{
scenario: 'admin, non-cloud, gateway off — service accounts link only',
role: USER_ROLES.ADMIN,
isCloudUser: false,
gatewayActive: false,
serviceAccountLink: true,
ingestionLink: false,
expectedText:
'This is NOT your ingestion or Service account key. Find your Service account here.',
},
{
scenario: 'viewer, cloud, gateway off — ingestion link only',
role: USER_ROLES.VIEWER,
isCloudUser: true,
gatewayActive: false,
serviceAccountLink: false,
ingestionLink: true,
expectedText:
'This is NOT your ingestion or Service account key. Find your Ingestion key here.',
},
{
scenario: 'admin, cloud, gateway off — both links',
role: USER_ROLES.ADMIN,
isCloudUser: true,
gatewayActive: false,
serviceAccountLink: true,
ingestionLink: true,
expectedText:
'This is NOT your ingestion or Service account key. Find your Service account here and Ingestion key here.',
},
{
scenario: 'admin, non-cloud, gateway on — both links',
role: USER_ROLES.ADMIN,
isCloudUser: false,
gatewayActive: true,
serviceAccountLink: true,
ingestionLink: true,
expectedText:
'This is NOT your ingestion or Service account key. Find your Service account here and Ingestion key here.',
},
{
scenario: 'editor, non-cloud, gateway on — ingestion link only',
role: USER_ROLES.EDITOR,
isCloudUser: false,
gatewayActive: true,
serviceAccountLink: false,
ingestionLink: true,
expectedText:
'This is NOT your ingestion or Service account key. Find your Ingestion key here.',
},
])(
'$scenario',
({
role,
isCloudUser,
gatewayActive,
serviceAccountLink,
ingestionLink,
expectedText,
}) => {
renderCallout(role, isCloudUser, gatewayActive);
const description = getDescription();
expect(description).toBeInTheDocument();
expect(description).toHaveTextContent(expectedText);
if (serviceAccountLink) {
expect(
screen.getByRole('link', { name: /Service account here/ }),
).toBeInTheDocument();
} else {
expect(
screen.queryByRole('link', { name: /Service account here/ }),
).not.toBeInTheDocument();
}
if (ingestionLink) {
expect(
screen.getByRole('link', { name: /Ingestion key here/ }),
).toBeInTheDocument();
} else {
expect(
screen.queryByRole('link', { name: /Ingestion key here/ }),
).not.toBeInTheDocument();
}
},
);
});
describe('Link routing', () => {
it('should link to service accounts settings', () => {
renderCallout(USER_ROLES.ADMIN, false, false);
const link = screen.getByRole('link', {
name: /Service account here/,
}) as HTMLAnchorElement;
expect(link.getAttribute('href')).toBe(ROUTES.SERVICE_ACCOUNTS_SETTINGS);
});
it('should link to ingestion settings', () => {
renderCallout(USER_ROLES.VIEWER, true, false);
const link = screen.getByRole('link', {
name: /Ingestion key here/,
}) as HTMLAnchorElement;
expect(link.getAttribute('href')).toBe(ROUTES.INGESTION_SETTINGS);
});
});
describe('Dismissal functionality', () => {
it('should hide callout when dismiss button is clicked', async () => {
const user = userEvent.setup();
renderCallout(USER_ROLES.ADMIN, false, false);
expect(getDescription()).toBeInTheDocument();
await user.click(screen.getByRole('button'));
expect(queryDescription()).not.toBeInTheDocument();
});
it('should persist dismissal in localStorage', async () => {
const user = userEvent.setup();
renderCallout(USER_ROLES.ADMIN, false, false);
await user.click(screen.getByRole('button'));
expect(
localStorage.getItem(LOCALSTORAGE.LICENSE_KEY_CALLOUT_DISMISSED),
).toBe('true');
});
it('should not render when localStorage dismissal is set', () => {
localStorage.setItem(LOCALSTORAGE.LICENSE_KEY_CALLOUT_DISMISSED, 'true');
renderCallout(USER_ROLES.ADMIN, false, false);
expect(queryDescription()).not.toBeInTheDocument();
});
});
});