feat: continued post qa for servers in app (#5818)
* fix: intercom in app * feat: Logs.vue dynamic console resizing with window + padding problem * fix: search highlight with decorator + change to be better * fix: qa * fix: allow paper+purpur into app csp * fix: lint
This commit is contained in:
@@ -1,43 +1,162 @@
|
||||
import type { Terminal } from '@xterm/xterm'
|
||||
import type { IDecoration, Terminal } from '@xterm/xterm'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { LogLevel, LogLine } from '../types'
|
||||
|
||||
export type FilterPredicate = (line: LogLine) => boolean
|
||||
|
||||
function highlightMatches(text: string, query: string): string {
|
||||
if (!query) return text
|
||||
const lower = text.toLowerCase()
|
||||
let result = ''
|
||||
let pos = 0
|
||||
while (pos < text.length) {
|
||||
const idx = lower.indexOf(query, pos)
|
||||
if (idx === -1) {
|
||||
result += text.slice(pos)
|
||||
break
|
||||
}
|
||||
result += text.slice(pos, idx)
|
||||
result += `\x1b[1;7m${text.slice(idx, idx + query.length)}\x1b[27;22m`
|
||||
pos = idx + query.length
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function colorize(line: LogLine, searchQuery?: string): string {
|
||||
const text = searchQuery ? highlightMatches(line.text, searchQuery) : line.text
|
||||
export function colorize(line: LogLine, _searchQuery?: string): string {
|
||||
switch (line.level) {
|
||||
case 'error':
|
||||
return `\x1b[31;40m${text}\x1b[K\x1b[0m`
|
||||
return `\x1b[31;40m${line.text}\x1b[K\x1b[0m`
|
||||
case 'warn':
|
||||
return `\x1b[33;40m${text}\x1b[K\x1b[0m`
|
||||
return `\x1b[33;40m${line.text}\x1b[K\x1b[0m`
|
||||
case 'debug':
|
||||
case 'trace':
|
||||
return `\x1b[90m${text}\x1b[0m`
|
||||
return `\x1b[90m${line.text}\x1b[0m`
|
||||
default:
|
||||
return text
|
||||
return line.text
|
||||
}
|
||||
}
|
||||
|
||||
const HIGHLIGHT_BG = '#ffd60a'
|
||||
const HIGHLIGHT_FG = '#000000'
|
||||
|
||||
const terminalDecorations = new WeakMap<Terminal, IDecoration[]>()
|
||||
const activeQueries = new WeakMap<Terminal, string>()
|
||||
const highlightVersions = new WeakMap<Terminal, number>()
|
||||
|
||||
function getDecorationList(terminal: Terminal): IDecoration[] {
|
||||
let list = terminalDecorations.get(terminal)
|
||||
if (!list) {
|
||||
list = []
|
||||
terminalDecorations.set(terminal, list)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
function bumpVersion(terminal: Terminal): number {
|
||||
const next = (highlightVersions.get(terminal) ?? 0) + 1
|
||||
highlightVersions.set(terminal, next)
|
||||
return next
|
||||
}
|
||||
|
||||
export function getHighlightVersion(terminal: Terminal): number {
|
||||
return highlightVersions.get(terminal) ?? 0
|
||||
}
|
||||
|
||||
export function clearSearchHighlights(terminal: Terminal) {
|
||||
const existing = terminalDecorations.get(terminal)
|
||||
if (existing) {
|
||||
for (const d of existing) d.dispose()
|
||||
existing.length = 0
|
||||
}
|
||||
activeQueries.delete(terminal)
|
||||
bumpVersion(terminal)
|
||||
}
|
||||
|
||||
function walkBackToLogicalStart(terminal: Terminal, row: number): number {
|
||||
const buffer = terminal.buffer.active
|
||||
let y = Math.max(0, row)
|
||||
while (y > 0) {
|
||||
const line = buffer.getLine(y)
|
||||
if (!line?.isWrapped) break
|
||||
y--
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
function scanRange(
|
||||
terminal: Terminal,
|
||||
query: string,
|
||||
startRow: number,
|
||||
endRow: number,
|
||||
out: IDecoration[],
|
||||
) {
|
||||
const buffer = terminal.buffer.active
|
||||
const cols = terminal.cols
|
||||
const cursorAbsolute = buffer.baseY + buffer.cursorY
|
||||
let y = startRow
|
||||
while (y <= endRow) {
|
||||
const head = buffer.getLine(y)
|
||||
if (!head) break
|
||||
const lineStart = y
|
||||
let text = head.translateToString(false)
|
||||
y++
|
||||
while (y < buffer.length) {
|
||||
const next = buffer.getLine(y)
|
||||
if (!next?.isWrapped) break
|
||||
text += next.translateToString(false)
|
||||
y++
|
||||
}
|
||||
const lower = text.toLowerCase()
|
||||
let pos = 0
|
||||
while (true) {
|
||||
const idx = lower.indexOf(query, pos)
|
||||
if (idx === -1) break
|
||||
let remaining = query.length
|
||||
let rowAbs = lineStart + Math.floor(idx / cols)
|
||||
let col = idx % cols
|
||||
while (remaining > 0) {
|
||||
const amount = Math.min(cols - col, remaining)
|
||||
const marker = terminal.registerMarker(rowAbs - cursorAbsolute)
|
||||
if (marker) {
|
||||
const decoration = terminal.registerDecoration({
|
||||
marker,
|
||||
x: col,
|
||||
width: amount,
|
||||
layer: 'top',
|
||||
backgroundColor: HIGHLIGHT_BG,
|
||||
foregroundColor: HIGHLIGHT_FG,
|
||||
})
|
||||
if (decoration) out.push(decoration)
|
||||
}
|
||||
remaining -= amount
|
||||
rowAbs++
|
||||
col = 0
|
||||
}
|
||||
pos = idx + query.length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function applySearchHighlights(terminal: Terminal, query: string): number {
|
||||
const trimmed = query.trim().toLowerCase()
|
||||
const list = getDecorationList(terminal)
|
||||
for (const d of list) d.dispose()
|
||||
list.length = 0
|
||||
const version = bumpVersion(terminal)
|
||||
if (!trimmed) {
|
||||
activeQueries.delete(terminal)
|
||||
return version
|
||||
}
|
||||
activeQueries.set(terminal, trimmed)
|
||||
const endRow = terminal.buffer.active.length - 1
|
||||
scanRange(terminal, trimmed, 0, endRow, list)
|
||||
return version
|
||||
}
|
||||
|
||||
export function highlightAppendedRange(terminal: Terminal, fromRow: number, version: number) {
|
||||
if (getHighlightVersion(terminal) !== version) return
|
||||
const query = activeQueries.get(terminal)
|
||||
if (!query) return
|
||||
const scanFrom = walkBackToLogicalStart(terminal, fromRow)
|
||||
const list = getDecorationList(terminal)
|
||||
const survivors: IDecoration[] = []
|
||||
for (const d of list) {
|
||||
if (d.marker.line >= scanFrom) {
|
||||
d.dispose()
|
||||
} else {
|
||||
survivors.push(d)
|
||||
}
|
||||
}
|
||||
list.length = 0
|
||||
list.push(...survivors)
|
||||
const endRow = terminal.buffer.active.length - 1
|
||||
if (scanFrom > endRow) return
|
||||
scanRange(terminal, query, scanFrom, endRow, list)
|
||||
}
|
||||
|
||||
export type ConditionalLevel = 'debug' | 'trace'
|
||||
|
||||
export function useConsoleFilters() {
|
||||
@@ -80,18 +199,23 @@ export function rewriteTerminal(
|
||||
searchQuery?: string,
|
||||
callback?: () => void,
|
||||
) {
|
||||
clearSearchHighlights(terminal)
|
||||
terminal.reset()
|
||||
terminal.write('\x1b[?25l')
|
||||
|
||||
const filtered = predicate ? allLines.filter(predicate) : allLines
|
||||
if (filtered.length === 0) {
|
||||
if (searchQuery) applySearchHighlights(terminal, searchQuery)
|
||||
callback?.()
|
||||
return
|
||||
}
|
||||
|
||||
terminal.write('\x1b[?2026h')
|
||||
terminal.write(filtered.map((line) => colorize(line, searchQuery)).join('\r\n'), () => {
|
||||
terminal.write(filtered.map((line) => colorize(line)).join('\r\n'), () => {
|
||||
terminal.write('\x1b[?2026l')
|
||||
if (searchQuery) {
|
||||
applySearchHighlights(terminal, searchQuery)
|
||||
}
|
||||
callback?.()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
export {
|
||||
applySearchHighlights,
|
||||
clearSearchHighlights,
|
||||
colorize,
|
||||
type ConditionalLevel,
|
||||
type FilterPredicate,
|
||||
getHighlightVersion,
|
||||
highlightAppendedRange,
|
||||
rewriteTerminal,
|
||||
useConsoleFilters,
|
||||
} from './console-filtering'
|
||||
|
||||
@@ -111,7 +111,14 @@ import { injectNotificationManager } from '#ui/providers/web-notifications.ts'
|
||||
|
||||
import ConsoleActionButtons from './components/ConsoleActionButtons.vue'
|
||||
import ConsoleFilterPills from './components/ConsoleFilterPills.vue'
|
||||
import { colorize, rewriteTerminal, useConsoleFilters } from './composables'
|
||||
import {
|
||||
clearSearchHighlights,
|
||||
colorize,
|
||||
getHighlightVersion,
|
||||
highlightAppendedRange,
|
||||
rewriteTerminal,
|
||||
useConsoleFilters,
|
||||
} from './composables'
|
||||
import type { ConditionalLevel } from './composables/console-filtering'
|
||||
import { injectConsoleManager } from './providers'
|
||||
import type { LogLevel, LogLine } from './types'
|
||||
@@ -279,18 +286,21 @@ watch(ctx.logLines, (lines, oldLines) => {
|
||||
}
|
||||
|
||||
const predicate = buildCombinedPredicate()
|
||||
const query = activeSearchQuery()
|
||||
const newLines: string[] = []
|
||||
for (let i = lastWrittenIndex; i < lines.length; i++) {
|
||||
if (!predicate || predicate(lines[i])) {
|
||||
newLines.push(colorize(lines[i], query))
|
||||
newLines.push(colorize(lines[i]))
|
||||
}
|
||||
}
|
||||
if (newLines.length > 0) {
|
||||
const buffer = term.buffer.active
|
||||
const onFreshLine = buffer.cursorX === 0
|
||||
const data = onFreshLine ? newLines.join('\r\n') : '\r\n' + newLines.join('\r\n')
|
||||
term.write(data)
|
||||
const fromRow = buffer.baseY + buffer.cursorY
|
||||
const version = getHighlightVersion(term)
|
||||
term.write(data, () => {
|
||||
highlightAppendedRange(term, fromRow, version)
|
||||
})
|
||||
}
|
||||
lastWrittenIndex = lines.length
|
||||
})
|
||||
@@ -307,6 +317,8 @@ function handleCommand(cmd: string) {
|
||||
}
|
||||
|
||||
function handleClear() {
|
||||
const term = terminalRef.value?.terminal
|
||||
if (term) clearSearchHighlights(term)
|
||||
terminalRef.value?.reset()
|
||||
lastWrittenIndex = 0
|
||||
ctx.onClear?.()
|
||||
|
||||
@@ -31,7 +31,12 @@
|
||||
</span>
|
||||
</div>
|
||||
<span class="stat-drop-shadow text-4xl font-bold text-contrast">
|
||||
{{ metric.value }}
|
||||
{{ metric.value
|
||||
}}<span
|
||||
v-if="metric.secondary"
|
||||
class="ml-1 text-sm font-normal stat-drop-shadow text-secondary"
|
||||
>{{ metric.secondary }}</span
|
||||
>
|
||||
</span>
|
||||
<!-- <div
|
||||
class="absolute -left-8 -top-4 -z-10 h-28 w-56 rounded-full bg-surface-3 opacity-50 blur-lg"
|
||||
@@ -88,6 +93,13 @@ const isRamAsBytesForcedByFeatureFlag = computed(
|
||||
() => featureFlags?.serverRamAsBytesAlwaysOn?.value ?? false,
|
||||
)
|
||||
|
||||
const showRamAsBytes = computed(
|
||||
() =>
|
||||
props.showMemoryAsBytes ||
|
||||
isRamAsBytesForcedByFeatureFlag.value ||
|
||||
userPreferences.value.ramAsNumber,
|
||||
)
|
||||
|
||||
const stats = shallowRef(
|
||||
props.data?.current || {
|
||||
cpu_percent: 0,
|
||||
@@ -174,6 +186,7 @@ const metrics = computed(() => {
|
||||
const storageMetric = {
|
||||
title: 'Storage',
|
||||
value: props.loading ? '0 B' : formatBytes(stats.value.storage_usage_bytes ?? 0),
|
||||
secondary: null as string | null,
|
||||
icon: FolderOpenIcon,
|
||||
showGraph: false,
|
||||
chartOptions: null as ReturnType<typeof buildChartOptions> | null,
|
||||
@@ -186,6 +199,7 @@ const metrics = computed(() => {
|
||||
{
|
||||
title: 'CPU',
|
||||
value: '0.00%',
|
||||
secondary: null as string | null,
|
||||
icon: CpuIcon,
|
||||
showGraph: true,
|
||||
chartOptions: cpuChartOptions.value,
|
||||
@@ -195,6 +209,7 @@ const metrics = computed(() => {
|
||||
{
|
||||
title: 'Memory',
|
||||
value: '0.00%',
|
||||
secondary: null as string | null,
|
||||
icon: DatabaseIcon,
|
||||
showGraph: true,
|
||||
chartOptions: ramChartOptions.value,
|
||||
@@ -209,6 +224,7 @@ const metrics = computed(() => {
|
||||
{
|
||||
title: 'CPU',
|
||||
value: `${cpuPercent.value.toFixed(2)}%`,
|
||||
secondary: null as string | null,
|
||||
icon: CpuIcon,
|
||||
showGraph: true,
|
||||
chartOptions: cpuChartOptions.value,
|
||||
@@ -217,12 +233,12 @@ const metrics = computed(() => {
|
||||
},
|
||||
{
|
||||
title: 'Memory',
|
||||
value:
|
||||
props.showMemoryAsBytes ||
|
||||
isRamAsBytesForcedByFeatureFlag.value ||
|
||||
userPreferences.value.ramAsNumber
|
||||
? formatBytes(stats.value.ram_usage_bytes ?? 0)
|
||||
: `${ramPercent.value.toFixed(2)}%`,
|
||||
value: showRamAsBytes.value
|
||||
? formatBytes(stats.value.ram_usage_bytes ?? 0)
|
||||
: `${ramPercent.value.toFixed(2)}%`,
|
||||
secondary: showRamAsBytes.value
|
||||
? `/ ${formatBytes(stats.value.ram_total_bytes ?? 0)}`
|
||||
: (null as string | null),
|
||||
icon: DatabaseIcon,
|
||||
showGraph: true,
|
||||
chartOptions: ramChartOptions.value,
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
<div
|
||||
v-else-if="serverData"
|
||||
data-pyro-server-manager-root
|
||||
class="experimental-styles-within relative mx-auto pb-12 box-border flex min-h-[calc(100svh-100px)] w-full min-w-0 flex-col gap-4 px-6 transition-all duration-300"
|
||||
class="experimental-styles-within relative mx-auto pb-6 box-border flex min-h-[calc(100svh-100px)] w-full min-w-0 flex-col gap-4 px-6 transition-all duration-300"
|
||||
:style="{
|
||||
'--server-bg-image': serverImage
|
||||
? `url(${serverImage})`
|
||||
@@ -1493,7 +1493,11 @@ onMounted(() => {
|
||||
})
|
||||
}
|
||||
|
||||
if (props.authUser && props.fetchIntercomToken) {
|
||||
let intercomInitialized = false
|
||||
const tryInitIntercom = () => {
|
||||
if (intercomInitialized) return
|
||||
if (!props.authUser || !props.fetchIntercomToken) return
|
||||
intercomInitialized = true
|
||||
props
|
||||
.fetchIntercomToken()
|
||||
.then(({ token }) => {
|
||||
@@ -1504,9 +1508,20 @@ onMounted(() => {
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
intercomInitialized = false
|
||||
console.warn('[PYROSERVERS][INTERCOM] failed to initialize secure support chat', error)
|
||||
})
|
||||
}
|
||||
tryInitIntercom()
|
||||
const stopIntercomWatch = watch(
|
||||
() => props.authUser,
|
||||
(user) => {
|
||||
if (user) {
|
||||
tryInitIntercom()
|
||||
stopIntercomWatch()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
DOMPurify.addHook(
|
||||
'afterSanitizeAttributes',
|
||||
|
||||
Reference in New Issue
Block a user