mirror of
https://github.com/enso-org/enso.git
synced 2024-11-29 12:43:53 +03:00
Redesign versions checker dialog (#11540)
* fix versions checker dialog * Adress the issues * Fixes * Fix integration tests * fix prettier * Fix dialog --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
parent
008aa19530
commit
9dc4e9b090
@ -365,10 +365,13 @@
|
|||||||
"startWithATemplate": "Discover",
|
"startWithATemplate": "Discover",
|
||||||
"openInfoMenu": "Open info menu",
|
"openInfoMenu": "Open info menu",
|
||||||
"noProjectIsCurrentlyOpen": "No project is currently open.",
|
"noProjectIsCurrentlyOpen": "No project is currently open.",
|
||||||
"versionOutdatedTitle": "Upgrade Enso Now",
|
"versionOutdatedTitle": "Update available",
|
||||||
"versionOutdatedPrompt": "Download the latest version to get the latest upgrades and Cloud functionality.",
|
"versionOutdatedPrompt": "A new version of Enso is available. We recommend updating to access the latest features, improvements, and security enhancements.",
|
||||||
"yourVersion": "Your version:",
|
"yourVersion": "Your version:",
|
||||||
"latestVersion": "Latest version:",
|
"latestVersion": "$0 ($1)",
|
||||||
|
"changeLog": "View changelog",
|
||||||
|
"downloadingAppMessage": "The latest version of Enso is now downloading, which may take a while depending on your internet speed. Please check your downloads folder, and you may close this dialog.",
|
||||||
|
"remindMeLater": "Remind me later",
|
||||||
"offlineTitle": "You are offline",
|
"offlineTitle": "You are offline",
|
||||||
"offlineErrorMessage": "It seems like you are offline. Please make sure you are connected to the internet and try again",
|
"offlineErrorMessage": "It seems like you are offline. Please make sure you are connected to the internet and try again",
|
||||||
"offlineToastMessage": "You are offline. Some features may be unavailable.",
|
"offlineToastMessage": "You are offline. Some features may be unavailable.",
|
||||||
|
@ -145,6 +145,8 @@ interface PlaceholderOverrides {
|
|||||||
readonly arbitraryFieldTooLarge: [maxSize: string]
|
readonly arbitraryFieldTooLarge: [maxSize: string]
|
||||||
readonly arbitraryFieldTooSmall: [minSize: string]
|
readonly arbitraryFieldTooSmall: [minSize: string]
|
||||||
readonly uploadLargeFileStatus: [uploadedParts: number, totalParts: number]
|
readonly uploadLargeFileStatus: [uploadedParts: number, totalParts: number]
|
||||||
|
|
||||||
|
readonly latestVersion: [version: string, date: string]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An tuple of `string` for placeholders for each {@link TextId}. */
|
/** An tuple of `string` for placeholders for each {@link TextId}. */
|
||||||
|
4
app/gui/src/dashboard/assets/download.svg
Normal file
4
app/gui/src/dashboard/assets/download.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.75 14C4.16421 14 4.5 14.3358 4.5 14.75V17.25C4.5 18.4926 5.50736 19.5 6.75 19.5H17.25C18.4926 19.5 19.5 18.4926 19.5 17.25V14.75C19.5 14.3358 19.8358 14 20.25 14C20.6642 14 21 14.3358 21 14.75V17.25C21 19.3211 19.3211 21 17.25 21H6.75C4.67893 21 3 19.3211 3 17.25V14.75C3 14.3358 3.33579 14 3.75 14Z" fill="black"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 15.75C12.1989 15.75 12.3897 15.671 12.5303 15.5303L16.0303 12.0303C16.3232 11.7374 16.3232 11.2626 16.0303 10.9697C15.7374 10.6768 15.2626 10.6768 14.9697 10.9697L12.75 13.1893V3.75C12.75 3.33579 12.4142 3 12 3C11.5858 3 11.25 3.33579 11.25 3.75V13.1893L9.03033 10.9697C8.73744 10.6768 8.26256 10.6768 7.96967 10.9697C7.67678 11.2626 7.67678 11.7374 7.96967 12.0303L11.4697 15.5303C11.6103 15.671 11.8011 15.75 12 15.75Z" fill="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 961 B |
3
app/gui/src/dashboard/assets/new_tab.svg
Normal file
3
app/gui/src/dashboard/assets/new_tab.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 5C13.4477 5 13 4.55228 13 4C13 3.44772 13.4477 3 14 3H20C20.5523 3 21 3.44772 21 4V10C21 10.5523 20.5523 11 20 11C19.4477 11 19 10.5523 19 10V6.41421L11.7071 13.7071C11.3166 14.0976 10.6834 14.0976 10.2929 13.7071C9.90237 13.3166 9.90237 12.6834 10.2929 12.2929L17.5858 5H14ZM8.7587 5L9 5C9.55229 5 10 5.44772 10 6C10 6.55228 9.55229 7 9 7H8.8C7.94342 7 7.36113 7.00078 6.91104 7.03755C6.47262 7.07337 6.24842 7.1383 6.09202 7.21799C5.7157 7.40973 5.40973 7.71569 5.21799 8.09202C5.1383 8.24842 5.07337 8.47262 5.03755 8.91104C5.00078 9.36113 5 9.94342 5 10.8V15.2C5 16.0566 5.00078 16.6389 5.03755 17.089C5.07337 17.5274 5.1383 17.7516 5.21799 17.908C5.40973 18.2843 5.7157 18.5903 6.09202 18.782C6.24842 18.8617 6.47262 18.9266 6.91104 18.9624C7.36113 18.9992 7.94342 19 8.8 19H13.2C14.0566 19 14.6389 18.9992 15.089 18.9624C15.5274 18.9266 15.7516 18.8617 15.908 18.782C16.2843 18.5903 16.5903 18.2843 16.782 17.908C16.8617 17.7516 16.9266 17.5274 16.9624 17.089C16.9992 16.6389 17 16.0566 17 15.2V15C17 14.4477 17.4477 14 18 14C18.5523 14 19 14.4477 19 15V15.2413C19 16.0463 19 16.7106 18.9558 17.2518C18.9099 17.8139 18.8113 18.3306 18.564 18.816C18.1805 19.5686 17.5686 20.1805 16.816 20.564C16.3306 20.8113 15.8139 20.9099 15.2518 20.9558C14.7106 21 14.0463 21 13.2413 21H8.75868C7.95372 21 7.28936 21 6.74818 20.9558C6.18608 20.9099 5.66937 20.8113 5.18404 20.564C4.43139 20.1805 3.81947 19.5686 3.43597 18.816C3.18868 18.3306 3.09012 17.8139 3.04419 17.2518C2.99998 16.7106 2.99999 16.0463 3 15.2413V10.7587C2.99999 9.95373 2.99998 9.28937 3.04419 8.74817C3.09012 8.18608 3.18868 7.66937 3.43597 7.18404C3.81947 6.43139 4.43139 5.81947 5.18404 5.43597C5.66937 5.18868 6.18608 5.09012 6.74817 5.04419C7.28937 4.99998 7.95373 4.99999 8.7587 5Z" fill="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
3
app/gui/src/dashboard/assets/snooze.svg
Normal file
3
app/gui/src/dashboard/assets/snooze.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M18.75 1C18.3358 1 18 1.33579 18 1.75C18 2.16421 18.3358 2.5 18.75 2.5V1ZM22.25 1.75L22.842 2.21046C23.0179 1.98435 23.0496 1.67781 22.9237 1.42049C22.7979 1.16316 22.5364 1 22.25 1V1.75ZM18.75 6.25L18.158 5.78954C17.9821 6.01565 17.9504 6.32219 18.0763 6.57951C18.2021 6.83684 18.4635 7 18.75 7V6.25ZM22.25 7C22.6642 7 23 6.66421 23 6.25C23 5.83579 22.6642 5.5 22.25 5.5V7ZM12.75 7.75C12.75 7.33579 12.4142 7 12 7C11.5858 7 11.25 7.33579 11.25 7.75H12.75ZM12 12H11.25C11.25 12.1989 11.329 12.3897 11.4697 12.5303L12 12ZM13.9697 15.0303C14.2626 15.3232 14.7374 15.3232 15.0303 15.0303C15.3232 14.7374 15.3232 14.2626 15.0303 13.9697L13.9697 15.0303ZM15.216 4.12926C15.5994 4.28609 16.0373 4.10243 16.1942 3.71906C16.351 3.33568 16.1673 2.89776 15.784 2.74093L15.216 4.12926ZM21.6304 9.29774C21.5187 8.89887 21.1048 8.66608 20.7059 8.77779C20.3071 8.88949 20.0743 9.3034 20.186 9.70226L21.6304 9.29774ZM18.75 2.5H22.25V1H18.75V2.5ZM21.658 1.28954L18.158 5.78954L19.342 6.71046L22.842 2.21046L21.658 1.28954ZM18.75 7H22.25V5.5H18.75V7ZM11.25 7.75V12H12.75V7.75H11.25ZM11.4697 12.5303L13.9697 15.0303L15.0303 13.9697L12.5303 11.4697L11.4697 12.5303ZM20.5 12C20.5 16.6944 16.6944 20.5 12 20.5V22C17.5228 22 22 17.5228 22 12H20.5ZM12 20.5C7.30558 20.5 3.5 16.6944 3.5 12H2C2 17.5228 6.47715 22 12 22V20.5ZM3.5 12C3.5 7.30558 7.30558 3.5 12 3.5V2C6.47715 2 2 6.47715 2 12H3.5ZM12 3.5C13.1396 3.5 14.225 3.72384 15.216 4.12926L15.784 2.74093C14.6157 2.26304 13.3377 2 12 2V3.5ZM20.186 9.70226C20.3904 10.4323 20.5 11.2027 20.5 12H22C22 11.0647 21.8714 10.1581 21.6304 9.29774L20.186 9.70226Z" fill="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
@ -3,17 +3,15 @@
|
|||||||
*
|
*
|
||||||
* Close button for a dialog.
|
* Close button for a dialog.
|
||||||
*/
|
*/
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import invariant from 'tiny-invariant'
|
import invariant from 'tiny-invariant'
|
||||||
|
|
||||||
import * as eventCallback from '#/hooks/eventCallbackHooks'
|
import { useEventCallback } from '#/hooks/eventCallbackHooks'
|
||||||
|
import { type ButtonProps, Button } from '../Button'
|
||||||
import * as button from '../Button'
|
|
||||||
import * as dialogProvider from './DialogProvider'
|
import * as dialogProvider from './DialogProvider'
|
||||||
|
|
||||||
/** Props for {@link Close} component. */
|
/** Props for {@link Close} component. */
|
||||||
export type CloseProps = button.ButtonProps
|
export type CloseProps = ButtonProps
|
||||||
|
|
||||||
/** Close button for a dialog. */
|
/** Close button for a dialog. */
|
||||||
export function Close(props: CloseProps) {
|
export function Close(props: CloseProps) {
|
||||||
@ -21,12 +19,10 @@ export function Close(props: CloseProps) {
|
|||||||
|
|
||||||
invariant(dialogContext, 'Close must be used inside a DialogProvider')
|
invariant(dialogContext, 'Close must be used inside a DialogProvider')
|
||||||
|
|
||||||
const onPressCallback = eventCallback.useEventCallback<
|
const onPressCallback = useEventCallback<NonNullable<ButtonProps['onPress']>>((event) => {
|
||||||
NonNullable<button.ButtonProps['onPress']>
|
|
||||||
>((event) => {
|
|
||||||
dialogContext.close()
|
dialogContext.close()
|
||||||
return props.onPress?.(event)
|
return props.onPress?.(event)
|
||||||
})
|
})
|
||||||
|
|
||||||
return <button.Button {...props} onPress={onPressCallback} />
|
return <Button {...props} onPress={onPressCallback} />
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import type { Spring } from '#/utilities/motion'
|
|||||||
import { motion } from '#/utilities/motion'
|
import { motion } from '#/utilities/motion'
|
||||||
import type { VariantProps } from '#/utilities/tailwindVariants'
|
import type { VariantProps } from '#/utilities/tailwindVariants'
|
||||||
import { tv } from '#/utilities/tailwindVariants'
|
import { tv } from '#/utilities/tailwindVariants'
|
||||||
|
import { Close } from './Close'
|
||||||
import * as dialogProvider from './DialogProvider'
|
import * as dialogProvider from './DialogProvider'
|
||||||
import * as dialogStackProvider from './DialogStackProvider'
|
import * as dialogStackProvider from './DialogStackProvider'
|
||||||
import type * as types from './types'
|
import type * as types from './types'
|
||||||
@ -109,11 +110,11 @@ const DIALOG_STYLES = tv({
|
|||||||
padding: {
|
padding: {
|
||||||
none: { content: 'p-0' },
|
none: { content: 'p-0' },
|
||||||
small: { content: 'px-1 pt-3.5 pb-3.5' },
|
small: { content: 'px-1 pt-3.5 pb-3.5' },
|
||||||
medium: { content: 'px-3.5 pt-3.5 pb-3.5' },
|
medium: { content: 'px-4 pt-3 pb-4' },
|
||||||
large: { content: 'px-8 pt-3.5 pb-5' },
|
large: { content: 'px-8 pt-5 pb-5' },
|
||||||
xlarge: { content: 'p-12 pt-3.5 pb-8' },
|
xlarge: { content: 'p-12 pt-6 pb-8' },
|
||||||
xxlarge: { content: 'p-16 pt-3.5 pb-12' },
|
xxlarge: { content: 'p-16 pt-8 pb-12' },
|
||||||
xxxlarge: { content: 'p-20 pt-3.5 pb-16' },
|
xxxlarge: { content: 'p-20 pt-10 pb-16' },
|
||||||
},
|
},
|
||||||
scrolledToTop: { true: { header: 'border-transparent' } },
|
scrolledToTop: { true: { header: 'border-transparent' } },
|
||||||
},
|
},
|
||||||
@ -140,7 +141,7 @@ const DIALOG_STYLES = tv({
|
|||||||
hideCloseButton: false,
|
hideCloseButton: false,
|
||||||
size: 'medium',
|
size: 'medium',
|
||||||
padding: 'medium',
|
padding: 'medium',
|
||||||
rounded: 'xxlarge',
|
rounded: 'xxxlarge',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -351,3 +352,5 @@ const TYPE_TO_DIALOG_TYPE: Record<
|
|||||||
modal: 'dialog',
|
modal: 'dialog',
|
||||||
fullscreen: 'dialog-fullscreen',
|
fullscreen: 'dialog-fullscreen',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Dialog.Close = Close
|
||||||
|
@ -28,7 +28,7 @@ export interface StepperProps {
|
|||||||
| ((props: BaseRenderProps) => string | null | undefined)
|
| ((props: BaseRenderProps) => string | null | undefined)
|
||||||
| null
|
| null
|
||||||
| undefined
|
| undefined
|
||||||
readonly renderStep: (props: RenderStepProps) => React.ReactNode
|
readonly renderStep?: ((props: RenderStepProps) => React.ReactNode) | null
|
||||||
readonly style?:
|
readonly style?:
|
||||||
| React.CSSProperties
|
| React.CSSProperties
|
||||||
| ((props: BaseRenderProps) => React.CSSProperties | undefined)
|
| ((props: BaseRenderProps) => React.CSSProperties | undefined)
|
||||||
@ -101,6 +101,7 @@ export function Stepper(props: StepperProps) {
|
|||||||
<stepperProvider.StepperProvider
|
<stepperProvider.StepperProvider
|
||||||
value={{ totalSteps, currentStep, goToStep, nextStep, previousStep, state }}
|
value={{ totalSteps, currentStep, goToStep, nextStep, previousStep, state }}
|
||||||
>
|
>
|
||||||
|
{renderStep == null ? null : (
|
||||||
<div className={styles.steps()}>
|
<div className={styles.steps()}>
|
||||||
{Array.from({ length: totalSteps }).map((_, index) => {
|
{Array.from({ length: totalSteps }).map((_, index) => {
|
||||||
const renderStepProps = {
|
const renderStepProps = {
|
||||||
@ -117,13 +118,20 @@ export function Stepper(props: StepperProps) {
|
|||||||
isDisabled: index > currentStep,
|
isDisabled: index > currentStep,
|
||||||
} satisfies RenderStepProps
|
} satisfies RenderStepProps
|
||||||
|
|
||||||
|
const nextRenderStep = renderStep(renderStepProps)
|
||||||
|
|
||||||
|
if (nextRenderStep == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className={styles.step({})}>
|
<div key={index} className={styles.step({})}>
|
||||||
{renderStep(renderStepProps)}
|
{nextRenderStep}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={styles.content()}>
|
<div className={styles.content()}>
|
||||||
<AnimatePresence initial={false} mode="sync" custom={direction}>
|
<AnimatePresence initial={false} mode="sync" custom={direction}>
|
||||||
|
@ -45,6 +45,7 @@ export interface UseStepperStateResult {
|
|||||||
readonly percentComplete: number
|
readonly percentComplete: number
|
||||||
readonly nextStep: () => void
|
readonly nextStep: () => void
|
||||||
readonly previousStep: () => void
|
readonly previousStep: () => void
|
||||||
|
readonly resetStepper: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -67,6 +68,10 @@ export function useStepperState(props: StepperStateProps): UseStepperStateResult
|
|||||||
const onStepChangeStableCallback = eventCallbackHooks.useEventCallback(onStepChange)
|
const onStepChangeStableCallback = eventCallbackHooks.useEventCallback(onStepChange)
|
||||||
const onCompletedStableCallback = eventCallbackHooks.useEventCallback(onCompleted)
|
const onCompletedStableCallback = eventCallbackHooks.useEventCallback(onCompleted)
|
||||||
|
|
||||||
|
const resetStepper = eventCallbackHooks.useEventCallback(() => {
|
||||||
|
privateSetCurrentStep(() => ({ current: defaultStep, direction: 'initial' }))
|
||||||
|
})
|
||||||
|
|
||||||
const setCurrentStep = eventCallbackHooks.useEventCallback(
|
const setCurrentStep = eventCallbackHooks.useEventCallback(
|
||||||
(step: number | ((current: number) => number)) => {
|
(step: number | ((current: number) => number)) => {
|
||||||
React.startTransition(() => {
|
React.startTransition(() => {
|
||||||
@ -128,5 +133,6 @@ export function useStepperState(props: StepperStateProps): UseStepperStateResult
|
|||||||
percentComplete,
|
percentComplete,
|
||||||
nextStep,
|
nextStep,
|
||||||
previousStep,
|
previousStep,
|
||||||
|
resetStepper,
|
||||||
} satisfies UseStepperStateResult
|
} satisfies UseStepperStateResult
|
||||||
}
|
}
|
||||||
|
@ -1,88 +1,250 @@
|
|||||||
/** @file Check the version. */
|
/** @file Check the version. */
|
||||||
import * as React from 'react'
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
import { useQuery } from '@tanstack/react-query'
|
|
||||||
|
|
||||||
|
import DownloadIcon from '#/assets/download.svg'
|
||||||
|
import NewTabIcon from '#/assets/new_tab.svg'
|
||||||
|
import SnoozeIcon from '#/assets/snooze.svg'
|
||||||
import { IS_DEV_MODE } from 'enso-common/src/detect'
|
import { IS_DEV_MODE } from 'enso-common/src/detect'
|
||||||
|
|
||||||
import { useToastAndLog } from '#/hooks/toastAndLogHooks'
|
import { useToastAndLog } from '#/hooks/toastAndLogHooks'
|
||||||
|
|
||||||
import { useEnableVersionChecker } from '#/components/Devtools'
|
import { useEnableVersionChecker, useSetEnableVersionChecker } from '#/components/Devtools'
|
||||||
import { useLocalBackend } from '#/providers/BackendProvider'
|
import { useLocalBackend } from '#/providers/BackendProvider'
|
||||||
import { useText } from '#/providers/TextProvider'
|
import { useText } from '#/providers/TextProvider'
|
||||||
|
|
||||||
import { Button, ButtonGroup, Dialog, Text } from '#/components/AriaComponents'
|
import { Button, ButtonGroup, Dialog, Text } from '#/components/AriaComponents'
|
||||||
|
|
||||||
|
import { Stepper } from '#/components/Stepper'
|
||||||
|
import { useEventCallback } from '#/hooks/eventCallbackHooks'
|
||||||
import { download } from '#/utilities/download'
|
import { download } from '#/utilities/download'
|
||||||
import { getDownloadUrl, getLatestRelease, LATEST_RELEASE_PAGE_URL } from '#/utilities/github'
|
import { getDownloadUrl, getLatestRelease } from '#/utilities/github'
|
||||||
|
import { startTransition, useState } from 'react'
|
||||||
|
|
||||||
// ======================
|
const CURRENT_VERSION = process.env.ENSO_CLOUD_DASHBOARD_VERSION ?? 'unknown-dev'
|
||||||
// === VersionChecker ===
|
const CURRENT_VERSION_IS_DEV = CURRENT_VERSION.endsWith('-dev')
|
||||||
// ======================
|
const CURRENT_VERSION_IS_NIGHTLY = CURRENT_VERSION.includes('-nightly')
|
||||||
|
const CURRENT_VERSION_NUMBER = getVersionNumber(CURRENT_VERSION)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||||
|
const STALE_TIME = 24 * 60 * 60 * 1000 // 1 day
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||||
|
const STALE_TIME_ERROR = 10 * 60 * 1000 // 10 minutes
|
||||||
|
|
||||||
/** Check the version. */
|
/** Check the version. */
|
||||||
export default function VersionChecker() {
|
export default function VersionChecker() {
|
||||||
const [isOpen, setIsOpen] = React.useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const { getText } = useText()
|
|
||||||
|
const { getText, locale } = useText()
|
||||||
const toastAndLog = useToastAndLog()
|
const toastAndLog = useToastAndLog()
|
||||||
const localBackend = useLocalBackend()
|
const localBackend = useLocalBackend()
|
||||||
|
|
||||||
const supportsLocalBackend = localBackend != null
|
const supportsLocalBackend = localBackend != null
|
||||||
const enableVersionChecker = useEnableVersionChecker() ?? (!IS_DEV_MODE && supportsLocalBackend)
|
const overrideValue = useEnableVersionChecker()
|
||||||
|
const setOverrideValue = useSetEnableVersionChecker()
|
||||||
|
const shouldOverride = overrideValue ?? false
|
||||||
|
|
||||||
|
const enableVersionChecker = IS_DEV_MODE ? shouldOverride : supportsLocalBackend
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const metadataQuery = useQuery({
|
const metadataQuery = useQuery({
|
||||||
queryKey: ['latestRelease', enableVersionChecker],
|
queryKey: ['latestRelease'],
|
||||||
queryFn: () => (enableVersionChecker ? getLatestRelease() : null),
|
queryFn: async () => {
|
||||||
|
const latestRelease = await getLatestRelease()
|
||||||
|
return { ...latestRelease, isPostponed: false }
|
||||||
|
},
|
||||||
|
select: (data) => {
|
||||||
|
const versionNumber = getVersionNumber(data.tag_name)
|
||||||
|
const publishedAt = new Date(data.published_at).toLocaleString(locale, {
|
||||||
|
dateStyle: 'long',
|
||||||
})
|
})
|
||||||
const latestVersion = metadataQuery.data?.tag_name
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
if (versionNumber == null) {
|
||||||
if (latestVersion != null && latestVersion !== process.env.ENSO_CLOUD_DASHBOARD_VERSION) {
|
return {
|
||||||
setIsOpen(true)
|
versionNumber: CURRENT_VERSION_NUMBER,
|
||||||
|
publishedAt,
|
||||||
|
tagName: CURRENT_VERSION,
|
||||||
|
htmlUrl: data.html_url,
|
||||||
|
isPostponed: data.isPostponed,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [latestVersion])
|
|
||||||
|
|
||||||
return (
|
return {
|
||||||
<Dialog
|
versionNumber,
|
||||||
title={getText('versionOutdatedTitle')}
|
publishedAt,
|
||||||
modalProps={{ isOpen }}
|
tagName: data.tag_name,
|
||||||
onOpenChange={setIsOpen}
|
htmlUrl: data.html_url,
|
||||||
>
|
isPostponed: data.isPostponed,
|
||||||
<div className="flex flex-col gap-3">
|
}
|
||||||
<Text className="text-center text-sm">{getText('versionOutdatedPrompt')}</Text>
|
},
|
||||||
<div className="flex flex-col">
|
enabled: enableVersionChecker,
|
||||||
<Text className="text-center text-sm">
|
meta: { persist: false },
|
||||||
{getText('yourVersion')}{' '}
|
staleTime: (query) => {
|
||||||
<Text className="text-sm font-semibold text-danger">
|
if (query.state.error) {
|
||||||
{process.env.ENSO_CLOUD_DASHBOARD_VERSION ?? getText('unknownPlaceholder')}
|
return STALE_TIME_ERROR
|
||||||
</Text>
|
}
|
||||||
</Text>
|
|
||||||
<Text className="text-center text-sm">
|
return STALE_TIME
|
||||||
{getText('latestVersion')}{' '}
|
},
|
||||||
<Text className="text-sm font-semibold text-accent">
|
})
|
||||||
{latestVersion ?? getText('unknownPlaceholder')}
|
|
||||||
</Text>
|
const { stepperState, isLastStep, resetStepper } = Stepper.useStepperState({ steps: 2 })
|
||||||
</Text>
|
|
||||||
</div>
|
const remindLater = useEventCallback(() => {
|
||||||
<ButtonGroup className="justify-center">
|
setIsOpen(false)
|
||||||
<Button
|
// User asked to be reminded later, so we suppress the dialog from showing again for next 24 hours.
|
||||||
size="medium"
|
queryClient.setQueryData(['latestRelease'], { ...metadataQuery.data, isPostponed: true })
|
||||||
variant="accent"
|
})
|
||||||
onPress={async () => {
|
|
||||||
|
const onDownload = useEventCallback(async () => {
|
||||||
const downloadUrl = await getDownloadUrl()
|
const downloadUrl = await getDownloadUrl()
|
||||||
|
|
||||||
if (downloadUrl == null) {
|
if (downloadUrl == null) {
|
||||||
toastAndLog('noAppDownloadError')
|
toastAndLog('noAppDownloadError')
|
||||||
} else {
|
} else {
|
||||||
download(downloadUrl)
|
download(downloadUrl)
|
||||||
|
stepperState.nextStep()
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!metadataQuery.isSuccess) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadataQuery.data.isPostponed) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const { versionNumber, tagName, htmlUrl, publishedAt } = metadataQuery.data
|
||||||
|
const latestVersionNumber = versionNumber
|
||||||
|
const latestVersion = tagName
|
||||||
|
|
||||||
|
const shouldBeShown = (() => {
|
||||||
|
if (shouldOverride) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latestVersionNumber == null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CURRENT_VERSION_NUMBER == null || CURRENT_VERSION_IS_DEV || CURRENT_VERSION_IS_NIGHTLY) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return latestVersionNumber > CURRENT_VERSION_NUMBER
|
||||||
|
})()
|
||||||
|
|
||||||
|
if (!shouldBeShown) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isOpen && !isLastStep) {
|
||||||
|
startTransition(() => {
|
||||||
|
setIsOpen(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
title={getText('versionOutdatedTitle')}
|
||||||
|
size="large"
|
||||||
|
modalProps={{ isOpen }}
|
||||||
|
isDismissable={isLastStep}
|
||||||
|
hideCloseButton={!isLastStep}
|
||||||
|
isKeyboardDismissDisabled={!isLastStep}
|
||||||
|
onOpenChange={(openChange) => {
|
||||||
|
startTransition(() => {
|
||||||
|
if (!openChange && overrideValue === true) {
|
||||||
|
setOverrideValue(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLastStep) {
|
||||||
|
remindLater()
|
||||||
|
}
|
||||||
|
|
||||||
|
resetStepper()
|
||||||
|
|
||||||
|
setIsOpen(openChange)
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
{() => (
|
||||||
|
<Stepper state={stepperState} renderStep={null}>
|
||||||
|
<Stepper.StepContent index={0}>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Text className="text-center text-sm" balance>
|
||||||
|
{getText('versionOutdatedPrompt')}
|
||||||
|
</Text>
|
||||||
|
<div className="mb-4 mt-3 flex flex-col items-center">
|
||||||
|
<Text.Group>
|
||||||
|
<Text variant="h1">{getText('latestVersion', latestVersion, publishedAt)}</Text>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
href={htmlUrl}
|
||||||
|
target="_blank"
|
||||||
|
icon={NewTabIcon}
|
||||||
|
iconPosition="end"
|
||||||
|
>
|
||||||
|
{getText('changeLog')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Text variant="body-sm">
|
||||||
|
{getText('yourVersion')}{' '}
|
||||||
|
<Text weight="bold" variant="body">
|
||||||
|
{CURRENT_VERSION}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</Text.Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ButtonGroup className="justify-center">
|
||||||
|
<Button
|
||||||
|
size="medium"
|
||||||
|
variant="outline"
|
||||||
|
fullWidth
|
||||||
|
onPress={remindLater}
|
||||||
|
icon={SnoozeIcon}
|
||||||
|
iconPosition="end"
|
||||||
|
>
|
||||||
|
{getText('remindMeLater')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="medium"
|
||||||
|
fullWidth
|
||||||
|
variant="primary"
|
||||||
|
onPress={onDownload}
|
||||||
|
icon={DownloadIcon}
|
||||||
|
iconPosition="end"
|
||||||
>
|
>
|
||||||
{getText('download')}
|
{getText('download')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="medium" href={LATEST_RELEASE_PAGE_URL} target="_blank">
|
|
||||||
{getText('seeLatestRelease')}
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
|
</Stepper.StepContent>
|
||||||
|
|
||||||
|
<Stepper.StepContent index={1}>
|
||||||
|
<div className="flex flex-col items-center text-center">
|
||||||
|
<Text balance variant="body">
|
||||||
|
{getText('downloadingAppMessage')}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Dialog.Close variant="primary" className="mt-4 min-w-48">
|
||||||
|
{getText('close')}
|
||||||
|
</Dialog.Close>
|
||||||
|
</div>
|
||||||
|
</Stepper.StepContent>
|
||||||
|
</Stepper>
|
||||||
|
)}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the version number from a version string.
|
||||||
|
* @param version - The version string.
|
||||||
|
* @returns The version number, or null if the version string is not a valid version number.
|
||||||
|
*/
|
||||||
|
function getVersionNumber(version: string) {
|
||||||
|
const versionNumber = Number(version.replace('.', ''))
|
||||||
|
return isNaN(versionNumber) ? null : versionNumber
|
||||||
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
/** @file Commonly used functions for electron tests */
|
/** @file Commonly used functions for electron tests */
|
||||||
|
|
||||||
import { _electron, expect, type Page, test } from '@playwright/test'
|
import { _electron, expect, type Page, test } from '@playwright/test'
|
||||||
|
import { TEXTS } from 'enso-common/src/text'
|
||||||
import fs from 'node:fs/promises'
|
import fs from 'node:fs/promises'
|
||||||
import os from 'node:os'
|
import os from 'node:os'
|
||||||
import pathModule from 'node:path'
|
import pathModule from 'node:path'
|
||||||
|
|
||||||
const LOADING_TIMEOUT = 10000
|
const LOADING_TIMEOUT = 10000
|
||||||
|
const TEXT = TEXTS.english
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests run on electron executable.
|
* Tests run on electron executable.
|
||||||
@ -45,19 +47,20 @@ export async function loginAsTestUser(page: Page) {
|
|||||||
'Cannot log in; `ENSO_TEST_USER` and `ENSO_TEST_USER_PASSWORD` env variables are not provided',
|
'Cannot log in; `ENSO_TEST_USER` and `ENSO_TEST_USER_PASSWORD` env variables are not provided',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
await page.getByRole('textbox', { name: 'email' }).click()
|
await page.getByRole('textbox', { name: 'email' }).fill(process.env.ENSO_TEST_USER)
|
||||||
await page.keyboard.insertText(process.env.ENSO_TEST_USER)
|
await page.getByRole('textbox', { name: 'password' }).fill(process.env.ENSO_TEST_USER_PASSWORD)
|
||||||
await page.keyboard.press('Tab')
|
await page.getByTestId('form-submit-button').click()
|
||||||
await page.keyboard.insertText(process.env.ENSO_TEST_USER_PASSWORD)
|
|
||||||
await page.keyboard.press('Enter')
|
|
||||||
|
|
||||||
// Accept terms screen
|
await page
|
||||||
await expect(page.getByText('I agree')).toHaveCount(2)
|
.getByRole('group', { name: TEXT.licenseAgreementCheckbox })
|
||||||
await expect(page.getByRole('button')).toHaveCount(1)
|
.getByText(TEXT.licenseAgreementCheckbox)
|
||||||
for (const checkbox of await page.getByText('I agree').all()) {
|
.click()
|
||||||
await checkbox.click()
|
await page
|
||||||
}
|
.getByRole('group', { name: TEXT.privacyPolicyCheckbox })
|
||||||
await page.getByRole('button').click()
|
.getByText(TEXT.privacyPolicyCheckbox)
|
||||||
|
.click()
|
||||||
|
|
||||||
|
await page.getByTestId('form-submit-button').click()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user