From a8757e2e50e3269309bd00d803e7448f6c9bb15d Mon Sep 17 00:00:00 2001 From: sua yoo Date: Wed, 15 Jun 2022 18:50:14 -0700 Subject: [PATCH] Screencast UX enhancements (#251) * animate starting state * consistent fixed-size slots for each browser (url + screencast) * add tooltip for expected number of browsers (workers x scale) --- frontend/src/components/screencast.ts | 162 ++++++++++++++++----- frontend/src/pages/archive/crawl-detail.ts | 34 ++++- 2 files changed, 154 insertions(+), 42 deletions(-) diff --git a/frontend/src/components/screencast.ts b/frontend/src/components/screencast.ts index 9fd4dd04..737262df 100644 --- a/frontend/src/components/screencast.ts +++ b/frontend/src/components/screencast.ts @@ -1,4 +1,5 @@ import { LitElement, html, css } from "lit"; +import { msg, localized, str } from "@lit/localize"; import { property, state } from "lit/decorators.js"; type Message = { @@ -33,6 +34,7 @@ type CloseMessage = Message & { * > * ``` */ +@localized() export class Screencast extends LitElement { static styles = css` .wrapper { @@ -49,15 +51,30 @@ export class Screencast extends LitElement { gap: 0.5rem; } + .screen-count { + color: var(--sl-color-neutral-400); + font-size: var(--sl-font-size-small); + margin-bottom: var(--sl-spacing-x-small); + } + + .screen-count span, + .screen-count sl-icon { + display: inline-block; + vertical-align: middle; + } + .screen { - border: 1px solid var(--sl-color-neutral-100); + border: 1px solid var(--sl-panel-border-color); border-radius: var(--sl-border-radius-medium); - cursor: pointer; - transition: opacity 0.1s border-color 0.1s; overflow: hidden; } - .screen:hover { + .screen[role="button"] { + cursor: pointer; + transition: opacity 0.1s border-color 0.1s; + } + + .screen[role="button"]:hover { opacity: 0.8; border-color: var(--sl-color-neutral-300); } @@ -66,19 +83,17 @@ export class Screencast extends LitElement { margin: 0; } - figcaption { - flex: 1; - border-bottom-width: 1px; - border-bottom-color: var(--sl-panel-border-color); - color: var(--sl-color-neutral-600); - font-size: var(--sl-font-size-small); + .caption { padding: var(--sl-spacing-x-small); + flex: 1; + border-bottom: 1px solid var(--sl-panel-border-color); + color: var(--sl-color-neutral-600); } - figcaption, + .caption, .dialog-label { display: block; - font-size: var(--sl-font-size-small); + font-size: var(--sl-font-size-x-small); line-height: 1; /* Truncate: */ overflow: hidden; @@ -90,7 +105,12 @@ export class Screencast extends LitElement { max-width: 40em; } - img { + .frame { + background-color: var(--sl-color-neutral-50); + overflow: hidden; + } + + .frame > img { display: block; width: 100%; height: auto; @@ -109,11 +129,15 @@ export class Screencast extends LitElement { @property({ type: String }) crawlId?: string; + @property({ type: Number }) + scale: number = 1; + @property({ type: Array }) watchIPs: string[] = []; + // List of browser screens @state() - private dataList: Array = []; + private dataList: Array = []; @state() private isConnecting: boolean = false; @@ -123,12 +147,15 @@ export class Screencast extends LitElement { // Websocket connections private wsMap: Map = new Map(); - - // Page image data - private imageDataMap: Map = new Map(); - - private screenCount = 1; + // Map data order to screen data + private dataMap: { [index: number]: ScreencastMessage | null } = {}; + // Map page ID to data order + private pageOrderMap: Map = new Map(); + // Number of available browsers. + // Multiply by scale to get available browser window count + private browsersCount = 0; private screenWidth = 640; + private screenHeight = 480; shouldUpdate(changedProperties: Map) { if (changedProperties.size === 1 && changedProperties.has("watchIPs")) { @@ -170,26 +197,56 @@ export class Screencast extends LitElement { render() { return html`
- ${this.isConnecting + ${this.isConnecting || !this.dataList.length ? html`
` - : ""} + : html` +
+ ${msg( + str`Running in ${ + this.browsersCount * this.scale + } browser windows` + )} + +
+ `} +
${this.dataList.map( - (pageData) => html`
(this.focusedScreenData = pageData)} - > -
${pageData.url}
- -
` + (pageData) => + html`
(this.focusedScreenData = pageData) + : () => {}} + > +
+ ${pageData?.url || html` `} +
+
+ ${pageData + ? html`` + : ""} +
+
` )}
@@ -272,8 +329,20 @@ export class Screencast extends LitElement { message: InitMessage | ScreencastMessage | CloseMessage ) { if (message.msg === "init") { - this.screenCount = message.browsers; + this.dataList = Array.from( + { length: message.browsers * this.scale }, + () => null + ); + this.dataMap = this.dataList.reduce( + (acc, val, i) => ({ + ...acc, + [i]: val, + }), + {} + ); + this.browsersCount = message.browsers; this.screenWidth = message.width; + this.screenHeight = message.height; } else { const { id } = message; @@ -287,26 +356,39 @@ export class Screencast extends LitElement { this.isConnecting = false; } - this.imageDataMap.set(id, message); + let idx = this.pageOrderMap.get(id); + + if (idx === undefined) { + // Find and fill first empty slot + idx = this.dataList.indexOf(null); + + if (idx === -1) { + console.debug("no empty slots"); + } + + this.pageOrderMap.set(id, idx); + } if (this.focusedScreenData?.id === id) { this.focusedScreenData = message; } + this.dataMap[idx] = message; this.updateDataList(); } else if (message.msg === "close") { - this.imageDataMap.delete(id); - this.updateDataList(); + const idx = this.pageOrderMap.get(id); + + if (idx !== undefined && idx !== null) { + this.dataMap[idx] = null; + this.updateDataList(); + this.pageOrderMap.set(id, -1); + } } } } updateDataList() { - // keep same number of data entries (probably should only decrease if scale is reduced) - this.dataList = [ - ...this.imageDataMap.values(), - ...this.dataList.slice(this.imageDataMap.size), - ]; + this.dataList = Object.values(this.dataMap); } unfocusScreen() { diff --git a/frontend/src/pages/archive/crawl-detail.ts b/frontend/src/pages/archive/crawl-detail.ts index 7e13c283..95ade4b4 100644 --- a/frontend/src/pages/archive/crawl-detail.ts +++ b/frontend/src/pages/archive/crawl-detail.ts @@ -8,6 +8,7 @@ import type { AuthState } from "../../utils/AuthService"; import LiteElement, { html } from "../../utils/LiteElement"; import { CopyButton } from "../../components/copy-button"; import type { Crawl } from "./types"; +import { times } from "lodash"; type SectionName = "overview" | "watch" | "replay" | "files" | "logs"; @@ -80,6 +81,16 @@ export class CrawlDetail extends LiteElement { this.fetchCrawl(); } + updated(changedProperties: Map) { + const prevCrawl = changedProperties.get("crawl"); + + if (prevCrawl && this.crawl) { + if (prevCrawl.state === "running" && this.crawl.state !== "running") { + this.crawlDone(); + } + } + } + connectedCallback(): void { // Set initial active section based on URL #hash value const hash = window.location.hash.slice(1); @@ -433,8 +444,8 @@ export class CrawlDetail extends LiteElement { ${isStarting ? html`
-

- ${msg("Crawl is starting...")} +

+ ${msg("Crawl starting...")}

` : isRunning @@ -444,6 +455,7 @@ export class CrawlDetail extends LiteElement { authToken=${authToken} archiveId=${this.crawl.aid} crawlId=${this.crawlId!} + scale=${this.crawl.scale} .watchIPs=${this.crawl.watchIPs || []} > @@ -852,6 +864,24 @@ export class CrawlDetail extends LiteElement { window.clearTimeout(this.timerId); } + /** Callback when crawl is no longer running */ + private crawlDone() { + if (!this.crawl) return; + + this.notify({ + message: msg( + html`Done crawling ${this.crawl.configName}.` + ), + type: "success", + icon: "check2-circle", + }); + + if (this.sectionName === "watch") { + // Show replay tab + this.sectionName = "replay"; + } + } + /** * Enter fullscreen mode * @param id ID of element to fullscreen