Skip to content

feat(site): implement notification ui #14175

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

Merged
merged 34 commits into from
Aug 9, 2024
Merged

Conversation

BrunoQuaresma
Copy link
Collaborator

@BrunoQuaresma BrunoQuaresma commented Aug 5, 2024

Deployment Notification Settings:

Screenshot 2024-08-06 at 10 32 40

User Notification Settings:

Screenshot 2024-08-06 at 10 34 06

To QA this locally you can run:

CODER_EXPERIMENTS=*,notifications ./scripts/develop.sh

@BrunoQuaresma BrunoQuaresma self-assigned this Aug 5, 2024
@wiz-inc-56b28a72bd
Copy link

Wiz Scan Summary

IaC Misconfigurations 0C 0H 0M 0L 0I
Vulnerabilities 0C 0H 0M 0L 0I
Sensitive Data 0C 0H 0M 0L 1I
Total 0C 0H 0M 0L 1I
Secrets 0🔑

Copy link

alwaysmeticulous bot commented Aug 5, 2024

🤖 Meticulous spotted visual differences in 203 of 1313 screens tested: view and approve differences detected.

Last updated for commit 7858a5a. This comment will update as new commits are pushed.

@BrunoQuaresma BrunoQuaresma marked this pull request as ready for review August 6, 2024 13:32
@BrunoQuaresma BrunoQuaresma requested review from dannykopping, a team and jaaydenh and removed request for a team August 6, 2024 13:34
Copy link
Contributor

@dannykopping dannykopping left a comment

Choose a reason for hiding this comment

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

I'm not too well-versed on the frontend so I won't comment on the code itself, but I did highlight a couple aspects for discussion.

@@ -187,6 +201,13 @@ const EventsView: FC<EventsViewProps> = ({
Webhook method is enabled, but the endpoint is not configured.
</Alert>
)}

{isUsingSmpt && !smtpConfig && (
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think SMTP config can be blank.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

From the types, it could be tho, but you shared this message a few minutes ago:

We can't assume SMTP will always be configured. We should have the same check here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done!

Copy link
Member

@Parkreiner Parkreiner left a comment

Choose a reason for hiding this comment

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

A lot of this looks really good, but I've got some concerns about how the UI is architected right now, not just from a coding standpoint, but also a UI standpoint

Setting this to "Request changes" to be on the safe side, but if you think that these are acceptable, I can change my tune and flip the approval status

Comment on lines 35 to 42
Object.entries(data.template_disabled_map).map(
([id, disabled]) =>
({
id,
disabled,
updated_at: new Date().toISOString(),
}) as NotificationPreference,
),
Copy link
Member

Choose a reason for hiding this comment

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

For this part, I think it'd be better to swap as for satisfies. That way, you can't accidentally pass in too many properties, and you don't get runtime/compile-time drift

I also tried adding the type parameter to the .map callback, but that didn't raise any complaints about extra properties

Comment on lines +19 to +29
export const castNotificationMethod = (value: string) => {
if (notificationMethods.includes(value as NotificationMethod)) {
return value as NotificationMethod;
}

throw new Error(
`Invalid notification method: ${value}. Accepted values: ${notificationMethods.join(
", ",
)}`,
);
};
Copy link
Member

Choose a reason for hiding this comment

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

Tagging this for now while I look through the other files. I don't fully understand the point of this function, and at the very least, I think that it should be redefined as a type predicate

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is just easier to use IMO.

Type predicate:

const isNotificationMethod = (v: string): v is NotificationMethod => {}
// Usage
values.map(v => {
   if(!isNotificationMethod(v)) {
      throw new Error("v is not valid") 
   }
   return <Component method={method} />
})

Cast function

const castNotificationMethod = (v: string): NotificationMethod => {}
// Usage
values.map(v => {
   const method = castNotificationMethod(v)
   return <Component method={method} />
})

endpoint: "https://example.com",
},
},
} as DeploymentValues,
Copy link
Member

Choose a reason for hiding this comment

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

Another case where I think as can be swapped for satisfies

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In this case, as is intentional. I want to be able to use partial data and not have to pass more than 100+ values.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, sorry. Misread that

});
const ready =
templatesByGroup.data && dispatchMethods.data && deploymentValues;
const tab = searchParams.get("tab") || "events";
Copy link
Member

