1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-08 15:25:22 +02:00

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

* EE-2141 Add pagination to kubeconfig download dialog
This commit is contained in:
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;
});
}
}