From 0516b2545b087ec37f644a16977e3da15254020e Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Wed, 19 Feb 2025 22:45:58 +0700 Subject: [PATCH 1/5] Allow TRANSACTOR_OVERRIDE in front service Signed-off-by: Andrey Sobolev --- pods/front/src/__start.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pods/front/src/__start.ts b/pods/front/src/__start.ts index 9a19915188..69c05de9a1 100644 --- a/pods/front/src/__start.ts +++ b/pods/front/src/__start.ts @@ -48,5 +48,6 @@ startFront(metricsContext, { AI_URL: process.env.AI_URL, TELEGRAM_BOT_URL: process.env.TELEGRAM_BOT_URL, STATS_URL: process.env.STATS_API ?? process.env.STATS_URL, - BACKUP_URL: process.env.BACKUP_URL + BACKUP_URL: process.env.BACKUP_URL, + TRANSACTOR_OVERRIDE: process.env.TRANSACTOR_OVERRIDE }) From 9e202352ebe801d726ad80e6433e06d77b400ae4 Mon Sep 17 00:00:00 2001 From: Alexey Zinoviev Date: Thu, 20 Feb 2025 09:00:36 +0400 Subject: [PATCH 2/5] uberf-9492: allow restricting hostnames for print service (#8059) Signed-off-by: Alexey Zinoviev --- services/print/pod-print/src/config.ts | 6 +++++- services/print/pod-print/src/main.ts | 2 +- services/print/pod-print/src/server.ts | 16 +++++++++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/services/print/pod-print/src/config.ts b/services/print/pod-print/src/config.ts index 26de472d07..37bac22e3c 100644 --- a/services/print/pod-print/src/config.ts +++ b/services/print/pod-print/src/config.ts @@ -5,14 +5,18 @@ export interface Config { Port: number Secret: string + AllowedHostnames: string[] } const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined) const config: Config = (() => { + const allowedHostnames = process.env.ALLOWED_HOSTNAMES + const params: Partial = { Port: parseNumber(process.env.PORT) ?? 4005, - Secret: process.env.SECRET + Secret: process.env.SECRET, + AllowedHostnames: allowedHostnames == null ? [] : allowedHostnames.split(',') } const missingEnv = (Object.keys(params) as Array).filter((key) => params[key] === undefined) diff --git a/services/print/pod-print/src/main.ts b/services/print/pod-print/src/main.ts index bf77e34e98..5a6894a2d8 100644 --- a/services/print/pod-print/src/main.ts +++ b/services/print/pod-print/src/main.ts @@ -17,7 +17,7 @@ export const main = async (): Promise => { setupMetadata() const storageConfig = storageConfigFromEnv() - const { app, close } = createServer(storageConfig) + const { app, close } = createServer(storageConfig, config.AllowedHostnames) const server = listen(app, config.Port) const shutdown = (): void => { diff --git a/services/print/pod-print/src/server.ts b/services/print/pod-print/src/server.ts index 3d19d011d0..2ee4b349f0 100644 --- a/services/print/pod-print/src/server.ts +++ b/services/print/pod-print/src/server.ts @@ -110,9 +110,13 @@ const wrapRequest = (fn: AsyncRequestHandler) => (req: Request, res: Response, n handleRequest(fn, req, res, next) } -export function createServer (storageConfig: StorageConfiguration): { app: Express, close: () => void } { +export function createServer ( + storageConfig: StorageConfiguration, + allowedHostnames: string[] +): { app: Express, close: () => void } { const storageAdapter = buildStorageFromConfig(storageConfig) const measureCtx = initStatisticsContext('print', {}) + const whitelistedHostnames = allowedHostnames.length > 0 ? new Set(allowedHostnames) : null const app = express() app.use(cors()) @@ -124,6 +128,16 @@ export function createServer (storageConfig: StorageConfiguration): { app: Expre const rawlink = req.query.link as string const link = decodeURIComponent(rawlink) + // Verify that link is from the same host and protocol is among the allowed + const url = new URL(link) + if ( + !['http:', 'https:'].includes(url.protocol) || + (whitelistedHostnames != null && !whitelistedHostnames.has(url.hostname)) + ) { + console.error(`Rejected processing unexpected link: ${link}. Token: ${JSON.stringify(token)}`) + throw new ApiError(403, 'Cannot process provided link') + } + const kind = req.query.kind as PrintOptions['kind'] if (kind !== undefined && !validKinds.includes(kind as any)) { From 3cc324da77f7c63e88fa0703dd008f1b2c0b7f24 Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Thu, 20 Feb 2025 22:28:59 +0700 Subject: [PATCH 3/5] UBERF-9496 (#8061) Signed-off-by: Andrey Sobolev --- packages/text/src/markdown/serializer.ts | 9 +- server/server/src/client.ts | 5 +- server/server/src/utils.ts | 61 +-------- services/github/pod-github/src/platform.ts | 32 +++-- .../github/pod-github/src/sync/issueBase.ts | 12 +- .../github/pod-github/src/sync/projects.ts | 2 +- services/github/pod-github/src/sync/utils.ts | 7 +- services/github/pod-github/src/worker.ts | 117 ++++++++++-------- 8 files changed, 114 insertions(+), 131 deletions(-) diff --git a/packages/text/src/markdown/serializer.ts b/packages/text/src/markdown/serializer.ts index b3c7b9695c..a78d8572db 100644 --- a/packages/text/src/markdown/serializer.ts +++ b/packages/text/src/markdown/serializer.ts @@ -466,7 +466,11 @@ export class MarkdownState implements IState { for (let i = 0; i < len; i++) { const mark = state.marks[i] - if (!this.marks[mark.type].mixable) break + const mm = this.marks[mark.type] + if (mm == null) { + break + } + if (!mm.mixable) break this.reorderMixableMark(state, mark, i, len) } } @@ -735,6 +739,9 @@ export class MarkdownState implements IState { let value = mark.attrs?.marker if (value === undefined) { const info = this.marks[mark.type] + if (info == null) { + throw new Error(`No info for mark ${mark.type}`) + } value = open ? info.open : info.close } return typeof value === 'string' ? value : value(this, mark, parent, index) ?? '' diff --git a/server/server/src/client.ts b/server/server/src/client.ts index b453c71a48..052b3d970a 100644 --- a/server/server/src/client.ts +++ b/server/server/src/client.ts @@ -47,7 +47,6 @@ import { type Workspace } from '@hcengineering/server-core' import { type Token } from '@hcengineering/server-token' -import { handleSend } from './utils' const useReserveContext = (process.env.USE_RESERVE_CTX ?? 'true') === 'true' @@ -222,9 +221,7 @@ export class ClientSession implements Session { this.useCompression ) } else { - void handleSend(ctx, socket, { result: tx }, 1024 * 1024, this.binaryMode, this.useCompression).catch((err) => { - ctx.error('failed to broadcast', err) - }) + socket.send(ctx, { result: tx }, this.binaryMode, this.useCompression) } } diff --git a/server/server/src/utils.ts b/server/server/src/utils.ts index 3725c6699b..429029d659 100644 --- a/server/server/src/utils.ts +++ b/server/server/src/utils.ts @@ -13,7 +13,7 @@ // limitations under the License. // -import { type FindResult, type MeasureContext } from '@hcengineering/core' +import { type MeasureContext } from '@hcengineering/core' import type { AddSessionActive, @@ -23,7 +23,6 @@ import type { Session } from '@hcengineering/server-core' -import { toFindResult } from '@hcengineering/core' import { type Response } from '@hcengineering/rpc' import type { Token } from '@hcengineering/server-token' @@ -80,64 +79,12 @@ export function processRequest ( } } -export async function sendResponse ( +export function sendResponse ( ctx: MeasureContext, session: Session, socket: ConnectionSocket, resp: Response ): Promise { - await handleSend(ctx, socket, resp, 1024 * 1024, session.binaryMode, session.useCompression) -} - -function waitNextTick (): Promise | undefined { - return new Promise((resolve) => { - setImmediate(resolve) - }) -} -export async function handleSend ( - ctx: MeasureContext, - ws: ConnectionSocket, - msg: Response, - chunkLimit: number, - useBinary: boolean, - useCompression: boolean -): Promise { - // ws.send(msg) - if (Array.isArray(msg.result) && msg.result.length > 1 && chunkLimit > 0) { - // Split and send by chunks - const data = [...msg.result] - - let cid = 1 - const dataSize = JSON.stringify(data).length - const avg = Math.round(dataSize / data.length) - const itemChunk = Math.round(chunkLimit / avg) + 1 - - while (data.length > 0 && !ws.isClosed) { - let itemChunkCurrent = itemChunk - if (data.length - itemChunk < itemChunk / 2) { - itemChunkCurrent = data.length - } - const chunk: FindResult = toFindResult(data.splice(0, itemChunkCurrent)) - if (data.length === 0) { - const orig = msg.result as FindResult - chunk.total = orig.total ?? 0 - chunk.lookupMap = orig.lookupMap - } - if (chunk !== undefined) { - ws.send( - ctx, - { ...msg, result: chunk, chunk: { index: cid, final: data.length === 0 } }, - useBinary, - useCompression - ) - } - cid++ - - if (data.length > 0 && !ws.isClosed) { - await waitNextTick() - } - } - } else { - ws.send(ctx, msg, useBinary, useCompression) - } + socket.send(ctx, resp, session.binaryMode, session.useCompression) + return Promise.resolve() } diff --git a/services/github/pod-github/src/platform.ts b/services/github/pod-github/src/platform.ts index 7e81679fb1..4de5d9befe 100644 --- a/services/github/pod-github/src/platform.ts +++ b/services/github/pod-github/src/platform.ts @@ -11,6 +11,7 @@ import core, { ClientConnectEvent, DocumentUpdate, isActiveMode, + isDeletingMode, MeasureContext, RateLimiter, Ref, @@ -712,33 +713,36 @@ export class PlatformWorker { return Array.from(workspaces) } - async checkWorkspaceIsActive (token: string, workspace: string): Promise { + async checkWorkspaceIsActive ( + token: string, + workspace: string + ): Promise<{ workspaceInfo: ClientWorkspaceInfo | undefined, needRecheck: boolean }> { let workspaceInfo: ClientWorkspaceInfo | undefined try { workspaceInfo = await getWorkspaceInfo(token) } catch (err: any) { this.ctx.error('Workspace not found:', { workspace }) - return + return { workspaceInfo: undefined, needRecheck: false } } if (workspaceInfo?.workspace === undefined) { this.ctx.error('No workspace exists for workspaceId', { workspace }) - return + return { workspaceInfo: undefined, needRecheck: false } + } + if (workspaceInfo?.disabled === true || isDeletingMode(workspaceInfo?.mode)) { + this.ctx.warn('Workspace is disabled', { workspace }) + return { workspaceInfo: undefined, needRecheck: false } } if (!isActiveMode(workspaceInfo?.mode)) { - this.ctx.warn('Workspace is in maitenance, skipping for now.', { workspace }) - return - } - if (workspaceInfo?.disabled === true) { - this.ctx.warn('Workspace is disabled', { workspace }) - return + this.ctx.warn('Workspace is in maitenance, skipping for now.', { workspace, mode: workspaceInfo?.mode }) + return { workspaceInfo: undefined, needRecheck: true } } const lastVisit = (Date.now() - workspaceInfo.lastVisit) / (3600 * 24 * 1000) // In days if (config.WorkspaceInactivityInterval > 0 && lastVisit > config.WorkspaceInactivityInterval) { this.ctx.warn('Workspace is inactive for too long, skipping for now.', { workspace }) - return + return { workspaceInfo: undefined, needRecheck: true } } - return workspaceInfo + return { workspaceInfo, needRecheck: true } } private async checkWorkspaces (): Promise { @@ -781,9 +785,11 @@ export class PlatformWorker { }, { mode: 'github' } ) - const workspaceInfo = await this.checkWorkspaceIsActive(token, workspace) + const { workspaceInfo, needRecheck } = await this.checkWorkspaceIsActive(token, workspace) if (workspaceInfo === undefined) { - errors++ + if (needRecheck) { + errors++ + } return } try { diff --git a/services/github/pod-github/src/sync/issueBase.ts b/services/github/pod-github/src/sync/issueBase.ts index db23087359..5e47588a31 100644 --- a/services/github/pod-github/src/sync/issueBase.ts +++ b/services/github/pod-github/src/sync/issueBase.ts @@ -497,7 +497,15 @@ export abstract class IssueSyncManagerBase { return (pField.node.options ?? []).find((it) => it.id === field.optionId) } - findOptionId (container: ContainerFocus, fieldId: string, value: string, target: IssueSyncTarget): string | undefined { + findOptionId ( + container: ContainerFocus, + fieldId: string, + value: string | null, + target: IssueSyncTarget + ): string | undefined { + if (value == null) { + return + } const structure = container.container.projectStructure.get(target.target._id) if (structure === undefined) { return @@ -506,7 +514,7 @@ export abstract class IssueSyncManagerBase { if (pField === undefined) { return undefined } - return (pField.node.options ?? []).find((it) => it.name.toLowerCase() === value.toLowerCase())?.id + return (pField.node.options ?? []).find((it) => it.name?.toLowerCase() === value.toLowerCase())?.id } async toPlatformField ( diff --git a/services/github/pod-github/src/sync/projects.ts b/services/github/pod-github/src/sync/projects.ts index d22cd0cb55..b6a991b47c 100644 --- a/services/github/pod-github/src/sync/projects.ts +++ b/services/github/pod-github/src/sync/projects.ts @@ -344,7 +344,7 @@ export class ProjectsSyncManager implements DocSyncManager { derivedClient: TxOperations, deleteExisting: boolean ): Promise { - return false + return true } async externalSync ( diff --git a/services/github/pod-github/src/sync/utils.ts b/services/github/pod-github/src/sync/utils.ts index 21ec147567..dd6d2c257c 100644 --- a/services/github/pod-github/src/sync/utils.ts +++ b/services/github/pod-github/src/sync/utils.ts @@ -127,6 +127,9 @@ export async function getSinceRaw ( export function gqlp (params: Record): string { let result = '' let first = true + function escape (str: string): string { + return str.replace(/"/g, '\\"') + } for (const [k, v] of Object.entries(params)) { if (v !== undefined) { if (!first) { @@ -136,9 +139,9 @@ export function gqlp (params: Record `"${it}"`).join(', ')}]` + result += `${k}: [${v.map((it) => `"${escape(it)}"`).join(', ')}]` } else { - result += `${k}: "${v}"` + result += `${k}: "${escape(v)}"` } } } diff --git a/services/github/pod-github/src/worker.ts b/services/github/pod-github/src/worker.ts index 318d9a9089..7c2531a452 100644 --- a/services/github/pod-github/src/worker.ts +++ b/services/github/pod-github/src/worker.ts @@ -226,7 +226,10 @@ export class GithubWorker implements IntegrationManager { } } - async getAccountU (user: User): Promise { + async getAccountU (user?: User): Promise { + if (user == null) { + return undefined + } return await this.getAccount({ id: user.node_id, login: user.login, @@ -1101,38 +1104,41 @@ export class GithubWorker implements IntegrationManager { this.ctx.error('Failed to perform full sync', { error: err }) }) } - - const { projects, repositories } = await this.collectActiveProjects() - if (projects.length === 0 && repositories.length === 0) { - await this.waitChanges() - continue - } - - // Check if we have documents with external sync request's pending. - const hadExternalChanges = await this.performExternalSync( - projects, - repositories, - 'externalVersion', - githubExternalSyncVersion - ) - const hadSyncChanges = await this.performSync(projects, repositories) - - // Perform derived operations - // Sync derived external data, like pull request reviews, files etc. - const hadDerivedChanges = await this.performExternalSync( - projects, - repositories, - 'derivedVersion', - githubDerivedSyncVersion - ) - - if (!hadExternalChanges && !hadSyncChanges && !hadDerivedChanges) { - if (this.previousWait !== 0) { - this.ctx.info('Wait for changes:', { previousWait: this.previousWait, workspace: this.workspace.name }) - this.previousWait = 0 + try { + const { projects, repositories } = await this.collectActiveProjects() + if (projects.length === 0 && repositories.length === 0) { + await this.waitChanges() + continue } - // Wait until some sync documents will be modified, updated. - await this.waitChanges() + + // Check if we have documents with external sync request's pending. + const hadExternalChanges = await this.performExternalSync( + projects, + repositories, + 'externalVersion', + githubExternalSyncVersion + ) + const hadSyncChanges = await this.performSync(projects, repositories) + + // Perform derived operations + // Sync derived external data, like pull request reviews, files etc. + const hadDerivedChanges = await this.performExternalSync( + projects, + repositories, + 'derivedVersion', + githubDerivedSyncVersion + ) + + if (!hadExternalChanges && !hadSyncChanges && !hadDerivedChanges) { + if (this.previousWait !== 0) { + this.ctx.info('Wait for changes:', { previousWait: this.previousWait, workspace: this.workspace.name }) + this.previousWait = 0 + } + // Wait until some sync documents will be modified, updated. + await this.waitChanges() + } + } catch (err: any) { + this.ctx.error('failed to perform sync', { err, workspace: this.workspace.name }) } } } @@ -1327,25 +1333,30 @@ export class GithubWorker implements IntegrationManager { const targetProject = await this.client.findOne(github.mixin.GithubProject, { _id: existing.space as Ref }) - if (await mapper.handleDelete(existing, info, derivedClient, false, parent)) { - const h = this._client.getHierarchy() - await derivedClient.remove(info) - if (h.hasMixin(existing, github.mixin.GithubIssue)) { - const mixinData = this._client.getHierarchy().as(existing, github.mixin.GithubIssue) - await this._client.update( - mixinData, - { - url: '', - githubNumber: 0, - repository: '' as Ref - }, - false, - Date.now(), - existing.modifiedBy - ) + try { + if (await mapper.handleDelete(existing, info, derivedClient, false, parent)) { + const h = this._client.getHierarchy() + await derivedClient.remove(info) + if (h.hasMixin(existing, github.mixin.GithubIssue)) { + const mixinData = this._client.getHierarchy().as(existing, github.mixin.GithubIssue) + await this._client.update( + mixinData, + { + url: '', + githubNumber: 0, + repository: '' as Ref + }, + false, + Date.now(), + existing.modifiedBy + ) + } + return } - return + } catch (err: any) { + this.ctx.error('failed to handle delete', { err }) } + if (targetProject !== undefined) { // We need to sync into new project. await derivedClient.update(info, { @@ -1361,8 +1372,12 @@ export class GithubWorker implements IntegrationManager { } if (info.deleted === true) { - if (await mapper.handleDelete(existing, info, derivedClient, true)) { - await derivedClient.remove(info) + try { + if (await mapper.handleDelete(existing, info, derivedClient, true)) { + await derivedClient.remove(info) + } + } catch (err: any) { + this.ctx.error('failed to handle delete', { err }) } return } From 8bbba7bc8af85e5117d46e8f2f2cb5f21456a7b3 Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Thu, 20 Feb 2025 21:43:19 +0500 Subject: [PATCH 4/5] Minor fixes (#8062) Signed-off-by: Denis Bykhov --- plugins/hr-assets/lang/ru.json | 6 +- .../issues/timereport/TimePresenter.svelte | 61 +++++++++---------- tests/sanity/tests/tracker/tracker.utils.ts | 2 +- 3 files changed, 32 insertions(+), 37 deletions(-) diff --git a/plugins/hr-assets/lang/ru.json b/plugins/hr-assets/lang/ru.json index fe24bea6db..323e6f1934 100644 --- a/plugins/hr-assets/lang/ru.json +++ b/plugins/hr-assets/lang/ru.json @@ -27,11 +27,11 @@ "NoEmployeesInDepartment": "Нет сотрудников в выбранном департаменте", "Vacation": "Отпуск", "Sick": "Больничный", - "PTO": "Отпуск", - "PTOs": "Отпускных дней", + "PTO": "Отгул(PTO)", + "PTOs": "Отгулов", "Remote": "Удаленно", "Overtime": "Переработка", - "PTO2": "Отпуск/2", + "PTO2": "Отгул(PTO)/2", "Overtime2": "Переработка/2", "EditRequest": "Редактировать {type}", "EditRequestType": "Редактировать тип", diff --git a/plugins/tracker-resources/src/components/issues/timereport/TimePresenter.svelte b/plugins/tracker-resources/src/components/issues/timereport/TimePresenter.svelte index cf31db36b4..9364b42829 100644 --- a/plugins/tracker-resources/src/components/issues/timereport/TimePresenter.svelte +++ b/plugins/tracker-resources/src/components/issues/timereport/TimePresenter.svelte @@ -13,9 +13,9 @@ // limitations under the License. --> @@ -63,13 +64,7 @@ class:link={kind === 'link'} class:fs-bold={accent} on:click - use:tooltip={{ - component: Label, - props: { - label: tracker.string.TimeSpendHours, - params: { value } - } - }} + use:tooltip={{ label: getEmbeddedLabel(label) }} > {label} diff --git a/tests/sanity/tests/tracker/tracker.utils.ts b/tests/sanity/tests/tracker/tracker.utils.ts index 7d87d0f70c..fede06cee5 100644 --- a/tests/sanity/tests/tracker/tracker.utils.ts +++ b/tests/sanity/tests/tracker/tracker.utils.ts @@ -294,7 +294,7 @@ export function convertEstimation (estimation: number | string): string { const days = Math.floor(value / hoursInWorkingDay) const hours = Math.floor(value % hoursInWorkingDay) - const minutes = Math.floor((value % 1) * 60) + const minutes = Math.round((value % 1) * 60) const result = [ ...(days === 0 ? [] : [`${days}d`]), ...(hours === 0 ? [] : [`${hours}h`]), From 84f0e530df47bf52a03368ffd3c5be182800e231 Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Thu, 20 Feb 2025 22:13:04 +0500 Subject: [PATCH 5/5] Lq fix (#8065) * query prefomance fixes Signed-off-by: Denis Bykhov * Fix Signed-off-by: Denis Bykhov * Fix Signed-off-by: Denis Bykhov --------- Signed-off-by: Denis Bykhov --- packages/query/src/index.ts | 1 + packages/query/src/refs.ts | 25 ++++++++++++++++++- .../src/components/SpaceHeader.svelte | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/query/src/index.ts b/packages/query/src/index.ts index 4f8cd2c5c0..536dfe664c 100644 --- a/packages/query/src/index.ts +++ b/packages/query/src/index.ts @@ -522,6 +522,7 @@ export class LiveQuery implements WithTx, Client { if (q.options?.lookup !== undefined) { options.lookup = q.options?.lookup } + const showArchived = shouldShowArchived(q.query, q.options) options.showArchived = showArchived diff --git a/packages/query/src/refs.ts b/packages/query/src/refs.ts index 51d9354f2a..a044270328 100644 --- a/packages/query/src/refs.ts +++ b/packages/query/src/refs.ts @@ -28,8 +28,10 @@ export class Refs { if (q.options?.projection !== undefined) { return } + const params = ':' + JSON.stringify(q.options?.lookup ?? {}) + ':' + JSON.stringify(q.options?.associations ?? {}) for (const d of docs) { - const classKey = Hierarchy.mixinOrClass(d) + ':' + JSON.stringify(q.options?.lookup ?? {}) + const classKey = Hierarchy.mixinOrClass(d) + params + let docMap = this.documentRefs.get(classKey) if (docMap === undefined) { if (clean) { @@ -90,6 +92,27 @@ export class Refs { return toFindResult(this.getHierarchy().clone([q[0]]), 1) } } + if (options.lookup === undefined && options.associations === undefined) { + const keys = Array.from(this.documentRefs.keys()) + for (const key of keys) { + if (key.startsWith(_class + ':')) { + const docs = this.documentRefs.get(key) + if (docs !== undefined) { + const _docs = Array.from(docs.values()).map((it) => it.doc) + + const q = matchQuery(_docs, query, _class, this.getHierarchy()) + if (q.length > 0) { + const clonedDoc = this.getHierarchy().clone(q[0]) + const { $lookup, $associations, ...clean } = clonedDoc + if (this.getHierarchy().isMixin(_class)) { + return toFindResult([this.getHierarchy().as(clean, _class)] as T[], 1) + } + return toFindResult([clean], 1) + } + } + } + } + } } return null } diff --git a/plugins/view-resources/src/components/SpaceHeader.svelte b/plugins/view-resources/src/components/SpaceHeader.svelte index 5574bd4358..fd6e6d655f 100644 --- a/plugins/view-resources/src/components/SpaceHeader.svelte +++ b/plugins/view-resources/src/components/SpaceHeader.svelte @@ -28,7 +28,7 @@ hideExtra={!$$slots.extra && modeSelectorProps === undefined} > - +