Choose a reason for hiding this comment

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

Do you think this might be a good use case for useSearchParamsKey? It'd have to be changed a little bit to account for the || instead of the ??, though

Copy link
Collaborator Author

@BrunoQuaresma BrunoQuaresma Aug 8, 2024

Choose a reason for hiding this comment

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

I don't think it would make too much difference since I'm using regukar links to update the search params. I'm using || instead of ?? to grab empty strings as well.

}) => {
return (
<Stack spacing={3}>
{Object.entries(templatesByGroup).map(([group, templates]) => (
Copy link
Member

Choose a reason for hiding this comment

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

Could this have a .sort tacked on to make sure there's no risk of the UI elements jumping around? Mainly worried that as the objects gets updated over time, the entries array could serialize the values in different orders across re-renders

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Going to implement this in the selectTemplatesByGroup

value={value}
/>
</ListItem>
<Divider css={styles.divider} />
Copy link
Member

Choose a reason for hiding this comment

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

Not a big deal, but could this changed so that we check the array index and conditionally choose not to render out the divider if it's the last one? I know we have the last-child styling, but it feels like it could be a bit more fragile over time

templatesByGroup,
}) => {
return (
<Stack spacing={3}>
Copy link
Member

Choose a reason for hiding this comment

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

I think the tables could use a little more spacing between them. They have the borders, so there's no real risk of them getting confused for each other, but they feel a little tense right now

{ready ? (
<Stack spacing={3}>
{Object.entries(templatesByGroup.data).map(([group, templates]) => {
const allDisabled = templates.some((tpl) => {
Copy link
Member

Choose a reason for hiding this comment

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

I think this is a bit confusing from a user standpoint
Screenshot 2024-08-08 at 11 39 21 AM

When I see a parent switch that's off, I assume the whole group is off

I think I would expect this behavior:

  1. When the parent switch is off, all child switches are off
  2. When the parent switch is on, at least one child switch is on
  3. Switching the parent switch from on to off turns all child switches off
  4. Switching the parent switch from off to on turns all child switches on
  5. Switching a child switch to on will switch the parent to on if the parent isn't already on
  6. If a child switch is the only one that's on, and it's flipped off, the parent switch flips off, too

Copy link
Member

Choose a reason for hiding this comment

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

I also think the current email icons are a bit misleading, because before I tried interacting with them, I expected them to be buttons. As in, clicking one would send off an email in some way

Not sure what a good change would be. Maybe something badge-like?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Related to the first comment, I see how this can be confusing. I will put more thought into it. I just would rather do it in a different PR to avoid this one getting longer than it is.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I also think the current email icons are a bit misleading, because before I tried interacting with them, I expected them to be buttons. As in, clicking one would send off an email in some way

I can try a badge and see how it looks like

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I can deal with a slightly confusing UI (or at least confusing from my point of view) as long as it works predictably

Comment on lines 123 to 129
onToggle={(checked) => {
const updated = { ...disabledPreferences.data };
for (const tpl of templates) {
updated[tpl.id] = !checked;
}
return updated;
}}
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 the best alternative is, but this feels like a code smell to me. Feels like it's breaking out of React's one-way data binding because even though there's shared state between the container and the individual switches, the parent container has little say in how the state is allowed to change. I don't think this is big enough of an edge case to justify breaking React conventions

My gut feeling is also that, just to reduce coupling, any handler like onToggle should always be a void function

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think this is big enough of an edge case to justify breaking React conventions

Is there a place where I can read about this convention?

My gut feeling is also that, just to reduce coupling, any handler like onToggle should always be a void function

I don't know if I agree with this... what are the pros and cons?

Eg. An onChange handler returns an event and not a void. I think it is pretty common to have handlers returning data.

Copy link
Member

@Parkreiner Parkreiner Aug 9, 2024

Choose a reason for hiding this comment

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

To be clear, onChange and all the event handlers receive an event as input, but they don't return anything. This is the type React has:

type React.EventHandler<E extends SyntheticEvent<any>> = (event: E) => void;

In real HTML, you can have the event handlers return booleans to affect event bubbling behavior, but React shaves that off for the synthetic events.

Like, if I have a component like this:

function MyComponent () {
  const [color, setColor] = useState("blue");
  return <button onClick={() => setColor("red"}>Click me</button>;
}

I don't think anyone would expect the button to have any way to affect MyComponent's state, beyond the onClick. But even then, MyComponent is in full control of the event handler definition – it fully prescribes how its own state is allowed to change. MyComponent has a dependency on button, but everything related to its state is self-contained in the one component. button doesn't have direct access to the state setter, so all it can do is follow the very restricted set of instructions it's given

But with the switches, the main state is being split across four different places: the RQ cache living outside the UI tree, the parent page component, the individual switches, and the mutation key factory function. It felt like I had to jump through a lot of hoops (and two different files) to figure out exactly how the state was changing

I don't think there's a way to fully consolidate the state in one place, just because we do need RQ, but four places for one main piece of state feels like a bit much to me

My preference would be to have the switches be "dumb" and just call an event handler when clicked. Then put all the state related to the functionality inside the parent page component

Copy link
Member

@Parkreiner Parkreiner Aug 9, 2024

Choose a reason for hiding this comment

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

But as far as conventions around one-way data-binding/unidirectional data flow, I tried looking on React.dev, and I'm surprised there wasn't too much dedicated information on it. There's Thinking in React, particularly parts 4 and 5, but I think it's the sort of thing that's not explicitly shouted out, even though it informs pretty much every lesson on the site

The main reason I know about all this is because I just happen to know that React was designed around the flux architecture pattern. Facebook has an archive page describing what's it's about

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think I better understand your concerns now. Thanks for explaining them to me. I will fix it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done!

Copy link

github-actions bot commented Aug 8, 2024


✔️ PR 14175 Updated successfully.
🚀 Access the credentials here.

cc: @BrunoQuaresma

@BrunoQuaresma BrunoQuaresma changed the title feat(site): add notifications ui feat(site): implement notification ui Aug 8, 2024
@stirby
Copy link
Collaborator

stirby commented Aug 8, 2024

The UI feels great. The profile settings page is perfect.

Why do we allow administrators to select webhook delivery methods if no endpoint is configured? Can we prevent them from selecting it?

Screenshot 2024-08-08 at 4 01 51 PM

@BrunoQuaresma
Copy link
Collaborator Author

Why do we allow administrators to select webhook delivery methods if no endpoint is configured? Can we prevent them from selecting it?

I've been thinking about that too. I'm wondering if we could validate this on the dispatch-methods endpoint by only returning the methods that would work, or by not allowing Coder Server to start without the correct notification configurations. What do you think, @dannykopping?

@dannykopping
Copy link
Contributor

I'm wondering if we could validate this on the dispatch-methods endpoint by only returning the methods that would work

I'm not sure we want to add this complexity. What you have currently is the best way IMHO; this prompts folks to configure their server correctly if things are not correct.

or by not allowing Coder Server to start without the correct notification configurations

No, because that would require everyone to always set webhook endpoints (we use a default value for SMTP smarthosts but may drop that ahead of the release).

Copy link
Contributor

@dannykopping dannykopping left a comment

Choose a reason for hiding this comment

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

LGTM, one nit and a suggestion but I don't need to review again.

Comment on lines 160 to 162
<SidebarNavSubItem href="notifications">
Notifications
</SidebarNavSubItem>
Copy link
Member

Choose a reason for hiding this comment

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

Bruno, you're so good at your job. This was the first thing I thought to check, knowing how easy it would be to miss adding it here. 😄 Should it be behind the same experiment check as in the other sidebar tho?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks @aslilac ❤️. Yes, it definitely needs to be under the experiment.

@BrunoQuaresma BrunoQuaresma dismissed Parkreiner’s stale review August 9, 2024 16:14

I've applied the required changes and received approval from another engineer

Copy link
Member

@Parkreiner Parkreiner left a comment

Choose a reason for hiding this comment

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

Looks good! Thanks for putting up with all my nagging

endpoint: "https://example.com",
},
},
} as DeploymentValues,
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, sorry. Misread that

@BrunoQuaresma BrunoQuaresma merged commit 21942af into main Aug 9, 2024
31 of 32 checks passed
@BrunoQuaresma BrunoQuaresma deleted the bq/user-notifications branch August 9, 2024 16:43
@github-actions github-actions bot locked and limited conversation to collaborators Aug 9, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants