Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions backend/btrixcloud/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1850,6 +1850,23 @@ class OrgQuotasIn(BaseModel):
giftedExecMinutes: Optional[int] = None


# ============================================================================
class Plan(BaseModel):
"""Available Browsertrix plan, from env"""

id: str
name: str
org_quotas: OrgQuotas
testmode: bool = False


# ============================================================================
class PlansResponse(BaseModel):
"""Response for plans api endpoint"""

plans: list[Plan]


# ============================================================================
class SubscriptionEventOut(BaseModel):
"""Fields to add to output models for subscription events"""
Expand Down
15 changes: 15 additions & 0 deletions backend/btrixcloud/orgs.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from typing import Optional, TYPE_CHECKING, Dict, Callable, List, AsyncGenerator, Any

from pydantic import ValidationError
from pymongo import ReturnDocument
from pymongo.collation import Collation
from pymongo.errors import AutoReconnect, DuplicateKeyError
Expand All @@ -29,6 +30,7 @@
WAITING_STATES,
BaseCrawl,
Organization,
PlansResponse,
StorageRef,
OrgQuotas,
OrgQuotasIn,
Expand Down Expand Up @@ -1615,6 +1617,19 @@ async def rename_org(

return {"updated": True}

@app.get("/orgs/plans", tags=["organizations"], response_model=PlansResponse)
async def get_plans(user: User = Depends(user_dep)):
if not user.is_superuser:
raise HTTPException(status_code=403, detail="Not Allowed")
plans_json = os.environ.get("AVAILABLE_PLANS")
if not plans_json:
return PlansResponse(plans=[])
try:
plans = PlansResponse.model_validate_json(plans_json)
return plans
except ValidationError as err:
raise HTTPException(status_code=400, detail="invalid_plans") from err

@router.post("/quotas", tags=["organizations"], response_model=UpdatedResponse)
async def update_quotas(
quotas: OrgQuotasIn,
Expand Down
1 change: 1 addition & 0 deletions chart/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ data:

CLEANUP_FILES_AFTER_MINUTES: "{{ .Values.cleanup_files_after_minutes | default 1440 }}"

AVAILABLE_PLANS: {{ .Values.available_plans | toJson }}

---
apiVersion: v1
Expand Down
1 change: 0 additions & 1 deletion chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,6 @@ ingress:
# alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS": 443}]'
# alb.ingress.kubernetes.io/certificate-arn: "certificate-arn"


ingress_class: nginx

# Optional: Front-end injected script
Expand Down
66 changes: 4 additions & 62 deletions frontend/src/components/orgs-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,58 +389,10 @@ export class OrgsList extends BtrixElement {
}

private renderOrgQuotas() {
return html`
<btrix-dialog
id="orgQuotaDialog"
.label=${msg(str`Quotas for: ${this.currOrg?.name || ""}`)}
@sl-after-hide=${() => (this.currOrg = null)}
>
${when(this.currOrg?.quotas, (quotas) =>
Object.entries(quotas).map(([key, value]) => {
let label;
switch (key) {
case "maxConcurrentCrawls":
label = msg("Max Concurrent Crawls");
break;
case "maxPagesPerCrawl":
label = msg("Max Pages Per Crawl");
break;
case "storageQuota":
label = msg("Org Storage Quota (GB)");
value = Math.floor(value / 1e9);
break;
case "maxExecMinutesPerMonth":
label = msg("Max Execution Minutes Per Month");
break;
case "extraExecMinutes":
label = msg("Extra Execution Minutes");
break;
case "giftedExecMinutes":
label = msg("Gifted Execution Minutes");
break;
default:
label = msg("Unlabeled");
}
return html` <sl-input
class="mb-3 last:mb-0"
name=${key}
label=${label}
value=${value}
type="number"
@sl-input="${this.onUpdateQuota}"
></sl-input>`;
}),
)}
<div slot="footer" class="flex justify-end">
<sl-button
size="small"
@click="${this.onSubmitQuotas}"
variant="primary"
>${msg("Update Quotas")}
</sl-button>
</div>
</btrix-dialog>
`;
return html`<btrix-org-quota-editor
id="orgQuotaDialog"
.activeOrg=${this.currOrg}
></btrix-org-quota-editor>`;
}

private renderOrgProxies() {
Expand Down Expand Up @@ -708,16 +660,6 @@ export class OrgsList extends BtrixElement {
}
}

private onSubmitQuotas() {
if (this.currOrg) {
this.dispatchEvent(
new CustomEvent("update-quotas", { detail: this.currOrg }),
);

void this.orgQuotaDialog?.hide();
}
}

private onSubmitProxies() {
if (this.currOrg) {
this.dispatchEvent(
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/components/ui/data-grid/cellDirective.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { Directive, type PartInfo } from "lit/directive.js";

import type { DataGridCell } from "./data-grid-cell";
import type { GridColumn } from "./types";
import type { GridColumn, GridItem } from "./types";

/**
* Directive for replacing `renderCell` and `renderEditCell`
* methods with custom render functions.
*/
export class CellDirective extends Directive {
private readonly element?: DataGridCell;
export class CellDirective<T extends GridItem> extends Directive {
private readonly element?: DataGridCell<T>;

constructor(partInfo: PartInfo & { element?: DataGridCell }) {
constructor(partInfo: PartInfo & { element?: DataGridCell<T> }) {
super(partInfo);
this.element = partInfo.element;
}

render(col: GridColumn) {
render(col: GridColumn<T>) {
if (!this.element) return;

if (col.renderCell) {
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/components/ui/data-grid/controllers/focus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {

import type { DataGridCell } from "../data-grid-cell";
import type { DataGridRow } from "../data-grid-row";
import { type GridItem } from "../types";

type Options = {
/**
Expand All @@ -21,11 +22,13 @@ type Options = {
/**
* Utilities for managing focus in a data grid.
*/
export class DataGridFocusController implements ReactiveController {
readonly #host: DataGridRow | DataGridCell;
export class DataGridFocusController<const T extends GridItem = GridItem>
implements ReactiveController
{
readonly #host: DataGridRow<T> | DataGridCell<T>;

constructor(
host: DataGridRow | DataGridCell,
host: DataGridRow<T> | DataGridCell<T>,
opts: Options = {
setFocusOnTabbable: false,
},
Expand Down
68 changes: 47 additions & 21 deletions frontend/src/components/ui/data-grid/data-grid-cell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,23 @@ import { html, type TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import get from "lodash/fp/get";
import { type Primitive } from "type-fest";

import { TableCell } from "../table/table-cell";

import type {
GridColumn,
GridColumnNumberType,
GridColumnSelectType,
GridItem,
GridItemValue,
} from "./types";
import { GridColumnType } from "./types";

import { DataGridFocusController } from "@/components/ui/data-grid/controllers/focus";
import type { UrlInput } from "@/components/ui/url-input";
import { tw } from "@/utils/tailwind";

const cellInputStyle = [
export const cellInputStyle = [
tw`size-full [--sl-input-background-color-hover:transparent] [--sl-input-background-color:transparent] [--sl-input-border-radius-medium:0] [--sl-input-spacing-medium:var(--sl-spacing-small)] focus:z-10`,
// TODO We need to upgrade to Tailwind v4 for inset rings to actually work
// tw`focus-within:part-[base]:inset-ring-2`,
Expand All @@ -31,8 +32,8 @@ const cellInputStyle = [

export type InputElement = SlInput | SlSelect | UrlInput;

export type CellEditEventDetail = {
field: GridColumn["field"];
export type CellEditEventDetail<T extends GridItem = GridItem> = {
field: keyof T;
value: InputElement["value"];
validity: InputElement["validity"];
validationMessage: InputElement["validationMessage"];
Expand All @@ -43,15 +44,17 @@ export type CellEditEventDetail = {
* @fires btrix-change CustomEvent
*/
@customElement("btrix-data-grid-cell")
export class DataGridCell extends TableCell {
export class DataGridCell<
const T extends GridItem = GridItem,
> extends TableCell {
@property({ type: Object })
column?: GridColumn;
column?: GridColumn<T>;

@property({ type: Object })
item?: GridItem;
item?: T;

@property({ type: String })
value?: GridItemValue;
value?: T[keyof T];

@property({ type: Boolean })
editable = false;
Expand All @@ -68,7 +71,7 @@ export class DataGridCell extends TableCell {
@property({ type: Number, reflect: true })
tabindex = 0;

readonly #focus = new DataGridFocusController(this, {
readonly #focus = new DataGridFocusController<T>(this, {
setFocusOnTabbable: true,
});

Expand All @@ -88,7 +91,7 @@ export class DataGridCell extends TableCell {
if (!this.column) return null;

return this.shadowRoot!.querySelector<InputElement>(
`[name=${this.column.field}]`,
`[name=${String(this.column.field)}]`,
);
}

Expand Down Expand Up @@ -116,20 +119,20 @@ export class DataGridCell extends TableCell {
return this.renderEditCell({ item: this.item, value: this.value });
}

return this.renderCell({ item: this.item });
return html`${this.renderCell({ item: this.item })}`;
}

renderCell = ({ item }: { item: GridItem }) => {
renderCell = ({ item }: { item: T }): string | TemplateResult<1> => {
return html`${(this.column && get(this.column.field, item)) ?? ""}`;
};

renderEditCell = ({
item,
value: cellValue,
}: {
item: GridItem;
value?: GridItemValue;
}) => {
item: T;
value?: T[keyof T];
}): TemplateResult<1> => {
const col = this.column;

if (!col) return html``;
Expand All @@ -141,7 +144,7 @@ export class DataGridCell extends TableCell {
return html`
<div class="box-border w-full p-1">
<sl-select
name=${col.field}
name=${String(col.field)}
value=${value}
placeholder=${ifDefined(col.inputPlaceholder)}
class="w-full min-w-[5em]"
Expand All @@ -162,30 +165,53 @@ export class DataGridCell extends TableCell {
}
case GridColumnType.URL:
return html`<btrix-url-input
name=${col.field}
name=${String(col.field)}
class=${clsx(cellInputStyle)}
value=${value}
placeholder=${ifDefined(col.inputPlaceholder)}
?required=${col.required}
hideHelpText
>
</btrix-url-input>`;
case GridColumnType.Number: {
const { min, max, step } = col as GridColumnNumberType<T>;
return html`<sl-input
name=${String(col.field)}
class=${clsx(cellInputStyle)}
type="number"
value=${value}
placeholder=${ifDefined(col.inputPlaceholder)}
?required=${col.required}
min=${ifDefined(this.evalWithItemIfFn(min))}
max=${ifDefined(this.evalWithItemIfFn(max))}
step=${ifDefined(this.evalWithItemIfFn(step))}
></sl-input>`;
}
default:
break;
}

return html`
<sl-input
name=${col.field}
name=${String(col.field)}
class=${clsx(cellInputStyle)}
type=${col.inputType === GridColumnType.Number ? "number" : "text"}
type="text"
value=${value}
placeholder=${ifDefined(col.inputPlaceholder)}
?required=${col.required}
></sl-input>
`;
};

private evalWithItemIfFn<U extends Primitive>(
maybeFn: U | ((item: T | undefined) => U),
) {
if (typeof maybeFn === "function") {
return maybeFn(this.item);
}
return maybeFn;
}

private readonly onInput = (e: Event) => {
if (!this.column) return;

Expand All @@ -194,7 +220,7 @@ export class DataGridCell extends TableCell {
const input = e.target as InputElement;

this.dispatchEvent(
new CustomEvent<CellEditEventDetail>("btrix-input", {
new CustomEvent<CellEditEventDetail<T>>("btrix-input", {
detail: {
field: this.column.field,
value: input.value,
Expand All @@ -215,7 +241,7 @@ export class DataGridCell extends TableCell {
const input = e.target as InputElement;

this.dispatchEvent(
new CustomEvent<CellEditEventDetail>("btrix-change", {
new CustomEvent<CellEditEventDetail<T>>("btrix-change", {
detail: {
field: this.column.field,
value: input.value,
Expand Down
Loading
Loading