mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-15 16:30:27 +01:00
Compare commits
3 Commits
chore/json
...
dismissble
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d75731a39 | ||
|
|
43b6a14a1e | ||
|
|
09d2db297b |
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user