Allow superadmins to create org from UI (#563)
This commit is contained in:
parent
4875d7727d
commit
17e1628d2d
@ -55,7 +55,7 @@ export class InviteForm extends LiteElement {
|
||||
|
||||
<div>
|
||||
<sl-button
|
||||
variant="primary"
|
||||
size="small"
|
||||
type="submit"
|
||||
?loading=${this.isSubmitting}
|
||||
?disabled=${this.isSubmitting}
|
||||
|
@ -41,7 +41,7 @@ export class OrgsList extends LiteElement {
|
||||
(this.userInfo.isAdmin ||
|
||||
isOwner(org.users[this.userInfo.id].role))
|
||||
? html`<sl-tag size="small" variant="primary"
|
||||
>${msg("Owner")}</sl-tag
|
||||
>${msg("Admin")}</sl-tag
|
||||
>`
|
||||
: ""}
|
||||
</li>
|
||||
|
@ -268,7 +268,14 @@ export class App extends LiteElement {
|
||||
class="max-w-screen-lg mx-auto pl-3 box-border h-12 flex items-center justify-between"
|
||||
>
|
||||
<div>
|
||||
<a href="${homeHref}" @click="${this.navLink}"
|
||||
<a
|
||||
href=${homeHref}
|
||||
@click=${(e: any) => {
|
||||
if (isAdmin) {
|
||||
this.clearSelectedOrg();
|
||||
}
|
||||
this.navLink(e);
|
||||
}}
|
||||
><h1 class="text-sm hover:text-neutral-400 font-medium">
|
||||
${msg("Browsertrix Cloud")}
|
||||
</h1></a
|
||||
@ -280,6 +287,15 @@ export class App extends LiteElement {
|
||||
<div
|
||||
class="text-xs md:text-sm grid grid-flow-col gap-3 md:gap-5 items-center"
|
||||
>
|
||||
<a
|
||||
class="text-neutral-500 hover:text-neutral-400 font-medium"
|
||||
href="/"
|
||||
@click=${(e: any) => {
|
||||
this.clearSelectedOrg();
|
||||
this.navLink(e);
|
||||
}}
|
||||
>${msg("Dashboard")}</a
|
||||
>
|
||||
<a
|
||||
class="text-neutral-500 hover:text-neutral-400 font-medium"
|
||||
href="/crawls"
|
||||
@ -366,11 +382,16 @@ export class App extends LiteElement {
|
||||
<sl-menu
|
||||
@sl-select=${(e: CustomEvent) => {
|
||||
const { value } = e.detail.item;
|
||||
this.navigate(`/orgs/${value}${value ? "/crawls" : ""}`);
|
||||
if (this.userInfo) {
|
||||
this.persistUserSettings(this.userInfo.id, { orgId: value });
|
||||
if (value) {
|
||||
this.navigate(`/orgs/${value}/crawls`);
|
||||
if (this.userInfo) {
|
||||
this.persistUserSettings(this.userInfo.id, { orgId: value });
|
||||
}
|
||||
} else {
|
||||
console.debug("User info not set");
|
||||
if (this.userInfo) {
|
||||
this.clearSelectedOrg();
|
||||
}
|
||||
this.navigate(`/`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@ -544,6 +565,11 @@ export class App extends LiteElement {
|
||||
class="w-full md:bg-neutral-50"
|
||||
@navigate=${this.onNavigateTo}
|
||||
@logged-in=${this.onLoggedIn}
|
||||
@update-user-info=${(e: CustomEvent) => {
|
||||
e.stopPropagation();
|
||||
this.updateUserInfo();
|
||||
}}
|
||||
@notify="${this.onNotify}"
|
||||
.authState=${this.authService.authState}
|
||||
.userInfo=${this.userInfo}
|
||||
.orgId=${this.selectedOrgId}
|
||||
@ -878,6 +904,13 @@ export class App extends LiteElement {
|
||||
private unpersistUserSettings(userId: string) {
|
||||
window.localStorage.removeItem(`${App.storageKey}.${userId}`);
|
||||
}
|
||||
|
||||
private clearSelectedOrg() {
|
||||
this.selectedOrgId = undefined;
|
||||
if (this.userInfo) {
|
||||
this.persistUserSettings(this.userInfo.id, { orgId: "" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("browsertrix-app", App);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { state, property } from "lit/decorators.js";
|
||||
import { msg, localized } from "@lit/localize";
|
||||
import { msg, localized, str } from "@lit/localize";
|
||||
import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js";
|
||||
|
||||
import type { AuthState } from "../utils/AuthService";
|
||||
import type { CurrentUser } from "../types/user";
|
||||
@ -23,6 +24,15 @@ export class Home extends LiteElement {
|
||||
@state()
|
||||
private orgList?: OrgData[];
|
||||
|
||||
@state()
|
||||
private isAddingOrg = false;
|
||||
|
||||
@state()
|
||||
private isAddOrgFormVisible = false;
|
||||
|
||||
@state()
|
||||
private isSubmittingNewOrg = false;
|
||||
|
||||
connectedCallback() {
|
||||
if (this.authState) {
|
||||
super.connectedCallback();
|
||||
@ -81,56 +91,7 @@ export class Home extends LiteElement {
|
||||
|
||||
private renderLoggedInAdmin() {
|
||||
if (this.orgList!.length) {
|
||||
return html`
|
||||
<section class="border rounded-lg bg-white p-4 md:p-6 mb-5">
|
||||
<form
|
||||
@submit=${(e: SubmitEvent) => {
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const id = formData.get("crawlId");
|
||||
this.navTo(`/crawls/crawl/${id}`);
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-wrap items-center">
|
||||
<div
|
||||
class="w-full md:w-min grow-0 mr-8 text-lg font-medium whitespace-nowrap"
|
||||
>
|
||||
${msg("Go to Crawl")}
|
||||
</div>
|
||||
<div class="grow mt-2 md:mt-0 md:mr-2">
|
||||
<sl-input
|
||||
name="crawlId"
|
||||
placeholder=${msg("Enter Crawl ID")}
|
||||
required
|
||||
></sl-input>
|
||||
</div>
|
||||
<div class="grow-0 mt-2 md:mt-0 text-right">
|
||||
<sl-button variant="neutral" type="submit">
|
||||
<sl-icon slot="prefix" name="arrow-right-circle"></sl-icon>
|
||||
${msg("Go")}</sl-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="grid grid-cols-3 gap-8">
|
||||
<div class="col-span-3 md:col-span-2">
|
||||
<section>
|
||||
<h2 class="text-lg font-medium mb-3 mt-2">${msg("All Organizations")}</h2>
|
||||
<btrix-orgs-list
|
||||
.userInfo=${this.userInfo}
|
||||
.orgList=${this.orgList}
|
||||
></btrix-orgs-list>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-span-3 md:col-span-1 md:mt-12">
|
||||
<section class="md:border md:rounded-lg md:bg-white p-3 md:p-8">
|
||||
<h2 class="text-lg font-medium mb-4">${msg("Invite a User")}</h2>
|
||||
${this.renderInvite()}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return this.renderAdminOrgs();
|
||||
}
|
||||
|
||||
return html`
|
||||
@ -144,6 +105,113 @@ export class Home extends LiteElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderAdminOrgs() {
|
||||
return html`
|
||||
<section class="border rounded-lg bg-white p-4 md:p-6 mb-5">
|
||||
<form
|
||||
@submit=${(e: SubmitEvent) => {
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const id = formData.get("crawlId");
|
||||
this.navTo(`/crawls/crawl/${id}`);
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-wrap items-center">
|
||||
<div
|
||||
class="w-full md:w-min grow-0 mr-8 text-lg font-medium whitespace-nowrap"
|
||||
>
|
||||
${msg("Go to Crawl")}
|
||||
</div>
|
||||
<div class="grow mt-2 md:mt-0 md:mr-2">
|
||||
<sl-input
|
||||
name="crawlId"
|
||||
placeholder=${msg("Enter Crawl ID")}
|
||||
required
|
||||
></sl-input>
|
||||
</div>
|
||||
<div class="grow-0 mt-2 md:mt-0 text-right">
|
||||
<sl-button variant="neutral" type="submit">
|
||||
<sl-icon slot="prefix" name="arrow-right-circle"></sl-icon>
|
||||
${msg("Go")}</sl-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="grid grid-cols-3 gap-8">
|
||||
<div class="col-span-3 md:col-span-2">
|
||||
<section>
|
||||
<header class="flex items-start justify-between">
|
||||
<h2 class="text-lg font-medium mb-3 mt-2">
|
||||
${msg("All Organizations")}
|
||||
</h2>
|
||||
<sl-button
|
||||
variant="primary"
|
||||
@click=${() => (this.isAddingOrg = true)}
|
||||
>
|
||||
<sl-icon slot="prefix" name="plus-lg"></sl-icon>
|
||||
${msg("New Organization")}
|
||||
</sl-button>
|
||||
</header>
|
||||
<btrix-orgs-list
|
||||
.userInfo=${this.userInfo}
|
||||
.orgList=${this.orgList}
|
||||
></btrix-orgs-list>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-span-3 md:col-span-1">
|
||||
<section class="md:border md:rounded-lg md:bg-white p-3 md:p-8">
|
||||
<h2 class="text-lg font-medium mb-4">${msg("Invite a User")}</h2>
|
||||
${this.renderInvite()}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<btrix-dialog
|
||||
label=${msg("New Organization")}
|
||||
?open=${this.isAddingOrg}
|
||||
@sl-request-close=${() => (this.isAddingOrg = false)}
|
||||
@sl-show=${() => (this.isAddOrgFormVisible = true)}
|
||||
@sl-after-hide=${() => (this.isAddOrgFormVisible = false)}
|
||||
>
|
||||
${this.isAddOrgFormVisible
|
||||
? html`
|
||||
<form
|
||||
id="newOrgForm"
|
||||
@reset=${() => (this.isAddingOrg = false)}
|
||||
@submit=${this.onSubmitNewOrg}
|
||||
>
|
||||
<div class="mb-5">
|
||||
<sl-input
|
||||
name="name"
|
||||
label=${msg("Org Name")}
|
||||
placeholder=${msg("My Organization")}
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</sl-input>
|
||||
</div>
|
||||
</form>
|
||||
<div slot="footer" class="flex justify-between">
|
||||
<sl-button form="newOrgForm" type="reset" size="small"
|
||||
>${msg("Cancel")}</sl-button
|
||||
>
|
||||
<sl-button
|
||||
form="newOrgForm"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
size="small"
|
||||
?loading=${this.isSubmittingNewOrg}
|
||||
?disabled=${this.isSubmittingNewOrg}
|
||||
>${msg("Create Org")}</sl-button
|
||||
>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</btrix-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderLoggedInNonAdmin() {
|
||||
if (this.orgList && !this.orgList.length) {
|
||||
return html`<div class="border rounded-lg bg-white p-4 md:p-8">
|
||||
@ -188,4 +256,45 @@ export class Home extends LiteElement {
|
||||
|
||||
return data.orgs;
|
||||
}
|
||||
|
||||
private async onSubmitNewOrg(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
const formEl = e.target as HTMLFormElement;
|
||||
if (!(await this.checkFormValidity(formEl))) return;
|
||||
|
||||
const params = serialize(formEl);
|
||||
this.isSubmittingNewOrg = true;
|
||||
|
||||
try {
|
||||
await this.apiFetch(`/orgs/create`, this.authState!, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
|
||||
this.fetchOrgs();
|
||||
this.notify({
|
||||
message: msg(str`Created new org named "${params.name}".`),
|
||||
variant: "success",
|
||||
icon: "check2-circle",
|
||||
duration: 8000,
|
||||
});
|
||||
this.isAddingOrg = false;
|
||||
} catch (e: any) {
|
||||
this.notify({
|
||||
message: e.isApiError
|
||||
? e.message
|
||||
: msg("Sorry, couldn't create organization at this time."),
|
||||
variant: "danger",
|
||||
icon: "exclamation-octagon",
|
||||
});
|
||||
}
|
||||
|
||||
this.isSubmittingNewOrg = false;
|
||||
}
|
||||
|
||||
async checkFormValidity(formEl: HTMLFormElement) {
|
||||
await this.updateComplete;
|
||||
return !formEl.querySelector("[data-invalid]");
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import type { ViewState } from "../../utils/APIRouter";
|
||||
import type { AuthState } from "../../utils/AuthService";
|
||||
import type { CurrentUser } from "../../types/user";
|
||||
import type { OrgData } from "../../utils/orgs";
|
||||
import { isOwner, isCrawler } from "../../utils/orgs";
|
||||
import { isAdmin, isCrawler } from "../../utils/orgs";
|
||||
import LiteElement, { html } from "../../utils/LiteElement";
|
||||
import { needLogin } from "../../utils/auth";
|
||||
import "./crawl-configs-detail";
|
||||
@ -67,9 +67,9 @@ export class Org extends LiteElement {
|
||||
return this.userInfo.orgs.find(({ id }) => id === this.orgId)!;
|
||||
}
|
||||
|
||||
get isOwner() {
|
||||
get isAdmin() {
|
||||
const userOrg = this.userOrg;
|
||||
if (userOrg) return isOwner(userOrg.role);
|
||||
if (userOrg) return isAdmin(userOrg.role);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -125,7 +125,7 @@ export class Org extends LiteElement {
|
||||
tabPanelContent = this.renderBrowserProfiles();
|
||||
break;
|
||||
case "settings": {
|
||||
if (this.isOwner) {
|
||||
if (this.isAdmin) {
|
||||
tabPanelContent = this.renderOrgSettings();
|
||||
break;
|
||||
}
|
||||
@ -167,7 +167,7 @@ export class Org extends LiteElement {
|
||||
label: msg("Browser Profiles"),
|
||||
})
|
||||
)}
|
||||
${when(this.isOwner, () =>
|
||||
${when(this.isAdmin || this.userInfo?.isAdmin, () =>
|
||||
this.renderNavTab({
|
||||
tabName: "settings",
|
||||
label: msg("Org Settings"),
|
||||
|
@ -1,7 +1,8 @@
|
||||
// From UserRole in backend
|
||||
export type UserRole = "viewer" | "crawler" | "owner";
|
||||
export type UserRole = "viewer" | "crawler" | "owner" | "superadmin";
|
||||
|
||||
export const AccessCode: Record<UserRole, number> = {
|
||||
superadmin: 100,
|
||||
viewer: 10,
|
||||
crawler: 20,
|
||||
owner: 40,
|
||||
|
@ -7,6 +7,12 @@ export function isOwner(accessCode?: typeof AccessCode[UserRole]): boolean {
|
||||
return accessCode === AccessCode.owner;
|
||||
}
|
||||
|
||||
export function isAdmin(accessCode?: typeof AccessCode[UserRole]): boolean {
|
||||
if (!accessCode) return false;
|
||||
|
||||
return accessCode >= AccessCode.owner;
|
||||
}
|
||||
|
||||
export function isCrawler(accessCode?: typeof AccessCode[UserRole]): boolean {
|
||||
if (!accessCode) return false;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user