From 3a301206b493f9933520af8d9ec22dd8799a3f2c Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Mon, 30 Dec 2024 19:29:09 +0000 Subject: [PATCH 01/11] RepositoryForm -> ContainerRepositoryForm --- .../{repository-form.tsx => container-repository-form.tsx} | 2 +- src/components/index.ts | 2 +- src/containers/execution-environment-detail/base.tsx | 4 ++-- .../execution-environment-list/execution-environment-list.tsx | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) rename src/components/{repository-form.tsx => container-repository-form.tsx} (99%) diff --git a/src/components/repository-form.tsx b/src/components/container-repository-form.tsx similarity index 99% rename from src/components/repository-form.tsx rename to src/components/container-repository-form.tsx index 1ad1f4b..5ba0506 100644 --- a/src/components/repository-form.tsx +++ b/src/components/container-repository-form.tsx @@ -70,7 +70,7 @@ interface IState { formErrors: ErrorMessagesType; } -export class RepositoryForm extends Component { +export class ContainerRepositoryForm extends Component { constructor(props) { super(props); this.state = { diff --git a/src/components/index.ts b/src/components/index.ts index cd601ec..c660d0e 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -19,6 +19,7 @@ export { CollectionListItem } from './collection-list-item'; export { CollectionUsedbyDependenciesList } from './collection-usedby-dependencies-list'; export { CompoundFilter, type FilterOption } from './compound-filter'; export { ConfirmModal } from './confirm-modal'; +export { ContainerRepositoryForm } from './container-repository-form'; export { CopyCollectionToRepositoryModal } from './copy-collection-to-repository-modal'; export { CopyURL } from './copy-url'; export { DarkmodeSwitcher } from './darkmode-switcher'; @@ -114,7 +115,6 @@ export { PurgeTaskModal } from './purge-task-modal'; export { RemoteForm } from './remote-form'; export { RepairTaskModal } from './repair-task-modal'; export { RepositoryBadge } from './repository-badge'; -export { RepositoryForm } from './repository-form'; export { ResourcesForm } from './resources-form'; export { RoleForm } from './role-form'; export { RoleHeader } from './role-header'; diff --git a/src/containers/execution-environment-detail/base.tsx b/src/containers/execution-environment-detail/base.tsx index d8af749..2abd10d 100644 --- a/src/containers/execution-environment-detail/base.tsx +++ b/src/containers/execution-environment-detail/base.tsx @@ -12,12 +12,12 @@ import { AppContext, type IAppContextType } from 'src/app-context'; import { AlertList, type AlertType, + ContainerRepositoryForm, DeleteExecutionEnvironmentModal, ExecutionEnvironmentHeader, LoadingPage, Main, NotFound, - RepositoryForm, StatefulDropdown, closeAlert, } from 'src/components'; @@ -203,7 +203,7 @@ export function withContainerRepo(WrappedComponent) { />
{editing && ( - { const distributionPulpId = pulp?.distribution?.id; const { alerts } = this.state; return ( - Date: Mon, 30 Dec 2024 19:35:42 +0000 Subject: [PATCH 02/11] AnsibleRepositoryForm -> RepositoryForm --- src/components/index.ts | 2 +- .../{ansible-repository-form.tsx => repository-form.tsx} | 2 +- src/containers/ansible-repository/edit.tsx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/components/{ansible-repository-form.tsx => repository-form.tsx} (99%) diff --git a/src/components/index.ts b/src/components/index.ts index c660d0e..75bb6d5 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,6 +1,5 @@ export { AccessTab } from './access-tab'; export { AlertList, type AlertType, closeAlert } from './alert-list'; -export { AnsibleRepositoryForm } from './ansible-repository-form'; export { AppliedFilters } from './applied-filters'; export { ApprovalRow } from './approval-row'; export { ApproveModal } from './approve-modal'; @@ -115,6 +114,7 @@ export { PurgeTaskModal } from './purge-task-modal'; export { RemoteForm } from './remote-form'; export { RepairTaskModal } from './repair-task-modal'; export { RepositoryBadge } from './repository-badge'; +export { RepositoryForm } from './repository-form'; export { ResourcesForm } from './resources-form'; export { RoleForm } from './role-form'; export { RoleHeader } from './role-header'; diff --git a/src/components/ansible-repository-form.tsx b/src/components/repository-form.tsx similarity index 99% rename from src/components/ansible-repository-form.tsx rename to src/components/repository-form.tsx index b7f36bf..7757690 100644 --- a/src/components/ansible-repository-form.tsx +++ b/src/components/repository-form.tsx @@ -32,7 +32,7 @@ interface IProps { updateRepository: (r) => void; } -export const AnsibleRepositoryForm = ({ +export const RepositoryForm = ({ allowEditName, errorMessages, onCancel, diff --git a/src/containers/ansible-repository/edit.tsx b/src/containers/ansible-repository/edit.tsx index 4e7a32a..23207cc 100644 --- a/src/containers/ansible-repository/edit.tsx +++ b/src/containers/ansible-repository/edit.tsx @@ -4,7 +4,7 @@ import { AnsibleRepositoryAPI, type AnsibleRepositoryType, } from 'src/api'; -import { AnsibleRepositoryForm, Page } from 'src/components'; +import { Page, RepositoryForm } from 'src/components'; import { Paths, formatPath } from 'src/paths'; import { parsePulpIDFromURL, taskAlert } from 'src/utilities'; @@ -179,7 +179,7 @@ const AnsibleRepositoryEdit = Page({ }; return ( - Date: Mon, 28 Oct 2024 22:07:17 +0000 Subject: [PATCH 03/11] pulp_file: Repositories & Remotes --- src/actions/file-remote-create.tsx | 9 + src/actions/file-remote-delete.tsx | 44 ++++ src/actions/file-remote-edit.tsx | 9 + src/actions/file-repository-create.tsx | 9 + src/actions/file-repository-delete.tsx | 99 +++++++ src/actions/file-repository-edit.tsx | 9 + src/actions/file-repository-sync.tsx | 148 +++++++++++ src/actions/index.ts | 7 + src/api/ansible-remote.ts | 35 ++- src/api/ansible-repository.ts | 20 ++ src/api/index.ts | 9 +- src/api/response-types/ansible-remote.ts | 33 --- src/api/response-types/ansible-repository.ts | 20 -- src/app-routes.tsx | 36 +++ src/components/remote-form.tsx | 24 +- src/components/repository-form.tsx | 19 +- src/containers/ansible-remote/edit.tsx | 10 +- src/containers/ansible-repository/edit.tsx | 1 + .../execution-environment/registry-list.tsx | 22 +- src/containers/file-remote/detail.tsx | 40 +++ src/containers/file-remote/edit.tsx | 150 +++++++++++ src/containers/file-remote/list.tsx | 75 ++++++ src/containers/file-remote/tab-details.tsx | 69 +++++ src/containers/file-repository/detail.tsx | 134 ++++++++++ src/containers/file-repository/edit.tsx | 181 +++++++++++++ src/containers/file-repository/list.tsx | 124 +++++++++ .../file-repository/tab-details.tsx | 55 ++++ .../file-repository/tab-distributions.tsx | 126 +++++++++ .../tab-repository-versions.tsx | 241 ++++++++++++++++++ src/containers/index.ts | 6 + src/menu.tsx | 8 + src/utilities/index.ts | 1 + src/utilities/plugin-repository-base-path.ts | 71 ++++++ 33 files changed, 1754 insertions(+), 90 deletions(-) create mode 100644 src/actions/file-remote-create.tsx create mode 100644 src/actions/file-remote-delete.tsx create mode 100644 src/actions/file-remote-edit.tsx create mode 100644 src/actions/file-repository-create.tsx create mode 100644 src/actions/file-repository-delete.tsx create mode 100644 src/actions/file-repository-edit.tsx create mode 100644 src/actions/file-repository-sync.tsx delete mode 100644 src/api/response-types/ansible-remote.ts delete mode 100644 src/api/response-types/ansible-repository.ts create mode 100644 src/containers/file-remote/detail.tsx create mode 100644 src/containers/file-remote/edit.tsx create mode 100644 src/containers/file-remote/list.tsx create mode 100644 src/containers/file-remote/tab-details.tsx create mode 100644 src/containers/file-repository/detail.tsx create mode 100644 src/containers/file-repository/edit.tsx create mode 100644 src/containers/file-repository/list.tsx create mode 100644 src/containers/file-repository/tab-details.tsx create mode 100644 src/containers/file-repository/tab-distributions.tsx create mode 100644 src/containers/file-repository/tab-repository-versions.tsx create mode 100644 src/utilities/plugin-repository-base-path.ts diff --git a/src/actions/file-remote-create.tsx b/src/actions/file-remote-create.tsx new file mode 100644 index 0000000..b9c6489 --- /dev/null +++ b/src/actions/file-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 fileRemoteCreateAction = Action({ + title: msg`Add remote`, + onClick: (item, { navigate }) => + navigate(formatPath(Paths.file.remote.edit, { name: '_' })), +}); diff --git a/src/actions/file-remote-delete.tsx b/src/actions/file-remote-delete.tsx new file mode 100644 index 0000000..f037500 --- /dev/null +++ b/src/actions/file-remote-delete.tsx @@ -0,0 +1,44 @@ +import { msg, t } from '@lingui/core/macro'; +import { FileRemoteAPI } from 'src/api'; +import { DeleteRemoteModal } from 'src/components'; +import { + handleHttpError, + parsePulpIDFromURL, + taskAlert, + waitForTaskUrl, +} from 'src/utilities'; +import { Action } from './action'; + +export const fileRemoteDeleteAction = 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 FileRemoteAPI.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/file-remote-edit.tsx b/src/actions/file-remote-edit.tsx new file mode 100644 index 0000000..1910241 --- /dev/null +++ b/src/actions/file-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 fileRemoteEditAction = Action({ + title: msg`Edit`, + onClick: ({ name }, { navigate }) => + navigate(formatPath(Paths.file.remote.edit, { name })), +}); diff --git a/src/actions/file-repository-create.tsx b/src/actions/file-repository-create.tsx new file mode 100644 index 0000000..f03f33e --- /dev/null +++ b/src/actions/file-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 fileRepositoryCreateAction = Action({ + title: msg`Add repository`, + onClick: (item, { navigate }) => + navigate(formatPath(Paths.file.repository.edit, { name: '_' })), +}); diff --git a/src/actions/file-repository-delete.tsx b/src/actions/file-repository-delete.tsx new file mode 100644 index 0000000..5865379 --- /dev/null +++ b/src/actions/file-repository-delete.tsx @@ -0,0 +1,99 @@ +import { msg, t } from '@lingui/core/macro'; +import { FileDistributionAPI, FileRepositoryAPI } from 'src/api'; +import { DeleteRepositoryModal } from 'src/components'; +import { + handleHttpError, + parsePulpIDFromURL, + taskAlert, + waitForTaskUrl, +} from 'src/utilities'; +import { Action } from './action'; + +export const fileRepositoryDeleteAction = 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 FileDistributionAPI.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 = FileRepositoryAPI.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 FileDistributionAPI.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/file-repository-edit.tsx b/src/actions/file-repository-edit.tsx new file mode 100644 index 0000000..ecff934 --- /dev/null +++ b/src/actions/file-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 fileRepositoryEditAction = Action({ + title: msg`Edit`, + onClick: ({ name }, { navigate }) => + navigate(formatPath(Paths.file.repository.edit, { name })), +}); diff --git a/src/actions/file-repository-sync.tsx b/src/actions/file-repository-sync.tsx new file mode 100644 index 0000000..775a7fb --- /dev/null +++ b/src/actions/file-repository-sync.tsx @@ -0,0 +1,148 @@ +import { msg, t } from '@lingui/core/macro'; +import { Button, FormGroup, Modal, Switch } from '@patternfly/react-core'; +import { useEffect, useState } from 'react'; +import { FileRepositoryAPI } from 'src/api'; +import { HelpButton, Spinner } from 'src/components'; +import { handleHttpError, parsePulpIDFromURL, taskAlert } from 'src/utilities'; +import { Action } from './action'; + +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 fileRepositorySyncAction = 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('file.change_collectionremote'), + 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 FileRepositoryAPI.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 dec174a..a493b36 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -13,3 +13,10 @@ 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 { fileRemoteCreateAction } from './file-remote-create'; +export { fileRemoteDeleteAction } from './file-remote-delete'; +export { fileRemoteEditAction } from './file-remote-edit'; +export { fileRepositoryCreateAction } from './file-repository-create'; +export { fileRepositoryDeleteAction } from './file-repository-delete'; +export { fileRepositoryEditAction } from './file-repository-edit'; +export { fileRepositorySyncAction } from './file-repository-sync'; diff --git a/src/api/ansible-remote.ts b/src/api/ansible-remote.ts index 767e14a..98c1ecd 100644 --- a/src/api/ansible-remote.ts +++ b/src/api/ansible-remote.ts @@ -1,5 +1,38 @@ import { PulpAPI } from './pulp'; -import { type AnsibleRemoteType } from './response-types/ansible-remote'; + +export class AnsibleRemoteType { + auth_url: string; + ca_cert: string; + client_cert: string; + download_concurrency: number; + name: string; + proxy_url: string; + pulp_href?: string; + rate_limit: number; + requirements_file: string; + tls_validation: boolean; + url: string; + signed_only: boolean; + sync_dependencies?: boolean; + + // connect_timeout + // headers + // max_retries + // policy + // 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[]; +} // simplified version of smartUpdate from execution-environment-registry function smartUpdate( diff --git a/src/api/ansible-repository.ts b/src/api/ansible-repository.ts index 24bf181..7a0758a 100644 --- a/src/api/ansible-repository.ts +++ b/src/api/ansible-repository.ts @@ -1,4 +1,24 @@ import { PulpAPI } from './pulp'; +import { type LastSyncType } from './response-types/remote'; + +export class AnsibleRepositoryType { + description: string; + last_sync_task?: LastSyncType; + latest_version_href?: string; + name: string; + private?: boolean; + pulp_created?: string; + pulp_href?: string; + pulp_labels?: Record; + remote?: string; + retain_repo_versions: number; + + // gpgkey + // last_synced_metadata_time + // versions_href + + my_permissions?: string[]; +} const base = new PulpAPI(); diff --git a/src/api/index.ts b/src/api/index.ts index db9a2c3..cd7304b 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,7 +1,10 @@ export { ActivitiesAPI } from './activities'; export { AnsibleDistributionAPI } from './ansible-distribution'; -export { AnsibleRemoteAPI } from './ansible-remote'; -export { AnsibleRepositoryAPI } from './ansible-repository'; +export { AnsibleRemoteAPI, type AnsibleRemoteType } from './ansible-remote'; +export { + AnsibleRepositoryAPI, + type AnsibleRepositoryType, +} from './ansible-repository'; export { CertificateUploadAPI } from './certificate-upload'; export { CollectionAPI } from './collection'; export { CollectionVersionAPI } from './collection-version'; @@ -37,8 +40,6 @@ export { OrphanCleanupAPI } from './orphan-cleanup'; export { PulpLoginAPI } from './pulp-login'; export { PulpStatusAPI } from './pulp-status'; export { RepairAPI } from './repair'; -export { type AnsibleRemoteType } from './response-types/ansible-remote'; -export { type AnsibleRepositoryType } from './response-types/ansible-repository'; export { type CollectionDetailType, type CollectionUploadType, diff --git a/src/api/response-types/ansible-remote.ts b/src/api/response-types/ansible-remote.ts deleted file mode 100644 index 88c60a8..0000000 --- a/src/api/response-types/ansible-remote.ts +++ /dev/null @@ -1,33 +0,0 @@ -export class AnsibleRemoteType { - auth_url: string; - ca_cert: string; - client_cert: string; - download_concurrency: number; - name: string; - proxy_url: string; - pulp_href?: string; - rate_limit: number; - requirements_file: string; - tls_validation: boolean; - url: string; - signed_only: boolean; - sync_dependencies?: boolean; - - // connect_timeout - // headers - // max_retries - // policy - // 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[]; -} diff --git a/src/api/response-types/ansible-repository.ts b/src/api/response-types/ansible-repository.ts deleted file mode 100644 index aafd0be..0000000 --- a/src/api/response-types/ansible-repository.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { type LastSyncType } from './remote'; - -export class AnsibleRepositoryType { - description: string; - last_sync_task?: LastSyncType; - latest_version_href?: string; - name: string; - private?: boolean; - pulp_created?: string; - pulp_href?: string; - pulp_labels?: Record; - remote?: string; - retain_repo_versions: number; - - // gpgkey - // last_synced_metadata_time - // versions_href - - my_permissions?: string[]; -} diff --git a/src/app-routes.tsx b/src/app-routes.tsx index 9617058..610ad0e 100644 --- a/src/app-routes.tsx +++ b/src/app-routes.tsx @@ -29,6 +29,12 @@ import { ExecutionEnvironmentList, ExecutionEnvironmentManifest, ExecutionEnvironmentRegistryList, + FileRemoteDetail, + FileRemoteEdit, + FileRemoteList, + FileRepositoryDetail, + FileRepositoryEdit, + FileRepositoryList, GroupDetail, GroupList, LoginPage, @@ -161,6 +167,36 @@ const routes: IRouteConfig[] = [ path: Paths.ansible.repository.list, beta: true, }, + { + component: FileRemoteDetail, + path: Paths.file.remote.detail, + beta: true, + }, + { + component: FileRemoteEdit, + path: Paths.file.remote.edit, + beta: true, + }, + { + component: FileRemoteList, + path: Paths.file.remote.list, + beta: true, + }, + { + component: FileRepositoryDetail, + path: Paths.file.repository.detail, + beta: true, + }, + { + component: FileRepositoryEdit, + path: Paths.file.repository.edit, + beta: true, + }, + { + component: FileRepositoryList, + path: Paths.file.repository.list, + beta: true, + }, { component: UserProfile, path: Paths.core.user.profile, diff --git a/src/components/remote-form.tsx b/src/components/remote-form.tsx index 75b5408..e47a108 100644 --- a/src/components/remote-form.tsx +++ b/src/components/remote-form.tsx @@ -37,11 +37,11 @@ interface IProps { allowEditName?: boolean; closeModal: () => void; errorMessages: ErrorMessagesType; + plugin: 'ansible' | 'container' | 'file'; remote: RemoteType; - remoteType: 'registry' | 'ansible-remote'; saveRemote: () => void; - showModal?: boolean; showMain?: boolean; + showModal?: boolean; title?: string; updateRemote: (remote) => void; } @@ -146,7 +146,7 @@ export class RemoteForm extends Component { // Shim in a default concurrency value to pass form validation if ( - this.props.remoteType !== 'registry' && + this.props.plugin !== 'container' && this.props.remote.download_concurrency === null ) { this.updateRemote(10, 'download_concurrency'); @@ -157,11 +157,11 @@ export class RemoteForm extends Component { const { allowEditName, closeModal, + plugin, remote, saveRemote, showMain, showModal, - remoteType, title, } = this.props; @@ -173,15 +173,15 @@ export class RemoteForm extends Component { let disabledFields = allowEditName ? [] : ['name']; const isCommunityRemote = - remoteType === 'ansible-remote' && - remote?.url === 'https://galaxy.ansible.com/api/'; + plugin === 'ansible' && remote?.url === 'https://galaxy.ansible.com/api/'; - switch (remoteType) { - case 'ansible-remote': - // require only name, url; nothing disabled + switch (plugin) { + case 'ansible': + // nothing disabled break; - case 'registry': + case 'container': + case 'file': disabledFields = disabledFields.concat([ 'auth_url', 'token', @@ -930,7 +930,7 @@ export class RemoteForm extends Component { } private isValid(requiredFields) { - const { remote, remoteType } = this.props; + const { plugin, remote } = this.props; for (const field of requiredFields) { if (!remote[field] || remote[field] === '') { @@ -938,7 +938,7 @@ export class RemoteForm extends Component { } } - if (remoteType === 'ansible-remote') { + if (plugin !== 'container') { // only required in remotes, not registries if (remote.download_concurrency < 1) { return false; diff --git a/src/components/repository-form.tsx b/src/components/repository-form.tsx index 7757690..a85691b 100644 --- a/src/components/repository-form.tsx +++ b/src/components/repository-form.tsx @@ -8,7 +8,11 @@ import { TextInput, } from '@patternfly/react-core'; import { useEffect, useState } from 'react'; -import { AnsibleRemoteAPI, type AnsibleRepositoryType } from 'src/api'; +import { + AnsibleRemoteAPI, + type AnsibleRepositoryType, + FileRemoteAPI, +} from 'src/api'; import { FormFieldHelper, HelpButton, @@ -20,7 +24,7 @@ import { import { type ErrorMessagesType, errorMessage, - repositoryBasePath, + pluginRepositoryBasePath, } from 'src/utilities'; interface IProps { @@ -28,6 +32,7 @@ interface IProps { errorMessages: ErrorMessagesType; onCancel: () => void; onSave: ({ createDistribution }) => void; + plugin: 'ansible' | 'file'; repository: AnsibleRepositoryType; updateRepository: (r) => void; } @@ -37,6 +42,7 @@ export const RepositoryForm = ({ errorMessages, onCancel, onSave, + plugin, repository, updateRepository, }: IProps) => { @@ -106,7 +112,12 @@ export const RepositoryForm = ({ const [remotesError, setRemotesError] = useState(null); const loadRemotes = (name?) => { setRemotesError(null); - AnsibleRemoteAPI.list({ ...(name ? { name__icontains: name } : {}) }) + (plugin === 'ansible' + ? AnsibleRemoteAPI.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 }))), ) @@ -126,7 +137,7 @@ export const RepositoryForm = ({ return; } - repositoryBasePath(repository.name, repository.pulp_href) + pluginRepositoryBasePath(plugin, repository.name, repository.pulp_href) .catch(() => null) .then(onDistributionsLoad); }, [repository?.pulp_href]); diff --git a/src/containers/ansible-remote/edit.tsx b/src/containers/ansible-remote/edit.tsx index 41c573d..1e967be 100644 --- a/src/containers/ansible-remote/edit.tsx +++ b/src/containers/ansible-remote/edit.tsx @@ -152,13 +152,13 @@ const AnsibleRemoteEdit = Page({ return ( setState({ remoteToEdit: r })} - remoteType='ansible-remote' - showMain saveRemote={saveRemote} - errorMessages={errorMessages} - closeModal={closeModal} + showMain + updateRemote={(r) => setState({ remoteToEdit: r })} /> ); }, diff --git a/src/containers/ansible-repository/edit.tsx b/src/containers/ansible-repository/edit.tsx index 23207cc..3638c7e 100644 --- a/src/containers/ansible-repository/edit.tsx +++ b/src/containers/ansible-repository/edit.tsx @@ -184,6 +184,7 @@ const AnsibleRepositoryEdit = Page({ errorMessages={errorMessages} onCancel={closeModal} onSave={saveRepository} + plugin='ansible' repository={repositoryToEdit} updateRepository={(r) => setState({ repositoryToEdit: r })} /> diff --git a/src/containers/execution-environment/registry-list.tsx b/src/containers/execution-environment/registry-list.tsx index a8776c2..6e451f2 100644 --- a/src/containers/execution-environment/registry-list.tsx +++ b/src/containers/execution-environment/registry-list.tsx @@ -160,9 +160,17 @@ class ExecutionEnvironmentRegistryList extends Component { /> {showRemoteFormModal && ( + this.setState({ + remoteToEdit: null, + remoteUnmodified: null, + showRemoteFormModal: false, + }) + } + errorMessages={remoteFormErrors} + plugin='container' remote={remoteToEdit} - remoteType='registry' - updateRemote={(r: RemoteType) => this.setState({ remoteToEdit: r })} saveRemote={() => { const { remoteFormNew, remoteToEdit } = this.state; const newRemote = { ...remoteToEdit }; @@ -200,19 +208,11 @@ class ExecutionEnvironmentRegistryList extends Component { this.setState({ remoteFormErrors: mapErrorMessages(err) }), ); }} - errorMessages={remoteFormErrors} showModal={showRemoteFormModal} - closeModal={() => - this.setState({ - remoteToEdit: null, - remoteUnmodified: null, - showRemoteFormModal: false, - }) - } - allowEditName={remoteFormNew} title={ remoteFormNew ? t`Add remote registry` : t`Edit remote registry` } + updateRemote={(r: RemoteType) => this.setState({ remoteToEdit: r })} /> )} {showDeleteModal && remoteToEdit && ( diff --git a/src/containers/file-remote/detail.tsx b/src/containers/file-remote/detail.tsx new file mode 100644 index 0000000..74c8c96 --- /dev/null +++ b/src/containers/file-remote/detail.tsx @@ -0,0 +1,40 @@ +import { msg, t } from '@lingui/core/macro'; +import { fileRemoteDeleteAction, fileRemoteEditAction } from 'src/actions'; +import { FileRemoteAPI, type FileRemoteType } from 'src/api'; +import { PageWithTabs } from 'src/components'; +import { Paths, formatPath } from 'src/paths'; +import { DetailsTab } from './tab-details'; + +const FileRemoteDetail = PageWithTabs({ + breadcrumbs: ({ name }) => + [ + { url: formatPath(Paths.file.remote.list), name: t`Remotes` }, + { url: formatPath(Paths.file.remote.detail, { name }), name }, + ].filter(Boolean), + displayName: 'FileRemoteDetail', + errorTitle: msg`Remote could not be displayed.`, + headerActions: [fileRemoteEditAction, fileRemoteDeleteAction], + listUrl: formatPath(Paths.file.remote.list), + query: ({ name }) => + FileRemoteAPI.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.file.remote.detail, { name }, { tab: 'details' }), + }, + ], +}); + +export default FileRemoteDetail; diff --git a/src/containers/file-remote/edit.tsx b/src/containers/file-remote/edit.tsx new file mode 100644 index 0000000..43255f7 --- /dev/null +++ b/src/containers/file-remote/edit.tsx @@ -0,0 +1,150 @@ +import { msg, t } from '@lingui/core/macro'; +import { FileRemoteAPI, type FileRemoteType } from 'src/api'; +import { Page, RemoteForm } from 'src/components'; +import { Paths, formatPath } from 'src/paths'; +import { parsePulpIDFromURL, taskAlert } from 'src/utilities'; + +const initialRemote: FileRemoteType = { + name: '', + url: '', + ca_cert: null, + client_cert: null, + tls_validation: true, + proxy_url: null, + download_concurrency: null, + rate_limit: null, + requirements_file: null, + auth_url: null, + signed_only: false, + + hidden_fields: [ + 'client_key', + 'proxy_username', + 'proxy_password', + 'username', + 'password', + 'token', + ].map((name) => ({ name, is_set: false })), +}; + +const FileRemoteEdit = Page({ + breadcrumbs: ({ name }) => + [ + { url: formatPath(Paths.file.remote.list), name: t`Remotes` }, + name && { url: formatPath(Paths.file.remote.detail, { name }), name }, + name ? { name: t`Edit` } : { name: t`Add` }, + ].filter(Boolean), + + displayName: 'FileRemoteEdit', + errorTitle: msg`Remote could not be displayed.`, + listUrl: formatPath(Paths.file.remote.list), + query: ({ name }) => + FileRemoteAPI.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; + delete data.signed_only; + + // api requires traling slash, fix the trivial case + if (data.url && !data.url.includes('?') && !data.url.endsWith('/')) { + data.url += '/'; + } + + const promise = !item + ? FileRemoteAPI.create(data) + : FileRemoteAPI.patch(parsePulpIDFromURL(item.pulp_href), data); + + 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.file.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.file.remote.detail, { + name: item.name, + }) + : formatPath(Paths.file.remote.list), + ); + }; + + return ( + setState({ remoteToEdit: r })} + /> + ); + }, +}); + +export default FileRemoteEdit; diff --git a/src/containers/file-remote/list.tsx b/src/containers/file-remote/list.tsx new file mode 100644 index 0000000..c7e622d --- /dev/null +++ b/src/containers/file-remote/list.tsx @@ -0,0 +1,75 @@ +import { msg, t } from '@lingui/core/macro'; +import { Td, Tr } from '@patternfly/react-table'; +import { Link } from 'react-router'; +import { + fileRemoteCreateAction, + fileRemoteDeleteAction, + fileRemoteEditAction, +} from 'src/actions'; +import { FileRemoteAPI, type FileRemoteType } from 'src/api'; +import { CopyURL, ListItemActions, ListPage } from 'src/components'; +import { Paths, formatPath } from 'src/paths'; +import { parsePulpIDFromURL } from 'src/utilities'; + +const listItemActions = [ + // Edit + fileRemoteEditAction, + // Delete + fileRemoteDeleteAction, +]; + +const FileRemoteList = ListPage({ + defaultPageSize: 10, + defaultSort: '-pulp_created', + displayName: 'FileRemoteList', + errorTitle: msg`Remotes could not be displayed.`, + filterConfig: () => [ + { + id: 'name__icontains', + title: t`Remote name`, + }, + ], + headerActions: [fileRemoteCreateAction], // Add remote + listItemActions, + noDataButton: fileRemoteCreateAction.button, + noDataDescription: msg`Remotes will appear once created.`, + noDataTitle: msg`No remotes yet`, + query: ({ params }) => FileRemoteAPI.list(params), + renderTableRow(item: FileRemoteType, index: number, actionContext) { + const { name, pulp_href, url } = item; + const id = parsePulpIDFromURL(pulp_href); + + const kebabItems = listItemActions.map((action) => + action.dropdownItem({ ...item, id }, actionContext), + ); + + return ( + + + + {name} + + + + + + + + ); + }, + sortHeaders: [ + { + title: msg`Remote name`, + type: 'alpha', + id: 'name', + }, + { + title: msg`URL`, + type: 'alpha', + id: 'url', + }, + ], + title: msg`Remotes`, +}); + +export default FileRemoteList; diff --git a/src/containers/file-remote/tab-details.tsx b/src/containers/file-remote/tab-details.tsx new file mode 100644 index 0000000..bad002d --- /dev/null +++ b/src/containers/file-remote/tab-details.tsx @@ -0,0 +1,69 @@ +import { t } from '@lingui/core/macro'; +import { type FileRemoteType } from 'src/api'; +import { + CopyURL, + Details, + LazyRepositories, + PulpCodeBlock, +} from 'src/components'; + +interface TabProps { + item: FileRemoteType; + actionContext: object; +} + +const MaybeCode = ({ code, filename }: { code: string; filename: string }) => + code ? : <>{t`None`}; + +export const DetailsTab = ({ item }: TabProps) => ( +
, + }, + { + 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: , + }, + { + label: t`YAML requirements`, + value: ( + + ), + }, + ]} + /> +); diff --git a/src/containers/file-repository/detail.tsx b/src/containers/file-repository/detail.tsx new file mode 100644 index 0000000..e6a4c9e --- /dev/null +++ b/src/containers/file-repository/detail.tsx @@ -0,0 +1,134 @@ +import { msg, t } from '@lingui/core/macro'; +import { Trans } from '@lingui/react/macro'; +import { + fileRepositoryDeleteAction, + fileRepositoryEditAction, + fileRepositorySyncAction, +} from 'src/actions'; +import { + FileRemoteAPI, + type FileRemoteType, + FileRepositoryAPI, + type FileRepositoryType, +} from 'src/api'; +import { PageWithTabs } from 'src/components'; +import { Paths, formatPath } from 'src/paths'; +import { + lastSyncStatus, + lastSynced, + parsePulpIDFromURL, + repositoryBasePath, +} from 'src/utilities'; +import { DetailsTab } from './tab-details'; +import { DistributionsTab } from './tab-distributions'; +import { RepositoryVersionsTab } from './tab-repository-versions'; + +const FileRepositoryDetail = PageWithTabs< + FileRepositoryType & { remote?: FileRemoteType } +>({ + breadcrumbs: ({ name, tab, params: { repositoryVersion } }) => + [ + { url: formatPath(Paths.file.repository.list), name: t`Repositories` }, + { url: formatPath(Paths.file.repository.detail, { name }), name }, + tab === 'repository-versions' && repositoryVersion + ? { + url: formatPath(Paths.file.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: 'FileRepositoryDetail', + errorTitle: msg`Repository could not be displayed.`, + headerActions: [ + fileRepositoryEditAction, + fileRepositorySyncAction, + fileRepositoryDeleteAction, + ], + headerDetails: (item) => ( + <> + {item?.last_sync_task && ( +

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

+ )} + + ), + listUrl: formatPath(Paths.file.repository.list), + query: ({ name }) => + FileRepositoryAPI.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([ + repositoryBasePath(repository.name, repository.pulp_href).catch( + err(null), + ), + repository.remote + ? FileRemoteAPI.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.file.repository.detail, + { name }, + { tab: 'details' }, + ), + }, + { + active: tab === 'repository-versions', + title: t`Versions`, + link: formatPath( + Paths.file.repository.detail, + { name }, + { tab: 'repository-versions' }, + ), + }, + { + active: tab === 'distributions', + title: t`Distributions`, + link: formatPath( + Paths.file.repository.detail, + { name }, + { tab: 'distributions' }, + ), + }, + ], +}); + +export default FileRepositoryDetail; diff --git a/src/containers/file-repository/edit.tsx b/src/containers/file-repository/edit.tsx new file mode 100644 index 0000000..4d6d00b --- /dev/null +++ b/src/containers/file-repository/edit.tsx @@ -0,0 +1,181 @@ +import { msg, t } from '@lingui/core/macro'; +import { + FileDistributionAPI, + FileRepositoryAPI, + type FileRepositoryType, +} from 'src/api'; +import { Page, RepositoryForm } from 'src/components'; +import { Paths, formatPath } from 'src/paths'; +import { parsePulpIDFromURL, taskAlert } from 'src/utilities'; + +const initialRepository: FileRepositoryType = { + name: '', + description: '', + retain_repo_versions: 1, + pulp_labels: {}, + remote: null, +}; + +const FileRepositoryEdit = Page({ + breadcrumbs: ({ name }) => + [ + { url: formatPath(Paths.file.repository.list), name: t`Repositories` }, + name && { + url: formatPath(Paths.file.repository.detail, { name }), + name, + }, + name ? { name: t`Edit` } : { name: t`Add` }, + ].filter(Boolean), + + displayName: 'FileRepositoryEdit', + errorTitle: msg`Repository could not be displayed.`, + listUrl: formatPath(Paths.file.repository.list), + query: ({ name }) => + FileRepositoryAPI.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 + ? FileRepositoryAPI.create(data).then(({ data: newData }) => { + queueAlert({ + variant: 'success', + title: t`Successfully created repository ${data.name}`, + }); + + return newData.pulp_href; + }) + : FileRepositoryAPI.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) => + FileDistributionAPI.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 FileDistributionAPI.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.file.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.file.repository.detail, { + name: item.name, + }) + : formatPath(Paths.file.repository.list), + ); + }; + + return ( + setState({ repositoryToEdit: r })} + /> + ); + }, +}); + +export default FileRepositoryEdit; diff --git a/src/containers/file-repository/list.tsx b/src/containers/file-repository/list.tsx new file mode 100644 index 0000000..33fa6e9 --- /dev/null +++ b/src/containers/file-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 { + fileRepositoryCreateAction, + fileRepositoryDeleteAction, + fileRepositoryEditAction, + fileRepositorySyncAction, +} from 'src/actions'; +import { + FileRemoteAPI, + FileRepositoryAPI, + type FileRepositoryType, +} 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 + fileRepositoryEditAction, + // Sync + fileRepositorySyncAction, + // Delete + fileRepositoryDeleteAction, +]; + +const typeaheadQuery = ({ inputText, selectedFilter, setState }) => { + if (selectedFilter !== 'remote') { + return; + } + + return FileRemoteAPI.list({ name__icontains: inputText }) + .then(({ data: { results } }) => + results.map(({ name, pulp_href }) => ({ id: pulp_href, title: name })), + ) + .then((remotes) => setState({ remotes })); +}; + +const FileRepositoryList = ListPage({ + defaultPageSize: 10, + defaultSort: '-pulp_created', + displayName: 'FileRepositoryList', + 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: [fileRepositoryCreateAction], // Add repository + listItemActions, + noDataButton: fileRepositoryCreateAction.button, + noDataDescription: msg`Repositories will appear once created.`, + noDataTitle: msg`No repositories yet`, + query: ({ params }) => FileRepositoryAPI.list(params), + typeaheadQuery, + renderTableRow(item: FileRepositoryType, 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 FileRepositoryList; diff --git a/src/containers/file-repository/tab-details.tsx b/src/containers/file-repository/tab-details.tsx new file mode 100644 index 0000000..9118c1a --- /dev/null +++ b/src/containers/file-repository/tab-details.tsx @@ -0,0 +1,55 @@ +import { t } from '@lingui/core/macro'; +import { Link } from 'react-router'; +import { type FileRemoteType, type FileRepositoryType } from 'src/api'; +import { CopyURL, Details, PulpLabels } from 'src/components'; +import { Paths, formatPath } from 'src/paths'; +import { getRepoURL } from 'src/utilities'; + +interface TabProps { + item: FileRepositoryType & { + distroBasePath?: string; + remote?: FileRemoteType; + }; + 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` + ), + }, + ]} + /> + ); +}; diff --git a/src/containers/file-repository/tab-distributions.tsx b/src/containers/file-repository/tab-distributions.tsx new file mode 100644 index 0000000..fd71a20 --- /dev/null +++ b/src/containers/file-repository/tab-distributions.tsx @@ -0,0 +1,126 @@ +import { t } from '@lingui/core/macro'; +import { Td, Tr } from '@patternfly/react-table'; +import { FileDistributionAPI, type FileRepositoryType } from 'src/api'; +import { ClipboardCopy, DateComponent, DetailList } from 'src/components'; +import { getRepoURL } from 'src/utilities'; + +interface TabProps { + item: FileRepositoryType; + 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 FileDistributionAPI.list({ + repository: item.pulp_href, + ...newParams, + }); + }; + + const cliConfig = (base_path) => + [ + '[galaxy]', + `server_list = ${base_path}`, + '', + `[galaxy_server.${base_path}]`, + `url=${getRepoURL(base_path)}`, + 'token=', + ].join('\n'); + + 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/file-repository/tab-repository-versions.tsx b/src/containers/file-repository/tab-repository-versions.tsx new file mode 100644 index 0000000..d2a9ca0 --- /dev/null +++ b/src/containers/file-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 { FileRepositoryAPI } 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 FileRepositoryVersionType { + 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 }) => + FileRepositoryAPI.listVersions(pulpId, params); + const queryDetail = ({ number }) => + FileRepositoryAPI.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: FileRepositoryVersionType, + 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 0c3bcbd..c75db44 100644 --- a/src/containers/index.ts +++ b/src/containers/index.ts @@ -20,6 +20,12 @@ export { default as ExecutionEnvironmentDetailImages } from './execution-environ export { default as ExecutionEnvironmentList } from './execution-environment-list/execution-environment-list'; export { default as ExecutionEnvironmentManifest } from './execution-environment-manifest/execution-environment-manifest'; export { default as ExecutionEnvironmentRegistryList } from './execution-environment/registry-list'; +export { default as FileRemoteDetail } from './file-remote/detail'; +export { default as FileRemoteEdit } from './file-remote/edit'; +export { default as FileRemoteList } from './file-remote/list'; +export { default as FileRepositoryDetail } from './file-repository/detail'; +export { default as FileRepositoryEdit } from './file-repository/edit'; +export { default as FileRepositoryList } from './file-repository/list'; export { default as GroupDetail } from './group-management/group-detail'; export { default as GroupList } from './group-management/group-list'; export { default as LoginPage } from './login/login'; diff --git a/src/menu.tsx b/src/menu.tsx index b76c6c6..880b30d 100644 --- a/src/menu.tsx +++ b/src/menu.tsx @@ -99,6 +99,14 @@ function standaloneMenu() { }), ], ), + menuSection('Pulp file', { condition: and(loggedIn, hasPlugin('file')) }, [ + menuItem(t`Repositories`, { + url: formatPath(Paths.file.repository.list), + }), + menuItem(t`Remotes`, { + url: formatPath(Paths.file.remote.list), + }), + ]), menuSection('Pulp RPM', { condition: and(loggedIn, hasPlugin('rpm')) }, [ menuItem(t`RPMs`, { url: formatPath(Paths.rpm.package.list), diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 2f396cc..135a249 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -25,6 +25,7 @@ export { parsePulpResource, translatePulpResourceToURL, } from './parse-pulp-id'; +export { pluginRepositoryBasePath } from './plugin-repository-base-path'; export { plugin_versions } from './plugin-version'; export { RepoSigningUtils } from './repo-signing'; export { repositoryBasePath, repositoryDistro } from './repository-distro'; diff --git a/src/utilities/plugin-repository-base-path.ts b/src/utilities/plugin-repository-base-path.ts new file mode 100644 index 0000000..73d2985 --- /dev/null +++ b/src/utilities/plugin-repository-base-path.ts @@ -0,0 +1,71 @@ +import { t } from '@lingui/core/macro'; +import { + AnsibleDistributionAPI, + AnsibleRepositoryAPI, + FileDistributionAPI, + FileRepositoryAPI, +} from 'src/api'; + +// returns the preferred distribution base_path given a repo name +// if there is a distribution with the same name as the repository, it will be used (as long as it's connected to the right repo too) +// if not, the oldest will be used +// reject if no distributions or repository +// optional pulp_href param skips repo lookup + +function plugin2api(plugin) { + switch (plugin) { + case 'ansible': + return { + DistributionAPI: AnsibleDistributionAPI, + RepositoryAPI: AnsibleRepositoryAPI, + }; + case 'file': + return { + DistributionAPI: FileDistributionAPI, + RepositoryAPI: FileRepositoryAPI, + }; + default: + return {}; + } +} + +export function pluginRepositoryBasePath( + plugin, + name, + pulp_href?, +): Promise { + const { RepositoryAPI, DistributionAPI } = plugin2api(plugin); + + return Promise.all([ + pulp_href + ? Promise.resolve({ name, pulp_href }) + : RepositoryAPI.list({ name, page_size: 1 }).then(firstResult), + DistributionAPI.list({ name, page_size: 1 }).then(firstResult), + ]).then(async ([repository, distribution]) => { + if (!repository) { + return Promise.reject(t`Failed to find repository ${name}`); + } + + if (distribution && distribution.repository === repository.pulp_href) { + return distribution.base_path; + } + + distribution = await DistributionAPI.list({ + repository: repository.pulp_href, + sort: 'pulp_created', + page_size: 1, + }).then(firstResult); + + if (!distribution) { + return Promise.reject( + t`Failed to find a distribution for repository ${name}`, + ); + } + + return distribution.base_path; + }); +} + +function firstResult({ data: { results } }) { + return results[0]; +} From ac169bf0fe743a5b0ce7c4e894e6f4c8f27e37f8 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Mon, 30 Dec 2024 21:30:50 +0000 Subject: [PATCH 04/11] {Ansible,File}{Repository,Remote}{Detail,Edit,List} - unbeta --- src/app-routes.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/app-routes.tsx b/src/app-routes.tsx index 610ad0e..fdd494b 100644 --- a/src/app-routes.tsx +++ b/src/app-routes.tsx @@ -140,62 +140,50 @@ const routes: IRouteConfig[] = [ { component: AnsibleRemoteDetail, path: Paths.ansible.remote.detail, - beta: true, }, { component: AnsibleRemoteEdit, path: Paths.ansible.remote.edit, - beta: true, }, { component: AnsibleRemoteList, path: Paths.ansible.remote.list, - beta: true, }, { component: AnsibleRepositoryDetail, path: Paths.ansible.repository.detail, - beta: true, }, { component: AnsibleRepositoryEdit, path: Paths.ansible.repository.edit, - beta: true, }, { component: AnsibleRepositoryList, path: Paths.ansible.repository.list, - beta: true, }, { component: FileRemoteDetail, path: Paths.file.remote.detail, - beta: true, }, { component: FileRemoteEdit, path: Paths.file.remote.edit, - beta: true, }, { component: FileRemoteList, path: Paths.file.remote.list, - beta: true, }, { component: FileRepositoryDetail, path: Paths.file.repository.detail, - beta: true, }, { component: FileRepositoryEdit, path: Paths.file.repository.edit, - beta: true, }, { component: FileRepositoryList, path: Paths.file.repository.list, - beta: true, }, { component: UserProfile, From 8a26149217cdcb1ac592342eb949956612eebc11 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Mon, 30 Dec 2024 21:44:51 +0000 Subject: [PATCH 05/11] file repository distributions: show pulp-cli command to add it from elsewhere --- src/containers/file-repository/tab-distributions.tsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/containers/file-repository/tab-distributions.tsx b/src/containers/file-repository/tab-distributions.tsx index fd71a20..463f748 100644 --- a/src/containers/file-repository/tab-distributions.tsx +++ b/src/containers/file-repository/tab-distributions.tsx @@ -41,14 +41,7 @@ export const DistributionsTab = ({ }; const cliConfig = (base_path) => - [ - '[galaxy]', - `server_list = ${base_path}`, - '', - `[galaxy_server.${base_path}]`, - `url=${getRepoURL(base_path)}`, - 'token=', - ].join('\n'); + `pulp file remote create --name "${item.name}" --url "${getRepoURL(base_path)}"`; const renderTableRow = ( item: Distribution, @@ -65,7 +58,7 @@ export const DistributionsTab = ({ - + {cliConfig(base_path)} From 46813cea08ac3761d955097b43beca27f749cc43 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Mon, 30 Dec 2024 22:15:09 +0000 Subject: [PATCH 06/11] LazyRepositories - support ansible, file, rpm --- src/components/index.ts | 1 - src/components/lazy-repositories.tsx | 40 +++++-- src/components/lazy-rpm-repository.tsx | 106 ------------------ src/containers/ansible-remote/tab-details.tsx | 4 +- src/containers/file-remote/tab-details.tsx | 2 +- .../file-repository/tab-distributions.tsx | 7 +- src/containers/rpm/package-list.tsx | 4 +- src/utilities/index.ts | 5 +- src/utilities/plugin-repository-base-path.ts | 8 +- 9 files changed, 51 insertions(+), 126 deletions(-) delete mode 100644 src/components/lazy-rpm-repository.tsx diff --git a/src/components/index.ts b/src/components/index.ts index 75bb6d5..b953c46 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -53,7 +53,6 @@ export { ImportModal } from './import-modal'; export { LanguageSwitcher } from './language-switcher'; export { LazyDistributions } from './lazy-distributions'; export { LazyRepositories } from './lazy-repositories'; -export { LazyRPMRepository } from './lazy-rpm-repository'; export { LinkTabs, type LinkTabsProps } from './link-tabs'; export { ListItemActions } from './list-item-actions'; export { diff --git a/src/components/lazy-repositories.tsx b/src/components/lazy-repositories.tsx index a9ce123..972ca61 100644 --- a/src/components/lazy-repositories.tsx +++ b/src/components/lazy-repositories.tsx @@ -2,18 +2,20 @@ import { t } from '@lingui/core/macro'; import { Button } from '@patternfly/react-core'; import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; import { useEffect, useState } from 'react'; -import { Link } from 'react-router'; -import { AnsibleRepositoryAPI } from 'src/api'; -import { Spinner, Tooltip } from 'src/components'; +import { MaybeLink, Spinner, Tooltip } from 'src/components'; import { Paths, formatPath } from 'src/paths'; -import { errorMessage } from 'src/utilities'; +import { errorMessage, plugin2api } from 'src/utilities'; export const LazyRepositories = ({ + content_href, emptyText, - remoteHref, + plugin, + remote_href, }: { + content_href?: string; emptyText?: string; - remoteHref: string; + plugin: 'ansible' | 'file' | 'rpm'; + remote_href?: string; }) => { const [repositories, setRepositories] = useState([]); const [count, setCount] = useState(null); @@ -22,7 +24,13 @@ export const LazyRepositories = ({ const [loading, setLoading] = useState(true); const query = (prepend?) => { - AnsibleRepositoryAPI.list({ remote: remoteHref, page, page_size: 10 }) + plugin2api(plugin) + .RepositoryAPI.list({ + ...(content_href ? { with_content: content_href } : null), + ...(remote_href ? { remote: remote_href } : null), + page, + page_size: 10, + }) .then(({ data: { count, results } }) => { setRepositories(prepend ? [...prepend, ...results] : results); setCount(count); @@ -39,7 +47,7 @@ export const LazyRepositories = ({ }; useEffect(() => { - if (!remoteHref) { + if (!remote_href && !content_href) { setRepositories([]); setCount(null); setPage(1); @@ -55,7 +63,7 @@ export const LazyRepositories = ({ setLoading(true); query(); - }, [remoteHref]); + }, [content_href, remote_href]); // support pagination, but page == 1 is handled above useEffect(() => { @@ -78,6 +86,8 @@ export const LazyRepositories = ({ setPage((page) => page + 1); }; + const pluginPaths = Paths[plugin] as Record>; + return loading ? ( ) : error ? ( @@ -87,16 +97,22 @@ export const LazyRepositories = ({ {repositories?.map?.(({ name }, index) => ( <> {index ? ', ' : ''} - + {name} - + ))} {!repositories?.length ? (emptyText ?? '---') : null} {count > repositories?.length ? ( <> {' '} - (more) + {t`(more)`} ) : null} diff --git a/src/components/lazy-rpm-repository.tsx b/src/components/lazy-rpm-repository.tsx deleted file mode 100644 index f61315a..0000000 --- a/src/components/lazy-rpm-repository.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { t } from '@lingui/core/macro'; -import { Button } from '@patternfly/react-core'; -import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; -import { useEffect, useState } from 'react'; -// import { Link } from 'react-router'; -import { RPMRepositoryAPI } from 'src/api'; -import { Spinner, Tooltip } from 'src/components'; -// import { Paths, formatPath } from 'src/paths'; -import { errorMessage } from 'src/utilities'; - -// FIXME: merge with LazyRepositories, parametrize api, query, link -export const LazyRPMRepository = ({ - content_href, -}: { - content_href: string; -}) => { - const [repositories, setRepositories] = useState([]); - const [count, setCount] = useState(null); - const [page, setPage] = useState(1); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(true); - - const query = (prepend?) => { - RPMRepositoryAPI.list({ with_content: content_href, page, page_size: 10 }) - .then(({ data: { count, results } }) => { - setRepositories(prepend ? [...prepend, ...results] : results); - setCount(count); - setError(null); - setLoading(false); - }) - .catch((e) => { - const { status, statusText } = e.response; - setRepositories(prepend || []); - setCount(null); - setError(errorMessage(status, statusText)); - setLoading(false); - }); - }; - - useEffect(() => { - if (!content_href) { - setRepositories([]); - setCount(null); - setPage(1); - setError(null); - setLoading(false); - return; - } - - setRepositories([]); - setCount(null); - setPage(1); - setError(null); - setLoading(true); - - query(); - }, [content_href]); - - // support pagination, but page == 1 is handled above - useEffect(() => { - if (page === 1) { - return; - } - - query(repositories); - }, [page]); - - const errorElement = error && ( - - - - ); - - const loadMore = () => { - setPage((page) => page + 1); - }; - - return loading ? ( - - ) : error ? ( - errorElement - ) : ( - <> - {repositories?.map?.(({ name }, index) => ( - <> - {index ? ', ' : ''} - {name} - {/* FIXME: ansible -> rpm - - {name} - - */} - - ))} - {!repositories?.length ? '?' : null} - {count > repositories?.length ? ( - <> - {' '} - {t`(more)`} - - ) : null} - - ); -}; diff --git a/src/containers/ansible-remote/tab-details.tsx b/src/containers/ansible-remote/tab-details.tsx index 0907147..88b2fbc 100644 --- a/src/containers/ansible-remote/tab-details.tsx +++ b/src/containers/ansible-remote/tab-details.tsx @@ -53,7 +53,9 @@ export const DetailsTab = ({ item }: TabProps) => ( { label: t`Rate limit`, value: item?.rate_limit ?? t`None` }, { label: t`Repositories`, - value: , + value: ( + + ), }, { label: t`YAML requirements`, diff --git a/src/containers/file-remote/tab-details.tsx b/src/containers/file-remote/tab-details.tsx index bad002d..b281f95 100644 --- a/src/containers/file-remote/tab-details.tsx +++ b/src/containers/file-remote/tab-details.tsx @@ -53,7 +53,7 @@ export const DetailsTab = ({ item }: TabProps) => ( { label: t`Rate limit`, value: item?.rate_limit ?? t`None` }, { label: t`Repositories`, - value: , + value: , }, { label: t`YAML requirements`, diff --git a/src/containers/file-repository/tab-distributions.tsx b/src/containers/file-repository/tab-distributions.tsx index 463f748..f6a26ca 100644 --- a/src/containers/file-repository/tab-distributions.tsx +++ b/src/containers/file-repository/tab-distributions.tsx @@ -58,7 +58,12 @@ export const DistributionsTab = ({ - + {cliConfig(base_path)} diff --git a/src/containers/rpm/package-list.tsx b/src/containers/rpm/package-list.tsx index ec409d2..16ccc54 100644 --- a/src/containers/rpm/package-list.tsx +++ b/src/containers/rpm/package-list.tsx @@ -1,7 +1,7 @@ import { msg, t } from '@lingui/core/macro'; import { Td, Tr } from '@patternfly/react-table'; import { RPMPackageAPI } from 'src/api'; -import { LazyRPMRepository, ListItemActions, ListPage } from 'src/components'; +import { LazyRepositories, ListItemActions, ListPage } from 'src/components'; interface RPMPackage { name: string; @@ -42,7 +42,7 @@ const RPMPackageList = ListPage({ {version} {arch} - + diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 135a249..90f0969 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -25,7 +25,10 @@ export { parsePulpResource, translatePulpResourceToURL, } from './parse-pulp-id'; -export { pluginRepositoryBasePath } from './plugin-repository-base-path'; +export { + plugin2api, + pluginRepositoryBasePath, +} from './plugin-repository-base-path'; export { plugin_versions } from './plugin-version'; export { RepoSigningUtils } from './repo-signing'; export { repositoryBasePath, repositoryDistro } from './repository-distro'; diff --git a/src/utilities/plugin-repository-base-path.ts b/src/utilities/plugin-repository-base-path.ts index 73d2985..b11237d 100644 --- a/src/utilities/plugin-repository-base-path.ts +++ b/src/utilities/plugin-repository-base-path.ts @@ -4,6 +4,7 @@ import { AnsibleRepositoryAPI, FileDistributionAPI, FileRepositoryAPI, + RPMRepositoryAPI, } from 'src/api'; // returns the preferred distribution base_path given a repo name @@ -12,7 +13,7 @@ import { // reject if no distributions or repository // optional pulp_href param skips repo lookup -function plugin2api(plugin) { +export function plugin2api(plugin) { switch (plugin) { case 'ansible': return { @@ -24,6 +25,11 @@ function plugin2api(plugin) { DistributionAPI: FileDistributionAPI, RepositoryAPI: FileRepositoryAPI, }; + case 'rpm': + return { + // FIXME: DistributionAPI: RPMDistributionAPI, + RepositoryAPI: RPMRepositoryAPI, + }; default: return {}; } From ba273ded64bd31403ce3f532db2c23f0e63b72ac Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Mon, 30 Dec 2024 22:19:49 +0000 Subject: [PATCH 07/11] LazyDistributions - plugin --- src/components/lazy-distributions.tsx | 18 ++++++++++-------- src/components/repository-form.tsx | 3 ++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/lazy-distributions.tsx b/src/components/lazy-distributions.tsx index 75bf082..320b519 100644 --- a/src/components/lazy-distributions.tsx +++ b/src/components/lazy-distributions.tsx @@ -2,15 +2,16 @@ import { t } from '@lingui/core/macro'; import { Button } from '@patternfly/react-core'; import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; import { useEffect, useState } from 'react'; -import { AnsibleDistributionAPI } from 'src/api'; import { Spinner, Tooltip } from 'src/components'; -import { errorMessage } from 'src/utilities'; +import { errorMessage, plugin2api } from 'src/utilities'; export const LazyDistributions = ({ emptyText, + plugin, repositoryHref, }: { emptyText?: string; + plugin: 'ansible' | 'file' | 'rpm'; repositoryHref: string; }) => { const [distributions, setDistributions] = useState([]); @@ -20,12 +21,13 @@ export const LazyDistributions = ({ const [loading, setLoading] = useState(true); const query = (prepend?) => { - AnsibleDistributionAPI.list({ - repository: repositoryHref, - sort: 'pulp_created', - page, - page_size: 10, - }) + plugin2api(plugin) + .DistributionAPI.list({ + repository: repositoryHref, + sort: 'pulp_created', + page, + page_size: 10, + }) .then(({ data: { count, results } }) => { setDistributions(prepend ? [...prepend, ...results] : results); setCount(count); diff --git a/src/components/repository-form.tsx b/src/components/repository-form.tsx index a85691b..a3e13c6 100644 --- a/src/components/repository-form.tsx +++ b/src/components/repository-form.tsx @@ -32,7 +32,7 @@ interface IProps { errorMessages: ErrorMessagesType; onCancel: () => void; onSave: ({ createDistribution }) => void; - plugin: 'ansible' | 'file'; + plugin: 'ansible' | 'file' | 'rpm'; repository: AnsibleRepositoryType; updateRepository: (r) => void; } @@ -162,6 +162,7 @@ export const RepositoryForm = ({ t`Content in repositories without a distribution will not be visible to clients for sync, download or search.`, <> From 2edbb65ba7709b370028929ee4783b26666fc1c2 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Mon, 30 Dec 2024 22:36:50 +0000 Subject: [PATCH 08/11] RemoteForm: fixup ids --- src/components/remote-form.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/remote-form.tsx b/src/components/remote-form.tsx index e47a108..b74df69 100644 --- a/src/components/remote-form.tsx +++ b/src/components/remote-form.tsx @@ -453,7 +453,7 @@ export class RemoteForm extends Component { 'requirements_file' in errorMessages ? 'error' : 'default' } isRequired={requiredFields.includes('requirements_file')} - id='yaml' + id='requirements_file' type='text' filename={filename('requirements_file')} value={this.props.remote.requirements_file || ''} @@ -741,7 +741,7 @@ export class RemoteForm extends Component { 'client_key' in errorMessages ? 'error' : 'default' } isRequired={requiredFields.includes('client_key')} - id='yaml' + id='client_key' type='text' filename={filename('client_key')} value={this.props.remote.client_key || ''} @@ -773,7 +773,7 @@ export class RemoteForm extends Component { 'client_cert' in errorMessages ? 'error' : 'default' } isRequired={requiredFields.includes('client_cert')} - id='yaml' + id='client_cert' type='text' filename={filename('client_cert')} value={this.props.remote.client_cert || ''} @@ -820,7 +820,7 @@ export class RemoteForm extends Component { Date: Mon, 30 Dec 2024 22:51:23 +0000 Subject: [PATCH 09/11] FileRemote - remote requirements_file, auth_url, signed_only from type --- src/api/file-remote.ts | 5 +---- src/containers/file-remote/edit.tsx | 4 ---- src/containers/file-remote/tab-details.tsx | 9 --------- 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/api/file-remote.ts b/src/api/file-remote.ts index f25b2ab..7324c87 100644 --- a/src/api/file-remote.ts +++ b/src/api/file-remote.ts @@ -1,8 +1,6 @@ import { PulpAPI } from './pulp'; -// FIXME export class FileRemoteType { - auth_url: string; ca_cert: string; client_cert: string; download_concurrency: number; @@ -10,16 +8,15 @@ export class FileRemoteType { proxy_url: string; pulp_href?: string; rate_limit: number; - requirements_file: string; tls_validation: boolean; url: string; - signed_only: boolean; sync_dependencies?: boolean; // connect_timeout // headers // max_retries // policy + // prn // pulp_created // pulp_labels // pulp_last_updated diff --git a/src/containers/file-remote/edit.tsx b/src/containers/file-remote/edit.tsx index 43255f7..1c9c444 100644 --- a/src/containers/file-remote/edit.tsx +++ b/src/containers/file-remote/edit.tsx @@ -13,9 +13,6 @@ const initialRemote: FileRemoteType = { proxy_url: null, download_concurrency: null, rate_limit: null, - requirements_file: null, - auth_url: null, - signed_only: false, hidden_fields: [ 'client_key', @@ -78,7 +75,6 @@ const FileRemoteEdit = Page({ } delete data.my_permissions; - delete data.signed_only; // api requires traling slash, fix the trivial case if (data.url && !data.url.includes('?') && !data.url.endsWith('/')) { diff --git a/src/containers/file-remote/tab-details.tsx b/src/containers/file-remote/tab-details.tsx index b281f95..00f0159 100644 --- a/src/containers/file-remote/tab-details.tsx +++ b/src/containers/file-remote/tab-details.tsx @@ -55,15 +55,6 @@ export const DetailsTab = ({ item }: TabProps) => ( label: t`Repositories`, value: , }, - { - label: t`YAML requirements`, - value: ( - - ), - }, ]} /> ); From 710e5538995bd2497e76af1410d7662e16e2b932 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Mon, 30 Dec 2024 22:52:39 +0000 Subject: [PATCH 10/11] FileRemoteAPI - add smartUpdate, use --- src/api/file-remote.ts | 19 ++++++++++++++++++- src/containers/file-remote/edit.tsx | 6 +++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/api/file-remote.ts b/src/api/file-remote.ts index 7324c87..fc8c486 100644 --- a/src/api/file-remote.ts +++ b/src/api/file-remote.ts @@ -32,6 +32,22 @@ export class FileRemoteType { my_permissions?: string[]; } +// simplified version of smartUpdate from execution-environment-registry +function smartUpdate(remote: FileRemoteType, unmodifiedRemote: FileRemoteType) { + 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 FileRemoteAPI = { @@ -43,5 +59,6 @@ export const FileRemoteAPI = { list: (params?) => base.list(`remotes/file/file/`, params), - patch: (id, data) => base.http.patch(`remotes/file/file/${id}/`, data), + smartUpdate: (id, newValue: FileRemoteType, oldValue: FileRemoteType) => + base.http.put(`remotes/file/file/${id}/`, smartUpdate(newValue, oldValue)), }; diff --git a/src/containers/file-remote/edit.tsx b/src/containers/file-remote/edit.tsx index 1c9c444..bdf314c 100644 --- a/src/containers/file-remote/edit.tsx +++ b/src/containers/file-remote/edit.tsx @@ -83,7 +83,11 @@ const FileRemoteEdit = Page({ const promise = !item ? FileRemoteAPI.create(data) - : FileRemoteAPI.patch(parsePulpIDFromURL(item.pulp_href), data); + : FileRemoteAPI.smartUpdate( + parsePulpIDFromURL(item.pulp_href), + data, + item, + ); promise .then(({ data: task }) => { From 8c8973fb4ca254483f14e1f8043ce3d3efac97f0 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Mon, 30 Dec 2024 23:02:42 +0000 Subject: [PATCH 11/11] Repository/Remote delete: fixup task api url --- src/api/index.ts | 1 - src/api/task-management.ts | 11 ----------- src/api/task.ts | 7 +++++-- src/containers/task-management/task-detail.tsx | 10 +++++----- src/containers/task-management/task-list-view.tsx | 6 +++--- 5 files changed, 13 insertions(+), 22 deletions(-) delete mode 100644 src/api/task-management.ts diff --git a/src/api/index.ts b/src/api/index.ts index cd7304b..d149932 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -81,6 +81,5 @@ export { SignContainersAPI } from './sign-containers'; export { SigningServiceAPI, type SigningServiceType } from './signing-service'; export { TagAPI } from './tag'; export { TaskAPI } from './task'; -export { TaskManagementAPI } from './task-management'; export { TaskPurgeAPI } from './task-purge'; export { UserAPI } from './user'; diff --git a/src/api/task-management.ts b/src/api/task-management.ts deleted file mode 100644 index 7734d93..0000000 --- a/src/api/task-management.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { PulpAPI } from './pulp'; - -const base = new PulpAPI(); - -export const TaskManagementAPI = { - get: (id) => base.http.get(`tasks/${id}/`), - - list: (params?) => base.list(`tasks/`, params), - - patch: (id, data) => base.http.patch(`tasks/${id}/`, data), -}; diff --git a/src/api/task.ts b/src/api/task.ts index 47e1317..661f55f 100644 --- a/src/api/task.ts +++ b/src/api/task.ts @@ -2,7 +2,10 @@ import { PulpAPI } from './pulp'; const base = new PulpAPI(); -// FIXME HubAPI export const TaskAPI = { - get: (id) => base.http.get(`v3/tasks/${id}/`), + get: (id) => base.http.get(`tasks/${id}/`), + + list: (params?) => base.list(`tasks/`, params), + + patch: (id, data) => base.http.patch(`tasks/${id}/`, data), }; diff --git a/src/containers/task-management/task-detail.tsx b/src/containers/task-management/task-detail.tsx index 0aa413c..5b56f99 100644 --- a/src/containers/task-management/task-detail.tsx +++ b/src/containers/task-management/task-detail.tsx @@ -14,7 +14,7 @@ import CubesIcon from '@patternfly/react-icons/dist/esm/icons/cubes-icon'; import { capitalize } from 'lodash'; import { Component, Fragment } from 'react'; import { Link } from 'react-router'; -import { GenericPulpAPI, TaskManagementAPI, type TaskType } from 'src/api'; +import { GenericPulpAPI, TaskAPI, type TaskType } from 'src/api'; import { AlertList, type AlertType, @@ -385,7 +385,7 @@ class TaskDetail extends Component { private cancelTask() { const { task, taskName } = this.state; - TaskManagementAPI.patch(parsePulpIDFromURL(task.pulp_href), { + TaskAPI.patch(parsePulpIDFromURL(task.pulp_href), { state: 'canceled', }) .then(() => { @@ -436,7 +436,7 @@ class TaskDetail extends Component { } const taskId = this.props.routeParams.task; - return TaskManagementAPI.get(taskId) + return TaskAPI.get(taskId) .then((result) => { const allRelatedTasks = []; let parentTask = null; @@ -450,7 +450,7 @@ class TaskDetail extends Component { if (result.data.parent_task) { const parentTaskId = parsePulpIDFromURL(result.data.parent_task); allRelatedTasks.push( - TaskManagementAPI.get(parentTaskId) + TaskAPI.get(parentTaskId) .then((result) => { parentTask = result.data; }) @@ -462,7 +462,7 @@ class TaskDetail extends Component { result.data.child_tasks.forEach((child) => { const childTaskId = parsePulpIDFromURL(child); allRelatedTasks.push( - TaskManagementAPI.get(childTaskId) + TaskAPI.get(childTaskId) .then((result) => { childTasks.push(result.data); }) diff --git a/src/containers/task-management/task-list-view.tsx b/src/containers/task-management/task-list-view.tsx index 22a9582..f1700a3 100644 --- a/src/containers/task-management/task-list-view.tsx +++ b/src/containers/task-management/task-list-view.tsx @@ -12,7 +12,7 @@ import { Link } from 'react-router'; import { OrphanCleanupAPI, RepairAPI, - TaskManagementAPI, + TaskAPI, TaskPurgeAPI, type TaskType, } from 'src/api'; @@ -456,7 +456,7 @@ export class TaskListView extends Component { } private selectedTask({ pulp_href }, name) { - TaskManagementAPI.patch(parsePulpIDFromURL(pulp_href), { + TaskAPI.patch(parsePulpIDFromURL(pulp_href), { state: 'canceled', }) .then(() => { @@ -494,7 +494,7 @@ export class TaskListView extends Component { private queryTasks() { this.setState({ loading: true }, () => { - TaskManagementAPI.list(this.state.params) + TaskAPI.list(this.state.params) .then((result) => { this.setState({ items: result.data.results,