Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ PulpUI is a community driven single-page application that talks to a running [pu
### Plugins
- Ansible (repositories, remotes, collections, namespaces, approvals, imports)
- File (repositories, remotes)
- Python (repositories, remotes, packages)
- RPM (repositories, remotes, content)
- Container (execution environments, tags, manifests)

Expand Down
33 changes: 33 additions & 0 deletions e2e/smoke/pages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,39 @@ test.describe('pages of a signed-in session', () => {
await expect(page.locator('#architectures')).toBeVisible();
});

test('Python repositories', async ({ page }) => {
await page.goto('python/repositories');
await expect(title(page, 'Repositories')).toBeVisible();
await expect(page.getByText('No repositories yet')).toBeVisible();

// pulp_python repositories can refuse package substitution, so the form
// carries settings the other plugins have no use for
await page.getByRole('button', { name: 'Add repository' }).click();
await expect(page.locator('#allow_package_substitution')).toBeVisible();
await expect(page.locator('#error_on_reject')).toBeVisible();
});

test('Python remotes', async ({ page }) => {
await page.goto('python/remotes');
await expect(title(page, 'Remotes')).toBeVisible();
await expect(page.getByText('No remotes yet')).toBeVisible();

// a Python remote filters what it syncs by project, package type and
// platform, and offers the download policy
await page.getByRole('button', { name: 'Add remote' }).click();
await expect(page.locator('#includes')).toBeVisible();
await expect(page.locator('#excludes')).toBeVisible();
await expect(page.locator('#keep_latest_packages')).toBeVisible();
await expect(page.locator('#package_types_sdist')).toBeVisible();
await expect(page.locator('#policy')).toHaveValue('on_demand');
});

test('Python packages', async ({ page }) => {
await page.goto('python/packages');
await expect(title(page, 'Packages')).toBeVisible();
await expect(page.getByText('No packages yet')).toBeVisible();
});

test('RPM repositories', async ({ page }) => {
await page.goto('rpm/repositories');
await expect(title(page, 'Repositories')).toBeVisible();
Expand Down
7 changes: 7 additions & 0 deletions src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ export { fileRepositoryCreateAction } from './file-repository-create';
export { fileRepositoryDeleteAction } from './file-repository-delete';
export { fileRepositoryEditAction } from './file-repository-edit';
export { fileRepositorySyncAction } from './file-repository-sync';
export { pythonRemoteCreateAction } from './python-remote-create';
export { pythonRemoteDeleteAction } from './python-remote-delete';
export { pythonRemoteEditAction } from './python-remote-edit';
export { pythonRepositoryCreateAction } from './python-repository-create';
export { pythonRepositoryDeleteAction } from './python-repository-delete';
export { pythonRepositoryEditAction } from './python-repository-edit';
export { pythonRepositorySyncAction } from './python-repository-sync';
export { rpmRemoteCreateAction } from './rpm-remote-create';
export { rpmRemoteDeleteAction } from './rpm-remote-delete';
export { rpmRemoteEditAction } from './rpm-remote-edit';
Expand Down
9 changes: 9 additions & 0 deletions src/actions/python-remote-create.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { msg } from '@lingui/core/macro';
import { Paths, formatPath } from 'src/paths';
import { Action } from './action';

export const pythonRemoteCreateAction = Action({
title: msg`Add remote`,
onClick: (item, { navigate }) =>
navigate(formatPath(Paths.python.remote.edit, { name: '_' })),
});
44 changes: 44 additions & 0 deletions src/actions/python-remote-delete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { msg, t } from '@lingui/core/macro';
import { PythonRemoteAPI } from 'src/api';

Check warning on line 2 in src/actions/python-remote-delete.tsx

View workflow job for this annotation

GitHub Actions / pr-checks

'src/api' import is restricted from being used. Import the specific file and not the barrel
import { DeleteRemoteModal } from 'src/components';
import {

Check warning on line 4 in src/actions/python-remote-delete.tsx

View workflow job for this annotation

GitHub Actions / pr-checks

'src/utilities' import is restricted from being used. Import the specific file and not the barrel
handleHttpError,
parsePulpIDFromURL,
taskAlert,
waitForTaskUrl,
} from 'src/utilities';
import { Action } from './action';

export const pythonRemoteDeleteAction = Action({
title: msg`Delete`,
modal: ({ addAlert, listQuery, setState, state }) =>
state.deleteModalOpen ? (
<DeleteRemoteModal
closeAction={() => 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 PythonRemoteAPI.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),
);
}
9 changes: 9 additions & 0 deletions src/actions/python-remote-edit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { msg } from '@lingui/core/macro';
import { Paths, formatPath } from 'src/paths';
import { Action } from './action';

export const pythonRemoteEditAction = Action({
title: msg`Edit`,
onClick: ({ name }, { navigate }) =>
navigate(formatPath(Paths.python.remote.edit, { name })),
});
9 changes: 9 additions & 0 deletions src/actions/python-repository-create.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { msg } from '@lingui/core/macro';
import { Paths, formatPath } from 'src/paths';
import { Action } from './action';

export const pythonRepositoryCreateAction = Action({
title: msg`Add repository`,
onClick: (item, { navigate }) =>
navigate(formatPath(Paths.python.repository.edit, { name: '_' })),
});
123 changes: 123 additions & 0 deletions src/actions/python-repository-delete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { msg, t } from '@lingui/core/macro';
import { PythonDistributionAPI, PythonRepositoryAPI } from 'src/api';

Check warning on line 2 in src/actions/python-repository-delete.tsx

View workflow job for this annotation

GitHub Actions / pr-checks

'src/api' import is restricted from being used. Import the specific file and not the barrel
import { DeleteRepositoryModal } from 'src/components';
import {

Check warning on line 4 in src/actions/python-repository-delete.tsx

View workflow job for this annotation

GitHub Actions / pr-checks

'src/utilities' import is restricted from being used. Import the specific file and not the barrel
handleHttpError,
parsePulpIDFromURL,
taskAlert,
waitForTaskUrl,
} from 'src/utilities';
import { Action } from './action';

export const pythonRepositoryDeleteAction = Action({
title: msg`Delete`,
modal: ({ addAlert, listQuery, setState, state }) =>
state.deleteModalOpen ? (
<DeleteRepositoryModal
closeAction={() => 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,
},
}),
});

const DISTRIBUTION_PAGE_SIZE = 100;

// A repository can be serving more distributions than a single page holds, and
// any the lookup misses are left pointing at a repository that no longer exists.
async function listDistributions(repository) {
const distributions = [];
let page = 1;
let count = Infinity;

while (distributions.length < count) {
const { data } = await PythonDistributionAPI.list({
repository,
page,
page_size: DISTRIBUTION_PAGE_SIZE,
});

// Also stops the loop should count ever disagree with what the pages return.
if (!data.results?.length) {
break;
}

distributions.push(...data.results);
count = data.count;
page++;
}

return distributions;
}

async function deleteRepository(
{ name, pulp_href, pulpId },
{ addAlert, setState, listQuery },
) {
const distributionsToDelete = await listDistributions(pulp_href).catch(
(e) => {
handleHttpError(
t`Failed to list distributions, removing only the repository.`,
() => null,
addAlert,
)(e);
return [];
},
);

const deleteRepo = PythonRepositoryAPI.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 PythonDistributionAPI.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();
});
}
9 changes: 9 additions & 0 deletions src/actions/python-repository-edit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { msg } from '@lingui/core/macro';
import { Paths, formatPath } from 'src/paths';
import { Action } from './action';

export const pythonRepositoryEditAction = Action({
title: msg`Edit`,
onClick: ({ name }, { navigate }) =>
navigate(formatPath(Paths.python.repository.edit, { name })),
});
63 changes: 63 additions & 0 deletions src/actions/python-repository-sync.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { msg, t } from '@lingui/core/macro';
import { PythonRepositoryAPI } from 'src/api';
import { SyncModal } from 'src/components';
import { handleHttpError, parsePulpIDFromURL, taskAlert } from 'src/utilities';
import { Action } from './action';

// pulp_python syncs additively unless mirror is requested. Mirroring deletes
// local content the remote no longer has, so it is the direction to opt into
// rather than out of, as for deb.
const MIRROR_BY_DEFAULT = false;

export const pythonRepositorySyncAction = Action({
title: msg`Sync`,
modal: ({ addAlert, query, setState, state }) =>
state.syncModalOpen ? (
<SyncModal
closeAction={() => setState({ syncModalOpen: null })}
defaultMirror={MIRROR_BY_DEFAULT}
showOptimize={false}
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('python.change_pythonrepository'),
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 PythonRepositoryAPI.sync(pulpId, {
mirror: syncParams?.mirror ?? MIRROR_BY_DEFAULT,
})
.then(({ data }) => {
addAlert(taskAlert(data.task, t`Sync started for repository "${name}".`));

query();
})
.catch(
handleHttpError(
t`Failed to sync repository "${name}"`,
() => null,
addAlert,
),
);
}
12 changes: 12 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ export {
type UserType,
} from './response-types/user';
export { RoleAPI } from './role';
export { PythonDistributionAPI } from './python-distribution';
export {
PythonRemoteAPI,
type PythonPackageType,
type PythonPlatform,
type PythonRemoteType,
} from './python-remote';
export {
PythonRepositoryAPI,
type PythonRepositoryType,
} from './python-repository';
export { PythonPackageAPI } from './python-package';
export { RPMPackageAPI } from './rpm-package';
export { RPMDistributionAPI } from './rpm-distribution';
export { RPMRemoteAPI, type RPMRemoteType } from './rpm-remote';
Expand Down
11 changes: 11 additions & 0 deletions src/api/python-distribution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { PulpAPI } from './pulp';

const base = new PulpAPI();

export const PythonDistributionAPI = {
create: (data) => base.http.post(`distributions/python/pypi/`, data),

delete: (id) => base.http.delete(`distributions/python/pypi/${id}/`),

list: (params?) => base.list(`distributions/python/pypi/`, params),
};
7 changes: 7 additions & 0 deletions src/api/python-package.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { PulpAPI } from './pulp';

const base = new PulpAPI();

export const PythonPackageAPI = {
list: (params?) => base.list(`content/python/packages/`, params),
};
Loading
Loading