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

Resolve name conflicts in packages #3596

Draft
wants to merge 58 commits into
base: master
Choose a base branch
from
Draft

Conversation

fiskus
Copy link
Member

@fiskus fiskus commented Jul 20, 2023

  • Added "Rename" button and prompt for a new file name
  • Added new properties to files:
    • conflict is this file conflicted with some other file
    • changed what was changed in this file
  • Removed unused FilesSelector

// TODO:

@codecov
Copy link

codecov bot commented Jul 20, 2023

Codecov Report

Merging #3596 (e33f96e) into master (34aa05f) will decrease coverage by 0.09%.
The diff coverage is 2.67%.

@@            Coverage Diff             @@
##           master    #3596      +/-   ##
==========================================
- Coverage   36.14%   36.05%   -0.09%     
==========================================
  Files         676      677       +1     
  Lines       29673    29754      +81     
  Branches     4321     4340      +19     
==========================================
+ Hits        10724    10727       +3     
- Misses      17812    17883      +71     
- Partials     1137     1144       +7     
Flag Coverage Δ
api-python 91.35% <ø> (ø)
catalog 9.81% <2.67%> (-0.03%) ⬇️
lambda 86.03% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files Changed Coverage Δ
catalog/app/components/Dialog/Prompt.tsx 0.00% <0.00%> (ø)
catalog/app/components/FileEditor/CreateFile.tsx 0.00% <0.00%> (ø)
...p/containers/Bucket/PackageDialog/EditFileMeta.tsx 0.00% <0.00%> (ø)
...p/containers/Bucket/PackageDialog/EditFileName.tsx 0.00% <0.00%> (ø)
...app/containers/Bucket/PackageDialog/FilesInput.tsx 0.00% <0.00%> (ø)
...iners/Bucket/PackageDialog/PackageCreationForm.tsx 0.00% <0.00%> (ø)
.../containers/Bucket/PackageDialog/PackageDialog.tsx 26.07% <27.27%> (+0.24%) ⬆️

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@fiskus
Copy link
Member Author

fiskus commented Aug 3, 2023

I deployed it to searchminimal

@fiskus fiskus requested a review from nl0 August 3, 2023 09:44
Copy link
Member

@nl0 nl0 left a comment

Choose a reason for hiding this comment

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

First, I'd like to point out that this feature is essential and we were missing it for so long. However, I feel like the current implementation is not yet ready for the prime time.

  1. Here's some UX issues / suggestions that are (probably) easy to address:

    1. In the file rename dialog enter triggers package push -- probably should trigger only rename instead.

    2. In the file rename dialog "submit" button label seems too generic, "rename" or smth would be more appropriate.

    3. Instead of always succeeding, I think it's better to check if the name is already taken and disallow submitting to prevent automatic rename (which is kinda confusing when it happens).

    4. Instead of treating conflicts as errors, maybe it would make more sense to just show warnings when auto-renaming files, and don't block submit.

    5. When there are conflicts, "files should match schema" error message is shown, which is confusing and doesn't reflect reality.

    6. Auto-renaming is sometimes buggy and leads to smth like file (conflict 1) (conflict 1).ext instead of file (conflict 2).ext.

    7. (Nice-to-have, tangential) Consider adding a tooltip describing the state of an entry (e.g. added from s3, removed, renamed from $key, etc. showing the physical key may be useful as well).

    8. (Nice-to-have, tangential) Adding an option to toggle deleted files visibility may be useful to reduce visual clutter, tho it feels orthogonal and somewhat out of scope.

  2. My main complaint, tho, is I feel like we're stretching the model too far and the abstractions start leaking -- this makes the logic harder to follow and less predictable, ultimately leading to unexpected errors and difficulties maintaining this code and developing it further. So my advice is to adjust the model to accommodate the new requirements and make it more consistent. Some suggestions:

    1. Use composition: contain / reference File objects instead of mutating (extending) them directly.

    2. "renamed" state implementation is inconsistent with the model, since the model only has the notion of existing / added / deleted entries. Instead of tracking this as "changed" prop, maybe on first rename we should (1) "add" an entry with the new logical key and set "renamedFrom" (or smth like that) prop on it pointing to the original logical key; and (2) mark the original entry as deleted -- this feels more in-line with our current state model and UX. The added entry with "renamedFrom" prop may be rendered with the "revert" action instead of "remove".

