diff --git a/app/portainer/home/EnvironmentList/EnvironmentList.tsx b/app/portainer/home/EnvironmentList/EnvironmentList.tsx index dbae0e16e..3c64d0060 100644 --- a/app/portainer/home/EnvironmentList/EnvironmentList.tsx +++ b/app/portainer/home/EnvironmentList/EnvironmentList.tsx @@ -302,7 +302,19 @@ export function EnvironmentList({ onClickItem, onRefresh }: Props) { )}
- +
- kubeconfig - + <> + + {prompt()} + ); function handleClick() { @@ -34,48 +45,28 @@ export function KubeconfigButton({ environments }: Props) { category: 'kubernetes', }); - showKubeconfigModal(environments); - } -} - -function isKubeconfigButtonVisible(environments: Environment[]) { - if (window.location.protocol !== 'https:') { - return false; - } - return environments.some((env) => isKubernetesEnvironment(env.Type)); -} - -async function showKubeconfigModal(environments: Environment[]) { - const kubeEnvironments = environments.filter((env) => - isKubernetesEnvironment(env.Type) - ); - const options = kubeEnvironments.map((environment) => ({ - text: `${environment.Name} (${environment.URL})`, - value: `${environment.Id}`, - })); - - let expiryMessage = ''; - try { - expiryMessage = await kcService.expiryMessage(); - } catch (e) { - notifications.error('Failed fetching kubeconfig expiry time', e as Error); + setIsOpen(true); } - confirmKubeconfigSelection( - options, - expiryMessage, - async (selectedEnvironmentIDs: string[]) => { - if (selectedEnvironmentIDs.length === 0) { - notifications.warning('No environment was selected', ''); - return; - } - try { - await kcService.downloadKubeconfigFile( - selectedEnvironmentIDs.map((id) => parseInt(id, 10)) - ); - } catch (e) { - notifications.error('Failed downloading kubeconfig file', e as Error); - } + function handleClose() { + setIsOpen(false); + } + + function isKubeconfigButtonVisible(environments: Environment[]) { + if (window.location.protocol !== 'https:') { + return false; } - ); + return environments.some((env) => isKubernetesEnvironment(env.Type)); + } + + function prompt() { + return ( + isOpen && ( + + ) + ); + } } diff --git a/app/portainer/home/EnvironmentList/KubeconfigPrompt.module.css b/app/portainer/home/EnvironmentList/KubeconfigPrompt.module.css new file mode 100644 index 000000000..4cd959232 --- /dev/null +++ b/app/portainer/home/EnvironmentList/KubeconfigPrompt.module.css @@ -0,0 +1,9 @@ +.checkbox { + padding-left: 0.5rem; +} + +.dialog { + display: flex; + justify-content: center; + align-items: center; +} diff --git a/app/portainer/home/EnvironmentList/KubeconfigPrompt.tsx b/app/portainer/home/EnvironmentList/KubeconfigPrompt.tsx new file mode 100644 index 000000000..8a6493226 --- /dev/null +++ b/app/portainer/home/EnvironmentList/KubeconfigPrompt.tsx @@ -0,0 +1,141 @@ +import { useState } from 'react'; +import { useQuery } from 'react-query'; +import { DialogOverlay } from '@reach/dialog'; + +import * as kcService from '@/kubernetes/services/kubeconfig.service'; +import * as notifications from '@/portainer/services/notifications'; +import { Button } from '@/portainer/components/Button'; +import { Checkbox } from '@/portainer/components/form-components/Checkbox'; +import { EnvironmentType } from '@/portainer/environments/types'; +import { EnvironmentsQueryParams } from '@/portainer/environments/environment.service/index'; +import { PaginationControls } from '@/portainer/components/pagination-controls'; +import { usePaginationLimitState } from '@/portainer/hooks/usePaginationLimitState'; +import { useEnvironmentList } from '@/portainer/environments/queries/useEnvironmentList'; + +import { useSelection } from './KubeconfigSelection'; +import styles from './KubeconfigPrompt.module.css'; +import '@reach/dialog/styles.css'; + +export interface KubeconfigPromptProps { + envQueryParams: EnvironmentsQueryParams; + onClose: () => void; +} +const storageKey = 'home_endpoints'; + +export function KubeconfigPrompt({ + envQueryParams, + onClose, +}: KubeconfigPromptProps) { + const [page, setPage] = useState(1); + const [pageLimit, setPageLimit] = usePaginationLimitState(storageKey); + const kubeServiceExpiryQuery = useQuery(['kubeServiceExpiry'], async () => { + const expiryMessage = await kcService.expiryMessage(); + return expiryMessage; + }); + const { selection, toggle: toggleSelection, selectionSize } = useSelection(); + const { environments, totalCount } = useEnvironmentList({ + ...envQueryParams, + page, + pageLimit, + types: [ + EnvironmentType.KubernetesLocal, + EnvironmentType.AgentOnKubernetes, + EnvironmentType.EdgeAgentOnKubernetes, + ], + }); + const isAllPageSelected = environments.every((env) => selection[env.Id]); + + return ( + +
+
+
+ +
Download kubeconfig file
+
+
+
+
+ + Select the kubernetes environments to add to the kubeconfig + file. You may select across multiple pages. + + + {kubeServiceExpiryQuery.data} + +
+
+
+ +
+
+ {environments.map((env) => ( +
+ + toggleSelection(env.Id, !selection[env.Id]) + } + /> +
+ ))} +
+
+ +
+
+
+
+ + +
+
+
+
+ ); + + function handleSelectAll() { + environments.forEach((env) => toggleSelection(env.Id, !isAllPageSelected)); + } + + function handleDownload() { + confirmKubeconfigSelection(); + } + + async function confirmKubeconfigSelection() { + if (selectionSize === 0) { + notifications.warning('No environment was selected', ''); + return; + } + try { + await kcService.downloadKubeconfigFile( + Object.keys(selection).map(Number) + ); + onClose(); + } catch (e) { + notifications.error('Failed downloading kubeconfig file', e as Error); + } + } +} diff --git a/app/portainer/home/EnvironmentList/KubeconfigSelection.ts b/app/portainer/home/EnvironmentList/KubeconfigSelection.ts new file mode 100644 index 000000000..ee4b07ba8 --- /dev/null +++ b/app/portainer/home/EnvironmentList/KubeconfigSelection.ts @@ -0,0 +1,27 @@ +import { useState } from 'react'; + +import { EnvironmentId } from '@/portainer/environments/types'; + +export function useSelection() { + const [selection, setSelection] = useState>( + {} + ); + + const selectionSize = Object.keys(selection).length; + + return { selection, toggle, selectionSize }; + + function toggle(id: EnvironmentId, selected: boolean) { + setSelection((prevSelection) => { + const newSelection = { ...prevSelection }; + + if (!selected) { + delete newSelection[id]; + } else { + newSelection[id] = true; + } + + return newSelection; + }); + } +} diff --git a/package.json b/package.json index 3b66e4cbc..f4fe031b9 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@lineup-lite/hooks": "^1.6.0", "@nxmix/tokenize-ansi": "^3.0.0", "@open-amt-cloud-toolkit/ui-toolkit-react": "2.0.0", + "@reach/dialog": "^0.17.0", "@reach/menu-button": "^0.16.1", "@uirouter/angularjs": "1.0.11", "@uirouter/react": "^1.0.7", diff --git a/yarn.lock b/yarn.lock index 9cf950823..b1f511632 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1158,6 +1158,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.12.13": + version "7.18.3" + resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.18.3.tgz#c7b654b57f6f63cf7f8b418ac9ca04408c4579f4" + integrity sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.12.7", "@babel/template@^7.16.7", "@babel/template@^7.3.3": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" @@ -1939,6 +1946,18 @@ "@reach/utils" "0.16.0" tslib "^2.3.0" +"@reach/dialog@^0.17.0": + version "0.17.0" + resolved "https://registry.npmmirror.com/@reach/dialog/-/dialog-0.17.0.tgz#81c48dd4405945dfc6b6c3e5e125db2c4324e9e8" + integrity sha512-AnfKXugqDTGbeG3c8xDcrQDE4h9b/vnc27Sa118oQSquz52fneUeX9MeFb5ZEiBJK8T5NJpv7QUTBIKnFCAH5A== + dependencies: + "@reach/portal" "0.17.0" + "@reach/utils" "0.17.0" + prop-types "^15.7.2" + react-focus-lock "^2.5.2" + react-remove-scroll "^2.4.3" + tslib "^2.3.0" + "@reach/dropdown@0.16.2": version "0.16.2" resolved "https://registry.yarnpkg.com/@reach/dropdown/-/dropdown-0.16.2.tgz#4aa7df0f716cb448d01bc020d54df595303d5fa6" @@ -1987,6 +2006,15 @@ tiny-warning "^1.0.3" tslib "^2.3.0" +"@reach/portal@0.17.0": + version "0.17.0" + resolved "https://registry.npmmirror.com/@reach/portal/-/portal-0.17.0.tgz#1dd69ffc8ffc8ba3e26dd127bf1cc4b15f0c6bdc" + integrity sha512-+IxsgVycOj+WOeNPL2NdgooUdHPSY285wCtj/iWID6akyr4FgGUK7sMhRM9aGFyrGpx2vzr+eggbUmAVZwOz+A== + dependencies: + "@reach/utils" "0.17.0" + tiny-warning "^1.0.3" + tslib "^2.3.0" + "@reach/rect@0.16.0": version "0.16.0" resolved "https://registry.yarnpkg.com/@reach/rect/-/rect-0.16.0.tgz#78cf6acefe2e83d3957fa84f938f6e1fc5700f16" @@ -2006,6 +2034,14 @@ tiny-warning "^1.0.3" tslib "^2.3.0" +"@reach/utils@0.17.0": + version "0.17.0" + resolved "https://registry.npmmirror.com/@reach/utils/-/utils-0.17.0.tgz#3d1d2ec56d857f04fe092710d8faee2b2b121303" + integrity sha512-M5y8fCBbrWeIsxedgcSw6oDlAMQDkl5uv3VnMVJ7guwpf4E48Xlh1v66z/1BgN/WYe2y8mB/ilFD2nysEfdGeA== + dependencies: + tiny-warning "^1.0.3" + tslib "^2.3.0" + "@sgratzl/boxplots@^1.2.2": version "1.3.0" resolved "https://registry.yarnpkg.com/@sgratzl/boxplots/-/boxplots-1.3.0.tgz#c9063d98e33a15f880cf4bd3531be71497e2a94e" @@ -7037,6 +7073,11 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +detect-node-es@^1.1.0: + version "1.1.0" + resolved "https://registry.npmmirror.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" + integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== + detect-node@^2.0.4, detect-node@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" @@ -8613,6 +8654,13 @@ fn.name@1.x.x: resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== +focus-lock@^0.11.2: + version "0.11.2" + resolved "https://registry.npmmirror.com/focus-lock/-/focus-lock-0.11.2.tgz#aeef3caf1cea757797ac8afdebaec8fd9ab243ed" + integrity sha512-pZ2bO++NWLHhiKkgP1bEXHhR1/OjVcSvlCJ98aNJDFeb7H5OOQaO+SKOZle6041O9rv2tmbrO4JzClAvDUHf0g== + dependencies: + tslib "^2.0.3" + follow-redirects@^1.0.0, follow-redirects@^1.14.4: version "1.14.8" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" @@ -8895,6 +8943,11 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" +get-nonce@^1.0.0: + version "1.0.1" + resolved "https://registry.npmmirror.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" + integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -14213,6 +14266,13 @@ rc-util@^5.16.1, rc-util@^5.2.1, rc-util@^5.3.0, rc-util@^5.5.0: react-is "^16.12.0" shallowequal "^1.1.0" +react-clientside-effect@^1.2.6: + version "1.2.6" + resolved "https://registry.npmmirror.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz#29f9b14e944a376b03fb650eed2a754dd128ea3a" + integrity sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg== + dependencies: + "@babel/runtime" "^7.12.13" + react-colorful@^5.1.2: version "5.5.1" resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.5.1.tgz#29d9c4e496f2ca784dd2bb5053a3a4340cfaf784" @@ -14282,6 +14342,18 @@ react-feather@^2.0.9: dependencies: prop-types "^15.7.2" +react-focus-lock@^2.5.2: + version "2.9.1" + resolved "https://registry.npmmirror.com/react-focus-lock/-/react-focus-lock-2.9.1.tgz#094cfc19b4f334122c73bb0bff65d77a0c92dd16" + integrity sha512-pSWOQrUmiKLkffPO6BpMXN7SNKXMsuOakl652IBuALAu1esk+IcpJyM+ALcYzPTTFz1rD0R54aB9A4HuP5t1Wg== + dependencies: + "@babel/runtime" "^7.0.0" + focus-lock "^0.11.2" + prop-types "^15.6.2" + react-clientside-effect "^1.2.6" + use-callback-ref "^1.3.0" + use-sidecar "^1.1.2" + react-helmet-async@^1.0.7: version "1.2.2" resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-1.2.2.tgz#38d58d32ebffbc01ba42b5ad9142f85722492389" @@ -14352,6 +14424,25 @@ react-refresh@^0.11.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== +react-remove-scroll-bar@^2.3.1: + version "2.3.1" + resolved "https://registry.npmmirror.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.1.tgz#9f13b05b249eaa57c8d646c1ebb83006b3581f5f" + integrity sha512-IvGX3mJclEF7+hga8APZczve1UyGMkMG+tjS0o/U1iLgvZRpjFAQEUBJ4JETfvbNlfNnZnoDyWJCICkA15Mghg== + dependencies: + react-style-singleton "^2.2.0" + tslib "^2.0.0" + +react-remove-scroll@^2.4.3: + version "2.5.3" + resolved "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.5.3.tgz#a152196e710e8e5811be39dc352fd8a90b05c961" + integrity sha512-NQ1bXrxKrnK5pFo/GhLkXeo3CrK5steI+5L+jynwwIemvZyfXqaL0L5BzwJd7CSwNCU723DZaccvjuyOdoy3Xw== + dependencies: + react-remove-scroll-bar "^2.3.1" + react-style-singleton "^2.2.0" + tslib "^2.0.0" + use-callback-ref "^1.3.0" + use-sidecar "^1.1.2" + react-router-dom@^6.0.0: version "6.2.1" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.2.1.tgz#32ec81829152fbb8a7b045bf593a22eadf019bec" @@ -14398,6 +14489,15 @@ react-sizeme@^3.0.1: shallowequal "^1.1.0" throttle-debounce "^3.0.1" +react-style-singleton@^2.2.0: + version "2.2.0" + resolved "https://registry.npmmirror.com/react-style-singleton/-/react-style-singleton-2.2.0.tgz#70f45f5fef97fdb9a52eed98d1839fa6b9032b22" + integrity sha512-nK7mN92DMYZEu3cQcAhfwE48NpzO5RpxjG4okbSqRRbfal9Pk+fG2RdQXTMp+f6all1hB9LIJSt+j7dCYrU11g== + dependencies: + get-nonce "^1.0.0" + invariant "^2.2.4" + tslib "^2.0.0" + react-syntax-highlighter@^13.5.3: version "13.5.3" resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-13.5.3.tgz#9712850f883a3e19eb858cf93fad7bb357eea9c6" @@ -16944,6 +17044,13 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +use-callback-ref@^1.3.0: + version "1.3.0" + resolved "https://registry.npmmirror.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5" + integrity sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w== + dependencies: + tslib "^2.0.0" + use-composed-ref@^1.0.0: version "1.2.1" resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.2.1.tgz#9bdcb5ccd894289105da2325e1210079f56bf849" @@ -16961,6 +17068,14 @@ use-latest@^1.0.0: dependencies: use-isomorphic-layout-effect "^1.0.0" +use-sidecar@^1.1.2: + version "1.1.2" + resolved "https://registry.npmmirror.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" + integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw== + dependencies: + detect-node-es "^1.1.0" + tslib "^2.0.0" + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"