Selaa lähdekoodia

Reworks gates for Graph & Focus

main
Eric Amodio 1 vuosi sitten
vanhempi
commit
fff4d61d5f
16 muutettua tiedostoa jossa 298 lisäystä ja 487 poistoa
  1. +1
    -1
      README.md
  2. +50
    -42
      src/plus/webviews/focus/focusWebview.ts
  3. +27
    -1
      src/subscription.ts
  4. +26
    -25
      src/webviews/apps/plus/focus/focus.html
  5. +8
    -21
      src/webviews/apps/plus/focus/focus.scss
  6. +10
    -12
      src/webviews/apps/plus/focus/focus.ts
  7. +13
    -215
      src/webviews/apps/plus/graph/GraphWrapper.tsx
  8. +9
    -10
      src/webviews/apps/plus/graph/graph.scss
  9. +0
    -1
      src/webviews/apps/plus/graph/graph.tsx
  10. +0
    -144
      src/webviews/apps/shared/components/account/account-badge.ts
  11. +8
    -2
      src/webviews/apps/shared/components/button.ts
  12. +116
    -0
      src/webviews/apps/shared/components/feature-gate-badge.ts
  13. +15
    -12
      src/webviews/apps/shared/components/feature-gate.ts
  14. +7
    -0
      src/webviews/apps/shared/components/react/feature-gate-badge.tsx
  15. +7
    -0
      src/webviews/apps/shared/components/react/feature-gate.tsx
  16. +1
    -1
      walkthroughs/plus/commit-graph.md

+ 1
- 1
README.md Näytä tiedosto

