diff --git a/new-ui/src/pages/playground/components/PlaygroundTestConnectButton/PlaygroundTestConnectButton.tsx b/new-ui/src/pages/playground/components/PlaygroundTestConnectButton/PlaygroundTestConnectButton.tsx
index 05395f36d..06a0f700d 100644
--- a/new-ui/src/pages/playground/components/PlaygroundTestConnectButton/PlaygroundTestConnectButton.tsx
+++ b/new-ui/src/pages/playground/components/PlaygroundTestConnectButton/PlaygroundTestConnectButton.tsx
@@ -27,6 +27,10 @@ export const PlaygroundTestConnectButton = () => {
+ Connected (loading)
+
+
+
Interactive (active: {active ? 'true' : 'false'})
setActive((v) => !v)} />
diff --git a/new-ui/src/shared/components/LocationCard/components/ConnectButton/ConnectButton.tsx b/new-ui/src/shared/components/LocationCard/components/ConnectButton/ConnectButton.tsx
index 9d5dfbf65..34e0a3edb 100644
--- a/new-ui/src/shared/components/LocationCard/components/ConnectButton/ConnectButton.tsx
+++ b/new-ui/src/shared/components/LocationCard/components/ConnectButton/ConnectButton.tsx
@@ -1,8 +1,12 @@
+import { motion } from 'motion/react';
+import { useEffect, useRef, useState } from 'react';
+import { motionTransitionStandard } from '../../../../consts';
import { TooltipContent } from '../../../../providers/tooltip/TooltipContent';
import { TooltipProvider } from '../../../../providers/tooltip/TooltipContext';
import { TooltipTrigger } from '../../../../providers/tooltip/TooltipTrigger';
import { isPresent } from '../../../../utils/isPresent';
import { Icon, type IconKindValue } from '../../../Icon';
+import { LoaderSpinner } from '../../../LoaderSpinner/LoaderSpinner';
import './style.scss';
import clsx from 'clsx';
@@ -10,6 +14,7 @@ interface Props {
active: boolean;
onClick: () => void;
disabled?: boolean;
+ loading?: boolean;
icon?: IconKindValue | null;
text?: string | null;
tooltip?: string | null;
@@ -22,8 +27,35 @@ export const ConnectButton = ({
text,
tooltip,
disabled = false,
+ loading = false,
}: Props) => {
const label = isPresent(text) ? text : active ? 'Disconnect' : 'Connect VPN';
+ const isLoading = loading && !disabled;
+ const [swapDirection, setSwapDirection] = useState<'to-loading' | 'to-content' | null>(
+ null,
+ );
+ const previousLoadingRef = useRef(isLoading);
+
+ useEffect(() => {
+ if (previousLoadingRef.current !== isLoading) {
+ setSwapDirection(isLoading ? 'to-loading' : 'to-content');
+ previousLoadingRef.current = isLoading;
+ }
+ }, [isLoading]);
+
+ const contentTransition = {
+ ...motionTransitionStandard,
+ delay:
+ !isLoading && swapDirection === 'to-content'
+ ? motionTransitionStandard.duration
+ : 0,
+ };
+
+ const loaderTransition = {
+ ...motionTransitionStandard,
+ delay:
+ isLoading && swapDirection === 'to-loading' ? motionTransitionStandard.duration : 0,
+ };
const button = (
// aria-disabled instead of disabled, a disabled button gets no hover to open the tooltip
@@ -34,14 +66,32 @@ export const ConnectButton = ({
disconnected: !active,
icon: isPresent(icon),
disabled,
+ loading: isLoading,
})}
- aria-disabled={disabled}
+ aria-disabled={disabled || loading}
onClick={() => {
- if (!disabled) onClick();
+ if (!disabled && !loading) onClick();
}}
>
- {isPresent(icon) && }
- {label}
+
+ {isPresent(icon) && }
+ {label}
+
+
+
+
);
diff --git a/new-ui/src/shared/components/LocationCard/components/ConnectButton/style.scss b/new-ui/src/shared/components/LocationCard/components/ConnectButton/style.scss
index 915045edc..a78b1955e 100644
--- a/new-ui/src/shared/components/LocationCard/components/ConnectButton/style.scss
+++ b/new-ui/src/shared/components/LocationCard/components/ConnectButton/style.scss
@@ -3,6 +3,7 @@
--shadow: 0 4px 5px 0 rgb(53 84 179 / 7%);
--color: var(--fg-action);
--border: transparent;
+ --loader-bg: transparent;
background: var(--bg);
border: 1px solid var(--border);
@@ -13,6 +14,7 @@
transition-timing-function: ease-in-out;
cursor: pointer;
display: inline-flex;
+ position: relative;
width: 100%;
align-items: center;
justify-content: center;
@@ -21,19 +23,31 @@
box-sizing: border-box;
padding: 0 var(--spacing-lg);
- &:not(.connected, .disabled):hover {
+ &:not(.connected, .disabled, .loading):hover {
--bg: var(--bg-white-80);
}
+ &:not(.disabled).loading {
+ --bg: var(--c-white-10);
+ --loader-bg: var(--bg-white-10);
+
+ cursor: default;
+ }
+
&.connected {
--bg: transparent;
--color: var(--fg-white-100);
--border: var(--border-default);
- &:not(.disabled):hover {
+ &:not(.disabled, .loading):hover {
--border: var(--border-default);
--bg: var(--bg-white-10);
}
+
+ &:not(.disabled).loading {
+ --bg: transparent;
+ --border: var(--border-disabled);
+ }
}
&.disabled {
@@ -44,14 +58,35 @@
cursor: not-allowed;
}
- .icon {
- --icon-color: var(--color);
+ & > .content {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: inherit;
- margin-right: var(--spacing-sm);
+ & > .icon {
+ --icon-color: var(--color);
+
+ margin-right: var(--spacing-sm);
+ }
+
+ & > p {
+ font: var(--t-body-sm-600);
+ color: inherit;
+ }
}
- p {
- font: var(--t-body-sm-600);
- color: inherit;
+ & > .loader-overlay {
+ display: flex;
+ flex-flow: row;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ border-radius: inherit;
+ background-color: var(--loader-bg);
+ pointer-events: none;
}
}
diff --git a/new-ui/src/shared/components/LocationCard/components/LocationCardConnectButton.tsx b/new-ui/src/shared/components/LocationCard/components/LocationCardConnectButton.tsx
index 05fd85e10..df71004f7 100644
--- a/new-ui/src/shared/components/LocationCard/components/LocationCardConnectButton.tsx
+++ b/new-ui/src/shared/components/LocationCard/components/LocationCardConnectButton.tsx
@@ -56,7 +56,7 @@ export const LocationCardConnectButton = () => {
},
});
- const isBusy = isConnecting || isDisconnecting || isOpeningConfiguration;
+ const isBusy = isConnecting || isOpeningConfiguration;
const handleClick = () => {
if (location.active) {
@@ -89,6 +89,7 @@ export const LocationCardConnectButton = () => {
active={location.active}
onClick={handleClick}
disabled={isBusy || isBlocked}
+ loading={isDisconnecting}
tooltip={isBlocked ? connectBlockedTooltip : null}
/>
);
diff --git a/new-ui/src/shared/components/OverviewLocationCard/OverviewLocationCard.tsx b/new-ui/src/shared/components/OverviewLocationCard/OverviewLocationCard.tsx
index 65e7ff496..4561959ef 100644
--- a/new-ui/src/shared/components/OverviewLocationCard/OverviewLocationCard.tsx
+++ b/new-ui/src/shared/components/OverviewLocationCard/OverviewLocationCard.tsx
@@ -71,7 +71,7 @@ export const OverviewLocationCard = ({ location, instance }: Props) => {
const { mutate: configureMfa, isPending: isOpeningConfiguration } =
useConfigureFactorsScreen();
- const isBusy = isConnecting || isDisconnecting || isOpeningConfiguration;
+ const isBusy = isConnecting || isOpeningConfiguration;
const connectionAbility = useConnectionAbility(location, instance);
@@ -143,6 +143,7 @@ export const OverviewLocationCard = ({ location, instance }: Props) => {
active={location.active}
onClick={handleConnectClick}
disabled={isBusy || isBlocked}
+ loading={isDisconnecting}
tooltip={isBlocked ? connectBlockedTooltip : null}
/>