feat(kubeconfig): pagination for downloading kubeconfigs EE-2141 (#6895)

* EE-2141 Add pagination to kubeconfig download dialog
pull/7057/head
Chao Geng 2022-06-10 11:42:27 +08:00 committed by GitHub
parent be11dfc231
commit b6309682ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 348 additions and 52 deletions

View File

@ -302,7 +302,19 @@ export function EnvironmentList({ onClickItem, onRefresh }: Props) {
)}
</div>
<div className={styles.kubeconfigButton}>
<KubeconfigButton environments={environments} />
<KubeconfigButton
environments={environments}
envQueryParams={{
types: platformType,
search: debouncedTextFilter,
status: statusFilter,
tagIds: tagFilter?.length ? tagFilter : undefined,
groupIds: groupFilter,
sort: sortByFilter,
order: sortByDescending ? 'desc' : 'asc',
edgeDeviceFilter: 'none',
}}
/>
</div>
<div className={styles.filterSearchbar}>
<FilterSearchBar

View File

@ -1,16 +1,24 @@
import * as kcService from '@/kubernetes/services/kubeconfig.service';
import * as notifications from '@/portainer/services/notifications';
import { confirmKubeconfigSelection } from '@/portainer/services/modal.service/prompt';
import { useState } from 'react';
import { Environment } from '@/portainer/environments/types';
import { EnvironmentsQueryParams } from '@/portainer/environments/environment.service/index';
import { isKubernetesEnvironment } from '@/portainer/environments/utils';
import { trackEvent } from '@/angulartics.matomo/analytics-services';
import { Button } from '@/portainer/components/Button';
interface Props {
environments?: Environment[];
}
import { KubeconfigPrompt } from './KubeconfigPrompt';
import '@reach/dialog/styles.css';
export interface KubeconfigButtonProps {
environments: Environment[];
envQueryParams: EnvironmentsQueryParams;
}
export function KubeconfigButton({
environments,
envQueryParams,
}: KubeconfigButtonProps) {
const [isOpen, setIsOpen] = useState(false);
export function KubeconfigButton({ environments }: Props) {
if (!environments) {
return null;
}
@ -20,9 +28,12 @@ export function KubeconfigButton({ environments }: Props) {
}
return (
<Button onClick={handleClick}>
<i className="fas fa-download space-right" /> kubeconfig
</Button>
<>
<Button onClick={handleClick}>
<i className="fas fa-download space-right" /> kubeconfig
</Button>
{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 && (
<KubeconfigPrompt
envQueryParams={envQueryParams}
onClose={handleClose}
/>
)
);
}
}

View File

@ -0,0 +1,9 @@
.checkbox {
padding-left: 0.5rem;
}
.dialog {
display: flex;
justify-content: center;
align-items: center;
}

View File

@ -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 (
<DialogOverlay
className={styles.dialog}
aria-label="Kubeconfig View"
role="dialog"
>
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" onClick={onClose}>
×
</button>
<h5 className="modal-title">Download kubeconfig file</h5>
</div>
<div className="modal-body">
<form className="bootbox-form">
<div className="bootbox-prompt-message">
<span>
Select the kubernetes environments to add to the kubeconfig
file. You may select across multiple pages.
</span>
<span className="space-left">
{kubeServiceExpiryQuery.data}
</span>
</div>
</form>
<br />
<Checkbox
id="settings-container-truncate-nae"
label="Select all (in this page)"
checked={isAllPageSelected}
onChange={handleSelectAll}
/>
<div className="datatable">
<div className="bootbox-checkbox-list">
{environments.map((env) => (
<div className={styles.checkbox}>
<Checkbox
id={`${env.Id}`}
label={`${env.Name} (${env.URL})`}
checked={!!selection[env.Id]}
onChange={() =>
toggleSelection(env.Id, !selection[env.Id])
}
/>
</div>
))}
</div>
<div className="footer">
<PaginationControls
showAll={totalCount <= 100}
page={page}
onPageChange={setPage}
pageLimit={pageLimit}
onPageLimitChange={setPageLimit}
totalCount={totalCount}
/>
</div>
</div>
</div>
<div className="modal-footer">
<Button onClick={onClose} color="default">
Cancel
</Button>
<Button onClick={handleDownload}>Download File</Button>
</div>
</div>
</div>
</DialogOverlay>
);
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);
}
}
}

View File

@ -0,0 +1,27 @@
import { useState } from 'react';
import { EnvironmentId } from '@/portainer/environments/types';
export function useSelection() {
const [selection, setSelection] = useState<Record<EnvironmentId, boolean>>(
{}
);
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;
});
}
}

View File

@ -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",

115
yarn.lock
View File

@ -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"