) {
if (
(changedProperties.has("workflowId") && this.workflowId) ||
(changedProperties.get("isEditing") === true && this.isEditing === false)
) {
this.fetchWorkflow();
this.fetchSeeds();
}
if (changedProperties.has("isEditing")) {
if (this.isEditing) {
this.stopPoll();
} else {
this.getActivePanelFromHash();
}
}
if (
!this.isEditing &&
changedProperties.has("activePanel") &&
this.activePanel
) {
if (!this.isPanelHeaderVisible) {
// Scroll panel header into view
this.querySelector("btrix-tab-list")?.scrollIntoView({
behavior: "smooth",
});
}
if (this.activePanel === "crawls") {
this.fetchCrawls();
}
}
}
private getActivePanelFromHash = async () => {
await this.updateComplete;
if (this.isEditing) return;
const hashValue = window.location.hash.slice(1);
if (SECTIONS.includes(hashValue as any)) {
this.activePanel = hashValue as Tab;
} else {
this.goToTab(DEFAULT_SECTION, { replace: true });
}
};
private goToTab(tab: Tab, { replace = false } = {}) {
const path = `${window.location.href.split("#")[0]}#${tab}`;
if (replace) {
window.history.replaceState(null, "", path);
} else {
window.history.pushState(null, "", path);
}
this.activePanel = tab;
}
private async fetchWorkflow() {
this.stopPoll();
this.isLoading = true;
try {
const prevLastCrawlId = this.lastCrawlId;
this.getWorkflowPromise = this.getWorkflow();
this.workflow = await this.getWorkflowPromise;
this.lastCrawlId = this.workflow.lastCrawlId;
this.lastCrawlStartTime = this.workflow.lastCrawlStartTime;
if (this.lastCrawlId) {
if (this.workflow.isCrawlRunning) {
this.fetchCurrentCrawlStats();
this.fetchCrawlLogs();
} else if (this.lastCrawlId !== prevLastCrawlId) {
this.logs = undefined;
this.fetchCrawlLogs();
}
}
// TODO: Check if storage quota has been exceeded here by running
// crawl??
} catch (e: any) {
this.notify({
message:
e.statusCode === 404
? msg("Workflow not found.")
: msg("Sorry, couldn't retrieve Workflow at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
this.isLoading = false;
if (!this.isEditing) {
// Restart timer for next poll
this.timerId = window.setTimeout(() => {
this.fetchWorkflow();
}, 1000 * POLL_INTERVAL_SECONDS);
}
}
render() {
if (this.isEditing && this.isCrawler) {
return html`
${when(this.workflow, this.renderEditor)}
`;
}
return html`
${this.renderHeader()}
${when(this.workflow, this.renderTabList, this.renderLoading)}
(this.openDialogName = undefined)}
@sl-show=${this.showDialog}
@sl-after-hide=${() => (this.isDialogVisible = false)}
>
${msg(
"Pages crawled so far will be saved and marked as incomplete. Are you sure you want to stop crawling?"
)}
(this.openDialogName = undefined)}
>${msg("Keep Crawling")}
{
await this.stop();
this.openDialogName = undefined;
}}
>${msg("Stop Crawling")}
(this.openDialogName = undefined)}
@sl-show=${this.showDialog}
@sl-after-hide=${() => (this.isDialogVisible = false)}
>
${msg(
"Canceling will discard all pages crawled. Are you sure you want to discard them?"
)}
(this.openDialogName = undefined)}
>${msg("Keep Crawling")}
{
await this.cancel();
this.openDialogName = undefined;
}}
>${msg("Cancel & Discard Crawl")}
(this.openDialogName = undefined)}
@sl-show=${this.showDialog}
@sl-after-hide=${() => (this.isDialogVisible = false)}
>
${msg(
"All files and logs associated with this crawl will also be deleted, and the crawl will be removed from any Collection it is a part of."
)}
(this.openDialogName = undefined)}
>${msg("Cancel")}
{
this.openDialogName = undefined;
if (this.crawlToDelete) {
await this.deleteCrawl(this.crawlToDelete);
}
}}
>${msg("Delete Crawl")}
`;
}
private renderHeader(workflowId?: string) {
return html`
${workflowId
? msg(html`Back to ${this.renderName()}`)
: msg("Back to Crawl Workflows")}
`;
}
private renderTabList = () => html`
(this.isPanelHeaderVisible = detail.entry.isIntersecting)}
>
${this.renderPanelHeader()}
${this.renderTab("crawls")} ${this.renderTab("watch")}
${this.renderTab("logs")} ${this.renderTab("settings")}
${this.renderCrawls()}
${until(
this.getWorkflowPromise?.then(
() => html`
${when(this.activePanel === "watch", () =>
this.workflow?.isCrawlRunning
? html`
${this.renderCurrentCrawl()}
${this.renderWatchCrawl()}`
: this.renderInactiveWatchCrawl()
)}
`
)
)}
${this.renderLogs()}
${this.renderSettings()}
`;
private renderPanelHeader() {
if (!this.activePanel) return;
if (this.activePanel === "crawls") {
return html`
${this.tabLabels[this.activePanel]}
${when(
this.crawls,
() =>
html`
(${this.crawls!.total.toLocaleString()}${this.workflow
?.isCrawlRunning
? html` + 1 `
: ""})
`
)}
`;
}
if (this.activePanel === "settings" && this.isCrawler == true) {
return html` ${this.tabLabels[this.activePanel]}
this.navTo(
`/orgs/${this.appState.orgSlug}/workflows/crawl/${this.workflow?.id}?edit`
)}
>
`;
}
if (this.activePanel === "watch" && this.isCrawler == true) {
return html` ${this.tabLabels[this.activePanel]}
(this.openDialogName = "scale")}
>
${msg("Edit Crawler Instances")}
`;
}
if (this.activePanel === "logs") {
const authToken = this.authState!.headers.Authorization.split(" ")[1];
const isDownloadEnabled = Boolean(
this.logs?.total &&
this.workflow?.lastCrawlId &&
!this.workflow.isCrawlRunning
);
return html` ${this.tabLabels[this.activePanel]}
${msg("Download Logs")}
`;
}
return html`${this.tabLabels[this.activePanel]} `;
}
private renderTab(tabName: Tab, { disabled = false } = {}) {
const isActive = tabName === this.activePanel;
let className = "text-neutral-600 hover:bg-neutral-50";
if (isActive) {
className = "text-blue-600 bg-blue-50 shadow-sm";
} else if (disabled) {
className = "text-neutral-300 cursor-not-allowed";
}
return html`
{
if (disabled) e.preventDefault();
}}
>
${this.tabLabels[tabName]}
`;
}
private renderEditor = () => html`
${this.renderHeader(this.workflow!.id)}
${when(
!this.isLoading && this.seeds,
() => html`
this.navTo(
`${this.orgBasePath}/workflows/crawl/${this.workflow!.id}`
)}
>
`,
this.renderLoading
)}
`;
private renderActions = () => {
if (!this.workflow) return;
const workflow = this.workflow;
return html`
${when(
this.workflow?.isCrawlRunning,
() => html`
(this.openDialogName = "stop")}
?disabled=${!this.lastCrawlId ||
this.isCancelingOrStoppingCrawl ||
this.workflow?.lastCrawlStopping}
>
${msg("Stop")}
(this.openDialogName = "cancel")}
?disabled=${!this.lastCrawlId || this.isCancelingOrStoppingCrawl}
>
${msg("Cancel")}
`,
() => html`
this.runNow()}
>
${msg("Run Crawl")}
`
)}
${msg("Actions")}
${when(
this.workflow?.isCrawlRunning,
// HACK shoelace doesn't current have a way to override non-hover
// color without resetting the --sl-color-neutral-700 variable
() => html`
(this.openDialogName = "stop")}
?disabled=${workflow.lastCrawlStopping ||
this.isCancelingOrStoppingCrawl}
>
${msg("Stop Crawl")}
(this.openDialogName = "cancel")}
>
${msg("Cancel & Discard Crawl")}
`,
() => html`
this.runNow()}
>
${msg("Run Crawl")}
`
)}
${when(
workflow.isCrawlRunning,
() => html`
(this.openDialogName = "scale")}>
${msg("Edit Crawler Instances")}
(this.openDialogName = "exclusions")}
>
${msg("Edit Exclusions")}
`
)}
this.navTo(
`/orgs/${this.appState.orgSlug}/workflows/crawl/${workflow.id}?edit`
)}
>
${msg("Edit Workflow Settings")}
CopyButton.copyToClipboard(workflow.tags.join(", "))}
?disabled=${!workflow.tags.length}
>
${msg("Copy Tags")}
this.duplicateConfig()}>
${msg("Duplicate Workflow")}
${when(!this.lastCrawlId, () => {
const shouldDeactivate = workflow.crawlCount && !workflow.inactive;
return html`
shouldDeactivate ? this.deactivate() : this.delete()}
>
${shouldDeactivate
? msg("Deactivate Workflow")
: msg("Delete Workflow")}
`;
})}
`;
};
private renderDetails() {
return html`
${this.renderDetailItem(
msg("Status"),
() => html`
`
)}
${this.renderDetailItem(
msg("Total Size"),
() => html` `
)}
${this.renderDetailItem(msg("Schedule"), () =>
this.workflow!.schedule
? html`
${humanizeSchedule(this.workflow!.schedule, {
length: "short",
})}
`
: html`${msg("No Schedule")} `
)}
${this.renderDetailItem(msg("Created By"), () =>
msg(
str`${this.workflow!.createdByName} on ${this.dateFormatter.format(
new Date(`${this.workflow!.created}Z`)
)}`
)
)}
`;
}
private renderDetailItem(
label: string | TemplateResult,
renderContent: () => any
) {
return html`
${when(
this.workflow,
renderContent,
() => html` `
)}
`;
}
private renderName() {
if (!this.workflow) return "";
if (this.workflow.name) return this.workflow.name;
const { seedCount, firstSeed } = this.workflow;
if (seedCount === 1) {
return firstSeed;
}
const remainderCount = seedCount - 1;
if (remainderCount === 1) {
return msg(
html`${firstSeed}
+${remainderCount} URL `
);
}
return msg(
html`${firstSeed}
+${remainderCount} URLs `
);
}
private renderCrawls() {
return html`
${msg("View:")}
{
const value = (e.target as SlSelect).value as CrawlState[];
await this.updateComplete;
this.filterBy = {
...this.filterBy,
state: value,
};
this.fetchCrawls();
}}
>
${inactiveCrawlStates.map(this.renderStatusMenuItem)}
${when(
this.workflow?.isCrawlRunning,
() => html``
)}
${msg("Start Time")}
${when(
this.crawls,
() =>
this.crawls!.items.map(
(crawl: Crawl) => html`
${when(
this.isCrawler,
() => html`
this.confirmDeleteCrawl(crawl)}
>
${msg("Delete Crawl")}
`
)} `
),
() => html`
`
)}
${when(
this.crawls && !this.crawls.items.length,
() => html`
${this.crawls?.total
? msg("No matching crawls found.")
: msg("No crawls yet.")}
`
)}
`;
}
private renderStatusMenuItem = (state: CrawlState) => {
const { icon, label } = CrawlStatus.getContent(state);
return html`${icon}${label} `;
};
private renderCurrentCrawl = () => {
const skeleton = html` `;
return html`
${this.renderDetailItem(msg("Pages Crawled"), () =>
this.lastCrawlStats
? msg(
str`${this.numberFormatter.format(
+(this.lastCrawlStats.done || 0)
)} / ${this.numberFormatter.format(
+(this.lastCrawlStats.found || 0)
)}`
)
: html` `
)}
${this.renderDetailItem(msg("Run Duration"), () =>
this.lastCrawlStartTime
? RelativeDuration.humanize(
new Date().valueOf() -
new Date(`${this.lastCrawlStartTime}Z`).valueOf()
)
: skeleton
)}
${this.renderDetailItem(msg("Crawl Size"), () =>
this.workflow
? html` `
: skeleton
)}
${this.renderDetailItem(msg("Crawler Instances"), () =>
this.workflow ? this.workflow.scale : skeleton
)}
`;
};
private renderWatchCrawl = () => {
if (!this.authState || !this.workflow?.lastCrawlState) return "";
let waitingMsg = null;
switch (this.workflow.lastCrawlState) {
case "starting":
waitingMsg = msg("Crawl starting...");
break;
case "waiting_capacity":
waitingMsg = msg(
"Crawl waiting for available resources before it can continue..."
);
break;
case "waiting_org_limit":
waitingMsg = msg(
"Crawl waiting for others to finish, concurrent limit per Organization reached..."
);
break;
}
const isRunning = this.workflow.lastCrawlState === "running";
const isStopping = this.workflow.lastCrawlStopping;
const authToken = this.authState.headers.Authorization.split(" ")[1];
return html`
${waitingMsg
? html``
: isActive(this.workflow.lastCrawlState)
? html`
${isStopping
? html`
${msg("Crawl stopping...")}
`
: ""}
`
: this.renderInactiveCrawlMessage()}
${when(
isRunning,
() => html`
${this.renderCrawlErrors()}
${this.renderExclusions()}
(this.openDialogName = undefined)}
@sl-show=${this.showDialog}
@sl-after-hide=${() => (this.isDialogVisible = false)}
>
${this.isDialogVisible ? this.renderEditScale() : ""}
`
)}
`;
};
private renderInactiveWatchCrawl() {
return html`
${msg("Crawl is not currently running.")}
${when(
this.workflow?.lastCrawlId,
() => html`
${msg("Replay Latest Crawl")}
`
)}
${when(
this.isCrawler,
() => html`
this.runNow()}
>
${msg("Run Crawl")}
`
)}
`;
}
private renderInactiveCrawlMessage() {
return html`
${msg("Crawl is not running.")}
`;
}
private renderLogs() {
return html`
${when(
this.workflow?.isCrawlRunning,
() => html`
`
)}
${when(
this.lastCrawlId,
() =>
this.logs?.total
? html`
{
await this.fetchCrawlLogs({
page: e.detail.page,
});
// Scroll to top of list
this.scrollIntoView();
}}
> `
: html`
${this.workflow?.lastCrawlState === "waiting_capacity"
? msg("Error logs currently not available.")
: msg("No error logs found yet for latest crawl.")}
`,
() => this.renderNoCrawlLogs()
)}
`;
}
private renderNoCrawlLogs() {
return html`
${msg("Logs will show here after you run a crawl.")}
this.runNow()}
>
${msg("Run Crawl")}
`;
}
private renderCrawlErrors() {
return html`
${msg("Error Logs")}
${this.logs?.total
? this.logs?.total.toLocaleString()
: 0}
${when(
this.logs?.total && this.logs.total > LOGS_PAGE_SIZE,
() => html`
${msg(
str`Displaying latest ${LOGS_PAGE_SIZE.toLocaleString()} errors of ${this.logs!.total.toLocaleString()}.`
)}
`
)}
`;
}
private renderExclusions() {
return html`
${msg("Crawl URLs")}
(this.openDialogName = "exclusions")}
>
${msg("Edit Exclusions")}
${when(
this.lastCrawlId,
() => html`
`
)}
(this.openDialogName = undefined)}
@sl-show=${this.showDialog}
@sl-after-hide=${() => (this.isDialogVisible = false)}
>
${this.workflow && this.isDialogVisible
? html` `
: ""}
${msg("Done Editing")}
`;
}
private renderEditScale() {
if (!this.workflow) return;
const scaleOptions = [
{
value: 1,
label: "1×",
},
{
value: 2,
label: "2×",
},
{
value: 3,
label: "3×",
},
];
return html`
${scaleOptions.map(
({ value, label }) => html`
{
await this.scale(value);
this.openDialogName = undefined;
}}
?disabled=${this.isSubmittingUpdate}
>${label}
`
)}
(this.openDialogName = undefined)}
>${msg("Cancel")}
`;
}
private renderSettings() {
return html``;
}
private renderLoading = () => html`
`;
private showDialog = async () => {
await this.getWorkflowPromise;
this.isDialogVisible = true;
};
private handleExclusionChange() {
this.fetchWorkflow();
}
private async scale(value: Crawl["scale"]) {
if (!this.lastCrawlId) return;
this.isSubmittingUpdate = true;
try {
const data = await this.apiFetch<{ scaled: boolean }>(
`/orgs/${this.orgId}/crawls/${this.lastCrawlId}/scale`,
this.authState!,
{
method: "POST",
body: JSON.stringify({ scale: +value }),
}
);
if (data.scaled) {
this.fetchWorkflow();
this.notify({
message: msg("Updated crawl scale."),
variant: "success",
icon: "check2-circle",
});
} else {
throw new Error("unhandled API response");
}
} catch {
this.notify({
message: msg("Sorry, couldn't change crawl scale at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
this.isSubmittingUpdate = false;
}
private async getWorkflow(): Promise {
const data: Workflow = await this.apiFetch(
`/orgs/${this.orgId}/crawlconfigs/${this.workflowId}`,
this.authState!
);
return data;
}
private async onCloseExclusions() {
const editor = this.querySelector("btrix-exclusion-editor");
if (editor && editor instanceof ExclusionEditor) {
await editor.onClose();
}
this.openDialogName = undefined;
}
private async fetchSeeds(): Promise {
try {
this.getSeedsPromise = this.getSeeds();
this.seeds = await this.getSeedsPromise;
} catch {
this.notify({
message: msg(
"Sorry, couldn't retrieve all crawl settings at this time."
),
variant: "danger",
icon: "exclamation-octagon",
});
}
}
private async getSeeds() {
const data = await this.apiFetch>(
`/orgs/${this.orgId}/crawlconfigs/${this.workflowId}/seeds`,
this.authState!
);
return data;
}
private async fetchCrawls() {
try {
this.crawls = await this.getCrawls();
} catch {
this.notify({
message: msg("Sorry, couldn't get crawls at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
}
private async getCrawls() {
const query = queryString.stringify(
{
state: this.filterBy.state,
cid: this.workflowId,
sortBy: "started",
},
{
arrayFormat: "comma",
}
);
const data = await this.apiFetch>(
`/orgs/${this.orgId}/crawls?${query}`,
this.authState!
);
return data;
}
private async fetchCurrentCrawlStats() {
if (!this.lastCrawlId) return;
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);
}
}
private stopPoll() {
window.clearTimeout(this.timerId);
}
private async getCrawl(crawlId: Crawl["id"]): Promise {
const data = await this.apiFetch(
`/orgs/${this.orgId}/crawls/${crawlId}/replay.json`,
this.authState!
);
return data;
}
/**
* Create a new template using existing template data
*/
private async duplicateConfig() {
if (!this.workflow) await this.getWorkflowPromise;
if (!this.seeds) await this.getSeedsPromise;
await this.updateComplete;
if (!this.workflow) return;
const workflowParams: WorkflowParams = {
...this.workflow,
name: this.workflow.name ? msg(str`${this.workflow.name} Copy`) : "",
};
this.navTo(
`${this.orgBasePath}/workflows?new&jobType=${workflowParams.jobType}`,
{
workflow: workflowParams,
seeds: this.seeds?.items,
}
);
this.notify({
message: msg(str`Copied Workflow to new template.`),
variant: "success",
icon: "check2-circle",
});
}
private async deactivate(): Promise {
if (!this.workflow) return;
try {
await this.apiFetch(
`/orgs/${this.orgId}/crawlconfigs/${this.workflow.id}`,
this.authState!,
{
method: "DELETE",
}
);
this.workflow = {
...this.workflow,
inactive: true,
};
this.notify({
message: msg(html`Deactivated ${this.renderName()} .`),
variant: "success",
icon: "check2-circle",
});
} catch {
this.notify({
message: msg("Sorry, couldn't deactivate Workflow at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
}
private async delete(): Promise {
if (!this.workflow) return;
const isDeactivating = this.workflow.crawlCount > 0;
try {
await this.apiFetch(
`/orgs/${this.orgId}/crawlconfigs/${this.workflow.id}`,
this.authState!,
{
method: "DELETE",
}
);
this.navTo(`${this.orgBasePath}/workflows/crawls`);
this.notify({
message: isDeactivating
? msg(html`Deactivated ${this.renderName()} .`)
: msg(html`Deleted ${this.renderName()} .`),
variant: "success",
icon: "check2-circle",
});
} catch {
this.notify({
message: isDeactivating
? msg("Sorry, couldn't deactivate Workflow at this time.")
: msg("Sorry, couldn't delete Workflow at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
}
private async cancel() {
if (!this.lastCrawlId) return;
this.isCancelingOrStoppingCrawl = true;
try {
const data = await this.apiFetch<{ success: boolean }>(
`/orgs/${this.orgId}/crawls/${this.lastCrawlId}/cancel`,
this.authState!,
{
method: "POST",
}
);
if (data.success === true) {
this.fetchWorkflow();
} else {
throw data;
}
} catch {
this.notify({
message: msg("Something went wrong, couldn't cancel crawl."),
variant: "danger",
icon: "exclamation-octagon",
});
}
this.isCancelingOrStoppingCrawl = false;
}
private async stop() {
if (!this.lastCrawlId) return;
this.isCancelingOrStoppingCrawl = true;
try {
const data = await this.apiFetch<{ success: boolean }>(
`/orgs/${this.orgId}/crawls/${this.lastCrawlId}/stop`,
this.authState!,
{
method: "POST",
}
);
if (data.success === true) {
this.fetchWorkflow();
} else {
throw data;
}
} catch {
this.notify({
message: msg("Something went wrong, couldn't stop crawl."),
variant: "danger",
icon: "exclamation-octagon",
});
}
this.isCancelingOrStoppingCrawl = false;
}
private async runNow(): Promise {
try {
const data = await this.apiFetch<{ started: string | null }>(
`/orgs/${this.orgId}/crawlconfigs/${this.workflow!.id}/run`,
this.authState!,
{
method: "POST",
}
);
this.lastCrawlId = data.started;
// remove 'Z' from timestamp to match API response
this.lastCrawlStartTime = new Date().toISOString().slice(0, -1);
this.logs = undefined;
this.fetchWorkflow();
this.goToTab("watch");
this.notify({
message: msg("Starting crawl."),
variant: "success",
icon: "check2-circle",
});
} catch (e: any) {
let message = msg("Sorry, couldn't run crawl at this time.");
if (e.isApiError && e.statusCode === 403) {
if (e.details === "storage_quota_reached") {
message = msg("Your org does not have enough storage to run crawls.");
} else if (e.details === "exec_minutes_quota_reached") {
message = msg(
"Your org has used all of its execution minutes for this month."
);
} else {
message = msg("You do not have permission to run crawls.");
}
}
this.notify({
message: message,
variant: "danger",
icon: "exclamation-octagon",
});
}
}
private confirmDeleteCrawl = (crawl: Crawl) => {
this.crawlToDelete = crawl;
this.openDialogName = "delete";
};
private async deleteCrawl(crawl: Crawl) {
try {
const _data = await this.apiFetch(
`/orgs/${crawl.oid}/crawls/delete`,
this.authState!,
{
method: "POST",
body: JSON.stringify({
crawl_ids: [crawl.id],
}),
}
);
this.crawlToDelete = null;
this.crawls = {
...this.crawls!,
items: this.crawls!.items.filter((c) => c.id !== crawl.id),
};
this.notify({
message: msg(`Successfully deleted crawl`),
variant: "success",
icon: "check2-circle",
});
this.fetchCrawls();
} catch (e: any) {
if (this.crawlToDelete) {
this.confirmDeleteCrawl(this.crawlToDelete);
}
let message = msg(
str`Sorry, couldn't delete archived item at this time.`
);
if (e.isApiError) {
if (e.details == "not_allowed") {
message = msg(
str`Only org owners can delete other users' archived items.`
);
} else if (e.message) {
message = e.message;
}
}
this.notify({
message: message,
variant: "danger",
icon: "exclamation-octagon",
});
}
}
private async fetchCrawlLogs(
params: Partial = {}
): Promise {
try {
this.logs = await this.getCrawlErrors(params);
} catch (e: any) {
if (e.isApiError && e.statusCode === 503) {
// do nothing, keep logs if previously loaded
} else {
this.notify({
message: msg(
"Sorry, couldn't retrieve crawl error logs at this time."
),
variant: "danger",
icon: "exclamation-octagon",
});
}
}
}
private async getCrawlErrors(params: Partial) {
const page = params.page || this.logs?.page || 1;
const pageSize = params.pageSize || this.logs?.pageSize || LOGS_PAGE_SIZE;
const data = await this.apiFetch>(
`/orgs/${this.orgId}/crawls/${
this.workflow!.lastCrawlId
}/errors?page=${page}&pageSize=${pageSize}`,
this.authState!
);
return data;
}
}