mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-04 17:30:34 +01:00
Compare commits
128 Commits
feat/overv
...
infraM/v2_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a949993430 | ||
|
|
14a032119a | ||
|
|
e78dfc1622 | ||
|
|
f34a33e08b | ||
|
|
46e833faba | ||
|
|
4bd7492629 | ||
|
|
24fe9a986d | ||
|
|
56e79be6cd | ||
|
|
92d297ac9d | ||
|
|
b3c352609c | ||
|
|
bdbaa32485 | ||
|
|
9503cdff36 | ||
|
|
5a18786ab2 | ||
|
|
648154df14 | ||
|
|
98eb002e07 | ||
|
|
720379db9f | ||
|
|
6ad14e7151 | ||
|
|
181fca064b | ||
|
|
a5e39ca6bd | ||
|
|
b35c6676f9 | ||
|
|
1095caa123 | ||
|
|
9043b49762 | ||
|
|
d4084a7494 | ||
|
|
27c564b3bf | ||
|
|
f02c491828 | ||
|
|
3d53b8f77f | ||
|
|
dffe94fec4 | ||
|
|
c9360fcf13 | ||
|
|
b5ab45db20 | ||
|
|
08f76aca78 | ||
|
|
d81cec4c29 | ||
|
|
49744c6104 | ||
|
|
2147627baf | ||
|
|
824f92a88f | ||
|
|
983d4fe4f2 | ||
|
|
833af794c3 | ||
|
|
21b51d1fcc | ||
|
|
56f22682c8 | ||
|
|
9c8359940c | ||
|
|
4050880275 | ||
|
|
5e775f64f2 | ||
|
|
0189f23f46 | ||
|
|
49a36d4e3d | ||
|
|
9407d658ab | ||
|
|
5035712485 | ||
|
|
bab17c3615 | ||
|
|
37b44f4db9 | ||
|
|
99dd6e5f1e | ||
|
|
9c7131fa6a | ||
|
|
ad889a2e1d | ||
|
|
a4f6d0cbf5 | ||
|
|
589bed7c16 | ||
|
|
93843a1f48 | ||
|
|
88c43108fc | ||
|
|
ed4cf540e8 | ||
|
|
9e2dfa9033 | ||
|
|
d98d5d68ee | ||
|
|
2cb1c3b73b | ||
|
|
ae7ca497ad | ||
|
|
a579916961 | ||
|
|
4a16d56abf | ||
|
|
642b5ac3f0 | ||
|
|
a12112619c | ||
|
|
014785f1bc | ||
|
|
58ee797b10 | ||
|
|
82d236742f | ||
|
|
397e1ad5be | ||
|
|
8d6b25ca9b | ||
|
|
5fa6bd8b8d | ||
|
|
bd9977483b | ||
|
|
50fbdfeeef | ||
|
|
e2b1b73e87 | ||
|
|
cb9f3fd3e5 | ||
|
|
232acc343d | ||
|
|
2025afdccc | ||
|
|
d2f4d4af93 | ||
|
|
47ff7bbb8e | ||
|
|
724071c5dc | ||
|
|
4d24979358 | ||
|
|
042943b10a | ||
|
|
48a9be7ec8 | ||
|
|
a9504b2120 | ||
|
|
8755887c4a | ||
|
|
4cb4662b3a | ||
|
|
e6900dabc8 | ||
|
|
c1ba389b63 | ||
|
|
3a1f40234f | ||
|
|
2e4891fa63 | ||
|
|
04ebc0bec7 | ||
|
|
271f9b81ed | ||
|
|
6fa815c294 | ||
|
|
63ec518efb | ||
|
|
c4ca20dd90 | ||
|
|
e56cc4222b | ||
|
|
07d2944d7c | ||
|
|
dea01ae36a | ||
|
|
62ea5b54e2 | ||
|
|
e549a7e42f | ||
|
|
90e2ebb11f | ||
|
|
61baa1be7a | ||
|
|
b946fa665f | ||
|
|
2e049556e4 | ||
|
|
492a5e70d7 | ||
|
|
ba1f2771e8 | ||
|
|
7458fb4855 | ||
|
|
5f55f3938b | ||
|
|
3e8102485c | ||
|
|
861c682ea5 | ||
|
|
c8e5895dff | ||
|
|
82d72e7edb | ||
|
|
a3f8ecaaf1 | ||
|
|
19aada656c | ||
|
|
b21bb4280f | ||
|
|
bc0a4fdb5c | ||
|
|
37fb0e9254 | ||
|
|
aecfa1a174 | ||
|
|
b869d23d94 | ||
|
|
6ee3d44f76 | ||
|
|
462e554107 | ||
|
|
66afa73e6f | ||
|
|
54c604bcf4 | ||
|
|
c1be02ba54 | ||
|
|
d3c7ba8f45 | ||
|
|
039c4a0496 | ||
|
|
51a94b6bbc | ||
|
|
bbfbb94f52 | ||
|
|
d1eb9ef16f | ||
|
|
3db00f8bc3 |
22
frontend/src/components/CodeBlock/CodeBlock.module.scss
Normal file
22
frontend/src/components/CodeBlock/CodeBlock.module.scss
Normal file
@@ -0,0 +1,22 @@
|
||||
.codeBlock {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.codeBlockSyntaxHighlighter {
|
||||
background-color: var(--l2-background) !important;
|
||||
border-radius: 4px !important;
|
||||
border: 1px solid var(--l2-border) !important;
|
||||
color: var(--l2-foreground) !important;
|
||||
|
||||
pre {
|
||||
color: var(--l2-foreground) !important;
|
||||
font-family: 'Geist Mono' !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
code {
|
||||
color: var(--l1-foreground) !important;
|
||||
font-family: 'Geist Mono' !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
46
frontend/src/components/CodeBlock/CodeBlock.test.tsx
Normal file
46
frontend/src/components/CodeBlock/CodeBlock.test.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
import CodeBlock from './CodeBlock';
|
||||
|
||||
const mockCopyToClipboard = jest.fn();
|
||||
|
||||
jest.mock('react-use', () => ({
|
||||
useCopyToClipboard: (): [unknown, (text: string) => void] => [
|
||||
undefined,
|
||||
mockCopyToClipboard,
|
||||
],
|
||||
}));
|
||||
|
||||
describe('CodeBlock', () => {
|
||||
beforeEach(() => {
|
||||
mockCopyToClipboard.mockReset();
|
||||
});
|
||||
|
||||
it('renders code block mode by default', () => {
|
||||
render(<CodeBlock code={'const x = 1;\n'} language="javascript" />);
|
||||
|
||||
const container = screen.getByTestId('code-block-container');
|
||||
expect(container).toBeInTheDocument();
|
||||
expect(container).toHaveTextContent('const x = 1;');
|
||||
});
|
||||
|
||||
it('renders inline code when inline is true', () => {
|
||||
render(<CodeBlock code="inline value" inline />);
|
||||
|
||||
const inlineCode = screen.getByText('inline value');
|
||||
expect(inlineCode.tagName.toLowerCase()).toBe('code');
|
||||
expect(screen.queryByTestId('code-block-container')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('copies code and triggers callback', async () => {
|
||||
const onCopy = jest.fn();
|
||||
render(<CodeBlock code="SELECT * FROM logs;" onCopy={onCopy} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /copy code/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith('SELECT * FROM logs;');
|
||||
});
|
||||
expect(onCopy).toHaveBeenCalledWith('SELECT * FROM logs;');
|
||||
});
|
||||
});
|
||||
89
frontend/src/components/CodeBlock/CodeBlock.tsx
Normal file
89
frontend/src/components/CodeBlock/CodeBlock.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Check, Copy } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui';
|
||||
import SyntaxHighlighter, {
|
||||
a11yDark,
|
||||
} from 'components/MarkdownRenderer/syntaxHighlighter';
|
||||
|
||||
import styles from './CodeBlock.module.scss';
|
||||
|
||||
export interface CodeBlockProps {
|
||||
code: string;
|
||||
language?: string;
|
||||
className?: string;
|
||||
inline?: boolean;
|
||||
showLineNumbers?: boolean;
|
||||
showCopyButton?: boolean;
|
||||
onCopy?: (copiedCode: string) => void;
|
||||
}
|
||||
|
||||
function CodeBlock({
|
||||
code,
|
||||
language = 'text',
|
||||
className,
|
||||
inline = false,
|
||||
showLineNumbers = false,
|
||||
showCopyButton = true,
|
||||
onCopy,
|
||||
}: CodeBlockProps): JSX.Element {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
const normalizedCode = useMemo(() => code?.replace(/\n$/, '') ?? '', [code]);
|
||||
|
||||
const handleCopy = (): void => {
|
||||
copyToClipboard(normalizedCode);
|
||||
setIsCopied(true);
|
||||
onCopy?.(normalizedCode);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
if (inline) {
|
||||
return <code className={className}>{normalizedCode}</code>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.codeBlock} ${className}`}
|
||||
style={{ position: 'relative' }}
|
||||
data-testid="code-block-container"
|
||||
>
|
||||
{showCopyButton ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={handleCopy}
|
||||
prefix={isCopied ? <Check size={14} /> : <Copy size={14} />}
|
||||
aria-label="Copy code"
|
||||
title={isCopied ? 'Copied' : 'Copy'}
|
||||
style={{ position: 'absolute', right: 8, top: 8, zIndex: 1 }}
|
||||
/>
|
||||
) : null}
|
||||
<SyntaxHighlighter
|
||||
style={a11yDark}
|
||||
language={language}
|
||||
PreTag="div"
|
||||
showLineNumbers={showLineNumbers}
|
||||
wrapLongLines
|
||||
className={styles.codeBlockSyntaxHighlighter}
|
||||
>
|
||||
{normalizedCode}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CodeBlock.defaultProps = {
|
||||
language: 'text',
|
||||
className: undefined,
|
||||
inline: false,
|
||||
showLineNumbers: false,
|
||||
showCopyButton: true,
|
||||
onCopy: undefined,
|
||||
};
|
||||
|
||||
export default CodeBlock;
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Alert, Spin } from 'antd';
|
||||
import { LoaderCircle, TriangleAlert } from 'lucide-react';
|
||||
import { Callout } from '@signozhq/ui';
|
||||
import { Spin } from 'antd';
|
||||
import { LoaderCircle } from 'lucide-react';
|
||||
|
||||
import { ModalStateEnum } from '../types';
|
||||
import { ModalStateEnum } from '../HeroSection/types';
|
||||
|
||||
function AlertMessage({
|
||||
modalState,
|
||||
@@ -12,14 +12,13 @@ function AlertMessage({
|
||||
switch (modalState) {
|
||||
case ModalStateEnum.WAITING:
|
||||
return (
|
||||
<Alert
|
||||
message={
|
||||
<Callout
|
||||
title={
|
||||
<div className="cloud-account-setup-form__alert-message">
|
||||
<Spin
|
||||
indicator={
|
||||
<LoaderCircle
|
||||
size={14}
|
||||
color={Color.BG_AMBER_400}
|
||||
className="anticon anticon-loading anticon-spin ant-spin-dot"
|
||||
/>
|
||||
}
|
||||
@@ -28,21 +27,19 @@ function AlertMessage({
|
||||
<span className="retry-time">10</span> secs...
|
||||
</div>
|
||||
}
|
||||
className="cloud-account-setup-form__alert"
|
||||
type="warning"
|
||||
type="info"
|
||||
showIcon={false}
|
||||
/>
|
||||
);
|
||||
case ModalStateEnum.ERROR:
|
||||
return (
|
||||
<Alert
|
||||
message={
|
||||
<Callout
|
||||
title={
|
||||
<div className="cloud-account-setup-form__alert-message">
|
||||
<TriangleAlert type="solid" size={15} color={Color.BG_SAKURA_400} />
|
||||
{`We couldn't establish a connection to your AWS account. Please try again`}
|
||||
</div>
|
||||
}
|
||||
type="error"
|
||||
className="cloud-account-setup-form__alert"
|
||||
/>
|
||||
);
|
||||
default:
|
||||
@@ -117,6 +117,12 @@
|
||||
min-width: 140px !important;
|
||||
}
|
||||
|
||||
&.azure {
|
||||
.ant-select-selector {
|
||||
min-width: 282px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-item-option-active {
|
||||
background: var(--l3-background) !important;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from '@signozhq/ui';
|
||||
@@ -6,19 +6,29 @@ import { Select, Skeleton } from 'antd';
|
||||
import { SelectProps } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useListAccounts } from 'api/generated/services/cloudintegration';
|
||||
import cx from 'classnames';
|
||||
import { getAccountById } from 'container/Integrations/CloudIntegration/utils';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import {
|
||||
CloudAccount as IntegrationCloudAccount,
|
||||
IntegrationType,
|
||||
} from 'container/Integrations/types';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { ChevronDown, Dot, PencilLine, Plug, Plus } from 'lucide-react';
|
||||
|
||||
import { mapAccountDtoToAwsCloudAccount } from '../../mapAwsCloudAccountFromDto';
|
||||
import { CloudAccount } from '../../types';
|
||||
import AccountSettingsModal from './AccountSettingsModal';
|
||||
import CloudAccountSetupModal from './CloudAccountSetupModal';
|
||||
import AzureCloudAccountSetupModal from '../../AzureCloudServices/AddNewAccount/CloudAccountSetupModal';
|
||||
import AzureAccountSettingsModal from '../../AzureCloudServices/EditAccount/AccountSettingsModal';
|
||||
import {
|
||||
mapAccountDtoToAwsCloudAccount,
|
||||
mapAccountDtoToAzureCloudAccount,
|
||||
} from '../../mapCloudAccountFromDto';
|
||||
import AwsCloudAccountSetupModal from '../AddNewAccount/CloudAccountSetupModal';
|
||||
import AwsAccountSettingsModal from '../EditAccount/AccountSettingsModal';
|
||||
import { CloudAccount as AwsCloudAccount } from '../types';
|
||||
|
||||
import './AccountActions.style.scss';
|
||||
|
||||
function AccountActionsRenderer({
|
||||
type,
|
||||
accounts,
|
||||
isLoading,
|
||||
activeAccount,
|
||||
@@ -27,9 +37,10 @@ function AccountActionsRenderer({
|
||||
onIntegrationModalOpen,
|
||||
onAccountSettingsModalOpen,
|
||||
}: {
|
||||
accounts: CloudAccount[] | undefined;
|
||||
type: IntegrationType;
|
||||
accounts: IntegrationCloudAccount[] | undefined;
|
||||
isLoading: boolean;
|
||||
activeAccount: CloudAccount | null;
|
||||
activeAccount: IntegrationCloudAccount | null;
|
||||
selectOptions: SelectProps['options'];
|
||||
onAccountChange: (value: string) => void;
|
||||
onIntegrationModalOpen: () => void;
|
||||
@@ -57,9 +68,11 @@ function AccountActionsRenderer({
|
||||
<Select
|
||||
value={activeAccount?.providerAccountId}
|
||||
options={selectOptions}
|
||||
rootClassName="cloud-account-selector"
|
||||
rootClassName={cx('cloud-account-selector', {
|
||||
[type.toLowerCase()]: type,
|
||||
})}
|
||||
popupMatchSelectWidth={false}
|
||||
placeholder="Select AWS Account"
|
||||
placeholder={`Select ${type} Account`}
|
||||
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
|
||||
onChange={onAccountChange}
|
||||
/>
|
||||
@@ -102,21 +115,49 @@ function AccountActionsRenderer({
|
||||
);
|
||||
}
|
||||
|
||||
function AccountActions(): JSX.Element {
|
||||
function AccountActions({ type }: { type: IntegrationType }): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data: listAccountsResponse, isLoading } = useListAccounts({
|
||||
cloudProvider: INTEGRATION_TYPES.AWS,
|
||||
cloudProvider: type,
|
||||
});
|
||||
const accounts = useMemo((): CloudAccount[] | undefined => {
|
||||
|
||||
const accounts = useMemo((): IntegrationCloudAccount[] | undefined => {
|
||||
const raw = listAccountsResponse?.data?.accounts;
|
||||
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
return raw
|
||||
.map(mapAccountDtoToAwsCloudAccount)
|
||||
.filter((account): account is CloudAccount => account !== null);
|
||||
}, [listAccountsResponse]);
|
||||
|
||||
const mappedAccounts: IntegrationCloudAccount[] = [];
|
||||
|
||||
if (type === IntegrationType.AWS_SERVICES) {
|
||||
raw.forEach((account) => {
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
const mapped = mapAccountDtoToAwsCloudAccount(account);
|
||||
if (mapped) {
|
||||
mappedAccounts.push(mapped);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (type === IntegrationType.AZURE_SERVICES) {
|
||||
raw.forEach((account) => {
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
const mapped = mapAccountDtoToAzureCloudAccount(account);
|
||||
if (mapped) {
|
||||
mappedAccounts.push(mapped);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mappedAccounts;
|
||||
}, [listAccountsResponse, type]);
|
||||
|
||||
const initialAccount = useMemo(
|
||||
() =>
|
||||
@@ -127,9 +168,8 @@ function AccountActions(): JSX.Element {
|
||||
[accounts, urlQuery],
|
||||
);
|
||||
|
||||
const [activeAccount, setActiveAccount] = useState<CloudAccount | null>(
|
||||
initialAccount,
|
||||
);
|
||||
const [activeAccount, setActiveAccount] =
|
||||
useState<IntegrationCloudAccount | null>(initialAccount);
|
||||
|
||||
// Update state when initial value changes
|
||||
useEffect(() => {
|
||||
@@ -149,16 +189,17 @@ function AccountActions(): JSX.Element {
|
||||
}, [initialAccount]);
|
||||
|
||||
const [isIntegrationModalOpen, setIsIntegrationModalOpen] = useState(false);
|
||||
|
||||
const startAccountConnectionAttempt = (): void => {
|
||||
setIsIntegrationModalOpen(true);
|
||||
logEvent('AWS Integration: Account connection attempt started', {});
|
||||
logEvent(`${type} Integration: Account connection attempt started`, {});
|
||||
};
|
||||
|
||||
const [isAccountSettingsModalOpen, setIsAccountSettingsModalOpen] =
|
||||
useState(false);
|
||||
const openAccountSettings = (): void => {
|
||||
setIsAccountSettingsModalOpen(true);
|
||||
logEvent('AWS Integration: Account settings viewed', {
|
||||
logEvent(`${type} Integration: Account settings viewed`, {
|
||||
cloudAccountId: activeAccount?.cloud_account_id,
|
||||
});
|
||||
};
|
||||
@@ -166,13 +207,16 @@ function AccountActions(): JSX.Element {
|
||||
// log telemetry event when an account is viewed.
|
||||
useEffect(() => {
|
||||
if (activeAccount) {
|
||||
logEvent('AWS Integration: Account viewed', {
|
||||
logEvent(`${type} Integration: Account viewed`, {
|
||||
cloudAccountId: activeAccount?.cloud_account_id,
|
||||
status: activeAccount?.status,
|
||||
enabledRegions: activeAccount?.config?.regions,
|
||||
enabledRegions:
|
||||
'regions' in activeAccount.config
|
||||
? activeAccount.config.regions
|
||||
: activeAccount.config.resource_groups,
|
||||
});
|
||||
}
|
||||
}, [activeAccount]);
|
||||
}, [activeAccount, type]);
|
||||
|
||||
const selectOptions: SelectProps['options'] = useMemo(
|
||||
() =>
|
||||
@@ -188,6 +232,7 @@ function AccountActions(): JSX.Element {
|
||||
return (
|
||||
<div className="hero-section__actions">
|
||||
<AccountActionsRenderer
|
||||
type={type}
|
||||
accounts={accounts}
|
||||
isLoading={isLoading}
|
||||
activeAccount={activeAccount}
|
||||
@@ -204,17 +249,39 @@ function AccountActions(): JSX.Element {
|
||||
/>
|
||||
|
||||
{isIntegrationModalOpen && (
|
||||
<CloudAccountSetupModal
|
||||
onClose={(): void => setIsIntegrationModalOpen(false)}
|
||||
/>
|
||||
<>
|
||||
{type === IntegrationType.AWS_SERVICES && (
|
||||
<AwsCloudAccountSetupModal
|
||||
onClose={(): void => setIsIntegrationModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{type === IntegrationType.AZURE_SERVICES && (
|
||||
<AzureCloudAccountSetupModal
|
||||
onClose={(): void => setIsIntegrationModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{isAccountSettingsModalOpen && activeAccount && (
|
||||
<AccountSettingsModal
|
||||
onClose={(): void => setIsAccountSettingsModalOpen(false)}
|
||||
account={activeAccount}
|
||||
setActiveAccount={setActiveAccount}
|
||||
/>
|
||||
<>
|
||||
{type === IntegrationType.AWS_SERVICES && (
|
||||
<AwsAccountSettingsModal
|
||||
onClose={(): void => setIsAccountSettingsModalOpen(false)}
|
||||
account={activeAccount as AwsCloudAccount}
|
||||
setActiveAccount={
|
||||
setActiveAccount as Dispatch<SetStateAction<AwsCloudAccount | null>>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{type === IntegrationType.AZURE_SERVICES && (
|
||||
<AzureAccountSettingsModal
|
||||
onClose={(): void => setIsAccountSettingsModalOpen(false)}
|
||||
account={activeAccount}
|
||||
setActiveAccount={setActiveAccount}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,346 @@
|
||||
.cloud-account-setup-modal {
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
|
||||
[data-slot='drawer-title'] {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
scrollbar-width: thin;
|
||||
padding-right: 16px;
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--l1-border);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--l1-background);
|
||||
}
|
||||
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--l3-background) var(--l1-background);
|
||||
}
|
||||
|
||||
.cloud-account-setup-prerequisites {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
&__title {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__list-item {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
&-bullet {
|
||||
color: var(--primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&__list-item-highlight {
|
||||
color: var(--l1-foreground);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-account-setup-how-it-works-accordion {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 24px 0;
|
||||
|
||||
&__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l2-border);
|
||||
background: var(--l2-background);
|
||||
padding: 4px 16px 4px 0px;
|
||||
|
||||
&.open {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
animation: cloud-account-setup-accordion-reveal 220ms ease-out forwards;
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
border-top: none;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
|
||||
border: 1px solid var(--l2-border);
|
||||
background: var(--l2-background);
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
color: var(--l1-foreground);
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-account-setup-form__code-block-tabs {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l2-border);
|
||||
background: var(--l2-background);
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
margin-bottom: 12px;
|
||||
|
||||
&-title {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
&-description {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
}
|
||||
|
||||
[role='tablist'] {
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
[role='tabpanel'] {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
[data-slot='tabs-trigger'] {
|
||||
padding: 4px 24px !important;
|
||||
border: none !important;
|
||||
background-color: transparent !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cloud-account-setup-accordion-reveal {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.account-setup-modal-footer {
|
||||
&__confirm-button {
|
||||
background: var(--primary-background);
|
||||
color: var(--primary-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
&__confirm-selection-count {
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
&__close-button {
|
||||
background: var(--l1-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 2px;
|
||||
color: var(--l1-foreground);
|
||||
font-family: 'Inter';
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-account-setup-form {
|
||||
.disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&,
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
&__alert {
|
||||
width: 100%;
|
||||
|
||||
[data-slot='callout'] {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--l1-foreground);
|
||||
|
||||
.retry-time {
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
&__description {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
&__select {
|
||||
.ant-select-selection-item {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
&__form-item {
|
||||
margin: 0;
|
||||
}
|
||||
&__include-all-regions-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
margin-bottom: 12px;
|
||||
&-label {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
&__note {
|
||||
padding: 12px;
|
||||
color: var(--callout-primary-description);
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
letter-spacing: -0.06px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--primary-background) 10%, transparent);
|
||||
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
|
||||
}
|
||||
&__submit-button {
|
||||
border-radius: 2px;
|
||||
background: var(--primary-background);
|
||||
color: var(--primary-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
&-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
ActiveViewEnum,
|
||||
IntegrationModalProps,
|
||||
ModalStateEnum,
|
||||
} from '../types';
|
||||
import { RegionForm } from './RegionForm';
|
||||
} from '../../../HeroSection/types';
|
||||
import { RegionForm } from '../RegionForm/RegionForm';
|
||||
|
||||
import './CloudAccountSetupModal.style.scss';
|
||||
|
||||
@@ -74,8 +74,6 @@ function CloudAccountSetupModal({
|
||||
isConnectionParamsLoading,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
isLoading,
|
||||
isGeneratingUrl,
|
||||
handleConnectionSuccess,
|
||||
handleConnectionTimeout,
|
||||
handleConnectionError,
|
||||
@@ -9,10 +9,10 @@ import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { Save } from 'lucide-react';
|
||||
|
||||
import logEvent from '../../../../../../api/common/logEvent';
|
||||
import { CloudAccount } from '../../types';
|
||||
import { RegionSelector } from './RegionSelector';
|
||||
import RemoveIntegrationAccount from './RemoveIntegrationAccount';
|
||||
import logEvent from '../../../../../api/common/logEvent';
|
||||
import RemoveIntegrationAccount from '../../RemoveAccount/RemoveIntegrationAccount';
|
||||
import { RegionSelector } from '../RegionForm/RegionSelector';
|
||||
import { CloudAccount } from '../types';
|
||||
|
||||
import './AccountSettingsModal.style.scss';
|
||||
|
||||
@@ -110,11 +110,7 @@ function AccountSettingsModal({
|
||||
form,
|
||||
selectedRegions,
|
||||
includeAllRegions,
|
||||
account?.id,
|
||||
handleRemoveIntegrationAccountSuccess,
|
||||
isSaveDisabled,
|
||||
handleSubmit,
|
||||
isLoading,
|
||||
account?.providerAccountId,
|
||||
setSelectedRegions,
|
||||
setIncludeAllRegions,
|
||||
]);
|
||||
@@ -133,6 +129,7 @@ function AccountSettingsModal({
|
||||
<RemoveIntegrationAccount
|
||||
accountId={account?.id}
|
||||
onRemoveIntegrationAccountSuccess={handleRemoveIntegrationAccountSuccess}
|
||||
cloudProvider={INTEGRATION_TYPES.AWS}
|
||||
/>
|
||||
|
||||
<Button
|
||||
@@ -1,28 +0,0 @@
|
||||
import awsDarkLogoUrl from '@/assets/Logos/aws-dark.svg';
|
||||
|
||||
import AccountActions from './components/AccountActions';
|
||||
|
||||
import './HeroSection.style.scss';
|
||||
|
||||
function HeroSection(): JSX.Element {
|
||||
return (
|
||||
<div className="hero-section">
|
||||
<div className="hero-section__details">
|
||||
<div className="hero-section__details-header">
|
||||
<div className="hero-section__icon">
|
||||
<img src={awsDarkLogoUrl} alt="AWS" />
|
||||
</div>
|
||||
|
||||
<div className="hero-section__details-title">AWS</div>
|
||||
</div>
|
||||
<div className="hero-section__details-description">
|
||||
AWS is a cloud computing platform that provides a range of services for
|
||||
building and running applications.
|
||||
</div>
|
||||
</div>
|
||||
<AccountActions />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HeroSection;
|
||||
@@ -1,180 +0,0 @@
|
||||
.cloud-account-setup-modal {
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
|
||||
[data-slot='drawer-title'] {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.account-setup-modal-footer {
|
||||
&__confirm-button {
|
||||
background: var(--primary-background);
|
||||
color: var(--primary-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
&__confirm-selection-count {
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
&__close-button {
|
||||
background: var(--l1-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 2px;
|
||||
color: var(--l1-foreground);
|
||||
font-family: 'Inter';
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-account-setup-form {
|
||||
.disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&,
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
&__alert {
|
||||
&.ant-alert {
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
line-height: 22px; /* 157.143% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
&.ant-alert-error {
|
||||
color: var(--danger-foreground);
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--danger-background) 10%, transparent);
|
||||
background: color-mix(in srgb, var(--danger-background) 10%, transparent);
|
||||
}
|
||||
|
||||
&.ant-alert-warning {
|
||||
color: var(--warning-foreground);
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--warning-background) 10%, transparent);
|
||||
background: color-mix(in srgb, var(--warning-background) 10%, transparent);
|
||||
}
|
||||
|
||||
&-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--l1-foreground);
|
||||
|
||||
.retry-time {
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
&__title {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
&__description {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
&__select {
|
||||
.ant-select-selection-item {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
&__form-item {
|
||||
margin: 0;
|
||||
}
|
||||
&__include-all-regions-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
margin-bottom: 12px;
|
||||
&-label {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
&__note {
|
||||
padding: 12px;
|
||||
color: var(--callout-primary-description);
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
letter-spacing: -0.06px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--primary-background) 10%, transparent);
|
||||
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
|
||||
}
|
||||
&__submit-button {
|
||||
border-radius: 2px;
|
||||
background: var(--primary-background);
|
||||
color: var(--primary-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
&-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { ChevronDown } from 'lucide-react';
|
||||
import { Region } from 'utils/regions';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { RegionSelector } from './RegionSelector';
|
||||
import { RegionSelector } from './RegionForm/RegionSelector';
|
||||
|
||||
// Form section components
|
||||
function RegionDeploymentSection({
|
||||
@@ -3,15 +3,18 @@ import { Form } from 'antd';
|
||||
import { useGetAccount } from 'api/generated/services/cloudintegration';
|
||||
import cx from 'classnames';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import {
|
||||
ModalStateEnum,
|
||||
RegionFormProps,
|
||||
} from 'container/Integrations/HeroSection/types';
|
||||
import { regions } from 'utils/regions';
|
||||
|
||||
import { ModalStateEnum, RegionFormProps } from '../types';
|
||||
import AlertMessage from './AlertMessage';
|
||||
import AlertMessage from '../../AlertMessage';
|
||||
import {
|
||||
ComplianceNote,
|
||||
MonitoringRegionsSection,
|
||||
RegionDeploymentSection,
|
||||
} from './IntegrateNowFormSections';
|
||||
} from '../IntegrateNowFormSections';
|
||||
import RenderConnectionFields from './RenderConnectionParams';
|
||||
|
||||
export function RegionForm({
|
||||
@@ -76,8 +79,6 @@ export function RegionForm({
|
||||
layout="vertical"
|
||||
onFinish={onSubmit}
|
||||
>
|
||||
<AlertMessage modalState={modalState} />
|
||||
|
||||
<div
|
||||
className={cx(`cloud-account-setup-form__content`, {
|
||||
disabled: isFormDisabled,
|
||||
@@ -100,6 +101,10 @@ export function RegionForm({
|
||||
isFormDisabled={isFormDisabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="cloud-account-setup-form__alert">
|
||||
<AlertMessage modalState={modalState} />
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { useListAccounts } from 'api/generated/services/cloudintegration';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import { mapAccountDtoToAwsCloudAccount } from '../mapAwsCloudAccountFromDto';
|
||||
import { mapAccountDtoToAwsCloudAccount } from '../../mapCloudAccountFromDto';
|
||||
import { CloudAccount } from '../types';
|
||||
|
||||
import './S3BucketsSelector.styles.scss';
|
||||
|
||||
@@ -12,14 +12,14 @@ import {
|
||||
useUpdateService,
|
||||
} from 'api/generated/services/cloudintegration';
|
||||
import {
|
||||
CloudintegrationtypesServiceConfigDTO,
|
||||
CloudintegrationtypesServiceDTO,
|
||||
ListServicesMetadata200,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import CloudServiceDataCollected from 'components/CloudIntegrations/CloudServiceDataCollected/CloudServiceDataCollected';
|
||||
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
|
||||
import ServiceDashboards from 'container/Integrations/CloudIntegration/AmazonWebServices/ServiceDashboards/ServiceDashboards';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { IServiceStatus } from 'container/Integrations/types';
|
||||
import ServiceDashboards from 'container/Integrations/CloudIntegration/ServiceDashboards/ServiceDashboards';
|
||||
import { IntegrationType, IServiceStatus } from 'container/Integrations/types';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { Save, X } from 'lucide-react';
|
||||
|
||||
@@ -36,7 +36,81 @@ type ServiceDetailsData = CloudintegrationtypesServiceDTO & {
|
||||
status?: IServiceStatus;
|
||||
};
|
||||
|
||||
function ServiceDetails(): JSX.Element | null {
|
||||
const EMPTY_FORM_VALUES: ServiceConfigFormValues = {
|
||||
logsEnabled: false,
|
||||
metricsEnabled: false,
|
||||
s3BucketsByRegion: {},
|
||||
};
|
||||
|
||||
function getInitialFormValues(
|
||||
type: IntegrationType,
|
||||
serviceDetailsData?: ServiceDetailsData,
|
||||
): ServiceConfigFormValues {
|
||||
const integrationConfig =
|
||||
type === IntegrationType.AWS_SERVICES
|
||||
? serviceDetailsData?.cloudIntegrationService?.config?.aws
|
||||
: serviceDetailsData?.cloudIntegrationService?.config?.azure;
|
||||
|
||||
return {
|
||||
logsEnabled: integrationConfig?.logs?.enabled || false,
|
||||
metricsEnabled: integrationConfig?.metrics?.enabled || false,
|
||||
s3BucketsByRegion:
|
||||
type === IntegrationType.AWS_SERVICES
|
||||
? serviceDetailsData?.cloudIntegrationService?.config?.aws?.logs
|
||||
?.s3Buckets || {}
|
||||
: {},
|
||||
};
|
||||
}
|
||||
|
||||
function getServiceConfigPayload({
|
||||
type,
|
||||
serviceId,
|
||||
logsEnabled,
|
||||
metricsEnabled,
|
||||
isLogsSupported,
|
||||
isMetricsSupported,
|
||||
s3BucketsByRegion,
|
||||
}: {
|
||||
type: IntegrationType;
|
||||
serviceId: string;
|
||||
logsEnabled: boolean;
|
||||
metricsEnabled: boolean;
|
||||
isLogsSupported: boolean;
|
||||
isMetricsSupported: boolean;
|
||||
s3BucketsByRegion: Record<string, string[]>;
|
||||
}): CloudintegrationtypesServiceConfigDTO {
|
||||
if (type === IntegrationType.AWS_SERVICES) {
|
||||
return {
|
||||
aws: {
|
||||
logs: {
|
||||
enabled: isLogsSupported ? logsEnabled : false,
|
||||
s3Buckets:
|
||||
serviceId === 's3sync' && isLogsSupported ? s3BucketsByRegion : {},
|
||||
},
|
||||
metrics: {
|
||||
enabled: isMetricsSupported ? metricsEnabled : false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
azure: {
|
||||
logs: {
|
||||
enabled: isLogsSupported ? logsEnabled : false,
|
||||
},
|
||||
metrics: {
|
||||
enabled: isMetricsSupported ? metricsEnabled : false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function ServiceDetails({
|
||||
type,
|
||||
}: {
|
||||
type: IntegrationType;
|
||||
}): JSX.Element | null {
|
||||
const urlQuery = useUrlQuery();
|
||||
const cloudAccountId = urlQuery.get('cloudAccountId');
|
||||
const serviceId = urlQuery.get('service');
|
||||
@@ -51,7 +125,7 @@ function ServiceDetails(): JSX.Element | null {
|
||||
isLoading: isServiceDetailsLoading,
|
||||
} = useGetService(
|
||||
{
|
||||
cloudProvider: INTEGRATION_TYPES.AWS,
|
||||
cloudProvider: type,
|
||||
serviceId: serviceId || '',
|
||||
},
|
||||
{
|
||||
@@ -65,10 +139,17 @@ function ServiceDetails(): JSX.Element | null {
|
||||
},
|
||||
);
|
||||
|
||||
const awsConfig = serviceDetailsData?.cloudIntegrationService?.config?.aws;
|
||||
const integrationConfig =
|
||||
type === IntegrationType.AWS_SERVICES
|
||||
? serviceDetailsData?.cloudIntegrationService?.config?.aws
|
||||
: serviceDetailsData?.cloudIntegrationService?.config?.azure;
|
||||
const isServiceEnabledInPersistedConfig =
|
||||
Boolean(awsConfig?.logs?.enabled) || Boolean(awsConfig?.metrics?.enabled);
|
||||
Boolean(integrationConfig?.logs?.enabled) ||
|
||||
Boolean(integrationConfig?.metrics?.enabled);
|
||||
const serviceDetailsId = serviceDetailsData?.id;
|
||||
const isLogsSupported = serviceDetailsData?.supportedSignals?.logs || false;
|
||||
const isMetricsSupported =
|
||||
serviceDetailsData?.supportedSignals?.metrics || false;
|
||||
|
||||
const {
|
||||
control,
|
||||
@@ -77,43 +158,31 @@ function ServiceDetails(): JSX.Element | null {
|
||||
watch,
|
||||
formState: { isDirty },
|
||||
} = useForm<ServiceConfigFormValues>({
|
||||
defaultValues: {
|
||||
logsEnabled: awsConfig?.logs?.enabled || false,
|
||||
metricsEnabled: awsConfig?.metrics?.enabled || false,
|
||||
s3BucketsByRegion: awsConfig?.logs?.s3Buckets || {},
|
||||
},
|
||||
defaultValues: getInitialFormValues(type, serviceDetailsData),
|
||||
});
|
||||
|
||||
const resetToAwsConfig = useCallback((): void => {
|
||||
reset({
|
||||
logsEnabled: awsConfig?.logs?.enabled || false,
|
||||
metricsEnabled: awsConfig?.metrics?.enabled || false,
|
||||
s3BucketsByRegion: awsConfig?.logs?.s3Buckets || {},
|
||||
});
|
||||
}, [awsConfig, reset]);
|
||||
const resetToConfig = useCallback((): void => {
|
||||
reset(getInitialFormValues(type, serviceDetailsData));
|
||||
}, [reset, serviceDetailsData, type]);
|
||||
|
||||
// Ensure form state does not leak across service switches while new details load.
|
||||
useEffect(() => {
|
||||
reset({
|
||||
logsEnabled: false,
|
||||
metricsEnabled: false,
|
||||
s3BucketsByRegion: {},
|
||||
});
|
||||
reset(EMPTY_FORM_VALUES);
|
||||
}, [reset, serviceId]);
|
||||
|
||||
useEffect(() => {
|
||||
resetToAwsConfig();
|
||||
}, [resetToAwsConfig, serviceDetailsId]);
|
||||
resetToConfig();
|
||||
}, [resetToConfig, serviceDetailsId]);
|
||||
|
||||
// log telemetry event on visiting details of a service.
|
||||
useEffect(() => {
|
||||
if (serviceId) {
|
||||
logEvent('AWS Integration: Service viewed', {
|
||||
logEvent(`${type} Integration: Service viewed`, {
|
||||
cloudAccountId,
|
||||
serviceId,
|
||||
});
|
||||
}
|
||||
}, [cloudAccountId, serviceId]);
|
||||
}, [cloudAccountId, serviceId, type]);
|
||||
|
||||
const { mutate: updateService, isLoading: isUpdatingServiceConfig } =
|
||||
useUpdateService();
|
||||
@@ -121,8 +190,8 @@ function ServiceDetails(): JSX.Element | null {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleDiscard = useCallback((): void => {
|
||||
resetToAwsConfig();
|
||||
}, [resetToAwsConfig]);
|
||||
resetToConfig();
|
||||
}, [resetToConfig]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (values: ServiceConfigFormValues): Promise<void> => {
|
||||
@@ -141,25 +210,25 @@ function ServiceDetails(): JSX.Element | null {
|
||||
return;
|
||||
}
|
||||
|
||||
const serviceConfigPayload = getServiceConfigPayload({
|
||||
type,
|
||||
serviceId,
|
||||
logsEnabled,
|
||||
metricsEnabled,
|
||||
isLogsSupported,
|
||||
isMetricsSupported,
|
||||
s3BucketsByRegion: normalizedS3BucketsByRegion,
|
||||
});
|
||||
|
||||
updateService(
|
||||
{
|
||||
pathParams: {
|
||||
cloudProvider: INTEGRATION_TYPES.AWS,
|
||||
cloudProvider: type,
|
||||
id: cloudAccountId,
|
||||
serviceId,
|
||||
},
|
||||
data: {
|
||||
config: {
|
||||
aws: {
|
||||
logs: {
|
||||
enabled: logsEnabled,
|
||||
s3Buckets: normalizedS3BucketsByRegion,
|
||||
},
|
||||
metrics: {
|
||||
enabled: metricsEnabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
config: serviceConfigPayload,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -170,7 +239,7 @@ function ServiceDetails(): JSX.Element | null {
|
||||
|
||||
const servicesListQueryKey = getListServicesMetadataQueryKey(
|
||||
{
|
||||
cloudProvider: INTEGRATION_TYPES.AWS,
|
||||
cloudProvider: type,
|
||||
},
|
||||
{
|
||||
cloud_integration_id: cloudAccountId,
|
||||
@@ -203,7 +272,7 @@ function ServiceDetails(): JSX.Element | null {
|
||||
invalidateGetService(
|
||||
queryClient,
|
||||
{
|
||||
cloudProvider: INTEGRATION_TYPES.AWS,
|
||||
cloudProvider: type,
|
||||
serviceId,
|
||||
},
|
||||
{
|
||||
@@ -214,14 +283,14 @@ function ServiceDetails(): JSX.Element | null {
|
||||
invalidateListServicesMetadata(
|
||||
queryClient,
|
||||
{
|
||||
cloudProvider: INTEGRATION_TYPES.AWS,
|
||||
cloudProvider: type,
|
||||
},
|
||||
{
|
||||
cloud_integration_id: cloudAccountId,
|
||||
},
|
||||
);
|
||||
|
||||
logEvent('AWS Integration: Service settings saved', {
|
||||
logEvent(`${type} Integration: Service settings saved`, {
|
||||
cloudAccountId,
|
||||
serviceId,
|
||||
logsEnabled,
|
||||
@@ -241,7 +310,16 @@ function ServiceDetails(): JSX.Element | null {
|
||||
console.error('Form submission failed:', error);
|
||||
}
|
||||
},
|
||||
[serviceId, cloudAccountId, updateService, queryClient, reset],
|
||||
[
|
||||
serviceId,
|
||||
cloudAccountId,
|
||||
updateService,
|
||||
queryClient,
|
||||
reset,
|
||||
type,
|
||||
isLogsSupported,
|
||||
isMetricsSupported,
|
||||
],
|
||||
);
|
||||
|
||||
if (isServiceDetailsLoading) {
|
||||
@@ -262,10 +340,6 @@ function ServiceDetails(): JSX.Element | null {
|
||||
const logsEnabled = watch('logsEnabled');
|
||||
const s3BucketsByRegion = watch('s3BucketsByRegion');
|
||||
|
||||
const isLogsSupported = serviceDetailsData?.supportedSignals?.logs || false;
|
||||
const isMetricsSupported =
|
||||
serviceDetailsData?.supportedSignals?.metrics || false;
|
||||
|
||||
const hasUnsavedChanges = isDirty;
|
||||
|
||||
const isS3SyncBucketsMissing =
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import HeroSection from './HeroSection/HeroSection';
|
||||
import ServiceDetails from './ServiceDetails/ServiceDetails';
|
||||
import ServicesList from './ServicesList';
|
||||
|
||||
import './ServicesTabs.style.scss';
|
||||
|
||||
function ServicesTabs(): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const cloudAccountId = urlQuery.get('cloudAccountId') || '';
|
||||
|
||||
return (
|
||||
<div className="services-tabs">
|
||||
<HeroSection />
|
||||
|
||||
<div className="services-section">
|
||||
<div className="services-section__sidebar">
|
||||
<ServicesList cloudAccountId={cloudAccountId} />
|
||||
</div>
|
||||
<div className="services-section__content">
|
||||
<ServiceDetails />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServicesTabs;
|
||||
@@ -29,7 +29,7 @@ jest.mock('components/MarkdownRenderer/MarkdownRenderer', () => ({
|
||||
MarkdownRenderer: (): JSX.Element => <div data-testid="markdown-renderer" />,
|
||||
}));
|
||||
jest.mock(
|
||||
'container/Integrations/CloudIntegration/AmazonWebServices/ServiceDashboards/ServiceDashboards',
|
||||
'container/Integrations/CloudIntegration/ServiceDashboards/ServiceDashboards',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div data-testid="service-dashboards" />,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { render, RenderResult, screen, waitFor } from '@testing-library/react';
|
||||
import { IntegrationType } from 'container/Integrations/types';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
|
||||
import ServiceDetails from '../ServiceDetails/ServiceDetails';
|
||||
@@ -11,10 +12,11 @@ import { accountsResponse } from './mockData';
|
||||
const renderServiceDetails = (
|
||||
_initialConfigLogsS3Buckets: Record<string, string[]> = {},
|
||||
_serviceId = 's3sync',
|
||||
type: IntegrationType = IntegrationType.AWS_SERVICES,
|
||||
): RenderResult =>
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<ServiceDetails />
|
||||
<ServiceDetails type={type} />
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { CloudintegrationtypesAccountDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { CloudAccount } from './types';
|
||||
|
||||
export function mapAccountDtoToAwsCloudAccount(
|
||||
account: CloudintegrationtypesAccountDTO,
|
||||
): CloudAccount | null {
|
||||
if (!account.providerAccountId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
cloud_account_id: account.id,
|
||||
config: {
|
||||
regions: account.config?.aws?.regions ?? [],
|
||||
},
|
||||
status: {
|
||||
integration: {
|
||||
last_heartbeat_ts_ms: account.agentReport?.timestampMillis ?? 0,
|
||||
},
|
||||
},
|
||||
providerAccountId: account.providerAccountId,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { ChevronDown, ChevronRight } from '@signozhq/icons';
|
||||
import { Button, Callout, DrawerWrapper, Tabs } from '@signozhq/ui';
|
||||
import { Form, Select, Spin } from 'antd';
|
||||
import { useGetAccount } from 'api/generated/services/cloudintegration';
|
||||
import { CloudintegrationtypesAccountDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import CodeBlock from 'components/CodeBlock/CodeBlock';
|
||||
import {
|
||||
AZURE_REGIONS,
|
||||
INTEGRATION_TYPES,
|
||||
} from 'container/Integrations/constants';
|
||||
import {
|
||||
IntegrationModalProps,
|
||||
ModalStateEnum,
|
||||
} from 'container/Integrations/HeroSection/types';
|
||||
import { LoaderCircle, SquareArrowOutUpRight } from 'lucide-react';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { useIntegrationModal } from '../../../../../hooks/integration/azure/useIntegrationModal';
|
||||
import RenderConnectionFields from '../../AmazonWebServices/RegionForm/RenderConnectionParams';
|
||||
|
||||
import '../../AmazonWebServices/AddNewAccount/CloudAccountSetupModal.style.scss';
|
||||
|
||||
const AZURE_CLI_DESC =
|
||||
'Paste the following command if you have Azure CLI setup locally on your machine or use BASH CloudShell on Azure portal with above mentioned permissions.';
|
||||
const AZURE_POWERSHELL_DESC =
|
||||
'Paste the following command in PowerShell CloudShell on Azure portal, you can switch to PowerShell on Azure portal.';
|
||||
|
||||
function CloudAccountSetupModal({
|
||||
onClose,
|
||||
}: IntegrationModalProps): JSX.Element {
|
||||
const {
|
||||
form,
|
||||
modalState,
|
||||
isLoading,
|
||||
accountId,
|
||||
connectionCommands,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
connectionParams,
|
||||
isConnectionParamsLoading,
|
||||
handleConnectionSuccess,
|
||||
handleConnectionTimeout,
|
||||
handleConnectionError,
|
||||
} = useIntegrationModal({ onClose });
|
||||
|
||||
const startTimeRef = useRef(Date.now());
|
||||
const refetchInterval = 10 * 1000;
|
||||
const errorTimeout = 10 * 60 * 1000;
|
||||
|
||||
const [isHowItWorksOpen, setIsHowItWorksOpen] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState('cli');
|
||||
|
||||
useGetAccount(
|
||||
{
|
||||
cloudProvider: INTEGRATION_TYPES.AZURE,
|
||||
id: accountId ?? '',
|
||||
},
|
||||
{
|
||||
query: {
|
||||
enabled: Boolean(accountId) && modalState === ModalStateEnum.WAITING,
|
||||
refetchInterval,
|
||||
select: (response): CloudintegrationtypesAccountDTO => response.data,
|
||||
onSuccess: (account) => {
|
||||
const isConnected =
|
||||
Boolean(account.providerAccountId) && account.removedAt === null;
|
||||
|
||||
if (isConnected) {
|
||||
handleConnectionSuccess({
|
||||
cloudAccountId: account.providerAccountId ?? account.id,
|
||||
status: account.agentReport,
|
||||
});
|
||||
} else if (Date.now() - startTimeRef.current >= errorTimeout) {
|
||||
handleConnectionTimeout({ id: accountId });
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
handleConnectionError();
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const renderAlert = useCallback((): JSX.Element | null => {
|
||||
if (modalState === ModalStateEnum.WAITING) {
|
||||
return (
|
||||
<div className="cloud-account-setup-form__alert">
|
||||
<Callout
|
||||
title={
|
||||
<div className="cloud-account-setup-form__alert-message">
|
||||
<Spin
|
||||
indicator={
|
||||
<LoaderCircle
|
||||
size={14}
|
||||
className="anticon anticon-loading anticon-spin ant-spin-dot"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
Waiting for Azure account connection, retrying in{' '}
|
||||
<span className="retry-time">10</span> secs...
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
showIcon={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (modalState === ModalStateEnum.ERROR) {
|
||||
return (
|
||||
<div className="cloud-account-setup-form__alert">
|
||||
<Callout
|
||||
title={
|
||||
<div className="cloud-account-setup-form__alert-message">
|
||||
We couldn't establish a connection to your Azure account. Please
|
||||
try again
|
||||
</div>
|
||||
}
|
||||
type="error"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [modalState]);
|
||||
|
||||
const footer = (
|
||||
<div className="cloud-account-setup-modal__footer">
|
||||
{modalState === ModalStateEnum.FORM && (
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
prefix={<SquareArrowOutUpRight size={17} color={Color.BG_VANILLA_100} />}
|
||||
onClick={handleSubmit}
|
||||
loading={isLoading}
|
||||
>
|
||||
Generate Azure Setup Commands
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<DrawerWrapper
|
||||
open={true}
|
||||
className="cloud-account-setup-modal"
|
||||
onOpenChange={(open): void => {
|
||||
if (!open) {
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
direction="right"
|
||||
showCloseButton
|
||||
title="Add Azure Account"
|
||||
width="wide"
|
||||
footer={footer}
|
||||
>
|
||||
<div className="cloud-account-setup-modal__content">
|
||||
<div className="cloud-account-setup-prerequisites">
|
||||
<div className="cloud-account-setup-prerequisites__title">
|
||||
Prerequisites
|
||||
</div>
|
||||
|
||||
<ul className="cloud-account-setup-prerequisites__list">
|
||||
<li className="cloud-account-setup-prerequisites__list-item">
|
||||
<span className="cloud-account-setup-prerequisites__list-item-bullet">
|
||||
—
|
||||
</span>{' '}
|
||||
<span className="cloud-account-setup-prerequisites__list-item-text">
|
||||
Ensure that you're logged in to the Azure workspace which you want
|
||||
to monitor.
|
||||
</span>
|
||||
</li>
|
||||
<li className="cloud-account-setup-prerequisites__list-item">
|
||||
<span className="cloud-account-setup-prerequisites__list-item-bullet">
|
||||
—
|
||||
</span>{' '}
|
||||
<span className="cloud-account-setup-prerequisites__list-item-text">
|
||||
Ensure that you either have the{' '}
|
||||
<span className="cloud-account-setup-prerequisites__list-item-highlight">
|
||||
Owner
|
||||
</span>{' '}
|
||||
role OR
|
||||
</span>
|
||||
</li>
|
||||
<li className="cloud-account-setup-prerequisites__list-item">
|
||||
<span className="cloud-account-setup-prerequisites__list-item-bullet">
|
||||
—
|
||||
</span>{' '}
|
||||
<span className="cloud-account-setup-prerequisites__list-item-text">
|
||||
Both the{' '}
|
||||
<span className="cloud-account-setup-prerequisites__list-item-highlight">
|
||||
Contributor
|
||||
</span>{' '}
|
||||
and{' '}
|
||||
<span className="cloud-account-setup-prerequisites__list-item-highlight">
|
||||
user access admin
|
||||
</span>{' '}
|
||||
roles
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="cloud-account-setup-how-it-works-accordion">
|
||||
<div
|
||||
className={`cloud-account-setup-how-it-works-accordion__title ${
|
||||
isHowItWorksOpen ? 'open' : ''
|
||||
}`}
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
color="secondary"
|
||||
onClick={(): void => setIsHowItWorksOpen(!isHowItWorksOpen)}
|
||||
prefix={isHowItWorksOpen ? <ChevronDown /> : <ChevronRight />}
|
||||
/>
|
||||
|
||||
<span className="cloud-account-setup-how-it-works-accordion__title-text">
|
||||
How it works?
|
||||
</span>
|
||||
</div>
|
||||
{isHowItWorksOpen && (
|
||||
<div className="cloud-account-setup-how-it-works-accordion__description">
|
||||
<div className="cloud-account-setup-how-it-works-accordion__description-item">
|
||||
SigNoz will create new resource-group to manage the resources required
|
||||
for this integration. The following steps will create a User-Assigned
|
||||
Managed Identity with the necessary permissions and follows the
|
||||
Principle of Least Privilege.
|
||||
</div>
|
||||
<div className="cloud-account-setup-how-it-works__description-item">
|
||||
Once the Integration template is deployed, you can enable the services
|
||||
you want to monitor right here in Signoz dashboard.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
className="cloud-account-setup-form"
|
||||
layout="vertical"
|
||||
initialValues={{ resourceGroups: [] }}
|
||||
>
|
||||
<div className="cloud-account-setup-form__content">
|
||||
<div className="cloud-account-setup-form__form-group">
|
||||
<div className="cloud-account-setup-form__title">
|
||||
Where should we deploy the SigNoz collector resources?
|
||||
</div>
|
||||
<div className="cloud-account-setup-form__description">
|
||||
Choose the Azure region for deployment.
|
||||
</div>
|
||||
<Form.Item
|
||||
name="region"
|
||||
rules={[{ required: true, message: 'Please select a region' }]}
|
||||
className="cloud-account-setup-form__form-item"
|
||||
>
|
||||
<Select
|
||||
placeholder="e.g. East US"
|
||||
options={AZURE_REGIONS.map((region) => ({
|
||||
label: `${region.label} (${region.value})`,
|
||||
value: region.value,
|
||||
}))}
|
||||
getPopupContainer={popupContainer}
|
||||
disabled={modalState === ModalStateEnum.WAITING}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<div className="cloud-account-setup-form__form-group">
|
||||
<div className="cloud-account-setup-form__title">
|
||||
Which resource groups do you want to monitor?
|
||||
</div>
|
||||
<div className="cloud-account-setup-form__description">
|
||||
Add one or more Azure resource group names.
|
||||
</div>
|
||||
<Form.Item
|
||||
name="resourceGroups"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
type: 'array',
|
||||
min: 1,
|
||||
message: 'Please add at least one resource group',
|
||||
},
|
||||
]}
|
||||
className="cloud-account-setup-form__form-item"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder="e.g. prod-platform-rg"
|
||||
tokenSeparators={[',']}
|
||||
disabled={modalState === ModalStateEnum.WAITING}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<RenderConnectionFields
|
||||
isConnectionParamsLoading={isConnectionParamsLoading}
|
||||
connectionParams={connectionParams}
|
||||
isFormDisabled={modalState === ModalStateEnum.WAITING}
|
||||
/>
|
||||
|
||||
{connectionCommands && (
|
||||
<div className="cloud-account-setup-form__code-block-tabs-container">
|
||||
<div className="cloud-account-setup-form__code-block-tabs-header">
|
||||
<div className="cloud-account-setup-form__code-block-tabs-header-title">
|
||||
Deploy Agent
|
||||
</div>
|
||||
<div className="cloud-account-setup-form__code-block-tabs-header-description">
|
||||
{activeTab === 'cli' ? AZURE_CLI_DESC : AZURE_POWERSHELL_DESC}
|
||||
</div>
|
||||
</div>
|
||||
<Tabs
|
||||
className="cloud-account-setup-form__code-block-tabs"
|
||||
items={[
|
||||
{
|
||||
key: 'cli',
|
||||
label: 'CLI',
|
||||
children: <CodeBlock code={connectionCommands?.cliCommand || ''} />,
|
||||
},
|
||||
{
|
||||
key: 'powershell',
|
||||
label: 'PowerShell',
|
||||
children: (
|
||||
<CodeBlock
|
||||
code={connectionCommands?.cloudPowerShellCommand || ''}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
value={activeTab}
|
||||
onChange={(key): void => setActiveTab(key)}
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{renderAlert()}
|
||||
|
||||
{modalState === ModalStateEnum.WAITING && (
|
||||
<div className="cloud-account-setup-status-message">
|
||||
After running the command, return here and wait for automatic connection
|
||||
detection.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</DrawerWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default CloudAccountSetupModal;
|
||||
@@ -0,0 +1,150 @@
|
||||
import { Dispatch, SetStateAction, useMemo } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Button, DrawerWrapper } from '@signozhq/ui';
|
||||
import { Form, Select } from 'antd';
|
||||
import { invalidateListAccounts } from 'api/generated/services/cloudintegration';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { CloudAccount } from 'container/Integrations/types';
|
||||
import { Save } from 'lucide-react';
|
||||
|
||||
import { useAccountSettingsModal } from '../../../../../hooks/integration/azure/useAccountSettingsModal';
|
||||
import RemoveIntegrationAccount from '../../RemoveAccount/RemoveIntegrationAccount';
|
||||
|
||||
import '../../AmazonWebServices/EditAccount/AccountSettingsModal.style.scss';
|
||||
|
||||
interface AccountSettingsModalProps {
|
||||
onClose: () => void;
|
||||
account: CloudAccount;
|
||||
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
|
||||
}
|
||||
|
||||
function AccountSettingsModal({
|
||||
onClose,
|
||||
account,
|
||||
setActiveAccount,
|
||||
}: AccountSettingsModalProps): JSX.Element {
|
||||
const {
|
||||
form,
|
||||
isLoading,
|
||||
resourceGroups,
|
||||
isSaveDisabled,
|
||||
setResourceGroups,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
} = useAccountSettingsModal({ onClose, account, setActiveAccount });
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const azureConfig = useMemo(
|
||||
() => ('deployment_region' in account.config ? account.config : null),
|
||||
[account.config],
|
||||
);
|
||||
|
||||
return (
|
||||
<DrawerWrapper
|
||||
open={true}
|
||||
className="account-settings-modal"
|
||||
title="Account Settings"
|
||||
direction="right"
|
||||
showCloseButton
|
||||
onOpenChange={(open): void => {
|
||||
if (!open) {
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
width="wide"
|
||||
footer={
|
||||
<div className="account-settings-modal__footer">
|
||||
<RemoveIntegrationAccount
|
||||
accountId={account?.id}
|
||||
onRemoveIntegrationAccountSuccess={(): void => {
|
||||
void invalidateListAccounts(queryClient, {
|
||||
cloudProvider: INTEGRATION_TYPES.AZURE,
|
||||
});
|
||||
setActiveAccount(null);
|
||||
handleClose();
|
||||
}}
|
||||
cloudProvider={INTEGRATION_TYPES.AZURE}
|
||||
/>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
disabled={isSaveDisabled}
|
||||
onClick={handleSubmit}
|
||||
loading={isLoading}
|
||||
prefix={<Save size={14} />}
|
||||
>
|
||||
Update Changes
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
resourceGroups: azureConfig?.resource_groups || [],
|
||||
}}
|
||||
>
|
||||
<div className="account-settings-modal__body">
|
||||
<div className="account-settings-modal__body-account-info">
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details">
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details-title">
|
||||
Connected Account details
|
||||
</div>
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details-account-id">
|
||||
Azure Subscription:{' '}
|
||||
<span className="account-settings-modal__body-account-info-connected-account-details-account-id-account-id">
|
||||
{account?.providerAccountId}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{azureConfig?.deployment_region && (
|
||||
<div className="account-settings-modal__body-region-selector">
|
||||
<div className="account-settings-modal__body-region-selector-title">
|
||||
Deployment region
|
||||
</div>
|
||||
<div className="account-settings-modal__body-region-selector-description">
|
||||
{azureConfig.deployment_region}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="account-settings-modal__body-region-selector">
|
||||
<div className="account-settings-modal__body-region-selector-title">
|
||||
Resource groups
|
||||
</div>
|
||||
<div className="account-settings-modal__body-region-selector-description">
|
||||
Update the resource groups that should be monitored.
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
name="resourceGroups"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
type: 'array',
|
||||
min: 1,
|
||||
message: 'Please add at least one resource group',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
value={resourceGroups}
|
||||
onChange={(values): void => {
|
||||
setResourceGroups(values);
|
||||
form.setFieldValue('resourceGroups', values);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</DrawerWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountSettingsModal;
|
||||
@@ -1,16 +1,15 @@
|
||||
import { IntegrationType } from 'container/Integrations/types';
|
||||
|
||||
import AWSTabs from './AmazonWebServices/ServicesTabs';
|
||||
import Header from './Header/Header';
|
||||
import ServicesTabs from './ServiceTabs/ServicesTabs';
|
||||
|
||||
import './CloudIntegration.styles.scss';
|
||||
|
||||
const CloudIntegration = ({ type }: { type: IntegrationType }): JSX.Element => {
|
||||
return (
|
||||
<div className="cloud-integration-container">
|
||||
<Header title={type} />
|
||||
|
||||
{type === IntegrationType.AWS_SERVICES && <AWSTabs />}
|
||||
<Header type={type} />
|
||||
<ServicesTabs type={type} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Blocks, LifeBuoy } from 'lucide-react';
|
||||
|
||||
import './Header.styles.scss';
|
||||
|
||||
function Header({ title }: { title: IntegrationType }): JSX.Element {
|
||||
function Header({ type }: { type: IntegrationType }): JSX.Element {
|
||||
return (
|
||||
<div className="cloud-header">
|
||||
<div className="cloud-header__navigation">
|
||||
@@ -25,27 +25,30 @@ function Header({ title }: { title: IntegrationType }): JSX.Element {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <div className="cloud-header__breadcrumb-title">{title}</div>,
|
||||
title: <div className="cloud-header__breadcrumb-title">{type}</div>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="cloud-header__actions">
|
||||
<Button
|
||||
variant="solid"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={(): void => {
|
||||
window.open(
|
||||
'https://signoz.io/blog/native-aws-integrations-with-autodiscovery/',
|
||||
'_blank',
|
||||
);
|
||||
}}
|
||||
prefix={<LifeBuoy size={12} />}
|
||||
>
|
||||
Get Help
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{type === IntegrationType.AWS_SERVICES && (
|
||||
<div className="cloud-header__actions">
|
||||
<Button
|
||||
variant="solid"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={(): void => {
|
||||
window.open(
|
||||
'https://signoz.io/blog/native-aws-integrations-with-autodiscovery/',
|
||||
'_blank',
|
||||
);
|
||||
}}
|
||||
prefix={<LifeBuoy size={12} />}
|
||||
>
|
||||
Get Help
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
.remove-integration-account-modal {
|
||||
&__cloud-provider {
|
||||
color: var(--l1-foreground);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
background-color: var(--l1-background);
|
||||
border: 1px solid var(--l3-background);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.ant-modal-close {
|
||||
@@ -4,16 +4,21 @@ import { Modal } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useDisconnectAccount } from 'api/generated/services/cloudintegration';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { INTEGRATION_TELEMETRY_EVENTS } from 'container/Integrations/constants';
|
||||
import {
|
||||
INTEGRATION_TELEMETRY_EVENTS,
|
||||
INTEGRATION_TYPES,
|
||||
} from 'container/Integrations/constants';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { Unlink } from 'lucide-react';
|
||||
|
||||
import './RemoveIntegrationAccount.scss';
|
||||
|
||||
function RemoveIntegrationAccount({
|
||||
cloudProvider,
|
||||
accountId,
|
||||
onRemoveIntegrationAccountSuccess,
|
||||
}: {
|
||||
cloudProvider: string;
|
||||
accountId: string;
|
||||
onRemoveIntegrationAccountSuccess: () => void;
|
||||
}): JSX.Element {
|
||||
@@ -39,12 +44,13 @@ function RemoveIntegrationAccount({
|
||||
},
|
||||
});
|
||||
const handleOk = (): void => {
|
||||
logEvent(INTEGRATION_TELEMETRY_EVENTS.AWS_INTEGRATION_ACCOUNT_REMOVED, {
|
||||
logEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATION_ACCOUNT_REMOVED, {
|
||||
accountId,
|
||||
integration: cloudProvider,
|
||||
});
|
||||
disconnectAccount({
|
||||
pathParams: {
|
||||
cloudProvider: 'aws',
|
||||
cloudProvider,
|
||||
id: accountId,
|
||||
},
|
||||
});
|
||||
@@ -78,13 +84,28 @@ function RemoveIntegrationAccount({
|
||||
loading: isRemoveIntegrationLoading,
|
||||
}}
|
||||
>
|
||||
Removing this account will remove all components created for sending
|
||||
telemetry to SigNoz in your AWS account within the next ~15 minutes
|
||||
(cloudformation stacks named signoz-integration-telemetry-collection in
|
||||
enabled regions). <br />
|
||||
<br />
|
||||
After that, you can delete the cloudformation stack that was created
|
||||
manually when connecting this account.
|
||||
{cloudProvider === INTEGRATION_TYPES.AWS ? (
|
||||
<>
|
||||
Removing this account will remove all components created for sending
|
||||
telemetry to SigNoz in your AWS account within the next ~15 minutes
|
||||
(cloudformation stacks named signoz-integration-telemetry-collection in
|
||||
enabled regions). <br />
|
||||
<br />
|
||||
After that, you can delete the cloudformation stack that was created
|
||||
manually when connecting this account.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Removing this account will remove all components created for sending
|
||||
telemetry to SigNoz in your Azure subscription within the next ~15 minutes
|
||||
(deployment stack named signoz-integration-telemetry will be deleted
|
||||
automatically). <br />
|
||||
<br />
|
||||
After that, you have to manually delete 'signoz-integration'
|
||||
deployment stack that was created while connecting this account (Takes ~20
|
||||
minutes to delete).
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
@@ -1,7 +1,7 @@
|
||||
import cx from 'classnames';
|
||||
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
||||
|
||||
import { Service } from './types';
|
||||
import { Service } from './AmazonWebServices/types';
|
||||
|
||||
function ServiceItem({
|
||||
service,
|
||||
@@ -0,0 +1,30 @@
|
||||
import { IntegrationType } from 'container/Integrations/types';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import HeroSection from '../../HeroSection/HeroSection';
|
||||
import ServiceDetails from '../AmazonWebServices/ServiceDetails/ServiceDetails';
|
||||
import ServicesList from '../ServicesList';
|
||||
|
||||
import './ServicesTabs.style.scss';
|
||||
|
||||
function ServicesTabs({ type }: { type: IntegrationType }): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const cloudAccountId = urlQuery.get('cloudAccountId') || '';
|
||||
|
||||
return (
|
||||
<div className="services-tabs">
|
||||
<HeroSection type={type} />
|
||||
|
||||
<div className="services-section">
|
||||
<div className="services-section__sidebar">
|
||||
<ServicesList cloudAccountId={cloudAccountId} type={type} />
|
||||
</div>
|
||||
<div className="services-section__content">
|
||||
<ServiceDetails type={type} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServicesTabs;
|
||||
@@ -4,15 +4,20 @@ import { Skeleton } from 'antd';
|
||||
import { useListServicesMetadata } from 'api/generated/services/cloudintegration';
|
||||
import type { CloudintegrationtypesServiceMetadataDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import cx from 'classnames';
|
||||
import { IntegrationType } from 'container/Integrations/types';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
import emptyStateIconUrl from '@/assets/Icons/emptyState.svg';
|
||||
|
||||
interface ServicesListProps {
|
||||
cloudAccountId: string;
|
||||
type: IntegrationType;
|
||||
}
|
||||
|
||||
function ServicesList({ cloudAccountId }: ServicesListProps): JSX.Element {
|
||||
function ServicesList({
|
||||
cloudAccountId,
|
||||
type,
|
||||
}: ServicesListProps): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const navigate = useNavigate();
|
||||
const hasValidCloudAccountId = Boolean(cloudAccountId);
|
||||
@@ -22,7 +27,7 @@ function ServicesList({ cloudAccountId }: ServicesListProps): JSX.Element {
|
||||
|
||||
const { data: servicesMetadata, isLoading } = useListServicesMetadata(
|
||||
{
|
||||
cloudProvider: 'aws',
|
||||
cloudProvider: type,
|
||||
},
|
||||
serviceQueryParams,
|
||||
);
|
||||
@@ -0,0 +1,49 @@
|
||||
import { CloudintegrationtypesAccountDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { CloudAccount as IntegrationCloudAccount } from 'container/Integrations/types';
|
||||
|
||||
import { CloudAccount as AwsCloudAccount } from './AmazonWebServices/types';
|
||||
|
||||
export function mapAccountDtoToAwsCloudAccount(
|
||||
account: CloudintegrationtypesAccountDTO,
|
||||
): AwsCloudAccount | null {
|
||||
if (!account.providerAccountId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
cloud_account_id: account.id,
|
||||
config: {
|
||||
regions: account.config?.aws?.regions ?? [],
|
||||
},
|
||||
status: {
|
||||
integration: {
|
||||
last_heartbeat_ts_ms: account.agentReport?.timestampMillis ?? 0,
|
||||
},
|
||||
},
|
||||
providerAccountId: account.providerAccountId,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapAccountDtoToAzureCloudAccount(
|
||||
account: CloudintegrationtypesAccountDTO,
|
||||
): IntegrationCloudAccount | null {
|
||||
if (!account.providerAccountId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
cloud_account_id: account.id,
|
||||
config: {
|
||||
deployment_region: account.config?.azure?.deploymentRegion ?? '',
|
||||
resource_groups: account.config?.azure?.resourceGroups ?? [],
|
||||
},
|
||||
status: {
|
||||
integration: {
|
||||
last_heartbeat_ts_ms: account.agentReport?.timestampMillis ?? 0,
|
||||
},
|
||||
},
|
||||
providerAccountId: account.providerAccountId,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,32 @@
|
||||
import { ONE_CLICK_INTEGRATIONS } from '../constants';
|
||||
import { IntegrationType } from '../types';
|
||||
|
||||
export const getAccountById = <T extends { cloud_account_id: string }>(
|
||||
accounts: T[],
|
||||
accountId: string,
|
||||
): T | null =>
|
||||
accounts.find((account) => account.cloud_account_id === accountId) || null;
|
||||
|
||||
interface IntegrationMetadata {
|
||||
title: string;
|
||||
description: string;
|
||||
logo: string;
|
||||
}
|
||||
|
||||
export const getIntegrationMetadata = (
|
||||
type: IntegrationType,
|
||||
): IntegrationMetadata => {
|
||||
const integration = ONE_CLICK_INTEGRATIONS.find(
|
||||
(integration) => integration.id === type,
|
||||
);
|
||||
|
||||
if (!integration) {
|
||||
return { title: '', description: '', logo: '' };
|
||||
}
|
||||
|
||||
return {
|
||||
title: integration.title,
|
||||
description: integration.description,
|
||||
logo: integration.icon,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { IntegrationType } from 'container/Integrations/types';
|
||||
|
||||
import AccountActions from '../CloudIntegration/AmazonWebServices/AccountActions/AccountActions';
|
||||
import { getIntegrationMetadata } from '../CloudIntegration/utils';
|
||||
|
||||
import './HeroSection.style.scss';
|
||||
|
||||
function HeroSection({ type }: { type: IntegrationType }): JSX.Element {
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
logo: integrationLogo,
|
||||
} = getIntegrationMetadata(type);
|
||||
|
||||
return (
|
||||
<div className="hero-section">
|
||||
<div className="hero-section__details">
|
||||
<div className="hero-section__details-header">
|
||||
<div className="hero-section__icon">
|
||||
<img src={integrationLogo} alt={type} />
|
||||
</div>
|
||||
|
||||
<div className="hero-section__details-title">{title}</div>
|
||||
</div>
|
||||
<div className="hero-section__details-description">{description}</div>
|
||||
</div>
|
||||
|
||||
<AccountActions type={type} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HeroSection;
|
||||
@@ -9,53 +9,6 @@
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.error-container {
|
||||
display: flex;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
.error-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 300px;
|
||||
gap: 15px;
|
||||
|
||||
.error-btns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
.retry-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.contact-support {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
.text {
|
||||
color: var(--callout-primary-description);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-state-svg {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-integration-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -327,6 +280,36 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-container {
|
||||
display: flex;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
.error-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 300px;
|
||||
gap: 15px;
|
||||
|
||||
.error-btns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.error-state-svg {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.remove-integration-modal {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from '@signozhq/ui';
|
||||
import { Flex, Skeleton, Typography } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -55,8 +54,19 @@ function IntegrationDetailPage(): JSX.Element {
|
||||
),
|
||||
);
|
||||
|
||||
if (integrationId === INTEGRATION_TYPES.AWS) {
|
||||
return <CloudIntegration type={IntegrationType.AWS_SERVICES} />;
|
||||
if (
|
||||
integrationId === INTEGRATION_TYPES.AWS ||
|
||||
integrationId === INTEGRATION_TYPES.AZURE
|
||||
) {
|
||||
return (
|
||||
<CloudIntegration
|
||||
type={
|
||||
integrationId === INTEGRATION_TYPES.AWS
|
||||
? IntegrationType.AWS_SERVICES
|
||||
: IntegrationType.AZURE_SERVICES
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -85,20 +95,20 @@ function IntegrationDetailPage(): JSX.Element {
|
||||
<div className="error-btns">
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
color="secondary"
|
||||
onClick={(): Promise<any> => refetch()}
|
||||
prefix={<RotateCw size={14} />}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
<div
|
||||
className="contact-support"
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
onClick={(): void => handleContactSupport(isCloudUserVal)}
|
||||
suffix={<MoveUpRight size={12} />}
|
||||
>
|
||||
<Typography.Link className="text">Contact Support </Typography.Link>
|
||||
|
||||
<MoveUpRight size={14} color={Color.BG_ROBIN_400} />
|
||||
</div>
|
||||
Contact Support
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@ function OneClickIntegrations(props: OneClickIntegrationsProps): JSX.Element {
|
||||
if (!query) {
|
||||
return ONE_CLICK_INTEGRATIONS;
|
||||
}
|
||||
|
||||
return ONE_CLICK_INTEGRATIONS.filter(
|
||||
(integration) =>
|
||||
integration.title.toLowerCase().includes(query) ||
|
||||
|
||||
@@ -14,8 +14,8 @@ export const INTEGRATION_TELEMETRY_EVENTS = {
|
||||
'Integrations Detail Page: Clicked remove Integration button for integration',
|
||||
INTEGRATIONS_DETAIL_CONFIGURE_INSTRUCTION:
|
||||
'Integrations Detail Page: Navigated to configure an integration',
|
||||
AWS_INTEGRATION_ACCOUNT_REMOVED:
|
||||
'AWS Integration Detail page: Clicked remove Integration button for integration',
|
||||
INTEGRATION_ACCOUNT_REMOVED:
|
||||
'Integration Detail page: Clicked remove Integration button for integration',
|
||||
};
|
||||
|
||||
export const INTEGRATION_TYPES = {
|
||||
@@ -53,7 +53,7 @@ export const AZURE_INTEGRATION = {
|
||||
is_new: true,
|
||||
};
|
||||
|
||||
export const ONE_CLICK_INTEGRATIONS = [AWS_INTEGRATION];
|
||||
export const ONE_CLICK_INTEGRATIONS = [AWS_INTEGRATION, AZURE_INTEGRATION];
|
||||
|
||||
export const AZURE_REGIONS: AzureRegion[] = [
|
||||
{
|
||||
@@ -81,6 +81,7 @@ export const AZURE_REGIONS: AzureRegion[] = [
|
||||
{ label: 'Central India', value: 'centralindia', geography: 'India' },
|
||||
{ label: 'Central US', value: 'centralus', geography: 'United States' },
|
||||
{ label: 'Chile Central', value: 'chilecentral', geography: 'Chile' },
|
||||
{ label: 'Denmark East', value: 'denmarkeast', geography: 'Denmark' },
|
||||
{ label: 'East Asia', value: 'eastasia', geography: 'Asia Pacific' },
|
||||
{ label: 'East US', value: 'eastus', geography: 'United States' },
|
||||
{ label: 'East US 2', value: 'eastus2', geography: 'United States' },
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
} from './CloudIntegration/AmazonWebServices/types';
|
||||
|
||||
export enum IntegrationType {
|
||||
AWS_SERVICES = 'aws-services',
|
||||
AZURE_SERVICES = 'azure-services',
|
||||
AWS_SERVICES = 'aws',
|
||||
AZURE_SERVICES = 'azure',
|
||||
}
|
||||
|
||||
interface LogField {
|
||||
@@ -89,6 +89,7 @@ export interface CloudAccount {
|
||||
cloud_account_id: string;
|
||||
config: AzureCloudAccountConfig | AWSCloudAccountConfig;
|
||||
status: AccountStatus | IServiceStatus;
|
||||
providerAccountId: string;
|
||||
}
|
||||
|
||||
export interface AzureCloudAccountConfig {
|
||||
|
||||
@@ -7,6 +7,13 @@ import {
|
||||
GetIntegrationStatusProps,
|
||||
} from 'types/api/integrations/types';
|
||||
|
||||
export function isOneClickIntegration(integrationId: string): boolean {
|
||||
return (
|
||||
integrationId === INTEGRATION_TYPES.AWS ||
|
||||
integrationId === INTEGRATION_TYPES.AZURE
|
||||
);
|
||||
}
|
||||
|
||||
export const useGetIntegrationStatus = ({
|
||||
integrationId,
|
||||
}: GetIntegrationPayloadProps): UseQueryResult<
|
||||
@@ -20,5 +27,5 @@ export const useGetIntegrationStatus = ({
|
||||
enabled:
|
||||
!!integrationId &&
|
||||
integrationId !== '' &&
|
||||
integrationId !== INTEGRATION_TYPES.AWS,
|
||||
!isOneClickIntegration(integrationId),
|
||||
});
|
||||
|
||||
@@ -20,11 +20,11 @@ import {
|
||||
CloudintegrationtypesCredentialsDTO,
|
||||
CloudintegrationtypesPostableAccountDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import {
|
||||
ActiveViewEnum,
|
||||
ModalStateEnum,
|
||||
} from 'container/Integrations/CloudIntegration/AmazonWebServices/HeroSection/types';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
} from 'container/Integrations/HeroSection/types';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
import { regions } from 'utils/regions';
|
||||
|
||||
|
||||
142
frontend/src/hooks/integration/azure/useAccountSettingsModal.ts
Normal file
142
frontend/src/hooks/integration/azure/useAccountSettingsModal.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { toast } from '@signozhq/ui';
|
||||
import { Form } from 'antd';
|
||||
import { FormInstance } from 'antd/lib';
|
||||
import { useUpdateAccount } from 'api/generated/services/cloudintegration';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { CloudAccount } from 'container/Integrations/types';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import logEvent from '../../../api/common/logEvent';
|
||||
|
||||
interface UseAccountSettingsModalProps {
|
||||
onClose: () => void;
|
||||
account: CloudAccount;
|
||||
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
|
||||
}
|
||||
|
||||
interface UseAccountSettingsModal {
|
||||
form: FormInstance;
|
||||
isLoading: boolean;
|
||||
resourceGroups: string[];
|
||||
isSaveDisabled: boolean;
|
||||
setResourceGroups: Dispatch<SetStateAction<string[]>>;
|
||||
handleSubmit: () => Promise<void>;
|
||||
handleClose: () => void;
|
||||
}
|
||||
|
||||
export function useAccountSettingsModal({
|
||||
onClose,
|
||||
account,
|
||||
setActiveAccount,
|
||||
}: UseAccountSettingsModalProps): UseAccountSettingsModal {
|
||||
const [form] = Form.useForm();
|
||||
const { mutate: updateAccount, isLoading } = useUpdateAccount();
|
||||
const accountConfig = useMemo(
|
||||
() => ('deployment_region' in account.config ? account.config : null),
|
||||
[account.config],
|
||||
);
|
||||
const [resourceGroups, setResourceGroups] = useState<string[]>(
|
||||
accountConfig?.resource_groups || [],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!accountConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.setFieldsValue({
|
||||
region: accountConfig.deployment_region,
|
||||
resourceGroups: accountConfig.resource_groups,
|
||||
});
|
||||
setResourceGroups(accountConfig.resource_groups);
|
||||
}, [accountConfig, form]);
|
||||
|
||||
const handleSubmit = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
updateAccount(
|
||||
{
|
||||
pathParams: {
|
||||
cloudProvider: INTEGRATION_TYPES.AZURE,
|
||||
id: account?.id || '',
|
||||
},
|
||||
data: {
|
||||
config: {
|
||||
azure: {
|
||||
resourceGroups: values.resourceGroups || [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
const nextConfig = {
|
||||
deployment_region: accountConfig?.deployment_region || '',
|
||||
resource_groups: values.resourceGroups || [],
|
||||
};
|
||||
|
||||
setActiveAccount({
|
||||
...account,
|
||||
config: nextConfig,
|
||||
});
|
||||
onClose();
|
||||
|
||||
toast.success('Account settings updated successfully', {
|
||||
position: 'bottom-right',
|
||||
});
|
||||
|
||||
logEvent('Azure Integration: Account settings updated', {
|
||||
cloudAccountId: account.cloud_account_id,
|
||||
deploymentRegion: nextConfig.deployment_region,
|
||||
resourceGroups: nextConfig.resource_groups,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error('Failed to update account settings', {
|
||||
description: error?.message,
|
||||
position: 'bottom-right',
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Form submission failed:', error);
|
||||
}
|
||||
}, [form, updateAccount, account, setActiveAccount, onClose]);
|
||||
|
||||
const isSaveDisabled = useMemo(() => {
|
||||
if (!accountConfig) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const formResourceGroups = resourceGroups || [];
|
||||
|
||||
return isEqual(
|
||||
[...formResourceGroups].sort(),
|
||||
[...accountConfig.resource_groups].sort(),
|
||||
);
|
||||
}, [accountConfig, resourceGroups, form]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
return {
|
||||
form,
|
||||
isLoading,
|
||||
resourceGroups,
|
||||
isSaveDisabled,
|
||||
setResourceGroups,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
};
|
||||
}
|
||||
188
frontend/src/hooks/integration/azure/useIntegrationModal.ts
Normal file
188
frontend/src/hooks/integration/azure/useIntegrationModal.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { Dispatch, SetStateAction, useCallback, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { toast } from '@signozhq/ui';
|
||||
import { Form, FormInstance } from 'antd';
|
||||
import {
|
||||
CreateAccountMutationResult,
|
||||
GetConnectionCredentialsQueryResult,
|
||||
invalidateListAccounts,
|
||||
useCreateAccount,
|
||||
useGetConnectionCredentials,
|
||||
} from 'api/generated/services/cloudintegration';
|
||||
import {
|
||||
CloudintegrationtypesCredentialsDTO,
|
||||
CloudintegrationtypesPostableAccountDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { ModalStateEnum } from 'container/Integrations/HeroSection/types';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
|
||||
import logEvent from '../../../api/common/logEvent';
|
||||
|
||||
interface UseIntegrationModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface UseAzureIntegrationModal {
|
||||
form: FormInstance;
|
||||
modalState: ModalStateEnum;
|
||||
isLoading: boolean;
|
||||
accountId?: string;
|
||||
connectionCommands: {
|
||||
cliCommand: string;
|
||||
cloudPowerShellCommand: string;
|
||||
} | null;
|
||||
setModalState: Dispatch<SetStateAction<ModalStateEnum>>;
|
||||
handleSubmit: () => Promise<void>;
|
||||
handleClose: () => void;
|
||||
connectionParams?: CloudintegrationtypesCredentialsDTO;
|
||||
isConnectionParamsLoading: boolean;
|
||||
handleConnectionSuccess: (payload: {
|
||||
cloudAccountId: string;
|
||||
status?: unknown;
|
||||
}) => void;
|
||||
handleConnectionTimeout: (payload: { id?: string }) => void;
|
||||
handleConnectionError: () => void;
|
||||
}
|
||||
|
||||
export function useIntegrationModal({
|
||||
onClose,
|
||||
}: UseIntegrationModalProps): UseAzureIntegrationModal {
|
||||
const queryClient = useQueryClient();
|
||||
const [form] = Form.useForm();
|
||||
const [modalState, setModalState] = useState<ModalStateEnum>(
|
||||
ModalStateEnum.FORM,
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [accountId, setAccountId] = useState<string | undefined>(undefined);
|
||||
const [connectionCommands, setConnectionCommands] = useState<{
|
||||
cliCommand: string;
|
||||
cloudPowerShellCommand: string;
|
||||
} | null>(null);
|
||||
|
||||
const handleClose = useCallback((): void => {
|
||||
setModalState(ModalStateEnum.FORM);
|
||||
setConnectionCommands(null);
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
const handleConnectionSuccess = useCallback(
|
||||
(payload: { cloudAccountId: string; status?: unknown }): void => {
|
||||
logEvent('Azure Integration: Account connected', {
|
||||
cloudAccountId: payload.cloudAccountId,
|
||||
status: payload.status,
|
||||
});
|
||||
toast.success('Azure account connected successfully', {
|
||||
position: 'bottom-right',
|
||||
});
|
||||
void invalidateListAccounts(queryClient, {
|
||||
cloudProvider: INTEGRATION_TYPES.AZURE,
|
||||
});
|
||||
handleClose();
|
||||
},
|
||||
[handleClose, queryClient],
|
||||
);
|
||||
|
||||
const handleConnectionTimeout = useCallback(
|
||||
(payload: { id?: string }): void => {
|
||||
setModalState(ModalStateEnum.ERROR);
|
||||
logEvent('Azure Integration: Account connection attempt timed out', {
|
||||
id: payload.id,
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleConnectionError = useCallback((): void => {
|
||||
setModalState(ModalStateEnum.ERROR);
|
||||
}, []);
|
||||
|
||||
const { mutate: createAccount } = useCreateAccount();
|
||||
const handleError = useAxiosError();
|
||||
|
||||
const { data: connectionParams, isLoading: isConnectionParamsLoading } =
|
||||
useGetConnectionCredentials<GetConnectionCredentialsQueryResult>(
|
||||
{
|
||||
cloudProvider: INTEGRATION_TYPES.AZURE,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
onError: handleError,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const values = await form.validateFields();
|
||||
|
||||
const payload: CloudintegrationtypesPostableAccountDTO = {
|
||||
config: {
|
||||
azure: {
|
||||
deploymentRegion: values.region,
|
||||
resourceGroups: values.resourceGroups || [],
|
||||
},
|
||||
},
|
||||
credentials: {
|
||||
ingestionUrl: connectionParams?.data?.ingestionUrl || values.ingestionUrl,
|
||||
ingestionKey: connectionParams?.data?.ingestionKey || values.ingestionKey,
|
||||
sigNozApiUrl: connectionParams?.data?.sigNozApiUrl || values.sigNozApiUrl,
|
||||
sigNozApiKey: connectionParams?.data?.sigNozApiKey || values.sigNozApiKey,
|
||||
},
|
||||
};
|
||||
|
||||
createAccount(
|
||||
{
|
||||
pathParams: { cloudProvider: INTEGRATION_TYPES.AZURE },
|
||||
data: payload,
|
||||
},
|
||||
{
|
||||
onSuccess: (response: CreateAccountMutationResult) => {
|
||||
const nextAccountId = response.data.id;
|
||||
const artifact = response.data.connectionArtifact.azure;
|
||||
|
||||
logEvent('Azure Integration: Account connection commands generated', {
|
||||
id: nextAccountId,
|
||||
});
|
||||
|
||||
setConnectionCommands({
|
||||
cliCommand: artifact?.cliCommand || '',
|
||||
cloudPowerShellCommand: artifact?.cloudPowerShellCommand || '',
|
||||
});
|
||||
setModalState(ModalStateEnum.WAITING);
|
||||
setAccountId(nextAccountId);
|
||||
},
|
||||
onError: () => {
|
||||
setModalState(ModalStateEnum.ERROR);
|
||||
toast.error('Failed to create account connection', {
|
||||
position: 'bottom-right',
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Form submission failed:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [form, connectionParams, createAccount]);
|
||||
|
||||
return {
|
||||
form,
|
||||
modalState,
|
||||
isLoading,
|
||||
accountId,
|
||||
connectionCommands,
|
||||
setModalState,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
connectionParams: connectionParams?.data as
|
||||
| CloudintegrationtypesCredentialsDTO
|
||||
| undefined,
|
||||
isConnectionParamsLoading,
|
||||
handleConnectionSuccess,
|
||||
handleConnectionTimeout,
|
||||
handleConnectionError,
|
||||
};
|
||||
}
|
||||
@@ -48,5 +48,23 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/infra_monitoring/onboarding", handler.New(
|
||||
provider.authZ.ViewAccess(provider.infraMonitoringHandler.GetOnboarding),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetOnboarding",
|
||||
Tags: []string{"inframonitoring"},
|
||||
Summary: "Get Onboarding Status for Infra Monitoring",
|
||||
Description: "Returns the per-tab readiness of the infra-monitoring section selected by the 'type' query parameter (hosts, processes, pods, nodes, deployments, daemonsets, statefulsets, jobs, namespaces, clusters, volumes). For each collector receiver or processor that contributes required metrics or attributes, lists what is present and what is missing, with a prebuilt user-facing message and a docs link per missing component. Default-enabled metrics are those expected as soon as the receiver is configured; optional metrics require 'enabled: true' in receiver config. 'ready' is true only when every missing list is empty.",
|
||||
RequestQuery: new(inframonitoringtypes.PostableOnboarding),
|
||||
Response: new(inframonitoringtypes.Onboarding),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func newConfig() factory.Config {
|
||||
Agent: AgentConfig{
|
||||
// we will maintain the latest version of cloud integration agent from here,
|
||||
// till we automate it externally or figure out a way to validate it.
|
||||
Version: "v0.0.9",
|
||||
Version: "v0.0.10",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,30 @@ func NewHandler(m inframonitoring.Module) inframonitoring.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) GetOnboarding(rw http.ResponseWriter, req *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(req.Context())
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
var parsedReq inframonitoringtypes.PostableOnboarding
|
||||
if err := binding.Query.BindQuery(req.URL.Query(), &parsedReq); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.module.GetOnboarding(req.Context(), orgID, &parsedReq)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *handler) ListHosts(rw http.ResponseWriter, req *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(req.Context())
|
||||
if err != nil {
|
||||
|
||||
@@ -434,6 +434,57 @@ func (m *module) getMetricsExistenceAndEarliestTime(ctx context.Context, metricN
|
||||
return missingMetrics, globalMinFirstReported, nil
|
||||
}
|
||||
|
||||
// getAttributesExistence returns the subset of attrNames that are missing —
|
||||
// i.e. have never been reported as a label on any of the given metricNames.
|
||||
// Presence is checked against distributed_metadata without a time-range filter.
|
||||
func (m *module) getAttributesExistence(ctx context.Context, metricNames, attrNames []string) ([]string, error) {
|
||||
if len(attrNames) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if len(metricNames) == 0 {
|
||||
return nil, errors.NewInternalf(errors.CodeInternal, "getAttributesExistence: metricNames must not be empty")
|
||||
}
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select("attr_name", "count(*) AS cnt")
|
||||
sb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, telemetrymetrics.AttributesMetadataTableName))
|
||||
sb.Where(
|
||||
sb.In("metric_name", sqlbuilder.List(metricNames)),
|
||||
sb.In("attr_name", sqlbuilder.List(attrNames)),
|
||||
)
|
||||
sb.GroupBy("attr_name")
|
||||
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
present := make(map[string]bool, len(attrNames))
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var cnt uint64
|
||||
if err := rows.Scan(&name, &cnt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if name != "" && cnt > 0 {
|
||||
present[name] = true
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var missing []string
|
||||
for _, a := range attrNames {
|
||||
if !present[a] {
|
||||
missing = append(missing, a)
|
||||
}
|
||||
}
|
||||
return missing, nil
|
||||
}
|
||||
|
||||
// getMetadata fetches the latest values of additionalCols for each unique combination of groupBy keys,
|
||||
// within the given time range and metric names. It uses argMax(tuple(...), unix_milli) to ensure
|
||||
// we always pick attribute values from the latest timestamp for each group.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package implinframonitoring
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
|
||||
// The types in this file are only used within the implinframonitoring package, and are not exposed outside.
|
||||
// They are primarily used for internal processing and structuring of data within the module's implementation.
|
||||
|
||||
@@ -23,3 +25,50 @@ type podPhaseCounts struct {
|
||||
Failed int
|
||||
Unknown int
|
||||
}
|
||||
|
||||
// bucketSplit carries the up-to-six entries a single spec bucket contributes
|
||||
// to an onboarding response. Any field may be nil if the bucket doesn't
|
||||
// populate that dimension.
|
||||
type bucketSplit struct {
|
||||
PresentDefault *inframonitoringtypes.MetricsComponentEntry
|
||||
PresentOptional *inframonitoringtypes.MetricsComponentEntry
|
||||
PresentAttrs *inframonitoringtypes.AttributesComponentEntry
|
||||
MissingDefault *inframonitoringtypes.MissingMetricsComponentEntry
|
||||
MissingOptional *inframonitoringtypes.MissingMetricsComponentEntry
|
||||
MissingAttrs *inframonitoringtypes.MissingAttributesComponentEntry
|
||||
}
|
||||
|
||||
// onboardingComponentBucket is a single collector component's contribution
|
||||
// toward a single infra-monitoring tab's readiness. Any of the three dimension
|
||||
// slices (DefaultMetrics, OptionalMetrics, RequiredAttrs) may be empty — the
|
||||
// bucketizer in Phase 4 skips empty dimensions.
|
||||
type onboardingComponentBucket struct {
|
||||
Component inframonitoringtypes.AssociatedComponent
|
||||
DefaultMetrics []string
|
||||
OptionalMetrics []string
|
||||
RequiredAttrs []string
|
||||
DocumentationLink string
|
||||
}
|
||||
|
||||
// onboardingSpec defines, for one OnboardingType, the full set of
|
||||
// component-scoped buckets that must be satisfied for the tab to be ready.
|
||||
type onboardingSpec struct {
|
||||
Buckets []onboardingComponentBucket
|
||||
}
|
||||
|
||||
func (s onboardingSpec) getAllMetrics() []string {
|
||||
var out []string
|
||||
for _, b := range s.Buckets {
|
||||
out = append(out, b.DefaultMetrics...)
|
||||
out = append(out, b.OptionalMetrics...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s onboardingSpec) getAllAttrs() []string {
|
||||
var out []string
|
||||
for _, b := range s.Buckets {
|
||||
out = append(out, b.RequiredAttrs...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -47,6 +47,80 @@ func NewModule(
|
||||
}
|
||||
}
|
||||
|
||||
// GetOnboarding runs a per-type readiness check: for the requested
|
||||
// infra-monitoring tab, reports which required metrics and attributes are
|
||||
// present vs missing, grouped by the collector component that produces them.
|
||||
// Ready is true iff every missing list is empty.
|
||||
func (m *module) GetOnboarding(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableOnboarding) (*inframonitoringtypes.Onboarding, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
spec, err := getSpecForType(req.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allMetrics := spec.getAllMetrics()
|
||||
allAttrs := spec.getAllAttrs()
|
||||
|
||||
missingMetricsList, _, err := m.getMetricsExistenceAndEarliestTime(ctx, allMetrics)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
missingMetricsMap := make(map[string]bool, len(missingMetricsList))
|
||||
for _, name := range missingMetricsList {
|
||||
missingMetricsMap[name] = true
|
||||
}
|
||||
|
||||
missingAttrsList, err := m.getAttributesExistence(ctx, allMetrics, allAttrs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
missingAttrsMap := make(map[string]bool, len(missingAttrsList))
|
||||
for _, name := range missingAttrsList {
|
||||
missingAttrsMap[name] = true
|
||||
}
|
||||
|
||||
resp := &inframonitoringtypes.Onboarding{
|
||||
Type: req.Type,
|
||||
PresentDefaultEnabledMetrics: []inframonitoringtypes.MetricsComponentEntry{},
|
||||
PresentOptionalMetrics: []inframonitoringtypes.MetricsComponentEntry{},
|
||||
PresentRequiredAttributes: []inframonitoringtypes.AttributesComponentEntry{},
|
||||
MissingDefaultEnabledMetrics: []inframonitoringtypes.MissingMetricsComponentEntry{},
|
||||
MissingOptionalMetrics: []inframonitoringtypes.MissingMetricsComponentEntry{},
|
||||
MissingRequiredAttributes: []inframonitoringtypes.MissingAttributesComponentEntry{},
|
||||
}
|
||||
|
||||
for _, b := range spec.Buckets {
|
||||
s := splitBucket(b, missingMetricsMap, missingAttrsMap)
|
||||
if s.PresentDefault != nil {
|
||||
resp.PresentDefaultEnabledMetrics = append(resp.PresentDefaultEnabledMetrics, *s.PresentDefault)
|
||||
}
|
||||
if s.PresentOptional != nil {
|
||||
resp.PresentOptionalMetrics = append(resp.PresentOptionalMetrics, *s.PresentOptional)
|
||||
}
|
||||
if s.PresentAttrs != nil {
|
||||
resp.PresentRequiredAttributes = append(resp.PresentRequiredAttributes, *s.PresentAttrs)
|
||||
}
|
||||
if s.MissingDefault != nil {
|
||||
resp.MissingDefaultEnabledMetrics = append(resp.MissingDefaultEnabledMetrics, *s.MissingDefault)
|
||||
}
|
||||
if s.MissingOptional != nil {
|
||||
resp.MissingOptionalMetrics = append(resp.MissingOptionalMetrics, *s.MissingOptional)
|
||||
}
|
||||
if s.MissingAttrs != nil {
|
||||
resp.MissingRequiredAttributes = append(resp.MissingRequiredAttributes, *s.MissingAttrs)
|
||||
}
|
||||
}
|
||||
|
||||
resp.Ready = len(resp.MissingDefaultEnabledMetrics) == 0 &&
|
||||
len(resp.MissingOptionalMetrics) == 0 &&
|
||||
len(resp.MissingRequiredAttributes) == 0
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (m *module) ListHosts(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableHosts) (*inframonitoringtypes.Hosts, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, err
|
||||
|
||||
114
pkg/modules/inframonitoring/implinframonitoring/onboarding.go
Normal file
114
pkg/modules/inframonitoring/implinframonitoring/onboarding.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
)
|
||||
|
||||
// splitBucket partitions one component bucket's metric and attribute lists
|
||||
// against the module-wide missing sets into up to six response entries.
|
||||
// Empty partitions are left nil so callers can skip them.
|
||||
func splitBucket(b onboardingComponentBucket, missingMetrics, missingAttrs map[string]bool) bucketSplit {
|
||||
var s bucketSplit
|
||||
presentDef, missDef := partitionList(b.DefaultMetrics, missingMetrics)
|
||||
if len(presentDef) > 0 {
|
||||
s.PresentDefault = &inframonitoringtypes.MetricsComponentEntry{
|
||||
Metrics: presentDef,
|
||||
AssociatedComponent: b.Component,
|
||||
}
|
||||
}
|
||||
if len(missDef) > 0 {
|
||||
s.MissingDefault = &inframonitoringtypes.MissingMetricsComponentEntry{
|
||||
MetricsComponentEntry: inframonitoringtypes.MetricsComponentEntry{
|
||||
Metrics: missDef,
|
||||
AssociatedComponent: b.Component,
|
||||
},
|
||||
Message: buildMissingDefaultMetricsMessage(missDef, b.Component.Name),
|
||||
DocumentationLink: b.DocumentationLink,
|
||||
}
|
||||
}
|
||||
|
||||
presentOpt, missOpt := partitionList(b.OptionalMetrics, missingMetrics)
|
||||
if len(presentOpt) > 0 {
|
||||
s.PresentOptional = &inframonitoringtypes.MetricsComponentEntry{
|
||||
Metrics: presentOpt,
|
||||
AssociatedComponent: b.Component,
|
||||
}
|
||||
}
|
||||
if len(missOpt) > 0 {
|
||||
s.MissingOptional = &inframonitoringtypes.MissingMetricsComponentEntry{
|
||||
MetricsComponentEntry: inframonitoringtypes.MetricsComponentEntry{
|
||||
Metrics: missOpt,
|
||||
AssociatedComponent: b.Component,
|
||||
},
|
||||
Message: buildMissingOptionalMetricsMessage(missOpt, b.Component.Name),
|
||||
DocumentationLink: b.DocumentationLink,
|
||||
}
|
||||
}
|
||||
|
||||
presentA, missA := partitionList(b.RequiredAttrs, missingAttrs)
|
||||
if len(presentA) > 0 {
|
||||
s.PresentAttrs = &inframonitoringtypes.AttributesComponentEntry{
|
||||
Attributes: presentA,
|
||||
AssociatedComponent: b.Component,
|
||||
}
|
||||
}
|
||||
if len(missA) > 0 {
|
||||
s.MissingAttrs = &inframonitoringtypes.MissingAttributesComponentEntry{
|
||||
AttributesComponentEntry: inframonitoringtypes.AttributesComponentEntry{
|
||||
Attributes: missA,
|
||||
AssociatedComponent: b.Component,
|
||||
},
|
||||
Message: buildMissingRequiredAttrsMessage(missA, b.Component.Name),
|
||||
DocumentationLink: b.DocumentationLink,
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// getSpecForType returns the onboardingSpec for a given OnboardingType, or an error if the type is invalid.
|
||||
func getSpecForType(t inframonitoringtypes.OnboardingType) (*onboardingSpec, error) {
|
||||
spec, ok := onboardingSpecs[t]
|
||||
if !ok {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "no onboarding spec for type: %s", t)
|
||||
}
|
||||
return &spec, nil
|
||||
}
|
||||
|
||||
// partitionList splits items into those NOT in `missing` and those in `missing`.
|
||||
// Preserves input order.
|
||||
func partitionList(items []string, missing map[string]bool) (present, miss []string) {
|
||||
for _, x := range items {
|
||||
if missing[x] {
|
||||
miss = append(miss, x)
|
||||
} else {
|
||||
present = append(present, x)
|
||||
}
|
||||
}
|
||||
return present, miss
|
||||
}
|
||||
|
||||
func buildMissingDefaultMetricsMessage(metrics []string, componentName string) string {
|
||||
return fmt.Sprintf(
|
||||
"Missing default metrics %s from %s. Learn how to configure here.",
|
||||
strings.Join(metrics, ", "), componentName,
|
||||
)
|
||||
}
|
||||
|
||||
func buildMissingOptionalMetricsMessage(metrics []string, componentName string) string {
|
||||
return fmt.Sprintf(
|
||||
"Missing optional metrics %s from %s. Learn how to enable here.",
|
||||
strings.Join(metrics, ", "), componentName,
|
||||
)
|
||||
}
|
||||
|
||||
func buildMissingRequiredAttrsMessage(attrs []string, componentName string) string {
|
||||
return fmt.Sprintf(
|
||||
"Missing required attributes %s from %s. Learn how to configure here.",
|
||||
strings.Join(attrs, ", "), componentName,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
package implinframonitoring
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
|
||||
// Component names — the 5 OTel collector receivers/processors that produce
|
||||
// metrics and resource attributes consumed by infra-monitoring tabs. Bare
|
||||
// strings on purpose (not wrapped enums) — the list is open-ended enough that
|
||||
// an enum adds more friction than value.
|
||||
const (
|
||||
componentNameHostMetricsReceiver = "hostmetricsreceiver"
|
||||
componentNameKubeletStatsReceiver = "kubeletstatsreceiver"
|
||||
componentNameK8sClusterReceiver = "k8sclusterreceiver"
|
||||
componentNameResourceDetectionProcessor = "resourcedetectionprocessor"
|
||||
componentNameK8sAttributesProcessor = "k8sattributesprocessor"
|
||||
)
|
||||
|
||||
// Documentation links — one per component. User-facing; emitted on missing-entries.
|
||||
const (
|
||||
docLinkHostMetricsReceiver = "https://signoz.io/docs/infrastructure-monitoring/hostmetrics/#step-2-configure-the-collector"
|
||||
docLinkKubeletStatsReceiver = "https://signoz.io/docs/infrastructure-monitoring/k8s-metrics/#setting-up-kubelet-stats-monitoring"
|
||||
docLinkK8sClusterReceiver = "https://signoz.io/docs/infrastructure-monitoring/k8s-metrics/#setting-up-k8s-cluster-monitoring"
|
||||
docLinkResourceDetectionProcessor = "https://signoz.io/docs/infrastructure-monitoring/hostmetrics/#host-name-is-blankempty"
|
||||
docLinkK8sAttributesProcessor = "https://signoz.io/docs/infrastructure-monitoring/k8s-metrics/#2-enable-kubernetes-metadata"
|
||||
)
|
||||
|
||||
var (
|
||||
componentHostMetricsReceiver = inframonitoringtypes.AssociatedComponent{
|
||||
Type: inframonitoringtypes.OnboardingComponentTypeReceiver,
|
||||
Name: componentNameHostMetricsReceiver,
|
||||
}
|
||||
componentKubeletStatsReceiver = inframonitoringtypes.AssociatedComponent{
|
||||
Type: inframonitoringtypes.OnboardingComponentTypeReceiver,
|
||||
Name: componentNameKubeletStatsReceiver,
|
||||
}
|
||||
componentK8sClusterReceiver = inframonitoringtypes.AssociatedComponent{
|
||||
Type: inframonitoringtypes.OnboardingComponentTypeReceiver,
|
||||
Name: componentNameK8sClusterReceiver,
|
||||
}
|
||||
componentResourceDetectionProcessor = inframonitoringtypes.AssociatedComponent{
|
||||
Type: inframonitoringtypes.OnboardingComponentTypeProcessor,
|
||||
Name: componentNameResourceDetectionProcessor,
|
||||
}
|
||||
componentK8sAttributesProcessor = inframonitoringtypes.AssociatedComponent{
|
||||
Type: inframonitoringtypes.OnboardingComponentTypeProcessor,
|
||||
Name: componentNameK8sAttributesProcessor,
|
||||
}
|
||||
)
|
||||
|
||||
// onboardingSpecs is the single lookup table the module consults for a type's
|
||||
// readiness contract. Every OnboardingType value must have an entry here.
|
||||
var onboardingSpecs = map[inframonitoringtypes.OnboardingType]onboardingSpec{
|
||||
inframonitoringtypes.OnboardingTypeHosts: hostsSpec,
|
||||
inframonitoringtypes.OnboardingTypeProcesses: processesSpec,
|
||||
inframonitoringtypes.OnboardingTypePods: podsSpec,
|
||||
inframonitoringtypes.OnboardingTypeNodes: nodesSpec,
|
||||
inframonitoringtypes.OnboardingTypeDeployments: deploymentsSpec,
|
||||
inframonitoringtypes.OnboardingTypeDaemonsets: daemonsetsSpec,
|
||||
inframonitoringtypes.OnboardingTypeStatefulsets: statefulsetsSpec,
|
||||
inframonitoringtypes.OnboardingTypeJobs: jobsSpec,
|
||||
inframonitoringtypes.OnboardingTypeNamespaces: namespacesSpec,
|
||||
inframonitoringtypes.OnboardingTypeClusters: clustersSpec,
|
||||
inframonitoringtypes.OnboardingTypeVolumes: volumesSpec,
|
||||
}
|
||||
|
||||
// Per-type specs. Every metric and attribute is spelled out in its own spec
|
||||
// on purpose — no shared slices, no concatenation helpers. Repetition is
|
||||
// cheaper than indirection when auditing what each tab actually requires.
|
||||
|
||||
var hostsSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentHostMetricsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"system.cpu.time",
|
||||
"system.memory.usage",
|
||||
"system.cpu.load_average.15m",
|
||||
"system.filesystem.usage",
|
||||
},
|
||||
DocumentationLink: docLinkHostMetricsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentResourceDetectionProcessor,
|
||||
RequiredAttrs: []string{"host.name"},
|
||||
DocumentationLink: docLinkResourceDetectionProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var processesSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentHostMetricsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"process.cpu.time",
|
||||
"process.memory.usage",
|
||||
},
|
||||
RequiredAttrs: []string{"process.pid"},
|
||||
DocumentationLink: docLinkHostMetricsReceiver,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var podsSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.memory.working_set",
|
||||
},
|
||||
OptionalMetrics: []string{
|
||||
"k8s.pod.cpu_request_utilization",
|
||||
"k8s.pod.cpu_limit_utilization",
|
||||
"k8s.pod.memory_request_utilization",
|
||||
"k8s.pod.memory_limit_utilization",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{"k8s.pod.phase"},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.pod.uid"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var nodesSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.node.cpu.usage",
|
||||
"k8s.node.memory.working_set",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.node.allocatable_cpu",
|
||||
"k8s.node.allocatable_memory", // k8s.node.allocatable_cpu and k8s.node.allocatable_memory are
|
||||
// controlled by allocatable_types_to_report config option (Check // https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/4f9a578b210a6dcb9f9bf47942f27208b5765298/receiver/k8sclusterreceiver/metadata.yaml#L805-L806)
|
||||
"k8s.node.condition_ready", // # k8s.node.condition_* metrics (k8s.node.condition_ready, k8s.node.condition_memory_pressure, etc) are controlled# by node_conditions_to_report config option.
|
||||
// By default, only k8s.node.condition_ready is enabled. (Check https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/4f9a578b210a6dcb9f9bf47942f27208b5765298/receiver/k8sclusterreceiver/metadata.yaml#L802)
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.node.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var deploymentsSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.memory.working_set",
|
||||
},
|
||||
OptionalMetrics: []string{
|
||||
"k8s.pod.cpu_request_utilization",
|
||||
"k8s.pod.cpu_limit_utilization",
|
||||
"k8s.pod.memory_request_utilization",
|
||||
"k8s.pod.memory_limit_utilization",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.container.restarts",
|
||||
"k8s.deployment.desired",
|
||||
"k8s.deployment.available",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.deployment.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var daemonsetsSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.memory.working_set",
|
||||
},
|
||||
OptionalMetrics: []string{
|
||||
"k8s.pod.cpu_request_utilization",
|
||||
"k8s.pod.cpu_limit_utilization",
|
||||
"k8s.pod.memory_request_utilization",
|
||||
"k8s.pod.memory_limit_utilization",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.container.restarts",
|
||||
"k8s.daemonset.desired_scheduled_nodes",
|
||||
"k8s.daemonset.current_scheduled_nodes",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.daemonset.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var statefulsetsSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.memory.working_set",
|
||||
},
|
||||
OptionalMetrics: []string{
|
||||
"k8s.pod.cpu_request_utilization",
|
||||
"k8s.pod.cpu_limit_utilization",
|
||||
"k8s.pod.memory_request_utilization",
|
||||
"k8s.pod.memory_limit_utilization",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.container.restarts",
|
||||
"k8s.statefulset.desired_pods",
|
||||
"k8s.statefulset.current_pods",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.statefulset.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var jobsSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.memory.working_set",
|
||||
},
|
||||
OptionalMetrics: []string{
|
||||
"k8s.pod.cpu_request_utilization",
|
||||
"k8s.pod.cpu_limit_utilization",
|
||||
"k8s.pod.memory_request_utilization",
|
||||
"k8s.pod.memory_limit_utilization",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.container.restarts",
|
||||
"k8s.job.desired_successful_pods",
|
||||
"k8s.job.active_pods",
|
||||
"k8s.job.failed_pods",
|
||||
"k8s.job.successful_pods",
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.job.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var namespacesSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.pod.cpu.usage",
|
||||
"k8s.pod.memory.working_set",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{"k8s.pod.phase"},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.namespace.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var clustersSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.node.cpu.usage",
|
||||
"k8s.node.memory.working_set",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sClusterReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.node.allocatable_cpu",
|
||||
"k8s.node.allocatable_memory", //k8s.node.allocatable_cpu and k8s.node.allocatable_memory are
|
||||
// controlled by allocatable_types_to_report config option (Check // https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/4f9a578b210a6dcb9f9bf47942f27208b5765298/receiver/k8sclusterreceiver/metadata.yaml#L805-L806)
|
||||
},
|
||||
DocumentationLink: docLinkK8sClusterReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentResourceDetectionProcessor,
|
||||
RequiredAttrs: []string{"k8s.cluster.name"},
|
||||
DocumentationLink: docLinkResourceDetectionProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var volumesSpec = onboardingSpec{
|
||||
Buckets: []onboardingComponentBucket{
|
||||
{
|
||||
Component: componentKubeletStatsReceiver,
|
||||
DefaultMetrics: []string{
|
||||
"k8s.volume.available",
|
||||
"k8s.volume.capacity",
|
||||
"k8s.volume.inodes",
|
||||
"k8s.volume.inodes.free",
|
||||
"k8s.volume.inodes.used",
|
||||
},
|
||||
DocumentationLink: docLinkKubeletStatsReceiver,
|
||||
},
|
||||
{
|
||||
Component: componentK8sAttributesProcessor,
|
||||
RequiredAttrs: []string{"k8s.persistentvolumeclaim.name"},
|
||||
DocumentationLink: docLinkK8sAttributesProcessor,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package implinframonitoring
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Component used across splitBucket cases — it's a processor so the test
|
||||
// doesn't carry any receiver semantics.
|
||||
var testComponent = inframonitoringtypes.AssociatedComponent{
|
||||
Type: inframonitoringtypes.OnboardingComponentTypeReceiver,
|
||||
Name: "testreceiver",
|
||||
}
|
||||
|
||||
const testDocLink = "https://example.com/docs"
|
||||
|
||||
func TestSplitBucket(t *testing.T) {
|
||||
type want struct {
|
||||
presentDefault []string
|
||||
presentOptional []string
|
||||
presentAttrs []string
|
||||
missingDefault []string
|
||||
missingOptional []string
|
||||
missingAttrs []string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
bucket onboardingComponentBucket
|
||||
missingMetrics map[string]bool
|
||||
missingAttrs map[string]bool
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "empty bucket — nothing to emit",
|
||||
bucket: onboardingComponentBucket{Component: testComponent, DocumentationLink: testDocLink},
|
||||
missingMetrics: map[string]bool{},
|
||||
missingAttrs: map[string]bool{},
|
||||
want: want{},
|
||||
},
|
||||
{
|
||||
name: "all default metrics present",
|
||||
bucket: onboardingComponentBucket{
|
||||
Component: testComponent,
|
||||
DefaultMetrics: []string{"m1", "m2"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{},
|
||||
missingAttrs: map[string]bool{},
|
||||
want: want{
|
||||
presentDefault: []string{"m1", "m2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all default metrics missing",
|
||||
bucket: onboardingComponentBucket{
|
||||
Component: testComponent,
|
||||
DefaultMetrics: []string{"m1", "m2"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{"m1": true, "m2": true},
|
||||
missingAttrs: map[string]bool{},
|
||||
want: want{
|
||||
missingDefault: []string{"m1", "m2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mixed default metrics",
|
||||
bucket: onboardingComponentBucket{
|
||||
Component: testComponent,
|
||||
DefaultMetrics: []string{"m1", "m2", "m3"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{"m2": true},
|
||||
missingAttrs: map[string]bool{},
|
||||
want: want{
|
||||
presentDefault: []string{"m1", "m3"},
|
||||
missingDefault: []string{"m2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only optional metrics — all missing",
|
||||
bucket: onboardingComponentBucket{
|
||||
Component: testComponent,
|
||||
OptionalMetrics: []string{"opt1", "opt2"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{"opt1": true, "opt2": true},
|
||||
missingAttrs: map[string]bool{},
|
||||
want: want{
|
||||
missingOptional: []string{"opt1", "opt2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only required attrs — all present",
|
||||
bucket: onboardingComponentBucket{
|
||||
Component: testComponent,
|
||||
RequiredAttrs: []string{"a1", "a2"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{},
|
||||
missingAttrs: map[string]bool{},
|
||||
want: want{
|
||||
presentAttrs: []string{"a1", "a2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only required attrs — all missing",
|
||||
bucket: onboardingComponentBucket{
|
||||
Component: testComponent,
|
||||
RequiredAttrs: []string{"a1"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{},
|
||||
missingAttrs: map[string]bool{"a1": true},
|
||||
want: want{
|
||||
missingAttrs: []string{"a1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "every dimension populated on both sides",
|
||||
bucket: onboardingComponentBucket{
|
||||
Component: testComponent,
|
||||
DefaultMetrics: []string{"d1", "d2"},
|
||||
OptionalMetrics: []string{"o1", "o2"},
|
||||
RequiredAttrs: []string{"a1", "a2"},
|
||||
DocumentationLink: testDocLink,
|
||||
},
|
||||
missingMetrics: map[string]bool{"d2": true, "o1": true},
|
||||
missingAttrs: map[string]bool{"a2": true},
|
||||
want: want{
|
||||
presentDefault: []string{"d1"},
|
||||
missingDefault: []string{"d2"},
|
||||
presentOptional: []string{"o2"},
|
||||
missingOptional: []string{"o1"},
|
||||
presentAttrs: []string{"a1"},
|
||||
missingAttrs: []string{"a2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := splitBucket(tt.bucket, tt.missingMetrics, tt.missingAttrs)
|
||||
|
||||
requireMetricsEntry(t, "presentDefault", got.PresentDefault, tt.want.presentDefault, false)
|
||||
requireMetricsEntry(t, "presentOptional", got.PresentOptional, tt.want.presentOptional, false)
|
||||
requireAttrsEntry(t, "presentAttrs", got.PresentAttrs, tt.want.presentAttrs, false)
|
||||
|
||||
requireMissingMetrics(t, "missingDefault", got.MissingDefault, tt.want.missingDefault)
|
||||
requireMissingMetrics(t, "missingOptional", got.MissingOptional, tt.want.missingOptional)
|
||||
requireMissingAttrs(t, "missingAttrs", got.MissingAttrs, tt.want.missingAttrs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPartitionList(t *testing.T) {
|
||||
present, missing := partitionList(
|
||||
[]string{"a", "b", "c", "d"},
|
||||
map[string]bool{"b": true, "d": true},
|
||||
)
|
||||
require.Equal(t, []string{"a", "c"}, present)
|
||||
require.Equal(t, []string{"b", "d"}, missing)
|
||||
}
|
||||
|
||||
func TestMissingMessageTemplates(t *testing.T) {
|
||||
require.Equal(t,
|
||||
"Missing default metrics m1, m2 from comp. Learn how to configure here.",
|
||||
buildMissingDefaultMetricsMessage([]string{"m1", "m2"}, "comp"),
|
||||
)
|
||||
require.Equal(t,
|
||||
"Missing optional metrics m1 from comp. Learn how to enable here.",
|
||||
buildMissingOptionalMetricsMessage([]string{"m1"}, "comp"),
|
||||
)
|
||||
require.Equal(t,
|
||||
"Missing required attributes a1 from comp. Learn how to configure here.",
|
||||
buildMissingRequiredAttrsMessage([]string{"a1"}, "comp"),
|
||||
)
|
||||
require.Equal(t,
|
||||
"Missing required attributes a1, a2 from comp. Learn how to configure here.",
|
||||
buildMissingRequiredAttrsMessage([]string{"a1", "a2"}, "comp"),
|
||||
)
|
||||
}
|
||||
|
||||
// TestOnboardingSpecs_CoverAllTypes ensures the spec map has an entry for
|
||||
// every OnboardingType — prevents silently shipping an onboarding type that
|
||||
// has no spec and would 500 at runtime.
|
||||
func TestOnboardingSpecs_CoverAllTypes(t *testing.T) {
|
||||
for _, tp := range inframonitoringtypes.ValidOnboardingTypes {
|
||||
_, ok := onboardingSpecs[tp]
|
||||
require.True(t, ok, "missing onboarding spec for type %s", tp)
|
||||
}
|
||||
require.Len(t, onboardingSpecs, len(inframonitoringtypes.ValidOnboardingTypes))
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
func requireMetricsEntry(t *testing.T, name string, got *inframonitoringtypes.MetricsComponentEntry, wantMetrics []string, _ bool) {
|
||||
t.Helper()
|
||||
if len(wantMetrics) == 0 {
|
||||
require.Nil(t, got, name)
|
||||
return
|
||||
}
|
||||
require.NotNil(t, got, name)
|
||||
require.Equal(t, wantMetrics, got.Metrics, name)
|
||||
require.Equal(t, testComponent, got.AssociatedComponent, name)
|
||||
}
|
||||
|
||||
func requireAttrsEntry(t *testing.T, name string, got *inframonitoringtypes.AttributesComponentEntry, wantAttrs []string, _ bool) {
|
||||
t.Helper()
|
||||
if len(wantAttrs) == 0 {
|
||||
require.Nil(t, got, name)
|
||||
return
|
||||
}
|
||||
require.NotNil(t, got, name)
|
||||
require.Equal(t, wantAttrs, got.Attributes, name)
|
||||
require.Equal(t, testComponent, got.AssociatedComponent, name)
|
||||
}
|
||||
|
||||
func requireMissingMetrics(t *testing.T, name string, got *inframonitoringtypes.MissingMetricsComponentEntry, wantMetrics []string) {
|
||||
t.Helper()
|
||||
if len(wantMetrics) == 0 {
|
||||
require.Nil(t, got, name)
|
||||
return
|
||||
}
|
||||
require.NotNil(t, got, name)
|
||||
require.Equal(t, wantMetrics, got.Metrics, name)
|
||||
require.Equal(t, testComponent, got.AssociatedComponent, name)
|
||||
require.NotEmpty(t, got.Message, name)
|
||||
require.Equal(t, testDocLink, got.DocumentationLink, name)
|
||||
}
|
||||
|
||||
func requireMissingAttrs(t *testing.T, name string, got *inframonitoringtypes.MissingAttributesComponentEntry, wantAttrs []string) {
|
||||
t.Helper()
|
||||
if len(wantAttrs) == 0 {
|
||||
require.Nil(t, got, name)
|
||||
return
|
||||
}
|
||||
require.NotNil(t, got, name)
|
||||
require.Equal(t, wantAttrs, got.Attributes, name)
|
||||
require.Equal(t, testComponent, got.AssociatedComponent, name)
|
||||
require.NotEmpty(t, got.Message, name)
|
||||
require.Equal(t, testDocLink, got.DocumentationLink, name)
|
||||
}
|
||||
@@ -11,9 +11,11 @@ import (
|
||||
type Handler interface {
|
||||
ListHosts(http.ResponseWriter, *http.Request)
|
||||
ListPods(http.ResponseWriter, *http.Request)
|
||||
GetOnboarding(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
type Module interface {
|
||||
ListHosts(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableHosts) (*inframonitoringtypes.Hosts, error)
|
||||
ListPods(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostablePods) (*inframonitoringtypes.Pods, error)
|
||||
GetOnboarding(ctx context.Context, orgID valuer.UUID, req *inframonitoringtypes.PostableOnboarding) (*inframonitoringtypes.Onboarding, error)
|
||||
}
|
||||
|
||||
83
pkg/types/inframonitoringtypes/onboarding.go
Normal file
83
pkg/types/inframonitoringtypes/onboarding.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
// PostableOnboarding is the request for GET /api/v2/infra_monitoring/onboarding.
|
||||
// The single `type` query param selects which infra-monitoring subsection the
|
||||
// readiness check runs for.
|
||||
type PostableOnboarding struct {
|
||||
Type OnboardingType `query:"type" required:"true"`
|
||||
}
|
||||
|
||||
// Validate rejects empty/unknown onboarding types.
|
||||
func (req *PostableOnboarding) Validate() error {
|
||||
if req == nil {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "request is nil")
|
||||
}
|
||||
|
||||
if req.Type.IsZero() {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "type is required")
|
||||
}
|
||||
|
||||
if !slices.Contains(ValidOnboardingTypes, req.Type) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid type: %s", req.Type)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Onboarding is the response for GET /api/v2/infra_monitoring/onboarding.
|
||||
//
|
||||
// The three present/missing pairs partition a type's requirements into three
|
||||
// dimensions — default-enabled metrics, optional metrics, required attributes —
|
||||
// each bucketed by the collector component (receiver or processor) that
|
||||
// produces it. Ready is true iff every Missing* array is empty.
|
||||
type Onboarding struct {
|
||||
Type OnboardingType `json:"type" required:"true"`
|
||||
Ready bool `json:"ready" required:"true"`
|
||||
PresentDefaultEnabledMetrics []MetricsComponentEntry `json:"presentDefaultEnabledMetrics" required:"true"`
|
||||
PresentOptionalMetrics []MetricsComponentEntry `json:"presentOptionalMetrics" required:"true"`
|
||||
PresentRequiredAttributes []AttributesComponentEntry `json:"presentRequiredAttributes" required:"true"`
|
||||
MissingDefaultEnabledMetrics []MissingMetricsComponentEntry `json:"missingDefaultEnabledMetrics" required:"true"`
|
||||
MissingOptionalMetrics []MissingMetricsComponentEntry `json:"missingOptionalMetrics" required:"true"`
|
||||
MissingRequiredAttributes []MissingAttributesComponentEntry `json:"missingRequiredAttributes" required:"true"`
|
||||
}
|
||||
|
||||
// AssociatedComponent identifies the collector receiver or processor that a
|
||||
// metric or attribute originates from. Name is free-form (e.g. "kubeletstatsreceiver").
|
||||
type AssociatedComponent struct {
|
||||
Type OnboardingComponentType `json:"type" required:"true"`
|
||||
Name string `json:"name" required:"true"`
|
||||
}
|
||||
|
||||
// MetricsComponentEntry lists metrics that share a single associated component.
|
||||
type MetricsComponentEntry struct {
|
||||
Metrics []string `json:"metrics" required:"true"`
|
||||
AssociatedComponent AssociatedComponent `json:"associatedComponent" required:"true"`
|
||||
}
|
||||
|
||||
// AttributesComponentEntry lists resource attributes that share a single associated component.
|
||||
type AttributesComponentEntry struct {
|
||||
Attributes []string `json:"attributes" required:"true"`
|
||||
AssociatedComponent AssociatedComponent `json:"associatedComponent" required:"true"`
|
||||
}
|
||||
|
||||
// MissingMetricsComponentEntry extends MetricsComponentEntry with a user-facing
|
||||
// message and a docs link for fixing the missing metrics.
|
||||
type MissingMetricsComponentEntry struct {
|
||||
MetricsComponentEntry
|
||||
Message string `json:"message" required:"true"`
|
||||
DocumentationLink string `json:"documentationLink" required:"true"`
|
||||
}
|
||||
|
||||
// MissingAttributesComponentEntry extends AttributesComponentEntry with a user-facing
|
||||
// message and a docs link for fixing the missing attributes.
|
||||
type MissingAttributesComponentEntry struct {
|
||||
AttributesComponentEntry
|
||||
Message string `json:"message" required:"true"`
|
||||
DocumentationLink string `json:"documentationLink" required:"true"`
|
||||
}
|
||||
71
pkg/types/inframonitoringtypes/onboarding_constants.go
Normal file
71
pkg/types/inframonitoringtypes/onboarding_constants.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
// OnboardingType identifies a single infra-monitoring subsection (UI tab).
|
||||
// One value per v1/v2 list API we surface in the infra-monitoring section.
|
||||
type OnboardingType struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
OnboardingTypeHosts = OnboardingType{valuer.NewString("hosts")}
|
||||
OnboardingTypeProcesses = OnboardingType{valuer.NewString("processes")}
|
||||
OnboardingTypePods = OnboardingType{valuer.NewString("pods")}
|
||||
OnboardingTypeNodes = OnboardingType{valuer.NewString("nodes")}
|
||||
OnboardingTypeDeployments = OnboardingType{valuer.NewString("deployments")}
|
||||
OnboardingTypeDaemonsets = OnboardingType{valuer.NewString("daemonsets")}
|
||||
OnboardingTypeStatefulsets = OnboardingType{valuer.NewString("statefulsets")}
|
||||
OnboardingTypeJobs = OnboardingType{valuer.NewString("jobs")}
|
||||
OnboardingTypeNamespaces = OnboardingType{valuer.NewString("namespaces")}
|
||||
OnboardingTypeClusters = OnboardingType{valuer.NewString("clusters")}
|
||||
OnboardingTypeVolumes = OnboardingType{valuer.NewString("volumes")}
|
||||
)
|
||||
|
||||
func (OnboardingType) Enum() []any {
|
||||
return []any{
|
||||
OnboardingTypeHosts,
|
||||
OnboardingTypeProcesses,
|
||||
OnboardingTypePods,
|
||||
OnboardingTypeNodes,
|
||||
OnboardingTypeDeployments,
|
||||
OnboardingTypeDaemonsets,
|
||||
OnboardingTypeStatefulsets,
|
||||
OnboardingTypeJobs,
|
||||
OnboardingTypeNamespaces,
|
||||
OnboardingTypeClusters,
|
||||
OnboardingTypeVolumes,
|
||||
}
|
||||
}
|
||||
|
||||
var ValidOnboardingTypes = []OnboardingType{
|
||||
OnboardingTypeHosts,
|
||||
OnboardingTypeProcesses,
|
||||
OnboardingTypePods,
|
||||
OnboardingTypeNodes,
|
||||
OnboardingTypeDeployments,
|
||||
OnboardingTypeDaemonsets,
|
||||
OnboardingTypeStatefulsets,
|
||||
OnboardingTypeJobs,
|
||||
OnboardingTypeNamespaces,
|
||||
OnboardingTypeClusters,
|
||||
OnboardingTypeVolumes,
|
||||
}
|
||||
|
||||
// OnboardingComponentType tags each AssociatedComponent as either a receiver or a processor.
|
||||
// Only these two values are ever written by the module.
|
||||
type OnboardingComponentType struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
OnboardingComponentTypeReceiver = OnboardingComponentType{valuer.NewString("receiver")}
|
||||
OnboardingComponentTypeProcessor = OnboardingComponentType{valuer.NewString("processor")}
|
||||
)
|
||||
|
||||
func (OnboardingComponentType) Enum() []any {
|
||||
return []any{
|
||||
OnboardingComponentTypeReceiver,
|
||||
OnboardingComponentTypeProcessor,
|
||||
}
|
||||
}
|
||||
110
pkg/types/inframonitoringtypes/onboarding_test.go
Normal file
110
pkg/types/inframonitoringtypes/onboarding_test.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPostableOnboarding_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req *PostableOnboarding
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
req: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty type",
|
||||
req: &PostableOnboarding{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unknown type",
|
||||
req: &PostableOnboarding{Type: OnboardingType{valuer.NewString("foo")}},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "hosts",
|
||||
req: &PostableOnboarding{Type: OnboardingTypeHosts},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "processes",
|
||||
req: &PostableOnboarding{Type: OnboardingTypeProcesses},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "pods",
|
||||
req: &PostableOnboarding{Type: OnboardingTypePods},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "nodes",
|
||||
req: &PostableOnboarding{Type: OnboardingTypeNodes},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "deployments",
|
||||
req: &PostableOnboarding{Type: OnboardingTypeDeployments},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "daemonsets",
|
||||
req: &PostableOnboarding{Type: OnboardingTypeDaemonsets},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "statefulsets",
|
||||
req: &PostableOnboarding{Type: OnboardingTypeStatefulsets},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "jobs",
|
||||
req: &PostableOnboarding{Type: OnboardingTypeJobs},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "namespaces",
|
||||
req: &PostableOnboarding{Type: OnboardingTypeNamespaces},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "clusters",
|
||||
req: &PostableOnboarding{Type: OnboardingTypeClusters},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "volumes",
|
||||
req: &PostableOnboarding{Type: OnboardingTypeVolumes},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.req.Validate()
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Ast(err, errors.TypeInvalidInput), "expected error to be of type InvalidInput")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidOnboardingTypes_MatchesEnum ensures the ValidOnboardingTypes slice
|
||||
// stays in sync with the Enum() list — both must cover every OnboardingType value.
|
||||
func TestValidOnboardingTypes_MatchesEnum(t *testing.T) {
|
||||
enum := OnboardingType{}.Enum()
|
||||
require.Equal(t, len(enum), len(ValidOnboardingTypes))
|
||||
for i, v := range enum {
|
||||
require.Equal(t, v, ValidOnboardingTypes[i])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user