summaryrefslogblamecommitdiff
path: root/makima/frontend/src/hooks/useDirectives.ts
blob: 6e1654f02379ad4d08c9b7bc385402f3c0715160 (plain) (tree)









































































































































































































































































































                                                                                             
import { useState, useCallback, useEffect, useRef } from "react";
import {
  listDirectives,
  getDirective,
  createDirective,
  updateDirective,
  archiveDirective,
  startDirective,
  pauseDirective,
  resumeDirective,
  stopDirective,
  getDirectiveGraph,
  subscribeToDirectiveEvents,
  type DirectiveSummary,
  type DirectiveWithProgress,
  type DirectiveGraphResponse,
  type DirectiveStatus,
  type DirectiveEvent,
  type CreateDirectiveRequest,
  type UpdateDirectiveRequest,
  type StartDirectiveResponse,
} from "../lib/api";

interface UseDirectivesResult {
  directives: DirectiveSummary[];
  loading: boolean;
  error: string | null;
  refresh: () => Promise<void>;
  createNewDirective: (req: CreateDirectiveRequest) => Promise<DirectiveWithProgress | null>;
  updateExistingDirective: (
    directiveId: string,
    req: UpdateDirectiveRequest
  ) => Promise<DirectiveWithProgress | null>;
  archiveExistingDirective: (directiveId: string) => Promise<boolean>;
  getDirectiveById: (directiveId: string) => Promise<DirectiveWithProgress | null>;
  getGraph: (directiveId: string) => Promise<DirectiveGraphResponse | null>;
  start: (directiveId: string) => Promise<StartDirectiveResponse | null>;
  pause: (directiveId: string) => Promise<boolean>;
  resume: (directiveId: string) => Promise<boolean>;
  stop: (directiveId: string) => Promise<boolean>;
}

