|
| 1 | +import { localized, msg } from "@lit/localize"; |
| 2 | +import clsx from "clsx"; |
| 3 | +import { css, html, nothing } from "lit"; |
| 4 | +import { customElement, property, state } from "lit/decorators.js"; |
| 5 | + |
| 6 | +import { TailwindElement } from "@/classes/TailwindElement"; |
| 7 | +import { noData } from "@/strings/ui"; |
| 8 | +import { CrawlLogContext, CrawlLogLevel, type CrawlLog } from "@/types/crawler"; |
| 9 | +import { truncate } from "@/utils/css"; |
| 10 | +import { stopProp } from "@/utils/events"; |
| 11 | +import { tw } from "@/utils/tailwind"; |
| 12 | + |
| 13 | +const labelFor: Record<CrawlLogContext, string> = { |
| 14 | + [CrawlLogContext.General]: msg("General", { |
| 15 | + desc: "'General' crawl log context type", |
| 16 | + }), |
| 17 | + [CrawlLogContext.Behavior]: msg("Page Behavior"), |
| 18 | + [CrawlLogContext.BehaviorScript]: msg("Built-in Behavior"), |
| 19 | + [CrawlLogContext.BehaviorScriptCustom]: msg("Custom Behavior Script"), |
| 20 | +}; |
| 21 | + |
| 22 | +const contextLevelFor: Record<CrawlLogContext, number> = { |
| 23 | + [CrawlLogContext.Behavior]: 1, |
| 24 | + [CrawlLogContext.BehaviorScript]: 2, |
| 25 | + [CrawlLogContext.General]: 3, |
| 26 | + [CrawlLogContext.BehaviorScriptCustom]: 4, |
| 27 | +}; |
| 28 | +// Minimum context level to highlight |
| 29 | +const MIN_CONTEXT_LEVEL = 3 as const; |
| 30 | + |
| 31 | +/** |
| 32 | + * Tabular list of logs |
| 33 | + */ |
| 34 | +@customElement("btrix-crawl-log-table") |
| 35 | +@localized() |
| 36 | +export class CrawlLogTable extends TailwindElement { |
| 37 | + static styles = [ |
| 38 | + truncate, |
| 39 | + css` |
| 40 | + pre { |
| 41 | + white-space: pre-wrap; |
| 42 | + font-family: var(--sl-font-mono); |
| 43 | + font-size: var(--sl-font-size-x-small); |
| 44 | + margin: 0; |
| 45 | + padding: var(--sl-spacing-small); |
| 46 | + border: 1px solid var(--sl-panel-border-color); |
| 47 | + border-radius: var(--sl-border-radius-medium); |
| 48 | + } |
| 49 | + `, |
| 50 | + ]; |
| 51 | + |
| 52 | + @property({ type: Array }) |
| 53 | + logs?: CrawlLog[]; |
| 54 | + |
| 55 | + /** |
| 56 | + * Number to offset index by, e.g. for pagination |
| 57 | + */ |
| 58 | + @property({ type: Number }) |
| 59 | + offset = 1; |
| 60 | + |
| 61 | + @state() |
| 62 | + private selectedLog: |
| 63 | + | (CrawlLog & { |
| 64 | + index: number; |
| 65 | + }) |
| 66 | + | null = null; |
| 67 | + |
| 68 | + render() { |
| 69 | + if (!this.logs) return; |
| 70 | + |
| 71 | + const rowClasses = tw`grid grid-cols-[5rem_2.5rem_20rem_1fr] leading-[1.3]`; |
| 72 | + |
| 73 | + return html`<btrix-numbered-list class="text-xs"> |
| 74 | + <btrix-numbered-list-header slot="header"> |
| 75 | + <div class=${rowClasses}> |
| 76 | + <div class="px-2">${msg("Timestamp")}</div> |
| 77 | + <div class="text-center">${msg("Level")}</div> |
| 78 | + <div class="px-2">${msg("Message")}</div> |
| 79 | + <div class="px-2">${msg("Page URL")}</div> |
| 80 | + </div> |
| 81 | + </btrix-numbered-list-header> |
| 82 | + ${this.logs.map((log: CrawlLog, idx) => { |
| 83 | + const selected = this.selectedLog?.index === idx; |
| 84 | + return html` |
| 85 | + <btrix-numbered-list-item |
| 86 | + class="group" |
| 87 | + hoverable |
| 88 | + ?selected=${selected} |
| 89 | + aria-selected="${selected}" |
| 90 | + @click=${() => { |
| 91 | + this.selectedLog = { |
| 92 | + index: idx, |
| 93 | + ...log, |
| 94 | + }; |
| 95 | + }} |
| 96 | + > |
| 97 | + <div slot="marker" class="min-w-[3ch]"> |
| 98 | + ${idx + 1 + this.offset}. |
| 99 | + </div> |
| 100 | + <div |
| 101 | + class=${clsx( |
| 102 | + rowClasses, |
| 103 | + (contextLevelFor[log.context as unknown as CrawlLogContext] || |
| 104 | + 0) < MIN_CONTEXT_LEVEL |
| 105 | + ? tw`text-stone-400` |
| 106 | + : tw`text-stone-800`, |
| 107 | + tw`group-hover:text-inherit`, |
| 108 | + )} |
| 109 | + > |
| 110 | + <div> |
| 111 | + <sl-tooltip |
| 112 | + placement="bottom" |
| 113 | + @sl-hide=${stopProp} |
| 114 | + @sl-after-hide=${stopProp} |
| 115 | + > |
| 116 | + <btrix-format-date |
| 117 | + date=${log.timestamp} |
| 118 | + hour="2-digit" |
| 119 | + minute="2-digit" |
| 120 | + second="2-digit" |
| 121 | + hour-format="24" |
| 122 | + > |
| 123 | + </btrix-format-date> |
| 124 | + <btrix-format-date |
| 125 | + slot="content" |
| 126 | + date=${log.timestamp} |
| 127 | + month="long" |
| 128 | + day="numeric" |
| 129 | + year="numeric" |
| 130 | + hour="numeric" |
| 131 | + minute="numeric" |
| 132 | + second="numeric" |
| 133 | + time-zone-name="short" |
| 134 | + > |
| 135 | + </btrix-format-date> |
| 136 | + </sl-tooltip> |
| 137 | + </div> |
| 138 | + <div class="pr-4 text-center"> |
| 139 | + <sl-tooltip |
| 140 | + class="capitalize" |
| 141 | + content=${log.logLevel} |
| 142 | + placement="bottom" |
| 143 | + @sl-hide=${stopProp} |
| 144 | + @sl-after-hide=${stopProp} |
| 145 | + > |
| 146 | + ${this.renderLevel(log)} |
| 147 | + </sl-tooltip> |
| 148 | + </div> |
| 149 | + <div class="whitespace-pre-wrap">${log.message}</div> |
| 150 | + ${log.details.page |
| 151 | + ? html` |
| 152 | + <div class="truncate" title="${log.details.page}"> |
| 153 | + ${log.details.page} |
| 154 | + </div> |
| 155 | + ` |
| 156 | + : html`<div class="text-neutral-400 group-hover:text-inherit"> |
| 157 | + ${noData} |
| 158 | + </div>`} |
| 159 | + </div> |
| 160 | + </btrix-numbered-list-item> |
| 161 | + `; |
| 162 | + })} |
| 163 | + </btrix-numbered-list> |
| 164 | +
|
| 165 | + <btrix-dialog |
| 166 | + .label=${msg("Log Details")} |
| 167 | + .open=${!!this.selectedLog} |
| 168 | + class="[--width:40rem]" |
| 169 | + @sl-show=${stopProp} |
| 170 | + @sl-after-show=${stopProp} |
| 171 | + @sl-hide=${stopProp} |
| 172 | + @sl-after-hide=${(e: CustomEvent) => { |
| 173 | + stopProp(e); |
| 174 | + this.selectedLog = null; |
| 175 | + }} |
| 176 | + >${this.renderLogDetails()}</btrix-dialog |
| 177 | + > `; |
| 178 | + } |
| 179 | + |
| 180 | + private renderLevel(log: CrawlLog) { |
| 181 | + const logLevel = log.logLevel; |
| 182 | + const contextLevel = |
| 183 | + contextLevelFor[log.context as unknown as CrawlLogContext] || 0; |
| 184 | + const baseClasses = tw`size-4 group-hover:text-inherit`; |
| 185 | + |
| 186 | + switch (logLevel) { |
| 187 | + case CrawlLogLevel.Fatal: |
| 188 | + return html` |
| 189 | + <sl-icon |
| 190 | + name="exclamation-octagon-fill" |
| 191 | + class=${clsx(tw`text-danger-500`, baseClasses)} |
| 192 | + ></sl-icon> |
| 193 | + `; |
| 194 | + case CrawlLogLevel.Error: |
| 195 | + return html` |
| 196 | + <sl-icon |
| 197 | + name="exclamation-triangle-fill" |
| 198 | + class=${clsx(tw`text-danger-500`, baseClasses)} |
| 199 | + ></sl-icon> |
| 200 | + `; |
| 201 | + case CrawlLogLevel.Warning: |
| 202 | + return html` |
| 203 | + <sl-icon |
| 204 | + name="exclamation-diamond-fill" |
| 205 | + class=${clsx(tw`text-warning-500`, baseClasses)} |
| 206 | + ></sl-icon> |
| 207 | + `; |
| 208 | + case CrawlLogLevel.Info: |
| 209 | + return html` |
| 210 | + <sl-icon |
| 211 | + name="info-circle-fill" |
| 212 | + class=${clsx( |
| 213 | + tw`text-neutral-400`, |
| 214 | + contextLevel < MIN_CONTEXT_LEVEL && tw`opacity-30`, |
| 215 | + baseClasses, |
| 216 | + )} |
| 217 | + ></sl-icon> |
| 218 | + `; |
| 219 | + case CrawlLogLevel.Debug: |
| 220 | + return html` |
| 221 | + <sl-icon |
| 222 | + name="bug" |
| 223 | + class=${clsx(tw`text-neutral-400`, baseClasses)} |
| 224 | + ></sl-icon> |
| 225 | + `; |
| 226 | + default: |
| 227 | + return html` |
| 228 | + <sl-icon |
| 229 | + name="question-lg" |
| 230 | + class=${clsx(tw`text-neutral-300`, baseClasses)} |
| 231 | + ></sl-icon> |
| 232 | + `; |
| 233 | + break; |
| 234 | + } |
| 235 | + } |
| 236 | + |
| 237 | + private renderLogDetails() { |
| 238 | + if (!this.selectedLog) return; |
| 239 | + const { context, details } = this.selectedLog; |
| 240 | + const { page, stack, ...unknownDetails } = details; |
| 241 | + |
| 242 | + return html` |
| 243 | + <btrix-desc-list> |
| 244 | + <btrix-desc-list-item label=${msg("TIMESTAMP")}> |
| 245 | + ${this.selectedLog.timestamp} |
| 246 | + </btrix-desc-list-item> |
| 247 | + <btrix-desc-list-item label=${msg("CONTEXT")}> |
| 248 | + ${Object.values(CrawlLogContext).includes( |
| 249 | + context as unknown as CrawlLogContext, |
| 250 | + ) |
| 251 | + ? labelFor[context as CrawlLogContext] |
| 252 | + : html`<span class="capitalize">${context}</span>`} |
| 253 | + </btrix-desc-list-item> |
| 254 | + <btrix-desc-list-item label=${msg("MESSAGE")}> |
| 255 | + ${this.selectedLog.message} |
| 256 | + </btrix-desc-list-item> |
| 257 | + ${page |
| 258 | + ? html`<btrix-desc-list-item label=${msg("PAGE")}> |
| 259 | + ${this.renderPage(page)} |
| 260 | + </btrix-desc-list-item>` |
| 261 | + : nothing} |
| 262 | + ${stack |
| 263 | + ? html`<btrix-desc-list-item label=${msg("STACK")}> |
| 264 | + ${this.renderPre(stack)} |
| 265 | + </btrix-desc-list-item>` |
| 266 | + : nothing} |
| 267 | + ${Object.entries(unknownDetails).map( |
| 268 | + ([key, value]) => html` |
| 269 | + <btrix-desc-list-item label=${key.toUpperCase()}> |
| 270 | + ${typeof value !== "string" && typeof value !== "number" |
| 271 | + ? this.renderPre(value) |
| 272 | + : value |
| 273 | + ? html`<span class="break-all">${value}</span>` |
| 274 | + : noData} |
| 275 | + </btrix-desc-list-item> |
| 276 | + `, |
| 277 | + )} |
| 278 | + </btrix-desc-list> |
| 279 | + `; |
| 280 | + } |
| 281 | + |
| 282 | + private renderPage(page: string) { |
| 283 | + return html` |
| 284 | + <sl-tooltip |
| 285 | + content=${msg("Open live page in new tab")} |
| 286 | + @sl-hide=${stopProp} |
| 287 | + @sl-after-hide=${stopProp} |
| 288 | + > |
| 289 | + <a |
| 290 | + class="break-all text-blue-500 hover:text-blue-400" |
| 291 | + href=${page} |
| 292 | + target="_blank" |
| 293 | + rel="noopener noreferrer nofollow" |
| 294 | + >${page}</a |
| 295 | + > |
| 296 | + </sl-tooltip> |
| 297 | + `; |
| 298 | + } |
| 299 | + |
| 300 | + private renderPre(value: unknown) { |
| 301 | + let str = value; |
| 302 | + if (typeof value !== "string") { |
| 303 | + str = JSON.stringify(value, null, 2); |
| 304 | + } |
| 305 | + return html`<pre |
| 306 | + class="overflow-auto whitespace-pre" |
| 307 | + ><code>${str}</code></pre>`; |
| 308 | + } |
| 309 | +} |
0 commit comments