-
Notifications
You must be signed in to change notification settings - Fork 925
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
Conversation
Wiz Scan Summary
|
🤖 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. |
There was a problem hiding this 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.
coderd/database/migrations/000239_update_notification_templates.up.sql
Outdated
Show resolved
Hide resolved
site/src/pages/UserSettingsPage/NotificationsPage/NotificationsPage.tsx
Outdated
Show resolved
Hide resolved
site/src/pages/DeploySettingsPage/NotificationsPage/NotificationsPage.stories.tsx
Show resolved
Hide resolved
@@ -187,6 +201,13 @@ const EventsView: FC<EventsViewProps> = ({ | |||
Webhook method is enabled, but the endpoint is not configured. | |||
</Alert> | |||
)} | |||
|
|||
{isUsingSmpt && !smtpConfig && ( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
There was a problem hiding this 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
Object.entries(data.template_disabled_map).map( | ||
([id, disabled]) => | ||
({ | ||
id, | ||
disabled, | ||
updated_at: new Date().toISOString(), | ||
}) as NotificationPreference, | ||
), |
There was a problem hiding this comment.
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
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( | ||
", ", | ||
)}`, | ||
); | ||
}; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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]) => ( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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} /> |
There was a problem hiding this comment.
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}> |
There was a problem hiding this comment.
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) => { |
There was a problem hiding this comment.
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
When I see a parent switch that's off, I assume the whole group is off
I think I would expect this behavior:
- When the parent switch is off, all child switches are off
- When the parent switch is on, at least one child switch is on
- Switching the parent switch from on to off turns all child switches off
- Switching the parent switch from off to on turns all child switches on
- Switching a child switch to on will switch the parent to on if the parent isn't already on
- If a child switch is the only one that's on, and it's flipped off, the parent switch flips off, too
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
onToggle={(checked) => { | ||
const updated = { ...disabledPreferences.data }; | ||
for (const tpl of templates) { | ||
updated[tpl.id] = !checked; | ||
} | ||
return updated; | ||
}} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
✔️ PR 14175 Updated successfully.
|
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? |
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.
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). |
There was a problem hiding this 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.
site/src/pages/DeploySettingsPage/NotificationsPage/NotificationsPage.tsx
Outdated
Show resolved
Hide resolved
<SidebarNavSubItem href="notifications"> | ||
Notifications | ||
</SidebarNavSubItem> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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.
I've applied the required changes and received approval from another engineer
There was a problem hiding this 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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, sorry. Misread that
Deployment Notification Settings:
User Notification Settings:
To QA this locally you can run: