diff --git a/app/docker/__module.js b/app/docker/__module.js index 74e9a80dd..de09c8719 100644 --- a/app/docker/__module.js +++ b/app/docker/__module.js @@ -1,5 +1,7 @@ import angular from 'angular'; +import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; + import { EnvironmentStatus } from '@/react/portainer/environments/types'; import { reactModule } from './react'; @@ -16,14 +18,17 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([ abstract: true, onEnter: /* @ngInject */ function onEnter(endpoint, $async, $state, EndpointService, Notifications, StateManager, SystemService) { return $async(async () => { - if (![1, 2, 4].includes(endpoint.Type)) { + const dockerTypes = [PortainerEndpointTypes.DockerEnvironment, PortainerEndpointTypes.AgentOnDockerEnvironment, PortainerEndpointTypes.EdgeAgentOnDockerEnvironment]; + + if (!dockerTypes.includes(endpoint.Type)) { $state.go('portainer.home'); return; } + try { const status = await checkEndpointStatus(endpoint); - if (endpoint.Type !== 4) { + if (endpoint.Type !== PortainerEndpointTypes.EdgeAgentOnDockerEnvironment) { await updateEndpointStatus(endpoint, status); } endpoint.Status = status; @@ -34,16 +39,22 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([ await StateManager.updateEndpointState(endpoint); } catch (e) { - Notifications.error('Failed loading environment', e); - $state.go('portainer.home', {}, { reload: true }); + let params = {}; + + if (endpoint.Type == PortainerEndpointTypes.EdgeAgentOnDockerEnvironment) { + params = { redirect: true, environmentId: endpoint.Id, environmentName: endpoint.Name, route: 'docker.dashboard' }; + } else { + Notifications.error('Failed loading environment', e); + } + $state.go('portainer.home', params, { reload: true, inherit: false }); } async function checkEndpointStatus(endpoint) { try { await SystemService.ping(endpoint.Id); - return 1; + return EnvironmentStatus.Up; } catch (e) { - return 2; + return EnvironmentStatus.Down; } } diff --git a/app/kubernetes/__module.js b/app/kubernetes/__module.js index f3edc768d..5ebe986fb 100644 --- a/app/kubernetes/__module.js +++ b/app/kubernetes/__module.js @@ -1,3 +1,7 @@ +import { EnvironmentStatus } from '@/react/portainer/environments/types'; + +import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; + import registriesModule from './registries'; import customTemplateModule from './custom-templates'; import { reactModule } from './react'; @@ -16,31 +20,43 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo onEnter: /* @ngInject */ function onEnter($async, $state, endpoint, KubernetesHealthService, KubernetesNamespaceService, Notifications, StateManager) { return $async(async () => { - if (![5, 6, 7].includes(endpoint.Type)) { + const kubeTypes = [ + PortainerEndpointTypes.KubernetesLocalEnvironment, + PortainerEndpointTypes.AgentOnKubernetesEnvironment, + PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment, + ]; + + if (!kubeTypes.includes(endpoint.Type)) { $state.go('portainer.home'); return; } try { - if (endpoint.Type === 7) { + if (endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) { //edge try { await KubernetesHealthService.ping(endpoint.Id); - endpoint.Status = 1; + endpoint.Status = EnvironmentStatus.Up; } catch (e) { - endpoint.Status = 2; + endpoint.Status = EnvironmentStatus.Down; } } await StateManager.updateEndpointState(endpoint); - if (endpoint.Type === 7 && endpoint.Status === 2) { + if (endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment && endpoint.Status === EnvironmentStatus.Down) { throw new Error('Unable to contact Edge agent, please ensure that the agent is properly running on the remote environment.'); } await KubernetesNamespaceService.get(); } catch (e) { - Notifications.error('Failed loading environment', e); - $state.go('portainer.home', {}, { reload: true }); + let params = {}; + + if (endpoint.Type == PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) { + params = { redirect: true, environmentId: endpoint.Id, environmentName: endpoint.Name, route: 'kubernetes.dashboard' }; + } else { + Notifications.error('Failed loading environment', e); + } + $state.go('portainer.home', params, { reload: true, inherit: false }); } }); }, diff --git a/app/portainer/__module.js b/app/portainer/__module.js index 98d6f21af..b43401dd5 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -300,7 +300,7 @@ angular var home = { name: 'portainer.home', - url: '/home', + url: '/home?redirect&environmentId&environmentName&route', views: { 'content@': { component: 'homeView', diff --git a/app/react/components/modals/Dialog.tsx b/app/react/components/modals/Dialog.tsx index c46cdc7c1..39c5e033e 100644 --- a/app/react/components/modals/Dialog.tsx +++ b/app/react/components/modals/Dialog.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from 'react'; +import { ReactNode, useEffect, useState, useRef } from 'react'; import { Button } from '@@/buttons'; @@ -26,6 +26,37 @@ export function Dialog({ }: Props) { const ariaLabel = requireString(title) || requireString(message) || 'Dialog'; + const [count, setCount] = useState(0); + const countRef = useRef(count); + countRef.current = count; + + useEffect(() => { + let retFn; + + // only countdown the first button with non-zero timeout + for (let i = 0; i < buttons.length; i++) { + const button = buttons[i]; + if (button.timeout) { + setCount(button.timeout as number); + + const intervalID = setInterval(() => { + const count = countRef.current; + + setCount(count - 1); + if (count === 1) { + onSubmit(button.value); + } + }, 1000); + + retFn = () => clearInterval(intervalID); + + break; + } + } + + return retFn; + }, [buttons, onSubmit]); + return ( onSubmit()} aria-label={ariaLabel}> {title && } @@ -39,7 +70,7 @@ export function Dialog({ key={index} size="medium" > - {button.label} + {button.label} {button.timeout && count ? `(${count})` : null} ))} diff --git a/app/react/components/modals/types.ts b/app/react/components/modals/types.ts index 6720d8622..b56a3a352 100644 --- a/app/react/components/modals/types.ts +++ b/app/react/components/modals/types.ts @@ -7,6 +7,7 @@ export interface ButtonOptions { className?: string; color?: ComponentProps['color']; value?: TValue; + timeout?: number; } export interface ButtonsOptions { diff --git a/app/react/components/modals/utils.ts b/app/react/components/modals/utils.ts index 62de850e0..ec0f7d5c7 100644 --- a/app/react/components/modals/utils.ts +++ b/app/react/components/modals/utils.ts @@ -6,9 +6,10 @@ import { ButtonOptions } from './types'; export function buildConfirmButton( label = 'Confirm', - color: ComponentProps['color'] = 'primary' + color: ComponentProps['color'] = 'primary', + timeout = 0 ): ButtonOptions { - return { label, color, value: true }; + return { label, color, value: true, timeout }; } export function buildCancelButton(label = 'Cancel'): ButtonOptions { diff --git a/app/react/portainer/HomeView/EdgeLoadingSpinner.module.css b/app/react/portainer/HomeView/EdgeLoadingSpinner.module.css index 82dc7f30c..d6ec1181c 100644 --- a/app/react/portainer/HomeView/EdgeLoadingSpinner.module.css +++ b/app/react/portainer/HomeView/EdgeLoadingSpinner.module.css @@ -3,7 +3,7 @@ height: 100%; text-align: center; display: flex; - flex-direction: column; + flex-direction: row; align-items: center; justify-content: center; } diff --git a/app/react/portainer/HomeView/HomeView.tsx b/app/react/portainer/HomeView/HomeView.tsx index 8961ecbf3..6e5e83ad6 100644 --- a/app/react/portainer/HomeView/HomeView.tsx +++ b/app/react/portainer/HomeView/HomeView.tsx @@ -1,5 +1,5 @@ -import { useRouter } from '@uirouter/react'; -import { useState } from 'react'; +import { useCurrentStateAndParams, useRouter } from '@uirouter/react'; +import { useEffect, useState } from 'react'; import { Environment } from '@/react/portainer/environments/types'; import { snapshotEndpoints } from '@/react/portainer/environments/environment.service'; @@ -9,6 +9,7 @@ import * as notifications from '@/portainer/services/notifications'; import { confirm } from '@@/modals/confirm'; import { PageHeader } from '@@/PageHeader'; import { ModalType } from '@@/modals'; +import { buildConfirmButton } from '@@/modals/utils'; import { EnvironmentList } from './EnvironmentList'; import { EdgeLoadingSpinner } from './EdgeLoadingSpinner'; @@ -17,10 +18,37 @@ import { LicenseNodePanel } from './LicenseNodePanel'; import { BackupFailedPanel } from './BackupFailedPanel'; export function HomeView() { - const [connectingToEdgeEndpoint, setConnectingToEdgeEndpoint] = - useState(false); + const { params } = useCurrentStateAndParams(); + const [connectingToEdgeEndpoint, setConnectingToEdgeEndpoint] = useState( + !!params.redirect + ); const router = useRouter(); + + useEffect(() => { + async function redirect() { + const options = { + title: `Failed connecting to ${params.environmentName}`, + message: `There was an issue connecting to edge agent via tunnel. Click 'Retry' below to retry now, or wait 10 seconds to automatically retry.`, + confirmButton: buildConfirmButton('Retry', 'primary', 10), + modalType: ModalType.Destructive, + }; + + if (await confirm(options)) { + setConnectingToEdgeEndpoint(true); + router.stateService.go(params.route, { + endpointId: params.environmentId, + }); + } else { + router.stateService.go('portainer.home', {}, { inherit: false }); + } + } + + if (params.redirect) { + redirect(); + } + }, [params, setConnectingToEdgeEndpoint, router]); + return ( <>