Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fall back to sorting by name on ties #535

Merged

Conversation

allisonkarlitskaya
Copy link
Member

If we tie on the mtime or the file size then fall back to sorting based on name — it's always unique.

This should give us a fully-defined sort order for all directories.

Copy link
Member

@jelly jelly left a comment

Choose a reason for hiding this comment

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

Nice!

@allisonkarlitskaya
Copy link
Member Author

I think the debian fail is relevant, so probably don't merge this yet...

@jelly
Copy link
Member

jelly commented Jun 17, 2024

I think the debian fail is relevant, so probably don't merge this yet...

Seems so! I'll download and take a look

@jelly
Copy link
Member

jelly commented Jun 18, 2024

So on debian testing the ordering on size is correct except for the first directory:

  • eee
  • Eee
  • ddd

Should be

  • ddd
  • eee
  • Eee

ddd is a symlink to /tmp so should be treated as a directory:

{
    "type": "lnk",
    "group": "root",
    "size": 4,
    "target": "/tmp",
    "mode": 511,
    "mtime": 1718701039.496,
    "user": "root",
    "name": "ddd",
    "to": "dir",
    "category": null
}

In fedora VM

{
    "size": 4,
    "mtime": 1718701354.1252325,
    "user": "root",
    "target": "/tmp",
    "mode": 511,
    "group": "root",
    "type": "lnk",
    "name": "ddd",
    "to": "dir",
    "category": null
}

So we first so a dir_sort and then a sort on size. As this is a symlink its size is 4 and the other directory's size is bigger, interestingly this does not happen on Fedora, I assume that's because of btrfs.

Btrfs

lrwxrwxrwx. 1 root root   4 Jun 18 09:02 ddd -> /tmp
drwxr-xr-x. 1 root root   0 Jun 18 09:02 eee
drwxr-xr-x. 1 root root   0 Jun 18 09:02 Eee

Debian

-rw-r--r-- 1 root root  10M Jun 18 08:57 BBB
drwxr-xr-x 2 root root 4.0K Jun 18 08:57 Eee
-rw-r--r-- 1 root root    4 Jun 18 08:57 aaa
-rw-r--r-- 1 root root    0 Jun 18 06:57 ccc
lrwxrwxrwx 1 root root    4 Jun 18 08:57 ddd -> /tmp
drwxr-xr-x 2 root root 4.0K Jun 18 08:57 eee

@jelly jelly self-requested a review June 18, 2024 09:25
Copy link
Member

@jelly jelly left a comment

Choose a reason for hiding this comment

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

Needs a fix for sorting on directories as they are compared by size when they are both a directory.

Copy link
Member

@jelly jelly left a comment

Choose a reason for hiding this comment

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

Ironically this fails on typescript checks :-)

@@ -423,7 +444,7 @@ const Row = React.memo(function Item({ file, isSelected }) {
dataLabel="date"
modifier="nowrap"
>
{timeformat.dateTime(file.mtime * 1000)}
{file.mtime ? timeformat.dateTime(file.mtime * 1000) : null}
Copy link
Member

Choose a reason for hiding this comment

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

null would show nothing, that's probably fine. Wondering if we might want to show no date.

@@ -354,6 +354,11 @@ class TestFiles(testlib.MachineCase):

self.enter_files()

# set a bogus sort value in localStorage to make sure we handle it gracefully
b.eval_js("""window.localStorage.setItem("files:sort", 'bzzt')""")
Copy link
Member

Choose a reason for hiding this comment

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

🐝

src/files-card-body.tsx Show resolved Hide resolved
@allisonkarlitskaya
Copy link
Member Author

The pixel test fails are kinda interesting. Sometimes the / separators in the path bar get rendered a little bit thicker or thinner. I wonder what's causing that...

https://cockpit-logs.us-east-1.linodeobjects.com/pull-535-2f0f2494-20240619-195556-fedora-40/log.html

image

@allisonkarlitskaya
Copy link
Member Author

The pixel failures on fedora-40 are also broken on main and fixed by #576.

@@ -474,7 +474,7 @@ export const fileActions = (path, selected, setSelected, clipboard, setClipboard
);
},
},
{ type: "divider" },
null,
Copy link
Member

Choose a reason for hiding this comment

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

This feels super non-obvious. null doesn't give me a hint that we are rendering a divider.

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay. I brought back { type: "divider" } with a bit of discriminated union magic (and a bonus of some extra code in typescript for my efforts).

src/files-card-body.tsx Show resolved Hide resolved
The following commit will port files-card-body to TypeScript, revealing
several safety issues with the menu items handling code.  Fix them up
before we do that:

 - it's difficult to reason about the types of menu items when we have
   two similar but different things: items and dividers.  Instead of
   using { type: "divider" } as a placeholder for a divider, we could
   use null, but this would have a negative impact on readabilty.
   Fortunately, with explicit typing we can form what is essentially
   discriminated union.  Move the menu code to its own TypeScript file,
   continuing the shrinking of fileActions.jsx, and add a type for menu
   items.  Now TypeScript can reason about the properties that are
   present on all non-divider items (`title` and `onClick`).  This
   prevents TypeScript from complaining that `onClick` might be
   `undefined`, for example.  The explicit typing also means that if we
   make a mistake when defining a menu item, we'll see the type error
   directly at the point of the mistake.

 - className is only defined on some items, but we do an unconditional
   concatenation of `"context-menu-option " + item.className`.  That
   means that we set the class to `context-menu-option undefined`, which
   is definitely not what we wanted

 - strictly speaking we're not allowed to pass isDisabled={undefined},
   but we're doing that for most items.  Provide a default of false.

 - not really a type issue, but strange: we're using a translated string
   as a key= on our <MenuItem/> elements when we have a unique ID.  Use
   the id instead, which is what the sidebar does.
