Compare commits

..

3 Commits

41 changed files with 173 additions and 3103 deletions

View File

@@ -1,113 +0,0 @@
import axios from 'api';
import { CloudintegrationtypesCredentialsDTO } from 'api/generated/services/sigNoz.schemas';
import {
CloudAccount,
ServiceData,
} from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
import {
AzureCloudAccountConfig,
AzureService,
AzureServiceConfigPayload,
} from 'container/Integrations/types';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
AccountConfigResponse,
AWSAccountConfigPayload,
} from 'types/api/integrations/aws';
import {
AzureAccountConfig,
IAzureDeploymentCommands,
} from 'types/api/integrations/types';
export const getCloudIntegrationAccounts = async (
cloudServiceId: string,
): Promise<CloudAccount[]> => {
const response = await axios.get(
`/cloud-integrations/${cloudServiceId}/accounts`,
);
return response.data.data.accounts;
};
export const getCloudIntegrationServices = async (
cloudServiceId: string,
cloudAccountId?: string,
): Promise<AzureService[]> => {
const params = cloudAccountId
? { cloud_account_id: cloudAccountId }
: undefined;
const response = await axios.get(
`/cloud-integrations/${cloudServiceId}/services`,
{
params,
},
);
return response.data.data.services;
};
export const getCloudIntegrationServiceDetails = async (
cloudServiceId: string,
serviceId: string,
cloudAccountId?: string,
): Promise<ServiceData> => {
const params = cloudAccountId
? { cloud_account_id: cloudAccountId }
: undefined;
const response = await axios.get(
`/cloud-integrations/${cloudServiceId}/services/${serviceId}`,
{ params },
);
return response.data.data;
};
export const updateAccountConfig = async (
cloudServiceId: string,
accountId: string,
payload: AWSAccountConfigPayload | AzureAccountConfig,
): Promise<AccountConfigResponse> => {
const response = await axios.post<AccountConfigResponse>(
`/cloud-integrations/${cloudServiceId}/accounts/${accountId}/config`,
payload,
);
return response.data;
};
export const updateServiceConfig = async (
cloudServiceId: string,
serviceId: string,
payload: AzureServiceConfigPayload,
): Promise<AzureServiceConfigPayload> => {
const response = await axios.post<AzureServiceConfigPayload>(
`/cloud-integrations/${cloudServiceId}/services/${serviceId}/config`,
payload,
);
return response.data;
};
export const getAzureDeploymentCommands = async (params: {
agent_config: CloudintegrationtypesCredentialsDTO;
account_config: AzureCloudAccountConfig;
}): Promise<IAzureDeploymentCommands> => {
const response = await axios.post(
`/cloud-integrations/azure/accounts/generate-connection-url`,
params,
);
return response.data.data;
};
export const removeIntegrationAccount = async ({
cloudServiceId,
accountId,
}: {
cloudServiceId: string;
accountId: string;
}): Promise<SuccessResponse<Record<string, never>> | ErrorResponse> => {
const response = await axios.post(
`/cloud-integrations/${cloudServiceId}/accounts/${accountId}/disconnect`,
);
return response.data;
};

View File

@@ -1,109 +0,0 @@
.cloud-integration-accounts {
padding: 0px 16px;
display: flex;
flex-direction: column;
gap: 16px;
.selected-cloud-integration-account-section {
display: flex;
flex-direction: column;
gap: 16px;
.selected-cloud-integration-account-section-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
border-radius: 4px;
border: 1px solid var(--l3-background);
background: var(--l1-background);
.selected-cloud-integration-account-section-header-title {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
.selected-cloud-integration-account-status {
display: flex;
border-right: 1px solid var(--l3-background);
border-radius: none;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
line-height: 32px;
}
.selected-cloud-integration-account-section-header-title-text {
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
.azure-cloud-account-selector {
.ant-select {
background-color: transparent !important;
border: none !important;
box-shadow: none !important;
.ant-select-selector {
background: transparent !important;
border: none !important;
box-shadow: none !important;
}
}
}
}
}
.selected-cloud-integration-account-settings {
display: flex;
flex-direction: row;
gap: 16px;
line-height: 32px;
margin-right: 8px;
cursor: pointer;
}
}
.account-settings-container {
display: flex;
flex-direction: column;
gap: 16px;
}
.add-new-cloud-integration-account-button {
display: flex;
flex-direction: row;
gap: 16px;
line-height: 32px;
margin-right: 8px;
cursor: pointer;
}
}
}
.cloud-integration-accounts-drawer-content {
height: 100%;
max-height: calc(100vh - 120px); // Account for drawer header and padding
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
&::-webkit-scrollbar {
width: 0.2rem;
}
.edit-account-content,
.add-new-account-content {
flex: 1;
min-height: 0; // Allows flex children to shrink below content size
}
}

View File

@@ -1,169 +0,0 @@
import { useState } from 'react';
import { Button } from '@signozhq/button';
import { Color } from '@signozhq/design-tokens';
import { DrawerWrapper } from '@signozhq/drawer';
import { Select } from 'antd';
import {
GetConnectionCredentialsQueryResult,
useGetConnectionCredentials,
} from 'api/generated/services/cloudintegration';
import { CloudintegrationtypesCredentialsDTO } from 'api/generated/services/sigNoz.schemas';
import ConnectNewAzureAccount from 'container/Integrations/CloudIntegration/AzureServices/AzureAccount/ConnectNewAzureAccount';
import EditAzureAccount from 'container/Integrations/CloudIntegration/AzureServices/AzureAccount/EditAzureAccount';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import { CloudAccount } from 'container/Integrations/types';
import { Dot, PencilLine, Plus } from 'lucide-react';
import './CloudIntegrationAccounts.styles.scss';
export type DrawerMode = 'edit' | 'add';
export default function CloudIntegrationAccounts({
selectedAccount,
accounts,
isLoadingAccounts,
onSelectAccount,
refetchAccounts,
}: {
selectedAccount: CloudAccount | null;
accounts: CloudAccount[];
isLoadingAccounts: boolean;
onSelectAccount: (account: CloudAccount) => void;
refetchAccounts: () => void;
}): JSX.Element {
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [mode, setMode] = useState<DrawerMode>('add');
const handleDrawerOpenChange = (open: boolean): void => {
setIsDrawerOpen(open);
};
const handleEditAccount = (): void => {
setMode('edit');
setIsDrawerOpen(true);
};
const handleAddNewAccount = (): void => {
setMode('add');
setIsDrawerOpen(true);
};
const {
data: connectionParams,
isLoading: isConnectionParamsLoading,
} = useGetConnectionCredentials<GetConnectionCredentialsQueryResult>({
cloudProvider: INTEGRATION_TYPES.AZURE,
});
const handleSelectAccount = (value: string): void => {
const account = accounts.find(
(account) => account.cloud_account_id === value,
);
if (account) {
onSelectAccount(account);
}
};
const handleAccountConnected = (): void => {
refetchAccounts();
};
const handleAccountUpdated = (): void => {
refetchAccounts();
};
const renderDrawerContent = (): JSX.Element => {
return (
<div className="cloud-integration-accounts-drawer-content">
{mode === 'edit' ? (
<div className="edit-account-content">
<EditAzureAccount
selectedAccount={selectedAccount as CloudAccount}
connectionParams={
connectionParams?.data as CloudintegrationtypesCredentialsDTO
}
isConnectionParamsLoading={isConnectionParamsLoading}
onAccountUpdated={handleAccountUpdated}
/>
</div>
) : (
<div className="add-new-account-content">
<ConnectNewAzureAccount
connectionParams={
connectionParams?.data as CloudintegrationtypesCredentialsDTO
}
isConnectionParamsLoading={isConnectionParamsLoading}
onAccountConnected={handleAccountConnected}
/>
</div>
)}
</div>
);
};
return (
<div className="cloud-integration-accounts">
{selectedAccount && (
<div className="selected-cloud-integration-account-section">
<div className="selected-cloud-integration-account-section-header">
<div className="selected-cloud-integration-account-section-header-title">
<div className="selected-cloud-integration-account-status">
<Dot size={24} color={Color.BG_FOREST_500} />
</div>
<div className="selected-cloud-integration-account-section-header-title-text">
Subscription ID :
<span className="azure-cloud-account-selector">
<Select
value={selectedAccount?.cloud_account_id}
options={accounts.map((account) => ({
label: account.cloud_account_id,
value: account.cloud_account_id,
}))}
onChange={handleSelectAccount}
loading={isLoadingAccounts}
placeholder="Select Account"
/>
</span>
</div>
</div>
<div className="selected-cloud-integration-account-settings">
<Button
variant="link"
color="secondary"
prefixIcon={<PencilLine size={14} />}
onClick={handleEditAccount}
>
Edit Account
</Button>
<Button
variant="link"
color="secondary"
prefixIcon={<Plus size={14} />}
onClick={handleAddNewAccount}
>
Add New Account
</Button>
</div>
</div>
</div>
)}
<div className="account-settings-container">
<DrawerWrapper
open={isDrawerOpen}
onOpenChange={handleDrawerOpenChange}
type="panel"
header={{
title: mode === 'add' ? 'Connect with Azure' : 'Edit Azure Account',
}}
content={renderDrawerContent()}
showCloseButton
allowOutsideClick={mode === 'edit'}
direction="right"
/>
</div>
</div>
);
}

View File

@@ -1,3 +0,0 @@
import CloudIntegrationAccounts from './CloudIntegrationAccounts';
export default CloudIntegrationAccounts;

View File

