${waitingMsg}
@@ -1062,60 +1160,77 @@ export class WorkflowDetail extends BtrixElement {
};
private renderInactiveWatchCrawl() {
+ if (!this.workflow) return;
+
+ if (!this.lastCrawlId || !this.workflow.lastCrawlSize) {
+ return this.renderInactiveCrawlMessage();
+ }
+
+ return html`
+
+ ${this.renderReplay()}
+
+ `;
+ }
+
+ private renderInactiveCrawlMessage() {
+ if (!this.workflow) return;
+
+ let message = msg("This workflow hasn’t been run yet.");
+
+ if (this.lastCrawlId) {
+ if (this.workflow.lastCrawlState === "canceled") {
+ message = msg("This crawl can’t be replayed since it was canceled.");
+ } else {
+ message = msg("Replay is not enabled on this crawl.");
+ }
+ }
+
return html`
-
- ${msg("Crawl workflow is not currently running.")}
-
-
- ${when(
- this.workflow?.lastCrawlId && this.workflow,
- (workflow) => html`
+
${message}
+
+ ${when(
+ this.isCrawler && !this.lastCrawlId,
+ () => html`
${this.renderRunNowButton()}
`,
+ )}
+ ${when(
+ this.lastCrawlId,
+ () =>
+ html`
-
- ${msg("Replay Latest Crawl")}
-
- ${when(
- this.isCrawler,
- () =>
- html`
-
- ${msg("QA Latest Crawl")}
- `,
- )}
- `,
- () => (this.isCrawler ? this.renderRunNowButton() : nothing),
- )}
-
+ ${msg("View Crawl Details")}
+
+
+
`,
+ )}
`;
}
- private renderInactiveCrawlMessage() {
+ private renderReplay() {
+ if (!this.workflow || !this.lastCrawlId) return;
+
+ const replaySource = `/api/orgs/${this.workflow.oid}/crawls/${this.lastCrawlId}/replay.json`;
+ const headers = this.authState?.headers;
+ const config = JSON.stringify({ headers });
+
return html`
-
-
${msg("Crawl is not running.")}
-
+
`;
}
@@ -1123,29 +1238,16 @@ export class WorkflowDetail extends BtrixElement {
return html`
${when(
- this.lastCrawlId || this.workflow?.lastCrawlId,
- (crawlId) =>
- this.workflow?.isCrawlRunning
- ? html`
-
-
- ${msg(
- "You are viewing error and behavior logs for the currently running crawl.",
- )}
-
- ${msg("Watch Crawl")}
-
-
-
-
- `
- : html`
`,
+ this.lastCrawlId,
+ (crawlId) => html`
+
+ `,
() => this.renderNoCrawlLogs(),
)}
@@ -1189,19 +1291,6 @@ export class WorkflowDetail extends BtrixElement {
`;
}
- private renderWatchLogs() {
- if (!this.lastCrawlId) return;
-
- return html`
-
- `;
- }
-
private renderExclusions() {
return html`
@@ -1398,7 +1487,7 @@ export class WorkflowDetail extends BtrixElement {
),
variant: "danger",
icon: "exclamation-octagon",
- id: "archived-item-retrieve-error",
+ id: "data-retrieve-error",
});
}
}
@@ -1418,7 +1507,7 @@ export class WorkflowDetail extends BtrixElement {
message: msg("Sorry, couldn't get crawls at this time."),
variant: "danger",
icon: "exclamation-octagon",
- id: "archived-item-retrieve-error",
+ id: "data-retrieve-error",
});
}
}
@@ -1441,21 +1530,43 @@ export class WorkflowDetail extends BtrixElement {
return data;
}
- private async fetchCurrentCrawlStats() {
+ private stopPoll() {
+ window.clearTimeout(this.timerId);
+ }
+
+ private async fetchLastCrawl() {
if (!this.lastCrawlId) return;
+ let crawlState: CrawlState | null = null;
+
try {
- // TODO see if API can pass stats in GET workflow
- const { stats } = await this.getCrawl(this.lastCrawlId);
- this.lastCrawlStats = stats;
- } catch (e) {
- // TODO handle error
- console.debug(e);
+ const { stats, pageCount, reviewStatus, state } = await this.getCrawl(
+ this.lastCrawlId,
+ );
+ this.lastCrawl = { stats, pageCount, reviewStatus };
+
+ crawlState = state;
+ } catch {
+ this.notify.toast({
+ message: msg("Sorry, couldn't retrieve latest crawl at this time."),
+ variant: "danger",
+ icon: "exclamation-octagon",
+ id: "data-retrieve-error",
+ });
}
- }
- private stopPoll() {
- window.clearTimeout(this.timerId);
+ if (
+ !this.logTotals ||
+ (crawlState && isActive({ state: crawlState })) ||
+ this.workflowTab === WorkflowTab.Logs
+ ) {
+ try {
+ this.logTotals = await this.getLogTotals(this.lastCrawlId);
+ } catch (err) {
+ // Fail silently, since we're fetching just the total
+ console.debug(err);
+ }
+ }
}
private async getCrawl(crawlId: Crawl["id"]): Promise {
@@ -1466,6 +1577,26 @@ export class WorkflowDetail extends BtrixElement {
return data;
}
+ private async getLogTotals(
+ crawlId: Crawl["id"],
+ ): Promise {
+ const query = queryString.stringify({ pageSize: 1 });
+
+ const [errors, behaviors] = await Promise.all([
+ this.api.fetch>(
+ `/orgs/${this.orgId}/crawls/${crawlId}/errors?${query}`,
+ ),
+ this.api.fetch>(
+ `/orgs/${this.orgId}/crawls/${crawlId}/behaviorLogs?${query}`,
+ ),
+ ]);
+
+ return {
+ errors: errors.total,
+ behaviors: behaviors.total,
+ };
+ }
+
/**
* Create a new template using existing template data
*/
@@ -1593,7 +1724,7 @@ export class WorkflowDetail extends BtrixElement {
this.lastCrawlId = data.started;
this.lastCrawlStartTime = new Date().toISOString();
void this.fetchWorkflow();
- this.goToTab("watch");
+ this.navigate.to(`${this.basePath}/${WorkflowTab.LatestCrawl}`);
this.notify.toast({
message: msg("Starting crawl."),
@@ -1680,4 +1811,29 @@ export class WorkflowDetail extends BtrixElement {
});
}
}
+
+ /**
+ * Handle redirects to new tabs introduced in
+ * https://github.com/webrecorder/browsertrix/issues/2603
+ */
+ private redirectHash() {
+ const hashValue = window.location.hash.slice(1);
+
+ switch (hashValue) {
+ case "watch":
+ this.navigate.to(`${this.basePath}/${WorkflowTab.LatestCrawl}`, {
+ replace: true,
+ });
+ break;
+ case "crawls":
+ case "logs":
+ case "settings":
+ this.navigate.to(`${this.basePath}/${hashValue}`, {
+ replace: true,
+ });
+ break;
+ default:
+ break;
+ }
+ }
}
diff --git a/frontend/src/pages/org/workflows-list.ts b/frontend/src/pages/org/workflows-list.ts
index b77e9c73d8..5f5ce071a8 100644
--- a/frontend/src/pages/org/workflows-list.ts
+++ b/frontend/src/pages/org/workflows-list.ts
@@ -24,6 +24,7 @@ import { type SelectEvent } from "@/components/ui/search-combobox";
import { ClipboardController } from "@/controllers/clipboard";
import type { SelectJobTypeEvent } from "@/features/crawl-workflows/new-workflow-dialog";
import { pageHeader } from "@/layouts/pageHeader";
+import { WorkflowTab } from "@/routes";
import scopeTypeLabels from "@/strings/crawl-workflows/scopeType";
import { deleteConfirmation } from "@/strings/ui";
import type { APIPaginatedList, APIPaginationQuery } from "@/types/api";
@@ -585,7 +586,7 @@ export class WorkflowsList extends BtrixElement {
this.navigate.to(
- `${this.navigate.orgBasePath}/workflows/${workflow.id}#watch`,
+ `${this.navigate.orgBasePath}/workflows/${workflow.id}/${WorkflowTab.LatestCrawl}`,
{
dialog: "scale",
},
@@ -598,7 +599,7 @@ export class WorkflowsList extends BtrixElement {
?disabled=${workflow.lastCrawlState !== "running"}
@click=${() =>
this.navigate.to(
- `${this.navigate.orgBasePath}/workflows/${workflow.id}#watch`,
+ `${this.navigate.orgBasePath}/workflows/${workflow.id}/${WorkflowTab.LatestCrawl}`,
{
dialog: "exclusions",
},
@@ -900,7 +901,8 @@ export class WorkflowsList extends BtrixElement {
Watch crawl`,
diff --git a/frontend/src/pages/org/workflows-new.ts b/frontend/src/pages/org/workflows-new.ts
index 9a41f92a18..5b274f2ccb 100644
--- a/frontend/src/pages/org/workflows-new.ts
+++ b/frontend/src/pages/org/workflows-new.ts
@@ -95,7 +95,7 @@ export class WorkflowsNew extends LiteElement {
return html`
${this.renderBreadcrumbs()}
${msg("New Crawl Workflow")}
;
* In its most basic configuration, the only required fields
* are a list of items, and a list of columns that define which
* key-value pairs of an item should be displayed.
+ *
+ * Nested keys are supported by specifying a deep path, e.g.
+ * `object.nestedObject.key`.
*/
export const Basic: Story = {
args: {},
@@ -103,7 +106,7 @@ export const ColumnWidths: Story = {
*/
export const RemoveRows: Story = {
args: {
- removeRows: true,
+ rowsRemovable: true,
},
};
@@ -112,7 +115,7 @@ export const RemoveRows: Story = {
*/
export const AddRows: Story = {
args: {
- addRows: true,
+ rowsAddible: true,
defaultItem: {
a: "A",
b: "--",
@@ -129,7 +132,7 @@ export const AddRows: Story = {
export const AddRowsInput: Story = {
name: "Add more than one row",
args: {
- addRows: true,
+ rowsAddible: true,
addRowsInputValue: 5,
defaultItem: {
a: "A",
@@ -141,6 +144,18 @@ export const AddRowsInput: Story = {
},
};
+/**
+ * Rows can be selected.
+ *
+ * Open your browser console logs to view the clicked row.
+ */
+export const SelectRow: Story = {
+ args: {
+ items: makeItems(5),
+ rowsSelectable: true,
+ },
+};
+
/**
* Cells can be editable.
*/
@@ -262,9 +277,9 @@ export const FormControl: Story = {
}
formControlLabel="Page QA Table"
stickyHeader="table"
- addRows
+ rowsAddible
addRowsInputValue="10"
- removeRows
+ rowsRemovable
editCells
>
${renderRows(
diff --git a/frontend/src/stories/components/DataGrid.ts b/frontend/src/stories/components/DataGrid.ts
index 78262dae23..6a3ba8ba40 100644
--- a/frontend/src/stories/components/DataGrid.ts
+++ b/frontend/src/stories/components/DataGrid.ts
@@ -3,6 +3,7 @@ import { ifDefined } from "lit/directives/if-defined.js";
import { nanoid } from "nanoid";
import type { DataGrid } from "@/components/ui/data-grid/data-grid";
+import type { BtrixSelectRowEvent } from "@/components/ui/data-grid/events/btrix-select-row";
import "@/components/ui/data-grid";
@@ -37,9 +38,11 @@ export const renderComponent = ({
items,
formControlLabel,
stickyHeader,
- addRows,
+ rowsAddible,
addRowsInputValue,
- removeRows,
+ rowsRemovable,
+ rowsSelectable,
+ selectMode,
editCells,
defaultItem,
}: Partial) => {
@@ -50,10 +53,15 @@ export const renderComponent = ({
.defaultItem=${defaultItem}
formControlLabel=${ifDefined(formControlLabel)}
stickyHeader=${ifDefined(stickyHeader)}
- ?addRows=${addRows}
+ ?rowsAddible=${rowsAddible}
addRowsInputValue=${ifDefined(addRowsInputValue)}
- ?removeRows=${removeRows}
+ ?rowsRemovable=${rowsRemovable}
+ ?rowsSelectable=${rowsSelectable}
+ selectMode=${ifDefined(selectMode)}
?editCells=${editCells}
+ @btrix-select-row=${(e: BtrixSelectRowEvent) => {
+ console.log("row clicked:", e.detail);
+ }}
>
`;
diff --git a/frontend/src/strings/archived-items/tooltips.ts b/frontend/src/strings/archived-items/tooltips.ts
new file mode 100644
index 0000000000..9705de3a35
--- /dev/null
+++ b/frontend/src/strings/archived-items/tooltips.ts
@@ -0,0 +1,6 @@
+import { msg } from "@lit/localize";
+
+export const tooltipFor = {
+ downloadMultWacz: msg(msg("Download Files as Multi-WACZ")),
+ downloadLogs: msg("Download Entire Log File"),
+};
diff --git a/frontend/src/theme.stylesheet.css b/frontend/src/theme.stylesheet.css
index 9cd979404c..4a2321dd73 100644
--- a/frontend/src/theme.stylesheet.css
+++ b/frontend/src/theme.stylesheet.css
@@ -260,7 +260,7 @@
/* Style tooltip with white background */
sl-tooltip.invert-tooltip {
--sl-tooltip-arrow-size: 0;
- --sl-tooltip-background-color: var(--sl-color-neutral-0);
+ --sl-tooltip-background-color: var(--sl-color-neutral-50);
--sl-tooltip-color: var(--sl-color-neutral-700);
}
diff --git a/frontend/src/utils/pluralize.ts b/frontend/src/utils/pluralize.ts
index 5fac6a9a6b..0131538b81 100644
--- a/frontend/src/utils/pluralize.ts
+++ b/frontend/src/utils/pluralize.ts
@@ -169,6 +169,58 @@ const plurals = {
id: "rows.plural.other",
}),
},
+ errors: {
+ zero: msg("errors", {
+ desc: 'plural form of "errors" for zero errors',
+ id: "errors.plural.zero",
+ }),
+ one: msg("error", {
+ desc: 'singular form for "error"',
+ id: "errors.plural.one",
+ }),
+ two: msg("errors", {
+ desc: 'plural form of "errors" for two errors',
+ id: "errors.plural.two",
+ }),
+ few: msg("errors", {
+ desc: 'plural form of "errors" for few errors',
+ id: "errors.plural.few",
+ }),
+ many: msg("errors", {
+ desc: 'plural form of "errors" for many errors',
+ id: "errors.plural.many",
+ }),
+ other: msg("errors", {
+ desc: 'plural form of "errors" for multiple/other errors',
+ id: "errors.plural.other",
+ }),
+ },
+ browserWindows: {
+ zero: msg("browser windows", {
+ desc: 'plural form of "browser windows" for zero browser windows',
+ id: "browserWindows.plural.zero",
+ }),
+ one: msg("browser window", {
+ desc: 'singular form for "browser window"',
+ id: "browserWindows.plural.one",
+ }),
+ two: msg("browser windows", {
+ desc: 'plural form of "browser windows" for two browser windows',
+ id: "browserWindows.plural.two",
+ }),
+ few: msg("browser windows", {
+ desc: 'plural form of "browser windows" for few browser windows',
+ id: "browserWindows.plural.few",
+ }),
+ many: msg("browser windows", {
+ desc: 'plural form of "browser windows" for many browser windows',
+ id: "browserWindows.plural.many",
+ }),
+ other: msg("browser windows", {
+ desc: 'plural form of "browser windows" for multiple/other browser windows',
+ id: "browserWindows.plural.other",
+ }),
+ },
};
export const pluralOf = (word: keyof typeof plurals, count: number) => {
diff --git a/version.txt b/version.txt
index 41c11ffb73..4a02d2c317 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-1.16.1
+1.16.2