Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/api/container-remote-native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { PulpAPI } from './pulp';

const base = new PulpAPI();

export interface ContainerRemoteNativeType {
pulp_href: string;
name: string;
url: string;
pulp_created: string;
pulp_last_updated: string;
policy: string;
tls_validation: boolean;
proxy_url: string | null;
ca_cert: string | null;
client_cert: string | null;
hidden_fields: { name: string; is_set: boolean }[];
}

export const ContainerRemoteNativeAPI = {
get: (id: string) => base.http.get(`remotes/container/container/${id}/`),

list: (params?) => base.list('remotes/container/container/', params),
};
46 changes: 46 additions & 0 deletions src/api/container-repository-native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { PulpAPI } from './pulp';
import { config } from 'src/ui-config';

const base = new PulpAPI();

const toRelativeHref = (href: string) =>
href?.replace(config.API_BASE_PATH, '') || href;

export interface ContainerRepositoryNativeType {
pulp_href: string;
name: string;
description: string | null;
remote: string | null;
pulp_created: string;
pulp_last_updated: string;
latest_version_href?: string;
retain_repo_versions?: number;
pulp_labels?: Record<string, string>;
}

export interface ContainerRepositoryVersionType {
pulp_href: string;
pulp_created: string;
number: number;
repository: string;
base_version: string | null;
content_summary: {
added: Record<string, { count: number; href: string }>;
removed: Record<string, { count: number; href: string }>;
present: Record<string, { count: number; href: string }>;
};
}

export const ContainerRepositoryNativeAPI = {
get: (id: string) => base.http.get(`repositories/container/container/${id}/`),

getByHref: (href: string) => base.http.get(toRelativeHref(href)),

list: (params?) => base.list('repositories/container/container/', params),

listVersions: (id: string, params?) =>
base.list(`repositories/container/container/${id}/versions/`, params),

listVersionsByHref: (href: string, params?) =>
base.list(`${toRelativeHref(href)}versions/`, params),
};
9 changes: 9 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ export {
ContainerDistributionAPI,
ContainerPullThroughDistributionAPI,
} from './container-distribution';
export {
ContainerRepositoryNativeAPI,
type ContainerRepositoryNativeType,
type ContainerRepositoryVersionType,
} from './container-repository-native';
export {
ContainerRemoteNativeAPI,
type ContainerRemoteNativeType,
} from './container-remote-native';
export { ContainerTagAPI } from './container-tag';
export { ExecutionEnvironmentAPI } from './execution-environment';
export { ExecutionEnvironmentNamespaceAPI } from './execution-environment-namespace';
Expand Down
168 changes: 168 additions & 0 deletions src/components/container-repository-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { t } from '@lingui/core/macro';
import {
Nav,
NavExpandable,
NavItem,
Title,
} from '@patternfly/react-core';
import { useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router';
import {
ContainerDistributionAPI,
} from 'src/api';
import { EmptyStateNoData } from 'src/components';
import { Paths, formatEEPath } from 'src/paths';
import { NavList, SearchInput, Spinner } from './patternfly-wrappers/l10n';

interface NativeContainerDistributionType {
name: string;
base_path: string;
remote: string | null;
description: string | null;
pulp_created: string;
}

type RepositoryTypeGroup = 'local' | 'remote';

interface IProps {
selectedRepository?: string;
}

const PAGE_SIZE = 200;

const repositoryType = (
repository: NativeContainerDistributionType,
): RepositoryTypeGroup =>
repository.remote ? 'remote' : 'local';

async function loadAllRepositories() {
const result = await ContainerDistributionAPI.list({
page_size: PAGE_SIZE,
sort: 'name',
});
return result.data.results as NativeContainerDistributionType[];
}

export const ContainerRepositorySidebar = ({ selectedRepository }: IProps) => {
const [filter, setFilter] = useState('');
const [loading, setLoading] = useState(true);
const [repositories, setRepositories] = useState<NativeContainerDistributionType[]>([]);
const [error, setError] = useState<string>(null);
const typeLabels: Record<RepositoryTypeGroup, string> = {
local: t`Local`,
remote: t`Remote`,
};

useEffect(() => {
let cancelled = false;

loadAllRepositories()
.then((items) => {
if (!cancelled) {
setRepositories(items);
setLoading(false);
}
})
.catch((e) => {
if (!cancelled) {
setError(e?.message || t`Failed to load repositories.`);
setLoading(false);
}
});

return () => {
cancelled = true;
};
}, []);

const groupedRepositories = useMemo(() => {
const normalizedFilter = filter.toLowerCase();
const filtered = repositories.filter(({ name, base_path }) => {
const haystack = [name, base_path]
.filter(Boolean)
.join('/')
.toLowerCase();

return haystack.includes(normalizedFilter);
});

return {
local: filtered.filter((repository) => repositoryType(repository) === 'local'),
remote: filtered.filter((repository) => repositoryType(repository) === 'remote'),
};
}, [filter, repositories]);

const hasResults = Object.values(groupedRepositories).some(
({ length }) => length > 0,
);

return (
<aside className='container-repository-sidebar'>
<div className='container-repository-sidebar__header'>
<Title headingLevel='h2' size='lg'>
{t`Repository types`}
</Title>
<SearchInput
value={filter}
onChange={(_event, value) => setFilter(value)}
onClear={() => setFilter('')}
aria-label={t`Filter repositories`}
placeholder={t`Filter repositories`}
/>
</div>

{loading ? (
<div className='container-repository-sidebar__status'>
<Spinner size='md' />
</div>
) : error ? (
<EmptyStateNoData
title={t`Repositories unavailable`}
description={error}
/>
) : !hasResults ? (
<EmptyStateNoData
title={t`No matching repositories`}
description={
filter
? t`No repositories match the current filter.`
: t`No repositories are available.`
}
/>
) : (
<Nav theme='light' aria-label={t`Container repositories by type`}>
<NavList>
{(['local', 'remote'] as RepositoryTypeGroup[])
.filter((type) => groupedRepositories[type].length > 0)
.map((type) => (
<NavExpandable
key={type}
title={`${typeLabels[type]} (${groupedRepositories[type].length})`}
isExpanded
>
{groupedRepositories[type].map((repository) => {
const link = formatEEPath(Paths.container.repository.detail, {
container: repository.base_path,
});

return (
<NavItem
key={repository.name}
isActive={selectedRepository === repository.name}
>
<Link to={link}>
<span className='container-repository-sidebar__link'>
{repository.name}
</span>
</Link>
</NavItem>
);
})}
</NavExpandable>
))}
</NavList>
</Nav>
)}
</aside>
);
};
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export { CollectionUsedbyDependenciesList } from './collection-usedby-dependenci
export { CompoundFilter, type FilterOption } from './compound-filter';
export { ConfirmModal } from './confirm-modal';
export { ContainerRepositoryForm } from './container-repository-form';
export { ContainerRepositorySidebar } from './container-repository-sidebar';
export { CopyCollectionToRepositoryModal } from './copy-collection-to-repository-modal';
export { CopyURL } from './copy-url';
export { DarkmodeSwitcher } from './darkmode-switcher';
Expand Down
Loading