Skip to content

Conversation

@miguelbribeiro
Copy link
Contributor

Allow editing timers created with Cockpit on the web interface. The CreateTimerDialogBody was reused and the current settings are read and prefilled. Unfortunately, I couldn't come up with a good way of editing timers created externally: they support way too many settings to display in the UI, and you would need to fetch all of them from systemd and convert them back to the unit file format.

image

Related: #9467

@miguelbribeiro miguelbribeiro force-pushed the edit-timers branch 2 times, most recently from 86020e9 to 0a75f4a Compare June 18, 2025 10:44
Copy link
Member

@Venefilyn Venefilyn left a comment

Choose a reason for hiding this comment

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

Lots of good stuff here! Thanks for taking the time to go through this. I did some quick runthrough but need to test it a bit more.

@jelly do you have time to review this? Otherwise I can get to it at some point

Add a marker in the unit files of timers (and their services) created
with Cockpit. Those timers can be edited by reading the current settings
from the systemd's dbus API and reusing the CreateTimerDialog

Related: cockpit-project#9467
@miguelbribeiro
Copy link
Contributor Author

Sorry for the delay, I suddenly couldn't run the tests locally anymore and only had the time to fix it yesterday. I can squash the new commits if no more changes are needed.

@miguelbribeiro miguelbribeiro requested a review from Venefilyn June 26, 2025 10:21
}

componentDidMount() {
cockpit.file(this.props.unit.FragmentPath).read()
Copy link
Member

Choose a reason for hiding this comment

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

This could avoid doing an expensive lookup for every time if we first checked if the FragmentPath includes /etc/systemd

componentDidMount() {
cockpit.file(this.props.unit.FragmentPath).read()
.then(content => {
if (content.startsWith("# Managed by Cockpit.\n")) {
Copy link
Member

Choose a reason for hiding this comment

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

When navigating to an non-existing timer the page now raises an ooops. So we need to make sure FragmentPath isn't empty.

Uncaught TypeError: Cannot read properties of null (reading 'startsWith')
    at Promise2.<anonymous> (service-details.jsx:266:33)

editTimerAction() {
async function getCommand(bus, serviceUnitName) {
const serviceDbusPath = await bus.call(s_bus.O_MANAGER, s_bus.I_MANAGER, "LoadUnit", [serviceUnitName]);
const serviceProperties = await bus.call(serviceDbusPath[0], s_bus.I_PROPS, "GetAll", [s_bus.I_SERVICE]);
Copy link
Member

Choose a reason for hiding this comment

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

This needs some try/catch and useful error logging as far as I know LoadUnit can fail if the unit was for example edited by someone manually.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I read the dbus API documentation and there's nothing about LoadUnit failing in that case. I also tried editing an unit on my system and the call doesn't fail. Is there something I'm missing?

if (commands.length === 1) {
return join_command_arguments(commands[0][0], commands[0][1]);
} else {
return null;
Copy link
Member

Choose a reason for hiding this comment

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

How can this ever happen? No command? There should be an assert or at least a console.warn in case this happens.

const result = [command];
for (const arg of args.slice(1)) {
// if argument has whitespace, enclose in quotes and escape quotes
if (/\s/.test(arg)) {
Copy link
Member

Choose a reason for hiding this comment

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

So this is for the case where ExecStart=/usr/bin/bash -c "echo hai" which is good common case to handle.

But: /usr/bin/bash -c 'echo "hai"' would also be valid and editing this would change the command.

This is maybe something where we want to run shlex.quote.

time_ = b.val(f"[data-index='{index}'] .pf-v6-c-date-picker:nth-child(2) input")
self.assertIn((date, time_), (("2036-10-01", "01:22"), ("2036-08-01", "18:00")))

b.click("#timer-dialog-close-button")
Copy link
Member

Choose a reason for hiding this comment

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

Please wait till the dialog is closed, so add b.wait_not_present("#timer-dialog")

self.assertIn((date, time_), (("2036-10-01", "01:22"), ("2036-08-01", "18:00")))

b.click("#timer-dialog-close-button")
b.go("/system/services#/?type=timer&owner=system")
Copy link
Member

Choose a reason for hiding this comment

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

This needs a wait condition to wait until the page is loaded, otherwise this will become a flaky test, so please add self.wait_page_load()

self.allow_journal_messages("cockpit-session: admin: couldn't close session: System error: Unknown error -7")
self.reboot()

m.execute("timedatectl set-time '2036-01-01 15:30:00'")
Copy link
Member

Choose a reason for hiding this comment

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

All of above can be shared with testCreate in a new function

if (commands.length === 1) {
return join_command_arguments(commands[0][0], commands[0][1]);
} else {
return null;
Copy link
Contributor

Choose a reason for hiding this comment

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

This added line is not executed by any test. Details

Comment on lines +43 to +51
if (seconds % 604800 === 0) {
result.delayNumber = seconds / 604800;
result.delayUnit = "weeks";
} else if (seconds % 3600 === 0) {
result.delayNumber = seconds / 3600;
result.delayUnit = "hours";
} else if (seconds % 60 === 0) {
result.delayNumber = seconds / 60;
result.delayUnit = "minutes";
Copy link
Contributor

Choose a reason for hiding this comment

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

These 9 added lines are not executed by any test. Details


return result;
} else {
return null;
Copy link
Contributor

Choose a reason for hiding this comment

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

This added line is not executed by any test. Details

Comment on lines +149 to +154
{timer && !timer.delay && <FormAlert>
<Alert variant="danger" title={_("Failed to get the starting conditions for the timer")} isInline />
</FormAlert>}
{timer && !timer.command && <FormAlert>
<Alert variant="danger" title={_("Failed to get the timer command")} isInline />
</FormAlert>}
Copy link
Contributor

Choose a reason for hiding this comment

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

These 6 added lines are not executed by any test. Details

return arr;
})}
appendTo={() => document.body} />
appendTo={() => document.body}
Copy link
Contributor

Choose a reason for hiding this comment

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

This added line is not executed by any test. Details

@Venefilyn
Copy link
Member

Heya! Just checking how it's going here @miguelbribeiro :)

@miguelbribeiro
Copy link
Contributor Author

Heya! Just checking how it's going here @miguelbribeiro :)

Hey, I got busy with some other stuff back then and then kind of forgot about this PR. I'll try to finish it next week.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants