Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: Add support for update checks and notifications#4810

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
mafredri merged 14 commits intomainfrommafredri/outdated-coder-installation-notice
Dec 1, 2022
Merged
Show file tree
Hide file tree
Changes from1 commit
Commits
Show all changes
14 commits
Select commitHold shift + click to select a range
cc28c1b
feat: Add support for checking for updates
mafredriOct 31, 2022
c163af8
wip(site): Add support for informing of updates
mafredriOct 31, 2022
e9ed0b7
Add /api/v2/updatecheck to noauthorize list
mafredriOct 31, 2022
64ad0f5
Fix edge case where update check has never succeeded
mafredriOct 31, 2022
0238d5a
Fix logging
mafredriOct 31, 2022
9e1ea9e
Use AlertBanner for coder version notice
mafredriNov 29, 2022
979ff80
Update golden files
mafredriNov 29, 2022
b80de3c
Fix margins, improve alert banner and add stories
mafredriNov 30, 2022
81d11dd
Add authorization, dismissal and fetch after login
mafredriNov 30, 2022
392d06e
Apply suggestions from code review
mafredriDec 1, 2022
ad38c3b
Add forgotten UpdateCheckBanner.stories.tsx
mafredriNov 30, 2022
1c9dbc1
fix(api): Amend PR comments
mafredriDec 1, 2022
49a7bfc
fix(site): Always show prefix for update check errors, remove as const
mafredriDec 1, 2022
44f6475
fix(site): Add jest tests for UpdateCheckBanner
mafredriDec 1, 2022
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
PrevPrevious commit
NextNext commit
Add authorization, dismissal and fetch after login
  • Loading branch information
