AWS Integration changes (#7025)

* fix: update AWS accounts API response to return accounts list

* feat: display skeleton UI for account actions and refactored rendering logic

* chore: update AWS service naming from "AWS Web Services" to "Amazon Web Services"

* feat: aws integration success modal changes

* feat: auto-select first service when no service is active

* feat: display 'enable service' if service hasn't been configured and 'Configure (x/2)' if configured

* fix: display no data yet if status is not available

* feat: properly handle remove integration account flow

* fix: rename accountId param to cloudAccountId

* fix: update the aws service list and details api parameter from account_id to cloud_account_id

* fix: fix the issue of stale service config modal enabled/disabled state

* chore: improve the UI of configure button

* feat: add connection parameters support for AWS cloud integration

* feat: add optional link support for cloud service dashboards

* fix: get the correct supported signals count + a minor refactoring

* fix: remove cloudAccountId on success of account remove

* chore: update the remove integration copy

* refactor: add react query key for AWS connection parameters

* fix: correct typo in integration loading state variable name

* refactor: move skeleton inline styles to style file and do overall refactoring

* chore: address the requested changes
This commit is contained in:
Shaheer Kochai 2025-02-06 15:13:19 +04:30 committed by GitHub
parent b215c6a0ce
commit c3164912e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 620 additions and 190 deletions

View File

@ -0,0 +1,19 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
const removeAwsIntegrationAccount = async (
accountId: string,
): Promise<SuccessResponse<Record<string, never>> | ErrorResponse> => {
const response = await axios.post(
`/cloud-integrations/aws/accounts/${accountId}/disconnect`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default removeAwsIntegrationAccount;

View File

@ -9,19 +9,22 @@ import {
import { import {
AccountConfigPayload, AccountConfigPayload,
AccountConfigResponse, AccountConfigResponse,
ConnectionParams,
ConnectionUrlResponse, ConnectionUrlResponse,
} from 'types/api/integrations/aws'; } from 'types/api/integrations/aws';
export const getAwsAccounts = async (): Promise<CloudAccount[]> => { export const getAwsAccounts = async (): Promise<CloudAccount[]> => {
const response = await axios.get('/cloud-integrations/aws/accounts'); const response = await axios.get('/cloud-integrations/aws/accounts');
return response.data.data; return response.data.data.accounts;
}; };
export const getAwsServices = async ( export const getAwsServices = async (
accountId?: string, cloudAccountId?: string,
): Promise<Service[]> => { ): Promise<Service[]> => {
const params = accountId ? { account_id: accountId } : undefined; const params = cloudAccountId
? { cloud_account_id: cloudAccountId }
: undefined;
const response = await axios.get('/cloud-integrations/aws/services', { const response = await axios.get('/cloud-integrations/aws/services', {
params, params,
}); });
@ -31,9 +34,11 @@ export const getAwsServices = async (
export const getServiceDetails = async ( export const getServiceDetails = async (
serviceId: string, serviceId: string,
accountId?: string, cloudAccountId?: string,
): Promise<ServiceData> => { ): Promise<ServiceData> => {
const params = accountId ? { account_id: accountId } : undefined; const params = cloudAccountId
? { cloud_account_id: cloudAccountId }
: undefined;
const response = await axios.get( const response = await axios.get(
`/cloud-integrations/aws/services/${serviceId}`, `/cloud-integrations/aws/services/${serviceId}`,
{ params }, { params },
@ -74,3 +79,10 @@ export const updateServiceConfig = async (
); );
return response.data; return response.data;
}; };
export const getConnectionParams = async (): Promise<ConnectionParams> => {
const response = await axios.get(
'/cloud-integrations/aws/accounts/generate-connection-params',
);
return response.data.data;
};

View File

@ -41,5 +41,6 @@ export const REACT_QUERY_KEY = {
AWS_UPDATE_ACCOUNT_CONFIG: 'AWS_UPDATE_ACCOUNT_CONFIG', AWS_UPDATE_ACCOUNT_CONFIG: 'AWS_UPDATE_ACCOUNT_CONFIG',
AWS_UPDATE_SERVICE_CONFIG: 'AWS_UPDATE_SERVICE_CONFIG', AWS_UPDATE_SERVICE_CONFIG: 'AWS_UPDATE_SERVICE_CONFIG',
AWS_GENERATE_CONNECTION_URL: 'AWS_GENERATE_CONNECTION_URL', AWS_GENERATE_CONNECTION_URL: 'AWS_GENERATE_CONNECTION_URL',
AWS_GET_CONNECTION_PARAMS: 'AWS_GET_CONNECTION_PARAMS',
GET_ATTRIBUTE_VALUES: 'GET_ATTRIBUTE_VALUES', GET_ATTRIBUTE_VALUES: 'GET_ATTRIBUTE_VALUES',
}; };

View File

@ -24,7 +24,9 @@ function Header(): JSX.Element {
}, },
{ {
title: ( title: (
<div className="cloud-header__breadcrumb-title">AWS web services</div> <div className="cloud-header__breadcrumb-title">
Amazon Web Services
</div>
), ),
}, },
]} ]}

View File

@ -21,7 +21,7 @@ function HeroSection(): JSX.Element {
<img src="/Logos/aws-dark.svg" alt="aws-logo" /> <img src="/Logos/aws-dark.svg" alt="aws-logo" />
</div> </div>
<div className="hero-section__details"> <div className="hero-section__details">
<div className="title">AWS Web Services</div> <div className="title">Amazon Web Services</div>
<div className="description"> <div className="description">
One-click setup for AWS monitoring with SigNoz One-click setup for AWS monitoring with SigNoz
</div> </div>

View File

@ -1,41 +1,56 @@
.hero-section__actions { .hero-section {
margin-top: 12px; &__actions {
margin-top: 12px;
&-with-account { &-with-account {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
} }
}
.hero-section__action-buttons {
display: flex;
align-items: center;
gap: 8px;
}
.hero-section__action-button {
font-family: 'Inter';
border-radius: 2px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
line-height: 16px;
padding: 8px 17px;
&.primary {
background: var(--bg-robin-500);
border: none;
color: var(--bg-vanilla-100);
} }
&.secondary { &__input-skeleton {
width: 300px;
margin-bottom: 16px;
}
&__new-account-button-skeleton {
width: 180px;
margin-right: 8px;
}
&__account-settings-button-skeleton {
width: 140px;
}
&__action-buttons {
display: flex; display: flex;
align-items: center; align-items: center;
border: 1px solid var(--bg-ink-300); gap: 8px;
color: var(--bg-vanilla-100); }
&__action-button {
font-family: 'Inter';
border-radius: 2px; border-radius: 2px;
background: var(--bg-slate-400); cursor: pointer;
box-shadow: none; font-size: 12px;
font-weight: 500;
line-height: 16px;
padding: 8px 17px;
&.primary {
background: var(--bg-robin-500);
border: none;
color: var(--bg-vanilla-100);
}
&.secondary {
display: flex;
align-items: center;
border: 1px solid var(--bg-ink-300);
color: var(--bg-vanilla-100);
border-radius: 2px;
background: var(--bg-slate-400);
box-shadow: none;
}
} }
} }

View File

@ -1,7 +1,7 @@
import './AccountActions.style.scss'; import './AccountActions.style.scss';
import { Color } from '@signozhq/design-tokens'; import { Color } from '@signozhq/design-tokens';
import { Button, Select } from 'antd'; import { Button, Select, Skeleton } from 'antd';
import { SelectProps } from 'antd/lib'; import { SelectProps } from 'antd/lib';
import { useAwsAccounts } from 'hooks/integrations/aws/useAwsAccounts'; import { useAwsAccounts } from 'hooks/integrations/aws/useAwsAccounts';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
@ -53,15 +53,100 @@ const getAccountById = (
): CloudAccount | null => ): CloudAccount | null =>
accounts.find((account) => account.cloud_account_id === accountId) || null; accounts.find((account) => account.cloud_account_id === accountId) || null;
function AccountActionsRenderer({
accounts,
isLoading,
activeAccount,
selectOptions,
onAccountChange,
onIntegrationModalOpen,
onAccountSettingsModalOpen,
}: {
accounts: CloudAccount[] | undefined;
isLoading: boolean;
activeAccount: CloudAccount | null;
selectOptions: SelectProps['options'];
onAccountChange: (value: string) => void;
onIntegrationModalOpen: () => void;
onAccountSettingsModalOpen: () => void;
}): JSX.Element {
if (isLoading) {
return (
<div className="hero-section__actions-with-account">
<Skeleton.Input
active
size="large"
block
className="hero-section__input-skeleton"
/>
<div className="hero-section__action-buttons">
<Skeleton.Button
active
size="large"
className="hero-section__new-account-button-skeleton"
/>
<Skeleton.Button
active
size="large"
className="hero-section__account-settings-button-skeleton"
/>
</div>
</div>
);
}
if (accounts?.length) {
return (
<div className="hero-section__actions-with-account">
<Select
value={`Account: ${activeAccount?.cloud_account_id}`}
options={selectOptions}
rootClassName="cloud-account-selector"
placeholder="Select AWS Account"
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
optionRender={(option): JSX.Element =>
renderOption(option, activeAccount?.cloud_account_id)
}
onChange={onAccountChange}
/>
<div className="hero-section__action-buttons">
<Button
type="primary"
className="hero-section__action-button primary"
onClick={onIntegrationModalOpen}
>
Add New AWS Account
</Button>
<Button
type="default"
className="hero-section__action-button secondary"
onClick={onAccountSettingsModalOpen}
>
Account Settings
</Button>
</div>
</div>
);
}
return (
<Button
className="hero-section__action-button primary"
onClick={onIntegrationModalOpen}
>
Integrate Now
</Button>
);
}
function AccountActions(): JSX.Element { function AccountActions(): JSX.Element {
const urlQuery = useUrlQuery(); const urlQuery = useUrlQuery();
const navigate = useNavigate(); const navigate = useNavigate();
const { data: accounts } = useAwsAccounts(); const { data: accounts, isLoading } = useAwsAccounts();
const initialAccount = useMemo( const initialAccount = useMemo(
() => () =>
accounts?.length accounts?.length
? getAccountById(accounts, urlQuery.get('accountId') || '') || accounts[0] ? getAccountById(accounts, urlQuery.get('cloudAccountId') || '') ||
accounts[0]
: null, : null,
[accounts, urlQuery], [accounts, urlQuery],
); );
@ -74,7 +159,7 @@ function AccountActions(): JSX.Element {
useEffect(() => { useEffect(() => {
if (initialAccount !== null) { if (initialAccount !== null) {
setActiveAccount(initialAccount); setActiveAccount(initialAccount);
urlQuery.set('accountId', initialAccount.cloud_account_id); urlQuery.set('cloudAccountId', initialAccount.cloud_account_id);
navigate({ search: urlQuery.toString() }); navigate({ search: urlQuery.toString() });
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -98,60 +183,35 @@ function AccountActions(): JSX.Element {
return ( return (
<div className="hero-section__actions"> <div className="hero-section__actions">
{accounts?.length ? ( <AccountActionsRenderer
<div className="hero-section__actions-with-account"> accounts={accounts}
<Select isLoading={isLoading}
value={`Account: ${activeAccount?.cloud_account_id}`} activeAccount={activeAccount}
options={selectOptions} selectOptions={selectOptions}
rootClassName="cloud-account-selector" onAccountChange={(value): void => {
placeholder="Select AWS Account" if (accounts) {
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />} setActiveAccount(getAccountById(accounts, value));
optionRender={(option): JSX.Element => urlQuery.set('cloudAccountId', value);
renderOption(option, activeAccount?.cloud_account_id) navigate({ search: urlQuery.toString() });
} }
onChange={(value): void => { }}
setActiveAccount(getAccountById(accounts, value)); onIntegrationModalOpen={(): void => setIsIntegrationModalOpen(true)}
urlQuery.set('accountId', value); onAccountSettingsModalOpen={(): void => setIsAccountSettingsModalOpen(true)}
navigate({ search: urlQuery.toString() }); />
}}
/> {isIntegrationModalOpen && (
<div className="hero-section__action-buttons"> <CloudAccountSetupModal
<Button onClose={(): void => setIsIntegrationModalOpen(false)}
type="primary" />
className="hero-section__action-button primary"
onClick={(): void => setIsIntegrationModalOpen(true)}
>
Add New AWS Account
</Button>
<Button
type="default"
className="hero-section__action-button secondary"
onClick={(): void => setIsAccountSettingsModalOpen(true)}
>
Account Settings
</Button>
</div>
</div>
) : (
<Button
className="hero-section__action-button primary"
onClick={(): void => setIsIntegrationModalOpen(true)}
>
Integrate Now
</Button>
)} )}
<CloudAccountSetupModal {isAccountSettingsModalOpen && (
isOpen={isIntegrationModalOpen} <AccountSettingsModal
onClose={(): void => setIsIntegrationModalOpen(false)} onClose={(): void => setIsAccountSettingsModalOpen(false)}
/> account={activeAccount as CloudAccount}
setActiveAccount={setActiveAccount}
<AccountSettingsModal />
isOpen={isAccountSettingsModalOpen} )}
onClose={(): void => setIsAccountSettingsModalOpen(false)}
account={activeAccount as CloudAccount}
setActiveAccount={setActiveAccount}
/>
</div> </div>
); );
} }

View File

@ -2,27 +2,27 @@ import './AccountSettingsModal.style.scss';
import { Form, Select, Switch } from 'antd'; import { Form, Select, Switch } from 'antd';
import SignozModal from 'components/SignozModal/SignozModal'; import SignozModal from 'components/SignozModal/SignozModal';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { import {
getRegionPreviewText, getRegionPreviewText,
useAccountSettingsModal, useAccountSettingsModal,
} from 'hooks/integrations/aws/useAccountSettingsModal'; } from 'hooks/integrations/aws/useAccountSettingsModal';
import IntergrationsUninstallBar from 'pages/Integrations/IntegrationDetailPage/IntegrationsUninstallBar'; import useUrlQuery from 'hooks/useUrlQuery';
import { ConnectionStates } from 'pages/Integrations/IntegrationDetailPage/TestConnection'; import history from 'lib/history';
import { AWS_INTEGRATION } from 'pages/Integrations/IntegrationsList';
import { Dispatch, SetStateAction, useCallback } from 'react'; import { Dispatch, SetStateAction, useCallback } from 'react';
import { useQueryClient } from 'react-query';
import { CloudAccount } from '../../ServicesSection/types'; import { CloudAccount } from '../../ServicesSection/types';
import { RegionSelector } from './RegionSelector'; import { RegionSelector } from './RegionSelector';
import RemoveIntegrationAccount from './RemoveIntegrationAccount';
interface AccountSettingsModalProps { interface AccountSettingsModalProps {
isOpen: boolean;
onClose: () => void; onClose: () => void;
account: CloudAccount; account: CloudAccount;
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>; setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
} }
function AccountSettingsModal({ function AccountSettingsModal({
isOpen,
onClose, onClose,
account, account,
setActiveAccount, setActiveAccount,
@ -42,6 +42,16 @@ function AccountSettingsModal({
handleClose, handleClose,
} = useAccountSettingsModal({ onClose, account, setActiveAccount }); } = useAccountSettingsModal({ onClose, account, setActiveAccount });
const queryClient = useQueryClient();
const urlQuery = useUrlQuery();
const handleRemoveIntegrationAccountSuccess = (): void => {
queryClient.invalidateQueries([REACT_QUERY_KEY.AWS_ACCOUNTS]);
urlQuery.delete('cloudAccountId');
handleClose();
history.replace({ search: urlQuery.toString() });
};
const renderRegionSelector = useCallback(() => { const renderRegionSelector = useCallback(() => {
if (isRegionSelectOpen) { if (isRegionSelectOpen) {
return ( return (
@ -120,7 +130,7 @@ function AccountSettingsModal({
return ( return (
<SignozModal <SignozModal
open={isOpen} open
title={modalTitle} title={modalTitle}
onCancel={handleClose} onCancel={handleClose}
onOk={handleSubmit} onOk={handleSubmit}
@ -164,12 +174,9 @@ function AccountSettingsModal({
</Form.Item> </Form.Item>
<div className="integration-detail-content"> <div className="integration-detail-content">
<IntergrationsUninstallBar <RemoveIntegrationAccount
integrationTitle={AWS_INTEGRATION.title} accountId={account?.id}
integrationId={AWS_INTEGRATION.id} onRemoveIntegrationAccountSuccess={handleRemoveIntegrationAccountSuccess}
onUnInstallSuccess={handleClose}
removeIntegrationTitle="Remove"
connectionStatus={ConnectionStates.Connected}
/> />
</div> </div>
</div> </div>

View File

@ -2,11 +2,9 @@ import './CloudAccountSetupModal.style.scss';
import { Color } from '@signozhq/design-tokens'; import { Color } from '@signozhq/design-tokens';
import SignozModal from 'components/SignozModal/SignozModal'; import SignozModal from 'components/SignozModal/SignozModal';
import ROUTES from 'constants/routes';
import { useIntegrationModal } from 'hooks/integrations/aws/useIntegrationModal'; import { useIntegrationModal } from 'hooks/integrations/aws/useIntegrationModal';
import { SquareArrowOutUpRight } from 'lucide-react'; import { SquareArrowOutUpRight } from 'lucide-react';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom-v5-compat';
import { import {
ActiveViewEnum, ActiveViewEnum,
@ -18,7 +16,6 @@ import { RegionSelector } from './RegionSelector';
import { SuccessView } from './SuccessView'; import { SuccessView } from './SuccessView';
function CloudAccountSetupModal({ function CloudAccountSetupModal({
isOpen,
onClose, onClose,
}: IntegrationModalProps): JSX.Element { }: IntegrationModalProps): JSX.Element {
const { const {
@ -41,6 +38,8 @@ function CloudAccountSetupModal({
accountId, accountId,
selectedDeploymentRegion, selectedDeploymentRegion,
handleRegionChange, handleRegionChange,
connectionParams,
isConnectionParamsLoading,
} = useIntegrationModal({ onClose }); } = useIntegrationModal({ onClose });
const renderContent = useCallback(() => { const renderContent = useCallback(() => {
@ -71,6 +70,8 @@ function CloudAccountSetupModal({
accountId={accountId} accountId={accountId}
selectedDeploymentRegion={selectedDeploymentRegion} selectedDeploymentRegion={selectedDeploymentRegion}
handleRegionChange={handleRegionChange} handleRegionChange={handleRegionChange}
connectionParams={connectionParams}
isConnectionParamsLoading={isConnectionParamsLoading}
/> />
); );
}, [ }, [
@ -86,6 +87,8 @@ function CloudAccountSetupModal({
accountId, accountId,
selectedDeploymentRegion, selectedDeploymentRegion,
handleRegionChange, handleRegionChange,
connectionParams,
isConnectionParamsLoading,
setSelectedRegions, setSelectedRegions,
setIncludeAllRegions, setIncludeAllRegions,
]); ]);
@ -96,11 +99,6 @@ function CloudAccountSetupModal({
[selectedRegions, allRegions], [selectedRegions, allRegions],
); );
const navigate = useNavigate();
const handleGoToDashboards = useCallback((): void => {
navigate(ROUTES.ALL_DASHBOARD);
}, [navigate]);
const getModalConfig = useCallback(() => { const getModalConfig = useCallback(() => {
// Handle success state first // Handle success state first
if (modalState === ModalStateEnum.SUCCESS) { if (modalState === ModalStateEnum.SUCCESS) {
@ -108,11 +106,11 @@ function CloudAccountSetupModal({
title: 'AWS Webservice Integration', title: 'AWS Webservice Integration',
okText: ( okText: (
<div className="cloud-account-setup-success-view__footer-button"> <div className="cloud-account-setup-success-view__footer-button">
Go to Dashboards Continue
</div> </div>
), ),
block: true, block: true,
onOk: handleGoToDashboards, onOk: handleClose,
cancelButtonProps: { style: { display: 'none' } }, cancelButtonProps: { style: { display: 'none' } },
disabled: false, disabled: false,
}; };
@ -151,7 +149,7 @@ function CloudAccountSetupModal({
isLoading, isLoading,
isGeneratingUrl, isGeneratingUrl,
activeView, activeView,
handleGoToDashboards, handleClose,
setActiveView, setActiveView,
]); ]);
@ -159,7 +157,7 @@ function CloudAccountSetupModal({
return ( return (
<SignozModal <SignozModal
open={isOpen} open
className="cloud-account-setup-modal" className="cloud-account-setup-modal"
title={modalConfig.title} title={modalConfig.title}
onCancel={handleClose} onCancel={handleClose}

View File

@ -12,6 +12,7 @@ import {
MonitoringRegionsSection, MonitoringRegionsSection,
RegionDeploymentSection, RegionDeploymentSection,
} from './IntegrateNowFormSections'; } from './IntegrateNowFormSections';
import RenderConnectionFields from './RenderConnectionParams';
const allRegions = (): string[] => const allRegions = (): string[] =>
regions.flatMap((r) => r.subRegions.map((sr) => sr.name)); regions.flatMap((r) => r.subRegions.map((sr) => sr.name));
@ -35,6 +36,8 @@ export function RegionForm({
accountId, accountId,
selectedDeploymentRegion, selectedDeploymentRegion,
handleRegionChange, handleRegionChange,
connectionParams,
isConnectionParamsLoading,
}: RegionFormProps): JSX.Element { }: RegionFormProps): JSX.Element {
const startTimeRef = useRef(Date.now()); const startTimeRef = useRef(Date.now());
const refetchInterval = 10 * 1000; const refetchInterval = 10 * 1000;
@ -88,6 +91,11 @@ export function RegionForm({
isFormDisabled={isFormDisabled} isFormDisabled={isFormDisabled}
/> />
<ComplianceNote /> <ComplianceNote />
<RenderConnectionFields
isConnectionParamsLoading={isConnectionParamsLoading}
connectionParams={connectionParams}
isFormDisabled={isFormDisabled}
/>
</div> </div>
</Form> </Form>
); );

View File

@ -0,0 +1,48 @@
.remove-integration-account {
display: flex;
justify-content: space-between;
padding: 16px;
border-radius: 4px;
border: 1px solid rgba(218, 85, 101, 0.2);
background: rgba(218, 85, 101, 0.06);
&__header {
display: flex;
flex-direction: column;
gap: 6px;
}
&__title {
color: var(--bg-cherry-500);
font-size: 14px;
letter-spacing: -0.07px;
}
&__subtitle {
color: var(--bg-cherry-300);
font-size: 14px;
line-height: 22px;
letter-spacing: -0.07px;
}
&__button {
display: flex;
align-items: center;
background: var(--bg-cherry-500);
border: none;
color: var(--bg-vanilla-100);
font-size: 12px;
font-weight: 500;
line-height: 13.3px; /* 110.833% */
padding: 9px 13px;
.ant-btn-icon {
margin-inline-end: 4px !important;
}
&:hover {
&.ant-btn-default {
color: var(--bg-vanilla-300) !important;
}
}
}
}

View File

@ -0,0 +1,94 @@
import './RemoveIntegrationAccount.scss';
import { Button, Modal } from 'antd';
import logEvent from 'api/common/logEvent';
import removeAwsIntegrationAccount from 'api/Integrations/removeAwsIntegrationAccount';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { useNotifications } from 'hooks/useNotifications';
import { X } from 'lucide-react';
import { INTEGRATION_TELEMETRY_EVENTS } from 'pages/Integrations/utils';
import { useState } from 'react';
import { useMutation } from 'react-query';
function RemoveIntegrationAccount({
accountId,
onRemoveIntegrationAccountSuccess,
}: {
accountId: string;
onRemoveIntegrationAccountSuccess: () => void;
}): JSX.Element {
const { notifications } = useNotifications();
const [isModalOpen, setIsModalOpen] = useState(false);
const showModal = (): void => {
setIsModalOpen(true);
};
const {
mutate: removeIntegration,
isLoading: isRemoveIntegrationLoading,
} = useMutation(removeAwsIntegrationAccount, {
onSuccess: () => {
onRemoveIntegrationAccountSuccess?.();
setIsModalOpen(false);
},
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
});
const handleOk = (): void => {
logEvent(INTEGRATION_TELEMETRY_EVENTS.AWS_INTEGRATION_ACCOUNT_REMOVED, {
accountId,
});
removeIntegration(accountId);
};
const handleCancel = (): void => {
setIsModalOpen(false);
};
return (
<div className="remove-integration-account">
<div className="remove-integration-account__header">
<div className="remove-integration-account__title">Remove Integration</div>
<div className="remove-integration-account__subtitle">
Removing this integration won&apos;t delete any existing data but will stop
collecting new data from AWS.
</div>
</div>
<Button
className="remove-integration-account__button"
icon={<X size={14} />}
onClick={(): void => showModal()}
>
Remove
</Button>
<Modal
className="remove-integration-modal"
open={isModalOpen}
title="Remove integration"
onOk={handleOk}
onCancel={handleCancel}
okText="Remove Integration"
okButtonProps={{
danger: true,
disabled: isRemoveIntegrationLoading,
}}
>
<div className="remove-integration-modal__text">
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.
</div>
</Modal>
</div>
);
}
export default RemoveIntegrationAccount;

View File

@ -0,0 +1,61 @@
import { Form, Input } from 'antd';
import { ConnectionParams } from 'types/api/integrations/aws';
function RenderConnectionFields({
isConnectionParamsLoading,
connectionParams,
isFormDisabled,
}: {
isConnectionParamsLoading?: boolean;
connectionParams?: ConnectionParams | null;
isFormDisabled?: boolean;
}): JSX.Element | null {
if (
isConnectionParamsLoading ||
(!!connectionParams?.ingestion_url &&
!!connectionParams?.ingestion_key &&
!!connectionParams?.signoz_api_url)
) {
return null;
}
return (
<Form.Item name="connection_params">
{!connectionParams?.ingestion_url && (
<Form.Item
name="ingestion_url"
label="Ingestion URL"
rules={[{ required: true, message: 'Please enter ingestion URL' }]}
>
<Input placeholder="Enter ingestion URL" disabled={isFormDisabled} />
</Form.Item>
)}
{!connectionParams?.ingestion_key && (
<Form.Item
name="ingestion_key"
label="Ingestion Key"
rules={[{ required: true, message: 'Please enter ingestion key' }]}
>
<Input placeholder="Enter ingestion key" disabled={isFormDisabled} />
</Form.Item>
)}
{!connectionParams?.signoz_api_url && (
<Form.Item
name="signoz_api_url"
label="SigNoz API URL"
rules={[{ required: true, message: 'Please enter SigNoz API URL' }]}
>
<Input placeholder="Enter SigNoz API URL" disabled={isFormDisabled} />
</Form.Item>
)}
</Form.Item>
);
}
RenderConnectionFields.defaultProps = {
connectionParams: null,
isFormDisabled: false,
isConnectionParamsLoading: false,
};
export default RenderConnectionFields;

View File

@ -53,23 +53,18 @@ export function SuccessView(): JSX.Element {
WHAT NEXT WHAT NEXT
</h4> </h4>
<div className="what-next-items-wrapper"> <div className="what-next-items-wrapper">
{[ <Alert
'Understand your AWS services with SigNozs out-of-the-box dashboards', message={
'Set up alerts for real-time monitoring.', <div className="what-next-items-wrapper__item">
'Track logs and traces.', <div className="what-next-item-bullet-icon"></div>
].map((item) => ( <div className="what-next-item-text">
<Alert Set up your AWS services effortlessly under your enabled account.
key={item}
message={
<div className="what-next-items-wrapper__item">
<div className="what-next-item-bullet-icon"></div>
<div className="what-next-item-text">{item}</div>
</div> </div>
} </div>
type="info" }
className="what-next-items-wrapper__item" type="info"
/> className="what-next-items-wrapper__item"
))} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
import { FormInstance } from 'antd'; import { FormInstance } from 'antd';
import { Dispatch, SetStateAction } from 'react'; import { Dispatch, SetStateAction } from 'react';
import { ConnectionParams } from 'types/api/integrations/aws';
export enum ActiveViewEnum { export enum ActiveViewEnum {
SELECT_REGIONS = 'select-regions', SELECT_REGIONS = 'select-regions',
@ -25,9 +26,10 @@ export interface RegionFormProps {
accountId?: string; accountId?: string;
selectedDeploymentRegion: string | undefined; selectedDeploymentRegion: string | undefined;
handleRegionChange: (value: string) => void; handleRegionChange: (value: string) => void;
connectionParams?: ConnectionParams;
isConnectionParamsLoading?: boolean;
} }
export interface IntegrationModalProps { export interface IntegrationModalProps {
isOpen: boolean;
onClose: () => void; onClose: () => void;
} }

View File

@ -1,3 +1,5 @@
import { Link } from 'react-router-dom';
import { ServiceData } from './types'; import { ServiceData } from './types';
function DashboardItem({ function DashboardItem({
@ -5,8 +7,8 @@ function DashboardItem({
}: { }: {
dashboard: ServiceData['assets']['dashboards'][number]; dashboard: ServiceData['assets']['dashboards'][number];
}): JSX.Element { }): JSX.Element {
return ( const content = (
<div className="cloud-service-dashboard-item"> <>
<div className="cloud-service-dashboard-item__title">{dashboard.title}</div> <div className="cloud-service-dashboard-item__title">{dashboard.title}</div>
<div className="cloud-service-dashboard-item__preview"> <div className="cloud-service-dashboard-item__preview">
<img <img
@ -15,6 +17,18 @@ function DashboardItem({
className="cloud-service-dashboard-item__preview-image" className="cloud-service-dashboard-item__preview-image"
/> />
</div> </div>
</>
);
return (
<div className="cloud-service-dashboard-item">
{dashboard.url ? (
<Link to={dashboard.url} className="cloud-service-dashboard-item__link">
{content}
</Link>
) : (
content
)}
</div> </div>
); );
} }

View File

@ -1,4 +1,3 @@
import { Color } from '@signozhq/design-tokens';
import { Button, Tabs, TabsProps } from 'antd'; import { Button, Tabs, TabsProps } from 'antd';
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer'; import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
@ -8,8 +7,7 @@ import { IServiceStatus } from 'container/CloudIntegrationPage/ServicesSection/t
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useServiceDetails } from 'hooks/integrations/aws/useServiceDetails'; import { useServiceDetails } from 'hooks/integrations/aws/useServiceDetails';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import { Wrench } from 'lucide-react'; import { useMemo, useState } from 'react';
import { useState } from 'react';
import ConfigureServiceModal from './ConfigureServiceModal'; import ConfigureServiceModal from './ConfigureServiceModal';
@ -38,7 +36,7 @@ const getStatus = (
function ServiceStatus({ function ServiceStatus({
serviceStatus, serviceStatus,
}: { }: {
serviceStatus: IServiceStatus | null; serviceStatus: IServiceStatus | undefined;
}): JSX.Element { }): JSX.Element {
const logsLastReceivedTimestamp = serviceStatus?.logs?.last_received_ts_ms; const logsLastReceivedTimestamp = serviceStatus?.logs?.last_received_ts_ms;
const metricsLastReceivedTimestamp = const metricsLastReceivedTimestamp =
@ -54,7 +52,7 @@ function ServiceStatus({
function ServiceDetails(): JSX.Element | null { function ServiceDetails(): JSX.Element | null {
const urlQuery = useUrlQuery(); const urlQuery = useUrlQuery();
const accountId = urlQuery.get('accountId'); const cloudAccountId = urlQuery.get('cloudAccountId');
const serviceId = urlQuery.get('service'); const serviceId = urlQuery.get('service');
const [isConfigureServiceModalOpen, setIsConfigureServiceModalOpen] = useState( const [isConfigureServiceModalOpen, setIsConfigureServiceModalOpen] = useState(
false, false,
@ -62,7 +60,24 @@ function ServiceDetails(): JSX.Element | null {
const { data: serviceDetailsData, isLoading } = useServiceDetails( const { data: serviceDetailsData, isLoading } = useServiceDetails(
serviceId || '', serviceId || '',
accountId || undefined, cloudAccountId || undefined,
);
// eslint-disable-next-line @typescript-eslint/naming-convention
const { config, supported_signals } = serviceDetailsData ?? {};
const totalSupportedSignals = Object.entries(supported_signals || {}).filter(
([, value]) => !!value,
).length;
const enabledSignals = useMemo(
() =>
Object.values(config || {}).filter((item) => item && item.enabled).length,
[config],
);
const isAnySignalConfigured = useMemo(
() => !!config?.logs?.enabled || !!config?.metrics?.enabled,
[config],
); );
if (isLoading) { if (isLoading) {
@ -96,16 +111,22 @@ function ServiceDetails(): JSX.Element | null {
<div className="service-details__title-bar"> <div className="service-details__title-bar">
<div className="service-details__details-title">Details</div> <div className="service-details__details-title">Details</div>
<div className="service-details__right-actions"> <div className="service-details__right-actions">
{serviceDetailsData?.status && ( <ServiceStatus serviceStatus={serviceDetailsData.status} />
<ServiceStatus serviceStatus={serviceDetailsData.status} />
)} {!!cloudAccountId && isAnySignalConfigured ? (
{!!accountId && (
<Button <Button
className="configure-button" className="configure-button configure-button--default"
onClick={(): void => setIsConfigureServiceModalOpen(true)} onClick={(): void => setIsConfigureServiceModalOpen(true)}
> >
<Wrench size={12} color={Color.BG_VANILLA_400} /> Configure ({enabledSignals}/{totalSupportedSignals})
Configure </Button>
) : (
<Button
type="primary"
className="configure-button configure-button--primary"
onClick={(): void => setIsConfigureServiceModalOpen(true)}
>
Enable Service
</Button> </Button>
)} )}
</div> </div>
@ -119,15 +140,17 @@ function ServiceDetails(): JSX.Element | null {
<div className="service-details__tabs"> <div className="service-details__tabs">
<Tabs items={tabItems} /> <Tabs items={tabItems} />
</div> </div>
<ConfigureServiceModal {isConfigureServiceModalOpen && (
isOpen={isConfigureServiceModalOpen} <ConfigureServiceModal
onClose={(): void => setIsConfigureServiceModalOpen(false)} isOpen
serviceName={serviceDetailsData.title} onClose={(): void => setIsConfigureServiceModalOpen(false)}
serviceId={serviceId || ''} serviceName={serviceDetailsData.title}
cloudAccountId={accountId || ''} serviceId={serviceId || ''}
initialConfig={serviceDetailsData.config} cloudAccountId={cloudAccountId || ''}
supportedSignals={serviceDetailsData.supported_signals || {}} initialConfig={serviceDetailsData.config}
/> supportedSignals={serviceDetailsData.supported_signals || {}}
/>
)}
</div> </div>
); );
} }

View File

@ -1,26 +1,34 @@
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { useGetAccountServices } from 'hooks/integrations/aws/useGetAccountServices'; import { useGetAccountServices } from 'hooks/integrations/aws/useGetAccountServices';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import { useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom-v5-compat'; import { useNavigate } from 'react-router-dom-v5-compat';
import ServiceItem from './ServiceItem'; import ServiceItem from './ServiceItem';
interface ServicesListProps { interface ServicesListProps {
accountId: string; cloudAccountId: string;
filter: 'all_services' | 'enabled' | 'available'; filter: 'all_services' | 'enabled' | 'available';
} }
function ServicesList({ accountId, filter }: ServicesListProps): JSX.Element { function ServicesList({
cloudAccountId,
filter,
}: ServicesListProps): JSX.Element {
const urlQuery = useUrlQuery(); const urlQuery = useUrlQuery();
const navigate = useNavigate(); const navigate = useNavigate();
const { data: services = [], isLoading } = useGetAccountServices(accountId); const { data: services = [], isLoading } = useGetAccountServices(
cloudAccountId,
);
const activeService = urlQuery.get('service'); const activeService = urlQuery.get('service');
const handleServiceClick = (serviceId: string): void => { const handleActiveService = useCallback(
urlQuery.set('service', serviceId); (serviceId: string): void => {
navigate({ search: urlQuery.toString() }); urlQuery.set('service', serviceId);
}; navigate({ search: urlQuery.toString() });
},
[navigate, urlQuery],
);
const filteredServices = useMemo(() => { const filteredServices = useMemo(() => {
if (filter === 'all_services') return services; if (filter === 'all_services') return services;
@ -32,6 +40,12 @@ function ServicesList({ accountId, filter }: ServicesListProps): JSX.Element {
}); });
}, [services, filter]); }, [services, filter]);
useEffect(() => {
if (activeService || !services?.length) return;
handleActiveService(services[0].id);
}, [services, activeService, handleActiveService]);
if (isLoading) return <Spinner size="large" height="25vh" />; if (isLoading) return <Spinner size="large" height="25vh" />;
if (!services) return <div>No services found</div>; if (!services) return <div>No services found</div>;
@ -41,7 +55,7 @@ function ServicesList({ accountId, filter }: ServicesListProps): JSX.Element {
<ServiceItem <ServiceItem
key={service.id} key={service.id}
service={service} service={service}
onClick={handleServiceClick} onClick={handleActiveService}
isActive={service.id === activeService} isActive={service.id === activeService}
/> />
))} ))}

View File

@ -135,18 +135,25 @@
color: var(--bg-cherry-400); color: var(--bg-cherry-400);
} }
} }
.configure-button { .configure-button {
display: flex;
align-items: center;
gap: 6px;
color: var(--bg-vanilla-400);
background: var(--bg-ink-300);
border: 1px solid var(--bg-slate-400);
border-radius: 2px; border-radius: 2px;
font-size: 12px; font-size: 12px;
font-weight: 400;
line-height: 10px; /* 83.333% */
letter-spacing: 0.12px; letter-spacing: 0.12px;
font-weight: 500;
width: 116px;
box-shadow: none;
&--default {
color: var(--bg-vanilla-400);
background: var(--bg-slate-400);
border: 1px solid var(--bg-slate-400);
}
&--primary {
background-color: var(--bg-robin-500);
color: var(--bg-vanilla-100);
font-weight: 500;
color: var(--Vanilla-100, #fff);
}
} }
} }
} }

View File

@ -20,17 +20,17 @@ export enum ServiceFilterType {
} }
interface ServicesFilterProps { interface ServicesFilterProps {
accountId: string; cloudAccountId: string;
onFilterChange: (value: ServiceFilterType) => void; onFilterChange: (value: ServiceFilterType) => void;
} }
function ServicesFilter({ function ServicesFilter({
accountId, cloudAccountId,
onFilterChange, onFilterChange,
}: ServicesFilterProps): JSX.Element | null { }: ServicesFilterProps): JSX.Element | null {
const { data: services, isLoading } = useQuery( const { data: services, isLoading } = useQuery(
[REACT_QUERY_KEY.AWS_SERVICES, accountId], [REACT_QUERY_KEY.AWS_SERVICES, cloudAccountId],
() => getAwsServices(accountId), () => getAwsServices(cloudAccountId),
); );
const { enabledCount, availableCount } = useMemo(() => { const { enabledCount, availableCount } = useMemo(() => {
@ -77,7 +77,7 @@ function ServicesFilter({
function ServicesSection(): JSX.Element { function ServicesSection(): JSX.Element {
const urlQuery = useUrlQuery(); const urlQuery = useUrlQuery();
const accountId = urlQuery.get('accountId') || ''; const cloudAccountId = urlQuery.get('cloudAccountId') || '';
const [activeFilter, setActiveFilter] = useState< const [activeFilter, setActiveFilter] = useState<
'all_services' | 'enabled' | 'available' 'all_services' | 'enabled' | 'available'
@ -86,8 +86,11 @@ function ServicesSection(): JSX.Element {
return ( return (
<div className="services-section"> <div className="services-section">
<div className="services-section__sidebar"> <div className="services-section__sidebar">
<ServicesFilter accountId={accountId} onFilterChange={setActiveFilter} /> <ServicesFilter
<ServicesList accountId={accountId} filter={activeFilter} /> cloudAccountId={cloudAccountId}
onFilterChange={setActiveFilter}
/>
<ServicesList cloudAccountId={cloudAccountId} filter={activeFilter} />
</div> </div>
<div className="services-section__content"> <div className="services-section__content">
<ServiceDetails /> <ServiceDetails />

View File

@ -0,0 +1,17 @@
import { getConnectionParams } from 'api/integrations/aws';
import { AxiosError } from 'axios';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { ConnectionParams } from 'types/api/integrations/aws';
export function useConnectionParams({
options,
}: {
options?: UseQueryOptions<ConnectionParams, AxiosError>;
}): UseQueryResult<ConnectionParams, AxiosError> {
return useQuery<ConnectionParams, AxiosError>(
[REACT_QUERY_KEY.AWS_GET_CONNECTION_PARAMS],
getConnectionParams,
options,
);
}

View File

@ -4,6 +4,7 @@ import {
ActiveViewEnum, ActiveViewEnum,
ModalStateEnum, ModalStateEnum,
} from 'container/CloudIntegrationPage/HeroSection/types'; } from 'container/CloudIntegrationPage/HeroSection/types';
import useAxiosError from 'hooks/useAxiosError';
import { import {
Dispatch, Dispatch,
SetStateAction, SetStateAction,
@ -13,11 +14,13 @@ import {
useState, useState,
} from 'react'; } from 'react';
import { import {
ConnectionParams,
ConnectionUrlResponse, ConnectionUrlResponse,
GenerateConnectionUrlPayload, GenerateConnectionUrlPayload,
} from 'types/api/integrations/aws'; } from 'types/api/integrations/aws';
import { regions } from 'utils/regions'; import { regions } from 'utils/regions';
import { useConnectionParams } from './useConnectionParams';
import { useGenerateConnectionUrl } from './useGenerateConnectionUrl'; import { useGenerateConnectionUrl } from './useGenerateConnectionUrl';
interface UseIntegrationModalProps { interface UseIntegrationModalProps {
@ -44,6 +47,8 @@ interface UseIntegrationModal {
accountId?: string; accountId?: string;
selectedDeploymentRegion: string | undefined; selectedDeploymentRegion: string | undefined;
handleRegionChange: (value: string) => void; handleRegionChange: (value: string) => void;
connectionParams?: ConnectionParams;
isConnectionParamsLoading: boolean;
} }
export function useIntegrationModal({ export function useIntegrationModal({
@ -102,6 +107,12 @@ export function useIntegrationModal({
isLoading: isGeneratingUrl, isLoading: isGeneratingUrl,
} = useGenerateConnectionUrl(); } = useGenerateConnectionUrl();
const handleError = useAxiosError();
const {
data: connectionParams,
isLoading: isConnectionParamsLoading,
} = useConnectionParams({ options: { onError: handleError } });
const handleGenerateUrl = useCallback( const handleGenerateUrl = useCallback(
(payload: GenerateConnectionUrlPayload): void => { (payload: GenerateConnectionUrlPayload): void => {
generateUrl(payload, { generateUrl(payload, {
@ -126,6 +137,9 @@ export function useIntegrationModal({
const payload: GenerateConnectionUrlPayload = { const payload: GenerateConnectionUrlPayload = {
agent_config: { agent_config: {
region: values.region, region: values.region,
ingestion_url: connectionParams?.ingestion_url || values.ingestion_url,
ingestion_key: connectionParams?.ingestion_key || values.ingestion_key,
signoz_api_url: connectionParams?.signoz_api_url || values.signoz_api_url,
}, },
account_config: { account_config: {
regions: includeAllRegions ? ['all'] : selectedRegions, regions: includeAllRegions ? ['all'] : selectedRegions,
@ -138,7 +152,13 @@ export function useIntegrationModal({
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, [form, includeAllRegions, selectedRegions, handleGenerateUrl]); }, [
form,
includeAllRegions,
selectedRegions,
handleGenerateUrl,
connectionParams,
]);
return { return {
form, form,
@ -160,5 +180,7 @@ export function useIntegrationModal({
setModalState, setModalState,
selectedDeploymentRegion, selectedDeploymentRegion,
handleRegionChange, handleRegionChange,
connectionParams,
isConnectionParamsLoading,
}; };
} }

View File

@ -5,11 +5,11 @@ import { useQuery, UseQueryResult } from 'react-query';
export function useServiceDetails( export function useServiceDetails(
serviceId: string, serviceId: string,
accountId?: string, cloudAccountId?: string,
): UseQueryResult<ServiceData> { ): UseQueryResult<ServiceData> {
return useQuery( return useQuery(
[REACT_QUERY_KEY.AWS_SERVICE_DETAILS, serviceId, accountId], [REACT_QUERY_KEY.AWS_SERVICE_DETAILS, serviceId, cloudAccountId],
() => getServiceDetails(serviceId, accountId), () => getServiceDetails(serviceId, cloudAccountId),
{ {
enabled: !!serviceId, enabled: !!serviceId,
}, },

View File

@ -16,7 +16,7 @@ import { handleContactSupport, INTEGRATION_TYPES } from './utils';
export const AWS_INTEGRATION = { export const AWS_INTEGRATION = {
id: INTEGRATION_TYPES.AWS_INTEGRATION, id: INTEGRATION_TYPES.AWS_INTEGRATION,
title: 'AWS Web Services', title: 'Amazon Web Services',
description: 'One-click setup for AWS monitoring with SigNoz', description: 'One-click setup for AWS monitoring with SigNoz',
author: { author: {
name: 'SigNoz', name: 'SigNoz',

View File

@ -19,6 +19,8 @@ export const INTEGRATION_TELEMETRY_EVENTS = {
'Integrations Detail Page: Clicked remove Integration button for integration', 'Integrations Detail Page: Clicked remove Integration button for integration',
INTEGRATIONS_DETAIL_CONFIGURE_INSTRUCTION: INTEGRATIONS_DETAIL_CONFIGURE_INSTRUCTION:
'Integrations Detail Page: Navigated to configure an integration', 'Integrations Detail Page: Navigated to configure an integration',
AWS_INTEGRATION_ACCOUNT_REMOVED:
'AWS Integration Detail page: Clicked remove Integration button for integration',
}; };
export const INTEGRATION_TYPES = { export const INTEGRATION_TYPES = {

View File

@ -1,9 +1,15 @@
import { CloudAccount } from 'container/CloudIntegrationPage/ServicesSection/types'; import { CloudAccount } from 'container/CloudIntegrationPage/ServicesSection/types';
export interface ConnectionParams {
ingestion_url?: string;
ingestion_key?: string;
signoz_api_url?: string;
}
export interface GenerateConnectionUrlPayload { export interface GenerateConnectionUrlPayload {
agent_config: { agent_config: {
region: string; region: string;
}; } & ConnectionParams;
account_config: { account_config: {
regions: string[]; regions: string[];
}; };