Skip to content
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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "About app" modal #9833

Merged
merged 29 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0b5f077
WIP: Version page
somebody1234 May 1, 2024
e5d374b
Auto-generate version in development mode
somebody1234 May 1, 2024
20c1507
Also show Electron version
somebody1234 May 1, 2024
747c9b7
Switch to use actual versions
somebody1234 May 2, 2024
391c457
Merge branch 'develop' into wip/sb/about-app
somebody1234 May 2, 2024
f60a2cc
Also show user agent
somebody1234 May 2, 2024
9c6e95b
Add copy-paste support
somebody1234 May 2, 2024
0118cd3
Adjust styles
somebody1234 May 2, 2024
5a4d655
Fix lint errors
somebody1234 May 2, 2024
9401e6b
Switch to dialog components
somebody1234 May 2, 2024
5cf728c
Generate UI and string from array instead of generating string from UI
somebody1234 May 2, 2024
9eb4189
WIP: Fix "about" modal interactions
somebody1234 May 2, 2024
b3c61c4
Fix closing "about" modal
somebody1234 May 2, 2024
aa173e0
WIP: Menu entry for opening "about" modal
somebody1234 May 2, 2024
31c38db
Fix "about" button in system menu
somebody1234 May 2, 2024
c017e16
WIP: Refactor `TheModal` out of dashboard
somebody1234 May 2, 2024
7edd217
Add top right menu to authentication pages
somebody1234 May 2, 2024
960cf56
Fix modal not being closeable on auth pages
somebody1234 May 2, 2024
d3625e5
Fix lint errors
somebody1234 May 2, 2024
aa49c77
Fix E2E tests
somebody1234 May 3, 2024
4dc92b4
Fix logging out
somebody1234 May 3, 2024
49ae1b2
Merge branch 'develop' into wip/sb/about-app
somebody1234 May 9, 2024
beecc9e
Prettier
somebody1234 May 9, 2024
f67ba44
Merge branch 'develop' into wip/sb/about-app
somebody1234 May 10, 2024
1d0ae10
Prettier
somebody1234 May 10, 2024
0da91d4
Hide menu bar (was enabled for debugging purposes)
somebody1234 May 13, 2024
1340cfd
Address code review
somebody1234 May 14, 2024
034517e
Merge branch 'develop' into wip/sb/about-app
somebody1234 May 14, 2024
013a769
Use `useLayoutEffect`
somebody1234 May 14, 2024
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
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
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
Loading