From 5daf550cb87d103ed6a5a8ac41b44c1d595e21bb Mon Sep 17 00:00:00 2001 From: sua yoo Date: Tue, 3 Jan 2023 15:37:32 -0800 Subject: [PATCH] Frontend archives -> teams migration (#429) --- frontend/src/components/account-settings.ts | 9 +- frontend/src/index.ts | 296 ++++++++++++++------ frontend/src/pages/archive/crawls-list.ts | 8 +- frontend/src/pages/archive/index.ts | 49 ++-- frontend/src/pages/archives.ts | 2 +- frontend/src/pages/home.ts | 28 +- frontend/src/theme.ts | 5 + frontend/src/types/user.ts | 1 + 8 files changed, 268 insertions(+), 130 deletions(-) diff --git a/frontend/src/components/account-settings.ts b/frontend/src/components/account-settings.ts index 09cbe779..9725c8d4 100644 --- a/frontend/src/components/account-settings.ts +++ b/frontend/src/components/account-settings.ts @@ -233,11 +233,18 @@ export class AccountSettings extends LiteElement { } return html`
-

${msg("Account settings")}

+

${msg("Account Settings")}

${successMessage}
+
+
${msg("Name")}
+
+ ${this.userInfo?.name} +
+
+
${msg("Email")}
diff --git a/frontend/src/index.ts b/frontend/src/index.ts index 0f414a50..78a9cb9e 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -1,7 +1,7 @@ import type { TemplateResult } from "lit"; import { render } from "lit"; import { property, state, query } from "lit/decorators.js"; -import { ifDefined } from "lit/directives/if-defined.js"; +import { when } from "lit/directives/when.js"; import { msg, localized } from "@lit/localize"; import type { SlDialog } from "@shoelace-style/shoelace"; import "tailwindcss/tailwind.css"; @@ -15,6 +15,7 @@ import type { LoggedInEvent } from "./utils/AuthService"; import type { ViewState } from "./utils/APIRouter"; import type { CurrentUser } from "./types/user"; import type { AuthStorageEventData } from "./utils/AuthService"; +import type { Archive, ArchiveData } from "./utils/archives"; import theme from "./theme"; import { ROUTES, DASHBOARD_ROUTE } from "./routes"; import "./shoelace"; @@ -30,6 +31,14 @@ type DialogContent = { noHeader?: boolean; }; +type APIUser = { + id: string; + email: string; + name: string; + is_verified: boolean; + is_superuser: boolean; +}; + /** * @event navigate * @event notify @@ -64,10 +73,19 @@ export class App extends LiteElement { @state() private isRegistrationEnabled?: boolean; + @state() + private teams?: ArchiveData[]; + + // Store selected team ID for when navigating from + // pages without associated team (e.g. user account) + @state() + private selectedTeamId?: string; + async connectedCallback() { const authState = await AuthService.initSessionStorage(); if (authState) { this.authService.saveLogin(authState); + this.updateUserInfo(); } this.syncViewState(); super.connectedCallback(); @@ -77,6 +95,19 @@ export class App extends LiteElement { }); this.startSyncBrowserTabs(); + this.fetchAppSettings(); + } + + willUpdate(changedProperties: Map) { + if (changedProperties.has("userInfo") && this.userInfo) { + this.selectedTeamId = this.userInfo.defaultTeamId; + } + if ( + changedProperties.get("viewState") && + this.viewState.route === "archive" + ) { + this.selectedTeamId = this.viewState.params.id; + } } private syncViewState() { @@ -95,11 +126,7 @@ export class App extends LiteElement { } } - async firstUpdated() { - if (this.authService.authState) { - this.updateUserInfo(); - } - + private async fetchAppSettings() { const settings = await this.getAppSettings(); if (settings) { @@ -111,22 +138,48 @@ export class App extends LiteElement { private async updateUserInfo() { try { - const data = await this.getUserInfo(); + const [userInfoResp, archivesResp] = await Promise.allSettled([ + this.getUserInfo(), + // TODO see if we can add API endpoint to retrieve first archive + this.getArchives(), + ]); - this.userInfo = { - id: data.id, - email: data.email, - name: data.name, - isVerified: data.is_verified, - isAdmin: data.is_superuser, - }; + const userInfoSuccess = userInfoResp.status === "fulfilled"; + let defaultTeamId: CurrentUser["defaultTeamId"]; + + if (archivesResp.status === "fulfilled") { + const { archives } = archivesResp.value; + this.teams = archives; + if (userInfoSuccess) { + const userInfo = userInfoResp.value; + if (archives.length && !userInfo?.is_superuser) { + defaultTeamId = archives[0].id; + } + } + } else { + throw archivesResp.reason; + } + + if (userInfoSuccess) { + const { value } = userInfoResp; + this.userInfo = { + id: value.id, + email: value.email, + name: value.name, + isVerified: value.is_verified, + isAdmin: value.is_superuser, + defaultTeamId, + }; + } else { + throw userInfoResp.reason; + } } catch (err: any) { if (err?.message === "Unauthorized") { console.debug( "Unauthorized with authState:", this.authService.authState ); - this.authService.logout(); + this.clearUser(); this.navigate(ROUTES.login); } } @@ -205,8 +258,12 @@ export class App extends LiteElement { `; } - renderNavBar() { + private renderNavBar() { const isAdmin = this.userInfo?.isAdmin; + let homeHref = "/"; + if (!isAdmin && this.selectedTeamId) { + homeHref = `/archives/${this.selectedTeamId}/crawls`; + } return html`
@@ -214,7 +271,7 @@ export class App extends LiteElement { class="max-w-screen-lg mx-auto pl-3 box-border h-12 flex items-center justify-between" >
-

${msg("Browsertrix Cloud")}

- ${msg("All Archives")} +
${this.authService.authState - ? html` - + ? html` ${this.renderTeams()} + + - -
- ${isAdmin - ? html` -
- ${msg("admin")} -
- ` + +
${this.renderMenuUserInfo()}
+ + this.navigate(ROUTES.accountSettings)} + > + + ${msg("Account Settings")} + + ${this.userInfo?.isAdmin + ? html` this.navigate(ROUTES.usersInvite)} + > + + ${msg("Invite Users")} + ` : ""} -
- ${this.userInfo?.name} -
-
- ${this.userInfo?.email} -
-
- - this.navigate(ROUTES.accountSettings)} - > - - ${msg("Your account")} - - ${this.userInfo?.isAdmin - ? html` this.navigate(ROUTES.usersInvite)} - > - - ${msg("Invite Users")} - ` - : ""} - - - - ${msg("Log Out")} - -
-
` + + + + ${msg("Log Out")} + + +
` : html`
${msg("Log In")} ${this.isRegistrationEnabled @@ -314,7 +347,91 @@ export class App extends LiteElement { `; } - renderFooter() { + private renderTeams() { + if (!this.teams || this.teams.length < 2 || !this.userInfo) return; + + const selectedOption = this.selectedTeamId + ? this.teams.find(({ id }) => id === this.selectedTeamId) + : { id: "", name: msg("All Teams") }; + if (!selectedOption) { + console.debug( + `Could't find team with ID ${this.selectedTeamId}`, + this.teams + ); + return; + } + + return html` + + ${selectedOption.name} + { + const { value } = e.detail.item; + this.navigate(`/archives/${value}${value ? "/crawls" : ""}`); + }} + > + ${when( + this.userInfo.isAdmin, + () => html` + ${msg("All Teams")} + + ` + )} + ${this.teams.map( + (team) => html` + ${team.name} + ` + )} + + + `; + } + + private renderMenuUserInfo() { + if (!this.userInfo) return; + if (this.userInfo.isAdmin) { + return html` +
+ ${msg("admin")} +
+
${this.userInfo?.name}
+
+ ${this.userInfo?.email} +
+ `; + } + + if (this.teams?.length === 1) { + return html` +
+ ${this.teams![0].name} +
+
${this.userInfo?.name}
+
+ ${this.userInfo?.email} +
+ `; + } + + return html` +
${this.userInfo?.name}
+
+ ${this.userInfo?.email} +
+ `; + } + + private renderFooter() { return html`