Skip to content

Commit 81d11dd

Browse files
committed
Add authorization, dismissal and fetch after login
1 parent b80de3c commit 81d11dd

File tree

6 files changed

+168
-48
lines changed

6 files changed

+168
-48
lines changed

site/src/components/AlertBanner/AlertBanner.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ import { AlertBannerCtas } from "./AlertBannerCtas"
1616
* @param text: default text to be displayed to the user; useful for warnings or as a fallback error message
1717
* @param error: should be passed in if the severity is 'Error'; warnings can use 'text' instead
1818
* @param actions: an array of CTAs passed in by the consumer
19-
* @param dismissible: determines whether or not the banner should have a `Dismiss` CTA
2019
* @param retry: a handler to retry the action that spawned the error
20+
* @param dismissible: determines whether or not the banner should have a `Dismiss` CTA
21+
* @param onDismiss: a handler that is called when the `Dismiss` CTA is clicked, after the animation has finished
2122
*/
2223
export const AlertBanner: FC<React.PropsWithChildren<AlertBannerProps>> = ({
2324
children,
@@ -27,6 +28,7 @@ export const AlertBanner: FC<React.PropsWithChildren<AlertBannerProps>> = ({
2728
actions = [],
2829
retry,
2930
dismissible = false,
31+
onDismiss,
3032
}) => {
3133
const { t } = useTranslation("common")
3234

@@ -50,7 +52,7 @@ export const AlertBanner: FC<React.PropsWithChildren<AlertBannerProps>> = ({
5052
const [showDetails, setShowDetails] = useState(false)
5153

5254
return (
53-
<Collapse in={open}>
55+
<Collapse in={open} onExited={() => onDismiss && onDismiss()}>
5456
<Stack
5557
className={classes.alertContainer}
5658
direction="row"

site/src/components/AlertBanner/alertTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ export interface AlertBannerProps {
99
error?: ApiError | Error | unknown
1010
actions?: ReactElement[]
1111
dismissible?: boolean
12+
onDismiss?: () => void
1213
retry?: () => void
1314
}

site/src/components/AuthAndFrame/AuthAndFrame.tsx

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { makeStyles } from "@material-ui/core/styles"
22
import { useActor } from "@xstate/react"
33
import { Loader } from "components/Loader/Loader"
4-
import { FC, Suspense, useContext } from "react"
4+
import { FC, Suspense, useContext, useEffect } from "react"
55
import { XServiceContext } from "../../xServices/StateContext"
66
import { Footer } from "../Footer/Footer"
77
import { Navbar } from "../Navbar/Navbar"
@@ -19,20 +19,35 @@ interface AuthAndFrameProps {
1919
export const AuthAndFrame: FC<AuthAndFrameProps> = ({ children }) => {
2020
const styles = useStyles({})
2121
const xServices = useContext(XServiceContext)
22+
const [authState] = useActor(xServices.authXService)
2223
const [buildInfoState] = useActor(xServices.buildInfoXService)
23-
const [updateCheckState] = useActor(xServices.updateCheckXService)
24+
const [updateCheckState, updateCheckSend] = useActor(
25+
xServices.updateCheckXService,
26+
)
27+
28+
useEffect(() => {
29+
if (authState.matches("signedIn")) {
30+
updateCheckSend("CHECK")
31+
} else {
32+
updateCheckSend("CLEAR")
33+
}
34+
}, [authState, updateCheckSend])
2435

2536
return (
2637
<RequireAuth>
2738
<div className={styles.site}>
2839
<Navbar />
29-
<div className={styles.siteBanner}>
30-
<Margins>
31-
<UpdateCheckBanner
32-
updateCheck={updateCheckState.context.updateCheck}
33-
/>
34-
</Margins>
35-
</div>
40+
{updateCheckState.context.show && (
41+
<div className={styles.updateCheckBanner}>
42+
<Margins>
43+
<UpdateCheckBanner
44+
updateCheck={updateCheckState.context.updateCheck}
45+
error={updateCheckState.context.error}
46+
onDismiss={() => updateCheckSend("DISMISS")}
47+
/>
48+
</Margins>
49+
</div>
50+
)}
3651
<div className={styles.siteContent}>
3752
<Suspense fallback={<Loader />}>{children}</Suspense>
3853
</div>
@@ -48,12 +63,15 @@ const useStyles = makeStyles((theme) => ({
4863
minHeight: "100vh",
4964
flexDirection: "column",
5065
},
51-
siteBanner: {
66+
updateCheckBanner: {
67+
// Add spacing at the top and remove some from the bottom. Removal
68+
// is necessary to avoid a visual jerk when the banner is dismissed.
69+
// It also give a more pleasant distance to the site content when
70+
// the banner is visible.
5271
marginTop: theme.spacing(2),
72+
marginBottom: -theme.spacing(2),
5373
},
5474
siteContent: {
5575
flex: 1,
56-
// Accommodate for banner margin since it is dismissible.
57-
marginTop: -theme.spacing(2),
5876
},
5977
}))

site/src/components/UpdateCheckBanner/UpdateCheckBanner.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,19 @@ import * as TypesGen from "api/typesGenerated"
55

66
export interface UpdateCheckBannerProps {
77
updateCheck?: TypesGen.UpdateCheckResponse
8+
error?: Error | unknown
9+
onDismiss?: () => void
810
}
911

1012
export const UpdateCheckBanner: React.FC<
1113
React.PropsWithChildren<UpdateCheckBannerProps>
12-
> = ({ updateCheck }) => {
14+
> = ({ updateCheck, error, onDismiss }) => {
1315
const { t } = useTranslation("common")
1416

1517
return (
1618
<>
17-
{updateCheck && !updateCheck.current && (
18-
<AlertBanner severity="info" dismissible>
19+
{!error && updateCheck && !updateCheck.current && (
20+
<AlertBanner severity="info" onDismiss={onDismiss} dismissible>
1921
<div>
2022
<Trans
2123
t={t}
@@ -32,6 +34,15 @@ export const UpdateCheckBanner: React.FC<
3234
</div>
3335
</AlertBanner>
3436
)}
37+
{error && (
38+
<AlertBanner
39+
severity="error"
40+
error={error}
41+
text={t("updateCheck.error")}
42+
onDismiss={onDismiss}
43+
dismissible
44+
/>
45+
)}
3546
</>
3647
)
3748
}

site/src/i18n/en/common.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"select": "Select emoji"
3838
},
3939
"updateCheck": {
40-
"message": "Coder {{version}} is now available. View the <4>release notes</4> and <7>upgrade instructions</7> for more information."
40+
"message": "Coder {{version}} is now available. View the <4>release notes</4> and <7>upgrade instructions</7> for more information.",
41+
"error": "Coder update check failed."
4142
}
4243
}
Lines changed: 117 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,172 @@
11
import { assign, createMachine } from "xstate"
2-
import * as API from "api/api"
3-
import * as TypesGen from "api/typesGenerated"
2+
import { checkAuthorization, getUpdateCheck } from "api/api"
3+
import { AuthorizationResponse, UpdateCheckResponse } from "api/typesGenerated"
44

5-
export const Language = {
6-
updateAvailable: "New version available",
7-
updateAvailableMessage: (
8-
version: string,
9-
url: string,
10-
upgrade_instructions_url: string,
11-
): string =>
12-
`Coder ${version} is now available at ${url}. See ${upgrade_instructions_url} for information on how to upgrade.`,
13-
}
5+
export const checks = {
6+
viewUpdateCheck: "viewUpdateCheck",
7+
} as const
8+
9+
export const permissionsToCheck = {
10+
[checks.viewUpdateCheck]: {
11+
object: {
12+
resource_type: "update_check",
13+
},
14+
action: "read",
15+
},
16+
} as const
17+
18+
export type Permissions = Record<keyof typeof permissionsToCheck, boolean>
1419

1520
export interface UpdateCheckContext {
16-
getUpdateCheckError?: Error | unknown
17-
updateCheck?: TypesGen.UpdateCheckResponse
21+
show: boolean
22+
updateCheck?: UpdateCheckResponse
23+
permissions?: Permissions
24+
error?: Error | unknown
1825
}
1926

27+
export type UpdateCheckEvent =
28+
| { type: "CHECK" }
29+
| { type: "CLEAR" }
30+
| { type: "DISMISS" }
31+
2032
export const updateCheckMachine = createMachine(
2133
{
2234
id: "updateCheckState",
2335
predictableActionArguments: true,
2436
tsTypes: {} as import("./updateCheckXService.typegen").Typegen0,
2537
schema: {
2638
context: {} as UpdateCheckContext,
39+
events: {} as UpdateCheckEvent,
2740
services: {} as {
41+
checkPermissions: {
42+
data: AuthorizationResponse
43+
}
2844
getUpdateCheck: {
29-
data: TypesGen.UpdateCheckResponse
45+
data: UpdateCheckResponse
3046
}
3147
},
3248
},
3349
context: {
34-
updateCheck: undefined,
50+
show: false,
3551
},
36-
initial: "gettingUpdateCheck",
52+
initial: "idle",
3753
states: {
38-
gettingUpdateCheck: {
54+
idle: {
55+
on: {
56+
CHECK: {
57+
target: "fetchingPermissions",
58+
},
59+
},
60+
},
61+
fetchingPermissions: {
62+
invoke: {
63+
src: "checkPermissions",
64+
id: "checkPermissions",
65+
onDone: [
66+
{
67+
actions: ["assignPermissions"],
68+
target: "checkingPermissions",
69+
},
70+
],
71+
onError: [
72+
{
73+
actions: ["assignError"],
74+
target: "show",
75+
},
76+
],
77+
},
78+
},
79+
checkingPermissions: {
80+
always: [
81+
{
82+
target: "fetchingUpdateCheck",
83+
cond: "canViewUpdateCheck",
84+
},
85+
{
86+
target: "dismissOrClear",
87+
cond: "canNotViewUpdateCheck",
88+
},
89+
],
90+
},
91+
fetchingUpdateCheck: {
3992
invoke: {
4093
src: "getUpdateCheck",
4194
id: "getUpdateCheck",
4295
onDone: [
4396
{
44-
actions: ["assignUpdateCheck", "clearGetUpdateCheckError"],
45-
target: "#updateCheckState.success",
97+
actions: ["assignUpdateCheck", "clearError"],
98+
target: "show",
4699
},
47100
],
48101
onError: [
49102
{
50-
actions: ["assignGetUpdateCheckError", "clearUpdateCheck"],
51-
target: "#updateCheckState.failure",
103+
actions: ["assignError", "clearUpdateCheck"],
104+
target: "show",
52105
},
53106
],
54107
},
55108
},
56-
success: {
57-
type: "final",
109+
show: {
110+
entry: "assignShow",
111+
always: [
112+
{
113+
target: "dismissOrClear",
114+
},
115+
],
116+
},
117+
dismissOrClear: {
118+
on: {
119+
DISMISS: {
120+
actions: ["assignHide"],
121+
target: "dismissed",
122+
},
123+
CLEAR: {
124+
actions: ["clearUpdateCheck", "clearError", "assignHide"],
125+
target: "idle",
126+
},
127+
},
58128
},
59-
failure: {
129+
dismissed: {
60130
type: "final",
61131
},
62132
},
63133
},
64134
{
65135
services: {
66-
getUpdateCheck: API.getUpdateCheck,
136+
checkPermissions: async () =>
137+
checkAuthorization({ checks: permissionsToCheck }),
138+
getUpdateCheck: getUpdateCheck,
67139
},
68140
actions: {
141+
assignPermissions: assign({
142+
permissions: (_, event) => event.data as Permissions,
143+
}),
144+
assignShow: assign({
145+
show: true,
146+
}),
147+
assignHide: assign({
148+
show: false,
149+
}),
69150
assignUpdateCheck: assign({
70151
updateCheck: (_, event) => event.data,
71152
}),
72-
clearUpdateCheck: assign((context: UpdateCheckContext) => ({
153+
clearUpdateCheck: assign((context) => ({
73154
...context,
74155
updateCheck: undefined,
75156
})),
76-
assignGetUpdateCheckError: assign({
77-
getUpdateCheckError: (_, event) => event.data,
157+
assignError: assign({
158+
error: (_, event) => event.data,
78159
}),
79-
clearGetUpdateCheckError: assign((context: UpdateCheckContext) => ({
160+
clearError: assign((context) => ({
80161
...context,
81-
getUpdateCheckError: undefined,
162+
error: undefined,
82163
})),
83164
},
165+
guards: {
166+
canViewUpdateCheck: (context) =>
167+
context.permissions?.[checks.viewUpdateCheck] || false,
168+
canNotViewUpdateCheck: (context) =>
169+
!context.permissions?.[checks.viewUpdateCheck],
170+
},
84171
},
85172
)

0 commit comments

Comments
 (0)