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
+
+
+
+
+
+
+
+ {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"