feat: application n8n Pilot complète (Expo managed workflow)
- Stack : Expo Router, Axios, Zustand, React Native Paper (thème sombre), date-fns - Sécurité : secrets dans Android Keystore via expo-secure-store, TLS obligatoire, headers X-N8N-API-KEY + X-App-Token injectés par intercepteur Axios - API : client.ts centralisé + workflows.ts + executions.ts (TypeScript strict) - Store : Zustand appStore avec chargement depuis secure store au démarrage - Hooks : usePolling (générique), useWorkflows, useExecutions - Composants : StatusBadge, WorkflowCard, ExecutionCard, SkeletonLoader - Screens : Dashboard, Workflows, Executions, Logs (détail exécution), Settings - Navigation Expo Router : 4 tabs + stack Logs + écran Setup initial - Docs : INSTALL.md, UPDATE.md, BACKUP.md, HAPROXY.md, SECURITY.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useAppStore } from '../store/appStore';
|
||||
import {
|
||||
fetchExecutions,
|
||||
fetchExecutionById,
|
||||
deleteExecution,
|
||||
Execution,
|
||||
ExecutionStatus,
|
||||
FetchExecutionsParams,
|
||||
} from '../api/executions';
|
||||
import { usePolling } from './usePolling';
|
||||
|
||||
/** Options de filtrage passées au hook à l'initialisation */
|
||||
interface UseExecutionsOptions {
|
||||
/** Nombre max d'exécutions à charger */
|
||||
limit?: number;
|
||||
/** Filtre de statut initial */
|
||||
statusFilter?: ExecutionStatus;
|
||||
/** Restreindre aux exécutions d'un workflow spécifique */
|
||||
workflowId?: string;
|
||||
}
|
||||
|
||||
/** Interface des valeurs et actions exposées par le hook */
|
||||
interface UseExecutionsResult {
|
||||
executions: Execution[];
|
||||
isLoading: boolean;
|
||||
isRefreshing: boolean;
|
||||
/** Exécution chargée en détail (avec logs de nœuds) */
|
||||
selectedExecution: Execution | null;
|
||||
/** Recharge la liste (pull-to-refresh) */
|
||||
refresh: () => Promise<void>;
|
||||
/**
|
||||
* Charge les logs complets d'une exécution pour l'écran de détail.
|
||||
* Peuple selectedExecution.
|
||||
*
|
||||
* @param id - Identifiant de l'exécution
|
||||
*/
|
||||
loadExecutionDetail: (id: string) => Promise<void>;
|
||||
/**
|
||||
* Supprime une exécution de l'historique et la retire de l'état local.
|
||||
*
|
||||
* @param id - Identifiant de l'exécution à supprimer
|
||||
*/
|
||||
removeExecution: (id: string) => Promise<void>;
|
||||
/**
|
||||
* Change le filtre de statut actif et relance le chargement.
|
||||
*
|
||||
* @param status - Statut cible ou undefined pour tout afficher
|
||||
*/
|
||||
setStatusFilter: (status: ExecutionStatus | undefined) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook de gestion des exécutions : listing avec filtres, polling, détail et suppression.
|
||||
* Sépare clairement la liste (vue Executions) du détail (vue Logs).
|
||||
*/
|
||||
export const useExecutions = (options: UseExecutionsOptions = {}): UseExecutionsResult => {
|
||||
const { preferences, config } = useAppStore();
|
||||
const [executions, setExecutions] = useState<Execution[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [selectedExecution, setSelectedExecution] = useState<Execution | null>(null);
|
||||
const [statusFilter, setStatusFilter] = useState<ExecutionStatus | undefined>(
|
||||
options.statusFilter
|
||||
);
|
||||
|
||||
/** Charge la liste avec les filtres courants */
|
||||
const loadExecutions = useCallback(async (): Promise<void> => {
|
||||
const params: FetchExecutionsParams = {
|
||||
limit: options.limit ?? 50,
|
||||
...(statusFilter !== undefined && { status: statusFilter }),
|
||||
...(options.workflowId && { workflowId: options.workflowId }),
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await fetchExecutions(params);
|
||||
setExecutions(result.data);
|
||||
} catch {
|
||||
// Géré par l'intercepteur Axios
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [options.limit, options.workflowId, statusFilter]);
|
||||
|
||||
/** Polling automatique — désactivé si l'app n'est pas encore configurée */
|
||||
usePolling({
|
||||
callback: loadExecutions,
|
||||
interval: preferences.pollingInterval,
|
||||
enabled: config.isConfigured,
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
const refresh = useCallback(async (): Promise<void> => {
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
await loadExecutions();
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
}, [loadExecutions]);
|
||||
|
||||
/**
|
||||
* Charge les logs détaillés d'une exécution.
|
||||
* Appelle GET /executions/:id qui inclut le champ `data` avec les nœuds.
|
||||
*/
|
||||
const loadExecutionDetail = useCallback(async (id: string): Promise<void> => {
|
||||
try {
|
||||
const detail = await fetchExecutionById(id);
|
||||
setSelectedExecution(detail);
|
||||
} catch {
|
||||
// Géré par l'intercepteur Axios
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Supprime une exécution côté serveur et la retire optimistiquement de l'état local.
|
||||
*/
|
||||
const removeExecution = useCallback(async (id: string): Promise<void> => {
|
||||
try {
|
||||
await deleteExecution(id);
|
||||
setExecutions((prev) => prev.filter((e) => e.id !== id));
|
||||
} catch {
|
||||
// Géré par l'intercepteur Axios
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
executions,
|
||||
isLoading,
|
||||
isRefreshing,
|
||||
selectedExecution,
|
||||
refresh,
|
||||
loadExecutionDetail,
|
||||
removeExecution,
|
||||
setStatusFilter,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user