diff --git a/src/api/controller.ts b/src/api/controller.ts deleted file mode 100644 index c4e24a184d..0000000000 --- a/src/api/controller.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { HubAPI } from './hub'; - -export class API extends HubAPI { - apiPath = '_ui/v1/controllers/'; - - // list(params?) -} - -export const ControllerAPI = new API(); diff --git a/src/api/index.ts b/src/api/index.ts index db4c579190..29ae1dd42d 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -10,7 +10,6 @@ export { CollectionAPI } from './collection'; export { CollectionVersionAPI } from './collection-version'; export { ContainerDistributionAPI } from './container-distribution'; export { ContainerTagAPI } from './container-tag'; -export { ControllerAPI } from './controller'; export { ExecutionEnvironmentAPI } from './execution-environment'; export { ExecutionEnvironmentNamespaceAPI } from './execution-environment-namespace'; export { ExecutionEnvironmentRegistryAPI } from './execution-environment-registry'; diff --git a/src/components/index.ts b/src/components/index.ts index 5585e2f34f..c814c97a6d 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -106,7 +106,6 @@ export { PermissionCategories } from './permission-categories'; export { PermissionChipSelector } from './permission-chip-selector'; export { PreviewRoles } from './preview-roles'; export { ProviderLink } from './provider-link'; -export { PublishToControllerModal } from './publish-to-controller-modal'; export { PulpLabels } from './pulp-labels'; export { CollectionRatings, RoleRatings } from './ratings'; export { RemoteForm } from './remote-form'; diff --git a/src/components/publish-to-controller-modal.tsx b/src/components/publish-to-controller-modal.tsx deleted file mode 100644 index 9e54c543dc..0000000000 --- a/src/components/publish-to-controller-modal.tsx +++ /dev/null @@ -1,380 +0,0 @@ -import { Trans, t } from '@lingui/macro'; -import { - Button, - DescriptionList, - DescriptionListDescription, - DescriptionListGroup, - DescriptionListTerm, - Flex, - FlexItem, - List, - ListItem, - Modal, -} from '@patternfly/react-core'; -import TagIcon from '@patternfly/react-icons/dist/esm/icons/tag-icon'; -import React, { useEffect, useState } from 'react'; -import { ControllerAPI, ExecutionEnvironmentAPI } from 'src/api'; -import { - AlertList, - AppliedFilters, - CompoundFilter, - EmptyStateFilter, - EmptyStateNoData, - ExternalLink, - HubCopyButton, - HubPagination, - LoadingSpinner, - ShaLabel, - Typeahead, - closeAlert, -} from 'src/components'; -import { errorMessage, filterIsSet, getContainersURL } from 'src/utilities'; - -interface IProps { - image: string; - digest?: string; - isOpen: boolean; - onClose: () => void; - tag?: string; -} - -const initialState = { - alerts: [], - controllers: null, - controllerCount: 0, - controllerParams: { page: 1, page_size: 10 }, - digest: null, - digestByTag: {}, - loading: true, - tag: null, - tagResults: [], - tagSelection: [], - inputText: '', -}; - -export const PublishToControllerModal = (props: IProps) => { - const [alerts, setAlerts] = useState(initialState.alerts); - const [controllers, setControllers] = useState(initialState.controllers); - const [controllerCount, setControllerCount] = useState( - initialState.controllerCount, - ); - const [controllerParams, setControllerParams] = useState( - initialState.controllerParams, - ); - const [digest, setDigest] = useState(initialState.digest); - const [digestByTag, setDigestByTag] = useState(initialState.digestByTag); - const [loading, setLoading] = useState(initialState.loading); - const [tag, setTag] = useState(initialState.tag); - const [tagResults, setTagResults] = useState(initialState.tagResults); - const [tagSelection, setTagSelection] = useState(initialState.tagSelection); - - const [inputText, setInputText] = useState(initialState.inputText); - - useEffect(() => { - const { image, isOpen } = props; - if (isOpen) { - // load on open - fetchData(image); - } else { - // reset on close - setAlerts(initialState.alerts); - setControllers(initialState.controllers); - setControllerCount(initialState.controllerCount); - setControllerParams(initialState.controllerParams); - setDigest(initialState.digest); - setDigestByTag(initialState.digestByTag); - setLoading(initialState.loading); - setTag(initialState.tag); - setTagResults(initialState.tagResults); - setTagSelection(initialState.tagSelection); - - setInputText(initialState.inputText); - } - }, [props.isOpen]); - - useEffect(() => { - fetchControllers(); - }, [controllerParams]); - - function fetchControllers() { - return ControllerAPI.list(controllerParams) - .then(({ data }) => { - const controllers = data.data.map((c) => c.host); - const controllerCount = data.meta.count; - - setControllers(controllers); - setControllerCount(controllerCount); - - return controllers; - }) - .catch((e) => { - const { status, statusText } = e.response; - setAlerts([ - ...alerts, - { - variant: 'danger', - title: t`Controllers list could not be displayed.`, - description: errorMessage(status, statusText), - }, - ]); - }); - } - - function fetchTags(image, name?) { - // filter tags by digest when provided from Images list - const { digest } = props; - - return ExecutionEnvironmentAPI.tags(image, { - sort: '-created_at', - ...(digest ? { tagged_manifest__digest: digest } : {}), - ...(name ? { name__icontains: name } : {}), - }) - .then(({ data }) => { - const tags = data.data.map( - ({ name: tag, tagged_manifest: { digest } }) => ({ digest, tag }), - ); - - const digestByTag = {}; - tags.forEach(({ digest, tag }) => (digestByTag[tag] = digest)); - - const tagResults = tags.map(({ tag }) => ({ id: tag, name: tag })); - - setDigestByTag(digestByTag); - setTagResults(tagResults); - - return { digestByTag, tags }; - }) - .catch((e) => { - const { status, statusText } = e.response; - setAlerts([ - ...alerts, - { - variant: 'danger', - title: t`Tags could not be displayed.`, - description: errorMessage(status, statusText), - }, - ]); - }); - } - - function fetchData(image) { - const controllers = fetchControllers(); - const tagsPromises = fetchTags(image).then(({ tags, digestByTag }) => { - // tags and digestByTag must be passed this way from fetchTags, otherwise, closure - // will see old value of both variables set in fetchTags - // and additionaly, tags state is not needed at all because of that - - // preselect tag if present - let { digest, tag } = props; - tag ||= tags[0]?.tag; // default to first tag unless in props (tags already filtered by digest if in props) - digest ||= digestByTag[tag]; // set digest by tag unless in props - - setDigest(digest); - setTag(tag); - setTagSelection(tag ? [{ id: tag, name: tag }] : []); - }); - - Promise.all([controllers, tagsPromises]).then(() => { - setLoading(false); - }); - } - - function renderControllers() { - const { image, isOpen } = props; - - if (!isOpen || !controllers) { - return null; - } - - if (controllers.length === 0) { - // EmptyStateNoData already handled in render() - return ; - } - - if (!digest && !tag) { - return t`No tag or digest selected.`; - } - - const imageUrl = encodeURIComponent( - getContainersURL({ - name: image, - tag, - digest, - }), - ); - - return ( - - {controllers.map((host) => { - const href = `${host}/#/execution_environments/add?image=${imageUrl}`; - - return ( - - {host} - - - ); - })} - - ); - } - - const { image, isOpen, onClose } = props; - - // redirects to ./2.x (latest) - const docsLink = UI_DOCS_URL; - const noData = - controllers?.length === 0 && - !filterIsSet(controllerParams, ['host__icontains']); - - const notListedMessage = ( - <> - {t`If the Controller is not listed in the table, check settings.py.`}{' '} - {docsLink && {t`Learn more`}} - - ); - - const Spacer = () =>
; - - return ( - - {t`Close`} - , - ]} - > - closeAlert(i, { alerts, setAlerts })} - /> - {loading && ( -
- -
- )} - {noData && !loading ? ( - - ) : null} - - {isOpen && !loading && !noData && controllers && ( - <> - - - {t`Execution Environment`} - {image} - - - {t`Tag`} - - - - fetchTags(image, name)} - onClear={() => { - setTag(null); - setTagSelection([]); - }} - onSelect={(event, value) => { - const digest = digestByTag[value.toString()]; - setTag(digest && value.toString()); - setTagSelection([{ id: value, name: value }]); - setDigest(digest); - }} - placeholderText={t`Select a tag`} - results={tagResults} - selections={tagSelection} - toggleIcon={} - /> - - - - - - {digest && ( - <> - - {t`Digest`} - - - - - - )} - - - - Click on the Controller URL that you want to use the above execution - environment in, and it will launch that Controller's console. - Log in (if necessary) and follow the steps to complete the - configuration. - - - - - - setInputText(text)} - updateParams={(controllerParams) => { - setControllerParams(controllerParams); - }} - params={controllerParams} - filterConfig={[ - { - id: 'host__icontains', - title: t`Controller name`, - }, - ]} - /> - - - - { - setControllerParams(controllerParams); - }} - count={controllerCount} - isTop - /> - - - - { - setControllerParams(controllerParams); - }} - params={controllerParams} - ignoredParams={['page_size', 'page']} - niceNames={{ - host__icontains: t`Controller name`, - }} - /> - - - {renderControllers()} - - - { - setControllerParams(controllerParams); - }} - count={controllerCount} - isTop - /> - -
{notListedMessage}
- - )} -
- ); -}; diff --git a/src/containers/execution-environment-detail/base.tsx b/src/containers/execution-environment-detail/base.tsx index b6b1c7c47f..8041521b80 100644 --- a/src/containers/execution-environment-detail/base.tsx +++ b/src/containers/execution-environment-detail/base.tsx @@ -13,9 +13,9 @@ import { type AlertType, DeleteExecutionEnvironmentModal, ExecutionEnvironmentHeader, + ExternalLink, LoadingPage, Main, - PublishToControllerModal, RepositoryForm, StatefulDropdown, closeAlert, @@ -27,12 +27,12 @@ import { RepoSigningUtils, type RouteProps, canSignEE, + controllerURL, taskAlert, waitForTask, } from 'src/utilities'; interface IState { - publishToController: { digest?: string; image: string; tag?: string }; repo: ContainerRepositoryType; loading: boolean; redirect: string; @@ -65,7 +65,6 @@ export function withContainerRepo(WrappedComponent) { super(props); this.state = { - publishToController: null, repo: undefined, loading: true, redirect: undefined, @@ -124,17 +123,16 @@ export function withContainerRepo(WrappedComponent) { ), { - this.setState({ - publishToController: { - image: this.state.repo.name, - }, - }); - }} - > - {t`Use in Controller`} - , + key='use-in-controller' + component={ + + {t`Use in Controller`} + + } + />, hasPermission('container.delete_containerrepository') && ( truthy); - const { alerts, repo, publishToController, showDeleteModal } = this.state; + const { alerts, repo, showDeleteModal } = this.state; // move to Owner tab when it can have its own breadcrumbs const { group: groupId } = ParamHelper.parseParamString( @@ -176,13 +174,6 @@ export function withContainerRepo(WrappedComponent) { }) } /> - this.setState({ publishToController: null })} - tag={publishToController?.tag} - /> {showDeleteModal && ( this.props.addAlert(alert)} containerRepository={this.props.containerRepository} /> - this.setState({ publishToController: null })} - tag={publishToController?.tag} - />
@@ -389,19 +380,20 @@ class ExecutionEnvironmentDetailImages extends Component< ), { - this.setState({ - publishToController: { + key='use-in-controller' + component={ + - {t`Use in Controller`} - , + })} + variant='menu' + > + {t`Use in Controller`} + + } + />, hasPermission('container.delete_containerrepository') && ( { items: [], loading: true, params, - publishToController: null, showRemoteModal: false, unauthorized: false, showDeleteModal: false, @@ -126,7 +124,6 @@ class ExecutionEnvironmentList extends Component { items, loading, params, - publishToController, showRemoteModal, unauthorized, showDeleteModal, @@ -171,13 +168,6 @@ class ExecutionEnvironmentList extends Component { }) } /> - this.setState({ publishToController: null })} - tag={publishToController?.tag} - /> {showRemoteModal && this.renderRemoteModal(itemToEdit)} @@ -376,17 +366,16 @@ class ExecutionEnvironmentList extends Component { ), { - this.setState({ - publishToController: { - image: item.name, - }, - }); - }} - > - {t`Use in Controller`} - , + key='use-in-controller' + component={ + + {t`Use in Controller`} + + } + />, hasPermission('container.delete_containerrepository') && ( { - const num = (~~(Math.random() * 1000000)).toString(); // FIXME: maybe drop everywhere once AAH-1095 is fixed - - before(() => { - cy.login(); - cy.deleteRegistries(); - cy.deleteContainers(); - cy.addRemoteRegistry(`docker${num}`, 'https://registry.hub.docker.com/'); - - cy.addRemoteContainer({ - name: `remotepine${num}`, - upstream_name: 'library/alpine', - registry: `docker${num}`, - include_tags: 'latest', - }); - - cy.visit(`${uiPrefix}containers/`); - cy.contains('.body', `remotepine${num}`, { timeout: 10000 }); - - cy.syncRemoteContainer(`remotepine${num}`); - cy.addLocalContainer(`localpine${num}`, 'alpine'); - }); - - beforeEach(() => { - cy.login(); - cy.menuGo('Execution Environments > Execution Environments'); - }); - - it('admin sees containers', () => { - // table headers - [ - 'Container repository name', - 'Description', - 'Created', - 'Last modified', - 'Container registry type', - ].forEach((header) => - cy.get('tr[data-cy="SortTable-headers"] th').contains(header), - ); - - // one row of each type available - cy.contains('.pf-v5-c-label', 'Remote'); - cy.contains('.pf-v5-c-label', 'Local'); - }); - - const list = (type) => - cy - .contains('.pf-v5-c-label', type) - .parents('tr') - .find('button[aria-label="Actions"]') - .click() - .parents('tr') - .contains('.pf-v5-c-dropdown__menu-item', 'Use in Controller') - .click(); - - const detail = (type) => { - cy.contains('.pf-v5-c-label', type).parents('tr').find('td a').click(); - - ['Detail', 'Activity', 'Images'].forEach((tab) => - cy.contains('.pf-v5-c-tabs__item', tab), - ); - - cy.get('button[aria-label="Actions"]') - .click() - .parent() - .contains('.pf-v5-c-dropdown__menu-item', 'Use in Controller') - .click(); - }; - - ['Remote', 'Local'].forEach((type) => { - [list, detail].forEach((opener) => { - it(`Use in Controller - ${type} ${opener.name}`, () => { - opener(type); - - // sporadic failure - // shows links - cy.contains('a', 'https://www.example.com') - .should('have.attr', 'href') - .and( - 'match', - /^https:\/\/www\.example\.com\/#\/execution_environments\/add\?image=.*latest$/, - ); - cy.contains('a', 'https://another.example.com'); - cy.get('ul.pf-v5-c-list > li > a').should('have.length', 2); - - // filter controllers - cy.get('input[placeholder="Filter by controller name"]') - .click() - .type('another{enter}'); - cy.contains('a', 'https://another.example.com'); - cy.get('ul.pf-v5-c-list > li > a').should('have.length', 1); - cy.contains('a', 'https://www.example.com').should('not.exist'); - - // unset tag, see digest - cy.get('.pf-m-typeahead .pf-v5-c-select__toggle-clear').click(); - cy.contains('a', 'https://another.example.com') - .should('have.attr', 'href') - .and( - 'match', - /^https:\/\/another\.example\.com\/#\/execution_environments\/add\?image=.*sha256.*$/, - ); - - // search tag - cy.get('.pf-v5-c-select__toggle-typeahead input').click(); - cy.contains('.pf-v5-c-select__menu', 'latest').click(); - cy.contains('a', 'https://another.example.com') - .should('have.attr', 'href') - .and( - 'match', - /^https:\/\/another\.example\.com\/#\/execution_environments\/add\?image=.*latest$/, - ); - - // unfilter controllers - cy.contains('Clear all filters').click(); - cy.get('ul.pf-v5-c-list > li > a').should('have.length', 2); - - // leave - cy.get('button[aria-label="Close"]').click(); - }); - }); - }); -}); diff --git a/test/cypress/e2e/repo/container-signing.js b/test/cypress/e2e/repo/container-signing.js index 59484be1a3..ce451a2b24 100644 --- a/test/cypress/e2e/repo/container-signing.js +++ b/test/cypress/e2e/repo/container-signing.js @@ -100,7 +100,9 @@ describe('Container Signing', () => { cy.visit(`${uiPrefix}containers/local1`); // this is now covered by alert that should not be here in the future cy.get('button[aria-label="Actions"]').click({ force: true }); - cy.contains('[role="menuitem"]', 'Use in Controller'); - cy.contains('[role="menuitem"]', 'Sign').should('not.exist'); + cy.contains('[role=menu] li a', 'Use in Controller') + .should('have.attr', 'href') + .and('match', /^http.*\/execution-environments\/add.*local1%3Alatest$/); + cy.contains('[role=menu] li', 'Sign').should('not.exist'); }); });