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:
somebody1234 2024-06-27 00:14:09 +10:00 committed by GitHub
parent 2638ba8d8e
commit 64d51c2020
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 332 additions and 41 deletions

View 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

View File

@ -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>

View File

@ -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
}

View File

@ -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
}

View File

@ -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()

View File

@ -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) }
: {})}

View File

@ -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>
)
}

View File

@ -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}

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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"

View File

@ -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))

View File

@ -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'

View File

@ -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"

View File

@ -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}

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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,

View File

@ -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()

View File

@ -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(

View File

@ -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`

View File

@ -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;

View File

@ -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",

View File

@ -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]

View File

@ -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)',