Disable opening projects that are already opened by other users (#7660)

- Closes https://github.com/enso-org/cloud-v2/issues/568
- Disable project with an `openedBy` that is not the current user

Fixes other issues:
- Fixes freshly restored saved projects not being unset when clicking stop before the editor first opens
- Changes "unable to" in errors to "could not", for consistency
- Users with insufficient permissions now see a network (node graph) icon instead of a play button:

![image](https://github.com/enso-org/enso/assets/4046547/0464ae66-4da7-4374-a4aa-80dd74fa1dc0)

# Important Notes
None
This commit is contained in:
somebody1234 2023-08-28 23:21:36 +10:00 committed by GitHub
parent 01aab4a2cc
commit 704ddff624
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 284 additions and 166 deletions

View File

@ -392,7 +392,7 @@ export default [
'@typescript-eslint/no-magic-numbers': [
'error',
{
ignore: [0, 1, 2],
ignore: [-1, 0, 1, 2],
ignoreArrayIndexes: true,
ignoreEnums: true,
detectObjects: true,

View File

@ -5,15 +5,9 @@ import * as newtype from '../newtype'
import * as permissions from './permissions'
import * as uniqueString from '../uniqueString'
// =============
// === Types ===
// =============
/** The {@link Backend} variant. If a new variant is created, it should be added to this enum. */
export enum BackendType {
local = 'local',
remote = 'remote',
}
// ================
// === Newtypes ===
// ================
// These are constructor functions that construct values of the type they are named after.
/* eslint-disable @typescript-eslint/no-redeclare */
@ -76,6 +70,46 @@ export const Subject = newtype.newtypeConstructor<Subject>()
/* eslint-enable @typescript-eslint/no-redeclare */
// ========================
// === PermissionAction ===
// ========================
/** Backend representation of user permission types. */
export enum PermissionAction {
own = 'Own',
admin = 'Admin',
edit = 'Edit',
read = 'Read',
readAndDocs = 'Read_docs',
readAndExec = 'Read_exec',
view = 'View',
viewAndDocs = 'View_docs',
viewAndExec = 'View_exec',
}
/** Whether each {@link PermissionAction} can execute a project. */
export const PERMISSION_ACTION_CAN_EXECUTE: Record<PermissionAction, boolean> = {
[PermissionAction.own]: true,
[PermissionAction.admin]: true,
[PermissionAction.edit]: true,
[PermissionAction.read]: false,
[PermissionAction.readAndDocs]: false,
[PermissionAction.readAndExec]: true,
[PermissionAction.view]: false,
[PermissionAction.viewAndDocs]: false,
[PermissionAction.viewAndExec]: true,
}
// =============
// === Types ===
// =============
/** The {@link Backend} variant. If a new variant is created, it should be added to this enum. */
export enum BackendType {
local = 'local',
remote = 'remote',
}
/** A user/organization in the application. These are the primary owners of a project. */
export interface UserOrOrganization {
id: UserOrOrganizationId
@ -112,6 +146,28 @@ export enum ProjectState {
/** Wrapper around a project state value. */
export interface ProjectStateType {
type: ProjectState
/* eslint-disable @typescript-eslint/naming-convention */
volume_id: string
instance_id?: string
execute_async?: boolean
address?: string
security_group_id?: string
ec2_id?: string
ec2_public_ip_address?: string
current_session_id?: string
opened_by?: string
/* eslint-enable @typescript-eslint/naming-convention */
}
export const IS_PROJECT_STATE_OPENING_OR_OPENED: Record<ProjectState, boolean> = {
[ProjectState.created]: false,
[ProjectState.new]: false,
[ProjectState.openInProgress]: true,
[ProjectState.provisioned]: true,
[ProjectState.opened]: true,
[ProjectState.closed]: false,
[ProjectState.placeholder]: true,
[ProjectState.closing]: false,
}
/** Common `Project` fields returned by all `Project`-related endpoints. */
@ -158,6 +214,7 @@ export interface Project extends ListedProject {
/** This must not be null as it is required to determine the base URL for backend assets. */
ideVersion: VersionNumber
engineVersion: VersionNumber | null
openedBy?: EmailAddress
}
/** Information required to open a project. */
@ -293,19 +350,6 @@ export interface SimpleUser {
email: EmailAddress
}
/** Backend representation of user permission types. */
export enum PermissionAction {
own = 'Own',
admin = 'Admin',
edit = 'Edit',
read = 'Read',
readAndDocs = 'Read_docs',
readAndExec = 'Read_exec',
view = 'View',
viewAndDocs = 'View_docs',
viewAndExec = 'View_exec',
}
/** User permission for a specific user. */
export interface UserPermission {
user: User

View File

@ -68,6 +68,12 @@ export default function AssetContextMenu(props: AssetContextMenuProps) {
const managesThisAsset =
self?.permission === backendModule.PermissionAction.own ||
self?.permission === backendModule.PermissionAction.admin
const canExecute =
self?.permission != null && backendModule.PERMISSION_ACTION_CAN_EXECUTE[self.permission]
const isOtherUserUsingProject =
backendModule.assetIsProject(asset) &&
organization != null &&
asset.projectState.opened_by !== organization.email
const setAsset = React.useCallback(
(valueOrUpdater: React.SetStateAction<backendModule.AnyAsset>) => {
if (typeof valueOrUpdater === 'function') {
@ -84,20 +90,22 @@ export default function AssetContextMenu(props: AssetContextMenuProps) {
return (
<ContextMenus hidden={hidden} key={asset.id} event={event}>
<ContextMenu hidden={hidden}>
{asset.type === backendModule.AssetType.project && (
<MenuEntry
hidden={hidden}
action={shortcuts.KeyboardAction.open}
doAction={() => {
unsetModal()
dispatchAssetEvent({
type: assetEventModule.AssetEventType.openProject,
id: asset.id,
shouldAutomaticallySwitchPage: true,
})
}}
/>
)}
{asset.type === backendModule.AssetType.project &&
canExecute &&
!isOtherUserUsingProject && (
<MenuEntry
hidden={hidden}
action={shortcuts.KeyboardAction.open}
doAction={() => {
unsetModal()
dispatchAssetEvent({
type: assetEventModule.AssetEventType.openProject,
id: asset.id,
shouldAutomaticallySwitchPage: true,
})
}}
/>
)}
{asset.type === backendModule.AssetType.project &&
backend.type === backendModule.BackendType.local && (
<MenuEntry
@ -146,21 +154,23 @@ export default function AssetContextMenu(props: AssetContextMenuProps) {
}}
/>
)}
<MenuEntry
hidden={hidden}
disabled={
asset.type !== backendModule.AssetType.project &&
asset.type !== backendModule.AssetType.directory
}
action={shortcuts.KeyboardAction.rename}
doAction={() => {
setRowState(oldRowState => ({
...oldRowState,
isEditingName: true,
}))
unsetModal()
}}
/>
{canExecute && !isOtherUserUsingProject && (
<MenuEntry
hidden={hidden}
disabled={
asset.type !== backendModule.AssetType.project &&
asset.type !== backendModule.AssetType.directory
}
action={shortcuts.KeyboardAction.rename}
doAction={() => {
setRowState(oldRowState => ({
...oldRowState,
isEditingName: true,
}))
unsetModal()
}}
/>
)}
<MenuEntry
hidden={hidden}
disabled
@ -169,18 +179,20 @@ export default function AssetContextMenu(props: AssetContextMenuProps) {
// No backend support yet.
}}
/>
<MenuEntry
hidden={hidden}
action={shortcuts.KeyboardAction.moveToTrash}
doAction={() => {
setModal(
<ConfirmDeleteModal
description={`the ${asset.type} '${asset.title}'`}
doDelete={doDelete}
/>
)
}}
/>
{managesThisAsset && !isOtherUserUsingProject && (
<MenuEntry
hidden={hidden}
action={shortcuts.KeyboardAction.moveToTrash}
doAction={() => {
setModal(
<ConfirmDeleteModal
description={`the ${asset.type} '${asset.title}'`}
doDelete={doDelete}
/>
)
}}
/>
)}
<ContextMenuSeparator hidden={hidden} />
{managesThisAsset && (
<MenuEntry
@ -229,14 +241,16 @@ export default function AssetContextMenu(props: AssetContextMenuProps) {
// No backend support yet.
}}
/>
<MenuEntry
hidden={hidden}
disabled
action={shortcuts.KeyboardAction.cut}
doAction={() => {
// No backend support yet.
}}
/>
{!isOtherUserUsingProject && (
<MenuEntry
hidden={hidden}
disabled
action={shortcuts.KeyboardAction.cut}
doAction={() => {
// No backend support yet.
}}
/>
)}
<MenuEntry
hidden={hidden}
disabled

View File

@ -94,7 +94,7 @@ export default function AssetRow(props: AssetRowProps) {
})
} catch (error) {
setPresence(presenceModule.Presence.present)
toastAndLog('Unable to delete project', error)
toastAndLog(`Could not delete ${backendModule.ASSET_TYPE_NAME[asset.type]}`, error)
}
}, [
backend,
@ -146,7 +146,10 @@ export default function AssetRow(props: AssetRowProps) {
})
} catch (error) {
setPresence(presenceModule.Presence.present)
toastAndLog('Unable to delete project', error)
toastAndLog(
`Could not delete ${backendModule.ASSET_TYPE_NAME[asset.type]}`,
error
)
}
}
break

View File

@ -538,7 +538,8 @@ export default function AssetsTable(props: AssetsTableProps) {
modifiedAt: dateTime.toRfc3339(new Date()),
parentId: event.parentId ?? backend.rootDirectoryId(organization),
permissions: permissions.tryGetSingletonOwnerPermission(organization, user),
projectState: { type: backendModule.ProjectState.placeholder },
// eslint-disable-next-line @typescript-eslint/naming-convention
projectState: { type: backendModule.ProjectState.placeholder, volume_id: '' },
type: backendModule.AssetType.project,
}
if (
@ -588,9 +589,8 @@ export default function AssetsTable(props: AssetsTableProps) {
parentId,
permissions: permissions.tryGetSingletonOwnerPermission(organization, user),
modifiedAt: dateTime.toRfc3339(new Date()),
projectState: {
type: backendModule.ProjectState.new,
},
// eslint-disable-next-line @typescript-eslint/naming-convention
projectState: { type: backendModule.ProjectState.new, volume_id: '' },
}))
if (
event.parentId != null &&

View File

@ -91,7 +91,7 @@ export default function ManagePermissionsModal(props: ManagePermissionsModalProp
// and `organization` is absent only when offline - in which case the user should only
// be able to access the local backend.
// This MUST be an error, otherwise the hooks below are considered as conditionally called.
throw new Error('Unable to share projects on the local backend.')
throw new Error('Cannot share projects on the local backend.')
} else {
const listedUsers = hooks.useAsyncEffect([], () => backend.listUsers(), [])
const allUsers = React.useMemo(
@ -190,7 +190,7 @@ export default function ManagePermissionsModal(props: ManagePermissionsModalProp
const usernames = addedUsersPermissions.map(
userPermissions => userPermissions.user.user_name
)
toastAndLog(`Unable to set permissions for ${usernames.join(', ')}`, error)
toastAndLog(`Could not set permissions for ${usernames.join(', ')}`, error)
}
}
}
@ -221,7 +221,7 @@ export default function ManagePermissionsModal(props: ManagePermissionsModalProp
)
)
}
toastAndLog(`Unable to set permissions of '${userToDelete.user_email}'`, error)
toastAndLog(`Could not set permissions of '${userToDelete.user_email}'`, error)
}
}
}

View File

@ -7,8 +7,10 @@ import PlayIcon from 'enso-assets/play.svg'
import StopIcon from 'enso-assets/stop.svg'
import * as assetEventModule from '../events/assetEvent'
import * as authProvider from '../../authentication/providers/auth'
import * as backendModule from '../backend'
import * as backendProvider from '../../providers/backend'
import * as errorModule from '../../error'
import * as hooks from '../../hooks'
import * as localStorageModule from '../localStorage'
import * as localStorageProvider from '../../providers/localStorage'
@ -79,6 +81,8 @@ export default function ProjectIcon(props: ProjectIconProps) {
openIde,
} = props
const { backend } = backendProvider.useBackend()
const { organization } = authProvider.useNonPartialUserSession()
const { localStorage } = localStorageProvider.useLocalStorage()
const { unsetModal } = modalProvider.useSetModal()
const { localStorage } = localStorageProvider.useLocalStorage()
const toastAndLog = hooks.useToastAndLog()
@ -88,10 +92,16 @@ export default function ProjectIcon(props: ProjectIconProps) {
if (typeof stateOrUpdater === 'function') {
setItem(oldItem => ({
...oldItem,
projectState: { type: stateOrUpdater(oldItem.projectState.type) },
projectState: {
...oldItem.projectState,
type: stateOrUpdater(oldItem.projectState.type),
},
}))
} else {
setItem(oldItem => ({ ...oldItem, projectState: { type: stateOrUpdater } }))
setItem(oldItem => ({
...oldItem,
projectState: { ...oldItem.projectState, type: stateOrUpdater },
}))
}
},
[/* should never change */ setItem]
@ -105,16 +115,14 @@ export default function ProjectIcon(props: ProjectIconProps) {
const [toastId, setToastId] = React.useState<toast.Id | null>(null)
const [openProjectAbortController, setOpenProjectAbortController] =
React.useState<AbortController | null>(null)
const isOtherUserUsingProject = item.projectState.opened_by !== organization?.email
const openProject = React.useCallback(async () => {
setState(backendModule.ProjectState.openInProgress)
try {
switch (backend.type) {
case backendModule.BackendType.remote: {
if (
state !== backendModule.ProjectState.openInProgress &&
state !== backendModule.ProjectState.opened
) {
if (!backendModule.IS_PROJECT_STATE_OPENING_OR_OPENED[state]) {
setToastId(toast.toast.loading(LOADING_MESSAGE))
await backend.openProject(item.id, null, item.title)
}
@ -142,7 +150,15 @@ export default function ProjectIcon(props: ProjectIconProps) {
}
}
} catch (error) {
toastAndLog(`Could not open project '${item.title}'`, error)
const project = await backend.getProjectDetails(item.id, item.title)
setItem(oldItem => ({
...oldItem,
projectState: project.state,
}))
toastAndLog(
errorModule.tryGetMessage(error)?.slice(0, -1) ??
`Could not open project '${item.title}'`
)
setState(backendModule.ProjectState.closed)
}
}, [
@ -151,18 +167,11 @@ export default function ProjectIcon(props: ProjectIconProps) {
item,
/* should never change */ toastAndLog,
/* should never change */ setState,
/* should never change */ setItem,
])
React.useEffect(() => {
if (item.projectState.type === backendModule.ProjectState.openInProgress) {
void openProject()
}
// This MUST only run once, when the component is initially mounted.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
React.useEffect(() => {
setItem(oldItem => ({ ...oldItem, projectState: { type: state } }))
setItem(oldItem => ({ ...oldItem, projectState: { ...oldItem.projectState, type: state } }))
}, [state, /* should never change */ setItem])
React.useEffect(() => {
@ -212,7 +221,9 @@ export default function ProjectIcon(props: ProjectIconProps) {
case assetEventModule.AssetEventType.openProject: {
if (event.id !== item.id) {
setShouldOpenWhenReady(false)
void closeProject(false)
if (!isOtherUserUsingProject) {
void closeProject(false)
}
} else {
setShouldOpenWhenReady(true)
setShouldSwitchPage(event.shouldAutomaticallySwitchPage)
@ -226,7 +237,9 @@ export default function ProjectIcon(props: ProjectIconProps) {
setOnSpinnerStateChange(null)
openProjectAbortController?.abort()
setOpenProjectAbortController(null)
void closeProject(false)
if (!isOtherUserUsingProject) {
void closeProject(false)
}
break
}
case assetEventModule.AssetEventType.newProject: {
@ -292,7 +305,7 @@ export default function ProjectIcon(props: ProjectIconProps) {
case backendModule.ProjectState.closed:
return (
<button
className="w-6"
className="w-6 disabled:opacity-50"
onClick={clickEvent => {
clickEvent.stopPropagation()
unsetModal()
@ -307,7 +320,11 @@ export default function ProjectIcon(props: ProjectIconProps) {
case backendModule.ProjectState.placeholder:
return (
<button
className="w-6"
disabled={isOtherUserUsingProject}
{...(isOtherUserUsingProject
? { title: 'Someone else is using this project.' }
: {})}
className="w-6 disabled:opacity-50"
onClick={async clickEvent => {
clickEvent.stopPropagation()
unsetModal()
@ -324,7 +341,11 @@ export default function ProjectIcon(props: ProjectIconProps) {
return (
<>
<button
className="w-6"
disabled={isOtherUserUsingProject}
{...(isOtherUserUsingProject
? { title: 'Someone else has this project open.' }
: {})}
className="w-6 disabled:opacity-50"
onClick={async clickEvent => {
clickEvent.stopPropagation()
unsetModal()
@ -336,16 +357,18 @@ export default function ProjectIcon(props: ProjectIconProps) {
</div>
<SvgMask src={StopIcon} />
</button>
<button
className="w-6"
onClick={clickEvent => {
clickEvent.stopPropagation()
unsetModal()
openIde(true)
}}
>
<SvgMask src={ArrowUpIcon} />
</button>
{!isOtherUserUsingProject && (
<button
className="w-6"
onClick={clickEvent => {
clickEvent.stopPropagation()
unsetModal()
openIde(true)
}}
>
<SvgMask src={ArrowUpIcon} />
</button>
)}
</>
)
}

View File

@ -1,11 +1,15 @@
/** @file The icon and name of a {@link backendModule.ProjectAsset}. */
import * as React from 'react'
import NetworkIcon from 'enso-assets/network.svg'
import * as assetEventModule from '../events/assetEvent'
import * as assetListEventModule from '../events/assetListEvent'
import * as assetTreeNode from '../assetTreeNode'
import * as authProvider from '../../authentication/providers/auth'
import * as backendModule from '../backend'
import * as backendProvider from '../../providers/backend'
import * as errorModule from '../../error'
import * as eventModule from '../event'
import * as hooks from '../../hooks'
import * as indent from '../indent'
@ -17,6 +21,7 @@ import * as validation from '../validation'
import * as column from '../column'
import EditableSpan from './editableSpan'
import ProjectIcon from './projectIcon'
import SvgMask from '../../authentication/components/svgMask'
// ===================
// === ProjectName ===
@ -47,6 +52,7 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
} = props
const toastAndLog = hooks.useToastAndLog()
const { backend } = backendProvider.useBackend()
const { organization } = authProvider.useNonPartialUserSession()
const { shortcuts } = shortcutsProvider.useShortcuts()
const asset = item.item
if (asset.type !== backendModule.AssetType.project) {
@ -54,6 +60,13 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
throw new Error('`ProjectNameColumn` can only display project assets.')
}
const setAsset = assetTreeNode.useSetAsset(asset, setItem)
const ownPermission =
asset.permissions?.find(permission => permission.user.user_email === organization?.email) ??
null
const canExecute =
ownPermission != null &&
backendModule.PERMISSION_ACTION_CAN_EXECUTE[ownPermission.permission]
const isOtherUserUsingProject = asset.projectState.opened_by !== organization?.email
const doRename = async (newName: string) => {
try {
@ -68,7 +81,7 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
)
return
} catch (error) {
toastAndLog('Unable to rename project', error)
toastAndLog(errorModule.tryGetMessage(error) ?? 'Could not rename project.')
throw error
}
}
@ -102,7 +115,10 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
setAsset({
...asset,
id: createdProject.projectId,
projectState: { type: backendModule.ProjectState.placeholder },
projectState: {
...asset.projectState,
type: backendModule.ProjectState.placeholder,
},
})
dispatchAssetEvent({
type: assetEventModule.AssetEventType.openProject,
@ -202,7 +218,11 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
item.depth
)}`}
onClick={event => {
if (!rowState.isEditingName && eventModule.isDoubleClick(event)) {
if (
!rowState.isEditingName &&
!isOtherUserUsingProject &&
eventModule.isDoubleClick(event)
) {
// It is a double click; open the project.
dispatchAssetEvent({
type: assetEventModule.AssetEventType.openProject,
@ -221,18 +241,22 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
}
}}
>
<ProjectIcon
keyProp={item.key}
item={asset}
setItem={setAsset}
assetEvents={assetEvents}
doOpenManually={doOpenManually}
appRunner={appRunner}
openIde={switchPage => {
doOpenIde(asset, switchPage)
}}
onClose={doCloseIde}
/>
{!canExecute ? (
<SvgMask src={NetworkIcon} className="m-1" />
) : (
<ProjectIcon
keyProp={item.key}
item={asset}
setItem={setAsset}
assetEvents={assetEvents}
doOpenManually={doOpenManually}
appRunner={appRunner}
openIde={switchPage => {
doOpenIde(asset, switchPage)
}}
onClose={doCloseIde}
/>
)}
<EditableSpan
editable={rowState.isEditingName}
onSubmit={async newTitle => {
@ -263,7 +287,11 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
}
: {})}
className={`bg-transparent grow px-2 ${
rowState.isEditingName ? 'cursor-text' : 'cursor-pointer'
rowState.isEditingName
? 'cursor-text'
: canExecute && !isOtherUserUsingProject
? 'cursor-pointer'
: ''
}`}
>
{asset.title}

View File

@ -48,7 +48,7 @@ export default function UserPermissions(props: UserPermissionsProps) {
setUserPermissions(userPermissions)
outerSetUserPermission(userPermissions)
toastAndLog(
`Unable to set permissions of '${newUserPermissions.user.user_email}'`,
`Could not set permissions of '${newUserPermissions.user.user_email}'`,
error
)
}

View File

@ -76,6 +76,8 @@ export class LocalBackend extends backend.Backend {
: project.id === LocalBackend.currentlyOpeningProjectId
? backend.ProjectState.openInProgress
: backend.ProjectState.closed,
// eslint-disable-next-line @typescript-eslint/naming-convention
volume_id: '',
},
}))
}
@ -92,6 +94,8 @@ export class LocalBackend extends backend.Backend {
packageName: project.name,
state: {
type: backend.ProjectState.closed,
// eslint-disable-next-line @typescript-eslint/naming-convention
volume_id: '',
},
jsonAddress: null,
binaryAddress: null,
@ -118,6 +122,8 @@ export class LocalBackend extends backend.Backend {
packageName: body.projectName,
state: {
type: backend.ProjectState.closed,
// eslint-disable-next-line @typescript-eslint/naming-convention
volume_id: '',
},
}
}
@ -135,7 +141,7 @@ export class LocalBackend extends backend.Backend {
return
} catch (error) {
throw new Error(
`Unable to close project ${
`Could not close project ${
title != null ? `'${title}'` : `with ID '${projectId}'`
}: ${errorModule.tryGetMessage(error) ?? 'unknown error'}.`
)
@ -178,6 +184,8 @@ export class LocalBackend extends backend.Backend {
: project.lastOpened != null
? backend.ProjectState.closed
: backend.ProjectState.created,
// eslint-disable-next-line @typescript-eslint/naming-convention
volume_id: '',
},
}
}
@ -199,6 +207,8 @@ export class LocalBackend extends backend.Backend {
projectId,
state: {
type: backend.ProjectState.opened,
// eslint-disable-next-line @typescript-eslint/naming-convention
volume_id: '',
},
}
}
@ -223,7 +233,7 @@ export class LocalBackend extends backend.Backend {
return
} catch (error) {
throw new Error(
`Unable to open project ${
`Could not open project ${
title != null ? `'${title}'` : `with ID '${projectId}'`
}: ${errorModule.tryGetMessage(error) ?? 'unknown error'}.`
)
@ -291,7 +301,7 @@ export class LocalBackend extends backend.Backend {
return
} catch (error) {
throw new Error(
`Unable to delete project ${
`Could not delete project ${
title != null ? `'${title}'` : `with ID '${projectId}'`
}: ${errorModule.tryGetMessage(error) ?? 'unknown error'}.`
)
@ -322,7 +332,7 @@ export class LocalBackend extends backend.Backend {
/** @throws An error stating that the operation is intentionally unavailable on the local
* backend. */
invalidOperation(): never {
throw new Error('Unable to manage users, folders, files, and secrets on the local backend.')
throw new Error('Cannot manage users, folders, files, and secrets on the local backend.')
}
/** Return an empty array. This function should never need to be called. */

View File

@ -48,11 +48,7 @@ export async function waitUntilProjectIsReady(
abortController: AbortController = new AbortController()
) {
let project = await backend.getProjectDetails(item.id, item.title)
if (
project.state.type !== backendModule.ProjectState.openInProgress &&
project.state.type !== backendModule.ProjectState.provisioned &&
project.state.type !== backendModule.ProjectState.opened
) {
if (!backendModule.IS_PROJECT_STATE_OPENING_OR_OPENED[project.state.type]) {
await backend.openProject(item.id, null, item.title)
}
let nextCheckTimestamp = 0
@ -247,7 +243,7 @@ export class RemoteBackend extends backendModule.Backend {
async listUsers(): Promise<backendModule.SimpleUser[]> {
const response = await this.get<ListUsersResponseBody>(LIST_USERS_PATH)
if (!responseIsSuccessful(response)) {
return this.throw(`Unable to list users in the organization.`)
return this.throw(`Could not list users in the organization.`)
} else {
return (await response.json()).users
}
@ -259,7 +255,7 @@ export class RemoteBackend extends backendModule.Backend {
): Promise<backendModule.UserOrOrganization> {
const response = await this.post<backendModule.UserOrOrganization>(CREATE_USER_PATH, body)
if (!responseIsSuccessful(response)) {
return this.throw('Unable to create user.')
return this.throw('Could not create user.')
} else {
return await response.json()
}
@ -269,7 +265,7 @@ export class RemoteBackend extends backendModule.Backend {
async inviteUser(body: backendModule.InviteUserRequestBody): Promise<void> {
const response = await this.post(INVITE_USER_PATH, body)
if (!responseIsSuccessful(response)) {
return this.throw(`Unable to invite user '${body.userEmail}'.`)
return this.throw(`Could not invite user '${body.userEmail}'.`)
} else {
return
}
@ -282,7 +278,7 @@ export class RemoteBackend extends backendModule.Backend {
body
)
if (!responseIsSuccessful(response)) {
return this.throw(`Unable to set permissions.`)
return this.throw(`Could not set permissions.`)
} else {
return
}
@ -321,12 +317,12 @@ export class RemoteBackend extends backendModule.Backend {
return []
} else if (query.parentId != null) {
return this.throw(
`Unable to list directory ${
`Could not list folder ${
title != null ? `'${title}'` : `with ID '${query.parentId}'`
}.`
)
} else {
return this.throw('Unable to list root directory.')
return this.throw('Could not list root folder.')
}
} else {
return (await response.json()).assets
@ -360,7 +356,7 @@ export class RemoteBackend extends backendModule.Backend {
body
)
if (!responseIsSuccessful(response)) {
return this.throw(`Unable to create directory with name '${body.title}'.`)
return this.throw(`Could not create folder with name '${body.title}'.`)
} else {
return await response.json()
}
@ -380,7 +376,7 @@ export class RemoteBackend extends backendModule.Backend {
)
if (!responseIsSuccessful(response)) {
return this.throw(
`Unable to update directory ${
`Could not update folder ${
title != null ? `'${title}'` : `with ID '${directoryId}'`
}.`
)
@ -396,7 +392,7 @@ export class RemoteBackend extends backendModule.Backend {
const response = await this.delete(deleteDirectoryPath(directoryId))
if (!responseIsSuccessful(response)) {
return this.throw(
`Unable to delete directory ${
`Could not delete folder ${
title != null ? `'${title}'` : `with ID '${directoryId}'`
}.`
)
@ -411,7 +407,7 @@ export class RemoteBackend extends backendModule.Backend {
async listProjects(): Promise<backendModule.ListedProject[]> {
const response = await this.get<ListProjectsResponseBody>(LIST_PROJECTS_PATH)
if (!responseIsSuccessful(response)) {
return this.throw('Unable to list projects.')
return this.throw('Could not list projects.')
} else {
return (await response.json()).projects.map(project => ({
...project,
@ -435,7 +431,7 @@ export class RemoteBackend extends backendModule.Backend {
): Promise<backendModule.CreatedProject> {
const response = await this.post<backendModule.CreatedProject>(CREATE_PROJECT_PATH, body)
if (!responseIsSuccessful(response)) {
return this.throw(`Unable to create project with name '${body.projectName}'.`)
return this.throw(`Could not create project with name '${body.projectName}'.`)
} else {
return await response.json()
}
@ -448,7 +444,7 @@ export class RemoteBackend extends backendModule.Backend {
const response = await this.post(closeProjectPath(projectId), {})
if (!responseIsSuccessful(response)) {
return this.throw(
`Unable to close project ${
`Could not close project ${
title != null ? `'${title}'` : `with ID '${projectId}'`
}.`
)
@ -467,7 +463,7 @@ export class RemoteBackend extends backendModule.Backend {
const response = await this.get<backendModule.ProjectRaw>(getProjectDetailsPath(projectId))
if (!responseIsSuccessful(response)) {
return this.throw(
`Unable to get details of project ${
`Could not get details of project ${
title != null ? `'${title}'` : `with ID '${projectId}'`
}.`
)
@ -515,7 +511,7 @@ export class RemoteBackend extends backendModule.Backend {
)
if (!responseIsSuccessful(response)) {
return this.throw(
`Unable to open project ${title != null ? `'${title}'` : `with ID '${projectId}'`}.`
`Could not open project ${title != null ? `'${title}'` : `with ID '${projectId}'`}.`
)
} else {
return
@ -536,7 +532,7 @@ export class RemoteBackend extends backendModule.Backend {
)
if (!responseIsSuccessful(response)) {
return this.throw(
`Unable to update project ${
`Could not update project ${
title != null ? `'${title}'` : `with ID '${projectId}'`
}.`
)
@ -552,7 +548,7 @@ export class RemoteBackend extends backendModule.Backend {
const response = await this.delete(deleteProjectPath(projectId))
if (!responseIsSuccessful(response)) {
return this.throw(
`Unable to delete project ${
`Could not delete project ${
title != null ? `'${title}'` : `with ID '${projectId}'`
}.`
)
@ -571,7 +567,7 @@ export class RemoteBackend extends backendModule.Backend {
const response = await this.get<backendModule.ResourceUsage>(checkResourcesPath(projectId))
if (!responseIsSuccessful(response)) {
return this.throw(
`Unable to get resource usage for project ${
`Could not get resource usage for project ${
title != null ? `'${title}'` : `with ID '${projectId}'`
}.`
)
@ -586,7 +582,7 @@ export class RemoteBackend extends backendModule.Backend {
async listFiles(): Promise<backendModule.File[]> {
const response = await this.get<ListFilesResponseBody>(LIST_FILES_PATH)
if (!responseIsSuccessful(response)) {
return this.throw('Unable to list files.')
return this.throw('Could not list files.')
} else {
return (await response.json()).files
}
@ -642,7 +638,7 @@ export class RemoteBackend extends backendModule.Backend {
const response = await this.delete(deleteFilePath(fileId))
if (!responseIsSuccessful(response)) {
return this.throw(
`Unable to delete file ${title != null ? `'${title}'` : `with ID '${fileId}'`}.`
`Could not delete file ${title != null ? `'${title}'` : `with ID '${fileId}'`}.`
)
} else {
return
@ -657,7 +653,7 @@ export class RemoteBackend extends backendModule.Backend {
): Promise<backendModule.SecretAndInfo> {
const response = await this.post<backendModule.SecretAndInfo>(CREATE_SECRET_PATH, body)
if (!responseIsSuccessful(response)) {
return this.throw(`Unable to create secret with name '${body.secretName}'.`)
return this.throw(`Could not create secret with name '${body.secretName}'.`)
} else {
return await response.json()
}
@ -673,7 +669,7 @@ export class RemoteBackend extends backendModule.Backend {
const response = await this.get<backendModule.Secret>(getSecretPath(secretId))
if (!responseIsSuccessful(response)) {
return this.throw(
`Unable to get secret ${title != null ? `'${title}'` : `with ID '${secretId}'`}.`
`Could not get secret ${title != null ? `'${title}'` : `with ID '${secretId}'`}.`
)
} else {
return await response.json()
@ -686,7 +682,7 @@ export class RemoteBackend extends backendModule.Backend {
async listSecrets(): Promise<backendModule.SecretInfo[]> {
const response = await this.get<ListSecretsResponseBody>(LIST_SECRETS_PATH)
if (!responseIsSuccessful(response)) {
return this.throw('Unable to list secrets.')
return this.throw('Could not list secrets.')
} else {
return (await response.json()).secrets
}
@ -699,7 +695,7 @@ export class RemoteBackend extends backendModule.Backend {
const response = await this.delete(deleteSecretPath(secretId))
if (!responseIsSuccessful(response)) {
return this.throw(
`Unable to delete secret ${title != null ? `'${title}'` : `with ID '${secretId}'`}.`
`Could not delete secret ${title != null ? `'${title}'` : `with ID '${secretId}'`}.`
)
} else {
return
@ -719,7 +715,7 @@ export class RemoteBackend extends backendModule.Backend {
/* eslint-enable @typescript-eslint/naming-convention */
})
if (!responseIsSuccessful(response)) {
return this.throw(`Unable to create create tag with name '${body.name}'.`)
return this.throw(`Could not create create tag with name '${body.name}'.`)
} else {
return await response.json()
}
@ -738,7 +734,7 @@ export class RemoteBackend extends backendModule.Backend {
}).toString()
)
if (!responseIsSuccessful(response)) {
return this.throw(`Unable to list tags of type '${params.tagType}'.`)
return this.throw(`Could not list tags of type '${params.tagType}'.`)
} else {
return (await response.json()).tags
}
@ -750,7 +746,7 @@ export class RemoteBackend extends backendModule.Backend {
async deleteTag(tagId: backendModule.TagId): Promise<void> {
const response = await this.delete(deleteTagPath(tagId))
if (!responseIsSuccessful(response)) {
return this.throw(`Unable to delete tag with ID '${tagId}'.`)
return this.throw(`Could not delete tag with ID '${tagId}'.`)
} else {
return
}
@ -772,7 +768,7 @@ export class RemoteBackend extends backendModule.Backend {
}).toString()
)
if (!responseIsSuccessful(response)) {
return this.throw(`Unable to list versions of type '${params.versionType}'.`)
return this.throw(`Could not list versions of type '${params.versionType}'.`)
} else {
return (await response.json()).versions
}

View File

@ -20,7 +20,7 @@ const HTTP_STATUS_OK = 200
// `outputPath` does not have to be a real directory because `write` is `false`,
// meaning that files will not be written to the filesystem.
// However, the path should still be non-empty in order for `esbuild.serve` to work properly.
const ARGS: bundler.Arguments = { outputPath: '/', devMode: true }
const ARGS: bundler.Arguments = { outputPath: '/', devMode: process.env.DEV_MODE !== 'false' }
const OPTS = bundler.bundlerOptions(ARGS)
OPTS.define.REDIRECT_OVERRIDE = JSON.stringify(`http://localhost:${PORT}`)
OPTS.entryPoints.push(