@@ -1,50 +0,0 @@
.cloud-integrations-header-section {
display: flex;
flex-direction: column;
gap: 16px;
border-bottom: 1px solid var(--l3-background);
.cloud-integrations-header {
display: flex;
flex-direction: row;
align-items: center;
padding: 16px;
padding-bottom: 0px;
gap: 16px;
.cloud-integrations-title-section {
display: flex;
flex-direction: column;
gap: 4px;
.cloud-integrations-title {
display: flex;
align-items: center;
gap: 16px;
.cloud-integrations-icon {
width: 40px;
height: 40px;
}
color: var(--l1-foreground);
font-family: Inter;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 32px; /* 200% */
letter-spacing: -0.08px;
}
.cloud-integrations-description {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
}
}

View File

@@ -1,61 +0,0 @@
import {
AWS_INTEGRATION,
AZURE_INTEGRATION,
} from 'container/Integrations/constants';
import { CloudAccount, IntegrationType } from 'container/Integrations/types';
import CloudIntegrationAccounts from '../CloudIntegrationAccounts';
import './CloudIntegrationsHeader.styles.scss';
export default function CloudIntegrationsHeader({
cloudServiceId,
selectedAccount,
accounts,
isLoadingAccounts,
onSelectAccount,
refetchAccounts,
}: {
selectedAccount: CloudAccount | null;
accounts: CloudAccount[] | [];
isLoadingAccounts: boolean;
onSelectAccount: (account: CloudAccount) => void;
cloudServiceId: IntegrationType;
refetchAccounts: () => void;
}): JSX.Element {
const INTEGRATION_DATA =
cloudServiceId === IntegrationType.AWS_SERVICES
? AWS_INTEGRATION
: AZURE_INTEGRATION;
return (
<div className="cloud-integrations-header-section">
<div className="cloud-integrations-header">
<div className="cloud-integrations-title-section">
<div className="cloud-integrations-title">
<img
className="cloud-integrations-icon"
src={INTEGRATION_DATA.icon}
alt={INTEGRATION_DATA.icon_alt}
/>
{INTEGRATION_DATA.title}
</div>
<div className="cloud-integrations-description">
{INTEGRATION_DATA.description}
</div>
</div>
</div>
<div className="cloud-integrations-accounts-list">
<CloudIntegrationAccounts
selectedAccount={selectedAccount}
accounts={accounts}
isLoadingAccounts={isLoadingAccounts}
onSelectAccount={onSelectAccount}
refetchAccounts={refetchAccounts}
/>
</div>
</div>
);
}

View File

@@ -1,3 +0,0 @@
import CloudIntegrationsHeader from './CloudIntegrationsHeader';
export default CloudIntegrationsHeader;

View File

@@ -1,19 +1,4 @@
export const REACT_QUERY_KEY = {
// Cloud Integration Query Keys
CLOUD_INTEGRATION_ACCOUNTS: 'CLOUD_INTEGRATION_ACCOUNTS',
CLOUD_INTEGRATION_SERVICES: 'CLOUD_INTEGRATION_SERVICES',
CLOUD_INTEGRATION_SERVICE_DETAILS: 'CLOUD_INTEGRATION_SERVICE_DETAILS',
CLOUD_INTEGRATION_ACCOUNT_STATUS: 'CLOUD_INTEGRATION_ACCOUNT_STATUS',
CLOUD_INTEGRATION_UPDATE_ACCOUNT_CONFIG:
'CLOUD_INTEGRATION_UPDATE_ACCOUNT_CONFIG',
CLOUD_INTEGRATION_UPDATE_SERVICE_CONFIG:
'CLOUD_INTEGRATION_UPDATE_SERVICE_CONFIG',
CLOUD_INTEGRATION_GENERATE_CONNECTION_URL:
'CLOUD_INTEGRATION_GENERATE_CONNECTION_URL',
CLOUD_INTEGRATION_GET_CONNECTION_PARAMS:
'CLOUD_INTEGRATION_GET_CONNECTION_PARAMS',
CLOUD_INTEGRATION_GET_DEPLOYMENT_COMMANDS:
'CLOUD_INTEGRATION_GET_DEPLOYMENT_COMMANDS',
/**
* For any query that should support AutoRefresh and min/max time is from DateTimeSelectionV2
* You can prefix the query with this KEY, it will allow the queries to be automatically refreshed

View File

@@ -5,11 +5,13 @@ import { Color } from '@signozhq/design-tokens';
import { Select, Skeleton } from 'antd';
import { SelectProps } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { useListAccounts } from 'api/generated/services/cloudintegration';
import { getAccountById } from 'container/Integrations/CloudIntegration/utils';
import { useGetCloudIntegrationAccounts } from 'hooks/integration/useGetCloudIntegrationAccounts';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
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';
@@ -53,7 +55,7 @@ function AccountActionsRenderer({
<span className="account-selector">
<Select
value={activeAccount?.cloud_account_id}
value={activeAccount?.providerAccountId}
options={selectOptions}
rootClassName="cloud-account-selector"
placeholder="Select AWS Account"
@@ -102,7 +104,18 @@ function AccountActionsRenderer({
function AccountActions(): JSX.Element {
const urlQuery = useUrlQuery();
const navigate = useNavigate();
const { data: accounts, isLoading } = useGetCloudIntegrationAccounts('aws');
const { data: listAccountsResponse, isLoading } = useListAccounts({
cloudProvider: INTEGRATION_TYPES.AWS,
});
const accounts = useMemo((): CloudAccount[] | undefined => {
const raw = listAccountsResponse?.data?.accounts;
if (!raw) {
return undefined;
}
return raw
.map(mapAccountDtoToAwsCloudAccount)
.filter((account): account is CloudAccount => account !== null);
}, [listAccountsResponse]);
const initialAccount = useMemo(
() =>
@@ -166,7 +179,7 @@ function AccountActions(): JSX.Element {
accounts?.length
? accounts.map((account) => ({
value: account.cloud_account_id,
label: account.cloud_account_id,
label: account.providerAccountId,
}))
: [],
[accounts],

View File

@@ -3,7 +3,8 @@ import { useQueryClient } from 'react-query';
import { Button } from '@signozhq/button';
import { DrawerWrapper } from '@signozhq/drawer';
import { Form } from 'antd';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { invalidateListAccounts } from 'api/generated/services/cloudintegration';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import { useAccountSettingsModal } from 'hooks/integration/aws/useAccountSettingsModal';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
@@ -43,7 +44,9 @@ function AccountSettingsModal({
const urlQuery = useUrlQuery();
const handleRemoveIntegrationAccountSuccess = useCallback((): void => {
queryClient.invalidateQueries([REACT_QUERY_KEY.CLOUD_INTEGRATION_ACCOUNTS]);
void invalidateListAccounts(queryClient, {
cloudProvider: INTEGRATION_TYPES.AWS,
});
urlQuery.delete('cloudAccountId');
setActiveAccount(null);
handleClose();

View File

@@ -1,9 +1,7 @@
import { useCallback } from 'react';
import { useQueryClient } from 'react-query';
import { Button } from '@signozhq/button';
import { Color } from '@signozhq/design-tokens';
import { DrawerWrapper } from '@signozhq/drawer';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useIntegrationModal } from 'hooks/integration/aws/useIntegrationModal';
import { SquareArrowOutUpRight } from 'lucide-react';
@@ -13,18 +11,15 @@ import {
ModalStateEnum,
} from '../types';
import { RegionForm } from './RegionForm';
import { SuccessView } from './SuccessView';
import './CloudAccountSetupModal.style.scss';
function CloudAccountSetupModal({
onClose,
}: IntegrationModalProps): JSX.Element {
const queryClient = useQueryClient();
const {
form,
modalState,
setModalState,
isLoading,
activeView,
selectedRegions,
@@ -40,19 +35,17 @@ function CloudAccountSetupModal({
handleRegionChange,
connectionParams,
isConnectionParamsLoading,
handleConnectionSuccess,
handleConnectionTimeout,
handleConnectionError,
} = useIntegrationModal({ onClose });
const renderContent = useCallback(() => {
if (modalState === ModalStateEnum.SUCCESS) {
return <SuccessView />;
}
return (
<div className="cloud-account-setup-modal__content">
<RegionForm
form={form}
modalState={modalState}
setModalState={setModalState}
selectedRegions={selectedRegions}
includeAllRegions={includeAllRegions}
onRegionSelect={handleRegionSelect}
@@ -63,6 +56,9 @@ function CloudAccountSetupModal({
isConnectionParamsLoading={isConnectionParamsLoading}
setSelectedRegions={setSelectedRegions}
setIncludeAllRegions={setIncludeAllRegions}
onConnectionSuccess={handleConnectionSuccess}
onConnectionTimeout={handleConnectionTimeout}
onConnectionError={handleConnectionError}
/>
<div className="cloud-account-setup-modal__footer">
@@ -73,6 +69,12 @@ function CloudAccountSetupModal({
<SquareArrowOutUpRight size={17} color={Color.BG_VANILLA_100} />
}
onClick={handleSubmit}
disabled={
selectedRegions.length === 0 ||
isLoading ||
isGeneratingUrl ||
modalState === ModalStateEnum.WAITING
}
>
Launch Cloud Formation Template
</Button>
@@ -82,7 +84,6 @@ function CloudAccountSetupModal({
}, [
modalState,
form,
setModalState,
selectedRegions,
includeAllRegions,
handleRegionSelect,
@@ -93,6 +94,11 @@ function CloudAccountSetupModal({
isConnectionParamsLoading,
setSelectedRegions,
setIncludeAllRegions,
isLoading,
isGeneratingUrl,
handleConnectionSuccess,
handleConnectionTimeout,
handleConnectionError,
]);
const getSelectedRegionsCount = useCallback(
@@ -101,26 +107,6 @@ function CloudAccountSetupModal({
);
const getModalConfig = useCallback(() => {
// Handle success state first
if (modalState === ModalStateEnum.SUCCESS) {
return {
title: 'AWS Integration',
okText: (
<div className="cloud-account-setup-success-view__footer-button">
Continue
</div>
),
block: true,
onOk: (): void => {
queryClient.invalidateQueries([REACT_QUERY_KEY.AWS_ACCOUNTS]);
handleClose();
},
cancelButtonProps: { style: { display: 'none' } },
disabled: false,
};
}
// Handle other views
const viewConfigs = {
[ActiveViewEnum.FORM]: {
title: 'Add AWS Account',
@@ -153,9 +139,7 @@ function CloudAccountSetupModal({
isLoading,
isGeneratingUrl,
activeView,
handleClose,
setActiveView,
queryClient,
]);
const modalConfig = getModalConfig();

View File

@@ -5,7 +5,6 @@ import cx from 'classnames';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import { regions } from 'utils/regions';
import logEvent from '../../../../../../api/common/logEvent';
import { ModalStateEnum, RegionFormProps } from '../types';
import AlertMessage from './AlertMessage';
import {
@@ -18,7 +17,6 @@ import RenderConnectionFields from './RenderConnectionParams';
export function RegionForm({
form,
modalState,
setModalState,
selectedRegions,
onSubmit,
accountId,
@@ -27,6 +25,9 @@ export function RegionForm({
isConnectionParamsLoading,
setSelectedRegions,
setIncludeAllRegions,
onConnectionSuccess,
onConnectionTimeout,
onConnectionError,
}: RegionFormProps): JSX.Element {
const startTimeRef = useRef(Date.now());
const refetchInterval = 10 * 1000;
@@ -50,20 +51,16 @@ export function RegionForm({
const cloudAccountId =
response.data.providerAccountId ?? response.data.id;
setModalState(ModalStateEnum.SUCCESS);
logEvent('AWS Integration: Account connected', {
onConnectionSuccess({
cloudAccountId,
status: response.data.agentReport,
});
} else if (Date.now() - startTimeRef.current >= errorTimeout) {
setModalState(ModalStateEnum.ERROR);
logEvent('AWS Integration: Account connection attempt timed out', {
id: accountId,
});
onConnectionTimeout({ id: accountId });
}
},
onError: () => {
setModalState(ModalStateEnum.ERROR);
onConnectionError();
},
},
},

View File

@@ -1,156 +0,0 @@
.cloud-account-setup-success-view {
display: flex;
flex-direction: column;
gap: 40px;
text-align: center;
padding-top: 34px;
p,
h3,
h4 {
margin: 0;
}
&__content {
display: flex;
flex-direction: column;
gap: 14px;
.cloud-account-setup-success-view {
&__title {
h3 {
color: var(--bg-vanilla-100);
font-size: 20px;
font-weight: 500;
line-height: 32px;
}
}
&__description {
color: var(--bg-vanilla-400);
font-size: 14px;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
}
}
}
&__what-next {
display: flex;
flex-direction: column;
gap: 18px;
text-align: left;
&-title {
color: var(--bg-slate-50);
font-size: 11px;
font-weight: 500;
line-height: 18px;
letter-spacing: 0.88px;
text-transform: uppercase;
}
.what-next-items-wrapper {
display: flex;
flex-direction: column;
gap: 12px;
&__item {
display: flex;
gap: 10px;
align-items: baseline;
&.ant-alert {
padding: 14px;
border-radius: 8px;
font-size: 14px;
line-height: 20px; /* 142.857% */
letter-spacing: -0.21px;
}
&.ant-alert-info {
border: 1px solid rgba(63, 94, 204, 0.5);
background: rgba(78, 116, 248, 0.2);
color: var(--bg-robin-400);
}
.what-next-item {
color: var(--bg-robin-400);
&-bullet-icon {
font-size: 20px;
line-height: 20px;
}
&-text {
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.21px;
}
}
}
}
}
&__footer {
padding-top: 18px;
.ant-btn {
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
font-size: 14px;
font-weight: 500;
line-height: 20px;
height: 36px;
}
}
}
.lottie-container {
position: absolute;
width: 743.5px;
height: 990.342px;
top: -100px;
left: -36px;
z-index: 1;
}
.lightMode {
.cloud-account-setup-success-view {
&__content {
.cloud-account-setup-success-view {
&__title {
h3 {
color: var(--bg-ink-500);
}
}
&__description {
color: var(--bg-ink-400);
}
}
}
&__what-next {
&-title {
color: var(--bg-ink-500);
}
.what-next-items-wrapper {
&__item {
&.ant-alert-info {
border: 1px solid rgba(63, 94, 204, 0.2);
background: rgba(78, 116, 248, 0.1);
color: var(--bg-robin-500);
}
.what-next-item {
color: var(--bg-robin-500);
&-text {
color: var(--bg-robin-500);
}
}
}
}
}
&__footer {
.ant-btn {
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
&:hover {
background: var(--bg-robin-400);
}
}
}
}
}

