diff --git a/src/actions/deb-remote-create.tsx b/src/actions/deb-remote-create.tsx new file mode 100644 index 00000000..34059aaf --- /dev/null +++ b/src/actions/deb-remote-create.tsx @@ -0,0 +1,9 @@ +import { msg } from '@lingui/core/macro'; +import { Paths, formatPath } from 'src/paths'; +import { Action } from './action'; + +export const debRemoteCreateAction = Action({ + title: msg`Add remote`, + onClick: (item, { navigate }) => + navigate(formatPath(Paths.deb.remote.edit, { name: '_' })), +}); diff --git a/src/actions/deb-remote-delete.tsx b/src/actions/deb-remote-delete.tsx new file mode 100644 index 00000000..1efb8016 --- /dev/null +++ b/src/actions/deb-remote-delete.tsx @@ -0,0 +1,44 @@ +import { msg, t } from '@lingui/core/macro'; +import { DebRemoteAPI } from 'src/api'; +import { DeleteRemoteModal } from 'src/components'; +import { + handleHttpError, + parsePulpIDFromURL, + taskAlert, + waitForTaskUrl, +} from 'src/utilities'; +import { Action } from './action'; + +export const debRemoteDeleteAction = Action({ + title: msg`Delete`, + modal: ({ addAlert, listQuery, setState, state }) => + state.deleteModalOpen ? ( + setState({ deleteModalOpen: null })} + deleteAction={() => + deleteRemote(state.deleteModalOpen, { addAlert, setState, listQuery }) + } + name={state.deleteModalOpen.name} + /> + ) : null, + onClick: ( + { name, id, pulp_href }: { name: string; id?: string; pulp_href?: string }, + { setState }, + ) => + setState({ + deleteModalOpen: { pulpId: id || parsePulpIDFromURL(pulp_href), name }, + }), +}); + +function deleteRemote({ name, pulpId }, { addAlert, setState, listQuery }) { + return DebRemoteAPI.delete(pulpId) + .then(({ data }) => { + addAlert(taskAlert(data.task, t`Removal started for remote ${name}`)); + setState({ deleteModalOpen: null }); + return waitForTaskUrl(data.task); + }) + .then(() => listQuery()) + .catch( + handleHttpError(t`Failed to remove remote ${name}`, () => null, addAlert), + ); +} diff --git a/src/actions/deb-remote-edit.tsx b/src/actions/deb-remote-edit.tsx new file mode 100644 index 00000000..49a5356e --- /dev/null +++ b/src/actions/deb-remote-edit.tsx @@ -0,0 +1,9 @@ +import { msg } from '@lingui/core/macro'; +import { Paths, formatPath } from 'src/paths'; +import { Action } from './action'; + +export const debRemoteEditAction = Action({ + title: msg`Edit`, + onClick: ({ name }, { navigate }) => + navigate(formatPath(Paths.deb.remote.edit, { name })), +}); diff --git a/src/actions/deb-repository-create.tsx b/src/actions/deb-repository-create.tsx new file mode 100644 index 00000000..77d52bb3 --- /dev/null +++ b/src/actions/deb-repository-create.tsx @@ -0,0 +1,9 @@ +import { msg } from '@lingui/core/macro'; +import { Paths, formatPath } from 'src/paths'; +import { Action } from './action'; + +export const debRepositoryCreateAction = Action({ + title: msg`Add repository`, + onClick: (item, { navigate }) => + navigate(formatPath(Paths.deb.repository.edit, { name: '_' })), +}); diff --git a/src/actions/deb-repository-delete.tsx b/src/actions/deb-repository-delete.tsx new file mode 100644 index 00000000..d5cf5476 --- /dev/null +++ b/src/actions/deb-repository-delete.tsx @@ -0,0 +1,99 @@ +import { msg, t } from '@lingui/core/macro'; +import { DebDistributionAPI, DebRepositoryAPI } from 'src/api'; +import { DeleteRepositoryModal } from 'src/components'; +import { + handleHttpError, + parsePulpIDFromURL, + taskAlert, + waitForTaskUrl, +} from 'src/utilities'; +import { Action } from './action'; + +export const debRepositoryDeleteAction = Action({ + title: msg`Delete`, + modal: ({ addAlert, listQuery, setState, state }) => + state.deleteModalOpen ? ( + setState({ deleteModalOpen: null })} + deleteAction={() => + deleteRepository(state.deleteModalOpen, { + addAlert, + listQuery, + setState, + }) + } + name={state.deleteModalOpen.name} + /> + ) : null, + onClick: ( + { name, id, pulp_href }: { name: string; id?: string; pulp_href?: string }, + { setState }, + ) => + setState({ + deleteModalOpen: { + pulpId: id || parsePulpIDFromURL(pulp_href), + name, + pulp_href, + }, + }), +}); + +async function deleteRepository( + { name, pulp_href, pulpId }, + { addAlert, setState, listQuery }, +) { + // TODO: handle more pages + const distributionsToDelete = await DebDistributionAPI.list({ + repository: pulp_href, + page: 1, + page_size: 100, + }) + .then(({ data: { results } }) => results || []) + .catch((e) => { + handleHttpError( + t`Failed to list distributions, removing only the repository.`, + () => null, + addAlert, + )(e); + return []; + }); + + const deleteRepo = DebRepositoryAPI.delete(pulpId) + .then(({ data }) => { + addAlert(taskAlert(data.task, t`Removal started for repository ${name}`)); + return waitForTaskUrl(data.task); + }) + .catch( + handleHttpError( + t`Failed to remove repository ${name}`, + () => setState({ deleteModalOpen: null }), + addAlert, + ), + ); + + const deleteDistribution = ({ name, pulp_href }) => { + const distribution_id = parsePulpIDFromURL(pulp_href); + return DebDistributionAPI.delete(distribution_id) + .then(({ data }) => { + addAlert( + taskAlert(data.task, t`Removal started for distribution ${name}`), + ); + return waitForTaskUrl(data.task); + }) + .catch( + handleHttpError( + t`Failed to remove distribution ${name}`, + () => null, + addAlert, + ), + ); + }; + + return Promise.all([ + deleteRepo, + ...distributionsToDelete.map(deleteDistribution), + ]).then(() => { + setState({ deleteModalOpen: null }); + listQuery(); + }); +} diff --git a/src/actions/deb-repository-edit.tsx b/src/actions/deb-repository-edit.tsx new file mode 100644 index 00000000..1ffa21a7 --- /dev/null +++ b/src/actions/deb-repository-edit.tsx @@ -0,0 +1,9 @@ +import { msg } from '@lingui/core/macro'; +import { Paths, formatPath } from 'src/paths'; +import { Action } from './action'; + +export const debRepositoryEditAction = Action({ + title: msg`Edit`, + onClick: ({ name }, { navigate }) => + navigate(formatPath(Paths.deb.repository.edit, { name })), +}); diff --git a/src/actions/deb-repository-sync.tsx b/src/actions/deb-repository-sync.tsx new file mode 100644 index 00000000..83dec043 --- /dev/null +++ b/src/actions/deb-repository-sync.tsx @@ -0,0 +1,149 @@ +import { msg, t } from '@lingui/core/macro'; +import { Button, FormGroup, Modal, Switch } from '@patternfly/react-core'; +import { useEffect, useState } from 'react'; +import { DebRepositoryAPI } from 'src/api'; +import { HelpButton, Spinner } from 'src/components'; +import { handleHttpError, parsePulpIDFromURL, taskAlert } from 'src/utilities'; +import { Action } from './action'; + +// as in ansible-repository-sync and file-repository-sync +const SyncModal = ({ + closeAction, + syncAction, + name, +}: { + closeAction: () => null; + syncAction: (syncParams) => Promise; + name: string; +}) => { + const [pending, setPending] = useState(false); + const [syncParams, setSyncParams] = useState({ + mirror: true, + optimize: true, + }); + + useEffect(() => { + setPending(false); + setSyncParams({ mirror: true, optimize: true }); + }, [name]); + + if (!name) { + return null; + } + + return ( + + + , + , + ]} + isOpen + onClose={closeAction} + title={t`Sync repository "${name}"`} + variant='medium' + > + + } + > + + setSyncParams({ ...syncParams, mirror }) + } + label={t`Content not present in remote repository will be removed from the local repository`} + labelOff={t`Sync will only add missing content`} + /> + +
+ + } + > + + setSyncParams({ ...syncParams, optimize }) + } + label={t`Only perform the sync if changes are reported by the remote server.`} + labelOff={t`Force a sync to happen.`} + /> + +
+
+ ); +}; + +export const debRepositorySyncAction = Action({ + title: msg`Sync`, + modal: ({ addAlert, query, setState, state }) => + state.syncModalOpen ? ( + setState({ syncModalOpen: null })} + syncAction={(syncParams) => + syncRepository(state.syncModalOpen, { addAlert, query }, syncParams) + } + name={state.syncModalOpen.name} + /> + ) : null, + onClick: ({ name, pulp_href }, { setState }) => + setState({ + syncModalOpen: { name, pulp_href }, + }), + visible: (_item, { hasPermission }) => + hasPermission('deb.change_aptrepository'), + disabled: ({ remote, last_sync_task }) => { + if (!remote) { + return t`There are no remotes associated with this repository.`; + } + + if ( + last_sync_task && + ['running', 'waiting'].includes(last_sync_task.state) + ) { + return t`Sync task is already queued.`; + } + }, +}); + +function syncRepository({ name, pulp_href }, { addAlert, query }, syncParams) { + const pulpId = parsePulpIDFromURL(pulp_href); + return DebRepositoryAPI.sync(pulpId, syncParams || { mirror: true }) + .then(({ data }) => { + addAlert(taskAlert(data.task, t`Sync started for repository "${name}".`)); + + query(); + }) + .catch( + handleHttpError( + t`Failed to sync repository "${name}"`, + () => null, + addAlert, + ), + ); +} diff --git a/src/actions/index.ts b/src/actions/index.ts index a493b36a..8fac49b5 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -13,6 +13,13 @@ export { ansibleRepositoryDeleteAction } from './ansible-repository-delete'; export { ansibleRepositoryEditAction } from './ansible-repository-edit'; export { ansibleRepositorySyncAction } from './ansible-repository-sync'; export { ansibleRepositoryVersionRevertAction } from './ansible-repository-version-revert'; +export { debRemoteCreateAction } from './deb-remote-create'; +export { debRemoteDeleteAction } from './deb-remote-delete'; +export { debRemoteEditAction } from './deb-remote-edit'; +export { debRepositoryCreateAction } from './deb-repository-create'; +export { debRepositoryDeleteAction } from './deb-repository-delete'; +export { debRepositoryEditAction } from './deb-repository-edit'; +export { debRepositorySyncAction } from './deb-repository-sync'; export { fileRemoteCreateAction } from './file-remote-create'; export { fileRemoteDeleteAction } from './file-remote-delete'; export { fileRemoteEditAction } from './file-remote-edit'; diff --git a/src/api/deb-distribution.ts b/src/api/deb-distribution.ts new file mode 100644 index 00000000..cfcb5a23 --- /dev/null +++ b/src/api/deb-distribution.ts @@ -0,0 +1,11 @@ +import { PulpAPI } from './pulp'; + +const base = new PulpAPI(); + +export const DebDistributionAPI = { + create: (data) => base.http.post(`distributions/deb/apt/`, data), + + delete: (id) => base.http.delete(`distributions/deb/apt/${id}/`), + + list: (params?) => base.list(`distributions/deb/apt/`, params), +}; diff --git a/src/api/deb-remote.ts b/src/api/deb-remote.ts new file mode 100644 index 00000000..32e74716 --- /dev/null +++ b/src/api/deb-remote.ts @@ -0,0 +1,71 @@ +import { PulpAPI } from './pulp'; + +export class DebRemoteType { + architectures: string; + ca_cert: string; + client_cert: string; + components: string; + distributions: string; + download_concurrency: number; + gpgkey: string; + ignore_missing_package_indices?: boolean; + name: string; + proxy_url: string; + pulp_href?: string; + rate_limit: number; + sync_installer?: boolean; + sync_sources?: boolean; + sync_udebs?: boolean; + tls_validation: boolean; + url: string; + + // connect_timeout + // headers + // max_retries + // policy + // prn + // pulp_created + // pulp_labels + // pulp_last_updated + // sock_connect_timeout + // sock_read_timeout + // total_timeout + + hidden_fields: { + is_set: boolean; + name: string; + }[]; + + my_permissions?: string[]; +} + +// as in file-remote +function smartUpdate(remote: DebRemoteType, unmodifiedRemote: DebRemoteType) { + for (const field of Object.keys(remote)) { + if (remote[field] === '') { + remote[field] = null; + } + + // API returns headers:null bull doesn't accept it .. and we don't edit headers + if (remote[field] === null && unmodifiedRemote[field] === null) { + delete remote[field]; + } + } + + return remote; +} + +const base = new PulpAPI(); + +export const DebRemoteAPI = { + create: (data) => base.http.post(`remotes/deb/apt/`, data), + + delete: (id) => base.http.delete(`remotes/deb/apt/${id}/`), + + get: (id) => base.http.get(`remotes/deb/apt/${id}/`), + + list: (params?) => base.list(`remotes/deb/apt/`, params), + + smartUpdate: (id, newValue: DebRemoteType, oldValue: DebRemoteType) => + base.http.put(`remotes/deb/apt/${id}/`, smartUpdate(newValue, oldValue)), +}; diff --git a/src/api/deb-repository.ts b/src/api/deb-repository.ts new file mode 100644 index 00000000..f3d9ff2f --- /dev/null +++ b/src/api/deb-repository.ts @@ -0,0 +1,42 @@ +import { PulpAPI } from './pulp'; + +export class DebRepositoryType { + autopublish?: boolean; + description: string | null; + latest_version_href?: string; + name: string; + prn?: string; + publish_upstream_release_fields?: boolean; + pulp_created?: string; + pulp_href?: string; + pulp_labels: Record; + pulp_last_updated?: string; + remote: string | null; + retain_repo_versions: number; + signing_service?: string | null; + versions_href?: string; +} + +const base = new PulpAPI(); + +export const DebRepositoryAPI = { + create: (data) => base.http.post(`repositories/deb/apt/`, data), + + delete: (id) => base.http.delete(`repositories/deb/apt/${id}/`), + + list: (params?) => base.list(`repositories/deb/apt/`, params), + + listVersions: (id: string, params?) => + base.list(`repositories/deb/apt/${id}/versions/`, params), + + revert: (id: string, version_href) => + base.http.post(`repositories/deb/apt/${id}/modify/`, { + base_version: version_href, + }), + + sync: (id: string, body = {}) => + base.http.post(`repositories/deb/apt/${id}/sync/`, body), + + update: (id: string, data) => + base.http.put(`repositories/deb/apt/${id}/`, data), +}; diff --git a/src/api/index.ts b/src/api/index.ts index d1499327..e39f6c98 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -13,6 +13,9 @@ export { ContainerPullThroughDistributionAPI, } from './container-distribution'; export { ContainerTagAPI } from './container-tag'; +export { DebDistributionAPI } from './deb-distribution'; +export { DebRemoteAPI, type DebRemoteType } from './deb-remote'; +export { DebRepositoryAPI, type DebRepositoryType } from './deb-repository'; export { ExecutionEnvironmentAPI } from './execution-environment'; export { ExecutionEnvironmentNamespaceAPI } from './execution-environment-namespace'; export { ExecutionEnvironmentRegistryAPI } from './execution-environment-registry'; diff --git a/src/api/response-types/remote.ts b/src/api/response-types/remote.ts index e88b7a22..abac5057 100644 --- a/src/api/response-types/remote.ts +++ b/src/api/response-types/remote.ts @@ -34,6 +34,15 @@ export class RemoteType { ca_cert?: string; sync_dependencies?: boolean; + // deb (remotes/deb/apt) only; `distributions` is required there + architectures?: string; + components?: string; + distributions?: string; + gpgkey?: string; + sync_installer?: boolean; + sync_sources?: boolean; + sync_udebs?: boolean; + hidden_fields: { name: string; is_set: boolean }[]; repositories: { diff --git a/src/app-routes.tsx b/src/app-routes.tsx index dd21576b..552578fe 100644 --- a/src/app-routes.tsx +++ b/src/app-routes.tsx @@ -19,6 +19,12 @@ import { CollectionDistributions, CollectionDocs, CollectionImportLog, + DebRemoteDetail, + DebRemoteEdit, + DebRemoteList, + DebRepositoryDetail, + DebRepositoryEdit, + DebRepositoryList, EditNamespace, EditRole, EditUser, @@ -161,6 +167,30 @@ const routes: IRouteConfig[] = [ component: AnsibleRepositoryList, path: Paths.ansible.repository.list, }, + { + component: DebRemoteDetail, + path: Paths.deb.remote.detail, + }, + { + component: DebRemoteEdit, + path: Paths.deb.remote.edit, + }, + { + component: DebRemoteList, + path: Paths.deb.remote.list, + }, + { + component: DebRepositoryDetail, + path: Paths.deb.repository.detail, + }, + { + component: DebRepositoryEdit, + path: Paths.deb.repository.edit, + }, + { + component: DebRepositoryList, + path: Paths.deb.repository.list, + }, { component: FileRemoteDetail, path: Paths.file.remote.detail, diff --git a/src/components/lazy-distributions.tsx b/src/components/lazy-distributions.tsx index 320b5199..68eefd9d 100644 --- a/src/components/lazy-distributions.tsx +++ b/src/components/lazy-distributions.tsx @@ -11,7 +11,7 @@ export const LazyDistributions = ({ repositoryHref, }: { emptyText?: string; - plugin: 'ansible' | 'file' | 'rpm'; + plugin: 'ansible' | 'deb' | 'file' | 'rpm'; repositoryHref: string; }) => { const [distributions, setDistributions] = useState([]); diff --git a/src/components/lazy-repositories.tsx b/src/components/lazy-repositories.tsx index 972ca616..b3aaf150 100644 --- a/src/components/lazy-repositories.tsx +++ b/src/components/lazy-repositories.tsx @@ -14,7 +14,7 @@ export const LazyRepositories = ({ }: { content_href?: string; emptyText?: string; - plugin: 'ansible' | 'file' | 'rpm'; + plugin: 'ansible' | 'deb' | 'file' | 'rpm'; remote_href?: string; }) => { const [repositories, setRepositories] = useState([]); diff --git a/src/components/remote-form.tsx b/src/components/remote-form.tsx index b74df696..45a37736 100644 --- a/src/components/remote-form.tsx +++ b/src/components/remote-form.tsx @@ -37,7 +37,7 @@ interface IProps { allowEditName?: boolean; closeModal: () => void; errorMessages: ErrorMessagesType; - plugin: 'ansible' | 'container' | 'file'; + plugin: 'ansible' | 'container' | 'deb' | 'file'; remote: RemoteType; saveRemote: () => void; showMain?: boolean; @@ -62,6 +62,7 @@ interface IState { client_key: FormFilename; client_cert: FormFilename; ca_cert: FormFilename; + gpgkey: FormFilename; }; } @@ -120,7 +121,7 @@ export class RemoteForm extends Component { constructor(props) { super(props); - const { requirements_file, client_key, client_cert, ca_cert } = + const { requirements_file, client_key, client_cert, ca_cert, gpgkey } = props.remote || {}; this.state = { @@ -141,6 +142,10 @@ export class RemoteForm extends Component { name: ca_cert ? 'ca_cert' : '', original: !!ca_cert, }, + gpgkey: { + name: gpgkey ? 'gpgkey' : '', + original: !!gpgkey, + }, }, }; @@ -169,7 +174,10 @@ export class RemoteForm extends Component { return null; } - const requiredFields = ['name', 'url']; + // pulp_deb rejects a remote with no suites to sync, so unlike every other + // plugin `distributions` is required rather than merely available. + const requiredFields = + plugin === 'deb' ? ['name', 'url', 'distributions'] : ['name', 'url']; let disabledFields = allowEditName ? [] : ['name']; const isCommunityRemote = @@ -181,6 +189,7 @@ export class RemoteForm extends Component { break; case 'container': + case 'deb': case 'file': disabledFields = disabledFields.concat([ 'auth_url', @@ -245,7 +254,7 @@ export class RemoteForm extends Component { isCommunityRemote, }: { extra?: ReactNode; isCommunityRemote: boolean }, ) { - const { errorMessages, remote } = this.props; + const { errorMessages, plugin, remote } = this.props; const { filenames } = this.state; const { collection_signing } = (this.context as IAppContextType) .featureFlags; @@ -329,6 +338,162 @@ export class RemoteForm extends Component { /> + {plugin === 'deb' ? ( + <> + + } + isRequired={requiredFields.includes('distributions')} + > + + this.updateRemote(value, 'distributions') + } + /> + + {errorMessages['distributions']} + + + + + } + > + + this.updateRemote(value, 'components') + } + /> + + {errorMessages['components']} + + + + + } + > + + this.updateRemote(value, 'architectures') + } + /> + + {errorMessages['architectures']} + + + + + } + > + { + this.setState({ + filenames: { + ...filenames, + gpgkey: { name: '', original: false }, + }, + }); + this.updateRemote(null, 'gpgkey'); + }} + /> + + {errorMessages['gpgkey']} + + + + + + this.updateRemote(value, 'sync_sources') + } + label={t`Source packages will be synchronized`} + labelOff={t`Source packages will be skipped`} + /> + + + + + this.updateRemote(value, 'sync_udebs') + } + label={t`Installer packages will be synchronized`} + labelOff={t`Installer packages will be skipped`} + /> + + + + + this.updateRemote(value, 'sync_installer') + } + label={t`Installer files will be synchronized`} + labelOff={t`Installer files will be skipped`} + /> + + + ) : null} + {!disabledFields.includes('signed_only') && collection_signing ? ( void; onSave: ({ createDistribution }) => void; - plugin: 'ansible' | 'file' | 'rpm'; + plugin: 'ansible' | 'deb' | 'file' | 'rpm'; repository: AnsibleRepositoryType; updateRepository: (r) => void; } @@ -114,9 +115,11 @@ export const RepositoryForm = ({ setRemotesError(null); (plugin === 'ansible' ? AnsibleRemoteAPI.list({ ...(name ? { name__icontains: name } : {}) }) - : plugin === 'file' - ? FileRemoteAPI.list({ ...(name ? { name__icontains: name } : {}) }) - : Promise.reject(plugin) + : plugin === 'deb' + ? DebRemoteAPI.list({ ...(name ? { name__icontains: name } : {}) }) + : plugin === 'file' + ? FileRemoteAPI.list({ ...(name ? { name__icontains: name } : {}) }) + : Promise.reject(plugin) ) .then(({ data }) => setRemotes(data.results.map((r) => ({ ...r, id: r.pulp_href }))), diff --git a/src/containers/deb-remote/detail.tsx b/src/containers/deb-remote/detail.tsx new file mode 100644 index 00000000..95bac232 --- /dev/null +++ b/src/containers/deb-remote/detail.tsx @@ -0,0 +1,40 @@ +import { msg, t } from '@lingui/core/macro'; +import { debRemoteDeleteAction, debRemoteEditAction } from 'src/actions'; +import { DebRemoteAPI, type DebRemoteType } from 'src/api'; +import { PageWithTabs } from 'src/components'; +import { Paths, formatPath } from 'src/paths'; +import { DetailsTab } from './tab-details'; + +const DebRemoteDetail = PageWithTabs({ + breadcrumbs: ({ name }) => + [ + { url: formatPath(Paths.deb.remote.list), name: t`Remotes` }, + { url: formatPath(Paths.deb.remote.detail, { name }), name }, + ].filter(Boolean), + displayName: 'DebRemoteDetail', + errorTitle: msg`Remote could not be displayed.`, + headerActions: [debRemoteEditAction, debRemoteDeleteAction], + listUrl: formatPath(Paths.deb.remote.list), + query: ({ name }) => + DebRemoteAPI.list({ name }) + .then(({ data: { results } }) => results[0]) + .then( + (remote) => + remote || + // using the list api, so an empty array is really a 404 + Promise.reject({ response: { status: 404 } }), + ), + renderTab: (tab, item, actionContext) => + ({ + details: , + })[tab], + tabs: (tab, name) => [ + { + active: tab === 'details', + title: t`Details`, + link: formatPath(Paths.deb.remote.detail, { name }, { tab: 'details' }), + }, + ], +}); + +export default DebRemoteDetail; diff --git a/src/containers/deb-remote/edit.tsx b/src/containers/deb-remote/edit.tsx new file mode 100644 index 00000000..31078241 --- /dev/null +++ b/src/containers/deb-remote/edit.tsx @@ -0,0 +1,155 @@ +import { msg, t } from '@lingui/core/macro'; +import { DebRemoteAPI, type DebRemoteType } from 'src/api'; +import { Page, RemoteForm } from 'src/components'; +import { Paths, formatPath } from 'src/paths'; +import { parsePulpIDFromURL, taskAlert } from 'src/utilities'; + +const initialRemote: DebRemoteType = { + name: '', + url: '', + // Required by the API, unlike every other plugin's remote: a deb remote with + // no suites to sync is rejected rather than syncing everything. + distributions: '', + components: null, + architectures: null, + gpgkey: null, + ca_cert: null, + client_cert: null, + tls_validation: true, + proxy_url: null, + download_concurrency: null, + rate_limit: null, + + hidden_fields: [ + 'client_key', + 'proxy_username', + 'proxy_password', + 'username', + 'password', + ].map((name) => ({ name, is_set: false })), +}; + +const DebRemoteEdit = Page({ + breadcrumbs: ({ name }) => + [ + { url: formatPath(Paths.deb.remote.list), name: t`Remotes` }, + name && { url: formatPath(Paths.deb.remote.detail, { name }), name }, + name ? { name: t`Edit` } : { name: t`Add` }, + ].filter(Boolean), + + displayName: 'DebRemoteEdit', + errorTitle: msg`Remote could not be displayed.`, + listUrl: formatPath(Paths.deb.remote.list), + query: ({ name }) => + DebRemoteAPI.list({ name }).then(({ data: { results } }) => results[0]), + title: ({ name }) => name || t`Add new remote`, + transformParams: ({ name, ...rest }) => ({ + ...rest, + name: name !== '_' ? name : null, + }), + + render: (item, { navigate, queueAlert, state, setState }) => { + if (!state.remoteToEdit) { + const remoteToEdit = { + ...initialRemote, + ...item, + }; + setState({ remoteToEdit, errorMessages: {} }); + } + + const { remoteToEdit, errorMessages } = state; + if (!remoteToEdit) { + return null; + } + + const saveRemote = () => { + const { remoteToEdit } = state; + + const data = { ...remoteToEdit }; + + if (!item) { + // prevent "This field may not be blank." when writing in and then deleting username/password/etc + // only when creating, edit diffs with item + Object.keys(data).forEach((k) => { + if (data[k] === '' || data[k] == null) { + delete data[k]; + } + }); + + delete data.hidden_fields; + } + + delete data.my_permissions; + + // api requires traling slash, fix the trivial case + if (data.url && !data.url.includes('?') && !data.url.endsWith('/')) { + data.url += '/'; + } + + const promise = !item + ? DebRemoteAPI.create(data) + : DebRemoteAPI.smartUpdate( + parsePulpIDFromURL(item.pulp_href), + data, + item, + ); + + promise + .then(({ data: task }) => { + setState({ + errorMessages: {}, + remoteToEdit: undefined, + }); + + queueAlert( + item + ? taskAlert(task, t`Update started for remote ${data.name}`) + : { + variant: 'success', + title: t`Successfully created remote ${data.name}`, + }, + ); + + navigate( + formatPath(Paths.deb.remote.detail, { + name: data.name, + }), + ); + }) + .catch(({ response: { data } }) => + setState({ + errorMessages: { + __nofield: data.non_field_errors || data.detail, + ...data, + }, + }), + ); + }; + + const closeModal = () => { + setState({ errorMessages: {}, remoteToEdit: undefined }); + navigate( + item + ? formatPath(Paths.deb.remote.detail, { + name: item.name, + }) + : formatPath(Paths.deb.remote.list), + ); + }; + + return ( + setState({ remoteToEdit: r })} + /> + ); + }, +}); + +export default DebRemoteEdit; diff --git a/src/containers/deb-remote/list.tsx b/src/containers/deb-remote/list.tsx new file mode 100644 index 00000000..56bbb27f --- /dev/null +++ b/src/containers/deb-remote/list.tsx @@ -0,0 +1,79 @@ +import { msg, t } from '@lingui/core/macro'; +import { Td, Tr } from '@patternfly/react-table'; +import { Link } from 'react-router'; +import { + debRemoteCreateAction, + debRemoteDeleteAction, + debRemoteEditAction, +} from 'src/actions'; +import { DebRemoteAPI, type DebRemoteType } from 'src/api'; +import { CopyURL, ListItemActions, ListPage } from 'src/components'; +import { Paths, formatPath } from 'src/paths'; +import { parsePulpIDFromURL } from 'src/utilities'; + +const listItemActions = [ + // Edit + debRemoteEditAction, + // Delete + debRemoteDeleteAction, +]; + +const DebRemoteList = ListPage({ + defaultPageSize: 10, + defaultSort: '-pulp_created', + displayName: 'DebRemoteList', + errorTitle: msg`Remotes could not be displayed.`, + filterConfig: () => [ + { + id: 'name__icontains', + title: t`Remote name`, + }, + ], + headerActions: [debRemoteCreateAction], // Add remote + listItemActions, + noDataButton: debRemoteCreateAction.button, + noDataDescription: msg`Remotes will appear once created.`, + noDataTitle: msg`No remotes yet`, + query: ({ params }) => DebRemoteAPI.list(params), + renderTableRow(item: DebRemoteType, index: number, actionContext) { + const { distributions, name, pulp_href, url } = item; + const id = parsePulpIDFromURL(pulp_href); + + const kebabItems = listItemActions.map((action) => + action.dropdownItem({ ...item, id }, actionContext), + ); + + return ( + + + {name} + + + + + {distributions || '---'} + + + ); + }, + sortHeaders: [ + { + title: msg`Remote name`, + type: 'alpha', + id: 'name', + }, + { + title: msg`URL`, + type: 'alpha', + id: 'url', + }, + { + title: msg`Distributions`, + type: 'none', + id: 'distributions', + }, + ], + title: msg`Remotes`, +}); + +export default DebRemoteList; diff --git a/src/containers/deb-remote/tab-details.tsx b/src/containers/deb-remote/tab-details.tsx new file mode 100644 index 00000000..36e099fd --- /dev/null +++ b/src/containers/deb-remote/tab-details.tsx @@ -0,0 +1,83 @@ +import { t } from '@lingui/core/macro'; +import { type DebRemoteType } from 'src/api'; +import { + CopyURL, + Details, + LazyRepositories, + PulpCodeBlock, +} from 'src/components'; + +interface TabProps { + item: DebRemoteType; + actionContext: object; +} + +const MaybeCode = ({ code, filename }: { code: string; filename: string }) => + code ? : <>{t`None`}; + +export const DetailsTab = ({ item }: TabProps) => ( +
, + }, + // The APT-specific fields. `distributions` is required by the API, the + // other two default to every component / architecture the release offers. + { label: t`Distributions`, value: item?.distributions || t`None` }, + { label: t`Components`, value: item?.components || t`All` }, + { label: t`Architectures`, value: item?.architectures || t`All` }, + { + label: t`Sync sources`, + value: item?.sync_sources ? t`Enabled` : t`Disabled`, + }, + { + label: t`Sync installer packages`, + value: item?.sync_udebs ? t`Enabled` : t`Disabled`, + }, + { + label: t`Sync installer files`, + value: item?.sync_installer ? t`Enabled` : t`Disabled`, + }, + { + label: t`GPG key`, + value: ( + + ), + }, + { + label: t`Proxy URL`, + value: , + }, + { + label: t`TLS validation`, + value: item?.tls_validation ? t`Enabled` : t`Disabled`, + }, + { + label: t`Client certificate`, + value: ( + + ), + }, + { + label: t`CA certificate`, + value: ( + + ), + }, + { + label: t`Download concurrency`, + value: item?.download_concurrency ?? t`None`, + }, + { label: t`Rate limit`, value: item?.rate_limit ?? t`None` }, + { + label: t`Repositories`, + value: , + }, + ]} + /> +); diff --git a/src/containers/deb-repository/detail.tsx b/src/containers/deb-repository/detail.tsx new file mode 100644 index 00000000..1b4c3f39 --- /dev/null +++ b/src/containers/deb-repository/detail.tsx @@ -0,0 +1,138 @@ +import { msg, t } from '@lingui/core/macro'; +import { Trans } from '@lingui/react/macro'; +import { + debRepositoryDeleteAction, + debRepositoryEditAction, + debRepositorySyncAction, +} from 'src/actions'; +import { + DebRemoteAPI, + type DebRemoteType, + DebRepositoryAPI, + type DebRepositoryType, +} from 'src/api'; +import { PageWithTabs } from 'src/components'; +import { Paths, formatPath } from 'src/paths'; +import { + lastSyncStatus, + lastSynced, + parsePulpIDFromURL, + pluginRepositoryBasePath, +} from 'src/utilities'; +import { DetailsTab } from './tab-details'; +import { DistributionsTab } from './tab-distributions'; +import { RepositoryVersionsTab } from './tab-repository-versions'; + +const DebRepositoryDetail = PageWithTabs< + DebRepositoryType & { remote?: DebRemoteType } +>({ + breadcrumbs: ({ name, tab, params: { repositoryVersion } }) => + [ + { url: formatPath(Paths.deb.repository.list), name: t`Repositories` }, + { url: formatPath(Paths.deb.repository.detail, { name }), name }, + tab === 'repository-versions' && repositoryVersion + ? { + url: formatPath(Paths.deb.repository.detail, { name }, { tab }), + name: t`Versions`, + } + : null, + tab === 'repository-versions' && repositoryVersion + ? { name: t`Version ${repositoryVersion}` } + : null, + tab === 'repository-versions' && !repositoryVersion + ? { name: t`Versions` } + : null, + ].filter(Boolean), + displayName: 'DebRepositoryDetail', + errorTitle: msg`Repository could not be displayed.`, + headerActions: [ + debRepositoryEditAction, + debRepositorySyncAction, + debRepositoryDeleteAction, + ], + headerDetails: (item) => ( + <> + {item?.last_sync_task && ( +

