Skip to content

Commit

Permalink
Adds graph onboard tracking
Browse files Browse the repository at this point in the history
  • Loading branch information
d13 committed Aug 5, 2024
1 parent ce00751 commit b22338d
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 48 deletions.
7 changes: 7 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { Subscription, SubscriptionPlanId, SubscriptionState } from './plus
import type { SupportedCloudIntegrationIds } from './plus/integrations/authentication/models';
import type { Integration } from './plus/integrations/integration';
import type { IntegrationId } from './plus/integrations/providers/models';
import type { OnboardingState } from './plus/webviews/graph/protocol';
import type { TelemetryEventData } from './telemetry/telemetry';
import type { TrackedUsage, TrackedUsageKeys } from './telemetry/usageTracker';

Expand Down Expand Up @@ -946,6 +947,7 @@ export type GlobalStorage = {
'launchpad:groups:collapsed': StoredFocusGroup[];
'launchpad:indicator:hasLoaded': boolean;
'launchpad:indicator:hasInteracted': string;
'graph:onboarding': Record<string, OnboardingState>;
} & { [key in `confirm:ai:tos:${AIProviders}`]: boolean } & {
[key in `provider:authentication:skip:${string}`]: boolean;
} & { [key in `gk:${string}:checkin`]: Stored<StoredGKCheckInResponse> } & {
Expand Down Expand Up @@ -1301,6 +1303,11 @@ export type TelemetryEvents = {
/** Sent when a VS Code command is executed by a GitLens provided action */
'command/core': { command: string };

'graph/onboarding/state': {
name: string;
state: OnboardingState;
};

/** Sent when the user takes an action on a launchpad item */
'launchpad/title/action': LaunchpadEventData & {
action: 'feedback' | 'open-on-gkdev' | 'refresh' | 'settings' | 'connect';
Expand Down
25 changes: 23 additions & 2 deletions src/plus/webviews/graph/graphWebview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ import type {
GraphUpstreamMetadata,
GraphUpstreamStatusContextValue,
GraphWorkingTreeStats,
OnboardingState,
OpenPullRequestDetailsParams,
SearchOpenInViewParams,
SearchParams,
Expand Down Expand Up @@ -174,6 +175,7 @@ import {
UpdateExcludeTypeCommand,
UpdateGraphConfigurationCommand,
UpdateIncludeOnlyRefsCommand,
UpdateOnboardingStateCommand,
UpdateRefsVisibilityCommand,
UpdateSelectionCommand,
} from './protocol';
Expand Down Expand Up @@ -683,6 +685,9 @@ export class GraphWebviewProvider implements WebviewProvider<State, State, Graph
case UpdateIncludeOnlyRefsCommand.is(e):
this.updateIncludeOnlyRefs(this._graph, e.params.refs);
break;
case UpdateOnboardingStateCommand.is(e):
this.updateOnboardingState(e.params.name, e.params.state);
break;
}
}

Expand Down Expand Up @@ -732,6 +737,21 @@ export class GraphWebviewProvider implements WebviewProvider<State, State, Graph
}
}

private updateOnboardingState(name: string, state: OnboardingState) {
const storedState = this.container.storage.get('graph:onboarding');
if (storedState == null) {
void this.container.storage.store('graph:onboarding', { [name]: state });
this.container.telemetry.sendEvent('graph/onboarding/state', { name: name, state: state });
return;
}
storedState[name] = {
...storedState[name],
...state,
};
void this.container.storage.store('graph:onboarding', storedState);
this.container.telemetry.sendEvent('graph/onboarding/state', { name: name, state: storedState[name] });
}

private _showActiveSelectionDetailsDebounced:
| Deferrable<GraphWebviewProvider['showActiveSelectionDetails']>
| undefined = undefined;
Expand Down Expand Up @@ -2146,13 +2166,13 @@ export class GraphWebviewProvider implements WebviewProvider<State, State, Graph

private async getState(deferRows?: boolean): Promise<State> {
if (this.container.git.repositoryCount === 0) {
return { ...this.host.baseWebviewState, allowed: true, repositories: [] };
return { ...this.host.baseWebviewState, allowed: true, repositories: [], onboarding: undefined };
}

if (this.repository == null) {
this.repository = this.container.git.getBestRepositoryOrFirst();
if (this.repository == null) {
return { ...this.host.baseWebviewState, allowed: true, repositories: [] };
return { ...this.host.baseWebviewState, allowed: true, repositories: [], onboarding: undefined };
}
}

Expand Down Expand Up @@ -2282,6 +2302,7 @@ export class GraphWebviewProvider implements WebviewProvider<State, State, Graph
includeOnlyRefs: data != null ? this.getIncludeOnlyRefs(data) ?? {} : {},
nonce: this.host.cspNonce,
workingTreeStats: getSettledValue(workingStatsResult) ?? { added: 0, deleted: 0, modified: 0 },
onboarding: this.container.storage.get('graph:onboarding'),
};
}

Expand Down
16 changes: 16 additions & 0 deletions src/plus/webviews/graph/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ export type GraphMinimapMarkerTypes =