Port files-card-body.{jsx -> tsx} in preparation for work on the sorting
function. Create an enum type for our various sort options and use it in
preference to hard-coded string values: that let's us drop the
`default:` on our `switch()` over the possible sort types: TypeScript is
now doing exhaustiveness checking for us.

This found some bugs:

  - an invalid sort order value in localestore of the browser could
    cause an oops by returning an undefined sort function (due to the
    explicit return in default case of our `switch` in `compare()`.  We
    now use an as_sort() function when reading the value to make sure
    it's one of our supported sort orders.

  - fsinfo says that the `.size` and `.mtime` fields might be missing,
    so we need to handle those cases, both in sorting and on display.
    `.name` is guaranteed to be present, since we set it ourselves

  - there were some very fine edge cases in our handling of selection:
    it's not clear if these could lead to errors in practice, but let's
    clean them up
If we tie on the mtime or the file size then fall back to sorting based
on name — it's always unique.

This should give us a fully-defined sort order for all directories.

We can now remove a TODO: from a testcase.

// treat non-regular files and infos with missing 'size' field as having size of zero
const size = (a: FolderFileInfo) => (a.type === "reg" && a.size) || 0;
const mtime = (a: FolderFileInfo) => a.mtime || 0; // fallbak for missing .mtime field
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.

case Sort.za:
return (a, b) => dir_sort(a, b) || name_sort(b, a);
case Sort.first_modified:
return (a, b) => dir_sort(a, b) || (mtime(a) - mtime(b)) || name_sort(a, b);
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.

case Sort.first_modified:
return (a, b) => dir_sort(a, b) || (mtime(a) - mtime(b)) || name_sort(a, b);
case Sort.last_modified:
return (a, b) => dir_sort(a, b) || (mtime(b) - mtime(a)) || name_sort(a, b);
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.

@@ -423,7 +447,7 @@ const Row = React.memo(function Item({ file, isSelected }) {
dataLabel="date"
modifier="nowrap"
>
{timeformat.dateTime(file.mtime * 1000)}
{file.mtime ? timeformat.dateTime(file.mtime * 1000) : 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.

"-R",
...clipboard,
currentPath
]).catch(err => addAlert(err.message, AlertVariant.danger, `${new Date().getTime()}`));
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.

{
id: "copy-item",
title: _("Copy"),
onClick: () => setClipboard(selected.map(s => path.join("/") + "/" + s.name)),
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.

Copy link
Member

@jelly jelly left a comment

Choose a reason for hiding this comment

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

Some of the commits could have been separate PRs, the menu changes will need another round of rebasing for feature PRs.

@@ -55,7 +56,7 @@ export const SettingsDropdown = ({ showHidden, setShowHidden }) => {
return (
<Dropdown
Copy link
Member

Choose a reason for hiding this comment

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

Note to self, this is basically the KebabDropdown...

const _ = cockpit.gettext;

type MenuItem = { type: "divider" } | {
type?: never,
Copy link
Member

Choose a reason for hiding this comment

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

TypeScript introduced a new type never, which indicates the values that will never occur.

But uh, this will be set to a string at some point?

Copy link
Member Author

Choose a reason for hiding this comment

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

No.

This is basically a way of discriminating the union: either we have items that set type to the literal string "divider", or it's forbidden from appearing at all (never).

/*
* This file is part of Cockpit.
*
* Copyright (C) 2017 Red Hat, Inc.
Copy link
Member

Choose a reason for hiding this comment

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

2017? Sounds like a long time ago :)

"-R",
...clipboard,
currentPath
]).catch(err => addAlert(err.message, AlertVariant.danger, `${new Date().getTime()}`));
Copy link
Member

Choose a reason for hiding this comment

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

At some point a huge function becomes unreadable in a list

Copy link
Member Author

Choose a reason for hiding this comment

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

I can move it back out of you prefer. It could also be a separate function — there are some other cases similar to this.

@jelly jelly self-requested a review June 20, 2024 12:40
@jelly
Copy link
Member

jelly commented Jun 20, 2024

Needs some small fixes, so I retracted my +1.

@allisonkarlitskaya
Copy link
Member Author

I opened cockpit-project/cockpit#20635 for the arch failure — it's definitely not our issue, but it seems to be aggravated by the test case change.

I'm trying it again. We can't really land this with a test that goes red 50% of the time, though...

@jelly
Copy link
Member

jelly commented Jun 20, 2024

So this needs a naughty now, as the Arch test always fails?

@allisonkarlitskaya
Copy link
Member Author

So this needs a naughty now, as the Arch test always fails?

Ya, It's a minor abuse of naughties, but from the standpoint of cockpit-files, cockpit-bridge is part of the set of system packages that we get from arch... The fact that it's only one distro where this happens also makes it feel more "right". And even once we fix this in the bridge, we need to wait for it to filter in...

@allisonkarlitskaya
Copy link
Member Author

arch@bots#6532 went green so I consider this PR good to go once cockpit-project/bots#6532 lands.

@allisonkarlitskaya allisonkarlitskaya merged commit ccf1f9b into cockpit-project:main Jun 20, 2024
15 checks passed
@allisonkarlitskaya allisonkarlitskaya deleted the sort-fallback branch June 20, 2024 14:04
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.

None yet

3 participants