mirror of
https://github.com/enso-org/enso.git
synced 2024-11-25 21:25:20 +03:00
Check version (#10646)
- Close https://github.com/enso-org/cloud-v2/issues/1299 - Add component that checks whether the current version of the desktop app is out of date - Add Devtools toggle so that the functionality is testable in dev servers # Important Notes - This functionality is disabled when it is not applicable: - On the Electron watch mode (as development branches do not need to be the latest version) - Note however that built apps (`./run ide build`) do still have the check enabled. - On the cloud dashboard without Electron (as it cannot be updated)
This commit is contained in:
parent
3536a18efd
commit
7a26334519
@ -49,6 +49,7 @@ import * as backendHooks from '#/hooks/backendHooks'
|
||||
|
||||
import AuthProvider, * as authProvider from '#/providers/AuthProvider'
|
||||
import BackendProvider from '#/providers/BackendProvider'
|
||||
import DevtoolsProvider from '#/providers/EnsoDevtoolsProvider'
|
||||
import * as httpClientProvider from '#/providers/HttpClientProvider'
|
||||
import InputBindingsProvider from '#/providers/InputBindingsProvider'
|
||||
import LocalStorageProvider, * as localStorageProvider from '#/providers/LocalStorageProvider'
|
||||
@ -72,6 +73,7 @@ import * as subscribeSuccess from '#/pages/subscribe/SubscribeSuccess'
|
||||
|
||||
import type * as editor from '#/layouts/Editor'
|
||||
import * as openAppWatcher from '#/layouts/OpenAppWatcher'
|
||||
import VersionChecker from '#/layouts/VersionChecker'
|
||||
|
||||
import * as devtools from '#/components/Devtools'
|
||||
import * as errorBoundary from '#/components/ErrorBoundary'
|
||||
@ -508,7 +510,12 @@ function AppRouter(props: AppRouterProps) {
|
||||
</router.Routes>
|
||||
)
|
||||
|
||||
let result = routes
|
||||
let result = (
|
||||
<>
|
||||
<VersionChecker />
|
||||
{routes}
|
||||
</>
|
||||
)
|
||||
|
||||
result = <errorBoundary.ErrorBoundary>{result}</errorBoundary.ErrorBoundary>
|
||||
result = <InputBindingsProvider inputBindings={inputBindings}>{result}</InputBindingsProvider>
|
||||
@ -555,8 +562,8 @@ function AppRouter(props: AppRouterProps) {
|
||||
{result}
|
||||
</httpClientProvider.HttpClientProvider>
|
||||
)
|
||||
|
||||
result = <LoggerProvider logger={logger}>{result}</LoggerProvider>
|
||||
result = <DevtoolsProvider>{result}</DevtoolsProvider>
|
||||
|
||||
return result
|
||||
}
|
||||
|
@ -7,11 +7,17 @@ import * as React from 'react'
|
||||
|
||||
import * as reactQuery from '@tanstack/react-query'
|
||||
|
||||
import { IS_DEV_MODE } from 'enso-common/src/detect'
|
||||
|
||||
import DevtoolsLogo from '#/assets/enso_logo.svg'
|
||||
|
||||
import * as billing from '#/hooks/billing'
|
||||
|
||||
import * as authProvider from '#/providers/AuthProvider'
|
||||
import {
|
||||
useEnableVersionChecker,
|
||||
useSetEnableVersionChecker,
|
||||
} from '#/providers/EnsoDevtoolsProvider'
|
||||
import * as textProvider from '#/providers/TextProvider'
|
||||
|
||||
import * as aria from '#/components/aria'
|
||||
@ -54,6 +60,8 @@ export function EnsoDevtools(props: EnsoDevtoolsProps) {
|
||||
const { getText } = textProvider.useText()
|
||||
const { authQueryKey } = authProvider.useAuth()
|
||||
const session = authProvider.useFullUserSession()
|
||||
const enableVersionChecker = useEnableVersionChecker()
|
||||
const setEnableVersionChecker = useSetEnableVersionChecker()
|
||||
|
||||
const [features, setFeatures] = React.useState<
|
||||
Record<billing.PaywallFeatureName, PaywallDevtoolsFeatureConfiguration>
|
||||
@ -146,6 +154,31 @@ export function EnsoDevtools(props: EnsoDevtoolsProps) {
|
||||
|
||||
<ariaComponents.Separator orientation="horizontal" className="my-3" />
|
||||
|
||||
<ariaComponents.Text variant="subtitle" className="mb-2">
|
||||
{getText('productionOnlyFeatures')}
|
||||
</ariaComponents.Text>
|
||||
<div className="flex flex-col">
|
||||
<aria.Switch
|
||||
className="group flex items-center gap-1"
|
||||
isSelected={enableVersionChecker ?? !IS_DEV_MODE}
|
||||
onChange={setEnableVersionChecker}
|
||||
>
|
||||
<div className="box-border flex h-4 w-[28px] shrink-0 cursor-default items-center rounded-full bg-primary/30 bg-clip-padding p-0.5 shadow-inner outline-none ring-black transition duration-200 ease-in-out group-focus-visible:ring-2 group-pressed:bg-primary/60 group-selected:bg-primary group-selected:group-pressed:bg-primary/50">
|
||||
<span className="aspect-square h-full flex-none translate-x-0 transform rounded-full bg-white transition duration-200 ease-in-out group-selected:translate-x-[100%]" />
|
||||
</div>
|
||||
|
||||
<ariaComponents.Text className="flex-1">
|
||||
{getText('enableVersionChecker')}
|
||||
</ariaComponents.Text>
|
||||
</aria.Switch>
|
||||
|
||||
<ariaComponents.Text variant="body" color="disabled">
|
||||
{getText('enableVersionCheckerDescription')}
|
||||
</ariaComponents.Text>
|
||||
</div>
|
||||
|
||||
<ariaComponents.Separator orientation="horizontal" className="my-3" />
|
||||
|
||||
<ariaComponents.Text variant="subtitle" className="mb-2">
|
||||
{getText('paywallDevtoolsPaywallFeaturesToggles')}
|
||||
</ariaComponents.Text>
|
||||
|
88
app/dashboard/src/layouts/VersionChecker.tsx
Normal file
88
app/dashboard/src/layouts/VersionChecker.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
/** @file Check the version. */
|
||||
import * as React from 'react'
|
||||
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
import { IS_DEV_MODE } from 'enso-common/src/detect'
|
||||
|
||||
import { useToastAndLog } from '#/hooks/toastAndLogHooks'
|
||||
|
||||
import { useLocalBackend } from '#/providers/BackendProvider'
|
||||
import { useEnableVersionChecker } from '#/providers/EnsoDevtoolsProvider'
|
||||
import { useText } from '#/providers/TextProvider'
|
||||
|
||||
import { Button, ButtonGroup, Dialog, Text } from '#/components/AriaComponents'
|
||||
|
||||
import { download } from '#/utilities/download'
|
||||
import { getDownloadUrl, getLatestRelease, LATEST_RELEASE_PAGE_URL } from '#/utilities/github'
|
||||
|
||||
// ======================
|
||||
// === VersionChecker ===
|
||||
// ======================
|
||||
|
||||
/** Check the version. */
|
||||
export default function VersionChecker() {
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
const { getText } = useText()
|
||||
const toastAndLog = useToastAndLog()
|
||||
const localBackend = useLocalBackend()
|
||||
const supportsLocalBackend = localBackend != null
|
||||
const enableVersionChecker = useEnableVersionChecker() ?? (!IS_DEV_MODE && supportsLocalBackend)
|
||||
|
||||
const metadataQuery = useQuery({
|
||||
queryKey: ['latestRelease', enableVersionChecker],
|
||||
queryFn: () => (enableVersionChecker ? getLatestRelease() : null),
|
||||
})
|
||||
const latestVersion = metadataQuery.data?.tag_name
|
||||
|
||||
React.useEffect(() => {
|
||||
if (latestVersion != null && latestVersion !== process.env.ENSO_CLOUD_DASHBOARD_VERSION) {
|
||||
setIsOpen(true)
|
||||
}
|
||||
}, [latestVersion])
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={getText('versionOutdatedTitle')}
|
||||
modalProps={{ isOpen }}
|
||||
onOpenChange={setIsOpen}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Text className="text-center text-sm">{getText('versionOutdatedPrompt')}</Text>
|
||||
<div className="flex flex-col">
|
||||
<Text className="text-center text-sm">
|
||||
{getText('yourVersion')}{' '}
|
||||
<Text className="text-sm font-semibold text-danger">
|
||||
{process.env.ENSO_CLOUD_DASHBOARD_VERSION ?? getText('unknownPlaceholder')}
|
||||
</Text>
|
||||
</Text>
|
||||
<Text className="text-center text-sm">
|
||||
{getText('latestVersion')}{' '}
|
||||
<Text className="text-sm font-semibold text-accent">
|
||||
{latestVersion ?? getText('unknownPlaceholder')}
|
||||
</Text>
|
||||
</Text>
|
||||
</div>
|
||||
<ButtonGroup className="justify-center">
|
||||
<Button
|
||||
size="medium"
|
||||
variant="tertiary"
|
||||
onPress={async () => {
|
||||
const downloadUrl = await getDownloadUrl()
|
||||
if (downloadUrl == null) {
|
||||
toastAndLog('noAppDownloadError')
|
||||
} else {
|
||||
download(downloadUrl)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{getText('download')}
|
||||
</Button>
|
||||
<Button size="medium" href={LATEST_RELEASE_PAGE_URL} target="_blank">
|
||||
{getText('seeLatestRelease')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
80
app/dashboard/src/providers/EnsoDevtoolsProvider.tsx
Normal file
80
app/dashboard/src/providers/EnsoDevtoolsProvider.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
/** @file The React provider (and associated hooks) for Data Catalog state. */
|
||||
import * as React from 'react'
|
||||
|
||||
import invariant from 'tiny-invariant'
|
||||
import * as zustand from 'zustand'
|
||||
|
||||
// =========================
|
||||
// === EnsoDevtoolsStore ===
|
||||
// =========================
|
||||
|
||||
/** The state of this zustand store. */
|
||||
interface EnsoDevtoolsStore {
|
||||
readonly showVersionChecker: boolean | null
|
||||
readonly setEnableVersionChecker: (showVersionChecker: boolean | null) => void
|
||||
}
|
||||
|
||||
// =======================
|
||||
// === ProjectsContext ===
|
||||
// =======================
|
||||
|
||||
/** State contained in a `ProjectsContext`. */
|
||||
export interface ProjectsContextType extends zustand.StoreApi<EnsoDevtoolsStore> {}
|
||||
|
||||
const EnsoDevtoolsContext = React.createContext<ProjectsContextType | null>(null)
|
||||
|
||||
/** Props for a {@link EnsoDevtoolsProvider}. */
|
||||
export interface ProjectsProviderProps extends Readonly<React.PropsWithChildren> {}
|
||||
|
||||
// ========================
|
||||
// === ProjectsProvider ===
|
||||
// ========================
|
||||
|
||||
/** A React provider (and associated hooks) for determining whether the current area
|
||||
* containing the current element is focused. */
|
||||
export default function EnsoDevtoolsProvider(props: ProjectsProviderProps) {
|
||||
const { children } = props
|
||||
const [store] = React.useState(() => {
|
||||
return zustand.createStore<EnsoDevtoolsStore>(set => ({
|
||||
showVersionChecker: false,
|
||||
setEnableVersionChecker: showVersionChecker => {
|
||||
set({ showVersionChecker })
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
return <EnsoDevtoolsContext.Provider value={store}>{children}</EnsoDevtoolsContext.Provider>
|
||||
}
|
||||
|
||||
// ============================
|
||||
// === useEnsoDevtoolsStore ===
|
||||
// ============================
|
||||
|
||||
/** The Enso devtools store. */
|
||||
function useEnsoDevtoolsStore() {
|
||||
const store = React.useContext(EnsoDevtoolsContext)
|
||||
|
||||
invariant(store, 'Enso Devtools store can only be used inside an `EnsoDevtoolsProvider`.')
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// === useEnableVersionChecker ===
|
||||
// ===============================
|
||||
|
||||
/** A function to set whether the version checker is forcibly shown/hidden. */
|
||||
export function useEnableVersionChecker() {
|
||||
const store = useEnsoDevtoolsStore()
|
||||
return zustand.useStore(store, state => state.showVersionChecker)
|
||||
}
|
||||
|
||||
// ==================================
|
||||
// === useSetEnableVersionChecker ===
|
||||
// ==================================
|
||||
|
||||
/** A function to set whether the version checker is forcibly shown/hidden. */
|
||||
export function useSetEnableVersionChecker() {
|
||||
const store = useEnsoDevtoolsStore()
|
||||
return zustand.useStore(store, state => state.setEnableVersionChecker)
|
||||
}
|
@ -9,13 +9,14 @@ import * as detect from 'enso-common/src/detect'
|
||||
// =================
|
||||
|
||||
const ONE_HOUR_MS = 3_600_000
|
||||
export const LATEST_RELEASE_PAGE_URL = 'https://github.com/enso-org/enso/releases/latest'
|
||||
|
||||
// ==================
|
||||
// === GitHub API ===
|
||||
// ==================
|
||||
|
||||
/** Metadata for a GitHub user. */
|
||||
interface GithubSimpleUser {
|
||||
interface GitHubSimpleUser {
|
||||
readonly name?: string
|
||||
readonly email?: string
|
||||
readonly login: string
|
||||
@ -59,7 +60,7 @@ interface GitHubReleaseAsset {
|
||||
readonly download_count: number
|
||||
readonly created_at: string
|
||||
readonly updated_at: string
|
||||
readonly uploader?: GithubSimpleUser
|
||||
readonly uploader?: GitHubSimpleUser
|
||||
}
|
||||
|
||||
/** Metadata for a GitHub release. */
|
||||
@ -71,7 +72,7 @@ interface GitHubRelease {
|
||||
readonly tarball_url?: string
|
||||
readonly zipball_url?: string
|
||||
readonly id: number
|
||||
readonly author: GithubSimpleUser
|
||||
readonly author: GitHubSimpleUser
|
||||
readonly node_id: string
|
||||
/** The name of the tag. */
|
||||
readonly tag_name: string
|
||||
@ -101,7 +102,7 @@ interface CachedRelease {
|
||||
const LOCAL_STORAGE_KEY = `${common.PRODUCT_NAME.toLowerCase()}-cached-release`
|
||||
|
||||
/** Gets the metadata for the latest release of the app. */
|
||||
async function getLatestRelease() {
|
||||
export async function getLatestRelease() {
|
||||
const savedCachedRelease = localStorage.getItem(LOCAL_STORAGE_KEY)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const cachedRelease: CachedRelease | null =
|
||||
|
@ -189,6 +189,7 @@
|
||||
"views": "Views",
|
||||
"likes": "Likes",
|
||||
"shortcuts": "Shortcuts",
|
||||
"download": "Download",
|
||||
"email": "Email",
|
||||
"emailIsRequired": "Email is required",
|
||||
"emailIsInvalid": "Email is invalid",
|
||||
@ -226,6 +227,7 @@
|
||||
"metadata": "Metadata",
|
||||
"path": "Path",
|
||||
"reload": "Reload",
|
||||
"seeLatestRelease": "See latest release",
|
||||
|
||||
"enterSecretPath": "Enter secret path",
|
||||
"enterText": "Enter text",
|
||||
@ -281,6 +283,7 @@
|
||||
"stopExecution": "Stop execution",
|
||||
"openInEditor": "Open in editor",
|
||||
|
||||
"unknownPlaceholder": "unknown",
|
||||
"expand": "Expand",
|
||||
"collapse": "Collapse",
|
||||
"sortAscending": "Sort Ascending",
|
||||
@ -303,12 +306,16 @@
|
||||
"cancelEdit": "Cancel Edit",
|
||||
"loadingAppMessage": "Logging in to Enso...",
|
||||
"appErroredMessage": "Enso encountered an unrecoverable error.",
|
||||
"appErroredPrompt": "Please try refreshing or installing an updateed version.",
|
||||
"appErroredPrompt": "Please try refreshing or installing an updated version.",
|
||||
"discoverWhatsNew": "Discover what’s new",
|
||||
"sampleAndCommunityProjects": "Sample and community projects",
|
||||
"startWithATemplate": "Start with a template",
|
||||
"openInfoMenu": "Open info menu",
|
||||
"noProjectIsCurrentlyOpen": "No project is currently open.",
|
||||
"versionOutdatedTitle": "Upgrade Enso Now",
|
||||
"versionOutdatedPrompt": "Download the latest version to get the latest upgrades and Cloud functionality.",
|
||||
"yourVersion": "Your version:",
|
||||
"latestVersion": "Latest version:",
|
||||
"offlineTitle": "You are offline",
|
||||
"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.",
|
||||
@ -319,6 +326,7 @@
|
||||
"cloudUnavailableOfflineDescriptionOfferLocal": "Alternatively, you can work on your local projects.",
|
||||
"loginUnavailableOffline": "Login functionality is unavailable offline. Please connect to the internet to log in.",
|
||||
"loginUnavailableOfflineLocal": "After logging in, you can work offline with your local projects.",
|
||||
"productionOnlyFeatures": "Production-only features",
|
||||
"switchToLocal": "Switch to Local",
|
||||
"switchToCloud": "Switch to Cloud",
|
||||
"notEnabledTitle": "Your cloud experience is in progress",
|
||||
@ -401,6 +409,8 @@
|
||||
"showPassword": "Show password",
|
||||
"copiedToClipboard": "Copied to clipboard",
|
||||
"noResultsFound": "No results found.",
|
||||
"enableVersionChecker": "Enable Version Checker",
|
||||
"enableVersionCheckerDescription": "Show a dialog if the current version of the desktop app does not match the latest version.",
|
||||
|
||||
"deleteLabelActionText": "delete the label '$0'",
|
||||
"deleteSelectedAssetActionText": "delete '$0'",
|
||||
|
Loading…
Reference in New Issue
Block a user