Skip to content
21 changes: 21 additions & 0 deletions cypress/e2e/smoke.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,27 @@ describe('UI smoke tests', () => {
// TODO
});

it('Deb repositories', () => {
cy.ui('deb/repositories');
cy.assertTitle('Repositories');

cy.contains('No repositories yet');
});

it('Deb remotes', () => {
cy.ui('deb/remotes');
cy.assertTitle('Remotes');

cy.contains('No remotes yet');

// an apt remote cannot sync without being told which suites to fetch, so the
// form carries fields the other plugins have no use for
cy.contains('button', 'Add remote').click();
cy.get('#distributions');
cy.get('#components');
cy.get('#architectures');
});

it('File repositories', () => {
cy.ui('file/repositories');
cy.assertTitle('Repositories');
Expand Down
9 changes: 9 additions & 0 deletions src/actions/deb-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 debRemoteCreateAction = Action({
title: msg`Add remote`,
onClick: (item, { navigate }) =>
navigate(formatPath(Paths.deb.remote.edit, { name: '_' })),
});
44 changes: 44 additions & 0 deletions src/actions/deb-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 { DebRemoteAPI } from 'src/api';
import { DeleteRemoteModal } from 'src/components';
import {
handleHttpError,
parsePulpIDFromURL,
taskAlert,
waitForTaskUrl,
} from 'src/utilities';
import { Action } from './action';

export const debRemoteDeleteAction = 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 DebRemoteAPI.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/deb-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 debRemoteEditAction = Action({
title: msg`Edit`,
onClick: ({ name }, { navigate }) =>
navigate(formatPath(Paths.deb.remote.edit, { name })),
});
9 changes: 9 additions & 0 deletions src/actions/deb-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 debRepositoryCreateAction = Action({
title: msg`Add repository`,
onClick: (item, { navigate }) =>
navigate(formatPath(Paths.deb.repository.edit, { name: '_' })),
});
123 changes: 123 additions & 0 deletions src/actions/deb-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 { DebDistributionAPI, DebRepositoryAPI } from 'src/api';
import { DeleteRepositoryModal } from 'src/components';
import {
handleHttpError,
parsePulpIDFromURL,
taskAlert,
waitForTaskUrl,
} from 'src/utilities';
import { Action } from './action';

export const debRepositoryDeleteAction = 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 DebDistributionAPI.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 = DebRepositoryAPI.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 DebDistributionAPI.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/deb-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 debRepositoryEditAction = Action({
title: msg`Edit`,
onClick: ({ name }, { navigate }) =>
navigate(formatPath(Paths.deb.repository.edit, { name })),
});
152 changes: 152 additions & 0 deletions src/actions/deb-repository-sync.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { msg, t } from '@lingui/core/macro';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MUST (mirror default): Good to see this flagged on the PR already - I'd agree it's worth resolving to match what pulp_deb defaults to, since that's the safer, non-destruction option. Aware ansible and file hardcode true too, but I'll raise a separate issue to track.

SHOULD (SyncModal duplication): Also flagged by you already - agreed this is worth doing, but I'd suggest pulling SyncModal out into a shared component as a precursor PR rather than here, since it's identical to file and ansible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flipped to mirror: false. It was written out in three places, so it is one DEFAULT_SYNC_PARAMS constant now, with a note there on why deb does not follow ansible and file.

On the SyncModal duplication: agreed, and agreed it belongs in its own PR rather than this one. I will follow up with one that pulls it out for ansible and file, and drop this copy once that lands.

2f2e296

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #363 for the extraction. Once that lands I will rebase this branch on it and drop the copy here.

import { Button, FormGroup, Modal, Switch } from '@patternfly/react-core';
import { useEffect, useState } from 'react';
import { DebRepositoryAPI } from 'src/api';
import { HelpButton, Spinner } from 'src/components';
import { handleHttpError, parsePulpIDFromURL, taskAlert } from 'src/utilities';
import { Action } from './action';

// pulp_deb's own API default for a sync. ansible and file hardcode mirror: true
// instead, and this deliberately does not follow them: mirroring deletes local
// content the remote no longer has, so it is the direction to opt into rather
// than out of.
const DEFAULT_SYNC_PARAMS = { mirror: false, optimize: true };

// otherwise as in ansible-repository-sync and file-repository-sync
const SyncModal = ({
closeAction,
syncAction,
name,
}: {
closeAction: () => null;
syncAction: (syncParams) => Promise<void>;
name: string;
}) => {
const [pending, setPending] = useState(false);
const [syncParams, setSyncParams] = useState(DEFAULT_SYNC_PARAMS);

useEffect(() => {
setPending(false);
setSyncParams(DEFAULT_SYNC_PARAMS);
}, [name]);

if (!name) {
return null;
}

return (
<Modal
actions={[
<div data-cy='sync-button' key='sync'>
<Button
key='sync'
onClick={() => {
setPending(true);
syncAction(syncParams)
.then(closeAction)
.finally(() => setPending(false));
}}
variant='primary'
isDisabled={pending}
>
{t`Sync`}
{pending && <Spinner size='sm' />}
</Button>
</div>,
<Button key='close' onClick={closeAction} variant='link'>
{t`Close`}
</Button>,
]}
isOpen
onClose={closeAction}
title={t`Sync repository "${name}"`}
variant='medium'
>
<FormGroup
label={t`Mirror`}
labelIcon={
<HelpButton
content={t`If selected, all content that is not present in the remote repository will be removed from the local repository; otherwise, sync will add missing content.`}
/>
}
>
<Switch
isChecked={syncParams.mirror}
onChange={(_event, mirror) =>
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`}
/>
</FormGroup>
<br />
<FormGroup
label={t`Optimize`}
labelIcon={
<HelpButton
content={t`Only perform the sync if changes are reported by the remote server. To force a sync to happen, deselect this option.`}
/>
}
>
<Switch
isChecked={syncParams.optimize}
onChange={(_event, optimize) =>
setSyncParams({ ...syncParams, optimize })
}
label={t`Only perform the sync if changes are reported by the remote server.`}
labelOff={t`Force a sync to happen.`}
/>
</FormGroup>
<br />
</Modal>
);
};

export const debRepositorySyncAction = Action({
title: msg`Sync`,
modal: ({ addAlert, query, setState, state }) =>
state.syncModalOpen ? (
<SyncModal
closeAction={() => 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('deb.change_aptrepository'),
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 DebRepositoryAPI.sync(pulpId, syncParams || DEFAULT_SYNC_PARAMS)
.then(({ data }) => {
addAlert(taskAlert(data.task, t`Sync started for repository "${name}".`));

query();
})
.catch(
handleHttpError(
t`Failed to sync repository "${name}"`,
() => null,
addAlert,
),
);
}
Loading