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:
parent
be11dfc231
commit
b6309682ef
7 changed files with 348 additions and 52 deletions
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
.checkbox {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
141
app/portainer/home/EnvironmentList/KubeconfigPrompt.tsx
Normal file
141
app/portainer/home/EnvironmentList/KubeconfigPrompt.tsx
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
27
app/portainer/home/EnvironmentList/KubeconfigSelection.ts
Normal file
27
app/portainer/home/EnvironmentList/KubeconfigSelection.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue