feat: Clean up settings UI (#2018)

- Renames "Org Settings" -> "Settings"
- Reduces gap between settings panel heading and panel
- Always show "Pending Invites" section and update heading styles to
match panel heading
- Update "Current Plan" and "Usage History" sections to be on the same
hierarchical level under "Billing"
- Refactors `<btrix-org>` to move `isAdmin` and `isCrawler` helpers to
app state
This commit is contained in:
sua yoo 2024-08-19 13:37:41 -07:00 committed by GitHub
parent 9a7033875b
commit 4c7f1aa3ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 208 additions and 176 deletions

View File

@ -90,7 +90,8 @@ export class TabList extends TailwindElement {
"header"
"main";
grid-template-columns: 1fr;
grid-gap: 1.5rem;
grid-column-gap: 1.5rem;
grid-row-gap: 1rem;
}
@media only screen and (min-width: ${TWO_COL_SCREEN_MIN_CSS}) {

View File

@ -387,7 +387,7 @@ export class App extends LiteElement {
<sl-menu-item
@click=${() => this.navigate(ROUTES.accountSettings)}
>
<sl-icon slot="prefix" name="gear"></sl-icon>
<sl-icon slot="prefix" name="person-gear"></sl-icon>
${msg("Account Settings")}
</sl-menu-item>
${this.userInfo?.isSuperAdmin

View File

@ -34,9 +34,6 @@ export class Dashboard extends LiteElement {
@property({ type: Boolean })
isCrawler?: boolean;
@property({ type: Boolean })
isAdmin?: boolean;
@state()
private metrics?: Metrics;
@ -74,7 +71,7 @@ export class Dashboard extends LiteElement {
${this.userOrg?.name}
</h1>
${when(
this.isAdmin,
this.appState.isAdmin,
() =>
html` <sl-icon-button
href=${`${this.orgBasePath}/settings`}

View File

@ -22,7 +22,7 @@ import { isApiError } from "@/utils/api";
import type { ViewState } from "@/utils/APIRouter";
import { DEFAULT_MAX_SCALE } from "@/utils/crawler";
import LiteElement, { html } from "@/utils/LiteElement";
import { isAdmin, isCrawler, type OrgData } from "@/utils/orgs";
import { type OrgData } from "@/utils/orgs";
import { AppStateService } from "@/utils/state";
import "./workflow-detail";
@ -113,18 +113,6 @@ export class Org extends LiteElement {
@state()
private isCreateDialogVisible = false;
private get isAdmin() {
const userOrg = this.appState.userOrg;
if (userOrg) return isAdmin(userOrg.role);
return false;
}
private get isCrawler() {
const userOrg = this.appState.userOrg;
if (userOrg) return isCrawler(userOrg.role);
return false;
}
connectedCallback() {
super.connectedCallback();
this.addEventListener(
@ -263,7 +251,8 @@ export class Org extends LiteElement {
["collections", this.renderCollections],
[
"settings",
() => (this.isAdmin ? this.renderOrgSettings() : html``),
() =>
this.appState.isAdmin ? this.renderOrgSettings() : html``,
],
],
() =>
@ -304,17 +293,17 @@ export class Org extends LiteElement {
label: msg("Collections"),
path: "collections",
})}
${when(this.isCrawler, () =>
${when(this.appState.isCrawler, () =>
this.renderNavTab({
tabName: "browser-profiles",
label: msg("Browser Profiles"),
path: "browser-profiles",
}),
)}
${when(this.isAdmin || this.userInfo?.isSuperAdmin, () =>
${when(this.appState.isAdmin || this.userInfo?.isSuperAdmin, () =>
this.renderNavTab({
tabName: "settings",
label: msg("Org Settings"),
label: msg("Settings"),
path: "settings",
}),
)}
@ -356,7 +345,7 @@ export class Org extends LiteElement {
}
private renderNewResourceDialogs() {
if (!this.orgId || !this.isCrawler) {
if (!this.orgId || !this.appState.isCrawler) {
return;
}
if (!this.isCreateDialogVisible) {
@ -413,8 +402,8 @@ export class Org extends LiteElement {
private readonly renderDashboard = () => {
return html`
<btrix-dashboard
?isCrawler=${this.isCrawler}
?isAdmin=${this.isAdmin}
?isCrawler=${this.appState.isCrawler}
?isAdmin=${this.appState.isAdmin}
@select-new-dialog=${this.onSelectNewDialog}
></btrix-dashboard>
`;
@ -425,7 +414,7 @@ export class Org extends LiteElement {
if (params.itemId) {
if (params.qaTab) {
if (!this.isCrawler) {
if (!this.appState.isCrawler) {
return html`<btrix-not-found
class="flex items-center justify-center"
></btrix-not-found>`;
@ -445,12 +434,12 @@ export class Org extends LiteElement {
collectionId=${params.collectionId || ""}
workflowId=${params.workflowId || ""}
itemType=${params.itemType || "crawl"}
?isCrawler=${this.isCrawler}
?isCrawler=${this.appState.isCrawler}
></btrix-archived-item-detail>`;
}
return html`<btrix-archived-items
?isCrawler=${this.isCrawler}
?isCrawler=${this.appState.isCrawler}
itemType=${ifDefined(params.itemType || undefined)}
@select-new-dialog=${this.onSelectNewDialog}
></btrix-archived-items>`;
@ -470,7 +459,7 @@ export class Org extends LiteElement {
workflowId=${workflowId}
openDialogName=${this.viewStateData?.dialog}
?isEditing=${isEditing}
?isCrawler=${this.isCrawler}
?isCrawler=${this.appState.isCrawler}
.maxScale=${this.maxScale}
></btrix-workflow-detail>
`;
@ -481,7 +470,7 @@ export class Org extends LiteElement {
return html` <btrix-workflows-new
class="col-span-5 mt-6"
?isCrawler=${this.isCrawler}
?isCrawler=${this.appState.isCrawler}
.initialWorkflow=${workflow}
.initialSeeds=${seeds}
jobType=${ifDefined(params.jobType)}
@ -490,7 +479,6 @@ export class Org extends LiteElement {
}
return html`<btrix-workflows-list
?isCrawler=${this.isCrawler}
@select-new-dialog=${this.onSelectNewDialog}
></btrix-workflows-list>`;
};
@ -501,7 +489,7 @@ export class Org extends LiteElement {
if (params.browserProfileId) {
return html`<btrix-browser-profiles-detail
profileId=${params.browserProfileId}
?isCrawler=${this.isCrawler}
?isCrawler=${this.appState.isCrawler}
></btrix-browser-profiles-detail>`;
}
@ -520,7 +508,7 @@ export class Org extends LiteElement {
}
return html`<btrix-browser-profiles-list
?isCrawler=${this.isCrawler}
?isCrawler=${this.appState.isCrawler}
@select-new-dialog=${this.onSelectNewDialog}
></btrix-browser-profiles-list>`;
};
@ -533,12 +521,12 @@ export class Org extends LiteElement {
collectionId=${params.collectionId}
collectionTab=${(params.collectionTab as CollectionTab | undefined) ||
"replay"}
?isCrawler=${this.isCrawler}
?isCrawler=${this.appState.isCrawler}
></btrix-collection-detail>`;
}
return html`<btrix-collections-list
?isCrawler=${this.isCrawler}
?isCrawler=${this.appState.isCrawler}
@select-new-dialog=${this.onSelectNewDialog}
></btrix-collections-list>`;
};

View File

@ -84,32 +84,38 @@ export class OrgSettingsBilling extends BtrixElement {
render() {
return html`
<div class="rounded-lg border">
<section class="-mt-5">
${columns([
[
html`
<h4 class="form-label text-neutral-800">
${msg("Current Plan")}
</h4>
<div class="rounded border px-4 pb-4">
${when(
this.org,
(org) => html`
<div
class="mb-3 flex items-center justify-between border-b py-2"
>
<div
class="flex items-center gap-2 text-base font-semibold leading-none"
>
${this.renderSubscriptionDetails(org.subscription)}
</div>
${org.subscription
<div class="mt-5 rounded-lg border px-4 pb-4">
<div
class="mb-3 flex items-center justify-between border-b py-2"
>
<div
class="flex items-center gap-2 text-base font-semibold leading-none"
>
${when(
this.org,
(org) => this.renderSubscriptionDetails(org.subscription),
() => html` <sl-skeleton></sl-skeleton> `,
)}
</div>
${when(
this.org,
(org) =>
org.subscription
? this.renderPortalLink()
: this.salesEmail
? this.renderContactSalesLink(this.salesEmail)
: nothing}
</div>
${org.subscription?.futureCancelDate
: nothing,
() => html` <sl-skeleton></sl-skeleton> `,
)}
</div>
${when(
this.org,
(org) =>
org.subscription?.futureCancelDate
? html`
<div
class="mb-3 flex items-center gap-2 border-b pb-3 text-neutral-500"
@ -134,12 +140,20 @@ export class OrgSettingsBilling extends BtrixElement {
</span>
</div>
`
: nothing}
<h5 class="mb-2 mt-4 text-xs leading-none text-neutral-500">
${msg("Monthly quota")}
</h5>
${this.renderQuotas(org.quotas)}
`,
: nothing,
() => html` <sl-skeleton></sl-skeleton> `,
)}
<h5 class="mb-2 mt-4 text-xs leading-none text-neutral-500">
${msg("Monthly quota")}
</h5>
${when(
this.org,
(org) => this.renderQuotas(org.quotas),
() =>
html` <sl-skeleton class="mb-2"></sl-skeleton>
<sl-skeleton class="mb-2"></sl-skeleton>
<sl-skeleton class="mb-2"></sl-skeleton>
<sl-skeleton class="mb-2"></sl-skeleton>`,
)}
</div>
`,
@ -182,14 +196,20 @@ export class OrgSettingsBilling extends BtrixElement {
`,
],
])}
<div class="p-4">
<btrix-section-heading style="--margin: var(--sl-spacing-medium)">
<h4>${msg("Usage History")}</h4>
</btrix-section-heading>
<btrix-usage-history-table></btrix-usage-history-table>
</div>
</div>
</section>
<section class="mt-7">
<header>
<h3 class="mb-2 text-lg font-medium">${msg("Usage History")}</h3>
</header>
${when(
this.org,
() => html` <btrix-usage-history-table></btrix-usage-history-table> `,
() =>
html`<div class="flex justify-center rounded border p-5 text-xl">
<sl-spinner></sl-spinner>
</div>`,
)}
</section>
`;
}

View File

@ -3,6 +3,7 @@ import type { SlInput } from "@shoelace-style/shoelace";
import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js";
import { html, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { choose } from "lit/directives/choose.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { when } from "lit/directives/when.js";
@ -104,27 +105,33 @@ export class OrgSettings extends BtrixElement {
</header>
<btrix-tab-list activePanel=${this.activePanel} hideIndicator>
<header slot="header" class="flex h-5 items-end justify-between">
${when(
this.activePanel === "members",
() => html`
<h3>${msg("Active Members")}</h3>
<sl-button
href=${`${this.navigate.orgBasePath}/settings/members?invite`}
variant="primary"
size="small"
@click=${this.navigate.link}
>
<sl-icon
slot="prefix"
name="person-add"
aria-hidden="true"
library="default"
></sl-icon>
${msg("Invite New Member")}
</sl-button>
`,
() => html` <h3>${this.tabLabels[this.activePanel]}</h3> `,
<header slot="header" class="flex h-7 items-end justify-between">
${choose(
this.activePanel,
[
[
"members",
() => html`
<h3>${msg("Active Members")}</h3>
<sl-button
href=${`${this.navigate.orgBasePath}/settings/members?invite`}
variant="primary"
size="small"
@click=${this.navigate.link}
>
<sl-icon
slot="prefix"
name="person-add"
aria-hidden="true"
library="default"
></sl-icon>
${msg("Invite New Member")}
</sl-button>
`,
],
["billing", () => html`<h3>${msg("Current Plan")}</h3> `],
],
() => html`<h3>${this.tabLabels[this.activePanel]}</h3>`,
)}
</header>
${this.renderTab("information", "settings")}
@ -167,67 +174,69 @@ export class OrgSettings extends BtrixElement {
return html`<div class="rounded-lg border">
<form @submit=${this.onOrgInfoSubmit}>
${columns([
[
html`
<sl-input
class="with-max-help-text mb-2"
name="orgName"
size="small"
label=${msg("Org Name")}
placeholder=${msg("My Organization")}
autocomplete="off"
value=${this.userOrg.name}
minlength="2"
required
help-text=${this.validateOrgNameMax.helpText}
@sl-input=${this.validateOrgNameMax.validate}
></sl-input>
`,
msg(
"Name of your organization that is visible to all org members.",
),
],
[
html`
<sl-input
class="mb-2"
name="orgSlug"
size="small"
label=${msg("Custom URL Identifier")}
placeholder="my-organization"
autocomplete="off"
value=${this.orgSlug || ""}
minlength="2"
maxlength="30"
required
help-text=${msg(
str`Org home page: ${window.location.protocol}//${
window.location.hostname
}/orgs/${
this.slugValue
? slugifyStrict(this.slugValue)
: this.orgSlug
}`,
)}
@sl-input=${this.handleSlugInput}
></sl-input>
`,
msg(
"Customize your organization's web address for accessing Browsertrix.",
),
],
[
html`
<btrix-copy-field
class="mb-2"
label=${msg("Org ID")}
value=${this.orgId}
></btrix-copy-field>
`,
msg("Use this ID to reference this org in the Browsertrix API."),
],
])}
<div class="p-5">
${columns([
[
html`
<sl-input
class="with-max-help-text mb-2"
name="orgName"
size="small"
label=${msg("Org Name")}
placeholder=${msg("My Organization")}
autocomplete="off"
value=${this.userOrg.name}
minlength="2"
required
help-text=${this.validateOrgNameMax.helpText}
@sl-input=${this.validateOrgNameMax.validate}
></sl-input>
`,
msg(
"Name of your organization that is visible to all org members.",
),
],
[
html`
<sl-input
class="mb-2"
name="orgSlug"
size="small"
label=${msg("Custom URL Identifier")}
placeholder="my-organization"
autocomplete="off"
value=${this.orgSlug || ""}
minlength="2"
maxlength="30"
required
help-text=${msg(
str`Org home page: ${window.location.protocol}//${
window.location.hostname
}/orgs/${
this.slugValue
? slugifyStrict(this.slugValue)
: this.orgSlug
}`,
)}
@sl-input=${this.handleSlugInput}
></sl-input>
`,
msg(
"Customize your organization's web address for accessing Browsertrix.",
),
],
[
html`
<btrix-copy-field
class="mb-2"
label=${msg("Org ID")}
value=${this.orgId}
></btrix-copy-field>
`,
msg("Use this ID to reference this org in the Browsertrix API."),
],
])}
</div>
<footer class="flex justify-end border-t px-4 py-3">
<sl-button
class="inline-control-button"
@ -281,14 +290,13 @@ export class OrgSettings extends BtrixElement {
</btrix-data-table>
</section>
${when(
this.pendingInvites.length,
() => html`
<section class="mt-7">
<h3 class="mb-2 text-lg font-semibold">
${msg("Pending Invites")}
</h3>
<section class="mt-7">
<header>
<h3 class="mb-2 text-lg font-medium">${msg("Pending Invites")}</h3>
</header>
${when(
this.pendingInvites.length,
() => html`
<btrix-data-table
.columns=${[
msg("Email"),
@ -303,9 +311,16 @@ export class OrgSettings extends BtrixElement {
.columnWidths=${columnWidths}
>
</btrix-data-table>
</section>
`,
)}
`,
() => html`
<p
class="rounded border bg-neutral-50 p-3 text-center text-neutral-500"
>
${msg("No pending invites to show.")}
</p>
`,
)}
</section>
<btrix-dialog
.label=${msg("Invite New Member")}

View File

@ -9,7 +9,7 @@ export function columns(
cols: [TemplateResult<1>, TemplateResult<1> | string][],
) {
return html`
<div class="grid grid-cols-5 gap-5 p-5">
<div class="grid grid-cols-5 gap-5">
${cols.map(
([main, info]) => html`
<div class=${tw`col-span-5 self-baseline md:col-span-3`}>${main}</div>

View File

@ -1,7 +1,7 @@
import { localized, msg, str } from "@lit/localize";
import type { SlCheckbox } from "@shoelace-style/shoelace";
import { type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { when } from "lit/directives/when.js";
import queryString from "query-string";
@ -70,9 +70,6 @@ export class WorkflowsList extends LiteElement {
firstSeed: msg("Crawl Start URL"),
};
@property({ type: Boolean })
isCrawler!: boolean;
@state()
private workflows?: APIPaginatedList<ListWorkflow>;
@ -190,12 +187,13 @@ export class WorkflowsList extends LiteElement {
render() {
return html`
<header class="contents">
<div class="mb-4 flex w-full justify-between">
<h1 class="text-xl font-semibold leading-8">
<div class="mb-4 flex w-full justify-end gap-2">
<h1 class="mr-auto text-xl font-semibold leading-8">
${msg("Crawl Workflows")}
</h1>
${when(
this.isCrawler,
this.appState.isCrawler,
() => html`
<sl-button
variant="primary"
@ -413,7 +411,7 @@ export class WorkflowsList extends LiteElement {
private renderMenuItems(workflow: ListWorkflow) {
return html`
${when(
workflow.isCrawlRunning && this.isCrawler,
workflow.isCrawlRunning && this.appState.isCrawler,
// HACK shoelace doesn't current have a way to override non-hover
// color without resetting the --sl-color-neutral-700 variable
() => html`
@ -434,7 +432,7 @@ export class WorkflowsList extends LiteElement {
`,
)}
${when(
this.isCrawler && !workflow.isCrawlRunning,
this.appState.isCrawler && !workflow.isCrawlRunning,
() => html`
<sl-menu-item
style="--sl-color-neutral-700: var(--success)"
@ -447,7 +445,7 @@ export class WorkflowsList extends LiteElement {
`,
)}
${when(
workflow.isCrawlRunning && this.isCrawler,
workflow.isCrawlRunning && this.appState.isCrawler,
// HACK shoelace doesn't current have a way to override non-hover
// color without resetting the --sl-color-neutral-700 variable
() => html`
@ -480,7 +478,7 @@ export class WorkflowsList extends LiteElement {
`,
)}
${when(
this.isCrawler,
this.appState.isCrawler,
() =>
html` <sl-divider></sl-divider>
<sl-menu-item
@ -501,7 +499,7 @@ export class WorkflowsList extends LiteElement {
${msg("Copy Tags")}
</sl-menu-item>
${when(
this.isCrawler,
this.appState.isCrawler,
() =>
html` <sl-menu-item
?disabled=${isArchivingDisabled(this.org, true)}

View File

@ -9,6 +9,7 @@ import type { AppSettings } from "@/types/app";
import { authSchema, type Auth } from "@/types/auth";
import type { OrgData } from "@/types/org";
import { userInfoSchema, type UserInfo, type UserOrg } from "@/types/user";
import { isAdmin, isCrawler } from "@/utils/orgs";
export { use };
@ -46,6 +47,18 @@ export function makeAppStateService() {
get orgId() {
return this.userOrg?.id || "";
}
get isAdmin() {
const userOrg = this.userOrg;
if (userOrg) return isAdmin(userOrg.role);
return false;
}
get isCrawler() {
const userOrg = this.userOrg;
if (userOrg) return isCrawler(userOrg.role);
return false;
}
}
const appState = new AppState();