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

Harden the role editor by falling back to YAML #52034

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

bl-nero
Copy link
Contributor

@bl-nero bl-nero commented Feb 11, 2025

Enables the user to recover by editing role using YAML in case if an unexpected error happens either in rendering or initial role conversion. Also reorganizes error handling to give users more meaningful messages.

Screenshot 2025-02-13 at 15 14 22
Screenshot 2025-02-13 at 15 13 17
Screenshot 2025-02-13 at 15 09 42
Screenshot 2025-02-13 at 15 06 09

Followed up by https://github.com/gravitational/teleport.e/pull/6076.

Tested along with https://github.com/gravitational/teleport.e/pull/6076 in the following scenarios:

  • Error thrown by roleToRoleEditorModel()
  • Error thrown while rendering the StandardEditor component
  • Access graph server unavailable when opening the role editor or in the middle of editing session

In each case, the user can still edit and save the role.

@bl-nero bl-nero added no-changelog Indicates that a PR does not require a changelog entry backport/branch/v17 labels Feb 11, 2025
Enables the user to recover by editing role using YAML in case if an
unexpected error happens either in rendering or initial role conversion.
Also reorganizes error handling to give users more meaningful messages.
@bl-nero bl-nero force-pushed the bl-nero/role-editor-catch-errors branch from 32d509e to 33a25ed Compare February 17, 2025 10:08
@bl-nero bl-nero marked this pull request as ready for review February 17, 2025 10:22
@bl-nero bl-nero requested a review from avatus February 17, 2025 10:22
@ryanclark
Copy link
Contributor

Are these the best error messages to surface to the user? HTTP status code and the URL?

@bl-nero
Copy link
Contributor Author

bl-nero commented Feb 18, 2025

Are these the best error messages to surface to the user? HTTP status code and the URL?

@ryanclark I bet they are not, and we may refine it further. However, right now, it's only the HTTP status code and the URL, which gives users much less information about the context.

@avatus
Copy link
Contributor

avatus commented Feb 18, 2025

Are these the best error messages to surface to the user? HTTP status code and the URL?

@ryanclark I bet they are not, and we may refine it further. However, right now, it's only the HTTP status code and the URL, which gives users much less information about the context.

im not sure what this means? "what" is only the status code and URL? what you're displaying? i think thats what ryan is saying. or are you saying that somehow thats the only error we get from X backend?

@bl-nero
Copy link
Contributor Author

bl-nero commented Feb 18, 2025

OK, so to clarify. Here is our current UX when I kill the access graph server:

Screenshot 2025-02-18 at 15 54 57

And here's what happens if I inject an error into one of the aforementioned places in our frontend code:

Screenshot 2025-02-18 at 15 56 02

@bl-nero
Copy link
Contributor Author

bl-nero commented Feb 18, 2025

@avatus And replying to your question: yes, that's all we got from Teleport backend. Just an error code and an empty response body.

@bl-nero
Copy link
Contributor Author

bl-nero commented Feb 18, 2025

And I guess it may be more useful if we said something like "502: bad gateway" instead, but this is how our error-handling code works right now; changing it would have global implications and I'd rather do it separately after having a discussion with the UX team.

Copy link
Member

@ravicious ravicious left a comment

Choose a reason for hiding this comment

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

I looked at the test only so far, I haven't had the time to look at the code yet, I'll do so tomorrow.

throw new Error('oh noes, it crashed');
});
// Ignore the error being reported on the console.
jest.spyOn(console, 'error').mockImplementation();
Copy link
Member

Choose a reason for hiding this comment

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

We shouldn't mock console functions without also validating that we received the expected calls on them. Otherwise we risk introducing unwanted errors like act warnings without realizing it.

https://github.com/ValentinH/jest-fail-on-console/?tab=readme-ov-file#but-i-have-some-expected-console-errorswarning

Patch

Copy then pbpaste | git apply.

