diff --git a/cypress/e2e/hub/namespaces.cy.ts b/cypress/e2e/hub/namespaces.cy.ts
index 7ed02d779d..13b947ab87 100644
--- a/cypress/e2e/hub/namespaces.cy.ts
+++ b/cypress/e2e/hub/namespaces.cy.ts
@@ -73,16 +73,20 @@ describe('Namespaces', () => {
cy.clickButton(/^Clear all filters$/);
});
- it('verify user and team access for namespace', () => {
+ it('verify user and team access tabs are available for a namespace', () => {
const nameSpaceName = `test_namespace_access_${randomString(5, undefined, {
isLowercase: true,
})}`;
cy.createNamespace(nameSpaceName);
cy.navigateTo('hub', Namespaces.url);
- // cy.setTablePageSize('50');
+ cy.get('[data-cy="table-view"]').click();
cy.clickTableRow(nameSpaceName);
cy.verifyPageTitle(nameSpaceName);
- cy.clickTab(/^Access$/, true);
- cy.pause();
+ cy.clickTab(/^User access$/, true);
+ cy.contains(/^There are currently no users added.$/);
+ cy.clickTab(/^Team access$/, true);
+ cy.contains(/^There are currently no teams added.$/);
+ // TODO: tests for adding/removing users/roles when those features are implemented
+ cy.deleteNamespace(nameSpaceName);
});
});
diff --git a/cypress/fixtures/hub_namespace.json b/cypress/fixtures/hub_namespace.json
new file mode 100644
index 0000000000..015c4891ab
--- /dev/null
+++ b/cypress/fixtures/hub_namespace.json
@@ -0,0 +1,52 @@
+{
+ "meta": {
+ "count": 1
+ },
+ "links": {
+ "first": "/api/automation-hub/_ui/v1/namespaces/?include_related=my_permissions&limit=1&name=test2&offset=0",
+ "previous": null,
+ "next": null,
+ "last": "/api/automation-hub/_ui/v1/namespaces/?include_related=my_permissions&limit=1&name=test2&offset=0"
+ },
+ "data": [
+ {
+ "pulp_href": "/api/automation-hub/pulp/api/v3/pulp_ansible/namespaces/2/",
+ "id": 2,
+ "name": "test2",
+ "company": "",
+ "email": "",
+ "avatar_url": "",
+ "description": "",
+ "groups": [
+ {
+ "id": 1,
+ "name": "Test Group",
+ "object_roles": ["galaxy.collection_publisher"]
+ }
+ ],
+ "users": [
+ {
+ "id": 2,
+ "name": "new-user",
+ "object_roles": ["galaxy.collection_namespace_owner", "galaxy.collection_publisher"]
+ },
+ {
+ "id": 3,
+ "name": "obj-role-test-user",
+ "object_roles": ["galaxy.collection_namespace_owner"]
+ }
+ ],
+ "related_fields": {
+ "my_permissions": [
+ "galaxy.add_namespace",
+ "galaxy.change_namespace",
+ "galaxy.delete_namespace",
+ "galaxy.upload_to_namespace",
+ "galaxy.view_namespace"
+ ]
+ },
+ "metadata_sha256": "7e4efa6ba218f9d9a6394bba10d0bb6b452bbe24078bc4b8f62dfe727390dd42",
+ "avatar_sha256": null
+ }
+ ]
+}
diff --git a/cypress/fixtures/hub_namespace_permissions.json b/cypress/fixtures/hub_namespace_permissions.json
new file mode 100644
index 0000000000..d28870335b
--- /dev/null
+++ b/cypress/fixtures/hub_namespace_permissions.json
@@ -0,0 +1,15 @@
+{
+ "count": 1,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "pulp_href": "/api/automation-hub/pulp/api/v3/roles/018d1319-5a39-77cb-b368-00f504c0889e/",
+ "pulp_created": "2024-01-16T16:27:25.114238Z",
+ "name": "galaxy.collection_namespace_owner",
+ "description": null,
+ "permissions": ["galaxy.change_namespace", "galaxy.upload_to_namespace"],
+ "locked": true
+ }
+ ]
+}
diff --git a/frontend/hub/access/roles/components/RolePermissions.tsx b/frontend/hub/access/roles/components/RolePermissions.tsx
index 5de8577a1e..15331a7aa4 100644
--- a/frontend/hub/access/roles/components/RolePermissions.tsx
+++ b/frontend/hub/access/roles/components/RolePermissions.tsx
@@ -125,8 +125,8 @@ export function usePermissionCategories(
() =>
allGroups.map((group) => ({
...group,
- selectedPermissions: group.allPermissions.filter((permission) =>
- permissions?.includes(permission)
+ selectedPermissions: group.allPermissions.filter(
+ (permission) => permissions?.includes(permission)
),
availablePermissions: group.allPermissions.filter(
(permission) => !permissions?.includes(permission)
diff --git a/frontend/hub/namespaces/HubNamespacePage/HubNamespaceAccessRoles.cy.tsx b/frontend/hub/namespaces/HubNamespacePage/HubNamespaceAccessRoles.cy.tsx
new file mode 100644
index 0000000000..ca82fc9e41
--- /dev/null
+++ b/frontend/hub/namespaces/HubNamespacePage/HubNamespaceAccessRoles.cy.tsx
@@ -0,0 +1,117 @@
+import * as useHubContext from '../../common/useHubContext';
+import mockUser from '../../../../cypress/fixtures/hub_admin.json';
+import mockNamespaceResponse from '../../../../cypress/fixtures/hub_namespace.json';
+import { hubAPI, pulpAPI } from '../../common/api/formatPath';
+import { HubNamespaceAccessRoles } from './HubNamespaceAccessRoles';
+
+describe('HubNamespaceAccessRoles', () => {
+ beforeEach(() => {
+ cy.intercept(
+ {
+ method: 'GET',
+ url: hubAPI`/_ui/v1/namespaces/*`,
+ },
+ mockNamespaceResponse
+ ).as('namespaceDetails');
+ });
+ it('Roles list renders', () => {
+ cy.stub(useHubContext, 'useHubContext').callsFake(() => ({
+ user: mockUser,
+ hasPermission: () => true,
+ }));
+ cy.mount(, {
+ path: '/namespaces/:id/users/:username',
+ initialEntries: ['/namespaces/2/users/new-user'],
+ });
+ cy.get('tbody').find('tr').should('have.length', 2);
+ });
+ it('Role can be expanded to view permissions', () => {
+ cy.intercept(
+ {
+ method: 'GET',
+ url: pulpAPI`/roles/*`,
+ },
+ {
+ fixture: 'hub_namespace_permissions.json',
+ }
+ ).as('roleDetails');
+ cy.stub(useHubContext, 'useHubContext').callsFake(() => ({
+ user: mockUser,
+ hasPermission: () => true,
+ featureFlags: {},
+ }));
+ cy.mount(, {
+ path: '/namespaces/:id/users/:username',
+ initialEntries: ['/namespaces/2/users/new-user'],
+ });
+ cy.get('#expand-toggle0 > .pf-v5-c-table__toggle-icon').click();
+ cy.contains(/^Change and upload collections to namespaces.$/);
+ cy.contains(/^Change namespace$/);
+ cy.contains(/^Upload to namespace$/);
+ });
+ it('Filter roles by name', () => {
+ cy.stub(useHubContext, 'useHubContext').callsFake(() => ({
+ user: mockUser,
+ hasPermission: () => true,
+ }));
+ cy.mount(, {
+ path: '/namespaces/:id/users/:username',
+ initialEntries: ['/namespaces/2/users/new-user'],
+ });
+ cy.filterTableByText('namespace');
+ cy.contains('galaxy.collection_namespace_owner');
+ cy.get('tbody').find('tr').should('have.length', 1);
+ cy.clickButton(/^Clear all filters$/);
+ });
+ it('Add/delete role actions are enabled for a user with permission to edit access for the namespace', () => {
+ cy.stub(useHubContext, 'useHubContext').callsFake(() => ({
+ user: mockUser,
+ hasPermission: () => true,
+ }));
+ cy.mount(, {
+ path: '/namespaces/:id/users/:username',
+ initialEntries: ['/namespaces/2/users/new-user'],
+ });
+ cy.contains('a[data-cy="add-role"]', /^Add role$/).should(
+ 'have.attr',
+ 'aria-disabled',
+ 'false'
+ );
+ cy.contains('tr', 'galaxy.collection_namespace_owner').within(() => {
+ cy.get('button.toggle-kebab').click();
+ cy.contains('.pf-v5-c-dropdown__menu-item', /^Delete role$/).should(
+ 'have.attr',
+ 'aria-disabled',
+ 'false'
+ );
+ });
+ });
+ it('Add/delete role actions are disabled for a user without permission to edit access for the namespace', () => {
+ const mockEmptyUsersResponse = { ...mockNamespaceResponse };
+ mockEmptyUsersResponse.data[0].related_fields.my_permissions = [];
+ cy.intercept(
+ {
+ method: 'GET',
+ url: hubAPI`/_ui/v1/namespaces/*`,
+ },
+ mockEmptyUsersResponse
+ ).as('nonEmptyListWithoutPermissions');
+ cy.stub(useHubContext, 'useHubContext').callsFake(() => ({
+ user: { ...mockUser, is_superuser: false },
+ hasPermission: () => false,
+ }));
+ cy.mount(, {
+ path: '/namespaces/:id/users/:username',
+ initialEntries: ['/namespaces/2/users/new-user'],
+ });
+ cy.contains('a[data-cy="add-role"]', /^Add role$/).should('have.attr', 'aria-disabled', 'true');
+ cy.contains('tr', 'galaxy.collection_namespace_owner').within(() => {
+ cy.get('button.toggle-kebab').click();
+ cy.contains('.pf-v5-c-dropdown__menu-item', /^Delete role$/).should(
+ 'have.attr',
+ 'aria-disabled',
+ 'true'
+ );
+ });
+ });
+});
diff --git a/frontend/hub/namespaces/HubNamespacePage/HubNamespacePage.tsx b/frontend/hub/namespaces/HubNamespacePage/HubNamespacePage.tsx
index 4c51a3c840..d267e4513c 100644
--- a/frontend/hub/namespaces/HubNamespacePage/HubNamespacePage.tsx
+++ b/frontend/hub/namespaces/HubNamespacePage/HubNamespacePage.tsx
@@ -86,11 +86,11 @@ export function HubNamespacePage() {
page: HubRoute.NamespaceCLI,
},
{
- label: t('User Access'),
+ label: t('User access'),
page: HubRoute.NamespaceUserAccess,
},
{
- label: t('Team Access'),
+ label: t('Team access'),
page: HubRoute.NamespaceTeamAccess,
},
]}
diff --git a/frontend/hub/namespaces/HubNamespacePage/HubNamespaceTeamAccess.cy.tsx b/frontend/hub/namespaces/HubNamespacePage/HubNamespaceTeamAccess.cy.tsx
new file mode 100644
index 0000000000..50637e74e6
--- /dev/null
+++ b/frontend/hub/namespaces/HubNamespacePage/HubNamespaceTeamAccess.cy.tsx
@@ -0,0 +1,131 @@
+import * as useHubContext from '../../common/useHubContext';
+import mockUser from '../../../../cypress/fixtures/hub_admin.json';
+import mockNamespaceResponse from '../../../../cypress/fixtures/hub_namespace.json';
+import { hubAPI } from '../../common/api/formatPath';
+import { HubNamespaceTeamAccess } from './HubNamespaceTeamAccess';
+
+describe('HubNamespaceTeamAccess', () => {
+ describe('Non-empty list', () => {
+ beforeEach(() => {
+ cy.intercept(
+ {
+ method: 'GET',
+ url: hubAPI`/_ui/v1/namespaces/*`,
+ },
+ mockNamespaceResponse
+ ).as('nonEmptyListWithPermissions');
+ });
+ it('Teams list renders', () => {
+ cy.stub(useHubContext, 'useHubContext').callsFake(() => ({
+ user: mockUser,
+ hasPermission: () => true,
+ }));
+ cy.mount();
+ cy.get('tbody').find('tr').should('have.length', 1);
+ });
+ it('Filter teams by name', () => {
+ cy.stub(useHubContext, 'useHubContext').callsFake(() => ({
+ user: mockUser,
+ hasPermission: () => true,
+ }));
+ cy.mount();
+ cy.filterTableByText('Test Group');
+ cy.contains('Test Group');
+ cy.get('tbody').find('tr').should('have.length', 1);
+ cy.clickButton(/^Clear all filters$/);
+ });
+ it('Add/delete team actions are enabled for a user with permission to edit access for the namespace', () => {
+ cy.stub(useHubContext, 'useHubContext').callsFake(() => ({
+ user: mockUser,
+ hasPermission: () => true,
+ }));
+ cy.mount();
+ cy.contains('a[data-cy="add-team"]', /^Add team$/).should(
+ 'have.attr',
+ 'aria-disabled',
+ 'false'
+ );
+ cy.contains('tr', 'Test Group').within(() => {
+ cy.get('button.toggle-kebab').click();
+ cy.contains('.pf-v5-c-dropdown__menu-item', /^Delete team$/).should(
+ 'have.attr',
+ 'aria-disabled',
+ 'false'
+ );
+ });
+ });
+ it('Add/delete team actions are disabled for a user without permission to edit access for the namespace', () => {
+ const mockEmptyUsersResponse = { ...mockNamespaceResponse };
+ mockEmptyUsersResponse.data[0].related_fields.my_permissions = [];
+ cy.intercept(
+ {
+ method: 'GET',
+ url: hubAPI`/_ui/v1/namespaces/*`,
+ },
+ mockEmptyUsersResponse
+ ).as('nonEmptyListWithoutPermissions');
+ cy.stub(useHubContext, 'useHubContext').callsFake(() => ({
+ user: { ...mockUser, is_superuser: false },
+ hasPermission: () => false,
+ }));
+ cy.mount();
+ cy.contains('a[data-cy="add-team"]', /^Add team$/).should(
+ 'have.attr',
+ 'aria-disabled',
+ 'true'
+ );
+ cy.contains('tr', 'Test Group').within(() => {
+ cy.get('button.toggle-kebab').click();
+ cy.contains('.pf-v5-c-dropdown__menu-item', /^Delete team$/).should(
+ 'have.attr',
+ 'aria-disabled',
+ 'true'
+ );
+ });
+ });
+ });
+ describe('Empty list', () => {
+ it('Empty state is displayed correctly for a user with permissions to add teams', () => {
+ const mockEmptyUsersResponse = { ...mockNamespaceResponse };
+ mockEmptyUsersResponse.data[0].groups = [];
+ cy.intercept(
+ {
+ method: 'GET',
+ url: hubAPI`/_ui/v1/namespaces/*`,
+ },
+ mockEmptyUsersResponse
+ ).as('emptyList');
+ cy.stub(useHubContext, 'useHubContext').callsFake(() => ({
+ user: mockUser,
+ hasPermission: () => true,
+ }));
+ cy.mount();
+ cy.contains(/^There are currently no teams added.$/);
+ cy.contains(/^Please add a team by using the button below.$/);
+ cy.contains('a', /^Add team$/).should('be.visible');
+ cy.contains('a', /^Add team$/).should('not.be.disabled');
+ });
+ it('Empty state is displayed correctly for user without permission to add teams', () => {
+ const mockEmptyUsersResponse = { ...mockNamespaceResponse };
+ mockEmptyUsersResponse.data[0].groups = [];
+ mockEmptyUsersResponse.data[0].related_fields.my_permissions = [];
+ cy.intercept(
+ {
+ method: 'GET',
+ url: hubAPI`/_ui/v1/namespaces/*`,
+ },
+ mockEmptyUsersResponse
+ ).as('emptyList');
+ cy.stub(useHubContext, 'useHubContext').callsFake(() => ({
+ user: { ...mockUser, is_superuser: false },
+ hasPermission: () => false,
+ }));
+ cy.mount();
+ cy.contains(/^You do not have permission to add a team.$/);
+ cy.contains(
+ /^Please contact your organization administrator if there is an issue with your access.$/
+ );
+ cy.contains('a', /^Add team$/).should('not.exist');
+ });
+ });
+});
diff --git a/frontend/hub/namespaces/HubNamespacePage/HubNamespaceUserAccess.cy.tsx b/frontend/hub/namespaces/HubNamespacePage/HubNamespaceUserAccess.cy.tsx
new file mode 100644
index 0000000000..4918f5d829
--- /dev/null
+++ b/frontend/hub/namespaces/HubNamespacePage/HubNamespaceUserAccess.cy.tsx
@@ -0,0 +1,131 @@
+import * as useHubContext from '../../common/useHubContext';
+import mockUser from '../../../../cypress/fixtures/hub_admin.json';
+import mockNamespaceResponse from '../../../../cypress/fixtures/hub_namespace.json';
+import { HubNamespaceUserAccess } from './HubNamespaceUserAccess';
+import { hubAPI } from '../../common/api/formatPath';
+
+describe('HubNamespaceUserAccess', () => {
+ describe('Non-empty list', () => {
+ beforeEach(() => {
+ cy.intercept(
+ {
+ method: 'GET',
+ url: hubAPI`/_ui/v1/namespaces/*`,
+ },
+ mockNamespaceResponse
+ ).as('nonEmptyListWithPermissions');
+ });
+ it('Users list renders', () => {
+ cy.stub(useHubContext, 'useHubContext').callsFake(() => ({
+ user: mockUser,
+ hasPermission: () => true,
+ }));
+ cy.mount();
+ cy.get('tbody').find('tr').should('have.length', 2);
+ });
+ it('Filter users by name', () => {
+ cy.stub(useHubContext, 'useHubContext').callsFake(() => ({
+ user: mockUser,
+ hasPermission: () => true,
+ }));
+ cy.mount();
+ cy.filterTableByText('new-user');
+ cy.contains('new-user');
+ cy.get('tbody').find('tr').should('have.length', 1);
+ cy.clickButton(/^Clear all filters$/);
+ });
+ it('Add/delete user actions are enabled for a user with permission to edit access for the namespace', () => {
+ cy.stub(useHubContext, 'useHubContext').callsFake(() => ({
+ user: mockUser,
+ hasPermission: () => true,
+ }));
+ cy.mount();
+ cy.contains('a[data-cy="add-user"]', /^Add user$/).should(
+ 'have.attr',
+ 'aria-disabled',
+ 'false'
+ );
+ cy.contains('tr', 'new-user').within(() => {
+ cy.get('button.toggle-kebab').click();
+ cy.contains('.pf-v5-c-dropdown__menu-item', /^Delete user$/).should(
+ 'have.attr',
+ 'aria-disabled',
+ 'false'
+ );
+ });
+ });
+ it('Add/delete user actions are disabled for a user without permission to edit access for the namespace', () => {
+ const mockEmptyUsersResponse = { ...mockNamespaceResponse };
+ mockEmptyUsersResponse.data[0].related_fields.my_permissions = [];
+ cy.intercept(
+ {
+ method: 'GET',
+ url: hubAPI`/_ui/v1/namespaces/*`,
+ },
+ mockEmptyUsersResponse
+ ).as('nonEmptyListWithoutPermissions');
+ cy.stub(useHubContext, 'useHubContext').callsFake(() => ({
+ user: { ...mockUser, is_superuser: false },
+ hasPermission: () => false,
+ }));
+ cy.mount();
+ cy.contains('a[data-cy="add-user"]', /^Add user$/).should(
+ 'have.attr',
+ 'aria-disabled',
+ 'true'
+ );
+ cy.contains('tr', 'new-user').within(() => {
+ cy.get('button.toggle-kebab').click();
+ cy.contains('.pf-v5-c-dropdown__menu-item', /^Delete user$/).should(
+ 'have.attr',
+ 'aria-disabled',
+ 'true'
+ );
+ });
+ });
+ });
+ describe('Empty list', () => {
+ it('Empty state is displayed correctly for a user with permissions to add users', () => {
+ const mockEmptyUsersResponse = { ...mockNamespaceResponse };
+ mockEmptyUsersResponse.data[0].users = [];
+ cy.intercept(
+ {
+ method: 'GET',
+ url: hubAPI`/_ui/v1/namespaces/*`,
+ },
+ mockEmptyUsersResponse
+ ).as('emptyList');
+ cy.stub(useHubContext, 'useHubContext').callsFake(() => ({
+ user: mockUser,
+ hasPermission: () => true,
+ }));
+ cy.mount();
+ cy.contains(/^There are currently no users added.$/);
+ cy.contains(/^Please add a user by using the button below.$/);
+ cy.contains('a', /^Add user$/).should('be.visible');
+ cy.contains('a', /^Add user$/).should('not.be.disabled');
+ });
+ it('Empty state is displayed correctly for user without permission to add users', () => {
+ const mockEmptyUsersResponse = { ...mockNamespaceResponse };
+ mockEmptyUsersResponse.data[0].users = [];
+ mockEmptyUsersResponse.data[0].related_fields.my_permissions = [];
+ cy.intercept(
+ {
+ method: 'GET',
+ url: hubAPI`/_ui/v1/namespaces/*`,
+ },
+ mockEmptyUsersResponse
+ ).as('emptyListWithoutPermissions');
+ cy.stub(useHubContext, 'useHubContext').callsFake(() => ({
+ user: { ...mockUser, is_superuser: false },
+ hasPermission: () => false,
+ }));
+ cy.mount();
+ cy.contains(/^You do not have permission to add a user.$/);
+ cy.contains(
+ /^Please contact your organization administrator if there is an issue with your access.$/
+ );
+ cy.contains('a', /^Add user$/).should('not.exist');
+ });
+ });
+});