export const supportedRefMetadataTypes: GraphRefMetadataType[] = ['upstream', 'pullRequest', 'issue'];

export interface OnboardingState {
completed?: boolean;
dismissed?: boolean;
step?: string;
}

export interface State extends WebviewState {
windowFocused?: boolean;
repositories?: GraphRepository[];
Expand Down Expand Up @@ -124,6 +130,7 @@ export interface State extends WebviewState {
bottom: number;
};
theming?: { cssVariables: CssVariables; themeOpacityFactor: number };
onboarding: Record<string, OnboardingState> | undefined;
}

export interface BranchState extends GitTrackingState {
Expand Down Expand Up @@ -304,6 +311,15 @@ export interface UpdateSelectionParams {
}
export const UpdateSelectionCommand = new IpcCommand<UpdateSelectionParams>(scope, 'selection/update');

export interface UpdateOnboardingStateParams {
name: string;
state: OnboardingState;
}
export const UpdateOnboardingStateCommand = new IpcCommand<UpdateOnboardingStateParams>(
scope,
'onboarding/update/state',
);

// REQUESTS

export interface EnsureRowParams {
Expand Down
121 changes: 75 additions & 46 deletions src/webviews/apps/plus/graph/GraphWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import type {
} from '@gitkraken/gitkraken-components';
import GraphContainer, { CommitDateTimeSources, refZone } from '@gitkraken/gitkraken-components';
import { VSCodeCheckbox, VSCodeRadio, VSCodeRadioGroup } from '@vscode/webview-ui-toolkit/react';
import { driver } from 'driver.js';
import type { FormEvent, MouseEvent, ReactElement } from 'react';
import React, { createElement, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { getPlatform } from '@env/platform';
Expand All @@ -40,6 +39,7 @@ import type {
GraphSearchResults,
GraphSearchResultsError,
InternalNotificationType,
OnboardingState,
State,
UpdateGraphConfigurationParams,
UpdateStateCallback,
Expand Down Expand Up @@ -76,6 +76,7 @@ import { GlSearchBox } from '../../shared/components/search/react';
import type { SearchNavigationEventDetail } from '../../shared/components/search/search-box';
import type { DateTimeFormat } from '../../shared/date';
import { formatDate, fromNow } from '../../shared/date';
import { createOnboarding } from '../../shared/onboarding';
import { GlGraphHover } from './hover/graphHover.react';
import type { GraphMinimapDaySelectedEventDetail } from './minimap/minimap';
import { GlGraphMinimapContainer } from './minimap/minimap-container.react';
Expand Down Expand Up @@ -106,6 +107,7 @@ export interface GraphWrapperProps {
onExcludeType?: (key: keyof GraphExcludeTypes, value: boolean) => void;
onIncludeOnlyRef?: (all: boolean) => void;
onUpdateGraphConfiguration?: (changes: UpdateGraphConfigurationParams['changes']) => void;
onOnboardingStateChanged?: (name: string, state: OnboardingState) => void;
}

const getGraphDateFormatter = (config?: GraphComponentConfig): OnFormatCommitDateTime => {
Expand Down Expand Up @@ -227,6 +229,7 @@ export function GraphWrapper({
onExcludeType,
onIncludeOnlyRef,
onUpdateGraphConfiguration,
onOnboardingStateChanged,
}: GraphWrapperProps) {
const graphRef = useRef<GraphContainer>(null);

Expand Down Expand Up @@ -396,55 +399,81 @@ export function GraphWrapper({
useEffect(() => subscriber?.(updateState), []);

useLayoutEffect(() => {
const driverObj = driver({
showProgress: true,
steps: [
{
popover: {
title: 'Welcome to the Commit Graph',
description:
'It helps visualize your repository commit history and give you information about branches, commits, and collaborators all in one view.',
},
const onboardingKey = 'graph-tour';
const onboardingState = state.onboarding?.[onboardingKey];
if (onboardingState?.dismissed === true) {
return;
}

const steps = [
{
key: `${onboardingKey}-welcome`,
popover: {
title: 'Welcome to the Commit Graph',
description:
'It helps visualize your repository commit history and give you information about branches, commits, and collaborators all in one view.',
},
{
element: '#graph-repo-actions',
popover: {
title: 'Repository Actions',
description:
"Quickly switch repos, branches, see a branch's PR info, push/pull/fetch, and more.",
},
},
{
key: `${onboardingKey}-repo-actions`,
element: '#graph-repo-actions',
popover: {
title: 'Repository Actions',
description: "Quickly switch repos, branches, see a branch's PR info, push/pull/fetch, and more.",
},
{
element: '#graph-search',
popover: {
title: 'Rich Commit Search',
description:
'Highlight all matching results across your entire repository when searching for a commit, message, author, a changed file or files, or even a specific code change.',
},
},
{
key: `${onboardingKey}-search`,
element: '#graph-search',
popover: {
title: 'Rich Commit Search',
description:
'Highlight all matching results across your entire repository when searching for a commit, message, author, a changed file or files, or even a specific code change.',
},
{
element: '#graph-minimap',
popover: {
title: 'Minimap',
description:
'Quickly see the activity of the repository, see the HEAD/upstream, branches (local and remote), and easily jump to them. ',
},
},
{
key: `${onboardingKey}-minimap`,
element: '#graph-minimap',
popover: {
title: 'Minimap',
description:
'Quickly see the activity of the repository, see the HEAD/upstream, branches (local and remote), and easily jump to them. ',
},
{
element: '#main',
popover: {
title: 'Commit Graph',
description: 'The Commit Graph is a visualization of your repository history.',
},
},
{
key: `${onboardingKey}-graph`,
element: '#main',
popover: {
title: 'Commit Graph',
description: 'The Commit Graph is a visualization of your repository history.',
},
{
popover: {
title: 'Done',
description: "That's it for now. Enjoy! Please see this walkthrough for more information.",
},
},
{
key: `${onboardingKey}-done`,
popover: {
title: 'Done',
description: "That's it for now. Enjoy! Please see this walkthrough for more information.",
},
],
});
},
];

const driverObj = createOnboarding(
steps,
{
onCloseClick: ($el, step, options) => {
console.log('onCloseClick', $el, step, options);
onOnboardingStateChanged?.(onboardingKey, { dismissed: true });
},
},
(key, step, options) => {
console.log('onHighlightedByKey', key, step, options);
onOnboardingStateChanged?.(onboardingKey, {
dismissed: false,
completed: key === `${onboardingKey}-done`,
step: key,
});
},
);

driverObj.drive();

Expand Down Expand Up @@ -1185,8 +1214,8 @@ export function GraphWrapper({
return (
<>
<header className="titlebar graph-app__header">
<div id="graph-repo-actions" className="titlebar__row titlebar__row--wrap">
<div className="titlebar__group">
<div className="titlebar__row titlebar__row--wrap">
<div id="graph-repo-actions" className="titlebar__group">
{repo && branchState?.provider?.url && (
<GlTooltip placement="bottom">
<a
Expand Down
7 changes: 7 additions & 0 deletions src/webviews/apps/plus/graph/graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
GraphMissingRefsMetadata,
GraphRefMetadataItem,
InternalNotificationType,
OnboardingState,
State,
UpdateGraphConfigurationParams,
UpdateStateCallback,
Expand Down Expand Up @@ -47,6 +48,7 @@ import {
UpdateExcludeTypeCommand,
UpdateGraphConfigurationCommand,
UpdateIncludeOnlyRefsCommand,
UpdateOnboardingStateCommand,
UpdateRefsVisibilityCommand,
UpdateSelectionCommand,
} from '../../../../plus/webviews/graph/protocol';
Expand Down Expand Up @@ -123,6 +125,7 @@ export class GraphApp extends App<State> {
onExcludeType={this.onExcludeType.bind(this)}
onIncludeOnlyRef={this.onIncludeOnlyRef.bind(this)}
onUpdateGraphConfiguration={this.onUpdateGraphConfiguration.bind(this)}
onOnboardingStateChanged={this.onOnboardingStateChanged.bind(this)}
/>,
$root,
);
Expand Down Expand Up @@ -638,6 +641,10 @@ export class GraphApp extends App<State> {
this.sendCommand(UpdateGraphConfigurationCommand, { changes: changes });
}

private onOnboardingStateChanged(name: string, state: OnboardingState) {
this.sendCommand(UpdateOnboardingStateCommand, { name: name, state: state });
}

private onSelectionChanged(rows: GraphRow[]) {
const selection = rows.filter(r => r != null).map(r => ({ id: r.sha, type: r.type as GitGraphRowType }));
this.sendCommand(UpdateSelectionCommand, {
Expand Down
50 changes: 50 additions & 0 deletions src/webviews/apps/shared/onboarding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { Config, Driver, DriveStep, State } from 'driver.js';
import { driver } from 'driver.js';

export type KeyedDriverHook = (
key: string,
step: DriveStep,
opts: {
config: Config;
state: State;
element: Element | undefined;
},
) => void;

export interface KeyedDriveStep extends DriveStep {
key: string;
}

export function createOnboarding(
steps: KeyedDriveStep[],
config: Exclude<Config, 'steps'> = {},
onHighlightedByKey?: KeyedDriverHook,
): Driver {
const driverConfig: Config = {
showProgress: true,
...config,
steps: steps.map(keyedStep => ({
...keyedStep,
onHighlighted:
keyedStep.onHighlighted != null || onHighlightedByKey != null
? (element, step, opts) => {
keyedStep.onHighlighted?.(element, step, opts);
onHighlightedByKey?.(keyedStep.key, step, { ...opts, element: element });
}
: undefined,
})),
onHighlighted:
onHighlightedByKey != null
? (element, step, opts) => {
config.onHighlighted?.(element, step, opts);

const keyedStep = steps.find(s => s.popover === step.popover);
if (keyedStep == null) return;

onHighlightedByKey(keyedStep.key, step, { ...opts, element: element });
}
: undefined,
};

return driver(driverConfig);
}

0 comments on commit b22338d

Please sign in to comment.