UBERF-5511: Fix query and include ibm plex mono (#4764)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-02-25 13:17:49 +07:00 committed by GitHub
parent 88b91d7ce0
commit 804f514f5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 162 additions and 72 deletions

1
.gitignore vendored
View File

@ -9,6 +9,7 @@ temp/
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
tests/sanity/screenshots
# Runtime data # Runtime data
*.pid *.pid

View File

@ -512,6 +512,9 @@ function getInNiN (query1: any, query2: any): any {
} }
export function cutObjectArray (obj: any): any { export function cutObjectArray (obj: any): any {
if (obj == null) {
return obj
}
const r = {} const r = {}
for (const key of Object.keys(obj)) { for (const key of Object.keys(obj)) {
if (Array.isArray(obj[key])) { if (Array.isArray(obj[key])) {

View File

@ -132,7 +132,7 @@ async function performESBuild(filesToTranspile) {
minify: false, minify: false,
outdir: 'lib', outdir: 'lib',
keepNames: true, keepNames: true,
sourcemap: 'external', sourcemap: 'inline',
allowOverwrite: true, allowOverwrite: true,
format: 'cjs', format: 'cjs',
plugins: [ plugins: [

View File

@ -69,6 +69,8 @@ interface Query {
options?: FindOptions<Doc> options?: FindOptions<Doc>
total: number total: number
callbacks: Map<string, Callback> callbacks: Map<string, Callback>
requested?: Promise<void>
} }
/** /**
@ -105,7 +107,7 @@ export class LiveQuery implements WithTx, Client {
for (const q of [...this.queue]) { for (const q of [...this.queue]) {
if (!this.removeFromQueue(q)) { if (!this.removeFromQueue(q)) {
try { try {
await this.refresh(q) void this.refresh(q)
} catch (err: any) { } catch (err: any) {
if (err instanceof PlatformError) { if (err instanceof PlatformError) {
if (err.message === 'connection closed') { if (err.message === 'connection closed') {
@ -119,7 +121,7 @@ export class LiveQuery implements WithTx, Client {
for (const v of this.queries.values()) { for (const v of this.queries.values()) {
for (const q of v) { for (const q of v) {
try { try {
await this.refresh(q) void this.refresh(q)
} catch (err: any) { } catch (err: any) {
if (err instanceof PlatformError) { if (err instanceof PlatformError) {
if (err.message === 'connection closed') { if (err.message === 'connection closed') {
@ -728,7 +730,21 @@ export class LiveQuery implements WithTx, Client {
return this.getHierarchy().clone(results) as T[] return this.getHierarchy().clone(results) as T[]
} }
private async refresh (q: Query): Promise<void> { private async refresh (q: Query, reRequest: boolean = false): Promise<void> {
if (q.requested !== undefined && !reRequest) {
// we already asked for refresh, just wait.
await q.requested
return
}
if (reRequest && q.requested !== undefined) {
await q.requested
}
q.requested = this.doRefresh(q)
await q.requested
q.requested = undefined
}
private async doRefresh (q: Query): Promise<void> {
const res = await this.client.findAll(q._class, q.query, q.options) const res = await this.client.findAll(q._class, q.query, q.options)
if (!deepEqual(res, q.result) || (res.total !== q.total && q.options?.total === true)) { if (!deepEqual(res, q.result) || (res.total !== q.total && q.options?.total === true)) {
q.result = res q.result = res
@ -737,18 +753,6 @@ export class LiveQuery implements WithTx, Client {
} }
} }
private triggerRefresh (q: Query): void {
const r: Promise<FindResult<Doc>> | FindResult<Doc> = this.client.findAll(q._class, q.query, q.options)
q.result = r
void r.then(async (qr) => {
const oldResult = q.result
if (!deepEqual(qr, oldResult) || (qr.total !== q.total && q.options?.total === true)) {
q.total = qr.total
await this.callback(q)
}
})
}
// Check if query is partially matched. // Check if query is partially matched.
private async matchQuery (q: Query, tx: TxUpdateDoc<Doc>, docCache: Map<string, Doc>): Promise<boolean> { private async matchQuery (q: Query, tx: TxUpdateDoc<Doc>, docCache: Map<string, Doc>): Promise<boolean> {
const clazz = this.getHierarchy().isMixin(q._class) ? this.getHierarchy().getBaseClass(q._class) : q._class const clazz = this.getHierarchy().isMixin(q._class) ? this.getHierarchy().getBaseClass(q._class) : q._class
@ -1137,16 +1141,31 @@ export class LiveQuery implements WithTx, Client {
const docCache = new Map<string, Doc>() const docCache = new Map<string, Doc>()
for (const tx of txes) { for (const tx of txes) {
if (tx._class === core.class.TxWorkspaceEvent) { if (tx._class === core.class.TxWorkspaceEvent) {
await this.checkUpdateEvents(tx) await this.checkUpdateEvents(tx as TxWorkspaceEvent)
await this.changePrivateHandler(tx) await this.changePrivateHandler(tx as TxWorkspaceEvent)
} }
result.push(await this._tx(tx, docCache)) result.push(await this._tx(tx, docCache))
} }
return result return result
} }
private async checkUpdateEvents (tx: Tx): Promise<void> { triggerCounter = 0
const evt = tx as TxWorkspaceEvent searchTriggerTimer: any
private async checkUpdateEvents (evt: TxWorkspaceEvent, trigger = true): Promise<void> {
clearTimeout(this.searchTriggerTimer)
// We need to add trigger once more, since elastic could have a huge lag with document availability.
if (trigger || this.triggerCounter > 0) {
if (trigger) {
this.triggerCounter = 5 // Schedule 5 refreshes on every 5 seconds.
}
setTimeout(() => {
this.triggerCounter--
void this.checkUpdateEvents(evt, false)
}, 5000)
}
const h = this.client.getHierarchy() const h = this.client.getHierarchy()
function hasClass (q: Query, classes: Ref<Class<Doc>>[]): boolean { function hasClass (q: Query, classes: Ref<Class<Doc>>[]): boolean {
return classes.includes(q._class) || classes.some((it) => h.isDerived(q._class, it) || h.isDerived(it, q._class)) return classes.includes(q._class) || classes.some((it) => h.isDerived(q._class, it) || h.isDerived(it, q._class))
@ -1157,7 +1176,7 @@ export class LiveQuery implements WithTx, Client {
if (hasClass(q, indexingParam._class) && q.query.$search !== undefined) { if (hasClass(q, indexingParam._class) && q.query.$search !== undefined) {
if (!this.removeFromQueue(q)) { if (!this.removeFromQueue(q)) {
try { try {
this.triggerRefresh(q) await this.refresh(q, true)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }
@ -1167,11 +1186,14 @@ export class LiveQuery implements WithTx, Client {
for (const v of this.queries.values()) { for (const v of this.queries.values()) {
for (const q of v) { for (const q of v) {
if (hasClass(q, indexingParam._class) && q.query.$search !== undefined) { if (hasClass(q, indexingParam._class) && q.query.$search !== undefined) {
console.log('Query update call', q)
try { try {
this.triggerRefresh(q) await this.refresh(q, true)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }
} else if (q.query.$search !== undefined) {
console.log('Query update mismatch class', q._class, indexingParam._class)
} }
} }
} }
@ -1182,7 +1204,7 @@ export class LiveQuery implements WithTx, Client {
if (hasClass(q, params._class)) { if (hasClass(q, params._class)) {
if (!this.removeFromQueue(q)) { if (!this.removeFromQueue(q)) {
try { try {
this.triggerRefresh(q) await this.refresh(q, true)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }
@ -1193,7 +1215,7 @@ export class LiveQuery implements WithTx, Client {
for (const q of v) { for (const q of v) {
if (hasClass(q, params._class)) { if (hasClass(q, params._class)) {
try { try {
this.triggerRefresh(q) await this.refresh(q, true)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }
@ -1203,8 +1225,7 @@ export class LiveQuery implements WithTx, Client {
} }
} }
private async changePrivateHandler (tx: Tx): Promise<void> { private async changePrivateHandler (evt: TxWorkspaceEvent): Promise<void> {
const evt = tx as TxWorkspaceEvent
if (evt.event === WorkspaceEvent.SecurityChange) { if (evt.event === WorkspaceEvent.SecurityChange) {
for (const q of [...this.queue]) { for (const q of [...this.queue]) {
if (typeof q.query.space !== 'string') { if (typeof q.query.space !== 'string') {

View File

@ -29,7 +29,7 @@
@import "./tables.scss"; @import "./tables.scss";
@import "./_text-editor.scss"; @import "./_text-editor.scss";
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;1,400;1,500&display=swap'); @import "./mono.scss";
@font-face { @font-face {
font-family: 'IBM Plex Sans'; font-family: 'IBM Plex Sans';

View File

@ -0,0 +1,36 @@
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
src: local('IBM Plex Mono'),
local('IBMPlexMono'),
url('../fonts/complete/woff2/mono/IBMPlexMono-Regular.woff2') format('woff2'),
url('../fonts/complete/woff/mono/IBMPlexMono-Regular.woff') format('woff');
}
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 500;
src: local('IBM Plex Mono Medium'),
local('IBMPlexMono-Medium'),
url('../fonts/complete/woff2/mono/IBMPlexMono-Medium.woff2') format('woff2'),
url('../fonts/complete/woff/mono/IBMPlexMono-Medium.woff') format('woff');
}
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 600;
src: local('IBM Plex Mono SemiBold'),
local('IBMPlexMono-SemiBold'),
url('../fonts/complete/woff2/mono/IBMPlexMono-SemiBold.woff2') format('woff2'),
url('../fonts/complete/woff/mono/IBMPlexMono-SemiBold.woff') format('woff');
}
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 700;
src: local('IBM Plex Mono Bold'),
local('IBMPlexMono-Bold'),
url('../fonts/complete/woff2/mono/IBMPlexMono-Bold.woff2') format('woff2'),
url('../fonts/complete/woff/mono/IBMPlexMono-Bold.woff') format('woff');
}

View File

@ -25,7 +25,7 @@
export let icon: Asset | AnySvelteComponent | ComponentType export let icon: Asset | AnySvelteComponent | ComponentType
export let width: string | undefined = undefined export let width: string | undefined = undefined
export let value: string | undefined = undefined export let value: string | undefined = ''
export let placeholder: IntlString = plugin.string.EditBoxPlaceholder export let placeholder: IntlString = plugin.string.EditBoxPlaceholder
export let placeholderParam: any | undefined = undefined export let placeholderParam: any | undefined = undefined
export let autoFocus: boolean = false export let autoFocus: boolean = false
@ -34,15 +34,15 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let textHTML: HTMLInputElement let textHTML: HTMLInputElement
let phTraslate: string = '' let phTranslate: string = ''
export function focus () { export function focus (): void {
textHTML.focus() textHTML.focus()
autoFocus = false autoFocus = false
} }
$: translate(placeholder, placeholderParam ?? {}).then((res) => { $: void translate(placeholder, placeholderParam ?? {}).then((res) => {
phTraslate = res phTranslate = res
}) })
$: if (textHTML !== undefined) { $: if (textHTML !== undefined) {
if (autoFocus) focus() if (autoFocus) focus()
@ -53,13 +53,13 @@
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
class="flex-between editbox {size}" class="flex-between editbox {size}"
style={width ? 'width: ' + width : ''} style={width != null ? 'width: ' + width : ''}
on:click={() => { on:click={() => {
textHTML.focus() textHTML.focus()
}} }}
> >
<div class="mr-2 content-dark-color"><Icon {icon} size={'small'} /></div> <div class="mr-2 content-dark-color"><Icon {icon} size={'small'} /></div>
<input bind:this={textHTML} type="text" bind:value placeholder={phTraslate} on:change on:input on:keydown /> <input bind:this={textHTML} type="text" bind:value placeholder={phTranslate} on:change on:input on:keydown />
<slot name="extra" /> <slot name="extra" />
<div class="flex-row-center flex-no-shrink"> <div class="flex-row-center flex-no-shrink">
{#if value} {#if value}

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte' import { createEventDispatcher, onDestroy } from 'svelte'
import EditWithIcon from './EditWithIcon.svelte' import EditWithIcon from './EditWithIcon.svelte'
import IconSearch from './icons/Search.svelte' import IconSearch from './icons/Search.svelte'
import plugin from '../plugin' import plugin from '../plugin'
@ -11,13 +11,16 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let timer: any let timer: any
function restartTimer () { function restartTimer (): void {
clearTimeout(timer) clearTimeout(timer)
timer = setTimeout(() => { timer = setTimeout(() => {
value = _search value = _search
dispatch('change', _search) dispatch('change', _search)
}, 500) }, 500)
} }
onDestroy(() => {
clearTimeout(timer)
})
</script> </script>
<EditWithIcon <EditWithIcon
@ -26,20 +29,14 @@
placeholder={plugin.string.Search} placeholder={plugin.string.Search}
bind:value={_search} bind:value={_search}
on:change={() => { on:change={() => {
if (_search === '') { restartTimer()
value = ''
dispatch('change', '')
}
}} }}
on:input={() => { on:input={() => {
restartTimer() restartTimer()
if (_search === '') {
value = ''
dispatch('change', '')
}
}} }}
on:keydown={(evt) => { on:keydown={(evt) => {
if (evt.key === 'Enter') { if (evt.key === 'Enter') {
clearTimeout(timer)
value = _search value = _search
dispatch('change', _search) dispatch('change', _search)
} }

View File

@ -14,6 +14,8 @@
// //
import core, { import core, {
DOMAIN_MODEL,
cutObjectArray,
type Account, type Account,
type AccountClient, type AccountClient,
type Class, type Class,
@ -23,20 +25,20 @@ import core, {
type FindOptions, type FindOptions,
type FindResult, type FindResult,
type Hierarchy, type Hierarchy,
type MeasureDoneOperation,
type ModelDb, type ModelDb,
type Ref, type Ref,
type SearchOptions,
type SearchQuery,
type SearchResult,
type Tx, type Tx,
type TxResult, type TxResult,
type WithLookup, type WithLookup
type SearchQuery,
type SearchOptions,
type SearchResult,
type MeasureDoneOperation,
DOMAIN_MODEL
} from '@hcengineering/core' } from '@hcengineering/core'
import { devModelId } from '@hcengineering/devmodel' import { devModelId } from '@hcengineering/devmodel'
import { Builder } from '@hcengineering/model' import { Builder } from '@hcengineering/model'
import { type IntlString, type Resources, getMetadata } from '@hcengineering/platform' import { getMetadata, type IntlString, type Resources } from '@hcengineering/platform'
import { testing } from '@hcengineering/ui'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import workbench from '@hcengineering/workbench' import workbench from '@hcengineering/workbench'
import ModelView from './components/ModelView.svelte' import ModelView from './components/ModelView.svelte'
@ -63,7 +65,11 @@ class ModelClient implements AccountClient {
client.notify = (...tx) => { client.notify = (...tx) => {
this.notify?.(...tx) this.notify?.(...tx)
if (this.notifyEnabled) { if (this.notifyEnabled) {
console.debug('devmodel# notify=>', tx, this.client.getModel(), getMetadata(devmodel.metadata.DevModel)) console.debug(
'devmodel# notify=>',
testing ? JSON.stringify(cutObjectArray(tx)).slice(0, 160) : tx,
getMetadata(devmodel.metadata.DevModel)
)
} }
} }
} }
@ -97,13 +103,11 @@ class ModelClient implements AccountClient {
if (this.notifyEnabled && !isModel) { if (this.notifyEnabled && !isModel) {
console.debug( console.debug(
'devmodel# findOne=>', 'devmodel# findOne=>',
isModel,
this.getHierarchy().findDomain(_class),
_class, _class,
query, testing ? JSON.stringify(cutObjectArray(query)) : query,
options, options,
'result => ', 'result => ',
result, testing ? JSON.stringify(cutObjectArray(result)) : result,
' =>model', ' =>model',
this.client.getModel(), this.client.getModel(),
getMetadata(devmodel.metadata.DevModel), getMetadata(devmodel.metadata.DevModel),
@ -125,10 +129,10 @@ class ModelClient implements AccountClient {
console.debug( console.debug(
'devmodel# findAll=>', 'devmodel# findAll=>',
_class, _class,
query, testing ? JSON.stringify(cutObjectArray(query)).slice(0, 160) : query,
options, options,
'result => ', 'result => ',
result, testing ? JSON.stringify(cutObjectArray(result)).slice(0, 160) : result,
' =>model', ' =>model',
this.client.getModel(), this.client.getModel(),
getMetadata(devmodel.metadata.DevModel), getMetadata(devmodel.metadata.DevModel),
@ -141,7 +145,13 @@ class ModelClient implements AccountClient {
async searchFulltext (query: SearchQuery, options: SearchOptions): Promise<SearchResult> { async searchFulltext (query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
const result = await this.client.searchFulltext(query, options) const result = await this.client.searchFulltext(query, options)
if (this.notifyEnabled) { if (this.notifyEnabled) {
console.debug('devmodel# searchFulltext=>', query, options, 'result => ', result) console.debug(
'devmodel# searchFulltext=>',
testing ? JSON.stringify(cutObjectArray(query)).slice(0, 160) : query,
options,
'result => ',
result
)
} }
return result return result
} }
@ -150,7 +160,13 @@ class ModelClient implements AccountClient {
const startTime = Date.now() const startTime = Date.now()
const result = await this.client.tx(tx) const result = await this.client.tx(tx)
if (this.notifyEnabled) { if (this.notifyEnabled) {
console.debug('devmodel# tx=>', tx, result, getMetadata(devmodel.metadata.DevModel), Date.now() - startTime) console.debug(
'devmodel# tx=>',
testing ? JSON.stringify(cutObjectArray(tx)).slice(0, 160) : tx,
result,
getMetadata(devmodel.metadata.DevModel),
Date.now() - startTime
)
} }
return result return result
} }

View File

@ -54,6 +54,9 @@
const limiter = new RateLimiter(10) const limiter = new RateLimiter(10)
const client = getClient()
const hierarchy = client.getHierarchy()
let docs: Doc[] = [] let docs: Doc[] = []
let fastDocs: Doc[] = [] let fastDocs: Doc[] = []
let slowDocs: Doc[] = [] let slowDocs: Doc[] = []
@ -146,12 +149,9 @@
return resultOptions return resultOptions
} }
$: dispatch('content', docs)
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const client = getClient() $: dispatch('content', docs)
const hierarchy = client.getHierarchy()
async function getResultQuery ( async function getResultQuery (
query: DocumentQuery<Doc>, query: DocumentQuery<Doc>,
@ -174,7 +174,7 @@
return result return result
} }
function uncheckAll () { function uncheckAll (): void {
dispatch('check', { docs, value: false }) dispatch('check', { docs, value: false })
selectedObjectIds = [] selectedObjectIds = []
} }
@ -197,7 +197,7 @@
<div class="list-container" bind:this={listDiv}> <div class="list-container" bind:this={listDiv}>
<ListCategories <ListCategories
bind:this={listCategories} bind:this={listCategories}
newObjectProps={() => (space ? { space } : {})} newObjectProps={() => (space != null ? { space } : {})}
{docs} {docs}
{_class} {_class}
{space} {space}

View File

@ -267,6 +267,8 @@ export class FullTextIndexPipeline implements FullTextPipeline {
await this.flush(flush ?? false) await this.flush(flush ?? false)
} }
triggerCounts = 0
triggerIndexing = (): void => {} triggerIndexing = (): void => {}
skippedReiterationTimeout: any skippedReiterationTimeout: any
currentStages: Record<string, number> = {} currentStages: Record<string, number> = {}
@ -341,6 +343,9 @@ export class FullTextIndexPipeline implements FullTextPipeline {
}) })
while (!this.cancelling) { while (!this.cancelling) {
// Clear triggers
this.triggerCounts = 0
this.stageChanged = 0
await this.metrics.with('initialize-stages', { workspace: this.workspace.name }, async () => { await this.metrics.with('initialize-stages', { workspace: this.workspace.name }, async () => {
await this.initializeStages() await this.initializeStages()
}) })
@ -360,7 +365,11 @@ export class FullTextIndexPipeline implements FullTextPipeline {
_classes.forEach((it) => this.broadcastClasses.add(it)) _classes.forEach((it) => this.broadcastClasses.add(it))
if (this.toIndex.size === 0 || this.stageChanged === 0) { if (this.triggerCounts > 0) {
console.log('No wait, trigger counts', this.triggerCounts)
}
if (this.toIndex.size === 0 && this.stageChanged === 0 && this.triggerCounts === 0) {
if (this.toIndex.size === 0) { if (this.toIndex.size === 0) {
console.log(this.workspace.name, 'Indexing complete', this.indexId) console.log(this.workspace.name, 'Indexing complete', this.indexId)
} }
@ -374,6 +383,7 @@ export class FullTextIndexPipeline implements FullTextPipeline {
await new Promise((resolve) => { await new Promise((resolve) => {
this.triggerIndexing = () => { this.triggerIndexing = () => {
this.triggerCounts++
resolve(null) resolve(null)
clearTimeout(this.skippedReiterationTimeout) clearTimeout(this.skippedReiterationTimeout)
} }

View File

@ -1,8 +1,8 @@
import { expect, type Locator, type Page } from '@playwright/test' import { expect, type Locator, type Page } from '@playwright/test'
import { NewIssue } from './types'
import path from 'path' import path from 'path'
import { CommonTrackerPage } from './common-tracker-page'
import { attachScreenshot, iterateLocator } from '../../utils' import { attachScreenshot, iterateLocator } from '../../utils'
import { CommonTrackerPage } from './common-tracker-page'
import { NewIssue } from './types'
export class IssuesPage extends CommonTrackerPage { export class IssuesPage extends CommonTrackerPage {
readonly page: Page readonly page: Page
@ -142,8 +142,14 @@ export class IssuesPage extends CommonTrackerPage {
} }
async searchIssueByName (issueName: string): Promise<void> { async searchIssueByName (issueName: string): Promise<void> {
for (let i = 0; i < 5; i++) {
await this.inputSearch.fill(issueName) await this.inputSearch.fill(issueName)
await this.page.waitForTimeout(3000) const v = await this.inputSearch.inputValue()
if (v === issueName) {
await this.inputSearch.press('Enter')
break
}
}
} }
async openIssueByName (issueName: string): Promise<void> { async openIssueByName (issueName: string): Promise<void> {