mirror of https://github.com/portainer/portainer
feat(home): add connect and browse buttons [EE-4182] (#8175)
parent
db9d87c918
commit
8936ae9b7a
|
@ -99,7 +99,7 @@ overrides:
|
|||
'@typescript-eslint/explicit-module-boundary-types': off
|
||||
'@typescript-eslint/no-unused-vars': 'error'
|
||||
'@typescript-eslint/no-explicit-any': 'error'
|
||||
'jsx-a11y/label-has-associated-control': ['error', { 'assert': 'either' }]
|
||||
'jsx-a11y/label-has-associated-control': ['error', { 'assert': 'either', controlComponents: ['Input', 'Checkbox'] }]
|
||||
'react/function-component-definition': ['error', { 'namedComponents': 'function-declaration' }]
|
||||
'react/jsx-no-bind': off
|
||||
'no-await-in-loop': 'off'
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import clsx from 'clsx';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
import { Button } from './buttons';
|
||||
|
@ -8,12 +9,14 @@ export function LinkButton({
|
|||
params,
|
||||
disabled,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: ComponentProps<typeof Button> & ComponentProps<typeof Link>) {
|
||||
const button = (
|
||||
<Button
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
className={clsx(className, '!m-0')}
|
||||
size="medium"
|
||||
disabled={disabled}
|
||||
>
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import { createStore } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
import { keyBuilder } from '@/react/hooks/useLocalStorage';
|
||||
|
||||
import { EnvironmentId } from '../portainer/environments/types';
|
||||
|
||||
export const environmentStore = createStore<{
|
||||
environmentId?: number;
|
||||
setEnvironmentId(id: EnvironmentId): void;
|
||||
clear(): void;
|
||||
}>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
environmentId: undefined,
|
||||
setEnvironmentId: (id: EnvironmentId) => set({ environmentId: id }),
|
||||
clear: () => set({ environmentId: undefined }),
|
||||
}),
|
||||
{
|
||||
name: keyBuilder('environmentId'),
|
||||
getStorage: () => sessionStorage,
|
||||
}
|
||||
)
|
||||
);
|
|
@ -0,0 +1,20 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
export function useListSelection<T>(
|
||||
initialValue: Array<T> = [],
|
||||
compareFn: (a: T, b: T) => boolean = (a, b) => a === b
|
||||
) {
|
||||
const [selectedItems, setSelectedItems] = useState<Array<T>>(initialValue);
|
||||
|
||||
function handleChangeSelect(currentItem: T, selected: boolean) {
|
||||
if (selected) {
|
||||
setSelectedItems((items) => [...items, currentItem]);
|
||||
} else {
|
||||
setSelectedItems((items) =>
|
||||
items.filter((item) => !compareFn(item, currentItem))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return [selectedItems, handleChangeSelect] as const;
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import { Wifi, WifiOff } from 'lucide-react';
|
||||
import { History, Wifi, WifiOff } from 'lucide-react';
|
||||
|
||||
import ClockRewind from '@/assets/ico/clock-rewind.svg?c';
|
||||
import { Environment } from '@/react/portainer/environments/types';
|
||||
import {
|
||||
getDashboardRoute,
|
||||
|
@ -11,6 +10,7 @@ import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
|||
import { Icon } from '@@/Icon';
|
||||
import { LinkButton } from '@@/LinkButton';
|
||||
|
||||
type BrowseStatus = 'snapshot' | 'connected' | 'disconnected';
|
||||
export function EnvironmentBrowseButtons({
|
||||
environment,
|
||||
onClickBrowse,
|
||||
|
@ -21,13 +21,13 @@ export function EnvironmentBrowseButtons({
|
|||
isActive: boolean;
|
||||
}) {
|
||||
const isEdgeAsync = checkEdgeAsync(environment);
|
||||
|
||||
const browseStatus = getStatus(isActive, isEdgeAsync);
|
||||
return (
|
||||
<div className="flex flex-col gap-1 ml-auto [&>*]:flex-1">
|
||||
{isBE && (
|
||||
<LinkButton
|
||||
icon={ClockRewind}
|
||||
disabled={!isEdgeAsync}
|
||||
icon={History}
|
||||
disabled={!isEdgeAsync || browseStatus === 'snapshot'}
|
||||
to="edge.browse.dashboard"
|
||||
params={{
|
||||
environmentId: environment.Id,
|
||||
|
@ -41,7 +41,7 @@ export function EnvironmentBrowseButtons({
|
|||
|
||||
<LinkButton
|
||||
icon={Wifi}
|
||||
disabled={isEdgeAsync}
|
||||
disabled={isEdgeAsync || browseStatus === 'connected'}
|
||||
to={getDashboardRoute(environment)}
|
||||
params={{
|
||||
endpointId: environment.Id,
|
||||
|
@ -53,14 +53,59 @@ export function EnvironmentBrowseButtons({
|
|||
Live connect
|
||||
</LinkButton>
|
||||
|
||||
{!isActive ? (
|
||||
<div className="min-h-[30px] vertical-center justify-center">
|
||||
<Icon icon={WifiOff} />
|
||||
Disconnected
|
||||
</div>
|
||||
) : (
|
||||
<div className="min-h-[30px]" />
|
||||
)}
|
||||
<BrowseStatusTag status={browseStatus} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getStatus(isActive: boolean, isEdgeAsync: boolean) {
|
||||
if (!isActive) {
|
||||
return 'disconnected';
|
||||
}
|
||||
|
||||
if (isEdgeAsync) {
|
||||
return 'snapshot';
|
||||
}
|
||||
|
||||
return 'connected';
|
||||
}
|
||||
|
||||
function BrowseStatusTag({ status }: { status: BrowseStatus }) {
|
||||
switch (status) {
|
||||
case 'snapshot':
|
||||
return <Snapshot />;
|
||||
case 'connected':
|
||||
return <Connected />;
|
||||
case 'disconnected':
|
||||
return <Disconnected />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function Disconnected() {
|
||||
return (
|
||||
<div className="min-h-[30px] vertical-center justify-center opacity-50">
|
||||
<Icon icon={WifiOff} />
|
||||
Disconnected
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Connected() {
|
||||
return (
|
||||
<div className="min-h-[30px] vertical-center gap-2 justify-center text-green-8 bg-green-3 rounded-lg">
|
||||
<div className="rounded-full h-2 w-2 bg-green-8" />
|
||||
Connected
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Snapshot() {
|
||||
return (
|
||||
<div className="min-h-[30px] vertical-center gap-2 justify-center text-warning-7 bg-warning-3 rounded-lg">
|
||||
<div className="rounded-full h-2 w-2 bg-warning-7" />
|
||||
Browsing Snapshot
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,7 +19,15 @@ interface Args {
|
|||
}
|
||||
|
||||
function Template({ environment }: Args) {
|
||||
return <EnvironmentItem environment={environment} onClick={() => {}} />;
|
||||
return (
|
||||
<EnvironmentItem
|
||||
environment={environment}
|
||||
onClickBrowse={() => {}}
|
||||
isActive={false}
|
||||
onSelect={() => {}}
|
||||
isSelected={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const DockerEnvironment: Story<Args> = Template.bind({});
|
||||
|
|
|
@ -44,7 +44,10 @@ function renderComponent(
|
|||
return renderWithQueryClient(
|
||||
<UserContext.Provider value={{ user }}>
|
||||
<EnvironmentItem
|
||||
onClick={() => {}}
|
||||
onSelect={jest.fn()}
|
||||
isSelected={false}
|
||||
isActive={false}
|
||||
onClickBrowse={() => {}}
|
||||
environment={env}
|
||||
groupName={group.Name}
|
||||
/>
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
PlatformType,
|
||||
} from '@/react/portainer/environments/types';
|
||||
import {
|
||||
getDashboardRoute,
|
||||
getPlatformType,
|
||||
isEdgeEnvironment,
|
||||
} from '@/react/portainer/environments/utils';
|
||||
|
@ -19,49 +18,54 @@ import { useTags } from '@/portainer/tags/queries';
|
|||
|
||||
import { EdgeIndicator } from '@@/EdgeIndicator';
|
||||
import { EnvironmentStatusBadge } from '@@/EnvironmentStatusBadge';
|
||||
import { Link } from '@@/Link';
|
||||
import { Checkbox } from '@@/form-components/Checkbox';
|
||||
|
||||
import { EnvironmentIcon } from './EnvironmentIcon';
|
||||
import { EnvironmentStats } from './EnvironmentStats';
|
||||
import { EngineVersion } from './EngineVersion';
|
||||
import { AgentVersionTag } from './AgentVersionTag';
|
||||
import { EnvironmentBrowseButtons } from './EnvironmentBrowseButtons';
|
||||
import { EditButtons } from './EditButtons';
|
||||
|
||||
interface Props {
|
||||
environment: Environment;
|
||||
groupName?: string;
|
||||
onClick(environment: Environment): void;
|
||||
onClickBrowse(): void;
|
||||
onSelect(isSelected: boolean): void;
|
||||
isSelected: boolean;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export function EnvironmentItem({ environment, onClick, groupName }: Props) {
|
||||
export function EnvironmentItem({
|
||||
environment,
|
||||
onClickBrowse,
|
||||
groupName,
|
||||
isActive,
|
||||
isSelected,
|
||||
onSelect,
|
||||
}: Props) {
|
||||
const isEdge = isEdgeEnvironment(environment.Type);
|
||||
|
||||
const snapshotTime = getSnapshotTime(environment);
|
||||
|
||||
const tags = useEnvironmentTagNames(environment.TagIds);
|
||||
const route = getDashboardRoute(environment);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onClick(environment)}
|
||||
className="bg-transparent border-0 !p-0 !m-0"
|
||||
>
|
||||
<Link
|
||||
className="blocklist-item flex no-link overflow-hidden min-h-[100px]"
|
||||
to={route}
|
||||
params={{
|
||||
endpointId: environment.Id,
|
||||
id: environment.Id,
|
||||
}}
|
||||
>
|
||||
<label className="relative">
|
||||
<div className="absolute top-2 left-2">
|
||||
<Checkbox
|
||||
id={`environment-select-${environment.Id}`}
|
||||
checked={isSelected}
|
||||
onChange={() => onSelect(!isSelected)}
|
||||
/>
|
||||
</div>
|
||||
<div className="blocklist-item flex overflow-hidden min-h-[100px]">
|
||||
<div className="ml-2 self-center flex justify-center">
|
||||
<EnvironmentIcon type={environment.Type} />
|
||||
</div>
|
||||
<div className="ml-3 mr-auto flex justify-center gap-3 flex-col items-start">
|
||||
<div className="space-x-3 flex items-center">
|
||||
<span className="font-bold">{environment.Name}</span>
|
||||
|
||||
{isEdge ? (
|
||||
<EdgeIndicator environment={environment} showLastCheckInDate />
|
||||
) : (
|
||||
|
@ -81,16 +85,13 @@ export function EnvironmentItem({ environment, onClick, groupName }: Props) {
|
|||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<EngineVersion environment={environment} />
|
||||
|
||||
{!isEdge && (
|
||||
<span className="text-muted small vertical-center">
|
||||
{stripProtocol(environment.URL)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="small text-muted space-x-2 vertical-center">
|
||||
{groupName && (
|
||||
<span className="font-semibold">
|
||||
|
@ -98,19 +99,16 @@ export function EnvironmentItem({ environment, onClick, groupName }: Props) {
|
|||
<span>{groupName}</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span className="vertical-center">
|
||||
<Tag className="icon icon-sm space-right" aria-hidden="true" />
|
||||
{tags}
|
||||
</span>
|
||||
|
||||
{isEdge && (
|
||||
<>
|
||||
<AgentVersionTag
|
||||
type={environment.Type}
|
||||
version={environment.Agent.Version}
|
||||
/>
|
||||
|
||||
{environment.Edge.AsyncMode && (
|
||||
<span className="vertical-center gap-1">
|
||||
<Globe
|
||||
|
@ -123,13 +121,16 @@ export function EnvironmentItem({ environment, onClick, groupName }: Props) {
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<EnvironmentStats environment={environment} />
|
||||
</div>
|
||||
|
||||
<EnvironmentBrowseButtons
|
||||
environment={environment}
|
||||
onClickBrowse={onClickBrowse}
|
||||
isActive={isActive}
|
||||
/>
|
||||
<EditButtons environment={environment} />
|
||||
</Link>
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ async function renderComponent(
|
|||
|
||||
const queries = renderWithQueryClient(
|
||||
<UserContext.Provider value={{ user }}>
|
||||
<EnvironmentList onClickItem={jest.fn()} onRefresh={jest.fn()} />
|
||||
<EnvironmentList onClickBrowse={jest.fn()} onRefresh={jest.fn()} />
|
||||
</UserContext.Provider>
|
||||
);
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { ReactNode, useEffect, useState } from 'react';
|
|||
import clsx from 'clsx';
|
||||
import { HardDrive, RefreshCcw } from 'lucide-react';
|
||||
import _ from 'lodash';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { usePaginationLimitState } from '@/react/hooks/usePaginationLimitState';
|
||||
import {
|
||||
|
@ -23,6 +24,8 @@ import { useAgentVersionsList } from '@/react/portainer/environments/queries/use
|
|||
import { EnvironmentsQueryParams } from '@/react/portainer/environments/environment.service';
|
||||
import { useUser } from '@/react/hooks/useUser';
|
||||
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
import { environmentStore } from '@/react/hooks/current-environment-store';
|
||||
import { useListSelection } from '@/react/hooks/useListSelection';
|
||||
|
||||
import { TableFooter } from '@@/datatables/TableFooter';
|
||||
import { TableActions, TableContainer, TableTitle } from '@@/datatables';
|
||||
|
@ -43,7 +46,7 @@ import styles from './EnvironmentList.module.css';
|
|||
import { UpdateBadge } from './UpdateBadge';
|
||||
|
||||
interface Props {
|
||||
onClickItem(environment: Environment): void;
|
||||
onClickBrowse(environment: Environment): void;
|
||||
onRefresh(): void;
|
||||
}
|
||||
|
||||
|
@ -67,8 +70,14 @@ enum ConnectionType {
|
|||
|
||||
const storageKey = 'home_endpoints';
|
||||
|
||||
export function EnvironmentList({ onClickItem, onRefresh }: Props) {
|
||||
export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
|
||||
const [selectedItems, handleChangeSelect] = useListSelection<Environment>(
|
||||
[],
|
||||
(a, b) => a.Id === b.Id
|
||||
);
|
||||
|
||||
const { isAdmin } = useUser();
|
||||
const { environmentId: currentEnvironmentId } = useStore(environmentStore);
|
||||
const [platformTypes, setPlatformTypes] = useHomePageFilter<
|
||||
Filter<PlatformType>[]
|
||||
>('platformType', []);
|
||||
|
@ -128,6 +137,7 @@ export function EnvironmentList({ onClickItem, onRefresh }: Props) {
|
|||
status: statusFilter,
|
||||
tagIds: tagFilter?.length ? tagFilter : undefined,
|
||||
groupIds: groupFilter,
|
||||
provisioned: true,
|
||||
edgeDevice: false,
|
||||
tagsPartialMatch: true,
|
||||
agentVersions: agentVersions.map((a) => a.value),
|
||||
|
@ -219,6 +229,7 @@ export function EnvironmentList({ onClickItem, onRefresh }: Props) {
|
|||
sort: sortByFilter,
|
||||
order: sortByDescending ? 'desc' : 'asc',
|
||||
}}
|
||||
selectedItems={selectedItems}
|
||||
/>
|
||||
</div>
|
||||
<div className={clsx(styles.filterSearchbar, 'ml-3')}>
|
||||
|
@ -315,7 +326,12 @@ export function EnvironmentList({ onClickItem, onRefresh }: Props) {
|
|||
groupName={
|
||||
groupsQuery.data?.find((g) => g.Id === env.GroupId)?.Name
|
||||
}
|
||||
onClick={onClickItem}
|
||||
onClickBrowse={() => onClickBrowse(env)}
|
||||
isActive={env.Id === currentEnvironmentId}
|
||||
isSelected={selectedItems.some(
|
||||
(selectedEnv) => selectedEnv.Id === env.Id
|
||||
)}
|
||||
onSelect={(selected) => handleChangeSelect(env, selected)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
|
|
@ -15,22 +15,31 @@ import '@reach/dialog/styles.css';
|
|||
export interface Props {
|
||||
environments: Environment[];
|
||||
envQueryParams: Query;
|
||||
selectedItems: Array<Environment>;
|
||||
}
|
||||
export function KubeconfigButton({ environments, envQueryParams }: Props) {
|
||||
export function KubeconfigButton({
|
||||
environments,
|
||||
envQueryParams,
|
||||
selectedItems,
|
||||
}: Props) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
if (!environments) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isKubeconfigButtonVisible(environments)) {
|
||||
return null;
|
||||
// return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={handleClick} size="medium" className="!ml-3">
|
||||
<Download className="lucide icon-white" aria-hidden="true" /> Kubeconfig
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
size="medium"
|
||||
className="!ml-3"
|
||||
disabled={selectedItems.some(
|
||||
(env) => !isKubernetesEnvironment(env.Type)
|
||||
)}
|
||||
icon={Download}
|
||||
>
|
||||
Kubeconfig
|
||||
</Button>
|
||||
{prompt()}
|
||||
</>
|
||||
|
@ -65,6 +74,11 @@ export function KubeconfigButton({ environments, envQueryParams }: Props) {
|
|||
<KubeconfigPrompt
|
||||
envQueryParams={envQueryParams}
|
||||
onClose={handleClose}
|
||||
selectedItems={
|
||||
selectedItems.length
|
||||
? selectedItems.map((env) => env.Id)
|
||||
: environments.map((env) => env.Id)
|
||||
}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
|
|
@ -1,35 +1,40 @@
|
|||
import { X } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
import { DialogOverlay } from '@reach/dialog';
|
||||
import { DialogContent, DialogOverlay } from '@reach/dialog';
|
||||
|
||||
import { downloadKubeconfigFile } from '@/react/kubernetes/services/kubeconfig.service';
|
||||
import * as notifications from '@/portainer/services/notifications';
|
||||
import { EnvironmentType } from '@/react/portainer/environments/types';
|
||||
import {
|
||||
Environment,
|
||||
EnvironmentType,
|
||||
} from '@/react/portainer/environments/types';
|
||||
import { usePaginationLimitState } from '@/react/hooks/usePaginationLimitState';
|
||||
import { usePublicSettings } from '@/react/portainer/settings/queries';
|
||||
import {
|
||||
Query,
|
||||
useEnvironmentList,
|
||||
} from '@/react/portainer/environments/queries/useEnvironmentList';
|
||||
import { useListSelection } from '@/react/hooks/useListSelection';
|
||||
|
||||
import { PaginationControls } from '@@/PaginationControls';
|
||||
import { Checkbox } from '@@/form-components/Checkbox';
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { useSelection } from './KubeconfigSelection';
|
||||
import styles from './KubeconfigPrompt.module.css';
|
||||
import '@reach/dialog/styles.css';
|
||||
|
||||
export interface KubeconfigPromptProps {
|
||||
envQueryParams: Query;
|
||||
onClose: () => void;
|
||||
selectedItems: Array<Environment['Id']>;
|
||||
}
|
||||
const storageKey = 'home_endpoints';
|
||||
|
||||
export function KubeconfigPrompt({
|
||||
envQueryParams,
|
||||
onClose,
|
||||
selectedItems,
|
||||
}: KubeconfigPromptProps) {
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageLimit, setPageLimit] = usePaginationLimitState(storageKey);
|
||||
|
@ -38,8 +43,10 @@ export function KubeconfigPrompt({
|
|||
select: (settings) => expiryMessage(settings.KubeconfigExpiry),
|
||||
});
|
||||
|
||||
const { selection, toggle: toggleSelection, selectionSize } = useSelection();
|
||||
const { environments, totalCount } = useEnvironmentList({
|
||||
const [selection, toggleSelection] =
|
||||
useListSelection<Environment['Id']>(selectedItems);
|
||||
|
||||
const { environments, totalCount, isLoading } = useEnvironmentList({
|
||||
...envQueryParams,
|
||||
page,
|
||||
pageLimit,
|
||||
|
@ -49,84 +56,96 @@ export function KubeconfigPrompt({
|
|||
EnvironmentType.EdgeAgentOnKubernetes,
|
||||
],
|
||||
});
|
||||
const isAllPageSelected = environments.every((env) => selection[env.Id]);
|
||||
const isAllPageSelected =
|
||||
!isLoading &&
|
||||
environments
|
||||
.filter((env) => env.Status <= 2)
|
||||
.every((env) => selection.includes(env.Id));
|
||||
|
||||
return (
|
||||
<DialogOverlay
|
||||
className={styles.dialog}
|
||||
aria-label="Kubeconfig View"
|
||||
role="dialog"
|
||||
onDismiss={onClose}
|
||||
>
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" onClick={onClose}>
|
||||
<X />
|
||||
</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">{expiryQuery.data}</span>
|
||||
</div>
|
||||
</form>
|
||||
<br />
|
||||
<div className="h-8 flex items-center">
|
||||
<Checkbox
|
||||
id="settings-container-truncate-name"
|
||||
label="Select all (in this page)"
|
||||
checked={isAllPageSelected}
|
||||
onChange={handleSelectAll}
|
||||
/>
|
||||
<DialogContent className="p-0 bg-transparent modal-dialog">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" onClick={onClose}>
|
||||
<X />
|
||||
</button>
|
||||
<h5 className="modal-title">Download kubeconfig file</h5>
|
||||
</div>
|
||||
<div className="datatable">
|
||||
<div className="bootbox-checkbox-list">
|
||||
{environments.map((env) => (
|
||||
<div
|
||||
key={env.Id}
|
||||
className={clsx(
|
||||
styles.checkbox,
|
||||
'h-8 flex items-center pt-1'
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
id={`${env.Id}`}
|
||||
label={`${env.Name} (${env.URL})`}
|
||||
checked={!!selection[env.Id]}
|
||||
onChange={() =>
|
||||
toggleSelection(env.Id, !selection[env.Id])
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="pt-3 flex justify-end w-full">
|
||||
<PaginationControls
|
||||
showAll={totalCount <= 100}
|
||||
page={page}
|
||||
onPageChange={setPage}
|
||||
pageLimit={pageLimit}
|
||||
onPageLimitChange={setPageLimit}
|
||||
totalCount={totalCount}
|
||||
<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">{expiryQuery.data}</span>
|
||||
</div>
|
||||
</form>
|
||||
<br />
|
||||
<div className="h-8 flex items-center">
|
||||
<Checkbox
|
||||
id="settings-container-truncate-name"
|
||||
label="Select all (in this page)"
|
||||
checked={isAllPageSelected}
|
||||
onChange={handleSelectAll}
|
||||
/>
|
||||
</div>
|
||||
<div className="datatable">
|
||||
<div className="bootbox-checkbox-list">
|
||||
{environments
|
||||
.filter((env) => env.Status <= 2)
|
||||
.map((env) => (
|
||||
<div
|
||||
key={env.Id}
|
||||
className={clsx(
|
||||
styles.checkbox,
|
||||
'h-8 flex items-center pt-1'
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
id={`${env.Id}`}
|
||||
label={`${env.Name} (${env.URL})`}
|
||||
checked={selection.includes(env.Id)}
|
||||
onChange={() =>
|
||||
toggleSelection(env.Id, !selection.includes(env.Id))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="pt-3 flex justify-end w-full">
|
||||
<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}
|
||||
disabled={selection.length === 0}
|
||||
>
|
||||
Download File
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<Button onClick={onClose} color="default">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleDownload} disabled={selectionSize < 1}>
|
||||
Download File
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogOverlay>
|
||||
);
|
||||
|
||||
|
@ -139,12 +158,12 @@ export function KubeconfigPrompt({
|
|||
}
|
||||
|
||||
async function confirmKubeconfigSelection() {
|
||||
if (selectionSize === 0) {
|
||||
if (selection.length === 0) {
|
||||
notifications.warning('No environment was selected', '');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await downloadKubeconfigFile(Object.keys(selection).map(Number));
|
||||
await downloadKubeconfigFile(selection);
|
||||
onClose();
|
||||
} catch (e) {
|
||||
notifications.error('Failed downloading kubeconfig file', e as Error);
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { EnvironmentId } from '@/react/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;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,10 +1 @@
|
|||
import { react2angular } from '@/react-tools/react2angular';
|
||||
|
||||
import { EnvironmentList } from './EnvironmentList';
|
||||
|
||||
export { EnvironmentList };
|
||||
|
||||
export const EnvironmentListAngular = react2angular(EnvironmentList, [
|
||||
'onClickItem',
|
||||
'onRefresh',
|
||||
]);
|
||||
export { EnvironmentList } from './EnvironmentList';
|
||||
|
|
|
@ -39,7 +39,7 @@ export function HomeView() {
|
|||
<EdgeLoadingSpinner />
|
||||
) : (
|
||||
<EnvironmentList
|
||||
onClickItem={handleClickItem}
|
||||
onClickBrowse={handleBrowseClick}
|
||||
onRefresh={confirmTriggerSnapshot}
|
||||
/>
|
||||
)}
|
||||
|
@ -64,7 +64,7 @@ export function HomeView() {
|
|||
}
|
||||
}
|
||||
|
||||
function handleClickItem(environment: Environment) {
|
||||
function handleBrowseClick(environment: Environment) {
|
||||
if (isEdgeEnvironment(environment.Type)) {
|
||||
setConnectingToEdgeEndpoint(true);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { useEffect } from 'react';
|
|||
import { X, Slash } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
import angular from 'angular';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import {
|
||||
PlatformType,
|
||||
|
@ -11,9 +12,9 @@ import {
|
|||
} from '@/react/portainer/environments/types';
|
||||
import { getPlatformType } from '@/react/portainer/environments/utils';
|
||||
import { useEnvironment } from '@/react/portainer/environments/queries/useEnvironment';
|
||||
import { useLocalStorage } from '@/react/hooks/useLocalStorage';
|
||||
import { EndpointProviderInterface } from '@/portainer/services/endpointProvider';
|
||||
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
import { EndpointProviderInterface } from '@/portainer/services/endpointProvider';
|
||||
import { environmentStore } from '@/react/hooks/current-environment-store';
|
||||
|
||||
import { Icon } from '@@/Icon';
|
||||
|
||||
|
@ -98,35 +99,34 @@ function Content({ environment, onClear }: ContentProps) {
|
|||
function useCurrentEnvironment() {
|
||||
const { params } = useCurrentStateAndParams();
|
||||
const router = useRouter();
|
||||
const [environmentId, setEnvironmentId] = useLocalStorage<
|
||||
EnvironmentId | undefined
|
||||
>('environmentId', undefined, sessionStorage);
|
||||
|
||||
const envStore = useStore(environmentStore);
|
||||
const { setEnvironmentId } = envStore;
|
||||
useEffect(() => {
|
||||
const environmentId = parseInt(params.endpointId, 10);
|
||||
if (params.endpointId && !Number.isNaN(environmentId)) {
|
||||
setEnvironmentId(environmentId);
|
||||
}
|
||||
}, [params.endpointId, setEnvironmentId]);
|
||||
}, [setEnvironmentId, params.endpointId, params.environmentId]);
|
||||
|
||||
return { query: useEnvironment(environmentId), clearEnvironment };
|
||||
return { query: useEnvironment(envStore.environmentId), clearEnvironment };
|
||||
|
||||
function clearEnvironment() {
|
||||
const $injector = angular.element(document).injector();
|
||||
$injector.invoke(
|
||||
/* @ngInject */ (EndpointProvider: EndpointProviderInterface) => {
|
||||
EndpointProvider.setCurrentEndpoint(null);
|
||||
if (!params.endpointId) {
|
||||
if (!params.endpointId && !params.environmentId) {
|
||||
document.title = 'Portainer';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (params.endpointId) {
|
||||
if (params.endpointId || params.environmentId) {
|
||||
router.stateService.go('portainer.home');
|
||||
}
|
||||
|
||||
setEnvironmentId(undefined);
|
||||
envStore.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue