Skip to content

Commit

Permalink
Vue entrypoint (#11960)
Browse files Browse the repository at this point in the history
Fixes #11185
Fixes #11923

I moved (and simplified) some setup. Mostly, the entry point sets up general behavior on page load, App.vue is the root, and the react-specific setups are now in `ReactRoot` component. The old App.vue is now just the ProjectView.vue.

Removed some options which are no longer relevant since there's just a single GUI package.

I tried to put the ProjectView component into dashboard layout, but unfortunately, veaury is not helpful here, as it unsets all styles in its wrapper.

Also, GUI no longer displays Help view; if there's an unrecognized option we just print a warning and continue.
  • Loading branch information
farmaazon authored Jan 8, 2025
1 parent da2898e commit 10bb0ae
Show file tree
Hide file tree
Showing 31 changed files with 572 additions and 685 deletions.
2 changes: 1 addition & 1 deletion app/gui/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const reactPreview: ReactPreview = {

(Story, context) => (
<>
<div className="enso-dashboard">
<div className="enso-app">
<Story {...context} />
</div>
<div id="enso-portal-root" className="enso-portal-root" />
Expand Down
2 changes: 1 addition & 1 deletion app/gui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
</head>
<body>
<div id="enso-spotlight" class="enso-spotlight"></div>
<div id="enso-dashboard" class="enso-dashboard"></div>
<div id="enso-app" class="enso-app"></div>
<div id="enso-chat" class="enso-chat"></div>
<div id="enso-portal-root" class="enso-portal-root"></div>
<script type="module" src="/src/entrypoint.ts"></script>
Expand Down
9 changes: 1 addition & 8 deletions app/gui/integration-test/dashboard/createAsset.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** @file Test copying, moving, cutting and pasting. */
import { expect, test, type Page } from '@playwright/test'
import { expect, test } from '@playwright/test'

import { mockAllAndLogin } from './actions'

Expand All @@ -12,13 +12,6 @@ const SECRET_NAME = 'a secret name'
/** The value of the created secret. */
const SECRET_VALUE = 'a secret value'

/** Find an editor container. */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function locateEditor(page: Page) {
// Test ID of a placeholder editor component used during testing.
return page.locator('.App')
}

test('create folder', ({ page }) =>
mockAllAndLogin({ page })
.createFolder()
Expand Down
9 changes: 1 addition & 8 deletions app/gui/integration-test/dashboard/driveView.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
/** @file Test the drive view. */
import { expect, test, type Locator, type Page } from '@playwright/test'
import { expect, test, type Locator } from '@playwright/test'

import { TEXT, mockAllAndLogin } from './actions'

/** Find an editor container. */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function locateEditor(page: Page) {
// Test ID of a placeholder editor component used during testing.
return page.locator('.App')
}

/** Find a button to close the project. */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function locateStopProjectButton(page: Locator) {
Expand Down
2 changes: 1 addition & 1 deletion app/gui/integration-test/dashboard/pageSwitcher.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { mockAllAndLogin } from './actions'
/** Find an editor container. */
function locateEditor(page: Page) {
// Test ID of a placeholder editor component used during testing.
return page.locator('.App')
return page.locator('.ProjectView')
}

/** Find a drive view. */
Expand Down
2 changes: 1 addition & 1 deletion app/gui/integration-test/dashboard/startModal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { mockAllAndLogin } from './actions'
/** Find an editor container. */
function locateEditor(page: Page) {
// Test ID of a placeholder editor component used during testing.
return page.locator('.App')
return page.locator('.ProjectView')
}

/** Find a samples list. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import assert from 'assert'
import * as actions from './actions'
import { computedContent } from './css'
import { expect } from './customExpect'
import { CONTROL_KEY } from './keyboard'
import * as locate from './locate'

test('Node can open and load visualization', async ({ page }) => {
Expand Down Expand Up @@ -50,10 +51,12 @@ test('Previewing visualization', async ({ page }) => {

test('Warnings visualization', async ({ page }) => {
await actions.goToGraph(page)

// Without centering the graph, menu sometimes goes out of the view.
await page.keyboard.press(`${CONTROL_KEY}+Shift+A`)
// Create a node, attach a warning, open the warnings-visualization.
await locate.addNewNodeButton(page).click()
const input = locate.componentBrowserInput(page).locator('input')

await input.fill('Warning.attach "Uh oh" 42')
await page.keyboard.press('Enter')
await expect(locate.componentBrowser(page)).toBeHidden()
Expand Down
78 changes: 78 additions & 0 deletions app/gui/src/App.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<script setup lang="ts">
import '@/assets/base.css'
import TooltipDisplayer from '@/components/TooltipDisplayer.vue'
import ProjectView from '@/ProjectView.vue'
import { provideAppClassSet } from '@/providers/appClass'
import { provideGuiConfig } from '@/providers/guiConfig'
import { provideTooltipRegistry } from '@/providers/tooltipRegistry'
import { registerAutoBlurHandler } from '@/util/autoBlur'
import { baseConfig, configValue, mergeConfig, type ApplicationConfigValue } from '@/util/config'
import { urlParams } from '@/util/urlParams'
import { useQueryClient } from '@tanstack/vue-query'
import { applyPureReactInVue } from 'veaury'
import { computed, onMounted } from 'vue'
import { ComponentProps } from 'vue-component-type-helpers'
import ReactRoot from './ReactRoot'
const _props = defineProps<{
// Used in Project View integration tests. Once both test projects will be merged, this should be
// removed
projectViewOnly?: { options: ComponentProps<typeof ProjectView> } | null
onAuthenticated?: (accessToken: string | null) => void
}>()
const classSet = provideAppClassSet()
const appTooltips = provideTooltipRegistry()
const appConfig = computed(() => {
const config = mergeConfig(baseConfig, urlParams(), {
onUnrecognizedOption: (p) => console.warn('Unrecognized option:', p),
})
return config
})
const appConfigValue = computed((): ApplicationConfigValue => configValue(appConfig.value))
const ReactRootWrapper = applyPureReactInVue(ReactRoot)
const queryClient = useQueryClient()
provideGuiConfig(appConfigValue)
registerAutoBlurHandler()
onMounted(() => {
if (appConfigValue.value.window.vibrancy) {
document.body.classList.add('vibrancy')
}
})
</script>

<template>
<div :class="['App', ...classSet.keys()]">
<ProjectView v-if="projectViewOnly" v-bind="projectViewOnly.options" />
<ReactRootWrapper
v-else
:config="appConfigValue"
:queryClient="queryClient"
@authenticated="onAuthenticated ?? (() => {})"
/>
</div>
<TooltipDisplayer :registry="appTooltips" />
</template>

<style>
.App {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
/*
TODO [ao]: Veaury adds a wrapping elements which have `style="all: unset"`, which in turn breaks our layout.
See https://github.com/gloriasoft/veaury/issues/158
*/
[__use_react_component_wrap],
[data-use-vue-component-wrap] {
display: contents !important;
}
</style>
76 changes: 76 additions & 0 deletions app/gui/src/ReactRoot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/** @file A file containing setup for React part of application. */

import App from '#/App.tsx'
import { ReactQueryDevtools } from '#/components/Devtools'
import { ErrorBoundary } from '#/components/ErrorBoundary'
import { OfflineNotificationManager } from '#/components/OfflineNotificationManager'
import { Suspense } from '#/components/Suspense'
import UIProviders from '#/components/UIProviders'
import LoadingScreen from '#/pages/authentication/LoadingScreen'
import { HttpClientProvider } from '#/providers/HttpClientProvider'
import LoggerProvider from '#/providers/LoggerProvider'
import HttpClient from '#/utilities/HttpClient'
import { ApplicationConfigValue } from '@/util/config'
import { QueryClientProvider } from '@tanstack/react-query'
import { QueryClient } from '@tanstack/vue-query'
import { IS_DEV_MODE, isOnElectron, isOnLinux } from 'enso-common/src/detect'
import { StrictMode } from 'react'
import invariant from 'tiny-invariant'

interface ReactRootProps {
config: ApplicationConfigValue
queryClient: QueryClient
classSet: Map<string, number>
onAuthenticated: (accessToken: string | null) => void
}

function resolveEnvUrl(url: string | undefined) {
return url?.replace('__HOSTNAME__', window.location.hostname)
}

/**
* A component gathering all views written currently in React with necessary contexts.
*/
export default function ReactRoot(props: ReactRootProps) {
const { config, queryClient, onAuthenticated } = props

const httpClient = new HttpClient()
const supportsDeepLinks = !IS_DEV_MODE && !isOnLinux() && isOnElectron()
const portalRoot = document.querySelector('#enso-portal-root')
const shouldUseAuthentication = config.authentication.enabled
const projectManagerUrl =
(config.engine.projectManagerUrl || resolveEnvUrl(PROJECT_MANAGER_URL)) ?? null
const ydocUrl = (config.engine.ydocUrl || resolveEnvUrl(YDOC_SERVER_URL)) ?? null
const initialProjectName = config.startup.project || null
invariant(portalRoot, 'PortalRoot element not found')

return (
<StrictMode>
<QueryClientProvider client={queryClient}>
<ErrorBoundary>
<UIProviders locale="en-US" portalRoot={portalRoot}>
<Suspense fallback={<LoadingScreen />}>
<OfflineNotificationManager>
<LoggerProvider logger={console}>
<HttpClientProvider httpClient={httpClient}>
<App
supportsDeepLinks={supportsDeepLinks}
supportsLocalBackend={!IS_CLOUD_BUILD}
isAuthenticationDisabled={!shouldUseAuthentication}
projectManagerUrl={projectManagerUrl}
ydocUrl={ydocUrl}
initialProjectName={initialProjectName}
onAuthenticated={onAuthenticated}
/>
</HttpClientProvider>
</LoggerProvider>
</OfflineNotificationManager>
</Suspense>

<ReactQueryDevtools />
</UIProviders>
</ErrorBoundary>
</QueryClientProvider>
</StrictMode>
)
}
17 changes: 3 additions & 14 deletions app/gui/src/dashboard/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ import Dashboard from '#/pages/dashboard/Dashboard'
import * as subscribe from '#/pages/subscribe/Subscribe'
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'

Expand Down Expand Up @@ -143,7 +142,6 @@ function getMainPageUrl() {

/** Global configuration for the `App` component. */
export interface AppProps {
readonly vibrancy: boolean
/** Whether the application may have the local backend running. */
readonly supportsLocalBackend: boolean
/** If true, the app can only be used in offline mode. */
Expand All @@ -153,15 +151,11 @@ export interface AppProps {
* the installed app on macOS and Windows.
*/
readonly supportsDeepLinks: boolean
/** Whether the dashboard should be rendered. */
readonly shouldShowDashboard: boolean
/** The name of the project to open on startup, if any. */
readonly initialProjectName: string | null
readonly onAuthenticated: (accessToken: string | null) => void
readonly projectManagerUrl: string | null
readonly ydocUrl: string | null
readonly appRunner: editor.GraphEditorRunner | null
readonly queryClient: reactQuery.QueryClient
}

/**
Expand Down Expand Up @@ -217,8 +211,7 @@ export default function App(props: AppProps) {

const { isOffline } = useOffline()
const { getText } = textProvider.useText()

const queryClient = props.queryClient
const queryClient = reactQuery.useQueryClient()

// Force all queries to be stale
// We don't use the `staleTime` option because it's not performant
Expand Down Expand Up @@ -304,8 +297,7 @@ export interface AppRouterProps extends AppProps {
* component as the component that defines the provider.
*/
function AppRouter(props: AppRouterProps) {
const { isAuthenticationDisabled, shouldShowDashboard } = props
const { onAuthenticated, projectManagerInstance } = props
const { isAuthenticationDisabled, onAuthenticated, projectManagerInstance } = props
const httpClient = useHttpClientStrict()
const logger = useLogger()
const navigate = router.useNavigate()
Expand Down Expand Up @@ -483,10 +475,7 @@ function AppRouter(props: AppRouterProps) {
<router.Route element={<SetupOrganizationAfterSubscribe />}>
<router.Route element={<InvitedToOrganizationModal />}>
<router.Route element={<openAppWatcher.OpenAppWatcher />}>
<router.Route
path={appUtils.DASHBOARD_PATH}
element={shouldShowDashboard && <Dashboard {...props} />}
/>
<router.Route path={appUtils.DASHBOARD_PATH} element={<Dashboard {...props} />} />

<router.Route
path={appUtils.SUBSCRIBE_PATH}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const IGNORE_INTERACT_OUTSIDE_ELEMENTS = [
// ReactQuery devtools
'.tsqd-parent-container',
// Our components that should ignore the interact outside event
':is(.enso-dashboard, .enso-chat, .enso-portal-root) [data-ignore-click-outside]',
':is(.enso-app, .enso-chat, .enso-portal-root) [data-ignore-click-outside]',
]

const IGNORE_INTERACT_OUTSIDE_ELEMENTS_SELECTOR = `:is(${IGNORE_INTERACT_OUTSIDE_ELEMENTS.join(', ')})`
Expand Down
Loading

0 comments on commit 10bb0ae

Please sign in to comment.