From 821eaf4fa318b4a53cfed44a523bd9cddbb86709 Mon Sep 17 00:00:00 2001 From: feeling Date: Fri, 8 May 2026 07:21:31 +0000 Subject: [PATCH] Upload files to "/" init commit --- README.md | 197 ++++++++++++ config.ini.default | 12 + index.html | 777 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 13 + server.js | 269 ++++++++++++++++ 5 files changed, 1268 insertions(+) create mode 100644 README.md create mode 100644 config.ini.default create mode 100644 index.html create mode 100644 package.json create mode 100644 server.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..11ac4cd --- /dev/null +++ b/README.md @@ -0,0 +1,197 @@ +# Jira Ticket Extractor - Mise à jour + +## Nouvelles fonctionnalités + +### 1. Labels des tickets +Les labels sont maintenant automatiquement extraits et affichés dans le tableau avec un design visuel distinctif (badges verts). + +**Affichage :** +- Chaque label apparaît comme un badge individuel +- Labels triés et espacés pour une meilleure lisibilité +- Si aucun label n'est présent, affiche " - " + +**Statistiques :** +- **Labels différents** : Nombre total de labels uniques dans l'extraction +- **Avec labels** : Nombre de tickets qui ont au moins un label + +### 2. Go Live Date (Champ personnalisé) +Le système supporte maintenant l'extraction et l'édition de dates personnalisées comme la "Go Live Date". + +**Configuration :** + +1. **Identifiez l'ID du champ personnalisé dans Jira :** + - Allez dans un ticket Jira + - Cliquez sur le paramètre de la date "Go Live Date" + - Regardez l'URL ou l'inspecteur d'éléments pour trouver l'ID (ex: `customfield_10206`) + - Ou demandez à votre administrateur Jira + +2. **Configurez dans le formulaire d'extraction :** + - Dans le champ "Champs personnalisés", entrez : `customfield_10206:Go Live Date` + - Format : `customfield_ID:NomAffiché` + - Pour plusieurs champs, séparez par des virgules : + ``` + customfield_10206:Go Live Date, customfield_10002:Production Date, customfield_10003:Release Date + ``` + +**Affichage automatique :** +- Les colonnes personnalisées sont automatiquement ajoutées au tableau +- Les champs contenant "date" dans le nom sont automatiquement formatés +- La date est affichée au format français (ex: 15 jan 2025) + +### 3. Édition des dates (Due Date & Go Live Date) ⭐ NOUVEAU +Vous pouvez maintenant modifier les dates directement depuis l'interface et les mettre à jour dans Jira ! + +**Comment modifier les dates :** + +1. **Extrayez d'abord les tickets** avec les champs personnalisés configurés +2. **Les colonnes de date sont maintenant éditables** : + - **Due Date** : toujours éditable + - **Go Live Date** (ou tout champ personnalisé contenant "date") : éditable +3. **Sélectionnez une nouvelle date** dans le sélecteur de date +4. **Cliquez sur le bouton "Update"** pour enregistrer les modifications dans Jira + +**Fonctionnalités de mise à jour :** +- Le bouton "Update" est désactivé tant que vous ne modifiez aucune date +- Dès que vous changez une date, le bouton s'active +- Le bouton affiche un spinner pendant la mise à jour +- Un message de confirmation apparaît après une mise à jour réussie +- Les modifications sont appliquées en temps réel dans Jira + +**Exemple :** +``` +1. Entrez : customfield_10206:Go Live Date +2. Cliquez sur "Lancer l'extraction" +3. Modifiez la Due Date ou Go Live Date dans le tableau +4. Cliquez sur "Update" pour envoyer à Jira +``` + +### 4. Roadmap Draw.io ⭐ NOUVEAU +Générez une roadmap visuelle au format Draw.io (.drawio) importable dans draw.io/diagrams.net ! + +**Contenu de la roadmap :** +- **Ticket Key** : Identifiant du ticket (ex: PROJ-123) +- **Summary** : Titre/description du ticket +- **Status** : Statut avec code couleur +- **Assigné** : Personne responsable du ticket +- **Timeline** : + - **Début** = Due Date + - **Fin** = Go Live Date (customfield_10206) + +**Comment utiliser :** + +1. **Extrayez les tickets** avec au moins la Due Date et idéalement la Go Live Date +2. **Cliquez sur l'onglet "Roadmap"** dans l'interface +3. **Filtrez si nécessaire** : + - "Tous les tickets" : Affiche tout + - "En cours uniquement" : Seulement les tickets en cours (In Progress) + - "Avec dates uniquement" : Seulement les tickets qui ont des dates +4. **Aperçu interactif** : La roadmap est affichée directement dans l'interface +5. **Téléchargez le .drawio** : Cliquez sur "Télécharger le .drawio" +6. **Importez dans Draw.io** : Ouvrez le fichier sur app.diagrams.net + +**Fonctionnalités :** +- Preview interactive dans l'interface +- Code couleur selon le statut : + - 🟢 Vert : Done/Fermé/Résolu + - 🔵 Bleu : En cours/Review + - 🟡 Orange : To Do/Open/Backlog + - 🔴 Rouge : Bloqué + - ⚪ Gris : Autre +- Timeline avec dates sur l'axe horizontal +- Chaque ticket affiche : Key, Summary, Assigné, Statut +- Format XML Draw.io standard, compatible avec draw.io/diagrams.net + +**Exemple d'utilisation :** +``` +1. Configurez : customfield_10206:Go Live Date +2. Cliquez sur "Lancer l'extraction" +3. Onglet "Roadmap" → Vous voyez la timeline +4. Filtrez si nécessaire (ex: "En cours uniquement") +5. Cliquez sur "Roadmap Draw.io" pour télécharger +6. Importez le fichier sur app.diagrams.net +``` + +## Tableau de bord mis à jour + +Le tableau de statistiques affiche maintenant : +- **Tickets** : Nombre total de tickets extraits +- **Statuts** : Nombre de statuts différents +- **Non assignés** : Tickets sans assigné +- **En retard** : Tickets dépassés et non terminés +- **Labels différents** : Nombre de labels uniques +- **Avec labels** : Tickets ayant au moins un label + +## Tableau amélioré + +Nouvelles colonnes : +- **Due Date** : Maintenant éditable avec un sélecteur de date +- **Labels** : Affiche tous les labels du ticket sous forme de badges +- **Go Live Date** (ou autres champs date) : Éditable si configuré +- **Actions** : Bouton "Update" pour envoyer les modifications à Jira +- **Colonnes personnalisées** : S'ajoutent automatiquement selon la configuration + +Nouveaux onglets : +- **Tableau** : Vue tabulaire des tickets avec édition des dates +- **Roadmap** : Visualisation timeline des tickets et export Draw.io +- **JSON Brut** : Données JSON brutes + +## Recherche et filtrage + +La barre de recherche fonctionne maintenant sur : +- Résumé, clé du ticket +- Statut, assigné, priorité +- **Labels** : Vous pouvez chercher un label spécifique +- **Go Live Date** et autres champs personnalisés + +## JSON brut + +Les données JSON incluent maintenant : +- `labels`: Tableau de tous les labels du ticket +- Tous les champs personnalisés configurés + +## Exemple d'utilisation + +### Extraction simple avec labels : +``` +- Username: votre@email.com +- API Token: votre-token +- Clé du projet: PROJ +- Champ personnalisé: (laisser vide pour n'extraire que les labels) +``` + +### Extraction et édition avec Go Live Date : +``` +- Username: votre@email.com +- API Token: votre-token +- Clé du projet: PROJ +- Champ personnalisé: customfield_10206:Go Live Date +``` +Après extraction : +1. Modifiez les dates dans le tableau +2. Cliquez sur "Update" pour synchroniser avec Jira + +### Extraction avec plusieurs champs personnalisés : +``` +- Champ personnalisé: customfield_10206:Go Live Date, customfield_10002:Production Date, customfield_10003:Release Version +``` + +## Démarrage + +```bash +# Installer les dépendances +npm install + +# Démarrer le serveur +npm start +``` + +Puis ouvrez `index.html` dans votre navigateur. + +## Notes importantes + +- Le serveur (server.js) extrait déjà les labels automatiquement - aucune configuration nécessaire +- Les champs personnalisés doivent être explicitement configurés via l'option "customFields" +- Les champs personnalisés de type date sont automatiquement reconnus et formatés +- Pour modifier des dates dans Jira, assurez-vous que votre utilisateur a les droits d'**écriture** sur les tickets +- Les dates sont envoyées à Jira au format ISO (YYYY-MM-DD) +- Le bouton "Update" envoie uniquement les champs modifiés pour optimiser les performances diff --git a/config.ini.default b/config.ini.default new file mode 100644 index 0000000..b5fad9b --- /dev/null +++ b/config.ini.default @@ -0,0 +1,12 @@ +[jira] +url=https://jira.yourdomain.com + +[proxy] +host=proxy.yourdomain.com +port=8080 +; Laisse vide si pas d'identifiant proxy +user= +pass= + +[server] +port=3000 \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..b4a3494 --- /dev/null +++ b/index.html @@ -0,0 +1,777 @@ + + + + + + Jira Ticket Extractor + + + + + +
+
+
+ +
+
+
+
+

