ui-nyla/src/components/AddConnectionWizard/AddConnectionWizard.tsx
ValueOn AG a6b37ed684 rag
2026-05-12 15:19:07 +02:00

293 lines
11 KiB
TypeScript

/**
* AddConnectionWizard
*
* Streamlined multi-step modal for adding a new connector.
* Steps are connector-type-aware:
* Base: Connector → Consent → Connect
* Microsoft: Connector → Consent → Admin Consent (optional) → Connect
* Infomaniak: Connector → Consent → PAT Input → (done)
*/
import React, { useState } from 'react';
import { Modal } from '../UiComponents/Modal/Modal';
import { FaGoogle, FaMicrosoft, FaTasks, FaCloud, FaCheck, FaArrowRight, FaShieldAlt } from 'react-icons/fa';
import styles from './AddConnectionWizard.module.css';
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export type ConnectorType = 'google' | 'msft' | 'clickup' | 'infomaniak';
type StepId = 'connector' | 'consent' | 'msftAdminConsent' | 'infomaniakPat' | 'connect';
interface WizardState {
currentStep: StepId;
connector: ConnectorType | null;
knowledgeEnabled: boolean;
infomaniakToken: string;
adminConsentDone: boolean;
}
const CONNECTOR_LABELS: Record<ConnectorType, string> = {
google: 'Google',
msft: 'Microsoft 365',
clickup: 'ClickUp',
infomaniak: 'Infomaniak',
};
const CONNECTOR_ICONS: Record<ConnectorType, React.ReactNode> = {
google: <FaGoogle style={{ color: '#4285f4' }} />,
msft: <FaMicrosoft style={{ color: '#00a4ef' }} />,
clickup: <FaTasks style={{ color: '#7b68ee' }} />,
infomaniak: <FaCloud style={{ color: '#0098db' }} />,
};
function _getSteps(connector: ConnectorType | null): StepId[] {
if (connector === 'msft') return ['connector', 'consent', 'msftAdminConsent', 'connect'];
if (connector === 'infomaniak') return ['connector', 'consent', 'infomaniakPat'];
return ['connector', 'consent', 'connect'];
}
// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------
interface AddConnectionWizardProps {
open: boolean;
onClose: () => void;
onConnect: (type: ConnectorType, knowledgeEnabled: boolean) => Promise<void>;
onInfomaniakConnect?: (token: string, knowledgeEnabled: boolean) => Promise<void>;
onMsftAdminConsent?: () => void;
isConnecting?: boolean;
}
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
export const AddConnectionWizard: React.FC<AddConnectionWizardProps> = ({
open,
onClose,
onConnect,
onInfomaniakConnect,
onMsftAdminConsent,
isConnecting = false,
}) => {
const [state, setState] = useState<WizardState>({
currentStep: 'connector',
connector: null,
knowledgeEnabled: false,
infomaniakToken: '',
adminConsentDone: false,
});
const reset = () =>
setState({ currentStep: 'connector', connector: null, knowledgeEnabled: false, infomaniakToken: '', adminConsentDone: false });
const handleClose = () => { reset(); onClose(); };
const steps = _getSteps(state.connector);
const stepIndex = steps.indexOf(state.currentStep);
const goNext = () => {
const nextIdx = stepIndex + 1;
if (nextIdx < steps.length) {
setState(s => ({ ...s, currentStep: steps[nextIdx] }));
}
};
const goBack = () => {
const prevIdx = stepIndex - 1;
if (prevIdx >= 0) {
setState(s => ({ ...s, currentStep: steps[prevIdx] }));
}
};
const selectConnector = (c: ConnectorType) => {
setState(s => ({ ...s, connector: c, currentStep: 'consent' }));
};
const setConsent = (enabled: boolean) => {
setState(s => ({ ...s, knowledgeEnabled: enabled }));
goNext();
};
const handleFinalConnect = async () => {
if (!state.connector) return;
if (state.connector === 'infomaniak' && onInfomaniakConnect) {
await onInfomaniakConnect(state.infomaniakToken, state.knowledgeEnabled);
} else {
await onConnect(state.connector, state.knowledgeEnabled);
}
reset();
onClose();
};
return (
<Modal open={open} onClose={handleClose} title="Verbindung hinzufügen" size="md" closeOnEscape>
{/* Stepper */}
<div className={styles.stepper}>
{steps.map((s, i) => (
<div
key={s}
className={[
styles.stepDot,
stepIndex === i ? styles.stepDotActive : '',
stepIndex > i ? styles.stepDotDone : '',
].join(' ')}
>
{stepIndex > i ? <FaCheck size={10} /> : i + 1}
</div>
))}
</div>
<div className={styles.body}>
{/* ---- Step: Connector ---- */}
{state.currentStep === 'connector' && (
<div className={styles.stepContent}>
<h3 className={styles.stepTitle}>Anbieter wählen</h3>
<p className={styles.stepHint}>Welchen Dienst möchtest du verbinden?</p>
<div className={styles.connectorGrid}>
{(['google', 'msft', 'clickup', 'infomaniak'] as ConnectorType[]).map(type => (
<button
key={type}
type="button"
className={styles.connectorCard}
onClick={() => selectConnector(type)}
>
<span className={styles.connectorIcon}>{CONNECTOR_ICONS[type]}</span>
<span className={styles.connectorLabel}>{CONNECTOR_LABELS[type]}</span>
</button>
))}
</div>
</div>
)}
{/* ---- Step: Consent ---- */}
{state.currentStep === 'consent' && (
<div className={styles.stepContent}>
<h3 className={styles.stepTitle}>Wissensdatenbank</h3>
<p className={styles.stepBody}>
Möchtest du Inhalte aus dieser Verbindung in deine persönliche
Wissensdatenbank aufnehmen, damit die KI beim Antworten auf Informationen
aus {state.connector ? CONNECTOR_LABELS[state.connector] : 'diesem Dienst'} zurückgreifen kann?
</p>
<p className={styles.stepHint}>
Du kannst dies später jederzeit in der UDB pro Datenquelle steuern.
</p>
<div className={styles.consentButtons}>
<button type="button" className={styles.consentButtonYes} onClick={() => setConsent(true)}>
<FaCheck /> Ja, aktivieren
</button>
<button type="button" className={styles.consentButtonNo} onClick={() => setConsent(false)}>
Nein, überspringen
</button>
</div>
<div className={styles.stepNavLeft}>
<button type="button" className={styles.navBack} onClick={goBack}>Zurück</button>
</div>
</div>
)}
{/* ---- Step: MSFT Admin Consent ---- */}
{state.currentStep === 'msftAdminConsent' && (
<div className={styles.stepContent}>
<div style={{ textAlign: 'center', marginBottom: 16 }}>
<FaShieldAlt size={32} style={{ color: '#00a4ef' }} />
</div>
<h3 className={styles.stepTitle}>Organisations-Zustimmung (optional)</h3>
<p className={styles.stepBody}>
Falls du Mandant-Administrator bist, kannst du jetzt für deine ganze Organisation zustimmen.
So müssen andere Benutzer nicht einzeln bestätigen.
</p>
<p className={styles.stepHint}>
Wenn du kein Admin bist oder dies später tun möchtest, überspringe diesen Schritt.
</p>
<div className={styles.consentButtons}>
<button
type="button"
className={styles.consentButtonYes}
onClick={() => { onMsftAdminConsent?.(); setState(s => ({ ...s, adminConsentDone: true })); goNext(); }}
>
<FaShieldAlt /> Admin-Zustimmung erteilen
</button>
<button type="button" className={styles.consentButtonNo} onClick={goNext}>
Überspringen
</button>
</div>
<div className={styles.stepNavLeft}>
<button type="button" className={styles.navBack} onClick={goBack}>Zurück</button>
</div>
</div>
)}
{/* ---- Step: Infomaniak PAT ---- */}
{state.currentStep === 'infomaniakPat' && (
<div className={styles.stepContent}>
<h3 className={styles.stepTitle}>Infomaniak Personal Access Token</h3>
<p className={styles.stepBody}>
Erstelle einen Personal Access Token in deinem Infomaniak-Konto und füge ihn hier ein.
</p>
<input
type="password"
placeholder="pat_..."
value={state.infomaniakToken}
onChange={e => setState(s => ({ ...s, infomaniakToken: e.target.value }))}
className={styles.patInput}
autoFocus
/>
<div className={styles.stepNav}>
<button type="button" className={styles.navBack} onClick={goBack}>Zurück</button>
<button
type="button"
className={styles.navConnect}
onClick={handleFinalConnect}
disabled={isConnecting || !state.infomaniakToken.trim()}
>
{isConnecting ? 'Verbinden…' : 'Verbinden'}
{!isConnecting && <FaArrowRight size={12} />}
</button>
</div>
</div>
)}
{/* ---- Step: Connect ---- */}
{state.currentStep === 'connect' && (
<div className={styles.stepContent}>
<h3 className={styles.stepTitle}>Verbindung herstellen</h3>
<div className={styles.summary}>
<div className={styles.summaryRow}>
<span className={styles.summaryKey}>Anbieter</span>
<span className={styles.summaryVal}>
{state.connector && CONNECTOR_ICONS[state.connector]}&nbsp;
{state.connector ? CONNECTOR_LABELS[state.connector] : '—'}
</span>
</div>
<div className={styles.summaryRow}>
<span className={styles.summaryKey}>Wissensdatenbank</span>
<span className={styles.summaryVal}>
{state.knowledgeEnabled ? '✓ Aktiv' : '✗ Nicht aktiv'}
</span>
</div>
</div>
<div className={styles.stepNav}>
<button type="button" className={styles.navBack} onClick={goBack}>Zurück</button>
<button
type="button"
className={styles.navConnect}
onClick={handleFinalConnect}
disabled={isConnecting}
>
{isConnecting ? 'Verbinden…' : `Mit ${state.connector ? CONNECTOR_LABELS[state.connector] : '…'} verbinden`}
{!isConnecting && <FaArrowRight size={12} />}
</button>
</div>
</div>
)}
</div>
</Modal>
);
};
export default AddConnectionWizard;