feat: aws integration (#6954)

* feat: aws Integration skeleton UI (#6758)

* feat: add AWS integration in the integrations list and redirect to the new Cloud Integration page

* feat: cloud integration details page header (i.e. breadcrumb and get help button) UI

* feat: hero section UI

* refactor: extract Header and HeroSection components from CloudIntegrationPage

* feat: services tab bar and sidebar UI

* feat: cloud integration details services UI

* refactor: group and extract cloud integration components to files

* fix: set default active service to the first service in the list if no service is specified

* feat: add NEW flag for AWS integration in the integrations list page

* chore: overall improvements

* chore: move cloud integration pages to /container

* fix: hero section background

* feat: aws Integration: Account setup basic UI and functionality (#6806)

* feat: implement basic cloud account management UI in HeroSection

* feat: aws Integration: Integrate now modal (#6807)

* feat: implement basic cloud account management UI in HeroSection

* feat: start working on integrate now modal UI

* feat: integrate now modal UI

* feat: integrate now modal states and json server API integration

* feat: get accounts from json-server API, and redirect Add new account to the integrations modal

* feat: display error state if last_heartbeat_ts_ms is null even after 5 minutes

* chore: update import path for regions data in useRegionSelection hook

* chore: move hero section components inside the HeroSection/components

* feat: create a reusable modal component

* refactor: make the cloud account setup modal readable / DRYer

* feat: aws Integration: Account settings modal (#6808)

* feat: implement basic cloud account management UI in HeroSection

* feat: start working on integrate now modal UI

* feat: get accounts from json-server API, and redirect Add new account to the integrations modal

* feat: integrate now modal UI

* feat: integrate now modal states and json server API integration

* feat: account settings

* feat: service status UI

* refactor: make account settings modal more readable and overall improvements

* feat: Get data from json server api data in service sections (#6809)

* feat: implement basic cloud account management UI in HeroSection

* feat: start working on integrate now modal UI

* feat: get accounts from json-server API, and redirect Add new account to the integrations modal

* refactor: make the cloud account setup modal readable / DRYer

* feat: integrate now modal states and json server API integration

* refactor: make account settings modal more readable and overall improvements

* feat: integrate now modal states and json server API integration

* feat: display error state if last_heartbeat_ts_ms is null even after 5 minutes

* feat: get the services list and details from json server API response

* feat: update account actions to set accountId in URL query on initial account load

* feat: configure service modal (#6814)

* feat: implement basic cloud account management UI in HeroSection

* feat: start working on integrate now modal UI

* feat: get accounts from json-server API, and redirect Add new account to the integrations modal

* refactor: make the cloud account setup modal readable / DRYer

* feat: integrate now modal states and json server API integration

* feat: get accounts from json-server API, and redirect Add new account to the integrations modal

* feat: integrate now modal states and json server API integration

* feat: get accounts from json-server API, and redirect Add new account to the integrations modal

* feat: display error state if last_heartbeat_ts_ms is null even after 5 minutes

* feat: account settings

* feat: service status UI

* feat: get the services list and details from json server API response

* feat: update account actions to set accountId in URL query on initial account load

* feat: configure service modal UI

* feat: configure service modal functionality and API changes

* feat: replace loading indicators with Spinner component in ServiceDetails and ServicesList

* fix: make the configure service modal work

* feat: light mode support and overall improvements to AWS integration page (#6817)

* refactor: make the cloud account setup modal readable / DRYer

* feat: integrate now modal states and json server API integration

* refactor: make account settings modal more readable and overall improvements

* fix: integrate now modal button improvements

* feat: aws integrations light mode

* refactor: overall improvements

* refactor: define react query keys in constant

* feat: services filter

* feat: render service overview as markdown

* feat: integrate AWS integration page API (#6851)

* feat: replace json-server APIs with actual APIs

* fix: add null checks and fix the issues

* chore: remove the console.log

* feat: temporarily hide AWS Integration from integrations list

* chore: add optimized png

* refactor: extract service filter types into an enum

* chore: remove console.log

* chore: remove duplicate files

* refactor: move regions to utils

* fix: get account id from url param

* chore: address PR review comments

* refactor: use the IntegrateNowFormSections inside RegionForm

* chore: move integrations select inline style to a common class

---------

Co-authored-by: Shaheer Kochai <ashaheerki@gmail.com>
This commit is contained in:
Yunus M 2025-01-30 21:51:49 +05:30 committed by GitHub
parent 3849ca1ecc
commit e542e96031
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 19439 additions and 22 deletions

View File

@ -107,6 +107,7 @@
"react-grid-layout": "^1.3.4",
"react-helmet-async": "1.3.0",
"react-i18next": "^11.16.1",
"react-lottie": "1.2.10",
"react-markdown": "8.0.7",
"react-query": "3.39.3",
"react-redux": "^7.2.2",
@ -178,6 +179,7 @@
"@types/react-dom": "18.0.10",
"@types/react-grid-layout": "^1.1.2",
"@types/react-helmet-async": "1.0.3",
"@types/react-lottie": "1.2.10",
"@types/react-redux": "^7.1.11",
"@types/react-resizable": "3.0.3",
"@types/react-router-dom": "^5.1.6",
@ -218,6 +220,7 @@
"portfinder-sync": "^0.0.2",
"postcss": "8.4.38",
"prettier": "2.2.1",
"prop-types": "15.8.1",
"raw-loader": "4.0.2",
"react-hooks-testing-library": "0.6.0",
"react-hot-loader": "^4.13.0",

View File

@ -0,0 +1,6 @@
<svg width="57" height="57" viewBox="0 0 57 57" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="solid-check-circle-2">
<path id="Vector" d="M28.2498 51.9638C41.1368 51.9638 51.5832 41.5175 51.5832 28.6305C51.5832 15.7435 41.1368 5.29712 28.2498 5.29712C15.3628 5.29712 4.9165 15.7435 4.9165 28.6305C4.9165 41.5175 15.3628 51.9638 28.2498 51.9638Z" fill="#4E74F8" stroke="#4E74F8" stroke-width="4.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M16.25 27.6304L24.2115 36.6304L39.25 22.6304" stroke="#121317" stroke-width="4.66667" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 629 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

View File

@ -0,0 +1,23 @@
<svg width="24" height="16" viewBox="0 0 24 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="amazon_web_services_logo.svg" clip-path="url(#clip0_2552_26387)">
<g id="Group">
<path id="Vector" d="M6.82058 6.05151C6.82058 6.34672 6.85997 6.58289 6.89935 6.76001C6.95843 6.93714 7.03721 7.11426 7.15537 7.33075C7.19476 7.38979 7.21445 7.44883 7.21445 7.50788C7.21445 7.5866 7.17506 7.66532 7.0569 7.74404L6.56456 8.07861C6.48579 8.11797 6.42671 8.15733 6.36762 8.15733C6.28885 8.15733 6.21008 8.11797 6.1313 8.03925C6.01314 7.92117 5.93437 7.80308 5.85559 7.66532C5.77682 7.52756 5.69804 7.38979 5.61927 7.19299C5.00876 7.92117 4.24071 8.27542 3.29542 8.27542C2.62584 8.27542 2.1138 8.07861 1.71993 7.70468C1.32606 7.33075 1.12912 6.81906 1.12912 6.18928C1.12912 5.52014 1.36544 4.96908 1.83809 4.57547C2.31074 4.18186 2.96063 3.96538 3.76807 3.96538C4.04378 3.96538 4.31949 3.98506 4.5952 4.02442C4.8906 4.06378 5.18601 4.12282 5.50111 4.20154V3.63081C5.50111 3.04039 5.38294 2.60742 5.12693 2.37125C4.87091 2.13508 4.45734 2.017 3.84684 2.017C3.57113 2.017 3.29542 2.05636 3.00001 2.1154C2.70461 2.19413 2.4289 2.27285 2.15319 2.39093C2.03503 2.43029 1.93656 2.46965 1.87748 2.48933C1.8184 2.48933 1.77901 2.50901 1.75932 2.50901C1.64115 2.50901 1.60177 2.43029 1.60177 2.27285V1.87924C1.60177 1.76115 1.62146 1.66275 1.66085 1.60371C1.70024 1.54467 1.77901 1.48563 1.87748 1.44626C2.15319 1.3085 2.48798 1.19042 2.86216 1.09201C3.25603 0.993612 3.6499 0.93457 4.08316 0.93457C5.02846 0.93457 5.69804 1.15106 6.15099 1.56435C6.58425 1.99732 6.80088 2.6271 6.80088 3.51272L6.82058 6.05151ZM3.63021 7.25203C3.88623 7.25203 4.16194 7.21267 4.43765 7.11426C4.71336 7.01586 4.96938 6.83874 5.18601 6.60257C5.30417 6.44513 5.40264 6.28768 5.46172 6.09088C5.50111 5.89407 5.54049 5.67758 5.54049 5.40206V5.06749C5.30417 5.00845 5.06785 4.96908 4.81183 4.92972C4.55581 4.89036 4.31949 4.89036 4.06347 4.89036C3.53174 4.89036 3.15756 4.98876 2.88185 5.20525C2.62584 5.42174 2.48798 5.71695 2.48798 6.11056C2.48798 6.48449 2.58645 6.76001 2.78338 6.93714C2.98032 7.17331 3.25603 7.25203 3.63021 7.25203ZM9.95187 8.11797C9.81401 8.11797 9.71554 8.09829 9.65646 8.03925C9.59738 7.99989 9.5383 7.88181 9.49891 7.72436L7.64771 1.62339C7.60832 1.46595 7.56894 1.36754 7.56894 1.3085C7.56894 1.19042 7.62802 1.1117 7.76587 1.1117H8.53392C8.69147 1.1117 8.78994 1.13138 8.84902 1.19042C8.9081 1.22978 8.96718 1.34786 9.00657 1.50531L10.326 6.72065L11.547 1.50531C11.5864 1.34786 11.6258 1.24946 11.7046 1.19042C11.7637 1.15106 11.8818 1.1117 12.0197 1.1117H12.6499C12.8074 1.1117 12.9059 1.13138 12.965 1.19042C13.0241 1.22978 13.0832 1.34786 13.1225 1.50531L14.3632 6.7797L15.7221 1.50531C15.7615 1.34786 15.8206 1.24946 15.8796 1.19042C15.9387 1.15106 16.0372 1.1117 16.1947 1.1117H16.9234C17.0416 1.1117 17.1203 1.17074 17.1203 1.3085C17.1203 1.34786 17.1203 1.38722 17.1007 1.42658C17.1007 1.46595 17.081 1.54467 17.0416 1.62339L15.1313 7.72436C15.0919 7.88181 15.0328 7.98021 14.9737 8.03925C14.9147 8.07861 14.8162 8.11797 14.6783 8.11797H14.0088C13.8512 8.11797 13.7527 8.09829 13.6937 8.03925C13.6346 7.98021 13.5755 7.88181 13.5361 7.72436L12.3151 2.64678L11.0941 7.72436C11.0547 7.88181 11.0153 7.98021 10.9365 8.03925C10.8775 8.09829 10.7593 8.11797 10.6214 8.11797H9.95187ZM20.0941 8.31478C19.6805 8.31478 19.267 8.27542 18.8731 8.17702C18.4792 8.07861 18.1641 7.98021 17.9672 7.86213C17.849 7.7834 17.7505 7.70468 17.7308 7.64564C17.7112 7.5866 17.6718 7.4882 17.6718 7.42915V7.03554C17.6718 6.8781 17.7308 6.79938 17.849 6.79938C17.8884 6.79938 17.9475 6.79938 17.9869 6.81906C18.0263 6.83874 18.105 6.85842 18.1838 6.89778C18.4595 7.01586 18.7352 7.11426 19.0503 7.17331C19.3654 7.23235 19.6805 7.27171 19.9956 7.27171C20.488 7.27171 20.8818 7.19299 21.1378 7.01586C21.4136 6.83874 21.5514 6.58289 21.5514 6.268C21.5514 6.05151 21.4726 5.87439 21.3348 5.71695C21.1969 5.5595 20.9212 5.44142 20.547 5.30365L19.4048 4.9494C18.8337 4.77228 18.4004 4.49675 18.1444 4.1425C17.8884 3.78825 17.7505 3.41432 17.7505 3.00103C17.7505 2.66646 17.8293 2.37125 17.9672 2.13508C18.105 1.89892 18.302 1.66275 18.5383 1.48563C18.7746 1.3085 19.0503 1.17074 19.3654 1.07233C19.6805 0.973931 20.0153 0.93457 20.3501 0.93457C20.5273 0.93457 20.7046 0.93457 20.8818 0.973931C21.0591 0.993612 21.2363 1.03297 21.3939 1.05265C21.5514 1.09201 21.709 1.13138 21.8468 1.17074C21.9847 1.2101 22.1028 1.26914 22.1816 1.3085C22.2998 1.36754 22.3785 1.42658 22.4179 1.50531C22.4573 1.56435 22.4967 1.66275 22.4967 1.76115V2.13508C22.4967 2.29253 22.4376 2.39093 22.3195 2.39093C22.2604 2.39093 22.1619 2.35157 22.0241 2.29253C21.5711 2.09572 21.0788 1.97764 20.5077 1.97764C20.0547 1.97764 19.7002 2.05636 19.4639 2.19413C19.2276 2.33189 19.0897 2.56806 19.0897 2.90263C19.0897 3.11911 19.1685 3.31592 19.326 3.45368C19.4836 3.61113 19.779 3.74889 20.1926 3.88665L21.3151 4.2409C21.8862 4.41803 22.2998 4.67388 22.5361 4.98876C22.7724 5.30365 22.8906 5.67758 22.8906 6.09088C22.8906 6.42545 22.8118 6.74033 22.6936 6.99618C22.5558 7.27171 22.3589 7.50788 22.1225 7.685C21.8862 7.88181 21.5908 8.01957 21.256 8.11797C20.8621 8.27542 20.488 8.31478 20.0941 8.31478Z" fill="white"/>
<g id="Group_2">
<path id="Vector_2" fill-rule="evenodd" clip-rule="evenodd" d="M21.5908 12.1525C18.9912 14.0615 15.2101 15.0849 11.9803 15.0849C7.43108 15.0849 3.33481 13.4121 0.242906 10.6174C0.00658259 10.4009 0.223213 10.1057 0.518617 10.2632C3.86653 12.2115 7.9825 13.3727 12.256 13.3727C15.1313 13.3727 18.302 12.7823 21.2166 11.5424C21.6302 11.3653 22.0044 11.8376 21.5908 12.1525Z" fill="#FF9900"/>
<path id="Vector_3" fill-rule="evenodd" clip-rule="evenodd" d="M22.6543 10.9324C22.3195 10.4994 20.4683 10.7356 19.6214 10.834C19.3654 10.8734 19.326 10.6372 19.5624 10.4798C21.0394 9.43669 23.4814 9.7319 23.7571 10.0861C24.0328 10.4404 23.6783 12.8808 22.2998 14.0419C22.0831 14.2191 21.8862 14.1207 21.9847 13.8845C22.2998 13.0973 22.989 11.3457 22.6543 10.9324Z" fill="#FF9900"/>
</g>
</g>
<g id="Group_3">
<path id="Vector_4" d="M6.82058 6.05151C6.82058 6.34672 6.85997 6.58289 6.89935 6.76001C6.95843 6.93714 7.03721 7.11426 7.15537 7.33075C7.19476 7.38979 7.21445 7.44883 7.21445 7.50788C7.21445 7.5866 7.17506 7.66532 7.0569 7.74404L6.56456 8.07861C6.48579 8.11797 6.42671 8.15733 6.36762 8.15733C6.28885 8.15733 6.21008 8.11797 6.1313 8.03925C6.01314 7.92117 5.93437 7.80308 5.85559 7.66532C5.77682 7.52756 5.69804 7.38979 5.61927 7.19299C5.00876 7.92117 4.24071 8.27542 3.29542 8.27542C2.62584 8.27542 2.1138 8.07861 1.71993 7.70468C1.32606 7.33075 1.12912 6.81906 1.12912 6.18928C1.12912 5.52014 1.36544 4.96908 1.83809 4.57547C2.31074 4.18186 2.96063 3.96538 3.76807 3.96538C4.04378 3.96538 4.31949 3.98506 4.5952 4.02442C4.8906 4.06378 5.18601 4.12282 5.50111 4.20154V3.63081C5.50111 3.04039 5.38294 2.60742 5.12693 2.37125C4.87091 2.13508 4.45734 2.017 3.84684 2.017C3.57113 2.017 3.29542 2.05636 3.00001 2.1154C2.70461 2.19413 2.4289 2.27285 2.15319 2.39093C2.03503 2.43029 1.93656 2.46965 1.87748 2.48933C1.8184 2.48933 1.77901 2.50901 1.75932 2.50901C1.64115 2.50901 1.60177 2.43029 1.60177 2.27285V1.87924C1.60177 1.76115 1.62146 1.66275 1.66085 1.60371C1.70024 1.54467 1.77901 1.48563 1.87748 1.44626C2.15319 1.3085 2.48798 1.19042 2.86216 1.09201C3.25603 0.993612 3.6499 0.93457 4.08316 0.93457C5.02846 0.93457 5.69804 1.15106 6.15099 1.56435C6.58425 1.99732 6.80088 2.6271 6.80088 3.51272L6.82058 6.05151ZM3.63021 7.25203C3.88623 7.25203 4.16194 7.21267 4.43765 7.11426C4.71336 7.01586 4.96938 6.83874 5.18601 6.60257C5.30417 6.44513 5.40264 6.28768 5.46172 6.09088C5.50111 5.89407 5.54049 5.67758 5.54049 5.40206V5.06749C5.30417 5.00845 5.06785 4.96908 4.81183 4.92972C4.55581 4.89036 4.31949 4.89036 4.06347 4.89036C3.53174 4.89036 3.15756 4.98876 2.88185 5.20525C2.62584 5.42174 2.48798 5.71695 2.48798 6.11056C2.48798 6.48449 2.58645 6.76001 2.78338 6.93714C2.98032 7.17331 3.25603 7.25203 3.63021 7.25203ZM9.95187 8.11797C9.81401 8.11797 9.71554 8.09829 9.65646 8.03925C9.59738 7.99989 9.5383 7.88181 9.49891 7.72436L7.64771 1.62339C7.60832 1.46595 7.56894 1.36754 7.56894 1.3085C7.56894 1.19042 7.62802 1.1117 7.76587 1.1117H8.53392C8.69147 1.1117 8.78994 1.13138 8.84902 1.19042C8.9081 1.22978 8.96718 1.34786 9.00657 1.50531L10.326 6.72065L11.547 1.50531C11.5864 1.34786 11.6258 1.24946 11.7046 1.19042C11.7637 1.15106 11.8818 1.1117 12.0197 1.1117H12.6499C12.8074 1.1117 12.9059 1.13138 12.965 1.19042C13.0241 1.22978 13.0832 1.34786 13.1225 1.50531L14.3632 6.7797L15.7221 1.50531C15.7615 1.34786 15.8206 1.24946 15.8796 1.19042C15.9387 1.15106 16.0372 1.1117 16.1947 1.1117H16.9234C17.0416 1.1117 17.1203 1.17074 17.1203 1.3085C17.1203 1.34786 17.1203 1.38722 17.1007 1.42658C17.1007 1.46595 17.081 1.54467 17.0416 1.62339L15.1313 7.72436C15.0919 7.88181 15.0328 7.98021 14.9737 8.03925C14.9147 8.07861 14.8162 8.11797 14.6783 8.11797H14.0088C13.8512 8.11797 13.7527 8.09829 13.6937 8.03925C13.6346 7.98021 13.5755 7.88181 13.5361 7.72436L12.3151 2.64678L11.0941 7.72436C11.0547 7.88181 11.0153 7.98021 10.9365 8.03925C10.8775 8.09829 10.7593 8.11797 10.6214 8.11797H9.95187ZM20.0941 8.31478C19.6805 8.31478 19.267 8.27542 18.8731 8.17702C18.4792 8.07861 18.1641 7.98021 17.9672 7.86213C17.849 7.7834 17.7505 7.70468 17.7308 7.64564C17.7112 7.5866 17.6718 7.4882 17.6718 7.42915V7.03554C17.6718 6.8781 17.7308 6.79938 17.849 6.79938C17.8884 6.79938 17.9475 6.79938 17.9869 6.81906C18.0263 6.83874 18.105 6.85842 18.1838 6.89778C18.4595 7.01586 18.7352 7.11426 19.0503 7.17331C19.3654 7.23235 19.6805 7.27171 19.9956 7.27171C20.488 7.27171 20.8818 7.19299 21.1378 7.01586C21.4136 6.83874 21.5514 6.58289 21.5514 6.268C21.5514 6.05151 21.4726 5.87439 21.3348 5.71695C21.1969 5.5595 20.9212 5.44142 20.547 5.30365L19.4048 4.9494C18.8337 4.77228 18.4004 4.49675 18.1444 4.1425C17.8884 3.78825 17.7505 3.41432 17.7505 3.00103C17.7505 2.66646 17.8293 2.37125 17.9672 2.13508C18.105 1.89892 18.302 1.66275 18.5383 1.48563C18.7746 1.3085 19.0503 1.17074 19.3654 1.07233C19.6805 0.973931 20.0153 0.93457 20.3501 0.93457C20.5273 0.93457 20.7046 0.93457 20.8818 0.973931C21.0591 0.993612 21.2363 1.03297 21.3939 1.05265C21.5514 1.09201 21.709 1.13138 21.8468 1.17074C21.9847 1.2101 22.1028 1.26914 22.1816 1.3085C22.2998 1.36754 22.3785 1.42658 22.4179 1.50531C22.4573 1.56435 22.4967 1.66275 22.4967 1.76115V2.13508C22.4967 2.29253 22.4376 2.39093 22.3195 2.39093C22.2604 2.39093 22.1619 2.35157 22.0241 2.29253C21.5711 2.09572 21.0788 1.97764 20.5077 1.97764C20.0547 1.97764 19.7002 2.05636 19.4639 2.19413C19.2276 2.33189 19.0897 2.56806 19.0897 2.90263C19.0897 3.11911 19.1685 3.31592 19.326 3.45368C19.4836 3.61113 19.779 3.74889 20.1926 3.88665L21.3151 4.2409C21.8862 4.41803 22.2998 4.67388 22.5361 4.98876C22.7724 5.30365 22.8906 5.67758 22.8906 6.09088C22.8906 6.42545 22.8118 6.74033 22.6936 6.99618C22.5558 7.27171 22.3589 7.50788 22.1225 7.685C21.8862 7.88181 21.5908 8.01957 21.256 8.11797C20.8621 8.27542 20.488 8.31478 20.0941 8.31478Z" fill="white"/>
<g id="Group_4">
<path id="Vector_5" fill-rule="evenodd" clip-rule="evenodd" d="M21.5908 12.1525C18.9912 14.0615 15.2101 15.0849 11.9803 15.0849C7.43108 15.0849 3.33481 13.4121 0.242906 10.6174C0.00658259 10.4009 0.223213 10.1057 0.518617 10.2632C3.86653 12.2115 7.9825 13.3727 12.256 13.3727C15.1313 13.3727 18.302 12.7823 21.2166 11.5424C21.6302 11.3653 22.0044 11.8376 21.5908 12.1525Z" fill="#FF9900"/>
<path id="Vector_6" fill-rule="evenodd" clip-rule="evenodd" d="M22.6543 10.9324C22.3195 10.4994 20.4683 10.7356 19.6214 10.834C19.3654 10.8734 19.326 10.6372 19.5624 10.4798C21.0394 9.43669 23.4814 9.7319 23.7571 10.0861C24.0328 10.4404 23.6783 12.8808 22.2998 14.0419C22.0831 14.2191 21.8862 14.1207 21.9847 13.8845C22.2998 13.0973 22.989 11.3457 22.6543 10.9324Z" fill="#FF9900"/>
</g>
</g>
</g>
<defs>
<clipPath id="clip0_2552_26387">
<rect width="23.7111" height="14.17" fill="white" transform="translate(0.14444 0.915039)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
<path fill="#252F3E" d="M4.51 7.687c0 .197.02.357.058.475.042.117.096.245.17.384a.233.233 0 01.037.123c0 .053-.032.107-.1.16l-.336.224a.255.255 0 01-.138.048c-.054 0-.107-.026-.16-.074a1.652 1.652 0 01-.192-.251 4.137 4.137 0 01-.165-.315c-.415.491-.936.737-1.564.737-.447 0-.804-.129-1.064-.385-.261-.256-.394-.598-.394-1.025 0-.454.16-.822.484-1.1.325-.278.756-.416 1.304-.416.18 0 .367.016.564.042.197.027.4.07.612.118v-.39c0-.406-.085-.689-.25-.854-.17-.166-.458-.246-.868-.246-.186 0-.377.022-.574.07a4.23 4.23 0 00-.575.181 1.525 1.525 0 01-.186.07.326.326 0 01-.085.016c-.075 0-.112-.054-.112-.166v-.262c0-.085.01-.15.037-.186a.399.399 0 01.15-.113c.185-.096.409-.176.67-.24.26-.07.537-.101.83-.101.633 0 1.096.144 1.394.432.293.288.442.726.442 1.314v1.73h.01zm-2.161.811c.175 0 .356-.032.548-.096.191-.064.362-.182.505-.342a.848.848 0 00.181-.341c.032-.129.054-.283.054-.465V7.03a4.43 4.43 0 00-.49-.09 3.996 3.996 0 00-.5-.033c-.357 0-.618.07-.793.214-.176.144-.26.347-.26.614 0 .25.063.437.196.566.128.133.314.197.559.197zm4.273.577c-.096 0-.16-.016-.202-.054-.043-.032-.08-.106-.112-.208l-1.25-4.127a.938.938 0 01-.049-.214c0-.085.043-.133.128-.133h.522c.1 0 .17.016.207.053.043.032.075.107.107.208l.894 3.535.83-3.535c.026-.106.058-.176.1-.208a.365.365 0 01.214-.053h.425c.102 0 .17.016.213.053.043.032.08.107.101.208l.841 3.578.92-3.578a.458.458 0 01.107-.208.346.346 0 01.208-.053h.495c.085 0 .133.043.133.133 0 .027-.006.054-.01.086a.76.76 0 01-.038.133l-1.283 4.127c-.032.107-.069.177-.111.209a.34.34 0 01-.203.053h-.457c-.101 0-.17-.016-.213-.053-.043-.038-.08-.107-.101-.214L8.213 5.37l-.82 3.439c-.026.107-.058.176-.1.213-.043.038-.118.054-.213.054h-.458zm6.838.144a3.51 3.51 0 01-.82-.096c-.266-.064-.473-.134-.612-.214-.085-.048-.143-.101-.165-.15a.378.378 0 01-.031-.149v-.272c0-.112.042-.166.122-.166a.3.3 0 01.096.016c.032.011.08.032.133.054.18.08.378.144.585.187.213.042.42.064.633.064.336 0 .596-.059.777-.176a.575.575 0 00.277-.508.52.52 0 00-.144-.373c-.095-.102-.276-.193-.537-.278l-.772-.24c-.388-.123-.676-.305-.851-.545a1.275 1.275 0 01-.266-.774c0-.224.048-.422.143-.593.096-.17.224-.32.384-.438.16-.122.34-.213.553-.277.213-.064.436-.091.67-.091.118 0 .24.005.357.021.122.016.234.038.346.06.106.026.208.052.303.085.096.032.17.064.224.096a.46.46 0 01.16.133.289.289 0 01.047.176v.251c0 .112-.042.171-.122.171a.552.552 0 01-.202-.064 2.427 2.427 0 00-1.022-.208c-.303 0-.543.048-.708.15-.165.1-.25.256-.25.475 0 .149.053.277.16.379.106.101.303.202.585.293l.756.24c.383.123.66.294.825.513.165.219.244.47.244.748 0 .23-.047.437-.138.619a1.436 1.436 0 01-.388.47c-.165.133-.362.23-.591.299-.24.075-.49.112-.761.112z"/>
<g fill="#F90" fill-rule="evenodd" clip-rule="evenodd">
<path d="M14.465 11.813c-1.75 1.297-4.294 1.986-6.481 1.986-3.065 0-5.827-1.137-7.913-3.027-.165-.15-.016-.353.18-.235 2.257 1.313 5.04 2.109 7.92 2.109 1.941 0 4.075-.406 6.039-1.239.293-.133.543.192.255.406z"/>
<path d="M15.194 10.98c-.223-.287-1.479-.138-2.048-.069-.17.022-.197-.128-.043-.24 1-.705 2.645-.502 2.836-.267.192.24-.053 1.89-.99 2.68-.143.123-.281.06-.218-.1.213-.53.687-1.72.463-2.003z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,76 @@
import axios from 'api';
import {
CloudAccount,
Service,
ServiceData,
UpdateServiceConfigPayload,
UpdateServiceConfigResponse,
} from 'container/CloudIntegrationPage/ServicesSection/types';
import {
AccountConfigPayload,
AccountConfigResponse,
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;
};
export const getAwsServices = async (
accountId?: string,
): Promise<Service[]> => {
const params = accountId ? { account_id: accountId } : undefined;
const response = await axios.get('/cloud-integrations/aws/services', {
params,
});
return response.data.data.services;
};
export const getServiceDetails = async (
serviceId: string,
accountId?: string,
): Promise<ServiceData> => {
const params = accountId ? { account_id: accountId } : undefined;
const response = await axios.get(
`/cloud-integrations/aws/services/${serviceId}`,
{ params },
);
return response.data.data;
};
export const generateConnectionUrl = async (params: {
agent_config: { region: string };
account_config: { regions: string[] };
account_id?: string;
}): Promise<ConnectionUrlResponse> => {
const response = await axios.post(
'/cloud-integrations/aws/accounts/generate-connection-url',
params,
);
return response.data.data;
};
export const updateAccountConfig = async (
accountId: string,
payload: AccountConfigPayload,
): Promise<AccountConfigResponse> => {
const response = await axios.post<AccountConfigResponse>(
`/cloud-integrations/aws/accounts/${accountId}/config`,
payload,
);
return response.data;
};
export const updateServiceConfig = async (
serviceId: string,
payload: UpdateServiceConfigPayload,
): Promise<UpdateServiceConfigResponse> => {
const response = await axios.post<UpdateServiceConfigResponse>(
`/cloud-integrations/aws/services/${serviceId}/config`,
payload,
);
return response.data;
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,117 @@
.signoz-modal {
.ant-modal-content {
padding: 0;
background: var(--bg-ink-400);
border: 1px solid var(--bg-slate-500);
}
.ant-modal-header {
background: var(--bg-ink-400);
border-bottom: none;
padding: 12px 24px 8px;
border-bottom: 1px solid var(--bg-slate-500);
margin-bottom: 0;
}
.ant-modal-close {
top: 15px;
height: 14px;
width: 14px;
.ant-modal-close-x {
font-size: 12px;
}
}
.ant-modal-title {
color: var(--bg-vanilla-100);
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
.ant-modal-body {
padding: 16px 24px 24px;
}
.ant-modal-footer {
padding: 24px;
display: flex;
justify-content: end;
gap: 12px;
}
.ant-typography {
color: var(--bg-vanilla-100);
}
.ant-select {
border-radius: 2px !important;
&-selector {
background: var(--bg-ink-300) !important;
border: 1px solid var(--bg-slate-400) !important;
border-radius: 2px !important;
}
}
.ant-select-dropdown {
background: var(--bg-ink-400);
border: 1px solid var(--bg-ink-300);
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
}
.ant-select-item {
color: var(--bg-vanilla-100);
&-option-selected {
background: var(--bg-ink-300);
}
}
}
.lightMode {
.signoz-modal {
.ant-modal-content {
background: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-300);
}
.ant-modal-header {
background: var(--bg-vanilla-100);
border-bottom-color: var(--bg-vanilla-300);
}
.ant-modal-title {
color: var(--bg-ink-500);
}
.ant-typography {
color: var(--bg-ink-500);
}
.ant-select {
&-selector {
background: var(--bg-vanilla-100) !important;
border-color: var(--bg-vanilla-300) !important;
}
}
.ant-select-dropdown {
background: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-300);
}
.ant-select-item {
color: var(--bg-ink-400);
&-option-selected {
background: var(--bg-vanilla-300);
color: var(--bg-ink-500);
}
&-option-active {
background: var(--bg-vanilla-200);
}
}
}
}

View File

@ -0,0 +1,25 @@
import './SignozModal.style.scss';
import { Modal, ModalProps } from 'antd';
function SignozModal({
children,
rootClassName = '',
...rest
}: ModalProps): JSX.Element {
return (
<Modal
centered
width={672}
cancelText="Close"
rootClassName={`signoz-modal ${rootClassName}`}
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
>
{children}
</Modal>
);
}
export default SignozModal;

View File

@ -32,5 +32,14 @@ export const REACT_QUERY_KEY = {
GET_JOB_LIST: 'GET_JOB_LIST',
GET_DAEMONSET_LIST: 'GET_DAEMONSET_LIST,',
GET_VOLUME_LIST: 'GET_VOLUME_LIST',
// AWS Integration Query Keys
AWS_ACCOUNTS: 'AWS_ACCOUNTS',
AWS_SERVICES: 'AWS_SERVICES',
AWS_SERVICE_DETAILS: 'AWS_SERVICE_DETAILS',
AWS_ACCOUNT_STATUS: 'AWS_ACCOUNT_STATUS',
AWS_UPDATE_ACCOUNT_CONFIG: 'AWS_UPDATE_ACCOUNT_CONFIG',
AWS_UPDATE_SERVICE_CONFIG: 'AWS_UPDATE_SERVICE_CONFIG',
AWS_GENERATE_CONNECTION_URL: 'AWS_GENERATE_CONNECTION_URL',
GET_ATTRIBUTE_VALUES: 'GET_ATTRIBUTE_VALUES',
};

View File

@ -22,6 +22,7 @@ import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { isNull } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { INTEGRATION_TYPES } from 'pages/Integrations/utils';
import { useAppContext } from 'providers/App/App';
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { Helmet } from 'react-helmet-async';
@ -292,6 +293,11 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
routeKey === 'MESSAGING_QUEUES_CELERY_TASK' ||
routeKey === 'MESSAGING_QUEUES_OVERVIEW';
const isCloudIntegrationPage = (): boolean =>
routeKey === 'INTEGRATIONS' &&
new URLSearchParams(window.location.search).get('integration') ===
INTEGRATION_TYPES.AWS_INTEGRATION;
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY';
const isAlertOverview = (): boolean => routeKey === 'ALERT_OVERVIEW';
@ -426,6 +432,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isAlertHistory() ||
isAlertOverview() ||
isMessagingQueues() ||
isCloudIntegrationPage() ||
isInfraMonitoring()
? 0
: '0 1rem',

View File

@ -0,0 +1,15 @@
import Header from './Header/Header';
import HeroSection from './HeroSection/HeroSection';
import ServicesTabs from './ServicesSection/ServicesTabs';
function CloudIntegrationPage(): JSX.Element {
return (
<div>
<Header />
<HeroSection />
<ServicesTabs />
</div>
);
}
export default CloudIntegrationPage;

View File

@ -0,0 +1,65 @@
.cloud-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 18px;
border-bottom: 1px solid var(--bg-slate-400);
&__navigation {
display: flex;
align-items: center;
padding: 6px 0px 6px;
}
&__breadcrumb-link {
display: flex;
align-items: center;
gap: 8px;
}
&__breadcrumb-title {
color: var(--bg-vanilla-400);
font-size: 14px;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
&__help {
display: flex;
align-items: center;
justify-content: center;
padding: 6px;
gap: 6px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
border-radius: 2px;
color: var(--bg-vanilla-400);
font-size: 12px;
line-height: 10px; /* 83.333% */
letter-spacing: 0.12px;
width: 113px;
height: 32px;
cursor: pointer;
}
}
.lightMode {
.cloud-header {
border-bottom: 1px solid var(--bg-slate-300);
&__breadcrumb-title {
color: var(--bg-ink-400);
}
&__help {
border-color: var(--bg-slate-300);
background: var(--bg-vanilla-100);
color: var(--bg-ink-400);
&:hover {
border-color: var(--bg-slate-400);
color: var(--bg-ink-500);
}
}
}
}

View File

@ -0,0 +1,43 @@
import './Header.styles.scss';
import Breadcrumb from 'antd/es/breadcrumb';
import ROUTES from 'constants/routes';
import { Blocks, LifeBuoy } from 'lucide-react';
import { Link } from 'react-router-dom';
function Header(): JSX.Element {
return (
<div className="cloud-header">
<div className="cloud-header__navigation">
<Breadcrumb
className="cloud-header__breadcrumb"
items={[
{
title: (
<Link to={ROUTES.INTEGRATIONS}>
<span className="cloud-header__breadcrumb-link">
<Blocks size={16} color="var(--bg-vanilla-400)" />
<span className="cloud-header__breadcrumb-title">Integrations</span>
</span>
</Link>
),
},
{
title: (
<div className="cloud-header__breadcrumb-title">AWS web services</div>
),
},
]}
/>
</div>
<div className="cloud-header__actions">
<button className="cloud-header__help" type="button">
<LifeBuoy size={12} />
Get Help
</button>
</div>
</div>
);
}
export default Header;

View File

@ -0,0 +1,72 @@
.hero-section {
height: 308px;
padding: 26px 16px;
display: flex;
gap: 24px;
position: relative;
overflow: hidden;
background-position: right;
background-size: cover;
background-repeat: no-repeat;
border-bottom: 1px solid var(--bg-slate-500);
&__icon {
height: fit-content;
background-color: var(--bg-ink-400);
padding: 12px;
border: 1px solid var(--bg-ink-300);
border-radius: 6px;
width: 60px;
height: 60px;
display: flex;
align-items: center;
img {
width: 100%;
}
}
&__details {
display: flex;
flex-direction: column;
gap: 12px;
.title {
color: var(--bg-vanilla-100);
font-size: 24px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.12px;
}
.description {
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 400;
line-height: 18px;
}
}
}
.lightMode {
.hero-section {
border-bottom: 1px solid var(--bg-vanilla-300);
&__icon {
background-color: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-300);
}
&__details {
.title {
color: var(--bg-ink-500);
}
.description {
color: var(--bg-ink-400);
}
}
}
}
.integrations-select {
height: 44px;
}

View File

@ -0,0 +1,34 @@
import './HeroSection.style.scss';
import { useIsDarkMode } from 'hooks/useDarkMode';
import AccountActions from './components/AccountActions';
function HeroSection(): JSX.Element {
const isDarkMode = useIsDarkMode();
return (
<div
className="hero-section"
style={
isDarkMode
? {
backgroundImage: `url('/Images/integrations-hero-bg.png')`,
}
: {}
}
>
<div className="hero-section__icon">
<img src="/Logos/aws-dark.svg" alt="aws-logo" />
</div>
<div className="hero-section__details">
<div className="title">AWS Web Services</div>
<div className="description">
One-click setup for AWS monitoring with SigNoz
</div>
<AccountActions />
</div>
</div>
);
}
export default HeroSection;

View File

@ -0,0 +1,127 @@
.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);
}
&.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;
}
}
.cloud-account-selector {
border-radius: 2px;
border: 1px solid var(--bg-ink-300);
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
rgba(18, 19, 23, 0.9) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
.ant-select-selector {
border-color: var(--bg-slate-400) !important;
background: var(--bg-ink-300) !important;
padding: 6px 8px !important;
}
.ant-select-selection-item {
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 400;
line-height: 16px;
}
.account-option-item {
display: flex;
align-items: center;
justify-content: space-between;
&__selected {
display: flex;
align-items: center;
justify-content: center;
height: 14px;
width: 14px;
background-color: rgba(192, 193, 195, 0.2); /* #C0C1C3 with 0.2 opacity */
border-radius: 2px;
}
}
}
.lightMode {
.hero-section__action-button {
&.primary {
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
}
&.secondary {
border-color: var(--bg-vanilla-300);
color: var(--bg-ink-400);
background: var(--bg-vanilla-100);
&:hover {
border-color: var(--bg-vanilla-400);
color: var(--bg-ink-500);
}
}
}
.cloud-account-selector {
background: var(--bg-vanilla-100);
.ant-select-selector {
background: var(--bg-vanilla-100) !important;
border-color: var(--bg-vanilla-400) !important;
}
.ant-select-item-option-active {
background: var(--bg-vanilla-400) !important;
}
.ant-select-selection-item {
color: var(--bg-ink-400);
}
&:hover {
.ant-select-selector {
border-color: var(--bg-vanilla-400) !important;
}
}
}
.account-option-item {
color: var(--bg-ink-400);
&__selected {
background: var(--bg-robin-500);
}
}
}

View File

@ -0,0 +1,159 @@
import './AccountActions.style.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Select } from 'antd';
import { SelectProps } from 'antd/lib';
import { useAwsAccounts } from 'hooks/integrations/aws/useAwsAccounts';
import useUrlQuery from 'hooks/useUrlQuery';
import { Check, ChevronDown } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom-v5-compat';
import { CloudAccount } from '../../ServicesSection/types';
import AccountSettingsModal from './AccountSettingsModal';
import CloudAccountSetupModal from './CloudAccountSetupModal';
interface AccountOptionItemProps {
label: React.ReactNode;
isSelected: boolean;
}
function AccountOptionItem({
label,
isSelected,
}: AccountOptionItemProps): JSX.Element {
return (
<div className="account-option-item">
{label}
{isSelected && (
<div className="account-option-item__selected">
<Check size={12} color={Color.BG_VANILLA_100} />
</div>
)}
</div>
);
}
function renderOption(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
option: any,
activeAccountId: string | undefined,
): JSX.Element {
return (
<AccountOptionItem
label={option.label}
isSelected={option.value === activeAccountId}
/>
);
}
const getAccountById = (
accounts: CloudAccount[],
accountId: string,
): CloudAccount | null =>
accounts.find((account) => account.cloud_account_id === accountId) || null;
function AccountActions(): JSX.Element {
const urlQuery = useUrlQuery();
const navigate = useNavigate();
const { data: accounts } = useAwsAccounts();
const initialAccount = useMemo(
() =>
accounts?.length
? getAccountById(accounts, urlQuery.get('accountId') || '') || accounts[0]
: null,
[accounts, urlQuery],
);
const [activeAccount, setActiveAccount] = useState<CloudAccount | null>(
initialAccount,
);
// Update state when initial value changes
useEffect(() => {
if (initialAccount !== null) {
setActiveAccount(initialAccount);
urlQuery.set('accountId', initialAccount.cloud_account_id);
navigate({ search: urlQuery.toString() });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialAccount]);
const [isIntegrationModalOpen, setIsIntegrationModalOpen] = useState(false);
const [isAccountSettingsModalOpen, setIsAccountSettingsModalOpen] = useState(
false,
);
const selectOptions: SelectProps['options'] = useMemo(
() =>
accounts?.length
? accounts.map((account) => ({
value: account.cloud_account_id,
label: account.cloud_account_id,
}))
: [],
[accounts],
);
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>
)}
<CloudAccountSetupModal
isOpen={isIntegrationModalOpen}
onClose={(): void => setIsIntegrationModalOpen(false)}
/>
<AccountSettingsModal
isOpen={isAccountSettingsModalOpen}
onClose={(): void => setIsAccountSettingsModalOpen(false)}
account={activeAccount as CloudAccount}
setActiveAccount={setActiveAccount}
/>
</div>
);
}
export default AccountActions;

View File

@ -0,0 +1,189 @@
.account-settings-modal {
&__title-account-id {
color: var(--bg-vanilla-100);
font-family: 'Geist Mono';
font-size: 14px;
font-weight: 600;
line-height: 20px;
}
&__body {
display: flex;
flex-direction: column;
gap: 17px;
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
padding: 14px;
&-account-info {
&-connected-account-details {
&-title {
color: var(--bg-vanilla-100);
font-size: 14px;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
&-account-id {
color: var(--bg-vanilla-400);
font-size: 12px;
line-height: 18px;
letter-spacing: -0.06px;
&-account-id {
font-family: 'Geist Mono';
font-size: 12px;
font-weight: 700;
line-height: 18px;
letter-spacing: -0.06px;
}
}
}
}
&-regions-switch {
display: flex;
flex-direction: column;
gap: 10px;
&-title {
color: var(--bg-vanilla-100);
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
}
&-switch {
display: flex;
align-items: center;
gap: 10px;
&-label {
color: var(--bg-vanilla-400);
background-color: transparent;
border: none;
font-family: Inter;
font-size: 12px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.005em;
cursor: pointer;
}
}
}
&-regions-select {
margin-top: 8px;
}
}
&__footer {
&-close-button,
&-save-button {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 12px;
font-weight: 500;
padding-left: 16px;
padding-right: 16px;
}
&-close-button {
border-radius: 2px;
background: var(--bg-slate-400);
border: none;
}
&-save-button {
&:disabled {
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
opacity: 0.6;
border: none;
}
border-radius: 2px;
margin: 0 !important;
}
}
.ant-modal-body {
padding-bottom: 0 !important;
}
.ant-modal-footer {
margin: 0;
padding: 24px 24px 12px;
}
.integration-detail-content {
margin: 0;
}
}
.lightMode {
.account-settings-modal {
&__title-account-id {
color: var(--bg-ink-500);
}
&__body {
border-color: var(--bg-vanilla-300);
&-account-info {
&-connected-account-details {
&-title {
color: var(--bg-ink-500);
}
&-account-id {
color: var(--bg-ink-400);
&-account-id {
color: var(--bg-ink-500);
}
}
}
}
&-regions-switch {
&-title {
color: var(--bg-ink-500);
}
&-switch {
&-label {
color: var(--bg-ink-400);
&:hover {
color: var(--bg-ink-500);
}
}
}
}
}
&__footer {
&-close-button,
&-save-button {
color: var(--bg-vanilla-100);
}
&-close-button {
background: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300);
color: var(--bg-ink-400);
&:hover {
border-color: var(--bg-vanilla-400);
color: var(--bg-ink-500);
}
}
&-save-button {
// Keep primary button same as dark mode
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
&:disabled {
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
opacity: 0.6;
}
&:not(:disabled):hover {
background: var(--bg-robin-400);
}
}
}
}
}

View File

@ -0,0 +1,181 @@
import './AccountSettingsModal.style.scss';
import { Form, Select, Switch } from 'antd';
import SignozModal from 'components/SignozModal/SignozModal';
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 { Dispatch, SetStateAction, useCallback } from 'react';
import { CloudAccount } from '../../ServicesSection/types';
import { RegionSelector } from './RegionSelector';
interface AccountSettingsModalProps {
isOpen: boolean;
onClose: () => void;
account: CloudAccount;
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
}
function AccountSettingsModal({
isOpen,
onClose,
account,
setActiveAccount,
}: AccountSettingsModalProps): JSX.Element {
const {
form,
isLoading,
selectedRegions,
includeAllRegions,
isRegionSelectOpen,
isSaveDisabled,
setSelectedRegions,
setIncludeAllRegions,
setIsRegionSelectOpen,
handleIncludeAllRegionsChange,
handleSubmit,
handleClose,
} = useAccountSettingsModal({ onClose, account, setActiveAccount });
const renderRegionSelector = useCallback(() => {
if (isRegionSelectOpen) {
return (
<RegionSelector
selectedRegions={selectedRegions}
setSelectedRegions={setSelectedRegions}
setIncludeAllRegions={setIncludeAllRegions}
/>
);
}
return (
<>
<div className="account-settings-modal__body-regions-switch-switch ">
<Switch
checked={includeAllRegions}
onChange={handleIncludeAllRegionsChange}
/>
<button
className="account-settings-modal__body-regions-switch-switch-label"
type="button"
onClick={(): void => handleIncludeAllRegionsChange(!includeAllRegions)}
>
Include all regions
</button>
</div>
<Select
suffixIcon={null}
placeholder="Select Region(s)"
className="cloud-account-setup-form__select account-settings-modal__body-regions-select integrations-select"
onClick={(): void => setIsRegionSelectOpen(true)}
mode="multiple"
maxTagCount={3}
value={getRegionPreviewText(selectedRegions)}
open={false}
/>
</>
);
}, [
isRegionSelectOpen,
selectedRegions,
includeAllRegions,
handleIncludeAllRegionsChange,
setIsRegionSelectOpen,
setSelectedRegions,
setIncludeAllRegions,
]);
const renderAccountDetails = useCallback(
() => (
<div className="account-settings-modal__body-account-info">
<div className="account-settings-modal__body-account-info-connected-account-details">
<div className="account-settings-modal__body-account-info-connected-account-details-title">
Connected Account details
</div>
<div className="account-settings-modal__body-account-info-connected-account-details-account-id">
AWS Account:{' '}
<span className="account-settings-modal__body-account-info-connected-account-details-account-id-account-id">
{account?.id}
</span>
</div>
</div>
</div>
),
[account?.id],
);
const modalTitle = (
<div className="account-settings-modal__title">
Account settings for{' '}
<span className="account-settings-modal__title-account-id">
{account?.id}
</span>
</div>
);
return (
<SignozModal
open={isOpen}
title={modalTitle}
onCancel={handleClose}
onOk={handleSubmit}
okText="Save"
okButtonProps={{
disabled: isSaveDisabled,
className: 'account-settings-modal__footer-save-button',
loading: isLoading,
}}
cancelButtonProps={{
className: 'account-settings-modal__footer-close-button',
}}
width={672}
rootClassName="account-settings-modal"
>
<Form
form={form}
layout="vertical"
initialValues={{
selectedRegions,
includeAllRegions,
}}
>
<div className="account-settings-modal__body">
{renderAccountDetails()}
<Form.Item
name="selectedRegions"
rules={[
{
validator: async (): Promise<void> => {
if (selectedRegions.length === 0) {
throw new Error('Please select at least one region to monitor');
}
},
message: 'Please select at least one region to monitor',
},
]}
>
{renderRegionSelector()}
</Form.Item>
<div className="integration-detail-content">
<IntergrationsUninstallBar
integrationTitle={AWS_INTEGRATION.title}
integrationId={AWS_INTEGRATION.id}
onUnInstallSuccess={handleClose}
removeIntegrationTitle="Remove"
connectionStatus={ConnectionStates.Connected}
/>
</div>
</div>
</Form>
</SignozModal>
);
}
export default AccountSettingsModal;

View File

@ -0,0 +1,53 @@
import { Color } from '@signozhq/design-tokens';
import { Alert, Spin } from 'antd';
import { LoaderCircle, TriangleAlert } from 'lucide-react';
import { ModalStateEnum } from '../types';
function AlertMessage({
modalState,
}: {
modalState: ModalStateEnum;
}): JSX.Element | null {
switch (modalState) {
case ModalStateEnum.WAITING:
return (
<Alert
message={
<div className="cloud-account-setup-form__alert-message">
<Spin
indicator={
<LoaderCircle
size={14}
color={Color.BG_AMBER_400}
className="anticon anticon-loading anticon-spin ant-spin-dot"
/>
}
/>
Waiting for connection, retrying in{' '}
<span className="retry-time">10</span> secs...
</div>
}
className="cloud-account-setup-form__alert"
type="warning"
/>
);
case ModalStateEnum.ERROR:
return (
<Alert
message={
<div className="cloud-account-setup-form__alert-message">
<TriangleAlert type="solid" size={15} color={Color.BG_SAKURA_400} />
{`We couldn't establish a connection to your AWS account. Please try again`}
</div>
}
type="error"
className="cloud-account-setup-form__alert"
/>
);
default:
return null;
}
}
export default AlertMessage;

View File

@ -0,0 +1,221 @@
.cloud-account-setup-modal {
.account-setup-modal-footer {
&__confirm-button {
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
font-size: 12px;
font-weight: 500;
}
&__confirm-selection-count {
font-family: 'Geist Mono';
}
&__close-button {
background: var(--bg-slate-400);
border-radius: 2px;
color: var(--bg-vanilla-100);
font-family: 'Inter';
font-size: 12px;
font-weight: 500;
}
}
.cloud-account-setup-form {
.disabled {
opacity: 0.4;
}
&,
&__content {
display: flex;
flex-direction: column;
gap: 38px;
}
&__alert {
&.ant-alert {
padding: 12px;
border-radius: 6px;
font-size: 14px;
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
}
&.ant-alert-error {
color: var(--bg-sakura-400);
border: 1px solid rgba(242, 71, 105, 0.1);
background: rgba(242, 71, 105, 0.1);
}
&.ant-alert-warning {
color: var(--bg-amber-400);
border: 1px solid rgba(255, 205, 86, 0.1);
background: rgba(255, 205, 86, 0.1);
}
&-message {
display: flex;
align-items: center;
gap: 8px;
.retry-time {
font-family: 'Geist Mono';
font-size: 14px;
font-weight: 600;
line-height: 22px;
letter-spacing: -0.07px;
}
}
}
&__form-group {
display: flex;
flex-direction: column;
gap: 12px;
}
&__title {
color: var(--bg-vanilla-100);
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
}
&__description {
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.06px;
}
&__select {
.ant-select-selection-item {
color: var(--bg-vanilla-100);
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
}
}
&__form-item {
margin: 0;
}
&__include-all-regions-switch {
display: flex;
align-items: center;
gap: 10px;
color: var(--bg-vanilla-400);
font-size: 12px;
line-height: 18px;
letter-spacing: -0.06px;
margin-bottom: 12px;
&-label {
background-color: transparent;
border: none;
color: var(--bg-vanilla-400);
font-size: 12px;
line-height: 18px;
letter-spacing: -0.06px;
cursor: pointer;
}
}
&__note {
padding: 12px;
color: var(--bg-robin-400);
font-size: 12px;
line-height: 22px;
letter-spacing: -0.06px;
border-radius: 4px;
border: 1px solid rgba(78, 116, 248, 0.1);
background: rgba(78, 116, 248, 0.1);
}
&__submit-button {
border-radius: 2px;
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
font-size: 14px;
font-weight: 500;
line-height: 20px;
&-content {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
&:disabled {
opacity: 0.4;
}
}
}
}
.lightMode {
.cloud-account-setup-modal {
.account-setup-modal-footer {
&__confirm-button {
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
}
&__close-button {
background: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300);
color: var(--bg-ink-400);
&:hover {
border-color: var(--bg-vanilla-400);
color: var(--bg-ink-500);
}
}
}
.cloud-account-setup-form {
&__title {
color: var(--bg-ink-500);
}
&__description {
color: var(--bg-ink-400);
}
&__select {
.ant-select-selection-item {
color: var(--bg-ink-500);
}
}
&__include-all-regions-switch {
color: var(--bg-ink-400);
&-label {
color: var(--bg-ink-400);
&:hover {
color: var(--bg-ink-500);
}
}
}
&__note {
color: var(--bg-robin-500);
border: 1px solid rgba(78, 116, 248, 0.2);
background: rgba(78, 116, 248, 0.1);
}
&__submit-button {
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
}
&__alert {
&.ant-alert-error {
color: var(--bg-cherry-500);
border: 1px solid rgba(242, 71, 105, 0.2);
background: rgba(242, 71, 105, 0.1);
}
&.ant-alert-warning {
color: var(--bg-amber-500);
border: 1px solid rgba(255, 205, 86, 0.2);
background: rgba(255, 205, 86, 0.1);
}
&-message {
.retry-time {
color: var(--bg-ink-500);
}
}
}
}
}
}

View File

@ -0,0 +1,185 @@
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,
IntegrationModalProps,
ModalStateEnum,
} from '../types';
import { RegionForm } from './RegionForm';
import { RegionSelector } from './RegionSelector';
import { SuccessView } from './SuccessView';
function CloudAccountSetupModal({
isOpen,
onClose,
}: IntegrationModalProps): JSX.Element {
const {
form,
modalState,
setModalState,
isLoading,
activeView,
selectedRegions,
includeAllRegions,
isGeneratingUrl,
setSelectedRegions,
setIncludeAllRegions,
handleIncludeAllRegionsChange,
handleRegionSelect,
handleSubmit,
handleClose,
setActiveView,
allRegions,
accountId,
selectedDeploymentRegion,
handleRegionChange,
} = useIntegrationModal({ onClose });
const renderContent = useCallback(() => {
if (modalState === ModalStateEnum.SUCCESS) {
return <SuccessView />;
}
if (activeView === ActiveViewEnum.SELECT_REGIONS) {
return (
<RegionSelector
selectedRegions={selectedRegions}
setSelectedRegions={setSelectedRegions}
setIncludeAllRegions={setIncludeAllRegions}
/>
);
}
return (
<RegionForm
form={form}
modalState={modalState}
setModalState={setModalState}
selectedRegions={selectedRegions}
includeAllRegions={includeAllRegions}
onIncludeAllRegionsChange={handleIncludeAllRegionsChange}
onRegionSelect={handleRegionSelect}
onSubmit={handleSubmit}
accountId={accountId}
selectedDeploymentRegion={selectedDeploymentRegion}
handleRegionChange={handleRegionChange}
/>
);
}, [
modalState,
activeView,
form,
setModalState,
selectedRegions,
includeAllRegions,
handleIncludeAllRegionsChange,
handleRegionSelect,
handleSubmit,
accountId,
selectedDeploymentRegion,
handleRegionChange,
setSelectedRegions,
setIncludeAllRegions,
]);
const getSelectedRegionsCount = useCallback(
(): number =>
selectedRegions.includes('all') ? allRegions.length : selectedRegions.length,
[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) {
return {
title: 'AWS Webservice Integration',
okText: (
<div className="cloud-account-setup-success-view__footer-button">
Go to Dashboards
</div>
),
block: true,
onOk: handleGoToDashboards,
cancelButtonProps: { style: { display: 'none' } },
disabled: false,
};
}
// Handle other views
const viewConfigs = {
[ActiveViewEnum.FORM]: {
title: 'Add AWS Account',
okText: (
<div className="cloud-account-setup-form__submit-button-content">
Launch Cloud Formation Template{' '}
<SquareArrowOutUpRight size={17} color={Color.BG_VANILLA_100} />
</div>
),
onOk: handleSubmit,
disabled:
isLoading || isGeneratingUrl || modalState === ModalStateEnum.WAITING,
cancelButtonProps: { style: { display: 'none' } },
},
[ActiveViewEnum.SELECT_REGIONS]: {
title: 'Which regions do you want to monitor?',
okText: `Confirm Selection (${getSelectedRegionsCount()})`,
onOk: (): void => setActiveView(ActiveViewEnum.FORM),
isLoading: isLoading || isGeneratingUrl,
cancelButtonProps: { style: { display: 'block' } },
disabled: false,
},
};
return viewConfigs[activeView];
}, [
modalState,
handleSubmit,
getSelectedRegionsCount,
isLoading,
isGeneratingUrl,
activeView,
handleGoToDashboards,
setActiveView,
]);
const modalConfig = getModalConfig();
return (
<SignozModal
open={isOpen}
className="cloud-account-setup-modal"
title={modalConfig.title}
onCancel={handleClose}
onOk={modalConfig.onOk}
okText={modalConfig.okText}
okButtonProps={{
loading: isLoading,
disabled: selectedRegions.length === 0 || modalConfig.disabled,
className:
activeView === ActiveViewEnum.FORM
? 'cloud-account-setup-form__submit-button'
: 'account-setup-modal-footer__confirm-button',
block: activeView === ActiveViewEnum.FORM,
}}
cancelButtonProps={modalConfig.cancelButtonProps}
width={672}
>
{renderContent()}
</SignozModal>
);
}
export default CloudAccountSetupModal;

View File

@ -0,0 +1,137 @@
import { Color } from '@signozhq/design-tokens';
import { Form, Select, Switch } from 'antd';
import { ChevronDown } from 'lucide-react';
import { Region } from 'utils/regions';
// Form section components
function RegionDeploymentSection({
regions,
selectedDeploymentRegion,
handleRegionChange,
isFormDisabled,
}: {
regions: Region[];
selectedDeploymentRegion: string | undefined;
handleRegionChange: (value: string) => void;
isFormDisabled: boolean;
}): JSX.Element {
return (
<div className="cloud-account-setup-form__form-group">
<div className="cloud-account-setup-form__title">
Where should we deploy the SigNoz Cloud stack?
</div>
<div className="cloud-account-setup-form__description">
Choose the AWS region for CloudFormation stack deployment
</div>
<Form.Item
name="region"
rules={[{ required: true, message: 'Please select a region' }]}
className="cloud-account-setup-form__form-item"
>
<Select
placeholder="e.g. US East (N. Virginia)"
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
className="cloud-account-setup-form__select integrations-select"
onChange={handleRegionChange}
value={selectedDeploymentRegion}
disabled={isFormDisabled}
>
{regions.flatMap((region) =>
region.subRegions.map((subRegion) => (
<Select.Option key={subRegion.id} value={subRegion.id}>
{subRegion.displayName}
</Select.Option>
)),
)}
</Select>
</Form.Item>
</div>
);
}
function MonitoringRegionsSection({
includeAllRegions,
selectedRegions,
onIncludeAllRegionsChange,
getRegionPreviewText,
onRegionSelect,
isFormDisabled,
}: {
includeAllRegions: boolean;
selectedRegions: string[];
onIncludeAllRegionsChange: (checked: boolean) => void;
getRegionPreviewText: (regions: string[]) => string[];
onRegionSelect: () => void;
isFormDisabled: boolean;
}): JSX.Element {
return (
<div className="cloud-account-setup-form__form-group">
<div className="cloud-account-setup-form__title">
Which regions do you want to monitor?
</div>
<div className="cloud-account-setup-form__description">
Choose only the regions you want SigNoz to monitor. You can enable all at
once, or pick specific ones:
</div>
<Form.Item
name="monitorRegions"
rules={[
{
validator: async (): Promise<void> => {
if (selectedRegions.length === 0) {
return Promise.reject();
}
return Promise.resolve();
},
message: 'Please select at least one region to monitor',
},
]}
className="cloud-account-setup-form__form-item"
>
<div className="cloud-account-setup-form__include-all-regions-switch">
<Switch
size="small"
checked={includeAllRegions}
onChange={onIncludeAllRegionsChange}
disabled={isFormDisabled}
/>
<button
className="cloud-account-setup-form__include-all-regions-switch-label"
type="button"
onClick={(): void =>
!isFormDisabled
? onIncludeAllRegionsChange(!includeAllRegions)
: undefined
}
>
Include all regions
</button>
</div>
<Select
suffixIcon={null}
placeholder="Select Region(s)"
className="cloud-account-setup-form__select integrations-select"
onClick={!isFormDisabled ? onRegionSelect : undefined}
mode="multiple"
maxTagCount={3}
value={getRegionPreviewText(selectedRegions)}
open={false}
/>
</Form.Item>
</div>
);
}
function ComplianceNote(): JSX.Element {
return (
<div className="cloud-account-setup-form__form-group">
<div className="cloud-account-setup-form__note">
Note: Some organizations may require the CloudFormation stack to be deployed
in the same region as their primary infrastructure for compliance or latency
reasons.
</div>
</div>
);
}
export { ComplianceNote, MonitoringRegionsSection, RegionDeploymentSection };

View File

@ -0,0 +1,94 @@
import { Form } from 'antd';
import cx from 'classnames';
import { useAccountStatus } from 'hooks/integrations/aws/useAccountStatus';
import { useRef } from 'react';
import { AccountStatusResponse } from 'types/api/integrations/aws';
import { regions } from 'utils/regions';
import { ModalStateEnum, RegionFormProps } from '../types';
import AlertMessage from './AlertMessage';
import {
ComplianceNote,
MonitoringRegionsSection,
RegionDeploymentSection,
} from './IntegrateNowFormSections';
const allRegions = (): string[] =>
regions.flatMap((r) => r.subRegions.map((sr) => sr.name));
const getRegionPreviewText = (regions: string[]): string[] => {
if (regions.includes('all')) {
return allRegions();
}
return regions;
};
export function RegionForm({
form,
modalState,
setModalState,
selectedRegions,
includeAllRegions,
onIncludeAllRegionsChange,
onRegionSelect,
onSubmit,
accountId,
selectedDeploymentRegion,
handleRegionChange,
}: RegionFormProps): JSX.Element {
const startTimeRef = useRef(Date.now());
const refetchInterval = 10 * 1000;
const errorTimeout = 5 * 60 * 1000;
const { isLoading: isAccountStatusLoading } = useAccountStatus(accountId, {
refetchInterval,
enabled: !!accountId && modalState === ModalStateEnum.WAITING,
onSuccess: (data: AccountStatusResponse) => {
if (data.data.status.integration.last_heartbeat_ts_ms !== null) {
setModalState(ModalStateEnum.SUCCESS);
} else if (Date.now() - startTimeRef.current >= errorTimeout) {
// 5 minutes in milliseconds
setModalState(ModalStateEnum.ERROR);
}
},
onError: () => {
setModalState(ModalStateEnum.ERROR);
},
});
const isFormDisabled =
modalState === ModalStateEnum.WAITING || isAccountStatusLoading;
return (
<Form
form={form}
className="cloud-account-setup-form"
layout="vertical"
onFinish={onSubmit}
>
<AlertMessage modalState={modalState} />
<div
className={cx(`cloud-account-setup-form__content`, {
disabled: isFormDisabled,
})}
>
<RegionDeploymentSection
regions={regions}
handleRegionChange={handleRegionChange}
isFormDisabled={isFormDisabled}
selectedDeploymentRegion={selectedDeploymentRegion}
/>
<MonitoringRegionsSection
includeAllRegions={includeAllRegions}
selectedRegions={selectedRegions}
onIncludeAllRegionsChange={onIncludeAllRegionsChange}
getRegionPreviewText={getRegionPreviewText}
onRegionSelect={onRegionSelect}
isFormDisabled={isFormDisabled}
/>
<ComplianceNote />
</div>
</Form>
);
}

View File

@ -0,0 +1,21 @@
.select-all {
margin-bottom: 20px;
}
.regions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(145px, 1fr));
gap: 8px;
}
.region-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.region-group label {
display: flex;
gap: 10px;
align-items: center;
}

