Skip to content

Commit

Permalink
Add "About app" modal (#9833)
Browse files Browse the repository at this point in the history
- Close #9433
- Add modal displaying version info of the app

# Important Notes
None
  • Loading branch information
somebody1234 authored May 14, 2024
1 parent cc8e5ae commit f041176
Show file tree
Hide file tree
Showing 36 changed files with 671 additions and 131 deletions.
14 changes: 11 additions & 3 deletions app/ide-desktop/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,8 @@ const RESTRICTED_SYNTAXES = [
export default [
eslintJs.configs.recommended,
{
// Playwright build cache.
ignores: ['**/.cache/**', '**/playwright-report', 'dist'],
// Playwright build cache and Vite build directory.
ignores: ['**/.cache/**', '**/playwright-report', '**/dist'],
},
{
settings: {
Expand Down Expand Up @@ -334,7 +334,7 @@ export default [
format: ['camelCase', 'PascalCase'],
},
{
selector: ['parameter', 'property', 'method'],
selector: ['parameter', 'method'],
format: ['camelCase'],
},
{
Expand All @@ -343,6 +343,14 @@ export default [
format: ['camelCase'],
leadingUnderscore: 'require',
},
{
selector: ['property'],
format: ['camelCase'],
filter: {
regex: '^(?:data-testid)$',
match: false,
},
},
],
'@typescript-eslint/no-confusing-void-expression': 'error',
'@typescript-eslint/no-empty-interface': 'off',
Expand Down
26 changes: 26 additions & 0 deletions app/ide-desktop/lib/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,32 @@ class App {
}
const window = new electron.BrowserWindow(windowPreferences)
window.setMenuBarVisibility(false)
const oldMenu = electron.Menu.getApplicationMenu()
if (oldMenu != null) {
const items = oldMenu.items.map(item => {
if (item.role !== 'help') {
return item
} else {
// `click` is a property that is intentionally removed from this
// destructured object, in order to satisfy TypeScript.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { click, ...passthrough } = item
return new electron.MenuItem({
...passthrough,
submenu: electron.Menu.buildFromTemplate([
new electron.MenuItem({
label: `About ${common.PRODUCT_NAME}`,
click: () => {
window.webContents.send(ipc.Channel.showAboutModal)
},
}),
]),
})
}
})
const newMenu = electron.Menu.buildFromTemplate(items)
electron.Menu.setApplicationMenu(newMenu)
}

if (this.args.groups.debug.options.devTools.value) {
window.webContents.openDevTools()
Expand Down
1 change: 1 addition & 0 deletions app/ide-desktop/lib/client/src/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ export enum Channel {
goForward = 'go-forward',
/** Channel for selecting files and directories using the system file browser. */
openFileBrowser = 'open-file-browser',
showAboutModal = 'show-about-modal',
}
29 changes: 29 additions & 0 deletions app/ide-desktop/lib/client/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import * as electron from 'electron'

import * as debug from 'debug'
import * as ipc from 'ipc'

// =================
Expand All @@ -23,6 +24,10 @@ const FILE_BROWSER_API_KEY = 'fileBrowserApi'

const NAVIGATION_API_KEY = 'navigationApi'

const MENU_API_KEY = 'menuApi'

const VERSION_INFO_KEY = 'versionInfo'

// =============================
// === importProjectFromPath ===
// =============================
Expand Down Expand Up @@ -172,3 +177,27 @@ const FILE_BROWSER_API = {
electron.ipcRenderer.invoke(ipc.Channel.openFileBrowser, kind),
}
electron.contextBridge.exposeInMainWorld(FILE_BROWSER_API_KEY, FILE_BROWSER_API)

// ====================
// === Version info ===
// ====================

electron.contextBridge.exposeInMainWorld(VERSION_INFO_KEY, debug.VERSION_INFO)

// ================
// === Menu API ===
// ================

let showAboutModalHandler: (() => void) | null = null

electron.ipcRenderer.on(ipc.Channel.showAboutModal, () => {
showAboutModalHandler?.()
})

const MENU_API = {
setShowAboutModalHandler: (callback: () => void) => {
showAboutModalHandler = callback
},
}

electron.contextBridge.exposeInMainWorld(MENU_API_KEY, MENU_API)
18 changes: 18 additions & 0 deletions app/ide-desktop/lib/common/src/appConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import * as url from 'node:url'

import BUILD_INFO from '../../../../../build.json' assert { type: 'json' }

// ===============================
// === readEnvironmentFromFile ===
// ===============================
Expand All @@ -11,7 +13,9 @@ import * as url from 'node:url'
* environment variable. Reads from `.env` if the variable is `production`, blank or absent.
* DOES NOT override existing environment variables if the variable is absent. */
export async function readEnvironmentFromFile() {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const environment = process.env.ENSO_CLOUD_ENVIRONMENT ?? null
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const isProduction = environment == null || environment === '' || environment === 'production'
const fileName = isProduction ? '.env' : `.${environment}.env`
const filePath = path.join(url.fileURLToPath(new URL('../../..', import.meta.url)), fileName)
Expand All @@ -38,13 +42,18 @@ export async function readEnvironmentFromFile() {
if (isProduction) {
entries = entries.filter(kv => {
const [k] = kv
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return process.env[k] == null
})
}
const variables = Object.fromEntries(entries)
if (!isProduction || entries.length > 0) {
Object.assign(process.env, variables)
}
// @ts-expect-error This is the only file where `process.env` should be written to.
process.env.ENSO_CLOUD_DASHBOARD_VERSION ??= BUILD_INFO.version
// @ts-expect-error This is the only file where `process.env` should be written to.
process.env.ENSO_CLOUD_DASHBOARD_COMMIT_HASH ??= BUILD_INFO.commit
} catch (error) {
if (missingKeys.length !== 0) {
console.warn('Could not load `.env` file; disabling cloud backend.')
Expand Down Expand Up @@ -100,6 +109,12 @@ export function getDefines(serverPort = 8080) {
'process.env.ENSO_CLOUD_GOOGLE_ANALYTICS_TAG': stringify(
process.env.ENSO_CLOUD_GOOGLE_ANALYTICS_TAG
),
'process.env.ENSO_CLOUD_DASHBOARD_VERSION': stringify(
process.env.ENSO_CLOUD_DASHBOARD_VERSION
),
'process.env.ENSO_CLOUD_DASHBOARD_COMMIT_HASH': stringify(
process.env.ENSO_CLOUD_DASHBOARD_COMMIT_HASH
),
/* eslint-enable @typescript-eslint/naming-convention */
}
}
Expand All @@ -119,12 +134,15 @@ const DUMMY_DEFINES = {
'process.env.ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID': '',
'process.env.ENSO_CLOUD_COGNITO_DOMAIN': '',
'process.env.ENSO_CLOUD_COGNITO_REGION': '',
'process.env.ENSO_CLOUD_DASHBOARD_VERSION': '0.0.1-testing',
'process.env.ENSO_CLOUD_DASHBOARD_COMMIT_HASH': 'abcdef0',
/* eslint-enable @typescript-eslint/naming-convention */
}

/** Load test environment variables, useful for when the Cloud backend is mocked or unnecessary. */
export function loadTestEnvironmentVariables() {
for (const [k, v] of Object.entries(DUMMY_DEFINES)) {
// @ts-expect-error This is the only file where `process.env` should be written to.
process.env[k.replace(/^process[.]env[.]/, '')] = v
}
}
1 change: 1 addition & 0 deletions app/ide-desktop/lib/common/src/buildUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const INDENT_SIZE = 4
* @throws {Error} If the environment variable is not set. */
export function requireEnv(name) {
const value = process.env[name]
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (value == null) {
throw new Error(`Could not find the environment variable '${name}'.`)
} else {
Expand Down
1 change: 1 addition & 0 deletions app/ide-desktop/lib/common/src/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// ===================

/** Return whether the current build is in development mode */
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
export const IS_DEV_MODE = process.env.NODE_ENV === 'development'

// ================
Expand Down
2 changes: 1 addition & 1 deletion app/ide-desktop/lib/common/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
"checkJs": false,
"skipLibCheck": false
},
"include": ["./src/"]
"include": ["./src/", "../types/"]
}
23 changes: 20 additions & 3 deletions app/ide-desktop/lib/dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ import InputBindingsProvider from '#/providers/InputBindingsProvider'
import LocalStorageProvider, * as localStorageProvider from '#/providers/LocalStorageProvider'
import LoggerProvider from '#/providers/LoggerProvider'
import type * as loggerProvider from '#/providers/LoggerProvider'
import ModalProvider from '#/providers/ModalProvider'
import ModalProvider, * as modalProvider from '#/providers/ModalProvider'
import * as navigator2DProvider from '#/providers/Navigator2DProvider'
import SessionProvider from '#/providers/SessionProvider'
import SupportsLocalBackendProvider from '#/providers/SupportsLocalBackendProvider'

import ConfirmRegistration from '#/pages/authentication/ConfirmRegistration'
import EnterOfflineMode from '#/pages/authentication/EnterOfflineMode'
Expand All @@ -74,6 +75,7 @@ import * as errorBoundary from '#/components/ErrorBoundary'
import * as loader from '#/components/Loader'
import * as rootComponent from '#/components/Root'

import AboutModal from '#/modals/AboutModal'
import * as setOrganizationNameModal from '#/modals/SetOrganizationNameModal'

import type Backend from '#/services/Backend'
Expand Down Expand Up @@ -197,7 +199,9 @@ export default function App(props: AppProps) {
/>
<Router basename={getMainPageUrl().pathname}>
<LocalStorageProvider>
<AppRouter {...props} projectManagerRootDirectory={rootDirectoryPath} />
<ModalProvider>
<AppRouter {...props} projectManagerRootDirectory={rootDirectoryPath} />
</ModalProvider>
</LocalStorageProvider>
</Router>
</reactQuery.QueryClientProvider>
Expand Down Expand Up @@ -226,6 +230,7 @@ function AppRouter(props: AppRouterProps) {
// eslint-disable-next-line no-restricted-properties
const navigate = router.useNavigate()
const { localStorage } = localStorageProvider.useLocalStorage()
const { setModal } = modalProvider.useSetModal()
const navigator2D = navigator2DProvider.useNavigator2D()
if (detect.IS_DEV_MODE) {
// @ts-expect-error This is used exclusively for debugging.
Expand Down Expand Up @@ -320,6 +325,14 @@ function AppRouter(props: AppRouterProps) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
null!

React.useEffect(() => {
if ('menuApi' in window) {
window.menuApi.setShowAboutModalHandler(() => {
setModal(<AboutModal />)
})
}
}, [/* should never change */ setModal])

React.useEffect(() => {
const onKeyDown = navigator2D.onKeyDown.bind(navigator2D)
document.addEventListener('keydown', onKeyDown)
Expand Down Expand Up @@ -438,8 +451,12 @@ function AppRouter(props: AppRouterProps) {
</router.Routes>
)
let result = routes
result = (
<SupportsLocalBackendProvider supportsLocalBackend={supportsLocalBackend}>
{result}
</SupportsLocalBackendProvider>
)
result = <InputBindingsProvider inputBindings={inputBindings}>{result}</InputBindingsProvider>
result = <ModalProvider>{result}</ModalProvider>
result = (
<AuthProvider
shouldStartInOfflineMode={isAuthenticationDisabled}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
/**
* @file
* A dialog is an overlay shown above other content in an application.
* Can be used to display alerts, confirmations, or other content.
*/
/** @file A dialog is an overlay shown above other content in an application.
* Can be used to display alerts, confirmations, or other content. */
import * as React from 'react'

import clsx from 'clsx'
Expand All @@ -16,55 +13,75 @@ import * as portal from '#/components/Portal'

import type * as types from './types'

const MODAL_CLASSES =
'fixed top-0 left-0 right-0 bottom-0 bg-black/[15%] flex items-center justify-center text-center'
// =================
// === Constants ===
// =================

const MODAL_CLASSES = 'fixed z-1 inset bg-dim flex items-center justify-center text-center'
const DIALOG_CLASSES =
'relative flex flex-col overflow-hidden rounded-xl text-left align-middle shadow-2xl bg-clip-padding border border-black/10 before:absolute before:inset before:h-full before:w-full before:rounded-xl before:bg-selected-frame before:backdrop-blur-default'
'relative flex flex-col overflow-hidden text-xs rounded-default text-left align-middle text-primary before:absolute before:inset before:rounded-default before:bg-selected-frame before:backdrop-blur-default'

const MODAL_CLASSES_BY_TYPE = {
modal: 'p-4',
const MODAL_CLASSES_BY_TYPE: Readonly<Record<types.DialogType, string>> = {
modal: '',
popover: '',
fullscreen: 'p-4',
} satisfies Record<types.DialogType, string>
}

const DIALOG_CLASSES_BY_TYPE = {
const DIALOG_CLASSES_BY_TYPE: Readonly<Record<types.DialogType, string>> = {
modal: 'w-full max-w-md min-h-[100px] max-h-[90vh]',
popover: 'rounded-lg',
fullscreen: 'w-full h-full max-w-full max-h-full bg-clip-border',
} satisfies Record<types.DialogType, string>
}

// ==============
// === Dialog ===
// ==============

/**
* A dialog is an overlay shown above other content in an application.
* Can be used to display alerts, confirmations, or other content.
*/
/** A dialog is an overlay shown above other content in an application.
* Can be used to display alerts, confirmations, or other content. */
export function Dialog(props: types.DialogProps) {
const {
children,
title,
type = 'modal',
isDismissible = true,
isDismissable = true,
isKeyboardDismissDisabled = false,
hideCloseButton = false,
className,
onOpenChange = () => {},
modalProps,
...ariaDialogProps
} = props
const cleanupRef = React.useRef(() => {})

const root = portal.useStrictPortalContext()

return (
<aria.Modal
className={tailwindMerge.twMerge(MODAL_CLASSES, [MODAL_CLASSES_BY_TYPE[type]])}
isDismissable={isDismissible}
className={tailwindMerge.twMerge(MODAL_CLASSES, MODAL_CLASSES_BY_TYPE[type])}
isDismissable={isDismissable}
isKeyboardDismissDisabled={isKeyboardDismissDisabled}
UNSTABLE_portalContainer={root.current}
onOpenChange={onOpenChange}
{...modalProps}
>
<aria.Dialog
className={tailwindMerge.twMerge(DIALOG_CLASSES, [DIALOG_CLASSES_BY_TYPE[type]], className)}
className={tailwindMerge.twMerge(DIALOG_CLASSES, DIALOG_CLASSES_BY_TYPE[type], className)}
{...ariaDialogProps}
ref={element => {
cleanupRef.current()
if (element == null) {
cleanupRef.current = () => {}
} else {
const onClick = (event: Event) => {
event.stopPropagation()
}
element.addEventListener('click', onClick)
cleanupRef.current = () => {
element.removeEventListener('click', onClick)
}
}
}}
>
{opts => (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface DialogProps extends aria.DialogProps {
* @default 'modal' */
readonly type?: DialogType
readonly title?: string
readonly isDismissible?: boolean
readonly isDismissable?: boolean
readonly hideCloseButton?: boolean
readonly onOpenChange?: (isOpen: boolean) => void
readonly isKeyboardDismissDisabled?: boolean
Expand Down
2 changes: 0 additions & 2 deletions app/ide-desktop/lib/dashboard/src/components/EditableSpan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ import * as sanitizedEventTargets from '#/utilities/sanitizedEventTargets'

/** Props for an {@link EditableSpan}. */
export interface EditableSpanProps {
// This matches the capitalization of `data-` attributes in React.
// eslint-disable-next-line @typescript-eslint/naming-convention
readonly 'data-testid'?: string
readonly className?: string
readonly editable?: boolean
Expand Down
Loading

0 comments on commit f041176

Please sign in to comment.