Skip to content

Commit

Permalink
chore: Improve time to load org UI (#2044)
Browse files Browse the repository at this point in the history
Improves time to first load an org with the following:
- Users user info from login response to set org slug and route user on
log in
- Stores user info in session storage so that it's available on reload
- Stores app settings in local storage until user logs out
- Loads critical org components synchronously
  • Loading branch information
SuaYoo authored Aug 26, 2024
1 parent 96e393e commit 2a057ed
Show file tree
Hide file tree
Showing 23 changed files with 171 additions and 132 deletions.
1 change: 0 additions & 1 deletion frontend/src/components/screencast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@ export class Screencast extends BtrixElement {
changedProperties: PropertyValues<this> & Map<string, unknown>,
) {
if (
changedProperties.get("appState.userOrg") ||
changedProperties.get("crawlId") ||
changedProperties.get("authToken")
) {
Expand Down
38 changes: 22 additions & 16 deletions frontend/src/components/ui/config-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import RegexColorize from "regex-colorize";
import { RelativeDuration } from "./relative-duration";

import type { CrawlConfig, Seed, SeedConfig } from "@/pages/org/types";
import type { AppSettings } from "@/types/app";
import type { Collection } from "@/types/collection";
import { isApiError } from "@/utils/api";
import { DEPTH_SUPPORTED_SCOPES } from "@/utils/crawler";
Expand Down Expand Up @@ -506,28 +507,33 @@ export class ConfigDetails extends LiteElement {

private async fetchAPIDefaults() {
try {
const resp = await fetch("/api/settings", {
headers: { "Content-Type": "application/json" },
});
if (!resp.ok) {
throw new Error(resp.statusText);
let settings: AppSettings;

if (!this.appState.settings) {
const resp = await fetch("/api/settings", {
headers: { "Content-Type": "application/json" },
});
if (!resp.ok) {
throw new Error(resp.statusText);
}
settings = (await resp.json()) as AppSettings;
} else {
settings = this.appState.settings;
}
const orgDefaults = {
...this.orgDefaults,
};
const data = (await resp.json()) as {
defaultBehaviorTimeSeconds: number;
defaultPageLoadTimeSeconds: number;
maxPagesPerCrawl: number;
};
if (data.defaultBehaviorTimeSeconds > 0) {
orgDefaults.behaviorTimeoutSeconds = data.defaultBehaviorTimeSeconds;

if (settings.defaultBehaviorTimeSeconds > 0) {
orgDefaults.behaviorTimeoutSeconds =
settings.defaultBehaviorTimeSeconds;
}
if (data.defaultPageLoadTimeSeconds > 0) {
orgDefaults.pageLoadTimeoutSeconds = data.defaultPageLoadTimeSeconds;
if (settings.defaultPageLoadTimeSeconds > 0) {
orgDefaults.pageLoadTimeoutSeconds =
settings.defaultPageLoadTimeSeconds;
}
if (data.maxPagesPerCrawl > 0) {
orgDefaults.maxPagesPerCrawl = data.maxPagesPerCrawl;
if (settings.maxPagesPerCrawl > 0) {
orgDefaults.maxPagesPerCrawl = settings.maxPagesPerCrawl;
}
this.orgDefaults = orgDefaults;
} catch (e) {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/features/accounts/account-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ export class AccountSettings extends LiteElement {
}),
});

AppStateService.updateUserInfo({
AppStateService.updateUser({
...this.userInfo,
name: newName,
});
Expand Down Expand Up @@ -381,7 +381,7 @@ export class AccountSettings extends LiteElement {
}),
});

AppStateService.updateUserInfo({
AppStateService.updateUser({
...this.userInfo,
email: newEmail,
});
Expand Down
42 changes: 19 additions & 23 deletions frontend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ export class App extends LiteElement {
this.authService.saveLogin(authState);
}
this.syncViewState();
if (authState) {
void this.updateUserInfo();
if (authState && !this.userInfo) {
void this.fetchAndUpdateUserInfo();
}
super.connectedCallback();

Expand All @@ -102,7 +102,10 @@ export class App extends LiteElement {
});

this.startSyncBrowserTabs();
void this.fetchAppSettings();

if (!this.appState.settings) {
void this.fetchAppSettings();
}
}

willUpdate(changedProperties: Map<string, unknown>) {
Expand Down Expand Up @@ -153,26 +156,14 @@ export class App extends LiteElement {
AppStateService.updateSettings(settings);
}

/**
* @deprecate Components should update user info directly through `AppStateService`
*/
private async updateUserInfo(e?: CustomEvent) {
private async fetchAndUpdateUserInfo(e?: CustomEvent) {
if (e) {
e.stopPropagation();
}
try {
const userInfo = await this.getUserInfo();
AppStateService.updateUserInfo(formatAPIUser(userInfo));
const orgs = userInfo.orgs;

if (
orgs.length &&
!this.userInfo!.isSuperAdmin &&
!this.appState.orgSlug
) {
const firstOrg = orgs[0].slug;
AppStateService.updateOrgSlug(firstOrg);
}
const user = await this.getUserInfo();

AppStateService.updateUser(formatAPIUser(user));
} catch (err) {
if ((err as Error | null | undefined)?.message === "Unauthorized") {
console.debug(
Expand Down Expand Up @@ -714,7 +705,6 @@ export class App extends LiteElement {
.viewStateData=${this.viewState.data}
.params=${this.viewState.params}
.maxScale=${this.appState.settings?.maxScale || DEFAULT_MAX_SCALE}
slug=${slug}
orgPath=${orgPath.split(slug)[1]}
orgTab=${orgTab as OrgTab}
></btrix-org>`;
Expand Down Expand Up @@ -867,7 +857,13 @@ export class App extends LiteElement {
this.onFirstLogin({ email: detail.username });
}

void this.updateUserInfo();
if (!this.userInfo) {
if (detail.user) {
AppStateService.updateUser(formatAPIUser(detail.user));
} else {
void this.fetchAndUpdateUserInfo();
}
}
}

onNeedLogin = (e: CustomEvent<NeedLoginEventDetail>) => {
Expand Down Expand Up @@ -901,7 +897,7 @@ export class App extends LiteElement {
};

onUserInfoChange(event: CustomEvent<Partial<UserInfo>>) {
AppStateService.updateUserInfo({
AppStateService.updateUser({
...this.userInfo,
...event.detail,
} as UserInfo);
Expand Down Expand Up @@ -1005,7 +1001,7 @@ export class App extends LiteElement {
if (data.value !== AuthService.storage.getItem()) {
if (data.value) {
this.authService.saveLogin(JSON.parse(data.value) as Auth);
void this.updateUserInfo();
void this.fetchAndUpdateUserInfo();
this.syncViewState();
} else {
this.clearUser();
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ export class Home extends LiteElement {
body: JSON.stringify(params),
});
const userInfo = await this.getUserInfo();
AppStateService.updateUserInfo(formatAPIUser(userInfo));
AppStateService.updateUser(formatAPIUser(userInfo));

this.notify({
message: msg(str`Created new org named "${params.name}".`),
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/pages/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import "./org";

import(/* webpackChunkName: "admin" */ "./admin");
import(/* webpackChunkName: "sign-up" */ "./sign-up");
import(/* webpackChunkName: "log-in" */ "./log-in");
import(/* webpackChunkName: "org" */ "./org");
import(/* webpackChunkName: "crawls" */ "./crawls");
import(/* webpackChunkName: "join" */ "./invite/join");
import(/* webpackChunkName: "verify" */ "./verify");
Expand Down
29 changes: 15 additions & 14 deletions frontend/src/pages/invite/accept.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AcceptInvite } from "./accept";
import type { OrgForm, OrgUpdatedDetail } from "@/pages/invite/ui/org-form";
import AuthService from "@/utils/AuthService";
import { AppStateService } from "@/utils/state";
import { formatAPIUser } from "@/utils/user";

const mockInviteInfo = {
inviterEmail: "inviter_fake_email@example.com",
Expand Down Expand Up @@ -224,6 +225,14 @@ describe("btrix-accept-invite", () => {
});

it("updates user app state on accept", async () => {
const user = {
id: "740d7b63-b257-4311-ba3f-adc46a5fafb8",
email: "fake@example.com",
name: "Fake User",
is_verified: false,
is_superuser: false,
orgs: [],
};
AppStateService.updateAuth(mockAuthState);
const el = await fixture<AcceptInvite>(
html`<btrix-accept-invite
Expand All @@ -233,16 +242,7 @@ describe("btrix-accept-invite", () => {
);

stub(el.navigate, "to");
stub(el, "_getCurrentUser").callsFake(async () =>
Promise.resolve({
id: "740d7b63-b257-4311-ba3f-adc46a5fafb8",
email: "fake@example.com",
name: "Fake User",
is_verified: false,
is_superuser: false,
orgs: [],
}),
);
stub(el, "_getCurrentUser").callsFake(async () => Promise.resolve(user));
stub(el.api, "fetch").callsFake(async () =>
Promise.resolve({
org: {
Expand All @@ -252,13 +252,14 @@ describe("btrix-accept-invite", () => {
},
}),
);
stub(AppStateService, "updateUserInfo");
stub(AppStateService, "updateOrgSlug");
stub(AppStateService, "updateUser");

await el._onAccept();

expect(AppStateService.updateUserInfo).to.have.callCount(1);
expect(AppStateService.updateOrgSlug).to.have.callCount(1);
expect(AppStateService.updateUser).to.have.been.calledWith(
formatAPIUser(user),
mockInviteInfo.orgSlug,
);
});

it("redirects to org dashboard on accept", async () => {
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/pages/invite/accept.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,7 @@ export class AcceptInvite extends BtrixElement {
} else {
const user = await this._getCurrentUser();

AppStateService.updateUserInfo(formatAPIUser(user));
AppStateService.updateOrgSlug(org.slug);
AppStateService.updateUser(formatAPIUser(user), org.slug);

await this.updateComplete;

Expand Down
29 changes: 15 additions & 14 deletions frontend/src/pages/invite/ui/org-form.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { OrgForm } from "./org-form";

import AuthService from "@/utils/AuthService";
import { AppStateService } from "@/utils/state";
import { formatAPIUser } from "@/utils/user";

describe("btrix-org-form", () => {
beforeEach(() => {
Expand Down Expand Up @@ -116,32 +117,32 @@ describe("btrix-org-form", () => {

describe("#_renameOrg", () => {
it("updates user app state on success", async () => {
const user = {
id: "740d7b63-b257-4311-ba3f-adc46a5fafb8",
email: "fake@example.com",
name: "Fake User",
is_verified: false,
is_superuser: false,
orgs: [],
};
const el = await fixture<OrgForm>(
html`<btrix-org-form
newOrgId="e21ab647-2d0e-489d-97d1-88ac91774942"
></btrix-org-form>`,
);
stub(el.api, "fetch").callsFake(async () => Promise.resolve());
stub(el, "_getCurrentUser").callsFake(async () =>
Promise.resolve({
id: "740d7b63-b257-4311-ba3f-adc46a5fafb8",
email: "fake@example.com",
name: "Fake User",
is_verified: false,
is_superuser: false,
orgs: [],
}),
);
stub(AppStateService, "updateUserInfo");
stub(AppStateService, "updateOrgSlug");
stub(el, "_getCurrentUser").callsFake(async () => Promise.resolve(user));
stub(AppStateService, "updateUser");

await el._renameOrg("e21ab647-2d0e-489d-97d1-88ac91774942", {
name: "Fake Org Name 2",
slug: "fake-org-name-2",
});

expect(AppStateService.updateUserInfo).to.have.callCount(1);
expect(AppStateService.updateOrgSlug).to.have.callCount(1);
expect(AppStateService.updateUser).to.have.been.calledWith(
formatAPIUser(user),
"fake-org-name-2",
);
});

it("fires the correct event on success", async () => {
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/pages/invite/ui/org-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,7 @@ export class OrgForm extends BtrixElement {
try {
const user = await this._getCurrentUser();

AppStateService.updateUserInfo(formatAPIUser(user));
AppStateService.updateOrgSlug(data.slug);
AppStateService.updateUser(formatAPIUser(user), data.slug);
} catch (e) {
console.debug(e);
}
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/pages/log-in.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { isApiError } from "@/utils/api";
import type { ViewState } from "@/utils/APIRouter";
import AuthService from "@/utils/AuthService";
import LiteElement, { html } from "@/utils/LiteElement";
import { AppStateService } from "@/utils/state";
import { formatAPIUser } from "@/utils/user";

type FormContext = {
successMessage?: string;
Expand Down Expand Up @@ -338,6 +340,12 @@ export class LogInPage extends LiteElement {
}

async checkBackendInitialized() {
if (this.appState.settings) {
this.formStateService.send("BACKEND_INITIALIZED");

return;
}

const resp = await fetch("/api/settings");
if (resp.status === 200) {
this.formStateService.send("BACKEND_INITIALIZED");
Expand All @@ -360,6 +368,10 @@ export class LogInPage extends LiteElement {
try {
const data = await AuthService.login({ email: username, password });

AppStateService.updateUser(formatAPIUser(data.user));

await this.updateComplete;

this.dispatchEvent(
AuthService.createLoggedInEvent({
...data,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { choose } from "lit/directives/choose.js";
import { guard } from "lit/directives/guard.js";
import { until } from "lit/directives/until.js";
import { when } from "lit/directives/when.js";
import { throttle } from "lodash/fp";
import throttle from "lodash/fp/throttle";
import queryString from "query-string";

import stylesheet from "./archived-item-qa.stylesheet.css";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/org/browser-profiles-detail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { html, nothing, type TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { when } from "lit/directives/when.js";
import { capitalize } from "lodash/fp";
import capitalize from "lodash/fp/capitalize";
import queryString from "query-string";

import type { Profile, ProfileWorkflow } from "./types";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/org/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class Dashboard extends LiteElement {
};

willUpdate(changedProperties: PropertyValues<this> & Map<string, unknown>) {
if (changedProperties.has("appState.userOrg") && this.orgId) {
if (changedProperties.has("appState.orgSlug") && this.orgId) {
void this.fetchMetrics();
}
}
Expand Down
Loading

0 comments on commit 2a057ed

Please sign in to comment.