diff --git a/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.test.tsx b/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.test.tsx
index dce371680e..49af6b8475 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.test.tsx
+++ b/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.test.tsx
@@ -401,6 +401,18 @@ test('YAML editor usable even if the standard one throws', async () => {
   expect(onSave).toHaveBeenCalledWith({
     yaml: '{"modified":1}',
   });
+
+  expect(console.error).toHaveBeenCalledTimes(4);
+  expect(console.error).toHaveBeenCalledWith(
+    expect.objectContaining({
+      message: expect.stringMatching('oh noes, it crashed'),
+    })
+  );
+  expect(console.error).toHaveBeenCalledWith(
+    expect.stringMatching(
+      'The above error occurred in the <mockConstructor> component'
+    )
+  );
 });
 
 it('YAML editor usable even if the initial conversion throws', async () => {
@@ -440,6 +452,16 @@ it('YAML editor usable even if the initial conversion throws', async () => {
   expect(onSave).toHaveBeenCalledWith({
     yaml: '{"modified":1}',
   });
+
+  expect(console.error).toHaveBeenCalledTimes(1);
+  expect(console.error).toHaveBeenCalledWith(
+    expect.any(String),
+    expect.any(String),
+    'Could not convert Role to a standard model',
+    expect.objectContaining({
+      message: expect.stringMatching('oh noes, it crashed'),
+    })
+  );
 });
 
 // Here's a trick: since we can't parse YAML back and forth, we use a

Copy link
Member

Choose a reason for hiding this comment

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

expect.objectContaining({message: …}) is a workaround I've learned some time ago for matching on error messages in Jest.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This seems rather fragile. We rely on how exactly the framework reports exceptions here. In the first snippet, where we assert it's been called 4 times, we also rely on the number of render calls. However, we don't even guarantee that nothing else happens; if the exception was thrown just once, and there was another error that originated from act, for example, this test would still pass (4 times, somewhere the expected error happens - we're clear). I'm not sure how to improve on this, though; if you consider making assertions on console messages important, I'd say we probably can leave with how we rely on the framework internals, but I think I'd rather move the assertions to the implementation block so that the test skips expected errors and throws on others.

Copy link
Member

Choose a reason for hiding this comment

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

but I think I'd rather move the assertions to the implementation block so that the test skips expected errors and throws on others

That would work too. That's what I initially wanted to do, but I couldn't make it work. I think it's because I was doing expect(foo).toEqual(…) from within mockImplementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, after some back and forth, I wasn't able to make a correct assertion on these errors that would be meaningful and not fragile. I'm in favor of not doing it at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we care about other unexpected errors that may have been thrown from our component, we still have a lot of other test cases to catch these.

Copy link
Member

Choose a reason for hiding this comment

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

I agree that the first bunch of expectations is rather fragile, but the second bunch seems to be more sturdy, as there is an expectation placed on a single call to console.error. Would you consider including at least that one?

@ravicious ravicious self-requested a review February 18, 2025 17:02
});

// The standard editor will automatically preview the changes based on state updates
// but the yaml editor needs to be told when to update (the preview button)
const handleYamlPreview = useCallback(async () => {
Copy link
Member

Choose a reason for hiding this comment

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

Nit, if it was important for handleYamlPreview to have a stable identity between renders before, then it should continue to use useCallback. useAsync itself cannot make the received callback stable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch.

Comment on lines 161 to 163
standardModel.roleModel !== undefined &&
!standardModel.roleModel.requiresReset &&
!validator.validate()
Copy link
Member

Choose a reason for hiding this comment

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

Just a theoretical question rather than a request because I don't have too much time to dive deep into this, but would it be possible to organize the code in such a way that we don't have to explicitly check for standardModel.roleModel being undefined?

I see it's repeated in a couple of places, I guess the reason behind it is that !standardModel.roleModel?.requiresReset would have different, unwanted semantics.

But it's not something that's done often and I feel like it's going to throw the reader of this code for a spin whenever they encounter one of those checks. So if it'd be possible to simplify it, then it'd only be better.

I suppose changing the underlying object shape might be difficult, but then maybe encapsulating this in a function would work?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm. Actually, I was considering replacing the requiresReset field with a function that would return a boolean looking at the number of conversion errors. So I'll do exactly this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, not exactly. There are three places in which I explicitly compare the model to undefined, and in each case, the logic is slightly different. I don' think that extracting it to a function brings us any closer to this being more understandable.

type RoleDiffProps = {
roleDiffElement: React.ReactNode;
updateRoleDiff: (role: Role) => void;
errorMessage: string;

/** @deprecated Use {@link RoleDiffProps.roleDiffAttempt} instead. */
Copy link
Member

Choose a reason for hiding this comment

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

Nit, but does using @link enriches the output in your editor? Because mine just renders it as {@link RoleDiffProps.roleDiffAttempt} and we don't generate docs from JSDoc anyway, so if it doesn't do much for other editors either, I'm thinking we could not use @link. 😄

I know we do the square brackets thing in Go (e.g. Use [roleDiffAttempt]), but arguably it's less disruptive to the surrounding text than @link.

Copy link
Contributor Author

@bl-nero bl-nero Feb 20, 2025

Choose a reason for hiding this comment

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

I'm using VS Code, and it supports @link, but doesn't support square brackets. What do you use? If your one supports square brackets, and this is the convention that we use, perhaps it's about plugin configuration.

@bl-nero bl-nero requested a review from ravicious February 20, 2025 18:12
throw new Error('oh noes, it crashed');
});
// Ignore the error being reported on the console.
jest.spyOn(console, 'error').mockImplementation();
Copy link
Member

Choose a reason for hiding this comment

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

I agree that the first bunch of expectations is rather fragile, but the second bunch seems to be more sturdy, as there is an expectation placed on a single call to console.error. Would you consider including at least that one?

@public-teleport-github-review-bot public-teleport-github-review-bot bot removed the request for review from rudream February 21, 2025 18:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport/branch/v17 no-changelog Indicates that a PR does not require a changelog entry size/md ui
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants