From 433e5c17c29ac005a216842c99c84774e47ecd81 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Thu, 15 Sep 2022 01:29:03 -0400 Subject: [PATCH] Adds vscode style progress indicator to the Graph Fixes progress indicators when loading new rows & other updates happen (e.g. avatars) --- src/plus/webviews/graph/protocol.ts | 4 +- src/webviews/apps/plus/graph/GraphWrapper.tsx | 47 +++++++--------- src/webviews/apps/plus/graph/graph.scss | 55 +++++++++++++++++++ src/webviews/apps/plus/graph/graph.tsx | 33 ++++++----- 4 files changed, 98 insertions(+), 41 deletions(-) diff --git a/src/plus/webviews/graph/protocol.ts b/src/plus/webviews/graph/protocol.ts index 5eb52bc81dbed..a7d986d730ca3 100644 --- a/src/plus/webviews/graph/protocol.ts +++ b/src/plus/webviews/graph/protocol.ts @@ -59,8 +59,8 @@ export interface GraphCompositeConfig extends GraphConfig { columns?: Record; } -export interface CommitListCallback { - (state: State): void; +export interface UpdateStateCallback { + (state: State, oldState: State): void; } // Commands diff --git a/src/webviews/apps/plus/graph/GraphWrapper.tsx b/src/webviews/apps/plus/graph/GraphWrapper.tsx index c0b2e024bee4f..a55165bd7b119 100644 --- a/src/webviews/apps/plus/graph/GraphWrapper.tsx +++ b/src/webviews/apps/plus/graph/GraphWrapper.tsx @@ -13,11 +13,11 @@ import type { GraphColumnConfig } from '../../../../config'; import { RepositoryVisibility } from '../../../../git/gitProvider'; import type { GitGraphRowType } from '../../../../git/models/graph'; import type { - CommitListCallback, DismissBannerParams, GraphCompositeConfig, GraphRepository, State, + UpdateStateCallback, } from '../../../../plus/webviews/graph/protocol'; import type { Subscription } from '../../../../subscription'; import { getSubscriptionTimeRemaining, SubscriptionState } from '../../../../subscription'; @@ -25,7 +25,7 @@ import { pluralize } from '../../../../system/string'; export interface GraphWrapperProps extends State { nonce?: string; - subscriber: (callback: CommitListCallback) => () => void; + subscriber: (callback: UpdateStateCallback) => () => void; onSelectRepository?: (repository: GraphRepository) => void; onColumnChange?: (name: string, settings: GraphColumnConfig) => void; onMissingAvatars?: (emails: { [email: string]: string }) => void; @@ -155,7 +155,7 @@ export function GraphWrapper({ trialBanner = true, onDismissBanner, }: GraphWrapperProps) { - const [graphList, setGraphList] = useState(rows); + const [graphRows, setGraphRows] = useState(rows); const [graphAvatars, setAvatars] = useState(avatars); const [reposList, setReposList] = useState(repositories); const [currentRepository, setCurrentRepository] = useState( @@ -182,48 +182,40 @@ export function GraphWrapper({ const [repoExpanded, setRepoExpanded] = useState(false); useEffect(() => { - if (mainRef.current === null) { - return; - } + if (mainRef.current === null) return; const setDimensionsDebounced = debounceFrame((width, height) => { setMainWidth(Math.floor(width)); setMainHeight(Math.floor(height) - graphHeaderOffset); }); - const resizeObserver = new ResizeObserver(entries => { - entries.forEach(entry => { - setDimensionsDebounced(entry.contentRect.width, entry.contentRect.height); - }); - }); + const resizeObserver = new ResizeObserver(entries => + entries.forEach(e => setDimensionsDebounced(e.contentRect.width, e.contentRect.height)), + ); resizeObserver.observe(mainRef.current); - return () => { - resizeObserver.disconnect(); - }; + return () => resizeObserver.disconnect(); }, [mainRef]); - function transformData(state: State) { - setGraphList(state.rows ?? []); + function transformData(state: State, oldState: State) { + if (!isLoading || oldState.rows !== state.rows) { + setIsLoading(state.rows == null); + } + + setGraphRows(state.rows ?? []); setAvatars(state.avatars ?? {}); setReposList(state.repositories ?? []); setCurrentRepository(reposList.find(item => item.path === state.selectedRepository)); setSelectedRows(state.selectedRows); setGraphColSettings(getGraphColSettingsModel(state.config)); setPagingState(state.paging); - setIsLoading(state.rows == null); setStyleProps(getStyleProps(state.mixedColumnColors)); setIsAllowed(state.allowed ?? false); setSubscriptionSnapshot(state.subscription); setIsPrivateRepo(state.selectedRepositoryVisibility === RepositoryVisibility.Private); } - useEffect(() => { - if (subscriber === undefined) { - return; - } - return subscriber(transformData); - }, []); + useEffect(() => subscriber?.(transformData), []); const handleSelectRepository = (item: GraphRepository) => { if (item != null && item !== currentRepository) { @@ -442,7 +434,7 @@ export function GraphWrapper({ cssVariables={styleProps.cssVariables} getExternalIcon={getIconElementLibrary} avatarUrlByEmail={graphAvatars} - graphRows={graphList} + graphRows={graphRows} height={mainHeight} isSelectedBySha={graphSelectedRows} hasMoreCommits={pagingState?.more} @@ -520,9 +512,9 @@ export function GraphWrapper({ )} - {isAllowed && graphList.length > 0 && ( + {isAllowed && graphRows.length > 0 && ( - showing {graphList.length} item{graphList.length ? 's' : ''} + showing {graphRows.length} item{graphRows.length ? 's' : ''} )} {isLoading && ( @@ -542,6 +534,9 @@ export function GraphWrapper({ +
+
+
); diff --git a/src/webviews/apps/plus/graph/graph.scss b/src/webviews/apps/plus/graph/graph.scss index 7fa67afb4cd65..ca2a9f62ebc76 100644 --- a/src/webviews/apps/plus/graph/graph.scss +++ b/src/webviews/apps/plus/graph/graph.scss @@ -409,6 +409,7 @@ a { &__footer { flex: none; + position: relative; } &__main { @@ -499,3 +500,57 @@ a { .mr-loose { margin-right: 0.5rem; } + +.progress-container { + position: absolute; + left: 0; + bottom: -2px; + z-index: 5; + height: 2px; + width: 100%; + overflow: hidden; + + & .progress-bar { + background-color: var(--vscode-progressBar-background); + display: none; + position: absolute; + left: 0; + width: 2%; + height: 2px; + } + + &.active .progress-bar { + display: inherit; + } + + &.discrete .progress-bar { + left: 0; + transition: width .1s linear; + } + + &.discrete.done .progress-bar { + width: 100%; + } + + &.infinite .progress-bar { + animation-name: progress; + animation-duration: 4s; + animation-iteration-count: infinite; + animation-timing-function: steps(100); + transform: translateZ(0); + } +} + +@keyframes progress { + 0% { + transform: translateX(0) scaleX(1); + } + + 50% { + transform: translateX(2500%) scaleX(3); + } + + to { + transform: translateX(4900%) scaleX(1); + } +} diff --git a/src/webviews/apps/plus/graph/graph.tsx b/src/webviews/apps/plus/graph/graph.tsx index d0e59bdefebcc..1acfe59b631c9 100644 --- a/src/webviews/apps/plus/graph/graph.tsx +++ b/src/webviews/apps/plus/graph/graph.tsx @@ -5,10 +5,10 @@ import { render, unmountComponentAtNode } from 'react-dom'; import type { GitGraphRowType } from 'src/git/models/graph'; import type { GraphColumnConfig } from '../../../../config'; import type { - CommitListCallback, DismissBannerParams, GraphRepository, State, + UpdateStateCallback, } from '../../../../plus/webviews/graph/protocol'; import { DidChangeAvatarsNotificationType, @@ -46,7 +46,7 @@ const graphLaneThemeColors = new Map([ ]); export class GraphApp extends App { - private callback?: CommitListCallback; + private callback?: UpdateStateCallback; constructor() { super('GraphApp'); @@ -61,7 +61,7 @@ export class GraphApp extends App { if ($root != null) { render( this.registerEvents(callback)} + subscriber={(callback: UpdateStateCallback) => this.registerEvents(callback)} onColumnChange={debounce( (name: string, settings: GraphColumnConfig) => this.onColumnChanged(name, settings), 250, @@ -96,15 +96,17 @@ export class GraphApp extends App { switch (msg.method) { case DidChangeNotificationType.method: onIpc(DidChangeNotificationType, msg, params => { + const old = this.state; this.setState({ ...this.state, ...params.state }); - this.refresh(this.state); + this.refresh(this.state, old); }); break; case DidChangeAvatarsNotificationType.method: onIpc(DidChangeAvatarsNotificationType, msg, params => { + const old = this.state; this.setState({ ...this.state, avatars: params.avatars }); - this.refresh(this.state); + this.refresh(this.state, old); }); break; @@ -172,38 +174,42 @@ export class GraphApp extends App { } } + const old = this.state; this.setState({ ...this.state, avatars: params.avatars, rows: rows, paging: params.paging, }); - this.refresh(this.state); + this.refresh(this.state, old); }); break; case DidChangeSelectionNotificationType.method: onIpc(DidChangeSelectionNotificationType, msg, params => { + const old = this.state; this.setState({ ...this.state, selectedRows: params.selection }); - this.refresh(this.state); + this.refresh(this.state, old); }); break; case DidChangeGraphConfigurationNotificationType.method: onIpc(DidChangeGraphConfigurationNotificationType, msg, params => { + const old = this.state; this.setState({ ...this.state, config: params.config }); - this.refresh(this.state); + this.refresh(this.state, old); }); break; case DidChangeSubscriptionNotificationType.method: onIpc(DidChangeSubscriptionNotificationType, msg, params => { + const old = this.state; this.setState({ ...this.state, subscription: params.subscription, allowed: params.allowed, }); - this.refresh(this.state); + this.refresh(this.state, old); }); break; @@ -213,8 +219,9 @@ export class GraphApp extends App { } protected override onThemeUpdated() { + const old = this.state; this.setState({ ...this.state, mixedColumnColors: undefined }); - this.refresh(this.state); + this.refresh(this.state, old); } protected override setState(state: State) { @@ -283,7 +290,7 @@ export class GraphApp extends App { }); } - private registerEvents(callback: CommitListCallback): () => void { + private registerEvents(callback: UpdateStateCallback): () => void { this.callback = callback; return () => { @@ -291,8 +298,8 @@ export class GraphApp extends App { }; } - private refresh(state: State) { - this.callback?.(state); + private refresh(state: State, oldState: State) { + this.callback?.(state, oldState); } }