View File

@ -0,0 +1,63 @@
import './RegionSelector.style.scss';
import { Checkbox } from 'antd';
import { useRegionSelection } from 'hooks/integrations/aws/useRegionSelection';
import { Dispatch, SetStateAction } from 'react';
import { regions } from 'utils/regions';
export function RegionSelector({
selectedRegions,
setSelectedRegions,
setIncludeAllRegions,
}: {
selectedRegions: string[];
setSelectedRegions: Dispatch<SetStateAction<string[]>>;
setIncludeAllRegions: Dispatch<SetStateAction<boolean>>;
}): JSX.Element {
const {
allRegionIds,
handleSelectAll,
handleRegionSelect,
} = useRegionSelection({
selectedRegions,
setSelectedRegions,
setIncludeAllRegions,
});
return (
<div className="region-selector">
<div className="select-all">
<Checkbox
checked={selectedRegions.includes('all')}
indeterminate={
selectedRegions.length > 20 &&
selectedRegions.length < allRegionIds.length
}
onChange={(e): void => handleSelectAll(e.target.checked)}
>
Select All Regions
</Checkbox>
</div>
<div className="regions-grid">
{regions.map((region) => (
<div key={region.id} className="region-group">
<h3>{region.name}</h3>
{region.subRegions.map((subRegion) => (
<Checkbox
key={subRegion.id}
checked={
selectedRegions.includes('all') ||
selectedRegions.includes(subRegion.id)
}
onChange={(): void => handleRegionSelect(subRegion.id)}
>
{subRegion.name}
</Checkbox>
))}
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,156 @@
.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

@ -0,0 +1,78 @@
import './SuccessView.style.scss';
import { Alert } from 'antd';
import integrationsSuccess from 'assets/Lotties/integrations-success.json';
import { useState } from 'react';
import Lottie from 'react-lottie';
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">
{[
'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>
</div>
}
type="info"
className="what-next-items-wrapper__item"
/>
))}
</div>
</div>
</div>
</>
);
}

View File

@ -0,0 +1,33 @@
import { FormInstance } from 'antd';
import { Dispatch, SetStateAction } from 'react';
export enum ActiveViewEnum {
SELECT_REGIONS = 'select-regions',
FORM = 'form',
}
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;
onIncludeAllRegionsChange: (checked: boolean) => void;
onRegionSelect: () => void;
onSubmit: () => Promise<void>;
accountId?: string;
selectedDeploymentRegion: string | undefined;
handleRegionChange: (value: string) => void;
}
export interface IntegrationModalProps {
isOpen: boolean;
onClose: () => void;
}

