Ctrl K
ComunidadGuías…

dashboard/src/hooks/use-query/useAppsQuery.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import axiosInstance from '@/config/axios';

export type Artifact = {
  ID: string;
  link: string;
  platform: string;
  arch: string;
  package: string;
  TufTaskID?: string | null;
  TufSigned?: boolean;
  IsFeed?: boolean;
};

export type ChangelogEntry = {
  Version: string;
  Changes: string;
  Date: string;
};

export type AppVersion = {
  ID: string;
  AppName: string;
  Version: string;
  Channel: string;
  Published: boolean;
  Critical: boolean;
  Intermediate: boolean;
  RolloutPercent?: number | null;
  Artifacts: Artifact[];
  Changelog: ChangelogEntry[];
  Updated_at: string;
};

export type DownloadMode = 'unlisted' | 'strict';

export type AppListItem = {
  ID: string;
  AppName: string;
  Logo: string;
  Description: string;
  Updated_at: string;
  isExpanded?: boolean;
  Private?: boolean;
  DownloadMode?: DownloadMode;
  Tuf?: boolean;
  Reports?: boolean;
  CdnEdge?: boolean;
};

export type PaginatedResponse<T> = {
  items: T[];
  total: number;
  page: number;
  limit: number;
};

export interface VersionFilters {
  channel: string;
  published: boolean | null;
  critical: boolean | null;
  platform: string;
  arch: string;
}

export type BulkDeleteOutcome = {
  deletedIds: string[];
  orphanedLinks: string[];
  remainingIds: string[];
  error: string | null;
};

// Mirrors MAX_BULK_DELETE_VERSIONS on the server. A page holds 9 versions, so a
// page-wide selection still goes out as one all-or-nothing request.
const BULK_DELETE_CHUNK = 10;

const buildSearchParams = (
  appName: string,
  page: number,
  limit: number,
  filters?: VersionFilters,
) => {
  const params = new URLSearchParams({
    app_name: appName,
    limit: limit.toString(),
    page: page.toString(),
  });

  if (filters) {
    if (filters.channel) params.append('channel', filters.channel);
    if (filters.published !== null) params.append('published', filters.published.toString());
    if (filters.critical !== null) params.append('critical', filters.critical.toString());
    if (filters.platform) params.append('platform', filters.platform);
    if (filters.arch) params.append('arch', filters.arch);
  }

  return params;
};

const requestVersionDelete = async (ids: string[]) => {
  const params = new URLSearchParams();
  ids.forEach(id => params.append('id', id));
  const response = await axiosInstance.delete(`/apps/delete?${params.toString()}`);
  return response.data;
};

const describeDeleteError = (err: unknown): string => {
  const data = (err as { response?: { data?: Record<string, unknown> } })?.response?.data;
  if (!data) {
    return (err as Error)?.message || 'Failed to delete versions';
  }

  const parts = [String(data.error || 'Failed to delete versions')];
  if (data.details) parts.push(String(data.details));
  for (const key of ['forbidden', 'not_found', 'apps'] as const) {
    const list = data[key];
    if (Array.isArray(list) && list.length > 0) {
      parts.push(`${key.replace('_', ' ')}: ${list.join(', ')}`);
    }
  }
  return parts.join(' - ');
};

