293 lines
11 KiB
TypeScript
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]}
|
|
{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;
|