const color = React.useMemo(() => {
if (!disabled && state !== 'invalid' && !R.isNil(value) && !R.isEmpty(value)) {
return 'primary'
}
Copy link
Member

Choose a reason for hiding this comment

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

what's the reason for implicitly returning undefined instead of explicit 'inherit'?

Copy link
Member Author

Choose a reason for hiding this comment

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

Just saved a couple of symbols, and done this consistently with EditFileName.tsx

@fiskus
Copy link
Member Author

fiskus commented Aug 3, 2023

Instead of treating conflicts as errors, maybe it would make more sense to just show warnings when auto-renaming files, and don't block submit.

I think it just unacceptable to submit files with auto-renaming files. It's unexpected for user, that he added a file with one name, but it was submitted with another name. Warning is easy to miss.

Instead of always succeeding, I think it's better to check if the name is already taken and disallow submitting to prevent automatic rename (which is kinda confusing when it happens).

Yes, good idea and probably easy. First, I interpreted it differently, and it could take a week :)

Auto-renaming is sometimes buggy and leads to smth like file (conflict 1) (conflict 1).ext instead of file (conflict 2).ext.

Ok, but I couldn't reproduce, I'll try to find it in code

nice to have: tooltip

Good idea, I'll implement it in standalone PR

nice to have: hide deleted

Hmm, could be useful, but I never felt I need this

@fiskus
Copy link
Member Author

fiskus commented Aug 3, 2023

On model

Yes, I agree, I tried to fit into model something that it wasn't designed for. I have two excuses and two solutions.

I wanted to do a minimal change, to have better chances to fit into horizon myself and to fit review into horizon. And I wanted to do a feature separately from refactoring. It's a large separate topic why it's a good idea to make it work first, then make it good (but of course, only if we don't forget and invest time in making good).

I suggest merging this (after addressing UX issues and bugs) and work on refactoring in a separate PR. Also, there is a spec on refactoring file types https://github.com/quiltdata/specs/pull/53, it, for example, has the same proposal to contain file in a separate property. I'll split the spec into three specs as noted in description

@fiskus
Copy link
Member Author

fiskus commented Aug 4, 2023

  • fixed form submit on rename file
  • "Submit" → "Rename"
  • "files should match schema" → "There are files with identical names. Rename or remove them please"

  • treat conflict errors as warnings -- skipped
  • nice-to-haves -- TODO
  • model adjustments -- TODO

  • Check if user renames to existing file -- providing data to the editing component would be too hacky, and moving editing component up looks a good thing (also, for performance), but it will take more time
  • "file (conflict 1) (conflict 1).ext" -- couldn't find this bug. Though, it can happen when you rename "file (conflict 2).ext" to "file (conflict 1).ext" (not to "file.ext"), manually

@nl0
Copy link
Member

nl0 commented Aug 4, 2023

I wanted to do a minimal change, to have better chances to fit into horizon myself and to fit review into horizon

well, in the end it did not fit, so i don't think we should rush and try to fit it anyways in some half-baked form

It's a large separate topic why it's a good idea to make it work first, then make it good (but of course, only if we don't forget and invest time in making good).

it's ok (in my book) to make it work and then make it good before shipping.

i can't agree that breaking things, shipping them, and then fixing them is a valid approach.

i'm kinda ok with expanding the model and postponing a proper refactoring, but i'm not ok with breaking the model, so i still feel like this requires addressing:

"renamed" state implementation is inconsistent with the model, since the model only has the notion of existing / added / deleted entries. Instead of tracking this as "changed" prop, maybe on first rename we should (1) "add" an entry with the new logical key and set "renamedFrom" (or smth like that) prop on it pointing to the original logical key; and (2) mark the original entry as deleted -- this feels more in-line with our current state model and UX. The added entry with "renamedFrom" prop may be rendered with the "revert" action instead of "remove".

since the current approach is too brittle and has edge cases that aren't handled

@nl0
Copy link
Member

nl0 commented Aug 4, 2023

I wanted to do a minimal change, to have better chances to fit into horizon myself and to fit review into horizon

well, in the end it did not fit, so i don't think we should rush and try to fit it anyways in some half-baked form

i realize that i am accountable for this as well to some extent (as a reviewer).

@drernie @akarve you can override and approve this PR if you think it's more important to get this out the door sooner and backlog the polish / refactoring, but i'm convinced that we should not compromise on quality. scope hammering is not compromising on quality, it's compromising on, well, scope, and this feature didn't make it.

@nl0
Copy link
Member

nl0 commented Aug 4, 2023

  • fixed form submit on rename file

👍

* Check if user renames to existing file -- providing data to the editing component would be too hacky, and moving editing component up looks a good thing (also, for performance), but it will take more time

just pass a callback to check if the file exists in the manifest?

* `"file (conflict 1) (conflict 1).ext"` -- couldn't find this bug. Though, it can happen when you rename `"file (conflict 2).ext"` to `"file (conflict 1).ext"` (not to `"file.ext"`), manually

not sure how i managed that lol

Screenshot 2023-08-04 at 13 57 57

@@ -25,10 +29,11 @@ function Dialog({
const error = React.useMemo(() => validate(value), [validate, value])
const handleChange = React.useCallback((event) => setValue(event.target.value), [])
const handleSubmit = React.useCallback(
(event) => {
(event: React.FormEvent) => {
// event.stopPropagation()
Copy link
Member

Choose a reason for hiding this comment

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

pls uncomment or remove


function resolveName(
name: string,
obj: Record<string, any>,
Copy link
Member

Choose a reason for hiding this comment

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

this argument could probably have a more descriptive name

Copy link
Member Author

Choose a reason for hiding this comment

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

It's here for keys only, so it doesn't matter what kind of object is here. Just some object with keys

const newName = resolveNameConflictRudely(name, attempt)
if (!obj[newName]) return newName

return resolveName(name, obj, attempt + 1)
Copy link
Member

Choose a reason for hiding this comment

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

i doubt that would be an issue in practice, but in theory this can lead to error by exceeding the recursion depth (since js doesn't have a tail-call optimization, unfortunately), so if we are being pedantic, this function should be rewritten using a loop instead of recursion

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you mean you don't want to use recursion in JS in general, or in this particular case?

Copy link
Member

Choose a reason for hiding this comment

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

i don't "don't want" to, it's just not safe (may lead to an exception) if the depth is unbounded, and here it well may be, since we don't control what files (and how many) a user adds.
idk, maybe it's kinda ok in practice in this particular case (source):

The maximal recursion depth is limited by JavaScript engine. We can rely on it being 10000, some engines allow more, but 100000 is probably out of limit for the majority of them. There are automatic optimizations that help alleviate this (“tail calls optimizations”), but they are not yet supported everywhere and work only in simple cases.

but in general, in js recursion shouldn't be used for possibly unbounded iterative processes (tho it's ok to use it to traverse recursive data structures, ofc)

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, but IMO it's an overkill to sacrifice elegance of recursion to cover edge case with 10000 duplicate names. Also, we already have a limit on number of files: 10000

@@ -86,32 +109,108 @@ export type FilesAction = tagged.InstanceOf<typeof FilesAction>

export type LocalFile = FileWithPath & FileWithHash

// TODO: queue/array
Copy link
Member

Choose a reason for hiding this comment

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

what does this comment mean?

Copy link
Member Author

Choose a reason for hiding this comment

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

Use a list of objects instead of a single one. So, we can use infinite "revert"

...mainItems,
[resolvedName]: setKeyValue(
'conflict',
resolvedName === path ? undefined : path,
Copy link
Member

Choose a reason for hiding this comment

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

if i read it correctly, this won't unset the prop on a File instance, because undefined is ignored (see line 160). or am i missing smth?

Copy link
Member Author

Choose a reason for hiding this comment

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

It creates a copy of a file, first without 'conflict' (at cloning step), and then 'conflict' is ignoring (at setting step)

Copy link
Member Author

Choose a reason for hiding this comment

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

Don't invest time in it, since in the next iteration I will not do cloning :)

) {
const resolvedName = resolveName(
path,
itemsToCheck ? { ...mainItems, ...itemsToCheck } : mainItems,
Copy link
Member

Choose a reason for hiding this comment

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

{...undefined} works just fine, so you can skip the check

function addFile<T extends AnyFile, M extends Record<string, AnyFile>>(
path: string,
file: T,
mainItems: M,
Copy link
Member

Choose a reason for hiding this comment

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

why do you need this type var here? you're not using it. or is it needed for the return type?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I can skip it now. Earlier types were stricter, but I gave up

@@ -625,6 +624,8 @@ function PackageCreationForm({
validate={validateFiles as FF.FieldValidator<$TSFixMe>}
validateFields={['files']}
errors={{
conflict:
'There are files with identical names. Rename or remove them please',
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
'There are files with identical names. Rename or remove them please',
'There are entries with conflicting logical keys. Rename or remove them please',

not sure if this is more user-friendly for less-tech-savvy people, but it seems more correct

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'm not sure if they know what logical key is, but I agree with the rest

logical_key: string
size: number
meta?: JsonRecord
}

export class EntryNameError extends BaseError {
static data: ValidationEntry
Copy link
Member

Choose a reason for hiding this comment

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

i don't quite get it why this is needed (or at least why it's static)

Copy link
Member Author

Choose a reason for hiding this comment

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

This is used for showing in which file there was an error. Under the FilesInput
static is a mistyping copypasta, thanks

Comment on lines +34 to +44
function resolveName(
name: string,
obj: Record<string, any>,
attempt: number = 1,
): string {
if (!obj[name]) return name

const newName = resolveNameConflictRudely(name, attempt)
if (!obj[newName]) return newName

return resolveName(name, obj, attempt + 1)
Copy link
Member

Choose a reason for hiding this comment

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

i'm not sure what sacrifice do you mean:

Suggested change
function resolveName(
name: string,
obj: Record<string, any>,
attempt: number = 1,
): string {
if (!obj[name]) return name
const newName = resolveNameConflictRudely(name, attempt)
if (!obj[newName]) return newName
return resolveName(name, obj, attempt + 1)
function resolveName(name: string, obj: Record<string, any>): string {
let newName = name, attempt = 1
while (newName in obj) newName = resolveNameConflictRudely(name, attempt++)
return newName

@fiskus fiskus marked this pull request as draft August 11, 2023 12:44
@drernie
Copy link
Contributor

drernie commented Aug 21, 2023

Is this obsoleted by @fiskus' work on Selection's last Horizon?

@fiskus
Copy link
Member Author

fiskus commented Aug 22, 2023

No, it's still relevant. Alexei couldn't approve this due to his disagreement on tradeoffs I made in design. To reduce tradeoffs and make more suitable design for the tasks, I need to refactor this code first. Also, this unification of types can help me refactor #3681

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