View File

@ -0,0 +1,36 @@
import { ServiceData } from './types';
function DashboardItem({
dashboard,
}: {
dashboard: ServiceData['assets']['dashboards'][number];
}): JSX.Element {
return (
<div className="cloud-service-dashboard-item">
<div className="cloud-service-dashboard-item__title">{dashboard.title}</div>
<div className="cloud-service-dashboard-item__preview">
<img
src={dashboard.image}
alt={dashboard.title}
className="cloud-service-dashboard-item__preview-image"
/>
</div>
</div>
);
}
function CloudServiceDashboards({
service,
}: {
service: ServiceData;
}): JSX.Element {
return (
<>
{service.assets.dashboards.map((dashboard) => (
<DashboardItem key={dashboard.id} dashboard={dashboard} />
))}
</>
);
}
export default CloudServiceDashboards;

View File

@ -0,0 +1,87 @@
import { Table } from 'antd';
import { ServiceData } from './types';
function CloudServiceDataCollected({
logsData,
metricsData,
}: {
logsData: ServiceData['data_collected']['logs'];
metricsData: ServiceData['data_collected']['metrics'];
}): JSX.Element {
const logsColumns = [
{
title: 'NAME',
dataIndex: 'name',
key: 'name',
width: '30%',
},
{
title: 'PATH',
dataIndex: 'path',
key: 'path',
width: '40%',
},
{
title: 'FACET TYPE',
dataIndex: 'type',
key: 'type',
width: '30%',
},
];
const metricsColumns = [
{
title: 'NAME',
dataIndex: 'name',
key: 'name',
width: '40%',
},
{
title: 'UNIT',
dataIndex: 'unit',
key: 'unit',
width: '30%',
},
{
title: 'TYPE',
dataIndex: 'type',
key: 'type',
width: '30%',
},
];
const tableProps = {
pagination: { pageSize: 20, hideOnSinglePage: true },
showHeader: true,
size: 'middle' as const,
bordered: false,
};
return (
<div className="cloud-service-data-collected">
<div className="cloud-service-data-collected__table">
<div className="cloud-service-data-collected__table-heading">Logs</div>
<Table
columns={logsColumns}
dataSource={logsData}
// eslint-disable-next-line react/jsx-props-no-spreading
{...tableProps}
className="cloud-service-data-collected__table-logs"
/>
</div>
<div className="cloud-service-data-collected__table">
<div className="cloud-service-data-collected__table-heading">Metrics</div>
<Table
columns={metricsColumns}
dataSource={metricsData}
// eslint-disable-next-line react/jsx-props-no-spreading
{...tableProps}
className="cloud-service-data-collected__table-metrics"
/>
</div>
</div>
);
}
export default CloudServiceDataCollected;

