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');
});
});