View File

@@ -1,73 +0,0 @@
import { useState } from 'react';
import Lottie from 'react-lottie';
import { Alert } from 'antd';
import integrationsSuccess from 'assets/Lotties/integrations-success.json';
import './SuccessView.style.scss';
export function SuccessView(): JSX.Element {
const [isAnimationComplete, setIsAnimationComplete] = useState(false);
const defaultOptions = {
loop: false,
autoplay: true,
animationData: integrationsSuccess,
rendererSettings: {
preserveAspectRatio: 'xMidYMid slice',
},
};
return (
<>
{!isAnimationComplete && (
<div className="lottie-container">
<Lottie
options={defaultOptions}
height={990.342}
width={743.5}
eventListeners={[
{
eventName: 'complete',
callback: (): void => setIsAnimationComplete(true),
},
]}
/>
</div>
)}
<div className="cloud-account-setup-success-view">
<div className="cloud-account-setup-success-view__icon">
<img src="Icons/solid-check-circle.svg" alt="Success" />
</div>
<div className="cloud-account-setup-success-view__content">
<div className="cloud-account-setup-success-view__title">
<h3>🎉 Success! </h3>
<h3>Your AWS Web Service integration is all set.</h3>
</div>
<div className="cloud-account-setup-success-view__description">
<p>Your observability journey is off to a great start. </p>
<p>Now that your data is flowing, heres what you can do next:</p>
</div>
</div>
<div className="cloud-account-setup-success-view__what-next">
<h4 className="cloud-account-setup-success-view__what-next-title">
WHAT NEXT
</h4>
<div className="what-next-items-wrapper">
<Alert
message={
<div className="what-next-items-wrapper__item">
<div className="what-next-item-bullet-icon"></div>
<div className="what-next-item-text">
Set up your AWS services effortlessly under your enabled account.
</div>
</div>
}
type="info"
className="what-next-items-wrapper__item"
/>
</div>
</div>
</div>
</>
);
}

View File

