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 {
AccountConfigPayload,
AccountConfigResponse,
ConnectionParams,
ConnectionUrlResponse,
} from 'types/api/integrations/aws';
export const getAwsAccounts = async (): Promise<CloudAccount[]> => {
const response = await axios.get('/cloud-integrations/aws/accounts');
return response.data.data;
return response.data.data.accounts;
};
export const getAwsServices = async (
accountId?: string,
cloudAccountId?: string,
): 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', {
params,
});
@ -31,9 +34,11 @@ export const getAwsServices = async (
export const getServiceDetails = async (
serviceId: string,
accountId?: string,
cloudAccountId?: string,
): Promise<ServiceData> => {
const params = accountId ? { account_id: accountId } : undefined;
const params = cloudAccountId
? { cloud_account_id: cloudAccountId }
: undefined;
const response = await axios.get(
`/cloud-integrations/aws/services/${serviceId}`,
{ params },
@ -74,3 +79,10 @@ export const updateServiceConfig = async (
);
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_SERVICE_CONFIG: 'AWS_UPDATE_SERVICE_CONFIG',
AWS_GENERATE_CONNECTION_URL: 'AWS_GENERATE_CONNECTION_URL',
AWS_GET_CONNECTION_PARAMS: 'AWS_GET_CONNECTION_PARAMS',
GET_ATTRIBUTE_VALUES: 'GET_ATTRIBUTE_VALUES',
};

View File

@ -24,7 +24,9 @@ function Header(): JSX.Element {
},
{
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" />
</div>
<div className="hero-section__details">
<div className="title">AWS Web Services</div>
<div className="title">Amazon Web Services</div>
<div className="description">
One-click setup for AWS monitoring with SigNoz
</div>

View File

@ -1,41 +1,56 @@
.hero-section__actions {
margin-top: 12px;
.hero-section {
&__actions {
margin-top: 12px;
&-with-account {
display: flex;
flex-direction: column;
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);
&-with-account {
display: flex;
flex-direction: column;
gap: 10px;
}
}
&.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;
align-items: center;
border: 1px solid var(--bg-ink-300);
color: var(--bg-vanilla-100);
gap: 8px;
}
&__action-button {
font-family: 'Inter';
border-radius: 2px;
background: var(--bg-slate-400);
box-shadow: none;
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 {
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 { Color } from '@signozhq/design-tokens';
import { Button, Select } from 'antd';
import { Button, Select, Skeleton } from 'antd';
import { SelectProps } from 'antd/lib';
import { useAwsAccounts } from 'hooks/integrations/aws/useAwsAccounts';
import useUrlQuery from 'hooks/useUrlQuery';
@ -53,15 +53,100 @@ const getAccountById = (
): CloudAccount | 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 {
const urlQuery = useUrlQuery();
const navigate = useNavigate();
const { data: accounts } = useAwsAccounts();
const { data: accounts, isLoading } = useAwsAccounts();
const initialAccount = useMemo(
() =>
accounts?.length
? getAccountById(accounts, urlQuery.get('accountId') || '') || accounts[0]
? getAccountById(accounts, urlQuery.get('cloudAccountId') || '') ||
accounts[0]
: null,
[accounts, urlQuery],
);
@ -74,7 +159,7 @@ function AccountActions(): JSX.Element {
useEffect(() => {
if (initialAccount !== null) {
setActiveAccount(initialAccount);
urlQuery.set('accountId', initialAccount.cloud_account_id);
urlQuery.set('cloudAccountId', initialAccount.cloud_account_id);
navigate({ search: urlQuery.toString() });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -98,60 +183,35 @@ function AccountActions(): JSX.Element {
return (
<div className="hero-section__actions">
{accounts?.length ? (
<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={(value): void => {
setActiveAccount(getAccountById(accounts, value));
urlQuery.set('accountId', value);
navigate({ search: urlQuery.toString() });
}}
/>
<div className="hero-section__action-buttons">
<Button
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>
<AccountActionsRenderer
accounts={accounts}
isLoading={isLoading}
activeAccount={activeAccount}
selectOptions={selectOptions}
onAccountChange={(value): void => {
if (accounts) {
setActiveAccount(getAccountById(accounts, value));
urlQuery.set('cloudAccountId', value);
navigate({ search: urlQuery.toString() });
}
}}
onIntegrationModalOpen={(): void => setIsIntegrationModalOpen(true)}
onAccountSettingsModalOpen={(): void => setIsAccountSettingsModalOpen(true)}
/>
{isIntegrationModalOpen && (
<CloudAccountSetupModal
onClose={(): void => setIsIntegrationModalOpen(false)}
/>
)}
<CloudAccountSetupModal
isOpen={isIntegrationModalOpen}
onClose={(): void => setIsIntegrationModalOpen(false)}
/>
<AccountSettingsModal
isOpen={isAccountSettingsModalOpen}
onClose={(): void => setIsAccountSettingsModalOpen(false)}
account={activeAccount as CloudAccount}
setActiveAccount={setActiveAccount}
/>
{isAccountSettingsModalOpen && (
<AccountSettingsModal
onClose={(): void => setIsAccountSettingsModalOpen(false)}
account={activeAccount as CloudAccount}
setActiveAccount={setActiveAccount}
/>
)}
</div>
);
}

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import {
MonitoringRegionsSection,
RegionDeploymentSection,
} from './IntegrateNowFormSections';
import RenderConnectionFields from './RenderConnectionParams';
const allRegions = (): string[] =>
regions.flatMap((r) => r.subRegions.map((sr) => sr.name));
@ -35,6 +36,8 @@ export function RegionForm({
accountId,
selectedDeploymentRegion,
handleRegionChange,
connectionParams,
isConnectionParamsLoading,
}: RegionFormProps): JSX.Element {
const startTimeRef = useRef(Date.now());
const refetchInterval = 10 * 1000;
@ -88,6 +91,11 @@ export function RegionForm({
isFormDisabled={isFormDisabled}
/>
<ComplianceNote />
<RenderConnectionFields
isConnectionParamsLoading={isConnectionParamsLoading}
connectionParams={connectionParams}
isFormDisabled={isFormDisabled}
/>
</div>
</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
</h4>
<div className="what-next-items-wrapper">
{[
'Understand your AWS services with SigNozs out-of-the-box dashboards',
'Set up alerts for real-time monitoring.',
'Track logs and traces.',
].map((item) => (
<Alert
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>
<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>
}
type="info"
className="what-next-items-wrapper__item"
/>
))}
</div>
}
type="info"
className="what-next-items-wrapper__item"
/>
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

@ -135,18 +135,25 @@
color: var(--bg-cherry-400);
}
}
.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;
font-size: 12px;
font-weight: 400;
line-height: 10px; /* 83.333% */
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 {
accountId: string;
cloudAccountId: string;
onFilterChange: (value: ServiceFilterType) => void;
}
function ServicesFilter({
accountId,
cloudAccountId,
onFilterChange,
}: ServicesFilterProps): JSX.Element | null {
const { data: services, isLoading } = useQuery(
[REACT_QUERY_KEY.AWS_SERVICES, accountId],
() => getAwsServices(accountId),
[REACT_QUERY_KEY.AWS_SERVICES, cloudAccountId],
() => getAwsServices(cloudAccountId),
);
const { enabledCount, availableCount } = useMemo(() => {
@ -77,7 +77,7 @@ function ServicesFilter({
function ServicesSection(): JSX.Element {
const urlQuery = useUrlQuery();
const accountId = urlQuery.get('accountId') || '';
const cloudAccountId = urlQuery.get('cloudAccountId') || '';
const [activeFilter, setActiveFilter] = useState<
'all_services' | 'enabled' | 'available'
@ -86,8 +86,11 @@ function ServicesSection(): JSX.Element {
return (
<div className="services-section">
<div className="services-section__sidebar">
<ServicesFilter accountId={accountId} onFilterChange={setActiveFilter} />
<ServicesList accountId={accountId} filter={activeFilter} />
<ServicesFilter
cloudAccountId={cloudAccountId}
onFilterChange={setActiveFilter}
/>
<ServicesList cloudAccountId={cloudAccountId} filter={activeFilter} />
</div>
<div className="services-section__content">
<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,
ModalStateEnum,
} from 'container/CloudIntegrationPage/HeroSection/types';
import useAxiosError from 'hooks/useAxiosError';
import {
Dispatch,
SetStateAction,
@ -13,11 +14,13 @@ import {
useState,
} from 'react';
import {
ConnectionParams,
ConnectionUrlResponse,
GenerateConnectionUrlPayload,
} from 'types/api/integrations/aws';
import { regions } from 'utils/regions';
import { useConnectionParams } from './useConnectionParams';
import { useGenerateConnectionUrl } from './useGenerateConnectionUrl';
interface UseIntegrationModalProps {
@ -44,6 +47,8 @@ interface UseIntegrationModal {
accountId?: string;
selectedDeploymentRegion: string | undefined;
handleRegionChange: (value: string) => void;
connectionParams?: ConnectionParams;
isConnectionParamsLoading: boolean;
}
export function useIntegrationModal({
@ -102,6 +107,12 @@ export function useIntegrationModal({
isLoading: isGeneratingUrl,
} = useGenerateConnectionUrl();
const handleError = useAxiosError();
const {
data: connectionParams,
isLoading: isConnectionParamsLoading,
} = useConnectionParams({ options: { onError: handleError } });
const handleGenerateUrl = useCallback(
(payload: GenerateConnectionUrlPayload): void => {
generateUrl(payload, {
@ -126,6 +137,9 @@ export function useIntegrationModal({
const payload: GenerateConnectionUrlPayload = {
agent_config: {
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: {
regions: includeAllRegions ? ['all'] : selectedRegions,
@ -138,7 +152,13 @@ export function useIntegrationModal({
} finally {
setIsLoading(false);
}
}, [form, includeAllRegions, selectedRegions, handleGenerateUrl]);
}, [
form,
includeAllRegions,
selectedRegions,
handleGenerateUrl,
connectionParams,
]);
return {
form,
@ -160,5 +180,7 @@ export function useIntegrationModal({
setModalState,
selectedDeploymentRegion,
handleRegionChange,
connectionParams,
isConnectionParamsLoading,
};
}

View File

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

View File

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

View File

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

View File

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