mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-08 17:08:58 +08:00
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:
parent
3849ca1ecc
commit
e542e96031
@ -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",
|
||||
|
6
frontend/public/Icons/solid-check-circle.svg
Normal file
6
frontend/public/Icons/solid-check-circle.svg
Normal 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 |
BIN
frontend/public/Images/integrations-hero-bg.png
Normal file
BIN
frontend/public/Images/integrations-hero-bg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 194 KiB |
23
frontend/public/Logos/aws-dark.svg
Normal file
23
frontend/public/Logos/aws-dark.svg
Normal 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 |
9
frontend/public/Logos/aws-light.svg
Normal file
9
frontend/public/Logos/aws-light.svg
Normal 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 |
76
frontend/src/api/integrations/aws/index.ts
Normal file
76
frontend/src/api/integrations/aws/index.ts
Normal 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;
|
||||
};
|
15119
frontend/src/assets/Lotties/integrations-success.json
Normal file
15119
frontend/src/assets/Lotties/integrations-success.json
Normal file
File diff suppressed because it is too large
Load Diff
117
frontend/src/components/SignozModal/SignozModal.style.scss
Normal file
117
frontend/src/components/SignozModal/SignozModal.style.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
frontend/src/components/SignozModal/SignozModal.tsx
Normal file
25
frontend/src/components/SignozModal/SignozModal.tsx
Normal 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;
|
@ -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',
|
||||
};
|
||||
|
@ -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',
|
||||
|
@ -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;
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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;
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
@ -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 };
|
@ -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>
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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, here’s 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 SigNoz’s 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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
@ -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,
|
||||
};
|
139
frontend/src/hooks/integrations/aws/useAccountSettingsModal.ts
Normal file
139
frontend/src/hooks/integrations/aws/useAccountSettingsModal.ts
Normal 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 };
|
21
frontend/src/hooks/integrations/aws/useAccountStatus.ts
Normal file
21
frontend/src/hooks/integrations/aws/useAccountStatus.ts
Normal 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,
|
||||
});
|
||||
}
|
8
frontend/src/hooks/integrations/aws/useAwsAccounts.ts
Normal file
8
frontend/src/hooks/integrations/aws/useAwsAccounts.ts
Normal 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);
|
||||
}
|
@ -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,
|
||||
});
|
||||
}
|
12
frontend/src/hooks/integrations/aws/useGetAccountServices.ts
Normal file
12
frontend/src/hooks/integrations/aws/useGetAccountServices.ts
Normal 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),
|
||||
);
|
||||
}
|
164
frontend/src/hooks/integrations/aws/useIntegrationModal.ts
Normal file
164
frontend/src/hooks/integrations/aws/useIntegrationModal.ts
Normal 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,
|
||||
};
|
||||
}
|
58
frontend/src/hooks/integrations/aws/useRegionSelection.ts
Normal file
58
frontend/src/hooks/integrations/aws/useRegionSelection.ts
Normal 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,
|
||||
};
|
||||
}
|
17
frontend/src/hooks/integrations/aws/useServiceDetails.ts
Normal file
17
frontend/src/hooks/integrations/aws/useServiceDetails.ts
Normal 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,
|
||||
},
|
||||
);
|
||||
}
|
@ -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),
|
||||
});
|
||||
}
|
@ -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),
|
||||
});
|
||||
}
|
@ -106,6 +106,9 @@ export const useThemeConfig = (): ThemeConfig => {
|
||||
Input: {
|
||||
colorBorder: isDarkMode ? '#1D212D' : '#E9E9E9',
|
||||
},
|
||||
Breadcrumb: {
|
||||
separatorMargin: 4,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
39
frontend/src/types/api/integrations/aws.ts
Normal file
39
frontend/src/types/api/integrations/aws.ts
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
152
frontend/src/utils/regions.ts
Normal file
152
frontend/src/utils/regions.ts
Normal 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 };
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user