Skip to content

Commit

Permalink
Merge branch 'develop' into wip/radeusgd/snowflake-oauth-prototype
Browse files Browse the repository at this point in the history
  • Loading branch information
radeusgd committed Jan 16, 2025
2 parents dbb802d + d2c279f commit 54f6cca
Show file tree
Hide file tree
Showing 32 changed files with 887 additions and 1,128 deletions.
1 change: 0 additions & 1 deletion .github/workflows/gui-changed-files.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ jobs:
.github/workflows/gui*
.github/workflows/storybook.yml
files_ignore: |
app/ide-desktop/**
app/gui/scripts/**
app/gui/.gitignore
.git-*
Expand Down
6 changes: 1 addition & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,7 @@ project/metals.sbt
## Build Cache ##
#################

bazel-bin
bazel-bazel
bazel-enso
bazel-out
bazel-testlogs
bazel-*
build-cache

##################
Expand Down
24 changes: 20 additions & 4 deletions app/common/src/services/Backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,25 @@ export interface User extends UserInfo {
readonly isOrganizationAdmin: boolean
readonly rootDirectoryId: DirectoryId
readonly profilePicture?: HttpsUrl
/**
* Contains the IDs of the user groups that the user is a member of.
* @deprecated Use `groups` instead.
*/
readonly userGroups: readonly UserGroupId[] | null
readonly removeAt?: dateTime.Rfc3339DateTime | null
readonly plan?: Plan | undefined
/**
* Contains the user groups that the user is a member of.
* Has enriched metadata, like the name of the group and the home directory ID.
*/
readonly groups?: readonly UserGroup[]
}

/** A user related to the current user. */
export interface UserGroup {
readonly id: UserGroupId
readonly name: string
readonly homeDirectoryId: DirectoryId
}