View File

@ -0,0 +1,89 @@
.configure-service-modal {
&__body {
display: flex;
flex-direction: column;
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
padding: 14px;
&-regions-switch-switch {
display: flex;
align-items: center;
gap: 6px;
&-label {
color: var(--bg-vanilla-100);
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
}
}
&-switch-description {
margin-top: 4px;
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.06px;
}
&-form-item {
&:last-child {
margin-bottom: 0px;
}
}
}
.ant-modal-body {
padding-bottom: 0;
}
.ant-modal-footer {
margin: 0;
padding-bottom: 12px;
}
}
.lightMode {
.configure-service-modal {
&__body {
border-color: var(--bg-vanilla-300);
&-regions-switch-switch {
&-label {
color: var(--bg-ink-500);
}
}
&-switch-description {
color: var(--bg-ink-400);
}
}
.ant-btn {
&.ant-btn-default {
background: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300);
color: var(--bg-ink-400);
&:hover {
border-color: var(--bg-vanilla-400);
color: var(--bg-ink-500);
}
}
&.ant-btn-primary {
// Keep primary button same as dark mode
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
&:hover {
background: var(--bg-robin-400);
}
&:disabled {
opacity: 0.6;
}
}
}
}
}

View File

@ -0,0 +1,199 @@
import './ConfigureServiceModal.styles.scss';
import { Form, Switch } from 'antd';
import SignozModal from 'components/SignozModal/SignozModal';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
ServiceConfig,
SupportedSignals,
} from 'container/CloudIntegrationPage/ServicesSection/types';
import { useUpdateServiceConfig } from 'hooks/integrations/aws/useUpdateServiceConfig';
import { useCallback, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
interface IConfigureServiceModalProps {
isOpen: boolean;
onClose: () => void;
serviceName: string;
serviceId: string;
cloudAccountId: string;
supportedSignals: SupportedSignals;
initialConfig?: ServiceConfig;
}
function ConfigureServiceModal({
isOpen,
onClose,
serviceName,
serviceId,
cloudAccountId,
initialConfig,
supportedSignals,
}: IConfigureServiceModalProps): JSX.Element {
const [form] = Form.useForm();
const [isLoading, setIsLoading] = useState(false);
// Track current form values
const [currentValues, setCurrentValues] = useState({
metrics: initialConfig?.metrics?.enabled || false,
logs: initialConfig?.logs?.enabled || false,
});
const {
mutate: updateServiceConfig,
isLoading: isUpdating,
} = useUpdateServiceConfig();
const queryClient = useQueryClient();
const handleSubmit = useCallback(async (): Promise<void> => {
try {
const values = await form.validateFields();
setIsLoading(true);
updateServiceConfig(
{
serviceId,
payload: {
cloud_account_id: cloudAccountId,
config: {
logs: {
enabled: values.logs,
},
metrics: {
enabled: values.metrics,
},
},
},
},
{
onSuccess: () => {
queryClient.invalidateQueries([
REACT_QUERY_KEY.AWS_SERVICE_DETAILS,
serviceId,
]);
onClose();
},
onError: (error) => {
console.error('Failed to update service config:', error);
},
},
);
} catch (error) {
console.error('Form submission failed:', error);
} finally {
setIsLoading(false);
}
}, [
form,
updateServiceConfig,
serviceId,
cloudAccountId,
queryClient,
onClose,
]);
const isSaveDisabled = useMemo(
() => currentValues.metrics === false && currentValues.logs === false,
[currentValues],
);
const handleClose = useCallback(() => {
form.resetFields();
onClose();
}, [form, onClose]);
return (
<SignozModal
title={
<div className="account-settings-modal__title">Configure {serviceName}</div>
}
centered
open={isOpen}
okText="Save"
okButtonProps={{
disabled: isSaveDisabled,
className: 'account-settings-modal__footer-save-button',
loading: isLoading || isUpdating,
}}
onCancel={handleClose}
onOk={handleSubmit}
cancelText="Close"
cancelButtonProps={{
className: 'account-settings-modal__footer-close-button',
}}
width={672}
rootClassName=" configure-service-modal"
>
<Form
form={form}
layout="vertical"
initialValues={{
metrics: initialConfig?.metrics?.enabled || false,
logs: initialConfig?.logs?.enabled || false,
}}
>
<div className=" configure-service-modal__body">
{supportedSignals.metrics && (
<Form.Item
name="metrics"
valuePropName="checked"
className="configure-service-modal__body-form-item"
>
<div className="configure-service-modal__body-regions-switch-switch">
<Switch
checked={currentValues.metrics}
onChange={(checked): void => {
setCurrentValues((prev) => ({ ...prev, metrics: checked }));
form.setFieldsValue({ metrics: checked });
}}
/>
<span className="configure-service-modal__body-regions-switch-switch-label">
Metric Collection
</span>
</div>
<div className="configure-service-modal__body-switch-description">
Metric Collection is enabled for this AWS account. We recommend keeping
this enabled, but you can disable metric collection if you do not want
to monitor your AWS infrastructure.
</div>
</Form.Item>
)}
{supportedSignals.logs && (
<Form.Item
name="logs"
valuePropName="checked"
className="configure-service-modal__body-form-item"
>
<div className="configure-service-modal__body-regions-switch-switch">
<Switch
checked={currentValues.logs}
onChange={(checked): void => {
setCurrentValues((prev) => ({ ...prev, logs: checked }));
form.setFieldsValue({ logs: checked });
}}
/>
<span className="configure-service-modal__body-regions-switch-switch-label">
Log Collection
</span>
</div>
<div className="configure-service-modal__body-switch-description">
To ingest logs from your AWS services, you must complete several steps
</div>
</Form.Item>
)}
</div>
</Form>
</SignozModal>
);
}
ConfigureServiceModal.defaultProps = {
initialConfig: {
metrics: { enabled: false },
logs: { enabled: false },
},
};
export default ConfigureServiceModal;

View File

@ -0,0 +1,135 @@
import { Color } from '@signozhq/design-tokens';
import { Button, Tabs, TabsProps } from 'antd';
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import Spinner from 'components/Spinner';
import CloudServiceDashboards from 'container/CloudIntegrationPage/ServicesSection/CloudServiceDashboards';
import CloudServiceDataCollected from 'container/CloudIntegrationPage/ServicesSection/CloudServiceDataCollected';
import { IServiceStatus } from 'container/CloudIntegrationPage/ServicesSection/types';
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 ConfigureServiceModal from './ConfigureServiceModal';
const getStatus = (
logsLastReceivedTimestamp: number | undefined,
metricsLastReceivedTimestamp: number | undefined,
): { text: string; className: string } => {
if (!logsLastReceivedTimestamp && !metricsLastReceivedTimestamp) {
return { text: 'No Data Yet', className: 'service-status--no-data' };
}
const latestTimestamp = Math.max(
logsLastReceivedTimestamp || 0,
metricsLastReceivedTimestamp || 0,
);
const isStale = dayjs().diff(dayjs(latestTimestamp), 'minute') > 30;
if (isStale) {
return { text: 'Stale Data', className: 'service-status--stale-data' };
}
return { text: 'Connected', className: 'service-status--connected' };
};
function ServiceStatus({
serviceStatus,
}: {
serviceStatus: IServiceStatus | null;
}): JSX.Element {
const logsLastReceivedTimestamp = serviceStatus?.logs?.last_received_ts_ms;
const metricsLastReceivedTimestamp =
serviceStatus?.metrics?.last_received_ts_ms;
const { text, className } = getStatus(
logsLastReceivedTimestamp,
metricsLastReceivedTimestamp,
);
return <div className={`service-status ${className}`}>{text}</div>;
}
function ServiceDetails(): JSX.Element | null {
const urlQuery = useUrlQuery();
const accountId = urlQuery.get('accountId');
const serviceId = urlQuery.get('service');
const [isConfigureServiceModalOpen, setIsConfigureServiceModalOpen] = useState(
false,
);
const { data: serviceDetailsData, isLoading } = useServiceDetails(
serviceId || '',
accountId || undefined,
);
if (isLoading) {
return <Spinner size="large" height="50vh" />;
}
if (!serviceDetailsData) {
return null;
}
const tabItems: TabsProps['items'] = [
{
key: 'dashboards',
label: `Dashboards (${serviceDetailsData?.assets.dashboards.length})`,
children: <CloudServiceDashboards service={serviceDetailsData} />,
},
{
key: 'data-collected',
label: 'Data Collected',
children: (
<CloudServiceDataCollected
logsData={serviceDetailsData?.data_collected.logs || []}
metricsData={serviceDetailsData?.data_collected.metrics || []}
/>
),
},
];
return (
<div className="service-details">
<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 && (
<Button
className="configure-button"
onClick={(): void => setIsConfigureServiceModalOpen(true)}
>
<Wrench size={12} color={Color.BG_VANILLA_400} />
Configure
</Button>
)}
</div>
</div>
<div className="service-details__overview">
<MarkdownRenderer
variables={{}}
markdownContent={serviceDetailsData?.overview}
/>
</div>
<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 || {}}
/>
</div>
);
}
export default ServiceDetails;