export const useAppsQuery = (
  appName?: string, 
  page: number = 1, 
  refreshKey: number = 0,
  filters?: VersionFilters
) => {
  const queryClient = useQueryClient();

  const { data: apps = [], isLoading, refetch } = useQuery<AppVersion[] | AppListItem[] | PaginatedResponse<AppVersion>>({
    queryKey: ['apps', appName, page, refreshKey, filters],
    queryFn: async () => {
      if (appName) {
        const params = buildSearchParams(appName, page, 9, filters);
        const response = await axiosInstance.get(`/search?${params.toString()}`);
        return response.data;
      } else {
        const response = await axiosInstance.get('/app/list');
        return response.data.apps || [];
      }
    },
  });

  const updateAppMutation = useMutation({
    mutationFn: async ({ 
      id, 
      data 
    }: { 
      id: string; 
      data: { 
        Published: boolean; 
        Critical: boolean;
        Intermediate: boolean;
        Changelog: string;
        Platform?: string;
        Arch?: string;
        Files?: File[];
        app_name: string;
        version: string;
        channel: string;
        updater?: string;
        signature?: string;
        rollout?: number;
      }
    }) => {
      const formData = new FormData();
      const dataObj = {
        id,
        app_name: data.app_name,
        version: data.version,
        channel: data.channel,
        publish: data.Published,
        critical: data.Critical,
        intermediate: data.Intermediate,
        platform: data.Platform,
        arch: data.Arch,
        changelog: data.Changelog,
        ...(data.updater && { updater: data.updater }),
        ...(data.signature && { signature: data.signature }),
        ...(data.rollout !== undefined && { rollout: data.rollout }),
      };

      formData.append('data', JSON.stringify(dataObj));

      if (data.Files && data.Files.length > 0) {
        data.Files.forEach((file) => {
          formData.append('file', file);
        });
      }
  
      await axiosInstance.post(`/apps/update`, formData, {
        headers: {
          'Content-Type': 'multipart/form-data',
        },
      });
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['apps'] });
    },
  });

  const deleteAppMutation = useMutation({
    mutationFn: async (ids: string | string[]) =>
      requestVersionDelete(Array.isArray(ids) ? ids : [ids]),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['apps'] });
    },
  });

  const deleteArtifactMutation = useMutation({
    mutationFn: async ({ 
      id, 
      appName, 
      version, 
      artifactIndex 
    }: { 
      id: string; 
      appName: string; 
      version: string; 
      artifactIndex: number;
    }) => {
      const formData = new FormData();
      const data = {
        id,
        app_name: appName,
        version,
        artifacts_to_delete: [artifactIndex.toString()]
      };
      formData.append('data', JSON.stringify(data));

      await axiosInstance.post('/artifact/delete', formData, {
        headers: {
          'Content-Type': 'multipart/form-data',
        },
      });
    },
    onSuccess: async () => {
      // Invalidate and refetch to ensure we have the latest data
      await queryClient.invalidateQueries({ queryKey: ['apps'] });
      await queryClient.refetchQueries({ queryKey: ['apps'] });
    },
  });

  const updateApp = async (id: string, data: {
    Published: boolean;
    Critical: boolean;
    Intermediate: boolean;
    Changelog: string;
    Platform?: string;
    Arch?: string;
    Files?: File[];
    app_name: string;
    version: string;
    channel: string;
    updater?: string;
    signature?: string;
    rollout?: number;
  }) => {
    await updateAppMutation.mutateAsync({ id, data });
  };

  const deleteApp = async (id: string) => {
    await deleteAppMutation.mutateAsync(id);
  };

  // The server refuses more ids than MAX_BULK_DELETE_VERSIONS, so a selection wider
  // than one page goes out in chunks. Each chunk is atomic on its own; the run stops
  // at the first failing chunk so the report can name what is still there.
  const deleteVersions = async (
    ids: string[],
    onProgress?: (done: number, total: number) => void,
  ): Promise<BulkDeleteOutcome> => {
    const deletedIds: string[] = [];
    const orphanedLinks: string[] = [];
    let chunkSize = BULK_DELETE_CHUNK;
    let index = 0;

    while (index < ids.length) {
      const chunk = ids.slice(index, index + chunkSize);
      try {
        const data = await requestVersionDelete(chunk);
        if (Array.isArray(data?.orphaned_links)) {
          orphanedLinks.push(...data.orphaned_links);
        }
        deletedIds.push(...chunk);
        index += chunk.length;
        onProgress?.(deletedIds.length, ids.length);
      } catch (err) {
        const data = (err as { response?: { data?: { limit?: number } } })?.response?.data;
        const serverLimit = typeof data?.limit === 'number' ? data.limit : 0;
        if (serverLimit > 0 && serverLimit < chunkSize) {
          chunkSize = serverLimit;
          continue;
        }
        return {
          deletedIds,
          orphanedLinks,
          remainingIds: ids.slice(index),
          error: describeDeleteError(err),
        };
      }
    }

    return { deletedIds, orphanedLinks, remainingIds: [], error: null };
  };

  const fetchAllMatchingVersions = async (total: number): Promise<AppVersion[]> => {
    if (!appName || total <= 0) return [];
    const params = buildSearchParams(appName, 1, total, filters);
    const response = await axiosInstance.get(`/search?${params.toString()}`);
    return (response.data?.items || []) as AppVersion[];
  };

  const deleteArtifact = async (id: string, appName: string, version: string, artifactIndex: number) => {
    await deleteArtifactMutation.mutateAsync({ id, appName, version, artifactIndex });
  };

  const getVersionById = (id: string): AppVersion | undefined => {
    
    if (Array.isArray(apps)) {
      const found = apps.find(app => app.ID === id) as AppVersion;
      return found;
    }
    if ('items' in apps) {
      const found = apps.items.find(app => app.ID === id) as AppVersion;
      return found;
    }
    return undefined;
  };

  return { apps, updateApp, deleteApp, deleteVersions, fetchAllMatchingVersions, getVersionById, deleteArtifact, isLoading, refetch };
};