Jira Ticket Extractor

+

Extraction via API locale — Proxy transparent

+
+
API : localhost:3000
+
+ +
+
Paramètres d'extraction
+
+ +
Le proxy d'entreprise et l'URL Jira sont gérés par le backend (config.ini). Ici, fournissez uniquement vos identifiants Jira et les filtres du projet.
+
+
+
+ + +
+
+ + + Jira Cloud : Personal Access Token. Jira Server : mot de passe. +
+
+ + +
+
+ + + Format : customfield_ID:NomAffiché, séparés par virgules. Exemple: customfield_10001:Go Live Date +
+
+ + +
+
+
+ + +
+
+ +
+
+
+
+
+ + + +
+
+ + + + +
+
+
+ +
+
+
+
+ Filtrer la roadmap : + + +
+
+
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..d375ab7 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "jira-extractor", + "version": "1.0.0", + "type": "module", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "axios": "^1.7.0", + "express": "^4.21.0", + "https-proxy-agent": "^9.0.0" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..abce94c --- /dev/null +++ b/server.js @@ -0,0 +1,269 @@ +import fs from 'fs'; +import express from 'express'; +import axios from 'axios'; +import https from 'https'; +import { HttpsProxyAgent } from 'https-proxy-agent'; // <-- AJOUT + +// ========================================== +// Lecture du fichier config.ini (inchangé) +// ========================================== +function parseIni(filePath) { + const config = {}; + let section = ''; + const content = fs.readFileSync(filePath, 'utf8'); + content.split('\n').forEach(line => { + line = line.trim(); + if (!line || line.startsWith('#') || line.startsWith(';')) return; + const sectionMatch = line.match(/^\[(.+)\]$/); + if (sectionMatch) { section = sectionMatch[1]; config[section] = {}; return; } + const kv = line.match(/^([^=]+)=(.*)$/); + if (kv && section) config[section][kv[1].trim()] = kv[2].trim(); + }); + return config; +} + +const config = parseIni('./config.ini'); + +if (!config.jira?.url || !config.proxy?.host) { + console.error('ERREUR: config.ini mal rempli.'); + process.exit(1); +} + +// ========================================== +// Configuration du client HTTP (Axios) +// ========================================== + +// Construire l'URL du proxy +const proxyUrl = new URL(`http://${config.proxy.host}:${config.proxy.port}`); +if (config.proxy.user && config.proxy.pass) { + proxyUrl.username = config.proxy.user; + proxyUrl.password = config.proxy.pass; +} + +// Créer l'agent de tunneling CONNECT +const proxyAgent = new HttpsProxyAgent(proxyUrl); + +const jiraClient = axios.create({ + baseURL: config.jira.url, + // IMPORTANT : désactiver le proxy interne d'Axios pour utiliser le nôtre + proxy: false, + httpsAgent: proxyAgent, + httpAgent: proxyAgent, + timeout: 60000 +}); + +// ========================================== +// DEBUG : Intercepteurs de requêtes +// ========================================== +jiraClient.interceptors.request.use(request => { + const authHeader = request.headers['Authorization'] || ''; + // On masque le token pour pas le laisser en clair dans les logs + const masked = authHeader.substring(0, 15) + '...[MASQUE]...' + authHeader.substring(authHeader.length - 5); + console.log(`\n[DEBUG REQUETE] ${request.method?.toUpperCase()} ${request.baseURL}${request.url}`); + console.log(`[DEBUG HEADER] Authorization: ${masked}`); + return request; +}); + +jiraClient.interceptors.response.use( + response => response, + error => { + if (error.response) { + console.error(`\n[DEBUG ERREUR JIRA] Statut : ${error.response.status}`); + console.error(`[DEBUG ERREUR JIRA] Headers:`, JSON.stringify(error.response.headers, null, 2)); + console.error(`[DEBUG ERREUR JIRA] Body :`, JSON.stringify(error.response.data, null, 2)); + } else { + console.error(`\n[DEBUG ERREUR RESEAU]`, error.message); + } + return Promise.reject(error); + } +); + +// ========================================== +// Serveur Express +// ========================================== +const app = express(); +app.use(express.json({ limit: '10mb' })); + +app.use(express.static('.')); // Sert index.html à / + + +// Endpoint d'extraction +app.post('/api/extract', async (req, res) => { + const { username, apiToken, projectKey, jql, customFields } = req.body; + + if (!username || !apiToken || !projectKey) { + return res.status(400).json({ error: "Les champs username, apiToken et projectKey sont requis." }); + } + + const auth = Buffer.from(`${username}:${apiToken}`).toString('base64'); + + // Construction de la liste des champs Jira à demander + const standardFields = [ + 'key','summary','description','status','assignee','reporter','priority', + 'created','updated','duedate','resolution','resolutiondate','issuetype', + 'labels','components','fixVersions','versions','project' + ]; + + // Parser les custom fields (ex: "customfield_10001:Production Date") + const customFieldsMap = []; + if (customFields) { + customFields.split(',').forEach(cf => { + const [id, label] = cf.split(':').map(s => s.trim()); + if (id) customFieldsMap.push({ id, label: label || id }); + }); + } + + const fieldsToRequest = [ + ...standardFields, + ...customFieldsMap.map(cf => cf.id) + ].join(','); + + const queryJql = jql || `project = "${projectKey}" ORDER BY created DESC`; + + let startAt = 0; + let total = 0; + let allTickets = []; + + try { + console.log(`[EXTRACT] Début pour ${projectKey} (JQL: ${queryJql.substring(0, 50)}...)`); + + // Pagination automatique + while (true) { + const response = await jiraClient.get('/rest/api/2/search', { + params: { + jql: queryJql, + startAt: startAt, + maxResults: 100, // Max standard pour Jira + fields: fieldsToRequest + }, + headers: { + //'Authorization': `Basic ${auth}`, + 'Authorization': `Bearer ${apiToken}`, + 'Accept': 'application/json' + } + }); + + if (startAt === 0) { + total = response.data.total; + console.log(`[EXTRACT] ${total} tickets trouvés. Récupération en cours...`); + } + + if (total === 0) break; + + // Mapping des tickets + for (const issue of response.data.issues) { + const f = issue.fields; + const ticket = { + key: issue.key, + url: `${config.jira.url}/browse/${issue.key}`, + summary: f.summary, + description: f.description, + status: f.status?.name, + statusCategory: f.status?.statusCategory?.name, + assignee: f.assignee?.displayName || f.assignee?.name, + reporter: f.reporter?.displayName || f.reporter?.name, + priority: f.priority?.name, + issueType: f.issuetype?.name, + created: f.created, + updated: f.updated, + dueDate: f.duedate, + resolution: f.resolution?.name, + resolutionDate: f.resolutiondate, + labels: f.labels || [], + components: f.components?.map(c => c.name).join(', ') || null, + fixVersions: f.fixVersions?.map(v => v.name).join(', ') || null, + projectName: f.project?.name + }; + + // Ajout des champs personnalisés + customFieldsMap.forEach(cf => { + const val = f[cf.id]; + if (val !== null && val !== undefined) { + // Pour les dates Jira, formater proprement + if (typeof val === 'string' && (val.match(/^\d{4}-\d{2}-\d{2}/) || val.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/))) { + ticket[cf.label] = val; + } else { + ticket[cf.label] = typeof val === 'object' ? (val.name || JSON.stringify(val)) : val; + } + } else { + ticket[cf.label] = null; + } + }); + + allTickets.push(ticket); + } + + startAt += response.data.issues.length; + if (startAt >= total) break; + } + + console.log(`[EXTRACT] Terminé. ${allTickets.length} tickets extraits.`); + res.json({ success: true, total: allTickets.length, tickets: allTickets }); + + } catch (error) { + console.error('[EXTRACT ERROR]', error.response?.data || error.message); + res.status(error.response?.status || 500).json({ + error: error.response?.data?.errorMessages?.[0] || error.message, + details: error.response?.data || null + }); + } +}); + +// Endpoint de mise à jour de ticket +app.put('/api/update', async (req, res) => { + const { username, apiToken, ticketKey, updates } = req.body; + + if (!username || !apiToken || !ticketKey || !updates) { + return res.status(400).json({ error: "Les champs username, apiToken, ticketKey et updates sont requis." }); + } + + // Construire le payload pour l'API Jira + const fields = {}; + const updateFields = {}; + + for (const [key, value] of Object.entries(updates)) { + if (key === 'duedate') { + // Due date est un champ standard + fields[key] = value || null; + } else if (key.startsWith('customfield_')) { + // Custom field + fields[key] = value || null; + } + } + + try { + console.log(`[UPDATE] Mise à jour du ticket ${ticketKey}:`, updates); + + const response = await jiraClient.put(`/rest/api/2/issue/${ticketKey}`, { + fields: fields + }, { + headers: { + 'Authorization': `Bearer ${apiToken}`, + 'Accept': 'application/json' + } + }); + + console.log(`[UPDATE] Ticket ${ticketKey} mis à jour avec succès.`); + res.json({ success: true, ticketKey, updatedFields: updates }); + + } catch (error) { + console.error('[UPDATE ERROR]', error.response?.data || error.message); + res.status(error.response?.status || 500).json({ + error: error.response?.data?.errorMessages?.[0] || error.message, + details: error.response?.data || null + }); + } +}); + +// Démarrage +const PORT = config.server?.port || 3000; +app.listen(PORT, '0.0.0.0', () => { + console.log(''); + console.log('============================================'); + console.log(' Jira Extractor API'); + console.log(` Local : http://localhost:${PORT}`); + console.log(` Jira : ${config.jira.url}`); + console.log(` Proxy : ${config.proxy.host}:${config.proxy.port}`); + console.log('============================================'); + console.log(''); +}); \ No newline at end of file