export function useDirectives(statusFilter?: DirectiveStatus): UseDirectivesResult {
  const [directives, setDirectives] = useState<DirectiveSummary[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  const fetchDirectives = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const response = await listDirectives(statusFilter);
      setDirectives(response.directives);
    } catch (err) {
      console.error("Failed to fetch directives:", err);
      setError(err instanceof Error ? err.message : "Failed to fetch directives");
    } finally {
      setLoading(false);
    }
  }, [statusFilter]);

  useEffect(() => {
    fetchDirectives();
  }, [fetchDirectives]);

  const createNewDirective = useCallback(
    async (req: CreateDirectiveRequest): Promise<DirectiveWithProgress | null> => {
      try {
        const directive = await createDirective(req);
        // Refresh the list
        await fetchDirectives();
        // Return the full directive with progress
        return await getDirective(directive.id);
      } catch (err) {
        console.error("Failed to create directive:", err);
        setError(err instanceof Error ? err.message : "Failed to create directive");
        return null;
      }
    },
    [fetchDirectives]
  );

  const updateExistingDirective = useCallback(
    async (
      directiveId: string,
      req: UpdateDirectiveRequest
    ): Promise<DirectiveWithProgress | null> => {
      try {
        await updateDirective(directiveId, req);
        // Refresh the list
        await fetchDirectives();
        // Return the updated directive
        return await getDirective(directiveId);
      } catch (err) {
        console.error("Failed to update directive:", err);
        setError(err instanceof Error ? err.message : "Failed to update directive");
        return null;
      }
    },
    [fetchDirectives]
  );

  const archiveExistingDirective = useCallback(
    async (directiveId: string): Promise<boolean> => {
      try {
        await archiveDirective(directiveId);
        // Refresh the list
        await fetchDirectives();
        return true;
      } catch (err) {
        console.error("Failed to archive directive:", err);
        setError(err instanceof Error ? err.message : "Failed to archive directive");
        return false;
      }
    },
    [fetchDirectives]
  );

  const getDirectiveById = useCallback(
    async (directiveId: string): Promise<DirectiveWithProgress | null> => {
      try {
        return await getDirective(directiveId);
      } catch (err) {
        console.error("Failed to get directive:", err);
        setError(err instanceof Error ? err.message : "Failed to get directive");
        return null;
      }
    },
    []
  );

  const getGraph = useCallback(
    async (directiveId: string): Promise<DirectiveGraphResponse | null> => {
      try {
        return await getDirectiveGraph(directiveId);
      } catch (err) {
        console.error("Failed to get directive graph:", err);
        setError(err instanceof Error ? err.message : "Failed to get directive graph");
        return null;
      }
    },
    []
  );

  const start = useCallback(
    async (directiveId: string): Promise<StartDirectiveResponse | null> => {
      try {
        const response = await startDirective(directiveId);
        await fetchDirectives();
        return response;
      } catch (err) {
        console.error("Failed to start directive:", err);
        setError(err instanceof Error ? err.message : "Failed to start directive");
        return null;
      }
    },
    [fetchDirectives]
  );

  const pause = useCallback(
    async (directiveId: string): Promise<boolean> => {
      try {
        await pauseDirective(directiveId);
        await fetchDirectives();
        return true;
      } catch (err) {
        console.error("Failed to pause directive:", err);
        setError(err instanceof Error ? err.message : "Failed to pause directive");
        return false;
      }
    },
    [fetchDirectives]
  );

  const resume = useCallback(
    async (directiveId: string): Promise<boolean> => {
      try {
        await resumeDirective(directiveId);
        await fetchDirectives();
        return true;
      } catch (err) {
        console.error("Failed to resume directive:", err);
        setError(err instanceof Error ? err.message : "Failed to resume directive");
        return false;
      }
    },
    [fetchDirectives]
  );

  const stop = useCallback(
    async (directiveId: string): Promise<boolean> => {
      try {
        await stopDirective(directiveId);
        await fetchDirectives();
        return true;
      } catch (err) {
        console.error("Failed to stop directive:", err);
        setError(err instanceof Error ? err.message : "Failed to stop directive");
        return false;
      }
    },
    [fetchDirectives]
  );

  return {
    directives,
    loading,
    error,
    refresh: fetchDirectives,
    createNewDirective,
    updateExistingDirective,
    archiveExistingDirective,
    getDirectiveById,
    getGraph,
    start,
    pause,
    resume,
    stop,
  };
}

/** Hook for subscribing to real-time directive events via SSE */
export function useDirectiveEventSubscription(
  directiveId: string | null,
  onEvent?: (event: DirectiveEvent) => void
): {
  events: DirectiveEvent[];
  isConnected: boolean;
  error: string | null;
} {
  const [events, setEvents] = useState<DirectiveEvent[]>([]);
  const [isConnected, setIsConnected] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const cleanupRef = useRef<(() => void) | null>(null);

  useEffect(() => {
    // Clean up any existing subscription
    if (cleanupRef.current) {
      cleanupRef.current();
      cleanupRef.current = null;
    }

    if (!directiveId) {
      setIsConnected(false);
      setEvents([]);
      return;
    }

    // Subscribe to events
    let mounted = true;

    const setupSubscription = async () => {
      try {
        const cleanup = await subscribeToDirectiveEvents(
          directiveId,
          (event) => {
            if (mounted) {
              setEvents((prev) => [...prev, event]);
              onEvent?.(event);
            }
          },
          (err) => {
            if (mounted) {
              setError(err.message);
              setIsConnected(false);
            }
          }
        );

        if (mounted) {
          cleanupRef.current = cleanup;
          setIsConnected(true);
          setError(null);
        } else {
          // Component unmounted during setup, clean up immediately
          cleanup();
        }
      } catch (err) {
        if (mounted) {
          setError(err instanceof Error ? err.message : "Failed to subscribe to events");
          setIsConnected(false);
        }
      }
    };

    setupSubscription();

    return () => {
      mounted = false;
      if (cleanupRef.current) {
        cleanupRef.current();
        cleanupRef.current = null;
      }
    };
  }, [directiveId, onEvent]);

  return { events, isConnected, error };
}