mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-19 08:20:34 +01:00
Compare commits
7 Commits
feat/cloud
...
no-auth-fe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5385986f5d | ||
|
|
df8b71ce25 | ||
|
|
2782a235cd | ||
|
|
1d341bb3fe | ||
|
|
7e4ab1aebf | ||
|
|
ce63f3bf53 | ||
|
|
e1c8b955d0 |
@@ -166,6 +166,8 @@ function createMockAppContext(
|
||||
userPreferences: [],
|
||||
hostsData: null,
|
||||
isLoggedIn: true,
|
||||
isNoAuthMode: false,
|
||||
isPreflightLoading: false,
|
||||
org: [{ createdAt: 0, id: 'org-id', displayName: 'Test Org' }],
|
||||
isFetchingUser: false,
|
||||
isFetchingActiveLicense: false,
|
||||
|
||||
@@ -59,6 +59,7 @@ function App(): JSX.Element {
|
||||
isLoggedIn: isLoggedInState,
|
||||
featureFlags,
|
||||
org,
|
||||
isPreflightLoading,
|
||||
} = useAppContext();
|
||||
const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
|
||||
const isAIAssistantEnabled = useIsAIAssistantEnabled();
|
||||
@@ -386,6 +387,10 @@ function App(): JSX.Element {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isCloudUser, isEnterpriseSelfHostedUser]);
|
||||
|
||||
if (isPreflightLoading) {
|
||||
return <Spinner tip="Loading..." />;
|
||||
}
|
||||
|
||||
// if the user is in logged in state
|
||||
if (isLoggedInState) {
|
||||
// if the setup calls are loading then return a spinner
|
||||
|
||||
@@ -144,18 +144,18 @@ const routes: AppRoutes[] = [
|
||||
// /trace-old serves V3 (URL-only access). Flip the two `component`
|
||||
// values back to release V3.
|
||||
{
|
||||
path: ROUTES.TRACE_DETAIL_OLD,
|
||||
path: ROUTES.TRACE_DETAIL,
|
||||
exact: true,
|
||||
component: TraceDetail,
|
||||
isPrivate: true,
|
||||
key: 'TRACE_DETAIL_OLD',
|
||||
key: 'TRACE_DETAIL',
|
||||
},
|
||||
{
|
||||
path: ROUTES.TRACE_DETAIL,
|
||||
path: ROUTES.TRACE_DETAIL_OLD,
|
||||
exact: true,
|
||||
component: TraceDetailV3,
|
||||
isPrivate: true,
|
||||
key: 'TRACE_DETAIL',
|
||||
key: 'TRACE_DETAIL_OLD',
|
||||
},
|
||||
{
|
||||
path: ROUTES.SETTINGS,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import axios from 'axios';
|
||||
import { getIsNoAuthMode } from 'utils/noAuthMode';
|
||||
|
||||
import { interceptorRejected } from '../index';
|
||||
|
||||
jest.mock('utils/noAuthMode', () => ({
|
||||
getIsNoAuthMode: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('api/v2/sessions/rotate/post', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('AppRoutes/utils', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../utils', () => ({
|
||||
Logout: jest.fn(),
|
||||
}));
|
||||
|
||||
// oxlint-disable-next-line typescript/no-require-imports typescript/no-var-requires
|
||||
const post = require('api/v2/sessions/rotate/post').default;
|
||||
// oxlint-disable-next-line typescript/no-require-imports typescript/no-var-requires
|
||||
const { Logout } = require('../utils');
|
||||
|
||||
describe('interceptorRejected — no-auth mode', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.spyOn(axios, 'isAxiosError').mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('does NOT call rotate or Logout when no-auth mode is enabled on 401', async () => {
|
||||
(getIsNoAuthMode as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const error = {
|
||||
isAxiosError: true,
|
||||
response: {
|
||||
status: 401,
|
||||
config: { url: '/dashboards', method: 'get' },
|
||||
},
|
||||
config: { url: '/dashboards', headers: {} },
|
||||
};
|
||||
|
||||
await interceptorRejected(error as any).catch(() => {});
|
||||
|
||||
expect(post).not.toHaveBeenCalled();
|
||||
expect(Logout).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('DOES attempt rotate when no-auth mode is disabled on 401', async () => {
|
||||
(getIsNoAuthMode as jest.Mock).mockReturnValue(false);
|
||||
(post as jest.Mock).mockResolvedValue({
|
||||
data: { accessToken: 'a', refreshToken: 'b' },
|
||||
});
|
||||
|
||||
const error = {
|
||||
isAxiosError: true,
|
||||
response: {
|
||||
status: 401,
|
||||
config: { url: '/dashboards', method: 'get' },
|
||||
},
|
||||
config: { url: '/dashboards', headers: {} },
|
||||
};
|
||||
|
||||
await interceptorRejected(error as any).catch(() => {});
|
||||
|
||||
expect(post).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import { Events } from 'constants/events';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { getBasePath } from 'utils/basePath';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
import { getIsNoAuthMode } from 'utils/noAuthMode';
|
||||
|
||||
import apiV1, { apiAlertManager, apiV2, apiV3, apiV4, apiV5 } from './apiV1';
|
||||
import { Logout } from './utils';
|
||||
@@ -108,7 +109,10 @@ export const interceptorRejected = async (
|
||||
if (axios.isAxiosError(value) && value.response) {
|
||||
const { response } = value;
|
||||
|
||||
const isNoAuthMode = getIsNoAuthMode();
|
||||
|
||||
if (
|
||||
!isNoAuthMode &&
|
||||
response.status === 401 &&
|
||||
// if the session rotate call or the create session errors out with 401 or the delete sessions call returns 401 then we do not retry!
|
||||
response.config.url !== '/sessions/rotate' &&
|
||||
@@ -140,16 +144,20 @@ export const interceptorRejected = async (
|
||||
return await Promise.resolve(reResponse);
|
||||
} catch (error) {
|
||||
if ((error as AxiosError)?.response?.status === 401) {
|
||||
Logout();
|
||||
void Logout();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Logout();
|
||||
void Logout();
|
||||
}
|
||||
}
|
||||
|
||||
if (response.status === 401 && response.config.url === '/sessions/rotate') {
|
||||
Logout();
|
||||
if (
|
||||
!isNoAuthMode &&
|
||||
response.status === 401 &&
|
||||
response.config.url === '/sessions/rotate'
|
||||
) {
|
||||
void Logout();
|
||||
}
|
||||
}
|
||||
return await Promise.reject(value);
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from 'api/generated/services/users';
|
||||
import { AxiosError } from 'axios';
|
||||
import { MemberRow } from 'components/MembersTable/MembersTable';
|
||||
import { NoAuthGuard } from 'components/NoAuthGuard';
|
||||
import RolesSelect, { useRoles } from 'components/RolesSelect';
|
||||
import SaveErrorItem from 'components/ServiceAccountDrawer/SaveErrorItem';
|
||||
import type { SaveError } from 'components/ServiceAccountDrawer/utils';
|
||||
@@ -613,39 +614,43 @@ function EditMemberDrawer({
|
||||
<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>
|
||||
<NoAuthGuard>
|
||||
<Button
|
||||
onClick={(): void => setShowDeleteConfirm(true)}
|
||||
disabled={isRootUser || isSelf}
|
||||
variant="link"
|
||||
color="destructive"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
{isInvited ? 'Revoke Invite' : 'Delete Member'}
|
||||
</Button>
|
||||
</NoAuthGuard>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
<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>
|
||||
<NoAuthGuard>
|
||||
<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>
|
||||
</NoAuthGuard>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -656,15 +661,17 @@ function EditMemberDrawer({
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
disabled={!isDirty || isSaving || isRootUser}
|
||||
onClick={handleSave}
|
||||
loading={isSaving}
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Member Details'}
|
||||
</Button>
|
||||
<NoAuthGuard>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
disabled={!isDirty || isSaving || isRootUser}
|
||||
onClick={handleSave}
|
||||
loading={isSaving}
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Member Details'}
|
||||
</Button>
|
||||
</NoAuthGuard>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.banner {
|
||||
height: var(--spacing-20);
|
||||
}
|
||||
18
frontend/src/components/NoAuthBanner/NoAuthBanner.tsx
Normal file
18
frontend/src/components/NoAuthBanner/NoAuthBanner.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { PersistedAnnouncementBanner } from '@signozhq/ui/announcement-banner';
|
||||
|
||||
import styles from './NoAuthBanner.module.scss';
|
||||
|
||||
export function NoAuthBanner(): JSX.Element {
|
||||
return (
|
||||
<PersistedAnnouncementBanner
|
||||
type="warning"
|
||||
storageKey="no-auth-banner-v1"
|
||||
testId="no-auth-banner"
|
||||
className={styles.banner}
|
||||
>
|
||||
No-auth mode: authentication is disabled, network is the trust boundary.
|
||||
</PersistedAnnouncementBanner>
|
||||
);
|
||||
}
|
||||
|
||||
export default NoAuthBanner;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
|
||||
import { NoAuthBanner } from '../NoAuthBanner';
|
||||
|
||||
describe('NoAuthBanner', () => {
|
||||
it('renders the no-auth message', () => {
|
||||
render(<NoAuthBanner />);
|
||||
expect(
|
||||
screen.getByText(/No-auth mode: authentication is disabled/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with the warning test id', () => {
|
||||
render(<NoAuthBanner />);
|
||||
expect(screen.getByTestId('no-auth-banner')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
46
frontend/src/components/NoAuthGuard/NoAuthGuard.tsx
Normal file
46
frontend/src/components/NoAuthGuard/NoAuthGuard.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
TooltipRoot,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@signozhq/ui/tooltip';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
export const DEFAULT_MESSAGE = 'Not available in no-auth mode';
|
||||
|
||||
interface NoAuthGuardProps {
|
||||
children: React.ReactElement;
|
||||
message?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function NoAuthGuard({
|
||||
children,
|
||||
message = DEFAULT_MESSAGE,
|
||||
disabled,
|
||||
}: NoAuthGuardProps): JSX.Element {
|
||||
const { isNoAuthMode } = useAppContext();
|
||||
|
||||
if (!isNoAuthMode) {
|
||||
return disabled ? React.cloneElement(children, { disabled: true }) : children;
|
||||
}
|
||||
|
||||
const disabledChild = React.cloneElement(children, { disabled: true });
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipRoot>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
data-no-auth-trigger
|
||||
style={{ display: 'inline-flex', cursor: 'not-allowed' }}
|
||||
>
|
||||
{disabledChild}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{message}</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import { render } from 'tests/test-utils';
|
||||
|
||||
import { DEFAULT_MESSAGE, NoAuthGuard } from '..';
|
||||
|
||||
describe('NoAuthGuard', () => {
|
||||
it('renders children unchanged when isNoAuthMode is false', () => {
|
||||
const { getByRole } = render(
|
||||
<NoAuthGuard>
|
||||
<button type="button">Action</button>
|
||||
</NoAuthGuard>,
|
||||
undefined,
|
||||
{ appContextOverrides: { isNoAuthMode: false } },
|
||||
);
|
||||
expect(getByRole('button', { name: 'Action' })).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('does not intercept onClick when isNoAuthMode is false', () => {
|
||||
const handleClick = jest.fn();
|
||||
const { getByRole } = render(
|
||||
<NoAuthGuard>
|
||||
<button type="button" onClick={handleClick}>
|
||||
Action
|
||||
</button>
|
||||
</NoAuthGuard>,
|
||||
undefined,
|
||||
{ appContextOverrides: { isNoAuthMode: false } },
|
||||
);
|
||||
getByRole('button', { name: 'Action' }).click();
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('disables children when isNoAuthMode is true', () => {
|
||||
const { getByRole } = render(
|
||||
<NoAuthGuard>
|
||||
<button type="button">Action</button>
|
||||
</NoAuthGuard>,
|
||||
undefined,
|
||||
{ appContextOverrides: { isNoAuthMode: true } },
|
||||
);
|
||||
expect(getByRole('button', { name: 'Action' })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('renders a tooltip trigger wrapper when isNoAuthMode is true', () => {
|
||||
const { container } = render(
|
||||
<NoAuthGuard>
|
||||
<button type="button">Action</button>
|
||||
</NoAuthGuard>,
|
||||
undefined,
|
||||
{ appContextOverrides: { isNoAuthMode: true } },
|
||||
);
|
||||
expect(
|
||||
container.querySelector('span[data-no-auth-trigger]'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('overrides existing disabled prop — no-auth always wins', () => {
|
||||
const { getByRole } = render(
|
||||
// eslint-disable-next-line react/button-has-type
|
||||
<NoAuthGuard>
|
||||
<button type="button" disabled={false}>
|
||||
Action
|
||||
</button>
|
||||
</NoAuthGuard>,
|
||||
undefined,
|
||||
{ appContextOverrides: { isNoAuthMode: true } },
|
||||
);
|
||||
expect(getByRole('button', { name: 'Action' })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('exports DEFAULT_MESSAGE as a non-empty string', () => {
|
||||
expect(typeof DEFAULT_MESSAGE).toBe('string');
|
||||
expect(DEFAULT_MESSAGE.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
1
frontend/src/components/NoAuthGuard/index.ts
Normal file
1
frontend/src/components/NoAuthGuard/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { DEFAULT_MESSAGE, NoAuthGuard } from './NoAuthGuard';
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { KeyRound, X } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Skeleton, Table } from 'antd';
|
||||
import { Skeleton, Table, Tooltip } from 'antd';
|
||||
import { DEFAULT_MESSAGE, NoAuthGuard } from 'components/NoAuthGuard';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import type { ColumnsType } from 'antd/es/table/interface';
|
||||
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
|
||||
@@ -33,6 +35,7 @@ interface KeysTabProps {
|
||||
interface BuildColumnsParams {
|
||||
isDisabled: boolean;
|
||||
accountId: string;
|
||||
isNoAuthMode: boolean;
|
||||
onRevokeClick: (keyId: string) => void;
|
||||
handleformatLastObservedAt: (
|
||||
lastObservedAt: Date | null | undefined,
|
||||
@@ -53,6 +56,7 @@ function formatExpiry(expiresAt: number): JSX.Element {
|
||||
function buildColumns({
|
||||
isDisabled,
|
||||
accountId,
|
||||
isNoAuthMode,
|
||||
onRevokeClick,
|
||||
handleformatLastObservedAt,
|
||||
}: BuildColumnsParams): ColumnsType<ServiceaccounttypesGettableFactorAPIKeyDTO> {
|
||||
@@ -110,28 +114,38 @@ function buildColumns({
|
||||
onClick: (e): void => e.stopPropagation(),
|
||||
style: { cursor: 'default' },
|
||||
}),
|
||||
render: (_, record): JSX.Element => (
|
||||
<AuthZTooltip
|
||||
checks={[
|
||||
buildAPIKeyDeletePermission(record.id),
|
||||
buildSADetachPermission(accountId),
|
||||
]}
|
||||
enabled={!isDisabled && !!accountId}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="destructive"
|
||||
disabled={isDisabled}
|
||||
onClick={(): void => {
|
||||
onRevokeClick(record.id);
|
||||
}}
|
||||
className="keys-tab__revoke-btn"
|
||||
render: (_, record): JSX.Element => {
|
||||
const tooltipTitle = isDisabled
|
||||
? 'Service account disabled'
|
||||
: isNoAuthMode
|
||||
? DEFAULT_MESSAGE
|
||||
: 'Revoke Key';
|
||||
return (
|
||||
<AuthZTooltip
|
||||
checks={[
|
||||
buildAPIKeyDeletePermission(record.id),
|
||||
buildSADetachPermission(accountId),
|
||||
]}
|
||||
enabled={!isDisabled && !!accountId}
|
||||
>
|
||||
<X size={12} />
|
||||
</Button>
|
||||
</AuthZTooltip>
|
||||
),
|
||||
<Tooltip title={tooltipTitle}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="destructive"
|
||||
disabled={isDisabled || isNoAuthMode}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
onRevokeClick(record.id);
|
||||
}}
|
||||
className="keys-tab__revoke-btn"
|
||||
>
|
||||
<X size={12} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</AuthZTooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -158,6 +172,7 @@ function KeysTab({
|
||||
parseAsString.withDefault(''),
|
||||
);
|
||||
const editKey = keys.find((k) => k.id === editKeyId) ?? null;
|
||||
const { isNoAuthMode } = useAppContext();
|
||||
|
||||
const handleformatLastObservedAt = useCallback(
|
||||
(lastObservedAt: Date | null | undefined): string =>
|
||||
@@ -177,10 +192,17 @@ function KeysTab({
|
||||
buildColumns({
|
||||
isDisabled,
|
||||
accountId,
|
||||
isNoAuthMode,
|
||||
onRevokeClick,
|
||||
handleformatLastObservedAt,
|
||||
}),
|
||||
[isDisabled, accountId, onRevokeClick, handleformatLastObservedAt],
|
||||
[
|
||||
isDisabled,
|
||||
accountId,
|
||||
isNoAuthMode,
|
||||
onRevokeClick,
|
||||
handleformatLastObservedAt,
|
||||
],
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
@@ -210,16 +232,18 @@ function KeysTab({
|
||||
checks={[APIKeyCreatePermission, buildSAAttachPermission(accountId)]}
|
||||
enabled={!isDisabled && !!accountId}
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
color="primary"
|
||||
onClick={async (): Promise<void> => {
|
||||
await setIsAddKeyOpen(true);
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
+ Add your first key
|
||||
</Button>
|
||||
<NoAuthGuard>
|
||||
<Button
|
||||
variant="link"
|
||||
color="primary"
|
||||
onClick={async (): Promise<void> => {
|
||||
await setIsAddKeyOpen(true);
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
+ Add your first key
|
||||
</Button>
|
||||
</NoAuthGuard>
|
||||
</AuthZTooltip>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -49,6 +49,7 @@ import APIError from 'types/api/error';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
|
||||
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
|
||||
import { NoAuthGuard } from 'components/NoAuthGuard';
|
||||
import AddKeyModal from './AddKeyModal';
|
||||
import DeleteAccountModal from './DeleteAccountModal';
|
||||
import KeysTab from './KeysTab';
|
||||
@@ -436,18 +437,20 @@ function ServiceAccountDrawer({
|
||||
]}
|
||||
enabled={!isDeleted && !!selectedAccountId}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
disabled={isDeleted}
|
||||
onClick={(): void => {
|
||||
void setIsAddKeyOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus size={12} />
|
||||
Add Key
|
||||
</Button>
|
||||
<NoAuthGuard>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
disabled={isDeleted}
|
||||
onClick={(): void => {
|
||||
void setIsAddKeyOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus size={12} />
|
||||
Add Key
|
||||
</Button>
|
||||
</NoAuthGuard>
|
||||
</AuthZTooltip>
|
||||
)}
|
||||
</div>
|
||||
@@ -550,16 +553,18 @@ function ServiceAccountDrawer({
|
||||
checks={[buildSADeletePermission(selectedAccountId ?? '')]}
|
||||
enabled={!!selectedAccountId}
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
color="destructive"
|
||||
onClick={(): void => {
|
||||
void setIsDeleteOpen(true);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
Delete Service Account
|
||||
</Button>
|
||||
<NoAuthGuard>
|
||||
<Button
|
||||
variant="link"
|
||||
color="destructive"
|
||||
onClick={(): void => {
|
||||
void setIsDeleteOpen(true);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
Delete Service Account
|
||||
</Button>
|
||||
</NoAuthGuard>
|
||||
</AuthZTooltip>
|
||||
)}
|
||||
{!isDeleted && (
|
||||
@@ -568,15 +573,17 @@ function ServiceAccountDrawer({
|
||||
<X size={14} />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
loading={isSaving}
|
||||
disabled={!isDirty}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
<NoAuthGuard>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
loading={isSaving}
|
||||
disabled={!isDirty}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</NoAuthGuard>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useQueryClient } from 'react-query';
|
||||
import { Button } from 'antd';
|
||||
import type { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import deleteChannel from 'api/channels/delete';
|
||||
import { NoAuthGuard } from 'components/NoAuthGuard';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
function Delete({ notifications, id }: DeleteProps): JSX.Element {
|
||||
@@ -35,14 +36,16 @@ function Delete({ notifications, id }: DeleteProps): JSX.Element {
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
type="link"
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<NoAuthGuard>
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
type="link"
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</NoAuthGuard>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import listUserPreferences from 'api/v1/user/preferences/list';
|
||||
import updateUserPreferenceAPI from 'api/v1/user/preferences/name/update';
|
||||
import Header from 'components/Header/Header';
|
||||
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||
import NoAuthBanner from 'components/NoAuthBanner/NoAuthBanner';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { ORG_PREFERENCES } from 'constants/orgPreferences';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -62,7 +63,7 @@ const homeInterval = 30 * 60 * 1000;
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export default function Home(): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
const { user, isNoAuthMode } = useAppContext();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
@@ -196,7 +197,7 @@ export default function Home(): JSX.Element {
|
||||
const { mutate: updateUserPreference } = useMutation(updateUserPreferenceAPI, {
|
||||
onSuccess: () => {
|
||||
setUpdatingUserPreferences(false);
|
||||
refetchUserPreferences();
|
||||
void refetchUserPreferences();
|
||||
},
|
||||
onError: () => {
|
||||
setUpdatingUserPreferences(false);
|
||||
@@ -204,7 +205,7 @@ export default function Home(): JSX.Element {
|
||||
});
|
||||
|
||||
const handleWillDoThisLater = (): void => {
|
||||
logEvent('Welcome Checklist: Will do this later clicked', {});
|
||||
void logEvent('Welcome Checklist: Will do this later clicked', {});
|
||||
setUpdatingUserPreferences(true);
|
||||
|
||||
updateUserPreference({
|
||||
@@ -271,11 +272,12 @@ export default function Home(): JSX.Element {
|
||||
}, [metricsOnboardingData, handleUpdateChecklistDoneItem]);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent('Homepage: Visited', {});
|
||||
void logEvent('Homepage: Visited', {});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="home-container">
|
||||
{isNoAuthMode && <NoAuthBanner />}
|
||||
<div className="sticky-header">
|
||||
<Header
|
||||
leftComponent={
|
||||
@@ -298,9 +300,9 @@ export default function Home(): JSX.Element {
|
||||
autoAdjustOverflow
|
||||
onOpenChange={(visible): void => {
|
||||
if (visible) {
|
||||
logEvent('Welcome Checklist: Expanded', {});
|
||||
void logEvent('Welcome Checklist: Expanded', {});
|
||||
} else {
|
||||
logEvent('Welcome Checklist: Minimized', {});
|
||||
void logEvent('Welcome Checklist: Minimized', {});
|
||||
}
|
||||
}}
|
||||
content={renderWelcomeChecklistModal()}
|
||||
@@ -353,7 +355,7 @@ export default function Home(): JSX.Element {
|
||||
className="active-ingestion-card-actions"
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
void logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Logs',
|
||||
});
|
||||
safeNavigate(ROUTES.LOGS_EXPLORER, {
|
||||
@@ -362,7 +364,7 @@ export default function Home(): JSX.Element {
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
void logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Logs',
|
||||
});
|
||||
history.push(ROUTES.LOGS_EXPLORER);
|
||||
@@ -396,7 +398,7 @@ export default function Home(): JSX.Element {
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
void logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Traces',
|
||||
});
|
||||
safeNavigate(ROUTES.TRACES_EXPLORER, {
|
||||
@@ -405,7 +407,7 @@ export default function Home(): JSX.Element {
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
void logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Traces',
|
||||
});
|
||||
history.push(ROUTES.TRACES_EXPLORER);
|
||||
@@ -439,7 +441,7 @@ export default function Home(): JSX.Element {
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
void logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Metrics',
|
||||
});
|
||||
safeNavigate(ROUTES.METRICS_EXPLORER, {
|
||||
@@ -448,7 +450,7 @@ export default function Home(): JSX.Element {
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
void logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Metrics',
|
||||
});
|
||||
history.push(ROUTES.METRICS_EXPLORER);
|
||||
@@ -496,7 +498,7 @@ export default function Home(): JSX.Element {
|
||||
className="periscope-btn secondary"
|
||||
prefix={<Wrench size={14} />}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
void logEvent('Homepage: Explore clicked', {
|
||||
source: 'Logs',
|
||||
});
|
||||
safeNavigate(ROUTES.LOGS_EXPLORER, {
|
||||
@@ -513,7 +515,7 @@ export default function Home(): JSX.Element {
|
||||
className="periscope-btn secondary"
|
||||
prefix={<Wrench size={14} />}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
void logEvent('Homepage: Explore clicked', {
|
||||
source: 'Traces',
|
||||
});
|
||||
safeNavigate(ROUTES.TRACES_EXPLORER, {
|
||||
@@ -530,7 +532,7 @@ export default function Home(): JSX.Element {
|
||||
className="periscope-btn secondary"
|
||||
prefix={<Wrench size={14} />}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
void logEvent('Homepage: Explore clicked', {
|
||||
source: 'Metrics',
|
||||
});
|
||||
safeNavigate(ROUTES.METRICS_EXPLORER_EXPLORER, {
|
||||
@@ -569,7 +571,7 @@ export default function Home(): JSX.Element {
|
||||
className="periscope-btn secondary"
|
||||
prefix={<Plus size={14} />}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
void logEvent('Homepage: Explore clicked', {
|
||||
source: 'Dashboards',
|
||||
});
|
||||
safeNavigate(ROUTES.ALL_DASHBOARD, {
|
||||
@@ -614,7 +616,7 @@ export default function Home(): JSX.Element {
|
||||
className="periscope-btn secondary"
|
||||
prefix={<Plus size={14} />}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
void logEvent('Homepage: Explore clicked', {
|
||||
source: 'Alerts',
|
||||
});
|
||||
safeNavigate(ROUTES.ALERTS_NEW, {
|
||||
|
||||
@@ -70,6 +70,7 @@ import {
|
||||
TriangleAlert,
|
||||
X,
|
||||
} from '@signozhq/icons';
|
||||
import { NoAuthGuard } from 'components/NoAuthGuard';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import {
|
||||
@@ -1006,21 +1007,25 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
</div>
|
||||
</div>
|
||||
<div className="action-btn">
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
suffix={<PenLine size={14} />}
|
||||
aria-label="Edit ingestion key"
|
||||
onClick={onEditKey}
|
||||
/>
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
color="destructive"
|
||||
suffix={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
|
||||
onClick={onDeleteKey}
|
||||
/>
|
||||
<NoAuthGuard>
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
suffix={<PenLine size={14} />}
|
||||
aria-label="Edit ingestion key"
|
||||
onClick={onEditKey}
|
||||
/>
|
||||
</NoAuthGuard>
|
||||
<NoAuthGuard>
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
color="destructive"
|
||||
suffix={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
|
||||
onClick={onDeleteKey}
|
||||
/>
|
||||
</NoAuthGuard>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
@@ -1123,28 +1128,32 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
<div className="actions">
|
||||
{hasLimits(signalName) ? (
|
||||
<>
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
prefix={<PenLine size={14} />}
|
||||
aria-label={`Edit ${signalName} limit`}
|
||||
disabled={
|
||||
!!(activeAPIKey?.id === APIKey?.id && activeSignal)
|
||||
}
|
||||
onClick={onEditSignalLimit}
|
||||
/>
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
color="destructive"
|
||||
prefix={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
|
||||
aria-label={`Delete ${signalName} limit`}
|
||||
disabled={
|
||||
!!(activeAPIKey?.id === APIKey?.id && activeSignal)
|
||||
}
|
||||
onClick={onDeleteSignalLimit}
|
||||
/>
|
||||
<NoAuthGuard>
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
prefix={<PenLine size={14} />}
|
||||
aria-label={`Edit ${signalName} limit`}
|
||||
disabled={
|
||||
!!(activeAPIKey?.id === APIKey?.id && activeSignal)
|
||||
}
|
||||
onClick={onEditSignalLimit}
|
||||
/>
|
||||
</NoAuthGuard>
|
||||
<NoAuthGuard>
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
color="destructive"
|
||||
prefix={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
|
||||
aria-label={`Delete ${signalName} limit`}
|
||||
disabled={
|
||||
!!(activeAPIKey?.id === APIKey?.id && activeSignal)
|
||||
}
|
||||
onClick={onDeleteSignalLimit}
|
||||
/>
|
||||
</NoAuthGuard>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
@@ -1679,14 +1688,16 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="solid"
|
||||
className="add-new-ingestion-key-btn"
|
||||
prefix={<Plus size={14} />}
|
||||
onClick={showAddModal}
|
||||
>
|
||||
New Ingestion key
|
||||
</Button>
|
||||
<NoAuthGuard>
|
||||
<Button
|
||||
variant="solid"
|
||||
className="add-new-ingestion-key-btn"
|
||||
prefix={<Plus size={14} />}
|
||||
onClick={showAddModal}
|
||||
>
|
||||
New Ingestion key
|
||||
</Button>
|
||||
</NoAuthGuard>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useListUsers } from 'api/generated/services/users';
|
||||
import EditMemberDrawer from 'components/EditMemberDrawer/EditMemberDrawer';
|
||||
import InviteMembersModal from 'components/InviteMembersModal/InviteMembersModal';
|
||||
import MembersTable, { MemberRow } from 'components/MembersTable/MembersTable';
|
||||
import { NoAuthGuard } from 'components/NoAuthGuard';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { toISOString } from 'utils/app';
|
||||
|
||||
@@ -21,7 +22,6 @@ const PAGE_SIZE = 20;
|
||||
function MembersSettings(): JSX.Element {
|
||||
const history = useHistory();
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const pageParam = parseInt(urlQuery.get('page') ?? '1', 10);
|
||||
const currentPage = Number.isNaN(pageParam) || pageParam < 1 ? 1 : pageParam;
|
||||
|
||||
@@ -146,7 +146,7 @@ function MembersSettings(): JSX.Element {
|
||||
: `Deleted ⎯ ${deletedCount}`;
|
||||
|
||||
const handleInviteComplete = useCallback((): void => {
|
||||
refetchUsers();
|
||||
void refetchUsers();
|
||||
}, [refetchUsers]);
|
||||
|
||||
const handleRowClick = useCallback((member: MemberRow): void => {
|
||||
@@ -158,7 +158,7 @@ function MembersSettings(): JSX.Element {
|
||||
}, []);
|
||||
|
||||
const handleMemberEditComplete = useCallback((): void => {
|
||||
refetchUsers();
|
||||
void refetchUsers();
|
||||
}, [refetchUsers]);
|
||||
|
||||
return (
|
||||
@@ -201,14 +201,16 @@ function MembersSettings(): JSX.Element {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={(): void => setIsInviteModalOpen(true)}
|
||||
>
|
||||
<Plus size={12} />
|
||||
Invite member
|
||||
</Button>
|
||||
<NoAuthGuard>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={(): void => setIsInviteModalOpen(true)}
|
||||
>
|
||||
<Plus size={12} />
|
||||
Invite member
|
||||
</Button>
|
||||
</NoAuthGuard>
|
||||
</div>
|
||||
</div>
|
||||
<MembersTable
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
updateMyPassword,
|
||||
useUpdateMyUserV2,
|
||||
} from 'api/generated/services/users';
|
||||
import { NoAuthGuard } from 'components/NoAuthGuard';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { Check, FileTerminal, Mail, User } from '@signozhq/icons';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
@@ -80,10 +81,10 @@ function UserInfo(): JSX.Element {
|
||||
currentPassword === updatePassword;
|
||||
|
||||
const onSaveHandler = async (): Promise<void> => {
|
||||
logEvent('Account Settings: Name Updated', {
|
||||
void logEvent('Account Settings: Name Updated', {
|
||||
name: changedName,
|
||||
});
|
||||
logEvent(
|
||||
void logEvent(
|
||||
'Account Settings: Name Updated',
|
||||
{
|
||||
name: changedName,
|
||||
@@ -144,14 +145,16 @@ function UserInfo(): JSX.Element {
|
||||
Update name
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<FileTerminal size={16} />}
|
||||
onClick={(): void => setIsResetPasswordModalOpen(true)}
|
||||
>
|
||||
Reset password
|
||||
</Button>
|
||||
<NoAuthGuard>
|
||||
<Button
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<FileTerminal size={16} />}
|
||||
onClick={(): void => setIsResetPasswordModalOpen(true)}
|
||||
>
|
||||
Reset password
|
||||
</Button>
|
||||
</NoAuthGuard>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Checkbox } from '@signozhq/ui/checkbox';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Input as AntdInput } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ArrowRight } from '@signozhq/icons';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import { OnboardingQuestionHeader } from '../OnboardingQuestionHeader';
|
||||
|
||||
@@ -33,31 +32,11 @@ const interestedInOptions: Record<string, string> = {
|
||||
openSourceTooling: 'Prefer open-source tooling',
|
||||
};
|
||||
|
||||
function seededShuffle<T>(array: T[], seed: string): T[] {
|
||||
const result = [...array];
|
||||
|
||||
let num = 0;
|
||||
for (let i = 0; i < seed.length; i++) {
|
||||
num = Math.imul(num + seed.charCodeAt(i), 2654435761);
|
||||
num = Math.abs(num);
|
||||
}
|
||||
|
||||
for (let i = result.length - 1; i > 0; i--) {
|
||||
num = Math.abs(Math.imul(num, 1664525) + 1013904223);
|
||||
const j = num % (i + 1);
|
||||
[result[i], result[j]] = [result[j], result[i]];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function AboutSigNozQuestions({
|
||||
signozDetails,
|
||||
setSignozDetails,
|
||||
onNext,
|
||||
}: AboutSigNozQuestionsProps): JSX.Element {
|
||||
const { versionData } = useAppContext();
|
||||
|
||||
const [interestInSignoz, setInterestInSignoz] = useState<string[]>(
|
||||
signozDetails?.interestInSignoz || [],
|
||||
);
|
||||
@@ -69,12 +48,6 @@ export function AboutSigNozQuestions({
|
||||
);
|
||||
const [isNextDisabled, setIsNextDisabled] = useState<boolean>(true);
|
||||
|
||||
const shuffledOptionKeys = useMemo(
|
||||
() =>
|
||||
seededShuffle(Object.keys(interestedInOptions), versionData?.version ?? ''),
|
||||
[versionData?.version],
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
if (
|
||||
discoverSignoz !== '' &&
|
||||
@@ -142,7 +115,7 @@ export function AboutSigNozQuestions({
|
||||
<div className="form-group">
|
||||
<div className="question">What got you interested in SigNoz?</div>
|
||||
<div className="checkbox-grid">
|
||||
{shuffledOptionKeys.map((option: string) => (
|
||||
{Object.keys(interestedInOptions).map((option: string) => (
|
||||
<div key={option} className="checkbox-item">
|
||||
<Checkbox
|
||||
id={`checkbox-${option}`}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { AxiosError } from 'axios';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { NoAuthGuard } from 'components/NoAuthGuard';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { ErrorV2Resp } from 'types/api';
|
||||
@@ -257,14 +258,16 @@ function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={onSubmitHandler}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
loading={isCreating || isUpdating}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
<NoAuthGuard>
|
||||
<Button
|
||||
onClick={onSubmitHandler}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
loading={isCreating || isUpdating}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</NoAuthGuard>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { NoAuthGuard } from 'components/NoAuthGuard';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { useUpdateAuthDomain } from 'api/generated/services/authdomains';
|
||||
import {
|
||||
@@ -65,7 +66,9 @@ function SSOEnforcementToggle({
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch disabled={isLoading} value={isChecked} onChange={onChangeHandler} />
|
||||
<NoAuthGuard>
|
||||
<Switch disabled={isLoading} value={isChecked} onChange={onChangeHandler} />
|
||||
</NoAuthGuard>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
|
||||
import { NoAuthGuard } from 'components/NoAuthGuard';
|
||||
import CopyToClipboard from 'periscope/components/CopyToClipboard';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
@@ -75,7 +76,7 @@ function AuthDomain(): JSX.Element {
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success('Domain deleted successfully');
|
||||
refetchAuthDomainListResponse();
|
||||
void refetchAuthDomainListResponse();
|
||||
hideDeleteModal();
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -153,20 +154,24 @@ function AuthDomain(): JSX.Element {
|
||||
width: 100,
|
||||
render: (_, record: AuthtypesGettableAuthDomainDTO): JSX.Element => (
|
||||
<section className="auth-domain-list-column-action">
|
||||
<Button
|
||||
className="auth-domain-list-action-link"
|
||||
onClick={(): void => setRecord(record)}
|
||||
variant="link"
|
||||
>
|
||||
Configure {SSOType.get(record.config?.ssoType || '')}
|
||||
</Button>
|
||||
<Button
|
||||
className="auth-domain-list-action-link delete"
|
||||
onClick={(): void => showDeleteModal(record)}
|
||||
variant="link"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<NoAuthGuard>
|
||||
<Button
|
||||
className="auth-domain-list-action-link"
|
||||
onClick={(): void => setRecord(record)}
|
||||
variant="link"
|
||||
>
|
||||
Configure {SSOType.get(record.config?.ssoType || '')}
|
||||
</Button>
|
||||
</NoAuthGuard>
|
||||
<NoAuthGuard>
|
||||
<Button
|
||||
className="auth-domain-list-action-link delete"
|
||||
onClick={(): void => showDeleteModal(record)}
|
||||
variant="link"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</NoAuthGuard>
|
||||
</section>
|
||||
),
|
||||
},
|
||||
@@ -178,17 +183,19 @@ function AuthDomain(): JSX.Element {
|
||||
<div className="auth-domain">
|
||||
<section className="auth-domain-header">
|
||||
<h3 className="auth-domain-title">Authenticated Domains</h3>
|
||||
<Button
|
||||
prefix={<Plus size="md" />}
|
||||
onClick={(): void => {
|
||||
setAddDomain(true);
|
||||
}}
|
||||
variant="solid"
|
||||
size="sm"
|
||||
color="primary"
|
||||
>
|
||||
Add Domain
|
||||
</Button>
|
||||
<NoAuthGuard>
|
||||
<Button
|
||||
prefix={<Plus size="md" />}
|
||||
onClick={(): void => {
|
||||
setAddDomain(true);
|
||||
}}
|
||||
variant="solid"
|
||||
size="sm"
|
||||
color="primary"
|
||||
>
|
||||
Add Domain
|
||||
</Button>
|
||||
</NoAuthGuard>
|
||||
</section>
|
||||
{formattedError && <ErrorContent error={formattedError} />}
|
||||
{!errorFetchingAuthDomainListResponse && (
|
||||
@@ -231,15 +238,16 @@ function AuthDomain(): JSX.Element {
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
prefix={<Trash2 size={16} />}
|
||||
onClick={handleDeleteDomain}
|
||||
className="delete-btn"
|
||||
loading={isLoading}
|
||||
>
|
||||
Delete Domain
|
||||
</Button>,
|
||||
<NoAuthGuard key="submit">
|
||||
<Button
|
||||
prefix={<Trash2 size={16} />}
|
||||
onClick={handleDeleteDomain}
|
||||
className="delete-btn"
|
||||
loading={isLoading}
|
||||
>
|
||||
Delete Domain
|
||||
</Button>
|
||||
</NoAuthGuard>,
|
||||
]}
|
||||
>
|
||||
<p className="delete-text">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Redirect, useHistory, useLocation } from 'react-router-dom';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { Trash2 } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
@@ -26,9 +26,7 @@ import type { AuthzResources } from '../utils';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { capitalize } from 'lodash-es';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||
import { RoleType } from 'types/roles';
|
||||
import { handleApiError, toAPIError } from 'utils/errorUtils';
|
||||
|
||||
@@ -54,9 +52,8 @@ function RoleDetailsPage(): JSX.Element {
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const { activeLicense, isFetchingActiveLicense } = useAppContext();
|
||||
|
||||
const authzResources: AuthzResources = permissionsType.data;
|
||||
const authzResources = permissionsType.data as unknown as AuthzResources;
|
||||
|
||||
// Extract roleId from URL pathname since useParams doesn't work in nested routing
|
||||
const roleIdMatch = pathname.match(ROLE_ID_REGEX);
|
||||
@@ -161,22 +158,6 @@ function RoleDetailsPage(): JSX.Element {
|
||||
},
|
||||
});
|
||||
|
||||
if (isFetchingActiveLicense) {
|
||||
return (
|
||||
<div className="role-details-page">
|
||||
<Skeleton
|
||||
active
|
||||
paragraph={{ rows: 8 }}
|
||||
className="role-details-skeleton"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeLicense?.status !== LicenseStatus.VALID) {
|
||||
return <Redirect to={ROUTES.ROLES_SETTINGS} />;
|
||||
}
|
||||
|
||||
if (!hasReadPermission && readPerms !== null) {
|
||||
return <PermissionDeniedFullPage permissionName="role:read" />;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
} from 'mocks-server/__mockdata__/roles';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
@@ -16,7 +15,6 @@ import {
|
||||
} from 'tests/test-utils';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import {
|
||||
invalidLicense,
|
||||
mockUseAuthZDenyAll,
|
||||
mockUseAuthZGrantAll,
|
||||
} from 'tests/authz-test-utils';
|
||||
@@ -232,28 +230,6 @@ describe('RoleDetailsPage', () => {
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('redirects to the roles list when license is not valid', async () => {
|
||||
render(
|
||||
<Switch>
|
||||
<Route path="/settings/roles/:roleId">
|
||||
<RoleDetailsPage />
|
||||
</Route>
|
||||
<Route path="/settings/roles" exact>
|
||||
<div data-testid="roles-list-redirect-target" />
|
||||
</Route>
|
||||
</Switch>,
|
||||
undefined,
|
||||
{
|
||||
initialRoute: `/settings/roles/${CUSTOM_ROLE_ID}`,
|
||||
appContextOverrides: { activeLicense: invalidLicense },
|
||||
},
|
||||
);
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('roles-list-redirect-target'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('permission side panel', () => {
|
||||
beforeEach(() => {
|
||||
// Both hooks mocked so data renders synchronously — no React Query scheduler or MSW round-trip.
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { ErrorType } from 'api/generatedAPIInstance';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { NoAuthGuard } from 'components/NoAuthGuard';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { handleApiError } from 'utils/errorUtils';
|
||||
|
||||
@@ -148,16 +149,17 @@ function CreateRoleModal({
|
||||
<X size={14} />
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={onSubmit}
|
||||
loading={isLoading}
|
||||
size="sm"
|
||||
>
|
||||
{isEditMode ? 'Save Changes' : 'Create Role'}
|
||||
</Button>,
|
||||
<NoAuthGuard key="submit">
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={onSubmit}
|
||||
loading={isLoading}
|
||||
size="sm"
|
||||
>
|
||||
{isEditMode ? 'Save Changes' : 'Create Role'}
|
||||
</Button>
|
||||
</NoAuthGuard>,
|
||||
]}
|
||||
destroyOnClose
|
||||
className="create-role-modal"
|
||||
|
||||
@@ -11,9 +11,7 @@ import { RoleListPermission } from 'hooks/useAuthZ/permissions/role.permissions'
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||
import { RoleType } from 'types/roles';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
|
||||
@@ -32,9 +30,6 @@ interface RolesListingTableProps {
|
||||
function RolesListingTable({
|
||||
searchQuery,
|
||||
}: RolesListingTableProps): JSX.Element {
|
||||
const { activeLicense } = useAppContext();
|
||||
const isValidLicense = activeLicense?.status === LicenseStatus.VALID;
|
||||
|
||||
const { permissions: listPerms, isLoading: isAuthZLoading } = useAuthZ([
|
||||
RoleListPermission,
|
||||
]);
|
||||
@@ -208,27 +203,19 @@ function RolesListingTable({
|
||||
const renderRow = (role: AuthtypesRoleDTO): JSX.Element => (
|
||||
<div
|
||||
key={role.id}
|
||||
className={`roles-table-row${isValidLicense ? ' roles-table-row--clickable' : ''}`}
|
||||
role={isValidLicense ? 'button' : undefined}
|
||||
tabIndex={isValidLicense ? 0 : undefined}
|
||||
onClick={
|
||||
isValidLicense
|
||||
? (): void => {
|
||||
if (role.id) {
|
||||
navigateToRole(role.id, role.name);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onKeyDown={
|
||||
isValidLicense
|
||||
? (e): void => {
|
||||
if ((e.key === 'Enter' || e.key === ' ') && role.id) {
|
||||
navigateToRole(role.id, role.name);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
className="roles-table-row roles-table-row--clickable"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(): void => {
|
||||
if (role.id) {
|
||||
navigateToRole(role.id, role.name);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if ((e.key === 'Enter' || e.key === ' ') && role.id) {
|
||||
navigateToRole(role.id, role.name);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="roles-table-cell roles-table-cell--name">
|
||||
{role.name ?? '—'}
|
||||
|
||||
@@ -4,8 +4,7 @@ import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
|
||||
import { RoleCreatePermission } from 'hooks/useAuthZ/permissions/role.permissions';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||
import { NoAuthGuard } from 'components/NoAuthGuard';
|
||||
|
||||
import CreateRoleModal from './RolesComponents/CreateRoleModal';
|
||||
import RolesListingTable from './RolesComponents/RolesListingTable';
|
||||
@@ -15,8 +14,6 @@ import './RolesSettings.styles.scss';
|
||||
function RolesSettings(): JSX.Element {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const { activeLicense } = useAppContext();
|
||||
const isValidLicense = activeLicense?.status === LicenseStatus.VALID;
|
||||
|
||||
return (
|
||||
<div className="roles-settings" data-testid="roles-settings">
|
||||
@@ -42,8 +39,8 @@ function RolesSettings(): JSX.Element {
|
||||
value={searchQuery}
|
||||
onChange={(e): void => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
{isValidLicense && (
|
||||
<AuthZTooltip checks={[RoleCreatePermission]}>
|
||||
<AuthZTooltip checks={[RoleCreatePermission]}>
|
||||
<NoAuthGuard>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
@@ -53,8 +50,8 @@ function RolesSettings(): JSX.Element {
|
||||
<Plus size={14} />
|
||||
Custom role
|
||||
</Button>
|
||||
</AuthZTooltip>
|
||||
)}
|
||||
</NoAuthGuard>
|
||||
</AuthZTooltip>
|
||||
</div>
|
||||
<RolesListingTable searchQuery={searchQuery} />
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { fireEvent, render, screen } from 'tests/test-utils';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import { invalidLicense, mockUseAuthZGrantAll } from 'tests/authz-test-utils';
|
||||
import { mockUseAuthZGrantAll } from 'tests/authz-test-utils';
|
||||
|
||||
import RolesSettings from '../RolesSettings';
|
||||
|
||||
@@ -176,26 +176,6 @@ describe('RolesSettings', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('hides the create button and disables row clicks when license is not valid', async () => {
|
||||
render(<RolesSettings />, undefined, {
|
||||
appContextOverrides: { activeLicense: invalidLicense },
|
||||
});
|
||||
|
||||
await expect(screen.findByText('signoz-admin')).resolves.toBeInTheDocument();
|
||||
|
||||
// Create button must be absent
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /custom role/i }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Rows must not carry the clickable class or button role
|
||||
const rows = document.querySelectorAll('.roles-table-row');
|
||||
rows.forEach((row) => {
|
||||
expect(row).not.toHaveClass('roles-table-row--clickable');
|
||||
expect(row.getAttribute('role')).not.toBe('button');
|
||||
});
|
||||
});
|
||||
|
||||
it('handles invalid dates gracefully by showing fallback', async () => {
|
||||
const invalidRole = {
|
||||
id: 'edge-0009',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
CoretypesResourceRefDTO,
|
||||
CoretypesObjectGroupDTO,
|
||||
CoretypesTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
@@ -7,7 +8,11 @@ import type {
|
||||
PermissionConfig,
|
||||
ResourceDefinition,
|
||||
} from '../PermissionSidePanel/PermissionSidePanel.types';
|
||||
import type { AuthzResources } from '../utils';
|
||||
|
||||
type AuthzResources = {
|
||||
resources: CoretypesResourceRefDTO[];
|
||||
relations: Record<string, string[]>;
|
||||
};
|
||||
import { PermissionScope } from '../PermissionSidePanel/PermissionSidePanel.types';
|
||||
import {
|
||||
buildConfig,
|
||||
@@ -36,14 +41,12 @@ jest.mock('../RoleDetails/constants', () => {
|
||||
|
||||
const dashboardResource: AuthzResources['resources'][number] = {
|
||||
kind: 'dashboard',
|
||||
type: 'metaresource',
|
||||
allowedVerbs: ['create', 'read', 'update', 'delete', 'list'],
|
||||
type: 'metaresource' as CoretypesTypeDTO,
|
||||
};
|
||||
|
||||
const alertResource: AuthzResources['resources'][number] = {
|
||||
kind: 'alert',
|
||||
type: 'metaresource',
|
||||
allowedVerbs: ['create', 'read', 'update', 'delete', 'list'],
|
||||
type: 'metaresource' as CoretypesTypeDTO,
|
||||
};
|
||||
|
||||
const baseAuthzResources: AuthzResources = {
|
||||
@@ -54,16 +57,6 @@ const baseAuthzResources: AuthzResources = {
|
||||
},
|
||||
};
|
||||
|
||||
// API payload resource refs — only kind+type, no allowedVerbs (matches CoretypesResourceRefDTO shape)
|
||||
const dashboardResourceRef = {
|
||||
kind: 'dashboard',
|
||||
type: 'metaresource' as CoretypesTypeDTO,
|
||||
};
|
||||
const alertResourceRef = {
|
||||
kind: 'alert',
|
||||
type: 'metaresource' as CoretypesTypeDTO,
|
||||
};
|
||||
|
||||
const resourceDefs: ResourceDefinition[] = [
|
||||
{
|
||||
id: 'metaresource:dashboard',
|
||||
@@ -114,7 +107,7 @@ describe('buildPatchPayload', () => {
|
||||
});
|
||||
|
||||
expect(result.additions).toStrictEqual([
|
||||
{ resource: dashboardResourceRef, selectors: [ID_B] },
|
||||
{ resource: dashboardResource, selectors: [ID_B] },
|
||||
]);
|
||||
expect(result.deletions).toBeNull();
|
||||
});
|
||||
@@ -149,7 +142,7 @@ describe('buildPatchPayload', () => {
|
||||
});
|
||||
|
||||
expect(result.deletions).toStrictEqual([
|
||||
{ resource: dashboardResourceRef, selectors: [ID_B] },
|
||||
{ resource: dashboardResource, selectors: [ID_B] },
|
||||
]);
|
||||
expect(result.additions).toBeNull();
|
||||
});
|
||||
@@ -214,10 +207,10 @@ describe('buildPatchPayload', () => {
|
||||
});
|
||||
|
||||
expect(result.deletions).toStrictEqual([
|
||||
{ resource: dashboardResourceRef, selectors: ['*'] },
|
||||
{ resource: dashboardResource, selectors: ['*'] },
|
||||
]);
|
||||
expect(result.additions).toStrictEqual([
|
||||
{ resource: dashboardResourceRef, selectors: [ID_A, ID_B] },
|
||||
{ resource: dashboardResource, selectors: [ID_A, ID_B] },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -248,7 +241,7 @@ describe('buildPatchPayload', () => {
|
||||
});
|
||||
|
||||
expect(result.deletions).toStrictEqual([
|
||||
{ resource: dashboardResourceRef, selectors: ['*'] },
|
||||
{ resource: dashboardResource, selectors: ['*'] },
|
||||
]);
|
||||
expect(result.additions).toBeNull();
|
||||
});
|
||||
@@ -271,7 +264,7 @@ describe('buildPatchPayload', () => {
|
||||
});
|
||||
|
||||
expect(result.deletions).toStrictEqual([
|
||||
{ resource: dashboardResourceRef, selectors: ['*'] },
|
||||
{ resource: dashboardResource, selectors: ['*'] },
|
||||
]);
|
||||
expect(result.additions).toBeNull();
|
||||
});
|
||||
@@ -294,7 +287,7 @@ describe('buildPatchPayload', () => {
|
||||
});
|
||||
|
||||
expect(result.additions).toStrictEqual([
|
||||
{ resource: dashboardResourceRef, selectors: ['*'] },
|
||||
{ resource: dashboardResource, selectors: ['*'] },
|
||||
]);
|
||||
expect(result.deletions).toBeNull();
|
||||
});
|
||||
@@ -320,7 +313,7 @@ describe('buildPatchPayload', () => {
|
||||
});
|
||||
|
||||
expect(result.deletions).toStrictEqual([
|
||||
{ resource: dashboardResourceRef, selectors: [ID_A, ID_B] },
|
||||
{ resource: dashboardResource, selectors: [ID_A, ID_B] },
|
||||
]);
|
||||
expect(result.additions).toBeNull();
|
||||
});
|
||||
@@ -346,7 +339,7 @@ describe('buildPatchPayload', () => {
|
||||
});
|
||||
|
||||
expect(result.additions).toStrictEqual([
|
||||
{ resource: dashboardResourceRef, selectors: [ID_A] },
|
||||
{ resource: dashboardResource, selectors: [ID_A] },
|
||||
]);
|
||||
expect(result.deletions).toBeNull();
|
||||
});
|
||||
@@ -392,7 +385,7 @@ describe('buildPatchPayload', () => {
|
||||
});
|
||||
|
||||
expect(result.additions).toStrictEqual([
|
||||
{ resource: alertResourceRef, selectors: [ID_B] },
|
||||
{ resource: alertResource, selectors: [ID_B] },
|
||||
]);
|
||||
expect(result.deletions).toBeNull();
|
||||
});
|
||||
@@ -401,7 +394,7 @@ describe('buildPatchPayload', () => {
|
||||
describe('objectsToPermissionConfig', () => {
|
||||
it('maps a wildcard selector to ALL scope', () => {
|
||||
const objects: CoretypesObjectGroupDTO[] = [
|
||||
{ resource: dashboardResourceRef, selectors: ['*'] },
|
||||
{ resource: dashboardResource, selectors: ['*'] },
|
||||
];
|
||||
|
||||
const result = objectsToPermissionConfig(objects, resourceDefs);
|
||||
@@ -414,7 +407,7 @@ describe('objectsToPermissionConfig', () => {
|
||||
|
||||
it('maps specific selectors to ONLY_SELECTED scope with the IDs', () => {
|
||||
const objects: CoretypesObjectGroupDTO[] = [
|
||||
{ resource: dashboardResourceRef, selectors: [ID_A, ID_B] },
|
||||
{ resource: dashboardResource, selectors: [ID_A, ID_B] },
|
||||
];
|
||||
|
||||
const result = objectsToPermissionConfig(objects, resourceDefs);
|
||||
@@ -573,41 +566,4 @@ describe('deriveResourcesForRelation', () => {
|
||||
deriveResourcesForRelation(baseAuthzResources, 'nonexistent'),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
describe('allowedVerbs filtering', () => {
|
||||
it('excludes resources whose allowedVerbs does not include the relation', () => {
|
||||
const authz: AuthzResources = {
|
||||
resources: [
|
||||
{
|
||||
kind: 'dashboard',
|
||||
type: 'metaresource',
|
||||
allowedVerbs: ['create', 'read', 'update', 'delete', 'list'],
|
||||
},
|
||||
{
|
||||
kind: 'alert',
|
||||
type: 'metaresource',
|
||||
allowedVerbs: ['create', 'read', 'update', 'delete', 'list', 'attach'],
|
||||
},
|
||||
],
|
||||
relations: { attach: ['metaresource'] },
|
||||
};
|
||||
|
||||
const result = deriveResourcesForRelation(authz, 'attach');
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('metaresource:alert');
|
||||
});
|
||||
|
||||
it('requires both type-relation match and allowedVerbs — neither condition alone is sufficient', () => {
|
||||
const authz: AuthzResources = {
|
||||
resources: [
|
||||
{ kind: 'dashboard', type: 'metaresource', allowedVerbs: ['read'] },
|
||||
{ kind: 'role', type: 'role', allowedVerbs: ['create'] },
|
||||
],
|
||||
relations: { create: ['metaresource'] },
|
||||
};
|
||||
|
||||
expect(deriveResourcesForRelation(authz, 'create')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import type {
|
||||
CoretypesObjectGroupDTO,
|
||||
CoretypesResourceRefDTO,
|
||||
CoretypesTypeDTO,
|
||||
CoretypesObjectGroupDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { capitalize } from 'lodash-es';
|
||||
@@ -22,11 +21,7 @@ import {
|
||||
} from './RoleDetails/constants';
|
||||
|
||||
export type AuthzResources = {
|
||||
resources: ReadonlyArray<{
|
||||
kind: string;
|
||||
type: string;
|
||||
allowedVerbs: readonly string[];
|
||||
}>;
|
||||
resources: ReadonlyArray<CoretypesResourceRefDTO>;
|
||||
relations: Readonly<Record<string, ReadonlyArray<string>>>;
|
||||
};
|
||||
|
||||
@@ -74,9 +69,7 @@ export function deriveResourcesForRelation(
|
||||
}
|
||||
const supportedTypes = authzResources.relations[relation] ?? [];
|
||||
return authzResources.resources
|
||||
.filter(
|
||||
(r) => supportedTypes.includes(r.type) && r.allowedVerbs.includes(relation),
|
||||
)
|
||||
.filter((r) => supportedTypes.includes(r.type))
|
||||
.map((r) => ({
|
||||
id: `${r.type}:${r.kind}`,
|
||||
kind: r.kind,
|
||||
@@ -148,7 +141,7 @@ export function buildPatchPayload({
|
||||
}
|
||||
const resourceDef: CoretypesResourceRefDTO = {
|
||||
kind: found.kind,
|
||||
type: found.type as CoretypesTypeDTO,
|
||||
type: found.type,
|
||||
};
|
||||
|
||||
const initialScope = initial?.scope ?? PermissionScope.NONE;
|
||||
|
||||
@@ -101,6 +101,8 @@ export function getAppContextMockState(
|
||||
userPreferences: null,
|
||||
hostsData: null,
|
||||
isLoggedIn: false,
|
||||
isNoAuthMode: false,
|
||||
isPreflightLoading: false,
|
||||
org: null,
|
||||
isFetchingUser: false,
|
||||
isFetchingActiveLicense: false,
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
} from './utils';
|
||||
|
||||
import './ServiceAccountsSettings.styles.scss';
|
||||
import { NoAuthGuard } from 'components/NoAuthGuard';
|
||||
|
||||
function ServiceAccountsSettings(): JSX.Element {
|
||||
const [currentPage, setPage] = useQueryState(
|
||||
@@ -264,16 +265,18 @@ function ServiceAccountsSettings(): JSX.Element {
|
||||
</div>
|
||||
|
||||
<AuthZTooltip checks={[SACreatePermission]}>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={async (): Promise<void> => {
|
||||
await setIsCreateModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus size={12} />
|
||||
New Service Account
|
||||
</Button>
|
||||
<NoAuthGuard>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={async (): Promise<void> => {
|
||||
await setIsCreateModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus size={12} />
|
||||
New Service Account
|
||||
</Button>
|
||||
</NoAuthGuard>
|
||||
</AuthZTooltip>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1120,6 +1120,7 @@
|
||||
|
||||
.user-settings-dropdown-logout-section {
|
||||
color: var(--danger-background);
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +134,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
featureFlags,
|
||||
trialInfo,
|
||||
isLoggedIn,
|
||||
isNoAuthMode,
|
||||
userPreferences,
|
||||
changelog,
|
||||
toggleChangelogModal,
|
||||
@@ -408,7 +409,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
);
|
||||
|
||||
const handleReorderShortcutNavItems = (): void => {
|
||||
logEvent('Sidebar V2: Save shortcuts clicked', {
|
||||
void logEvent('Sidebar V2: Save shortcuts clicked', {
|
||||
shortcuts: tempPinnedMenuItems.map((item) => item.key),
|
||||
});
|
||||
setPinnedMenuItems(tempPinnedMenuItems);
|
||||
@@ -436,7 +437,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
const isWorkspaceBlocked = trialInfo?.workSpaceBlock || false;
|
||||
|
||||
const onClickGetStarted = (event: MouseEvent): void => {
|
||||
logEvent('Sidebar: Menu clicked', {
|
||||
void logEvent('Sidebar: Menu clicked', {
|
||||
menuRoute: '/get-started',
|
||||
menuLabel: 'Get Started',
|
||||
});
|
||||
@@ -489,12 +490,14 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
isWorkspaceBlocked,
|
||||
isEnterpriseSelfHostedUser,
|
||||
isCommunityEnterpriseUser,
|
||||
isNoAuthMode,
|
||||
}),
|
||||
[
|
||||
isEnterpriseSelfHostedUser,
|
||||
isCommunityEnterpriseUser,
|
||||
user.email,
|
||||
isWorkspaceBlocked,
|
||||
isNoAuthMode,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -651,7 +654,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
} else if (item) {
|
||||
onClickHandler(item?.key as string, event);
|
||||
}
|
||||
logEvent('Sidebar V2: Menu clicked', {
|
||||
void logEvent('Sidebar V2: Menu clicked', {
|
||||
menuRoute: item?.key,
|
||||
menuLabel: item?.label,
|
||||
});
|
||||
@@ -794,7 +797,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
onTogglePin={
|
||||
allowPin
|
||||
? (item): void => {
|
||||
logEvent(
|
||||
void logEvent(
|
||||
`Sidebar V2: Menu item ${item.isPinned ? 'unpinned' : 'pinned'}`,
|
||||
{
|
||||
menuRoute: item.key,
|
||||
@@ -841,7 +844,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
const event = (info as SidebarItem & { domEvent?: MouseEvent }).domEvent;
|
||||
|
||||
if (item && !('type' in item)) {
|
||||
logEvent('Help Popover: Item clicked', {
|
||||
void logEvent('Help Popover: Item clicked', {
|
||||
menuRoute: item.key,
|
||||
menuLabel: String(item.label),
|
||||
});
|
||||
@@ -890,7 +893,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
menuLabel = item.label;
|
||||
}
|
||||
|
||||
logEvent('Settings Popover: Item clicked', {
|
||||
void logEvent('Settings Popover: Item clicked', {
|
||||
menuRoute: item?.key,
|
||||
menuLabel,
|
||||
});
|
||||
@@ -927,7 +930,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
}
|
||||
break;
|
||||
case 'logout':
|
||||
Logout();
|
||||
void Logout();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
@@ -1081,7 +1084,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
<div
|
||||
className="nav-section-title-icon reorder"
|
||||
onClick={(): void => {
|
||||
logEvent('Sidebar V2: Manage shortcuts clicked', {});
|
||||
void logEvent('Sidebar V2: Manage shortcuts clicked', {});
|
||||
setIsReorderShortcutNavItemsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
@@ -1128,7 +1131,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
return;
|
||||
}
|
||||
const newCollapsedState = !isMoreMenuCollapsed;
|
||||
logEvent('Sidebar V2: More menu clicked', {
|
||||
void logEvent('Sidebar V2: More menu clicked', {
|
||||
action: isMoreMenuCollapsed ? 'expand' : 'collapse',
|
||||
});
|
||||
setIsMoreMenuCollapsed(newCollapsedState);
|
||||
@@ -1234,14 +1237,14 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
open={isReorderShortcutNavItemsModalOpen}
|
||||
closable
|
||||
onCancel={(): void => {
|
||||
logEvent('Sidebar V2: Manage shortcuts dismissed', {});
|
||||
void logEvent('Sidebar V2: Manage shortcuts dismissed', {});
|
||||
hideReorderShortcutNavItemsModal();
|
||||
}}
|
||||
footer={[
|
||||
<Button
|
||||
key="cancel"
|
||||
onClick={(): void => {
|
||||
logEvent('Sidebar V2: Manage shortcuts dismissed', {});
|
||||
void logEvent('Sidebar V2: Manage shortcuts dismissed', {});
|
||||
hideReorderShortcutNavItemsModal();
|
||||
}}
|
||||
className="periscope-btn cancel-btn secondary-btn"
|
||||
|
||||
@@ -5,6 +5,7 @@ const BASE_PARAMS = {
|
||||
isWorkspaceBlocked: false,
|
||||
isEnterpriseSelfHostedUser: false,
|
||||
isCommunityEnterpriseUser: false,
|
||||
isNoAuthMode: false,
|
||||
};
|
||||
|
||||
describe('getUserSettingsDropdownMenuItems', () => {
|
||||
@@ -71,4 +72,15 @@ describe('getUserSettingsDropdownMenuItems', () => {
|
||||
expect(keys[3]).toBe('account');
|
||||
expect(keys[keys.length - 1]).toBe('logout');
|
||||
});
|
||||
|
||||
it('omits sign out and its preceding divider when isNoAuthMode=true', () => {
|
||||
const items =
|
||||
getUserSettingsDropdownMenuItems({ ...BASE_PARAMS, isNoAuthMode: true }) ??
|
||||
[];
|
||||
const keys = items.map((item: any) => item.key ?? item.type);
|
||||
|
||||
expect(keys).not.toContain('logout');
|
||||
// the trailing divider before logout should also be gone
|
||||
expect(keys[keys.length - 1]).toBe('keyboard-shortcuts');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { MenuProps } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
ArrowUpRight,
|
||||
BarChart,
|
||||
@@ -35,15 +37,13 @@ import {
|
||||
Users,
|
||||
Binoculars,
|
||||
} from '@signozhq/icons';
|
||||
import { Style } from '@signozhq/design-tokens';
|
||||
import { MenuProps } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
import {
|
||||
SecondaryMenuItemKey,
|
||||
SettingsNavSection,
|
||||
SidebarItem,
|
||||
} from './sideNav.types';
|
||||
import { Style } from '@signozhq/design-tokens';
|
||||
|
||||
export const getStartedMenuItem = {
|
||||
key: ROUTES.GET_STARTED,
|
||||
@@ -487,6 +487,7 @@ export interface UserSettingsMenuItemsParams {
|
||||
isWorkspaceBlocked: boolean;
|
||||
isEnterpriseSelfHostedUser: boolean;
|
||||
isCommunityEnterpriseUser: boolean;
|
||||
isNoAuthMode: boolean;
|
||||
}
|
||||
|
||||
export const getUserSettingsDropdownMenuItems = ({
|
||||
@@ -494,6 +495,7 @@ export const getUserSettingsDropdownMenuItems = ({
|
||||
isWorkspaceBlocked,
|
||||
isEnterpriseSelfHostedUser,
|
||||
isCommunityEnterpriseUser,
|
||||
isNoAuthMode,
|
||||
}: UserSettingsMenuItemsParams): MenuProps['items'] =>
|
||||
[
|
||||
{
|
||||
@@ -537,21 +539,25 @@ export const getUserSettingsDropdownMenuItems = ({
|
||||
icon: <Keyboard size={14} color={Style.L1_FOREGROUND} />,
|
||||
dataTestId: 'keyboard-shortcuts-nav-item',
|
||||
},
|
||||
{ type: 'divider' as const },
|
||||
{
|
||||
key: 'logout',
|
||||
label: (
|
||||
<span className="user-settings-dropdown-logout-section">Sign out</span>
|
||||
),
|
||||
icon: (
|
||||
<LogOut
|
||||
size={14}
|
||||
className="user-settings-dropdown-logout-section"
|
||||
color={Style.DANGER_BACKGROUND}
|
||||
/>
|
||||
),
|
||||
dataTestId: 'logout-nav-item',
|
||||
},
|
||||
...(isNoAuthMode
|
||||
? []
|
||||
: [
|
||||
{ type: 'divider' as const },
|
||||
{
|
||||
key: 'logout',
|
||||
label: (
|
||||
<span className="user-settings-dropdown-logout-section">Sign out</span>
|
||||
),
|
||||
icon: (
|
||||
<LogOut
|
||||
size={14}
|
||||
className="user-settings-dropdown-logout-section"
|
||||
color={Style.DANGER_BACKGROUND}
|
||||
/>
|
||||
),
|
||||
dataTestId: 'logout-nav-item',
|
||||
},
|
||||
]),
|
||||
].filter(Boolean);
|
||||
|
||||
/** Mapping of some newly added routes and their corresponding active sidebar menu key */
|
||||
|
||||
@@ -189,7 +189,7 @@ describe('Tooltip utils', () => {
|
||||
];
|
||||
}
|
||||
|
||||
it('builds tooltip content sorted by value descending with isActive flag set correctly', () => {
|
||||
it('builds tooltip content in series-index order with isActive flag set correctly', () => {
|
||||
const data: AlignedData = [[0], [10], [20], [30]];
|
||||
const series = createSeriesConfig();
|
||||
const dataIndexes = [null, 0, 0, 0];
|
||||
@@ -206,21 +206,21 @@ describe('Tooltip utils', () => {
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
// Sorted by value descending: B (20) before A (10)
|
||||
// Series are returned in series-index order (A=index 1 before B=index 2)
|
||||
expect(result[0]).toMatchObject<Partial<TooltipContentItem>>({
|
||||
label: 'B',
|
||||
value: 20,
|
||||
tooltipValue: 'formatted-20',
|
||||
color: 'color-2',
|
||||
isActive: true,
|
||||
});
|
||||
expect(result[1]).toMatchObject<Partial<TooltipContentItem>>({
|
||||
label: 'A',
|
||||
value: 10,
|
||||
tooltipValue: 'formatted-10',
|
||||
color: '#ff0000',
|
||||
isActive: false,
|
||||
});
|
||||
expect(result[1]).toMatchObject<Partial<TooltipContentItem>>({
|
||||
label: 'B',
|
||||
value: 20,
|
||||
tooltipValue: 'formatted-20',
|
||||
color: 'color-2',
|
||||
isActive: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('skips series with null data index or non-finite values', () => {
|
||||
@@ -274,7 +274,7 @@ describe('Tooltip utils', () => {
|
||||
expect(result[1].value).toBe(30);
|
||||
});
|
||||
|
||||
it('returns items sorted by value descending', () => {
|
||||
it('returns items in series-index order', () => {
|
||||
// Series values in non-sorted order: 3, 1, 4, 2
|
||||
const data: AlignedData = [[0], [3], [1], [4], [2]];
|
||||
const series: Series[] = [
|
||||
@@ -297,7 +297,7 @@ describe('Tooltip utils', () => {
|
||||
decimalPrecision,
|
||||
});
|
||||
|
||||
expect(result.map((item) => item.value)).toStrictEqual([4, 3, 2, 1]);
|
||||
expect(result.map((item) => item.value)).toStrictEqual([3, 1, 4, 2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -142,7 +142,5 @@ export function buildTooltipContent({
|
||||
}
|
||||
}
|
||||
|
||||
items.sort((a, b) => b.value - a.value);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -321,7 +321,7 @@ function SettingsPage(): JSX.Element {
|
||||
isDisabled={false}
|
||||
showIcon={false}
|
||||
onClick={(event): void => {
|
||||
logEvent('Settings V2: Menu clicked', {
|
||||
void logEvent('Settings V2: Menu clicked', {
|
||||
menuLabel: item.label,
|
||||
menuRoute: item.key,
|
||||
});
|
||||
|
||||
@@ -473,7 +473,6 @@ export const SpanDuration = memo(function SpanDuration({
|
||||
const columnDefHelper = createColumnHelper<SpanV3>();
|
||||
|
||||
const ROW_HEIGHT = 28;
|
||||
const WATERFALL_BOTTOM_PADDING = 24;
|
||||
const DEFAULT_SIDEBAR_WIDTH = 450;
|
||||
const MIN_SIDEBAR_WIDTH = 240;
|
||||
const MAX_SIDEBAR_WIDTH = 900;
|
||||
@@ -741,69 +740,53 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
);
|
||||
}, [spans, sidebarWidth]);
|
||||
|
||||
// Scroll a span to viewport center if it isn't already visible. Shared by
|
||||
// the two effects below — one keyed on interestedSpanId (chevron, boundary
|
||||
// pagination, deep-link to unloaded), the other on selectedSpan (in-window
|
||||
// URL navigation that doesn't mutate interestedSpanId).
|
||||
const scrollSpanIntoView = useCallback(
|
||||
(span: SpanV3, spansList: SpanV3[]): void => {
|
||||
if (!virtualizerRef.current) {
|
||||
return;
|
||||
}
|
||||
const idx = spansList.findIndex((s) => s.span_id === span.span_id);
|
||||
if (idx === -1) {
|
||||
return;
|
||||
}
|
||||
const scrollEl = scrollContainerRef.current;
|
||||
const scrollTop = scrollEl?.scrollTop ?? 0;
|
||||
const viewportHeight = scrollEl?.clientHeight ?? 0;
|
||||
const viewportStartIdx = Math.floor(scrollTop / ROW_HEIGHT);
|
||||
const viewportEndIdx =
|
||||
Math.ceil((scrollTop + viewportHeight) / ROW_HEIGHT) - 1;
|
||||
const isOnScreen =
|
||||
viewportHeight > 0 && idx >= viewportStartIdx && idx <= viewportEndIdx;
|
||||
if (isOnScreen) {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
virtualizerRef.current?.scrollToIndex(idx, {
|
||||
align: 'center',
|
||||
behavior: 'auto',
|
||||
});
|
||||
const sidebarScrollEl = scrollContainerRef.current?.querySelector(
|
||||
'.resizable-box__content',
|
||||
);
|
||||
if (sidebarScrollEl) {
|
||||
const targetScrollLeft = Math.max(0, span.level * CONNECTOR_WIDTH - 40);
|
||||
(sidebarScrollEl as HTMLElement).scrollLeft = targetScrollLeft;
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Scroll to the interested span only when it isn't already on screen.
|
||||
// Covers every entry point uniformly: deep-link, flamegraph click,
|
||||
// filter prev/next, browser back/forward all scroll only if needed;
|
||||
// waterfall row clicks and chevron expand/collapse don't yank the viewport
|
||||
// because the affected row is by definition already visible.
|
||||
useEffect(() => {
|
||||
if (interestedSpanId.spanId !== '') {
|
||||
if (interestedSpanId.spanId !== '' && virtualizerRef.current) {
|
||||
const idx = spans.findIndex(
|
||||
(span) => span.span_id === interestedSpanId.spanId,
|
||||
);
|
||||
if (idx !== -1) {
|
||||
scrollSpanIntoView(spans[idx], spans);
|
||||
const visible = virtualizerRef.current.getVirtualItems();
|
||||
const isOnScreen =
|
||||
visible.length > 0 &&
|
||||
idx >= visible[0].index &&
|
||||
idx <= visible[visible.length - 1].index;
|
||||
|
||||
if (!isOnScreen) {
|
||||
setTimeout(() => {
|
||||
virtualizerRef.current?.scrollToIndex(idx, {
|
||||
align: 'center',
|
||||
behavior: 'auto',
|
||||
});
|
||||
|
||||
// Auto-scroll sidebar horizontally to show the span name
|
||||
const span = spans[idx];
|
||||
const sidebarScrollEl = scrollContainerRef.current?.querySelector(
|
||||
'.resizable-box__content',
|
||||
);
|
||||
if (sidebarScrollEl) {
|
||||
const targetScrollLeft = Math.max(0, span.level * CONNECTOR_WIDTH - 40);
|
||||
sidebarScrollEl.scrollLeft = targetScrollLeft;
|
||||
}
|
||||
}, 400);
|
||||
}
|
||||
|
||||
setSelectedSpan(spans[idx]);
|
||||
}
|
||||
} else {
|
||||
setSelectedSpan((prev) => prev ?? spans[0]);
|
||||
setSelectedSpan((prev) => {
|
||||
if (!prev) {
|
||||
return spans[0];
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
}, [interestedSpanId, setSelectedSpan, spans, scrollSpanIntoView]);
|
||||
|
||||
// Covers URL-driven navigation to an already-loaded span (flamegraph /
|
||||
// filter / browser back) that the interestedSpanId-keyed effect doesn't see.
|
||||
useEffect(() => {
|
||||
if (selectedSpan) {
|
||||
scrollSpanIntoView(selectedSpan, spans);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedSpan, scrollSpanIntoView]);
|
||||
}, [interestedSpanId, setSelectedSpan, spans]);
|
||||
|
||||
const virtualItems = virtualizer.getVirtualItems();
|
||||
const leftRows = leftTable.getRowModel().rows;
|
||||
@@ -863,7 +846,7 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
<div
|
||||
className={styles.splitBody}
|
||||
style={{
|
||||
minHeight: virtualizer.getTotalSize() + WATERFALL_BOTTOM_PADDING,
|
||||
minHeight: virtualizer.getTotalSize(),
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -74,21 +74,17 @@ function TraceDetailsV3(): JSX.Element {
|
||||
onClose: handleSpanDetailsClose,
|
||||
});
|
||||
|
||||
const allSpansRef = useRef<SpanV3[]>([]);
|
||||
|
||||
// Refetch only when the URL target isn't already loaded. Keeps row clicks
|
||||
// and other in-window URL navigation from triggering a backend window slide.
|
||||
useEffect(() => {
|
||||
const spanId = urlQuery.get('spanId') || '';
|
||||
// Only update interestedSpanId when a new span is selected,
|
||||
// not when it's cleared (panel close) — avoids unnecessary API refetch
|
||||
if (!spanId) {
|
||||
return;
|
||||
}
|
||||
const idx = allSpansRef.current.findIndex((s) => s.span_id === spanId);
|
||||
if (idx !== -1) {
|
||||
setSelectedSpan(allSpansRef.current[idx]);
|
||||
return;
|
||||
}
|
||||
setInterestedSpanId({ spanId, isUncollapsed: true });
|
||||
setInterestedSpanId({
|
||||
spanId,
|
||||
isUncollapsed: true,
|
||||
});
|
||||
}, [urlQuery]);
|
||||
|
||||
// Hardcoded for now — fetch aggregations for all 3 candidate color-by fields
|
||||
@@ -149,10 +145,6 @@ function TraceDetailsV3(): JSX.Element {
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
allSpansRef.current = allSpans;
|
||||
}, [allSpans]);
|
||||
|
||||
// Frontend mode: expand all parents by default when full data arrives
|
||||
useEffect(() => {
|
||||
if (isFullDataLoaded && allSpans.length > 0) {
|
||||
|
||||
@@ -13,8 +13,11 @@ import { useQuery } from 'react-query';
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import { useGetHosts } from 'api/generated/services/zeus';
|
||||
import { useGetGlobalConfig } from 'api/generated/services/global';
|
||||
import { useGetMyUser } from 'api/generated/services/users';
|
||||
import listOrgPreferences from 'api/v1/org/preferences/list';
|
||||
import { clearAuthStorage } from 'utils/clearAuthStorage';
|
||||
import { setNoAuthMode } from 'utils/noAuthMode';
|
||||
import listUserPreferences from 'api/v1/user/preferences/list';
|
||||
import getUserVersion from 'api/v1/version/get';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
@@ -70,11 +73,50 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(
|
||||
(): boolean => getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true',
|
||||
);
|
||||
const [isNoAuthMode, setIsNoAuthMode] = useState<boolean>(false);
|
||||
const [isPreflightLoading, setIsPreflightLoading] = useState<boolean>(true);
|
||||
const [org, setOrg] = useState<Organization[] | null>(null);
|
||||
const [changelog, setChangelog] = useState<ChangelogSchema | null>(null);
|
||||
|
||||
const [showChangelogModal, setShowChangelogModal] = useState<boolean>(false);
|
||||
|
||||
// Pre-flight: discover auth mode from public global config.
|
||||
// On success: in impersonation mode → clear stale tokens, force isLoggedIn=true,
|
||||
// set noAuthMode singleton so the axios interceptor (outside React)
|
||||
// can skip the rotate-logout chain.
|
||||
// On failure: fail-safe to normal auth flow (treat as not no-auth).
|
||||
const { data: globalConfigData, isLoading: isFetchingGlobalConfig } =
|
||||
useGetGlobalConfig({
|
||||
query: {
|
||||
retry: 2,
|
||||
retryDelay: 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: Infinity,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isFetchingGlobalConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
const impersonationEnabled =
|
||||
globalConfigData?.data?.identN?.impersonation?.enabled === true;
|
||||
|
||||
if (impersonationEnabled) {
|
||||
clearAuthStorage();
|
||||
setLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN, 'true');
|
||||
setNoAuthMode(true);
|
||||
setIsNoAuthMode(true);
|
||||
setIsLoggedIn(true);
|
||||
} else {
|
||||
setNoAuthMode(false);
|
||||
setIsNoAuthMode(false);
|
||||
}
|
||||
|
||||
setIsPreflightLoading(false);
|
||||
}, [globalConfigData, isFetchingGlobalConfig]);
|
||||
|
||||
// fetcher for current user
|
||||
// user will only be fetched if the user id and token is present
|
||||
// if logged out and trying to hit any route none of these calls will trigger
|
||||
@@ -366,6 +408,9 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
|
||||
// global event listener for LOGOUT event to clean the app context state
|
||||
useGlobalEventListener('LOGOUT', () => {
|
||||
if (isNoAuthMode) {
|
||||
return;
|
||||
} // logout is meaningless in no-auth; defensively no-op
|
||||
setIsLoggedIn(false);
|
||||
setDefaultUser(getUserDefaults());
|
||||
setActiveLicense(null);
|
||||
@@ -385,6 +430,8 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
orgPreferences,
|
||||
hostsData,
|
||||
isLoggedIn,
|
||||
isNoAuthMode,
|
||||
isPreflightLoading,
|
||||
org,
|
||||
isFetchingUser,
|
||||
isFetchingActiveLicense,
|
||||
@@ -425,6 +472,8 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
isLoggedIn,
|
||||
hostsData,
|
||||
hostsFetchError,
|
||||
isNoAuthMode,
|
||||
isPreflightLoading,
|
||||
org,
|
||||
orgPreferences,
|
||||
activeLicenseRefetch,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ReactElement } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import { getIsNoAuthMode, setNoAuthMode } from 'utils/noAuthMode';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { SINGLE_FLIGHT_WAIT_TIME_MS } from 'hooks/useAuthZ/constants';
|
||||
import { server } from 'mocks-server/server';
|
||||
@@ -13,6 +14,7 @@ import { AppProvider, useAppContext } from '../App';
|
||||
|
||||
const MY_USER_URL = 'http://localhost/api/v2/users/me';
|
||||
const MY_ORG_URL = 'http://localhost/api/v2/orgs/me';
|
||||
const GLOBAL_CONFIG_URL = 'http://localhost/api/v1/global/config';
|
||||
|
||||
jest.mock('constants/env', () => ({
|
||||
ENVIRONMENT: { baseURL: 'http://localhost', wsURL: '' },
|
||||
@@ -336,3 +338,89 @@ describe('AppProvider when authz/check fails', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AppProvider no-auth preflight', () => {
|
||||
beforeEach(() => {
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setNoAuthMode(false);
|
||||
});
|
||||
|
||||
it('sets isNoAuthMode=true and noAuthMode singleton when impersonation is enabled', async () => {
|
||||
server.use(
|
||||
rest.get(GLOBAL_CONFIG_URL, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: { identN: { impersonation: { enabled: true } } },
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result } = renderHook(() => useAppContext(), { wrapper });
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.isNoAuthMode).toBe(true);
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
|
||||
expect(getIsNoAuthMode()).toBe(true);
|
||||
});
|
||||
|
||||
it('leaves isNoAuthMode=false and clears noAuthMode singleton when impersonation is disabled', async () => {
|
||||
server.use(
|
||||
rest.get(GLOBAL_CONFIG_URL, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: { identN: { impersonation: { enabled: false } } },
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result } = renderHook(() => useAppContext(), { wrapper });
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.isPreflightLoading).toBe(false);
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
|
||||
expect(result.current.isNoAuthMode).toBe(false);
|
||||
expect(getIsNoAuthMode()).toBe(false);
|
||||
});
|
||||
|
||||
it('transitions isPreflightLoading from true to false once preflight resolves', async () => {
|
||||
server.use(
|
||||
rest.get(GLOBAL_CONFIG_URL, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: { identN: { impersonation: { enabled: false } } },
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result } = renderHook(() => useAppContext(), { wrapper });
|
||||
|
||||
expect(result.current.isPreflightLoading).toBe(true);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.isPreflightLoading).toBe(false);
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,8 @@ export interface IAppContext {
|
||||
userPreferences: UserPreference[] | null;
|
||||
hostsData: GetHosts200 | null;
|
||||
isLoggedIn: boolean;
|
||||
isNoAuthMode: boolean;
|
||||
isPreflightLoading: boolean;
|
||||
org: Organization[] | null;
|
||||
isFetchingUser: boolean;
|
||||
isFetchingActiveLicense: boolean;
|
||||
|
||||
@@ -11,13 +11,6 @@ import type {
|
||||
} from 'hooks/useAuthZ/types';
|
||||
import { rest } from 'msw';
|
||||
import type { RestHandler } from 'msw';
|
||||
import {
|
||||
LicenseEvent,
|
||||
LicensePlatform,
|
||||
type LicenseResModel,
|
||||
LicenseState,
|
||||
LicenseStatus,
|
||||
} from 'types/api/licensesV3/getActive';
|
||||
|
||||
export const AUTHZ_CHECK_URL = `${ENVIRONMENT.baseURL || ''}/api/v1/authz/check`;
|
||||
|
||||
@@ -104,40 +97,6 @@ export function setupAuthzAllow(
|
||||
});
|
||||
}
|
||||
|
||||
export function buildLicense(
|
||||
overrides?: Partial<LicenseResModel>,
|
||||
): LicenseResModel {
|
||||
return {
|
||||
key: 'test-key',
|
||||
status: LicenseStatus.VALID,
|
||||
state: LicenseState.ACTIVATED,
|
||||
platform: LicensePlatform.CLOUD,
|
||||
event_queue: {
|
||||
created_at: '0',
|
||||
event: LicenseEvent.NO_EVENT,
|
||||
scheduled_at: '0',
|
||||
status: '',
|
||||
updated_at: '0',
|
||||
},
|
||||
plan: {
|
||||
created_at: '0',
|
||||
description: '',
|
||||
is_active: true,
|
||||
name: '',
|
||||
updated_at: '0',
|
||||
},
|
||||
plan_id: '0',
|
||||
free_until: '0',
|
||||
updated_at: '0',
|
||||
valid_from: 0,
|
||||
valid_until: 0,
|
||||
created_at: '0',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export const invalidLicense = buildLicense({ status: LicenseStatus.INVALID });
|
||||
|
||||
export function mockUseAuthZGrantAll(
|
||||
permissions: BrandedPermission[],
|
||||
_options?: UseAuthZOptions,
|
||||
|
||||
@@ -240,6 +240,8 @@ export function getAppContextMock(
|
||||
isFetchingOrgPreferences: false,
|
||||
orgPreferencesFetchError: null,
|
||||
isLoggedIn: true,
|
||||
isNoAuthMode: false,
|
||||
isPreflightLoading: false,
|
||||
showChangelogModal: false,
|
||||
updateUser: jest.fn(),
|
||||
updateOrg: jest.fn(),
|
||||
|
||||
39
frontend/src/utils/__tests__/clearAuthStorage.test.ts
Normal file
39
frontend/src/utils/__tests__/clearAuthStorage.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
|
||||
import { clearAuthStorage } from '../clearAuthStorage';
|
||||
|
||||
describe('clearAuthStorage', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('removes all auth-related localStorage keys', () => {
|
||||
localStorage.setItem(LOCALSTORAGE.AUTH_TOKEN, 'access');
|
||||
localStorage.setItem(LOCALSTORAGE.REFRESH_AUTH_TOKEN, 'refresh');
|
||||
localStorage.setItem(LOCALSTORAGE.IS_LOGGED_IN, 'true');
|
||||
localStorage.setItem(LOCALSTORAGE.LOGGED_IN_USER_EMAIL, 'old@example.com');
|
||||
localStorage.setItem(LOCALSTORAGE.LOGGED_IN_USER_NAME, 'old');
|
||||
localStorage.setItem(LOCALSTORAGE.IS_IDENTIFIED_USER, 'true');
|
||||
localStorage.setItem(LOCALSTORAGE.USER_ID, 'abc');
|
||||
|
||||
clearAuthStorage();
|
||||
|
||||
expect(localStorage.getItem(LOCALSTORAGE.AUTH_TOKEN)).toBeNull();
|
||||
expect(localStorage.getItem(LOCALSTORAGE.REFRESH_AUTH_TOKEN)).toBeNull();
|
||||
expect(localStorage.getItem(LOCALSTORAGE.IS_LOGGED_IN)).toBeNull();
|
||||
expect(localStorage.getItem(LOCALSTORAGE.LOGGED_IN_USER_EMAIL)).toBeNull();
|
||||
expect(localStorage.getItem(LOCALSTORAGE.LOGGED_IN_USER_NAME)).toBeNull();
|
||||
expect(localStorage.getItem(LOCALSTORAGE.IS_IDENTIFIED_USER)).toBeNull();
|
||||
expect(localStorage.getItem(LOCALSTORAGE.USER_ID)).toBeNull();
|
||||
});
|
||||
|
||||
it('preserves non-auth localStorage keys', () => {
|
||||
localStorage.setItem(LOCALSTORAGE.THEME, 'dark');
|
||||
localStorage.setItem(LOCALSTORAGE.AUTH_TOKEN, 'access');
|
||||
|
||||
clearAuthStorage();
|
||||
|
||||
expect(localStorage.getItem(LOCALSTORAGE.THEME)).toBe('dark');
|
||||
expect(localStorage.getItem(LOCALSTORAGE.AUTH_TOKEN)).toBeNull();
|
||||
});
|
||||
});
|
||||
16
frontend/src/utils/clearAuthStorage.ts
Normal file
16
frontend/src/utils/clearAuthStorage.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import deleteLocalStorageKey from 'api/browser/localstorage/remove';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
|
||||
const AUTH_KEYS: LOCALSTORAGE[] = [
|
||||
LOCALSTORAGE.AUTH_TOKEN,
|
||||
LOCALSTORAGE.REFRESH_AUTH_TOKEN,
|
||||
LOCALSTORAGE.IS_LOGGED_IN,
|
||||
LOCALSTORAGE.LOGGED_IN_USER_EMAIL,
|
||||
LOCALSTORAGE.LOGGED_IN_USER_NAME,
|
||||
LOCALSTORAGE.IS_IDENTIFIED_USER,
|
||||
LOCALSTORAGE.USER_ID,
|
||||
];
|
||||
|
||||
export const clearAuthStorage = (): void => {
|
||||
AUTH_KEYS.forEach((key) => deleteLocalStorageKey(key));
|
||||
};
|
||||
7
frontend/src/utils/noAuthMode.ts
Normal file
7
frontend/src/utils/noAuthMode.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
let _isNoAuthMode = false;
|
||||
|
||||
export const setNoAuthMode = (value: boolean): void => {
|
||||
_isNoAuthMode = value;
|
||||
};
|
||||
|
||||
export const getIsNoAuthMode = (): boolean => _isNoAuthMode;
|
||||
@@ -48,7 +48,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
HOME: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
ALERTS_NEW: ['ADMIN', 'EDITOR'],
|
||||
ORG_SETTINGS: ['ADMIN'],
|
||||
MY_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER', 'ANONYMOUS'],
|
||||
MY_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
SERVICE_MAP: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
ALL_CHANNELS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
INGESTION_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
@@ -72,7 +72,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
NOT_FOUND: ['ADMIN', 'VIEWER', 'EDITOR', 'ANONYMOUS'],
|
||||
PASSWORD_RESET: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
SERVICE_METRICS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER', 'ANONYMOUS'],
|
||||
SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
SIGN_UP: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
TRACES_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
TRACE: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
@@ -98,10 +98,10 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
GET_STARTED_AZURE_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
WORKSPACE_LOCKED: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
WORKSPACE_SUSPENDED: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
ROLES_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER', 'ANONYMOUS'],
|
||||
ROLE_DETAILS: ['ADMIN', 'EDITOR', 'VIEWER', 'ANONYMOUS'],
|
||||
ROLES_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
ROLE_DETAILS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
MEMBERS_SETTINGS: ['ADMIN'],
|
||||
SERVICE_ACCOUNTS_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER', 'ANONYMOUS'],
|
||||
SERVICE_ACCOUNTS_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
BILLING: ['ADMIN'],
|
||||
SUPPORT: ['ADMIN', 'EDITOR', 'VIEWER', 'ANONYMOUS'],
|
||||
SOMETHING_WENT_WRONG: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
|
||||
16
pkg/cache/memorycache/provider.go
vendored
16
pkg/cache/memorycache/provider.go
vendored
@@ -64,8 +64,7 @@ func New(ctx context.Context, settings factory.ProviderSettings, config cache.Co
|
||||
o.ObserveInt64(telemetry.setsRejected, int64(metrics.SetsRejected()), metric.WithAttributes(attributes...))
|
||||
o.ObserveInt64(telemetry.getsDropped, int64(metrics.GetsDropped()), metric.WithAttributes(attributes...))
|
||||
o.ObserveInt64(telemetry.getsKept, int64(metrics.GetsKept()), metric.WithAttributes(attributes...))
|
||||
o.ObserveInt64(telemetry.costUsed, int64(metrics.CostAdded())-int64(metrics.CostEvicted()), metric.WithAttributes(attributes...))
|
||||
o.ObserveInt64(telemetry.totalCost, cc.MaxCost(), metric.WithAttributes(attributes...))
|
||||
o.ObserveInt64(telemetry.totalCost, int64(cc.MaxCost()), metric.WithAttributes(attributes...))
|
||||
return nil
|
||||
},
|
||||
telemetry.cacheRatio,
|
||||
@@ -80,7 +79,6 @@ func New(ctx context.Context, settings factory.ProviderSettings, config cache.Co
|
||||
telemetry.setsRejected,
|
||||
telemetry.getsDropped,
|
||||
telemetry.getsKept,
|
||||
telemetry.costUsed,
|
||||
telemetry.totalCost,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -114,13 +112,11 @@ func (provider *provider) Set(ctx context.Context, orgID valuer.UUID, cacheKey s
|
||||
}
|
||||
|
||||
if cloneable, ok := data.(cachetypes.Cloneable); ok {
|
||||
cost := max(cloneable.Cost(), 1)
|
||||
// Clamp to a minimum of 1: ristretto treats cost 0 specially and we
|
||||
// never want zero-size entries to bypass admission accounting.
|
||||
span.SetAttributes(attribute.Bool("memory.cloneable", true))
|
||||
span.SetAttributes(attribute.Int64("memory.cost", cost))
|
||||
span.SetAttributes(attribute.Int64("memory.cost", 1))
|
||||
toCache := cloneable.Clone()
|
||||
if ok := provider.cc.SetWithTTL(strings.Join([]string{orgID.StringValue(), cacheKey}, "::"), toCache, cost, ttl); !ok {
|
||||
// In case of contention we are choosing to evict the cloneable entries first hence cost is set to 1
|
||||
if ok := provider.cc.SetWithTTL(strings.Join([]string{orgID.StringValue(), cacheKey}, "::"), toCache, 1, ttl); !ok {
|
||||
return errors.New(errors.TypeInternal, errors.CodeInternal, "error writing to cache")
|
||||
}
|
||||
|
||||
@@ -129,15 +125,15 @@ func (provider *provider) Set(ctx context.Context, orgID valuer.UUID, cacheKey s
|
||||
}
|
||||
|
||||
toCache, err := provider.marshalBinary(ctx, data)
|
||||
cost := int64(len(toCache))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cost := max(int64(len(toCache)), 1)
|
||||
|
||||
span.SetAttributes(attribute.Bool("memory.cloneable", false))
|
||||
span.SetAttributes(attribute.Int64("memory.cost", cost))
|
||||
|
||||
if ok := provider.cc.SetWithTTL(strings.Join([]string{orgID.StringValue(), cacheKey}, "::"), toCache, cost, ttl); !ok {
|
||||
if ok := provider.cc.SetWithTTL(strings.Join([]string{orgID.StringValue(), cacheKey}, "::"), toCache, 1, ttl); !ok {
|
||||
return errors.New(errors.TypeInternal, errors.CodeInternal, "error writing to cache")
|
||||
}
|
||||
|
||||
|
||||
43
pkg/cache/memorycache/provider_test.go
vendored
43
pkg/cache/memorycache/provider_test.go
vendored
@@ -31,10 +31,6 @@ func (cloneable *CloneableA) Clone() cachetypes.Cacheable {
|
||||
}
|
||||
}
|
||||
|
||||
func (cloneable *CloneableA) Cost() int64 {
|
||||
return int64(len(cloneable.Key)) + 16
|
||||
}
|
||||
|
||||
func (cloneable *CloneableA) MarshalBinary() ([]byte, error) {
|
||||
return json.Marshal(cloneable)
|
||||
}
|
||||
@@ -169,45 +165,6 @@ func TestSetGetWithDifferentTypes(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// LargeCloneable reports a large byte cost so we can test ristretto eviction
|
||||
// without allocating the full payload in memory.
|
||||
type LargeCloneable struct {
|
||||
Key string
|
||||
CostHint int64
|
||||
}
|
||||
|
||||
func (c *LargeCloneable) Clone() cachetypes.Cacheable {
|
||||
return &LargeCloneable{Key: c.Key, CostHint: c.CostHint}
|
||||
}
|
||||
|
||||
func (c *LargeCloneable) Cost() int64 { return c.CostHint }
|
||||
|
||||
func (c *LargeCloneable) MarshalBinary() ([]byte, error) { return json.Marshal(c) }
|
||||
|
||||
func (c *LargeCloneable) UnmarshalBinary(data []byte) error { return json.Unmarshal(data, c) }
|
||||
|
||||
func TestCloneableExceedingMaxCostIsRejected(t *testing.T) {
|
||||
const maxCost int64 = 1 << 20 // 1 MiB
|
||||
const oversize int64 = 2 << 20 // 2 MiB, larger than the entire cache
|
||||
|
||||
c, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: cache.Memory{
|
||||
NumCounters: 10 * 1000,
|
||||
MaxCost: maxCost,
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
|
||||
orgID := valuer.GenerateUUID()
|
||||
const key = "oversize-key"
|
||||
assert.NoError(t, c.Set(context.Background(), orgID, key,
|
||||
&LargeCloneable{Key: key, CostHint: oversize}, time.Minute))
|
||||
|
||||
// Ristretto rejects any entry with cost > MaxCost (policy.go:100). Probe
|
||||
// ristretto directly to confirm no admission, instead of relying on metrics.
|
||||
cc := c.(*provider).cc
|
||||
_, ok := cc.Get(strings.Join([]string{orgID.StringValue(), key}, "::"))
|
||||
assert.False(t, ok, "entry with Cost() > MaxCost must be rejected")
|
||||
}
|
||||
|
||||
func TestCloneableConcurrentSetGet(t *testing.T) {
|
||||
cache, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: cache.Memory{
|
||||
NumCounters: 10 * 1000,
|
||||
|
||||
53
pkg/cache/memorycache/telemetry.go
vendored
53
pkg/cache/memorycache/telemetry.go
vendored
@@ -7,18 +7,17 @@ import (
|
||||
|
||||
type telemetry struct {
|
||||
cacheRatio metric.Float64ObservableGauge
|
||||
cacheHits metric.Int64ObservableCounter
|
||||
cacheMisses metric.Int64ObservableCounter
|
||||
costAdded metric.Int64ObservableCounter
|
||||
costEvicted metric.Int64ObservableCounter
|
||||
keysAdded metric.Int64ObservableCounter
|
||||
keysEvicted metric.Int64ObservableCounter
|
||||
keysUpdated metric.Int64ObservableCounter
|
||||
setsDropped metric.Int64ObservableCounter
|
||||
setsRejected metric.Int64ObservableCounter
|
||||
getsDropped metric.Int64ObservableCounter
|
||||
getsKept metric.Int64ObservableCounter
|
||||
costUsed metric.Int64ObservableGauge
|
||||
cacheHits metric.Int64ObservableGauge
|
||||
cacheMisses metric.Int64ObservableGauge
|
||||
costAdded metric.Int64ObservableGauge
|
||||
costEvicted metric.Int64ObservableGauge
|
||||
keysAdded metric.Int64ObservableGauge
|
||||
keysEvicted metric.Int64ObservableGauge
|
||||
keysUpdated metric.Int64ObservableGauge
|
||||
setsDropped metric.Int64ObservableGauge
|
||||
setsRejected metric.Int64ObservableGauge
|
||||
getsDropped metric.Int64ObservableGauge
|
||||
getsKept metric.Int64ObservableGauge
|
||||
totalCost metric.Int64ObservableGauge
|
||||
}
|
||||
|
||||
@@ -29,67 +28,62 @@ func newMetrics(meter metric.Meter) (*telemetry, error) {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
cacheHits, err := meter.Int64ObservableCounter("signoz.cache.hits", metric.WithDescription("Hits is the number of Get calls where a value was found for the corresponding key."))
|
||||
cacheHits, err := meter.Int64ObservableGauge("signoz.cache.hits", metric.WithDescription("Hits is the number of Get calls where a value was found for the corresponding key."))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
cacheMisses, err := meter.Int64ObservableCounter("signoz.cache.misses", metric.WithDescription("Misses is the number of Get calls where a value was not found for the corresponding key"))
|
||||
cacheMisses, err := meter.Int64ObservableGauge("signoz.cache.misses", metric.WithDescription("Misses is the number of Get calls where a value was not found for the corresponding key"))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
costAdded, err := meter.Int64ObservableCounter("signoz.cache.cost.added", metric.WithDescription("CostAdded is the sum of costs that have been added (successful Set calls)"))
|
||||
costAdded, err := meter.Int64ObservableGauge("signoz.cache.cost.added", metric.WithDescription("CostAdded is the sum of costs that have been added (successful Set calls)"))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
costEvicted, err := meter.Int64ObservableCounter("signoz.cache.cost.evicted", metric.WithDescription("CostEvicted is the sum of all costs that have been evicted"))
|
||||
costEvicted, err := meter.Int64ObservableGauge("signoz.cache.cost.evicted", metric.WithDescription("CostEvicted is the sum of all costs that have been evicted"))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
keysAdded, err := meter.Int64ObservableCounter("signoz.cache.keys.added", metric.WithDescription("KeysAdded is the total number of Set calls where a new key-value item was added"))
|
||||
keysAdded, err := meter.Int64ObservableGauge("signoz.cache.keys.added", metric.WithDescription("KeysAdded is the total number of Set calls where a new key-value item was added"))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
keysEvicted, err := meter.Int64ObservableCounter("signoz.cache.keys.evicted", metric.WithDescription("KeysEvicted is the total number of keys evicted"))
|
||||
keysEvicted, err := meter.Int64ObservableGauge("signoz.cache.keys.evicted", metric.WithDescription("KeysEvicted is the total number of keys evicted"))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
keysUpdated, err := meter.Int64ObservableCounter("signoz.cache.keys.updated", metric.WithDescription("KeysUpdated is the total number of Set calls where the value was updated"))
|
||||
keysUpdated, err := meter.Int64ObservableGauge("signoz.cache.keys.updated", metric.WithDescription("KeysUpdated is the total number of Set calls where the value was updated"))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
setsDropped, err := meter.Int64ObservableCounter("signoz.cache.sets.dropped", metric.WithDescription("SetsDropped is the number of Set calls that don't make it into internal buffers (due to contention or some other reason)"))
|
||||
setsDropped, err := meter.Int64ObservableGauge("signoz.cache.sets.dropped", metric.WithDescription("SetsDropped is the number of Set calls that don't make it into internal buffers (due to contention or some other reason)"))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
setsRejected, err := meter.Int64ObservableCounter("signoz.cache.sets.rejected", metric.WithDescription("SetsRejected is the number of Set calls rejected by the policy (TinyLFU)"))
|
||||
setsRejected, err := meter.Int64ObservableGauge("signoz.cache.sets.rejected", metric.WithDescription("SetsRejected is the number of Set calls rejected by the policy (TinyLFU)"))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
getsDropped, err := meter.Int64ObservableCounter("signoz.cache.gets.dropped", metric.WithDescription("GetsDropped is the number of Get calls that don't make it into internal buffers (due to contention or some other reason)"))
|
||||
getsDropped, err := meter.Int64ObservableGauge("signoz.cache.gets.dropped", metric.WithDescription("GetsDropped is the number of Get calls that don't make it into internal buffers (due to contention or some other reason)"))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
getsKept, err := meter.Int64ObservableCounter("signoz.cache.gets.kept", metric.WithDescription("GetsKept is the number of Get calls that make it into internal buffers"))
|
||||
getsKept, err := meter.Int64ObservableGauge("signoz.cache.gets.kept", metric.WithDescription("GetsKept is the number of Get calls that make it into internal buffers"))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
costUsed, err := meter.Int64ObservableGauge("signoz.cache.cost.used", metric.WithDescription("CostUsed is the current retained cost in the cache (CostAdded - CostEvicted)."))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
totalCost, err := meter.Int64ObservableGauge("signoz.cache.total.cost", metric.WithDescription("TotalCost is the configured MaxCost ceiling for the cache."))
|
||||
totalCost, err := meter.Int64ObservableGauge("signoz.cache.total.cost", metric.WithDescription("TotalCost is the available cost configured for the cache"))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
@@ -111,7 +105,6 @@ func newMetrics(meter metric.Meter) (*telemetry, error) {
|
||||
setsRejected: setsRejected,
|
||||
getsDropped: getsDropped,
|
||||
getsKept: getsKept,
|
||||
costUsed: costUsed,
|
||||
totalCost: totalCost,
|
||||
}, nil
|
||||
}
|
||||
|
||||
4
pkg/cache/rediscache/provider_test.go
vendored
4
pkg/cache/rediscache/provider_test.go
vendored
@@ -29,10 +29,6 @@ func (cacheable *CacheableA) Clone() cachetypes.Cacheable {
|
||||
}
|
||||
}
|
||||
|
||||
func (cacheable *CacheableA) Cost() int64 {
|
||||
return int64(len(cacheable.Key)) + 16
|
||||
}
|
||||
|
||||
func (cacheable *CacheableA) MarshalBinary() ([]byte, error) {
|
||||
return json.Marshal(cacheable)
|
||||
}
|
||||
|
||||
@@ -335,8 +335,10 @@ func (q *querier) applyFormulas(ctx context.Context, results map[string]*qbtypes
|
||||
}
|
||||
case qbtypes.RequestTypeScalar:
|
||||
result := q.processScalarFormula(ctx, results, formula, req)
|
||||
// For scalar results, apply limit by processScalarFormula itself since it needs to be applied before converting back to scalar format
|
||||
results[name] = result
|
||||
if result != nil {
|
||||
result = q.applySeriesLimit(result, formula.Limit, formula.Order)
|
||||
results[name] = result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -524,9 +526,6 @@ func (q *querier) processScalarFormula(
|
||||
return nil
|
||||
}
|
||||
|
||||
// Apply ordering (and limit) before converting to scalar format.
|
||||
formulaSeries = qbtypes.ApplySeriesLimit(formulaSeries, formula.Order, formula.Limit)
|
||||
|
||||
// Convert back to scalar format
|
||||
scalarResult := &qbtypes.ScalarData{
|
||||
QueryName: formula.Name,
|
||||
|
||||
@@ -1,155 +1,15 @@
|
||||
package querier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
// scalarInputResult builds a ScalarData result with one group column ("service")
|
||||
// and one aggregation column ("__result"), holding the provided (service, value) rows.
|
||||
func scalarInputResult(queryName string, rows []struct {
|
||||
service string
|
||||
value float64
|
||||
}) *qbtypes.Result {
|
||||
serviceKey := telemetrytypes.TelemetryFieldKey{
|
||||
Name: "service",
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}
|
||||
resultKey := telemetrytypes.TelemetryFieldKey{
|
||||
Name: "__result",
|
||||
FieldDataType: telemetrytypes.FieldDataTypeFloat64,
|
||||
}
|
||||
|
||||
data := make([][]any, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
data = append(data, []any{r.service, r.value})
|
||||
}
|
||||
|
||||
return &qbtypes.Result{
|
||||
Value: &qbtypes.ScalarData{
|
||||
QueryName: queryName,
|
||||
Columns: []*qbtypes.ColumnDescriptor{
|
||||
{
|
||||
TelemetryFieldKey: serviceKey,
|
||||
QueryName: queryName,
|
||||
Type: qbtypes.ColumnTypeGroup,
|
||||
},
|
||||
{
|
||||
TelemetryFieldKey: resultKey,
|
||||
QueryName: queryName,
|
||||
AggregationIndex: 0,
|
||||
Type: qbtypes.ColumnTypeAggregation,
|
||||
},
|
||||
},
|
||||
Data: data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessScalarFormula_AppliesOrderAndLimit(t *testing.T) {
|
||||
q := &querier{
|
||||
logger: instrumentationtest.New().Logger(),
|
||||
}
|
||||
|
||||
// Mimic what a dashboard emits: orderBy keyed by the formula name ("F1"),
|
||||
// which applyFormulas rewrites to __result before sorting.
|
||||
orderByFormula := func(name string, dir qbtypes.OrderDirection) []qbtypes.OrderBy {
|
||||
return []qbtypes.OrderBy{
|
||||
{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: name,
|
||||
},
|
||||
},
|
||||
Direction: dir,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// A+B per service: a=101, b=11, c=2
|
||||
makeInputs := func() map[string]*qbtypes.Result {
|
||||
return map[string]*qbtypes.Result{
|
||||
"A": scalarInputResult("A", []struct {
|
||||
service string
|
||||
value float64
|
||||
}{
|
||||
{"a", 100},
|
||||
{"b", 10},
|
||||
{"c", 1},
|
||||
}),
|
||||
"B": scalarInputResult("B", []struct {
|
||||
service string
|
||||
value float64
|
||||
}{
|
||||
{"a", 1},
|
||||
{"b", 0},
|
||||
{"c", 1},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
makeReq := func(formula qbtypes.QueryBuilderFormula) *qbtypes.QueryRangeRequest {
|
||||
return &qbtypes.QueryRangeRequest{
|
||||
RequestType: qbtypes.RequestTypeScalar,
|
||||
CompositeQuery: qbtypes.CompositeQuery{
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{Type: qbtypes.QueryTypeBuilder, Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{Name: "A"}},
|
||||
{Type: qbtypes.QueryTypeBuilder, Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{Name: "B"}},
|
||||
{Type: qbtypes.QueryTypeFormula, Spec: formula},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("F1 desc with limit truncates and sorts", func(t *testing.T) {
|
||||
formula := qbtypes.QueryBuilderFormula{
|
||||
Name: "F1",
|
||||
Expression: "A + B",
|
||||
Order: orderByFormula("F1", qbtypes.OrderDirectionDesc),
|
||||
Limit: 2,
|
||||
}
|
||||
|
||||
out := q.applyFormulas(context.Background(), makeInputs(), makeReq(formula))
|
||||
got, ok := out["F1"]
|
||||
require.True(t, ok, "formula result missing")
|
||||
scalar, ok := got.Value.(*qbtypes.ScalarData)
|
||||
require.True(t, ok, "expected *ScalarData, got %T", got.Value)
|
||||
|
||||
// Limit=2 + F1 desc: the two largest __result rows in descending order.
|
||||
require.Len(t, scalar.Data, 2, "limit=2 was ignored before the fix")
|
||||
require.Equal(t, "a", scalar.Data[0][0])
|
||||
require.InDelta(t, 101.0, scalar.Data[0][1].(float64), 1e-9)
|
||||
require.Equal(t, "b", scalar.Data[1][0])
|
||||
require.InDelta(t, 10.0, scalar.Data[1][1].(float64), 1e-9)
|
||||
})
|
||||
|
||||
t.Run("F1 desc without limit sorts all rows", func(t *testing.T) {
|
||||
formula := qbtypes.QueryBuilderFormula{
|
||||
Name: "F1",
|
||||
Expression: "A / B",
|
||||
Order: orderByFormula("F1", qbtypes.OrderDirectionAsc),
|
||||
}
|
||||
|
||||
out := q.applyFormulas(context.Background(), makeInputs(), makeReq(formula))
|
||||
got, ok := out["F1"]
|
||||
require.True(t, ok)
|
||||
scalar, ok := got.Value.(*qbtypes.ScalarData)
|
||||
require.True(t, ok)
|
||||
|
||||
require.Len(t, scalar.Data, 2)
|
||||
require.Equal(t, "c", scalar.Data[0][0])
|
||||
require.InDelta(t, 1.0, scalar.Data[0][1].(float64), 1e-9)
|
||||
require.Equal(t, "a", scalar.Data[1][0])
|
||||
require.InDelta(t, 100.0, scalar.Data[1][1].(float64), 1e-9)
|
||||
})
|
||||
}
|
||||
|
||||
// Multiple series with different number of labels, shouldn't panic and should align labels correctly.
|
||||
func TestConvertTimeSeriesDataToScalar_RaggedLabels(t *testing.T) {
|
||||
label := func(name string, value any) *qbtypes.Label {
|
||||
|
||||
@@ -769,13 +769,6 @@ func ParseQueryRangeParams(r *http.Request) (*v3.QueryRangeParamsV3, *model.ApiE
|
||||
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
||||
}
|
||||
|
||||
// Clamp the top-level Step for PromQL
|
||||
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypePromQL {
|
||||
if minStep := common.MinAllowedStepInterval(queryRangeParams.Start, queryRangeParams.End); queryRangeParams.Step < minStep {
|
||||
queryRangeParams.Step = minStep
|
||||
}
|
||||
}
|
||||
|
||||
// prepare the variables for the corresponding query type
|
||||
formattedVars := make(map[string]interface{})
|
||||
for name, value := range queryRangeParams.Variables {
|
||||
|
||||
@@ -41,11 +41,6 @@ func (c *GetWaterfallSpansForTraceWithMetadataCache) Clone() cachetypes.Cacheabl
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GetWaterfallSpansForTraceWithMetadataCache) Cost() int64 {
|
||||
const perSpanBytes = 256
|
||||
return int64(c.TotalSpans) * perSpanBytes
|
||||
}
|
||||
|
||||
func (c *GetWaterfallSpansForTraceWithMetadataCache) MarshalBinary() (data []byte, err error) {
|
||||
return json.Marshal(c)
|
||||
}
|
||||
@@ -71,16 +66,6 @@ func (c *GetFlamegraphSpansForTraceCache) Clone() cachetypes.Cacheable {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GetFlamegraphSpansForTraceCache) Cost() int64 {
|
||||
const perSpanBytes = 128
|
||||
var spans int64
|
||||
for _, row := range c.SelectedSpans {
|
||||
spans += int64(len(row))
|
||||
}
|
||||
spans += int64(len(c.TraceRoots))
|
||||
return spans * perSpanBytes
|
||||
}
|
||||
|
||||
func (c *GetFlamegraphSpansForTraceCache) MarshalBinary() (data []byte, err error) {
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
@@ -203,8 +203,6 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewMigrateMetaresourcesTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddTagsFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddRoleCRUDTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddIntegrationDashboardsFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewMigrateCloudIntegrationDashboardsFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type addIntegrationDashboards struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
sqlschema sqlschema.SQLSchema
|
||||
}
|
||||
|
||||
func NewAddIntegrationDashboardsFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(
|
||||
factory.MustNewName("add_integration_dashboard"),
|
||||
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addIntegrationDashboards{sqlstore: sqlstore, sqlschema: sqlschema}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (m *addIntegrationDashboards) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(m.Up, m.Down)
|
||||
}
|
||||
|
||||
func (m *addIntegrationDashboards) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
// dashboard_id is knowingly kept loosing coupled with dashboard's id and is not a foreign key to dashboard's id.
|
||||
sqls := m.sqlschema.Operator().CreateTable(&sqlschema.Table{
|
||||
Name: "integration_dashboard",
|
||||
Columns: []*sqlschema.Column{
|
||||
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "dashboard_id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "provider", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "slug", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
|
||||
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
|
||||
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
},
|
||||
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{
|
||||
ColumnNames: []sqlschema.ColumnName{"id"},
|
||||
},
|
||||
})
|
||||
sqls = append(sqls, m.sqlschema.Operator().CreateIndex(
|
||||
&sqlschema.UniqueIndex{
|
||||
TableName: "integration_dashboard",
|
||||
ColumnNames: []sqlschema.ColumnName{"dashboard_id"},
|
||||
},
|
||||
)...)
|
||||
|
||||
for _, sql := range sqls {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *addIntegrationDashboards) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,851 +0,0 @@
|
||||
{
|
||||
"description": "View key AWS ECS metrics with an out of the box dashboard.\n",
|
||||
"image":"data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%3Csvg%20width%3D%2280px%22%20height%3D%2280px%22%20viewBox%3D%220%200%2080%2080%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%3C!--%20Generator%3A%20Sketch%2064%20(93537)%20-%20https%3A%2F%2Fsketch.com%20--%3E%3Ctitle%3EIcon-Architecture%2F64%2FArch_Amazon-Elastic-Container-Service_64%3C%2Ftitle%3E%3Cdesc%3ECreated%20with%20Sketch.%3C%2Fdesc%3E%3Cdefs%3E%3ClinearGradient%20x1%3D%220%25%22%20y1%3D%22100%25%22%20x2%3D%22100%25%22%20y2%3D%220%25%22%20id%3D%22linearGradient-1%22%3E%3Cstop%20stop-color%3D%22%23C8511B%22%20offset%3D%220%25%22%3E%3C%2Fstop%3E%3Cstop%20stop-color%3D%22%23FF9900%22%20offset%3D%22100%25%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Cg%20id%3D%22Icon-Architecture%2F64%2FArch_Amazon-Elastic-Container-Service_64%22%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cg%20id%3D%22Icon-Architecture-BG%2F64%2FContainers%22%20fill%3D%22url(%23linearGradient-1)%22%3E%3Crect%20id%3D%22Rectangle%22%20x%3D%220%22%20y%3D%220%22%20width%3D%2280%22%20height%3D%2280%22%3E%3C%2Frect%3E%3C%2Fg%3E%3Cpath%20d%3D%22M64%2C48.2340095%20L56%2C43.4330117%20L56%2C32.0000169%20C56%2C31.6440171%2055.812%2C31.3150172%2055.504%2C31.1360173%20L44%2C24.4260204%20L44%2C14.7520248%20L64%2C26.5710194%20L64%2C48.2340095%20Z%20M65.509%2C25.13902%20L43.509%2C12.139026%20C43.199%2C11.9560261%2042.818%2C11.9540261%2042.504%2C12.131026%20C42.193%2C12.3090259%2042%2C12.6410257%2042%2C13.0000256%20L42%2C25.0000201%20C42%2C25.3550199%2042.189%2C25.6840198%2042.496%2C25.8640197%20L54%2C32.5740166%20L54%2C44.0000114%20C54%2C44.3510113%2054.185%2C44.6770111%2054.486%2C44.857011%20L64.486%2C50.8570083%20C64.644%2C50.9520082%2064.822%2C51%2065%2C51%20C65.17%2C51%2065.34%2C50.9570082%2065.493%2C50.8700083%20C65.807%2C50.6930084%2066%2C50.3600085%2066%2C50%20L66%2C26.0000196%20C66%2C25.6460198%2065.814%2C25.31902%2065.509%2C25.13902%20L65.509%2C25.13902%20Z%20M40.445%2C66.863001%20L17%2C54.3990067%20L17%2C26.5710194%20L37%2C14.7520248%20L37%2C24.4510204%20L26.463%2C31.1560173%20C26.175%2C31.3400172%2026%2C31.6580171%2026%2C32.0000169%20L26%2C49.0000091%20C26%2C49.373009%2026.208%2C49.7150088%2026.538%2C49.8870087%20L39.991%2C56.8870055%20C40.28%2C57.0370055%2040.624%2C57.0380055%2040.912%2C56.8880055%20L53.964%2C50.1440086%20L61.996%2C54.9640064%20L40.445%2C66.863001%20Z%20M64.515%2C54.1420068%20L54.515%2C48.1420095%20C54.217%2C47.9640096%2053.849%2C47.9520096%2053.541%2C48.1120095%20L40.455%2C54.8730065%20L28%2C48.3930094%20L28%2C32.5490167%20L38.537%2C25.8440197%20C38.825%2C25.6600198%2039%2C25.3420199%2039%2C25.0000201%20L39%2C13.0000256%20C39%2C12.6410257%2038.808%2C12.3090259%2038.496%2C12.131026%20C38.184%2C11.9540261%2037.802%2C11.9560261%2037.491%2C12.139026%20L15.491%2C25.13902%20C15.187%2C25.31902%2015%2C25.6460198%2015%2C26.0000196%20L15%2C55%20C15%2C55.3690062%2015.204%2C55.7090061%2015.53%2C55.883006%20L39.984%2C68.8830001%20C40.131%2C68.961%2040.292%2C69%2040.453%2C69%20C40.62%2C69%2040.786%2C68.958%2040.937%2C68.8750001%20L64.484%2C55.875006%20C64.797%2C55.7020061%2064.993%2C55.3750062%2065.0001416%2C55.0180064%20C65.006%2C54.6600066%2064.821%2C54.3260067%2064.515%2C54.1420068%20L64.515%2C54.1420068%20Z%22%20id%3D%22Amazon-Elastic-Container-Service_Icon_64_Squid%22%20fill%3D%22%23FFFFFF%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E",
|
||||
"layout": [
|
||||
{
|
||||
"h": 6,
|
||||
"i": "f78becf8-0328-48b4-84b6-ff4dac325940",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"h": 6,
|
||||
"i": "2b4eac06-b426-4f78-b874-2e1734c4104b",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"h": 6,
|
||||
"i": "5bea2bc0-13a2-4937-bccb-60ffe8a43ad5",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 6
|
||||
},
|
||||
{
|
||||
"h": 6,
|
||||
"i": "6fac67b0-50ec-4b43-ac4b-320a303d0369",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 6
|
||||
}
|
||||
],
|
||||
"panelMap": {},
|
||||
"tags": [],
|
||||
"title": "AWS ECS Overview",
|
||||
"uploadedGrafana": false,
|
||||
"variables": {
|
||||
"51f4fa2b-89c7-47c2-9795-f32cffaab985": {
|
||||
"allSelected": false,
|
||||
"customValue": "",
|
||||
"description": "AWS Account ID",
|
||||
"id": "51f4fa2b-89c7-47c2-9795-f32cffaab985",
|
||||
"key": "51f4fa2b-89c7-47c2-9795-f32cffaab985",
|
||||
"modificationUUID": "7b814d17-8fff-4ed6-a4ea-90e3b1a97584",
|
||||
"multiSelect": false,
|
||||
"name": "Account",
|
||||
"order": 0,
|
||||
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.account.id') AS `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' GROUP BY `cloud.account.id`",
|
||||
"showALLOption": false,
|
||||
"sort": "DISABLED",
|
||||
"textboxValue": "",
|
||||
"type": "QUERY"
|
||||
},
|
||||
"9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0": {
|
||||
"allSelected": false,
|
||||
"customValue": "",
|
||||
"description": "Account Region",
|
||||
"id": "9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0",
|
||||
"key": "9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0",
|
||||
"modificationUUID": "3b5f499b-22a3-4c8a-847c-8d3811c9e6b2",
|
||||
"multiSelect": false,
|
||||
"name": "Region",
|
||||
"order": 1,
|
||||
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} GROUP BY region",
|
||||
"showALLOption": false,
|
||||
"sort": "ASC",
|
||||
"textboxValue": "",
|
||||
"type": "QUERY"
|
||||
},
|
||||
"bfbdbcbe-a168-4d81-b108-36339e249116": {
|
||||
"allSelected": true,
|
||||
"customValue": "",
|
||||
"description": "ECS Cluster Name",
|
||||
"id": "bfbdbcbe-a168-4d81-b108-36339e249116",
|
||||
"key": "bfbdbcbe-a168-4d81-b108-36339e249116",
|
||||
"modificationUUID": "9fb0d63c-ac6c-497d-82b3-17d95944e245",
|
||||
"multiSelect": true,
|
||||
"name": "Cluster",
|
||||
"order": 2,
|
||||
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'ClusterName') AS cluster\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} AND JSONExtractString(labels, 'cloud.region') IN {{.Region}}\nGROUP BY cluster",
|
||||
"showALLOption": true,
|
||||
"sort": "ASC",
|
||||
"textboxValue": "",
|
||||
"type": "QUERY"
|
||||
}
|
||||
},
|
||||
"version": "v4",
|
||||
"widgets": [
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "f78becf8-0328-48b4-84b6-ff4dac325940",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_ECS_MemoryUtilization_max--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_ECS_MemoryUtilization_max",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "max",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "26ac617d",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "57172ed9",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.account.id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.account.id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
},
|
||||
{
|
||||
"id": "49b9f85e",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Cluster"
|
||||
]
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ServiceName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ServiceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{ServiceName}} ({{ClusterName}})",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "max",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "max"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "56068fdd-d523-4117-92fa-87c6518ad07c",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Maximum Memory Utilization",
|
||||
"yAxisUnit": "none"
|
||||
},
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "2b4eac06-b426-4f78-b874-2e1734c4104b",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_ECS_MemoryUtilization_min--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_ECS_MemoryUtilization_min",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "min",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "cd4b8848",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "aa5115c6",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.account.id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.account.id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
},
|
||||
{
|
||||
"id": "f60677b6",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Cluster"
|
||||
]
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ServiceName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ServiceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{ServiceName}} ({{ClusterName}})",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "min",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "min"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "fb19342e-cbde-40d8-b12f-ad108698356b",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Minimum Memory Utilization",
|
||||
"yAxisUnit": "none"
|
||||
},
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "5bea2bc0-13a2-4937-bccb-60ffe8a43ad5",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_ECS_CPUUtilization_max--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_ECS_CPUUtilization_max",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "max",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "2c13c8ee",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "f489f6a8",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.account.id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.account.id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
},
|
||||
{
|
||||
"id": "94012320",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Cluster"
|
||||
]
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ServiceName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ServiceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{ServiceName}} ({{ClusterName}})",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "max",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "max"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "273e0a76-c780-4b9a-9b03-2649d4227173",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Maximum CPU Utilization",
|
||||
"yAxisUnit": "none"
|
||||
},
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "6fac67b0-50ec-4b43-ac4b-320a303d0369",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_ECS_CPUUtilization_min--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_ECS_CPUUtilization_min",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "min",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "758ba906",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "4ffe6bf7",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.account.id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.account.id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
},
|
||||
{
|
||||
"id": "53d98059",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Cluster"
|
||||
]
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ServiceName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ServiceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "ClusterName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "ClusterName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{ServiceName}} ({{ClusterName}})",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "min",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "min"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "c89482b3-5a98-4e2c-be0d-ef036d7dac05",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Minimum CPU Utilization",
|
||||
"yAxisUnit": "none"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1,818 +0,0 @@
|
||||
{
|
||||
"description": "View key AWS SNS metrics with an out of the box dashboard.",
|
||||
"image": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iODBweCIgaGVpZ2h0PSI4MHB4IiB2aWV3Qm94PSIwIDAgODAgODAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDY0ICg5MzUzNykgLSBodHRwczovL3NrZXRjaC5jb20gLS0+CiAgICA8dGl0bGU+SWNvbi1BcmNoaXRlY3R1cmUvNjQvQXJjaF9BV1MtU2ltcGxlLU5vdGlmaWNhdGlvbi1TZXJ2aWNlXzY0PC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+CiAgICAgICAgPGxpbmVhckdyYWRpZW50IHgxPSIwJSIgeTE9IjEwMCUiIHgyPSIxMDAlIiB5Mj0iMCUiIGlkPSJsaW5lYXJHcmFkaWVudC0xIj4KICAgICAgICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iI0IwMDg0RCIgb2Zmc2V0PSIwJSI+PC9zdG9wPgogICAgICAgICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjRkY0RjhCIiBvZmZzZXQ9IjEwMCUiPjwvc3RvcD4KICAgICAgICA8L2xpbmVhckdyYWRpZW50PgogICAgPC9kZWZzPgogICAgPGcgaWQ9Ikljb24tQXJjaGl0ZWN0dXJlLzY0L0FyY2hfQVdTLVNpbXBsZS1Ob3RpZmljYXRpb24tU2VydmljZV82NCIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgaWQ9Ikljb24tQXJjaGl0ZWN0dXJlLUJHLzY0L0FwcGxpY2F0aW9uLUludGVncmF0aW9uIiBmaWxsPSJ1cmwoI2xpbmVhckdyYWRpZW50LTEpIj4KICAgICAgICAgICAgPHJlY3QgaWQ9IlJlY3RhbmdsZSIgeD0iMCIgeT0iMCIgd2lkdGg9IjgwIiBoZWlnaHQ9IjgwIj48L3JlY3Q+CiAgICAgICAgPC9nPgogICAgICAgIDxwYXRoIGQ9Ik0xNywzOCBDMTguMTAzLDM4IDE5LDM4Ljg5NyAxOSw0MCBDMTksNDEuMTAzIDE4LjEwMyw0MiAxNyw0MiBDMTUuODk3LDQyIDE1LDQxLjEwMyAxNSw0MCBDMTUsMzguODk3IDE1Ljg5NywzOCAxNywzOCBMMTcsMzggWiBNNDEsNjQgQzI5LjMxNCw2NCAxOS4yODksNTUuNDY2IDE3LjE5NCw0My45OCBDMTguOTY1LDQzLjg5NCAyMC40MjcsNDIuNjU5IDIwLjg1Nyw0MSBMMjcsNDEgTDI3LDM5IEwyMC44NTcsMzkgQzIwLjQyNywzNy4zNDIgMTguOTY2LDM2LjEwNyAxNy4xOTUsMzYuMDIgQzE5LjI4NSwyNC43MSAyOS41MTEsMTYgNDEsMTYgQzQ1LjMxMywxNiA0OS44MzIsMTcuNjIyIDU0LjQyOSwyMC44MjEgTDU1LjU3MSwxOS4xNzkgQzUwLjYzMywxNS43NDMgNDUuNzMsMTQgNDEsMTQgQzI4LjI3LDE0IDE2Ljk0OSwyMy44NjUgMTUuMDYzLDM2LjUyMSBDMTMuODM5LDM3LjIwNyAxMywzOC41IDEzLDQwIEMxMyw0MS41IDEzLjgzOSw0Mi43OTMgMTUuMDYzLDQzLjQ3OCBDMTYuOTcsNTYuMzQxIDI4LjA1Niw2NiA0MSw2NiBDNDYuNDA3LDY2IDUxLjk0Miw2NC4xNTcgNTYuNTg1LDYwLjgxMSBMNTUuNDE1LDU5LjE4OSBDNTEuMTEsNjIuMjkyIDQ1Ljk5MSw2NCA0MSw2NCBMNDEsNjQgWiBNMzAuMTAxLDM2LjQ0MiBDMzEuOTU1LDM2Ljg5NSAzNC4yNzUsMzcgMzYsMzcgQzM3LjY0MiwzNyAzOS44MjMsMzYuOTA1IDQxLjYyOSwzNi41MDYgTDM3LjEwNSw0NS41NTMgQzM3LjAzNiw0NS42OTEgMzcsNDUuODQ1IDM3LDQ2IEwzNyw1MC40NTMgQzM2LjE5OSw1MC45NjQgMzQuODMzLDUxLjgxMiAzNCw1MS45ODYgTDM0LDQ2IEMzNCw0NS44NjggMzMuOTc0LDQ1LjczNyAzMy45MjMsNDUuNjE1IEwzMC4xMDEsMzYuNDQyIFogTTM2LDMzIEM0MC4wMjUsMzMgNDIuMTc0LDMzLjYwNCA0Mi44NDEsMzQgQzQyLjE3NCwzNC4zOTYgNDAuMDI1LDM1IDM2LDM1IEMzMS45NzUsMzUgMjkuODI2LDM0LjM5NiAyOS4xNTksMzQgQzI5LjgyNiwzMy42MDQgMzEuOTc1LDMzIDM2LDMzIEwzNiwzMyBaIE0zMyw1NCBMMzQsNTQgQzM0LjA0Myw1NCAzNC4wODYsNTMuOTk3IDM0LjEyOCw1My45OTIgQzM1LjM1Miw1My44MzMgMzYuOTA5LDUyLjg4NyAzOC4yNzIsNTIuMDEzIEwzOC41MzUsNTEuODQ1IEMzOC44MjQsNTEuNjYxIDM5LDUxLjM0MiAzOSw1MSBMMzksNDYuMjM2IEw0NC41NTksMzUuMTIgQzQ0LjgzMywzNC44MDEgNDUsMzQuNDM0IDQ1LDM0IEM0NSwzMS4zOSAzOS4zNjEsMzEgMzYsMzEgQzMyLjYzOSwzMSAyNywzMS4zOSAyNywzNCBDMjcsMzQuMzY2IDI3LjEyLDM0LjY4NCAyNy4zMiwzNC45NjcgTDMyLDQ2LjIgTDMyLDUzIEMzMiw1My41NTIgMzIuNDQ3LDU0IDMzLDU0IEwzMyw1NCBaIE02Miw1MyBDNjMuMTAzLDUzIDY0LDUzLjg5NyA2NCw1NSBDNjQsNTYuMTAzIDYzLjEwMyw1NyA2Miw1NyBDNjAuODk3LDU3IDYwLDU2LjEwMyA2MCw1NSBDNjAsNTMuODk3IDYwLjg5Nyw1MyA2Miw1MyBMNjIsNTMgWiBNNjIsMjMgQzYzLjEwMywyMyA2NCwyMy44OTcgNjQsMjUgQzY0LDI2LjEwMyA2My4xMDMsMjcgNjIsMjcgQzYwLjg5NywyNyA2MCwyNi4xMDMgNjAsMjUgQzYwLDIzLjg5NyA2MC44OTcsMjMgNjIsMjMgTDYyLDIzIFogTTY0LDM4IEM2NS4xMDMsMzggNjYsMzguODk3IDY2LDQwIEM2Niw0MS4xMDMgNjUuMTAzLDQyIDY0LDQyIEM2Mi44OTcsNDIgNjIsNDEuMTAzIDYyLDQwIEM2MiwzOC44OTcgNjIuODk3LDM4IDY0LDM4IEw2NCwzOCBaIE01NCw0MSBMNjAuMTQzLDQxIEM2MC41ODksNDIuNzIgNjIuMTQyLDQ0IDY0LDQ0IEM2Ni4yMDYsNDQgNjgsNDIuMjA2IDY4LDQwIEM2OCwzNy43OTQgNjYuMjA2LDM2IDY0LDM2IEM2Mi4xNDIsMzYgNjAuNTg5LDM3LjI4IDYwLjE0MywzOSBMNTQsMzkgTDU0LDI2IEw1OC4xNDMsMjYgQzU4LjU4OSwyNy43MiA2MC4xNDIsMjkgNjIsMjkgQzY0LjIwNiwyOSA2NiwyNy4yMDYgNjYsMjUgQzY2LDIyLjc5NCA2NC4yMDYsMjEgNjIsMjEgQzYwLjE0MiwyMSA1OC41ODksMjIuMjggNTguMTQzLDI0IEw1MywyNCBDNTIuNDQ3LDI0IDUyLDI0LjQ0OCA1MiwyNSBMNTIsMzkgTDQ1LDM5IEw0NSw0MSBMNTIsNDEgTDUyLDU1IEM1Miw1NS41NTIgNTIuNDQ3LDU2IDUzLDU2IEw1OC4xNDMsNTYgQzU4LjU4OSw1Ny43MiA2MC4xNDIsNTkgNjIsNTkgQzY0LjIwNiw1OSA2Niw1Ny4yMDYgNjYsNTUgQzY2LDUyLjc5NCA2NC4yMDYsNTEgNjIsNTEgQzYwLjE0Miw1MSA1OC41ODksNTIuMjggNTguMTQzLDU0IEw1NCw1NCBMNTQsNDEgWiIgaWQ9IkFXUy1TaW1wbGUtTm90aWZpY2F0aW9uLVNlcnZpY2VfSWNvbl82NF9TcXVpZCIgZmlsbD0iI0ZGRkZGRiI+PC9wYXRoPgogICAgPC9nPgo8L3N2Zz4=",
|
||||
"layout": [
|
||||
{
|
||||
"h": 6,
|
||||
"i": "4eb87f89-0213-4773-9b06-6aecc6701898",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"h": 6,
|
||||
"i": "7a010b4e-ea7c-4a45-a9eb-93af650c45b4",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"h": 6,
|
||||
"i": "2299d4e3-6c40-4bf2-a550-c7bb8a7acd38",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 6
|
||||
},
|
||||
{
|
||||
"h": 6,
|
||||
"i": "16eec8b7-de1a-4039-b180-24c7a6704b6e",
|
||||
"moved": false,
|
||||
"static": false,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 6
|
||||
}
|
||||
],
|
||||
"panelMap": {},
|
||||
"tags": [],
|
||||
"title": "SNS Overview",
|
||||
"uploadedGrafana": false,
|
||||
"variables": {
|
||||
"51f4fa2b-89c7-47c2-9795-f32cffaab985": {
|
||||
"allSelected": false,
|
||||
"customValue": "",
|
||||
"description": "AWS Account ID",
|
||||
"id": "51f4fa2b-89c7-47c2-9795-f32cffaab985",
|
||||
"key": "51f4fa2b-89c7-47c2-9795-f32cffaab985",
|
||||
"modificationUUID": "b7a6b06b-fa1f-4fb8-b70e-6bd9b350f29e",
|
||||
"multiSelect": false,
|
||||
"name": "Account",
|
||||
"order": 0,
|
||||
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.account.id') AS `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_SNS_PublishSize_count' GROUP BY `cloud.account.id`",
|
||||
"showALLOption": false,
|
||||
"sort": "DISABLED",
|
||||
"textboxValue": "",
|
||||
"type": "QUERY"
|
||||
},
|
||||
"9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0": {
|
||||
"allSelected": false,
|
||||
"customValue": "",
|
||||
"description": "Account Region",
|
||||
"id": "9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0",
|
||||
"key": "9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0",
|
||||
"modificationUUID": "8428a5de-bfd1-4a69-9601-63e3041cd556",
|
||||
"multiSelect": false,
|
||||
"name": "Region",
|
||||
"order": 1,
|
||||
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_SNS_PublishSize_count' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} GROUP BY region",
|
||||
"showALLOption": false,
|
||||
"sort": "ASC",
|
||||
"textboxValue": "",
|
||||
"type": "QUERY"
|
||||
},
|
||||
"bfbdbcbe-a168-4d81-b108-36339e249116": {
|
||||
"allSelected": true,
|
||||
"customValue": "",
|
||||
"description": "SNS Topic Name",
|
||||
"id": "bfbdbcbe-a168-4d81-b108-36339e249116",
|
||||
"modificationUUID": "dfed7272-16dc-4eb6-99bf-7c82fc8e04f0",
|
||||
"multiSelect": true,
|
||||
"name": "Topic",
|
||||
"order": 2,
|
||||
"queryValue": "SELECT DISTINCT JSONExtractString(labels, 'TopicName') AS topic\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_SNS_PublishSize_count' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} AND JSONExtractString(labels, 'cloud.region') IN {{.Region}}\nGROUP BY topic",
|
||||
"showALLOption": true,
|
||||
"sort": "ASC",
|
||||
"textboxValue": "",
|
||||
"type": "QUERY"
|
||||
}
|
||||
},
|
||||
"version": "v4",
|
||||
"widgets": [
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "4eb87f89-0213-4773-9b06-6aecc6701898",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_SNS_NumberOfMessagesPublished_max--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_SNS_NumberOfMessagesPublished_max",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "max",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "8fd51b53",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "TopicName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "TopicName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Topic"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "b18187c3",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "eebe4578",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.account.id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.account.id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "TopicName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "TopicName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{TopicName}}",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "max",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "max"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "9c67615a-55f7-42da-835c-86922f2ff8bb",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Number of Messages Published",
|
||||
"yAxisUnit": "none"
|
||||
},
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "7a010b4e-ea7c-4a45-a9eb-93af650c45b4",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_SNS_PublishSize_max--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_SNS_PublishSize_max",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "max",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "1aa0d1a9",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "TopicName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "TopicName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Topic"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "62255cff",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "17c7153e",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.account.id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.account.id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "TopicName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "TopicName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{TopicName}}",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "max",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "max"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "a635a15b-dfe6-4617-a82e-29d93e27deaf",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Published Message Size",
|
||||
"yAxisUnit": "decbytes"
|
||||
},
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "2299d4e3-6c40-4bf2-a550-c7bb8a7acd38",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_SNS_NumberOfNotificationsDelivered_max--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_SNS_NumberOfNotificationsDelivered_max",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "max",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "c96a4ac0",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "TopicName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "TopicName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Topic"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "8ca86829",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "8a444f66",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.account.id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.account.id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "TopicName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "TopicName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{TopicName}}",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "max",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "max"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "0d2fc26c-9b21-4dfc-b631-64b7c8d3bd71",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Number of Notifications Delivered",
|
||||
"yAxisUnit": "none"
|
||||
},
|
||||
{
|
||||
"bucketCount": 30,
|
||||
"bucketWidth": 0,
|
||||
"columnUnits": {},
|
||||
"description": "",
|
||||
"fillSpans": false,
|
||||
"id": "16eec8b7-de1a-4039-b180-24c7a6704b6e",
|
||||
"isLogScale": false,
|
||||
"isStacked": false,
|
||||
"mergeAllActiveQueries": false,
|
||||
"nullZeroValues": "zero",
|
||||
"opacity": "1",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregateAttribute": {
|
||||
"dataType": "float64",
|
||||
"id": "aws_SNS_NumberOfNotificationsFailed_max--float64--Gauge--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "aws_SNS_NumberOfNotificationsFailed_max",
|
||||
"type": "Gauge"
|
||||
},
|
||||
"aggregateOperator": "max",
|
||||
"dataSource": "metrics",
|
||||
"disabled": false,
|
||||
"expression": "A",
|
||||
"filters": {
|
||||
"items": [
|
||||
{
|
||||
"id": "6175f3d5",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "TopicName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "TopicName",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "in",
|
||||
"value": [
|
||||
"$Topic"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "e2084931",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.region--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.region",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Region"
|
||||
},
|
||||
{
|
||||
"id": "0b05209a",
|
||||
"key": {
|
||||
"dataType": "string",
|
||||
"id": "cloud.account.id--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "cloud.account.id",
|
||||
"type": "tag"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "$Account"
|
||||
}
|
||||
],
|
||||
"op": "AND"
|
||||
},
|
||||
"functions": [],
|
||||
"groupBy": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "TopicName--string--tag--false",
|
||||
"isColumn": false,
|
||||
"isJSON": false,
|
||||
"key": "TopicName",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"having": [],
|
||||
"legend": "{{TopicName}}",
|
||||
"limit": null,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "max",
|
||||
"stepInterval": 60,
|
||||
"timeAggregation": "max"
|
||||
}
|
||||
],
|
||||
"queryFormulas": []
|
||||
},
|
||||
"clickhouse_sql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"id": "526247af-6ac9-42ff-83e9-cce0e32a9e63",
|
||||
"promql": [
|
||||
{
|
||||
"disabled": false,
|
||||
"legend": "",
|
||||
"name": "A",
|
||||
"query": ""
|
||||
}
|
||||
],
|
||||
"queryType": "builder"
|
||||
},
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "body",
|
||||
"type": ""
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"name": "timestamp",
|
||||
"type": ""
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "serviceName--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "serviceName",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "name--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "name",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "float64",
|
||||
"id": "durationNano--float64--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "durationNano",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "httpMethod--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "httpMethod",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"dataType": "string",
|
||||
"id": "responseStatusCode--string--tag--true",
|
||||
"isColumn": true,
|
||||
"isJSON": false,
|
||||
"key": "responseStatusCode",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"softMax": 0,
|
||||
"softMin": 0,
|
||||
"stackedBarChart": false,
|
||||
"thresholds": [],
|
||||
"timePreferance": "GLOBAL_TIME",
|
||||
"title": "Number of Notifications Failed",
|
||||
"yAxisUnit": "none"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,282 +0,0 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
//go:embed 085_cloud_integration_dashboards
|
||||
var cloudIntegrationDashboardFiles embed.FS
|
||||
|
||||
var (
|
||||
CloudProviderAWS = valuer.NewString("aws")
|
||||
CloudProviderAzure = valuer.NewString("azure")
|
||||
)
|
||||
|
||||
type migrateCloudIntegrationDashboards struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
}
|
||||
|
||||
type cloudIntegrationAccountRow struct {
|
||||
bun.BaseModel `bun:"table:cloud_integration"`
|
||||
|
||||
ID string `bun:"id"`
|
||||
OrgID string `bun:"org_id"`
|
||||
Provider string `bun:"provider"`
|
||||
}
|
||||
|
||||
type cloudIntegrationServiceRow struct {
|
||||
bun.BaseModel `bun:"table:cloud_integration_service"`
|
||||
|
||||
Type string `bun:"type"`
|
||||
Config string `bun:"config"`
|
||||
CloudIntegrationID string `bun:"cloud_integration_id"`
|
||||
}
|
||||
|
||||
type cloudIntegrationServiceConfig struct {
|
||||
AWS *cloudIntegrationProviderConfig `json:"aws"`
|
||||
Azure *cloudIntegrationProviderConfig `json:"azure"`
|
||||
}
|
||||
|
||||
type cloudIntegrationProviderConfig struct {
|
||||
Metrics *cloudIntegrationMetricsConfig `json:"metrics"`
|
||||
}
|
||||
|
||||
type cloudIntegrationMetricsConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type cloudIntegrationDashboardRow struct {
|
||||
bun.BaseModel `bun:"table:dashboard"`
|
||||
|
||||
ID string `bun:"id,pk,type:text"`
|
||||
CreatedAt time.Time `bun:"created_at"`
|
||||
UpdatedAt time.Time `bun:"updated_at"`
|
||||
CreatedBy string `bun:"created_by,type:text"`
|
||||
UpdatedBy string `bun:"updated_by,type:text"`
|
||||
Data string `bun:"data,type:text"`
|
||||
Locked bool `bun:"locked"`
|
||||
OrgID string `bun:"org_id,type:text"`
|
||||
Source string `bun:"source,type:text"`
|
||||
}
|
||||
|
||||
type cloudIntegrationAccountMeta struct {
|
||||
orgID string
|
||||
provider string
|
||||
}
|
||||
|
||||
type cloudIntegrationOrgService struct {
|
||||
orgID string
|
||||
provider string
|
||||
serviceID string
|
||||
}
|
||||
|
||||
type integrationDashboardRow struct {
|
||||
bun.BaseModel `bun:"table:integration_dashboards"`
|
||||
|
||||
ID string `bun:"id,pk,type:text"`
|
||||
OrgID string `bun:"org_id,type:text"`
|
||||
DashboardID string `bun:"dashboard_id,type:text"`
|
||||
Provider string `bun:"provider,type:text"`
|
||||
Slug string `bun:"slug,type:text"`
|
||||
CreatedAt time.Time `bun:"created_at"`
|
||||
UpdatedAt time.Time `bun:"updated_at"`
|
||||
}
|
||||
|
||||
func NewMigrateCloudIntegrationDashboardsFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(
|
||||
factory.MustNewName("migrate_cloud_integration_dashboards"),
|
||||
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &migrateCloudIntegrationDashboards{sqlstore: sqlstore}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (m *migrateCloudIntegrationDashboards) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(m.Up, m.Down)
|
||||
}
|
||||
|
||||
func (m *migrateCloudIntegrationDashboards) Up(ctx context.Context, db *bun.DB) error {
|
||||
dashboardDefs, err := m.loadDashboardDefs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
var accounts []*cloudIntegrationAccountRow
|
||||
if err := tx.NewSelect().
|
||||
Model(&accounts).
|
||||
Where("removed_at IS NULL").
|
||||
Where("account_id IS NOT NULL").
|
||||
Scan(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
accountMap := make(map[string]cloudIntegrationAccountMeta, len(accounts))
|
||||
for _, a := range accounts {
|
||||
accountMap[a.ID] = cloudIntegrationAccountMeta{orgID: a.OrgID, provider: a.Provider}
|
||||
}
|
||||
|
||||
var services []*cloudIntegrationServiceRow
|
||||
if err := tx.NewSelect().Model(&services).Scan(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
var toProvision []cloudIntegrationOrgService
|
||||
|
||||
for _, svc := range services {
|
||||
meta, ok := accountMap[svc.CloudIntegrationID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
var cfg cloudIntegrationServiceConfig
|
||||
if err := json.Unmarshal([]byte(svc.Config), &cfg); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !m.isMetricsEnabled(&cfg, meta.provider) {
|
||||
continue
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s|%s|%s", meta.orgID, meta.provider, svc.Type)
|
||||
if _, dup := seen[key]; dup {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
toProvision = append(toProvision, cloudIntegrationOrgService{
|
||||
orgID: meta.orgID,
|
||||
provider: meta.provider,
|
||||
serviceID: svc.Type,
|
||||
})
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
for _, service := range toProvision {
|
||||
serviceDashboards, ok := dashboardDefs[service.provider][service.serviceID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for dashName, dashboardJSON := range serviceDashboards {
|
||||
slug := fmt.Sprintf("%s-%s-%s", service.provider, service.serviceID, dashName)
|
||||
|
||||
count, err := tx.NewSelect().
|
||||
TableExpr("integration_dashboard").
|
||||
Where("org_id = ?", service.orgID).
|
||||
Where("provider = ?", "cloud_integrations").
|
||||
Where("slug = ?", slug).
|
||||
Count(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
dashID := valuer.GenerateUUID().StringValue()
|
||||
|
||||
dashRow := &cloudIntegrationDashboardRow{
|
||||
ID: dashID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
CreatedBy: "",
|
||||
UpdatedBy: "",
|
||||
Data: string(dashboardJSON),
|
||||
Locked: true,
|
||||
OrgID: service.orgID,
|
||||
Source: "integration",
|
||||
}
|
||||
if _, err := tx.NewInsert().Model(dashRow).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
intRow := &integrationDashboardRow{
|
||||
ID: valuer.GenerateUUID().StringValue(),
|
||||
OrgID: service.orgID,
|
||||
DashboardID: dashID,
|
||||
Provider: "cloud_integration",
|
||||
Slug: slug,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if _, err := tx.NewInsert().Model(intRow).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (m *migrateCloudIntegrationDashboards) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *migrateCloudIntegrationDashboards) loadDashboardDefs() (map[string]map[string]map[string]json.RawMessage, error) {
|
||||
result := make(map[string]map[string]map[string]json.RawMessage)
|
||||
|
||||
err := fs.WalkDir(cloudIntegrationDashboardFiles, "085_cloud_integration_dashboards", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() || filepath.Ext(path) != ".json" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// path: 085_cloud_integration_dashboards/{provider}/{service}/{file}.json
|
||||
rel := strings.TrimPrefix(path, "085_cloud_integration_dashboards/")
|
||||
parts := strings.SplitN(rel, "/", 3)
|
||||
if len(parts) != 3 {
|
||||
return nil
|
||||
}
|
||||
provider := parts[0]
|
||||
serviceID := parts[1]
|
||||
dashName := strings.TrimSuffix(parts[2], ".json")
|
||||
|
||||
data, err := cloudIntegrationDashboardFiles.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result[provider] == nil {
|
||||
result[provider] = make(map[string]map[string]json.RawMessage)
|
||||
}
|
||||
if result[provider][serviceID] == nil {
|
||||
result[provider][serviceID] = make(map[string]json.RawMessage)
|
||||
}
|
||||
result[provider][serviceID][dashName] = json.RawMessage(data)
|
||||
return nil
|
||||
})
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (m *migrateCloudIntegrationDashboards) isMetricsEnabled(cfg *cloudIntegrationServiceConfig, provider string) bool {
|
||||
switch provider {
|
||||
case CloudProviderAWS.String():
|
||||
return cfg.AWS != nil && cfg.AWS.Metrics != nil && cfg.AWS.Metrics.Enabled
|
||||
case CloudProviderAzure.String():
|
||||
return cfg.Azure != nil && cfg.Azure.Metrics != nil && cfg.Azure.Metrics.Enabled
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -18,10 +18,6 @@ type Cloneable interface {
|
||||
// Creates a deep copy of the Cacheable. This method is useful for memory caches to avoid the need for serialization/deserialization. It also prevents
|
||||
// race conditions in the memory cache.
|
||||
Clone() Cacheable
|
||||
// Cost returns the weight of this entry for cost-based cache accounting
|
||||
// and eviction. Typically derived from the approximate retained byte size,
|
||||
// but the value represents cache cost, not literal bytes.
|
||||
Cost() int64
|
||||
}
|
||||
|
||||
func NewSha1CacheKey(val string) string {
|
||||
|
||||
@@ -59,21 +59,3 @@ func (c *CachedData) Clone() cachetypes.Cacheable {
|
||||
|
||||
return clonedCachedData
|
||||
}
|
||||
|
||||
// Cost approximates the retained bytes of this CachedData for use as the
|
||||
// ristretto cache cost. The dominant contributor is the serialized bucket
|
||||
// values (json.RawMessage); other fields are fixed-size or small strings.
|
||||
func (c *CachedData) Cost() int64 {
|
||||
var size int64
|
||||
for _, b := range c.Buckets {
|
||||
if b == nil {
|
||||
continue
|
||||
}
|
||||
// Value is the bulk of the payload
|
||||
size += int64(len(b.Value))
|
||||
}
|
||||
for _, w := range c.Warnings {
|
||||
size += int64(len(w))
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
6
tests/fixtures/querier.py
vendored
6
tests/fixtures/querier.py
vendored
@@ -200,8 +200,6 @@ def build_formula_query(
|
||||
*,
|
||||
functions: list[dict] | None = None,
|
||||
disabled: bool = False,
|
||||
order: list[dict] | None = None,
|
||||
limit: int | None = None,
|
||||
) -> dict:
|
||||
spec: dict[str, Any] = {
|
||||
"name": name,
|
||||
@@ -210,10 +208,6 @@ def build_formula_query(
|
||||
}
|
||||
if functions:
|
||||
spec["functions"] = functions
|
||||
if order:
|
||||
spec["order"] = order
|
||||
if limit is not None:
|
||||
spec["limit"] = limit
|
||||
return {"type": "builder_formula", "spec": spec}
|
||||
|
||||
|
||||
|
||||
@@ -11,11 +11,6 @@ from fixtures.logs import Logs
|
||||
from fixtures.querier import (
|
||||
assert_identical_query_response,
|
||||
assert_minutely_bucket_values,
|
||||
build_formula_query,
|
||||
build_group_by_field,
|
||||
build_logs_aggregation,
|
||||
build_order_by,
|
||||
build_scalar_query,
|
||||
find_named_result,
|
||||
index_series_by_label,
|
||||
make_query_request,
|
||||
@@ -2116,180 +2111,3 @@ def test_logs_fill_zero_formula_with_group_by(
|
||||
expected_by_ts=expectations[service_name],
|
||||
context=f"logs/fillZero/F1/{service_name}",
|
||||
)
|
||||
|
||||
|
||||
def test_logs_formula_orderby_and_limit(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
) -> None:
|
||||
"""
|
||||
Test that formula results are correctly ordered and limited when
|
||||
order and limit are applied on the formula.
|
||||
"""
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
logs: list[Logs] = []
|
||||
# For service-i (i in 0..9): insert (10 - i) ERROR logs and 2 INFO logs.
|
||||
# A counts ERROR, B counts INFO, so A/B = (10 - i) / 2.
|
||||
# service-0 ratio = 5.0 (highest), service-9 ratio = 0.5 (lowest).
|
||||
for i in range(10):
|
||||
for j in range(10 - i):
|
||||
logs.append(
|
||||
Logs(
|
||||
timestamp=now - timedelta(minutes=j + 1),
|
||||
resources={"service.name": f"service-{i}"},
|
||||
attributes={"code.file": "test.py"},
|
||||
body=f"Error log {i}-{j}",
|
||||
severity_text="ERROR",
|
||||
)
|
||||
)
|
||||
for k in range(2):
|
||||
logs.append(
|
||||
Logs(
|
||||
timestamp=now - timedelta(minutes=k + 1),
|
||||
resources={"service.name": f"service-{i}"},
|
||||
attributes={"code.file": "test.py"},
|
||||
body=f"Info log {i}-{k}",
|
||||
severity_text="INFO",
|
||||
)
|
||||
)
|
||||
# Extra INFO-only services that appear in B but not in A. The formula
|
||||
for name in ("service-info-only-1", "service-info-only-2"):
|
||||
for k in range(2):
|
||||
logs.append(
|
||||
Logs(
|
||||
timestamp=now - timedelta(minutes=k + 1),
|
||||
resources={"service.name": name},
|
||||
attributes={"code.file": "test.py"},
|
||||
body=f"Info log {name}-{k}",
|
||||
severity_text="INFO",
|
||||
)
|
||||
)
|
||||
|
||||
# Logs look like this (columns = minutes before `now`; query range is
|
||||
# (now - 15m, now], so the `now` column is the exclusive upper bound and
|
||||
# no log lands there). E = ERROR, I = INFO, X = both at that minute.
|
||||
#
|
||||
# t-10 t-9 t-8 t-7 t-6 t-5 t-4 t-3 t-2 t-1 |now | A B A/B
|
||||
# service-0: E E E E E E E E X X | | 10 2 5.0
|
||||
# service-1: . E E E E E E E X X | | 9 2 4.5
|
||||
# service-2: . . E E E E E E X X | | 8 2 4.0
|
||||
# service-3: . . . E E E E E X X | | 7 2 3.5
|
||||
# service-4: . . . . E E E E X X | | 6 2 3.0
|
||||
# service-5: . . . . . E E E X X | | 5 2 2.5
|
||||
# service-6: . . . . . . E E X X | | 4 2 2.0
|
||||
# service-7: . . . . . . . E X X | | 3 2 1.5
|
||||
# service-8: . . . . . . . . X X | | 2 2 1.0
|
||||
# service-9: . . . . . . . . I X | | 1 2 0.5
|
||||
# info-only-1: . . . . . . . . I I | | 0* 2 0.0
|
||||
# info-only-2: . . . . . . . . I I | | 0* 2 0.0
|
||||
#
|
||||
# * A is missing for the info-only services; because A is count(), the
|
||||
# formula evaluator defaults missing A to 0, yielding A/B = 0.
|
||||
insert_logs(logs)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
result = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((now - timedelta(minutes=15)).timestamp() * 1000),
|
||||
end_ms=int(now.timestamp() * 1000),
|
||||
request_type="scalar",
|
||||
queries=[
|
||||
build_scalar_query(
|
||||
name="A",
|
||||
signal="logs",
|
||||
aggregations=[build_logs_aggregation("count()")],
|
||||
group_by=[build_group_by_field("service.name")],
|
||||
filter_expression="severity_text = 'ERROR'",
|
||||
disabled=True,
|
||||
),
|
||||
build_scalar_query(
|
||||
name="B",
|
||||
signal="logs",
|
||||
aggregations=[build_logs_aggregation("count()")],
|
||||
group_by=[build_group_by_field("service.name")],
|
||||
filter_expression="severity_text = 'INFO'",
|
||||
disabled=True,
|
||||
),
|
||||
build_formula_query(
|
||||
"F1",
|
||||
"A / B",
|
||||
order=[build_order_by("__result", "desc")],
|
||||
limit=3,
|
||||
),
|
||||
build_formula_query(
|
||||
"F2",
|
||||
"A / B",
|
||||
order=[build_order_by("__result", "desc")],
|
||||
),
|
||||
build_formula_query(
|
||||
"F3",
|
||||
"A / B",
|
||||
order=[build_order_by("__result", "asc")],
|
||||
limit=3,
|
||||
),
|
||||
build_formula_query(
|
||||
"F4",
|
||||
"A / B",
|
||||
order=[build_order_by("__result", "asc")],
|
||||
),
|
||||
],
|
||||
)
|
||||
assert result.status_code == HTTPStatus.OK
|
||||
assert result.json()["status"] == "success"
|
||||
|
||||
results = result.json()["data"]["data"]["results"]
|
||||
|
||||
def extract_services_and_values(query_name: str) -> tuple[list, list]:
|
||||
res = find_named_result(results, query_name)
|
||||
assert res is not None, f"Expected formula result named {query_name}"
|
||||
cols = res["columns"]
|
||||
s_col = next(i for i, c in enumerate(cols) if c["name"] == "service.name")
|
||||
v_col = next(i for i, c in enumerate(cols) if c["name"] == "__result")
|
||||
rows = res["data"]
|
||||
return [row[s_col] for row in rows], [row[v_col] for row in rows]
|
||||
|
||||
# Because A is count(), canDefaultZero["A"] is true; the formula evaluator
|
||||
# defaults A to 0 for services that exist only in B. So the two INFO-only
|
||||
# services appear in the formula result with value 0.0 (extreme bottom in
|
||||
# desc order, extreme top in asc order). Their relative ordering is not
|
||||
# deterministic across separate formula evaluations (tied values).
|
||||
info_only_services = {"service-info-only-1", "service-info-only-2"}
|
||||
|
||||
# F2: desc, no limit -> 12 rows in descending order by value.
|
||||
f2_services, f2_values = extract_services_and_values("F2")
|
||||
assert len(f2_services) == 12, f"F2: expected 12 rows with no limit, got {len(f2_services)}"
|
||||
assert f2_values == [5.0, 4.5, 4.0, 3.5, 3.0, 2.5, 2.0, 1.5, 1.0, 0.5, 0.0, 0.0], f2_values
|
||||
# Top 10 have distinct positive values -> deterministic service ordering.
|
||||
assert f2_services[:10] == [f"service-{i}" for i in range(10)], f2_services[:10]
|
||||
# Tail 2 are the INFO-only services tied at 0.0 (order between them not guaranteed).
|
||||
assert set(f2_services[10:]) == info_only_services, f2_services[10:]
|
||||
|
||||
# F1: desc + limit 3 -> must be exactly the first 3 rows of F2.
|
||||
# Top 3 are not in the tie region, so prefix equality is safe.
|
||||
f1_services, f1_values = extract_services_and_values("F1")
|
||||
assert len(f1_services) == 3, f"F1: expected 3 rows after limit, got {len(f1_services)}"
|
||||
assert f1_services == f2_services[:3], f"F1 services {f1_services} are not the prefix of F2 services {f2_services}"
|
||||
assert f1_values == f2_values[:3], f"F1 values {f1_values} are not the prefix of F2 values {f2_values}"
|
||||
|
||||
# F4: asc, no limit -> 12 rows in ascending order by value.
|
||||
f4_services, f4_values = extract_services_and_values("F4")
|
||||
assert len(f4_services) == 12, f"F4: expected 12 rows with no limit, got {len(f4_services)}"
|
||||
assert f4_values == sorted(f4_values), f"F4 not ascending: {f4_values}"
|
||||
# First 2 are the INFO-only services tied at 0.0 (order between them not guaranteed).
|
||||
assert set(f4_services[:2]) == info_only_services, f4_services[:2]
|
||||
assert f4_values[:2] == [0.0, 0.0], f4_values[:2]
|
||||
# Tail 10 are service-9 down to service-0 by value.
|
||||
assert f4_services[2:] == [f"service-{i}" for i in reversed(range(10))], f4_services[2:]
|
||||
assert f4_values[2:] == [(10 - i) / 2 for i in reversed(range(10))], f4_values[2:]
|
||||
|
||||
# F3: asc + limit 3 -> values must match F4[:3] exactly; service set must
|
||||
# match too. Direct prefix equality on services would be flaky because the
|
||||
# two tied INFO-only entries can swap order between formula evaluations.
|
||||
f3_services, f3_values = extract_services_and_values("F3")
|
||||
assert len(f3_services) == 3, f"F3: expected 3 rows after limit, got {len(f3_services)}"
|
||||
assert f3_values == f4_values[:3], f"F3 values {f3_values} do not match F4[:3] values {f4_values[:3]}"
|
||||
assert set(f3_services) == set(f4_services[:3]), f"F3 services {f3_services} do not match F4[:3] services {f4_services[:3]}"
|
||||
|
||||
Reference in New Issue
Block a user