feat: TFS Sync
This commit is contained in:
@@ -1,20 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { JiraConfig } from '@/lib/types';
|
||||
import { TfsConfig } from '@/services/tfs';
|
||||
import { Header } from '@/components/ui/Header';
|
||||
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||
import { JiraConfigForm } from '@/components/settings/JiraConfigForm';
|
||||
import { JiraSync } from '@/components/jira/JiraSync';
|
||||
import { JiraLogs } from '@/components/jira/JiraLogs';
|
||||
import { JiraSchedulerConfig } from '@/components/jira/JiraSchedulerConfig';
|
||||
import { TfsConfigForm } from '@/components/settings/TfsConfigForm';
|
||||
import { TfsSync } from '@/components/tfs/TfsSync';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface IntegrationsSettingsPageClientProps {
|
||||
initialJiraConfig: JiraConfig;
|
||||
initialTfsConfig: TfsConfig;
|
||||
}
|
||||
|
||||
export function IntegrationsSettingsPageClient({
|
||||
initialJiraConfig
|
||||
initialJiraConfig,
|
||||
initialTfsConfig
|
||||
}: IntegrationsSettingsPageClientProps) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--background)]">
|
||||
@@ -44,122 +49,125 @@ export function IntegrationsSettingsPageClient({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Layout en 2 colonnes pour optimiser l'espace */}
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||||
|
||||
{/* Colonne principale: Configuration Jira */}
|
||||
<div className="xl:col-span-2 space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2 className="text-xl font-semibold flex items-center gap-2">
|
||||
<span className="text-blue-600">🏢</span>
|
||||
Jira Cloud
|
||||
</h2>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
Synchronisation automatique des tickets Jira vers TowerControl
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<JiraConfigForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Futures intégrations */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2 className="text-xl font-semibold">Autres intégrations</h2>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
Intégrations prévues pour les prochaines versions
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-lg">📧</span>
|
||||
<h3 className="font-medium">Slack/Teams</h3>
|
||||
</div>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
Notifications et commandes via chat
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-lg">🐙</span>
|
||||
<h3 className="font-medium">GitHub/GitLab</h3>
|
||||
</div>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
Synchronisation des issues et PR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-lg">📊</span>
|
||||
<h3 className="font-medium">Calendriers</h3>
|
||||
</div>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
Google Calendar, Outlook, etc.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-lg">⏱️</span>
|
||||
<h3 className="font-medium">Time tracking</h3>
|
||||
</div>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
Toggl, RescueTime, etc.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* Section Jira */}
|
||||
<div className="mb-12">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-mono font-bold text-[var(--foreground)] mb-2 flex items-center gap-2">
|
||||
<span className="text-blue-600">🏢</span>
|
||||
Jira Cloud
|
||||
</h2>
|
||||
<p className="text-[var(--muted-foreground)]">
|
||||
Synchronisation automatique des tickets Jira vers TowerControl
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Colonne latérale: Actions et Logs Jira */}
|
||||
<div className="space-y-4">
|
||||
{initialJiraConfig?.enabled && (
|
||||
<>
|
||||
{/* Dashboard Analytics */}
|
||||
{initialJiraConfig.projectKey && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h3 className="text-sm font-semibold">📊 Analytics d'équipe</h3>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<p className="text-xs text-[var(--muted-foreground)]">
|
||||
Surveillance du projet {initialJiraConfig.projectKey}
|
||||
</p>
|
||||
<Link
|
||||
href="/jira-dashboard"
|
||||
className="inline-flex items-center justify-center w-full px-3 py-2 text-sm font-medium bg-[var(--primary)] text-[var(--primary-foreground)] rounded-lg hover:bg-[var(--primary)]/90 transition-colors"
|
||||
>
|
||||
Voir le Dashboard
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<JiraSchedulerConfig />
|
||||
<JiraSync />
|
||||
<JiraLogs />
|
||||
</>
|
||||
)}
|
||||
|
||||
{!initialJiraConfig?.enabled && (
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||||
{/* Configuration Jira */}
|
||||
<div className="xl:col-span-2">
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-center py-6">
|
||||
<span className="text-4xl mb-4 block">🔧</span>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
Configurez Jira pour accéder aux outils de synchronisation
|
||||
</p>
|
||||
</div>
|
||||
<CardContent>
|
||||
<JiraConfigForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
<div className="mt-6">
|
||||
<JiraLogs />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions Jira */}
|
||||
<div className="space-y-4">
|
||||
{initialJiraConfig?.enabled ? (
|
||||
<>
|
||||
{/* Dashboard Analytics */}
|
||||
{initialJiraConfig.projectKey && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h3 className="text-sm font-semibold">📊 Analytics d'équipe</h3>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<p className="text-xs text-[var(--muted-foreground)]">
|
||||
Surveillance du projet {initialJiraConfig.projectKey}
|
||||
</p>
|
||||
<Link
|
||||
href="/jira-dashboard"
|
||||
className="inline-flex items-center justify-center w-full px-3 py-2 text-sm font-medium bg-[var(--primary)] text-[var(--primary-foreground)] rounded-lg hover:bg-[var(--primary)]/90 transition-colors"
|
||||
>
|
||||
Voir le Dashboard
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<JiraSchedulerConfig />
|
||||
<JiraSync />
|
||||
</>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-center py-6">
|
||||
<span className="text-4xl mb-4 block">🏢</span>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
Configurez Jira pour accéder aux outils de synchronisation
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Diviseur entre les sections */}
|
||||
<div className="relative my-12">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-[var(--border)]"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm uppercase">
|
||||
<span className="bg-[var(--background)] px-6 text-[var(--muted-foreground)] font-medium tracking-wider">
|
||||
• • •
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section TFS */}
|
||||
<div>
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-mono font-bold text-[var(--foreground)] mb-2 flex items-center gap-2">
|
||||
<span className="text-blue-500">🔧</span>
|
||||
Azure DevOps / TFS
|
||||
</h2>
|
||||
<p className="text-[var(--muted-foreground)]">
|
||||
Synchronisation des Pull Requests depuis Azure DevOps vers TowerControl
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||||
{/* Configuration TFS */}
|
||||
<div className="xl:col-span-2">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<TfsConfigForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Actions TFS */}
|
||||
<div className="space-y-4">
|
||||
{initialTfsConfig?.enabled ? (
|
||||
<TfsSync />
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-center py-6">
|
||||
<span className="text-4xl mb-4 block">🔧</span>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
Configurez Azure DevOps pour accéder aux outils de synchronisation
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ export function JiraConfigForm() {
|
||||
const { config, isLoading: configLoading, saveConfig, deleteConfig } = useJiraConfig();
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
enabled: false,
|
||||
baseUrl: '',
|
||||
email: '',
|
||||
apiToken: '',
|
||||
@@ -26,6 +27,7 @@ export function JiraConfigForm() {
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
setFormData({
|
||||
enabled: config.enabled || false,
|
||||
baseUrl: config.baseUrl || '',
|
||||
email: config.email || '',
|
||||
apiToken: config.apiToken || '',
|
||||
@@ -87,6 +89,7 @@ export function JiraConfigForm() {
|
||||
|
||||
if (result.success) {
|
||||
setFormData({
|
||||
enabled: false,
|
||||
baseUrl: '',
|
||||
email: '',
|
||||
apiToken: '',
|
||||
@@ -228,6 +231,27 @@ export function JiraConfigForm() {
|
||||
{/* Formulaire de configuration */}
|
||||
{showForm && (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Toggle d'activation */}
|
||||
<div className="flex items-center justify-between p-4 bg-[var(--muted)] rounded-lg">
|
||||
<div>
|
||||
<h4 className="font-medium">Activer l'intégration Jira</h4>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
Synchroniser les tickets Jira vers TowerControl
|
||||
</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.enabled}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, enabled: e.target.checked }))}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{formData.enabled && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
URL de base Jira Cloud
|
||||
@@ -362,6 +386,8 @@ export function JiraConfigForm() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
@@ -399,7 +425,7 @@ export function JiraConfigForm() {
|
||||
<li>Copiez le token généré</li>
|
||||
</ul>
|
||||
<p className="mt-3 text-xs">
|
||||
<strong>Note:</strong> Ces variables doivent être configurées dans l'environnement du serveur (JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN)
|
||||
<strong>Note:</strong> Les tickets Jira seront synchronisés comme tâches dans TowerControl pour faciliter le suivi.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
638
src/components/settings/TfsConfigForm.tsx
Normal file
638
src/components/settings/TfsConfigForm.tsx
Normal file
@@ -0,0 +1,638 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useTransition } from 'react';
|
||||
import { TfsConfig } from '@/services/tfs';
|
||||
import { getTfsConfig, saveTfsConfig, deleteAllTfsTasks } from '@/actions/tfs';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
|
||||
export function TfsConfigForm() {
|
||||
const [config, setConfig] = useState<TfsConfig>({
|
||||
enabled: false,
|
||||
organizationUrl: '',
|
||||
projectName: '',
|
||||
personalAccessToken: '',
|
||||
repositories: [],
|
||||
ignoredRepositories: [],
|
||||
});
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [message, setMessage] = useState<{
|
||||
type: 'success' | 'error';
|
||||
text: string;
|
||||
} | null>(null);
|
||||
const [testingConnection, setTestingConnection] = useState(false);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [deletingTasks, setDeletingTasks] = useState(false);
|
||||
|
||||
// Charger la configuration existante
|
||||
useEffect(() => {
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const result = await getTfsConfig();
|
||||
if (result.success) {
|
||||
setConfig(result.data);
|
||||
// Afficher le formulaire par défaut si TFS n'est pas configuré
|
||||
const isConfigured =
|
||||
result.data?.enabled &&
|
||||
result.data?.organizationUrl &&
|
||||
result.data?.personalAccessToken;
|
||||
if (!isConfigured) {
|
||||
setShowForm(true);
|
||||
}
|
||||
} else {
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: result.error || 'Erreur lors du chargement de la configuration',
|
||||
});
|
||||
setShowForm(true); // Afficher le formulaire en cas d'erreur
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur chargement config TFS:', error);
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: 'Erreur lors du chargement de la configuration',
|
||||
});
|
||||
setShowForm(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveConfig = () => {
|
||||
startTransition(async () => {
|
||||
setMessage(null);
|
||||
const result = await saveTfsConfig(config);
|
||||
|
||||
if (result.success) {
|
||||
setMessage({
|
||||
type: 'success',
|
||||
text: result.message || 'Configuration sauvegardée',
|
||||
});
|
||||
// Masquer le formulaire après une sauvegarde réussie
|
||||
setShowForm(false);
|
||||
} else {
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: result.error || 'Erreur lors de la sauvegarde',
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!confirm('Êtes-vous sûr de vouloir supprimer la configuration TFS ?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
setMessage(null);
|
||||
// Réinitialiser la config
|
||||
const resetConfig = {
|
||||
enabled: false,
|
||||
organizationUrl: '',
|
||||
projectName: '',
|
||||
personalAccessToken: '',
|
||||
repositories: [],
|
||||
ignoredRepositories: [],
|
||||
};
|
||||
|
||||
const result = await saveTfsConfig(resetConfig);
|
||||
|
||||
if (result.success) {
|
||||
setConfig(resetConfig);
|
||||
setMessage({ type: 'success', text: 'Configuration TFS supprimée' });
|
||||
setShowForm(true); // Afficher le formulaire pour reconfigurer
|
||||
} else {
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: result.error || 'Erreur lors de la suppression',
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const testConnection = async () => {
|
||||
try {
|
||||
setTestingConnection(true);
|
||||
setMessage(null);
|
||||
|
||||
// Sauvegarder d'abord la config
|
||||
const saveResult = await saveTfsConfig(config);
|
||||
if (!saveResult.success) {
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: saveResult.error || 'Erreur lors de la sauvegarde',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Attendre un peu que la configuration soit prise en compte
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
// Tester la connexion avec la route dédiée
|
||||
const response = await fetch('/api/tfs/test', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Test TFS - Réponse:', { status: response.status, result });
|
||||
|
||||
if (response.ok && result.connected) {
|
||||
setMessage({
|
||||
type: 'success',
|
||||
text: `Connexion Azure DevOps réussie ! ${result.message || ''}`,
|
||||
});
|
||||
} else {
|
||||
const errorMessage =
|
||||
result.error || result.details || 'Erreur de connexion inconnue';
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: `Connexion échouée: ${errorMessage}`,
|
||||
});
|
||||
console.error('Test TFS échoué:', result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur test connexion TFS:', error);
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: `Erreur réseau: ${error instanceof Error ? error.message : 'Erreur inconnue'}`,
|
||||
});
|
||||
} finally {
|
||||
setTestingConnection(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAllTasks = async () => {
|
||||
const confirmation = confirm(
|
||||
'Êtes-vous sûr de vouloir supprimer TOUTES les tâches TFS de la base locale ?\n\n' +
|
||||
'Cette action est irréversible et supprimera définitivement toutes les tâches ' +
|
||||
'synchronisées depuis Azure DevOps/TFS.\n\n' +
|
||||
'Cliquez sur OK pour confirmer la suppression.'
|
||||
);
|
||||
|
||||
if (!confirmation) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setDeletingTasks(true);
|
||||
setMessage(null);
|
||||
|
||||
const result = await deleteAllTfsTasks();
|
||||
|
||||
if (result.success) {
|
||||
setMessage({
|
||||
type: 'success',
|
||||
text:
|
||||
result.message ||
|
||||
'Toutes les tâches TFS ont été supprimées avec succès',
|
||||
});
|
||||
} else {
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: result.error || 'Erreur lors de la suppression des tâches TFS',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur suppression tâches TFS:', error);
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: `Erreur réseau: ${error instanceof Error ? error.message : 'Erreur inconnue'}`,
|
||||
});
|
||||
} finally {
|
||||
setDeletingTasks(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateConfig = (
|
||||
field: keyof TfsConfig,
|
||||
value: string | boolean | string[]
|
||||
) => {
|
||||
setConfig((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const updateArrayField = (
|
||||
field: 'repositories' | 'ignoredRepositories',
|
||||
value: string
|
||||
) => {
|
||||
const array = value
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item);
|
||||
updateConfig(field, array);
|
||||
};
|
||||
|
||||
const isTfsConfigured =
|
||||
config?.enabled && config?.organizationUrl && config?.personalAccessToken;
|
||||
const isLoadingState = isLoading || isPending || deletingTasks;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="text-sm text-[var(--muted-foreground)]">
|
||||
Chargement...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Statut actuel */}
|
||||
<div className="flex items-center justify-between p-4 bg-[var(--card)] rounded border">
|
||||
<div>
|
||||
<h3 className="font-medium">Statut de l'intégration</h3>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
{isTfsConfigured
|
||||
? 'Azure DevOps est configuré et prêt à être utilisé'
|
||||
: "Azure DevOps n'est pas configuré"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant={isTfsConfigured ? 'success' : 'danger'}>
|
||||
{isTfsConfigured ? '✓ Configuré' : '✗ Non configuré'}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setShowForm(!showForm)}
|
||||
>
|
||||
{showForm ? 'Masquer' : isTfsConfigured ? 'Modifier' : 'Configurer'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isTfsConfigured && (
|
||||
<div className="p-4 bg-[var(--card)] rounded border">
|
||||
<h3 className="font-medium mb-2">Configuration actuelle</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div>
|
||||
<span className="text-[var(--muted-foreground)]">
|
||||
URL d'organisation:
|
||||
</span>{' '}
|
||||
<code className="bg-[var(--background)] px-2 py-1 rounded text-xs">
|
||||
{config?.organizationUrl || 'Non définie'}
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[var(--muted-foreground)]">Projet:</span>{' '}
|
||||
<code className="bg-[var(--background)] px-2 py-1 rounded text-xs">
|
||||
{config?.projectName || "Toute l'organisation"}
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[var(--muted-foreground)]">Token PAT:</span>{' '}
|
||||
<code className="bg-[var(--background)] px-2 py-1 rounded text-xs">
|
||||
{config?.personalAccessToken ? '••••••••' : 'Non défini'}
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[var(--muted-foreground)]">
|
||||
Repositories surveillés:
|
||||
</span>{' '}
|
||||
{config?.repositories && config.repositories.length > 0 ? (
|
||||
<div className="mt-1 space-x-1">
|
||||
{config.repositories.map((repo) => (
|
||||
<code
|
||||
key={repo}
|
||||
className="bg-[var(--background)] px-2 py-1 rounded text-xs"
|
||||
>
|
||||
{repo}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs">Tous les repositories</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[var(--muted-foreground)]">
|
||||
Repositories ignorés:
|
||||
</span>{' '}
|
||||
{config?.ignoredRepositories &&
|
||||
config.ignoredRepositories.length > 0 ? (
|
||||
<div className="mt-1 space-x-1">
|
||||
{config.ignoredRepositories.map((repo) => (
|
||||
<code
|
||||
key={repo}
|
||||
className="bg-[var(--background)] px-2 py-1 rounded text-xs"
|
||||
>
|
||||
{repo}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs">Aucun</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions de gestion des données TFS */}
|
||||
{isTfsConfigured && (
|
||||
<div className="p-4 bg-[var(--card)] rounded border border-orange-200 dark:border-orange-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-medium text-orange-800 dark:text-orange-200">
|
||||
⚠️ Gestion des données
|
||||
</h3>
|
||||
<p className="text-sm text-orange-600 dark:text-orange-300">
|
||||
Supprimez toutes les tâches TFS synchronisées de la base locale
|
||||
</p>
|
||||
<p className="text-xs text-orange-500 dark:text-orange-400 mt-1">
|
||||
<strong>Attention:</strong> Cette action est irréversible et
|
||||
supprimera définitivement toutes les tâches importées depuis
|
||||
Azure DevOps.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="danger"
|
||||
onClick={handleDeleteAllTasks}
|
||||
disabled={deletingTasks}
|
||||
className="px-6"
|
||||
>
|
||||
{deletingTasks
|
||||
? 'Suppression...'
|
||||
: '🗑️ Supprimer toutes les tâches TFS'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Formulaire de configuration */}
|
||||
{showForm && (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSaveConfig();
|
||||
}}
|
||||
className="space-y-4"
|
||||
>
|
||||
{/* Toggle d'activation */}
|
||||
<div className="flex items-center justify-between p-4 bg-[var(--muted)] rounded-lg">
|
||||
<div>
|
||||
<h4 className="font-medium">Activer l'intégration TFS</h4>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
Synchroniser les Pull Requests depuis Azure DevOps
|
||||
</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enabled}
|
||||
onChange={(e) => updateConfig('enabled', e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{config.enabled && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
URL de l'organisation Azure DevOps
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
value={config.organizationUrl || ''}
|
||||
onChange={(e) =>
|
||||
updateConfig('organizationUrl', e.target.value)
|
||||
}
|
||||
placeholder="https://dev.azure.com/votre-organisation"
|
||||
className="w-full px-3 py-2 border border-[var(--border)] rounded bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||
L'URL de base de votre organisation Azure DevOps (ex:
|
||||
https://dev.azure.com/monentreprise)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Nom du projet (optionnel)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.projectName || ''}
|
||||
onChange={(e) => updateConfig('projectName', e.target.value)}
|
||||
placeholder="MonProjet (laisser vide pour toute l'organisation)"
|
||||
className="w-full px-3 py-2 border border-[var(--border)] rounded bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent"
|
||||
/>
|
||||
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||
Nom du projet spécifique ou laisser vide pour synchroniser les
|
||||
PRs de toute l'organisation
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Personal Access Token (PAT)
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={config.personalAccessToken || ''}
|
||||
onChange={(e) =>
|
||||
updateConfig('personalAccessToken', e.target.value)
|
||||
}
|
||||
placeholder="Votre token d'accès personnel"
|
||||
className="w-full px-3 py-2 border border-[var(--border)] rounded bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||
Créez un PAT depuis{' '}
|
||||
<a
|
||||
href="https://dev.azure.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[var(--primary)] hover:underline"
|
||||
>
|
||||
Azure DevOps
|
||||
</a>{' '}
|
||||
avec les permissions Code (read) et Pull Request (read)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Repositories à surveiller (optionnel)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.repositories?.join(', ') || ''}
|
||||
onChange={(e) =>
|
||||
updateArrayField('repositories', e.target.value)
|
||||
}
|
||||
placeholder="repo1, repo2, repo3"
|
||||
className="w-full px-3 py-2 border border-[var(--border)] rounded bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent"
|
||||
/>
|
||||
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||
Liste séparée par des virgules. Laisser vide pour surveiller
|
||||
tous les repositories.
|
||||
</p>
|
||||
{config.repositories && config.repositories.length > 0 && (
|
||||
<div className="mt-2 space-x-1">
|
||||
<span className="text-xs text-[var(--muted-foreground)]">
|
||||
Repositories surveillés:
|
||||
</span>
|
||||
{config.repositories.map((repo) => (
|
||||
<code
|
||||
key={repo}
|
||||
className="bg-[var(--muted)] text-[var(--muted-foreground)] px-2 py-1 rounded text-xs"
|
||||
>
|
||||
{repo}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Repositories à ignorer (optionnel)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.ignoredRepositories?.join(', ') || ''}
|
||||
onChange={(e) =>
|
||||
updateArrayField('ignoredRepositories', e.target.value)
|
||||
}
|
||||
placeholder="test-repo, demo-repo"
|
||||
className="w-full px-3 py-2 border border-[var(--border)] rounded bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent"
|
||||
/>
|
||||
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||
Repositories à exclure de la synchronisation, séparés par des
|
||||
virgules (ex: test-repo, demo-repo).
|
||||
</p>
|
||||
{config.ignoredRepositories &&
|
||||
config.ignoredRepositories.length > 0 && (
|
||||
<div className="mt-2 space-x-1">
|
||||
<span className="text-xs text-[var(--muted-foreground)]">
|
||||
Repositories ignorés:
|
||||
</span>
|
||||
{config.ignoredRepositories.map((repo) => (
|
||||
<code
|
||||
key={repo}
|
||||
className="bg-[var(--muted)] text-[var(--muted-foreground)] px-2 py-1 rounded text-xs"
|
||||
>
|
||||
{repo}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button type="submit" disabled={isLoadingState} className="flex-1">
|
||||
{isLoadingState
|
||||
? 'Sauvegarde...'
|
||||
: 'Sauvegarder la configuration'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={testConnection}
|
||||
disabled={
|
||||
testingConnection ||
|
||||
!config.organizationUrl ||
|
||||
!config.personalAccessToken
|
||||
}
|
||||
className="px-6"
|
||||
>
|
||||
{testingConnection ? 'Test...' : 'Tester'}
|
||||
</Button>
|
||||
|
||||
{isTfsConfigured && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handleDelete}
|
||||
disabled={isLoadingState}
|
||||
className="px-6"
|
||||
>
|
||||
Supprimer
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
|
||||
<h3 className="font-medium mb-2">
|
||||
💡 Instructions de configuration
|
||||
</h3>
|
||||
<div className="text-sm text-[var(--muted-foreground)] space-y-2">
|
||||
<p>
|
||||
<strong>1. URL d'organisation:</strong> Votre domaine Azure
|
||||
DevOps (ex: https://dev.azure.com/monentreprise)
|
||||
</p>
|
||||
<p>
|
||||
<strong>2. Nom du projet (optionnel):</strong> Spécifiez un
|
||||
projet pour limiter la synchronisation, ou laissez vide pour
|
||||
toute l'organisation
|
||||
</p>
|
||||
<p>
|
||||
<strong>3. Personal Access Token:</strong> Créez un PAT depuis
|
||||
Azure DevOps :
|
||||
</p>
|
||||
<ul className="ml-4 space-y-1 list-disc">
|
||||
<li>
|
||||
Allez sur{' '}
|
||||
<a
|
||||
href="https://dev.azure.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[var(--primary)] hover:underline"
|
||||
>
|
||||
dev.azure.com
|
||||
</a>
|
||||
</li>
|
||||
<li>Cliquez sur votre profil » Personal access tokens</li>
|
||||
<li>Cliquez sur "New Token"</li>
|
||||
<li>
|
||||
Sélectionnez les scopes: Code (read) et Pull Request (read)
|
||||
</li>
|
||||
<li>Copiez le token généré</li>
|
||||
</ul>
|
||||
<p className="mt-3 text-xs">
|
||||
<strong>🎯 Synchronisation intelligente:</strong> TowerControl
|
||||
récupère automatiquement toutes les Pull Requests vous
|
||||
concernant (créées par vous ou où vous êtes reviewer) dans
|
||||
l'organisation ou le projet configuré.
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
<strong>Note:</strong> Les PRs seront synchronisées comme tâches
|
||||
pour un suivi centralisé de vos activités.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{message && (
|
||||
<div
|
||||
className={`p-4 rounded border ${
|
||||
message.type === 'success'
|
||||
? 'bg-green-50 border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-200'
|
||||
: 'bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200'
|
||||
}`}
|
||||
>
|
||||
{message.text}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user