diff --git a/api/src/admin.ts b/api/src/admin.ts index 0689e31c..ec4b003f 100644 --- a/api/src/admin.ts +++ b/api/src/admin.ts @@ -1,7 +1,8 @@ import { resolve } from 'node:path' import { readFile } from 'node:fs/promises' import { Router } from 'express' -import { session } from '@data-fair/lib-express/index.js' +import { reqOrigin, session } from '@data-fair/lib-express' +import getApiDoc from './utils/api-docs.ts' const router = Router() export default router @@ -17,3 +18,8 @@ try { info = JSON.parse(await readFile(resolve(import.meta.dirname, '../../BUILD router.get('/info', (req, res) => { res.send(info) }) + +// Get the full API documentation of the service +router.get('/api-docs.json', async (req, res) => { + res.json(getApiDoc(reqOrigin(req))) +}) diff --git a/api/src/routers/processings.ts b/api/src/routers/processings.ts index bddd8953..b00244ac 100644 --- a/api/src/routers/processings.ts +++ b/api/src/routers/processings.ts @@ -10,7 +10,7 @@ import path from 'path' import resolvePath from 'resolve-path' import { nanoid } from 'nanoid' -import { session } from '@data-fair/lib-express/index.js' +import { reqOrigin, session } from '@data-fair/lib-express/index.js' import { httpError } from '@data-fair/lib-utils/http-errors.js' import { createNext } from '@data-fair/processings-shared/runs.ts' import { applyProcessing, deleteProcessing } from '../utils/runs.ts' @@ -18,6 +18,7 @@ import mongo from '#mongo' import config from '#config' import locks from '#locks' import { resolvedSchema as processingSchema } from '#types/processing/index.ts' +import getApiDoc from '../utils/api-docs.ts' import findUtils from '../utils/find.ts' import permissions from '../utils/permissions.ts' @@ -356,3 +357,13 @@ router.post('/:id/_trigger', async (req, res) => { if (!processing.active) return res.status(409).send('Le traitement n\'est pas actif') res.send(await createNext(mongo.db, locks, processing, true, req.query.delay ? Number(req.query.delay) : 0)) }) + +// Get the API documentation of a processing +router.get('/:id/api-docs.json', permissions.isSuperAdmin, async (req, res) => { + const processing = await mongo.processings.findOne({ _id: req.params.id }) + if (!processing) return res.status(404).send() + const pluginPath = path.join(pluginsDir, processing.plugin, 'plugin.json') + if (!await fs.pathExists(pluginPath)) return res.status(404).send('Plugin not found') + const plugin = await fs.readJson(pluginPath) + res.json(getApiDoc(reqOrigin(req), { processing, plugin })) +}) diff --git a/api/src/utils/api-docs.ts b/api/src/utils/api-docs.ts new file mode 100644 index 00000000..932f0670 --- /dev/null +++ b/api/src/utils/api-docs.ts @@ -0,0 +1,923 @@ +import jsonSchema from '@data-fair/lib-utils/json-schema.js' +import { type Processing, resolvedSchema as ProcessingSchema } from '#types/processing/index.ts' +import { type Plugin, resolvedSchema as PluginSchema } from '#types/plugin/index.ts' +import { readFileSync } from 'node:fs' +import path from 'path' + +const packageJson = JSON.parse(readFileSync(path.resolve(import.meta.dirname, '../../package.json'), 'utf-8')) + +// CTRL + K CTRL + 4 to fold operations levels + +export default (origin: string, options?: { processing?: Processing, plugin?: Plugin }) => { + if (options?.plugin?.processingConfigSchema) ProcessingSchema.properties.config = options?.plugin?.processingConfigSchema + + const doc: Record = { + openapi: '3.1.1', + info: { + title: options?.processing?.title ? `API du traitement : ${options.processing.title}` : 'API Traitements de données', + description: `Cette documentation interactive à destination des développeurs permet d'utiliser l'API du ${options?.processing?.title ? `traitement ${options.processing.title}` : 'service de traitements periodiques'}.`, + version: packageJson.version, + termsOfService: 'https://koumoul.com/pages/conditions-generales-dutilisation' + }, + servers: [{ + url: `${origin}/processings/api/v1${options?.processing?._id ? `/processings/${options.processing?._id}` : ''}`, + description: `Instance DataFair - ${new URL(origin).hostname}` + }], + paths: { + [options?.processing?._id ? '/api-docs.json' : '/admin/api-docs.json']: { + get: { + summary: 'Obtenir la documentation OpenAPI', + description: 'Accéder à cette documentation au format OpenAPI v3.', + operationId: 'getApiDoc', + responses: { + 200: { + description: 'La documentation de l\'API', + content: { + 'application/json': { + schema: { + type: 'object' + } + } + } + } + } + } + }, + + '/processings': { + get: { + summary: 'Obtenir la liste des traitements', + description: 'Accéder à la liste des traitements.', + operationId: 'getProcessings', + tags: ['Traitements'], + parameters: [ + { + name: 'select', + in: 'query', + description: 'Champs à inclure dans la réponse.', + schema: { + type: 'string', + title: 'Sélection des champs' + } + }, + { + name: 'q', + in: 'query', + description: 'Recherche textuelle.', + schema: { + type: 'string', + title: 'Recherche textuelle' + } + }, + { + name: 'size', + in: 'query', + description: 'Nombre maximum d\'éléments à retourner.', + schema: { + type: 'integer', + title: 'Taille de la page', + minimum: 1, + default: 10 + } + }, + { + name: 'page', + in: 'query', + description: 'Numéro de la page à retourner.', + schema: { + type: 'integer', + title: 'Numéro de la page' + } + }, + { + name: 'skip', + in: 'query', + description: 'Nombre d\'éléments à ignorer.', + schema: { + type: 'integer', + title: 'Éléments à ignorer' + } + }, + { + name: 'sort', + in: 'query', + description: 'Ordre de tri des résultats.', + schema: { + type: 'string', + title: 'Ordre de tri', + example: 'field1,-field2' + } + }, + { + name: 'statuses', + in: 'query', + description: 'Filtrer par statut de traitement.', + schema: { + type: 'string', + title: 'Statuts des traitements', + enum: [ + 'none', + 'error', + 'scheduled', + 'killed', + 'finished' + ] + } + }, + { + name: 'plugins', + in: 'query', + description: 'Filtrer par plugin utilisé.', + schema: { + type: 'string', + title: 'Plugins' + } + }, + { + name: 'owner', + in: 'query', + description: 'Filtrer par propriétaire des traitements.', + schema: { + type: 'string', + title: 'Propriétaire' + } + } + ], + responses: { + 200: { + description: 'La liste des traitements', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + count: { + type: 'integer', + description: 'Le nombre de traitements trouvés.' + }, + facets: { + type: 'object', + properties: { + plugins: { + type: 'object', + additionalProperties: { + type: 'integer', + description: 'Le nombre de traitements par plugins' + } + }, + statuses: { + type: 'object', + additionalProperties: { + type: 'integer', + description: 'Le nombre de traitements par statut' + } + }, + } + }, + results: { + type: 'array', + items: ProcessingSchema + } + } + } + } + } + } + } + }, + post: { + summary: 'Créer un traitement', + description: 'Créer un traitement.', + operationId: 'postProcessing', + tags: ['Traitements'], + requestBody: { + description: 'Le traitement à créer', + required: true, + content: { + 'application/json': { + schema: jsonSchema(ProcessingSchema) + .pickProperties(['owner', 'plugin', 'title']) + .removeFromRequired(['scheduling', '_id']) + .removeId() + .appendTitle(' post') + .schema, + example: { + plugin: '@data-fair/processing-export-file', + owner: { + type: 'organization', + id: 'koumoul', + name: 'Koumoul', + department: 'dep1', + departmentName: 'Department 1' + }, + title: 'Export File' + } + } + } + }, + responses: { + 200: { + description: 'Le traitement a été créé avec succès.', + content: { + 'application/json': { + schema: ProcessingSchema + } + } + }, + 400: { + description: 'Body de la requête invalide.' + }, + 403: { + description: 'L\'utilisateur n\'a pas le droit de créer ce traitement.' + } + } + } + }, + '/processings/{id}': { + parameters: [ + { + name: 'id', + in: 'path', + required: true, + description: 'L\'identifiant du traitement.', + schema: { + type: 'string', + title: 'Identifiant du traitement' + } + } + ], + get: { + summary: 'Lire les informations d\'un traitement', + description: 'Accéder aux données d\'un traitement.', + operationId: 'getProcessing', + tags: ['Traitements'], + responses: { + 200: { + description: 'Le traitement trouvé.', + content: { + 'application/json': { + schema: ProcessingSchema + } + } + }, + 403: { + description: 'L\'utilisateur n\'a pas le droit de voir ce traitement.' + }, + 404: { + description: 'Traitement non trouvé.' + } + } + }, + patch: { + summary: 'Mettre à jour un traitement', + description: 'Mettre à jour un traitement.', + operationId: 'patchProcessing', + tags: ['Traitements'], + requestBody: { + description: 'Le traitement à mettre à jour', + required: true, + content: { + 'application/json': { + schema: ProcessingSchema + } + } + }, + responses: { + 200: { + description: 'Le traitement a été mis à jour avec succès.', + content: { + 'application/json': { + schema: ProcessingSchema + } + } + }, + 400: { + description: 'Body de la requête invalide.' + }, + 403: { + description: 'L\'utilisateur n\'a pas le droit de mettre à jour ce traitement ou de faire cette modification sur ce traitement' + }, + 404: { + description: 'Traitement non trouvé.' + } + } + }, + delete: { + summary: 'Supprimer un traitement', + description: 'Supprimer un traitement.', + operationId: 'deleteProcessing', + tags: ['Traitements'], + responses: { + 204: { + description: 'Le traitement a été supprimé avec succès.' + }, + 403: { + description: 'L\'utilisateur n\'a pas le droit de supprimer ce traitement.' + }, + 404: { + description: 'Traitement non trouvé.' + } + } + } + }, + '/processings/{id}/webhook-key': { + parameters: [ + { + name: 'id', + in: 'path', + required: true, + description: 'L\'identifiant du traitement.', + schema: { + type: 'string', + title: 'Identifiant du traitement' + } + } + ], + get: { + summary: 'Lire la clé de webhook d\'un traitement', + description: 'Lire la clé de webhook d\'un traitement.', + operationId: 'getProcessingWebhookKey', + tags: ['Traitements'], + responses: { + 200: { + description: 'La clé de webhook du traitement.', + content: { + 'application/json': { + schema: { + type: 'string' + } + } + } + }, + 403: { + description: 'L\'utilisateur n\'a pas le droit de voir ce traitement.' + }, + 404: { + description: 'Traitement non trouvé.' + } + } + }, + delete: { + summary: 'Recréer la clé de webhook d\'un traitement', + description: 'Recréer la clé de webhook d\'un traitement.', + operationId: 'deleteProcessingWebhookKey', + tags: ['Traitements'], + responses: { + 200: { + description: 'La clé de webhook du traitement a été recréée avec succès.', + content: { + 'application/json': { + schema: { + type: 'string', + description: 'La nouvelle clé de webhook du traitement.' + } + } + } + }, + 403: { + description: 'L\'utilisateur n\'a pas le droit de voir ce traitement.' + }, + 404: { + description: 'Traitement non trouvé.' + } + } + } + }, + '/processings/{id}/_trigger': { + parameters: [ + { + name: 'id', + in: 'path', + required: true, + description: 'L\'identifiant du traitement.', + schema: { + type: 'string', + title: 'Identifiant du traitement' + } + } + ], + post: { + summary: 'Déclencher un traitement manuellement', + description: 'Déclencher un traitement manuellement.', + operationId: 'postProcessingTrigger', + tags: ['Traitements'], + parameters: [ + { + name: 'key', + in: 'query', + description: 'La clé de déclenchement du traitement.', + schema: { + type: 'string', + title: 'Clé de déclenchement' + } + } + ], + responses: { + 200: { + description: 'Le traitement a été déclenché avec succès.' + }, + 403: { + description: 'L\'utilisateur n\'a pas le droit de déclencher ce traitement, ou la clé de déclenchement n\'est pas valide.' + }, + 404: { + description: 'Traitement non trouvé.' + } + } + } + }, + + // Simplified routes when processing?._id is set + '/': { + get: { + summary: 'Lire les informations de ce traitement', + description: 'Accéder aux données de ce traitement.', + operationId: 'getProcessing', + responses: { + 200: { + description: 'Le traitement trouvé.', + content: { + 'application/json': { + schema: ProcessingSchema + } + } + }, + 403: { + description: 'L\'utilisateur n\'a pas le droit de voir ce traitement.' + }, + 404: { + description: 'Traitement non trouvé.' + } + } + }, + patch: { + summary: 'Mettre à jour ce traitement', + description: 'Mettre à jour ce traitement.', + operationId: 'patchProcessing', + requestBody: { + description: 'Le traitement à mettre à jour', + required: true, + content: { + 'application/json': { + schema: jsonSchema(ProcessingSchema) + .pickProperties(['title', 'active', 'config', 'owner', 'scheduling', 'permissions']) + .removeRequired() + .removeId() + .appendTitle(' patch') + .schema, + example: options?.processing + ? { + title: options.processing.title, + active: options.processing.active, + config: options.processing.config, + owner: options.processing.owner, + scheduling: options.processing.scheduling, + permissions: options.processing.permissions + } + : {} + } + } + }, + responses: { + 200: { + description: 'Le traitement a été mis à jour avec succès.', + content: { + 'application/json': { + schema: ProcessingSchema + } + } + }, + 400: { + description: 'Body de la requête invalide.' + }, + 403: { + description: 'L\'utilisateur n\'a pas le droit de mettre à jour ce traitement ou de faire cette modification sur ce traitement' + }, + 404: { + description: 'Traitement non trouvé.' + } + } + }, + delete: { + summary: 'Supprimer ce traitement', + description: 'Supprimer ce traitement.', + operationId: 'deleteProcessing', + responses: { + 204: { + description: 'Le traitement a été supprimé avec succès.' + }, + 403: { + description: 'L\'utilisateur n\'a pas le droit de supprimer ce traitement.' + }, + 404: { + description: 'Traitement non trouvé.' + } + } + } + }, + '/webhook-key': { + get: { + summary: 'Lire la clé de webhook de ce traitement', + description: 'Lire la clé de webhook de ce traitement.', + operationId: 'getProcessingWebhookKey', + responses: { + 200: { + description: 'La clé de webhook du traitement.', + content: { + 'application/json': { + schema: { + type: 'string' + } + } + } + }, + 403: { + description: 'L\'utilisateur n\'a pas le droit de voir ce traitement.' + }, + 404: { + description: 'Traitement non trouvé.' + } + } + }, + delete: { + summary: 'Recréer la clé de webhook de ce traitement', + description: 'Recréer la clé de webhook de ce traitement.', + operationId: 'deleteProcessingWebhookKey', + responses: { + 200: { + description: 'La clé de webhook du traitement a été recréée avec succès.', + content: { + 'application/json': { + schema: { + type: 'string', + description: 'La nouvelle clé de webhook du traitement.' + } + } + } + }, + 403: { + description: 'L\'utilisateur n\'a pas le droit de voir ce traitement.' + }, + 404: { + description: 'Traitement non trouvé.' + } + } + } + }, + '/_trigger': { + post: { + summary: 'Déclencher ce traitement manuellement', + description: 'Déclencher ce traitement manuellement.', + operationId: 'postProcessingTrigger', + parameters: [ + { + name: 'key', + in: 'query', + description: 'La clé de déclenchement du traitement.', + schema: { + type: 'string', + title: 'Clé de déclenchement' + } + } + ], + responses: { + 200: { + description: 'Le traitement a été déclenché avec succès.' + }, + 403: { + description: 'L\'utilisateur n\'a pas le droit de déclencher ce traitement, ou la clé de déclenchement n\'est pas valide.' + }, + 404: { + description: 'Traitement non trouvé.' + } + } + } + }, + + '/plugins-registry': { + get: { + summary: 'Obtenir la liste des plugins', + description: 'Accéder à la liste des plugins disponibles sur NPM.', + operationId: 'getPluginsRegistry', + tags: ['Plugins'], + parameters: [ + { + name: 'q', + in: 'query', + description: 'Le nom du plugin à rechercher.', + schema: { + type: 'string', + title: 'Nom du plugin à rechercher' + } + }, + { + name: 'showAll', + in: 'query', + description: 'Afficher tous les plugins disponibles (même ceux en version bêta). La requête peut prendre plus de temps.', + schema: { + type: 'boolean', + title: 'Afficher tous les plugins disponibles' + } + } + ], + responses: { + 200: { + description: 'La liste des plugins disponibles', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + count: { + type: 'integer', + description: 'Le nombre de plugins trouvés.' + }, + results: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Le nom du plugin.' + }, + description: { + type: 'string', + description: 'La description du plugin.' + }, + version: { + type: 'string', + description: 'La version du plugin.' + }, + distTag: { + type: 'string', + description: 'Le tag de distribution du plugin.', + example: 'latest' + } + } + } + } + } + } + } + } + }, + 400: { + description: 'Requête invalide, le paramètre "q" est mal formaté.' + }, + 429: { + description: 'Erreur renvoyée par l\'API NPM, trop de requêtes envoyées.' + }, + 500: { + description: 'Erreur interne (Il se peut que le service NPM soit indisponible).' + } + } + } + }, + '/plugins': { + get: { + summary: 'Obtenir la liste des plugins installés', + description: 'Accéder à la liste des plugins installés.', + operationId: 'getPlugins', + tags: ['Plugins'], + parameters: [ + { + name: 'privateAccess', + in: 'query', + description: 'Filtre par accès', + schema: { + type: 'string', + title: 'Filtre par accès', + example: 'type:id' + } + } + ], + responses: { + 200: { + description: 'La liste des plugins installés', + content: { + 'application/json': { + schema: { + count: { + type: 'integer', + description: 'Le nombre de plugins trouvés.' + }, + facets: { + type: 'object', + properties: { + usages: { + type: 'object', + additionalProperties: { + type: 'integer', + description: 'Le nombre de fois que le plugin est utilisé' + } + } + } + }, + results: { + type: 'array', + items: PluginSchema + } + } + } + } + }, + 400: { + description: 'Le paramètre "privateAccess" est manquant et l\'utilisateur n\'est pas super administrateur.' + }, + 403: { + description: 'Le privateAccess ne correspond pas avec l\'utilisateur authentifié.' + } + } + }, + post: { + summary: 'Installer un plugin', + description: 'Installer/Mettre à jour un plugin, voir même baisser en version un plugin. Cette requête prends beaucoup de temps à s\'executer, c\'est le temps que le plugin s\'installe sur le serveur.', + operationId: 'postPlugin', + tags: ['Plugins'], + requestBody: { + description: 'Le plugin à installer', + required: true, + content: { + 'application/json': { + schema: jsonSchema(PluginSchema) + .pickProperties(['distTag', 'name', 'version', 'description']) + .removeFromRequired(['description']) + .removeId() + .appendTitle(' post') + .schema, + example: { + name: '@data-fair/processing-export-file', + description: 'Export File', + version: '0.6.2', + distTag: 'latest' + } + } + } + }, + responses: { + 200: { + description: 'Le plugin a été installé avec succès.', + content: { + 'application/json': { + schema: PluginSchema + } + } + } + } + } + }, + '/plugins/{id}': { + parameters: [ + { + name: 'id', + in: 'path', + required: true, + description: 'L\'identifiant du plugin.', + schema: { + type: 'string', + title: 'Identifiant du plugin' + } + } + ], + get: { + summary: 'Lire les informations d\'un plugin', + description: 'Accéder aux données d\'un plugin.', + operationId: 'getPlugin', + tags: ['Plugins'], + responses: { + 200: { + description: 'Le plugin trouvé.', + content: { + 'application/json': { + schema: PluginSchema + } + } + }, + 404: { + description: 'Plugin non trouvé.' + } + } + }, + delete: { + summary: 'Supprimer un plugin', + description: 'Supprimer un plugin.', + operationId: 'deletePlugin', + tags: ['Plugins'], + responses: { + 204: { + description: 'Le plugin a été supprimé avec succès.' + } + } + }, + }, + '/plugins/{id}/config': { + parameters: [ + { + name: 'id', + in: 'path', + required: true, + description: 'L\'identifiant du plugin.', + schema: { + type: 'string', + title: 'Identifiant du plugin' + } + } + ], + put: { + summary: 'Mettre à jour la configuration d\'un plugin', + description: 'Mettre à jour la configuration d\'un plugin.', + operationId: 'putPluginConfig', + tags: ['Plugins'], + requestBody: { + description: 'La configuration du plugin', + required: true, + content: { + 'application/json': { + schema: { + type: 'object' + } + } + } + }, + } + }, + '/plugins/{id}/metadata': { + parameters: [ + { + name: 'id', + in: 'path', + required: true, + description: 'L\'identifiant du plugin.', + schema: { + type: 'string', + title: 'Identifiant du plugin' + } + } + ], + put: { + summary: 'Mettre à jour les métadonnées d\'un plugin', + description: 'Mettre à jour les métadonnées d\'un plugin.', + operationId: 'putPluginMetadata', + tags: ['Plugins'], + requestBody: { + description: 'Les métadonnées du plugin', + required: true, + content: { + 'application/json': { + schema: { + type: 'object' + } + } + } + }, + } + }, + '/plugins/{id}/access': { + parameters: [ + { + name: 'id', + in: 'path', + required: true, + description: 'L\'identifiant du plugin.', + schema: { + type: 'string', + title: 'Identifiant du plugin' + } + } + ], + put: { + summary: 'Mettre à jour les accès d\'un plugin', + description: 'Mettre à jour les accès d\'un plugin.', + operationId: 'putPluginAccess', + tags: ['Plugins'], + requestBody: { + description: 'les accès du plugin', + required: true, + content: { + 'application/json': { + schema: { + type: 'object' + } + } + } + }, + } + } + } + } + + if (options?.processing?._id) { + const pathsToKeep = ['/', '/webhook-key', '/_trigger', '/api-docs.json'] + const filteredPaths: any = {} + pathsToKeep.forEach(path => { + if (doc.paths[path]) filteredPaths[path] = doc.paths[path] + }) + doc.paths = filteredPaths + } else { + delete doc.paths['/'] + delete doc.paths['/webhook-key'] + delete doc.paths['/_trigger'] + } + + return doc +} diff --git a/api/types/plugin/schema.js b/api/types/plugin/schema.js index cbae94c7..41bb8ddc 100644 --- a/api/types/plugin/schema.js +++ b/api/types/plugin/schema.js @@ -2,7 +2,8 @@ export default { $id: 'https://github.com/data-fair/processings/plugin', 'x-exports': [ 'types', - 'validate' + 'validate', + 'resolvedSchema' ], title: 'plugin', type: 'object', @@ -20,7 +21,7 @@ export default { ], properties: { name: { - type: 'string' + type: 'string', }, description: { type: 'string' @@ -35,16 +36,20 @@ export default { type: 'string' }, pluginConfigSchema: { - type: 'object' + type: 'object', + description: 'Schema de configuration du plugin.', }, pluginMetadataSchema: { - type: 'object' + type: 'object', + description: 'Schema de configuration des metadata.' }, processingConfigSchema: { - type: 'object' + type: 'object', + description: 'Schema de configuration du traitement.' }, config: { - type: 'object' + type: 'object', + description: 'La configuration du plugin respectant le schema de configuration du plugin.', }, access: { type: 'object', @@ -73,6 +78,7 @@ export default { }, metadata: { type: 'object', + description: 'Les metadata du plugin respectant le schema de configuration des metadata.', additionalProperties: false, required: ['name', 'description', 'category', 'icon'], properties: { diff --git a/dev/resources/nginx.conf b/dev/resources/nginx.conf index 6c4e37c2..aa9b3048 100644 --- a/dev/resources/nginx.conf +++ b/dev/resources/nginx.conf @@ -92,8 +92,12 @@ http { proxy_pass http://localhost:8081/; } + location /openapi-viewer { + proxy_pass http://localhost:8083; + } + location /events/ { - proxy_pass http://localhost:8088; + proxy_pass http://localhost:8084; } } } diff --git a/docker-compose.yml b/docker-compose.yml index de871254..f3390aee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -60,7 +60,31 @@ services: - PUBLIC_URL=http://localhost:5600/data-fair - WS_PUBLIC_URL=ws://localhost:5600/data-fair - EXTRA_NAV_ITEMS=[{"id":"processings","can":"contrib","iframe":"http://localhost:5600/processings/processings","basePath":"/processings","icon":"mdi-cog-transfer-outline","title":"Traitements périodiques"}] - - EXTRA_ADMIN_NAV_ITEMS=[{"id":"processings","iframe":"http://localhost:5600/processings/admin","basePath":"/processings","icon":"mdi-cog-transfer-outline","title":"Traitements périodiques"}] + - EXTRA_ADMIN_NAV_ITEMS=[{"id":"processings","iframe":"http://localhost:5600/processings/admin","basePath":"/processings","icon":"mdi-cog-transfer-outline","title":"Traitements périodiques"},{"id":"processingsAdminDoc","href":"http://localhost:5600/openapi-viewer?urlType=processingsAdmin","icon":"mdi-cog-transfer-outline","title":"API Traitements périodiques"}] + - OBSERVER_ACTIVE=false + - OPENAPI_VIEWER_V2=true + + openapi-viewer: + profiles: + - dev + image: ghcr.io/data-fair/openapi-viewer:master + ports: + - 8083:8080 + environment: + - USE_SIMPLE_DIRECTORY=true + - ALLOWED_URLS={"processings":"http://localhost:5600/processings/api/v1/api-docs.json","processingsId":"http://localhost:5600/processings/api/v1/processings/{id}/api-docs.json","processingsAdmin":"http://localhost:5600/processings/api/v1/admin/api-docs.json"} + + events: + profiles: + - dev + image: ghcr.io/data-fair/events:main + network_mode: host + environment: + - PORT=8084 + - PRIVATE_DIRECTORY_URL=http://localhost:5600/simple-directory + - SECRET_IDENTITIES=secret-identities + - SECRET_EVENTS=secret-events + - SECRET_SENDMAILS=secret-sendmails - OBSERVER_ACTIVE=false ##### @@ -96,19 +120,6 @@ services: volumes: - mongo-data:/data/db - events: - profiles: - - dev - image: ghcr.io/data-fair/events:main - network_mode: host - environment: - - PORT=8088 - - PRIVATE_DIRECTORY_URL=http://localhost:5600/simple-directory - - SECRET_IDENTITIES=secret-identities - - SECRET_EVENTS=secret-events - - SECRET_SENDMAILS=secret-sendmails - - OBSERVER_ACTIVE=false - ##### # Api and worker in docker mode ##### diff --git a/ui/dts/auto-imports.d.ts b/ui/dts/auto-imports.d.ts index 83176695..bcfe2dff 100644 --- a/ui/dts/auto-imports.d.ts +++ b/ui/dts/auto-imports.d.ts @@ -39,6 +39,8 @@ declare global { const mdiBell: typeof import('@mdi/js')['mdiBell'] const mdiCheckCircle: typeof import('@mdi/js')['mdiCheckCircle'] const mdiClock: typeof import('@mdi/js')['mdiClock'] + const mdiCloud: typeof import('@mdi/js')['mdiCloud'] + const mdiContentDuplicate: typeof import('@mdi/js')['mdiContentDuplicate'] const mdiDatabase: typeof import('@mdi/js')['mdiDatabase'] const mdiDelete: typeof import('@mdi/js')['mdiDelete'] const mdiDotsVertical: typeof import('@mdi/js')['mdiDotsVertical'] @@ -159,6 +161,8 @@ declare module 'vue' { readonly mdiBell: UnwrapRef readonly mdiCheckCircle: UnwrapRef readonly mdiClock: UnwrapRef + readonly mdiCloud: UnwrapRef + readonly mdiContentDuplicate: UnwrapRef readonly mdiDatabase: UnwrapRef readonly mdiDelete: UnwrapRef readonly mdiDotsVertical: UnwrapRef diff --git a/ui/src/components/processing/processing-actions.vue b/ui/src/components/processing/processing-actions.vue index 67382905..57b8f120 100644 --- a/ui/src/components/processing/processing-actions.vue +++ b/ui/src/components/processing/processing-actions.vue @@ -71,6 +71,62 @@ + + + + + Vous êtes sur le point de créer une copie du traitement "{{ processing?.title }}". + + + + + + Annuler + + + Dupliquer + + + + + + + + Utiliser l'API + + (session.state.account) +const duplicateTitle = ref(`${properties.processing.title} (copie)`) const activeAccount = computed(() => session.state.account) @@ -293,6 +367,34 @@ const webhookLink = computed(() => { return link }) +const confirmDuplicate = useAsyncAction( + async () => { + if (!properties.processing) return + + const newProcessing = { + owner: properties.processing.owner, + plugin: properties.processing.plugin, + title: duplicateTitle.value || `${properties.processing.title} (copie)`, + config: properties.processing.config, + permissions: properties.processing.permissions, + scheduling: properties.processing.scheduling + } + + const created = await $fetch('/processings', { + method: 'POST', + body: JSON.stringify(newProcessing) + }) + + await router.push(`/processings/${created._id}`) + router.go(0) // Refresh the page to get the new processing + showDuplicateMenu.value = false + }, + { + error: 'Erreur lors de la duplication du traitement', + success: 'Traitement dupliqué !' + } +) + const confirmChangeOwner = useAsyncAction( async () => { await $fetch(`/processings/${properties.processing?._id}`, { diff --git a/ui/vite.config.ts b/ui/vite.config.ts index dd2f0ef5..5077cca0 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -50,6 +50,8 @@ export default defineConfig({ 'mdiBell', 'mdiCheckCircle', 'mdiClock', + 'mdiCloud', + 'mdiContentDuplicate', 'mdiDatabase', 'mdiDotsVertical', 'mdiDownload', diff --git a/worker/config/development.cjs b/worker/config/development.cjs index cfec443e..015bd436 100644 --- a/worker/config/development.cjs +++ b/worker/config/development.cjs @@ -6,7 +6,7 @@ module.exports = { secretKeys: { events: 'secret-events' }, - privateEventsUrl: 'http://localhost:8088', + privateEventsUrl: 'http://localhost:8084', observer: { port: 9091 },