Browse Source

Defers Graph stats loading for Changes/minimap

Improves the Graph minimap
  - Adds `gitlens.graph.minimap.dataType` setting to choose between commits or lines changed to be used for the minimap
  - Adds a configuration popover for the minimap
  - Adds a legend to the minimap
  - Removes minimap from being experimental
    - Renames `gitlens.graph.experimental.minimap.enabled` to `gitlens.graph.minimap.enabled`
    - Renames `gitlens.graph.experimental.minimap.additionalTypes` to `gitlens.graph.minimap.additionalTypes`
main
Eric Amodio 1 year ago
parent
commit
0439d723e2
16 changed files with 649 additions and 222 deletions
  1. +28
    -13
      package.json
  2. +7
    -25
      src/config.ts
  3. +60
    -4
      src/env/node/git/localGitProvider.ts
  4. +5
    -1
      src/git/models/graph.ts
  5. +16
    -4
      src/git/parsers/logParser.ts
  6. +15
    -11
      src/plus/github/githubGitProvider.ts
  7. +76
    -36
      src/plus/webviews/graph/graphWebview.ts
  8. +38
    -25
      src/plus/webviews/graph/protocol.ts
  9. +210
    -63
      src/webviews/apps/plus/graph/GraphWrapper.tsx
  10. +39
    -16
      src/webviews/apps/plus/graph/graph.scss
  11. +13
    -0
      src/webviews/apps/plus/graph/graph.tsx
  12. +63
    -17
      src/webviews/apps/plus/graph/minimap/minimap.ts
  13. +1
    -0
      src/webviews/apps/shared/components/menu/menu-label.ts
  14. +41
    -2
      src/webviews/apps/shared/components/overlays/pop-menu/pop-menu.ts
  15. +33
    -1
      src/webviews/apps/shared/components/search/search-input.ts
  16. +4
    -4
      yarn.lock

+ 28
- 13
package.json View File

