Add QA page analysis chart (#1725)
This commit is contained in:
parent
1f59c0b452
commit
dde1175bd0
@ -7,6 +7,7 @@
|
||||
"@cheap-glitch/mi-cron": "^1.0.1",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.2.1",
|
||||
"@lit/localize": "^0.12.1",
|
||||
"@lit/task": "^1.0.0",
|
||||
"@novnc/novnc": "^1.4.0-beta",
|
||||
"@rollup/plugin-commonjs": "^18.0.0",
|
||||
"@shoelace-style/shoelace": "~2.13.0",
|
||||
|
@ -8,10 +8,13 @@ export class Card extends TailwindElement {
|
||||
render() {
|
||||
return html`
|
||||
<section class="flex h-full flex-col rounded border p-4">
|
||||
<h2 class="mb-3 border-b pb-3 text-base font-semibold leading-none">
|
||||
<div
|
||||
id="cardHeading"
|
||||
class="mb-3 border-b pb-3 text-base font-semibold leading-none"
|
||||
>
|
||||
<slot name="title"></slot>
|
||||
</h2>
|
||||
<div class="flex-1">
|
||||
</div>
|
||||
<div class="flex-1" aria-labelledby="cardHeading">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot name="footer"></slot>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { css, html, LitElement, type PropertyValues } from "lit";
|
||||
import { css, html, type PropertyValues } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
@ -10,10 +10,12 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { when } from "lit/directives/when.js";
|
||||
import debounce from "lodash/fp/debounce";
|
||||
|
||||
import { TailwindElement } from "@/classes/TailwindElement";
|
||||
import type { UnderlyingFunction } from "@/types/utils";
|
||||
import { tw } from "@/utils/tailwind";
|
||||
|
||||
@customElement("btrix-meter-bar")
|
||||
export class MeterBar extends LitElement {
|
||||
export class MeterBar extends TailwindElement {
|
||||
/* Percentage of value / max */
|
||||
@property({ type: Number })
|
||||
value = 0;
|
||||
@ -43,7 +45,7 @@ export class MeterBar extends LitElement {
|
||||
}
|
||||
|
||||
@customElement("btrix-divided-meter-bar")
|
||||
export class DividedMeterBar extends LitElement {
|
||||
export class DividedMeterBar extends TailwindElement {
|
||||
/* Percentage of value / max */
|
||||
@property({ type: Number })
|
||||
value = 0;
|
||||
@ -101,7 +103,7 @@ export class DividedMeterBar extends LitElement {
|
||||
* ```
|
||||
*/
|
||||
@customElement("btrix-meter")
|
||||
export class Meter extends LitElement {
|
||||
export class Meter extends TailwindElement {
|
||||
@property({ type: Number })
|
||||
min = 0;
|
||||
|
||||
@ -111,25 +113,30 @@ export class Meter extends LitElement {
|
||||
@property({ type: Number })
|
||||
value = 0;
|
||||
|
||||
@property({ type: Array })
|
||||
subValues?: number[];
|
||||
|
||||
@property({ type: String })
|
||||
valueText?: string;
|
||||
|
||||
@query(".valueBar")
|
||||
private readonly valueBar?: HTMLElement;
|
||||
|
||||
@query(".labels")
|
||||
private readonly labels?: HTMLElement;
|
||||
|
||||
@query(".valueBar")
|
||||
private readonly valueBar?: HTMLElement;
|
||||
|
||||
@query(".maxText")
|
||||
private readonly maxText?: HTMLElement;
|
||||
|
||||
@queryAssignedElements({ slot: "valueLabel" })
|
||||
private readonly valueLabel!: HTMLElement[];
|
||||
|
||||
// postcss-lit-disable-next-line
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.meter {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.track {
|
||||
@ -185,6 +192,13 @@ export class Meter extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
// TODO refactor to check slot
|
||||
if (!this.valueLabel.length) {
|
||||
this.labels?.classList.add(tw`hidden`);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
// meter spec disallow values that exceed max
|
||||
const max = this.max ? Math.max(this.value, this.max) : this.value;
|
||||
|
@ -25,6 +25,9 @@ export class CrawlStatus extends TailwindElement {
|
||||
@property({ type: Boolean })
|
||||
stopping = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
hoist = false;
|
||||
|
||||
static styles = [
|
||||
animatePulse,
|
||||
css`
|
||||
@ -235,6 +238,7 @@ export class CrawlStatus extends TailwindElement {
|
||||
content=${label}
|
||||
@sl-hide=${(e: SlHideEvent) => e.stopPropagation()}
|
||||
@sl-after-hide=${(e: SlHideEvent) => e.stopPropagation()}
|
||||
.hoist=${this.hoist}
|
||||
>
|
||||
<div>${icon}</div>
|
||||
</sl-tooltip>
|
||||
|
@ -32,7 +32,14 @@ export class QaRunDropdown extends TailwindElement {
|
||||
<sl-dropdown @sl-select=${this.onSelect} distance="-2">
|
||||
<sl-button slot="trigger" variant="text" size="small" caret>
|
||||
${selectedRun
|
||||
? formatDate(selectedRun.finished)
|
||||
? html`<btrix-crawl-status
|
||||
type="qa"
|
||||
hideLabel
|
||||
state=${selectedRun.state}
|
||||
slot="prefix"
|
||||
hoist
|
||||
></btrix-crawl-status>
|
||||
${formatDate(selectedRun.finished)} `
|
||||
: msg("Select a QA run")}
|
||||
</sl-button>
|
||||
<sl-menu>
|
||||
@ -46,6 +53,13 @@ export class QaRunDropdown extends TailwindElement {
|
||||
?checked=${isSelected}
|
||||
>
|
||||
${formatDate(run.finished)}
|
||||
<btrix-crawl-status
|
||||
type="qa"
|
||||
hideLabel
|
||||
state=${run.state}
|
||||
slot="prefix"
|
||||
hoist
|
||||
></btrix-crawl-status>
|
||||
</sl-menu-item>
|
||||
`;
|
||||
})}
|
||||
|
@ -27,7 +27,11 @@ import type {
|
||||
import type { QARun } from "@/types/qa";
|
||||
import { isApiError } from "@/utils/api";
|
||||
import type { AuthState } from "@/utils/AuthService";
|
||||
import { finishedCrawlStates, isActive } from "@/utils/crawler";
|
||||
import {
|
||||
activeCrawlStates,
|
||||
finishedCrawlStates,
|
||||
isActive,
|
||||
} from "@/utils/crawler";
|
||||
import { humanizeExecutionSeconds } from "@/utils/executionTimeFormatter";
|
||||
import { getLocale } from "@/utils/localization";
|
||||
import { tw } from "@/utils/tailwind";
|
||||
@ -47,17 +51,9 @@ const SECTIONS = [
|
||||
type SectionName = (typeof SECTIONS)[number];
|
||||
|
||||
const POLL_INTERVAL_SECONDS = 5;
|
||||
const RUNNING_STATES = [
|
||||
"running",
|
||||
"starting",
|
||||
"waiting_capacity",
|
||||
"waiting_org_limit",
|
||||
"stopping",
|
||||
] as CrawlState[];
|
||||
|
||||
export const QA_RUNNING_STATES = [
|
||||
"starting",
|
||||
...RUNNING_STATES,
|
||||
...activeCrawlStates,
|
||||
] as CrawlState[];
|
||||
|
||||
/**
|
||||
@ -149,7 +145,7 @@ export class ArchivedItemDetail extends TailwindElement {
|
||||
|
||||
private get isActive(): boolean | null {
|
||||
if (!this.crawl) return null;
|
||||
return RUNNING_STATES.includes(this.crawl.state);
|
||||
return activeCrawlStates.includes(this.crawl.state);
|
||||
}
|
||||
|
||||
private get isQAActive(): boolean | null {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { localized, msg, str } from "@lit/localize";
|
||||
import { Task } from "@lit/task";
|
||||
import type {
|
||||
SlChangeEvent,
|
||||
SlSelect,
|
||||
@ -27,7 +28,6 @@ import { NavigateController } from "@/controllers/navigate";
|
||||
import { NotifyController } from "@/controllers/notify";
|
||||
import { iconFor as iconForPageReview } from "@/features/qa/page-list/helpers";
|
||||
import * as pageApproval from "@/features/qa/page-list/helpers/approval";
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import type { SelectDetail } from "@/features/qa/qa-run-dropdown";
|
||||
import type {
|
||||
APIPaginatedList,
|
||||
@ -36,11 +36,35 @@ import type {
|
||||
} from "@/types/api";
|
||||
import { type ArchivedItem, type ArchivedItemPage } from "@/types/crawler";
|
||||
import type { QARun } from "@/types/qa";
|
||||
import { type AuthState } from "@/utils/AuthService";
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { type Auth, type AuthState } from "@/utils/AuthService";
|
||||
import { finishedCrawlStates } from "@/utils/crawler";
|
||||
import { humanizeExecutionSeconds } from "@/utils/executionTimeFormatter";
|
||||
import { getLocale, pluralize } from "@/utils/localization";
|
||||
import { formatNumber, getLocale } from "@/utils/localization";
|
||||
import { pluralOf } from "@/utils/pluralize";
|
||||
|
||||
type QAStatsThreshold = {
|
||||
lowerBoundary: `${number}` | "No data";
|
||||
count: number;
|
||||
};
|
||||
type QAStats = Record<"screenshotMatch" | "textMatch", QAStatsThreshold[]>;
|
||||
|
||||
const qaStatsThresholds = [
|
||||
{
|
||||
lowerBoundary: "0.0",
|
||||
cssColor: "var(--sl-color-danger-500)",
|
||||
label: msg("Severe Inconsistencies"),
|
||||
},
|
||||
{
|
||||
lowerBoundary: "0.5",
|
||||
cssColor: "var(--sl-color-warning-500)",
|
||||
label: msg("Moderate Inconsistencies"),
|
||||
},
|
||||
{
|
||||
lowerBoundary: "0.9",
|
||||
cssColor: "var(--sl-color-success-500)",
|
||||
label: msg("Good Match"),
|
||||
},
|
||||
];
|
||||
|
||||
const notApplicable = () =>
|
||||
html`<span class="text-neutral-400">${msg("n/a")}</span>`;
|
||||
@ -98,6 +122,16 @@ export class ArchivedItemDetailQA extends TailwindElement {
|
||||
@state()
|
||||
private pages?: APIPaginatedList<ArchivedItemPage>;
|
||||
|
||||
private readonly qaStats = new Task(this, {
|
||||
task: async ([orgId, crawlId, qaRunId, authState]) => {
|
||||
if (!qaRunId || !authState) throw new Error("Missing args");
|
||||
const stats = await this.getQAStats(orgId, crawlId, qaRunId, authState);
|
||||
return stats;
|
||||
},
|
||||
args: () =>
|
||||
[this.orgId!, this.crawlId!, this.qaRunId, this.authState] as const,
|
||||
});
|
||||
|
||||
@state()
|
||||
private deleting: string | null = null;
|
||||
|
||||
@ -197,102 +231,24 @@ export class ArchivedItemDetailQA extends TailwindElement {
|
||||
<btrix-tab-group>
|
||||
<btrix-tab-group-tab slot="nav" panel="pages">
|
||||
<sl-icon name="file-richtext-fill"></sl-icon>
|
||||
${msg("Review Pages")}
|
||||
${msg("Pages")}
|
||||
</btrix-tab-group-tab>
|
||||
<btrix-tab-group-tab
|
||||
slot="nav"
|
||||
panel="runs"
|
||||
?disabled=${!this.qaRuns?.length}
|
||||
>
|
||||
<sl-icon name="list-ul"></sl-icon>
|
||||
${msg("Analysis Runs")}
|
||||
</btrix-tab-group-tab>
|
||||
${when(
|
||||
this.qaRuns,
|
||||
(qaRuns) => html`
|
||||
<btrix-tab-group-tab
|
||||
slot="nav"
|
||||
panel="runs"
|
||||
?disabled=${!qaRuns.length}
|
||||
>
|
||||
<sl-icon name="list-ul"></sl-icon>
|
||||
${msg("Analysis Runs")}
|
||||
</btrix-tab-group-tab>
|
||||
`,
|
||||
)}
|
||||
|
||||
<sl-divider></sl-divider>
|
||||
|
||||
<btrix-tab-group-panel name="pages" class="block">
|
||||
${
|
||||
// TODO un-hide this once we've got data in here
|
||||
nothing
|
||||
// <section class="mb-7">
|
||||
// <div class="mb-2 flex items-center">
|
||||
// <h4 class="mr-3 text-lg font-semibold leading-8">
|
||||
// ${msg("QA Analysis")}
|
||||
// </h4>
|
||||
// ${when(this.qaRuns, (qaRuns) => {
|
||||
// const finishedQARuns = qaRuns.filter(({ state }) =>
|
||||
// finishedCrawlStates.includes(state),
|
||||
// );
|
||||
${when(this.mostRecentNonFailedQARun && this.qaRuns, (qaRuns) =>
|
||||
this.renderAnalysis(qaRuns),
|
||||
)}
|
||||
|
||||
// if (!finishedQARuns.length) {
|
||||
// return nothing;
|
||||
// }
|
||||
|
||||
// const mostRecentSelected =
|
||||
// this.mostRecentNonFailedQARun &&
|
||||
// this.mostRecentNonFailedQARun.id === this.qaRunId;
|
||||
// const latestFinishedSelected =
|
||||
// this.qaRunId === finishedQARuns[0].id;
|
||||
|
||||
// return html`
|
||||
// <sl-tooltip
|
||||
// content=${mostRecentSelected
|
||||
// ? msg(
|
||||
// "You're viewing the latest results from a finished analysis run.",
|
||||
// )
|
||||
// : msg(
|
||||
// "You're viewing results from an older analysis run.",
|
||||
// )}
|
||||
// >
|
||||
// <sl-tag
|
||||
// size="small"
|
||||
// variant=${mostRecentSelected ? "success" : "warning"}
|
||||
// >
|
||||
// ${mostRecentSelected
|
||||
// ? msg("Current")
|
||||
// : latestFinishedSelected
|
||||
// ? msg("Last Finished")
|
||||
// : msg("Outdated")}
|
||||
// </sl-tag>
|
||||
// </sl-tooltip>
|
||||
// <btrix-qa-run-dropdown
|
||||
// .items=${finishedQARuns}
|
||||
// selectedId=${this.qaRunId || ""}
|
||||
// @btrix-select=${(e: CustomEvent<SelectDetail>) =>
|
||||
// (this.qaRunId = e.detail.item.id)}
|
||||
// ></btrix-qa-run-dropdown>
|
||||
// `;
|
||||
// })}
|
||||
// </div>
|
||||
// ${when(
|
||||
// this.qaRuns,
|
||||
// () =>
|
||||
// this.mostRecentNonFailedQARun
|
||||
// ? this.renderAnalysis()
|
||||
// : html`
|
||||
// <div
|
||||
// class="rounded-lg border bg-slate-50 p-4 text-center text-slate-600"
|
||||
// >
|
||||
// ${msg(
|
||||
// "This crawl hasn’t been analyzed yet. Run an analysis to access crawl quality metrics.",
|
||||
// )}
|
||||
// </div>
|
||||
// `,
|
||||
|
||||
// () =>
|
||||
// html`<div
|
||||
// class="grid h-[55px] place-content-center rounded-lg border bg-slate-50 p-4 text-lg text-slate-600"
|
||||
// >
|
||||
// <sl-spinner></sl-spinner>
|
||||
// </div>`,
|
||||
// )}
|
||||
// </section>
|
||||
}
|
||||
<div>
|
||||
<h4 class="mb-2 mt-4 text-lg leading-8">
|
||||
<span class="font-semibold">${msg("Pages")}</span> (${(
|
||||
@ -339,7 +295,7 @@ export class ArchivedItemDetailQA extends TailwindElement {
|
||||
class="col-span-4 flex h-full flex-col items-center justify-center gap-2 p-3 text-xs text-neutral-500"
|
||||
>
|
||||
<sl-icon name="slash-circle"></sl-icon>
|
||||
${msg("No analysis runs found")}
|
||||
${msg("No analysis runs, yet")}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -452,7 +408,7 @@ export class ArchivedItemDetailQA extends TailwindElement {
|
||||
${runToBeDeleted &&
|
||||
html`<div>
|
||||
${msg(
|
||||
str`This analysis run includes data for ${runToBeDeleted.stats.done} ${pluralize(runToBeDeleted.stats.done, { zero: msg("pages", { desc: 'plural form of "page" for zero pages', id: "pages.plural.zero" }), one: msg("page"), two: msg("pages", { desc: 'plural form of "page" for two pages', id: "pages.plural.two" }), few: msg("pages", { desc: 'plural form of "page" for few pages', id: "pages.plural.few" }), many: msg("pages", { desc: 'plural form of "page" for many pages', id: "pages.plural.many" }), other: msg("pages", { desc: 'plural form of "page" for multiple/other pages', id: "pages.plural.other" }) })} and was started on `,
|
||||
str`This analysis run includes data for ${runToBeDeleted.stats.done} ${pluralOf("pages", runToBeDeleted.stats.done)} and was started on `,
|
||||
)}
|
||||
<sl-format-date
|
||||
lang=${getLocale()}
|
||||
@ -492,10 +448,23 @@ export class ArchivedItemDetailQA extends TailwindElement {
|
||||
private readonly renderLoadingDetail = () =>
|
||||
html`<div class="min-w-32"><sl-spinner class="h-4 w-4"></sl-spinner></div>`;
|
||||
|
||||
private renderAnalysis() {
|
||||
private renderAnalysis(qaRuns: QARun[]) {
|
||||
const isRunning =
|
||||
this.mostRecentNonFailedQARun &&
|
||||
QA_RUNNING_STATES.includes(this.mostRecentNonFailedQARun.state);
|
||||
const qaRun = qaRuns.find(({ id }) => id === this.qaRunId);
|
||||
|
||||
if (!qaRun && isRunning) {
|
||||
return html`<btrix-alert class="mb-3" variant="success">
|
||||
${msg("Running QA analysis on pages...")}
|
||||
</btrix-alert>`;
|
||||
}
|
||||
|
||||
if (!qaRun) {
|
||||
return html`<btrix-alert class="mb-3" variant="warning">
|
||||
${msg("This analysis run doesn't exist.")}
|
||||
</btrix-alert>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
${isRunning
|
||||
@ -505,20 +474,199 @@ export class ArchivedItemDetailQA extends TailwindElement {
|
||||
)}
|
||||
</btrix-alert>`
|
||||
: nothing}
|
||||
<div class="flex flex-col gap-6 md:flex-row">
|
||||
<btrix-card class="flex-1">
|
||||
<span slot="title">${msg("Screenshots")}</span>
|
||||
TODO
|
||||
</btrix-card>
|
||||
<btrix-card class="flex-1">
|
||||
<span slot="title">${msg("Extracted Text")}</span>
|
||||
TODO
|
||||
</btrix-card>
|
||||
<btrix-card class="flex-1">
|
||||
<span slot="title">${msg("Page Resources")}</span>
|
||||
TODO
|
||||
</btrix-card>
|
||||
</div>
|
||||
<btrix-card>
|
||||
<div slot="title" class="flex flex-wrap justify-between">
|
||||
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
${msg("Page Match Analysis")}
|
||||
${when(this.qaRuns, (qaRuns) => {
|
||||
const finishedQARuns = qaRuns.filter(({ state }) =>
|
||||
finishedCrawlStates.includes(state),
|
||||
);
|
||||
|
||||
if (!finishedQARuns.length) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const mostRecentSelected =
|
||||
this.mostRecentNonFailedQARun &&
|
||||
this.mostRecentNonFailedQARun.id === this.qaRunId;
|
||||
const latestFinishedSelected =
|
||||
this.qaRunId === finishedQARuns[0].id;
|
||||
|
||||
return html`
|
||||
<div>
|
||||
<sl-tooltip
|
||||
content=${mostRecentSelected
|
||||
? msg(
|
||||
"You’re viewing the latest results from a finished analysis run.",
|
||||
)
|
||||
: msg(
|
||||
"You’re viewing results from an older analysis run.",
|
||||
)}
|
||||
>
|
||||
<sl-tag
|
||||
size="small"
|
||||
variant=${mostRecentSelected ? "success" : "warning"}
|
||||
>
|
||||
${mostRecentSelected
|
||||
? msg("Current")
|
||||
: latestFinishedSelected
|
||||
? msg("Last Finished")
|
||||
: msg("Outdated")}
|
||||
</sl-tag>
|
||||
</sl-tooltip>
|
||||
<btrix-qa-run-dropdown
|
||||
.items=${finishedQARuns}
|
||||
selectedId=${this.qaRunId || ""}
|
||||
@btrix-select=${(e: CustomEvent<SelectDetail>) =>
|
||||
(this.qaRunId = e.detail.item.id)}
|
||||
></btrix-qa-run-dropdown>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-neutral-500">
|
||||
${when(
|
||||
qaRun.state.startsWith("stop") ||
|
||||
(qaRun.state === "complete" &&
|
||||
qaRun.stats.done < qaRun.stats.found),
|
||||
() =>
|
||||
html`<sl-tooltip
|
||||
content=${qaRun.state.startsWith("stop")
|
||||
? msg("This analysis run was stopped and is not complete.")
|
||||
: msg(
|
||||
"Not all pages in this crawl were analyzed. This is likely because some pages are not HTML pages, but other types of documents.",
|
||||
)}
|
||||
class="[--max-width:theme(spacing.56)]"
|
||||
>
|
||||
<sl-icon
|
||||
name="exclamation-triangle-fill"
|
||||
class="text-warning"
|
||||
label=${msg("Note about page counts")}
|
||||
></sl-icon>
|
||||
</sl-tooltip> `,
|
||||
)}
|
||||
${when(
|
||||
qaRun.stats,
|
||||
(stats) => html`
|
||||
<div class="text-sm font-normal">
|
||||
${formatNumber(stats.done)} / ${formatNumber(stats.found)}
|
||||
${pluralOf("pages", stats.found)} ${msg("analyzed")}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
<sl-tooltip
|
||||
content=${msg(
|
||||
"Match analysis compares pages during a crawl against their replay during an analysis run. A good match indicates that the crawl is probably good, whereas severe inconsistencies may indicate a bad crawl.",
|
||||
)}
|
||||
>
|
||||
<sl-icon class="text-base" name="info-circle"></sl-icon>
|
||||
</sl-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<figure>
|
||||
<btrix-table class="grid-cols-[min-content_1fr]">
|
||||
<btrix-table-head class="sr-only">
|
||||
<btrix-table-header-cell>
|
||||
${msg("Statistic")}
|
||||
</btrix-table-header-cell>
|
||||
<btrix-table-header-cell>
|
||||
${msg("Chart")}
|
||||
</btrix-table-header-cell>
|
||||
</btrix-table-head>
|
||||
<btrix-table-body>
|
||||
<btrix-table-row>
|
||||
<btrix-table-cell class="font-medium">
|
||||
${msg("Screenshots")}
|
||||
</btrix-table-cell>
|
||||
<btrix-table-cell class="p-0">
|
||||
${this.qaStats.render({
|
||||
complete: ({ screenshotMatch }) =>
|
||||
this.renderMeter(qaRun.stats.found, screenshotMatch),
|
||||
pending: () => this.renderMeter(),
|
||||
initial: () => this.renderMeter(),
|
||||
})}
|
||||
</btrix-table-cell>
|
||||
</btrix-table-row>
|
||||
<btrix-table-row>
|
||||
<btrix-table-cell class="font-medium">
|
||||
${msg("Text")}
|
||||
</btrix-table-cell>
|
||||
<btrix-table-cell class="p-0">
|
||||
${this.qaStats.render({
|
||||
complete: ({ textMatch }) =>
|
||||
this.renderMeter(qaRun.stats.found, textMatch),
|
||||
pending: () => this.renderMeter(),
|
||||
initial: () => this.renderMeter(),
|
||||
})}
|
||||
</btrix-table-cell>
|
||||
</btrix-table-row>
|
||||
</btrix-table-body>
|
||||
</btrix-table>
|
||||
</figure>
|
||||
<figcaption slot="footer" class="mt-2">
|
||||
<dl class="flex flex-wrap items-center justify-end gap-4">
|
||||
${qaStatsThresholds.map(
|
||||
(threshold) => html`
|
||||
<div class="flex items-center gap-2">
|
||||
<dt
|
||||
class="h-4 w-4 flex-shrink-0 rounded"
|
||||
style="background-color: ${threshold.cssColor}"
|
||||
>
|
||||
<span class="sr-only">${threshold.lowerBoundary}</span>
|
||||
</dt>
|
||||
<dd>${threshold.label}</dd>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</dl>
|
||||
</figcaption>
|
||||
</btrix-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderMeter(pageCount?: number, barData?: QAStatsThreshold[]) {
|
||||
if (pageCount === undefined || !barData) {
|
||||
return html`<sl-skeleton
|
||||
class="h-4 flex-1 [--border-radius:var(--sl-border-radius-medium)]"
|
||||
effect="sheen"
|
||||
></sl-skeleton>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<btrix-meter class="flex-1" value=${pageCount}>
|
||||
${barData.map((bar) => {
|
||||
const threshold = qaStatsThresholds.find(
|
||||
({ lowerBoundary }) => bar.lowerBoundary === lowerBoundary,
|
||||
);
|
||||
const idx = threshold ? qaStatsThresholds.indexOf(threshold) : -1;
|
||||
|
||||
return html`
|
||||
<btrix-meter-bar
|
||||
value=${(bar.count / pageCount) * 100}
|
||||
style="--background-color: ${threshold?.cssColor}"
|
||||
aria-label=${bar.lowerBoundary}
|
||||
>
|
||||
<div class="text-center">
|
||||
${bar.lowerBoundary === "No data"
|
||||
? msg("No Data")
|
||||
: threshold?.label}
|
||||
<div class="text-xs opacity-80">
|
||||
${bar.lowerBoundary !== "No data"
|
||||
? html`${idx === 0
|
||||
? `<${+qaStatsThresholds[idx + 1].lowerBoundary * 100}%`
|
||||
: idx === qaStatsThresholds.length - 1
|
||||
? `>=${threshold ? +threshold.lowerBoundary * 100 : 0}%`
|
||||
: `${threshold ? +threshold.lowerBoundary * 100 : 0}-${+qaStatsThresholds[idx + 1].lowerBoundary * 100}%`}
|
||||
match <br />`
|
||||
: nothing}
|
||||
${formatNumber(bar.count)} ${pluralOf("pages", bar.count)}
|
||||
</div>
|
||||
</div>
|
||||
</btrix-meter-bar>
|
||||
`;
|
||||
})}
|
||||
</btrix-meter>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -612,11 +760,7 @@ export class ArchivedItemDetailQA extends TailwindElement {
|
||||
name="chat-square-text-fill"
|
||||
class="text-blue-600"
|
||||
></sl-icon>`,
|
||||
page.notes.length === 1
|
||||
? msg(str`1 comment`)
|
||||
: msg(
|
||||
str`${page.notes.length.toLocaleString()} comments`,
|
||||
),
|
||||
`${page.notes.length.toLocaleString()} ${pluralOf("comments", page.notes.length)}`,
|
||||
)
|
||||
: html`<span class="text-neutral-400"
|
||||
>${msg("None")}</span
|
||||
@ -709,7 +853,7 @@ export class ArchivedItemDetailQA extends TailwindElement {
|
||||
);
|
||||
}
|
||||
|
||||
async getQARunDownloadLink(qaRunId: string) {
|
||||
private async getQARunDownloadLink(qaRunId: string) {
|
||||
try {
|
||||
const { resources } = await this.api.fetch<QARun>(
|
||||
`/orgs/${this.orgId}/crawls/${this.crawlId}/qa/${qaRunId}/replay.json`,
|
||||
@ -722,7 +866,7 @@ export class ArchivedItemDetailQA extends TailwindElement {
|
||||
}
|
||||
}
|
||||
|
||||
async deleteQARun(id: string) {
|
||||
private async deleteQARun(id: string) {
|
||||
try {
|
||||
await this.api.fetch(
|
||||
`/orgs/${this.orgId}/crawls/${this.crawlId}/qa/delete`,
|
||||
@ -733,4 +877,26 @@ export class ArchivedItemDetailQA extends TailwindElement {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getQAStats(
|
||||
orgId: string,
|
||||
crawlId: string,
|
||||
qaRunId: string,
|
||||
authState: Auth,
|
||||
) {
|
||||
const query = queryString.stringify(
|
||||
{
|
||||
screenshotThresholds: [0.5, 0.9],
|
||||
textThresholds: [0.5, 0.9],
|
||||
},
|
||||
{
|
||||
arrayFormat: "comma",
|
||||
},
|
||||
);
|
||||
|
||||
return this.api.fetch<QAStats>(
|
||||
`/orgs/${orgId}/crawls/${crawlId}/qa/${qaRunId}/stats?${query}`,
|
||||
authState,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
63
frontend/src/utils/pluralize.ts
Normal file
63
frontend/src/utils/pluralize.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { msg } from "@lit/localize";
|
||||
|
||||
import { pluralize } from "./localization";
|
||||
|
||||
// Add to this as necessary!
|
||||
const plurals = {
|
||||
pages: {
|
||||
zero: msg("pages", {
|
||||
desc: 'plural form of "page" for zero pages',
|
||||
id: "pages.plural.zero",
|
||||
}),
|
||||
one: msg("page", {
|
||||
desc: 'singular form for "page"',
|
||||
id: "pages.plural.one",
|
||||
}),
|
||||
two: msg("pages", {
|
||||
desc: 'plural form of "page" for two pages',
|
||||
id: "pages.plural.two",
|
||||
}),
|
||||
few: msg("pages", {
|
||||
desc: 'plural form of "page" for few pages',
|
||||
id: "pages.plural.few",
|
||||
}),
|
||||
many: msg("pages", {
|
||||
desc: 'plural form of "page" for many pages',
|
||||
id: "pages.plural.many",
|
||||
}),
|
||||
other: msg("pages", {
|
||||
desc: 'plural form of "page" for multiple/other pages',
|
||||
id: "pages.plural.other",
|
||||
}),
|
||||
},
|
||||
comments: {
|
||||
zero: msg("comments", {
|
||||
desc: 'plural form of "comment" for zero comments',
|
||||
id: "comments.plural.zero",
|
||||
}),
|
||||
one: msg("comment", {
|
||||
desc: 'singular form for "comment"',
|
||||
id: "comments.plural.one",
|
||||
}),
|
||||
two: msg("comments", {
|
||||
desc: 'plural form of "comment" for two comments',
|
||||
id: "comments.plural.two",
|
||||
}),
|
||||
few: msg("comments", {
|
||||
desc: 'plural form of "comment" for few comments',
|
||||
id: "comments.plural.few",
|
||||
}),
|
||||
many: msg("comments", {
|
||||
desc: 'plural form of "comment" for many comments',
|
||||
id: "comments.plural.many",
|
||||
}),
|
||||
other: msg("comments", {
|
||||
desc: 'plural form of "comment" for multiple/other comments',
|
||||
id: "comments.plural.other",
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export const pluralOf = (word: keyof typeof plurals, count: number) => {
|
||||
return pluralize(count, plurals[word]);
|
||||
};
|
@ -811,6 +811,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz#d693d972974a354034454ec1317eb6afd0b00312"
|
||||
integrity sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g==
|
||||
|
||||
"@lit-labs/ssr-dom-shim@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.0.tgz#353ce4a76c83fadec272ea5674ede767650762fd"
|
||||
integrity sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g==
|
||||
|
||||
"@lit/localize-tools@^0.7.1":
|
||||
version "0.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@lit/localize-tools/-/localize-tools-0.7.1.tgz#cb80af296d99c029c59cec57813ae8f11d43a777"
|
||||
@ -847,6 +852,13 @@
|
||||
dependencies:
|
||||
"@lit-labs/ssr-dom-shim" "^1.0.0"
|
||||
|
||||
"@lit/reactive-element@^1.0.0 || ^2.0.0":
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.0.4.tgz#8f2ed950a848016383894a26180ff06c56ae001b"
|
||||
integrity sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==
|
||||
dependencies:
|
||||
"@lit-labs/ssr-dom-shim" "^1.2.0"
|
||||
|
||||
"@lit/reactive-element@^2.0.0":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.0.2.tgz#779ae9d265407daaf7737cb892df5ec2a86e22a0"
|
||||
@ -854,6 +866,13 @@
|
||||
dependencies:
|
||||
"@lit-labs/ssr-dom-shim" "^1.1.2"
|
||||
|
||||
"@lit/task@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@lit/task/-/task-1.0.0.tgz#61ae9ac6131368bbcf5f09ccade8037e6bb8705e"
|
||||
integrity sha512-7jocGBh3yGlo3kKxQggZph2txK4X5GYNWp2FAsmV9u2spzUypwrzRzXe8I72icAb02B00+k2nlvxVcrQB6vyrw==
|
||||
dependencies:
|
||||
"@lit/reactive-element" "^1.0.0 || ^2.0.0"
|
||||
|
||||
"@mdn/browser-compat-data@^4.0.0":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-4.2.1.tgz#1fead437f3957ceebe2e8c3f46beccdb9bc575b8"
|
||||
|
Loading…
Reference in New Issue
Block a user