feat: Disable archiving for read-only orgs (#1965)

Resolves https://github.com/webrecorder/browsertrix/issues/1915

### Changes

- Disables buttons to create resources, duplicate resources, run crawls,
and configure browser profiles.
- Updates copy from "read-only" -> "disable archiving"
This commit is contained in:
sua yoo 2024-07-25 15:42:04 -04:00 committed by GitHub
parent 27ee16d308
commit 2c89edcc36
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 162 additions and 108 deletions

View File

@ -156,17 +156,17 @@ export class OrgsList extends TailwindElement {
<btrix-dialog
class="[--width:36rem]"
id="orgReadOnlyDialog"
.label=${msg(str`Make Read-Only?`)}
.label=${msg(str`Disable Archiving?`)}
@sl-after-hide=${() => (this.currOrg = null)}
>
${when(this.currOrg, (org) => {
return html`
<p class="mb-3">
${msg(
html`Are you sure you want to make
<strong class="font-semibold">${org.name}</strong>
read-only? Members will no longer be able to crawl, upload
files, create browser profiles, or create collections.`,
html`Are you sure you want to disable archiving in
<strong class="font-semibold">${org.name}</strong> org?
Members will no longer be able to crawl, upload files, create
browser profiles, or create collections.`,
)}
</p>
<ul class="mb-3 text-neutral-600">
@ -195,7 +195,7 @@ export class OrgsList extends TailwindElement {
<sl-input
name="readOnlyReason"
label=${msg("Reason")}
placeholder=${msg("Enter reason for making org read-only")}
placeholder=${msg("Enter reason for disabling archiving")}
required
></sl-input>
</form>
@ -206,7 +206,7 @@ export class OrgsList extends TailwindElement {
@click=${this.orgReadOnlyDialog?.submit}
variant="primary"
>
${msg("Make Read-Only")}
${msg("Disable Archiving")}
</sl-button>
</div>
`;
@ -391,8 +391,8 @@ export class OrgsList extends TailwindElement {
this.notify.toast({
message: params.readOnly
? msg(str`Org "${org.name}" is read-only.`)
: msg(str`Org "${org.name}" is no longer read-only.`),
? msg(str`Archiving in "${org.name}" is disabled.`)
: msg(str`Archiving in "${org.name}" is re-enabled.`),
variant: "success",
icon: "check2-circle",
});
@ -401,7 +401,7 @@ export class OrgsList extends TailwindElement {
this.notify.toast({
message: msg(
"Sorry, couldn't update org read-only state at this time.",
"Sorry, couldn't update org archiving ability at this time.",
),
variant: "danger",
icon: "exclamation-octagon",
@ -477,13 +477,13 @@ export class OrgsList extends TailwindElement {
status = {
icon: html`<sl-icon
class="text-base text-neutral-400"
name="slash-circle"
label=${msg("Read-only")}
name="ban"
label=${msg("disabled")}
>
</sl-icon>`,
description: org.readOnlyReason
? `${msg("Read-only:")} ${org.readOnlyReason}`
: msg("Read-only (no reason specified)"),
? `${msg("Archiving Disabled:")} ${org.readOnlyReason}`
: msg("Archiving Disabled (no reason specified)"),
};
}
@ -550,7 +550,7 @@ export class OrgsList extends TailwindElement {
slot="prefix"
name="arrow-counterclockwise"
></sl-icon>
${msg("Undo Read-Only")}
${msg("Re-enable Archiving")}
</sl-menu-item>
`
: html`
@ -560,8 +560,8 @@ export class OrgsList extends TailwindElement {
void this.orgReadOnlyDialog?.show();
}}
>
<sl-icon slot="prefix" name="eye"></sl-icon>
${msg("Make Read-Only")}
<sl-icon slot="prefix" name="ban"></sl-icon>
${msg("Disable Archiving")}
</sl-menu-item>
`}
<sl-divider></sl-divider>

View File

@ -128,10 +128,8 @@ export class OrgStatusBanner extends TailwindElement {
return {
title:
daysDiff > 1
? msg(
str`Your org will be set to read-only mode in ${daysDiff} days`,
)
: msg("Your org will be set to read-only mode within one day"),
? msg(str`Archiving will be disabled in ${daysDiff} days`)
: msg("Archiving will be disabled within one day"),
detail: html`
<p>
${msg(
@ -161,7 +159,7 @@ export class OrgStatusBanner extends TailwindElement {
!!readOnly && readOnlyReason === OrgReadOnlyReason.SubscriptionPaused,
content: () => ({
title: msg(str`Your org has been set to read-only mode`),
title: msg(str`Archiving is disabled for this org`),
detail: msg(
html`Your subscription has been paused due to payment failure.
Please go to ${billingTabLink} to update your payment method.`,
@ -174,7 +172,7 @@ export class OrgStatusBanner extends TailwindElement {
readOnlyReason === OrgReadOnlyReason.SubscriptionCancelled,
content: () => ({
title: msg(str`This org has been set to read-only mode`),
title: msg(str`Archiving is disabled for this org`),
detail: msg(
`Your subscription has been canceled. Please contact Browsertrix support to renew your plan.`,
),
@ -184,7 +182,7 @@ export class OrgStatusBanner extends TailwindElement {
test: () => !!readOnly,
content: () => ({
title: msg(str`This org has been set to read-only mode`),
title: msg(str`Archiving is disabled for this org`),
detail: msg(`Please contact Browsertrix support to renew your plan.`),
}),
},

View File

@ -34,6 +34,8 @@ import {
} from "@/utils/crawler";
import { humanizeExecutionSeconds } from "@/utils/executionTimeFormatter";
import { getLocale } from "@/utils/localization";
import { isArchivingDisabled } from "@/utils/orgs";
import appState, { use } from "@/utils/state";
import { tw } from "@/utils/tailwind";
import "./ui/qa";
@ -89,6 +91,9 @@ export class ArchivedItemDetail extends TailwindElement {
@property({ type: Boolean })
isCrawler = false;
@use()
appState = appState;
@state()
private qaRunId?: string;
@ -125,6 +130,10 @@ export class ArchivedItemDetail extends TailwindElement {
@query("#cancelQARunDialog")
private readonly cancelQARunDialog?: Dialog | null;
private get org() {
return this.appState.org;
}
private get listUrl(): string {
let path = "items";
if (this.workflowId) {
@ -1077,7 +1086,7 @@ ${this.crawl?.description}
qaRuns.length === 0 ? "primary" : "default"
}"
@click=${() => void this.startQARun()}
?disabled=${qaIsRunning}
?disabled=${isArchivingDisabled(this.org, true) || qaIsRunning}
>
<sl-icon slot="prefix" name="microscope" library="app"></sl-icon>
${qaRuns.length ? msg("Rerun Analysis") : msg("Run Analysis")}

View File

@ -21,6 +21,8 @@ import type { APIPaginatedList, APIPaginationQuery } from "@/types/api";
import { isApiError } from "@/utils/api";
import type { Auth, AuthState } from "@/utils/AuthService";
import { finishedCrawlStates, isActive } from "@/utils/crawler";
import { isArchivingDisabled } from "@/utils/orgs";
import appState, { use } from "@/utils/state";
type ArchivedItems = APIPaginatedList<ArchivedItem>;
type SearchFields = "name" | "firstSeed";
@ -89,15 +91,15 @@ export class CrawlsList extends TailwindElement {
@property({ type: String })
orgId?: string;
@property({ type: Boolean })
orgStorageQuotaReached = false;
@property({ type: Boolean })
isCrawler!: boolean;
@property({ type: String })
itemType: ArchivedItem["type"] | null = null;
@use()
appState = appState;
@state()
private pagination: Required<APIPaginationQuery> = {
page: 1,
@ -141,6 +143,10 @@ export class CrawlsList extends TailwindElement {
@query("#stateSelect")
stateSelect?: SlSelect;
private get org() {
return this.appState.org;
}
private readonly archivedItemsTask = new Task(this, {
task: async (
[
@ -302,13 +308,13 @@ export class CrawlsList extends TailwindElement {
() => html`
<sl-tooltip
content=${msg("Org Storage Full")}
?disabled=${!this.orgStorageQuotaReached}
?disabled=${!this.org?.storageQuotaReached}
>
<sl-button
size="small"
variant="primary"
@click=${() => (this.isUploadingArchive = true)}
?disabled=${this.orgStorageQuotaReached}
?disabled=${isArchivingDisabled(this.org)}
>
<sl-icon slot="prefix" name="upload"></sl-icon>
${msg("Upload WACZ")}

View File

@ -18,6 +18,8 @@ import { isApiError } from "@/utils/api";
import type { AuthState } from "@/utils/AuthService";
import { maxLengthValidator } from "@/utils/form";
import { formatNumber, getLocale } from "@/utils/localization";
import { isArchivingDisabled } from "@/utils/orgs";
import appState, { use } from "@/utils/state";
const DESCRIPTION_MAXLENGTH = 500;
@ -46,6 +48,9 @@ export class BrowserProfilesDetail extends TailwindElement {
@property({ type: Boolean })
isCrawler = false;
@use()
appState = appState;
@state()
private profile?: Profile;
@ -83,6 +88,10 @@ export class BrowserProfilesDetail extends TailwindElement {
private readonly validateNameMax = maxLengthValidator(50);
private readonly validateDescriptionMax = maxLengthValidator(500);
get org() {
return this.appState.org;
}
disconnectedCallback() {
if (this.browserId) {
void this.deleteBrowser(this.browserId);
@ -247,7 +256,10 @@ export class BrowserProfilesDetail extends TailwindElement {
"View or edit the current browser profile configuration.",
)}
</p>
<sl-button @click=${this.startBrowserPreview}>
<sl-button
?disabled=${isArchivingDisabled(this.org)}
@click=${this.startBrowserPreview}
>
<sl-icon slot="prefix" name="gear"></sl-icon>
${msg("Configure Browser Profile")}
</sl-button>
@ -425,6 +437,8 @@ export class BrowserProfilesDetail extends TailwindElement {
}
private renderMenu() {
const archivingDisabled = isArchivingDisabled(this.org);
return html`
<sl-dropdown distance="4" placement="bottom-end">
<sl-button size="small" slot="trigger" caret>
@ -435,11 +449,17 @@ export class BrowserProfilesDetail extends TailwindElement {
<sl-icon slot="prefix" name="pencil"></sl-icon>
${msg("Edit Metadata")}
</sl-menu-item>
<sl-menu-item @click=${this.startBrowserPreview}>
<sl-menu-item
?disabled=${archivingDisabled}
@click=${this.startBrowserPreview}
>
<sl-icon slot="prefix" name="gear"></sl-icon>
${msg("Configure Browser Profile")}
</sl-menu-item>
<sl-menu-item @click=${() => void this.duplicateProfile()}>
<sl-menu-item
?disabled=${archivingDisabled}
@click=${() => void this.duplicateProfile()}
>
<sl-icon slot="prefix" name="files"></sl-icon>
${msg("Duplicate Profile")}
</sl-menu-item>

View File

@ -27,6 +27,8 @@ import type { Browser } from "@/types/browser";
import type { AuthState } from "@/utils/AuthService";
import { html } from "@/utils/LiteElement";
import { getLocale } from "@/utils/localization";
import { isArchivingDisabled } from "@/utils/orgs";
import appState, { use } from "@/utils/state";
import { tw } from "@/utils/tailwind";
const INITIAL_PAGE_SIZE = 20;
@ -52,6 +54,9 @@ export class BrowserProfilesList extends TailwindElement {
@property({ type: Boolean })
isCrawler = false;
@use()
appState = appState;
@state()
browserProfiles?: APIPaginatedList<Profile>;
@ -64,6 +69,10 @@ export class BrowserProfilesList extends TailwindElement {
@state()
private isLoading = true;
private get org() {
return this.appState.org;
}
static styles = css`
btrix-table {
grid-template-columns:
@ -119,6 +128,7 @@ export class BrowserProfilesList extends TailwindElement {
<sl-button
variant="primary"
size="small"
?disabled=${isArchivingDisabled(this.org)}
@click=${() => {
this.dispatchEvent(
new CustomEvent("select-new-dialog", {
@ -339,6 +349,7 @@ export class BrowserProfilesList extends TailwindElement {
<btrix-overflow-dropdown @click=${(e: Event) => e.preventDefault()}>
<sl-menu>
<sl-menu-item
?disabled=${isArchivingDisabled(this.org)}
@click=${() => {
void this.duplicateProfile(data);
}}

View File

@ -138,6 +138,7 @@ export class CollectionsList extends LiteElement {
<sl-button
variant="primary"
size="small"
?disabled=${!this.appState.org || this.appState.org.readOnly}
@click=${() => (this.openDialogName = "create")}
>
<sl-icon slot="prefix" name="plus-lg"></sl-icon>

View File

@ -104,7 +104,13 @@ export class Dashboard extends LiteElement {
);
}}
>
<sl-button slot="trigger" size="small" variant="primary" caret>
<sl-button
slot="trigger"
size="small"
variant="primary"
caret
?disabled=${this.org?.readOnly}
>
<sl-icon slot="prefix" name="plus-lg"></sl-icon>
${msg("Create New...")}
</sl-button>

View File

@ -508,7 +508,6 @@ export class Org extends LiteElement {
.authState=${this.authState!}
userId=${this.userInfo!.id}
orgId=${this.orgId}
?orgStorageQuotaReached=${this.org?.storageQuotaReached}
?isCrawler=${this.isCrawler}
itemType=${ifDefined(params.itemType || undefined)}
@select-new-dialog=${this.onSelectNewDialog}
@ -528,8 +527,6 @@ export class Org extends LiteElement {
class="col-span-5 mt-6"
.authState=${this.authState!}
orgId=${this.orgId}
?orgStorageQuotaReached=${this.org?.storageQuotaReached}
?orgExecutionMinutesQuotaReached=${this.org?.execMinutesQuotaReached}
workflowId=${workflowId}
openDialogName=${this.viewStateData?.dialog}
?isEditing=${isEditing}
@ -550,8 +547,6 @@ export class Org extends LiteElement {
.initialWorkflow=${workflow}
.initialSeeds=${seeds}
jobType=${ifDefined(params.jobType)}
?orgStorageQuotaReached=${this.org?.storageQuotaReached}
?orgExecutionMinutesQuotaReached=${this.org?.execMinutesQuotaReached}
@select-new-dialog=${this.onSelectNewDialog}
></btrix-workflows-new>`;
}
@ -559,8 +554,6 @@ export class Org extends LiteElement {
return html`<btrix-workflows-list
.authState=${this.authState!}
orgId=${this.orgId}
?orgStorageQuotaReached=${this.org?.storageQuotaReached}
?orgExecutionMinutesQuotaReached=${this.org?.execMinutesQuotaReached}
userId=${this.userInfo!.id}
?isCrawler=${this.isCrawler}
@select-new-dialog=${this.onSelectNewDialog}
@ -638,13 +631,11 @@ export class Org extends LiteElement {
return html`<btrix-org-settings
.authState=${this.authState}
.userInfo=${this.userInfo}
.org=${this.org}
.orgId=${this.orgId}
activePanel=${activePanel}
?isAddingMember=${isAddingMember}
@org-user-role-change=${this.onUserRoleChange}
@org-remove-member=${this.onOrgRemoveMember}
@btrix-update-org=${this.updateOrg}
></btrix-org-settings>`;
}

View File

@ -16,6 +16,7 @@ import type { Auth, AuthState } from "@/utils/AuthService";
import { humanizeSeconds } from "@/utils/executionTimeFormatter";
import { formatNumber, getLocale } from "@/utils/localization";
import { pluralOf } from "@/utils/pluralize";
import appState, { use } from "@/utils/state";
import { tw } from "@/utils/tailwind";
const linkClassList = tw`transition-color text-primary hover:text-primary-500`;
@ -24,9 +25,6 @@ const manageLinkClasslist = clsx(
tw`flex items-center gap-2 p-2 text-sm font-semibold leading-none`,
);
/**
* @fires btrix-update-org
*/
@localized()
@customElement("btrix-org-settings-billing")
export class OrgSettingsBilling extends TailwindElement {
@ -39,14 +37,18 @@ export class OrgSettingsBilling extends TailwindElement {
@property({ type: Object })
authState?: AuthState;
@property({ type: Object, noAccessor: true })
org?: OrgData;
@property({ type: String, noAccessor: true })
salesEmail?: string;
@use()
appState = appState;
private readonly api = new APIController(this);
private get org() {
return this.appState.org;
}
get portalUrlLabel() {
const subscription = this.org?.subscription;

View File

@ -18,7 +18,7 @@ import type { CurrentUser } from "@/types/user";
import { isApiError } from "@/utils/api";
import type { AuthState } from "@/utils/AuthService";
import { maxLengthValidator } from "@/utils/form";
import { AccessCode, isAdmin, isCrawler, type OrgData } from "@/utils/orgs";
import { AccessCode, isAdmin, isCrawler } from "@/utils/orgs";
import slugifyStrict from "@/utils/slugify";
import appState, { AppStateService, use } from "@/utils/state";
import { formatAPIUser } from "@/utils/user";
@ -72,9 +72,6 @@ export class OrgSettings extends TailwindElement {
@property({ type: String })
orgId!: string;
@property({ type: Object })
org!: OrgData;
@property({ type: String })
activePanel: Tab = "information";
@ -103,6 +100,10 @@ export class OrgSettings extends TailwindElement {
private readonly navigate = new NavigateController(this);
private readonly notify = new NotifyController(this);
private get org() {
return this.appState.org;
}
private get tabLabels(): Record<Tab, string> {
return {
information: msg("General"),
@ -148,8 +149,8 @@ export class OrgSettings extends TailwindElement {
aria-hidden="true"
library="default"
></sl-icon>
${msg("Invite New Member")}</sl-button
>
${msg("Invite New Member")}
</sl-button>
`,
() => html` <h3>${this.tabLabels[this.activePanel]}</h3> `,
)}
@ -168,7 +169,6 @@ export class OrgSettings extends TailwindElement {
</btrix-tab-panel>
<btrix-tab-panel name="billing">
<btrix-org-settings-billing
.org=${this.org}
.authState=${this.authState}
.salesEmail=${this.appState.settings?.salesEmail}
></btrix-org-settings-billing>
@ -192,6 +192,8 @@ export class OrgSettings extends TailwindElement {
}
private renderInformation() {
if (!this.org) return;
return html`<div class="rounded-lg border">
<form @submit=${this.onOrgInfoSubmit}>
${columns([
@ -274,8 +276,10 @@ export class OrgSettings extends TailwindElement {
}
private renderMembers() {
if (!this.org?.users) return;
const columnWidths = ["1fr", "2fr", "auto", "min-content"];
const rows = Object.entries(this.org.users!).map(([_id, user]) => [
const rows = Object.entries(this.org.users).map(([_id, user]) => [
user.name,
user.email,
this.renderUserRoleSelect(user),
@ -357,10 +361,12 @@ export class OrgSettings extends TailwindElement {
}
private renderRemoveMemberButton(member: Member) {
if (!this.org?.users) return;
let disableButton = false;
if (member.email === this.userInfo.email) {
const { [this.userInfo.id]: _currentUser, ...otherUsers } =
this.org.users!;
this.org.users;
const hasOtherAdmin = Object.values(otherUsers).some(({ role }) =>
isAdmin(role),
);
@ -461,7 +467,7 @@ export class OrgSettings extends TailwindElement {
private async getPendingInvites() {
const data = await this.api.fetch<APIPaginatedList<Invite>>(
`/orgs/${this.org.id}/invites`,
`/orgs/${this.org!.id}/invites`,
this.authState!,
);
@ -492,7 +498,7 @@ export class OrgSettings extends TailwindElement {
const params = {
name: orgName,
slug: this.org.slug,
slug: this.org!.slug,
};
if (this.slugValue) {
@ -576,7 +582,7 @@ export class OrgSettings extends TailwindElement {
this.notify.toast({
message: msg(
str`Successfully removed ${invite.email} from ${this.org.name}.`,
str`Successfully removed ${invite.email} from ${this.org!.name}.`,
),
variant: "success",
icon: "check2-circle",

View File

@ -33,6 +33,7 @@ import {
import { humanizeSchedule } from "@/utils/cron";
import LiteElement, { html } from "@/utils/LiteElement";
import { getLocale } from "@/utils/localization";
import { isArchivingDisabled } from "@/utils/orgs";
const SECTIONS = ["crawls", "watch", "settings", "logs"] as const;
type Tab = (typeof SECTIONS)[number];
@ -55,12 +56,6 @@ export class WorkflowDetail extends LiteElement {
@property({ type: String })
orgId!: string;
@property({ type: Boolean })
orgStorageQuotaReached = false;
@property({ type: Boolean })
orgExecutionMinutesQuotaReached = false;
@property({ type: String })
workflowId!: string;
@ -121,6 +116,10 @@ export class WorkflowDetail extends LiteElement {
@state()
private filterBy: Partial<Record<keyof Crawl, string | CrawlState[]>> = {};
private get org() {
return this.appState.org;
}
private readonly numberFormatter = new Intl.NumberFormat(getLocale(), {
// notation: "compact",
});
@ -572,9 +571,6 @@ export class WorkflowDetail extends LiteElement {
configId=${this.workflow!.id}
orgId=${this.orgId}
.authState=${this.authState}
?orgStorageQuotaReached=${this.orgStorageQuotaReached}
?orgExecutionMinutesQuotaReached=${this
.orgExecutionMinutesQuotaReached}
@reset=${() =>
this.navTo(
`${this.orgBasePath}/workflows/crawl/${this.workflow!.id}`,
@ -589,6 +585,8 @@ export class WorkflowDetail extends LiteElement {
if (!this.workflow) return;
const workflow = this.workflow;
const archivingDisabled = isArchivingDisabled(this.org, true);
return html`
${when(
this.workflow.isCrawlRunning,
@ -623,14 +621,13 @@ export class WorkflowDetail extends LiteElement {
content=${msg(
"Org Storage Full or Monthly Execution Minutes Reached",
)}
?disabled=${!this.orgStorageQuotaReached &&
!this.orgExecutionMinutesQuotaReached}
?disabled=${!this.org?.storageQuotaReached &&
!this.org?.execMinutesQuotaReached}
>
<sl-button
size="small"
variant="primary"
?disabled=${this.orgStorageQuotaReached ||
this.orgExecutionMinutesQuotaReached}
?disabled=${archivingDisabled}
@click=${() => void this.runNow()}
>
<sl-icon name="play" slot="prefix"></sl-icon>
@ -670,8 +667,7 @@ export class WorkflowDetail extends LiteElement {
() => html`
<sl-menu-item
style="--sl-color-neutral-700: var(--success)"
?disabled=${this.orgStorageQuotaReached ||
this.orgExecutionMinutesQuotaReached}
?disabled=${archivingDisabled}
@click=${() => void this.runNow()}
>
<sl-icon name="play" slot="prefix"></sl-icon>
@ -712,7 +708,10 @@ export class WorkflowDetail extends LiteElement {
<sl-icon name="tags" slot="prefix"></sl-icon>
${msg("Copy Tags")}
</sl-menu-item>
<sl-menu-item @click=${() => void this.duplicateConfig()}>
<sl-menu-item
?disabled=${archivingDisabled}
@click=${() => void this.duplicateConfig()}
>
<sl-icon name="files" slot="prefix"></sl-icon>
${msg("Duplicate Workflow")}
</sl-menu-item>
@ -1165,14 +1164,14 @@ export class WorkflowDetail extends LiteElement {
content=${msg(
"Org Storage Full or Monthly Execution Minutes Reached",
)}
?disabled=${!this.orgStorageQuotaReached &&
!this.orgExecutionMinutesQuotaReached}
?disabled=${!this.org?.storageQuotaReached &&
!this.org?.execMinutesQuotaReached}
>
<sl-button
size="small"
variant="primary"
?disabled=${this.orgStorageQuotaReached ||
this.orgExecutionMinutesQuotaReached}
?disabled=${this.org?.storageQuotaReached ||
this.org?.execMinutesQuotaReached}
@click=${() => void this.runNow()}
>
<sl-icon name="play" slot="prefix"></sl-icon>

View File

@ -68,6 +68,7 @@ import {
import { maxLengthValidator } from "@/utils/form";
import LiteElement, { html } from "@/utils/LiteElement";
import { getLocale } from "@/utils/localization";
import { isArchivingDisabled } from "@/utils/orgs";
import { regexEscape, regexUnescape } from "@/utils/string";
type NewCrawlConfigParams = WorkflowParams & {
@ -272,12 +273,6 @@ export class CrawlConfigEditor extends LiteElement {
@property({ type: Array })
initialSeeds?: Seed[];
@property({ type: Boolean })
orgStorageQuotaReached = false;
@property({ type: Boolean })
orgExecutionMinutesQuotaReached = false;
@state()
private showCrawlerChannels = false;
@ -303,6 +298,10 @@ export class CrawlConfigEditor extends LiteElement {
@state()
private serverError?: TemplateResult | string;
private get org() {
return this.appState.org;
}
private maxScale = DEFAULT_MAX_SCALE;
// For fuzzy search:
@ -590,7 +589,7 @@ export class CrawlConfigEditor extends LiteElement {
scheduleType: defaultFormState.scheduleType,
scheduleFrequency: defaultFormState.scheduleFrequency,
runNow:
this.orgStorageQuotaReached || this.orgExecutionMinutesQuotaReached
this.org?.storageQuotaReached || this.org?.execMinutesQuotaReached
? false
: defaultFormState.runNow,
tags: this.initialWorkflow.tags,
@ -921,8 +920,7 @@ export class CrawlConfigEditor extends LiteElement {
<sl-switch
class="mr-1"
?checked=${this.formState.runNow}
?disabled=${this.orgStorageQuotaReached ||
this.orgExecutionMinutesQuotaReached}
?disabled=${isArchivingDisabled(this.org, true)}
@sl-change=${(e: SlChangeEvent) => {
this.updateFormState(
{

View File

@ -17,6 +17,7 @@ import type { APIPaginatedList, APIPaginationQuery } from "@/types/api";
import { isApiError } from "@/utils/api";
import type { AuthState } from "@/utils/AuthService";
import LiteElement, { html } from "@/utils/LiteElement";
import { isArchivingDisabled } from "@/utils/orgs";
type SearchFields = "name" | "firstSeed";
type SortField = "lastRun" | "name" | "firstSeed" | "created" | "modified";
@ -76,12 +77,6 @@ export class WorkflowsList extends LiteElement {
@property({ type: String })
orgId!: string;
@property({ type: Boolean })
orgStorageQuotaReached = false;
@property({ type: Boolean })
orgExecutionMinutesQuotaReached = false;
@property({ type: String })
userId!: string;
@ -122,6 +117,10 @@ export class WorkflowsList extends LiteElement {
private getWorkflowsController: AbortController | null = null;
private timerId?: number;
private get org() {
return this.appState.org;
}
private get selectedSearchFilterKey() {
return Object.keys(WorkflowsList.FieldLabels).find((key) =>
Boolean((this.filterBy as Record<string, unknown>)[key]),
@ -215,6 +214,7 @@ export class WorkflowsList extends LiteElement {
<sl-button
variant="primary"
size="small"
?disabled=${this.org?.readOnly}
@click=${() => {
this.dispatchEvent(
new CustomEvent("select-new-dialog", {
@ -452,8 +452,7 @@ export class WorkflowsList extends LiteElement {
() => html`
<sl-menu-item
style="--sl-color-neutral-700: var(--success)"
?disabled=${this.orgStorageQuotaReached ||
this.orgExecutionMinutesQuotaReached}
?disabled=${isArchivingDisabled(this.org, true)}
@click=${() => void this.runNow(workflow)}
>
<sl-icon name="play" slot="prefix"></sl-icon>
@ -519,6 +518,7 @@ export class WorkflowsList extends LiteElement {
this.isCrawler,
() =>
html` <sl-menu-item
?disabled=${isArchivingDisabled(this.org, true)}
@click=${() => void this.duplicateConfig(workflow)}
>
<sl-icon name="files" slot="prefix"></sl-icon>

View File

@ -62,12 +62,6 @@ export class WorkflowsNew extends LiteElement {
@property({ type: String })
jobType?: JobType;
@property({ type: Boolean })
orgStorageQuotaReached = false;
@property({ type: Boolean })
orgExecutionMinutesQuotaReached = false;
// Use custom property accessor to prevent
// overriding default Workflow values
@property({ type: Object })
@ -80,6 +74,10 @@ export class WorkflowsNew extends LiteElement {
private _initialWorkflow: WorkflowParams = defaultValue;
private get org() {
return this.appState.org;
}
private renderHeader() {
const href = `${this.orgBasePath}/workflows/crawls`;
const label = msg("Back to Crawl Workflows");
@ -129,9 +127,6 @@ export class WorkflowsNew extends LiteElement {
jobType=${jobType}
orgId=${this.orgId}
.authState=${this.authState}
?orgStorageQuotaReached=${this.orgStorageQuotaReached}
?orgExecutionMinutesQuotaReached=${this
.orgExecutionMinutesQuotaReached}
@reset=${async (e: Event) => {
await (e.target as LitElement).updateComplete;
this.dispatchEvent(

View File

@ -1,4 +1,4 @@
import { AccessCode, type UserRole } from "@/types/org";
import { AccessCode, type OrgData, type UserRole } from "@/types/org";
export * from "@/types/org";
@ -19,3 +19,15 @@ export function isCrawler(accessCode?: (typeof AccessCode)[UserRole]): boolean {
return accessCode >= AccessCode.crawler;
}
export function isArchivingDisabled(
org?: OrgData | null,
checkExecMinutesQuota = false,
): boolean {
return Boolean(
!org ||
org.readOnly ||
org.storageQuotaReached ||
(checkExecMinutesQuota ? org.execMinutesQuotaReached : false),
);
}