diff --git a/app/ide-desktop/eslint.config.js b/app/ide-desktop/eslint.config.js
index f3453bc427a3..9c820d608023 100644
--- a/app/ide-desktop/eslint.config.js
+++ b/app/ide-desktop/eslint.config.js
@@ -198,6 +198,10 @@ const RESTRICTED_SYNTAXES = [
'TSAsExpression:has(TSUnknownKeyword, TSNeverKeyword, TSAnyKeyword) > TSAsExpression',
message: 'Use type assertions to specific types instead of `unknown`, `any` or `never`',
},
+ {
+ selector: 'IfStatement > ExpressionStatement',
+ message: 'Wrap `if` branches in `{}`',
+ },
]
/* eslint-disable @typescript-eslint/naming-convention */
diff --git a/app/ide-desktop/lib/content/esbuild-config.ts b/app/ide-desktop/lib/content/esbuild-config.ts
index f7a620ebfd68..3db5b078676b 100644
--- a/app/ide-desktop/lib/content/esbuild-config.ts
+++ b/app/ide-desktop/lib/content/esbuild-config.ts
@@ -96,7 +96,6 @@ export function bundlerOptions(args: Arguments) {
entryPoints: [
pathModule.resolve(THIS_PATH, 'src', 'index.ts'),
pathModule.resolve(THIS_PATH, 'src', 'index.html'),
- pathModule.resolve(THIS_PATH, 'src', 'run.js'),
pathModule.resolve(THIS_PATH, 'src', 'style.css'),
pathModule.resolve(THIS_PATH, 'src', 'docsStyle.css'),
...wasmArtifacts.split(pathModule.delimiter),
@@ -108,13 +107,9 @@ export function bundlerOptions(args: Arguments) {
outbase: 'src',
plugins: [
{
- // This is a workaround that is needed
- // because esbuild panics when using `loader: { '.js': 'copy' }`.
- // See https://github.com/evanw/esbuild/issues/3041.
- // Setting `loader: 'copy'` prevents this file from being converted to ESM
- // because of the `"type": "module"` in the `package.json`.
// This file MUST be in CommonJS format because it is loaded using `Function()`
- // in `ensogl/pack/js/src/runner/index.ts`
+ // in `ensogl/pack/js/src/runner/index.ts`.
+ // All other files are ESM because of `"type": "module"` in `package.json`.
name: 'pkg-js-is-cjs',
setup: build => {
build.onLoad({ filter: /[/\\]pkg.js$/ }, async ({ path }) => ({
diff --git a/app/ide-desktop/lib/content/src/index.html b/app/ide-desktop/lib/content/src/index.html
index 65625b65e350..d4e33dfb5555 100644
--- a/app/ide-desktop/lib/content/src/index.html
+++ b/app/ide-desktop/lib/content/src/index.html
@@ -37,7 +37,6 @@
-
diff --git a/app/ide-desktop/lib/content/src/index.ts b/app/ide-desktop/lib/content/src/index.ts
index e1d0145b2787..3b5da038dcf0 100644
--- a/app/ide-desktop/lib/content/src/index.ts
+++ b/app/ide-desktop/lib/content/src/index.ts
@@ -8,8 +8,6 @@ import * as authentication from 'enso-authentication'
import * as contentConfig from 'enso-content-config'
import * as app from '../../../../../target/ensogl-pack/linked-dist/index'
-import * as projectManager from './project_manager'
-import GLOBAL_CONFIG from '../../../../gui/config.yaml' assert { type: 'yaml' }
const logger = app.log.logger
@@ -25,6 +23,8 @@ const ESBUILD_EVENT_NAME = 'change'
const SECOND = 1000
/** Time in seconds after which a `fetchTimeout` ends. */
const FETCH_TIMEOUT = 300
+/** The `id` attribute of the element that the IDE will be rendered into. */
+const IDE_ELEMENT_ID = 'root'
// ===================
// === Live reload ===
@@ -119,104 +119,132 @@ function displayDeprecatedVersionDialog() {
}
// ========================
-// === Main Entry Point ===
+// === Main entry point ===
// ========================
interface StringConfig {
[key: string]: StringConfig | string
}
-class Main {
- async main(inputConfig: StringConfig) {
- const config = Object.assign(
- {
- loader: {
- wasmUrl: 'pkg-opt.wasm',
- jsUrl: 'pkg.js',
- assetsUrl: 'dynamic-assets',
- },
- },
- inputConfig
- )
-
- const appInstance = new app.App({
- config,
- configOptions: contentConfig.OPTIONS,
- packageInfo: {
- version: BUILD_INFO.version,
- engineVersion: BUILD_INFO.engineVersion,
+// Hack to mutate `configOptions.OPTIONS`
+let currentAppInstance: app.App | null = new app.App({
+ config: {
+ loader: {
+ wasmUrl: 'pkg-opt.wasm',
+ jsUrl: 'pkg.js',
+ assetsUrl: 'dynamic-assets',
+ },
+ },
+ configOptions: contentConfig.OPTIONS,
+ packageInfo: {
+ version: BUILD_INFO.version,
+ engineVersion: BUILD_INFO.engineVersion,
+ },
+})
+
+function tryStopProject() {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
+ currentAppInstance?.wasm?.drop?.()
+}
+
+async function runProject(inputConfig?: StringConfig) {
+ tryStopProject()
+ const rootElement = document.getElementById(IDE_ELEMENT_ID)
+ if (!rootElement) {
+ logger.error(`The root element (the element with ID '${IDE_ELEMENT_ID}') was not found.`)
+ } else {
+ while (rootElement.firstChild) {
+ rootElement.removeChild(rootElement.firstChild)
+ }
+ }
+
+ const config = Object.assign(
+ {
+ loader: {
+ wasmUrl: 'pkg-opt.wasm',
+ jsUrl: 'pkg.js',
+ assetsUrl: 'dynamic-assets',
},
- })
+ },
+ inputConfig
+ )
- if (appInstance.initialized) {
- if (contentConfig.OPTIONS.options.dataCollection.value) {
- // TODO: Add remote-logging here.
- }
- if (!(await checkMinSupportedVersion(contentConfig.OPTIONS))) {
- displayDeprecatedVersionDialog()
- } else {
- if (
- (contentConfig.OPTIONS.options.authentication.value ||
- contentConfig.OPTIONS.groups.featurePreview.options.newDashboard.value) &&
- contentConfig.OPTIONS.groups.startup.options.entry.value ===
- contentConfig.OPTIONS.groups.startup.options.entry.default
- ) {
- const hideAuth = () => {
- const auth = document.getElementById('dashboard')
- const ide = document.getElementById('root')
- if (auth) auth.style.display = 'none'
- if (ide) ide.style.display = ''
- }
- /** This package is an Electron desktop app (i.e., not in the Cloud), so
- * we're running on the desktop. */
- /** TODO [NP]: https://github.com/enso-org/cloud-v2/issues/345
- * `content` and `dashboard` packages **MUST BE MERGED INTO ONE**. The IDE
- * should only have one entry point. Right now, we have two. One for the cloud
- * and one for the desktop. Once these are merged, we can't hardcode the
- * platform here, and need to detect it from the environment. */
- const platform = authentication.Platform.desktop
- /** FIXME [PB]: https://github.com/enso-org/cloud-v2/issues/366
- * React hooks rerender themselves multiple times. It is resulting in multiple
- * Enso main scene being initialized. As a temporary workaround we check whether
- * appInstance was already ran. Target solution should move running appInstance
- * where it will be called only once. */
- let appInstanceRan = false
- const onAuthenticated = () => {
- if (
- !contentConfig.OPTIONS.groups.featurePreview.options.newDashboard.value
- ) {
- hideAuth()
- if (!appInstanceRan) {
- appInstanceRan = true
- void appInstance.run()
- }
- }
- }
- authentication.run({
- logger,
- platform,
- projectManager: projectManager.ProjectManager.default(),
- showDashboard:
- contentConfig.OPTIONS.groups.featurePreview.options.newDashboard.value,
- onAuthenticated,
- })
- } else {
- void appInstance.run()
- }
- const email = contentConfig.OPTIONS.groups.authentication.options.email.value
- // The default value is `""`, so a truthiness check is most appropriate here.
- if (email) {
- logger.log(`User identified as '${email}'.`)
- }
- }
+ currentAppInstance = new app.App({
+ config,
+ configOptions: contentConfig.OPTIONS,
+ packageInfo: {
+ version: BUILD_INFO.version,
+ engineVersion: BUILD_INFO.engineVersion,
+ },
+ })
+ console.log('bruh', currentAppInstance)
+
+ if (!currentAppInstance.initialized) {
+ console.error('Failed to initialize the application.')
+ } else {
+ if (contentConfig.OPTIONS.options.dataCollection.value) {
+ // TODO: Add remote-logging here.
+ }
+ if (!(await checkMinSupportedVersion(contentConfig.OPTIONS))) {
+ displayDeprecatedVersionDialog()
} else {
- console.error('Failed to initialize the application.')
+ const email = contentConfig.OPTIONS.groups.authentication.options.email.value
+ // The default value is `""`, so a truthiness check is most appropriate here.
+ if (email) {
+ logger.log(`User identified as '${email}'.`)
+ }
+ void currentAppInstance.run()
}
}
}
-const API = new Main()
-
-// @ts-expect-error `globalConfig.windowAppScopeName` is not known at typecheck time.
-// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
-window[GLOBAL_CONFIG.windowAppScopeName] = API
+if (
+ (contentConfig.OPTIONS.options.authentication.value ||
+ contentConfig.OPTIONS.groups.featurePreview.options.newDashboard.value) &&
+ contentConfig.OPTIONS.groups.startup.options.entry.value ===
+ contentConfig.OPTIONS.groups.startup.options.entry.default
+) {
+ window.tryStopProject = tryStopProject
+ window.runProject = runProject
+ const hideAuth = () => {
+ const auth = document.getElementById('dashboard')
+ const ide = document.getElementById('root')
+ if (auth) {
+ auth.style.display = 'none'
+ }
+ if (ide) {
+ ide.hidden = false
+ }
+ }
+ /** This package is an Electron desktop app (i.e., not in the Cloud), so
+ * we're running on the desktop. */
+ /** TODO [NP]: https://github.com/enso-org/cloud-v2/issues/345
+ * `content` and `dashboard` packages **MUST BE MERGED INTO ONE**. The IDE
+ * should only have one entry point. Right now, we have two. One for the cloud
+ * and one for the desktop. Once these are merged, we can't hardcode the
+ * platform here, and need to detect it from the environment. */
+ const platform = authentication.Platform.desktop
+ /** FIXME [PB]: https://github.com/enso-org/cloud-v2/issues/366
+ * React hooks rerender themselves multiple times. It is resulting in multiple
+ * Enso main scene being initialized. As a temporary workaround we check whether
+ * appInstance was already ran. Target solution should move running appInstance
+ * where it will be called only once. */
+ let appInstanceRan = false
+ const onAuthenticated = () => {
+ if (!contentConfig.OPTIONS.groups.featurePreview.options.newDashboard.value) {
+ hideAuth()
+ if (!appInstanceRan) {
+ appInstanceRan = true
+ void runProject()
+ }
+ }
+ }
+ authentication.run({
+ logger,
+ platform,
+ showDashboard: contentConfig.OPTIONS.groups.featurePreview.options.newDashboard.value,
+ onAuthenticated,
+ })
+} else {
+ void runProject()
+}
diff --git a/app/ide-desktop/lib/content/src/newtype.ts b/app/ide-desktop/lib/content/src/newtype.ts
deleted file mode 100644
index b3a85ec61518..000000000000
--- a/app/ide-desktop/lib/content/src/newtype.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/** @file TypeScript's closest equivalent of `newtype`s. */
-
-interface NewtypeVariant {
- // eslint-disable-next-line @typescript-eslint/naming-convention
- _$type: TypeName
-}
-
-/** Used to create a "branded type",
- * which contains a property that only exists at compile time.
- *
- * `Newtype` and `Newtype` are not compatible with each other,
- * however both are regular `string`s at runtime.
- *
- * This is useful in parameters that require values from a certain source,
- * for example IDs for a specific object type.
- *
- * It is similar to a `newtype` in other languages.
- * Note however because TypeScript is structurally typed,
- * a branded type is assignable to its base type:
- * `a: string = asNewtype>(b)` successfully typechecks. */
-export type Newtype = NewtypeVariant & T
-
-interface NotNewtype {
- // eslint-disable-next-line @typescript-eslint/naming-convention
- _$type?: never
-}
-
-export function asNewtype>(
- s: NotNewtype & Omit
-): T {
- // This cast is unsafe.
- // `T` has an extra property `_$type` which is used purely for typechecking
- // and does not exist at runtime.
- //
- // The property name is specifically chosen to trigger eslint's `naming-convention` lint,
- // so it should not be possible to accidentally create a value with such a type.
- // eslint-disable-next-line no-restricted-syntax
- return s as unknown as T
-}
diff --git a/app/ide-desktop/lib/content/src/project_manager.ts b/app/ide-desktop/lib/content/src/project_manager.ts
deleted file mode 100644
index 58777d0551db..000000000000
--- a/app/ide-desktop/lib/content/src/project_manager.ts
+++ /dev/null
@@ -1,166 +0,0 @@
-/** @file This module defines the Project Manager endpoint. */
-import * as newtype from './newtype'
-
-const PROJECT_MANAGER_ENDPOINT = 'ws://127.0.0.1:30535'
-
-// =============
-// === Types ===
-// =============
-
-export enum MissingComponentAction {
- fail = 'Fail',
- install = 'Install',
- forceInstallBroken = 'ForceInstallBroken',
-}
-
-interface Result {
- result: T
-}
-
-// This intentionally has the same brand as in the cloud backend API.
-export type ProjectId = newtype.Newtype
-export type ProjectName = newtype.Newtype
-export type UTCDateTime = newtype.Newtype
-
-interface ProjectMetadata {
- name: ProjectName
- namespace: string
- id: ProjectId
- engineVersion: string | null
- lastOpened: UTCDateTime | null
-}
-
-interface IpWithSocket {
- host: string
- port: number
-}
-
-interface ProjectList {
- projects: ProjectMetadata[]
-}
-
-interface CreateProject {
- projectId: ProjectId
-}
-
-interface OpenProject {
- engineVersion: string
- languageServerJsonAddress: IpWithSocket
- languageServerBinaryAddress: IpWithSocket
- projectName: ProjectName
- projectNamespace: string
-}
-
-// ================================
-// === Parameters for endpoints ===
-// ================================
-
-export interface OpenProjectParams {
- projectId: ProjectId
- missingComponentAction: MissingComponentAction
-}
-
-export interface CloseProjectParams {
- projectId: ProjectId
-}
-
-export interface ListProjectsParams {
- numberOfProjects?: number
-}
-
-export interface CreateProjectParams {
- name: ProjectName
- projectTemplate?: string
- version?: string
- missingComponentAction?: MissingComponentAction
-}
-
-export interface RenameProjectParams {
- projectId: ProjectId
- name: ProjectName
-}
-
-export interface DeleteProjectParams {
- projectId: ProjectId
-}
-
-export interface ListSamplesParams {
- projectId: ProjectId
-}
-
-// =======================
-// === Project Manager ===
-// =======================
-
-/** A WebSocket endpoint to the project manager. */
-export class ProjectManager {
- constructor(protected readonly connectionUrl: string) {}
-
- static default() {
- return new ProjectManager(PROJECT_MANAGER_ENDPOINT)
- }
-
- public async sendRequest(method: string, params: unknown): Promise> {
- const req = {
- jsonrpc: '2.0',
- id: 0,
- method,
- params,
- }
-
- const ws = new WebSocket(this.connectionUrl)
- return new Promise>((resolve, reject) => {
- ws.onopen = () => {
- ws.send(JSON.stringify(req))
- }
- ws.onmessage = event => {
- // There is no way to avoid this; `JSON.parse` returns `any`.
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
- resolve(JSON.parse(event.data))
- }
- ws.onerror = error => {
- reject(error)
- }
- }).finally(() => {
- ws.close()
- })
- }
-
- /** * Open an existing project. */
- public async openProject(params: OpenProjectParams): Promise> {
- return this.sendRequest('project/open', params)
- }
-
- /** * Close an open project. */
- public async closeProject(params: CloseProjectParams): Promise> {
- return this.sendRequest('project/close', params)
- }
-
- /** * Get the projects list, sorted by open time. */
- public async listProjects(params: ListProjectsParams): Promise> {
- return this.sendRequest('project/list', params)
- }
-
- /** * Create a new project. */
- public async createProject(params: CreateProjectParams): Promise> {
- return this.sendRequest('project/create', {
- missingComponentAction: MissingComponentAction.install,
- ...params,
- })
- }
-
- /** * Rename a project. */
- public async renameProject(params: RenameProjectParams): Promise> {
- return this.sendRequest('project/rename', params)
- }
-
- /** * Delete a project. */
- public async deleteProject(params: DeleteProjectParams): Promise> {
- return this.sendRequest('project/delete', params)
- }
-
- /** * Get the list of sample projects that are available to the user. */
- public async listSamples(params: ListSamplesParams): Promise> {
- return this.sendRequest('project/listSample', params)
- }
-}
diff --git a/app/ide-desktop/lib/content/src/run.js b/app/ide-desktop/lib/content/src/run.js
deleted file mode 100644
index 91a503a25ef6..000000000000
--- a/app/ide-desktop/lib/content/src/run.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/** @file This file is used to simply run the IDE. It can be not invoked if the IDE needs to be used
- * as a library. */
-
-void window.enso.main()
diff --git a/app/ide-desktop/lib/content/src/style.css b/app/ide-desktop/lib/content/src/style.css
index 6e0fcf478620..e8afd0d5b252 100644
--- a/app/ide-desktop/lib/content/src/style.css
+++ b/app/ide-desktop/lib/content/src/style.css
@@ -73,11 +73,6 @@
/* End of fonts */
-html,
-body {
- height: 100vh;
-}
-
body {
margin: 0;
overscroll-behavior: none;
diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx
index a6f5bac986bd..87e63b82d123 100644
--- a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx
+++ b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx
@@ -9,7 +9,8 @@ import toast from 'react-hot-toast'
import * as app from '../../components/app'
import * as authServiceModule from '../service'
-import * as backendService from '../../dashboard/service'
+import * as backendProvider from '../../providers/backend'
+import * as cloudService from '../../dashboard/cloudService'
import * as errorModule from '../../error'
import * as loggerProvider from '../../providers/logger'
import * as newtype from '../../newtype'
@@ -49,7 +50,7 @@ export interface FullUserSession {
/** User's email address. */
email: string
/** User's organization information. */
- organization: backendService.UserOrOrganization
+ organization: cloudService.UserOrOrganization
}
/** Object containing the currently signed-in user's session data, if the user has not yet set their
@@ -138,6 +139,7 @@ export function AuthProvider(props: AuthProviderProps) {
const { authService, children } = props
const { cognito } = authService
const { session } = sessionProvider.useSession()
+ const { setBackend } = backendProvider.useSetBackend()
const logger = loggerProvider.useLogger()
const navigate = router.useNavigate()
const onAuthenticated = react.useCallback(props.onAuthenticated, [])
@@ -157,7 +159,8 @@ export function AuthProvider(props: AuthProviderProps) {
} else {
const { accessToken, email } = session.val
- const backend = backendService.createBackend(accessToken, logger)
+ const backend = cloudService.createBackend(accessToken, logger)
+ setBackend(backend)
const organization = await backend.usersMe()
let newUserSession: UserSession
if (!organization) {
@@ -253,11 +256,11 @@ export function AuthProvider(props: AuthProviderProps) {
/** TODO [NP]: https://github.com/enso-org/cloud-v2/issues/343
* The API client is reinitialised on every request. That is an inefficient way of usage.
* Fix it by using React context and implementing it as a singleton. */
- const backend = backendService.createBackend(accessToken, logger)
+ const backend = cloudService.createBackend(accessToken, logger)
await backend.createUser({
userName: username,
- userEmail: newtype.asNewtype(email),
+ userEmail: newtype.asNewtype(email),
})
navigate(app.DASHBOARD_PATH)
toast.success(MESSAGES.setUsernameSuccess)
diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/components/app.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/components/app.tsx
index 279cd22ee9ff..bfc148108d13 100644
--- a/app/ide-desktop/lib/dashboard/src/authentication/src/components/app.tsx
+++ b/app/ide-desktop/lib/dashboard/src/authentication/src/components/app.tsx
@@ -38,12 +38,13 @@ import * as react from 'react'
import * as router from 'react-router-dom'
import * as toast from 'react-hot-toast'
-import * as projectManagerModule from 'enso-content/src/project_manager'
+import * as app from '../../../../../../../../target/ensogl-pack/linked-dist/index'
import * as authService from '../authentication/service'
import * as platformModule from '../platform'
import * as authProvider from '../authentication/providers/auth'
+import * as backendProvider from '../providers/backend'
import * as loggerProvider from '../providers/logger'
import * as modalProvider from '../providers/modal'
import * as sessionProvider from '../authentication/providers/session'
@@ -79,26 +80,16 @@ export const SET_USERNAME_PATH = '/set-username'
// === App ===
// ===========
-interface BaseAppProps {
+/** Global configuration for the `App` component. */
+export interface AppProps {
logger: loggerProvider.Logger
platform: platformModule.Platform
/** Whether the dashboard should be rendered. */
showDashboard: boolean
+ ide?: app.App
onAuthenticated: () => void
}
-interface DesktopAppProps extends BaseAppProps {
- platform: platformModule.Platform.desktop
- projectManager: projectManagerModule.ProjectManager
-}
-
-interface OtherAppProps extends BaseAppProps {
- platform: Exclude
-}
-
-/** Global configuration for the `App` component. */
-export type AppProps = DesktopAppProps | OtherAppProps
-
/** Component called by the parent module, returning the root React component for this
* package.
*
@@ -171,12 +162,15 @@ function AppRouter(props: AppProps) {
userSession={userSession}
registerAuthEventListener={registerAuthEventListener}
>
-
- {routes}
-
+ {/* @ts-expect-error Auth will always set this before dashboard is rendered. */}
+
+
+ {routes}
+
+
)
diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/components/svg.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/components/svg.tsx
index c1ecda9868db..a00b416e20e9 100644
--- a/app/ide-desktop/lib/dashboard/src/authentication/src/components/svg.tsx
+++ b/app/ide-desktop/lib/dashboard/src/authentication/src/components/svg.tsx
@@ -235,6 +235,71 @@ export const CLOSE_ICON = (
)
+export const CLOUD_ICON = (
+
+)
+
+export const COMPUTER_ICON = (
+
+)
+
+export interface StopIconProps {
+ className?: string
+}
+
+/** Icon displayed when a project is ready to stop. */
+export function StopIcon(props: StopIconProps) {
+ const { className } = props
+ return (
+
+ )
+}
+
// ===========
// === Svg ===
// ===========
diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/service.ts b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/cloudService.ts
similarity index 95%
rename from app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/service.ts
rename to app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/cloudService.ts
index e0727a5c75f6..e7fa1a07c28d 100644
--- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/service.ts
+++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/cloudService.ts
@@ -158,9 +158,15 @@ export interface CreatedProject extends BaseProject {
packageName: string
}
+/** A `Project` returned by the `listProjects` endpoint. */
+export interface ListedProjectRaw extends CreatedProject {
+ address: Address | null
+}
+
/** A `Project` returned by `listProjects`. */
export interface ListedProject extends CreatedProject {
- address: Address | null
+ binaryAddress: Address | null
+ jsonAddress: Address | null
}
/** A `Project` returned by `updateProject`. */
@@ -170,6 +176,12 @@ export interface UpdatedProject extends BaseProject {
engineVersion: VersionNumber | null
}
+/** A user/organization's project containing and/or currently executing code. */
+export interface ProjectRaw extends ListedProjectRaw {
+ ideVersion: VersionNumber | null
+ engineVersion: VersionNumber | null
+}
+
/** A user/organization's project containing and/or currently executing code. */
export interface Project extends ListedProject {
ideVersion: VersionNumber | null
@@ -417,7 +429,7 @@ interface ListDirectoryResponseBody {
/** HTTP response body for the "list projects" endpoint. */
interface ListProjectsResponseBody {
- projects: ListedProject[]
+ projects: ListedProjectRaw[]
}
/** HTTP response body for the "list files" endpoint. */
@@ -542,7 +554,17 @@ export class Backend {
if (response.status !== STATUS_OK) {
return this.throw('Unable to list projects.')
} else {
- return (await response.json()).projects
+ return (await response.json()).projects.map(project => ({
+ ...project,
+ jsonAddress:
+ project.address != null
+ ? newtype.asNewtype(`${project.address}json`)
+ : null,
+ binaryAddress:
+ project.address != null
+ ? newtype.asNewtype(`${project.address}binary`)
+ : null,
+ }))
}
}
@@ -574,11 +596,22 @@ export class Backend {
*
* @throws An error if a 401 or 404 status code was received. */
async getProjectDetails(projectId: ProjectId): Promise {
- const response = await this.get(getProjectDetailsPath(projectId))
+ const response = await this.get(getProjectDetailsPath(projectId))
if (response.status !== STATUS_OK) {
return this.throw(`Unable to get details of project with ID '${projectId}'.`)
} else {
- return await response.json()
+ const project = await response.json()
+ return {
+ ...project,
+ jsonAddress:
+ project.address != null
+ ? newtype.asNewtype(`${project.address}json`)
+ : null,
+ binaryAddress:
+ project.address != null
+ ? newtype.asNewtype(`${project.address}binary`)
+ : null,
+ }
}
}
diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx
index 9619f1b9b31a..a87f55139f44 100644
--- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx
+++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx
@@ -2,19 +2,20 @@
* interactive components. */
import * as react from 'react'
-import * as projectManagerModule from 'enso-content/src/project_manager'
-
-import * as auth from '../../authentication/providers/auth'
-import * as backend from '../service'
+import * as cloudService from '../cloudService'
import * as fileInfo from '../../fileInfo'
import * as hooks from '../../hooks'
-import * as loggerProvider from '../../providers/logger'
-import * as modalProvider from '../../providers/modal'
+import * as localService from '../localService'
import * as newtype from '../../newtype'
import * as platformModule from '../../platform'
import * as svg from '../../components/svg'
import * as uploadMultipleFiles from '../../uploadMultipleFiles'
+import * as auth from '../../authentication/providers/auth'
+import * as backendProvider from '../../providers/backend'
+import * as loggerProvider from '../../providers/logger'
+import * as modalProvider from '../../providers/modal'
+
import PermissionDisplay, * as permissionDisplay from './permissionDisplay'
import ContextMenu from './contextMenu'
import ContextMenuEntry from './contextMenuEntry'
@@ -73,8 +74,8 @@ enum Column {
export interface CreateFormProps {
left: number
top: number
- backend: backend.Backend
- directoryId: backend.DirectoryId
+ backend: cloudService.Backend
+ directoryId: cloudService.DirectoryId
onSuccess: () => void
}
@@ -89,23 +90,28 @@ export interface CreateFormProps {
// eslint-disable-next-line @typescript-eslint/no-inferrable-types
const EXPERIMENTAL: boolean = true
+/** The `id` attribute of the element into which the IDE will be rendered. */
+const IDE_ELEMENT_ID = 'root'
/** The `localStorage` key under which the ID of the current directory is stored. */
const DIRECTORY_STACK_KEY = 'enso-dashboard-directory-stack'
/** English names for the name column. */
-const ASSET_TYPE_NAME: Record = {
- [backend.AssetType.project]: 'Projects',
- [backend.AssetType.file]: 'Files',
- [backend.AssetType.secret]: 'Secrets',
- [backend.AssetType.directory]: 'Folders',
+const ASSET_TYPE_NAME: Record = {
+ [cloudService.AssetType.project]: 'Projects',
+ [cloudService.AssetType.file]: 'Files',
+ [cloudService.AssetType.secret]: 'Secrets',
+ [cloudService.AssetType.directory]: 'Folders',
} as const
/** Forms to create each asset type. */
-const ASSET_TYPE_CREATE_FORM: Record JSX.Element> = {
- [backend.AssetType.project]: ProjectCreateForm,
- [backend.AssetType.file]: FileCreateForm,
- [backend.AssetType.secret]: SecretCreateForm,
- [backend.AssetType.directory]: DirectoryCreateForm,
+const ASSET_TYPE_CREATE_FORM: Record<
+ cloudService.AssetType,
+ (props: CreateFormProps) => JSX.Element
+> = {
+ [cloudService.AssetType.project]: ProjectCreateForm,
+ [cloudService.AssetType.file]: FileCreateForm,
+ [cloudService.AssetType.secret]: SecretCreateForm,
+ [cloudService.AssetType.directory]: DirectoryCreateForm,
}
/** English names for every column except for the name column. */
@@ -121,23 +127,23 @@ const COLUMN_NAME: Record, string> = {
} as const
/** The corresponding `Permissions` for each backend `PermissionAction`. */
-const PERMISSION: Record = {
- [backend.PermissionAction.own]: { type: permissionDisplay.Permission.owner },
- [backend.PermissionAction.execute]: {
+const PERMISSION: Record = {
+ [cloudService.PermissionAction.own]: { type: permissionDisplay.Permission.owner },
+ [cloudService.PermissionAction.execute]: {
type: permissionDisplay.Permission.regular,
read: false,
write: false,
docsWrite: false,
exec: true,
},
- [backend.PermissionAction.edit]: {
+ [cloudService.PermissionAction.edit]: {
type: permissionDisplay.Permission.regular,
read: false,
write: true,
docsWrite: false,
exec: false,
},
- [backend.PermissionAction.read]: {
+ [cloudService.PermissionAction.read]: {
type: permissionDisplay.Permission.regular,
read: true,
write: false,
@@ -181,100 +187,128 @@ const COLUMNS_FOR: Record = {
// ========================
/** Returns the id of the root directory for a user or organization. */
-function rootDirectoryId(userOrOrganizationId: backend.UserOrOrganizationId) {
- return newtype.asNewtype(
- userOrOrganizationId.replace(/^organization-/, `${backend.AssetType.directory}-`)
+function rootDirectoryId(userOrOrganizationId: cloudService.UserOrOrganizationId) {
+ return newtype.asNewtype(
+ userOrOrganizationId.replace(/^organization-/, `${cloudService.AssetType.directory}-`)
)
}
+// FIXME[sb]: While this works, throwing a runtime error can be avoided
+// if types are properly narrowed, e.g. using a type guard instead.
+function asCloudBackend(
+ backend: cloudService.Backend | localService.Backend
+): cloudService.Backend {
+ if (!('checkResources' in backend)) {
+ throw new Error('This functionality only works with the cloud backend.')
+ } else {
+ return backend
+ }
+}
+
// =================
// === Dashboard ===
// =================
-interface BaseDashboardProps {
- logger: loggerProvider.Logger
+export interface DashboardProps {
platform: platformModule.Platform
}
-interface DesktopDashboardProps extends BaseDashboardProps {
- platform: platformModule.Platform.desktop
- projectManager: projectManagerModule.ProjectManager
-}
-
-interface OtherDashboardProps extends BaseDashboardProps {
- platform: Exclude
-}
-
-export type DashboardProps = DesktopDashboardProps | OtherDashboardProps
-
// TODO[sb]: Implement rename when clicking name of a selected row.
// There is currently no way to tell whether a row is selected from a column.
function Dashboard(props: DashboardProps) {
- const { logger, platform } = props
+ const { platform } = props
+ const logger = loggerProvider.useLogger()
const { accessToken, organization } = auth.useFullUserSession()
- const backendService = backend.createBackend(accessToken, logger)
+ const { backend } = backendProvider.useBackend()
+ const { setBackend } = backendProvider.useSetBackend()
const { modal } = modalProvider.useModal()
const { setModal, unsetModal } = modalProvider.useSetModal()
+ const [backendPlatform, setBackendPlatform] = react.useState(platformModule.Platform.cloud)
const [refresh, doRefresh] = hooks.useRefresh()
const [query, setQuery] = react.useState('')
const [directoryId, setDirectoryId] = react.useState(rootDirectoryId(organization.id))
const [directoryStack, setDirectoryStack] = react.useState<
- backend.Asset[]
+ cloudService.Asset[]
>([])
// Defined by the spec as `compact` by default, however it is not ready yet.
const [columnDisplayMode, setColumnDisplayMode] = react.useState(ColumnDisplayMode.release)
+ const [tab, setTab] = react.useState(Tab.dashboard)
+ const [project, setProject] = react.useState(null)
+ const [selectedAssets, setSelectedAssets] = react.useState([])
+ const [isFileBeingDragged, setIsFileBeingDragged] = react.useState(false)
const [projectAssets, setProjectAssetsRaw] = react.useState<
- backend.Asset[]
+ cloudService.Asset[]
>([])
const [directoryAssets, setDirectoryAssetsRaw] = react.useState<
- backend.Asset[]
+ cloudService.Asset[]
>([])
const [secretAssets, setSecretAssetsRaw] = react.useState<
- backend.Asset[]
+ cloudService.Asset[]
+ >([])
+ const [fileAssets, setFileAssetsRaw] = react.useState<
+ cloudService.Asset[]
>([])
- const [fileAssets, setFileAssetsRaw] = react.useState[]>(
- []
- )
const [visibleProjectAssets, setVisibleProjectAssets] = react.useState<
- backend.Asset[]
+ cloudService.Asset[]
>([])
const [visibleDirectoryAssets, setVisibleDirectoryAssets] = react.useState<
- backend.Asset[]
+ cloudService.Asset[]
>([])
const [visibleSecretAssets, setVisibleSecretAssets] = react.useState<
- backend.Asset[]
+ cloudService.Asset[]
>([])
const [visibleFileAssets, setVisibleFileAssets] = react.useState<
- backend.Asset[]
+ cloudService.Asset[]
>([])
- const [tab, setTab] = react.useState(Tab.dashboard)
- const [project, setProject] = react.useState(null)
-
- const [selectedAssets, setSelectedAssets] = react.useState([])
- const [isFileBeingDragged, setIsFileBeingDragged] = react.useState(false)
-
const directory = directoryStack[directoryStack.length - 1]
const parentDirectory = directoryStack[directoryStack.length - 2]
- function setProjectAssets(newProjectAssets: backend.Asset[]) {
+ react.useEffect(() => {
+ function onKeyDown(event: KeyboardEvent) {
+ if (
+ // On macOS, we need to check for combination of `alt` + `d` which is `∂` (`del`).
+ (event.key === 'd' || event.key === '∂') &&
+ event.ctrlKey &&
+ event.altKey &&
+ !event.shiftKey &&
+ !event.metaKey
+ ) {
+ setTab(Tab.dashboard)
+ const ideElement = document.getElementById(IDE_ELEMENT_ID)
+ if (ideElement) {
+ ideElement.hidden = true
+ }
+ }
+ }
+ document.addEventListener('keydown', onKeyDown)
+ return () => {
+ document.removeEventListener('keydown', onKeyDown)
+ }
+ }, [])
+
+ function setProjectAssets(
+ newProjectAssets: cloudService.Asset[]
+ ) {
setProjectAssetsRaw(newProjectAssets)
setVisibleProjectAssets(newProjectAssets.filter(asset => asset.title.includes(query)))
}
- function setDirectoryAssets(newDirectoryAssets: backend.Asset[]) {
+ function setDirectoryAssets(
+ newDirectoryAssets: cloudService.Asset[]
+ ) {
setDirectoryAssetsRaw(newDirectoryAssets)
setVisibleDirectoryAssets(newDirectoryAssets.filter(asset => asset.title.includes(query)))
}
- function setSecretAssets(newSecretAssets: backend.Asset[]) {
+ function setSecretAssets(newSecretAssets: cloudService.Asset[]) {
setSecretAssetsRaw(newSecretAssets)
setVisibleSecretAssets(newSecretAssets.filter(asset => asset.title.includes(query)))
}
- function setFileAssets(newFileAssets: backend.Asset[]) {
+ function setFileAssets(newFileAssets: cloudService.Asset[]) {
setFileAssetsRaw(newFileAssets)
setVisibleFileAssets(newFileAssets.filter(asset => asset.title.includes(query)))
}
@@ -287,7 +321,7 @@ function Dashboard(props: DashboardProps) {
)
}
- function enterDirectory(directoryAsset: backend.Asset) {
+ function enterDirectory(directoryAsset: cloudService.Asset) {
setDirectoryId(directoryAsset.id)
setDirectoryStack([...directoryStack, directoryAsset])
}
@@ -297,7 +331,7 @@ function Dashboard(props: DashboardProps) {
if (cachedDirectoryStackJson) {
// The JSON was inserted by the code below, so it will always have the right type.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
- const cachedDirectoryStack: backend.Asset[] =
+ const cachedDirectoryStack: cloudService.Asset[] =
JSON.parse(cachedDirectoryStackJson)
setDirectoryStack(cachedDirectoryStack)
const cachedDirectoryId = cachedDirectoryStack[cachedDirectoryStack.length - 1]?.id
@@ -317,9 +351,9 @@ function Dashboard(props: DashboardProps) {
/** React components for the name column. */
const nameRenderers: {
- [Type in backend.AssetType]: (asset: backend.Asset) => JSX.Element
+ [Type in cloudService.AssetType]: (asset: cloudService.Asset) => JSX.Element
} = {
- [backend.AssetType.project]: projectAsset => (
+ [cloudService.AssetType.project]: projectAsset => (