@ -80,7 +80,7 @@ No, the introduction of GitLens+ features has no impact on existing GitLens feat
<img src="https://raw.githubusercontent.com/gitkraken/vscode-gitlens/main/images/docs/commit-graph-illustrated.png" alt="Commit Graph" />
</p>
The _Commit Graph_ helps you easily visualize and keep track of all work in progress. Not only does it help you verify your changes, but also easily see changes made by others and when. Selecting a row within the graph will open in-depth information about a commit or stash in the new [Commit Details view](#commit-details-view-).
The _Commit Graph_ helps you easily visualize your repository and keep track of all work in progress. Not only does it help you verify your changes, but also easily see changes made by others and when. Selecting a row within the graph will open in-depth information about a commit or stash in the new [Commit Details view](#commit-details-view-).
Use the rich commit search to find exactly what you're looking for. It's powerful filters allow you to search by a specific commit, message, author, a changed file or files, or even a specific code change.

+ 50
- 42
src/plus/webviews/focus/focusWebview.ts Näytä tiedosto

@ -24,6 +24,7 @@ import { parseGitRemoteUrl } from '../../../git/parsers/remoteParser';
import type { RichRemoteProvider } from '../../../git/remotes/richRemoteProvider';
import { executeCommand, registerCommand } from '../../../system/command';
import { setContext } from '../../../system/context';
import { getSettledValue } from '../../../system/promise';
import type { IpcMessage } from '../../../webviews/protocol';
import { onIpc } from '../../../webviews/protocol';
import type { WebviewController, WebviewProvider } from '../../../webviews/webviewController';
@ -48,7 +49,6 @@ interface SearchedPullRequestWithRemote extends SearchedPullRequest {
}
export class FocusWebviewProvider implements WebviewProvider<State> {
private _bootstrapping = true;
private _pullRequests: SearchedPullRequestWithRemote[] = [];
private _issues: SearchedIssue[] = [];
private readonly _disposable: Disposable;
@ -223,10 +223,10 @@ export class FocusWebviewProvider implements WebviewProvider {
if (e.etag === this._etagSubscription) return;
this._etagSubscription = e.etag;
void this.notifyDidChangeState();
void this.notifyDidChangeState(true);
}
private async getState(deferState = false): Promise<State> {
private async getState(deferState?: boolean): Promise<State> {
const access = await this.container.git.access(PlusFeatures.Focus);
if (access.allowed !== true) {
return {
@ -240,49 +240,61 @@ export class FocusWebviewProvider implements WebviewProvider {
const connectedRepos = filterUsableRepos(githubRepos);
const hasConnectedRepos = connectedRepos.length > 0;
if (deferState || !hasConnectedRepos) {
if (!hasConnectedRepos) {
return {
timestamp: Date.now(),
access: access,
repos: (hasConnectedRepos ? connectedRepos : githubRepos).map(r => serializeRepoWithRichRemote(r)),
repos: githubRepos.map(r => serializeRepoWithRichRemote(r)),
};
}
const prs = await this.getMyPullRequests(connectedRepos);
const serializedPrs = prs.map(pr => ({
pullRequest: serializePullRequest(pr.pullRequest),
reasons: pr.reasons,
isCurrentBranch: pr.isCurrentBranch ?? false,
isCurrentWorktree: pr.isCurrentWorktree ?? false,
hasWorktree: pr.hasWorktree ?? false,
hasLocalBranch: pr.hasLocalBranch ?? false,
}));
const issues = await this.getMyIssues(connectedRepos);
const serializedIssues = issues.map(issue => ({
issue: serializeIssue(issue.issue),
reasons: issue.reasons,
}));
const repos = connectedRepos.map(r => serializeRepoWithRichRemote(r));
return {
timestamp: Date.now(),
access: access,
pullRequests: serializedPrs,
issues: serializedIssues,
repos: connectedRepos.map(r => serializeRepoWithRichRemote(r)),
};
}
const statePromise = Promise.allSettled([
this.getMyPullRequests(connectedRepos),
this.getMyIssues(connectedRepos),
]);
async includeBootstrap(): Promise<State> {
if (this._bootstrapping) {
const state = await this.getState(true);
if (state.access.allowed === true) {
void this.notifyDidChangeState();
}
return state;
async function getStateCore() {
const [prsResult, issuesResult] = await statePromise;
return {
timestamp: Date.now(),
access: access,
repos: repos,
pullRequests: getSettledValue(prsResult)?.map(pr => ({
pullRequest: serializePullRequest(pr.pullRequest),
reasons: pr.reasons,
isCurrentBranch: pr.isCurrentBranch ?? false,
isCurrentWorktree: pr.isCurrentWorktree ?? false,
hasWorktree: pr.hasWorktree ?? false,
hasLocalBranch: pr.hasLocalBranch ?? false,
})),
issues: getSettledValue(issuesResult)?.map(issue => ({
issue: serializeIssue(issue.issue),
reasons: issue.reasons,
})),
};
}
return this.getState();
if (deferState) {
queueMicrotask(async () => {
const state = await getStateCore();
void this.host.notify(DidChangeNotificationType, { state: state });
});
return {
timestamp: Date.now(),
access: access,
repos: repos,
};
}
const state = await getStateCore();
return state;
}
async includeBootstrap(): Promise<State> {
return this.getState(true);
}
private async getRichRepos(force?: boolean): Promise<RepoWithRichRemote[]> {
@ -425,12 +437,8 @@ export class FocusWebviewProvider implements WebviewProvider {
return this._issues;
}
private async notifyDidChangeState() {
// if (!this.host.visible) return;
const state = await this.getState();
this._bootstrapping = false;
void this.host.notify(DidChangeNotificationType, { state: state });
private async notifyDidChangeState(deferState?: boolean) {
void this.host.notify(DidChangeNotificationType, { state: await this.getState(deferState) });
}
}

+ 27
- 1
src/subscription.ts Näytä tiedosto

@ -142,6 +142,26 @@ export function getSubscriptionPlanName(id: SubscriptionPlanId) {
}
}
export function getSubscriptionStatePlanName(state: SubscriptionState | undefined, id: SubscriptionPlanId | undefined) {
switch (state) {
case SubscriptionState.FreePlusTrialExpired:
return getSubscriptionPlanName(SubscriptionPlanId.FreePlus);
case SubscriptionState.FreeInPreviewTrial:
return `${getSubscriptionPlanName(SubscriptionPlanId.Pro)} (Trial)`;
case SubscriptionState.FreePlusInTrial:
return `${getSubscriptionPlanName(id ?? SubscriptionPlanId.Pro)} (Trial)`;
case SubscriptionState.VerificationRequired:
return `GitLens (Unverified)`;
case SubscriptionState.Paid:
return getSubscriptionPlanName(id ?? SubscriptionPlanId.Pro);
case SubscriptionState.Free:
case SubscriptionState.FreePreviewTrialExpired:
case null:
default:
return 'GitLens';
}
}
const plansPriority = new Map<SubscriptionPlanId | undefined, number>([
[undefined, -1],
[SubscriptionPlanId.Free, 0],
@ -191,10 +211,16 @@ export function isSubscriptionPreviewTrialExpired(subscription: Optional
return remaining != null ? remaining <= 0 : undefined;
}
export function isSubscriptionStatePaidOrTrial(state: SubscriptionState): boolean {
export function isSubscriptionStatePaidOrTrial(state: SubscriptionState | undefined): boolean {
if (state == null) return false;
return (
state === SubscriptionState.Paid ||
state === SubscriptionState.FreeInPreviewTrial ||
state === SubscriptionState.FreePlusInTrial
);
}
export function isSubscriptionStateTrial(state: SubscriptionState | undefined): boolean {
if (state == null) return false;
return state === SubscriptionState.FreeInPreviewTrial || state === SubscriptionState.FreePlusInTrial;
}

+ 26
- 25
src/webviews/apps/plus/focus/focus.html Näytä tiedosto

@ -5,37 +5,38 @@
</head>
<body class="preload" data-placement="#{placement}">
<gk-feature-gate id="subscription-gate" class="scrollable"
><p slot="feature">
Helps you focus on what's important by providing you with a comprehensive list of all your pull requests
and issues on your GitHub repos.
</p></gk-feature-gate
>
<gk-feature-gate id="connection-gate" class="scrollable" visible="false">
<h3>No GitHub remotes are connected</h3>
<p>
This enables access to Pull Requests and Issues in the Focus View as well as provide additional
information inside hovers and the Commit Details view, such as auto-linked issues and pull requests and
avatars.
</p>
<gk-button appearance="alert" href="command:gitlens.connectRemoteProvider">Connect to GitHub</gk-button>
</gk-feature-gate>
<div class="app">
<header class="app__header" id="header">
<span class="badge">Preview</span>
<span class="app__header-group">
<account-badge id="account-badge"></account-badge>
<a
href="https://github.com/gitkraken/vscode-gitlens/discussions/2535"
title="Focus View Feedback"
aria-label="Focus View Feedback"
class="feedback-button"
><code-icon icon="feedback"></code-icon
></a>
</span>
<gk-feature-gate-badge id="subscription-gate-badge"></gk-feature-gate-badge>
<gk-button
appearance="toolbar"
href="https://github.com/gitkraken/vscode-gitlens/discussions/2535"
title="Focus View Feedback"
aria-label="Focus View Feedback"
><code-icon icon="feedback"></code-icon
></gk-button>
</header>
<div class="app__content" id="content">
<gk-feature-gate id="subscription-gate" class="scrollable"
><p slot="feature">
Helps you focus on what's important by providing you with a comprehensive list of all your pull
requests and issues on your GitHub repos.
</p></gk-feature-gate
>
<gk-feature-gate id="connection-gate" class="scrollable" visible="false">
<h3>No GitHub remotes are connected</h3>
<p>
This enables access to Pull Requests and Issues in the Focus View as well as provide additional
information inside hovers and the Commit Details view, such as auto-linked issues and pull
requests and avatars.
</p>
<gk-button appearance="alert" href="command:gitlens.connectRemoteProvider"
>Connect to GitHub</gk-button
>
</gk-feature-gate>
<main class="app__main">
<section class="focus-section app__section">
<header class="focus-section__header">

+ 8
- 21
src/webviews/apps/plus/focus/focus.scss Näytä tiedosto

@ -260,30 +260,17 @@ h3 {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
// padding-right: 0;
&__header {
flex: none;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.4rem 2rem;
margin: {
left: -2rem;
right: -2rem;
}
text-align: right;
background-color: var(--background-05);
&-group {
flex: none;
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
}
display: grid;
align-items: baseline;
padding: 0rem 2rem;
margin-left: -2rem;
margin-right: -2rem;
grid-template-columns: 1fr min-content min-content;
gap: 0.5rem;
z-index: 101;
}
&__content {

+ 10
- 12
src/webviews/apps/plus/focus/focus.ts Näytä tiedosto

@ -8,22 +8,22 @@ import {
import type { IpcMessage } from '../../../protocol';
import { onIpc } from '../../../protocol';
import { App } from '../../shared/appBase';
import type { AccountBadge } from '../../shared/components/account/account-badge';
import type { FeatureGate } from '../../shared/components/feature-gate';
import type { FeatureGateBadge } from '../../shared/components/feature-gate-badge';
import { DOM } from '../../shared/dom';
import type { IssueRow } from './components/issue-row';
import type { PullRequestRow } from './components/pull-request-row';
import '../../shared/components/button';
import '../../shared/components/code-icon';
import '../../shared/components/feature-gate';
import '../../shared/components/avatars/avatar-item';
import '../../shared/components/avatars/avatar-stack';
import '../../shared/components/table/table-container';
import '../../shared/components/table/table-row';
import '../../shared/components/table/table-cell';
import '../../shared/components/account/account-badge';
import '../../shared/components/feature-gate-badge';
import './components/issue-row';
import './components/pull-request-row';
import '../../shared/components/feature-gate';
import './focus.scss';
export class FocusApp extends App<State> {
@ -82,7 +82,7 @@ export class FocusApp extends App {
switch (msg.method) {
case DidChangeNotificationType.method:
onIpc(DidChangeNotificationType, msg, params => {
this.state = { ...this.state, ...params.state };
this.state = params.state;
this.setState(this.state);
this.renderContent();
});
@ -103,13 +103,11 @@ export class FocusApp extends App {
this.state.access.allowed === true && !(this.state.repos?.some(r => r.isConnected) ?? false);
}
const badgeEl = document.getElementById('account-badge')! as AccountBadge;
badgeEl.subscription = this.state.access.subscription.current;
const $badge = document.getElementById('subscription-gate-badge')! as FeatureGateBadge;
$badge.subscription = this.state.access.subscription.current;
if (this.state.access.allowed === true) {
this.renderPullRequests();
this.renderIssues();
}
this.renderPullRequests();
this.renderIssues();
}
renderPullRequests() {
@ -121,7 +119,7 @@ export class FocusApp extends App {
const noneEl = document.getElementById('no-pull-requests')!;
const loadingEl = document.getElementById('loading-pull-requests')!;
if (this.state.pullRequests == null) {
if (this.state.pullRequests == null || this.state.access.allowed !== true) {
noneEl.setAttribute('hidden', 'true');
loadingEl.removeAttribute('hidden');
} else if (this.state.pullRequests.length === 0) {
@ -156,7 +154,7 @@ export class FocusApp extends App {
const noneEl = document.getElementById('no-issues')!;
const loadingEl = document.getElementById('loading-issues')!;
if (this.state.issues == null) {
if (this.state.issues == null || this.state.access.allowed !== true) {
noneEl.setAttribute('hidden', 'true');
loadingEl.removeAttribute('hidden');
} else if (this.state.issues.length === 0) {

+ 13
- 215
src/webviews/apps/plus/graph/GraphWrapper.tsx Näytä tiedosto

@ -18,12 +18,10 @@ import type { FormEvent, ReactElement } from 'react';
import React, { createElement, useEffect, useMemo, useRef, useState } from 'react';
import { getPlatform } from '@env/platform';
import { DateStyle } from '../../../../config';
import { RepositoryVisibility } from '../../../../git/gitProvider';
import type { SearchQuery } from '../../../../git/search';
import type {
DidEnsureRowParams,
DidSearchParams,
DismissBannerParams,
GraphAvatars,
GraphColumnName,
GraphColumnsConfig,
@ -57,12 +55,12 @@ import {
DidSearchNotificationType,
} from '../../../../plus/webviews/graph/protocol';
import type { Subscription } from '../../../../subscription';
import { getSubscriptionTimeRemaining, SubscriptionState } from '../../../../subscription';
import { pluralize } from '../../../../system/string';
import type { IpcNotificationType } from '../../../protocol';
import { MenuDivider, MenuItem, MenuLabel, MenuList } from '../../shared/components/menu/react';
import { PopMenu } from '../../shared/components/overlays/pop-menu/react';
import { PopOver } from '../../shared/components/overlays/react';
import { FeatureGate } from '../../shared/components/react/feature-gate';
import { FeatureGateBadge } from '../../shared/components/react/feature-gate-badge';
import { SearchBox } from '../../shared/components/search/react';
import type { SearchNavigationEventDetail } from '../../shared/components/search/search-box';
import type { DateTimeFormat } from '../../shared/date';
@ -96,7 +94,6 @@ export interface GraphWrapperProps {
options?: { limit?: number; more?: boolean },
) => Promise<DidSearchParams | undefined>;
onSearchOpenInView?: (search: SearchQuery) => void;
onDismissBanner?: (key: DismissBannerParams['key']) => void;
onSelectionChange?: (rows: GraphRow[]) => void;
onEnsureRowPromise?: (id: string, select: boolean) => Promise<DidEnsureRowParams | undefined>;
onExcludeType?: (key: keyof GraphExcludeTypes, value: boolean) => void;
@ -187,7 +184,6 @@ export function GraphWrapper({
onSearchPromise,
onSearchOpenInView,
onSelectionChange,
onDismissBanner,
onExcludeType,
onIncludeOnlyRef,
onUpdateGraphConfiguration,
@ -222,12 +218,7 @@ export function GraphWrapper({
const [branchName, setBranchName] = useState(state.branchName);
const [lastFetched, setLastFetched] = useState(state.lastFetched);
const [windowFocused, setWindowFocused] = useState(state.windowFocused);
// account
const [showAccount, setShowAccount] = useState(state.trialBanner);
const [isAccessAllowed, setIsAccessAllowed] = useState(state.allowed ?? false);
const [isRepoPrivate, setIsRepoPrivate] = useState(
state.selectedRepositoryVisibility === RepositoryVisibility.Private,
);
const [allowed, setAllowed] = useState(state.allowed ?? false);
const [subscription, setSubscription] = useState<Subscription | undefined>(state.subscription);
// search state
const searchEl = useRef<any>(null);
@ -311,7 +302,7 @@ export function GraphWrapper({
setIncludeOnlyRefsById(state.includeOnlyRefs);
break;
case DidChangeSubscriptionNotificationType:
setIsAccessAllowed(state.allowed ?? false);
setAllowed(state.allowed ?? false);
setSubscription(state.subscription);
break;
case DidChangeWorkingTreeNotificationType:
@ -321,7 +312,7 @@ export function GraphWrapper({
setLastFetched(state.lastFetched);
break;
default: {
setIsAccessAllowed(state.allowed ?? false);
setAllowed(state.allowed ?? false);
if (!themingChanged) {
setStyleProps(state.theming);
}
@ -345,10 +336,8 @@ export function GraphWrapper({
setPagingHasMore(state.paging?.hasMore ?? false);
setRepos(state.repositories ?? []);
setRepo(repos.find(item => item.path === state.selectedRepository));
setIsRepoPrivate(state.selectedRepositoryVisibility === RepositoryVisibility.Private);
// setGraphDateFormatter(getGraphDateFormatter(config));
setSubscription(state.subscription);
setShowAccount(state.trialBanner ?? true);
const { results, resultsError } = getSearchResultModel(state);
setSearchResultsError(resultsError);
@ -980,186 +969,6 @@ export function GraphWrapper({
onSelectionChange?.(rows);
};
const handleDismissAccount = () => {
setShowAccount(false);
onDismissBanner?.('trial');
};
const renderAccountState = () => {
if (!subscription) return;
let label = subscription.plan.effective.name;
let isPro = true;
let subText;
switch (subscription.state) {
case SubscriptionState.Free:
case SubscriptionState.FreePreviewTrialExpired:
case SubscriptionState.FreePlusTrialExpired:
isPro = false;
label = 'GitLens Free';
break;
case SubscriptionState.FreeInPreviewTrial:
case SubscriptionState.FreePlusInTrial: {
const days = getSubscriptionTimeRemaining(subscription, 'days') ?? 0;
label = 'GitLens Pro (Trial)';
subText = `${days < 1 ? '<1 day' : pluralize('day', days)} left`;
break;
}
case SubscriptionState.VerificationRequired:
isPro = false;
label = `${label} (Unverified)`;
break;
}
return (
<span className="badge-container mr-loose">
<span className="badge is-help">
<span className={`repo-access${isPro ? ' is-pro' : ''}`}></span> {label}
{subText && (
<>
&nbsp;&nbsp;
<small>{subText}</small>
</>
)}
</span>
<PopOver placement="top end" className="badge-popover">
{isPro
? 'You have access to all GitLens features on any repo.'
: 'You have access to ✨ features on local & public repos, and all other GitLens features on any repo.'}
<br />
<br /> indicates a subscription is required to use this feature on privately hosted repos.
</PopOver>
</span>
);
};
const renderAlertContent = () => {
if (subscription == null || !isRepoPrivate || (isAccessAllowed && !showAccount)) return;
let icon = 'account';
let modifier = '';
let content;
let actions;
let days = 0;
if ([SubscriptionState.FreeInPreviewTrial, SubscriptionState.FreePlusInTrial].includes(subscription.state)) {
days = getSubscriptionTimeRemaining(subscription, 'days') ?? 0;
}
switch (subscription.state) {
case SubscriptionState.Free:
case SubscriptionState.Paid:
return;
case SubscriptionState.FreeInPreviewTrial:
icon = 'calendar';
modifier = 'neutral';
content = (
<>
<p className="alert__title">GitLens Pro Trial</p>
<p className="alert__message">
You have {days < 1 ? 'less than one day' : pluralize('day', days)} left in your 3-day
GitLens Pro trial. Don't worry if you need more time, you can extend your trial for an
additional free 7-days of the Commit Graph and other{' '}
<a href="command:gitlens.plus.learn">GitLens+ features</a> on private repos.
</p>
</>
);
break;
case SubscriptionState.FreePlusInTrial:
icon = 'calendar';
modifier = 'neutral';
content = (
<>
<p className="alert__title">GitLens Pro Trial</p>
<p className="alert__message">
You have {days < 1 ? 'less than one day' : pluralize('day', days)} left in your GitLens Pro
trial. Once your trial ends, you'll continue to have access to the Commit Graph and other{' '}
<a href="command:gitlens.plus.learn">GitLens+ features</a> on local and public repos, while
upgrading to GitLens Pro gives you access on private repos.
</p>
</>
);
break;
case SubscriptionState.FreePreviewTrialExpired:
icon = 'warning';
modifier = 'warning';
content = (
<>
<p className="alert__title">Extend Your GitLens Pro Trial</p>
<p className="alert__message">
Your free 3-day GitLens Pro trial has ended, extend your trial to get an additional free
7-days of the Commit Graph and other{' '}
<a href="command:gitlens.plus.learn">GitLens+ features</a> on private repos.
</p>
</>
);
actions = (
<a className="alert-action" href="command:gitlens.plus.loginOrSignUp">
Extend Pro Trial
</a>
);
break;
case SubscriptionState.FreePlusTrialExpired:
icon = 'warning';
modifier = 'warning';
content = (
<>
<p className="alert__title">GitLens Pro Trial Expired</p>
<p className="alert__message">
Your GitLens Pro trial has ended, please upgrade to GitLens Pro to continue to use the
Commit Graph and other <a href="command:gitlens.plus.learn">GitLens+ features</a> on private
repos.
</p>
</>
);
actions = (
<a className="alert-action" href="command:gitlens.plus.purchase">
Upgrade to Pro
</a>
);
break;
case SubscriptionState.VerificationRequired:
icon = 'unverified';
modifier = 'warning';
content = (
<>
<p className="alert__title">Please verify your email</p>
<p className="alert__message">
Before you can use <a href="command:gitlens.plus.learn">GitLens+ features</a> on private
repos, please verify your email address.
</p>
</>
);
actions = (
<>
<a className="alert-action" href="command:gitlens.plus.resendVerification">
Resend Verification Email
</a>
<a className="alert-action" href="command:gitlens.plus.validate">
Refresh Verification Status
</a>
</>
);
break;
}
return (
<section className="graph-app__banners">
<div className={`alert${modifier !== '' ? ` alert--${modifier}` : ''}`}>
<span className={`alert__icon codicon codicon-${icon}`}></span>
<div className="alert__content">
{content}
{actions && <div className="alert__actions">{actions}</div>}
</div>
{isAccessAllowed && (
<button className="alert__dismiss" type="button" onClick={() => handleDismissAccount()}>
<span className="codicon codicon-chrome-close"></span>
</button>
)}
</div>
</section>
);
};
const renderFetchAction = () => {
const lastFetchedDate = lastFetched && new Date(lastFetched);
const fetchedText = lastFetchedDate && lastFetchedDate.getTime() !== 0 ? fromNow(lastFetchedDate) : undefined;
@ -1238,9 +1047,8 @@ export function GraphWrapper({
return (
<>
{renderAlertContent()}
<header className="titlebar graph-app__header">
<div className="titlebar__row titlebar__row--wrap">
<div className={`titlebar__row titlebar__row--wrap${allowed ? '' : ' disallowed'}`}>
{repo && branchState?.provider?.url && (
<a
href={branchState.provider.url}
@ -1276,7 +1084,7 @@ export function GraphWrapper({
></span>
)}
</button>
{repo && (
{allowed && repo && (
<>
<span>
<span className="codicon codicon-chevron-right"></span>
@ -1299,17 +1107,9 @@ export function GraphWrapper({
{renderFetchAction()}
</>
)}
{renderAccountState()}
<a
href="https://github.com/gitkraken/vscode-gitlens/discussions/2158"
title="Commit Graph Feedback"
aria-label="Commit Graph Feedback"
className="action-button"
>
<span className="codicon codicon-feedback"></span>
</a>
<FeatureGateBadge subscription={subscription}></FeatureGateBadge>
</div>
{isAccessAllowed && (
{allowed && (
<div className="titlebar__row">
<div className="titlebar__group">
<PopMenu>
@ -1515,6 +1315,9 @@ export function GraphWrapper({
<div className="progress-bar"></div>
</div>
</header>
<FeatureGate className="graph-app__gate" appearance="alert" state={subscription?.state} visible={!allowed}>
<p slot="feature">Easily visualize your repository and keep track of all work in progress.</p>
</FeatureGate>
{graphConfig?.minimap && (
<GraphMinimap
ref={minimap as any}
@ -1527,12 +1330,7 @@ export function GraphWrapper({
onSelected={e => handleOnMinimapDaySelected(e as CustomEvent<GraphMinimapDaySelectedEventDetail>)}
></GraphMinimap>
)}
<main
id="main"
className={`graph-app__main${!isAccessAllowed ? ' is-gated' : ''}`}
aria-hidden={!isAccessAllowed}
>
{!isAccessAllowed && <div className="graph-app__cover"></div>}
<main id="main" className="graph-app__main" aria-hidden={!allowed}>
{repo !== undefined ? (
<>
<GraphContainer

+ 9
- 10
src/webviews/apps/plus/graph/graph.scss Näytä tiedosto

@ -800,7 +800,11 @@ button:not([disabled]),
grid-auto-flow: column;
white-space: nowrap;
justify-content: start;
grid-template-columns: min-content min-content min-content min-content min-content minmax(min-content, 1fr);
grid-template-columns: repeat(5, min-content) minmax(min-content, 1fr);
&.disallowed {
grid-template-columns: repeat(1, min-content) minmax(min-content, 1fr);
}
}
}
@ -843,19 +847,14 @@ button:not([disabled]),
margin-top: 0.5rem;
}
}
&__cover {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1999;
backdrop-filter: blur(4px) saturate(0.8);
&__gate {
top: 34px; /* height of the header bar */
}
&__header {
flex: none;
z-index: 2000;
z-index: 101;
position: relative;
margin: -1px -1px 0 -1px;
}

+ 0
- 1
src/webviews/apps/plus/graph/graph.tsx Näytä tiedosto

@ -116,7 +116,6 @@ export class GraphApp extends App {
rows => this.onSelectionChanged(rows),
250,
)}
onDismissBanner={key => this.onDismissBanner(key)}
onEnsureRowPromise={this.onEnsureRowPromise.bind(this)}
onExcludeType={this.onExcludeType.bind(this)}
onIncludeOnlyRef={this.onIncludeOnlyRef.bind(this)}

+ 0
- 144
src/webviews/apps/shared/components/account/account-badge.ts Näytä tiedosto

@ -1,144 +0,0 @@
import { attr, css, customElement, FASTElement, html, observable, volatile, when } from '@microsoft/fast-element';
import type { Subscription } from '../../../../../subscription';
import { getSubscriptionTimeRemaining, SubscriptionState } from '../../../../../subscription';
import { pluralize } from '../../../../../system/string';
import { focusOutline } from '../styles/a11y';
import { elementBase } from '../styles/base';
import '../overlays/pop-over';
const template = html<AccountBadge>`
<template>
<span class="badge is-help">
<span class="repo-access ${x => (x.isPro ? 'is-pro' : '')}"></span> ${x => x.label}
</span>
${when(x => x.subText != null, html<AccountBadge>`&nbsp;&nbsp;<small>${x => x.subText}</small>`)}
<pop-over placement="${x => x.placement}" class="badge-popover">
${x => x.popoverText}
<br /><br />
indicates a subscription is required to use this feature on privately hosted repos.
</pop-over>
</template>
`;
const styles = css`
${elementBase}
:host {
position: relative;
}
:host(:focus) {
${focusOutline}
}
.badge {
font-size: 1rem;
font-weight: 700;
text-transform: uppercase;
color: var(--color-foreground);
}
.badge.is-help {
cursor: help;
}
.badge small {
font-size: inherit;
opacity: 0.6;
font-weight: 400;
}
.badge-container {
position: relative;
}
.badge-popover {
width: max-content;
right: 0;
top: 100%;
text-align: left;
}
.badge:not(:hover) ~ .badge-popover {
display: none;
}
`;
@customElement({
name: 'account-badge',
template: template,
styles: styles,
})
export class AccountBadge extends FASTElement {
@attr
placement = 'top end';
@observable
subscription?: Subscription;
@volatile
get isPro() {
if (this.subscription == null) {
return false;
}
return ![
SubscriptionState.Free,
SubscriptionState.FreePreviewTrialExpired,
SubscriptionState.FreePlusTrialExpired,
SubscriptionState.VerificationRequired,
].includes(this.subscription.state);
}
@volatile
get isTrial() {
if (this.subscription == null) {
return false;
}
return [SubscriptionState.FreeInPreviewTrial, SubscriptionState.FreePlusInTrial].includes(
this.subscription?.state,
);
}
@volatile
get label() {
if (this.subscription == null) {
return 'GitLens Free';
}
let label = this.subscription.plan.effective.name;
switch (this.subscription?.state) {
case SubscriptionState.Free:
case SubscriptionState.FreePreviewTrialExpired:
case SubscriptionState.FreePlusTrialExpired:
label = 'GitLens Free';
break;
case SubscriptionState.FreeInPreviewTrial:
case SubscriptionState.FreePlusInTrial: {
label = 'GitLens Pro (Trial)';
break;
}
case SubscriptionState.VerificationRequired:
label = `${label} (Unverified)`;
break;
}
return label;
}
@volatile
get subText() {
if (this.isTrial) {
const days = getSubscriptionTimeRemaining(this.subscription!, 'days') ?? 0;
return `${days < 1 ? '<1 day' : pluralize('day', days)} left`;
}
return undefined;
}
@volatile
get popoverText() {
return this.isPro
? 'You have access to all GitLens features on any repo.'
: 'You have access to ✨ features on local & public repos, and all other GitLens features on any repo.';
}
}

+ 8
- 2
src/webviews/apps/shared/components/button.ts Näytä tiedosto

@ -68,8 +68,9 @@ export class GKButton extends LitElement {
--button-background: transparent;
--button-foreground: var(--vscode-foreground);
--button-hover-background: var(--vscode-toolbar-hoverBackground);
--button-padding: 0.45rem 0.4rem 0.14rem 0.4rem;
--button-line-height: 1.64;
--button-padding: 0.4rem;
--button-line-height: 1.6;
--button-border: transparent;
}
:host([appearance='alert']) {
@ -81,6 +82,11 @@ export class GKButton extends LitElement {
width: max-content;
}
:host([appearance='toolbar'][href]) > a {
display: flex;
align-items: center;
}
:host([appearance='alert'][href]) > a {
display: block;
width: max-content;

+ 116
- 0
src/webviews/apps/shared/components/feature-gate-badge.ts Näytä tiedosto

@ -0,0 +1,116 @@
import { css, html, LitElement, nothing } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import type { Subscription } from '../../../../subscription';
import {
getSubscriptionStatePlanName,
getSubscriptionTimeRemaining,
isSubscriptionStatePaidOrTrial,
isSubscriptionStateTrial,
SubscriptionState,
} from '../../../../subscription';
import '../../plus/shared/components/feature-gate-plus-state';
import { pluralize } from '../../../../system/string';
import { focusOutline } from './styles/lit/a11y.css';
import { elementBase } from './styles/lit/base.css';
import './overlays/pop-over';
@customElement('gk-feature-gate-badge')
export class FeatureGateBadge extends LitElement {
static override styles = [
elementBase,
css`
:host {
position: relative;
}
:host(:focus) {
${focusOutline}
}
.badge-container {
position: relative;
}
.badge {
cursor: help;
}
.badge.inactive {
filter: grayscale(100%);
}
.badge-popover {
width: max-content;
right: 0;
top: 100%;
overflow: hidden;
text-align: left;
}
.badge-footnote {
white-space: break-spaces;
}
.badge-trial-left {
font-weight: 400;
opacity: 0.6;
margin-left: 1rem;
}
.badge:not(:hover) ~ .badge-popover {
display: none;
}
`,
];
@property()
placement?: `${'top' | 'bottom'} ${'start' | 'end'}` = 'top end';
@property({ attribute: false })
subscription?: Subscription;
override render() {
const paidOrTrial = isSubscriptionStatePaidOrTrial(this.subscription?.state);
return html`
<span class="badge-container">
<span class="badge ${paidOrTrial ? 'active' : 'inactive'}"></span>
<pop-over placement="${this.placement}" class="badge-popover">
<span slot="heading"
>${getSubscriptionStatePlanName(
this.subscription?.state,
this.subscription?.plan.effective.id,
)}${this.trialHtml}</span
>
${this.footnoteHtml}
</pop-over>
</span>
`;
}
private get trialHtml() {
if (!isSubscriptionStateTrial(this.subscription?.state)) return nothing;
const days = getSubscriptionTimeRemaining(this.subscription!, 'days') ?? 0;
return html`<span class="badge-trial-left">${days < 1 ? '<1 day' : pluralize('day', days)} left</span>`;
}
private get footnoteHtml() {
switch (this.subscription?.state) {
case SubscriptionState.VerificationRequired:
case SubscriptionState.Free:
case SubscriptionState.FreePreviewTrialExpired:
return html`<span class="badge-footnote"
> Requires a trial or subscription for use on privately hosted repos.</span
>`;
case SubscriptionState.FreePlusTrialExpired:
case SubscriptionState.FreeInPreviewTrial:
case SubscriptionState.FreePlusInTrial:
return html`<span class="badge-footnote"
> Requires a subscription for use on privately hosted repos.</span
>`;
default:
return nothing;
}
}
}

+ 15
- 12
src/webviews/apps/shared/components/feature-gate.ts Näytä tiedosto

@ -24,7 +24,8 @@ export class FeatureGate extends LitElement {
box-sizing: border-box;
}
:host-context(body[data-placement='editor']) {
:host-context(body[data-placement='editor']),
:host([appearance='alert']) {
--background: transparent;
--foreground: var(--vscode-editor-foreground);
@ -55,7 +56,8 @@ export class FeatureGate extends LitElement {
height: min-content;
}
:host-context(body[data-placement='editor']) section {
:host-context(body[data-placement='editor']) section,
:host([appearance='alert']) section {
--section-foreground: var(--color-alert-foreground);
--section-background: var(--color-alert-infoBackground);
--section-border-color: var(--color-alert-infoBorder);
@ -71,40 +73,41 @@ export class FeatureGate extends LitElement {
padding: 1.3rem;
}
:host-context(body[data-placement='editor']) section ::slotted(gk-button) {
:host-context(body[data-placement='editor']) section ::slotted(gk-button),
:host([appearance='alert']) section ::slotted(gk-button) {
display: block;
margin-left: auto;
margin-right: auto;
}
`;
@property()
appearance?: 'alert' | 'welcome';
@property({ attribute: false, type: Number })
state?: SubscriptionState;
@property({ type: Boolean })
visible?: boolean;
@property({ reflect: true })
get appearance() {
return (document.body.getAttribute('data-placement') ?? 'editor') === 'editor' ? 'alert' : 'welcome';
}
override render() {
if (!this.visible || (this.state != null && isSubscriptionStatePaidOrTrial(this.state))) {
this.hidden = true;
return undefined;
}
const appearance =
this.appearance ?? (document.body.getAttribute('data-placement') ?? 'editor') === 'editor'
? 'alert'
: 'welcome';
this.hidden = false;
return html`
<section>
<slot>
<slot name="feature" hidden=${this.state === SubscriptionState.Free ? nothing : ''}></slot>
</slot>
<gk-feature-gate-plus-state
appearance=${this.appearance}
.state=${this.state}
></gk-feature-gate-plus-state>
<gk-feature-gate-plus-state appearance=${appearance} .state=${this.state}></gk-feature-gate-plus-state>
</section>
`;
}

+ 7
- 0
src/webviews/apps/shared/components/react/feature-gate-badge.tsx Näytä tiedosto

@ -0,0 +1,7 @@
import { FeatureGateBadge as featureGateBadgeComponent } from '../feature-gate-badge';
import { reactWrapper } from '../helpers/react-wrapper';
export const FeatureGateBadge = reactWrapper(featureGateBadgeComponent, {
name: 'gk-feature-gate-badge',
properties: ['placement', 'subscription'],
});

+ 7
- 0
src/webviews/apps/shared/components/react/feature-gate.tsx Näytä tiedosto

@ -0,0 +1,7 @@
import { FeatureGate as featureGateComponent } from '../feature-gate';
import { reactWrapper } from '../helpers/react-wrapper';
export const FeatureGate = reactWrapper(featureGateComponent, {
name: 'gk-feature-gate',
properties: ['state', 'visible'],
});

+ 1
- 1
walkthroughs/plus/commit-graph.md Näytä tiedosto

@ -4,6 +4,6 @@
<img src="../../images/docs/commit-graph-illustrated.png" alt="Commit Graph"/>
</p>
The Commit Graph helps you easily visualize and keep track of all work in progress. Not only does it help you verify your changes, but also easily see changes made by others and when.
The Commit Graph helps you easily visualize your repository and keep track of all work in progress. Not only does it help you verify your changes, but also easily see changes made by others and when.
Use the rich commit search to find exactly what you're looking for. It's powerful filters allow you to search by a specific commit, message, author, a changed file or files, or even a specific code change.

Ladataan…
Peruuta
Tallenna