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",
|
"@cheap-glitch/mi-cron": "^1.0.1",
|
||||||
"@ianvs/prettier-plugin-sort-imports": "^4.2.1",
|
"@ianvs/prettier-plugin-sort-imports": "^4.2.1",
|
||||||
"@lit/localize": "^0.12.1",
|
"@lit/localize": "^0.12.1",
|
||||||
|
"@lit/task": "^1.0.0",
|
||||||
"@novnc/novnc": "^1.4.0-beta",
|
"@novnc/novnc": "^1.4.0-beta",
|
||||||
"@rollup/plugin-commonjs": "^18.0.0",
|
"@rollup/plugin-commonjs": "^18.0.0",
|
||||||
"@shoelace-style/shoelace": "~2.13.0",
|
"@shoelace-style/shoelace": "~2.13.0",
|
||||||
|
@ -8,10 +8,13 @@ export class Card extends TailwindElement {
|
|||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<section class="flex h-full flex-col rounded border p-4">
|
<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>
|
<slot name="title"></slot>
|
||||||
</h2>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1" aria-labelledby="cardHeading">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
<slot name="footer"></slot>
|
<slot name="footer"></slot>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { css, html, LitElement, type PropertyValues } from "lit";
|
import { css, html, type PropertyValues } from "lit";
|
||||||
import {
|
import {
|
||||||
customElement,
|
customElement,
|
||||||
property,
|
property,
|
||||||
@ -10,10 +10,12 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
|||||||
import { when } from "lit/directives/when.js";
|
import { when } from "lit/directives/when.js";
|
||||||
import debounce from "lodash/fp/debounce";
|
import debounce from "lodash/fp/debounce";
|
||||||
|
|
||||||
|
import { TailwindElement } from "@/classes/TailwindElement";
|
||||||
import type { UnderlyingFunction } from "@/types/utils";
|
import type { UnderlyingFunction } from "@/types/utils";
|
||||||
|
import { tw } from "@/utils/tailwind";
|
||||||
|
|
||||||
@customElement("btrix-meter-bar")
|
@customElement("btrix-meter-bar")
|
||||||
export class MeterBar extends LitElement {
|
export class MeterBar extends TailwindElement {
|
||||||
/* Percentage of value / max */
|
/* Percentage of value / max */
|
||||||
@property({ type: Number })
|
@property({ type: Number })
|
||||||
value = 0;
|
value = 0;
|
||||||
@ -43,7 +45,7 @@ export class MeterBar extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@customElement("btrix-divided-meter-bar")
|
@customElement("btrix-divided-meter-bar")
|
||||||
export class DividedMeterBar extends LitElement {
|
export class DividedMeterBar extends TailwindElement {
|
||||||
/* Percentage of value / max */
|
/* Percentage of value / max */
|
||||||
@property({ type: Number })
|
@property({ type: Number })
|
||||||
value = 0;
|
value = 0;
|
||||||
@ -101,7 +103,7 @@ export class DividedMeterBar extends LitElement {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@customElement("btrix-meter")
|
@customElement("btrix-meter")
|
||||||
export class Meter extends LitElement {
|
export class Meter extends TailwindElement {
|
||||||
@property({ type: Number })
|
@property({ type: Number })
|
||||||
min = 0;
|
min = 0;
|
||||||
|
|
||||||
@ -111,25 +113,30 @@ export class Meter extends LitElement {
|
|||||||
@property({ type: Number })
|
@property({ type: Number })
|
||||||
value = 0;
|
value = 0;
|
||||||
|
|
||||||
@property({ type: Array })
|
|
||||||
subValues?: number[];
|
|
||||||
|
|
||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
valueText?: string;
|
valueText?: string;
|
||||||
|
|
||||||
@query(".valueBar")
|
|
||||||
private readonly valueBar?: HTMLElement;
|
|
||||||
|
|
||||||
@query(".labels")
|
@query(".labels")
|
||||||
private readonly labels?: HTMLElement;
|
private readonly labels?: HTMLElement;
|
||||||
|
|
||||||
|
@query(".valueBar")
|
||||||
|
private readonly valueBar?: HTMLElement;
|
||||||
|
|
||||||
@query(".maxText")
|
@query(".maxText")
|
||||||
private readonly maxText?: HTMLElement;
|
private readonly maxText?: HTMLElement;
|
||||||
|
|
||||||
|
@queryAssignedElements({ slot: "valueLabel" })
|
||||||
|
private readonly valueLabel!: HTMLElement[];
|
||||||
|
|
||||||
// postcss-lit-disable-next-line
|
// postcss-lit-disable-next-line
|
||||||
static styles = css`
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.meter {
|
.meter {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track {
|
.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() {
|
render() {
|
||||||
// meter spec disallow values that exceed max
|
// meter spec disallow values that exceed max
|
||||||
const max = this.max ? Math.max(this.value, this.max) : this.value;
|
const max = this.max ? Math.max(this.value, this.max) : this.value;
|
||||||
|
@ -25,6 +25,9 @@ export class CrawlStatus extends TailwindElement {
|
|||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
stopping = false;
|
stopping = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
hoist = false;
|
||||||
|
|
||||||
static styles = [
|
static styles = [
|
||||||
animatePulse,
|
animatePulse,
|
||||||
css`
|
css`
|
||||||
@ -235,6 +238,7 @@ export class CrawlStatus extends TailwindElement {
|
|||||||
content=${label}
|
content=${label}
|
||||||
@sl-hide=${(e: SlHideEvent) => e.stopPropagation()}
|
@sl-hide=${(e: SlHideEvent) => e.stopPropagation()}
|
||||||
@sl-after-hide=${(e: SlHideEvent) => e.stopPropagation()}
|
@sl-after-hide=${(e: SlHideEvent) => e.stopPropagation()}
|
||||||
|
.hoist=${this.hoist}
|
||||||
>
|
>
|
||||||
<div>${icon}</div>
|
<div>${icon}</div>
|
||||||
</sl-tooltip>
|
</sl-tooltip>
|
||||||
|
@ -32,7 +32,14 @@ export class QaRunDropdown extends TailwindElement {
|
|||||||
<sl-dropdown @sl-select=${this.onSelect} distance="-2">
|
<sl-dropdown @sl-select=${this.onSelect} distance="-2">
|
||||||
<sl-button slot="trigger" variant="text" size="small" caret>
|
<sl-button slot="trigger" variant="text" size="small" caret>
|
||||||
${selectedRun
|
${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")}
|
: msg("Select a QA run")}
|
||||||
</sl-button>
|
</sl-button>
|
||||||
<sl-menu>
|
<sl-menu>
|
||||||
@ -46,6 +53,13 @@ export class QaRunDropdown extends TailwindElement {
|
|||||||
?checked=${isSelected}
|
?checked=${isSelected}
|
||||||
>
|
>
|
||||||
${formatDate(run.finished)}
|
${formatDate(run.finished)}
|
||||||
|
<btrix-crawl-status
|
||||||
|
type="qa"
|
||||||
|
hideLabel
|
||||||
|
state=${run.state}
|
||||||
|
slot="prefix"
|
||||||
|
hoist
|
||||||
|
></btrix-crawl-status>
|
||||||
</sl-menu-item>
|
</sl-menu-item>
|
||||||
`;
|
`;
|
||||||
})}
|
})}
|
||||||
|
@ -27,7 +27,11 @@ import type {
|
|||||||
import type { QARun } from "@/types/qa";
|
import type { QARun } from "@/types/qa";
|
||||||
import { isApiError } from "@/utils/api";
|
import { isApiError } from "@/utils/api";
|
||||||
import type { AuthState } from "@/utils/AuthService";
|
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 { humanizeExecutionSeconds } from "@/utils/executionTimeFormatter";
|
||||||
import { getLocale } from "@/utils/localization";
|
import { getLocale } from "@/utils/localization";
|
||||||
import { tw } from "@/utils/tailwind";
|
import { tw } from "@/utils/tailwind";
|
||||||
@ -47,17 +51,9 @@ const SECTIONS = [
|
|||||||
type SectionName = (typeof SECTIONS)[number];
|
type SectionName = (typeof SECTIONS)[number];
|
||||||
|
|
||||||
const POLL_INTERVAL_SECONDS = 5;
|
const POLL_INTERVAL_SECONDS = 5;
|
||||||
const RUNNING_STATES = [
|
|
||||||
"running",
|
|
||||||
"starting",
|
|
||||||
"waiting_capacity",
|
|
||||||
"waiting_org_limit",
|
|
||||||
"stopping",
|
|
||||||
] as CrawlState[];
|
|
||||||
|
|
||||||
export const QA_RUNNING_STATES = [
|
export const QA_RUNNING_STATES = [
|
||||||
"starting",
|
"starting",
|
||||||
...RUNNING_STATES,
|
...activeCrawlStates,
|
||||||
] as CrawlState[];
|
] as CrawlState[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -149,7 +145,7 @@ export class ArchivedItemDetail extends TailwindElement {
|
|||||||
|
|
||||||
private get isActive(): boolean | null {
|
private get isActive(): boolean | null {
|
||||||
if (!this.crawl) return null;
|
if (!this.crawl) return null;
|
||||||
return RUNNING_STATES.includes(this.crawl.state);
|
return activeCrawlStates.includes(this.crawl.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get isQAActive(): boolean | null {
|
private get isQAActive(): boolean | null {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { localized, msg, str } from "@lit/localize";
|
import { localized, msg, str } from "@lit/localize";
|
||||||
|
import { Task } from "@lit/task";
|
||||||
import type {
|
import type {
|
||||||
SlChangeEvent,
|
SlChangeEvent,
|
||||||
SlSelect,
|
SlSelect,
|
||||||
@ -27,7 +28,6 @@ import { NavigateController } from "@/controllers/navigate";
|
|||||||
import { NotifyController } from "@/controllers/notify";
|
import { NotifyController } from "@/controllers/notify";
|
||||||
import { iconFor as iconForPageReview } from "@/features/qa/page-list/helpers";
|
import { iconFor as iconForPageReview } from "@/features/qa/page-list/helpers";
|
||||||
import * as pageApproval from "@/features/qa/page-list/helpers/approval";
|
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 { SelectDetail } from "@/features/qa/qa-run-dropdown";
|
||||||
import type {
|
import type {
|
||||||
APIPaginatedList,
|
APIPaginatedList,
|
||||||
@ -36,11 +36,35 @@ import type {
|
|||||||
} from "@/types/api";
|
} from "@/types/api";
|
||||||
import { type ArchivedItem, type ArchivedItemPage } from "@/types/crawler";
|
import { type ArchivedItem, type ArchivedItemPage } from "@/types/crawler";
|
||||||
import type { QARun } from "@/types/qa";
|
import type { QARun } from "@/types/qa";
|
||||||
import { type AuthState } from "@/utils/AuthService";
|
import { type Auth, type AuthState } from "@/utils/AuthService";
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
import { finishedCrawlStates } from "@/utils/crawler";
|
import { finishedCrawlStates } from "@/utils/crawler";
|
||||||
import { humanizeExecutionSeconds } from "@/utils/executionTimeFormatter";
|
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 = () =>
|
const notApplicable = () =>
|
||||||
html`<span class="text-neutral-400">${msg("n/a")}</span>`;
|
html`<span class="text-neutral-400">${msg("n/a")}</span>`;
|
||||||
@ -98,6 +122,16 @@ export class ArchivedItemDetailQA extends TailwindElement {
|
|||||||
@state()
|
@state()
|
||||||
private pages?: APIPaginatedList<ArchivedItemPage>;
|
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()
|
@state()
|
||||||
private deleting: string | null = null;
|
private deleting: string | null = null;
|
||||||
|
|
||||||
@ -197,102 +231,24 @@ export class ArchivedItemDetailQA extends TailwindElement {
|
|||||||
<btrix-tab-group>
|
<btrix-tab-group>
|
||||||
<btrix-tab-group-tab slot="nav" panel="pages">
|
<btrix-tab-group-tab slot="nav" panel="pages">
|
||||||
<sl-icon name="file-richtext-fill"></sl-icon>
|
<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>
|
</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>
|
<sl-divider></sl-divider>
|
||||||
|
|
||||||
<btrix-tab-group-panel name="pages" class="block">
|
<btrix-tab-group-panel name="pages" class="block">
|
||||||
${
|
${when(this.mostRecentNonFailedQARun && this.qaRuns, (qaRuns) =>
|
||||||
// TODO un-hide this once we've got data in here
|
this.renderAnalysis(qaRuns),
|
||||||
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),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// 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>
|
<div>
|
||||||
<h4 class="mb-2 mt-4 text-lg leading-8">
|
<h4 class="mb-2 mt-4 text-lg leading-8">
|
||||||
<span class="font-semibold">${msg("Pages")}</span> (${(
|
<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"
|
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>
|
<sl-icon name="slash-circle"></sl-icon>
|
||||||
${msg("No analysis runs found")}
|
${msg("No analysis runs, yet")}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -452,7 +408,7 @@ export class ArchivedItemDetailQA extends TailwindElement {
|
|||||||
${runToBeDeleted &&
|
${runToBeDeleted &&
|
||||||
html`<div>
|
html`<div>
|
||||||
${msg(
|
${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
|
<sl-format-date
|
||||||
lang=${getLocale()}
|
lang=${getLocale()}
|
||||||
@ -492,10 +448,23 @@ export class ArchivedItemDetailQA extends TailwindElement {
|
|||||||
private readonly renderLoadingDetail = () =>
|
private readonly renderLoadingDetail = () =>
|
||||||
html`<div class="min-w-32"><sl-spinner class="h-4 w-4"></sl-spinner></div>`;
|
html`<div class="min-w-32"><sl-spinner class="h-4 w-4"></sl-spinner></div>`;
|
||||||
|
|
||||||
private renderAnalysis() {
|
private renderAnalysis(qaRuns: QARun[]) {
|
||||||
const isRunning =
|
const isRunning =
|
||||||
this.mostRecentNonFailedQARun &&
|
this.mostRecentNonFailedQARun &&
|
||||||
QA_RUNNING_STATES.includes(this.mostRecentNonFailedQARun.state);
|
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`
|
return html`
|
||||||
${isRunning
|
${isRunning
|
||||||
@ -505,20 +474,199 @@ export class ArchivedItemDetailQA extends TailwindElement {
|
|||||||
)}
|
)}
|
||||||
</btrix-alert>`
|
</btrix-alert>`
|
||||||
: nothing}
|
: nothing}
|
||||||
<div class="flex flex-col gap-6 md:flex-row">
|
<btrix-card>
|
||||||
<btrix-card class="flex-1">
|
<div slot="title" class="flex flex-wrap justify-between">
|
||||||
<span slot="title">${msg("Screenshots")}</span>
|
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||||
TODO
|
${msg("Page Match Analysis")}
|
||||||
</btrix-card>
|
${when(this.qaRuns, (qaRuns) => {
|
||||||
<btrix-card class="flex-1">
|
const finishedQARuns = qaRuns.filter(({ state }) =>
|
||||||
<span slot="title">${msg("Extracted Text")}</span>
|
finishedCrawlStates.includes(state),
|
||||||
TODO
|
);
|
||||||
</btrix-card>
|
|
||||||
<btrix-card class="flex-1">
|
if (!finishedQARuns.length) {
|
||||||
<span slot="title">${msg("Page Resources")}</span>
|
return nothing;
|
||||||
TODO
|
}
|
||||||
</btrix-card>
|
|
||||||
</div>
|
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"
|
name="chat-square-text-fill"
|
||||||
class="text-blue-600"
|
class="text-blue-600"
|
||||||
></sl-icon>`,
|
></sl-icon>`,
|
||||||
page.notes.length === 1
|
`${page.notes.length.toLocaleString()} ${pluralOf("comments", page.notes.length)}`,
|
||||||
? msg(str`1 comment`)
|
|
||||||
: msg(
|
|
||||||
str`${page.notes.length.toLocaleString()} comments`,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: html`<span class="text-neutral-400"
|
: html`<span class="text-neutral-400"
|
||||||
>${msg("None")}</span
|
>${msg("None")}</span
|
||||||
@ -709,7 +853,7 @@ export class ArchivedItemDetailQA extends TailwindElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getQARunDownloadLink(qaRunId: string) {
|
private async getQARunDownloadLink(qaRunId: string) {
|
||||||
try {
|
try {
|
||||||
const { resources } = await this.api.fetch<QARun>(
|
const { resources } = await this.api.fetch<QARun>(
|
||||||
`/orgs/${this.orgId}/crawls/${this.crawlId}/qa/${qaRunId}/replay.json`,
|
`/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 {
|
try {
|
||||||
await this.api.fetch(
|
await this.api.fetch(
|
||||||
`/orgs/${this.orgId}/crawls/${this.crawlId}/qa/delete`,
|
`/orgs/${this.orgId}/crawls/${this.crawlId}/qa/delete`,
|
||||||
@ -733,4 +877,26 @@ export class ArchivedItemDetailQA extends TailwindElement {
|
|||||||
console.error(e);
|
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"
|
resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz#d693d972974a354034454ec1317eb6afd0b00312"
|
||||||
integrity sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g==
|
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":
|
"@lit/localize-tools@^0.7.1":
|
||||||
version "0.7.1"
|
version "0.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/@lit/localize-tools/-/localize-tools-0.7.1.tgz#cb80af296d99c029c59cec57813ae8f11d43a777"
|
resolved "https://registry.yarnpkg.com/@lit/localize-tools/-/localize-tools-0.7.1.tgz#cb80af296d99c029c59cec57813ae8f11d43a777"
|
||||||
@ -847,6 +852,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@lit-labs/ssr-dom-shim" "^1.0.0"
|
"@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":
|
"@lit/reactive-element@^2.0.0":
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.0.2.tgz#779ae9d265407daaf7737cb892df5ec2a86e22a0"
|
resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.0.2.tgz#779ae9d265407daaf7737cb892df5ec2a86e22a0"
|
||||||
@ -854,6 +866,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@lit-labs/ssr-dom-shim" "^1.1.2"
|
"@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":
|
"@mdn/browser-compat-data@^4.0.0":
|
||||||
version "4.2.1"
|
version "4.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-4.2.1.tgz#1fead437f3957ceebe2e8c3f46beccdb9bc575b8"
|
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