/** A `Directory` returned by `createDirectory`. */
Expand Down Expand Up @@ -488,7 +504,7 @@ export interface UserPermission {

/** User permission for a specific user group. */
export interface UserGroupPermission {
readonly userGroup: UserGroupInfo
readonly userGroup: UserGroup
readonly permission: permissions.PermissionAction
}

Expand Down Expand Up @@ -544,7 +560,7 @@ export function isUserGroupPermissionAnd(predicate: (permission: UserGroupPermis

/** Get the property representing the name on an arbitrary variant of {@link UserPermission}. */
export function getAssetPermissionName(permission: AssetPermission) {
return isUserPermission(permission) ? permission.user.name : permission.userGroup.groupName
return isUserPermission(permission) ? permission.user.name : permission.userGroup.name
}

/** Get the property representing the id on an arbitrary variant of {@link UserPermission}. */
Expand Down Expand Up @@ -1170,8 +1186,8 @@ export function compareAssetPermissions(a: AssetPermission, b: AssetPermission)
} else {
// NOTE [NP]: Although `userId` is unique, and therefore sufficient to sort permissions, sort
// name first, so that it's easier to find a permission in a long list (i.e., for readability).
const aName = 'user' in a ? a.user.name : a.userGroup.groupName
const bName = 'user' in b ? b.user.name : b.userGroup.groupName
const aName = 'user' in a ? a.user.name : a.userGroup.name
const bName = 'user' in b ? b.user.name : b.userGroup.name
const aUserId = 'user' in a ? a.user.userId : a.userGroup.id
const bUserId = 'user' in b ? b.user.userId : b.userGroup.id
return (
Expand Down
7 changes: 5 additions & 2 deletions app/gui/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import url from 'node:url'

const DEBUG = process.env.DEBUG_TEST === 'true'
const isCI = process.env.CI === 'true'

const isProd = process.env.PROD === 'true'
const TIMEOUT_MS = DEBUG ? 100_000_000 : 25_000

// We tend to use less CPU on CI to reduce the number of failures due to timeouts.
Expand Down Expand Up @@ -162,7 +162,10 @@ export default defineConfig({
reuseExistingServer: false,
},
{
command: `corepack pnpm exec vite -c vite.test.config.ts build && vite -c vite.test.config.ts preview --port ${ports.dashboard} --strictPort`,
command:
isCI || isProd ?
`corepack pnpm exec vite -c vite.test.config.ts build && vite -c vite.test.config.ts preview --port ${ports.dashboard} --strictPort`
: `NODE_ENV=test corepack pnpm exec vite -c vite.test.config.ts --port ${ports.dashboard}`,
timeout: 240 * 1000,
port: ports.dashboard,
reuseExistingServer: false,
Expand Down
66 changes: 35 additions & 31 deletions app/gui/project-manager-shim-middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import * as path from 'node:path'
import * as tar from 'tar'
import * as yaml from 'yaml'

import { COOP_COEP_CORP_HEADERS } from 'enso-common'
import GLOBAL_CONFIG from 'enso-common/src/config.json' with { type: 'json' }

import * as projectManagement from './projectManagement'
Expand All @@ -24,6 +23,11 @@ const HTTP_STATUS_BAD_REQUEST = 400
const HTTP_STATUS_NOT_FOUND = 404
const PROJECTS_ROOT_DIRECTORY = projectManagement.getProjectsDirectory()

const COMMON_HEADERS = {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Resource-Policy': 'same-origin',
}

// =============
// === Types ===
// =============
Expand Down Expand Up @@ -139,24 +143,24 @@ export default function projectManagerShimMiddleware(
const directory = url.searchParams.get('directory') ?? PROJECTS_ROOT_DIRECTORY
if (fileName == null) {
response
.writeHead(HTTP_STATUS_BAD_REQUEST, COOP_COEP_CORP_HEADERS)
.writeHead(HTTP_STATUS_BAD_REQUEST, COMMON_HEADERS)
.end('Request is missing search parameter `file_name`.')
} else {
const filePath = path.join(directory, fileName)
void fs
.writeFile(filePath, request)
.then(() => {
response
.writeHead(HTTP_STATUS_OK, [
['Content-Length', String(filePath.length)],
['Content-Type', 'text/plain'],
...COOP_COEP_CORP_HEADERS,
])
.writeHead(HTTP_STATUS_OK, {
'Content-Length': String(filePath.length),
'Content-Type': 'text/plain',
...COMMON_HEADERS,
})
.end(filePath)
})
.catch((e) => {
console.error(e)
response.writeHead(HTTP_STATUS_BAD_REQUEST, COOP_COEP_CORP_HEADERS).end()
response.writeHead(HTTP_STATUS_BAD_REQUEST, COMMON_HEADERS).end()
})
}
break
Expand All @@ -172,15 +176,15 @@ export default function projectManagerShimMiddleware(
.uploadBundle(request, directory, name)
.then((id) => {
response
.writeHead(HTTP_STATUS_OK, [
['Content-Length', String(id.length)],
['Content-Type', 'text/plain'],
...COOP_COEP_CORP_HEADERS,
])
.writeHead(HTTP_STATUS_OK, {
'Content-Length': String(id.length),
'Content-Type': 'text/plain',
...COMMON_HEADERS,
})
.end(id)
})
.catch(() => {
response.writeHead(HTTP_STATUS_BAD_REQUEST, COOP_COEP_CORP_HEADERS).end()
response.writeHead(HTTP_STATUS_BAD_REQUEST, COMMON_HEADERS).end()
})
break
}
Expand All @@ -193,7 +197,7 @@ export default function projectManagerShimMiddleware(
!cliArguments.every((item): item is string => typeof item === 'string')
) {
response
.writeHead(HTTP_STATUS_BAD_REQUEST, COOP_COEP_CORP_HEADERS)
.writeHead(HTTP_STATUS_BAD_REQUEST, COMMON_HEADERS)
.end('Command arguments must be an array of strings.')
} else {
void (async () => {
Expand Down Expand Up @@ -335,11 +339,11 @@ export default function projectManagerShimMiddleware(
}
const buffer = Buffer.from(result)
response
.writeHead(HTTP_STATUS_OK, [
['Content-Length', String(buffer.byteLength)],
['Content-Type', 'application/json'],
...COOP_COEP_CORP_HEADERS,
])
.writeHead(HTTP_STATUS_OK, {
'Content-Length': String(buffer.byteLength),
'Content-Type': 'application/json',
...COMMON_HEADERS,
})
.end(buffer)
})()
}
Expand Down Expand Up @@ -370,10 +374,10 @@ export default function projectManagerShimMiddleware(
'id' in metadata &&
metadata.id === uuid
) {
response.writeHead(HTTP_STATUS_OK, [
['Content-Type', 'application/gzip+x-enso-project'],
...COOP_COEP_CORP_HEADERS,
])
response.writeHead(HTTP_STATUS_OK, {
'Content-Type': 'application/gzip+x-enso-project',
...COMMON_HEADERS,
})
tar
.create({ gzip: true, cwd: projectRoot }, [projectRoot])
.pipe(response, { end: true })
Expand All @@ -386,22 +390,22 @@ export default function projectManagerShimMiddleware(
}
}
if (!success) {
response.writeHead(HTTP_STATUS_NOT_FOUND, COOP_COEP_CORP_HEADERS).end()
response.writeHead(HTTP_STATUS_NOT_FOUND, COMMON_HEADERS).end()
}
})
break
}
response.writeHead(HTTP_STATUS_NOT_FOUND, COOP_COEP_CORP_HEADERS).end()
response.writeHead(HTTP_STATUS_NOT_FOUND, COMMON_HEADERS).end()
break
}
}
} else if (request.method === 'GET' && requestPath === '/api/root-directory') {
response
.writeHead(HTTP_STATUS_OK, [
['Content-Length', String(PROJECTS_ROOT_DIRECTORY.length)],
['Content-Type', 'text/plain'],
...COOP_COEP_CORP_HEADERS,
])
.writeHead(HTTP_STATUS_OK, {
'Content-Length': String(PROJECTS_ROOT_DIRECTORY.length),
'Content-Type': 'text/plain',
...COMMON_HEADERS,
})
.end(PROJECTS_ROOT_DIRECTORY)
} else {
next()
Expand Down
6 changes: 4 additions & 2 deletions app/gui/src/dashboard/authentication/cognito.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export class Cognito {
localStorage.setItem(MOCK_EMAIL_KEY, username)
const result = await results.Result.wrapAsync(async () => {
listen.authEventListener?.(listen.AuthEvent.signIn)
await Promise.resolve()
return Promise.resolve(await this.userSession())
})
return result
.mapErr(original.intoAmplifyErrorOrThrow)
Expand All @@ -238,8 +238,10 @@ export class Cognito {

/** Sign out the current user. */
async signOut() {
listen.authEventListener?.(listen.AuthEvent.signOut)
this.isSignedIn = false
listen.authEventListener?.(listen.AuthEvent.signOut)
localStorage.removeItem(MOCK_EMAIL_KEY)
localStorage.removeItem(MOCK_ORGANIZATION_ID_KEY)
return Promise.resolve(null)
}

Expand Down
125 changes: 125 additions & 0 deletions app/gui/src/dashboard/components/Activity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* @file Activity component
*
* This component is used to suspend the rendering of a subtree until a promise is resolved.
*/
import { unsafeWriteValue } from '#/utilities/write'
import { Suspense, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useAwait } from './Await'

/**
* Props for {@link Activity}
*/
export interface ActivityProps {
/**
* The mode of the activity.
* - `active`: The subtree is active (default).
* - `inactive`: The activity of the subtree is paused.
* - `inactive-hidden`: The activity of the subtree is paused, and the subtree is hidden.
* @default 'active'
*/
readonly mode: 'active' | 'inactive-hidden' | 'inactive'
readonly children: React.ReactNode
}

/**
* A component that pauses all activity inside it's subtree.
*
* ---
* ## The component is EXPERIMENTAL, please use with caution.
* ---
*/
export function Activity(props: ActivityProps) {
const { mode, children } = props

const contentRef = useRef<HTMLDivElement>(null)

const [promise, setPromise] = useState<Promise<void> | null>(null)

const isActive = mode === 'active'
const fallback =
mode === 'inactive-hidden' ? null : <UnhideSuspendedTree contentRef={contentRef} />

useEffect(() => {
if (isActive) {
return
}

let resolve = () => {}

setPromise(
new Promise((res) => {
resolve = res
}),
)

return () => {
resolve()
setPromise(null)
}
}, [isActive])

return (
<div ref={contentRef} className="contents">
<Suspense fallback={fallback}>
<ActivityInner promise={promise}>{children}</ActivityInner>
</Suspense>
</div>
)
}

/**
* Props for {@link ActivityInner}
*/
interface ActivityInnerProps {
readonly children: React.ReactNode
readonly promise?: Promise<unknown> | null | undefined
}

/**
* A component that suspends the tree using promises.
* @param props - The props of the component.
* @returns The children of the component.
*/
function ActivityInner(props: ActivityInnerProps) {
const { promise, children } = props

// Suspend the subtree
useAwait(promise)

return children
}

/**
* Props for {@link UnhideSuspendedTree}
*/
interface UnhideSuspendedTreeProps {
readonly contentRef: React.RefObject<HTMLDivElement>
}

/**
* Hack, that unhides the suspended tree.
*/
function UnhideSuspendedTree(props: UnhideSuspendedTreeProps) {
const { contentRef } = props

useLayoutEffect(() => {
const element: HTMLDivElement | null = contentRef.current

if (element == null) {
return
}

const chidlren = element.childNodes

for (let i = 0; i < chidlren.length; i++) {
const child = chidlren[i]

if (child instanceof HTMLElement) {
unsafeWriteValue(child.style, 'display', '')
}
}
}, [contentRef])

return null
}
Loading

0 comments on commit 54f6cca

Please sign in to comment.