Skip to content

Commit

Permalink
Adds vscode style progress indicator to the Graph
Browse files Browse the repository at this point in the history
Fixes progress indicators when loading new rows & other updates happen (e.g. avatars)
  • Loading branch information
eamodio committed Sep 15, 2022
1 parent 96b163c commit 433e5c1
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 41 deletions.
4 changes: 2 additions & 2 deletions src/plus/webviews/graph/protocol.ts
Expand Up @@ -59,8 +59,8 @@ export interface GraphCompositeConfig extends GraphConfig {
columns?: Record<string, GraphColumnConfig>;
}

export interface CommitListCallback {
(state: State): void;
export interface UpdateStateCallback {
(state: State, oldState: State): void;
}

// Commands
Expand Down
47 changes: 21 additions & 26 deletions src/webviews/apps/plus/graph/GraphWrapper.tsx
Expand Up @@ -13,19 +13,19 @@ 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';
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;
Expand Down Expand Up @@ -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<GraphRepository | undefined>(
Expand All @@ -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) {
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -520,9 +512,9 @@ export function GraphWrapper({
)}
</div>
</div>
{isAllowed && graphList.length > 0 && (
{isAllowed && graphRows.length > 0 && (
<span className="actionbar__details">
showing {graphList.length} item{graphList.length ? 's' : ''}
showing {graphRows.length} item{graphRows.length ? 's' : ''}
</span>
)}
{isLoading && (
Expand All @@ -542,6 +534,9 @@ export function GraphWrapper({
<span className="codicon codicon-feedback"></span>
</a>
</div>
<div className={`progress-container infinite${isLoading ? ' active' : ''}`} role="progressbar">
<div className="progress-bar"></div>
</div>
</footer>
</>
);
Expand Down
55 changes: 55 additions & 0 deletions src/webviews/apps/plus/graph/graph.scss
Expand Up @@ -409,6 +409,7 @@ a {

&__footer {
flex: none;
position: relative;
}

&__main {
Expand Down Expand Up @@ -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);
}
}
33 changes: 20 additions & 13 deletions src/webviews/apps/plus/graph/graph.tsx
Expand Up @@ -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,
Expand Down Expand Up @@ -46,7 +46,7 @@ const graphLaneThemeColors = new Map([
]);

export class GraphApp extends App<State> {
private callback?: CommitListCallback;
private callback?: UpdateStateCallback;

constructor() {
super('GraphApp');
Expand All @@ -61,7 +61,7 @@ export class GraphApp extends App<State> {
if ($root != null) {
render(
<GraphWrapper
subscriber={(callback: CommitListCallback) => this.registerEvents(callback)}
subscriber={(callback: UpdateStateCallback) => this.registerEvents(callback)}
onColumnChange={debounce(
(name: string, settings: GraphColumnConfig) => this.onColumnChanged(name, settings),
250,
Expand Down Expand Up @@ -96,15 +96,17 @@ export class GraphApp extends App<State> {
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;

Expand Down Expand Up @@ -172,38 +174,42 @@ export class GraphApp extends App<State> {
}
}

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;

Expand All @@ -213,8 +219,9 @@ export class GraphApp extends App<State> {
}

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) {
Expand Down Expand Up @@ -283,16 +290,16 @@ export class GraphApp extends App<State> {
});
}

private registerEvents(callback: CommitListCallback): () => void {
private registerEvents(callback: UpdateStateCallback): () => void {
this.callback = callback;

return () => {
this.callback = undefined;
};
}

private refresh(state: State) {
this.callback?.(state);
private refresh(state: State, oldState: State) {
this.callback?.(state, oldState);
}
}

Expand Down

0 comments on commit 433e5c1

Please sign in to comment.