View File

@ -0,0 +1,39 @@
import cx from 'classnames';
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
import { Service } from './types';
function ServiceItem({
service,
onClick,
isActive,
}: {
service: Service;
onClick: (serviceName: string) => void;
isActive?: boolean;
}): JSX.Element {
return (
<button
className={cx('service-item', { active: isActive })}
onClick={(): void => onClick(service.id)}
type="button"
>
<div className="service-item__icon-wrapper">
<img
src={service.icon}
alt={service.title}
className="service-item__icon"
/>
</div>
<div className="service-item__title">
<LineClampedText text={service.title} />
</div>
</button>
);
}
ServiceItem.defaultProps = {
isActive: false,
};
export default ServiceItem;

View File

@ -0,0 +1,52 @@
import Spinner from 'components/Spinner';
import { useGetAccountServices } from 'hooks/integrations/aws/useGetAccountServices';
import useUrlQuery from 'hooks/useUrlQuery';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom-v5-compat';
import ServiceItem from './ServiceItem';
interface ServicesListProps {
accountId: string;
filter: 'all_services' | 'enabled' | 'available';
}
function ServicesList({ accountId, filter }: ServicesListProps): JSX.Element {
const urlQuery = useUrlQuery();
const navigate = useNavigate();
const { data: services = [], isLoading } = useGetAccountServices(accountId);
const activeService = urlQuery.get('service');
const handleServiceClick = (serviceId: string): void => {
urlQuery.set('service', serviceId);
navigate({ search: urlQuery.toString() });
};
const filteredServices = useMemo(() => {
if (filter === 'all_services') return services;
return services.filter((service) => {
const isEnabled =
service?.config?.logs?.enabled || service?.config?.metrics?.enabled;
return filter === 'enabled' ? isEnabled : !isEnabled;
});
}, [services, filter]);
if (isLoading) return <Spinner size="large" height="25vh" />;
if (!services) return <div>No services found</div>;
return (
<div className="services-list">
{filteredServices.map((service) => (
<ServiceItem
key={service.id}
service={service}
onClick={handleServiceClick}
isActive={service.id === activeService}
/>
))}
</div>
);
}
export default ServicesList;