@ -2189,14 +2189,14 @@
"enum": [
"localBranches",
"remoteBranches",
"tags",
"stashes"
"stashes",
"tags"
],
"enumDescriptions": [
"Marks the location of local branches",
"Marks the location of remote branches",
"Marks the location of tags",
"Marks the location of stashes"
"Marks the location of stashes",
"Marks the location of tags"
]
},
"minItems": 0,
@ -2349,14 +2349,29 @@
"scope": "window",
"order": 99
},
"gitlens.graph.experimental.minimap.enabled": {
"gitlens.graph.minimap.enabled": {
"type": "boolean",
"default": false,
"markdownDescription": "Specifies whether to show an experimental minimap of commit activity above the _Commit Graph_",
"markdownDescription": "Specifies whether to show a minimap of commit activity above the _Commit Graph_",
"scope": "window",
"order": 100
},
"gitlens.graph.experimental.minimap.additionalTypes": {
"gitlens.graph.minimap.dataType": {
"type": "string",
"default": "commits",
"enum": [
"commits",
"lines"
],
"enumDescriptions": [
"Shows the number of commits per day in the minimap",
"Shows the number of lines changed per day in the minimap"
],
"markdownDescription": "Specifies the data to show on the minimap in the _Commit Graph_",
"scope": "window",
"order": 101
},
"gitlens.graph.minimap.additionalTypes": {
"type": "array",
"default": [
"localBranches",
@ -2367,14 +2382,14 @@
"enum": [
"localBranches",
"remoteBranches",
"tags",
"stashes"
"stashes",
"tags"
],
"enumDescriptions": [
"Marks the location of local branches",
"Marks the location of remote branches",
"Marks the location of tags",
"Marks the location of stashes"
"Marks the location of stashes",
"Marks the location of tags"
]
},
"minItems": 0,
@ -2382,7 +2397,7 @@
"uniqueItems": true,
"markdownDescription": "Specifies additional markers to show on the minimap in the _Commit Graph_",
"scope": "window",
"order": 101
"order": 102
}
}
},
@ -14041,7 +14056,7 @@
"vscode:prepublish": "yarn run bundle"
},
"dependencies": {
"@gitkraken/gitkraken-components": "9.1.5",
"@gitkraken/gitkraken-components": "10.0.0",
"@microsoft/fast-element": "1.12.0",
"@microsoft/fast-react-wrapper": "0.3.18",
"@octokit/core": "4.2.1",

+ 7
- 25
src/config.ts View File

@ -305,25 +305,8 @@ export const enum GitCommandSorting {
Usage = 'usage',
}
export const enum GraphScrollMarkerTypes {
Selection = 'selection',
Head = 'head',
LocalBranches = 'localBranches',
RemoteBranches = 'remoteBranches',
Highlights = 'highlights',
Stashes = 'stashes',
Tags = 'tags',
}
export const enum GraphMinimapTypes {
Selection = 'selection',
Head = 'head',
LocalBranches = 'localBranches',
RemoteBranches = 'remoteBranches',
Highlights = 'highlights',
Stashes = 'stashes',
Tags = 'tags',
}
export type GraphScrollMarkersAdditionalTypes = 'localBranches' | 'remoteBranches' | 'stashes' | 'tags';
export type GraphMinimapMarkersAdditionalTypes = 'localBranches' | 'remoteBranches' | 'stashes' | 'tags';
export const enum GravatarDefaultStyle {
Faces = 'wavatar',
@ -429,11 +412,10 @@ export interface GraphConfig {
dateStyle: DateStyle | null;
defaultItemLimit: number;
dimMergeCommits: boolean;
experimental: {
minimap: {
enabled: boolean;
additionalTypes: GraphMinimapTypes[];
};
minimap: {
enabled: boolean;
dataType: 'commits' | 'lines';
additionalTypes: GraphMinimapMarkersAdditionalTypes[];
};
highlightRowsOnRefHover: boolean;
layout: 'editor' | 'panel';
@ -442,7 +424,7 @@ export interface GraphConfig {
showGhostRefsOnRowHover: boolean;
scrollMarkers: {
enabled: boolean;
additionalTypes: GraphScrollMarkerTypes[];
additionalTypes: GraphScrollMarkersAdditionalTypes[];
};
pullRequests: {
enabled: boolean;

+ 60
- 4
src/env/node/git/localGitProvider.ts View File

@ -69,6 +69,8 @@ import type {
GitGraphRowContexts,
GitGraphRowHead,
GitGraphRowRemoteHead,
GitGraphRowsStats,
GitGraphRowStats,
GitGraphRowTag,
} from '../../../git/models/graph';
import { GitGraphRowType } from '../../../git/models/graph';
@ -109,6 +111,7 @@ import {
createLogParserWithFiles,
getContributorsParser,
getGraphParser,
getGraphStatsParser,
getRefAndDateParser,
getRefParser,
GitLogParser,
@ -1723,13 +1726,16 @@ export class LocalGitProvider implements GitProvider, Disposable {
ref?: string;
},
): Promise<GitGraph> {
const parser = getGraphParser(options?.include?.stats);
const refParser = getRefParser();
const defaultLimit = options?.limit ?? configuration.get('graph.defaultItemLimit') ?? 5000;
const defaultPageLimit = configuration.get('graph.pageItemLimit') ?? 1000;
const ordering = configuration.get('graph.commitOrdering', undefined, 'date');
const deferStats = options?.include?.stats; // && defaultLimit > 1000;
const parser = getGraphParser(options?.include?.stats && !deferStats);
const refParser = getRefParser();
const statsParser = getGraphStatsParser();
const [refResult, stashResult, branchesResult, remotesResult, currentUserResult] = await Promise.allSettled([
this.git.log2(repoPath, undefined, ...refParser.arguments, '-n1', options?.ref ?? 'HEAD'),
this.getStash(repoPath),
@ -1769,6 +1775,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
const remappedIds = new Map<string, string>();
let total = 0;
let iterations = 0;
let pendingRowsStatsCount = 0;
async function getCommitsForGraphCore(
this: LocalGitProvider,
@ -1776,6 +1783,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
sha?: string,
cursor?: { sha: string; skip: number },
): Promise<GitGraph> {
const startTotal = total;
iterations++;
let log: string | string[] | undefined;
@ -1880,6 +1889,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
let remoteBranchId: string;
let remoteName: string;
let stashCommit: GitStashCommit | undefined;
let stats: GitGraphRowsStats | undefined;
let tagId: string;
let tagName: string;
let tip: string;
@ -2152,8 +2162,14 @@ export class LocalGitProvider implements GitProvider, Disposable {
remotes: refRemoteHeads,
tags: refTags,
contexts: contexts,
stats: commit.stats,
});
if (commit.stats != null) {
if (stats == null) {
stats = new Map<string, GitGraphRowStats>();
}
stats.set(commit.sha, commit.stats);
}
}
const startingCursor = cursor?.sha;
@ -2166,6 +2182,44 @@ export class LocalGitProvider implements GitProvider, Disposable {
}
: undefined;
let rowsStatsDeferred: GitGraph['rowsStatsDeferred'];
if (deferStats) {
if (stats == null) {
stats = new Map<string, GitGraphRowStats>();
}
pendingRowsStatsCount++;
// eslint-disable-next-line no-async-promise-executor
const promise = new Promise<void>(async resolve => {
try {
const args = [...statsParser.arguments];
if (startTotal === 0) {
args.push(`-n${total}`);
} else {
args.push(`-n${total - startTotal}`, `--skip=${startTotal}`);
}
args.push(`--${ordering}-order`, '--all');
const statsData = await this.git.log2(repoPath, stdin ? { stdin: stdin } : undefined, ...args);
if (statsData) {
const commitStats = statsParser.parse(statsData);
for (const stat of commitStats) {
stats!.set(stat.sha, stat.stats);
}
}
} finally {
pendingRowsStatsCount--;
resolve();
}
});
rowsStatsDeferred = {
isLoaded: () => pendingRowsStatsCount === 0,
promise: promise,
};
}
return {
repoPath: repoPath,
avatars: avatars,
@ -2177,6 +2231,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
downstreams: downstreamMap,
rows: rows,
id: sha,
rowsStats: stats,
rowsStatsDeferred: rowsStatsDeferred,
paging: {
limit: limit === 0 ? count : limit,

+ 5
- 1
src/git/models/graph.ts View File

@ -22,7 +22,6 @@ export interface GitGraphRow extends GraphRow {
remotes?: GitGraphRowRemoteHead[];
tags?: GitGraphRowTag[];
contexts?: GitGraphRowContexts;
stats?: GitGraphRowStats;
}
export interface GitGraph {
@ -43,6 +42,9 @@ export interface GitGraph {
readonly rows: GitGraphRow[];
readonly id?: string;
readonly rowsStats?: GitGraphRowsStats;
readonly rowsStatsDeferred?: { isLoaded: () => boolean; promise: Promise<void> };
readonly paging?: {
readonly limit: number | undefined;
readonly startingCursor: string | undefined;
@ -51,3 +53,5 @@ export interface GitGraph {
more?(limit: number, id?: string): Promise<GitGraph | undefined>;
}
export type GitGraphRowsStats = Map<string, GitGraphRowStats>;

+ 16
- 4
src/git/parsers/logParser.ts View File

@ -79,10 +79,13 @@ export type ParsedEntryWithFiles = { [K in keyof T]: string } & { files: Pars
export type ParserWithFiles<T> = Parser<ParsedEntryWithFiles<T>>;
export type ParsedStats = { files: number; additions: number; deletions: number };
export type ParsedEntryWithStats<T> = T & { stats?: ParsedStats };
export type ParsedEntryWithMaybeStats<T> = T & { stats?: ParsedStats };
export type ParserWithMaybeStats<T> = Parser<ParsedEntryWithMaybeStats<T>>;
export type ParsedEntryWithStats<T> = T & { stats: ParsedStats };
export type ParserWithStats<T> = Parser<ParsedEntryWithStats<T>>;
type ContributorsParserMaybeWithStats = ParserWithStats<{
type ContributorsParserMaybeWithStats = ParserWithMaybeStats<{
sha: string;
author: string;
email: string;
@ -115,7 +118,7 @@ export function getContributorsParser(stats?: boolean): ContributorsParserMaybeW
return _contributorsParser;
}
type GraphParserMaybeWithStats = ParserWithStats<{
type GraphParserMaybeWithStats = ParserWithMaybeStats<{
sha: string;
author: string;
authorEmail: string;
@ -161,6 +164,15 @@ export function getGraphParser(stats?: boolean): GraphParserMaybeWithStats {
return _graphParser;
}
let _graphStatsParser: ParserWithStats<{ sha: string }> | undefined;
export function getGraphStatsParser(): ParserWithStats<{ sha: string }> {
if (_graphStatsParser == null) {
_graphStatsParser = createLogParserWithStats({ sha: '%H' });
}
return _graphStatsParser;
}
type RefParser = Parser<string>;
let _refParser: RefParser | undefined;
@ -327,7 +339,7 @@ export function createLogParserWithFiles>(
export function createLogParserWithStats<T extends Record<string, unknown>>(
fieldMapping: ExtractAll<T, string>,
): ParserWithStats<T> {
function parseStats(fields: IterableIterator<string>, entry: ParsedEntryWithStats<T>) {
function parseStats(fields: IterableIterator<string>, entry: ParsedEntryWithMaybeStats<T>) {
const stats = fields.next().value;
const match = shortstatRegex.exec(stats);
if (match?.groups != null) {

+ 15
- 11
src/plus/github/githubGitProvider.ts View File

@ -39,7 +39,7 @@ import { GitUri } from '../../git/gitUri';
import type { GitBlame, GitBlameAuthor, GitBlameLine, GitBlameLines } from '../../git/models/blame';
import type { BranchSortOptions } from '../../git/models/branch';
import { getBranchId, getBranchNameWithoutRemote, GitBranch, sortBranches } from '../../git/models/branch';
import type { GitCommitLine, GitCommitStats } from '../../git/models/commit';
import type { GitCommitLine } from '../../git/models/commit';
import { getChangedFilesCount, GitCommit, GitCommitIdentity } from '../../git/models/commit';
import { deletedOrMissing, uncommitted } from '../../git/models/constants';
import { GitContributor } from '../../git/models/contributor';
@ -52,6 +52,8 @@ import type {
GitGraphRowContexts,
GitGraphRowHead,
GitGraphRowRemoteHead,
GitGraphRowsStats,
GitGraphRowStats,
GitGraphRowTag,
} from '../../git/models/graph';
import { GitGraphRowType } from '../../git/models/graph';
@ -1256,7 +1258,7 @@ export class GitHubGitProvider implements GitProvider, Disposable {
let refRemoteHeads: GitGraphRowRemoteHead[];
let refTags: GitGraphRowTag[];
let remoteBranchId: string;
let stats: GitCommitStats | undefined;
let stats: GitGraphRowsStats | undefined;
let tagId: string;
const headRefUpstreamName = headBranch.upstream?.name;
@ -1442,7 +1444,6 @@ export class GitHubGitProvider implements GitProvider, Disposable {
}),
};
stats = commit.stats;
rows.push({
sha: commit.sha,
parents: commit.parents,
@ -1456,15 +1457,18 @@ export class GitHubGitProvider implements GitProvider, Disposable {
remotes: refRemoteHeads,
tags: refTags,
contexts: contexts,
stats:
stats != null
? {
files: getChangedFilesCount(stats.changedFiles),
additions: stats.additions,
deletions: stats.deletions,
}
: undefined,
});
if (commit.stats != null) {
if (stats == null) {
stats = new Map<string, GitGraphRowStats>();
}
stats.set(commit.sha, {
files: getChangedFilesCount(commit.stats.changedFiles),
additions: commit.stats.additions,
deletions: commit.stats.deletions,
});
}
}
if (options?.ref === 'HEAD') {

+ 76
- 36
src/plus/webviews/graph/graphWebview.ts View File

@ -11,7 +11,7 @@ import type {
ShowCommitsInViewCommandArgs,
} from '../../../commands';
import { parseCommandContext } from '../../../commands/base';
import type { Config } from '../../../config';
import type { Config, GraphMinimapMarkersAdditionalTypes } from '../../../config';
import type { StoredGraphFilters, StoredGraphIncludeOnlyRef, StoredGraphRefType } from '../../../constants';
import { Commands, GlyphChars } from '../../../constants';
import type { Container } from '../../../container';
@ -115,12 +115,14 @@ import type {
GraphItemRefGroupContext,
GraphItemTypedContext,
GraphItemTypedContextValue,
GraphMinimapMarkerTypes,
GraphMissingRefsMetadataType,
GraphPullRequestContextValue,
GraphPullRequestMetadata,
GraphRefMetadata,
GraphRefMetadataType,
GraphRepository,
GraphScrollMarkerTypes,
GraphSelectedRows,
GraphStashContextValue,
GraphTagContextValue,
@ -145,6 +147,7 @@ import {
DidChangeRefsMetadataNotificationType,
DidChangeRefsVisibilityNotificationType,
DidChangeRowsNotificationType,
DidChangeRowsStatsNotificationType,
DidChangeSelectionNotificationType,
DidChangeSubscriptionNotificationType,
DidChangeWindowFocusNotificationType,
@ -159,9 +162,7 @@ import {
GetMissingAvatarsCommandType,
GetMissingRefsMetadataCommandType,
GetMoreRowsCommandType,
GraphMinimapMarkerTypes,
GraphRefMetadataTypes,
GraphScrollMarkerTypes,
SearchCommandType,
SearchOpenInViewCommandType,
supportedRefMetadataTypes,
@ -514,8 +515,28 @@ export class GraphWebviewProvider implements WebviewProvider {
if (config[key] !== params.changes[key]) {
switch (key) {
case 'minimap':
void configuration.updateEffective('graph.experimental.minimap.enabled', params.changes[key]);
void configuration.updateEffective('graph.minimap.enabled', params.changes[key]);
break;
case 'minimapDataType':
void configuration.updateEffective('graph.minimap.dataType', params.changes[key]);
break;
case 'minimapMarkerTypes': {
const additionalTypes: GraphMinimapMarkersAdditionalTypes[] = [];
const markers = params.changes[key] ?? [];
for (const marker of markers) {
switch (marker) {
case 'localBranches':
case 'remoteBranches':
case 'stashes':
case 'tags':
additionalTypes.push(marker);
break;
}
}
void configuration.updateEffective('graph.minimap.additionalTypes', additionalTypes);
break;
}
default:
// TODO:@eamodio add more config options as needed
debugger;
@ -582,14 +603,17 @@ export class GraphWebviewProvider implements WebviewProvider {
configuration.changed(e, 'graph.pullRequests.enabled') ||
configuration.changed(e, 'graph.showRemoteNames') ||
configuration.changed(e, 'graph.showUpstreamStatus') ||
configuration.changed(e, 'graph.experimental.minimap.enabled') ||
configuration.changed(e, 'graph.experimental.minimap.additionalTypes')
configuration.changed(e, 'graph.minimap.enabled') ||
configuration.changed(e, 'graph.minimap.dataType') ||
configuration.changed(e, 'graph.minimap.additionalTypes')
) {
void this.notifyDidChangeConfiguration();
if (
configuration.changed(e, 'graph.experimental.minimap.enabled') &&
configuration.get('graph.experimental.minimap.enabled') &&
(configuration.changed(e, 'graph.minimap.enabled') ||
configuration.changed(e, 'graph.minimap.dataType')) &&
configuration.get('graph.minimap.enabled') &&
configuration.get('graph.minimap.dataType') === 'lines' &&
!this._graph?.includes?.stats
) {
this.updateState();
@ -1279,18 +1303,21 @@ export class GraphWebviewProvider implements WebviewProvider {
private async notifyDidChangeRows(sendSelectedRows: boolean = false, completionId?: string) {
if (this._graph == null) return;
const data = this._graph;
const graph = this._graph;
return this.host.notify(
DidChangeRowsNotificationType,
{
rows: data.rows,
downstreams: Object.fromEntries(data.downstreams),
avatars: Object.fromEntries(data.avatars),
rows: graph.rows,
avatars: Object.fromEntries(graph.avatars),
downstreams: Object.fromEntries(graph.downstreams),
refsMetadata: this._refsMetadata != null ? Object.fromEntries(this._refsMetadata) : this._refsMetadata,
rowsStats: graph.rowsStats?.size ? Object.fromEntries(graph.rowsStats) : undefined,
rowsStatsLoading:
graph.rowsStatsDeferred?.isLoaded != null ? !graph.rowsStatsDeferred.isLoaded() : false,
selectedRows: sendSelectedRows ? this._selectedRows : undefined,
paging: {
startingCursor: data.paging?.startingCursor,
hasMore: data.paging?.hasMore ?? false,
startingCursor: graph.paging?.startingCursor,
hasMore: graph.paging?.hasMore ?? false,
},
},
completionId,
@ -1298,6 +1325,16 @@ export class GraphWebviewProvider implements WebviewProvider {
}
@debug()
private async notifyDidChangeRowsStats(graph: GitGraph) {
if (graph.rowsStats == null) return;
return this.host.notify(DidChangeRowsStatsNotificationType, {
rowsStats: Object.fromEntries(graph.rowsStats),
rowsStatsLoading: graph.rowsStatsDeferred?.isLoaded != null ? !graph.rowsStatsDeferred.isLoaded() : false,
});
}
@debug()
private async notifyDidChangeWorkingTree() {
if (!this.host.ready || !this.host.visible) {
this.host.addPendingIpcNotification(DidChangeWorkingTreeNotificationType, this._ipcNotificationMap, this);
@ -1608,10 +1645,11 @@ export class GraphWebviewProvider implements WebviewProvider {
dimMergeCommits: configuration.get('graph.dimMergeCommits'),
enableMultiSelection: false,
highlightRowsOnRefHover: configuration.get('graph.highlightRowsOnRefHover'),
minimap: configuration.get('graph.experimental.minimap.enabled'),
enabledMinimapMarkerTypes: this.getEnabledGraphMinimapMarkers(),
minimap: configuration.get('graph.minimap.enabled'),
minimapDataType: configuration.get('graph.minimap.dataType'),
minimapMarkerTypes: this.getMinimapMarkerTypes(),
scrollRowPadding: configuration.get('graph.scrollRowPadding'),
enabledScrollMarkerTypes: this.getEnabledGraphScrollMarkers(),
scrollMarkerTypes: this.getScrollMarkerTypes(),
showGhostRefsOnRowHover: configuration.get('graph.showGhostRefsOnRowHover'),
showRemoteNamesOnRefs: configuration.get('graph.showRemoteNames'),
idLength: configuration.get('advanced.abbreviatedShaLength'),
@ -1619,33 +1657,29 @@ export class GraphWebviewProvider implements WebviewProvider {
return config;
}
private getEnabledGraphScrollMarkers(): GraphScrollMarkerTypes[] {
const markersEnabled = configuration.get('graph.scrollMarkers.enabled');
if (!markersEnabled) return [];
private getScrollMarkerTypes(): GraphScrollMarkerTypes[] {
if (!configuration.get('graph.scrollMarkers.enabled')) return [];
const markers: GraphScrollMarkerTypes[] = [
GraphScrollMarkerTypes.Selection,
GraphScrollMarkerTypes.Highlights,
GraphScrollMarkerTypes.Head,
GraphScrollMarkerTypes.Upstream,
...(configuration.get('graph.scrollMarkers.additionalTypes') as unknown as GraphScrollMarkerTypes[]),
'selection',
'highlights',
'head',
'upstream',
...configuration.get('graph.scrollMarkers.additionalTypes'),
];
return markers;
}
private getEnabledGraphMinimapMarkers(): GraphMinimapMarkerTypes[] {
const markersEnabled = configuration.get('graph.experimental.minimap.enabled');
if (!markersEnabled) return [];
private getMinimapMarkerTypes(): GraphMinimapMarkerTypes[] {
if (!configuration.get('graph.minimap.enabled')) return [];
const markers: GraphMinimapMarkerTypes[] = [
GraphMinimapMarkerTypes.Selection,
GraphMinimapMarkerTypes.Highlights,
GraphMinimapMarkerTypes.Head,
GraphMinimapMarkerTypes.Upstream,
...(configuration.get(
'graph.experimental.minimap.additionalTypes',
) as unknown as GraphMinimapMarkerTypes[]),
'selection',
'highlights',
'head',
'upstream',
...configuration.get('graph.minimap.additionalTypes'),
];
return markers;
@ -1747,7 +1781,10 @@ export class GraphWebviewProvider implements WebviewProvider {
uri => this.host.asWebviewUri(uri),
{
include: {
stats: configuration.get('graph.experimental.minimap.enabled') || !columnSettings.changes.isHidden,
stats:
(configuration.get('graph.minimap.enabled') &&
configuration.get('graph.minimap.dataType') === 'lines') ||
!columnSettings.changes.isHidden,
},
limit: limit,
ref: ref,
@ -1817,6 +1854,7 @@ export class GraphWebviewProvider implements WebviewProvider {
avatars: data != null ? Object.fromEntries(data.avatars) : undefined,
refsMetadata: this.resetRefsMetadata() === null ? null : {},
loading: deferRows,
rowsStatsLoading: data?.rowsStatsDeferred?.isLoaded != null ? !data.rowsStatsDeferred.isLoaded() : false,
rows: data?.rows,
downstreams: data != null ? Object.fromEntries(data.downstreams) : undefined,
paging:
@ -1943,6 +1981,8 @@ export class GraphWebviewProvider implements WebviewProvider {
if (graph == null) {
this.resetRefsMetadata();
this.resetSearchState();
} else {
void graph.rowsStatsDeferred?.promise.then(() => void this.notifyDidChangeRowsStats(graph));
}
}

+ 38
- 25
src/plus/webviews/graph/protocol.ts View File

@ -16,11 +16,12 @@ import type {
RefMetadataItem,
RefMetadataType,
Remote,
RowStats,
Tag,
UpstreamMetadata,
WorkDirStats,
} from '@gitkraken/gitkraken-components';
import type { DateStyle } from '../../../config';
import type { Config, DateStyle } from '../../../config';
import type { RepositoryVisibility } from '../../../git/gitProvider';
import type { GitTrackingState } from '../../../git/models/branch';
import type { GitGraphRowType } from '../../../git/models/graph';
@ -59,27 +60,25 @@ export enum GraphRefMetadataTypes {
PullRequest = 'pullRequest',
}
export const enum GraphScrollMarkerTypes {
Selection = 'selection',
Head = 'head',
Highlights = 'highlights',
LocalBranches = 'localBranches',
RemoteBranches = 'remoteBranches',
Stashes = 'stashes',
Tags = 'tags',
Upstream = 'upstream',
}
export const enum GraphMinimapMarkerTypes {
Selection = 'selection',
Head = 'head',
Highlights = 'highlights',
LocalBranches = 'localBranches',
RemoteBranches = 'remoteBranches',
Stashes = 'stashes',
Tags = 'tags',
Upstream = 'upstream',
}
export type GraphScrollMarkerTypes =
| 'selection'
| 'head'
| 'highlights'
| 'localBranches'
| 'remoteBranches'
| 'stashes'
| 'tags'
| 'upstream';
export type GraphMinimapMarkerTypes =
| 'selection'
| 'head'
| 'highlights'
| 'localBranches'
| 'remoteBranches'
| 'stashes'
| 'tags'
| 'upstream';
export const supportedRefMetadataTypes: GraphRefMetadataType[] = Object.values(GraphRefMetadataTypes);
@ -100,6 +99,8 @@ export interface State {
loading?: boolean;
refsMetadata?: GraphRefsMetadata | null;
rows?: GraphRow[];
rowsStats?: Record<string, GraphRowStats>;
rowsStatsLoading?: boolean;
downstreams?: GraphDownstreams;
paging?: GraphPaging;
columns?: GraphColumnsSettings;
@ -176,9 +177,10 @@ export interface GraphComponentConfig {
enableMultiSelection?: boolean;
highlightRowsOnRefHover?: boolean;
minimap?: boolean;
enabledMinimapMarkerTypes?: GraphMinimapMarkerTypes[];
minimapDataType?: Config['graph']['minimap']['dataType'];
minimapMarkerTypes?: GraphMinimapMarkerTypes[];
scrollMarkerTypes?: GraphScrollMarkerTypes[];
scrollRowPadding?: number;
enabledScrollMarkerTypes?: GraphScrollMarkerTypes[];
showGhostRefsOnRowHover?: boolean;
showRemoteNamesOnRefs?: boolean;
idLength?: number;
@ -200,6 +202,7 @@ export type GraphIncludeOnlyRefs = IncludeOnlyRefsById;
export type GraphIncludeOnlyRef = GraphRefOptData;
export type GraphColumnName = GraphZoneType;
export type GraphRowStats = RowStats;
export type InternalNotificationType = 'didChangeTheme';
@ -371,14 +374,24 @@ export const DidChangeRefsVisibilityNotificationType = new IpcNotificationType
export interface DidChangeRowsParams {
rows: GraphRow[];
downstreams: { [upstreamName: string]: string[] };
avatars: { [email: string]: string };
downstreams: { [upstreamName: string]: string[] };
paging?: GraphPaging;
refsMetadata?: GraphRefsMetadata | null;
rowsStats?: Record<string, GraphRowStats>;
rowsStatsLoading: boolean;
selectedRows?: GraphSelectedRows;
}
export const DidChangeRowsNotificationType = new IpcNotificationType<DidChangeRowsParams>('graph/rows/didChange');
export interface DidChangeRowsStatsParams {
rowsStats: Record<string, GraphRowStats>;
rowsStatsLoading: boolean;
}
export const DidChangeRowsStatsNotificationType = new IpcNotificationType<DidChangeRowsStatsParams>(
'graph/rows/stats/didChange',
);
export interface DidChangeSelectionParams {
selection: GraphSelectedRows;
}

+ 210
- 63
src/webviews/apps/plus/graph/GraphWrapper.tsx View File

@ -30,6 +30,7 @@ import type {
GraphComponentConfig,
GraphExcludedRef,
GraphExcludeTypes,
GraphMinimapMarkerTypes,
GraphMissingRefsMetadata,
GraphRefMetadataItem,
GraphRepository,
@ -47,13 +48,13 @@ import {
DidChangeRefsMetadataNotificationType,
DidChangeRefsVisibilityNotificationType,
DidChangeRowsNotificationType,
DidChangeRowsStatsNotificationType,
DidChangeSelectionNotificationType,
DidChangeSubscriptionNotificationType,
DidChangeWindowFocusNotificationType,
DidChangeWorkingTreeNotificationType,
DidFetchNotificationType,
DidSearchNotificationType,
GraphMinimapMarkerTypes,
} from '../../../../plus/webviews/graph/protocol';
import type { Subscription } from '../../../../subscription';
import { getSubscriptionTimeRemaining, SubscriptionState } from '../../../../subscription';
@ -193,6 +194,8 @@ export function GraphWrapper({
const graphRef = useRef<GraphContainer>(null);
const [rows, setRows] = useState(state.rows ?? []);
const [rowsStats, setRowsStats] = useState(state.rowsStats);
const [rowsStatsLoading, setRowsStatsLoading] = useState(state.rowsStatsLoading);
const [avatars, setAvatars] = useState(state.avatars);
const [downstreams, setDownstreams] = useState(state.downstreams ?? {});
const [refsMetadata, setRefsMetadata] = useState(state.refsMetadata);
@ -274,6 +277,8 @@ export function GraphWrapper({
break;
case DidChangeRowsNotificationType:
setRows(state.rows ?? []);
setRowsStats(state.rowsStats);
setRowsStatsLoading(state.rowsStatsLoading);
setSelectedRows(state.selectedRows);
setAvatars(state.avatars);
setDownstreams(state.downstreams ?? {});
@ -281,6 +286,10 @@ export function GraphWrapper({
setPagingHasMore(state.paging?.hasMore ?? false);
setIsLoading(state.loading);
break;
case DidChangeRowsStatsNotificationType:
setRowsStats(state.rowsStats);
setRowsStatsLoading(state.rowsStatsLoading);
break;
case DidSearchNotificationType: {
const { results, resultsError } = getSearchResultModel(state);
setSearchResultsError(resultsError);
@ -319,6 +328,8 @@ export function GraphWrapper({
setLastFetched(state.lastFetched);
setColumns(state.columns);
setRows(state.rows ?? []);
setRowsStats(state.rowsStats);
setRowsStatsLoading(state.rowsStatsLoading);
setWorkingTreeStats(state.workingTreeStats ?? { added: 0, modified: 0, deleted: 0 });
setGraphConfig(state.config);
setSelectedRows(state.selectedRows);
@ -377,10 +388,13 @@ export function GraphWrapper({
const minimapData = useMemo(() => {
if (!graphConfig?.minimap) return undefined;
const showLinesChanged = (graphConfig?.minimapDataType ?? 'commits') === 'lines';
if (showLinesChanged && rowsStats == null) return undefined;
// Loops through all the rows and group them by day and aggregate the row.stats
const statsByDayMap = new Map<number, GraphMinimapStats>();
const markersByDay = new Map<number, GraphMinimapMarker[]>();
const enabledMinimapMarkers: GraphMinimapMarkerTypes[] = graphConfig?.enabledMinimapMarkerTypes ?? [];
const enabledMinimapMarkers: GraphMinimapMarkerTypes[] = graphConfig?.minimapMarkerTypes ?? [];
let rankedShas: {
head: string | undefined;
@ -409,7 +423,6 @@ export function GraphWrapper({
// Iterate in reverse order so that we can track the HEAD upstream properly
for (let i = rows.length - 1; i >= 0; i--) {
row = rows[i];
stats = row.stats;
day = getDay(row.date);
if (day !== prevDay) {
@ -424,8 +437,7 @@ export function GraphWrapper({
if (
row.heads?.length &&
(enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.Head) ||
enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.LocalBranches))
(enabledMinimapMarkers.includes('head') || enabledMinimapMarkers.includes('localBranches'))
) {
rankedShas.branch = row.sha;
@ -438,13 +450,13 @@ export function GraphWrapper({
}
if (
enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.LocalBranches) ||
(enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.Head) && h.isCurrentHead)
enabledMinimapMarkers.includes('localBranches') ||
(enabledMinimapMarkers.includes('head') && h.isCurrentHead)
) {
headMarkers.push({
type: 'branch',
name: h.name,
current: h.isCurrentHead && enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.Head),
current: h.isCurrentHead && enabledMinimapMarkers.includes('head'),
});
}
});
@ -459,9 +471,9 @@ export function GraphWrapper({
if (
row.remotes?.length &&
(enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.Upstream) ||
enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.RemoteBranches) ||
enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.LocalBranches))
(enabledMinimapMarkers.includes('upstream') ||
enabledMinimapMarkers.includes('remoteBranches') ||
enabledMinimapMarkers.includes('localBranches'))
) {
rankedShas.remote = row.sha;
@ -477,14 +489,14 @@ export function GraphWrapper({
}
if (
enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.RemoteBranches) ||
(enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.Upstream) && current) ||
(enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.LocalBranches) && hasDownstream)
enabledMinimapMarkers.includes('remoteBranches') ||
(enabledMinimapMarkers.includes('upstream') && current) ||
(enabledMinimapMarkers.includes('localBranches') && hasDownstream)
) {
remoteMarkers.push({
type: 'remote',
name: `${r.owner}/${r.name}`,
current: current && enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.Upstream),
current: current && enabledMinimapMarkers.includes('upstream'),
});
}
});
@ -497,7 +509,7 @@ export function GraphWrapper({
}
}
if (row.type === 'stash-node' && enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.Stashes)) {
if (row.type === 'stash-node' && enabledMinimapMarkers.includes('stashes')) {
stashMarker = { type: 'stash', name: row.message };
markers = markersByDay.get(day);
if (markers == null) {
@ -507,7 +519,7 @@ export function GraphWrapper({
}
}
if (row.tags?.length && enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.Tags)) {
if (row.tags?.length && enabledMinimapMarkers.includes('tags')) {
rankedShas.tag = row.sha;
tagMarkers = row.tags.map<GraphMinimapMarker>(t => ({
@ -525,42 +537,54 @@ export function GraphWrapper({
stat = statsByDayMap.get(day);
if (stat == null) {
stat =
stats != null
? {
activity: { additions: stats.additions, deletions: stats.deletions },
commits: 1,
files: stats.files,
sha: row.sha,
}
: {
commits: 1,
sha: row.sha,
};
statsByDayMap.set(day, stat);
if (showLinesChanged) {
stats = rowsStats![row.sha];
if (stats != null) {
stat = {
activity: { additions: stats.additions, deletions: stats.deletions },
commits: 1,
files: stats.files,
sha: row.sha,
};
statsByDayMap.set(day, stat);
}
} else {
stat = {
commits: 1,
sha: row.sha,
};
statsByDayMap.set(day, stat);
}
} else {
stat.commits++;
stat.sha = rankedShas.head ?? rankedShas.branch ?? rankedShas.remote ?? rankedShas.tag ?? stat.sha;
if (stats != null) {
if (stat.activity == null) {
stat.activity = { additions: stats.additions, deletions: stats.deletions };
} else {
stat.activity.additions += stats.additions;
stat.activity.deletions += stats.deletions;
if (showLinesChanged) {
stats = rowsStats![row.sha];
if (stats != null) {
if (stat.activity == null) {
stat.activity = { additions: stats.additions, deletions: stats.deletions };
} else {
stat.activity.additions += stats.additions;
stat.activity.deletions += stats.deletions;
}
stat.files = (stat.files ?? 0) + stats.files;
}
stat.files = (stat.files ?? 0) + stats.files;
}
}
}
return { stats: statsByDayMap, markers: markersByDay };
}, [rows, downstreams, graphConfig?.minimap, graphConfig?.enabledMinimapMarkerTypes]);
}, [
rows,
rowsStats,
downstreams,
graphConfig?.minimap,
graphConfig?.minimapDataType,
graphConfig?.minimapMarkerTypes,
]);
const minimapSearchResults = useMemo(() => {
if (
!graphConfig?.minimap ||
!graphConfig.enabledMinimapMarkerTypes?.includes(GraphMinimapMarkerTypes.Highlights)
) {
if (!graphConfig?.minimap || !graphConfig.minimapMarkerTypes?.includes('highlights')) {
return undefined;
}
@ -582,7 +606,7 @@ export function GraphWrapper({
}
return searchResultsByDay;
}, [searchResults, graphConfig?.minimap, graphConfig?.enabledMinimapMarkerTypes]);
}, [searchResults, graphConfig?.minimap, graphConfig?.minimapMarkerTypes]);
const handleOnMinimapDaySelected = (e: CustomEvent<GraphMinimapDaySelectedEventDetail>) => {
let { sha } = e.detail;
@ -600,10 +624,45 @@ export function GraphWrapper({
graphRef.current?.selectCommits([sha], false, true);
};
const handleOnToggleMinimap = (_e: React.MouseEvent) => {
const handleOnMinimapToggle = (_e: React.MouseEvent) => {
onUpdateGraphConfiguration?.({ minimap: !graphConfig?.minimap });
};
// This can only be applied to one radio button for now due to a bug in the component: https://github.com/microsoft/fast/issues/6381
const handleOnMinimapDataTypeChange = (e: Event | FormEvent<HTMLElement>) => {
if (graphConfig == null) return;
const $el = e.target as HTMLInputElement;
if ($el.value === 'commits') {
const minimapDataType = $el.checked ? 'commits' : 'lines';
if (graphConfig.minimapDataType === minimapDataType) return;
setGraphConfig({ ...graphConfig, minimapDataType: minimapDataType });
onUpdateGraphConfiguration?.({ minimapDataType: minimapDataType });
}
};
const handleOnMinimapAdditionalTypesChange = (e: Event | FormEvent<HTMLElement>) => {
if (graphConfig?.minimapMarkerTypes == null) return;
const $el = e.target as HTMLInputElement;
const value = $el.value as GraphMinimapMarkerTypes;
if ($el.checked) {
const index = graphConfig.minimapMarkerTypes.indexOf(value);
if (index !== -1) {
const minimapMarkerTypes = [...graphConfig.minimapMarkerTypes];
minimapMarkerTypes.splice(index, 1);
setGraphConfig({ ...graphConfig, minimapMarkerTypes: minimapMarkerTypes });
onUpdateGraphConfiguration?.({ minimapMarkerTypes: minimapMarkerTypes });
}
} else if (!graphConfig.minimapMarkerTypes.includes(value)) {
const minimapMarkerTypes = [...graphConfig.minimapMarkerTypes, value];
setGraphConfig({ ...graphConfig, minimapMarkerTypes: minimapMarkerTypes });
onUpdateGraphConfiguration?.({ minimapMarkerTypes: minimapMarkerTypes });
}
};
const handleOnGraphMouseLeave = (_event: any) => {
minimap.current?.unselect(undefined, true);
};
@ -1351,21 +1410,111 @@ export function GraphWrapper({
<span>
<span className="action-divider"></span>
</span>
<button
type="button"
role="checkbox"
className="action-button"
title="Toggle Minimap (Experimental)"
aria-label="Toggle Minimap (Experimental)"
aria-checked={graphConfig?.minimap ?? false}
onClick={handleOnToggleMinimap}
>
<span className="codicon codicon-graph-line action-button__icon"></span>
</button>
<span className="button-group">
<button
type="button"
role="checkbox"
className="action-button"
title="Toggle Minimap"
aria-label="Toggle Minimap"
aria-checked={graphConfig?.minimap ?? false}
onClick={handleOnMinimapToggle}
>
<span className="codicon codicon-graph-line action-button__icon"></span>
</button>
<PopMenu position="right" className="split-button-dropdown">
<button
type="button"
className="action-button"
slot="trigger"
title="Minimap Options"
>
<span
className="codicon codicon-chevron-down action-button__more"
aria-hidden="true"
></span>
</button>
<MenuList slot="content">
<MenuLabel>Show by day</MenuLabel>
<MenuItem role="none">
<VSCodeRadioGroup
orientation="vertical"
value={graphConfig?.minimapDataType ?? 'commits'}
>
<VSCodeRadio
name="minimap-datatype"
value="commits"
onChange={handleOnMinimapDataTypeChange}
>
Commits
</VSCodeRadio>
<VSCodeRadio name="minimap-datatype" value="lines">
Lines Changed (can take a while)
</VSCodeRadio>
</VSCodeRadioGroup>
</MenuItem>
<MenuDivider></MenuDivider>
<MenuLabel>Markers</MenuLabel>
<MenuItem role="none">
<VSCodeCheckbox
value="localBranches"
onChange={handleOnMinimapAdditionalTypesChange}
defaultChecked={
!(
graphConfig?.minimapMarkerTypes?.includes('localBranches') ??
false
)
}
>
Hide Local Branches
</VSCodeCheckbox>
</MenuItem>
<MenuItem role="none">
<VSCodeCheckbox
value="remoteBranches"
onChange={handleOnMinimapAdditionalTypesChange}
defaultChecked={
!(
graphConfig?.minimapMarkerTypes?.includes('remoteBranches') ??
true
)
}
>
Hide Remote Branches
</VSCodeCheckbox>
</MenuItem>
<MenuItem role="none">
<VSCodeCheckbox
value="stashes"
onChange={handleOnMinimapAdditionalTypesChange}
defaultChecked={
!(graphConfig?.minimapMarkerTypes?.includes('stashes') ?? false)
}
>
Hide Stashes
</VSCodeCheckbox>
</MenuItem>
<MenuItem role="none">
<VSCodeCheckbox
value="tags"
onChange={handleOnMinimapAdditionalTypesChange}
defaultChecked={
!(graphConfig?.minimapMarkerTypes?.includes('tags') ?? true)
}
>
Hide Tags
</VSCodeCheckbox>
</MenuItem>
</MenuList>
</PopMenu>
</span>
</div>
</div>
)}
<div className={`progress-container infinite${isLoading ? ' active' : ''}`} role="progressbar">
<div
className={`progress-container infinite${isLoading || rowsStatsLoading ? ' active' : ''}`}
role="progressbar"
>
<div className="progress-bar"></div>
</div>
</header>
@ -1374,6 +1523,7 @@ export function GraphWrapper({
ref={minimap as any}
activeDay={activeDay}
data={minimapData?.stats}
dataType={graphConfig?.minimapDataType ?? 'commits'}
markers={minimapData?.markers}
searchResults={minimapSearchResults}
visibleDays={visibleDays}
@ -1397,9 +1547,7 @@ export function GraphWrapper({
dimMergeCommits={graphConfig?.dimMergeCommits}
downstreamsByUpstream={downstreams}
enabledRefMetadataTypes={graphConfig?.enabledRefMetadataTypes}
enabledScrollMarkerTypes={
graphConfig?.enabledScrollMarkerTypes as GraphMarkerType[] | undefined
}
enabledScrollMarkerTypes={graphConfig?.scrollMarkerTypes as GraphMarkerType[] | undefined}
enableMultiSelection={graphConfig?.enableMultiSelection}
excludeRefsById={excludeRefsById}
excludeByType={excludeTypes}
@ -1432,6 +1580,8 @@ export function GraphWrapper({
onGraphVisibleRowsChanged={minimap.current ? handleOnGraphVisibleRowsChanged : undefined}
platform={clientPlatform}
refMetadataById={refsMetadata}
rowsStats={rowsStats}
rowsStatsLoading={rowsStatsLoading}
shaLength={graphConfig?.idLength}
themeOpacityFactor={styleProps?.themeOpacityFactor}
useAuthorInitialsForAvatars={!graphConfig?.avatars}
@ -1448,10 +1598,7 @@ export function GraphWrapper({
data-vscode-context={context?.header || JSON.stringify({ webviewItem: 'gitlens:graph:columns' })}
onClick={handleToggleColumnSettings}
>
<span
className="codicon codicon-settings-gear columnsettings__icon"
aria-label="Column Settings"
></span>
<span className="codicon codicon-settings-gear" aria-label="Column Settings"></span>
</button>
</main>
</>

+ 39
- 16
src/webviews/apps/plus/graph/graph.scss View File

@ -233,6 +233,10 @@ button:not([disabled]),
vertical-align: bottom;
}
.codicon[class*='codicon-graph-line'] {
transform: translateY(1px);
}
&__pill {
.is-ahead & {
background-color: var(--branch-status-ahead-pill-background);
@ -245,18 +249,6 @@ button:not([disabled]),
}
}
// &__icon {
// .is-ahead & {
// color: var(--branch-status-ahead-foreground);
// }
// .is-behind & {
// color: var(--branch-status-behind-foreground);
// }
// .is-ahead.is-behind & {
// color: var(--branch-status-both-foreground);
// }
// }
&__more,
&__more.codicon[class*='codicon-'] {
font-size: 1rem;
@ -338,6 +330,38 @@ button:not([disabled]),
border-color: var(--vscode-inputOption-activeBorder);
}
.button-group {
display: flex;
flex-direction: row;
align-items: stretch;
&:hover {
background-color: var(--color-graph-actionbar-selectedBackground);
}
> *:not(:first-child),
> *:not(:first-child) .action-button {
display: flex;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
> *:not(:first-child) .action-button {
padding-left: 0.25rem;
}
> *:not(:last-child),
> *:not(:last-child) .action-button {
padding-right: 0.5rem;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
// > *:not(:first-child) {
// border-left: 0.1rem solid var(--titlebar-fg);
// }
}
.repo-access {
font-size: 1.1em;
margin-right: 0.2rem;
@ -348,10 +372,10 @@ button:not([disabled]),
}
.column-button {
--column-button-height: 20px;
--column-button-height: 19px;
position: absolute;
top: 1px;
top: 3px;
right: 0;
z-index: 2;
@ -361,7 +385,7 @@ button:not([disabled]),
border: none;
color: var(--text-disabled, hsla(0, 0%, 100%, 0.4));
margin: 0;
padding: 0 4px;
padding: 0 2px;
height: var(--column-button-height);
cursor: pointer;
background-color: var(--color-graph-actionbar-background);
@ -385,7 +409,6 @@ button:not([disabled]),
.codicon[class*='codicon-'] {
font-size: 1.1rem;
position: relative;
top: 2px;
}
}

+ 13
- 0
src/webviews/apps/plus/graph/graph.tsx View File

@ -26,6 +26,7 @@ import {
DidChangeRefsMetadataNotificationType,
DidChangeRefsVisibilityNotificationType,
DidChangeRowsNotificationType,
DidChangeRowsStatsNotificationType,
DidChangeSelectionNotificationType,
DidChangeSubscriptionNotificationType,
DidChangeWindowFocusNotificationType,
@ -272,6 +273,10 @@ export class GraphApp extends App {
}
this.state.rows = rows;
this.state.paging = params.paging;
if (params.rowsStats != null) {
this.state.rowsStats = { ...this.state.rowsStats, ...params.rowsStats };
}
this.state.rowsStatsLoading = params.rowsStatsLoading;
if (params.selectedRows != null) {
this.state.selectedRows = params.selectedRows;
}
@ -280,6 +285,14 @@ export class GraphApp extends App {
});
break;
case DidChangeRowsStatsNotificationType.method:
onIpc(DidChangeRowsStatsNotificationType, msg, (params, type) => {
this.state.rowsStats = { ...this.state.rowsStats, ...params.rowsStats };
this.state.rowsStatsLoading = params.rowsStatsLoading;
this.setState(this.state, type);
});
break;
case DidSearchNotificationType.method:
onIpc(DidSearchNotificationType, msg, (params, type) => {
this.state.searchResults = params.results;

+ 63
- 17
src/webviews/apps/plus/graph/minimap/minimap.ts View File

@ -3,7 +3,7 @@ import type { Chart, DataItem, RegionOptions } from 'billboard.js';
import { groupByMap } from '../../../../../system/array';
import { debug } from '../../../../../system/decorators/log';
import { debounce } from '../../../../../system/function';
import { first, flatMap, map, some, union } from '../../../../../system/iterable';
import { first, flatMap, map, union } from '../../../../../system/iterable';
import { pluralize } from '../../../../../system/string';
import { formatDate, formatNumeric, fromNow } from '../../../shared/date';
@ -54,7 +54,14 @@ export interface GraphMinimapDaySelectedEventDetail {
}
const template = html<GraphMinimap>`<template>
<div id="spinner" ${ref('spinner')}><code-icon icon="loading" modifier="spin"></code-icon></div>
<div id="chart" ${ref('chart')}></div>
<div
class="legend"
title="${x => (x.dataType === 'lines' ? 'Showing lines changed per day' : 'Showing commits per day')}"
>
<code-icon icon="${x => (x.dataType === 'lines' ? 'request-changes' : 'git-commit')}"></code-icon>
</div>
</template>`;
const styles = css`
@ -69,11 +76,36 @@ const styles = css`
#chart {
height: 100%;
width: 100%;
width: calc(100% - 1rem);
overflow: hidden;
position: initial !important;
}
#spinner {
position: absolute;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
z-index: 1;
}
#spinner[aria-hidden='true'] {
display: none;
}
.legend {
position: absolute;
top: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
z-index: 1;
opacity: 0.7;
cursor: help;
}
.bb svg {
font: 10px var(--font-family);
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
@ -392,6 +424,7 @@ const markerZOrder = [
@customElement({ name: 'graph-minimap', template: template, styles: styles })
export class GraphMinimap extends FASTElement {
chart!: HTMLDivElement;
spinner!: HTMLDivElement;
private _chart!: Chart;
private _loadTimer: ReturnType<typeof setTimeout> | undefined;
@ -401,7 +434,6 @@ export class GraphMinimap extends FASTElement {
@observable
activeDay: number | undefined;
@debug({ singleLine: true })
protected activeDayChanged() {
this.select(this.activeDay);
}
@ -428,6 +460,12 @@ export class GraphMinimap extends FASTElement {
}
@observable
dataType: 'commits' | 'lines' = 'commits';
protected dataTypeChanged() {
this.dataChanged();
}
@observable
markers: Map<number, GraphMinimapMarker[]> | undefined;
protected markersChanged() {
this.dataChanged(undefined, undefined, true);
@ -444,7 +482,6 @@ export class GraphMinimap extends FASTElement {
@observable
visibleDays: { top: number; bottom: number } | undefined;
@debug({ singleLine: true })
protected visibleDaysChanged() {
this._chart?.regions.remove({ classes: ['visible-area'] });
if (this.visibleDays == null) return;
@ -466,10 +503,13 @@ export class GraphMinimap extends FASTElement {
}
private getInternalChart(): any {
return (this._chart as any).internal;
try {
return (this._chart as any)?.internal;
} catch {
return undefined;
}
}
@debug({ singleLine: true })
select(date: number | Date | undefined, trackOnly: boolean = false) {
if (date == null) {
this.unselect();
@ -481,6 +521,8 @@ export class GraphMinimap extends FASTElement {
if (d == null) return;
const internal = this.getInternalChart();
if (internal == null) return;
internal.showGridFocus([d]);
if (!trackOnly) {
@ -491,10 +533,9 @@ export class GraphMinimap extends FASTElement {
}
}
@debug({ singleLine: true })
unselect(date?: number | Date, focus: boolean = false) {
if (focus) {
this.getInternalChart().hideGridFocus();
this.getInternalChart()?.hideGridFocus();
return;
}
@ -616,13 +657,15 @@ export class GraphMinimap extends FASTElement {
@debug({ singleLine: true })
private async loadChartCore() {
if (!this.data?.size) {
this.spinner.setAttribute('aria-hidden', 'false');
this._chart?.destroy();
this._chart = undefined!;
return;
}
const hasActivity = some(this.data.values(), v => v?.activity != null);
const showLinesChanged = this.dataType === 'lines';
// Convert the map to an array dates and an array of stats
const dates = [];
@ -653,7 +696,7 @@ export class GraphMinimap extends FASTElement {
stat = this.data.get(day);
dates.push(day);
if (hasActivity) {
if (showLinesChanged) {
adds = stat?.activity?.additions ?? 0;
deletes = stat?.activity?.deletions ?? 0;
changes = adds + deletes;
@ -832,6 +875,7 @@ export class GraphMinimap extends FASTElement {
}
const stashesCount = groups?.get('stash')?.length ?? 0;
const showLinesChanged = this.dataType === 'lines';
return /*html*/ `<div class="bb-tooltip">
<div class="header">
@ -845,12 +889,12 @@ export class GraphMinimap extends FASTElement {
: `${pluralize('commit', stat?.commits ?? 0, {
format: c => formatNumeric(c),
zero: 'No',
})}, ${pluralize('file', stat?.commits ?? 0, {
format: c => formatNumeric(c),
zero: 'No',
})}${
hasActivity
? `, ${pluralize(
showLinesChanged
? `, ${pluralize('file', stat?.files ?? 0, {
format: c => formatNumeric(c),
zero: 'No',
})}, ${pluralize(
'line',
(stat?.activity?.additions ?? 0) +
(stat?.activity?.deletions ?? 0),
@ -858,9 +902,9 @@ export class GraphMinimap extends FASTElement {
format: c => formatNumeric(c),
zero: 'No',
},
)}`
)} changed`
: ''
} changed`
}`
}</span>
</div>
${
@ -957,6 +1001,8 @@ export class GraphMinimap extends FASTElement {
this._chart.regions(regions);
}
this.spinner.setAttribute('aria-hidden', 'true');
this.activeDayChanged();
}
}

+ 1
- 0
src/webviews/apps/shared/components/menu/menu-label.ts View File

@ -20,6 +20,7 @@ const styles = css`
margin: 0px;
color: var(--vscode-menu-foreground);
opacity: 0.6;
user-select: none;
}
`;

+ 41
- 2
src/webviews/apps/shared/components/overlays/pop-menu/pop-menu.ts View File

@ -16,13 +16,44 @@ const styles = css`
position: relative;
}
slot[name='content']::slotted(*)::before {
font: normal normal normal 14px/1 codicon;
display: inline-block;
text-decoration: none;
text-rendering: auto;
text-align: center;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
vertical-align: middle;
line-height: 2rem;
letter-spacing: normal;
content: '\\ea76';
position: absolute;
top: 2px;
right: 5px;
cursor: pointer;
pointer-events: all;
z-index: 10001;
}
slot[name='content']::slotted(*) {
position: absolute;
left: 0;
top: 100%;
z-index: 10000;
}
:host([position='left']) slot[name='content']::slotted(*) {
left: 0;
}
:host([position='right']) slot[name='content']::slotted(*) {
right: 0;
}
:host(:not([open])) slot[name='content']::slotted(*) {
display: none;
}
@ -33,6 +64,9 @@ export class PopMenu extends FASTElement {
@attr({ mode: 'boolean' })
open = false;
@attr()
position: 'left' | 'right' = 'left';
@observable
triggerNodes?: HTMLElement[];
@ -96,7 +130,12 @@ export class PopMenu extends FASTElement {
if (this.open === false) return;
const composedPath = e.composedPath();
if (!composedPath.includes(this)) {
if (
!composedPath.includes(this) ||
// If the ::before element is clicked and is the close icon, close the menu
(e.type === 'click' &&
window.getComputedStyle(composedPath[0] as Element, '::before').content === '"\uEA76"')
) {
this.open = false;
this.disposeTrackOutside();
}

+ 33
- 1
src/webviews/apps/shared/components/search/search-input.ts View File

@ -342,6 +342,32 @@ const styles = css`
display: block;
}
.helper::before {
font: normal normal normal 14px/1 codicon;
display: inline-block;
text-decoration: none;
text-rendering: auto;
text-align: center;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
vertical-align: middle;
line-height: 2rem;
letter-spacing: normal;
content: '\\ea76';
position: absolute;
top: 2px;
right: 5px;
cursor: pointer;
pointer-events: all;
z-index: 10001;
opacity: 0.6;
}
.helper-label {
text-transform: uppercase;
font-size: 0.84em;
@ -350,6 +376,7 @@ const styles = css`
padding-right: 0.6rem;
margin: 0;
opacity: 0.6;
user-select: none;
}
.helper-button {
@ -429,7 +456,12 @@ export class SearchInput extends FASTElement {
if (this.showHelp === false) return;
const composedPath = e.composedPath();
if (!composedPath.includes(this)) {
if (
!composedPath.includes(this) ||
// If the ::before element is clicked and is the close icon, close the menu
(e.type === 'click' &&
window.getComputedStyle(composedPath[0] as Element, '::before').content === '"\uEA76"')
) {
this.showHelp = false;
}
}

+ 4
- 4
yarn.lock View File

@ -202,10 +202,10 @@
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==
"@gitkraken/gitkraken-components@9.1.5":
version "9.1.5"
resolved "https://registry.yarnpkg.com/@gitkraken/gitkraken-components/-/gitkraken-components-9.1.5.tgz#96f8b92ef47ebaa4c041e7b61e4a2cce73c35473"
integrity sha512-dVIQEOeS4Fd8973eWP+bu7EOWqRuypz+gJvQ+gqrP3hfs7OWMTE6dD3PTO/vPlDNTlgqhjmPXR8mGYk3HBUqHQ==
"@gitkraken/gitkraken-components@10.0.0":
version "10.0.0"
resolved "https://registry.yarnpkg.com/@gitkraken/gitkraken-components/-/gitkraken-components-10.0.0.tgz#936c766427c7fbdaab2e7ac72bbce054de5fbcb1"
integrity sha512-1feNudTT69Dsaue5F+4KG0YtWnQTb2Tn2+0B8kMhFh61X+c7tcO6AUZ3D0kwXx/I5moowpsBnQXnYfaIKa4IXw==
dependencies:
"@axosoft/react-virtualized" "9.22.3-gitkraken.3"
classnames "2.3.2"

Loading…
Cancel
Save