blob: 99f07e0564082bfcda36ae5d50f91627b2ca5171 [file]
// Copyright (C) 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import m from 'mithril';
import {assertExists, assertTrue} from '../base/assert';
import {Registry} from '../base/registry';
import type {PageHandler, PageManager} from '../public/page';
import type {Analytics} from '../public/analytics';
import {Router} from './router';
import {Gate} from '../base/mithril_utils';
export class PageManagerImpl implements PageManager {
private readonly registry = new Registry<PageHandler>((x) => x.route);
private readonly previousPages = new Map<
string,
{page: string; subpage: string}
>();
// Track the current page for analytics - log when it changes
private currentPage: string | undefined;
private readonly analytics: Analytics;
constructor(analytics: Analytics) {
this.analytics = analytics;
}
registerPage(pageHandler: PageHandler): Disposable {
assertTrue(/^\/\w*$/.exec(pageHandler.route) !== null);
// The pluginId is injected by the proxy in AppImpl / TraceImpl. If this is
// undefined somebody (tests) managed to call this method without proxy.
return this.registry.register(pageHandler);
}
// Called by index.ts upon the main frame redraw callback.
renderPageForCurrentRoute(): m.Children {
const route = Router.parseFragment(location.hash);
// Log page changes to analytics
if (this.currentPage !== route.page) {
this.currentPage = route.page;
setTimeout(() => this.analytics.logEvent('User Actions', route.page), 0);
}
this.previousPages.set(route.page, {
page: route.page,
subpage: route.subpage,
});
// Render all pages, but display all inactive pages with display: none and
// avoid calling their view functions. This makes sure DOM state such as
// scrolling position is retained between page flips, which can be handy
// when quickly switching between pages that have long scrolling content
// such as the timeline page.
return Array.from(this.previousPages.entries())
.map(([key, {page, subpage}]) => {
const maybeRenderedPage = this.renderPageForRoute(page, subpage);
// If either the route doesn't exist or requires a trace but the trace
// is not loaded, fall back on the default route.
const renderedPage =
maybeRenderedPage ?? assertExists(this.renderPageForRoute('/', ''));
return [key, renderedPage];
})
.map(([key, page]) => {
return m(Gate, {open: key === route.page}, page);
});
}
// Will return undefined if either: (1) the route does not exist; (2) the
// route exists, it requires a trace, but there is no trace loaded.
private renderPageForRoute(page: string, subpage: string) {
const handler = this.registry.tryGet(page);
if (handler === undefined) {
return undefined;
}
return handler.render(subpage);
}
}