-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Suspensify PermissionsProvider
using useWrappedQuery
#21440
Changes from all commits
a0a2510
0a2fcf8
ce04373
7023b6f
cb606c0
f88888f
46c5c5d
f43e10c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,13 @@ | ||
import {gql, useQuery} from '@apollo/client'; | ||
import {ApolloError, gql} from '@apollo/client'; | ||
import * as React from 'react'; | ||
|
||
import { | ||
PermissionFragment, | ||
PermissionsQuery, | ||
PermissionsQueryVariables, | ||
} from './types/Permissions.types'; | ||
import {useWrappedQuery} from '../hooks/useWrappedQuery'; | ||
import {wrapPromise} from '../utils/wrapPromise'; | ||
|
||
// used in tests, to ensure against permission renames. Should make sure that the mapping in | ||
// extractPermissions is handled correctly | ||
|
@@ -111,53 +113,67 @@ type PermissionDisabledReasons = Record<keyof PermissionsMap, string>; | |
export type PermissionsState = { | ||
permissions: PermissionBooleans; | ||
disabledReasons: PermissionDisabledReasons; | ||
loading: boolean; | ||
}; | ||
|
||
type PermissionsContextType = { | ||
export type PermissionsResult = { | ||
error?: ApolloError; | ||
unscopedPermissions: PermissionsMap; | ||
locationPermissions: Record<string, PermissionsMap>; | ||
loading: boolean; | ||
// Raw unscoped permission data, for Cloud extraction | ||
rawUnscopedData: PermissionFragment[]; | ||
}; | ||
|
||
type PermissionsContextType = ReturnType<typeof wrapPromise<PermissionsResult>>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be nice for your wrapPromise util to expose a type helper that makes this |
||
|
||
export const PermissionsContext = React.createContext<PermissionsContextType>({ | ||
unscopedPermissions: extractPermissions([]), | ||
locationPermissions: {}, | ||
loading: true, | ||
rawUnscopedData: [], | ||
read() { | ||
return { | ||
error: undefined, | ||
unscopedPermissions: extractPermissions([]), | ||
rawUnscopedData: [], | ||
locationPermissions: {}, | ||
}; | ||
}, | ||
}); | ||
|
||
export const PermissionsProvider = (props: {children: React.ReactNode}) => { | ||
const {data, loading} = useQuery<PermissionsQuery, PermissionsQueryVariables>(PERMISSIONS_QUERY, { | ||
fetchPolicy: 'cache-first', // Not expected to change after initial load. | ||
}); | ||
const wrappedQuery = useWrappedQuery< | ||
PermissionsQuery, | ||
PermissionsQueryVariables, | ||
PermissionsResult | ||
>( | ||
{ | ||
query: PERMISSIONS_QUERY, | ||
fetchPolicy: 'cache-first', // Not expected to change after initial load. | ||
}, | ||
async (result) => { | ||
const {data, error} = result; | ||
|
||
const value = React.useMemo(() => { | ||
const unscopedPermissionsRaw = data?.unscopedPermissions || []; | ||
const unscopedPermissions = extractPermissions(unscopedPermissionsRaw); | ||
const unscopedPermissionsRaw = data?.unscopedPermissions || []; | ||
const unscopedPermissions = extractPermissions(unscopedPermissionsRaw); | ||
|
||
const locationEntries = | ||
data?.workspaceOrError.__typename === 'Workspace' | ||
? data.workspaceOrError.locationEntries | ||
: []; | ||
const locationEntries = | ||
data?.workspaceOrError.__typename === 'Workspace' | ||
? data.workspaceOrError.locationEntries | ||
: []; | ||
|
||
const locationPermissions: Record<string, PermissionsMap> = {}; | ||
locationEntries.forEach((locationEntry) => { | ||
const {name, permissions} = locationEntry; | ||
locationPermissions[name] = extractPermissions(permissions, unscopedPermissionsRaw); | ||
}); | ||
const locationPermissions: Record<string, PermissionsMap> = {}; | ||
locationEntries.forEach((locationEntry) => { | ||
const {name, permissions} = locationEntry; | ||
locationPermissions[name] = extractPermissions(permissions, unscopedPermissionsRaw); | ||
}); | ||
|
||
return { | ||
unscopedPermissions, | ||
locationPermissions, | ||
loading, | ||
rawUnscopedData: unscopedPermissionsRaw, | ||
}; | ||
}, [data, loading]); | ||
return { | ||
error, | ||
unscopedPermissions, | ||
locationPermissions, | ||
rawUnscopedData: unscopedPermissionsRaw, | ||
}; | ||
Comment on lines
+148
to
+170
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This all looks good to me! Just from reading this it's not clear that the use of useWrapped has suspense implications, it sorta just looks like a stylistic change moving the data transform into the hook. Maybe we call out in the naming that this significantly changes the loading semantics. |
||
}, | ||
); | ||
|
||
return <PermissionsContext.Provider value={value}>{props.children}</PermissionsContext.Provider>; | ||
return ( | ||
<PermissionsContext.Provider value={wrappedQuery}>{props.children}</PermissionsContext.Provider> | ||
); | ||
}; | ||
|
||
export const permissionResultForKey = ( | ||
|
@@ -191,7 +207,8 @@ const unpackPermissions = ( | |
* Retrieve a permission that is intentionally unscoped. | ||
*/ | ||
export const useUnscopedPermissions = (): PermissionsState => { | ||
const {unscopedPermissions, loading} = React.useContext(PermissionsContext); | ||
const {read} = React.useContext(PermissionsContext); | ||
const {unscopedPermissions} = read(); | ||
const unpacked = React.useMemo( | ||
() => unpackPermissions(unscopedPermissions), | ||
[unscopedPermissions], | ||
|
@@ -201,9 +218,8 @@ export const useUnscopedPermissions = (): PermissionsState => { | |
return { | ||
permissions: unpacked.booleans, | ||
disabledReasons: unpacked.disabledReasons, | ||
loading, | ||
}; | ||
}, [unpacked, loading]); | ||
}, [unpacked]); | ||
}; | ||
|
||
/** | ||
|
@@ -214,7 +230,8 @@ export const useUnscopedPermissions = (): PermissionsState => { | |
export const usePermissionsForLocation = ( | ||
locationName: string | null | undefined, | ||
): PermissionsState => { | ||
const {unscopedPermissions, locationPermissions, loading} = React.useContext(PermissionsContext); | ||
const {read} = React.useContext(PermissionsContext); | ||
const {unscopedPermissions, locationPermissions} = read(); | ||
let permissionsForLocation = unscopedPermissions; | ||
if (locationName && locationPermissions.hasOwnProperty(locationName)) { | ||
permissionsForLocation = locationPermissions[locationName]!; | ||
|
@@ -225,9 +242,8 @@ export const usePermissionsForLocation = ( | |
return { | ||
permissions: unpacked.booleans, | ||
disabledReasons: unpacked.disabledReasons, | ||
loading, | ||
}; | ||
}, [unpacked, loading]); | ||
}, [unpacked]); | ||
}; | ||
|
||
export const PERMISSIONS_QUERY = gql` | ||
|
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 have two questions here:
Why is it necessary to put the Suspense inside the Route and not inside the Switch on line 37? Does this really need to be repeated for every route separately like this? Is this primarily so the
id
can be passed?If the reason for Separate Pandas Dataframe Solid into Two Sources #1 is so that the suspense instance is replaced when you navigate, do these
/*
type routes all share the same tracked suspense? Is that ok?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.
It's not necessary, I was just replacing the existing
Suspense
calls withTrackedSuspense
in a follow up we can consolidate these but didn't want to unknowingly break anything by doing that