diff --git a/app/portainer/react/components/registries.ts b/app/portainer/react/components/registries.ts index 94502103d..08fe2bd5c 100644 --- a/app/portainer/react/components/registries.ts +++ b/app/portainer/react/components/registries.ts @@ -7,6 +7,8 @@ import { DefaultRegistryDomain, DefaultRegistryName, } from '@/react/portainer/registries/ListView/DefaultRegistry'; +import { RepositoriesDatatable } from '@/react/portainer/registries/repositories/ListView/RepositoriesDatatable'; +import { withUIRouter } from '@/react-tools/withUIRouter'; export const registriesModule = angular .module('portainer.app.react.components.registries', []) @@ -21,4 +23,8 @@ export const registriesModule = angular .component( 'defaultRegistryDomain', r2a(withReactQuery(DefaultRegistryDomain), []) + ) + .component( + 'registryRepositoriesDatatable', + r2a(withUIRouter(withReactQuery(RepositoriesDatatable)), ['dataset']) ).name; diff --git a/app/react/components/datatables/useTableState.ts b/app/react/components/datatables/useTableState.ts index 0994c4e50..22f196b77 100644 --- a/app/react/components/datatables/useTableState.ts +++ b/app/react/components/datatables/useTableState.ts @@ -25,6 +25,13 @@ export function useTableState< ); } +export function useTableStateWithStorage( + ...args: Parameters +) { + const [store] = useState(() => createPersistedStore(...args)); + return useTableState(store, args[0]); +} + export function useTableStateWithoutStorage( defaultSortKey?: string ): BasicTableSettings & { diff --git a/app/react/portainer/registries/queries/queryKeys.ts b/app/react/portainer/registries/queries/queryKeys.ts deleted file mode 100644 index eab8e728b..000000000 --- a/app/react/portainer/registries/queries/queryKeys.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const queryKeys = { - registries: () => ['registries'] as const, -}; diff --git a/app/react/portainer/registries/registry.service.ts b/app/react/portainer/registries/registry.service.ts index 36cd84838..75195cb00 100644 --- a/app/react/portainer/registries/registry.service.ts +++ b/app/react/portainer/registries/registry.service.ts @@ -1,6 +1,6 @@ import axios, { parseAxiosError } from '@/portainer/services/axios'; -import { Catalog, Repository } from './types/registry'; +import { Catalog } from './types/registry'; export async function listRegistryCatalogs(registryId: number) { try { @@ -12,21 +12,3 @@ export async function listRegistryCatalogs(registryId: number) { throw parseAxiosError(err as Error, 'Failed to get catalog of registry'); } } - -export async function listRegistryCatalogsRepository( - registryId: number, - repositoryName: string -) { - try { - const { data } = await axios.get( - `/registries/${registryId}/v2/${repositoryName}/tags/list`, - {} - ); - return data; - } catch (err) { - throw parseAxiosError( - err as Error, - 'Failed to get catelog repository of regisry' - ); - } -} diff --git a/app/react/portainer/registries/repositories/ListView/.keep b/app/react/portainer/registries/repositories/ListView/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/react/portainer/registries/repositories/ListView/RepositoriesDatatable.tsx b/app/react/portainer/registries/repositories/ListView/RepositoriesDatatable.tsx new file mode 100644 index 000000000..f73d09a03 --- /dev/null +++ b/app/react/portainer/registries/repositories/ListView/RepositoriesDatatable.tsx @@ -0,0 +1,23 @@ +import { Book } from 'lucide-react'; + +import { Datatable } from '@@/datatables'; +import { useTableStateWithStorage } from '@@/datatables/useTableState'; + +import { Repository } from './types'; +import { columns } from './columns'; + +export function RepositoriesDatatable({ dataset }: { dataset?: Repository[] }) { + const tableState = useTableStateWithStorage('registryRepositories'); + return ( + + ); +} diff --git a/app/react/portainer/registries/repositories/ListView/columns.tsx b/app/react/portainer/registries/repositories/ListView/columns.tsx new file mode 100644 index 000000000..c374840f6 --- /dev/null +++ b/app/react/portainer/registries/repositories/ListView/columns.tsx @@ -0,0 +1,68 @@ +import { createColumnHelper, CellContext } from '@tanstack/react-table'; +import { useCurrentStateAndParams } from '@uirouter/react'; + +import { Link } from '@@/Link'; + +import { useRepositoryTags } from './useRepositoryTags'; +import { Repository } from './types'; + +const helper = createColumnHelper(); + +export const columns = [ + helper.accessor('Name', { + header: 'Repository', + cell: NameCell, + }), + helper.display({ + header: 'Tags count', + cell: TagsCell, + }), +]; + +function useParams() { + const { + params: { endpointId, id }, + } = useCurrentStateAndParams(); + + const registryId = number(id); + + if (!registryId) { + throw new Error('Missing registry id'); + } + + return { + environmentId: number(endpointId), + registryId, + }; +} + +function number(value: string | undefined) { + const num = parseInt(value || '', 10); + return Number.isNaN(num) ? undefined : num; +} + +function NameCell({ getValue }: CellContext) { + const { environmentId } = useParams(); + const name = getValue(); + return ( + + {name} + + ); +} + +function TagsCell({ row }: CellContext) { + const { environmentId, registryId } = useParams(); + + const tagsQuery = useRepositoryTags({ + environmentId, + registryId, + repository: row.original.Name, + }); + + return tagsQuery.data?.tags.length || 0; +} diff --git a/app/react/portainer/registries/repositories/ListView/types.ts b/app/react/portainer/registries/repositories/ListView/types.ts new file mode 100644 index 000000000..ac602a066 --- /dev/null +++ b/app/react/portainer/registries/repositories/ListView/types.ts @@ -0,0 +1,3 @@ +export interface Repository { + Name: string; +} diff --git a/app/react/portainer/registries/repositories/ListView/useRepositoryTags.ts b/app/react/portainer/registries/repositories/ListView/useRepositoryTags.ts new file mode 100644 index 000000000..e5349dba9 --- /dev/null +++ b/app/react/portainer/registries/repositories/ListView/useRepositoryTags.ts @@ -0,0 +1,72 @@ +import { useQuery } from 'react-query'; +import _ from 'lodash'; + +import { Environment } from '@/react/portainer/environments/types'; +import axios from '@/portainer/services/axios'; + +import { Registry } from '../../types/registry'; +import { buildUrl } from '../../queries/build-url'; +import { queryKeys } from '../../queries/query-keys'; + +export function useRepositoryTags({ + registryId, + ...params +}: { + registryId: Registry['Id']; + repository: string; + environmentId?: Environment['Id']; +}) { + return useQuery({ + queryKey: [...queryKeys.item(registryId), params] as const, + queryFn: () => + getRepositoryTags({ + ...params, + registryId, + n: 100, + last: '', + }), + staleTime: 1 * 60 * 1000, // 1 minute + }); +} + +export async function getRepositoryTags( + { + environmentId, + registryId, + repository, + n, + last, + }: { + registryId: Registry['Id']; + repository: string; + environmentId?: Environment['Id']; + n?: number; + last?: string; + }, + acc: { name: string; tags: string[] } = { name: '', tags: [] } +): Promise<{ name: string; tags: string[] }> { + const { data, headers } = await axios.get<{ name: string; tags: string[] }>( + `${buildUrl(registryId)}/v2/${repository}/tags/list`, + { + params: { + id: registryId, + endpointId: environmentId, + repository, + n, + last, + }, + } + ); + acc.name = data.name; + acc.tags = _.concat(acc.tags, data.tags); + + if (headers.link) { + const last = data.tags[data.tags.length - 1]; + return getRepositoryTags( + { registryId, repository, n, last, environmentId }, + acc + ); + } + + return acc; +} diff --git a/app/react/portainer/registries/types/registry.ts b/app/react/portainer/registries/types/registry.ts index 002a9c20c..6bcd7bcf0 100644 --- a/app/react/portainer/registries/types/registry.ts +++ b/app/react/portainer/registries/types/registry.ts @@ -7,11 +7,6 @@ export type Catalog = { repositories: string[]; }; -export type Repository = { - name: string; - tags: string[]; -}; - export enum RegistryTypes { ANONYMOUS, QUAY,