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

ui/urql: transition to urql and use suspense to load user details #3430

Merged
merged 3 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions web/src/app/users/UserContactMethodList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from 'react'
import { gql, useQuery } from '@apollo/client'
import React, { useState, ReactNode } from 'react'
import { gql, useQuery } from 'urql'
import FlatList from '../lists/FlatList'
import { Button, Card, CardHeader, Grid, IconButton } from '@mui/material'
import makeStyles from '@mui/styles/makeStyles'
Expand All @@ -12,14 +12,14 @@ import UserContactMethodEditDialog from './UserContactMethodEditDialog'
import { Warning } from '../icons'
import UserContactMethodVerificationDialog from './UserContactMethodVerificationDialog'
import { useIsWidthDown } from '../util/useWidth'
import Spinner from '../loading/components/Spinner'
import { GenericError, ObjectNotFound } from '../error-pages'
import SendTestDialog from './SendTestDialog'
import AppLink from '../util/AppLink'
import { styles as globalStyles } from '../styles/materialStyles'
import { UserContactMethod } from '../../schema'
import UserContactMethodCreateDialog from './UserContactMethodCreateDialog'
import { useSessionInfo } from '../util/RequireConfig'
import { useSuspenseContext } from './UserDetails'

const query = gql`
query cmList($id: ID!) {
Expand Down Expand Up @@ -56,7 +56,7 @@ const useStyles = makeStyles((theme: Theme) => ({

export default function UserContactMethodList(
props: UserContactMethodListProps,
): JSX.Element {
): ReactNode {
const classes = useStyles()
const mobile = useIsWidthDown('md')

Expand All @@ -66,16 +66,18 @@ export default function UserContactMethodList(
const [showDeleteDialogByID, setShowDeleteDialogByID] = useState('')
const [showSendTestByID, setShowSendTestByID] = useState('')

const { loading, error, data } = useQuery(query, {
const [{ fetching, error, data }] = useQuery({
query,
variables: {
id: props.userID,
},
context: useSuspenseContext(),
})

const { userID: currentUserID } = useSessionInfo()
const isCurrentUser = props.userID === currentUserID

if (loading && !data) return <Spinner />
if (fetching && !data) return null
if (data && !data.user) return <ObjectNotFound type='user' />
if (error) return <GenericError error={error.message} />

Expand Down
51 changes: 33 additions & 18 deletions web/src/app/users/UserDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from 'react'
import { useQuery, gql } from 'urql'
import React, { Suspense, useState, useMemo } from 'react'
import { useQuery, gql, OperationContext } from 'urql'
import Delete from '@mui/icons-material/Delete'
import LockOpenIcon from '@mui/icons-material/LockOpen'
import DetailsPage from '../details/DetailsPage'
Expand Down Expand Up @@ -73,6 +73,15 @@ const profileQuery = gql`
}
`

export function useSuspenseContext(): Partial<OperationContext> {
Forfold marked this conversation as resolved.
Show resolved Hide resolved
return useMemo(
() => ({
suspense: true,
}),
[],
)
}

function serviceCount(onCallSteps: EscalationPolicyStep[] = []): number {
const svcs: { [Key: string]: boolean } = {}
;(onCallSteps || []).forEach((s) =>
Expand Down Expand Up @@ -107,6 +116,7 @@ export default function UserDetails(props: {
query: isAdmin || userID === currentUserID ? profileQuery : userQuery,
variables: { id: userID },
pause: !userID,
context: useSuspenseContext(),
})

const loading = !isSessionReady || isQueryLoading
Expand Down Expand Up @@ -234,22 +244,27 @@ export default function UserDetails(props: {
onClose={() => setCreateNR(false)}
/>
)}
<DetailsPage
avatar={<UserAvatar userID={userID} />}
title={user.name + (svcCount ? ' (On-Call)' : '')}
subheader={user.email}
pageContent={
<Grid container spacing={2}>
<UserContactMethodList userID={userID} readOnly={props.readOnly} />
<UserNotificationRuleList
userID={userID}
readOnly={props.readOnly}
/>
</Grid>
}
secondaryActions={options}
links={links}
/>
<Suspense fallback={<Spinner />}>
<DetailsPage
avatar={<UserAvatar userID={userID} />}
title={user.name + (svcCount ? ' (On-Call)' : '')}
subheader={user.email}
pageContent={
<Grid container spacing={2}>
<UserContactMethodList
userID={userID}
readOnly={props.readOnly}
/>
<UserNotificationRuleList
userID={userID}
readOnly={props.readOnly}
/>
</Grid>
}
secondaryActions={options}
links={links}
/>
</Suspense>
</React.Fragment>
)
}
129 changes: 67 additions & 62 deletions web/src/app/users/UserNotificationRuleList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, ReactNode } from 'react'
import { gql, QueryResult } from '@apollo/client'
import { gql, useQuery } from 'urql'
import {
Button,
Card,
Expand All @@ -10,14 +10,15 @@ import {
} from '@mui/material'
import makeStyles from '@mui/styles/makeStyles'
import { Add, Delete } from '@mui/icons-material'
import Query from '../util/Query'
import FlatList from '../lists/FlatList'
import { formatNotificationRule, sortNotificationRules } from './util'
import UserNotificationRuleDeleteDialog from './UserNotificationRuleDeleteDialog'
import { styles as globalStyles } from '../styles/materialStyles'
import UserNotificationRuleCreateDialog from './UserNotificationRuleCreateDialog'
import { useIsWidthDown } from '../util/useWidth'
import { User } from '../../schema'
import { useSuspenseContext } from './UserDetails'
import { ObjectNotFound, GenericError } from '../error-pages'

const query = gql`
query nrList($id: ID!) {
Expand Down Expand Up @@ -51,71 +52,75 @@ const useStyles = makeStyles((theme: Theme) => {
export default function UserNotificationRuleList(props: {
userID: string
readOnly: boolean
}): JSX.Element {
}): ReactNode {
const classes = useStyles()
const mobile = useIsWidthDown('md')
const [showAddDialog, setShowAddDialog] = useState(false)
const [deleteID, setDeleteID] = useState(null)

function renderList(user: User): ReactNode {
return (
<Grid item xs={12}>
<Card>
<CardHeader
className={classes.cardHeader}
titleTypographyProps={{ component: 'h2', variant: 'h5' }}
title='Notification Rules'
action={
!mobile ? (
<Button
title='Add Notification Rule'
variant='contained'
onClick={() => setShowAddDialog(true)}
startIcon={<Add />}
disabled={user.contactMethods.length === 0}
>
Add Rule
</Button>
) : null
}
/>
<FlatList
data-cy='notification-rules'
items={sortNotificationRules(user.notificationRules).map((nr) => ({
title: formatNotificationRule(nr.delayMinutes, nr.contactMethod),
secondaryAction: props.readOnly ? null : (
<IconButton
aria-label='Delete notification rule'
onClick={() => setDeleteID(nr.id)}
color='secondary'
>
<Delete />
</IconButton>
),
}))}
emptyMessage='No notification rules'
/>
</Card>
{showAddDialog && (
<UserNotificationRuleCreateDialog
userID={props.userID}
onClose={() => setShowAddDialog(false)}
/>
)}
{deleteID && (
<UserNotificationRuleDeleteDialog
ruleID={deleteID}
onClose={() => setDeleteID(null)}
/>
)}
</Grid>
)
}
const [{ data, error, fetching }] = useQuery({
query,
variables: { id: props.userID },
context: useSuspenseContext(),
})

if (fetching && !data) return null
if (data && !data.user)
return <ObjectNotFound type='notifcation rules list' />
if (error) return <GenericError error={error.message} />

const { user }: { user: User } = data

return (
<Query
query={query}
variables={{ id: props.userID }}
render={({ data }: QueryResult) => renderList(data.user)}
/>
<Grid item xs={12}>
<Card>
<CardHeader
className={classes.cardHeader}
titleTypographyProps={{ component: 'h2', variant: 'h5' }}
title='Notification Rules'
action={
!mobile ? (
<Button
title='Add Notification Rule'
variant='contained'
onClick={() => setShowAddDialog(true)}
startIcon={<Add />}
disabled={user.contactMethods.length === 0}
>
Add Rule
</Button>
) : null
}
/>
<FlatList
data-cy='notification-rules'
items={sortNotificationRules(user.notificationRules).map((nr) => ({
title: formatNotificationRule(nr.delayMinutes, nr.contactMethod),
secondaryAction: props.readOnly ? null : (
<IconButton
aria-label='Delete notification rule'
onClick={() => setDeleteID(nr.id)}
color='secondary'
>
<Delete />
</IconButton>
),
}))}
emptyMessage='No notification rules'
/>
</Card>
{showAddDialog && (
<UserNotificationRuleCreateDialog
userID={props.userID}
onClose={() => setShowAddDialog(false)}
/>
)}
{deleteID && (
<UserNotificationRuleDeleteDialog
ruleID={deleteID}
onClose={() => setDeleteID(null)}
/>
)}
</Grid>
)
}
9 changes: 1 addition & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2990,7 +2990,7 @@ __metadata:
languageName: node
linkType: hard

"@types/prop-types@npm:15.7.9":
"@types/prop-types@npm:15.7.9, @types/prop-types@npm:^15.7.9":
version: 15.7.9
resolution: "@types/prop-types@npm:15.7.9"
checksum: c7591d3ff7593e243908a07e1d3e2bb6e8879008af5800d8378115a90d0fdf669a1cae72a6d7f69e59c4fa7bb4c8ed61f6ebc1c520fe110c6f2b03ac02414072
Expand All @@ -3004,13 +3004,6 @@ __metadata:
languageName: node
linkType: hard

"@types/prop-types@npm:^15.7.9":
version: 15.7.9
resolution: "@types/prop-types@npm:15.7.9"
checksum: c7591d3ff7593e243908a07e1d3e2bb6e8879008af5800d8378115a90d0fdf669a1cae72a6d7f69e59c4fa7bb4c8ed61f6ebc1c520fe110c6f2b03ac02414072
languageName: node
linkType: hard

"@types/react-big-calendar@npm:1.6.5":
version: 1.6.5
resolution: "@types/react-big-calendar@npm:1.6.5"
Expand Down