@mafredri
mafredri committedNov 30, 2022
commit81d11dd01ce0e6e54778a038cbbd0107b9f083c3
6 changes: 4 additions & 2 deletionssite/src/components/AlertBanner/AlertBanner.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,8 +16,9 @@ import { AlertBannerCtas } from "./AlertBannerCtas"
* @param text: default text to be displayed to the user; useful for warnings or as a fallback error message
* @param error: should be passed in if the severity is 'Error'; warnings can use 'text' instead
* @param actions: an array of CTAs passed in by the consumer
* @param dismissible: determines whether or not the banner should have a `Dismiss` CTA
* @param retry: a handler to retry the action that spawned the error
* @param dismissible: determines whether or not the banner should have a `Dismiss` CTA
* @param onDismiss: a handler that is called when the `Dismiss` CTA is clicked, after the animation has finished
*/
export const AlertBanner: FC<React.PropsWithChildren<AlertBannerProps>> = ({
children,
Expand All@@ -27,6 +28,7 @@ export const AlertBanner: FC<React.PropsWithChildren<AlertBannerProps>> = ({
actions = [],
retry,
dismissible = false,
onDismiss,
}) => {
const { t } = useTranslation("common")

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

return (
<Collapse in={open}>
<Collapse in={open} onExited={() => onDismiss && onDismiss()}>
<Stack
className={classes.alertContainer}
direction="row"
Expand Down
1 change: 1 addition & 0 deletionssite/src/components/AlertBanner/alertTypes.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,5 +9,6 @@ export interface AlertBannerProps {
error?: ApiError | Error | unknown
actions?: ReactElement[]
dismissible?: boolean
onDismiss?: () => void
retry?: () => void
}
42 changes: 30 additions & 12 deletionssite/src/components/AuthAndFrame/AuthAndFrame.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
import { makeStyles } from "@material-ui/core/styles"
import { useActor } from "@xstate/react"
import { Loader } from "components/Loader/Loader"
import { FC, Suspense, useContext } from "react"
import { FC, Suspense, useContext, useEffect } from "react"
import { XServiceContext } from "../../xServices/StateContext"
import { Footer } from "../Footer/Footer"
import { Navbar } from "../Navbar/Navbar"
Expand All@@ -19,20 +19,35 @@ interface AuthAndFrameProps {
export const AuthAndFrame: FC<AuthAndFrameProps> = ({ children }) => {
const styles = useStyles({})
const xServices = useContext(XServiceContext)
const [authState] = useActor(xServices.authXService)
const [buildInfoState] = useActor(xServices.buildInfoXService)
const [updateCheckState] = useActor(xServices.updateCheckXService)
const [updateCheckState, updateCheckSend] = useActor(
xServices.updateCheckXService,
)

useEffect(() => {
if (authState.matches("signedIn")) {
updateCheckSend("CHECK")
} else {
updateCheckSend("CLEAR")
}
}, [authState, updateCheckSend])
Copy link
MemberAuthor

Choose a reason for hiding this comment

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

review(site): I'm not super happy with how this turned out. I kind of wanted to represent this in the updateCheckXService, but I couldn't figure out how to do inter-machine dependencies.

This snippet here enables e.g. the following scenario:

  1. User is loggedin as non-owner
  2. User logs out
  3. User logs in as owner
  4. Update notification is shown

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, inter-machine dependencies are tricky; I'm not sure if there's a better way.@presleyp do you have any workarounds? I think you looked at something like this recently with pagination.


return (
<RequireAuth>
<div className={styles.site}>
<Navbar />
<div className={styles.siteBanner}>
<Margins>
<UpdateCheckBanner
updateCheck={updateCheckState.context.updateCheck}
/>
</Margins>
</div>
{updateCheckState.context.show && (
<div className={styles.updateCheckBanner}>
<Margins>
<UpdateCheckBanner
updateCheck={updateCheckState.context.updateCheck}
error={updateCheckState.context.error}
onDismiss={() => updateCheckSend("DISMISS")}
Copy link
MemberAuthor

@mafredrimafredriNov 30, 2022
edited
Loading

Choose a reason for hiding this comment

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

review(site): Dismissal as any user (practically only an owner) will prevent notification from showing up until browser window is reloaded, even if logged in/out in-between. This seems like acceptable behavior (managed via XState service).

Kira-Pilot reacted with thumbs up emoji
/>
</Margins>
</div>
)}
<div className={styles.siteContent}>
<Suspense fallback={<Loader />}>{children}</Suspense>
</div>
Expand All@@ -48,12 +63,15 @@ const useStyles = makeStyles((theme) => ({
minHeight: "100vh",
flexDirection: "column",
},
siteBanner: {
updateCheckBanner: {
// Add spacing at the top and remove some from the bottom. Removal
// is necessary to avoid a visual jerk when the banner is dismissed.
// It also give a more pleasant distance to the site content when
// the banner is visible.
marginTop: theme.spacing(2),
marginBottom: -theme.spacing(2),
},
siteContent: {
flex: 1,
// Accommodate for banner margin since it is dismissible.
marginTop: -theme.spacing(2),
},
}))
17 changes: 14 additions & 3 deletionssite/src/components/UpdateCheckBanner/UpdateCheckBanner.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,17 +5,19 @@ import * as TypesGen from "api/typesGenerated"

export interface UpdateCheckBannerProps {
updateCheck?: TypesGen.UpdateCheckResponse
error?: Error | unknown
onDismiss?: () => void
}

export const UpdateCheckBanner: React.FC<
React.PropsWithChildren<UpdateCheckBannerProps>
> = ({ updateCheck }) => {
> = ({ updateCheck, error, onDismiss }) => {
const { t } = useTranslation("common")

return (
<>
{updateCheck && !updateCheck.current && (
<AlertBanner severity="info" dismissible>
{!error &&updateCheck && !updateCheck.current && (
<AlertBanner severity="info"onDismiss={onDismiss}dismissible>
<div>
<Trans
t={t}
Expand All@@ -32,6 +34,15 @@ export const UpdateCheckBanner: React.FC<
</div>
</AlertBanner>
)}
{error && (
<AlertBanner
severity="error"
error={error}
text={t("updateCheck.error")}
onDismiss={onDismiss}
dismissible
/>
)}
</>
Copy link
Member

Choose a reason for hiding this comment

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

Just curious why we throw an error if the check fails. Perhaps it would be better if this failed silently (we could throw something in the terminal if we wanted)? Maybe not, just a thought after seeing the error on local:
Screen Shot 2022-11-30 at 5 02 28 PM

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

I thought about it and I worried we wouldn't notice if it stops working for whatever reason. I can make it silent or add a prefix (Update check failed: Route not found.). Ideally we would keep it silent and log it to Sentry or a similar bug tracker.

Kira-Pilot reacted with thumbs up emoji
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, maybe we could just add a console.error since we don't have Sentry.

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

I refactored this now, it looks like this:https://www.chromatic.com/test?appId=624de63c6aacee003aa84340&id=6388bfa22e0a55f2ff0c1f1e

I'm still game to remove it if that's preferred. 👍🏻

Copy link
Member

Choose a reason for hiding this comment

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

@mafredri nope looks good!

)
}
3 changes: 2 additions & 1 deletionsite/src/i18n/en/common.json
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,7 @@
"select": "Select emoji"
},
"updateCheck": {
"message": "Coder {{version}} is now available. View the <4>release notes</4> and <7>upgrade instructions</7> for more information."
"message": "Coder {{version}} is now available. View the <4>release notes</4> and <7>upgrade instructions</7> for more information.",
"error": "Coder update check failed."
}
}
147 changes: 117 additions & 30 deletionssite/src/xServices/updateCheck/updateCheckXService.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,172 @@
import { assign, createMachine } from "xstate"
import* as API from "api/api"
import* as TypesGen from "api/typesGenerated"
import{ checkAuthorization, getUpdateCheck } from "api/api"
import{ AuthorizationResponse, UpdateCheckResponse } from "api/typesGenerated"

export const Language = {
updateAvailable: "New version available",
updateAvailableMessage: (
version: string,
url: string,
upgrade_instructions_url: string,
): string =>
`Coder ${version} is now available at ${url}. See ${upgrade_instructions_url} for information on how to upgrade.`,
}
export const checks = {
viewUpdateCheck: "viewUpdateCheck",
} as const

export const permissionsToCheck = {
[checks.viewUpdateCheck]: {
object: {
resource_type: "update_check",
},
action: "read",
},
} as const
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 why we need to export these constants or why we need to cast asconst although I see this pattern in our other machines.@presley (or@mafredri) is there something dumb I'm missing?

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

Same here, I thought it was weird but I kept them just in case. I'll remove them and see if something breaks 👍🏻


export type Permissions = Record<keyof typeof permissionsToCheck, boolean>

export interface UpdateCheckContext {
getUpdateCheckError?: Error | unknown
updateCheck?: TypesGen.UpdateCheckResponse
show: boolean
updateCheck?: UpdateCheckResponse
permissions?: Permissions
error?: Error | unknown
}

export type UpdateCheckEvent =
| { type: "CHECK" }
| { type: "CLEAR" }
| { type: "DISMISS" }

export const updateCheckMachine = createMachine(
{
id: "updateCheckState",
predictableActionArguments: true,
tsTypes: {} as import("./updateCheckXService.typegen").Typegen0,
schema: {
context: {} as UpdateCheckContext,
events: {} as UpdateCheckEvent,
services: {} as {
checkPermissions: {
data: AuthorizationResponse
}
getUpdateCheck: {
data:TypesGen.UpdateCheckResponse
data: UpdateCheckResponse
}
},
},
context: {
updateCheck: undefined,
show: false,
},
initial: "gettingUpdateCheck",
initial: "idle",
states: {
gettingUpdateCheck: {
idle: {
on: {
CHECK: {
target: "fetchingPermissions",
},
},
},
fetchingPermissions: {
invoke: {
src: "checkPermissions",
id: "checkPermissions",
onDone: [
{
actions: ["assignPermissions"],
target: "checkingPermissions",
},
],
onError: [
{
actions: ["assignError"],
target: "show",
},
],
},
},
checkingPermissions: {
always: [
{
target: "fetchingUpdateCheck",
cond: "canViewUpdateCheck",
},
{
target: "dismissOrClear",
cond: "canNotViewUpdateCheck",
},
],
},
fetchingUpdateCheck: {
invoke: {
src: "getUpdateCheck",
id: "getUpdateCheck",
onDone: [
{
actions: ["assignUpdateCheck", "clearGetUpdateCheckError"],
target: "#updateCheckState.success",
actions: ["assignUpdateCheck", "clearError"],
target: "show",
},
],
onError: [
{
actions: ["assignGetUpdateCheckError", "clearUpdateCheck"],
target: "#updateCheckState.failure",
actions: ["assignError", "clearUpdateCheck"],
target: "show",
},
],
},
},
success: {
type: "final",
show: {
entry: "assignShow",
always: [
{
target: "dismissOrClear",
},
],
},
dismissOrClear: {
on: {
DISMISS: {
actions: ["assignHide"],
target: "dismissed",
},
CLEAR: {
actions: ["clearUpdateCheck", "clearError", "assignHide"],
target: "idle",
},
},
},
failure: {
dismissed: {
type: "final",
},
},
},
{
services: {
getUpdateCheck: API.getUpdateCheck,
checkPermissions: async () =>
checkAuthorization({ checks: permissionsToCheck }),
getUpdateCheck: getUpdateCheck,
},
actions: {
assignPermissions: assign({
permissions: (_, event) => event.data as Permissions,
}),
assignShow: assign({
show: true,
}),
assignHide: assign({
show: false,
}),
assignUpdateCheck: assign({
updateCheck: (_, event) => event.data,
}),
clearUpdateCheck: assign((context: UpdateCheckContext) => ({
clearUpdateCheck: assign((context) => ({
...context,
updateCheck: undefined,
})),
assignGetUpdateCheckError: assign({
getUpdateCheckError: (_, event) => event.data,
assignError: assign({
error: (_, event) => event.data,
}),
clearGetUpdateCheckError: assign((context: UpdateCheckContext) => ({
clearError: assign((context) => ({
...context,
getUpdateCheckError: undefined,
error: undefined,
})),
},
guards: {
canViewUpdateCheck: (context) =>
context.permissions?.[checks.viewUpdateCheck] || false,
canNotViewUpdateCheck: (context) =>
!context.permissions?.[checks.viewUpdateCheck],
},
},
)

[8]ページ先頭

©2009-2025 Movatter.jp