mirror of
https://github.com/enso-org/enso.git
synced 2024-11-25 21:25:20 +03:00
Show Language Server Logs (#9745)
- Frontend side of https://github.com/enso-org/cloud-v2/issues/1046 - Add endpoint to list project sessions - Add button to project sessions to view logs associated with a specific project session # Important Notes None
This commit is contained in:
parent
2638ba8d8e
commit
64d51c2020
7
app/ide-desktop/lib/assets/logs.svg
Normal file
7
app/ide-desktop/lib/assets/logs.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.2" d="M1 0H15V13C15 14.1046 14.1046 15 13 15H3C1.89543 15 1 14.1046 1 13V0Z" fill="black" />
|
||||
<rect x="3" y="2" width="10" height="2" fill="black" />
|
||||
<rect opacity="0.8" x="3" y="5" width="10" height="2" fill="black" />
|
||||
<rect opacity="0.6" x="3" y="8" width="10" height="2" fill="black" />
|
||||
<rect opacity="0.4" x="3" y="11" width="6" height="2" fill="black" />
|
||||
</svg>
|
After Width: | Height: | Size: 498 B |
@ -224,7 +224,9 @@ export function Dialog(props: DialogProps) {
|
||||
}}
|
||||
>
|
||||
<errorBoundary.ErrorBoundary>
|
||||
<suspense.Suspense loaderProps={{ minHeight: 'h32' }}>
|
||||
<suspense.Suspense
|
||||
loaderProps={{ minHeight: type === 'fullscreen' ? 'full' : 'h32' }}
|
||||
>
|
||||
{typeof children === 'function' ? children(opts) : children}
|
||||
</suspense.Suspense>
|
||||
</errorBoundary.ErrorBoundary>
|
||||
|
@ -49,11 +49,10 @@ export function DialogStackProvider(props: React.PropsWithChildren) {
|
||||
} else {
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
console.warn(`
|
||||
DialogStackProvider: sliceFromStack: currentId ${currentId} does not match the last item in the stack \n
|
||||
This is no-op but it might be a sign of a bug in the application \n
|
||||
Usually, this means that the underlaying component was closed manually or
|
||||
the stack was not updated properly. \n
|
||||
`)
|
||||
DialogStackProvider: sliceFromStack: currentId ${currentId} does not match the last item in the stack. \
|
||||
This is no-op but it might be a sign of a bug in the application. \
|
||||
Usually, this means that the underlaying component was closed manually or the stack was not \
|
||||
updated properly.`)
|
||||
|
||||
return currentStack
|
||||
}
|
||||
@ -77,23 +76,22 @@ export function DialogStackProvider(props: React.PropsWithChildren) {
|
||||
* DialogStackRegistrar is a React component that registers a dialog in the dialog stack.
|
||||
*/
|
||||
export function DialogStackRegistrar(props: React.PropsWithChildren<DialogStackItem>) {
|
||||
const { children, id, type } = props
|
||||
const { children, id: idRaw, type: typeRaw } = props
|
||||
const idRef = React.useRef(idRaw)
|
||||
const typeRef = React.useRef(typeRaw)
|
||||
|
||||
const ctx = React.useContext(DialogStackContext)
|
||||
|
||||
invariant(ctx, 'DialogStackRegistrar must be used within a DialogStackProvider')
|
||||
|
||||
React.useEffect(() => {
|
||||
const id = idRef.current
|
||||
const type = typeRef.current
|
||||
ctx.add({ id, type })
|
||||
|
||||
return () => {
|
||||
ctx.slice(id)
|
||||
}
|
||||
// We don't want to re-run this effect on every render
|
||||
// As well as we don't want to re-run it when the id or type changes
|
||||
// This effect should run only once when the component mounts
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
}, [ctx])
|
||||
|
||||
return children
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ export interface DialogProps extends aria.DialogProps {
|
||||
readonly onOpenChange?: (isOpen: boolean) => void
|
||||
readonly isKeyboardDismissDisabled?: boolean
|
||||
readonly modalProps?: Pick<aria.ModalOverlayProps, 'className' | 'defaultOpen' | 'isOpen'>
|
||||
|
||||
readonly testId?: string
|
||||
}
|
||||
|
||||
|
@ -59,7 +59,7 @@ export interface ErrorDisplayProps extends errorBoundary.FallbackProps {
|
||||
}
|
||||
|
||||
/** Default fallback component to show when there is an error. */
|
||||
function ErrorDisplay(props: ErrorDisplayProps): React.JSX.Element {
|
||||
export function ErrorDisplay(props: ErrorDisplayProps): React.JSX.Element {
|
||||
const { resetErrorBoundary, error } = props
|
||||
|
||||
const { getText } = textProvider.useText()
|
||||
|
@ -202,7 +202,7 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
|
||||
return jsonSchema.constantValue(defs, childSchema).length === 1 ? null : (
|
||||
<div
|
||||
key={key}
|
||||
className="flex flex-wrap items-center gap-buttons"
|
||||
className="flex flex-wrap items-center gap-2"
|
||||
{...('description' in childSchema
|
||||
? { title: String(childSchema.description) }
|
||||
: {})}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/** @file A full-screen loading spinner. */
|
||||
import Spinner, * as spinnerModule from '#/components/Spinner'
|
||||
import StatelessSpinner, * as spinnerModule from '#/components/StatelessSpinner'
|
||||
|
||||
import * as twv from '#/utilities/tailwindVariants'
|
||||
|
||||
@ -74,7 +74,7 @@ export function Loader(props: LoaderProps) {
|
||||
|
||||
return (
|
||||
<div className={STYLES({ minHeight, className, color })}>
|
||||
<Spinner size={size} state={state} className="text-current" />
|
||||
<StatelessSpinner size={size} state={state} className="text-current" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ export default function ButtonRow(props: ButtonRowProps) {
|
||||
<FocusArea direction="horizontal">
|
||||
{innerProps => (
|
||||
<div
|
||||
className={tailwindMerge.twMerge('relative flex gap-buttons', positionClass)}
|
||||
className={tailwindMerge.twMerge('gap-buttons relative flex', positionClass)}
|
||||
{...innerProps}
|
||||
>
|
||||
{children}
|
||||
|
@ -7,12 +7,12 @@ import * as textProvider from '#/providers/TextProvider'
|
||||
import type * as assetEvent from '#/events/assetEvent'
|
||||
import type * as assetListEvent from '#/events/assetListEvent'
|
||||
|
||||
import AssetProjectSessions from '#/layouts/AssetProjectSessions'
|
||||
import AssetProperties from '#/layouts/AssetProperties'
|
||||
import AssetVersions from '#/layouts/AssetVersions/AssetVersions'
|
||||
import type Category from '#/layouts/CategorySwitcher/Category'
|
||||
|
||||
import * as ariaComponents from '#/components/AriaComponents'
|
||||
import HorizontalMenuBar from '#/components/styled/HorizontalMenuBar'
|
||||
|
||||
import * as backendModule from '#/services/Backend'
|
||||
import type Backend from '#/services/Backend'
|
||||
@ -30,6 +30,7 @@ import * as tailwindMerge from '#/utilities/tailwindMerge'
|
||||
enum AssetPanelTab {
|
||||
properties = 'properties',
|
||||
versions = 'versions',
|
||||
projectSessions = 'projectSessions',
|
||||
}
|
||||
|
||||
// ============================
|
||||
@ -85,6 +86,11 @@ export default function AssetPanel(props: AssetPanelProps) {
|
||||
savedTab === AssetPanelTab.versions
|
||||
) {
|
||||
return AssetPanelTab.properties
|
||||
} else if (
|
||||
item?.item.type !== backendModule.AssetType.project &&
|
||||
savedTab === AssetPanelTab.projectSessions
|
||||
) {
|
||||
return AssetPanelTab.properties
|
||||
} else {
|
||||
return savedTab
|
||||
}
|
||||
@ -110,16 +116,16 @@ export default function AssetPanel(props: AssetPanelProps) {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<HorizontalMenuBar className="mt-4">
|
||||
<ariaComponents.ButtonGroup className="mt-4 grow-0 basis-8">
|
||||
{item != null &&
|
||||
item.item.type !== backendModule.AssetType.secret &&
|
||||
item.item.type !== backendModule.AssetType.directory && (
|
||||
<ariaComponents.Button
|
||||
size="custom"
|
||||
variant="custom"
|
||||
size="medium"
|
||||
variant="ghost"
|
||||
className={tailwindMerge.twMerge(
|
||||
'button pointer-events-auto h-8 select-none bg-frame px-button-x leading-cozy transition-colors hover:bg-primary/[8%]',
|
||||
tab === AssetPanelTab.versions && 'bg-primary/[8%] active'
|
||||
'pointer-events-auto disabled:opacity-100',
|
||||
tab === AssetPanelTab.versions && 'bg-white opacity-100'
|
||||
)}
|
||||
onPress={() => {
|
||||
setTab(oldTab =>
|
||||
@ -132,7 +138,29 @@ export default function AssetPanel(props: AssetPanelProps) {
|
||||
{getText('versions')}
|
||||
</ariaComponents.Button>
|
||||
)}
|
||||
</HorizontalMenuBar>
|
||||
{item != null && item.item.type === backendModule.AssetType.project && (
|
||||
<ariaComponents.Button
|
||||
size="medium"
|
||||
variant="ghost"
|
||||
isDisabled={tab === AssetPanelTab.projectSessions}
|
||||
className={tailwindMerge.twMerge(
|
||||
'pointer-events-auto disabled:opacity-100',
|
||||
tab === AssetPanelTab.projectSessions && 'bg-white opacity-100'
|
||||
)}
|
||||
onPress={() => {
|
||||
setTab(oldTab =>
|
||||
oldTab === AssetPanelTab.projectSessions
|
||||
? AssetPanelTab.properties
|
||||
: AssetPanelTab.projectSessions
|
||||
)
|
||||
}}
|
||||
>
|
||||
{getText('projectSessions')}
|
||||
</ariaComponents.Button>
|
||||
)}
|
||||
{/* Spacing. The top right asset and user bars overlap this area. */}
|
||||
<div className="grow" />
|
||||
</ariaComponents.ButtonGroup>
|
||||
{item == null || setItem == null || backend == null ? (
|
||||
<div className="grid grow place-items-center text-lg">
|
||||
{getText('selectExactlyOneAssetToViewItsDetails')}
|
||||
@ -156,6 +184,10 @@ export default function AssetPanel(props: AssetPanelProps) {
|
||||
dispatchAssetListEvent={dispatchAssetListEvent}
|
||||
/>
|
||||
)}
|
||||
{tab === AssetPanelTab.projectSessions &&
|
||||
item.type === backendModule.AssetType.project && (
|
||||
<AssetProjectSessions backend={backend} item={item} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
@ -0,0 +1,55 @@
|
||||
/** @file Displays information describing a specific version of an asset. */
|
||||
import * as React from 'react'
|
||||
|
||||
import LogsIcon from 'enso-assets/logs.svg'
|
||||
|
||||
import * as textProvider from '#/providers/TextProvider'
|
||||
|
||||
import * as ariaComponents from '#/components/AriaComponents'
|
||||
import Button from '#/components/styled/Button'
|
||||
|
||||
import ProjectLogsModal from '#/modals/ProjectLogsModal'
|
||||
|
||||
import type * as backendModule from '#/services/Backend'
|
||||
import type Backend from '#/services/Backend'
|
||||
|
||||
import * as dateTime from '#/utilities/dateTime'
|
||||
|
||||
// ===========================
|
||||
// === AssetProjectSession ===
|
||||
// ===========================
|
||||
|
||||
/** Props for a {@link AssetProjectSession}. */
|
||||
export interface AssetProjectSessionProps {
|
||||
readonly backend: Backend
|
||||
readonly project: backendModule.ProjectAsset
|
||||
readonly projectSession: backendModule.ProjectSession
|
||||
}
|
||||
|
||||
/** Displays information describing a specific version of an asset. */
|
||||
export default function AssetProjectSession(props: AssetProjectSessionProps) {
|
||||
const { backend, project, projectSession } = props
|
||||
const { getText } = textProvider.useText()
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-1 shrink-0 select-none flex-row gap-4 rounded-2xl p-2">
|
||||
<div className="flex flex-1 flex-col">
|
||||
<time className="text-xs">
|
||||
{dateTime.formatDateTime(new Date(projectSession.createdAt))}
|
||||
</time>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<ariaComponents.DialogTrigger isOpen={isOpen} onOpenChange={setIsOpen}>
|
||||
<Button active image={LogsIcon} alt={getText('showLogs')} onPress={() => {}} />
|
||||
<ProjectLogsModal
|
||||
isOpen={isOpen}
|
||||
backend={backend}
|
||||
projectSessionId={projectSession.projectSessionId}
|
||||
projectTitle={project.title}
|
||||
/>
|
||||
</ariaComponents.DialogTrigger>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
/** @file A list of previous versions of an asset. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as reactQuery from '@tanstack/react-query'
|
||||
|
||||
import AssetProjectSession from '#/layouts/AssetProjectSession'
|
||||
|
||||
import * as loader from '#/components/Loader'
|
||||
|
||||
import type * as backendModule from '#/services/Backend'
|
||||
import type Backend from '#/services/Backend'
|
||||
|
||||
import type AssetTreeNode from '#/utilities/AssetTreeNode'
|
||||
|
||||
// ============================
|
||||
// === AssetProjectSessions ===
|
||||
// ============================
|
||||
|
||||
/** Props for a {@link AssetProjectSessions}. */
|
||||
export interface AssetProjectSessionsProps {
|
||||
readonly backend: Backend
|
||||
readonly item: AssetTreeNode<backendModule.ProjectAsset>
|
||||
}
|
||||
|
||||
/** A list of previous versions of an asset. */
|
||||
export default function AssetProjectSessions(props: AssetProjectSessionsProps) {
|
||||
return (
|
||||
<React.Suspense fallback={<loader.Loader />}>
|
||||
<AssetProjectSessionsInternal {...props} />
|
||||
</React.Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
// ====================================
|
||||
// === AssetProjectSessionsInternal ===
|
||||
// ====================================
|
||||
|
||||
/** Props for a {@link AssetProjectSessionsInternal}. */
|
||||
interface AssetProjectSessionsInternalProps extends AssetProjectSessionsProps {}
|
||||
|
||||
/** A list of previous versions of an asset. */
|
||||
function AssetProjectSessionsInternal(props: AssetProjectSessionsInternalProps) {
|
||||
const { backend, item } = props
|
||||
|
||||
const projectSessionsQuery = reactQuery.useSuspenseQuery({
|
||||
queryKey: ['getProjectSessions', item.item.id, item.item.title],
|
||||
queryFn: async () => {
|
||||
const sessions = await backend.listProjectSessions(item.item.id, item.item.title)
|
||||
return [...sessions].reverse()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto flex flex-col items-center overflow-y-auto overflow-x-hidden">
|
||||
{projectSessionsQuery.data.map(session => (
|
||||
<AssetProjectSession
|
||||
key={session.projectSessionId}
|
||||
backend={backend}
|
||||
project={item.item}
|
||||
projectSession={session}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -184,7 +184,7 @@ export default function AssetProperties(props: AssetPropertiesProps) {
|
||||
}}
|
||||
className="-m-multiline-input-p w-full resize-none rounded-input bg-frame p-multiline-input"
|
||||
/>
|
||||
<div className="flex gap-buttons">
|
||||
<div className="flex gap-2">
|
||||
<ariaComponents.Button
|
||||
size="custom"
|
||||
variant="custom"
|
||||
@ -259,7 +259,7 @@ export default function AssetProperties(props: AssetPropertiesProps) {
|
||||
setValue={setEditedDatalinkValue}
|
||||
/>
|
||||
{canEditThisAsset && (
|
||||
<div className="flex gap-buttons">
|
||||
<div className="flex gap-2">
|
||||
<ariaComponents.Button
|
||||
size="custom"
|
||||
variant="custom"
|
||||
|
@ -82,7 +82,7 @@ function Tags(props: InternalTagsProps) {
|
||||
return (
|
||||
<div
|
||||
data-testid="asset-search-tag-names"
|
||||
className="pointer-events-auto flex flex-wrap gap-buttons whitespace-nowrap px-search-suggestions"
|
||||
className="pointer-events-auto flex flex-wrap gap-2 whitespace-nowrap px-search-suggestions"
|
||||
>
|
||||
{(isCloud ? AssetQuery.tagNames : AssetQuery.localTagNames).flatMap(entry => {
|
||||
const [key, tag] = entry
|
||||
@ -316,7 +316,7 @@ export default function AssetSearchBar(props: AssetSearchBarProps) {
|
||||
{isCloud && labels.length !== 0 && (
|
||||
<div
|
||||
data-testid="asset-search-labels"
|
||||
className="pointer-events-auto flex gap-buttons p-search-suggestions"
|
||||
className="pointer-events-auto flex gap-2 p-search-suggestions"
|
||||
>
|
||||
{[...labels]
|
||||
.sort((a, b) => string.compareCaseInsensitive(a.value, b.value))
|
||||
|
@ -9,7 +9,7 @@ import * as textProvider from '#/providers/TextProvider'
|
||||
|
||||
import type * as assetListEvent from '#/events/assetListEvent'
|
||||
|
||||
import AssetVersion from '#/layouts/AssetVersion'
|
||||
import AssetVersion from '#/layouts/AssetVersions/AssetVersion'
|
||||
import * as useAssetVersions from '#/layouts/AssetVersions/useAssetVersions'
|
||||
|
||||
import Spinner from '#/components/Spinner'
|
||||
|
@ -28,7 +28,7 @@ export default function DeleteUserAccountSettingsSection() {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
className="flex flex-col items-start gap-settings-section-header rounded-2.5xl border-2 border-danger px-[1rem] pb-[0.9375rem] pt-[0.5625rem]"
|
||||
>
|
||||
<div className="flex gap-buttons">
|
||||
<div className="flex gap-2">
|
||||
<ariaComponents.Button
|
||||
size="medium"
|
||||
variant="delete"
|
||||
|
@ -99,7 +99,7 @@ export default function KeyboardShortcutsTable(props: KeyboardShortcutsTableProp
|
||||
{/* I don't know why this padding is needed,
|
||||
* given that this is a flex container. */}
|
||||
{/* eslint-disable-next-line no-restricted-syntax */}
|
||||
<div className="flex items-center gap-buttons pr-4">
|
||||
<div className="gap-buttons flex items-center pr-4">
|
||||
{info.bindings.map((binding, j) => (
|
||||
<div
|
||||
key={j}
|
||||
|
@ -86,7 +86,7 @@ export default function EditAssetDescriptionModal(props: EditAssetDescriptionMod
|
||||
|
||||
{error && <div className="relative text-sm text-red-500">{error.message}</div>}
|
||||
|
||||
<div className="relative flex gap-buttons">
|
||||
<div className="relative flex gap-2">
|
||||
<ariaComponents.Button variant="submit" type="submit" loading={isPending}>
|
||||
{actionButtonLabel}
|
||||
</ariaComponents.Button>
|
||||
|
@ -0,0 +1,60 @@
|
||||
/** @file A modal for showing logs for a project. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as reactQuery from '@tanstack/react-query'
|
||||
|
||||
import * as textProvider from '#/providers/TextProvider'
|
||||
|
||||
import * as ariaComponents from '#/components/AriaComponents'
|
||||
|
||||
import type * as backendModule from '#/services/Backend'
|
||||
import type Backend from '#/services/Backend'
|
||||
|
||||
// ========================
|
||||
// === ProjectLogsModal ===
|
||||
// ========================
|
||||
|
||||
/** Props for a {@link ProjectLogsModal}. */
|
||||
export interface ProjectLogsModalProps {
|
||||
readonly isOpen: boolean
|
||||
readonly backend: Backend
|
||||
readonly projectSessionId: backendModule.ProjectSessionId
|
||||
readonly projectTitle: string
|
||||
}
|
||||
|
||||
/** A modal for showing logs for a project. */
|
||||
export default function ProjectLogsModal(props: ProjectLogsModalProps) {
|
||||
const { isOpen } = props
|
||||
const { getText } = textProvider.useText()
|
||||
|
||||
return (
|
||||
<ariaComponents.Dialog title={getText('logs')} type="fullscreen">
|
||||
{isOpen && <ProjectLogsModalInternal {...props} />}
|
||||
</ariaComponents.Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
// ================================
|
||||
// === ProjectLogsModalInternal ===
|
||||
// ================================
|
||||
|
||||
/** Props for a {@link ProjectLogsModalInternal}. */
|
||||
interface ProjectLogsModalInternalProps extends ProjectLogsModalProps {}
|
||||
|
||||
/** A modal for showing logs for a project. */
|
||||
function ProjectLogsModalInternal(props: ProjectLogsModalInternalProps) {
|
||||
const { backend, projectSessionId, projectTitle } = props
|
||||
const logsQuery = reactQuery.useSuspenseQuery({
|
||||
queryKey: ['projectLogs', { projectSessionId, projectTitle }],
|
||||
queryFn: async () => {
|
||||
const logs = await backend.getProjectSessionLogs(projectSessionId, projectTitle)
|
||||
return logs.join('\n')
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<pre className="relative overflow-auto whitespace-pre-wrap">
|
||||
<code>{logsQuery.data}</code>
|
||||
</pre>
|
||||
)
|
||||
}
|
@ -51,6 +51,10 @@ export const FileId = newtype.newtypeConstructor<FileId>()
|
||||
export type SecretId = newtype.Newtype<string, 'SecretId'>
|
||||
export const SecretId = newtype.newtypeConstructor<SecretId>()
|
||||
|
||||
/** Unique identifier for a project session. */
|
||||
export type ProjectSessionId = newtype.Newtype<string, 'ProjectSessionId'>
|
||||
export const ProjectSessionId = newtype.newtypeConstructor<ProjectSessionId>()
|
||||
|
||||
/** Unique identifier for a Datalink. */
|
||||
export type DatalinkId = newtype.Newtype<string, 'DatalinkId'>
|
||||
export const DatalinkId = newtype.newtypeConstructor<DatalinkId>()
|
||||
@ -301,6 +305,15 @@ export interface ProjectStartupInfo {
|
||||
readonly accessToken: string | null
|
||||
}
|
||||
|
||||
/** A specific session of a project being opened and used. */
|
||||
export interface ProjectSession {
|
||||
readonly projectId: ProjectId
|
||||
readonly projectSessionId: ProjectSessionId
|
||||
readonly createdAt: dateTime.Rfc3339DateTime
|
||||
readonly closedAt?: dateTime.Rfc3339DateTime
|
||||
readonly userEmail: EmailAddress
|
||||
}
|
||||
|
||||
/** Metadata describing the location of an uploaded file. */
|
||||
export interface FileLocator {
|
||||
readonly fileId: FileId
|
||||
@ -1352,6 +1365,8 @@ export default abstract class Backend {
|
||||
abstract createProject(body: CreateProjectRequestBody): Promise<CreatedProject>
|
||||
/** Close a project. */
|
||||
abstract closeProject(projectId: ProjectId, title: string): Promise<void>
|
||||
/** Return a list of sessions for the current project. */
|
||||
abstract listProjectSessions(projectId: ProjectId, title: string): Promise<ProjectSession[]>
|
||||
/** Restore a project from a different version. */
|
||||
abstract restoreProject(
|
||||
projectId: ProjectId,
|
||||
@ -1370,6 +1385,11 @@ export default abstract class Backend {
|
||||
directoryId: DirectoryId | null,
|
||||
title: string
|
||||
): Promise<Project>
|
||||
/** Return Language Server logs for a project session. */
|
||||
abstract getProjectSessionLogs(
|
||||
projectSessionId: ProjectSessionId,
|
||||
title: string
|
||||
): Promise<string[]>
|
||||
/** Set a project to an open state. */
|
||||
abstract openProject(
|
||||
projectId: ProjectId,
|
||||
|
@ -636,6 +636,16 @@ export default class LocalBackend extends Backend {
|
||||
return this.invalidOperation()
|
||||
}
|
||||
|
||||
/** Invalid operation. */
|
||||
override listProjectSessions() {
|
||||
return this.invalidOperation()
|
||||
}
|
||||
|
||||
/** Invalid operation. */
|
||||
override getProjectSessionLogs() {
|
||||
return this.invalidOperation()
|
||||
}
|
||||
|
||||
/** Invalid operation. */
|
||||
override getFileContent() {
|
||||
return this.invalidOperation()
|
||||
|
@ -655,6 +655,22 @@ export default class RemoteBackend extends Backend {
|
||||
}
|
||||
}
|
||||
|
||||
/** List project sessions for a specific project.
|
||||
* @throws An error if a non-successful status code (not 200-299) was received. */
|
||||
override async listProjectSessions(
|
||||
projectId: backend.ProjectId,
|
||||
title: string
|
||||
): Promise<backend.ProjectSession[]> {
|
||||
const paramsString = new URLSearchParams({ projectId }).toString()
|
||||
const path = `${remoteBackendPaths.LIST_PROJECT_SESSIONS_PATH}?${paramsString}`
|
||||
const response = await this.get<backend.ProjectSession[]>(path)
|
||||
if (!responseIsSuccessful(response)) {
|
||||
return await this.throw(response, 'listProjectSessionsBackendError', title)
|
||||
} else {
|
||||
return await response.json()
|
||||
}
|
||||
}
|
||||
|
||||
/** Return details for a project.
|
||||
* @throws An error if a non-successful status code (not 200-299) was received. */
|
||||
override async getProjectDetails(
|
||||
@ -680,6 +696,21 @@ export default class RemoteBackend extends Backend {
|
||||
}
|
||||
}
|
||||
|
||||
/** Return Language Server logs for a project session.
|
||||
* @throws An error if a non-successful status code (not 200-299) was received. */
|
||||
override async getProjectSessionLogs(
|
||||
projectSessionId: backend.ProjectSessionId,
|
||||
title: string
|
||||
): Promise<string[]> {
|
||||
const path = remoteBackendPaths.getProjectSessionLogsPath(projectSessionId)
|
||||
const response = await this.get<string[]>(path)
|
||||
if (!responseIsSuccessful(response)) {
|
||||
return await this.throw(response, 'getProjectLogsBackendError', title)
|
||||
} else {
|
||||
return await response.json()
|
||||
}
|
||||
}
|
||||
|
||||
/** Prepare a project for execution.
|
||||
* @throws An error if a non-successful status code (not 200-299) was received. */
|
||||
override async openProject(
|
||||
|
@ -51,6 +51,8 @@ export const UPLOAD_FILE_PATH = 'files'
|
||||
export const CREATE_SECRET_PATH = 'secrets'
|
||||
/** Relative HTTP path to the "list secrets" endpoint of the Cloud backend API. */
|
||||
export const LIST_SECRETS_PATH = 'secrets'
|
||||
/** Relative HTTP path to the "list project sessions" endpoint of the Cloud backend API. */
|
||||
export const LIST_PROJECT_SESSIONS_PATH = 'project-sessions'
|
||||
/** Relative HTTP path to the "create datalink" endpoint of the Cloud backend API. */
|
||||
export const CREATE_DATALINK_PATH = 'datalinks'
|
||||
/** Relative HTTP path to the "create tag" endpoint of the Cloud backend API. */
|
||||
@ -108,6 +110,10 @@ export function closeProjectPath(projectId: backend.ProjectId) {
|
||||
export function getProjectDetailsPath(projectId: backend.ProjectId) {
|
||||
return `projects/${projectId}`
|
||||
}
|
||||
/** Relative HTTP path to the "get project logs" endpoint of the Cloud backend API. */
|
||||
export function getProjectSessionLogsPath(projectSessionId: backend.ProjectSessionId) {
|
||||
return `project-sessions/${projectSessionId}/logs`
|
||||
}
|
||||
/** Relative HTTP path to the "duplicate project" endpoint of the Cloud backend API. */
|
||||
export function duplicateProjectPath(projectId: backend.ProjectId) {
|
||||
return `projects/${projectId}/versions/clone`
|
||||
|
@ -30,7 +30,8 @@
|
||||
* current plan. */
|
||||
--missing-functionality-text-padding-x: 1rem;
|
||||
/* The horizontal gap between each icon in a list of buttons. */
|
||||
--buttons-gap: 0.5rem; /* The horizontal gap between each icon in a list of icons. */
|
||||
--buttons-gap: 0.5rem;
|
||||
/* The horizontal gap between each icon in a list of icons. */
|
||||
--icons-gap: 0.75rem;
|
||||
/* The gap between an icon and its associated text. */
|
||||
--samples-icon-with-text-gap: 0.375rem;
|
||||
|
@ -97,7 +97,6 @@
|
||||
"deleteInvitationBackendError": "Failed to delete invitation, please try again later",
|
||||
"resendInvitationBackendError": "Failed to resend the invitation, please try again later",
|
||||
"createPermissionBackendError": "Could not set permissions",
|
||||
"getMeBackendError": "Could not get user details",
|
||||
"listFolderBackendError": "Could not list folder '$0'",
|
||||
"listRootFolderBackendError": "Could not list root folder",
|
||||
"createFolderBackendError": "Could not create folder '$0'",
|
||||
@ -109,11 +108,13 @@
|
||||
"undoDeleteAssetBackendError": "Could not restore '$0' from Trash",
|
||||
"copyAssetBackendError": "Could not copy '$0' to '$1'",
|
||||
"listProjectsBackendError": "Could not list projects",
|
||||
"createProjectBackendError": "Could not create project with name '$0",
|
||||
"restoreProjectBackendError": "Could not restore project '$0",
|
||||
"duplicateProjectBackendError": "Could not duplicate project as '$0",
|
||||
"createProjectBackendError": "Could not create project with name '$0'",
|
||||
"restoreProjectBackendError": "Could not restore project '$0'",
|
||||
"duplicateProjectBackendError": "Could not duplicate project as '$0'",
|
||||
"closeProjectBackendError": "Could not close project '$0'",
|
||||
"listProjectSessionsBackendError": "Could not list sessions for project '$0'",
|
||||
"getProjectDetailsBackendError": "Could not get details of project '$0'",
|
||||
"getProjectLogsBackendError": "Could not get logs for project '$0'",
|
||||
"openProjectBackendError": "Could not open project '$0'",
|
||||
"openProjectMissingCredentialsBackendError": "Could not open project '$0': Missing credentials",
|
||||
"updateProjectBackendError": "Could not update project '$0'",
|
||||
@ -196,6 +197,9 @@
|
||||
"reset": "Reset",
|
||||
"members": "Members",
|
||||
"drop": "Drop",
|
||||
"projectSessions": "Sessions",
|
||||
"logs": "Logs",
|
||||
"showLogs": "Show Logs",
|
||||
"accept": "Accept",
|
||||
"reject": "Reject",
|
||||
"clearTrash": "Clear Trash",
|
||||
|
@ -85,7 +85,9 @@ interface PlaceholderOverrides {
|
||||
readonly restoreProjectBackendError: [string]
|
||||
readonly duplicateProjectBackendError: [string]
|
||||
readonly closeProjectBackendError: [string]
|
||||
readonly listProjectSessionsBackendError: [string]
|
||||
readonly getProjectDetailsBackendError: [string]
|
||||
readonly getProjectLogsBackendError: [string]
|
||||
readonly openProjectBackendError: [string]
|
||||
readonly openProjectMissingCredentialsBackendError: [string]
|
||||
readonly updateProjectBackendError: [string]
|
||||
|
@ -26,7 +26,7 @@ export default /** @satisfies {import('tailwindcss').Config} */ ({
|
||||
'selected-frame': 'rgb(255 255 255 / 70%)',
|
||||
'ide-bg': '#ebeef1',
|
||||
selected: 'rgb(255 255 255 / 40%)',
|
||||
'not-selected': 'rgb(0 0 0 / 15%)',
|
||||
'not-selected': 'rgb(0 0 0 / 30%)',
|
||||
// Should be `#3e515f14`, but `bg-opacity` does not work with RGBA.
|
||||
label: '#f0f1f3',
|
||||
help: '#3f68ce',
|
||||
@ -200,7 +200,6 @@ export default /** @satisfies {import('tailwindcss').Config} */ ({
|
||||
gap: {
|
||||
modal: 'var(--modal-gap)',
|
||||
subheading: 'var(--subheading-gap)',
|
||||
buttons: 'var(--buttons-gap)',
|
||||
icons: 'var(--icons-gap)',
|
||||
colors: 'var(--colors-gap)',
|
||||
'samples-icon-with-text': 'var(--samples-icon-with-text-gap)',
|
||||
|
Loading…
Reference in New Issue
Block a user