View File

@ -0,0 +1,431 @@
.services-tabs {
.ant-tabs-tab {
font-family: 'Inter';
padding: 16px 4px 14px;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
font-weight: 400;
&.ant-tabs-tab-active {
.ant-tabs-tab-btn {
color: var(--bg-vanilla-100);
}
}
}
.ant-tabs-ink-bar {
background: var(--bg-robin-500);
}
}
.services-section {
display: flex;
gap: 10px;
&__sidebar {
width: 16%;
padding: 0 16px;
}
&__content {
width: 84%;
padding: 16px;
}
}
.services-filter {
padding: 16px 0;
.ant-select-selector {
background-color: var(--bg-ink-300) !important;
border: 1px solid var(--bg-slate-400) !important;
color: var(--bg-vanilla-400) !important;
font-family: Inter;
font-size: 12px;
}
.ant-select-arrow {
color: var(--bg-vanilla-400);
}
}
.service-item {
display: flex;
gap: 12px;
align-items: center;
padding: 8px;
background-color: transparent;
border: none;
width: 100%;
cursor: pointer;
&:not(:last-child) {
border-bottom: 1px solid var(--bg-slate-400);
}
&.active {
background-color: var(--bg-ink-100);
}
&__icon-wrapper {
height: 40px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--bg-ink-300);
border: 1px solid var(--bg-slate-400);
border-radius: 4px;
.service-item__icon {
width: 24px;
height: 24px;
}
}
&__title {
color: var(--bg-vanilla-100);
font-size: 14px;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
text-align: left;
}
}
.service-details {
display: flex;
flex-direction: column;
gap: 10px;
&__title-bar {
display: flex;
justify-content: space-between;
align-items: center;
height: 48px;
border-bottom: 1px solid var(--bg-slate-400);
.service-details__details-title {
color: var(--bg-vanilla-400);
font-size: 14px;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
text-align: left;
}
.service-details__right-actions {
display: flex;
align-items: center;
gap: 8px;
.service-status {
display: flex;
align-items: center;
padding: 2px 8px;
font-family: 'Geist Mono';
font-size: 12px;
font-weight: 500;
letter-spacing: 0.54px;
border-radius: 2px;
line-height: normal;
&--connected {
border: 1px solid rgba(37, 225, 146, 0.1);
background: rgba(37, 225, 146, 0.1);
color: var(--bg-forest-400);
}
&--stale-data {
background: rgba(255, 215, 120, 0.1);
border: 1px solid rgba(255, 215, 120, 0.1);
color: var(--bg-amber-400);
}
&--no-data {
border: 1px solid rgba(234, 109, 113, 0.1);
background: rgba(234, 109, 113, 0.1);
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;
}
}
}
&__overview {
color: var(--bg-vanilla-400);
font-size: 14px;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
width: 800px;
}
&__tabs {
.ant-tabs {
&-ink-bar {
background-color: transparent;
}
&-nav {
padding: 8px 0 18px;
&-wrap {
padding: 0;
}
&::before {
border-bottom: none !important;
}
}
&-tab {
margin-left: 0 !important;
padding: 0;
&-btn {
padding: 10px 24px;
color: var(--text-vanilla-400) !important;
font-family: Inter;
font-size: 12px;
font-weight: 400;
line-height: 18px; /* 150% */
letter-spacing: -0.06px;
&[aria-selected='true'] {
color: var(--text-vanilla-100) !important;
}
}
&-active {
background: var(--bg-slate-400, #1d212d);
}
}
&-extra-content {
padding: 0 16px 16px;
}
&-nav-list {
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-400);
border-radius: 2px;
}
}
}
.cloud-service {
&-dashboard-item {
display: flex;
flex-direction: column;
gap: 10px;
&__title {
color: var(--bg-vanilla-100);
font-size: 18px;
font-weight: 500;
line-height: 20px; /* 111.111% */
letter-spacing: -0.09px;
}
&__preview-image {
width: 100%;
}
}
&-data-collected {
display: flex;
flex-direction: column;
gap: 24px;
&__table {
display: flex;
flex-direction: column;
gap: 8px;
.ant-table {
background: transparent;
border: 1px solid var(--bg-slate-400);
.ant-table-thead {
> tr > th {
&::before {
display: none;
}
background: transparent;
border: none;
color: var(--bg-vanilla-400);
font-size: 12px;
font-weight: 600;
line-height: 18px; /* 150% */
letter-spacing: 0.6px;
text-transform: uppercase;
}
}
.ant-table-tbody {
> tr {
&:nth-child(odd),
&:hover > td {
background: rgba(255, 255, 255, 0.01) !important;
}
> td {
&:first-child {
font-weight: 500;
}
border: none;
color: var(--bg-vanilla-400);
font-size: 14px;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
}
}
}
&__table-heading {
color: var(--bg-slate-50);
font-size: 11px;
font-weight: 500;
line-height: 18px; /* 163.636% */
letter-spacing: 0.88px;
text-transform: uppercase;
}
}
}
}
.lightMode {
.services-tabs {
.ant-tabs-tab {
&.ant-tabs-tab-active {
.ant-tabs-tab-btn {
color: var(--bg-ink-500);
}
}
}
}
.services-filter {
.ant-select-selector {
background-color: var(--bg-vanilla-100) !important;
border-color: var(--bg-vanilla-300) !important;
color: var(--bg-ink-400) !important;
}
.ant-select-arrow {
color: var(--bg-ink-400);
}
}
.service-item {
&:not(:last-child) {
border-bottom: 1px solid var(--bg-vanilla-300);
}
&.active {
background-color: var(--bg-vanilla-300);
}
&__icon-wrapper {
background-color: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-300);
}
&__title {
color: var(--bg-ink-500);
}
}
.service-details {
&__title-bar {
border-bottom: 1px solid var(--bg-vanilla-300);
.service-details__details-title {
color: var(--bg-ink-400);
}
.configure-button {
color: var(--bg-ink-400);
background: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-300);
&:hover {
border-color: var(--bg-vanilla-400);
color: var(--bg-ink-500);
}
}
.service-status {
&--connected {
border: 1px solid rgba(37, 225, 146, 0.2);
background: rgba(37, 225, 146, 0.1);
color: var(--bg-forest-500);
}
&--stale-data {
border: 1px solid rgba(255, 215, 120, 0.2);
background: rgba(255, 215, 120, 0.1);
color: var(--bg-amber-500);
}
&--no-data {
border: 1px solid rgba(234, 109, 113, 0.2);
background: rgba(234, 109, 113, 0.1);
color: var(--bg-cherry-500);
}
}
}
&__overview {
color: var(--bg-ink-400);
}
&__tabs {
.ant-tabs {
&-tab {
&-btn {
color: var(--bg-ink-400) !important;
&[aria-selected='true'] {
color: var(--bg-ink-500) !important;
}
}
&-active {
background: var(--bg-vanilla-300);
}
}
&-nav-list {
border-color: var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}
}
.cloud-service {
&-dashboard-item {
&__title {
color: var(--bg-ink-500);
}
}
&-data-collected {
&__table {
.ant-table {
border-color: var(--bg-vanilla-300);
.ant-table-thead {
> tr > th {
color: var(--bg-ink-400);
}
}
.ant-table-tbody {
> tr {
&:nth-child(odd),
&:hover > td {
background: var(--bg-vanilla-100) !important;
}
> td {
color: var(--bg-ink-400);
}
}
}
}
}
&__table-heading {
color: var(--bg-ink-500);
}
}
}
}
}

View File

@ -0,0 +1,115 @@
import './ServicesTabs.style.scss';
import { Color } from '@signozhq/design-tokens';
import type { SelectProps, TabsProps } from 'antd';
import { Select, Tabs } from 'antd';
import { getAwsServices } from 'api/integrations/aws';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useUrlQuery from 'hooks/useUrlQuery';
import { ChevronDown } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import ServiceDetails from './ServiceDetails';
import ServicesList from './ServicesList';
export enum ServiceFilterType {
ALL_SERVICES = 'all_services',
ENABLED = 'enabled',
AVAILABLE = 'available',
}
interface ServicesFilterProps {
accountId: string;
onFilterChange: (value: ServiceFilterType) => void;
}
function ServicesFilter({
accountId,
onFilterChange,
}: ServicesFilterProps): JSX.Element | null {
const { data: services, isLoading } = useQuery(
[REACT_QUERY_KEY.AWS_SERVICES, accountId],
() => getAwsServices(accountId),
);
const { enabledCount, availableCount } = useMemo(() => {
if (!services) return { enabledCount: 0, availableCount: 0 };
return services.reduce(
(acc, service) => {
const isEnabled =
service?.config?.logs?.enabled || service?.config?.metrics?.enabled;
return {
enabledCount: acc.enabledCount + (isEnabled ? 1 : 0),
availableCount: acc.availableCount + (isEnabled ? 0 : 1),
};
},
{ enabledCount: 0, availableCount: 0 },
);
}, [services]);
const selectOptions: SelectProps['options'] = useMemo(
() => [
{ value: 'all_services', label: `All Services (${services?.length || 0})` },
{ value: 'enabled', label: `Enabled (${enabledCount})` },
{ value: 'available', label: `Available (${availableCount})` },
],
[services, enabledCount, availableCount],
);
if (isLoading) return null;
if (!services?.length) return null;
return (
<div className="services-filter">
<Select
style={{ width: '100%' }}
defaultValue={ServiceFilterType.ALL_SERVICES}
options={selectOptions}
className="services-sidebar__select"
suffixIcon={<ChevronDown size={16} color={Color.BG_VANILLA_400} />}
onChange={onFilterChange}
/>
</div>
);
}
function ServicesSection(): JSX.Element {
const urlQuery = useUrlQuery();
const accountId = urlQuery.get('accountId') || '';
const [activeFilter, setActiveFilter] = useState<
'all_services' | 'enabled' | 'available'
>('all_services');
return (
<div className="services-section">
<div className="services-section__sidebar">
<ServicesFilter accountId={accountId} onFilterChange={setActiveFilter} />
<ServicesList accountId={accountId} filter={activeFilter} />
</div>
<div className="services-section__content">
<ServiceDetails />
</div>
</div>
);
}
function ServicesTabs(): JSX.Element {
const tabItems: TabsProps['items'] = [
{
key: 'services',
label: 'Services For Integration',
children: <ServicesSection />,
},
];
return (
<div className="services-tabs">
<Tabs defaultActiveKey="services" items={tabItems} />
</div>
);
}
export default ServicesTabs;

View File

@ -0,0 +1,135 @@
interface Service {
id: string;
title: string;
icon: string;
config: ServiceConfig;
}
interface Dashboard {
id: string;
url: string;
title: string;
description: string;
image: string;
}
interface LogField {
name: string;
path: string;
type: string;
}
interface Metric {
name: string;
type: string;
unit: string;
}
interface ConfigStatus {
enabled: boolean;
}
interface DataStatus {
last_received_ts_ms: number;
last_received_from: string;
}
interface ServiceConfig {
logs: ConfigStatus;
metrics: ConfigStatus;
}
interface IServiceStatus {
logs: DataStatus | null;
metrics: DataStatus | null;
}
interface SupportedSignals {
metrics: boolean;
logs: boolean;
}
interface ServiceData {
id: string;
title: string;
icon: string;
overview: string;
supported_signals: SupportedSignals;
assets: {
dashboards: Dashboard[];
};
data_collected: {
logs?: LogField[];
metrics: Metric[];
};
config?: ServiceConfig;
status?: IServiceStatus;
}
interface ServiceDetailsResponse {
status: 'success';
data: ServiceData;
}
interface CloudAccountConfig {
regions: string[];
}
interface IntegrationStatus {
last_heartbeat_ts_ms: number;
}
interface AccountStatus {
integration: IntegrationStatus;
}
interface CloudAccount {
id: string;
cloud_account_id: string;
config: CloudAccountConfig;
status: AccountStatus;
}
interface CloudAccountsData {
accounts: CloudAccount[];
}
interface UpdateServiceConfigPayload {
cloud_account_id: string;
config: {
logs: {
enabled: boolean;
};
metrics: {
enabled: boolean;
};
};
}
interface UpdateServiceConfigResponse {
status: string;
data: {
id: string;
config: {
logs: {
enabled: boolean;
};
metrics: {
enabled: boolean;
};
};
};
}
export type {
CloudAccount,
CloudAccountsData,
IServiceStatus,
Service,
ServiceConfig,
ServiceData,
ServiceDetailsResponse,
SupportedSignals,
UpdateServiceConfigPayload,
UpdateServiceConfigResponse,
};

View File

@ -0,0 +1,139 @@
import { Form } from 'antd';
import { FormInstance } from 'antd/lib';
import { CloudAccount } from 'container/CloudIntegrationPage/ServicesSection/types';
import { useUpdateAccountConfig } from 'hooks/integrations/aws/useUpdateAccountConfig';
import { isEqual } from 'lodash-es';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { AccountConfigPayload } from 'types/api/integrations/aws';
import { regions } from 'utils/regions';
interface UseAccountSettingsModalProps {
onClose: () => void;
account: CloudAccount;
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
}
interface UseAccountSettingsModal {
form: FormInstance;
isLoading: boolean;
selectedRegions: string[];
includeAllRegions: boolean;
isRegionSelectOpen: boolean;
isSaveDisabled: boolean;
setSelectedRegions: Dispatch<SetStateAction<string[]>>;
setIncludeAllRegions: Dispatch<SetStateAction<boolean>>;
setIsRegionSelectOpen: Dispatch<SetStateAction<boolean>>;
handleIncludeAllRegionsChange: (checked: boolean) => void;
handleSubmit: () => Promise<void>;
handleClose: () => void;
}
const allRegions = (): string[] =>
regions.flatMap((r) => r.subRegions.map((sr) => sr.name));
const getRegionPreviewText = (regions: string[] | undefined): string[] => {
if (!regions) return [];
if (regions.includes('all')) {
return allRegions();
}
return regions;
};
export function useAccountSettingsModal({
onClose,
account,
setActiveAccount,
}: UseAccountSettingsModalProps): UseAccountSettingsModal {
const [form] = Form.useForm();
const { mutate: updateConfig, isLoading } = useUpdateAccountConfig();
const accountRegions = useMemo(() => account?.config?.regions || [], [
account?.config?.regions,
]);
const [isInitialRegionsSet, setIsInitialRegionsSet] = useState(false);
const [selectedRegions, setSelectedRegions] = useState<string[]>([]);
const [includeAllRegions, setIncludeAllRegions] = useState(false);
const [isRegionSelectOpen, setIsRegionSelectOpen] = useState(false);
// Initialize regions from account when modal opens
useEffect(() => {
if (accountRegions.length > 0 && !isInitialRegionsSet) {
setSelectedRegions(accountRegions);
setIsInitialRegionsSet(true);
setIncludeAllRegions(accountRegions.includes('all'));
}
}, [accountRegions, isInitialRegionsSet]);
const handleSubmit = useCallback(async (): Promise<void> => {
try {
await form.validateFields();
const payload: AccountConfigPayload = {
config: {
regions: selectedRegions,
},
};
updateConfig(
{ accountId: account?.id, payload },
{
onSuccess: (response) => {
setActiveAccount(response.data);
onClose();
},
},
);
} catch (error) {
console.error('Form submission failed:', error);
}
}, [
form,
selectedRegions,
updateConfig,
account?.id,
setActiveAccount,
onClose,
]);
const isSaveDisabled = useMemo(
() => isEqual(selectedRegions.sort(), accountRegions.sort()),
[selectedRegions, accountRegions],
);
const handleIncludeAllRegionsChange = useCallback((checked: boolean): void => {
setIncludeAllRegions(checked);
if (checked) {
setSelectedRegions(['all']);
} else {
setSelectedRegions([]);
}
}, []);
const handleClose = useCallback(() => {
setIsRegionSelectOpen(false);
onClose();
}, [onClose]);
return {
form,
isLoading,
selectedRegions,
includeAllRegions,
isRegionSelectOpen,
isSaveDisabled,
setSelectedRegions,
setIncludeAllRegions,
setIsRegionSelectOpen,
handleIncludeAllRegionsChange,
handleSubmit,
handleClose,
};
}
export { getRegionPreviewText };

View File

@ -0,0 +1,21 @@
import axios from 'api';
import { AxiosError } from 'axios';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { AccountStatusResponse } from 'types/api/integrations/aws';
export function useAccountStatus(
accountId: string | undefined,
options: UseQueryOptions<AccountStatusResponse, AxiosError>,
): UseQueryResult<AccountStatusResponse, AxiosError> {
return useQuery<AccountStatusResponse, AxiosError>({
queryKey: [REACT_QUERY_KEY.AWS_ACCOUNT_STATUS, accountId],
queryFn: async () => {
const response = await axios.get<AccountStatusResponse>(
`/cloud-integrations/aws/accounts/${accountId}/status`,
);
return response.data;
},
...options,
});
}

View File

@ -0,0 +1,8 @@
import { getAwsAccounts } from 'api/integrations/aws';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { CloudAccount } from 'container/CloudIntegrationPage/ServicesSection/types';
import { useQuery, UseQueryResult } from 'react-query';
export function useAwsAccounts(): UseQueryResult<CloudAccount[]> {
return useQuery(REACT_QUERY_KEY.AWS_ACCOUNTS, getAwsAccounts);
}

View File

@ -0,0 +1,21 @@
import { generateConnectionUrl } from 'api/integrations/aws';
import { AxiosError } from 'axios';
import { useMutation, UseMutationResult } from 'react-query';
import {
ConnectionUrlResponse,
GenerateConnectionUrlPayload,
} from 'types/api/integrations/aws';
export function useGenerateConnectionUrl(): UseMutationResult<
ConnectionUrlResponse,
AxiosError,
GenerateConnectionUrlPayload
> {
return useMutation<
ConnectionUrlResponse,
AxiosError,
GenerateConnectionUrlPayload
>({
mutationFn: generateConnectionUrl,
});
}

View File

@ -0,0 +1,12 @@
import { getAwsServices } from 'api/integrations/aws';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { Service } from 'container/CloudIntegrationPage/ServicesSection/types';
import { useQuery, UseQueryResult } from 'react-query';
export function useGetAccountServices(
accountId?: string,
): UseQueryResult<Service[]> {
return useQuery([REACT_QUERY_KEY.AWS_SERVICES, accountId], () =>
getAwsServices(accountId),
);
}

View File

@ -0,0 +1,164 @@
import { Form } from 'antd';
import { FormInstance } from 'antd/lib';
import {
ActiveViewEnum,
ModalStateEnum,
} from 'container/CloudIntegrationPage/HeroSection/types';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import {
ConnectionUrlResponse,
GenerateConnectionUrlPayload,
} from 'types/api/integrations/aws';
import { regions } from 'utils/regions';
import { useGenerateConnectionUrl } from './useGenerateConnectionUrl';
interface UseIntegrationModalProps {
onClose: () => void;
}
interface UseIntegrationModal {
form: FormInstance;
modalState: ModalStateEnum;
setModalState: Dispatch<SetStateAction<ModalStateEnum>>;
isLoading: boolean;
activeView: ActiveViewEnum;
selectedRegions: string[];
includeAllRegions: boolean;
isGeneratingUrl: boolean;
setSelectedRegions: Dispatch<SetStateAction<string[]>>;
setIncludeAllRegions: Dispatch<SetStateAction<boolean>>;
handleIncludeAllRegionsChange: (checked: boolean) => void;
handleRegionSelect: () => void;
handleSubmit: () => Promise<void>;
handleClose: () => void;
setActiveView: (view: ActiveViewEnum) => void;
allRegions: string[];
accountId?: string;
selectedDeploymentRegion: string | undefined;
handleRegionChange: (value: string) => void;
}
export function useIntegrationModal({
onClose,
}: UseIntegrationModalProps): UseIntegrationModal {
const [form] = Form.useForm();
const [modalState, setModalState] = useState<ModalStateEnum>(
ModalStateEnum.FORM,
);
const [isLoading, setIsLoading] = useState(false);
const [accountId, setAccountId] = useState<string | undefined>(undefined);
const [activeView, setActiveView] = useState<ActiveViewEnum>(
ActiveViewEnum.FORM,
);
const [selectedRegions, setSelectedRegions] = useState<string[]>([]);
const [includeAllRegions, setIncludeAllRegions] = useState(false);
const [selectedDeploymentRegion, setSelectedDeploymentRegion] = useState<
string | undefined
>(undefined);
const allRegions = useMemo(
() => regions.flatMap((r) => r.subRegions.map((sr) => sr.name)),
[],
);
useEffect(() => {
form.setFieldsValue({ region: selectedDeploymentRegion });
}, [selectedDeploymentRegion, form]);
const handleRegionChange = (value: string): void => {
setSelectedDeploymentRegion(value);
};
const handleIncludeAllRegionsChange = useCallback((checked: boolean): void => {
setIncludeAllRegions(checked);
if (checked) {
setSelectedRegions(['all']);
} else {
setSelectedRegions([]);
}
}, []);
const handleRegionSelect = useCallback((): void => {
setActiveView(ActiveViewEnum.SELECT_REGIONS);
}, []);
const handleClose = useCallback((): void => {
setActiveView(ActiveViewEnum.FORM);
setSelectedRegions([]);
setIncludeAllRegions(false);
setModalState(ModalStateEnum.FORM);
onClose();
}, [onClose]);
const {
mutate: generateUrl,
isLoading: isGeneratingUrl,
} = useGenerateConnectionUrl();
const handleGenerateUrl = useCallback(
(payload: GenerateConnectionUrlPayload): void => {
generateUrl(payload, {
onSuccess: (data: ConnectionUrlResponse) => {
window.open(data.connection_url, '_blank');
setModalState(ModalStateEnum.WAITING);
setAccountId(data.account_id);
},
onError: () => {
setModalState(ModalStateEnum.ERROR);
},
});
},
[generateUrl],
);
const handleSubmit = useCallback(async (): Promise<void> => {
try {
setIsLoading(true);
const values = await form.validateFields();
const payload: GenerateConnectionUrlPayload = {
agent_config: {
region: values.region,
},
account_config: {
regions: includeAllRegions ? ['all'] : selectedRegions,
},
};
handleGenerateUrl(payload);
} catch (error) {
console.error('Form submission failed:', error);
} finally {
setIsLoading(false);
}
}, [form, includeAllRegions, selectedRegions, handleGenerateUrl]);
return {
form,
modalState,
isLoading,
activeView,
selectedRegions,
includeAllRegions,
isGeneratingUrl,
setSelectedRegions,
setIncludeAllRegions,
handleIncludeAllRegionsChange,
handleRegionSelect,
handleSubmit,
handleClose,
setActiveView,
allRegions,
accountId,
setModalState,
selectedDeploymentRegion,
handleRegionChange,
};
}

View File

@ -0,0 +1,58 @@
import { Dispatch, SetStateAction, useMemo } from 'react';
import { regions } from 'utils/regions';
interface UseRegionSelectionProps {
selectedRegions: string[];
setSelectedRegions: Dispatch<SetStateAction<string[]>>;
setIncludeAllRegions: Dispatch<SetStateAction<boolean>>;
}
interface UseRegionSelection {
allRegionIds: string[];
handleSelectAll: (checked: boolean) => void;
handleRegionSelect: (regionId: string) => void;
}
export function useRegionSelection({
selectedRegions,
setSelectedRegions,
setIncludeAllRegions,
}: UseRegionSelectionProps): UseRegionSelection {
const allRegionIds = useMemo(
() => regions.flatMap((r) => r.subRegions.map((sr) => sr.id)),
[],
);
const handleSelectAll = (checked: boolean): void => {
setIncludeAllRegions(checked);
setSelectedRegions(checked ? ['all'] : []);
};
const handleRegionSelect = (regionId: string): void => {
if (selectedRegions.includes('all')) {
const filteredRegionIds = allRegionIds.filter((id) => id !== regionId);
setSelectedRegions(filteredRegionIds);
setIncludeAllRegions(false);
return;
}
setSelectedRegions((prev) => {
const newSelection = prev.includes(regionId)
? prev.filter((id) => id !== regionId)
: [...prev, regionId];
if (newSelection.length === allRegionIds.length) {
setIncludeAllRegions(true);
return ['all'];
}
return newSelection;
});
};
return {
allRegionIds,
handleSelectAll,
handleRegionSelect,
};
}

View File

@ -0,0 +1,17 @@
import { getServiceDetails } from 'api/integrations/aws';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { ServiceData } from 'container/CloudIntegrationPage/ServicesSection/types';
import { useQuery, UseQueryResult } from 'react-query';
export function useServiceDetails(
serviceId: string,
accountId?: string,
): UseQueryResult<ServiceData> {
return useQuery(
[REACT_QUERY_KEY.AWS_SERVICE_DETAILS, serviceId, accountId],
() => getServiceDetails(serviceId, accountId),
{
enabled: !!serviceId,
},
);
}

View File

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

View File

@ -0,0 +1,45 @@
import { updateServiceConfig } from 'api/integrations/aws';
import { useMutation, UseMutationResult } from 'react-query';
interface UpdateServiceConfigPayload {
cloud_account_id: string;
config: {
logs: {
enabled: boolean;
};
metrics: {
enabled: boolean;
};
};
}
interface UpdateConfigResponse {
status: string;
data: {
id: string;
config: {
logs: {
enabled: boolean;
};
metrics: {
enabled: boolean;
};
};
};
}
interface UpdateConfigVariables {
serviceId: string;
payload: UpdateServiceConfigPayload;
}
export function useUpdateServiceConfig(): UseMutationResult<
UpdateConfigResponse,
Error,
UpdateConfigVariables
> {
return useMutation<UpdateConfigResponse, Error, UpdateConfigVariables>({
mutationFn: ({ serviceId, payload }) =>
updateServiceConfig(serviceId, payload),
});
}

View File

@ -106,6 +106,9 @@ export const useThemeConfig = (): ThemeConfig => {
Input: {
colorBorder: isDarkMode ? '#1D212D' : '#E9E9E9',
},
Breadcrumb: {
separatorMargin: 4,
},
},
};
};

