Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
BenElferink committed Nov 21, 2024
2 parents 12584d9 + 42ea556 commit cbbf379
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 148 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { GET_DESTINATION_TYPE_DETAILS } from '@/graphql';
import { Body, Container, SideMenuWrapper } from '../styled';
import { Divider, SectionTitle } from '@/reuseable-components';
import { ConnectionNotification } from './connection-notification';
import type { StepProps, DestinationInput, DestinationTypeItem, DestinationDetailsResponse, ConfiguredDestination } from '@/types';
import { useComputePlatform, useConnectDestinationForm, useConnectEnv, useDestinationFormData, useEditDestinationFormHandlers } from '@/hooks';
import { StepProps, DestinationInput, DestinationTypeItem, DestinationDetailsResponse, ConfiguredDestination } from '@/types';

const SIDE_MENU_DATA: StepProps[] = [
{
Expand All @@ -34,14 +34,24 @@ interface ConnectDestinationModalBodyProps {
export function ConnectDestinationModalBody({ destination, onSubmitRef, onFormValidChange }: ConnectDestinationModalBodyProps) {
const [destinationName, setDestinationName] = useState<string>('');
const [showConnectionError, setShowConnectionError] = useState(false);
const [isFormDirty, setIsFormDirty] = useState(false);

const { dynamicFields, exportedSignals, setDynamicFields, setExportedSignals } = useDestinationFormData();

const { connectEnv } = useConnectEnv();
const { refetch } = useComputePlatform();
const { buildFormDynamicFields } = useConnectDestinationForm();

const { handleDynamicFieldChange, handleSignalChange } = useEditDestinationFormHandlers(setExportedSignals, setDynamicFields);
const { handleDynamicFieldChange, handleSignalChange } = useEditDestinationFormHandlers(
(...params) => {
setIsFormDirty(true);
setExportedSignals(...params);
},
(...params) => {
setIsFormDirty(true);
setDynamicFields(...params);
},
);

const addConfiguredDestination = useAppStore(({ addConfiguredDestination }) => addConfiguredDestination);

Expand Down Expand Up @@ -96,6 +106,7 @@ export function ConnectDestinationModalBody({ destination, onSubmitRef, onFormVa
}, [destination]);

function onDynamicFieldChange(name: string, value: any) {
setIsFormDirty(true);
setShowConnectionError(false);
handleDynamicFieldChange(name, value);
}
Expand Down Expand Up @@ -161,26 +172,6 @@ export function ConnectDestinationModalBody({ destination, onSubmitRef, onFormVa
}
}

const actionButton = useMemo(() => {
if (!!destination?.testConnectionSupported) {
return (
<TestConnection
onError={() => {
setShowConnectionError(true);
onFormValidChange(false);
}}
destination={{
name: destinationName,
type: destination?.type || '',
exportedSignals,
fields: processFormFields(),
}}
/>
);
}
return null;
}, [destination, destinationName, exportedSignals, processFormFields, onFormValidChange]);

if (!destination) return null;

return (
Expand All @@ -190,7 +181,28 @@ export function ConnectDestinationModalBody({ destination, onSubmitRef, onFormVa
</SideMenuWrapper>

<Body>
<SectionTitle title='Create connection' description='Connect selected destination with Odigos.' actionButton={actionButton} />
<SectionTitle
title='Create connection'
description='Connect selected destination with Odigos.'
actionButton={
!!destination.testConnectionSupported ? (
<TestConnection
destination={{
name: destinationName,
type: destination.type || '',
exportedSignals,
fields: processFormFields(),
}}
isFormDirty={isFormDirty}
clearFormDirty={() => setIsFormDirty(false)}
onError={() => {
setShowConnectionError(true);
onFormValidChange(false);
}}
/>
) : undefined
}
/>
<ConnectionNotification showConnectionError={showConnectionError} destination={destination} />
<Divider margin='24px 0' />
<FormContainer
Expand Down
Original file line number Diff line number Diff line change
@@ -1,54 +1,54 @@
import React, { useEffect, useMemo } from 'react';
import Image from 'next/image';
import styled from 'styled-components';
import React, { useState } from 'react';
import { DestinationInput } from '@/types';
import { getStatusIcon } from '@/utils';
import { useTestConnection } from '@/hooks';
import type { DestinationInput } from '@/types';
import { Button, FadeLoader, Text } from '@/reuseable-components';

interface TestConnectionProps {
destination: DestinationInput | undefined;
onError?: () => void;
destination: DestinationInput;
isFormDirty: boolean;
clearFormDirty: () => void;
onError: () => void;
}

const ActionButton = styled(Button)<{ $isTestConnectionSuccess?: boolean }>`
const ActionButton = styled(Button)<{ $success?: boolean }>`
display: flex;
align-items: center;
gap: 8px;
background-color: ${({ $isTestConnectionSuccess }) => ($isTestConnectionSuccess ? 'rgba(129, 175, 101, 0.16)' : 'transparent')};
background-color: ${({ $success }) => ($success ? 'rgba(129, 175, 101, 0.16)' : 'transparent')};
`;

const ActionButtonText = styled(Text)<{ $isTestConnectionSuccess?: boolean }>`
const ActionButtonText = styled(Text)<{ $success?: boolean }>`
font-family: ${({ theme }) => theme.font_family.secondary};
font-weight: 500;
text-decoration: underline;
text-transform: uppercase;
font-size: 14px;
line-height: 157.143%;
color: ${({ theme, $isTestConnectionSuccess }) => ($isTestConnectionSuccess ? theme.text.success : theme.colors.white)};
color: ${({ theme, $success }) => ($success ? theme.text.success : theme.colors.white)};
`;

const TestConnection: React.FC<TestConnectionProps> = ({ destination, onError }) => {
const [isTestConnectionSuccess, setIsTestConnectionSuccess] = useState<boolean>(false);
const { testConnection, loading, error } = useTestConnection();
const TestConnection: React.FC<TestConnectionProps> = ({ destination, isFormDirty, clearFormDirty, onError }) => {
const { testConnection, loading, data } = useTestConnection();

const onButtonClick = async () => {
if (!destination) {
return;
}
const disabled = useMemo(() => !destination.fields.find((field) => !!field.value), [destination.fields]);
const success = useMemo(() => data?.testConnectionForDestination.succeeded || false, [data]);

const res = await testConnection(destination);
if (res) {
setIsTestConnectionSuccess(res.succeeded);
!res.succeeded && onError && onError();
useEffect(() => {
if (data) {
clearFormDirty();
if (!success) onError && onError();
}
};
}, [data, success]);

return (
<ActionButton variant={'secondary'} onClick={onButtonClick} $isTestConnectionSuccess={isTestConnectionSuccess}>
{isTestConnectionSuccess && <Image alt='checkmark' src='/icons/common/connection-succeeded.svg' width={16} height={16} />}
{loading && <FadeLoader />}
<ActionButton variant='secondary' disabled={disabled || !isFormDirty} onClick={() => testConnection(destination)} $success={success}>
{loading ? <FadeLoader /> : success ? <Image alt='checkmark' src={getStatusIcon('success')} width={16} height={16} /> : null}

<ActionButtonText size={14} $isTestConnectionSuccess={isTestConnectionSuccess}>
{loading ? 'Checking' : isTestConnectionSuccess ? 'Connection ok' : 'Test Connection'}
<ActionButtonText size={14} $success={success}>
{loading ? 'Checking' : success ? 'Connection OK' : 'Test Connection'}
</ActionButtonText>
</ActionButton>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { slide } from '@/styles';
import theme from '@/styles/theme';
import { useAppStore } from '@/store';
import styled from 'styled-components';
import { useSourceCRUD } from '@/hooks';
import { DeleteWarning } from '@/components';
import { Badge, Button, Divider, Text, Transition } from '@/reuseable-components';
import { useSourceCRUD, useTransition } from '@/hooks';
import { Badge, Button, Divider, Text } from '@/reuseable-components';

const Container = styled.div`
position: fixed;
Expand All @@ -24,6 +24,12 @@ const Container = styled.div`
`;

const MultiSourceControl = () => {
const Transition = useTransition({
container: Container,
animateIn: slide.in['center'],
animateOut: slide.out['center'],
});

const { sources, deleteSources } = useSourceCRUD();
const { configuredSources, setConfiguredSources } = useAppStore((state) => state);
const [isWarnModalOpen, setIsWarnModalOpen] = useState(false);
Expand All @@ -50,7 +56,7 @@ const MultiSourceControl = () => {

return (
<>
<Transition container={Container} enter={!!totalSelected} animateIn={slide.in['center']} animateOut={slide.out['center']}>
<Transition enter={!!totalSelected}>
<Text>Selected sources</Text>
<Badge label={totalSelected} filled />

Expand Down
1 change: 1 addition & 0 deletions frontend/webapp/hooks/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './useContainerSize';
export * from './useOnClickOutside';
export * from './useKeyDown';
export * from './useTimeAgo';
export * from './useTransition';
42 changes: 42 additions & 0 deletions frontend/webapp/hooks/common/useTransition.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, { PropsWithChildren, useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';
import type { IStyledComponentBase, Keyframes, Substitute } from 'styled-components/dist/types';

interface HookProps {
container: IStyledComponentBase<'web', Substitute<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>, {}>> & string;
animateIn: Keyframes;
animateOut?: Keyframes;
duration?: number; // in milliseconds
}

type TransitionProps = PropsWithChildren<{
enter: boolean;
[key: string]: any;
}>;

export const useTransition = ({ container, animateIn, animateOut, duration = 300 }: HookProps) => {
const Animated = styled(container)<{ $isEntering: boolean; $isLeaving: boolean }>`
animation-name: ${({ $isEntering, $isLeaving }) => ($isEntering ? animateIn : $isLeaving ? animateOut : 'none')};
animation-duration: ${duration}ms;
animation-fill-mode: forwards;
`;

const Transition = useCallback(({ children, enter, ...props }: TransitionProps) => {
const [mounted, setMounted] = useState(false);

useEffect(() => {
const t = setTimeout(() => setMounted(enter), duration + 50); // +50ms to ensure the animation is finished
return () => clearTimeout(t);
}, [enter, duration]);

return (
<Animated $isEntering={enter} $isLeaving={!enter && mounted} {...props}>
{children}
</Animated>
);

// do not add dependencies here, it will cause re-renders which we want to avoid
}, []);

return Transition;
};
41 changes: 14 additions & 27 deletions frontend/webapp/hooks/destinations/useTestConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,20 @@ interface TestConnectionResponse {
reason: string;
}

interface UseTestConnectionResult {
testConnection: (
destination: DestinationInput
) => Promise<TestConnectionResponse | undefined>;
loading: boolean;
error?: Error;
}

export const useTestConnection = (): UseTestConnectionResult => {
const [testConnectionMutation, { loading, error }] = useMutation<
{ testConnectionForDestination: TestConnectionResponse },
{ destination: DestinationInput }
>(TEST_CONNECTION_MUTATION);
export const useTestConnection = () => {
const [testConnectionMutation, { loading, error, data }] = useMutation<{ testConnectionForDestination: TestConnectionResponse }, { destination: DestinationInput }>(TEST_CONNECTION_MUTATION, {
onError: (error, clientOptions) => {
console.error('Error testing connection:', error);
},
onCompleted: (data, clientOptions) => {
console.log('Successfully tested connection:', data);
},
});

const testConnection = async (
destination: DestinationInput
): Promise<TestConnectionResponse | undefined> => {
try {
const { data } = await testConnectionMutation({
variables: { destination },
});
return data?.testConnectionForDestination;
} catch (err) {
console.error('Error testing connection:', err);
return undefined;
}
return {
testConnection: (destination: DestinationInput) => testConnectionMutation({ variables: { destination } }),
loading,
error,
data,
};

return { testConnection, loading, error };
};
34 changes: 16 additions & 18 deletions frontend/webapp/reuseable-components/drawer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { useKeyDown } from '@/hooks';
import styled from 'styled-components';
import { slide, Overlay } from '@/styles';
import { useKeyDown, useTransition } from '@/hooks';

interface DrawerProps {
interface Props {
isOpen: boolean;
onClose: () => void;
closeOnEscape?: boolean;
Expand All @@ -13,11 +13,9 @@ interface DrawerProps {
children: React.ReactNode;
}

// Styled-component for drawer container
const DrawerContainer = styled.div<{
$isOpen: DrawerProps['isOpen'];
$position: DrawerProps['position'];
$width: DrawerProps['width'];
const Container = styled.div<{
$position: Props['position'];
$width: Props['width'];
}>`
position: fixed;
top: 0;
Expand All @@ -28,26 +26,26 @@ const DrawerContainer = styled.div<{
background: ${({ theme }) => theme.colors.translucent_bg};
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
overflow-y: auto;
animation: ${({ $isOpen, $position = 'right' }) => ($isOpen ? slide.in[$position] : slide.out[$position])} 0.3s ease;
`;

export const Drawer: React.FC<DrawerProps> = ({ isOpen, onClose, position = 'right', width = '300px', children, closeOnEscape = true }) => {
useKeyDown(
{
key: 'Escape',
active: isOpen && closeOnEscape,
},
() => onClose(),
);
export const Drawer: React.FC<Props> = ({ isOpen, onClose, position = 'right', width = '300px', children, closeOnEscape = true }) => {
useKeyDown({ key: 'Escape', active: isOpen && closeOnEscape }, () => onClose());

const Transition = useTransition({
container: Container,
animateIn: slide.in[position],
animateOut: slide.out[position],
});

if (!isOpen) return null;

return ReactDOM.createPortal(
<>
<Overlay hidden={!isOpen} onClick={onClose} />
<DrawerContainer $isOpen={isOpen} $position={position} $width={width}>

<Transition enter={isOpen} $position={position} $width={width}>
{children}
</DrawerContainer>
</Transition>
</>,
document.body,
);
Expand Down
1 change: 0 additions & 1 deletion frontend/webapp/reuseable-components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,3 @@ export * from './drawer';
export * from './input-table';
export * from './status';
export * from './field-label';
export * from './transition';
Loading

0 comments on commit cbbf379

Please sign in to comment.