+ Last updated from registry {lastSynced(item)}{' '} + {lastSyncStatus(item)} +

+ )} + + ), + listUrl: formatPath(Paths.deb.repository.list), + query: ({ name }) => + DebRepositoryAPI.list({ name, page_size: 1 }) + .then(({ data: { results } }) => results[0]) + .then((repository) => { + // using the list api, so an empty array is really a 404 + if (!repository) { + return Promise.reject({ response: { status: 404 } }); + } + + const err = (val) => (e) => { + console.error(e); + return val; + }; + + return Promise.all([ + // the plugin-aware variant, so the deb distribution endpoint is the + // one consulted + pluginRepositoryBasePath( + 'deb', + repository.name, + repository.pulp_href, + ).catch(err(null)), + repository.remote + ? DebRemoteAPI.get(parsePulpIDFromURL(repository.remote)) + .then(({ data }) => data) + .catch(() => null) + : null, + ]).then(([distroBasePath, remote]) => ({ + ...repository, + distroBasePath, + remote, + })); + }), + renderTab: (tab, item, actionContext) => + ({ + details: , + 'repository-versions': ( + + ), + distributions: ( + + ), + })[tab], + tabs: (tab, name) => [ + { + active: tab === 'details', + title: t`Details`, + link: formatPath( + Paths.deb.repository.detail, + { name }, + { tab: 'details' }, + ), + }, + { + active: tab === 'repository-versions', + title: t`Versions`, + link: formatPath( + Paths.deb.repository.detail, + { name }, + { tab: 'repository-versions' }, + ), + }, + { + active: tab === 'distributions', + title: t`Distributions`, + link: formatPath( + Paths.deb.repository.detail, + { name }, + { tab: 'distributions' }, + ), + }, + ], +}); + +export default DebRepositoryDetail; diff --git a/src/containers/deb-repository/edit.tsx b/src/containers/deb-repository/edit.tsx new file mode 100644 index 00000000..9752aa6b --- /dev/null +++ b/src/containers/deb-repository/edit.tsx @@ -0,0 +1,179 @@ +import { msg, t } from '@lingui/core/macro'; +import { + DebDistributionAPI, + DebRepositoryAPI, + type DebRepositoryType, +} from 'src/api'; +import { Page, RepositoryForm } from 'src/components'; +import { Paths, formatPath } from 'src/paths'; +import { parsePulpIDFromURL, taskAlert } from 'src/utilities'; + +const initialRepository: DebRepositoryType = { + name: '', + description: '', + retain_repo_versions: 1, + pulp_labels: {}, + remote: null, +}; + +const DebRepositoryEdit = Page({ + breadcrumbs: ({ name }) => + [ + { url: formatPath(Paths.deb.repository.list), name: t`Repositories` }, + name && { + url: formatPath(Paths.deb.repository.detail, { name }), + name, + }, + name ? { name: t`Edit` } : { name: t`Add` }, + ].filter(Boolean), + + displayName: 'DebRepositoryEdit', + errorTitle: msg`Repository could not be displayed.`, + listUrl: formatPath(Paths.deb.repository.list), + query: ({ name }) => + DebRepositoryAPI.list({ name }).then(({ data: { results } }) => results[0]), + title: ({ name }) => name || t`Add new repository`, + transformParams: ({ name, ...rest }) => ({ + ...rest, + name: name !== '_' ? name : null, + }), + + render: (item, { navigate, queueAlert, state, setState }) => { + if (!state.repositoryToEdit) { + const repositoryToEdit = { + ...initialRepository, + ...item, + }; + setState({ repositoryToEdit, errorMessages: {} }); + } + + const { repositoryToEdit, errorMessages } = state; + if (!repositoryToEdit) { + return null; + } + + const saveRepository = ({ createDistribution }) => { + const { repositoryToEdit } = state; + + const data = { ...repositoryToEdit }; + + // prevent "This field may not be blank." for nullable fields + Object.keys(data).forEach((k) => { + if (data[k] === '') { + data[k] = null; + } + }); + + if (item) { + delete data.last_sync_task; + delete data.last_synced_metadata_time; + delete data.latest_version_href; + delete data.pulp_created; + delete data.pulp_href; + delete data.versions_href; + } + + data.pulp_labels ||= {}; + + let promise = !item + ? DebRepositoryAPI.create(data).then(({ data: newData }) => { + queueAlert({ + variant: 'success', + title: t`Successfully created repository ${data.name}`, + }); + + return newData.pulp_href; + }) + : DebRepositoryAPI.update( + parsePulpIDFromURL(item.pulp_href), + data, + ).then(({ data: task }) => { + queueAlert( + taskAlert(task, t`Update started for repository ${data.name}`), + ); + + return item.pulp_href; + }); + + if (createDistribution) { + // only alphanumerics, slashes, underscores and dashes are allowed in base_path, transform anything else to _ + const basePathTransform = (name) => + name.replaceAll(/[^-a-zA-Z0-9_/]/g, '_'); + let distributionName = data.name; + + promise = promise + .then((pulp_href) => + DebDistributionAPI.create({ + name: distributionName, + base_path: basePathTransform(distributionName), + repository: pulp_href, + }).catch(() => { + // if distribution already exists, try a numeric suffix to name & base_path + distributionName = + data.name + Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); + return DebDistributionAPI.create({ + name: distributionName, + base_path: basePathTransform(distributionName), + repository: pulp_href, + }); + }), + ) + .then(({ data: task }) => + queueAlert( + taskAlert( + task, + t`Creation started for distribution ${distributionName}`, + ), + ), + ); + } + + promise + .then(() => { + setState({ + errorMessages: {}, + repositoryToEdit: undefined, + }); + + navigate( + formatPath(Paths.deb.repository.detail, { + name: data.name, + }), + ); + }) + .catch(({ response: { data } }) => + setState({ + errorMessages: { + __nofield: data.non_field_errors || data.detail, + ...data, + }, + }), + ); + }; + + const closeModal = () => { + setState({ errorMessages: {}, repositoryToEdit: undefined }); + navigate( + item + ? formatPath(Paths.deb.repository.detail, { + name: item.name, + }) + : formatPath(Paths.deb.repository.list), + ); + }; + + return ( + setState({ repositoryToEdit: r })} + /> + ); + }, +}); + +export default DebRepositoryEdit; diff --git a/src/containers/deb-repository/list.tsx b/src/containers/deb-repository/list.tsx new file mode 100644 index 00000000..c27b5ab0 --- /dev/null +++ b/src/containers/deb-repository/list.tsx @@ -0,0 +1,124 @@ +import { msg, t } from '@lingui/core/macro'; +import { Td, Tr } from '@patternfly/react-table'; +import { Link } from 'react-router'; +import { + debRepositoryCreateAction, + debRepositoryDeleteAction, + debRepositoryEditAction, + debRepositorySyncAction, +} from 'src/actions'; +import { + DebRemoteAPI, + DebRepositoryAPI, + type DebRepositoryType, +} from 'src/api'; +import { + DateComponent, + ListItemActions, + ListPage, + PulpLabels, +} from 'src/components'; +import { Paths, formatPath } from 'src/paths'; +import { parsePulpIDFromURL } from 'src/utilities'; + +const listItemActions = [ + // Edit + debRepositoryEditAction, + // Sync + debRepositorySyncAction, + // Delete + debRepositoryDeleteAction, +]; + +const typeaheadQuery = ({ inputText, selectedFilter, setState }) => { + if (selectedFilter !== 'remote') { + return; + } + + return DebRemoteAPI.list({ name__icontains: inputText }) + .then(({ data: { results } }) => + results.map(({ name, pulp_href }) => ({ id: pulp_href, title: name })), + ) + .then((remotes) => setState({ remotes })); +}; + +const DebRepositoryList = ListPage({ + defaultPageSize: 10, + defaultSort: '-pulp_created', + displayName: 'DebRepositoryList', + errorTitle: msg`Repositories could not be displayed.`, + filterConfig: ({ state: { remotes } }) => [ + { + id: 'name__icontains', + title: t`Repository name`, + }, + { + id: 'pulp_label_select', + title: t`Pulp Label`, + }, + { + id: 'remote', + title: t`Remote`, + inputType: 'typeahead', + options: [ + { + id: 'null', + title: t`None`, + }, + ...(remotes || []), + ], + }, + ], + headerActions: [debRepositoryCreateAction], // Add repository + listItemActions, + noDataButton: debRepositoryCreateAction.button, + noDataDescription: msg`Repositories will appear once created.`, + noDataTitle: msg`No repositories yet`, + query: ({ params }) => DebRepositoryAPI.list(params), + typeaheadQuery, + renderTableRow(item: DebRepositoryType, index: number, actionContext) { + const { name, pulp_created, pulp_href, pulp_labels } = item; + const id = parsePulpIDFromURL(pulp_href); + + const kebabItems = listItemActions.map((action) => + action.dropdownItem({ ...item, id }, actionContext), + ); + + return ( + + + + {name} + + + + + + + + + + + ); + }, + sortHeaders: [ + { + title: msg`Repository name`, + type: 'alpha', + id: 'name', + }, + { + title: msg`Labels`, + type: 'none', + id: 'pulp_labels', + }, + { + title: msg`Created date`, + type: 'numeric', + id: 'pulp_created', + }, + ], + title: msg`Repositories`, +}); + +export default DebRepositoryList; diff --git a/src/containers/deb-repository/tab-details.tsx b/src/containers/deb-repository/tab-details.tsx new file mode 100644 index 00000000..06a07aff --- /dev/null +++ b/src/containers/deb-repository/tab-details.tsx @@ -0,0 +1,65 @@ +import { t } from '@lingui/core/macro'; +import { Link } from 'react-router'; +import { type DebRemoteType, type DebRepositoryType } from 'src/api'; +import { CopyURL, Details, PulpLabels } from 'src/components'; +import { Paths, formatPath } from 'src/paths'; +import { getRepoURL } from 'src/utilities'; + +interface TabProps { + item: DebRepositoryType & { + distroBasePath?: string; + remote?: DebRemoteType; + }; + actionContext: { addAlert: (alert) => void; state: { params } }; +} + +export const DetailsTab = ({ item }: TabProps) => { + return ( +
+ ) : ( + '---' + ), + }, + { + label: t`Labels`, + value: , + }, + { + label: t`Remote`, + value: item?.remote ? ( + + {item?.remote.name} + + ) : ( + t`None` + ), + }, + { + label: t`Autopublish`, + value: item?.autopublish ? t`Enabled` : t`Disabled`, + }, + { + label: t`Publish upstream release fields`, + value: item?.publish_upstream_release_fields + ? t`Enabled` + : t`Disabled`, + }, + ]} + /> + ); +}; diff --git a/src/containers/deb-repository/tab-distributions.tsx b/src/containers/deb-repository/tab-distributions.tsx new file mode 100644 index 00000000..5e4eb656 --- /dev/null +++ b/src/containers/deb-repository/tab-distributions.tsx @@ -0,0 +1,126 @@ +import { t } from '@lingui/core/macro'; +import { Td, Tr } from '@patternfly/react-table'; +import { DebDistributionAPI, type DebRepositoryType } from 'src/api'; +import { ClipboardCopy, DateComponent, DetailList } from 'src/components'; +import { getRepoURL } from 'src/utilities'; + +interface TabProps { + item: DebRepositoryType; + actionContext: { + addAlert: (alert) => void; + state: { params }; + hasPermission; + }; +} + +interface Distribution { + base_path: string; + client_url: string; + content_guard: string; + name: string; + pulp_created: string; + pulp_href: string; + pulp_labels: Record; + repository: string; + repository_version: string; +} + +export const DistributionsTab = ({ + item, + actionContext: { addAlert, hasPermission }, +}: TabProps) => { + const query = ({ params } = { params: null }) => { + const newParams = { ...params }; + newParams.ordering = newParams.sort; + delete newParams.sort; + + return DebDistributionAPI.list({ + repository: item.pulp_href, + ...newParams, + }); + }; + + // A deb remote requires `distributions`, so unlike the file equivalent this + // cannot be a complete command without knowing which suites to sync. + const cliConfig = (base_path) => + `pulp deb remote create --name "${item.name}" --url "${getRepoURL(base_path)}" --distributions ""`; + + const renderTableRow = ( + item: Distribution, + index: number, + _actionContext, + ) => { + const { name, base_path, pulp_created } = item; + + return ( + + {name} + {base_path} + + + + + + {cliConfig(base_path)} + + + + ); + }; + + return ( + + actionContext={{ + addAlert, + query, + hasPermission, + hasObjectPermission: (_p: string): boolean => true, + }} + defaultPageSize={10} + defaultSort={'name'} + errorTitle={t`Distributions could not be displayed.`} + filterConfig={[ + { + id: 'name__icontains', + title: t`Name`, + }, + { + id: 'base_path__icontains', + title: t`Base path`, + }, + ]} + noDataDescription={t`You can edit this repository to create a distribution.`} + noDataTitle={t`No distributions created`} + query={query} + renderTableRow={renderTableRow} + sortHeaders={[ + { + title: t`Name`, + type: 'alpha', + id: 'name', + }, + { + title: t`Base path`, + type: 'alpha', + id: 'base_path', + }, + { + title: t`Created`, + type: 'alpha', + id: 'pulp_created', + }, + { + title: t`CLI configuration`, + type: 'none', + id: '', + }, + ]} + title={t`Distributions`} + /> + ); +}; diff --git a/src/containers/deb-repository/tab-repository-versions.tsx b/src/containers/deb-repository/tab-repository-versions.tsx new file mode 100644 index 00000000..42cbb1b2 --- /dev/null +++ b/src/containers/deb-repository/tab-repository-versions.tsx @@ -0,0 +1,241 @@ +import { t } from '@lingui/core/macro'; +import { Table, Td, Th, Tr } from '@patternfly/react-table'; +import { useEffect, useState } from 'react'; +import { Link } from 'react-router'; +import { DebRepositoryAPI } from 'src/api'; +import { + DateComponent, + DetailList, + Details, + ListItemActions, + Spinner, +} from 'src/components'; +import { Paths, formatPath } from 'src/paths'; +import { parsePulpIDFromURL } from 'src/utilities'; + +interface TabProps { + item; + actionContext: { + addAlert: (alert) => void; + state: { params }; + hasPermission: (string) => boolean; + hasObjectPermission: (string) => boolean; + }; +} + +type ContentSummary = Record< + string, + { + count: number; + href: string; + } +>; + +interface DebRepositoryVersionType { + pulp_href: string; + pulp_created: string; + number: number; + repository: string; + base_version: null; + content_summary: { + added: ContentSummary; + removed: ContentSummary; + present: ContentSummary; + }; +} + +const ContentSummary = ({ data }: { data: object }) => { + if (!Object.keys(data).length) { + return <>{t`None`}; + } + + return ( + + + + + + {Object.entries(data).map(([k, v]) => ( + + + + + ))} +
{t`Count`}{t`Pulp type`}
{v['count']}{k}
+ ); +}; + +const BaseVersion = ({ + repositoryName, + data, +}: { + repositoryName: string; + data?: string; +}) => { + if (!data) { + return <>{t`None`}; + } + + const number = data.split('/').at(-2); + return ( + + {number} + + ); +}; + +export const RepositoryVersionsTab = ({ + item, + actionContext: { addAlert, state, hasPermission, hasObjectPermission }, +}: TabProps) => { + const pulpId = parsePulpIDFromURL(item.pulp_href); + const latest_href = item.latest_version_href; + const repositoryName = item.name; + const queryList = ({ params }) => + DebRepositoryAPI.listVersions(pulpId, params); + const queryDetail = ({ number }) => + DebRepositoryAPI.listVersions(pulpId, { number }); + const [modalState, setModalState] = useState({}); + const [version, setVersion] = useState(null); + + useEffect(() => { + if (state.params.repositoryVersion) { + queryDetail({ number: state.params.repositoryVersion }).then( + ({ data }) => { + if (!data?.results?.[0]) { + addAlert({ + variant: 'danger', + title: t`Failed to find repository version`, + }); + } + setVersion(data.results[0]); + }, + ); + } else { + setVersion(null); + } + }, [state.params.repositoryVersion]); + + const renderTableRow = ( + item: DebRepositoryVersionType, + index: number, + actionContext, + listItemActions, + ) => { + const { number, pulp_created, pulp_href } = item; + + const isLatest = latest_href === pulp_href; + + const kebabItems = listItemActions.map((action) => + action.dropdownItem({ ...item, isLatest, repositoryName }, actionContext), + ); + + return ( + + + + {number} + + {isLatest ? ' ' + t`(latest)` : null} + + + + + + + ); + }; + + return state.params.repositoryVersion ? ( + version ? ( +
, + }, + { + label: t`Content added`, + value: , + }, + { + label: t`Content removed`, + value: , + }, + { + label: t`Current content`, + value: , + }, + { + label: t`Base version`, + value: ( + + ), + }, + ]} + /> + ) : ( + + ) + ) : ( + + actionContext={{ + addAlert, + state: modalState, + setState: setModalState, + query: queryList, + hasPermission, + hasObjectPermission, // needs item=repository, not repository version + }} + defaultPageSize={10} + defaultSort={'-pulp_created'} + errorTitle={t`Repository versions could not be displayed.`} + filterConfig={null} + listItemActions={[]} + noDataButton={null} + noDataDescription={t`Repository versions will appear once the repository is modified.`} + noDataTitle={t`No repository versions yet`} + query={queryList} + renderTableRow={renderTableRow} + sortHeaders={[ + { + title: t`Version number`, + type: 'numeric', + id: 'number', + }, + { + title: t`Created date`, + type: 'numeric', + id: 'pulp_created', + }, + ]} + title={t`Repository versions`} + /> + ); +}; diff --git a/src/containers/index.ts b/src/containers/index.ts index c75db44e..cc1dd5e5 100644 --- a/src/containers/index.ts +++ b/src/containers/index.ts @@ -12,6 +12,12 @@ export { default as CollectionDetail } from './collection-detail/collection-deta export { default as CollectionDistributions } from './collection-detail/collection-distributions'; export { default as CollectionDocs } from './collection-detail/collection-docs'; export { default as CollectionImportLog } from './collection-detail/collection-import-log'; +export { default as DebRemoteDetail } from './deb-remote/detail'; +export { default as DebRemoteEdit } from './deb-remote/edit'; +export { default as DebRemoteList } from './deb-remote/list'; +export { default as DebRepositoryDetail } from './deb-repository/detail'; +export { default as DebRepositoryEdit } from './deb-repository/edit'; +export { default as DebRepositoryList } from './deb-repository/list'; export { default as EditNamespace } from './edit-namespace/edit-namespace'; export { default as ExecutionEnvironmentDetail } from './execution-environment-detail/execution-environment-detail'; export { default as ExecutionEnvironmentDetailAccess } from './execution-environment-detail/execution-environment-detail-access'; diff --git a/src/menu.tsx b/src/menu.tsx index 614e615b..5727d5d1 100644 --- a/src/menu.tsx +++ b/src/menu.tsx @@ -99,6 +99,14 @@ function standaloneMenu() { }), ], ), + menuSection('Pulp deb', { condition: and(loggedIn, hasPlugin('deb')) }, [ + menuItem(t`Repositories`, { + url: formatPath(Paths.deb.repository.list), + }), + menuItem(t`Remotes`, { + url: formatPath(Paths.deb.remote.list), + }), + ]), menuSection('Pulp file', { condition: and(loggedIn, hasPlugin('file')) }, [ menuItem(t`Repositories`, { url: formatPath(Paths.file.repository.list), diff --git a/src/paths.ts b/src/paths.ts index 02a6aff1..9e28bb7a 100644 --- a/src/paths.ts +++ b/src/paths.ts @@ -122,6 +122,18 @@ export const Paths = { profile: '/users/profile', }, }, + deb: { + remote: { + detail: '/deb/remotes/detail/:name', + edit: '/deb/remotes/edit/:name', + list: '/deb/remotes', + }, + repository: { + detail: '/deb/repositories/detail/:name', + edit: '/deb/repositories/edit/:name', + list: '/deb/repositories', + }, + }, file: { remote: { detail: '/file/remotes/detail/:name', diff --git a/src/utilities/plugin-repository-base-path.ts b/src/utilities/plugin-repository-base-path.ts index b11237d4..c6877f88 100644 --- a/src/utilities/plugin-repository-base-path.ts +++ b/src/utilities/plugin-repository-base-path.ts @@ -2,6 +2,8 @@ import { t } from '@lingui/core/macro'; import { AnsibleDistributionAPI, AnsibleRepositoryAPI, + DebDistributionAPI, + DebRepositoryAPI, FileDistributionAPI, FileRepositoryAPI, RPMRepositoryAPI, @@ -20,6 +22,11 @@ export function plugin2api(plugin) { DistributionAPI: AnsibleDistributionAPI, RepositoryAPI: AnsibleRepositoryAPI, }; + case 'deb': + return { + DistributionAPI: DebDistributionAPI, + RepositoryAPI: DebRepositoryAPI, + }; case 'file': return { DistributionAPI: FileDistributionAPI,