View File

@ -23,7 +23,7 @@ interface IntegrationDetailHeaderProps {
title: string;
description: string;
icon: string;
refetchIntegrationDetails: () => void;
onUnInstallSuccess: () => void;
connectionState: ConnectionStates;
connectionData: IntegrationConnectionStatus;
setActiveDetailTab: React.Dispatch<React.SetStateAction<string | null>>;
@ -39,7 +39,7 @@ function IntegrationDetailHeader(
description,
connectionState,
connectionData,
refetchIntegrationDetails,
onUnInstallSuccess,
setActiveDetailTab,
} = props;
const [isModalOpen, setIsModalOpen] = useState(false);
@ -62,7 +62,7 @@ function IntegrationDetailHeader(
installIntegration,
{
onSuccess: () => {
refetchIntegrationDetails();
onUnInstallSuccess();
},
onError: () => {
notifications.error({

View File

@ -300,13 +300,13 @@
.uninstall-integration-btn {
border-radius: 2px;
background: var(--Accent---Secondary-Cherry, #da5565);
border-color: unset !important;
background: var(--bg-cherry-500);
border: none !important;
padding: 9px 13px;
display: flex;
align-items: center;
justify-content: center;
color: var(--bg-ink-300);
color: var(--bg-vanilla-100);
text-align: center;
font-family: Inter;
font-size: 12px;
@ -317,7 +317,7 @@
.uninstall-integration-btn:hover {
&.ant-btn-default {
color: var(--bg-ink-300) !important;
color: var(--bg-vanilla-100) !important;
}
}
}

View File

@ -126,7 +126,7 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
logs: null,
metrics: null,
})}
refetchIntegrationDetails={refetch}
onUnInstallSuccess={refetch}
setActiveDetailTab={setActiveDetailTab}
/>
<IntegrationDetailContent
@ -140,7 +140,7 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
<IntergrationsUninstallBar
integrationTitle={defaultTo(integrationData?.title, '')}
integrationId={selectedIntegration}
refetchIntegrationDetails={refetch}
onUnInstallSuccess={refetch}
connectionStatus={connectionStatus}
/>
)}

View File

@ -15,8 +15,9 @@ import { ConnectionStates } from './TestConnection';
interface IntergrationsUninstallBarProps {
integrationTitle: string;
integrationId: string;
refetchIntegrationDetails: () => void;
onUnInstallSuccess: () => void;
connectionStatus: ConnectionStates;
removeIntegrationTitle?: string;
}
function IntergrationsUninstallBar(
props: IntergrationsUninstallBarProps,
@ -24,8 +25,9 @@ function IntergrationsUninstallBar(
const {
integrationTitle,
integrationId,
refetchIntegrationDetails,
onUnInstallSuccess,
connectionStatus,
removeIntegrationTitle = 'Remove from SigNoz',
} = props;
const { notifications } = useNotifications();
const [isModalOpen, setIsModalOpen] = useState(false);
@ -35,7 +37,7 @@ function IntergrationsUninstallBar(
isLoading: isUninstallLoading,
} = useMutation(unInstallIntegration, {
onSuccess: () => {
refetchIntegrationDetails();
onUnInstallSuccess?.();
setIsModalOpen(false);
},
onError: () => {
@ -79,7 +81,7 @@ function IntergrationsUninstallBar(
icon={<X size={14} />}
onClick={(): void => showModal()}
>
Remove from SigNoz
{removeIntegrationTitle}
</Button>
<Modal
className="remove-integration-modal"
@ -103,4 +105,8 @@ function IntergrationsUninstallBar(
);
}
IntergrationsUninstallBar.defaultProps = {
removeIntegrationTitle: 'Remove from SigNoz',
};
export default IntergrationsUninstallBar;

View File

@ -133,6 +133,19 @@
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 10px;
&__new-tag {
color: var(--bg-forest-400);
font-size: 12px;
font-weight: 500;
letter-spacing: 0.54px;
border-radius: 2px;
border: 1px solid rgba(37, 225, 146, 0.1);
background: rgba(37, 225, 146, 0.1);
padding: 2px 8px;
}
}
.description {

View File

@ -7,9 +7,18 @@ import { Button, List, Typography } from 'antd';
import { useGetAllIntegrations } from 'hooks/Integrations/useGetAllIntegrations';
import { MoveUpRight, RotateCw } from 'lucide-react';
import { Dispatch, SetStateAction, useMemo } from 'react';
import { IntegrationsProps } from 'types/api/integrations/types';
import { isCloudUser } from 'utils/app';
import { handleContactSupport } from './utils';
import { handleContactSupport, INTEGRATION_TYPES } from './utils';
export const AWS_INTEGRATION = {
id: INTEGRATION_TYPES.AWS_INTEGRATION,
title: 'AWS Web Services',
description: 'One-click setup for AWS monitoring with SigNoz',
icon: `Logos/aws-dark.svg`,
is_new: true,
};
interface IntegrationsListProps {
setSelectedIntegration: (id: string) => void;
@ -30,12 +39,24 @@ function IntegrationsList(props: IntegrationsListProps): JSX.Element {
} = useGetAllIntegrations();
const filteredDataList = useMemo(() => {
let integrationsList: IntegrationsProps[] = [];
// Temporarily hide AWS Integration from the list, uncomment when the BE changes are finalized
// if (AWS_INTEGRATION.title.toLowerCase().includes(searchTerm.toLowerCase())) {
// integrationsList.push(AWS_INTEGRATION);
// }
// Add other integrations
if (data?.data.data.integrations) {
return data?.data.data.integrations.filter((item) =>
item.title.toLowerCase().includes(searchTerm.toLowerCase()),
);
integrationsList = [
...integrationsList,
...data.data.data.integrations.filter((item) =>
item.title.toLowerCase().includes(searchTerm.toLowerCase()),
),
];
}
return [];
return integrationsList;
}, [data?.data.data.integrations, searchTerm]);
const loading = isLoading || isFetching || isRefetching;
@ -93,7 +114,10 @@ function IntegrationsList(props: IntegrationsListProps): JSX.Element {
<img src={item.icon} alt={item.title} className="list-item-image" />
</div>
<div className="list-item-details">
<Typography.Text className="heading">{item.title}</Typography.Text>
<Typography.Text className="heading">
{item.title}
{item.is_new && <div className="heading__new-tag">NEW</div>}
</Typography.Text>
<Typography.Text className="description">
{item.description}
</Typography.Text>

View File

@ -20,3 +20,7 @@ export const INTEGRATION_TELEMETRY_EVENTS = {
INTEGRATIONS_DETAIL_CONFIGURE_INSTRUCTION:
'Integrations Detail Page: Navigated to configure an integration',
};
export const INTEGRATION_TYPES = {
AWS_INTEGRATION: 'aws-integration',
};

View File

@ -2,18 +2,27 @@ import './IntegrationsModulePage.styles.scss';
import RouteTab from 'components/RouteTab';
import { TabRoutes } from 'components/RouteTab/types';
import CloudIntegrationPage from 'container/CloudIntegrationPage/CloudIntegrationPage';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { INTEGRATION_TYPES } from 'pages/Integrations/utils';
import { useLocation } from 'react-use';
import { installedIntegrations } from './constants';
function IntegrationsModulePage(): JSX.Element {
const { pathname } = useLocation();
const urlQuery = useUrlQuery();
const selectedIntegration = urlQuery.get('integration');
const routes: TabRoutes[] = [installedIntegrations];
return (
<div className="integrations-module-container">
<RouteTab routes={routes} activeKey={pathname} history={history} />
{selectedIntegration === INTEGRATION_TYPES.AWS_INTEGRATION ? (
<CloudIntegrationPage />
) : (
<RouteTab routes={routes} activeKey={pathname} history={history} />
)}
</div>
);
}

View File

@ -0,0 +1,39 @@
import { CloudAccount } from 'container/CloudIntegrationPage/ServicesSection/types';
export interface GenerateConnectionUrlPayload {
agent_config: {
region: string;
};
account_config: {
regions: string[];
};
account_id?: string;
}
export interface ConnectionUrlResponse {
connection_url: string;
account_id: string;
}
export interface AccountStatusResponse {
status: 'success';
data: {
id: string;
status: {
integration: {
last_heartbeat_ts_ms: number | null;
};
};
};
}
export interface AccountConfigPayload {
config: {
regions: string[];
};
}
export interface AccountConfigResponse {
status: string;
data: CloudAccount;
}

View File

@ -1,4 +1,4 @@
interface IntegrationsProps {
export interface IntegrationsProps {
author: {
email: string;
homepage: string;
@ -8,6 +8,7 @@ interface IntegrationsProps {
id: string;
icon: string;
is_installed: boolean;
is_new?: boolean;
title: string;
}

View File

@ -0,0 +1,152 @@
export interface Region {
id: string;
name: string;
subRegions: SubRegion[];
}
export interface SubRegion {
id: string;
name: string;
displayName: string;
}
const regions: Region[] = [
{
id: 'north-america',
name: 'North America',
subRegions: [
{ id: 'us-east-1', name: 'us-east-1', displayName: 'US East (N. Virginia)' },
{ id: 'us-east-2', name: 'us-east-2', displayName: 'US East (Ohio)' },
{
id: 'us-west-1',
name: 'us-west-1',
displayName: 'US West (N. California)',
},
{ id: 'us-west-2', name: 'us-west-2', displayName: 'US West (Oregon)' },
{
id: 'ca-central-1',
name: 'ca-central-1',
displayName: 'Canada (Central)',
},
{ id: 'ca-west-1', name: 'ca-west-1', displayName: 'Canada (West)' },
],
},
{
id: 'africa',
name: 'Africa',
subRegions: [
{ id: 'af-south-1', name: 'af-south-1', displayName: 'Africa (Cape Town)' },
],
},
{
id: 'asia-pacific',
name: 'Asia Pacific',
subRegions: [
{
id: 'ap-east-1',
name: 'ap-east-1',
displayName: 'Asia Pacific (Hong Kong)',
},
{
id: 'ap-northeast-1',
name: 'ap-northeast-1',
displayName: 'Asia Pacific (Tokyo)',
},
{
id: 'ap-northeast-2',
name: 'ap-northeast-2',
displayName: 'Asia Pacific (Seoul)',
},
{
id: 'ap-northeast-3',
name: 'ap-northeast-3',
displayName: 'Asia Pacific (Osaka)',
},
{
id: 'ap-south-1',
name: 'ap-south-1',
displayName: 'Asia Pacific (Mumbai)',
},
{
id: 'ap-south-2',
name: 'ap-south-2',
displayName: 'Asia Pacific (Hyderabad)',
},
{
id: 'ap-southeast-1',
name: 'ap-southeast-1',
displayName: 'Asia Pacific (Singapore)',
},
{
id: 'ap-southeast-2',
name: 'ap-southeast-2',
displayName: 'Asia Pacific (Sydney)',
},
{
id: 'ap-southeast-3',
name: 'ap-southeast-3',
displayName: 'Asia Pacific (Jakarta)',
},
{
id: 'ap-southeast-4',
name: 'ap-southeast-4',
displayName: 'Asia Pacific (Melbourne)',
},
{
id: 'ap-southeast-5',
name: 'ap-southeast-5',
displayName: 'Asia Pacific (Auckland)',
},
],
},
{
id: 'europe',
name: 'Europe',
subRegions: [
{
id: 'eu-central-1',
name: 'eu-central-1',
displayName: 'Europe (Frankfurt)',
},
{ id: 'eu-central-2', name: 'eu-central-2', displayName: 'Europe (Zurich)' },
{ id: 'eu-north-1', name: 'eu-north-1', displayName: 'Europe (Stockholm)' },
{ id: 'eu-south-1', name: 'eu-south-1', displayName: 'Europe (Milan)' },
{ id: 'eu-south-2', name: 'eu-south-2', displayName: 'Europe (Spain)' },
{ id: 'eu-west-1', name: 'eu-west-1', displayName: 'Europe (Ireland)' },
{ id: 'eu-west-2', name: 'eu-west-2', displayName: 'Europe (London)' },
{ id: 'eu-west-3', name: 'eu-west-3', displayName: 'Europe (Paris)' },
],
},
{
id: 'middle-east',
name: 'Middle East',
subRegions: [
{
id: 'il-central-1',
name: 'il-central-1',
displayName: 'Israel (Tel Aviv)',
},
{
id: 'me-central-1',
name: 'me-central-1',
displayName: 'Middle East (UAE)',
},
{
id: 'me-south-1',
name: 'me-south-1',
displayName: 'Middle East (Bahrain)',
},
],
},
{
id: 'south-america',
name: 'South America',
subRegions: [
{
id: 'sa-east-1',
name: 'sa-east-1',
displayName: 'South America (São Paulo)',
},
],
},
];
export { regions };

View File

@ -4320,6 +4320,13 @@
dependencies:
react-helmet-async "*"
"@types/react-lottie@1.2.10":
version "1.2.10"
resolved "https://registry.yarnpkg.com/@types/react-lottie/-/react-lottie-1.2.10.tgz#220f68a2dfa0d4b131ab4930e8bf166b9442c68c"
integrity sha512-rCd1p3US4ELKJlqwVnP0h5b24zt5p9OCvKUoNpYExLqwbFZMWEiJ6EGLMmH7nmq5V7KomBIbWO2X/XRFsL0vCA==
dependencies:
"@types/react" "*"
"@types/react-redux@^7.1.11", "@types/react-redux@^7.1.20":
version "7.1.25"
resolved "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz"
@ -11022,6 +11029,11 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
lottie-web@^5.12.2:
version "5.12.2"
resolved "https://registry.yarnpkg.com/lottie-web/-/lottie-web-5.12.2.tgz#579ca9fe6d3fd9e352571edd3c0be162492f68e5"
integrity sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==
lower-case@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz"
@ -13221,7 +13233,7 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@15, prop-types@15.x, prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
prop-types@15, prop-types@15.8.1, prop-types@15.x, prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -13941,6 +13953,15 @@ react-lifecycles-compat@^3.0.4:
resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-lottie@1.2.10:
version "1.2.10"
resolved "https://registry.yarnpkg.com/react-lottie/-/react-lottie-1.2.10.tgz#399f78a448a7833b2380d74fc489ecf15f8d18c7"
integrity sha512-x0eWX3Z6zSx1XM5QSjnLupc6D22LlMCB0PH06O/N/epR2hsLaj1Vxd9RtMnbbEHjJ/qlsgHJ6bpN3vnZI92hjw==
dependencies:
babel-runtime "^6.26.0"
lottie-web "^5.12.2"
prop-types "^15.6.1"
react-markdown@8.0.7, react-markdown@~8.0.0:
version "8.0.7"
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.7.tgz#c8dbd1b9ba5f1c5e7e5f2a44de465a3caafdf89b"