diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a671c9ed815fa..135a96d9a6033 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -386,6 +386,9 @@ importers: events: specifier: 3.3.0 version: 3.3.0 + react-markdown: + specifier: ^9.0.3 + version: 9.1.0(@types/react@18.3.10)(react@18.3.1) devDependencies: '@gravitational/build': specifier: workspace:* @@ -474,7 +477,7 @@ importers: version: 34.1.1 electron-builder: specifier: ^25.1.8 - version: 25.1.8(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)) + version: 25.1.8(electron-builder-squirrel-windows@25.1.8) electron-vite: specifier: ^2.3.0 version: 2.3.0(@swc/core@1.7.26)(vite@5.4.8(@types/node@22.7.4)(terser@5.31.1)) @@ -2562,6 +2565,9 @@ packages: '@types/escodegen@0.0.6': resolution: {integrity: sha512-AjwI4MvWx3HAOaZqYsjKWyEObT9lcVV0Y0V8nXo6cXzN8ZiMxVhf6F3d/UNvXVGKrEzL/Dluc5p+y9GkzlTWig==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@0.0.51': resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==} @@ -2586,6 +2592,9 @@ packages: '@types/graceful-fs@4.1.8': resolution: {integrity: sha512-NhRH7YzWq8WiNKVavKPBmtLYZHxNY19Hh+az28O/phfp68CF45pMFud+ZzJ8ewnxnC5smIdF3dqFeiSUQ5I+pw==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/history@4.7.11': resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} @@ -2623,6 +2632,9 @@ packages: resolution: {integrity: sha512-xoBtGl5R9jeKUhc8ZqeYaRDx04qqJ10yhhXYGmJ4Jr8qKpvMsDQQrNUvF/wUJ4klOtmJeJM+p2Xo3zp9uaC3tw==} deprecated: This is a stub types definition. keyv provides its own type definitions, so you do not need this installed. + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} @@ -2719,6 +2731,12 @@ packages: '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/verror@1.10.5': resolution: {integrity: sha512-9UjMCHK5GPgQRoNbqdLIAvAy0EInuiqbW0PBMtVP6B5B2HQJlvoJHM+KodPZMEjOa5VkSc+5LH7xy+cUzQdmHw==} @@ -3191,6 +3209,9 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -3319,6 +3340,9 @@ packages: caniuse-lite@1.0.30001651: resolution: {integrity: sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -3343,6 +3367,18 @@ packages: resolution: {integrity: sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==} engines: {node: '>=12.20'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} @@ -3430,6 +3466,9 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -3678,6 +3717,9 @@ packages: decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + decode-named-character-reference@1.0.2: + resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -3750,6 +3792,9 @@ packages: detect-node@2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4151,6 +4196,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -4192,6 +4240,9 @@ packages: resolution: {integrity: sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==} engines: {node: '>= 0.10.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + extract-zip@2.0.1: resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} engines: {node: '>= 10.17.0'} @@ -4514,6 +4565,12 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-to-jsx-runtime@2.3.3: + resolution: {integrity: sha512-pdpkP8YD4v+qMKn2lnKSiJvZvb3FunDmFYQvVOsoO08+eTNWdaWKPMrC5wwNICtU3dQWHhElj5Sf5jPEnv4qJg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + headers-polyfill@4.0.3: resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} @@ -4552,6 +4609,9 @@ packages: resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} engines: {node: '>=8'} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + htmlparser2@3.10.1: resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==} @@ -4645,6 +4705,9 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + inline-style-parser@0.2.4: + resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + internal-slot@1.0.7: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} @@ -4664,6 +4727,12 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-arguments@1.1.1: resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} engines: {node: '>= 0.4'} @@ -4712,6 +4781,9 @@ packages: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} engines: {node: '>= 0.4'} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -4740,6 +4812,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} @@ -4769,6 +4844,10 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} @@ -5251,6 +5330,9 @@ packages: long@5.2.3: resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -5299,6 +5381,30 @@ packages: resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} engines: {node: '>=10'} + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -5320,6 +5426,69 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} + micromark-core-commonmark@2.0.2: + resolution: {integrity: sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.0.4: + resolution: {integrity: sha512-N6hXjrin2GTJDe3MVjf5FuXpm12PGm80BrUAeub9XFXca8JZbP+oIwY4LJSVwFUCL1IPm/WwSVUN7goFHmSGGQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.1: + resolution: {integrity: sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==} + + micromark@4.0.1: + resolution: {integrity: sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -5643,6 +5812,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -5795,6 +5967,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@7.0.0: + resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==} + protobufjs@7.3.2: resolution: {integrity: sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==} engines: {node: '>=12.0.0'} @@ -5907,6 +6082,12 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-markdown@9.1.0: + resolution: {integrity: sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react-router-dom@5.3.4: resolution: {integrity: sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==} peerDependencies: @@ -5994,6 +6175,12 @@ packages: resolution: {integrity: sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==} engines: {node: '>=4'} + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.1: + resolution: {integrity: sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -6261,6 +6448,9 @@ packages: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + spawn-wrap@2.0.0: resolution: {integrity: sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==} engines: {node: '>=8'} @@ -6351,6 +6541,9 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -6386,6 +6579,9 @@ packages: style-mod@4.1.2: resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==} + style-to-object@1.0.8: + resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} + styled-components@6.1.13: resolution: {integrity: sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw==} engines: {node: '>= 16'} @@ -6519,9 +6715,15 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + triple-beam@1.3.0: resolution: {integrity: sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==} + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + truncate-utf8-bytes@1.0.2: resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} @@ -6659,6 +6861,9 @@ packages: resolution: {integrity: sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==} engines: {node: '>=4'} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + unique-filename@2.0.1: resolution: {integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -6667,6 +6872,21 @@ packages: resolution: {integrity: sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -6751,6 +6971,12 @@ packages: resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} engines: {node: '>=0.6.0'} + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-plugin-wasm@3.3.0: resolution: {integrity: sha512-tVhz6w+W9MVsOCHzxo6SSMSswCeIw4HTrXEi6qL3IRzATl83jl09JVO1djBqPSwfjgnpVHNLYcaMbaDX5WB/pg==} peerDependencies: @@ -7013,6 +7239,9 @@ packages: zone.js@0.14.7: resolution: {integrity: sha512-0w6DGkX2BPuiK/NLf+4A8FLE43QwBfuqz2dVgi/40Rj1WmqUskCqj329O/pwrqFJLG5X8wkeG2RhIAro441xtg==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: 7zip-bin@5.2.0: {} @@ -9539,6 +9768,10 @@ snapshots: '@types/escodegen@0.0.6': {} + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.6 + '@types/estree@0.0.51': {} '@types/estree@1.0.6': {} @@ -9572,6 +9805,10 @@ snapshots: dependencies: '@types/node': 20.16.10 + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/history@4.7.11': {} '@types/http-cache-semantics@4.0.1': {} @@ -9614,6 +9851,10 @@ snapshots: dependencies: keyv: 4.5.0 + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/mime@1.3.5': {} '@types/minimatch@5.1.2': {} @@ -9722,6 +9963,10 @@ snapshots: '@types/triple-beam@1.3.5': {} + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + '@types/verror@1.10.5': optional: true @@ -10043,7 +10288,7 @@ snapshots: app-builder-bin@5.0.0-alpha.10: {} - app-builder-lib@25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)): + app-builder-lib@25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8): dependencies: '@develar/schema-utils': 2.6.5 '@electron/notarize': 2.5.0 @@ -10350,6 +10595,8 @@ snapshots: babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.25.2) + bail@2.0.2: {} + balanced-match@1.0.2: {} bare-events@2.4.2: @@ -10542,6 +10789,8 @@ snapshots: caniuse-lite@1.0.30001651: {} + ccount@2.0.1: {} + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -10564,6 +10813,14 @@ snapshots: char-regex@2.0.1: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + chownr@2.0.0: {} chromium-pickle-js@0.2.0: {} @@ -10655,6 +10912,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} + commander@2.20.3: optional: true @@ -10908,6 +11167,10 @@ snapshots: decimal.js@10.4.3: {} + decode-named-character-reference@1.0.2: + dependencies: + character-entities: 2.0.2 + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -10961,6 +11224,10 @@ snapshots: detect-node@2.1.0: optional: true + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + diff-sequences@29.6.3: {} diffable-html@4.1.0: @@ -10978,7 +11245,7 @@ snapshots: dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8): dependencies: - app-builder-lib: 25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)) + app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8) builder-util: 25.1.7 builder-util-runtime: 9.2.10 fs-extra: 10.1.0 @@ -11064,7 +11331,7 @@ snapshots: electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8): dependencies: - app-builder-lib: 25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)) + app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8) archiver: 5.3.2 builder-util: 25.1.7 fs-extra: 10.1.0 @@ -11073,9 +11340,9 @@ snapshots: - dmg-builder - supports-color - electron-builder@25.1.8(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)): + electron-builder@25.1.8(electron-builder-squirrel-windows@25.1.8): dependencies: - app-builder-lib: 25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)) + app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8) builder-util: 25.1.7 builder-util-runtime: 9.2.10 chalk: 4.1.2 @@ -11355,7 +11622,7 @@ snapshots: debug: 4.3.6 enhanced-resolve: 5.17.0 eslint: 8.57.0 - eslint-module-utils: 2.8.2(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.2(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.8.0 is-bun-module: 1.1.0 @@ -11368,7 +11635,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -11380,7 +11647,7 @@ snapshots: - supports-color optional: true - eslint-module-utils@2.8.2(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.2(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -11406,7 +11673,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -11554,6 +11821,8 @@ snapshots: estraverse@5.3.0: {} + estree-util-is-identifier-name@3.0.0: {} + estree-walker@2.0.2: {} esutils@2.0.3: {} @@ -11628,6 +11897,8 @@ snapshots: transitivePeerDependencies: - supports-color + extend@3.0.2: {} + extract-zip@2.0.1: dependencies: debug: 4.3.7 @@ -11995,6 +12266,30 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-jsx-runtime@2.3.3: + dependencies: + '@types/estree': 1.0.6 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.0.0 + space-separated-tokens: 2.0.2 + style-to-object: 1.0.8 + unist-util-position: 5.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + headers-polyfill@4.0.3: {} highlight-words-core@1.2.3: {} @@ -12034,6 +12329,8 @@ snapshots: html-tags@3.3.1: {} + html-url-attributes@3.0.1: {} + htmlparser2@3.10.1: dependencies: domelementtype: 1.3.1 @@ -12145,6 +12442,8 @@ snapshots: ini@1.3.8: {} + inline-style-parser@0.2.4: {} + internal-slot@1.0.7: dependencies: es-errors: 1.3.0 @@ -12162,6 +12461,13 @@ snapshots: ipaddr.js@1.9.1: {} + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-arguments@1.1.1: dependencies: call-bind: 1.0.7 @@ -12211,6 +12517,8 @@ snapshots: dependencies: has-tostringtag: 1.0.2 + is-decimal@2.0.1: {} + is-docker@2.2.1: {} is-extglob@2.1.1: {} @@ -12231,6 +12539,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-interactive@1.0.0: {} is-lambda@1.0.1: {} @@ -12249,6 +12559,8 @@ snapshots: is-path-inside@3.0.3: {} + is-plain-obj@4.1.0: {} + is-plain-object@5.0.0: {} is-potential-custom-element-name@1.0.1: {} @@ -13054,6 +13366,8 @@ snapshots: long@5.2.3: {} + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -13117,6 +13431,95 @@ snapshots: escape-string-regexp: 4.0.0 optional: true + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.2.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + media-typer@0.3.0: {} memoize-one@6.0.0: {} @@ -13129,6 +13532,139 @@ snapshots: methods@1.1.2: {} + micromark-core-commonmark@2.0.2: + dependencies: + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.0.4 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.1 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.0.2 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.1 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.0.4: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.1: {} + + micromark@4.0.1: + dependencies: + '@types/debug': 4.1.12 + debug: 4.3.7 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.2 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.0.4 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -13484,6 +14020,16 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.0.2 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.24.7 @@ -13612,6 +14158,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-information@7.0.0: {} + protobufjs@7.3.2: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -13747,6 +14295,24 @@ snapshots: react-is@18.3.1: {} + react-markdown@9.1.0(@types/react@18.3.10)(react@18.3.1): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 18.3.10 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.3 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.0 + react: 18.3.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.1 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-router-dom@5.3.4(react@18.3.1): dependencies: '@babel/runtime': 7.25.0 @@ -13893,6 +14459,23 @@ snapshots: dependencies: es6-error: 4.1.1 + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.1 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.3 + require-directory@2.1.1: {} require-in-the-middle@7.3.0: @@ -14187,6 +14770,8 @@ snapshots: source-map@0.7.4: {} + space-separated-tokens@2.0.2: {} + spawn-wrap@2.0.0: dependencies: foreground-child: 2.0.0 @@ -14316,6 +14901,11 @@ snapshots: dependencies: safe-buffer: 5.2.1 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -14342,6 +14932,10 @@ snapshots: style-mod@4.1.2: {} + style-to-object@1.0.8: + dependencies: + inline-style-parser: 0.2.4 + styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@emotion/is-prop-valid': 1.2.2 @@ -14507,8 +15101,12 @@ snapshots: tree-kill@1.2.2: {} + trim-lines@3.0.1: {} + triple-beam@1.3.0: {} + trough@2.2.0: {} + truncate-utf8-bytes@1.0.2: dependencies: utf8-byte-length: 1.0.4 @@ -14641,6 +15239,16 @@ snapshots: unicode-property-aliases-ecmascript@2.0.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + unique-filename@2.0.1: dependencies: unique-slug: 3.0.0 @@ -14649,6 +15257,29 @@ snapshots: dependencies: imurmurhash: 0.1.4 + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + universalify@0.1.2: {} universalify@0.2.0: {} @@ -14723,6 +15354,16 @@ snapshots: extsprintf: 1.4.1 optional: true + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + vite-plugin-wasm@3.3.0(vite@5.4.8(@types/node@22.7.4)(terser@5.31.1)): dependencies: vite: 5.4.8(@types/node@22.7.4)(terser@5.31.1) @@ -15008,3 +15649,5 @@ snapshots: zod@3.23.8: {} zone.js@0.14.7: {} + + zwitch@2.0.4: {} diff --git a/web/__mocks__/react-markdown.js b/web/__mocks__/react-markdown.js new file mode 100644 index 0000000000000..9bed155671a2c --- /dev/null +++ b/web/__mocks__/react-markdown.js @@ -0,0 +1,7 @@ +// Manually mocks react-markdown for testing due to ES modules +// https://jestjs.io/docs/manual-mocks +function ReactMarkdown({ children }) { + return <>{children}; +} + +export default ReactMarkdown; diff --git a/web/packages/design/src/Tabs/Tabs.ts b/web/packages/design/src/Tabs/Tabs.ts new file mode 100644 index 0000000000000..cf2c1719a52f0 --- /dev/null +++ b/web/packages/design/src/Tabs/Tabs.ts @@ -0,0 +1,57 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { NavLink } from 'react-router-dom'; +import styled from 'styled-components'; + +export const TabsContainer = styled.div` + position: relative; + display: flex; + gap: ${p => p.theme.space[5]}px; + align-items: center; + padding: 0 ${p => p.theme.space[5]}px; + border-bottom: 1px solid ${p => p.theme.colors.spotBackground[0]}; +`; + +export const TabContainer = styled(NavLink)<{ selected?: boolean }>` + padding: ${p => p.theme.space[1] + p.theme.space[2]}px + ${p => p.theme.space[2]}px; + position: relative; + cursor: pointer; + z-index: 2; + opacity: ${p => (p.selected ? 1 : 0.5)}; + transition: opacity 0.3s linear; + color: ${p => p.theme.colors.text.main}; + font-weight: 300; + font-size: 22px; + line-height: ${p => p.theme.space[5]}px; + white-space: nowrap; + text-decoration: none; + + &:hover { + opacity: 1; + } +`; + +export const TabBorder = styled.div` + position: absolute; + bottom: -1px; + background: ${p => p.theme.colors.brand}; + height: 2px; + transition: all 0.3s cubic-bezier(0.19, 1, 0.22, 1); +`; diff --git a/web/packages/teleport/package.json b/web/packages/teleport/package.json index d76bc3addb6d4..e4ef12e983162 100644 --- a/web/packages/teleport/package.json +++ b/web/packages/teleport/package.json @@ -39,7 +39,8 @@ "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", "create-react-class": "^15.6.3", - "events": "3.3.0" + "events": "3.3.0", + "react-markdown": "^9.0.3" }, "devDependencies": { "@gravitational/build": "workspace:*", diff --git a/web/packages/teleport/src/Integrations/IntegrationList.tsx b/web/packages/teleport/src/Integrations/IntegrationList.tsx index 76de1610e11e7..cd8a0bdc92ecd 100644 --- a/web/packages/teleport/src/Integrations/IntegrationList.tsx +++ b/web/packages/teleport/src/Integrations/IntegrationList.tsx @@ -69,7 +69,7 @@ export function IntegrationList(props: Props) { } function getRowStyle(row: IntegrationLike): React.CSSProperties { - if (row.kind !== 'okta') return; + if (row.kind !== 'okta' && row.kind !== IntegrationKind.AwsOidc) return; return { cursor: 'pointer' }; } diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.story.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.story.tsx index 0540665408f80..06c54e9c99a3a 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.story.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.story.tsx @@ -16,25 +16,37 @@ * along with this program. If not, see . */ -import { addHours } from 'date-fns'; -import React from 'react'; +import { + makeErrorAttempt, + makeProcessingAttempt, + makeSuccessAttempt, +} from 'shared/hooks/useAsync'; +import cfg from 'teleport/config'; import { AwsOidcDashboard } from 'teleport/Integrations/status/AwsOidc/AwsOidcDashboard'; import { MockAwsOidcStatusProvider } from 'teleport/Integrations/status/AwsOidc/testHelpers/mockAwsOidcStatusProvider'; -import { AwsOidcStatusContextState } from 'teleport/Integrations/status/AwsOidc/useAwsOidcStatus'; -import { - IntegrationKind, - ResourceTypeSummary, -} from 'teleport/services/integrations'; +import { IntegrationKind } from 'teleport/services/integrations'; + +import { makeAwsOidcStatusContextState } from './testHelpers/makeAwsOidcStatusContextState'; export default { title: 'Teleport/Integrations/AwsOidc', }; +const setup = { + path: cfg.routes.integrationStatus, + initialEntries: [ + cfg.getIntegrationStatusRoute(IntegrationKind.AwsOidc, 'oidc-int'), + ], +}; + // Loaded dashboard with data for each aws resource and a navigation header export function Dashboard() { return ( - + ); @@ -42,12 +54,23 @@ export function Dashboard() { // Loaded dashboard with missing data for each aws resource and a navigation header export function DashboardMissingData() { + const state = makeAwsOidcStatusContextState(); + state.statsAttempt.data = undefined; + return ( + + + + ); +} + +// Loaded dashboard with missing data for each aws resource and a navigation header +export function DashboardMissingSummary() { const state = makeAwsOidcStatusContextState(); state.statsAttempt.data.awseks = undefined; state.statsAttempt.data.awsrds = undefined; state.statsAttempt.data.awsec2 = undefined; return ( - + ); @@ -56,10 +79,10 @@ export function DashboardMissingData() { // Loading screen export function StatsProcessing() { const props = makeAwsOidcStatusContextState({ - statsAttempt: { status: 'processing', data: null, statusText: '' }, + statsAttempt: makeProcessingAttempt(), }); return ( - + ); @@ -68,14 +91,10 @@ export function StatsProcessing() { // No header, no loading indicator export function IntegrationProcessing() { const props = makeAwsOidcStatusContextState({ - integrationAttempt: { - status: 'processing', - data: null, - statusText: '', - }, + integrationAttempt: makeProcessingAttempt(), }); return ( - + ); @@ -84,32 +103,39 @@ export function IntegrationProcessing() { // Loaded error message export function StatsFailed() { const props = makeAwsOidcStatusContextState({ - statsAttempt: { - status: 'error', - data: null, - statusText: 'failed to get stats', - error: {}, - }, + statsAttempt: makeErrorAttempt(new Error('failed to get stats')), }); return ( - + ); } -// Loaded dashboard with data for each aws resource but no navigation header +// Loaded error message export function IntegrationFailed() { const props = makeAwsOidcStatusContextState({ - integrationAttempt: { - status: 'error', - data: null, - statusText: 'failed to get integration', - error: {}, - }, + integrationAttempt: makeErrorAttempt( + new Error('failed to get integration') + ), }); return ( - + + + + ); +} + +// Loaded error message +export function BothFailed() { + const props = makeAwsOidcStatusContextState({ + statsAttempt: makeErrorAttempt(new Error('failed to get stats')), + integrationAttempt: makeErrorAttempt( + new Error('failed to get integration') + ), + }); + return ( + ); @@ -118,10 +144,10 @@ export function IntegrationFailed() { // Blank screen export function StatsNoData() { const props = makeAwsOidcStatusContextState({ - statsAttempt: { status: 'success', data: null, statusText: '' }, + statsAttempt: makeSuccessAttempt(null), }); return ( - + ); @@ -130,71 +156,11 @@ export function StatsNoData() { // No header, no loading indicator export function IntegrationNoData() { const props = makeAwsOidcStatusContextState({ - integrationAttempt: { - status: 'success', - data: null, - statusText: '', - }, + integrationAttempt: makeSuccessAttempt(null), }); return ( - + ); } - -function makeAwsOidcStatusContextState( - overrides: Partial = {} -): AwsOidcStatusContextState { - return Object.assign( - { - integrationAttempt: { - status: 'success', - statusText: '', - data: { - resourceType: 'integration', - name: 'integration-one', - kind: IntegrationKind.AwsOidc, - spec: { - roleArn: 'arn:aws:iam::111456789011:role/bar', - }, - statusCode: 1, - }, - }, - statsAttempt: { - status: 'success', - statusText: '', - data: { - name: 'integration-one', - subKind: IntegrationKind.AwsOidc, - awsoidc: { - roleArn: 'arn:aws:iam::111456789011:role/bar', - }, - awsec2: makeResourceTypeSummary(), - awsrds: makeResourceTypeSummary(), - awseks: makeResourceTypeSummary(), - }, - }, - }, - overrides - ); -} - -function makeResourceTypeSummary( - overrides: Partial = {} -): ResourceTypeSummary { - return Object.assign( - { - rulesCount: Math.floor(Math.random() * 100), - resourcesFound: Math.floor(Math.random() * 100), - resourcesEnrollmentFailed: Math.floor(Math.random() * 100), - resourcesEnrollmentSuccess: Math.floor(Math.random() * 100), - discoverLastSync: addHours( - new Date().getTime(), - -Math.floor(Math.random() * 100) - ), - ecsDatabaseServiceCount: Math.floor(Math.random() * 100), - }, - overrides - ); -} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.test.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.test.tsx index 580625fdacc74..753a13313fe1b 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.test.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.test.tsx @@ -17,80 +17,84 @@ */ import { within } from '@testing-library/react'; -import React from 'react'; +import { addHours } from 'date-fns'; import { render, screen } from 'design/utils/testing'; +import { makeSuccessAttempt } from 'shared/hooks/useAsync'; -import { addHours } from 'teleport/components/BannerList/useAlerts'; import { AwsOidcDashboard } from 'teleport/Integrations/status/AwsOidc/AwsOidcDashboard'; import { MockAwsOidcStatusProvider } from 'teleport/Integrations/status/AwsOidc/testHelpers/mockAwsOidcStatusProvider'; -import { IntegrationKind } from 'teleport/services/integrations'; +import { + IntegrationAwsOidc, + IntegrationKind, + IntegrationWithSummary, +} from 'teleport/services/integrations'; test('renders header and stats cards', () => { render( ({ + resourceType: 'integration', + name: 'integration-one', + kind: IntegrationKind.AwsOidc, + spec: { + roleArn: 'arn:aws:iam::111456789011:role/bar', }, - }, - statsAttempt: { - status: 'success', - statusText: '', - data: { - name: 'integration-one', - subKind: IntegrationKind.AwsOidc, - awsoidc: { - roleArn: 'arn:aws:iam::111456789011:role/bar', - }, - awsec2: { - rulesCount: 24, - resourcesFound: 12, - resourcesEnrollmentFailed: 3, - resourcesEnrollmentSuccess: 9, - discoverLastSync: new Date().getTime(), - ecsDatabaseServiceCount: 0, // irrelevant - }, - awsrds: { - rulesCount: 14, - resourcesFound: 5, - resourcesEnrollmentFailed: 5, - resourcesEnrollmentSuccess: 0, - discoverLastSync: addHours(new Date().getTime(), -4), - ecsDatabaseServiceCount: 8, // relevant - }, - awseks: { - rulesCount: 33, - resourcesFound: 3, - resourcesEnrollmentFailed: 0, - resourcesEnrollmentSuccess: 3, - discoverLastSync: addHours(new Date().getTime(), -48), - ecsDatabaseServiceCount: 0, // irrelevant - }, + statusCode: 1, + }), + statsAttempt: makeSuccessAttempt({ + name: 'integration-one', + subKind: IntegrationKind.AwsOidc, + awsoidc: { + roleArn: 'arn:aws:iam::111456789011:role/bar', }, - }, + awsec2: { + rulesCount: 24, + resourcesFound: 12, + resourcesEnrollmentFailed: 3, + resourcesEnrollmentSuccess: 9, + discoverLastSync: new Date().getTime(), + ecsDatabaseServiceCount: 0, // irrelevant + }, + awsrds: { + rulesCount: 14, + resourcesFound: 5, + resourcesEnrollmentFailed: 5, + resourcesEnrollmentSuccess: 0, + discoverLastSync: addHours(new Date().getTime(), -4).getTime(), + ecsDatabaseServiceCount: 8, // relevant + }, + awseks: { + rulesCount: 33, + resourcesFound: 3, + resourcesEnrollmentFailed: 0, + resourcesEnrollmentSuccess: 3, + discoverLastSync: addHours(new Date().getTime(), -48).getTime(), + ecsDatabaseServiceCount: 0, // irrelevant + }, + }), }} + path="" > ); - expect(screen.getByRole('link', { name: 'back' })).toHaveAttribute( + const breadcrumbs = screen.getByTestId('aws-oidc-header'); + expect(within(breadcrumbs).getByText('integration-one')).toBeInTheDocument(); + + const title = screen.getByTestId('aws-oidc-title'); + expect(within(title).getByRole('link', { name: 'back' })).toHaveAttribute( 'href', '/web/integrations' ); - expect(screen.getByText('integration-one')).toBeInTheDocument(); - expect(screen.getByLabelText('status')).toHaveAttribute('kind', 'success'); - expect(screen.getByLabelText('status')).toHaveTextContent('Running'); + expect(within(title).getByLabelText('status')).toHaveAttribute( + 'kind', + 'success' + ); + expect(within(title).getByLabelText('status')).toHaveTextContent('Running'); + expect(within(title).getByText('integration-one')).toBeInTheDocument(); const ec2 = screen.getByTestId('ec2-stats'); expect(within(ec2).getByTestId('sync')).toHaveTextContent( @@ -137,3 +141,59 @@ test('renders header and stats cards', () => { 'Failed Clusters 0' ); }); + +test('renders enroll cards', () => { + const zeroCount = { + rulesCount: 0, + resourcesFound: 0, + resourcesEnrollmentFailed: 0, + resourcesEnrollmentSuccess: 0, + discoverLastSync: new Date().getTime(), + ecsDatabaseServiceCount: 0, + }; + + render( + + + + ); + + expect( + within(screen.getByTestId('ec2-enroll')).getByRole('link', { + name: 'Enroll EC2', + }) + ).toBeInTheDocument(); + expect( + within(screen.getByTestId('rds-enroll')).getByRole('link', { + name: 'Enroll RDS', + }) + ).toBeInTheDocument(); + expect( + within(screen.getByTestId('eks-enroll')).getByRole('link', { + name: 'Enroll EKS', + }) + ).toBeInTheDocument(); +}); diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.tsx index 491c93d3b0858..0e788054ff828 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.tsx @@ -16,44 +16,77 @@ * along with this program. If not, see . */ -import React from 'react'; - -import { Flex, H2, Indicator } from 'design'; +import { Box, Flex, H2, Indicator } from 'design'; import { Danger } from 'design/Alert'; import { FeatureBox } from 'teleport/components/Layout'; import { AwsOidcHeader } from 'teleport/Integrations/status/AwsOidc/AwsOidcHeader'; +import { AwsOidcTitle } from 'teleport/Integrations/status/AwsOidc/AwsOidcTitle'; import { AwsResource, StatCard, } from 'teleport/Integrations/status/AwsOidc/StatCard'; +import { TaskAlert } from 'teleport/Integrations/status/AwsOidc/Tasks/TaskAlert'; import { useAwsOidcStatus } from 'teleport/Integrations/status/AwsOidc/useAwsOidcStatus'; export function AwsOidcDashboard() { const { statsAttempt, integrationAttempt } = useAwsOidcStatus(); - if (statsAttempt.status == 'processing') { - return ; + if ( + statsAttempt.status === 'processing' || + integrationAttempt.status === 'processing' + ) { + return ( + + + + ); + } + + if (integrationAttempt.status === 'error') { + return {integrationAttempt.statusText}; } - if (statsAttempt.status == 'error') { + + if (statsAttempt.status === 'error') { return {statsAttempt.statusText}; } - if (!statsAttempt.data) { + + if (!statsAttempt.data || !integrationAttempt.data) { return null; } - // todo (michellescripts) after routing, ensure this view can be sticky const { awsec2, awseks, awsrds } = statsAttempt.data; const { data: integration } = integrationAttempt; return ( - - {integration && } -

Auto-Enrollment

- - - - - -
+ <> + + + {integration && ( + <> + + + + )} + +

Auto-Enrollment

+ + + + + +
+ ); } diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx index f6efccd680aaf..851e05458142b 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx @@ -16,40 +16,92 @@ * along with this program. If not, see . */ -import React from 'react'; import { Link as InternalLink } from 'react-router-dom'; -import { ButtonIcon, Flex, Label, Text } from 'design'; -import { ArrowLeft } from 'design/Icon'; -import { HoverTooltip } from 'shared/components/ToolTip'; +import { ButtonText, Flex, Text } from 'design'; +import { Plugs } from 'design/Icon'; +import { HoverTooltip } from 'design/Tooltip'; import cfg from 'teleport/config'; -import { getStatusAndLabel } from 'teleport/Integrations/helpers'; -import { IntegrationAwsOidc } from 'teleport/services/integrations'; +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; +import { Integration } from 'teleport/services/integrations'; export function AwsOidcHeader({ integration, + resource, + tasks = false, }: { - integration: IntegrationAwsOidc; + integration: Integration; + resource?: AwsResource; + tasks?: boolean; }) { - const { status, labelKind } = getStatusAndLabel(integration); + const divider = ( + + / + + ); + return ( - + - - - + + - - {integration.name} - - + {!resource && !tasks ? ( + <> + {divider} + + {integration.name} + + + ) : ( + <> + {divider} + + {integration.name} + + + )} + {resource && ( + <> + {divider} + + {resource.toUpperCase()} + + + )} + {tasks && ( + <> + {divider} + + Pending Tasks + + + )} ); } diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcRoutes.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcRoutes.tsx index 470c4bf32e482..82ea7afe8a73f 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcRoutes.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcRoutes.tsx @@ -20,6 +20,8 @@ import React from 'react'; import { Route, Switch } from 'teleport/components/Router'; import cfg from 'teleport/config'; +import { Details } from 'teleport/Integrations/status/AwsOidc/Details/Details'; +import { Tasks } from 'teleport/Integrations/status/AwsOidc/Tasks/Tasks'; import { AwsOidcStatusProvider } from 'teleport/Integrations/status/AwsOidc/useAwsOidcStatus'; import { AwsOidcDashboard } from './AwsOidcDashboard'; @@ -29,7 +31,19 @@ export function AwsOidcRoutes() { + + . + */ +import { within } from '@testing-library/react'; +import { MemoryRouter } from 'react-router'; + +import { render, screen } from 'design/utils/testing'; + +import { AwsOidcTitle } from 'teleport/Integrations/status/AwsOidc/AwsOidcTitle'; +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; +import { + IntegrationAwsOidc, + IntegrationKind, + IntegrationStatusCode, +} from 'teleport/services/integrations'; + +const testIntegration: IntegrationAwsOidc = { + kind: IntegrationKind.AwsOidc, + name: 'some-name', + resourceType: 'integration', + spec: { + roleArn: '', + issuerS3Bucket: '', + issuerS3Prefix: '', + }, + statusCode: IntegrationStatusCode.Running, +}; + +test('renders with resource', () => { + render( + + + + ); + + expect(screen.getByRole('link', { name: 'back' })).toHaveAttribute( + 'href', + '/web/integrations/status/aws-oidc/some-name' + ); + expect(screen.getByText('EC2')).toBeInTheDocument(); + expect(screen.queryByText('some-name')).not.toBeInTheDocument(); + expect( + within(screen.getByLabelText('status')).getByText('Running') + ).toBeInTheDocument(); +}); + +test('renders without resource', () => { + render( + + + + ); + + expect(screen.getByRole('link', { name: 'back' })).toHaveAttribute( + 'href', + '/web/integrations' + ); + expect(screen.getByText('some-name')).toBeInTheDocument(); + expect( + within(screen.getByLabelText('status')).getByText('Running') + ).toBeInTheDocument(); +}); + +test('renders tasks', () => { + render( + + + + ); + + expect(screen.getByRole('link', { name: 'back' })).toHaveAttribute( + 'href', + '/web/integrations/status/aws-oidc/some-name' + ); + expect(screen.getByText('Pending Tasks')).toBeInTheDocument(); + expect( + within(screen.getByLabelText('status')).getByText('Running') + ).toBeInTheDocument(); +}); diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcTitle.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcTitle.tsx new file mode 100644 index 0000000000000..87d73a4627cc3 --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcTitle.tsx @@ -0,0 +1,84 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Link as InternalLink } from 'react-router-dom'; + +import { ButtonIcon, Flex, Label, Text } from 'design'; +import { ArrowLeft } from 'design/Icon'; +import { HoverTooltip } from 'design/Tooltip'; + +import cfg from 'teleport/config'; +import { getStatusAndLabel } from 'teleport/Integrations/helpers'; +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; +import { IntegrationAwsOidc } from 'teleport/services/integrations'; + +export function AwsOidcTitle({ + integration, + resource, + tasks, +}: { + integration: IntegrationAwsOidc; + resource?: AwsResource; + tasks?: boolean; +}) { + const { status, labelKind } = getStatusAndLabel(integration); + const content = getContent(integration, resource, tasks); + + return ( + + + + + + + + {content.content} + + + + ); +} + +function getContent( + integration: IntegrationAwsOidc, + resource?: AwsResource, + tasks?: boolean +): { to: string; helper: string; content: string } { + if (resource) { + return { + to: cfg.getIntegrationStatusRoute(integration.kind, integration.name), + helper: 'Back to integration', + content: resource.toUpperCase(), + }; + } + + if (tasks) { + return { + to: cfg.getIntegrationStatusRoute(integration.kind, integration.name), + helper: 'Back to integration', + content: 'Pending Tasks', + }; + } + + return { + to: cfg.routes.integrations, + helper: 'Back to integrations', + content: integration.name, + }; +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Agents.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Agents.tsx new file mode 100644 index 0000000000000..f1495291228f6 --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Agents.tsx @@ -0,0 +1,106 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { useEffect, useState } from 'react'; +import { useParams } from 'react-router'; + +import { Box, Flex, Indicator } from 'design'; +import { Danger } from 'design/Alert'; +import Table, { LabelCell } from 'design/DataTable'; +import { MultiselectMenu } from 'shared/components/Controls/MultiselectMenu'; +import { useAsync } from 'shared/hooks/useAsync'; + +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; +import { + AWSOIDCDeployedDatabaseService, + awsRegionMap, + IntegrationKind, + integrationService, + Regions, +} from 'teleport/services/integrations'; + +export function Agents() { + const { name, resourceKind } = useParams<{ + type: IntegrationKind; + name: string; + resourceKind: AwsResource; + }>(); + + const [regionFilter, setRegionFilter] = useState([]); + const [servicesAttempt, fetchServices] = useAsync(() => { + return integrationService.fetchAwsOidcDatabaseServices( + name, + resourceKind, + regionFilter + ); + }); + + useEffect(() => { + fetchServices(); + }, [regionFilter]); + + if (servicesAttempt.status === 'processing') { + return ( + + + + ); + } + + return ( + <> + {servicesAttempt.status === 'error' && ( + {servicesAttempt.statusText} + )} + ({ + value: r as Regions, + label: ( + +
{awsRegionMap[r]}  
+
{r}
+
+ ), + }))} + onChange={regions => setRegionFilter(regions)} + selected={regionFilter} + label="Region" + tooltip="Filter by region" + /> + + data={servicesAttempt.data?.services} + columns={[ + { + key: 'name', + headerText: 'Service Name', + }, + { + key: 'matchingLabels', + headerText: 'Tags', + render: ({ matchingLabels }) => ( + `${l.name}:${l.value}`)} + /> + ), + }, + ]} + emptyText={`No ${resourceKind.toUpperCase()} agents`} + /> + + ); +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Details.story.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Details.story.tsx new file mode 100644 index 0000000000000..92c38f30c8f90 --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Details.story.tsx @@ -0,0 +1,232 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { http, HttpResponse } from 'msw'; + +import cfg from 'teleport/config'; +import { Details } from 'teleport/Integrations/status/AwsOidc/Details/Details'; +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; +import { MockAwsOidcStatusProvider } from 'teleport/Integrations/status/AwsOidc/testHelpers/mockAwsOidcStatusProvider'; +import { IntegrationKind } from 'teleport/services/integrations'; + +import { makeAwsOidcStatusContextState } from '../testHelpers/makeAwsOidcStatusContextState'; +import { makeIntegrationDiscoveryRule } from '../testHelpers/makeIntegrationDiscoveryRule'; + +export default { + title: 'Teleport/Integrations/AwsOidc/Details', +}; + +const integrationName = 'integration-story'; + +// Empty ec2 details table +export function EC2Empty() { + return ( + +
+ + ); +} + +// Populated ec2 details table +export function EC2() { + return ( + +
+ + ); +} + +EC2.parameters = { + msw: { + handlers: [ + http.get( + cfg.getIntegrationRulesUrl(integrationName, AwsResource.ec2), + () => { + return HttpResponse.json({ + rules: rules, + nextKey: '1', + }); + } + ), + ], + }, +}; + +// Empty eks details table +export function EKSEmpty() { + return ( + +
+ + ); +} + +// Populated eks details table +export function EKS() { + return ( + +
+ + ); +} + +EKS.parameters = { + msw: { + handlers: [ + http.get( + cfg.getIntegrationRulesUrl(integrationName, AwsResource.eks), + () => { + return HttpResponse.json({ + rules: rules, + nextKey: '1', + }); + } + ), + ], + }, +}; + +// Empty rds details table +export function RDSEmpty() { + return ( + +
+ + ); +} + +// Populated eks details table +export function RDS() { + return ( + +
+ + ); +} + +RDS.parameters = { + msw: { + handlers: [ + http.get( + cfg.getIntegrationRulesUrl(integrationName, AwsResource.rds), + () => { + return HttpResponse.json({ + rules: rules, + nextKey: '1', + }); + } + ), + http.post( + cfg.getAwsOidcDatabaseServices(integrationName, AwsResource.rds, []), + () => { + return HttpResponse.json({ + services: [ + { + name: 'dev-db', + matchingLabels: [{ name: 'region', value: 'us-west-2' }], + }, + { + name: 'dev-db', + matchingLabels: [ + { name: 'region', value: 'us-west-1' }, + { name: '*', value: '*' }, + ], + }, + { + name: 'staging-db', + matchingLabels: [{ name: '*', value: '*' }], + }, + ], + }); + } + ), + ], + }, +}; + +function getPath(resource: AwsResource) { + return cfg.getIntegrationStatusResourcesRoute( + IntegrationKind.AwsOidc, + integrationName, + resource + ); +} + +const rules = [ + makeIntegrationDiscoveryRule({ + region: 'us-west-2', + labelMatcher: [ + { name: 'env', value: 'prod' }, + { name: 'key', value: '123' }, + ], + }), + makeIntegrationDiscoveryRule({ + region: 'us-west-2', + labelMatcher: [ + { name: 'env', value: 'prod' }, + { name: 'key', value: '123' }, + ], + }), + makeIntegrationDiscoveryRule({ + region: 'us-west-2', + labelMatcher: [ + { name: 'env', value: 'prod' }, + { name: 'key', value: '123' }, + ], + }), +]; diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Details.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Details.tsx new file mode 100644 index 0000000000000..a8e11cbc53ec5 --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Details.tsx @@ -0,0 +1,58 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { useParams } from 'react-router'; + +import { FeatureBox } from 'teleport/components/Layout'; +import { AwsOidcHeader } from 'teleport/Integrations/status/AwsOidc/AwsOidcHeader'; +import { AwsOidcTitle } from 'teleport/Integrations/status/AwsOidc/AwsOidcTitle'; +import { Rds } from 'teleport/Integrations/status/AwsOidc/Details/Rds'; +import { Rules } from 'teleport/Integrations/status/AwsOidc/Details/Rules'; +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; +import { TaskAlert } from 'teleport/Integrations/status/AwsOidc/Tasks/TaskAlert'; +import { useAwsOidcStatus } from 'teleport/Integrations/status/AwsOidc/useAwsOidcStatus'; +import { IntegrationKind } from 'teleport/services/integrations'; + +export function Details() { + const { resourceKind } = useParams<{ + type: IntegrationKind; + name: string; + resourceKind: AwsResource; + }>(); + + const { integrationAttempt } = useAwsOidcStatus(); + const { data: integration } = integrationAttempt; + return ( + <> + {integration && ( + + )} + + <> + {integration && ( + <> + + + + )} + + {resourceKind === AwsResource.rds ? : } + + + ); +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rds.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rds.tsx new file mode 100644 index 0000000000000..b46aadefd5ac1 --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rds.tsx @@ -0,0 +1,105 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { useEffect, useRef } from 'react'; +import { useLocation, useParams } from 'react-router'; + +import { TabBorder, TabContainer, TabsContainer } from 'design/Tabs/Tabs'; + +import cfg from 'teleport/config'; +import { Agents } from 'teleport/Integrations/status/AwsOidc/Details/Agents'; +import { Rules } from 'teleport/Integrations/status/AwsOidc/Details/Rules'; +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; +import { IntegrationKind } from 'teleport/services/integrations'; + +export enum RdsTab { + Agents = 'agents', + Rules = 'rules', +} + +export function Rds() { + const { type, name, resourceKind } = useParams<{ + type: IntegrationKind; + name: string; + resourceKind: AwsResource; + }>(); + + const { search } = useLocation(); + const searchParams = new URLSearchParams(search); + const tab = (searchParams.get('tab') as RdsTab) || RdsTab.Rules; + + const borderRef = useRef(null); + const parentRef = useRef(); + + // todo (michellescripts) the following implementation mimics the implementation of tabs in + // e/web/teleport/src/AccessMonitoring/AccessMonitoring.tsx which is refactored/moved into a shared + // design component, web/packages/design/src/Tabs/Tabs.ts. When refactoring AccessMonitoring to use the shared + // component, consider updating both instances logic to be plain css + useEffect(() => { + if (!parentRef.current || !borderRef.current) { + return; + } + + const activeElement = parentRef.current.querySelector( + `[data-tab-id="${tab}"]` + ); + + if (activeElement) { + const parentBounds = parentRef.current.getBoundingClientRect(); + const activeBounds = activeElement.getBoundingClientRect(); + + const left = activeBounds.left - parentBounds.left; + const width = activeBounds.width; + + borderRef.current.style.left = `${left}px`; + borderRef.current.style.width = `${width}px`; + } + }, [tab]); + + return ( + <> + + + Enrollment Rules + + + Agents + + + + {tab === RdsTab.Rules && } + {tab === RdsTab.Agents && } + + ); +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.test.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.test.tsx new file mode 100644 index 0000000000000..67d4b85c859ea --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.test.tsx @@ -0,0 +1,98 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { within } from '@testing-library/react'; +import { MemoryRouter } from 'react-router'; + +import { render, screen, waitFor } from 'design/utils/testing'; + +import { Route } from 'teleport/components/Router'; +import cfg from 'teleport/config'; +import { Rules } from 'teleport/Integrations/status/AwsOidc/Details/Rules'; +import { integrationService } from 'teleport/services/integrations'; + +import { makeIntegrationDiscoveryRule } from '../testHelpers/makeIntegrationDiscoveryRule'; + +test('renders region & labels from response', async () => { + jest.spyOn(integrationService, 'fetchIntegrationRules').mockResolvedValue({ + rules: [ + makeIntegrationDiscoveryRule({ + region: 'us-west-2', + labelMatcher: [ + { name: 'env', value: 'prod' }, + { name: 'key', value: '123' }, + ], + }), + makeIntegrationDiscoveryRule({ + region: 'us-east-2', + labelMatcher: [{ name: 'env', value: 'stage' }], + }), + makeIntegrationDiscoveryRule({ + region: 'us-west-1', + labelMatcher: [{ name: 'env', value: 'test' }], + }), + makeIntegrationDiscoveryRule({ + region: 'us-east-1', + labelMatcher: [{ name: 'env', value: 'dev' }], + }), + ], + nextKey: '', + }); + render( + + } + /> + + ); + + await waitFor(() => { + expect(screen.getByText('env:prod')).toBeInTheDocument(); + }); + + expect(getTableCellContents()).toEqual({ + header: ['Region', 'Labels'], + rows: [ + ['us-west-2', 'env:prodkey:123'], + ['us-east-2', 'env:stage'], + ['us-west-1', 'env:test'], + ['us-east-1', 'env:dev'], + ], + }); + + jest.clearAllMocks(); +}); + +function getTableCellContents() { + const [header, ...rows] = screen.getAllByRole('row'); + return { + header: within(header) + .getAllByRole('columnheader') + .map(cell => cell.textContent), + rows: rows.map(row => + within(row) + .getAllByRole('cell') + .map(cell => cell.textContent) + ), + }; +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.tsx new file mode 100644 index 0000000000000..6fdbd8a1bebcc --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.tsx @@ -0,0 +1,115 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { useEffect, useState } from 'react'; +import { useParams } from 'react-router'; + +import { Flex } from 'design'; +import Table, { LabelCell } from 'design/DataTable'; +import { MultiselectMenu } from 'shared/components/Controls/MultiselectMenu'; + +import { useServerSidePagination } from 'teleport/components/hooks'; +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; +import { + awsRegionMap, + IntegrationDiscoveryRule, + IntegrationKind, + integrationService, + Regions, +} from 'teleport/services/integrations'; + +export function Rules() { + const { name, resourceKind } = useParams<{ + type: IntegrationKind; + name: string; + resourceKind: AwsResource; + }>(); + + const [regionFilter, setRegionFilter] = useState([]); + const serverSidePagination = + useServerSidePagination({ + pageSize: 20, + fetchFunc: async () => { + const { rules, nextKey } = + await integrationService.fetchIntegrationRules( + name, + resourceKind, + regionFilter + ); + return { agents: rules, nextKey }; + }, + clusterId: '', + params: {}, + }); + + useEffect(() => { + serverSidePagination.fetch(); + }, [regionFilter]); + + return ( + <> + ({ + value: r as Regions, + label: ( + +
{awsRegionMap[r]}  
+
{r}
+
+ ), + }))} + onChange={regions => setRegionFilter(regions)} + selected={regionFilter} + label="Region" + tooltip="Filter by region" + /> + + data={serverSidePagination?.fetchedData?.agents} + columns={[ + { + key: 'region', + headerText: 'Region', + }, + { + key: 'labelMatcher', + headerText: getResourceTerm(resourceKind), + render: ({ labelMatcher }) => ( + `${l.name}:${l.value}`)} /> + ), + }, + ]} + emptyText={`No ${resourceKind.toUpperCase()} rules`} + pagination={{ pageSize: serverSidePagination.pageSize }} + fetching={{ + fetchStatus: serverSidePagination.fetchStatus, + onFetchNext: serverSidePagination.fetchNext, + onFetchPrev: serverSidePagination.fetchPrev, + }} + /> + + ); +} + +function getResourceTerm(resource: AwsResource): string { + switch (resource) { + case AwsResource.rds: + return 'Tags'; + default: + return 'Labels'; + } +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/EnrollCard.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/EnrollCard.tsx new file mode 100644 index 0000000000000..c025683e1c75f --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/EnrollCard.tsx @@ -0,0 +1,61 @@ +/** + * Teleport + * Copyright (C) 2025 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { Link as InternalLink } from 'react-router-dom'; +import styled from 'styled-components'; + +import { ButtonSecondary, Card, Flex, H2, ResourceIcon } from 'design'; + +import cfg from 'teleport/config'; +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; + +export function EnrollCard({ resource }: { resource: AwsResource }) { + return ( + + + + +

{resource.toUpperCase()}

+
+ + Enroll {resource.toUpperCase()} + +
+
+ ); +} + +const Enroll = styled(Card)` + width: 33%; + background-color: ${props => props.theme.colors.levels.surface}; + padding: ${props => props.theme.space[3]}px; + border-radius: ${props => props.theme.radii[2]}px; + border: ${props => `1px solid ${props.theme.colors.levels.surface}`}; + + &:hover { + background-color: ${props => props.theme.colors.levels.elevated}; + box-shadow: ${({ theme }) => theme.boxShadow[2]}; + } +`; diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/StatCard.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/StatCard.tsx index 5a3a5173376d1..bd56adf338dfd 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/StatCard.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/StatCard.tsx @@ -17,13 +17,19 @@ */ import { formatDistanceStrict } from 'date-fns'; -import React from 'react'; +import { Link as InternalLink } from 'react-router-dom'; +import styled from 'styled-components'; import { Card, Flex, H2, Text } from 'design'; import * as Icons from 'design/Icon'; import { ResourceIcon } from 'design/ResourceIcon'; -import { ResourceTypeSummary } from 'teleport/services/integrations'; +import cfg from 'teleport/config'; +import { EnrollCard } from 'teleport/Integrations/status/AwsOidc/EnrollCard'; +import { + IntegrationKind, + ResourceTypeSummary, +} from 'teleport/services/integrations'; export enum AwsResource { ec2 = 'ec2', @@ -32,22 +38,30 @@ export enum AwsResource { } type StatCardProps = { + name: string; resource: AwsResource; summary?: ResourceTypeSummary; }; -export function StatCard({ resource, summary }: StatCardProps) { +export function StatCard({ name, resource, summary }: StatCardProps) { const updated = summary?.discoverLastSync ? new Date(summary?.discoverLastSync) : undefined; const term = getResourceTerm(resource); + if (!summary || !foundResource(summary)) { + return ; + } + return ( - Enrollment Rules {summary?.rulesCount || 0} - {resource == AwsResource.rds && ( + {resource === AwsResource.rds && ( Agents {summary?.ecsDatabaseServiceCount || 0} @@ -96,7 +110,7 @@ export function StatCard({ resource, summary }: StatCardProps) { )} - + ); } @@ -111,3 +125,31 @@ function getResourceTerm(resource: AwsResource): string { return 'Instances'; } } + +function foundResource(resource: ResourceTypeSummary): boolean { + if (!resource || Object.keys(resource).length === 0) { + return false; + } + + if (resource.ecsDatabaseServiceCount != 0) { + return true; + } + + return resource.rulesCount != 0 || resource.resourcesFound != 0; +} + +export const SelectCard = styled(Card)` + width: 33%; + background-color: ${props => props.theme.colors.levels.surface}; + padding: ${props => props.theme.space[3]}px; + border-radius: ${props => props.theme.radii[2]}px; + border: ${props => `1px solid ${props.theme.colors.levels.surface}`}; + cursor: pointer; + text-decoration: none; + color: ${props => props.theme.colors.text.main}; + + &:hover { + background-color: ${props => props.theme.colors.levels.elevated}; + box-shadow: ${({ theme }) => theme.boxShadow[2]}; + } +`; diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/SidePanel.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/SidePanel.tsx new file mode 100644 index 0000000000000..817baf3bb40f2 --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/SidePanel.tsx @@ -0,0 +1,71 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { PropsWithChildren, ReactNode } from 'react'; +import styled from 'styled-components'; + +import { ButtonIcon, Flex } from 'design'; +import { Cross } from 'design/Icon'; + +export const SidePanel = ({ + onClose, + header, + footer, + disabled = false, + children, +}: PropsWithChildren & { + onClose: () => void; + header?: ReactNode; + footer?: ReactNode; + disabled?: boolean; +}) => { + return ( + + + {header} + + + + + + {children} + + + {footer} + + + ); +}; + +const Container = styled(Flex)` + flex-direction: column; + + height: calc(100vh - ${props => props.theme.topBarHeight[1]}px); +`; diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/Task.test.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/Task.test.tsx new file mode 100644 index 0000000000000..2b7119122bf69 --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/Task.test.tsx @@ -0,0 +1,199 @@ +/** + * Teleport + * Copyright (C) 2025 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { within } from '@testing-library/react'; + +import { render, screen } from 'design/utils/testing'; + +import { ContextProvider } from 'teleport/index'; +import { Task } from 'teleport/Integrations/status/AwsOidc/Tasks/Task'; +import { integrationService } from 'teleport/services/integrations'; +import TeleportContext from 'teleport/teleportContext'; + +test('renders ec2 impacts', async () => { + const ctx = new TeleportContext(); + jest.spyOn(integrationService, 'fetchUserTask').mockResolvedValue({ + name: 'df4d8288-7106-5a50-bb50-4b5858e48ad5', + taskType: 'discover-ec2', + state: 'OPEN', + integration: '', + lastStateChange: '2025-02-11T20:32:19.482607921Z', + issueType: 'ec2-ssm-invocation-failure', + description: + 'Teleport failed to access the SSM Agent to auto enroll the instance.\nSome instances failed to communicate with the AWS Systems Manager service to execute the install script.\n\nUsually this happens when:\n\n**Missing policies**\n\nThe IAM Role used by the integration might be missing some required permissions.\nEnsure the following actions are allowed in the IAM Role used by the integration:\n- `ec2:DescribeInstances`\n- `ssm:DescribeInstanceInformation`\n- `ssm:GetCommandInvocation`\n- `ssm:ListCommandInvocations`\n- `ssm:SendCommand`\n\n**SSM Document is invalid**\n\nTeleport uses an SSM Document to run an installation script.\nIf the document is changed or removed, it might no longer work.', + discoverEks: undefined, + discoverRds: undefined, + discoverEc2: { + region: 'us-east-2', + accountId: undefined, + ssmDocument: undefined, + installerScript: undefined, + instances: { + 'i-016e32a5882f5ee81': { + instance_id: 'i-016e32a5882f5ee81', + name: undefined, + invocationUrl: undefined, + discoveryConfig: undefined, + discoveryGroup: undefined, + syncTime: undefined, + }, + 'i-065818031835365cc': { + instance_id: 'i-065818031835365cc', + name: 'aws-test', + invocationUrl: undefined, + discoveryConfig: undefined, + discoveryGroup: undefined, + syncTime: undefined, + }, + }, + }, + }); + + render( + + {}} /> + + ); + + await screen.findByText('Details'); + + expect(getTableCellContents()).toEqual({ + header: ['Instance ID', 'Instance Name'], + rows: [ + ['i-016e32a5882f5ee81', ''], + ['i-065818031835365cc', 'aws-test'], + ], + }); + + jest.resetAllMocks(); +}); + +test('renders eks impacts', async () => { + const ctx = new TeleportContext(); + jest.spyOn(integrationService, 'fetchUserTask').mockResolvedValue({ + name: 'df4d8288-7106-5a50-bb50-4b5858e48ad5', + taskType: 'discover-eks', + state: 'OPEN', + integration: 'integration-001', + lastStateChange: '2025-02-11T20:32:19.482607921Z', + issueType: 'eks-failure', + description: + 'Only EKS Clusters whose status is active can be automatically enrolled into teleport.\n', + discoverEc2: undefined, + discoverRds: undefined, + discoverEks: { + accountId: undefined, + region: undefined, + appAutoDiscover: false, + clusters: { + 'i-016e32a5882f5ee81': { + name: 'i-016e32a5882f5ee81', + discoveryConfig: undefined, + discoveryGroup: undefined, + syncTime: undefined, + }, + 'i-065818031835365cc': { + name: 'i-065818031835365cc', + discoveryConfig: undefined, + discoveryGroup: undefined, + syncTime: undefined, + }, + }, + }, + }); + + render( + + {}} /> + + ); + + await screen.findByText('Details'); + + expect(getTableCellContents()).toEqual({ + header: ['Name'], + rows: [['i-016e32a5882f5ee81'], ['i-065818031835365cc']], + }); + jest.resetAllMocks(); +}); + +test('renders rds impacts', async () => { + const ctx = new TeleportContext(); + jest.spyOn(integrationService, 'fetchUserTask').mockResolvedValue({ + name: 'df4d8288-7106-5a50-bb50-4b5858e48ad5', + taskType: 'discover-rds', + state: 'OPEN', + integration: 'integration-001', + lastStateChange: '2025-02-11T20:32:19.482607921Z', + issueType: 'rds-failure', + description: + 'The Teleport Database Service uses [IAM authentication](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html) to communicate with RDS.\n', + discoverEks: undefined, + discoverEc2: undefined, + discoverRds: { + accountId: undefined, + region: undefined, + databases: { + 'i-016e32a5882f5ee81': { + name: 'i-016e32a5882f5ee81', + isCluster: undefined, + engine: undefined, + discoveryConfig: undefined, + discoveryGroup: undefined, + syncTime: undefined, + }, + 'i-065818031835365cc': { + name: 'i-065818031835365cc', + isCluster: undefined, + engine: undefined, + discoveryConfig: undefined, + discoveryGroup: undefined, + syncTime: undefined, + }, + }, + }, + }); + + render( + + {}} /> + + ); + + await screen.findByText('Details'); + + expect(getTableCellContents()).toEqual({ + header: ['Name'], + rows: [['i-016e32a5882f5ee81'], ['i-065818031835365cc']], + }); + jest.resetAllMocks(); +}); + +function getTableCellContents() { + const [header, ...rows] = screen.getAllByRole('row'); + return { + header: within(header) + .getAllByRole('columnheader') + .map(cell => cell.textContent), + rows: rows.map(row => + within(row) + .getAllByRole('cell') + .map(cell => cell.textContent) + ), + }; +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/Task.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/Task.tsx new file mode 100644 index 0000000000000..d11267bf89f8e --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/Task.tsx @@ -0,0 +1,236 @@ +/** + * Teleport + * Copyright (C) 2025 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { PropsWithChildren, useEffect } from 'react'; +import ReactMarkdown from 'react-markdown'; + +import { Alert, ButtonBorder, Flex, H2 } from 'design'; +import { Danger } from 'design/Alert'; +import Table from 'design/DataTable'; +import { TableColumn } from 'design/DataTable/types'; +import { H3, P2, Subtitle2 } from 'design/Text'; +import { useAsync } from 'shared/hooks/useAsync'; +import useAttempt from 'shared/hooks/useAttemptNext'; + +import { getResourceType } from 'teleport/Integrations/status/AwsOidc/helpers'; +import { + DiscoverEc2, + DiscoverEc2Instance, + DiscoverEks, + DiscoverEksCluster, + DiscoverRds, + DiscoverRdsDatabase, + integrationService, + UserTaskDetail, +} from 'teleport/services/integrations'; + +import { AwsResource } from '../StatCard'; +import { SidePanel } from './SidePanel'; + +export function Task({ + name, + close, +}: { + name: string; + close: (resolved: boolean) => void; +}) { + const { attempt, setAttempt } = useAttempt(''); + + const [taskAttempt, fetchTask] = useAsync(() => + integrationService.fetchUserTask(name) + ); + + useEffect(() => { + fetchTask(); + }, []); + + if (taskAttempt.status === 'error') { + return ( + close(false)}> + {taskAttempt.statusText} + + ); + } + + if (!taskAttempt.data) { + return null; + } + + function resolve() { + setAttempt({ status: 'processing' }); + integrationService + .resolveUserTask(name) + .then(() => { + setAttempt({ status: '', statusText: '' }); + close(true); + }) + .catch((err: Error) => + setAttempt({ status: 'failed', statusText: err.message }) + ); + } + + const impactedInstances = getImpactedInstances(taskAttempt.data); + const { resourceType, resource, impacts } = impactedInstances; + const table = makeImpactsTable(impactedInstances); + + return ( + close(false)} + header={

{taskAttempt.data.issueType}

} + footer={ + + Mark as Resolved + + } + disabled={attempt.status === 'processing'} + > + {attempt.status === 'failed' && ( + + Unable to resolve task + + )} + + {taskAttempt.data.integration} + + {resourceType.toUpperCase()} + {resource.region} +

Details

+ {taskAttempt.data.description} +

Impacted instances ({Object.keys(impacts).length})

+ + + ); +} + +type TableInstance = { + instanceId?: string; + name: string; +}; + +function makeImpactsTable(instances: ImpactedInstances): { + columns: TableColumn[]; + data: TableInstance[]; +} { + const { resourceType, impacts } = instances; + switch (resourceType) { + case AwsResource.ec2: + return { + columns: [ + { + key: 'instanceId', + headerText: 'Instance ID', + }, + { + key: 'name', + headerText: 'Instance Name', + }, + ], + data: Object.keys(impacts).map(i => ({ + instanceId: impacts[i].instance_id, + name: impacts[i].name, + })), + }; + case AwsResource.eks: + case AwsResource.rds: + return { + columns: [ + { + key: 'name', + headerText: 'Name', + }, + ], + data: Object.keys(impacts).map(i => ({ + name: impacts[i].name, + })), + }; + default: + resourceType satisfies never; + } +} + +type ImpactedInstances = + | { + resourceType: AwsResource.ec2; + resource: DiscoverEc2; + impacts: Record; + } + | { + resourceType: AwsResource.eks; + resource: DiscoverEks; + impacts: Record; + } + | { + resourceType: AwsResource.rds; + resource: DiscoverRds; + impacts: Record; + }; + +function getImpactedInstances(task: UserTaskDetail): ImpactedInstances { + const resourceType = getResourceType(task.taskType); + + switch (resourceType) { + case AwsResource.ec2: + return { + resourceType: resourceType, + resource: task.discoverEc2, + impacts: task.discoverEc2?.instances, + }; + case AwsResource.eks: + return { + resourceType: resourceType, + resource: task.discoverEks, + impacts: task.discoverEks?.clusters, + }; + case AwsResource.rds: + default: + return { + resourceType: resourceType, + resource: task.discoverRds, + impacts: task.discoverRds?.databases, + }; + } +} + +const Attribute = ({ + title = '', + children, +}: PropsWithChildren<{ title: string }>) => ( + + {title}: + + {children || `N/A`} + + +); diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/TaskAlert.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/TaskAlert.tsx new file mode 100644 index 0000000000000..6e8a01b68c409 --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/TaskAlert.tsx @@ -0,0 +1,77 @@ +/** + * Teleport + * Copyright (C) 2025 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { useEffect } from 'react'; +import { useHistory } from 'react-router'; + +import { Alert } from 'design'; +import { ArrowForward, BellRinging } from 'design/Icon'; +import { useAsync } from 'shared/hooks/useAsync'; + +import cfg from 'teleport/config'; +import { TaskState } from 'teleport/Integrations/status/AwsOidc/Tasks/constants'; +import { + IntegrationKind, + integrationService, +} from 'teleport/services/integrations'; + +export function TaskAlert({ + name, + kind = IntegrationKind.AwsOidc, +}: { + name: string; + kind?: IntegrationKind; +}) { + const history = useHistory(); + // todo (michellescripts) should we show the banner if there is an error + const [tasksAttempt, fetchTasks] = useAsync(() => + integrationService.fetchIntegrationUserTasksList(name, TaskState.Open) + ); + + useEffect(() => { + fetchTasks(); + }, []); + + const pendingTasksCount = + (tasksAttempt.status === 'success' && + tasksAttempt.data.items?.filter(t => t.state === TaskState.Open) + .length) || + 0; + + if (!pendingTasksCount) { + return null; + } + + return ( + + Resolve Now + + + ), + onClick: () => history.push(cfg.getIntegrationTasksRoute(kind, name)), + }} + > + {pendingTasksCount} Pending Tasks + + ); +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/Tasks.story.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/Tasks.story.tsx new file mode 100644 index 0000000000000..f69f2c295c415 --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/Tasks.story.tsx @@ -0,0 +1,193 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { http, HttpResponse } from 'msw'; + +import cfg from 'teleport/config'; +import { TaskState } from 'teleport/Integrations/status/AwsOidc/Tasks/constants'; +import { MockAwsOidcStatusProvider } from 'teleport/Integrations/status/AwsOidc/testHelpers/mockAwsOidcStatusProvider'; +import { IntegrationKind } from 'teleport/services/integrations'; + +import { makeAwsOidcStatusContextState } from '../testHelpers/makeAwsOidcStatusContextState'; +import { Tasks } from './Tasks'; + +export default { + title: 'Teleport/Integrations/AwsOidc/Tasks', +}; + +const integrationName = 'integration-story'; + +// Empty tasks table +export function TasksEmpty() { + return ( + + + + ); +} + +// Populated tasks table +export function TaskView() { + return ( + + + + ); +} + +TaskView.parameters = { + msw: { + handlers: [ + http.get( + cfg.getIntegrationUserTasksListUrl(integrationName, TaskState.Open), + () => { + return HttpResponse.json({ + items: [ + { + name: 'rds-detail', + taskType: 'discover-rds', + state: TaskState.Open, + issueType: 'rds-generic', + integration: integrationName, + lastStateChange: '2022-02-12T20:32:19.482607921Z', + }, + { + name: 'ec2-detail', + taskType: 'discover-ec2', + state: TaskState.Open, + issueType: 'ec2-ssm-invocation-failure', + integration: integrationName, + lastStateChange: '2025-02-11T20:32:19.482607921Z', + }, + { + name: 'ec2-detail', + taskType: 'discover-ec2', + state: TaskState.Open, + issueType: 'ec2-ssm-agent-not-registered', + integration: integrationName, + lastStateChange: '2025-02-11T20:32:13.61608091Z', + }, + { + name: 'no match', + state: TaskState.Open, + issueType: 'side panel error', + integration: integrationName, + lastStateChange: '0', + }, + { + name: 'eks-detail', + taskType: 'discover-eks', + state: TaskState.Open, + issueType: 'eks-failure', + integration: integrationName, + lastStateChange: '2025-02-11T20:32:13.61608091Z', + }, + ], + nextKey: '1', + }); + } + ), + http.get(cfg.getUserTaskUrl('ec2-detail'), () => { + return HttpResponse.json(ec2Detail); + }), + http.get(cfg.getUserTaskUrl('rds-detail'), () => { + return HttpResponse.json(rdsDetail); + }), + http.get(cfg.getUserTaskUrl('eks-detail'), () => { + return HttpResponse.json(eksDetail); + }), + ], + }, +}; + +const ec2Detail = { + name: 'df4d8288-7106-5a50-bb50-4b5858e48ad5', + taskType: 'discover-ec2', + state: 'OPEN', + integration: integrationName, + lastStateChange: '2025-02-11T20:32:19.482607921Z', + issueType: 'ec2-ssm-invocation-failure', + description: + 'Teleport failed to access the SSM Agent to auto enroll the instance.\nSome instances failed to communicate with the AWS Systems Manager service to execute the install script.\n\nUsually this happens when:\n\n**Missing policies**\n\nThe IAM Role used by the integration might be missing some required permissions.\nEnsure the following actions are allowed in the IAM Role used by the integration:\n- `ec2:DescribeInstances`\n- `ssm:DescribeInstanceInformation`\n- `ssm:GetCommandInvocation`\n- `ssm:ListCommandInvocations`\n- `ssm:SendCommand`\n\n**SSM Document is invalid**\n\nTeleport uses an SSM Document to run an installation script.\nIf the document is changed or removed, it might no longer work.', + discoverEc2: { + region: 'us-east-2', + instances: { + 'i-016e32a5882f5ee81': { + instance_id: 'i-016e32a5882f5ee81', + }, + 'i-065818031835365cc': { + instance_id: 'i-065818031835365cc', + name: 'aws-test', + }, + }, + }, +}; + +const rdsDetail = { + name: 'df4d8288-7106-5a50-bb50-4b5858e48ad5', + taskType: 'discover-rds', + state: 'OPEN', + integration: integrationName, + lastStateChange: '2025-02-11T20:32:19.482607921Z', + issueType: 'rds-failure', + description: + 'The Teleport Database Service uses [IAM authentication](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html) to communicate with RDS.\n', + discoverRds: { + databases: { + 'i-016e32a5882f5ee81': { + name: 'i-016e32a5882f5ee81', + }, + 'i-065818031835365cc': { + name: 'i-065818031835365cc', + }, + }, + }, +}; + +const eksDetail = { + name: 'df4d8288-7106-5a50-bb50-4b5858e48ad5', + taskType: 'discover-eks', + state: 'OPEN', + integration: integrationName, + lastStateChange: '2025-02-11T20:32:19.482607921Z', + issueType: 'eks-failure', + description: + 'Only EKS Clusters whose status is active can be automatically enrolled into teleport.\n', + discoverEks: { + clusters: { + 'i-016e32a5882f5ee81': { + name: 'i-016e32a5882f5ee81', + }, + 'i-065818031835365cc': { + name: 'i-065818031835365cc', + }, + }, + }, +}; diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/Tasks.test.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/Tasks.test.tsx new file mode 100644 index 0000000000000..e1806d98a897a --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/Tasks.test.tsx @@ -0,0 +1,80 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { createMemoryHistory } from 'history'; +import { Router } from 'react-router'; + +import { render, screen, userEvent, waitFor } from 'design/utils/testing'; + +import { ContextProvider } from 'teleport'; +import { Route } from 'teleport/components/Router'; +import cfg from 'teleport/config'; +import { Tasks } from 'teleport/Integrations/status/AwsOidc/Tasks/Tasks'; +import { makeAwsOidcStatusContextState } from 'teleport/Integrations/status/AwsOidc/testHelpers/makeAwsOidcStatusContextState'; +import { awsOidcStatusContext } from 'teleport/Integrations/status/AwsOidc/useAwsOidcStatus'; +import { + IntegrationKind, + integrationService, +} from 'teleport/services/integrations'; +import TeleportContext from 'teleport/teleportContext'; + +const integrationName = 'integration-test'; + +test('deep links an open task', async () => { + const ctx = new TeleportContext(); + jest + .spyOn(integrationService, 'fetchIntegrationUserTasksList') + .mockResolvedValue({ + items: [ + { + name: 'df4d8288-7106-5a50-bb50-4b5858e48ad5', + taskType: 'discover-rds', + state: 'OPEN', + integration: integrationName, + lastStateChange: '2025-02-11T20:32:19.482607921Z', + issueType: 'rds-failure', + }, + ], + nextKey: 'next', + }); + + const history = createMemoryHistory({ + initialEntries: [ + cfg.getIntegrationTasksRoute(IntegrationKind.AwsOidc, integrationName), + ], + }); + history.replace = jest.fn(); + + render( + + + + } /> + + + + ); + + await screen.findAllByText('Pending Tasks'); + await userEvent.click(screen.getByText('rds-failure')); + + await waitFor(() => + expect(history.replace).toHaveBeenCalledWith( + '/web/integrations/status/aws-oidc/integration-test/tasks?task=df4d8288-7106-5a50-bb50-4b5858e48ad5' + ) + ); +}); diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/Tasks.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/Tasks.tsx new file mode 100644 index 0000000000000..52f435750a8a3 --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/Tasks.tsx @@ -0,0 +1,225 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { useEffect, useState } from 'react'; +import { useHistory } from 'react-router'; +import styled, { useTheme } from 'styled-components'; + +import { ButtonBorder, Flex, Indicator } from 'design'; +import { Danger } from 'design/Alert'; +import Table, { Cell } from 'design/DataTable'; +import { Notification, NotificationItem } from 'shared/components/Notification'; + +import { useServerSidePagination } from 'teleport/components/hooks'; +import { FeatureBox } from 'teleport/components/Layout'; +import { AwsOidcHeader } from 'teleport/Integrations/status/AwsOidc/AwsOidcHeader'; +import { AwsOidcTitle } from 'teleport/Integrations/status/AwsOidc/AwsOidcTitle'; +import { getResourceType } from 'teleport/Integrations/status/AwsOidc/helpers'; +import { TaskState } from 'teleport/Integrations/status/AwsOidc/Tasks/constants'; +import { Task } from 'teleport/Integrations/status/AwsOidc/Tasks/Task'; +import { useAwsOidcStatus } from 'teleport/Integrations/status/AwsOidc/useAwsOidcStatus'; +import { integrationService, UserTask } from 'teleport/services/integrations'; + +export function Tasks() { + const theme = useTheme(); + const history = useHistory(); + const searchParams = new URLSearchParams(history.location.search); + const taskName = searchParams.get('task'); + const [notification, setNotification] = useState(); + + const { integrationAttempt } = useAwsOidcStatus(); + const { data: integration } = integrationAttempt; + const [selectedTask, setSelectedTask] = useState(''); + + const serverSidePagination = useServerSidePagination({ + pageSize: 20, + fetchFunc: async () => { + const { items, nextKey } = + await integrationService.fetchIntegrationUserTasksList( + integration.name, + TaskState.Open + ); + return { agents: items, nextKey }; + }, + clusterId: '', + params: {}, + }); + + useEffect(() => { + serverSidePagination.fetch(); + }, [integration]); + + // use updated query params to set/unset the task side panel + useEffect(() => { + if ( + taskName && + taskName !== '' && + serverSidePagination.fetchedData.agents && + selectedTask === '' + ) { + setSelectedTask(taskName); + } else { + setSelectedTask(''); + } + }, [taskName, serverSidePagination?.fetchedData]); + + if (integrationAttempt.status === 'processing') { + return ; + } + + if (serverSidePagination.attempt.status === 'processing') { + return ; + } + + if (integrationAttempt.status === 'error') { + return {integrationAttempt.statusText}; + } + + if (serverSidePagination.attempt.status === 'failed') { + return {serverSidePagination.attempt.statusText}; + } + + function closeTask(resolved: boolean) { + if (resolved) { + // If there are multiple pages, we would rather refresh the table with X results rather than + // use modifyFetchedData to remove the item. + serverSidePagination.fetch(); + + setNotification({ + content: { + description: + 'The task has been marked as resolved; it will reappear in the table if the issue persists after the next sync.', + }, + severity: 'success', + id: taskName, + }); + } + history.replace(history.location.pathname); + } + + function openTask(task: UserTask) { + if (selectedTask != '') { + return; + } + const urlParams = new URLSearchParams(); + urlParams.append('task', task.name); + history.replace(`${history.location.pathname}?${urlParams.toString()}`); + } + + return ( + + + {integration && ( + + )} + + {integration && ( + + )} + + data={serverSidePagination.fetchedData?.agents || []} + row={{ + onClick: row => { + if (selectedTask === '') { + openTask(row); + } + }, + getStyle: (row: UserTask) => { + if (selectedTask === '') { + return { cursor: 'pointer' }; + } + if (row.name === selectedTask) { + return { + backgroundColor: theme.colors.interactive.tonal.primary[0], + }; + } + }, + }} + columns={[ + { + key: 'taskType', + headerText: 'Type', + render: item => ( + {getResourceType(item.taskType).toUpperCase()} + ), + }, + { + key: 'issueType', + headerText: 'Issue Details', + }, + { + key: 'lastStateChange', + headerText: 'Timestamp (UTC)', + render: item => ( + {new Date(item.lastStateChange).toISOString()} + ), + }, + { + altKey: 'action', + headerText: 'Actions', + render: item => ( + + openTask(item)} + disabled={selectedTask != ''} + size="small" + > + View + + + ), + }, + ]} + emptyText={`No pending tasks`} + pagination={{ + pageSize: serverSidePagination.pageSize, + pagerPosition: 'both', + }} + fetching={{ + fetchStatus: serverSidePagination.fetchStatus, + onFetchNext: serverSidePagination.fetchNext, + onFetchPrev: serverSidePagination.fetchPrev, + }} + /> + {notification && ( + + setNotification(undefined)} + minWidth="432px" + /> + + )} + + + {selectedTask && } + + ); +} + +const NotificationContainer = styled.div` + position: absolute; + top: ${props => props.theme.space[10]}px; + right: ${props => props.theme.space[5]}px; +`; diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/constants.ts b/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/constants.ts new file mode 100644 index 0000000000000..206d79101c536 --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Tasks/constants.ts @@ -0,0 +1,22 @@ +/** + * Teleport + * Copyright (C) 2025 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +export enum TaskState { + Open = 'OPEN', + Resolved = 'RESOLVED', +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/helpers.ts b/web/packages/teleport/src/Integrations/status/AwsOidc/helpers.ts new file mode 100644 index 0000000000000..6ea5ec62a159c --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/helpers.ts @@ -0,0 +1,34 @@ +/** + * Teleport + * Copyright (C) 2025 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; + +export function getResourceType(type: string): AwsResource { + switch (type) { + case 'ec2': + case 'discover-ec2': + return AwsResource.ec2; + case 'eks': + case 'discover-eks': + return AwsResource.eks; + case 'rds': + case 'discover-rds': + default: + return AwsResource.rds; + } +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/testHelpers/makeAwsOidcStatusContextState.ts b/web/packages/teleport/src/Integrations/status/AwsOidc/testHelpers/makeAwsOidcStatusContextState.ts new file mode 100644 index 0000000000000..335236fa8a75f --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/testHelpers/makeAwsOidcStatusContextState.ts @@ -0,0 +1,75 @@ +/** + * Teleport + * Copyright (C) 2025 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { addHours } from 'date-fns'; + +import { makeSuccessAttempt } from 'shared/hooks/useAsync'; + +import { AwsOidcStatusContextState } from 'teleport/Integrations/status/AwsOidc/useAwsOidcStatus'; +import { + IntegrationKind, + ResourceTypeSummary, +} from 'teleport/services/integrations'; + +export function makeAwsOidcStatusContextState( + overrides: Partial = {} +): AwsOidcStatusContextState { + return Object.assign( + { + integrationAttempt: makeSuccessAttempt({ + resourceType: 'integration', + name: 'integration-one', + kind: IntegrationKind.AwsOidc, + spec: { + roleArn: 'arn:aws:iam::111456789011:role/bar', + }, + statusCode: 1, + }), + statsAttempt: makeSuccessAttempt({ + name: 'integration-one', + subKind: IntegrationKind.AwsOidc, + awsoidc: { + roleArn: 'arn:aws:iam::111456789011:role/bar', + }, + awsec2: makeResourceTypeSummary(), + awsrds: makeResourceTypeSummary(), + awseks: makeResourceTypeSummary(), + }), + }, + overrides + ); +} + +function makeResourceTypeSummary( + overrides: Partial = {} +): ResourceTypeSummary { + return Object.assign( + { + rulesCount: Math.floor(Math.random() * 100), + resourcesFound: Math.floor(Math.random() * 100), + resourcesEnrollmentFailed: Math.floor(Math.random() * 100), + resourcesEnrollmentSuccess: Math.floor(Math.random() * 100), + discoverLastSync: addHours( + new Date().getTime(), + -Math.floor(Math.random() * 100) + ), + ecsDatabaseServiceCount: Math.floor(Math.random() * 100), + }, + overrides + ); +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/testHelpers/makeIntegrationDiscoveryRule.ts b/web/packages/teleport/src/Integrations/status/AwsOidc/testHelpers/makeIntegrationDiscoveryRule.ts new file mode 100644 index 0000000000000..231eed4c8c69c --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/testHelpers/makeIntegrationDiscoveryRule.ts @@ -0,0 +1,34 @@ +/** + * Teleport + * Copyright (C) 2025 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { IntegrationDiscoveryRule } from 'teleport/services/integrations'; + +export function makeIntegrationDiscoveryRule( + overrides: Partial = {} +): IntegrationDiscoveryRule { + return Object.assign( + { + resourceType: '', + region: '', + labelMatcher: [], + discoveryConfig: '', + lastSync: 0, + }, + overrides + ); +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/testHelpers/mockAwsOidcStatusProvider.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/testHelpers/mockAwsOidcStatusProvider.tsx index 0a513d3d0b57f..fa9f86bc2fc6d 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/testHelpers/mockAwsOidcStatusProvider.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/testHelpers/mockAwsOidcStatusProvider.tsx @@ -20,6 +20,7 @@ import React from 'react'; import { MemoryRouter } from 'react-router'; import { ContextProvider } from 'teleport'; +import { Route } from 'teleport/components/Router'; import { awsOidcStatusContext, AwsOidcStatusContextState, @@ -29,17 +30,21 @@ import { createTeleportContext } from 'teleport/mocks/contexts'; export const MockAwsOidcStatusProvider = ({ children, value, + initialEntries, + path, }: { children?: React.ReactNode; value: AwsOidcStatusContextState; + path: string; + initialEntries?: string[]; }) => { const ctx = createTeleportContext(); return ( - + - {children} + <>{children}} /> diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/useAwsOidcStatus.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/useAwsOidcStatus.tsx index 3593bd916a3d4..aaf0acfd8b9e1 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/useAwsOidcStatus.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/useAwsOidcStatus.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React, { createContext, useContext, useEffect } from 'react'; +import { createContext, useContext, useEffect } from 'react'; import { useParams } from 'react-router'; import { Attempt, useAsync } from 'shared/hooks/useAsync'; diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts index aab94e2591476..b055a011cf982 100644 --- a/web/packages/teleport/src/config.ts +++ b/web/packages/teleport/src/config.ts @@ -28,6 +28,8 @@ import { } from 'shared/services'; import { mergeDeep } from 'shared/utils/highbar'; +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; +import { TaskState } from 'teleport/Integrations/status/AwsOidc/Tasks/constants'; import type { SortType } from 'teleport/services/agents'; import { AwsOidcPolicyPreset, @@ -204,6 +206,9 @@ const cfg = { headlessSso: `/web/headless/:requestId`, integrations: '/web/integrations', integrationStatus: '/web/integrations/status/:type/:name', + integrationTasks: '/web/integrations/status/:type/:name/tasks', + integrationStatusResources: + '/web/integrations/status/:type/:name/resources/:resourceKind', integrationEnroll: '/web/integrations/new/:type?', locks: '/web/locks', newLock: '/web/locks/new', @@ -359,6 +364,15 @@ const cfg = { integrationsPath: '/v1/webapi/sites/:clusterId/integrations/:name?', integrationStatsPath: '/v1/webapi/sites/:clusterId/integrations/:name/stats', + integrationRulesPath: + '/v1/webapi/sites/:clusterId/integrations/:name/discoveryrules?resourceType=:resourceType?&startKey=:startKey?&query=:query?&search=:search?&sort=:sort?&limit=:limit?®ions=:regions?', + awsOidcDatabaseServicesPath: + '/v1/webapi/sites/:clusterId/integrations/aws-oidc/:name/listdeployeddatabaseservices?resourceType=:resourceType?®ions=:regions?', + userTaskListByIntegrationPath: + '/v1/webapi/sites/:clusterId/usertask?integration=:name?&state=:state?', + userTaskPath: '/v1/webapi/sites/:clusterId/usertask/:name', + resolveUserTaskPath: '/v1/webapi/sites/:clusterId/usertask/:name/state', + thumbprintPath: '/v1/webapi/thumbprint', pingAwsOidcIntegrationPath: '/v1/webapi/sites/:clusterId/integrations/aws-oidc/:name/ping', @@ -590,6 +604,22 @@ const cfg = { return generatePath(cfg.routes.integrationStatus, { type, name }); }, + getIntegrationStatusResourcesRoute( + type: PluginKind | IntegrationKind, + name: string, + resourceKind: AwsResource + ) { + return generatePath(cfg.routes.integrationStatusResources, { + type, + name, + resourceKind, + }); + }, + + getIntegrationTasksRoute(type: PluginKind | IntegrationKind, name: string) { + return generatePath(cfg.routes.integrationTasks, { type, name }); + }, + getMsTeamsAppZipRoute(clusterId: string, plugin: string) { return generatePath(cfg.api.msTeamsAppZipPath, { clusterId, plugin }); }, @@ -1082,6 +1112,59 @@ const cfg = { }); }, + getIntegrationRulesUrl( + name: string, + resourceType: AwsResource, + regions?: string[] + ) { + const clusterId = cfg.proxyCluster; + return generateResourcePath(cfg.api.integrationRulesPath, { + clusterId, + name, + resourceType, + regions, + }); + }, + + getAwsOidcDatabaseServices( + name: string, + resourceType: AwsResource, + regions: string[] + ) { + const clusterId = cfg.proxyCluster; + return generateResourcePath(cfg.api.awsOidcDatabaseServicesPath, { + clusterId, + name, + resourceType, + regions, + }); + }, + + getIntegrationUserTasksListUrl(name: string, state: TaskState) { + const clusterId = cfg.proxyCluster; + return generatePath(cfg.api.userTaskListByIntegrationPath, { + clusterId, + name, + state, + }); + }, + + getUserTaskUrl(name: string) { + const clusterId = cfg.proxyCluster; + return generatePath(cfg.api.userTaskPath, { + clusterId, + name, + }); + }, + + getResolveUserTaskUrl(name: string) { + const clusterId = cfg.proxyCluster; + return generatePath(cfg.api.resolveUserTaskPath, { + clusterId, + name, + }); + }, + getPingAwsOidcIntegrationUrl({ integrationName, clusterId, @@ -1466,6 +1549,12 @@ export interface UrlKubeResourcesParams { kind: Omit; } +export interface UrlIntegrationParams { + name?: string; + resourceType?: string; + regions?: string[]; +} + export interface UrlDeployServiceIamConfigureScriptParams { integrationName: string; region: Regions; diff --git a/web/packages/teleport/src/generateResourcePath.test.ts b/web/packages/teleport/src/generateResourcePath.test.ts index cead245d24005..937a55c9599df 100644 --- a/web/packages/teleport/src/generateResourcePath.test.ts +++ b/web/packages/teleport/src/generateResourcePath.test.ts @@ -16,69 +16,107 @@ * along with this program. If not, see . */ -import cfg, { UrlKubeResourcesParams, UrlResourcesParams } from './config'; +import { + UrlIntegrationParams, + UrlKubeResourcesParams, + UrlResourcesParams, +} from './config'; import generateResourcePath from './generateResourcePath'; -test('undefined params are set to empty string', () => { +const fullParamPath = + '/v1/webapi/sites/:clusterId/:name/foo' + + '?kind=:kind?' + + '&kinds=:kinds?' + + '&kubeCluster=:kubeCluster?' + + '&kubeNamespace=:kubeNamespace?' + + '&limit=:limit?' + + '&pinnedOnly=:pinnedOnly?' + + '&query=:query?' + + '&resourceType=:resourceType?' + + '&search=:search?' + + '&searchAsRoles=:searchAsRoles?' + + '&sort=:sort?' + + '&startKey=:startKey?' + + '&includedResourceMode=:includedResourceMode?' + + '®ions=:regions?'; + +test('undefined params are set to empty', () => { expect( - generateResourcePath(cfg.api.unifiedResourcesPath, { clusterId: 'cluster' }) + generateResourcePath(fullParamPath, { + clusterId: 'some-cluster-id', + }) ).toStrictEqual( - '/v1/webapi/sites/cluster/resources?searchAsRoles=&limit=&startKey=&kinds=&query=&search=&sort=&pinnedOnly=&includedResourceMode=' + '/v1/webapi/sites/some-cluster-id//foo?kind=&kinds=&kubeCluster=&kubeNamespace=&limit=&pinnedOnly=&query=&resourceType=&search=&searchAsRoles=&sort=&startKey=&includedResourceMode=®ions=' ); }); +type allParams = UrlResourcesParams & + UrlKubeResourcesParams & + UrlIntegrationParams; + test('defined params are set', () => { - const unifiedParams: UrlResourcesParams = { - query: 'query', - search: 'search', - sort: { fieldName: 'field', dir: 'DESC' }, + const urlParams: allParams = { + includedResourceMode: 'all', + kind: 'some-kind', + kinds: ['app', 'db'], + kubeCluster: 'some-kube-cluster', + kubeNamespace: 'some-kube-namespace', limit: 100, - startKey: 'startkey', - searchAsRoles: 'yes', + name: 'some-name', pinnedOnly: true, - includedResourceMode: 'all', - kinds: ['app'], + query: 'some-query', + resourceType: 'some-resource-type', + search: 'some-search', + searchAsRoles: 'yes', + sort: { fieldName: 'sort-field', dir: 'DESC' }, + startKey: 'some-start-key', + regions: ['us-west-2', 'af-south-1'], }; expect( - generateResourcePath(cfg.api.unifiedResourcesPath, { - clusterId: 'cluster', - ...unifiedParams, + generateResourcePath(fullParamPath, { + clusterId: 'some-cluster-id', + ...urlParams, }) ).toStrictEqual( - '/v1/webapi/sites/cluster/resources?searchAsRoles=yes&limit=100&startKey=startkey&kinds=app&query=query&search=search&sort=field:desc&pinnedOnly=true&includedResourceMode=all' + '/v1/webapi/sites/some-cluster-id/some-name/foo?kind=some-kind&kinds=app&kinds=db&kubeCluster=some-kube-cluster&kubeNamespace=some-kube-namespace&limit=100&pinnedOnly=true&query=some-query&resourceType=some-resource-type&search=some-search&searchAsRoles=yes&sort=sort-field:desc&startKey=some-start-key&includedResourceMode=all®ions=us-west-2®ions=af-south-1' ); }); -test('defined params but set to empty values are set to empty string', () => { - const unifiedParams: UrlResourcesParams = { - query: '', - search: null, - limit: 0, - pinnedOnly: false, +test('defined params but set to empty values are set to empty', () => { + const urlParams: allParams = { + includedResourceMode: null, + kind: '', kinds: [], + kubeCluster: '', + kubeNamespace: '', + limit: 0, + name: '', + pinnedOnly: null, + query: '', + resourceType: '', + search: '', + searchAsRoles: '', + sort: null, + startKey: '', + regions: [], }; expect( - generateResourcePath(cfg.api.unifiedResourcesPath, { - clusterId: 'cluster', - ...unifiedParams, + generateResourcePath(fullParamPath, { + clusterId: 'some-cluster-id', + ...urlParams, }) ).toStrictEqual( - '/v1/webapi/sites/cluster/resources?searchAsRoles=&limit=&startKey=&kinds=&query=&search=&sort=&pinnedOnly=&includedResourceMode=' + '/v1/webapi/sites/some-cluster-id//foo?kind=&kinds=&kubeCluster=&kubeNamespace=&limit=&pinnedOnly=&query=&resourceType=&search=&searchAsRoles=&sort=&startKey=&includedResourceMode=®ions=' ); }); -test('defined kube related params are set', () => { - const params: UrlKubeResourcesParams = { - kind: 'namespace', - kubeCluster: 'kubecluster', - kubeNamespace: 'kubenamespace', - }; +test('unknown key values are not set even if declared in path', () => { + let unknownParamPath = '/v1/webapi/sites/view?foo=:foo?&bar=:bar?&baz=:baz?'; expect( - generateResourcePath(cfg.api.kubernetesResourcesPath, { - clusterId: 'cluster', - ...params, + generateResourcePath(unknownParamPath, { + foo: 'some-foo', + bar: 'some-bar', + baz: 'some-baz', }) - ).toStrictEqual( - '/v1/webapi/sites/cluster/kubernetes/resources?searchAsRoles=&limit=&startKey=&query=&search=&sort=&kubeCluster=kubecluster&kubeNamespace=kubenamespace&kind=namespace' - ); + ).toStrictEqual(unknownParamPath); }); diff --git a/web/packages/teleport/src/generateResourcePath.ts b/web/packages/teleport/src/generateResourcePath.ts index 908299cde6d88..d3245b2dea653 100644 --- a/web/packages/teleport/src/generateResourcePath.ts +++ b/web/packages/teleport/src/generateResourcePath.ts @@ -34,6 +34,8 @@ export default function generateResourcePath( ].dir.toLowerCase()}`; } else if (param === 'kinds') { processedParams[param] = (params[param] ?? []).join('&kinds='); + } else if (param === 'regions') { + processedParams[param] = (params[param] ?? []).join('®ions='); } else processedParams[param] = params[param] ? encodeURIComponent(params[param]) @@ -49,18 +51,23 @@ export default function generateResourcePath( } const output = path + // non-param .replace(':clusterId', params.clusterId) - .replace(':limit?', params.limit || '') - .replace(':startKey?', params.startKey || '') - .replace(':query?', processedParams.query || '') - .replace(':search?', processedParams.search || '') - .replace(':searchAsRoles?', processedParams.searchAsRoles || '') - .replace(':sort?', processedParams.sort || '') + .replace(':name', params.name || '') + // param .replace(':kind?', processedParams.kind || '') .replace(':kinds?', processedParams.kinds || '') .replace(':kubeCluster?', processedParams.kubeCluster || '') .replace(':kubeNamespace?', processedParams.kubeNamespace || '') + .replace(':limit?', params.limit || '') .replace(':pinnedOnly?', processedParams.pinnedOnly || '') + .replace(':query?', processedParams.query || '') + .replace(':resourceType?', params.resourceType || '') + .replace(':search?', processedParams.search || '') + .replace(':searchAsRoles?', processedParams.searchAsRoles || '') + .replace(':sort?', processedParams.sort || '') + .replace(':startKey?', params.startKey || '') + .replace(':regions?', processedParams.regions || '') .replace( ':includedResourceMode?', processedParams.includedResourceMode || '' diff --git a/web/packages/teleport/src/services/integrations/integrations.test.ts b/web/packages/teleport/src/services/integrations/integrations.test.ts index 1bb526752e29d..7b9b24accdf80 100644 --- a/web/packages/teleport/src/services/integrations/integrations.test.ts +++ b/web/packages/teleport/src/services/integrations/integrations.test.ts @@ -17,6 +17,8 @@ */ import cfg from 'teleport/config'; +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; +import { TaskState } from 'teleport/Integrations/status/AwsOidc/Tasks/constants'; import api from 'teleport/services/api'; import { integrationService } from './integrations'; @@ -234,6 +236,102 @@ describe('fetchAwsDatabases() request body formatting', () => { ); }); +test('fetch integration rules: fetchIntegrationRules()', async () => { + // test a valid response + jest.spyOn(api, 'get').mockResolvedValue({ + rules: [ + { + resourceType: 'eks', + region: 'us-west-2', + labelMatcher: [{ name: 'env', value: 'dev' }], + discoveryConfig: 'cfg', + lastSync: 1733782634, + }, + ], + nextKey: 'some-key', + }); + + let response = await integrationService.fetchIntegrationRules( + 'name', + AwsResource.eks + ); + expect(api.get).toHaveBeenCalledWith( + cfg.getIntegrationRulesUrl('name', AwsResource.eks, []) + ); + expect(response).toEqual({ + nextKey: 'some-key', + rules: [ + { + resourceType: 'eks', + region: 'us-west-2', + labelMatcher: [{ name: 'env', value: 'dev' }], + discoveryConfig: 'cfg', + lastSync: 1733782634, + }, + ], + }); + + // test null response + jest.spyOn(api, 'get').mockResolvedValue(null); + + response = await integrationService.fetchIntegrationRules( + 'name', + AwsResource.eks + ); + expect(response).toEqual({ + nextKey: undefined, + rules: [], + }); +}); + +test('fetch integration user task list: fetchIntegrationUserTasksList()', async () => { + // test a valid response + jest.spyOn(api, 'get').mockResolvedValue({ + items: [ + { + name: 'task-name', + taskType: 'task-type', + state: 'task-state', + issueType: 'issue-type', + integration: 'name', + }, + ], + nextKey: 'some-key', + }); + + let response = await integrationService.fetchIntegrationUserTasksList( + 'name', + TaskState.Open + ); + expect(api.get).toHaveBeenCalledWith( + cfg.getIntegrationUserTasksListUrl('name', TaskState.Open) + ); + expect(response).toEqual({ + nextKey: 'some-key', + items: [ + { + name: 'task-name', + taskType: 'task-type', + state: 'task-state', + issueType: 'issue-type', + integration: 'name', + }, + ], + }); + + // test null response + jest.spyOn(api, 'get').mockResolvedValue(null); + + response = await integrationService.fetchIntegrationUserTasksList( + 'name', + TaskState.Open + ); + expect(response).toEqual({ + nextKey: undefined, + items: [], + }); +}); + const nonAwsOidcIntegration = { name: 'non-aws-oidc-integration', subKind: 'abc', diff --git a/web/packages/teleport/src/services/integrations/integrations.ts b/web/packages/teleport/src/services/integrations/integrations.ts index 39a4ed25f5d45..6178a79cc8150 100644 --- a/web/packages/teleport/src/services/integrations/integrations.ts +++ b/web/packages/teleport/src/services/integrations/integrations.ts @@ -17,6 +17,8 @@ */ import cfg from 'teleport/config'; +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; +import { TaskState } from 'teleport/Integrations/status/AwsOidc/Tasks/constants'; import api from 'teleport/services/api'; import { App } from '../apps'; @@ -27,8 +29,10 @@ import { withUnsupportedLabelFeatureErrorConversion } from '../version/unsupport import { AwsDatabaseVpcsResponse, AwsOidcDeployDatabaseServicesRequest, + AWSOIDCDeployedDatabaseService, AwsOidcDeployServiceRequest, AwsOidcListDatabasesRequest, + AWSOIDCListDeployedDatabaseServiceResponse, AwsOidcPingRequest, AwsOidcPingResponse, AwsRdsDatabase, @@ -42,6 +46,7 @@ import { Integration, IntegrationCreateRequest, IntegrationCreateResult, + IntegrationDiscoveryRules, IntegrationKind, IntegrationListResponse, IntegrationStatusCode, @@ -65,6 +70,9 @@ import { SecurityGroup, SecurityGroupRule, Subnet, + UserTask, + UserTaskDetail, + UserTasksListResponse, } from './types'; export const integrationService = { @@ -493,8 +501,110 @@ export const integrationService = { return resp; }); }, + + fetchIntegrationRules( + name: string, + resourceType: AwsResource, + regions?: string[] + ): Promise { + return api + .get(cfg.getIntegrationRulesUrl(name, resourceType, regions)) + .then(resp => { + return { + rules: resp?.rules || [], + nextKey: resp?.nextKey, + }; + }); + }, + + fetchAwsOidcDatabaseServices( + name: string, + resourceType: AwsResource, + regions: string[] + ): Promise { + return api + .post(cfg.getAwsOidcDatabaseServices(name, resourceType, regions), null) + .then(resp => { + return { services: makeDatabaseServices(resp) }; + }); + }, + + fetchIntegrationUserTasksList( + name: string, + state: TaskState + ): Promise { + return api + .get(cfg.getIntegrationUserTasksListUrl(name, state)) + .then(resp => { + return { + items: resp?.items || [], + nextKey: resp?.nextKey, + }; + }); + }, + + fetchUserTask(name: string): Promise { + return api.get(cfg.getUserTaskUrl(name)).then(resp => { + return { + name: resp.name, + taskType: resp.taskType, + state: resp.state, + issueType: resp.issueType, + integration: resp.integration, + lastStateChange: resp.lastStateChange, + description: resp.description, + discoverEc2: { + instances: resp.discoverEc2?.instances, + accountId: resp.discoverEc2?.accountId, + region: resp.discoverEc2?.region, + ssmDocument: resp.discoverEc2?.ssmDocument, + installerScript: resp.discoverEc2?.installerScript, + }, + discoverEks: { + clusters: resp.discoverEks?.instances, + accountId: resp.discoverEks?.accountId, + region: resp.discoverEks?.region, + appAutoDiscover: resp.discoverEks?.appAutoDiscover, + }, + discoverRds: { + databases: resp.discoverRds?.instances, + accountId: resp.discoverRds?.accountId, + region: resp.discoverRds?.region, + }, + }; + }); + }, + + resolveUserTask(name: string): Promise { + return api + .put(cfg.getResolveUserTaskUrl(name), { + state: TaskState.Resolved, + }) + .then(resp => { + return { + name: resp.name, + taskType: resp.taskType, + state: resp.state, + issueType: resp.issueType, + integration: resp.integration, + lastStateChange: resp.lastStateChange, + }; + }); + }, }; +function makeDatabaseServices(json: any): AWSOIDCDeployedDatabaseService[] { + json = json ?? {}; + const { services } = json; + + return services?.map((service: AWSOIDCDeployedDatabaseService) => ({ + name: service.name ?? '', + dashboardUrl: service.dashboardUrl ?? '', + validTeleportConfig: service.validTeleportConfig ?? false, + matchingLabels: service.matchingLabels ?? [], + })); +} + export function makeIntegrations(json: any): Integration[] { json = json || []; return json.map(user => makeIntegration(user)); diff --git a/web/packages/teleport/src/services/integrations/types.ts b/web/packages/teleport/src/services/integrations/types.ts index 6b31782967788..d4be092746a97 100644 --- a/web/packages/teleport/src/services/integrations/types.ts +++ b/web/packages/teleport/src/services/integrations/types.ts @@ -354,6 +354,159 @@ export type IntegrationWithSummary = { awseks: ResourceTypeSummary; }; +// IntegrationDiscoveryRules contains the list of discovery rules for a given Integration. +export type IntegrationDiscoveryRules = { + // rules is the list of integration rules. + rules: IntegrationDiscoveryRule[]; + // nextKey is the position to resume listing rules. + nextKey: string; +}; + +// UserTasksListResponse contains a list of UserTasks. +// In case of exceeding the pagination limit (either via query param `limit` or the default 1000) +// a `nextToken` is provided and should be used to obtain the next page (as a query param `startKey`) +export type UserTasksListResponse = { + // items is a list of resources retrieved. + items: UserTask[]; + // nextKey is the position to resume listing events. + nextKey: string; +}; + +// UserTask describes UserTask fields. +// Used for listing User Tasks without receiving all the details. +export type UserTask = { + // name is the UserTask name. + name: string; + // taskType identifies this task's type. + taskType: string; + // state is the state for the User Task. + state: string; + // issueType identifies this task's issue type. + issueType: string; + // integration is the Integration Name this User Task refers to. + integration: string; + // lastStateChange indicates when the current's user task state was last changed. + lastStateChange: string; +}; + +// UserTaskDetail contains all the details for a User Task. +export type UserTaskDetail = UserTask & { + // description is a markdown document that explains the issue and how to fix it. + description: string; + // discoverEc2 contains the task details for the DiscoverEc2 tasks. + discoverEc2: DiscoverEc2; + // discoverEKS contains the task details for the DiscoverEKS tasks. + discoverEks: DiscoverEks; + // discoverRDS contains the task details for the DiscoverRDS tasks. + discoverRds: DiscoverRds; +}; + +// DiscoverEc2 contains the instances that failed to auto-enroll into the cluster. +export type DiscoverEc2 = { + // instances maps an instance id to the result of enrolling that instance into teleport. + instances: Record; + // accountID is the AWS Account ID for the instances. + accountId: string; + // region is the AWS Region where Teleport failed to enroll EC2 instances. + region: string; + // ssmDocument is the Amazon Systems Manager SSM Document name that was used to install teleport on the instance. + // In Amazon console, the document is at: + // https://REGION.console.aws.amazon.com/systems-manager/documents/SSM_DOCUMENT/description + ssmDocument: string; + // installerScript is the Teleport installer script that was used to install teleport on the instance. + installerScript: string; +}; + +// DiscoverEc2Instance contains the result of enrolling an AWS EC2 Instance. +export type DiscoverEc2Instance = { + // instanceID is the EC2 Instance ID that uniquely identifies the instance. + instance_id: string; + // name is the instance Name. + // Might be empty, if the instance doesn't have the Name tag. + name: string; + // invocationUrl is the url that points to the invocation. + // Empty if there was an error before installing the + invocationUrl: string; + // discoveryConfig is the discovery config name that originated this instance enrollment. + discoveryConfig: string; + // discoveryGroup is the DiscoveryGroup name that originated this task. + discoveryGroup: string; + // syncTime is the timestamp when the error was produced. + syncTime: number; +}; + +// DiscoverEks contains the clusters that failed to auto-enroll into the cluster. +export type DiscoverEks = { + // clusters maps a cluster name to the result of enrolling that cluster into teleport. + clusters: Record; + // accountId is the AWS Account ID for the cluster. + accountId: string; + // region is the AWS Region where Teleport failed to enroll EKS Clusters. + region: string; + // appAutoDiscover indicates whether the Kubernetes agent should auto enroll HTTP services as Teleport Apps. + appAutoDiscover: boolean; +}; + +// DiscoverEksCluster contains the result of enrolling an AWS EKS Cluster. +export type DiscoverEksCluster = { + // name is the cluster Name. + name: string; + // discoveryConfig is the discovery config name that originated this cluster enrollment. + discoveryConfig: string; + // discoveryGroup is the DiscoveryGroup name that originated this task. + discoveryGroup: string; + // syncTime is the timestamp when the error was produced. + syncTime: number; +}; + +// DiscoverRds contains the databases that failed to auto-enroll into teleport. +export type DiscoverRds = { + // databases maps a database resource id to the result of enrolling that database into teleport. + // For RDS Aurora Clusters, this is the DBClusterIdentifier. + // For other RDS databases, this is the DBInstanceIdentifier. + databases: Record; + // accountId is the AWS Account ID for the database. + accountId: string; + // region is the AWS Region where Teleport failed to enroll RDS databases. + region: string; +}; + +// DiscoverRdsDatabase contains the result of enrolling an AWS RDS database. +export type DiscoverRdsDatabase = { + // name is the database identifier. + // For RDS Aurora Clusters, this is the DBClusterIdentifier. + // For other RDS databases, this is the DBInstanceIdentifier. + name: string; + // isCluster indicates whether this database is a cluster or a single instance. + isCluster: boolean; + // engine indicates the engine name for this RDS. + // Eg, aurora-postgresql, postgresql + engine: string; + // discoveryConfig is the discovery config name that originated this database enrollment. + discoveryConfig: string; + // discoveryGroup is the DiscoveryGroup name that originated this task. + discoveryGroup: string; + // syncTime is the timestamp when the error was produced. + syncTime: number; +}; + +// IntegrationDiscoveryRule describes a discovery rule associated with an integration. +export type IntegrationDiscoveryRule = { + // resourceType indicates the type of resource that this rule targets. + // This is the same value that is set in DiscoveryConfig.AWS..Types + // Example: ec2, rds, eks + resourceType: string; + // region where this rule applies to. + region: string; + // labelMatcher is the set of labels that are used to filter the resources before trying to auto-enroll them. + labelMatcher: Label[]; + // discoveryConfig is the name of the DiscoveryConfig that created this rule. + discoveryConfig: string; + // lastSync contains the time when this rule was used. + // If empty, it indicates that the rule is not being used. + lastSync: number; +}; + // ResourceTypeSummary contains the summary of the enrollment rules and found resources by the integration. export type ResourceTypeSummary = { // rulesCount is the number of enrollment rules that are using this integration. @@ -376,6 +529,26 @@ export type ResourceTypeSummary = { ecsDatabaseServiceCount: number; }; +// AWSOIDCListDeployedDatabaseServiceResponse is a list of Teleport Database Services that are deployed as ECS Services. +export type AWSOIDCListDeployedDatabaseServiceResponse = { + // services are the ECS Services. + services: AWSOIDCDeployedDatabaseService[]; +}; + +// AWSOIDCDeployedDatabaseService represents a Teleport Database Service that is deployed in Amazon ECS. +export type AWSOIDCDeployedDatabaseService = { + // name is the ECS Service name. + name: string; + // dashboardUrl is the link to the ECS Service in Amazon Web Console. + dashboardUrl: string; + // validTeleportConfig returns whether this ECS Service has a valid Teleport Configuration for a deployed Database Service. + // ECS Services with non-valid configuration require the user to take action on them. + // No MatchingLabels are returned with an invalid configuration. + validTeleportConfig: boolean; + // matchingLabels are the labels that are used by the Teleport Database Service to know which databases it should proxy. + matchingLabels: Label[]; +}; + // awsRegionMap maps the AWS regions to it's region name // as defined in (omitted gov cloud regions): // https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html