diff --git a/src/actions/file-remote-create.tsx b/src/actions/file-remote-create.tsx new file mode 100644 index 00000000..b9c64890 --- /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 00000000..f037500e --- /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 00000000..19102419 --- /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 00000000..f03f33e5 --- /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 00000000..58653798 --- /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 00000000..ecff9346 --- /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 00000000..775a7fbf --- /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 dec174ae..a493b36a 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 767e14ab..98c1ecdc 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 24bf1819..7a0758a1 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/file-remote.ts b/src/api/file-remote.ts index f25b2ab1..fc8c486e 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 @@ -35,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 = { @@ -46,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/api/index.ts b/src/api/index.ts index db9a2c3f..d1499327 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, @@ -80,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/response-types/ansible-remote.ts b/src/api/response-types/ansible-remote.ts deleted file mode 100644 index 88c60a83..00000000 --- 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 aafd0bee..00000000 --- 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/api/task-management.ts b/src/api/task-management.ts deleted file mode 100644 index 7734d936..00000000 --- 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 47e1317d..661f55f3 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/app-routes.tsx b/src/app-routes.tsx index 9617058f..fdd494be 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, @@ -134,32 +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, + }, + { + component: FileRemoteEdit, + path: Paths.file.remote.edit, + }, + { + component: FileRemoteList, + path: Paths.file.remote.list, + }, + { + component: FileRepositoryDetail, + path: Paths.file.repository.detail, + }, + { + component: FileRepositoryEdit, + path: Paths.file.repository.edit, + }, + { + component: FileRepositoryList, + path: Paths.file.repository.list, }, { component: UserProfile, diff --git a/src/components/ansible-repository-form.tsx b/src/components/ansible-repository-form.tsx deleted file mode 100644 index b7f36bfc..00000000 --- a/src/components/ansible-repository-form.tsx +++ /dev/null @@ -1,280 +0,0 @@ -import { t } from '@lingui/core/macro'; -import { - ActionGroup, - Button, - Checkbox, - Form, - FormGroup, - TextInput, -} from '@patternfly/react-core'; -import { useEffect, useState } from 'react'; -import { AnsibleRemoteAPI, type AnsibleRepositoryType } from 'src/api'; -import { - FormFieldHelper, - HelpButton, - LazyDistributions, - PulpLabels, - Spinner, - Typeahead, -} from 'src/components'; -import { - type ErrorMessagesType, - errorMessage, - repositoryBasePath, -} from 'src/utilities'; - -interface IProps { - allowEditName: boolean; - errorMessages: ErrorMessagesType; - onCancel: () => void; - onSave: ({ createDistribution }) => void; - repository: AnsibleRepositoryType; - updateRepository: (r) => void; -} - -export const AnsibleRepositoryForm = ({ - allowEditName, - errorMessages, - onCancel, - onSave, - repository, - updateRepository, -}: IProps) => { - const requiredFields = []; - const disabledFields = allowEditName ? [] : ['name']; - - const formGroup = (fieldName, label, helperText, children) => ( - - {label} - - ) : ( - label - ) - } - isRequired={requiredFields.includes(fieldName)} - > - {children} - - {errorMessages[fieldName]} - - - ); - const inputField = (fieldName, label, helperText, props) => - formGroup( - fieldName, - label, - helperText, - - updateRepository({ ...repository, [fieldName]: value }) - } - {...props} - />, - ); - const stringField = (fieldName, label, helperText?) => - inputField(fieldName, label, helperText, { type: 'text' }); - const numericField = (fieldName, label, helperText?) => - inputField(fieldName, label, helperText, { type: 'number' }); - - const isValid = !requiredFields.find((field) => !repository[field]); - - const [createDistribution, setCreateDistribution] = useState(true); - const [disabledDistribution, setDisabledDistribution] = useState(false); - const onDistributionsLoad = (distroBasePath) => { - if (distroBasePath) { - setCreateDistribution(false); - setDisabledDistribution(true); - } else { - setCreateDistribution(true); - setDisabledDistribution(false); - } - }; - - const [remotes, setRemotes] = useState(null); - const [remotesError, setRemotesError] = useState(null); - const loadRemotes = (name?) => { - setRemotesError(null); - AnsibleRemoteAPI.list({ ...(name ? { name__icontains: name } : {}) }) - .then(({ data }) => - setRemotes(data.results.map((r) => ({ ...r, id: r.pulp_href }))), - ) - .catch((e) => { - const { status, statusText } = e.response; - setRemotes([]); - setRemotesError(errorMessage(status, statusText)); - }); - }; - - useEffect(() => loadRemotes(), []); - - useEffect(() => { - // create - if (!repository || !repository.name) { - onDistributionsLoad(null); - return; - } - - repositoryBasePath(repository.name, repository.pulp_href) - .catch(() => null) - .then(onDistributionsLoad); - }, [repository?.pulp_href]); - - const selectedRemote = remotes?.find?.( - ({ pulp_href }) => pulp_href === repository?.remote, - ); - - return ( -
- {stringField('name', t`Name`)} - {stringField('description', t`Description`)} - {numericField( - 'retain_repo_versions', - t`Retained number of versions`, - t`In order to retain all versions, leave this field blank.`, - )} - - {formGroup( - 'distributions', - t`Distributions`, - t`Content in repositories without a distribution will not be visible to clients for sync, download or search.`, - <> - -
- setCreateDistribution(value)} - label={t`Create a "${repository.name}" distribution`} - id='create_distribution' - /> - , - )} - - {formGroup( - 'pulp_labels', - t`Labels`, - t`Pulp labels in the form of 'key:value'.`, - <> -
{ - e.preventDefault(); - e.stopPropagation(); - }} - > - - updateRepository({ ...repository, pulp_labels: labels }) - } - /> -
- , - )} - - {formGroup( - 'private', - t`Make private`, - t`Make the repository private.`, - - updateRepository({ ...repository, private: value }) - } - />, - )} - - {formGroup( - 'remote', - t`Remote`, - t`Setting a remote allows a repository to sync from elsewhere.`, - <> -
- {remotes ? ( - - updateRepository({ ...repository, remote: null }) - } - onSelect={(_event, value) => - updateRepository({ - ...repository, - remote: remotes.find(({ name }) => name === value) - ?.pulp_href, - }) - } - placeholderText={t`Select a remote`} - results={remotes} - selections={ - selectedRemote - ? [ - { - name: selectedRemote.name, - id: selectedRemote.pulp_href, - }, - ] - : [] - } - /> - ) : null} - {remotesError ? ( - - {t`Failed to load remotes: ${remotesError}`} - - ) : null} - {!remotes && !remotesError ? : null} -
- , - )} - - {errorMessages['__nofield'] ? ( - - {errorMessages['__nofield']} - - ) : null} - - - - - - - ); -}; diff --git a/src/components/container-repository-form.tsx b/src/components/container-repository-form.tsx new file mode 100644 index 00000000..5ba05060 --- /dev/null +++ b/src/components/container-repository-form.tsx @@ -0,0 +1,534 @@ +import { t } from '@lingui/core/macro'; +import { + Button, + Form, + FormGroup, + InputGroup, + InputGroupItem, + Label, + Modal, + TextArea, + TextInput, +} from '@patternfly/react-core'; +import TagIcon from '@patternfly/react-icons/dist/esm/icons/tag-icon'; +import { Component } from 'react'; +import { + ContainerDistributionAPI, + ExecutionEnvironmentRegistryAPI, + ExecutionEnvironmentRemoteAPI, +} from 'src/api'; +import { + AlertList, + type AlertType, + FormFieldHelper, + HelpButton, + LabelGroup, + Spinner, + Typeahead, + closeAlert, +} from 'src/components'; +import { + type ErrorMessagesType, + alertErrorsWithoutFields, + isFieldValid, + isFormValid, + jsxErrorMessage, + mapErrorMessages, +} from 'src/utilities'; + +interface IProps { + name: string; + namespace: string; + description: string; + onSave: (Promise, state?: IState) => void; + onCancel: () => void; + permissions: string[]; + distributionPulpId: string; + + // remote only + isNew?: boolean; + isRemote?: boolean; + excludeTags?: string[]; + includeTags?: string[]; + registry?: string; // pk + upstreamName?: string; + remoteId?: string; + addAlert?: (variant, title, description?) => void; +} + +interface IState { + name: string; + description: string; + alerts: AlertType[]; + addTagsInclude: string; + addTagsExclude: string; + excludeTags?: string[]; + includeTags?: string[]; + registries?: { id: string; name: string }[]; + registrySelection?: { id: string; name: string }[]; + upstreamName: string; + formErrors: ErrorMessagesType; +} + +export class ContainerRepositoryForm extends Component { + constructor(props) { + super(props); + this.state = { + name: this.props.name || '', + description: this.props.description, + + addTagsInclude: '', + addTagsExclude: '', + excludeTags: this.props.excludeTags, + includeTags: this.props.includeTags, + registries: null, + registrySelection: [], + upstreamName: this.props.upstreamName || '', + formErrors: {}, + alerts: [], + }; + } + + componentDidMount() { + if (this.props.isRemote) { + this.loadRegistries() + .then((registries) => { + // prefill registry if passed from props + if (this.props.registry) { + this.setState({ + registrySelection: registries.filter( + ({ id }) => id === this.props.registry, + ), + }); + } + }) + .catch((e) => { + const { status, statusText } = e.response; + const errorTitle = t`Registries list could not be displayed.`; + this.addAlert({ + variant: 'danger', + title: errorTitle, + description: jsxErrorMessage(status, statusText), + }); + this.setState({ + formErrors: { ...this.state.formErrors, registries: errorTitle }, + }); + }); + } + } + + render() { + const { onSave, onCancel, namespace, isNew, isRemote } = this.props; + const { + addTagsExclude, + addTagsInclude, + alerts, + description, + excludeTags, + formErrors, + includeTags, + name, + registries, + registrySelection, + upstreamName, + } = this.state; + + return ( + onSave(this.onSave(), this.state)} + > + {t`Save`} + , + , + ]} + > + + closeAlert(i, { + alerts, + setAlerts: (alerts) => this.setState({ alerts }), + }) + } + /> +
+ {!isRemote ? ( + <> + + + + + + + + + ) : ( + <> + + { + this.setState({ name: value }); + this.validateName(value); + }} + validated={isFieldValid(this.state.formErrors, 'name')} + /> + + {this.state.formErrors['name']} + + + + + } + > + + this.setState({ upstreamName: value }) + } + /> + + + + {!formErrors?.registries && ( + <> + {registries ? ( + this.loadRegistries(name)} + onClear={() => this.setState({ registrySelection: [] })} + onSelect={(event, value) => + this.setState({ + registrySelection: registries.filter( + ({ name }) => name === value, + ), + formErrors: { ...formErrors, registry: null }, + }) + } + placeholderText={t`Select a registry`} + results={registries} + selections={registrySelection} + /> + ) : ( + + )} + + {this.state.formErrors['registries'] || + this.state.formErrors['registry']} + + + )} + + + + + + + this.setState({ addTagsInclude: val }) + } + onKeyUp={(e) => { + // l10n: don't translate + if (e.key === 'Enter') { + this.addTags(addTagsInclude, 'includeTags'); + } + }} + /> + + + + + + + + + + {includeTags.map((tag) => ( + + ))} + {!includeTags.length ? t`None` : null} + + + + + + + + this.setState({ addTagsExclude: val }) + } + onKeyUp={(e) => { + // l10n: don't translate + if (e.key === 'Enter') { + this.addTags(addTagsExclude, 'excludeTags'); + } + }} + /> + + + + + + + + + + {excludeTags.map((tag) => ( + + ))} + {!excludeTags.length ? t`None` : null} + + + + {!excludeTags.length && !includeTags.length ? ( + + + {t`Including all tags might transfer a lot of data.`}{' '} + + + + ) : null} + + )} + + +