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:
parent
27ee16d308
commit
2c89edcc36
@ -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>
|
||||
|
@ -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.`),
|
||||
}),
|
||||
},
|
||||
|
@ -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")}
|
||||
|
@ -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")}
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>`;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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(
|
||||
{
|
||||
|
@ -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>
|
||||
|
@ -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(
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user