@@ -11,13 +11,11 @@ export enum ModalStateEnum {
FORM = 'form',
WAITING = 'waiting',
ERROR = 'error',
SUCCESS = 'success',
}
export interface RegionFormProps {
form: FormInstance;
modalState: ModalStateEnum;
setModalState: Dispatch<SetStateAction<ModalStateEnum>>;
selectedRegions: string[];
includeAllRegions: boolean;
onRegionSelect: () => void;
@@ -28,6 +26,12 @@ export interface RegionFormProps {
isConnectionParamsLoading?: boolean;
setSelectedRegions: Dispatch<SetStateAction<string[]>>;
setIncludeAllRegions: Dispatch<SetStateAction<boolean>>;
onConnectionSuccess: (payload: {
cloudAccountId: string;
status?: unknown;
}) => void;
onConnectionTimeout: (payload: { id?: string }) => void;
onConnectionError: () => void;
}
export interface IntegrationModalProps {

View File

@@ -1,8 +1,12 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Select, Skeleton } from 'antd';
import { useGetCloudIntegrationAccounts } from 'hooks/integration/useGetCloudIntegrationAccounts';
import { useListAccounts } from 'api/generated/services/cloudintegration';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import useUrlQuery from 'hooks/useUrlQuery';
import { mapAccountDtoToAwsCloudAccount } from '../mapAwsCloudAccountFromDto';
import { CloudAccount } from '../types';
import './S3BucketsSelector.styles.scss';
interface S3BucketsSelectorProps {
@@ -21,7 +25,18 @@ function S3BucketsSelector({
disabled: isSelectorDisabled = false,
}: S3BucketsSelectorProps): JSX.Element {
const cloudAccountId = useUrlQuery().get('cloudAccountId');
const { data: accounts, isLoading } = useGetCloudIntegrationAccounts('aws');
const { data: listAccountsResponse, isLoading } = useListAccounts({
cloudProvider: INTEGRATION_TYPES.AWS,
});
const accounts = useMemo((): CloudAccount[] | undefined => {
const raw = listAccountsResponse?.data?.accounts;
if (!raw) {
return undefined;
}
return raw
.map(mapAccountDtoToAwsCloudAccount)
.filter((account): account is CloudAccount => account !== null);
}, [listAccountsResponse]);
const [bucketsByRegion, setBucketsByRegion] = useState<
Record<string, string[]>
>(initialBucketsByRegion);

View File

@@ -168,7 +168,6 @@
.aws-service-dashboards-items {
display: flex;
flex-direction: column;
gap: 16px;
.aws-service-dashboard-item {
display: flex;

View File

@@ -69,6 +69,8 @@ function ServiceDetails(): JSX.Element | null {
);
const awsConfig = serviceDetailsData?.cloudIntegrationService?.config?.aws;
const isServiceEnabledInPersistedConfig =
Boolean(awsConfig?.logs?.enabled) || Boolean(awsConfig?.metrics?.enabled);
const serviceDetailsId = serviceDetailsData?.id;
const {
@@ -255,9 +257,7 @@ function ServiceDetails(): JSX.Element | null {
// eslint-disable-next-line sonarjs/cognitive-complexity
const renderOverview = (): JSX.Element => {
const logsEnabled = watch('logsEnabled');
const metricsEnabled = watch('metricsEnabled');
const s3BucketsByRegion = watch('s3BucketsByRegion');
const isServiceConfigEnabled = logsEnabled || metricsEnabled;
const isLogsSupported = serviceDetailsData?.supportedSignals?.logs || false;
const isMetricsSupported =
@@ -378,7 +378,7 @@ function ServiceDetails(): JSX.Element | null {
/>
<ServiceDashboards
service={serviceDetailsData}
isInteractive={!isReadOnly && isServiceConfigEnabled}
isInteractive={!isReadOnly && isServiceEnabledInPersistedConfig}
/>
</div>
);

View File

@@ -80,7 +80,17 @@ function ServicesList({ cloudAccountId }: ServicesListProps): JSX.Element {
}
if (!awsServices?.length) {
return <div>No services found</div>;
return (
<div className="services-list-empty-message">
{' '}
<img
src="/Icons/emptyState.svg"
alt="no-services-found"
className="empty-state-svg"
/>{' '}
No services found
</div>
);
}
const isEnabledServicesEmpty = enabledServices.length === 0;

View File

@@ -55,6 +55,24 @@
}
}
.services-list-empty-message {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
color: var(--bg-vanilla-400);
font-size: 14px;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
.empty-state-svg {
height: 20px;
width: 20px;
}
}
.services-filter {
padding: 12px;

View File

@@ -0,0 +1,25 @@
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,
};
}

View File

@@ -47,6 +47,7 @@ interface CloudAccount {
cloud_account_id: string;
config: AWSCloudAccountConfig;
status: AccountStatus;
providerAccountId: string;
}
interface CloudAccountsData {

View File

@@ -1,345 +0,0 @@
.azure-account-container {
padding: 16px 8px;
.azure-account-title {
font-size: 24px;
font-weight: 600;
}
.azure-account-prerequisites-step {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px 0px;
.azure-account-prerequisites-step-description {
display: flex;
flex-direction: column;
gap: 16px;
.azure-account-prerequisites-step-description-item {
color: var(--l2-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px;
display: flex;
align-items: center;
gap: 8px;
.azure-account-prerequisites-step-description-item-bullet {
color: var(--primary-color);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
}
}
}
.azure-account-prerequisites-step-how-it-works {
.azure-account-prerequisites-step-how-it-works-title {
display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
cursor: pointer;
padding: 8px 16px;
border: 1px solid var(--l3-background);
border-radius: 4px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background: var(--l1-background);
.azure-account-prerequisites-step-how-it-works-title-icon {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
color: var(--l1-foreground);
}
.azure-account-prerequisites-step-how-it-works-title-text {
color: var(--l1-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 166.667% */
letter-spacing: -0.06px;
}
}
.azure-account-prerequisites-step-how-it-works-description {
padding: 16px;
color: var(--l2-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 150% */
letter-spacing: -0.06px;
border: 1px solid var(--l3-background);
border-top: none;
border-radius: 4px;
border-top-left-radius: 0;
border-top-right-radius: 0;
background: var(--l1-background);
display: flex;
flex-direction: column;
gap: 16px;
}
}
}
.azure-account-configure-agent-step {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px 0px;
}
.azure-account-deploy-agent-step {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px 0px;
.azure-account-deploy-agent-step-subtitle {
color: var(--l2-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
}
.azure-account-deploy-agent-step-commands {
display: flex;
flex-direction: column;
gap: 8px;
.azure-account-deploy-agent-step-commands-tabs {
width: 100%;
border-radius: 6px;
border: 1px solid var(--l3-background);
background: var(--l1-background);
padding: 8px;
// attribute - role="tabpanel"
[role='tabpanel'] {
width: 100%;
}
.azure-account-deploy-agent-step-commands-tabs-content {
width: 100%;
}
}
}
.azure-account-connection-status-container {
display: flex;
flex-direction: column;
gap: 16px;
.azure-account-connection-status-content {
width: 100%;
.azure-account-connection-status-callout {
width: 100%;
[data-slot='callout-title'] {
font-size: 13px;
font-weight: 400 !important;
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
}
}
}
.azure-account-connection-status-close-disclosure {
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
}
}
}
.azure-account-form {
display: flex;
flex-direction: column;
gap: 16px;
.azure-account-configure-agent-step-primary-region {
display: flex;
flex-direction: column;
gap: 8px;
.azure-account-configure-agent-step-primary-region-title {
color: var(--l1-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 150% */
letter-spacing: -0.06px;
}
.azure-account-configure-agent-step-primary-region-select {
display: flex;
flex-direction: column;
gap: 8px;
.azure-account-form-regions-selector {
display: flex;
flex-direction: column;
gap: 8px;
.ant-select {
width: 100%;
}
.azure-account-form-error {
color: var(--destructive);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
}
}
}
}
.azure-account-configure-agent-step-resource-groups {
display: flex;
flex-direction: column;
gap: 8px;
.azure-account-configure-agent-step-resource-groups-title {
color: var(--l1-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 150% */
letter-spacing: -0.06px;
}
.azure-account-form-resource-groups-selector {
display: flex;
flex-direction: column;
gap: 8px;
.ant-select {
width: 100%;
}
.azure-account-form-error {
color: var(--destructive);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
}
}
}
.azure-account-actions-container {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 8px;
}
}
.ant-select {
.ant-select-selector {
background: var(--l1-background);
border: 1px solid var(--l3-background);
border-radius: 6px;
}
.ant-select-selection-item {
color: var(--l1-foreground);
}
.ant-select-arrow {
color: var(--l1-foreground);
}
.ant-select-dropdown {
background: var(--l1-background);
border: 1px solid var(--l3-background);
border-radius: 6px;
}
.ant-select-item {
color: var(--l1-foreground);
}
.ant-select-item-option-active {
background: var(--l1-background);
}
}
.azure-account-disconnect-container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: 8px;
padding: 16px 0px;
}
}
.azure-account-disconnect-modal {
.ant-modal-content {
width: 480px;
min-height: 200px;
flex-shrink: 0;
background: var(--l1-background);
color: var(--l1-foreground);
border: 1px solid var(--l3-background);
border-radius: 4px;
padding: 12px;
}
.ant-modal-header {
background: var(--l1-background);
color: var(--l1-foreground);
.ant-modal-title {
color: var(--l1-foreground);
}
}
.ant-modal-body {
margin-top: 16px;
}
.ant-modal-close {
color: var(--l1-foreground);
}
.ant-modal-footer {
margin-top: 32px;
}
}

View File

@@ -1,245 +0,0 @@
import { useCallback, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useMutation, useQueryClient } from 'react-query';
import { Button } from '@signozhq/button';
import { toast } from '@signozhq/sonner';
import { Select } from 'antd';
import { Modal } from 'antd/lib';
import { CloudintegrationtypesCredentialsDTO } from 'api/generated/services/sigNoz.schemas';
import { removeIntegrationAccount } from 'api/integration';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
AZURE_REGIONS,
INTEGRATION_TYPES,
} from 'container/Integrations/constants';
import {
AzureCloudAccountConfig,
CloudAccount,
} from 'container/Integrations/types';
import { CornerDownRight, Unlink } from 'lucide-react';
interface AzureAccountFormProps {
mode?: 'add' | 'edit';
selectedAccount: CloudAccount | null;
connectionParams: CloudintegrationtypesCredentialsDTO;
isConnectionParamsLoading: boolean;
isLoading: boolean;
onSubmit: (values: {
primaryRegion: string;
resourceGroups: string[];
}) => void;
submitButtonText?: string;
showDisconnectAccountButton?: boolean;
}
type AzureAccountFormValues = {
primaryRegion: string;
resourceGroups: string[];
};
export const AzureAccountForm = ({
mode = 'add',
selectedAccount,
connectionParams,
isConnectionParamsLoading,
isLoading,
onSubmit,
submitButtonText = 'Fetch Deployment Command',
showDisconnectAccountButton = false,
}: AzureAccountFormProps): JSX.Element => {
const queryClient = useQueryClient();
const [isModalOpen, setIsModalOpen] = useState(false);
const {
control,
handleSubmit: handleFormSubmit,
} = useForm<AzureAccountFormValues>({
defaultValues: {
primaryRegion:
(selectedAccount?.config as AzureCloudAccountConfig)?.deployment_region ||
'',
resourceGroups:
(selectedAccount?.config as AzureCloudAccountConfig)?.resource_groups || [],
},
});
const onFormSubmit = useCallback(
(values: AzureAccountFormValues): void => {
onSubmit({
primaryRegion: values.primaryRegion,
resourceGroups: values.resourceGroups,
});
},
[onSubmit],
);
const handleDisconnect = (): void => {
setIsModalOpen(true);
};
const {
mutate: removeIntegration,
isLoading: isRemoveIntegrationLoading,
} = useMutation(removeIntegrationAccount, {
onSuccess: () => {
toast.success('Azure account disconnected successfully', {
description: 'Azure account disconnected successfully',
position: 'top-right',
duration: 3000,
});
queryClient.invalidateQueries([REACT_QUERY_KEY.CLOUD_INTEGRATION_ACCOUNTS]);
setIsModalOpen(false);
},
onError: (error) => {
console.error('Failed to remove integration:', error);
},
});
const handleOk = (): void => {
removeIntegration({
cloudServiceId: INTEGRATION_TYPES.AZURE,
accountId: selectedAccount?.id as string,
});
};
const handleCancel = (): void => {
setIsModalOpen(false);
};
return (
<form className="azure-account-form">
<div className="azure-account-configure-agent-step-primary-region">
<div className="azure-account-configure-agent-step-primary-region-title">
Select primary region
</div>
<div className="azure-account-configure-agent-step-primary-region-select">
<Controller
control={control}
name="primaryRegion"
rules={{
required: 'Please select a primary region',
}}
render={({ field, fieldState }): JSX.Element => (
<div className="azure-account-form-regions-selector">
<Select
{...field}
disabled={mode === 'edit'}
placeholder="Select primary region"
options={AZURE_REGIONS}
showSearch
filterOption={(input, option): boolean => {
const label = String(option?.label ?? '');
const value = String(option?.value ?? '');
return (
label.toLowerCase().includes(input.toLowerCase()) ||
value.toLowerCase().includes(input.toLowerCase())
);
}}
notFoundContent={null}
/>
{fieldState.error && (
<div className="azure-account-form-error">
{fieldState.error.message}
</div>
)}
</div>
)}
/>
</div>
</div>
<div className="azure-account-configure-agent-step-resource-groups">
<div className="azure-account-configure-agent-step-resource-groups-title">
Enter resource groups you want to monitor
</div>
<div className="azure-account-configure-agent-step-resource-groups-select">
<Controller
control={control}
name="resourceGroups"
rules={{
required: 'Please enter resource groups you want to monitor',
validate: (value: string[] | undefined): boolean | string =>
Array.isArray(value) && value.length > 0
? true
: 'Please enter resource groups you want to monitor',
}}
render={({ field, fieldState }): JSX.Element => (
<div className="azure-account-form-resource-groups-selector">
<Select
{...field}
placeholder="Enter resource groups you want to monitor"
options={[]}
mode="tags"
notFoundContent={null}
filterOption={false}
showSearch={false}
/>
{fieldState.error && (
<div className="azure-account-form-error">
{fieldState.error.message}
</div>
)}
</div>
)}
/>
</div>
</div>
<div className="azure-account-actions-container">
{showDisconnectAccountButton && (
<div className="azure-account-disconnect-container">
<Button
variant="solid"
color="destructive"
prefixIcon={<Unlink size={14} />}
size="sm"
onClick={handleDisconnect}
disabled={isRemoveIntegrationLoading}
>
Disconnect
</Button>
</div>
)}
<Button
variant="solid"
color="primary"
onClick={handleFormSubmit(onFormSubmit)}
size="sm"
prefixIcon={<CornerDownRight size={12} />}
loading={
isConnectionParamsLoading || isLoading || isRemoveIntegrationLoading
}
disabled={
isConnectionParamsLoading ||
!connectionParams ||
isLoading ||
isRemoveIntegrationLoading
}
>
{submitButtonText}
</Button>
</div>
<Modal
className="azure-account-disconnect-modal"
open={isModalOpen}
title="Remove integration"
onOk={handleOk}
onCancel={handleCancel}
okText="Remove Integration"
okButtonProps={{
danger: true,
}}
>
<div className="remove-integration-modal-content">
Removing this account will remove all components created for sending
telemetry to SigNoz in your Azure account within the next ~15 minutes
</div>
</Modal>
</form>
);
};

View File

@@ -1,379 +0,0 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { Callout } from '@signozhq/callout';
import { toast } from '@signozhq/sonner';
import Tabs from '@signozhq/tabs';
import { Steps } from 'antd';
import { StepsProps } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { CloudintegrationtypesCredentialsDTO } from 'api/generated/services/sigNoz.schemas';
import { getAzureDeploymentCommands } from 'api/integration';
import { CodeBlock } from 'components/CodeBlock';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import { useGetAccountStatus } from 'hooks/integration/useGetAccountStatus';
import { ChevronDown, ChevronRight } from 'lucide-react';
import {
AccountStatusResponse,
IAzureDeploymentCommands,
} from 'types/api/integrations/types';
import { AzureAccountForm } from './AzureAccountForm';
import './AzureAccount.styles.scss';
interface ConnectNewAzureAccountProps {
connectionParams: CloudintegrationtypesCredentialsDTO;
isConnectionParamsLoading: boolean;
onAccountConnected: () => void;
}
const PrerequisitesStep = (): JSX.Element => {
const [isHowItWorksOpen, setIsHowItWorksOpen] = useState(false);
const handleHowItWorksClick = (): void => {
setIsHowItWorksOpen(!isHowItWorksOpen);
};
return (
<div className="azure-account-prerequisites-step">
<div className="azure-account-prerequisites-step-description">
<div className="azure-account-prerequisites-step-description-item">
<span className="azure-account-prerequisites-step-description-item-bullet">
</span>{' '}
Ensure that youre logged in to the Azure portal or Azure CLI is setup for
your subscription
</div>
<div className="azure-account-prerequisites-step-description-item">
<span className="azure-account-prerequisites-step-description-item-bullet">
</span>{' '}
Ensure that you either have the OWNER role OR
</div>
<div className="azure-account-prerequisites-step-description-item">
<span className="azure-account-prerequisites-step-description-item-bullet">
</span>{' '}
Both the CONTRIBUTOR and USER ACCESS ADMIN roles.
</div>
</div>
<div className="azure-account-prerequisites-step-how-it-works">
<div
className="azure-account-prerequisites-step-how-it-works-title"
onClick={handleHowItWorksClick}
>
<div className="azure-account-prerequisites-step-how-it-works-title-icon">
{isHowItWorksOpen ? (
<ChevronDown size={16} />
) : (
<ChevronRight size={16} />
)}
</div>
<div className="azure-account-prerequisites-step-how-it-works-title-text">
How it works
</div>
</div>
{isHowItWorksOpen && (
<div className="azure-account-prerequisites-step-how-it-works-description">
<p>
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.
</p>
<p>
Once the Integration template is deployed, you can enable the services
you want to monitor right here in SigNoz dashboard.
</p>
</div>
)}
</div>
</div>
);
};
const ConnectionSuccess = {
type: 'success' as const,
title: 'Agent has been deployed successfully.',
description: 'You can now safely close this panel.',
};
const ConnectionWarning = {
type: 'warning' as const,
title: 'Listening for data...',
description:
'Do not close this panel until the agent stack is deployed successfully.',
};
export const ConfigureAgentStep = ({
connectionParams,
isConnectionParamsLoading,
setDeploymentCommands,
setAccountId,
}: {
connectionParams: CloudintegrationtypesCredentialsDTO;
isConnectionParamsLoading: boolean;
setDeploymentCommands: (deploymentCommands: IAzureDeploymentCommands) => void;
setAccountId: (accountId: string) => void;
}): JSX.Element => {
const [isFetchingDeploymentCommand, setIsFetchingDeploymentCommand] = useState(
false,
);
const getDeploymentCommand = async ({
primaryRegion,
resourceGroups,
}: {
primaryRegion: string;
resourceGroups: string[];
}): Promise<IAzureDeploymentCommands> => {
setIsFetchingDeploymentCommand(true);
return await getAzureDeploymentCommands({
agent_config: connectionParams,
account_config: {
deployment_region: primaryRegion,
resource_groups: resourceGroups,
},
});
};
const handleFetchDeploymentCommand = async ({
primaryRegion,
resourceGroups,
}: {
primaryRegion: string;
resourceGroups: string[];
}): Promise<void> => {
const deploymentCommands = await getDeploymentCommand({
primaryRegion,
resourceGroups,
});
setDeploymentCommands(deploymentCommands);
if (deploymentCommands.account_id) {
setAccountId(deploymentCommands.account_id);
}
setIsFetchingDeploymentCommand(false);
};
return (
<div className="azure-account-configure-agent-step">
<AzureAccountForm
selectedAccount={null}
connectionParams={connectionParams}
isConnectionParamsLoading={isConnectionParamsLoading}
onSubmit={handleFetchDeploymentCommand}
isLoading={isFetchingDeploymentCommand}
/>
</div>
);
};
const DeployAgentStep = ({
deploymentCommands,
accountId,
onAccountConnected,
}: {
deploymentCommands: IAzureDeploymentCommands | null;
accountId: string | null;
onAccountConnected: () => void;
}): JSX.Element => {
const [showConnectionStatus, setShowConnectionStatus] = useState(false);
const [isAccountConnected, setIsAccountConnected] = useState(false);
const COMMAND_PLACEHOLDER =
'// Select Primary Region and Resource Groups to fetch the deployment commands\n';
const handleCopyDeploymentCommand = (): void => {
setShowConnectionStatus(true);
};
const startTimeRef = useRef(Date.now());
const refetchInterval = 10 * 1000;
const errorTimeout = 10 * 60 * 1000;
useGetAccountStatus(INTEGRATION_TYPES.AZURE, accountId ?? undefined, {
refetchInterval,
enabled: !!accountId,
onSuccess: (data: AccountStatusResponse) => {
if (data.data.status.integration.last_heartbeat_ts_ms !== null) {
setIsAccountConnected(true);
setShowConnectionStatus(true);
onAccountConnected();
// setModalState(ModalStateEnum.SUCCESS);
toast.success('Azure Integration: Account connected', {
description: 'Azure Integration: Account connected',
position: 'top-right',
duration: 3000,
});
logEvent('Azure Integration: Account connected', {
cloudAccountId: data?.data?.cloud_account_id,
status: data?.data?.status,
});
} else if (Date.now() - startTimeRef.current >= errorTimeout) {
// setModalState(ModalStateEnum.ERROR);
toast.error('Azure Integration: Account connection attempt timed out', {
description: 'Azure Integration: Account connection attempt timed out',
position: 'top-right',
duration: 3000,
});
logEvent('Azure Integration: Account connection attempt timed out', {
id: deploymentCommands?.account_id,
});
}
},
onError: () => {
toast.error('Azure Integration: Account connection attempt timed out', {
description: 'Azure Integration: Account connection attempt timed out',
position: 'top-right',
duration: 3000,
});
},
});
useEffect(() => {
if (
deploymentCommands &&
(deploymentCommands.az_shell_connection_command ||
deploymentCommands.az_cli_connection_command)
) {
setTimeout(() => {
setShowConnectionStatus(true);
}, 3000);
}
}, [deploymentCommands]);
return (
<div className="azure-account-deploy-agent-step">
<div className="azure-account-deploy-agent-step-subtitle">
Copy the command and then use it to create the deployment stack.
</div>
<div className="azure-account-deploy-agent-step-commands">
<Tabs
className="azure-account-deploy-agent-step-commands-tabs"
defaultValue="azure-shell"
items={[
{
key: 'azure-shell',
label: 'Azure Shell',
children: (
<div className="azure-account-deploy-agent-step-commands-tabs-content">
<CodeBlock
language="typescript"
value={
deploymentCommands?.az_shell_connection_command ||
COMMAND_PLACEHOLDER
}
onCopy={handleCopyDeploymentCommand}
/>
</div>
),
},
{
key: 'azure-sdk',
label: 'Azure SDK',
children: (
<div className="azure-account-deploy-agent-step-commands-tabs-content">
<CodeBlock
language="typescript"
value={
deploymentCommands?.az_cli_connection_command || COMMAND_PLACEHOLDER
}
onCopy={handleCopyDeploymentCommand}
/>
</div>
),
},
]}
variant="primary"
/>
</div>
{showConnectionStatus && (
<div className="azure-account-connection-status-container">
<div className="azure-account-connection-status-content">
<Callout
className="azure-account-connection-status-callout"
type={
isAccountConnected ? ConnectionSuccess.type : ConnectionWarning.type
}
size="small"
showIcon
message={
isAccountConnected ? ConnectionSuccess.title : ConnectionWarning.title
}
/>
</div>
<div className="azure-account-connection-status-close-disclosure">
{isAccountConnected
? ConnectionSuccess.description
: ConnectionWarning.description}
</div>
</div>
)}
</div>
);
};
export default function ConnectNewAzureAccount({
connectionParams,
isConnectionParamsLoading,
onAccountConnected,
}: ConnectNewAzureAccountProps): JSX.Element {
const [
deploymentCommands,
setDeploymentCommands,
] = useState<IAzureDeploymentCommands | null>(null);
const [accountId, setAccountId] = useState<string | null>(null);
const steps = useMemo(() => {
const steps: StepsProps['items'] = [
{
title: 'Prerequisites',
description: <PrerequisitesStep />,
},
{
title: 'Configure Agent',
description: (
<ConfigureAgentStep
connectionParams={connectionParams}
isConnectionParamsLoading={isConnectionParamsLoading}
setDeploymentCommands={setDeploymentCommands}
setAccountId={setAccountId}
/>
),
},
{
title: 'Deploy Agent',
description: (
<DeployAgentStep
deploymentCommands={deploymentCommands}
accountId={accountId}
onAccountConnected={onAccountConnected}
/>
),
},
];
return steps;
}, [
connectionParams,
isConnectionParamsLoading,
deploymentCommands,
accountId,
onAccountConnected,
]);
return (
<div className="azure-account-container">
<Steps direction="vertical" current={1} items={steps} />
</div>
);
}

View File

@@ -1,88 +0,0 @@
import { useCallback } from 'react';
import { toast } from '@signozhq/sonner';
import { CloudintegrationtypesCredentialsDTO } from 'api/generated/services/sigNoz.schemas';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import { CloudAccount } from 'container/Integrations/types';
import { useUpdateAccountConfig } from 'hooks/integration/useUpdateAccountConfig';
import { AzureAccountConfig } from 'types/api/integrations/types';
import { AzureAccountForm } from './AzureAccountForm';
import './AzureAccount.styles.scss';
interface EditAzureAccountProps {
selectedAccount: CloudAccount;
connectionParams: CloudintegrationtypesCredentialsDTO;
isConnectionParamsLoading: boolean;
onAccountUpdated: () => void;
}
function EditAzureAccount({
selectedAccount,
connectionParams,
isConnectionParamsLoading,
onAccountUpdated,
}: EditAzureAccountProps): JSX.Element {
const {
mutate: updateAzureAccountConfig,
isLoading,
} = useUpdateAccountConfig();
const handleSubmit = useCallback(
async ({
primaryRegion,
resourceGroups,
}: {
primaryRegion: string;
resourceGroups: string[];
}): Promise<void> => {
try {
const payload: AzureAccountConfig = {
config: {
deployment_region: primaryRegion,
resource_groups: resourceGroups,
},
};
updateAzureAccountConfig(
{
cloudServiceId: INTEGRATION_TYPES.AZURE,
accountId: selectedAccount?.id,
payload,
},
{
onSuccess: () => {
toast.success('Success', {
description: 'Azure account updated successfully',
position: 'top-right',
duration: 3000,
});
onAccountUpdated();
},
},
);
} catch (error) {
console.error('Form submission failed:', error);
}
},
[updateAzureAccountConfig, selectedAccount?.id, onAccountUpdated],
);
return (
<div className="azure-account-container">
<AzureAccountForm
mode="edit"
selectedAccount={selectedAccount}
connectionParams={connectionParams}
isConnectionParamsLoading={isConnectionParamsLoading}
onSubmit={handleSubmit}
isLoading={isLoading}
submitButtonText="Save Changes"
showDisconnectAccountButton
/>
</div>
);
}
export default EditAzureAccount;

View File

@@ -1,201 +0,0 @@
.azure-service-details-container {
display: flex;
flex-direction: column;
width: 100%;
.azure-service-details-tabs {
margin-top: 8px;
// remove the padding left from the first div of the tabs component
// this needs to be handled in the tabs component
> div:first-child {
padding-left: 0;
}
.azure-service-details-data-collected-content-logs,
.azure-service-details-data-collected-content-metrics {
display: flex;
flex-direction: row;
gap: 8px;
.azure-service-details-data-collected-content-title {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
color: var(--l2-foreground);
/* Bifrost (Ancient)/Content/sm */
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
.azure-service-details-overview {
display: flex;
flex-direction: column;
gap: 16px;
.azure-service-details-overview-configuration {
display: flex;
flex-direction: column;
border-radius: 4px;
border: 1px solid var(--l3-background);
background: var(--l1-background);
.azure-service-details-overview-configuration-metrics {
border-top: 1px solid var(--l3-background);
}
.azure-service-details-overview-configuration-title {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
border-radius: 4px 4px 0 0;
border-bottom: 1px solid var(--l3-background);
padding: 8px 12px;
.azure-service-details-overview-configuration-title-text {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
color: var(--l1-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
}
.configuration-action {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
}
.azure-service-details-overview-configuration-content {
display: flex;
flex-direction: column;
gap: 16px;
padding: 12px;
background: var(--l1-background);
.azure-service-details-overview-configuration-content-item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 8px;
}
}
.azure-service-details-overview-configuration-actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
gap: 8px;
padding: 8px 12px;
border-top: 1px solid var(--l3-background);
background: var(--l1-background);
}
.azure-service-details-overview-configuration-title-text-select-all {
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 138.462% */
}
}
.azure-service-details-actions {
display: flex;
flex-direction: row;
gap: 8px;
padding: 12px 0;
}
.azure-service-dashboards {
border-radius: 6px;
border: 1px solid var(--l3-background);
background: var(--l1-background);
.azure-service-dashboards-title {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
color: var(--l2-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
padding: 8px 16px;
border-bottom: 1px solid var(--l3-background);
}
.azure-service-dashboards-items {
display: flex;
flex-direction: column;
gap: 16px;
.azure-service-dashboard-item {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px 16px 12px 16px;
&.azure-service-dashboard-item-clickable {
cursor: pointer;
&:hover {
background-color: var(--l1-background);
}
}
.azure-service-dashboard-item-title {
color: var(--l1-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.azure-service-dashboard-item-description {
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
}
}
}
}
}
}
}

View File

@@ -1,485 +0,0 @@
import { useEffect, useState } from 'react';
import { useQueryClient } from 'react-query';
import { Button } from '@signozhq/button';
import { Color } from '@signozhq/design-tokens';
import Tabs from '@signozhq/tabs';
import { Checkbox, Popover, Skeleton } from 'antd';
import { useGetService } from 'api/generated/services/cloudintegration';
import type { CloudintegrationtypesServiceMetadataDTO } from 'api/generated/services/sigNoz.schemas';
import CloudServiceDataCollected from 'components/CloudIntegrations/CloudServiceDataCollected/CloudServiceDataCollected';
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import type { AzureConfig, ServiceData } from 'container/Integrations/types';
import { useUpdateServiceConfig } from 'hooks/integration/useUpdateServiceConfig';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { isEqual } from 'lodash-es';
import { CheckCircle, Save, TriangleAlert, X } from 'lucide-react';
import { ConfigConnectionStatus } from '../../ConfigConnectionStatus/ConfigConnectionStatus';
import './AzureServiceDetails.styles.scss';
interface AzureServiceDetailsProps {
selectedService: CloudintegrationtypesServiceMetadataDTO | null;
cloudAccountId: string;
}
function configToMap(
config: AzureConfig[] | undefined,
): { [key: string]: boolean } {
return (config || []).reduce(
(acc: { [key: string]: boolean }, item: AzureConfig) => {
acc[item.name] = item.enabled;
return acc;
},
{},
);
}
export default function AzureServiceDetails({
selectedService,
cloudAccountId,
}: AzureServiceDetailsProps): JSX.Element {
const queryClient = useQueryClient();
const { safeNavigate } = useSafeNavigate();
const isReadOnly = !cloudAccountId;
const serviceQueryParams = cloudAccountId
? { cloud_integration_id: cloudAccountId }
: undefined;
const {
data: serviceDetailsResponse,
isLoading,
refetch: refetchServiceDetails,
} = useGetService(
{
cloudProvider: INTEGRATION_TYPES.AZURE,
serviceId: selectedService?.id || '',
},
{
...serviceQueryParams,
},
{
query: {
enabled: !!selectedService?.id,
},
},
);
const serviceDetailsData = serviceDetailsResponse?.data as
| ServiceData
| undefined;
const {
mutate: updateAzureServiceConfig,
isLoading: isUpdating,
} = useUpdateServiceConfig();
// Last saved/committed config — updated when data loads and on save success.
// Used for hasChanges and Discard so buttons hide immediately after save.
const [lastSavedSnapshot, setLastSavedSnapshot] = useState<{
logs: { [key: string]: boolean };
metrics: { [key: string]: boolean };
}>({ logs: {}, metrics: {} });
// Editable state
const [azureLogsEnabledAll, setAzureLogsEnabledAll] = useState<boolean>(false);
const [azureMetricsEnabledAll, setAzureMetricsEnabledAll] = useState<boolean>(
false,
);
const [logsConfig, updateLogsConfig] = useState<{ [key: string]: boolean }>(
{},
);
const [metricsConfigs, updateMetricsConfigs] = useState<{
[key: string]: boolean;
}>({});
// Sync state when serviceDetailsData loads
useEffect(() => {
if (!serviceDetailsData?.config) {
return;
}
const logs = configToMap(serviceDetailsData.config.logs as AzureConfig[]);
const metrics = configToMap(
serviceDetailsData.config.metrics as AzureConfig[],
);
if (Object.keys(logs).length > 0) {
updateLogsConfig(logs);
setAzureLogsEnabledAll(
!(serviceDetailsData.config.logs as AzureConfig[])?.some(
(log: AzureConfig) => !log.enabled,
),
);
}
if (Object.keys(metrics).length > 0) {
updateMetricsConfigs(metrics);
setAzureMetricsEnabledAll(
!(serviceDetailsData.config.metrics as AzureConfig[])?.some(
(metric: AzureConfig) => !metric.enabled,
),
);
}
setLastSavedSnapshot({ logs, metrics });
}, [serviceDetailsData]);
const hasChanges =
!isEqual(logsConfig, lastSavedSnapshot.logs) ||
!isEqual(metricsConfigs, lastSavedSnapshot.metrics);
const handleSave = (): void => {
if (!selectedService?.id || isReadOnly) {
return;
}
updateAzureServiceConfig(
{
cloudServiceId: INTEGRATION_TYPES.AZURE,
serviceId: selectedService?.id,
payload: {
cloud_account_id: cloudAccountId,
config: {
logs: Object.entries(logsConfig).map(([name, enabled]) => ({
name,
enabled,
})),
metrics: Object.entries(metricsConfigs).map(([name, enabled]) => ({
name,
enabled,
})),
},
},
},
{
onSuccess: (_, variables) => {
// Update snapshot immediately from what we saved (not current state)
const saved = variables.payload.config;
setLastSavedSnapshot({
logs: configToMap(saved.logs),
metrics: configToMap(saved.metrics),
});
queryClient.invalidateQueries([
REACT_QUERY_KEY.AWS_SERVICE_DETAILS,
selectedService?.id,
cloudAccountId,
]);
// Invalidate services list so Enabled/Not Enabled stays in sync
queryClient.invalidateQueries([INTEGRATION_TYPES.AZURE]);
refetchServiceDetails();
},
},
);
};
const handleDiscard = (): void => {
updateLogsConfig(lastSavedSnapshot.logs);
updateMetricsConfigs(lastSavedSnapshot.metrics);
setAzureLogsEnabledAll(
Object.values(lastSavedSnapshot.logs).every(Boolean) &&
Object.keys(lastSavedSnapshot.logs).length > 0,
);
setAzureMetricsEnabledAll(
Object.values(lastSavedSnapshot.metrics).every(Boolean) &&
Object.keys(lastSavedSnapshot.metrics).length > 0,
);
};
const handleAzureLogsEnableAllChange = (checked: boolean): void => {
setAzureLogsEnabledAll(checked);
updateLogsConfig((prev) =>
Object.fromEntries(Object.keys(prev).map((key) => [key, checked])),
);
};
const handleAzureMetricsEnableAllChange = (checked: boolean): void => {
setAzureMetricsEnabledAll(checked);
updateMetricsConfigs((prev) =>
Object.fromEntries(Object.keys(prev).map((key) => [key, checked])),
);
};
const handleAzureLogsEnabledChange = (
logName: string,
checked: boolean,
): void => {
updateLogsConfig((prev) => ({ ...prev, [logName]: checked }));
};
const handleAzureMetricsEnabledChange = (
metricName: string,
checked: boolean,
): void => {
updateMetricsConfigs((prev) => ({ ...prev, [metricName]: checked }));
};
// Keep "enable all" in sync when individual items change
useEffect(() => {
if (Object.keys(logsConfig).length > 0) {
const allEnabled = Object.values(logsConfig).every(Boolean);
setAzureLogsEnabledAll(allEnabled);
}
}, [logsConfig]);
useEffect(() => {
if (Object.keys(metricsConfigs).length > 0) {
const allEnabled = Object.values(metricsConfigs).every(Boolean);
setAzureMetricsEnabledAll(allEnabled);
}
}, [metricsConfigs]);
// eslint-disable-next-line sonarjs/cognitive-complexity
const renderOverview = (): JSX.Element => {
const dashboards = serviceDetailsData?.assets?.dashboards || [];
const isServiceConfigEnabled =
Object.values(logsConfig).some(Boolean) ||
Object.values(metricsConfigs).some(Boolean);
const logsStatus = serviceDetailsData?.status?.logs || null;
const metricsStatus = serviceDetailsData?.status?.metrics || null;
// connected if all config items are connected
const logsConnectionStatus = logsStatus?.every(
(log) => log.last_received_ts_ms > 0,
);
const metricsConnectionStatus = metricsStatus?.every(
(metric) => metric.last_received_ts_ms > 0,
);
if (isLoading) {
return (
<div className="azure-service-details-overview-loading">
<Skeleton active />
</div>
);
}
return (
<div className="azure-service-details-overview">
{!isLoading && (
<div className="azure-service-details-overview-configuration">
<div className="azure-service-details-overview-configuration-logs">
<div className="azure-service-details-overview-configuration-title">
<div className="azure-service-details-overview-configuration-title-text">
{logsStatus && (
<Popover
content={<ConfigConnectionStatus status={logsStatus} />}
trigger="hover"
placement="right"
overlayClassName="config-connection-status-popover"
>
<div className="aws-service-details-overview-configuration-title-text-icon">
{logsConnectionStatus ? (
<CheckCircle size={16} color={Color.BG_FOREST_500} />
) : (
<TriangleAlert size={16} color={Color.BG_AMBER_500} />
)}
</div>
</Popover>
)}
Azure Logs
</div>
<div className="configuration-action">
<Checkbox
checked={azureLogsEnabledAll}
indeterminate={
Object.values(logsConfig).some(Boolean) &&
!Object.values(logsConfig).every(Boolean)
}
onChange={(e): void =>
handleAzureLogsEnableAllChange(e.target.checked)
}
disabled={isUpdating || isReadOnly}
>
<span className="azure-service-details-overview-configuration-title-text-select-all">
Select All
</span>
</Checkbox>
</div>
</div>
<div className="azure-service-details-overview-configuration-content">
{logsConfig &&
Object.keys(logsConfig).length > 0 &&
Object.keys(logsConfig).map((logName: string) => (
<div
key={logName}
className="azure-service-details-overview-configuration-content-item"
>
<div className="azure-service-details-overview-configuration-content-item-text">
{logName}
</div>
<Checkbox
checked={logsConfig[logName]}
onChange={(e): void =>
handleAzureLogsEnabledChange(logName, e.target.checked)
}
disabled={isUpdating || isReadOnly}
/>
</div>
))}
</div>
</div>
<div className="azure-service-details-overview-configuration-metrics">
<div className="azure-service-details-overview-configuration-title">
<div className="azure-service-details-overview-configuration-title-text">
{metricsStatus && (
<Popover
content={<ConfigConnectionStatus status={metricsStatus} />}
trigger="hover"
placement="right"
overlayClassName="config-connection-status-popover"
>
<div className="aws-service-details-overview-configuration-title-text-icon">
{metricsConnectionStatus ? (
<CheckCircle size={16} color={Color.BG_FOREST_500} />
) : (
<TriangleAlert size={16} color={Color.BG_AMBER_500} />
)}
</div>
</Popover>
)}
Azure Metrics
</div>
<div className="configuration-action">
<Checkbox
checked={azureMetricsEnabledAll}
indeterminate={
Object.values(metricsConfigs).some(Boolean) &&
!Object.values(metricsConfigs).every(Boolean)
}
onChange={(e): void =>
handleAzureMetricsEnableAllChange(e.target.checked)
}
disabled={isUpdating || isReadOnly}
>
<span className="azure-service-details-overview-configuration-title-text-select-all">
Select All
</span>
</Checkbox>
</div>
</div>
<div className="azure-service-details-overview-configuration-content">
{metricsConfigs &&
Object.keys(metricsConfigs).length > 0 &&
Object.keys(metricsConfigs).map((metricName: string) => (
<div
key={metricName}
className="azure-service-details-overview-configuration-content-item"
>
<div className="azure-service-details-overview-configuration-content-item-text">
{metricName}
</div>
<Checkbox
checked={metricsConfigs[metricName]}
onChange={(e): void =>
handleAzureMetricsEnabledChange(metricName, e.target.checked)
}
disabled={isUpdating || isReadOnly}
/>
</div>
))}
</div>
</div>
{hasChanges && !isReadOnly && (
<div className="azure-service-details-overview-configuration-actions">
<Button
variant="solid"
color="secondary"
onClick={handleDiscard}
disabled={isUpdating}
size="xs"
prefixIcon={<X size={14} />}
>
Discard
</Button>
<Button
variant="solid"
color="primary"
onClick={handleSave}
loading={isUpdating}
disabled={isUpdating}
size="xs"
prefixIcon={<Save size={14} />}
>
Save
</Button>
</div>
)}
</div>
)}
<MarkdownRenderer
variables={{}}
markdownContent={serviceDetailsData?.overview}
/>
{dashboards.length > 0 && (
<div className="azure-service-dashboards">
<div className="azure-service-dashboards-title">Dashboards</div>
<div className="azure-service-dashboards-items">
{dashboards.map((dashboard) => (
<div
key={dashboard.id}
className={`azure-service-dashboard-item ${
!isReadOnly && isServiceConfigEnabled && dashboard.url
? 'azure-service-dashboard-item-clickable'
: ''
}`}
onClick={(): void => {
if (!isReadOnly && isServiceConfigEnabled && dashboard.url) {
safeNavigate(dashboard.url);
}
}}
>
<div className="azure-service-dashboard-item-title">
{dashboard.title}
</div>
<div className="azure-service-dashboard-item-description">
{dashboard.description}
</div>
</div>
))}
</div>
</div>
)}
</div>
);
};
const renderDataCollected = (): JSX.Element => {
return (
<div className="azure-service-details-data-collected-table">
<CloudServiceDataCollected
logsData={serviceDetailsData?.data_collected?.logs || []}
metricsData={serviceDetailsData?.data_collected?.metrics || []}
/>
</div>
);
};
return (
<div className="azure-service-details-container">
<Tabs
defaultValue="overview"
className="azure-service-details-tabs"
items={[
{
children: renderOverview(),
key: 'overview',
label: 'Overview',
},
{
children: renderDataCollected(),
key: 'data-collected',
label: 'Data Collected',
},
]}
variant="secondary"
/>
</div>
);
}

View File

@@ -1,182 +0,0 @@
.azure-services-container {
display: flex;
flex-direction: column;
height: calc(100% - 52px);
position: relative;
.azure-services-content {
height: 100%;
position: relative;
.azure-services-list-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 6px 10px;
height: 40px;
border-bottom: 1px solid var(--bg-slate-400);
.azure-services-views-btn-group {
border: 1px solid var(--bg-slate-400);
border-radius: 4px;
display: flex;
align-items: center;
.azure-services-views-btn {
&:first-child {
border-radius: 4px 0px 0px 4px;
}
&:last-child {
border-radius: 0px 4px 4px 0px;
}
&:hover {
background-color: var(--bg-slate-400);
}
&.active {
background-color: var(--bg-slate-400);
}
}
}
}
.azure-services-list-section {
height: 100%;
position: relative;
.azure-services-list-section-content {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
}
.azure-services-list-section-loading-skeleton {
display: flex;
flex-direction: row;
width: 100%;
.azure-services-list-section-loading-skeleton-sidebar {
width: 240px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 24px;
}
.azure-services-list-section-loading-skeleton-main {
flex: 1;
padding: 12px;
display: flex;
flex-direction: column;
gap: 24px;
}
}
.azure-services-list-view {
height: 100%;
.azure-services-list-view-sidebar {
width: 240px;
height: 100%;
border-right: 1px solid var(--l3-background);
padding: 12px;
.azure-services-list-view-sidebar-content {
display: flex;
flex-direction: column;
gap: 8px;
.azure-services-enabled {
display: flex;
flex-direction: column;
gap: 8px;
}
.azure-services-not-enabled {
display: flex;
flex-direction: column;
gap: 8px;
}
.azure-services-list-view-sidebar-content-header {
color: var(--l2-foreground);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
}
.azure-services-list-view-sidebar-content-item {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
.azure-services-list-view-sidebar-content-item-icon {
width: 20px;
height: 20px;
}
.azure-services-list-view-sidebar-content-item-title {
color: var(--l1-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
}
&:hover {
color: var(--l1-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
}
&.active {
color: var(--l1-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
background-color: var(--l3-background);
.azure-services-list-view-sidebar-content-item-title {
color: var(--l1-foreground);
}
}
}
}
}
.azure-services-list-view-main {
flex: 1;
padding: 12px;
}
}
.azure-services-grid-view {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
}
}
}

View File

@@ -1,72 +0,0 @@
import type { CloudintegrationtypesServiceMetadataDTO } from 'api/generated/services/sigNoz.schemas';
import cx from 'classnames';
interface AzureServicesListViewProps {
selectedService: CloudintegrationtypesServiceMetadataDTO | null;
enabledServices: CloudintegrationtypesServiceMetadataDTO[];
notEnabledServices: CloudintegrationtypesServiceMetadataDTO[];
onSelectService: (service: CloudintegrationtypesServiceMetadataDTO) => void;
}
export default function AzureServicesListView({
selectedService,
enabledServices,
notEnabledServices,
onSelectService,
}: AzureServicesListViewProps): JSX.Element {
const isEnabledServicesEmpty = enabledServices.length === 0;
const isNotEnabledServicesEmpty = notEnabledServices.length === 0;
const renderServiceItem = (
service: CloudintegrationtypesServiceMetadataDTO,
): JSX.Element => {
return (
<div
className={cx('azure-services-list-view-sidebar-content-item', {
active: service.id === selectedService?.id,
})}
key={service.id}
onClick={(): void => onSelectService(service)}
>
<img
src={service.icon}
alt={service.title}
className="azure-services-list-view-sidebar-content-item-icon"
/>
<div className="azure-services-list-view-sidebar-content-item-title">
{service.title}
</div>
</div>
);
};
return (
<div className="azure-services-list-view">
<div className="azure-services-list-view-sidebar">
<div className="azure-services-list-view-sidebar-content">
<div className="azure-services-enabled">
<div className="azure-services-list-view-sidebar-content-header">
Enabled
</div>
{enabledServices.map((service) => renderServiceItem(service))}
{isEnabledServicesEmpty && (
<div className="azure-services-list-view-sidebar-content-item-empty-message">
No enabled services
</div>
)}
</div>
{!isNotEnabledServicesEmpty && (
<div className="azure-services-not-enabled">
<div className="azure-services-list-view-sidebar-content-header">
Not Enabled
</div>
{notEnabledServices.map((service) => renderServiceItem(service))}
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,148 +0,0 @@
import { useEffect, useMemo, useState } from 'react';
import { Skeleton } from 'antd';
import { useListServicesMetadata } from 'api/generated/services/cloudintegration';
import type { CloudintegrationtypesServiceMetadataDTO } from 'api/generated/services/sigNoz.schemas';
import CloudIntegrationsHeader from 'components/CloudIntegrations/CloudIntegrationsHeader';
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
import { CloudAccount, IntegrationType } from 'container/Integrations/types';
import { useGetCloudIntegrationAccounts } from 'hooks/integration/useGetCloudIntegrationAccounts';
import useUrlQuery from 'hooks/useUrlQuery';
import { getAccountById } from '../utils';
import AzureServiceDetails from './AzureServiceDetails/AzureServiceDetails';
import AzureServicesListView from './AzureServicesListView';
import './AzureServices.styles.scss';
function AzureServices(): JSX.Element {
const urlQuery = useUrlQuery();
const [selectedAccount, setSelectedAccount] = useState<CloudAccount | null>(
null,
);
const [
selectedService,
setSelectedService,
] = useState<CloudintegrationtypesServiceMetadataDTO | null>(null);
const {
data: accounts = [],
isLoading: isLoadingAccounts,
isFetching: isFetchingAccounts,
refetch: refetchAccounts,
} = useGetCloudIntegrationAccounts(INTEGRATION_TYPES.AZURE);
const initialAccount = useMemo(
() =>
accounts?.length
? getAccountById(accounts, urlQuery.get('cloudAccountId') || '') ||
accounts[0]
: null,
[accounts, urlQuery],
);
// Sync selectedAccount with initialAccount when accounts load (enables Subscription ID display)
// Cast: hook returns AWS-typed CloudAccount[] but AZURE fetch returns Azure-shaped accounts
useEffect(() => {
setSelectedAccount(initialAccount as CloudAccount | null);
}, [initialAccount]);
const cloudAccountId = initialAccount?.cloud_account_id;
const serviceQueryParams = cloudAccountId
? { cloud_integration_id: cloudAccountId }
: undefined;
const {
data: servicesMetadata,
isLoading: isLoadingAzureServices,
} = useListServicesMetadata(
{ cloudProvider: INTEGRATION_TYPES.AZURE },
serviceQueryParams,
);
const azureServices = useMemo(() => servicesMetadata?.data?.services ?? [], [
servicesMetadata,
]);
const enabledServices = useMemo(
() => azureServices?.filter((service) => service.enabled) ?? [],
[azureServices],
);
// Derive from enabled to guarantee each service is in exactly one list
const enabledIds = useMemo(() => new Set(enabledServices.map((s) => s.id)), [
enabledServices,
]);
const notEnabledServices = useMemo(
() => azureServices?.filter((s) => !enabledIds.has(s.id)) ?? [],
[azureServices, enabledIds],
);
useEffect(() => {
const allServices = [...enabledServices, ...notEnabledServices];
// If a service is already selected and still exists in the refreshed list, keep it
if (selectedService && allServices.some((s) => s.id === selectedService.id)) {
// Update the selected service reference to the fresh object from the new list
const freshService = allServices.find((s) => s.id === selectedService.id);
if (freshService && freshService !== selectedService) {
setSelectedService(freshService);
}
return;
}
// No valid selection — pick a default
if (enabledServices.length > 0) {
setSelectedService(enabledServices[0]);
} else if (notEnabledServices.length > 0) {
setSelectedService(notEnabledServices[0]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [enabledServices, notEnabledServices]);
return (
<div className="azure-services-container">
<CloudIntegrationsHeader
cloudServiceId={IntegrationType.AZURE_SERVICES}
selectedAccount={selectedAccount}
accounts={accounts}
isLoadingAccounts={isLoadingAccounts}
onSelectAccount={setSelectedAccount}
refetchAccounts={refetchAccounts}
/>
<div className="azure-services-content">
<div className="azure-services-list-section">
{(isLoadingAzureServices || isFetchingAccounts) && (
<div className="azure-services-list-section-loading-skeleton">
<div className="azure-services-list-section-loading-skeleton-sidebar">
<Skeleton active />
<Skeleton active />
</div>
<div className="azure-services-list-section-loading-skeleton-main">
<Skeleton active />
<Skeleton active />
<Skeleton active />
</div>
</div>
)}
{!isLoadingAzureServices && !isFetchingAccounts && (
<div className="azure-services-list-section-content">
<AzureServicesListView
selectedService={selectedService}
enabledServices={enabledServices}
notEnabledServices={notEnabledServices}
onSelectService={setSelectedService}
/>
<AzureServiceDetails
selectedService={selectedService}
cloudAccountId={selectedAccount?.cloud_account_id || ''}
/>
</div>
)}
</div>
</div>
</div>
);
}
export default AzureServices;

View File

@@ -1,7 +1,6 @@
import { IntegrationType } from 'container/Integrations/types';
import AWSTabs from './AmazonWebServices/ServicesTabs';
import AzureServices from './AzureServices';
import Header from './Header/Header';
import './CloudIntegration.styles.scss';
@@ -12,7 +11,6 @@ const CloudIntegration = ({ type }: { type: IntegrationType }): JSX.Element => {
<Header title={type} />
{type === IntegrationType.AWS_SERVICES && <AWSTabs />}
{type === IntegrationType.AZURE_SERVICES && <AzureServices />}
</div>
);
};

View File

@@ -61,19 +61,8 @@ function IntegrationDetailPage(): JSX.Element {
),
);
if (
integrationId === INTEGRATION_TYPES.AWS ||
integrationId === INTEGRATION_TYPES.AZURE
) {
return (
<CloudIntegration
type={
integrationId === INTEGRATION_TYPES.AWS
? IntegrationType.AWS_SERVICES
: IntegrationType.AZURE_SERVICES
}
/>
);
if (integrationId === INTEGRATION_TYPES.AWS) {
return <CloudIntegration type={IntegrationType.AWS_SERVICES} />;
}
return (

View File

@@ -15,5 +15,5 @@ export const useGetIntegrationStatus = ({
useQuery<AxiosResponse<GetIntegrationStatusProps>, AxiosError>({
queryKey: ['integration-connection-status', integrationId],
queryFn: () => getIntegrationStatus({ integrationId }),
refetchInterval: 500000,
refetchInterval: 5000,
});

View File

@@ -134,7 +134,9 @@ export function useAccountSettingsModal({
}, [form, selectedRegions, updateAccount, account, setActiveAccount, onClose]);
const isSaveDisabled = useMemo(
() => isEqual(selectedRegions.sort(), accountRegions.sort()),
() =>
isEqual(selectedRegions.sort(), accountRegions.sort()) ||
selectedRegions.length === 0,
[selectedRegions, accountRegions],
);

View File

@@ -6,6 +6,7 @@ import {
useMemo,
useState,
} from 'react';
import { useQueryClient } from 'react-query';
import { toast } from '@signozhq/sonner';
import { Form, FormInstance } from 'antd';
import {
@@ -18,6 +19,7 @@ import {
CloudintegrationtypesCredentialsDTO,
CloudintegrationtypesPostableAccountDTO,
} from 'api/generated/services/sigNoz.schemas';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
ActiveViewEnum,
ModalStateEnum,
@@ -53,11 +55,18 @@ interface UseIntegrationModal {
handleRegionChange: (value: string) => void;
connectionParams?: CloudintegrationtypesCredentialsDTO;
isConnectionParamsLoading: boolean;
handleConnectionSuccess: (payload: {
cloudAccountId: string;
status?: unknown;
}) => void;
handleConnectionTimeout: (payload: { id?: string }) => void;
handleConnectionError: () => void;
}
export function useIntegrationModal({
onClose,
}: UseIntegrationModalProps): UseIntegrationModal {
const queryClient = useQueryClient();
const [form] = Form.useForm();
const [modalState, setModalState] = useState<ModalStateEnum>(
ModalStateEnum.FORM,
@@ -97,6 +106,35 @@ export function useIntegrationModal({
onClose();
}, [onClose]);
const handleConnectionSuccess = useCallback(
(payload: { cloudAccountId: string; status?: unknown }): void => {
logEvent('AWS Integration: Account connected', {
cloudAccountId: payload.cloudAccountId,
status: payload.status,
});
toast.success('AWS account connected successfully', {
position: 'bottom-right',
});
void queryClient.invalidateQueries([REACT_QUERY_KEY.AWS_ACCOUNTS]);
handleClose();
},
[handleClose, queryClient],
);
const handleConnectionTimeout = useCallback(
(payload: { id?: string }): void => {
setModalState(ModalStateEnum.ERROR);
logEvent('AWS Integration: Account connection attempt timed out', {
id: payload.id,
});
},
[],
);
const handleConnectionError = useCallback((): void => {
setModalState(ModalStateEnum.ERROR);
}, []);
const { mutate: generateUrl, isLoading: isGeneratingUrl } = useCreateAccount();
const handleError = useAxiosError();
@@ -199,5 +237,8 @@ export function useIntegrationModal({
| CloudintegrationtypesCredentialsDTO
| undefined,
isConnectionParamsLoading,
handleConnectionSuccess,
handleConnectionTimeout,
handleConnectionError,
};
}

View File

@@ -1,40 +0,0 @@
import { useQuery, UseQueryResult } from 'react-query';
import { listAccounts } from 'api/generated/services/cloudintegration';
import { CloudintegrationtypesAccountDTO } from 'api/generated/services/sigNoz.schemas';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { CloudAccount } from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
function mapAccountDTOToCloudAccount(
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,
},
},
};
}
export function useGetCloudIntegrationAccounts(
cloudServiceId: string,
): UseQueryResult<CloudAccount[]> {
return useQuery(
[REACT_QUERY_KEY.CLOUD_INTEGRATION_ACCOUNTS, cloudServiceId],
async () => {
const response = await listAccounts({ cloudProvider: cloudServiceId });
return response.data.accounts
.map(mapAccountDTOToCloudAccount)
.filter((account): account is CloudAccount => account !== null);
},
);
}

View File

@@ -1,27 +0,0 @@
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { CloudintegrationtypesCredentialsDTO } from 'api/generated/services/sigNoz.schemas';
import { getAzureDeploymentCommands } from 'api/integration';
import { AxiosError } from 'axios';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { AzureCloudAccountConfig } from 'container/Integrations/types';
import { IAzureDeploymentCommands } from 'types/api/integrations/types';
export function useGetDeploymentCommands({
azureAccountConfig,
connectionParams,
options,
}: {
azureAccountConfig: AzureCloudAccountConfig;
connectionParams: CloudintegrationtypesCredentialsDTO;
options?: UseQueryOptions<IAzureDeploymentCommands, AxiosError>;
}): UseQueryResult<IAzureDeploymentCommands, AxiosError> {
return useQuery<IAzureDeploymentCommands, AxiosError>(
[REACT_QUERY_KEY.CLOUD_INTEGRATION_GET_DEPLOYMENT_COMMANDS],
() =>
getAzureDeploymentCommands({
agent_config: connectionParams,
account_config: azureAccountConfig,
}),
options,
);
}

View File

@@ -1,19 +0,0 @@
import { useQuery, UseQueryResult } from 'react-query';
import { getCloudIntegrationServiceDetails } from 'api/integration';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { ServiceData } from 'container/Integrations/CloudIntegration/AmazonWebServices/types';
export function useGetCloudIntegrationServiceDetails(
cloudServiceId: string,
serviceId: string,
cloudAccountId?: string,
): UseQueryResult<ServiceData> {
return useQuery(
[REACT_QUERY_KEY.AWS_SERVICE_DETAILS, serviceId, cloudAccountId],
() =>
getCloudIntegrationServiceDetails(cloudServiceId, serviceId, cloudAccountId),
{
enabled: !!serviceId,
},
);
}

View File

@@ -1,24 +0,0 @@
import { useMutation, UseMutationResult } from 'react-query';
import { updateAccountConfig } from 'api/integration';
import {
AccountConfigResponse,
AWSAccountConfigPayload,
} from 'types/api/integrations/aws';
import { AzureAccountConfig } from 'types/api/integrations/types';
interface UpdateConfigVariables {
cloudServiceId: string;
accountId: string;
payload: AWSAccountConfigPayload | AzureAccountConfig;
}
export function useUpdateAccountConfig(): UseMutationResult<
AccountConfigResponse,
Error,
UpdateConfigVariables
> {
return useMutation<AccountConfigResponse, Error, UpdateConfigVariables>({
mutationFn: ({ cloudServiceId, accountId, payload }) =>
updateAccountConfig(cloudServiceId, accountId, payload),
});
}

View File

@@ -1,22 +0,0 @@
import { useMutation, UseMutationResult } from 'react-query';
import { updateServiceConfig } from 'api/integration';
import { AzureServiceConfigPayload } from 'container/Integrations/types';
// }
interface UpdateConfigVariables {
cloudServiceId: string;
serviceId: string;
payload: AzureServiceConfigPayload;
}
export function useUpdateServiceConfig(): UseMutationResult<
AzureServiceConfigPayload,
Error,
UpdateConfigVariables
> {
return useMutation<AzureServiceConfigPayload, Error, UpdateConfigVariables>({
mutationFn: ({ cloudServiceId, serviceId, payload }) =>
updateServiceConfig(cloudServiceId, serviceId, payload),
});
}