Skip to content

Commit cc110f1

Browse files
committed
Adds a sidebar to Graph
1 parent 1c989b0 commit cc110f1

File tree

10 files changed

+424
-176
lines changed

10 files changed

+424
-176
lines changed

package.json

Lines changed: 173 additions & 165 deletions
Large diffs are not rendered by default.

src/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,9 @@ export interface GraphConfig {
414414
readonly showGhostRefsOnRowHover: boolean;
415415
readonly showRemoteNames: boolean;
416416
readonly showUpstreamStatus: boolean;
417+
readonly sidebar: {
418+
readonly enabled: boolean;
419+
};
417420
readonly statusBar: {
418421
readonly enabled: boolean;
419422
};

src/plus/webviews/graph/graphWebview.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ import { gate } from '../../../system/decorators/gate';
9898
import { debug, log } from '../../../system/decorators/log';
9999
import type { Deferrable } from '../../../system/function';
100100
import { debounce, disposableInterval } from '../../../system/function';
101-
import { find, last, map } from '../../../system/iterable';
101+
import { count, find, last, map } from '../../../system/iterable';
102102
import { updateRecordValue } from '../../../system/object';
103103
import {
104104
getSettledValue,
@@ -118,6 +118,7 @@ import type { ConnectionStateChangeEvent } from '../../integrations/integrationS
118118
import type {
119119
BranchState,
120120
DidChangeRefsVisibilityParams,
121+
DidGetCountParams,
121122
DidGetRowHoverParams,
122123
DidSearchParams,
123124
DoubleClickedParams,
@@ -190,6 +191,7 @@ import {
190191
DidSearchNotification,
191192
DoubleClickedCommandType,
192193
EnsureRowRequest,
194+
GetCountsRequest,
193195
GetMissingAvatarsCommand,
194196
GetMissingRefsMetadataCommand,
195197
GetMoreRowsCommand,
@@ -679,6 +681,9 @@ export class GraphWebviewProvider implements WebviewProvider<State, State, Graph
679681
case EnsureRowRequest.is(e):
680682
void this.onEnsureRowRequest(EnsureRowRequest, e);
681683
break;
684+
case GetCountsRequest.is(e):
685+
void this.onGetCounts(GetCountsRequest, e);
686+
break;
682687
case GetMissingAvatarsCommand.is(e):
683688
void this.onGetMissingAvatars(e.params);
684689
break;
@@ -720,6 +725,24 @@ export class GraphWebviewProvider implements WebviewProvider<State, State, Graph
720725
break;
721726
}
722727
}
728+
private async onGetCounts<T extends typeof GetCountsRequest>(requestType: T, msg: IpcCallMessageType<T>) {
729+
let counts: DidGetCountParams;
730+
if (this._graph != null) {
731+
const tags = await this.container.git.getTags(this._graph.repoPath);
732+
counts = {
733+
branches: count(this._graph.branches?.values(), b => !b.remote),
734+
remotes: this._graph.remotes.size,
735+
stashes: this._graph.stashes?.size,
736+
// Subtract the default worktree
737+
worktrees: this._graph.worktrees != null ? this._graph.worktrees.length - 1 : undefined,
738+
tags: tags.values.length,
739+
};
740+
} else {
741+
counts = undefined;
742+
}
743+
744+
void this.host.respond(requestType, msg, counts);
745+
}
723746

724747
updateGraphConfig(params: UpdateGraphConfigurationParams) {
725748
const config = this.getComponentConfig();
@@ -2161,6 +2184,7 @@ export class GraphWebviewProvider implements WebviewProvider<State, State, Graph
21612184
scrollMarkerTypes: this.getScrollMarkerTypes(),
21622185
showGhostRefsOnRowHover: configuration.get('graph.showGhostRefsOnRowHover'),
21632186
showRemoteNamesOnRefs: configuration.get('graph.showRemoteNames'),
2187+
sidebar: configuration.get('graph.sidebar.enabled') ?? true,
21642188
};
21652189
return config;
21662190
}

src/plus/webviews/graph/protocol.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ export interface GraphComponentConfig {
190190
scrollRowPadding?: number;
191191
showGhostRefsOnRowHover?: boolean;
192192
showRemoteNamesOnRefs?: boolean;
193+
sidebar: boolean;
193194
}
194195

195196
export interface GraphColumnConfig {
@@ -222,12 +223,6 @@ export type UpdateStateCallback = (
222223

223224
export const ChooseRepositoryCommand = new IpcCommand(scope, 'chooseRepository');
224225

225-
export interface ChooseRefParams {
226-
alt: boolean;
227-
}
228-
export type DidChooseRefParams = { name: string; sha: string } | undefined;
229-
export const ChooseRefRequest = new IpcRequest<ChooseRefParams, DidChooseRefParams>(scope, 'chooseRef');
230-
231226
export type DoubleClickedParams =
232227
| {
233228
type: 'ref';
@@ -307,6 +302,12 @@ export const UpdateSelectionCommand = new IpcCommand<UpdateSelectionParams>(scop
307302

308303
// REQUESTS
309304

305+
export interface ChooseRefParams {
306+
alt: boolean;
307+
}
308+
export type DidChooseRefParams = { name: string; sha: string } | undefined;
309+
export const ChooseRefRequest = new IpcRequest<ChooseRefParams, DidChooseRefParams>(scope, 'chooseRef');
310+
310311
export interface EnsureRowParams {
311312
id: string;
312313
select?: boolean;
@@ -317,6 +318,18 @@ export interface DidEnsureRowParams {
317318
}
318319
export const EnsureRowRequest = new IpcRequest<EnsureRowParams, DidEnsureRowParams>(scope, 'rows/ensure');
319320

321+
export interface GetCountParams {}
322+
export type DidGetCountParams =
323+
| {
324+
branches: number;
325+
remotes: number;
326+
stashes?: number;
327+
tags: number;
328+
worktrees?: number;
329+
}
330+
| undefined;
331+
export const GetCountsRequest = new IpcRequest<GetCountParams, DidGetCountParams>(scope, 'counts');
332+
320333
export type GetRowHoverParams = {
321334
type: GitGraphRowType;
322335
id: string;

src/webviews/apps/plus/graph/GraphWrapper.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ import { formatDate, fromNow } from '../../shared/date';
8585
import { GlGraphHover } from './hover/graphHover.react';
8686
import type { GraphMinimapDaySelectedEventDetail } from './minimap/minimap';
8787
import { GlGraphMinimapContainer } from './minimap/minimap-container.react';
88+
import { GlGraphSideBar } from './sidebar/sidebar.react';
8889

8990
export interface GraphWrapperProps {
9091
nonce?: string;
@@ -1588,6 +1589,14 @@ export function GraphWrapper({
15881589
></GlGraphMinimapContainer>
15891590
<GlGraphHover ref={hover as any} id="commit-hover" distance={0} skidding={15}></GlGraphHover>
15901591
<main id="main" className="graph-app__main" aria-hidden={!allowed}>
1592+
<GlGraphSideBar
1593+
enabled={graphConfig?.sidebar}
1594+
include={
1595+
repo?.isVirtual
1596+
? ['branches', 'remotes', 'tags']
1597+
: ['branches', 'remotes', 'tags', 'stashes', 'worktrees']
1598+
}
1599+
></GlGraphSideBar>
15911600
{repo !== undefined ? (
15921601
<>
15931602
<GraphContainer

src/webviews/apps/plus/graph/graph.scss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,7 @@ gl-feature-gate gl-feature-badge {
963963
flex: 1 1 auto;
964964
overflow: hidden;
965965
position: relative;
966+
display: flex;
966967
}
967968

968969
&__main.is-gated {
@@ -971,6 +972,16 @@ gl-feature-gate gl-feature-badge {
971972
}
972973
}
973974

975+
.gk-graph:not(.ref-zone) {
976+
flex: 1 1 auto;
977+
position: relative;
978+
}
979+
980+
// Add when graph ref-zone "container" changes
981+
// .gk-graph.ref-zone {
982+
// position: absolute;
983+
// }
984+
974985
.gk-graph .graph-header {
975986
& .resizable-handle.horizontal {
976987
--sash-size: 4px;

src/webviews/apps/plus/graph/graph.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ import {
5454
UpdateSelectionCommand,
5555
} from '../../../../plus/webviews/graph/protocol';
5656
import { Color, getCssVariable, mix, opacity } from '../../../../system/color';
57-
import { debug } from '../../../../system/decorators/log';
57+
import { debug, log } from '../../../../system/decorators/log';
5858
import { debounce } from '../../../../system/function';
5959
import { getLogScope, setLogScopeExit } from '../../../../system/logger.scope';
6060
import type { IpcMessage, IpcNotification } from '../../../protocol';
@@ -84,12 +84,11 @@ export class GraphApp extends App<State> {
8484
super('GraphApp');
8585
}
8686

87+
@log()
8788
protected override onBind() {
8889
const disposables = super.onBind?.() ?? [];
8990
// disposables.push(DOM.on(window, 'keyup', e => this.onKeyUp(e)));
9091

91-
this.log(`onBind()`);
92-
9392
this.ensureTheming(this.state);
9493

9594
const $root = document.getElementById('root');
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { reactWrapper } from '../../../shared/components/helpers/react-wrapper';
2+
import { GlGraphSideBar as GlGraphSideBarWC } from './sidebar';
3+
4+
export interface GlGraphSideBar extends GlGraphSideBarWC {}
5+
export const GlGraphSideBar = reactWrapper(GlGraphSideBarWC, { tagName: 'gl-graph-sidebar' });
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { consume } from '@lit/context';
2+
import { Task } from '@lit/task';
3+
import { css, html, LitElement, nothing } from 'lit';
4+
import { customElement, property } from 'lit/decorators.js';
5+
import { repeat } from 'lit/directives/repeat.js';
6+
import { DidChangeNotification, GetCountsRequest } from '../../../../../plus/webviews/graph/protocol';
7+
import { ipcContext } from '../../../shared/context';
8+
import type { Disposable } from '../../../shared/events';
9+
import type { HostIpc } from '../../../shared/ipc';
10+
import '../../../shared/components/code-icon';
11+
import '../../../shared/components/overlays/tooltip';
12+
13+
interface Icon {
14+
type: IconTypes;
15+
icon: string;
16+
command: string;
17+
tooltip: string;
18+
}
19+
type IconTypes = 'branches' | 'remotes' | 'stashes' | 'tags' | 'worktrees';
20+
const icons: Icon[] = [
21+
{ type: 'branches', icon: 'gl-branches-view', command: 'gitlens.showBranchesView', tooltip: 'Branches' },
22+
{ type: 'remotes', icon: 'gl-remotes-view', command: 'gitlens.showRemotesView', tooltip: 'Remotes' },
23+
{ type: 'stashes', icon: 'gl-stashes-view', command: 'gitlens.showStashesView', tooltip: 'Stashes' },
24+
{ type: 'tags', icon: 'gl-tags-view', command: 'gitlens.showTagsView', tooltip: 'Tags' },
25+
{ type: 'worktrees', icon: 'gl-worktrees-view', command: 'gitlens.showWorktreesView', tooltip: 'Worktrees' },
26+
];
27+
28+
type Counts = Record<IconTypes, number | undefined>;
29+
30+
@customElement('gl-graph-sidebar')
31+
export class GlGraphSideBar extends LitElement {
32+
static override styles = css`
33+
.sidebar {
34+
display: flex;
35+
flex-direction: column;
36+
align-items: center;
37+
gap: 1.4rem;
38+
background-color: var(--color-graph-background);
39+
color: var(--titlebar-fg);
40+
width: 2.6rem;
41+
font-size: 9px;
42+
font-weight: 600;
43+
height: 100vh;
44+
padding: 3rem 0;
45+
z-index: 1040;
46+
}
47+
48+
.item {
49+
color: inherit;
50+
text-decoration: none;
51+
display: flex;
52+
flex-direction: column;
53+
align-items: center;
54+
cursor: pointer;
55+
}
56+
57+
.item:hover {
58+
color: var(--color-foreground);
59+
text-decoration: none;
60+
}
61+
62+
.count {
63+
color: var(--color-foreground--50);
64+
/* color: var(--color-highlight); */
65+
margin-top: 0.4rem;
66+
}
67+
68+
.count.error {
69+
color: var(--vscode-errorForeground);
70+
opacity: 0.6;
71+
}
72+
`;
73+
74+
@property({ type: Boolean })
75+
enabled = true;
76+
77+
@property({ type: Array })
78+
include?: IconTypes[];
79+
80+
@consume({ context: ipcContext })
81+
private _ipc!: HostIpc;
82+
private _disposable: Disposable | undefined;
83+
private _countsTask = new Task(this, {
84+
args: () => [this.fetchCounts()],
85+
task: ([counts]) => counts,
86+
autoRun: false,
87+
});
88+
89+
override connectedCallback() {
90+
super.connectedCallback();
91+
92+
this._disposable = this._ipc.onReceiveMessage(msg => {
93+
switch (true) {
94+
case DidChangeNotification.is(msg):
95+
this._counts = undefined;
96+
this.requestUpdate();
97+
break;
98+
99+
case GetCountsRequest.is(msg):
100+
this._counts = Promise.resolve(msg.params as Counts);
101+
this.requestUpdate();
102+
break;
103+
}
104+
});
105+
}
106+
107+
override disconnectedCallback() {
108+
super.disconnectedCallback();
109+
110+
this._disposable?.dispose();
111+
}
112+
113+
private _counts: Promise<Counts | undefined> | undefined;
114+
private async fetchCounts() {
115+
if (this._counts == null) {
116+
const ipc = this._ipc;
117+
if (ipc != null) {
118+
async function fetch() {
119+
const rsp = await ipc.sendRequest(GetCountsRequest, {});
120+
return rsp as Counts;
121+
}
122+
this._counts = fetch();
123+
} else {
124+
this._counts = Promise.resolve(undefined);
125+
}
126+
}
127+
return this._counts;
128+
}
129+
130+
override render() {
131+
if (!this.enabled) return nothing;
132+
133+
if (this._counts == null) {
134+
void this._countsTask.run();
135+
}
136+
137+
return html`<section class="sidebar">
138+
${repeat(
139+
icons,
140+
i => i,
141+
i => this.renderIcon(i),
142+
)}
143+
</section>`;
144+
}
145+
146+
private renderIcon(icon: Icon) {
147+
if (this.include != null && !this.include.includes(icon.type)) return;
148+
149+
return html`<gl-tooltip placement="right" content="${icon.tooltip}">
150+
<a class="item" href="command:${icon.command}">
151+
<code-icon icon="${icon.icon}"></code-icon>
152+
${this._countsTask.render({
153+
pending: () =>
154+
html`<span class="count"
155+
><code-icon icon="loading" modifier="spin" size="9"></code-icon
156+
></span>`,
157+
complete: c => renderCount(c?.[icon.type]),
158+
error: () => html`<span class="count error"><code-icon icon="warning" size="9"></code-icon></span>`,
159+
})}
160+
</a>
161+
</gl-tooltip>`;
162+
}
163+
}
164+
165+
function renderCount(count: number | undefined) {
166+
if (count == null) return nothing;
167+
168+
return html`<span class="count">${count > 999 ? '1K+' : String(count)}</span>`;
169+
}

yarn.lock

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,13 +517,20 @@
517517
resolved "https://registry.yarnpkg.com/@lit/react/-/react-1.0.5.tgz#9c53a8d719f91ef7edca0bdd68f5589ea579ffc1"
518518
integrity sha512-RSHhrcuSMa4vzhqiTenzXvtQ6QDq3hSPsnHHO3jaPmmvVFeoNNm4DHoQ0zLdKAUvY3wP3tTENSUf7xpyVfrDEA==
519519

520-
"@lit/reactive-element@^1.6.2 || ^2.0.0", "@lit/reactive-element@^2.0.4":
520+
"@lit/reactive-element@^1.0.0 || ^2.0.0", "@lit/reactive-element@^1.6.2 || ^2.0.0", "@lit/reactive-element@^2.0.4":
521521
version "2.0.4"
522522
resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.0.4.tgz#8f2ed950a848016383894a26180ff06c56ae001b"
523523
integrity sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==
524524
dependencies:
525525
"@lit-labs/ssr-dom-shim" "^1.2.0"
526526

527+
528+
version "1.0.1"
529+
resolved "https://registry.yarnpkg.com/@lit/task/-/task-1.0.1.tgz#7462aeaa973766822567f5ca90fe157404e8eb81"
530+
integrity sha512-fVLDtmwCau8NywnFIXaJxsCZjzaIxnVq+cFRKYC1Y4tA4/0rMTvF6DLZZ2JE51BwzOluaKtgJX8x1QDsQtAaIw==
531+
dependencies:
532+
"@lit/reactive-element" "^1.0.0 || ^2.0.0"
533+
527534
"@microsoft/[email protected]", "@microsoft/fast-element@^1.12.0", "@microsoft/fast-element@^1.13.0":
528535
version "1.13.0"
529536
resolved "https://registry.yarnpkg.com/@microsoft/fast-element/-/fast-element-1.13.0.tgz#d390ff13697064a48dc6ad6bb332a5f5489f73f8"

0 commit comments

Comments
 (0)