Browse Source

Adds gating to the commit graph

main
Keith Daulton 2 years ago
parent
commit
d40056c1c1
6 changed files with 329 additions and 33 deletions
  1. +41
    -5
      src/plus/webviews/graph/graphWebview.ts
  2. +13
    -0
      src/plus/webviews/graph/protocol.ts
  3. +150
    -11
      src/webviews/apps/plus/graph/GraphWrapper.tsx
  4. +71
    -7
      src/webviews/apps/plus/graph/graph.scss
  5. +14
    -0
      src/webviews/apps/plus/graph/graph.tsx
  6. +40
    -10
      src/webviews/apps/shared/theme.ts

+ 41
- 5
src/plus/webviews/graph/graphWebview.ts View File

@ -7,6 +7,7 @@ import { configuration } from '../../../configuration';
import { Commands, ContextKeys } from '../../../constants';
import type { Container } from '../../../container';
import { setContext } from '../../../context';
import { PlusFeatures } from '../../../features';
import type { GitCommit } from '../../../git/models/commit';
import type { GitGraph } from '../../../git/models/graph';
import type { Repository, RepositoryChangeEvent } from '../../../git/models/repository';
@ -22,12 +23,14 @@ import { RepositoryFolderNode } from '../../../views/nodes/viewNode';
import type { IpcMessage } from '../../../webviews/protocol';
import { onIpc } from '../../../webviews/protocol';
import { WebviewBase } from '../../../webviews/webviewBase';
import type { SubscriptionChangeEvent } from '../../subscription/subscriptionService';
import { ensurePlusFeaturesEnabled } from '../../subscription/utils';
import type { GraphCompositeConfig, GraphRepository, State } from './protocol';
import {
DidChangeCommitsNotificationType,
DidChangeGraphConfigurationNotificationType,
DidChangeNotificationType,
DidChangeSubscriptionNotificationType,
DismissPreviewCommandType,
GetMoreCommitsCommandType,
UpdateColumnCommandType,
@ -69,6 +72,7 @@ export class GraphWebview extends WebviewBase {
return this._selection;
}
private _etagSubscription?: number;
private _etagRepository?: number;
private _repositoryEventsDisposable: Disposable | undefined;
private _repositoryGraph?: GitGraph;
@ -88,12 +92,16 @@ export class GraphWebview extends WebviewBase {
'graphWebview',
Commands.ShowGraphPage,
);
this.disposables.push(configuration.onDidChange(this.onConfigurationChanged, this), {
dispose: () => {
this._statusBarItem?.dispose();
void this._repositoryEventsDisposable?.dispose();
this.disposables.push(
configuration.onDidChange(this.onConfigurationChanged, this),
{
dispose: () => {
this._statusBarItem?.dispose();
void this._repositoryEventsDisposable?.dispose();
},
},
});
this.container.subscription.onDidChange(this.onSubscriptionChanged, this),
);
this.onConfigurationChanged();
}
@ -232,6 +240,13 @@ export class GraphWebview extends WebviewBase {
this.updateState();
}
private onSubscriptionChanged(e: SubscriptionChangeEvent) {
if (e.etag === this._etagSubscription) return;
this._etagSubscription = e.etag;
void this.notifyDidChangeSubscription();
}
private onThemeChanged(theme: ColorTheme) {
if (this._theme != null) {
if (
@ -341,6 +356,17 @@ export class GraphWebview extends WebviewBase {
}
@debug()
private async notifyDidChangeSubscription() {
if (!this.isReady || !this.visible) return false;
const access = await this.container.git.access(PlusFeatures.Graph, this.repository?.path);
return this.notify(DidChangeSubscriptionNotificationType, {
subscription: access.subscription.current,
allowed: access.allowed,
});
}
@debug()
private async notifyDidChangeCommits() {
if (!this.isReady || !this.visible) return false;
@ -385,6 +411,13 @@ export class GraphWebview extends WebviewBase {
// If we have a set of data refresh to the same set
const limit = this._repositoryGraph?.paging?.limit ?? config.defaultItemLimit;
// only check on private
const access = await this.container.git.access(PlusFeatures.Graph, this.repository?.path);
// TODO: probably not the right place to set this
if (this._etagSubscription == null) {
this._etagSubscription = this.container.subscription.etag;
}
const data = await this.container.git.getCommitsForGraph(
this.repository.path,
this._panel!.webview.asWebviewUri,
@ -396,6 +429,9 @@ export class GraphWebview extends WebviewBase {
previewBanner: this.previewBanner,
repositories: formatRepositories(this.container.git.openRepositories),
selectedRepository: this.repository.path,
selectedVisibility: access.visibility,
subscription: access.subscription.current,
allowed: access.allowed,
rows: data.rows,
paging: {
startingCursor: data.paging?.startingCursor,

+ 13
- 0
src/plus/webviews/graph/protocol.ts View File

@ -1,10 +1,15 @@
import type { CommitType, GraphRow, Remote } from '@gitkraken/gitkraken-components';
import type { GraphColumnConfig, GraphConfig } from '../../../config';
import type { RepositoryVisibility } from '../../../git/gitProvider';
import type { Subscription } from '../../../subscription';
import { IpcCommandType, IpcNotificationType } from '../../../webviews/protocol';
export interface State {
repositories?: GraphRepository[];
selectedRepository?: string;
selectedVisibility?: RepositoryVisibility;
subscription?: Subscription;
allowed?: boolean;
rows?: GraphRow[];
paging?: GraphPaging;
config?: GraphCompositeConfig;
@ -94,6 +99,14 @@ export const DidChangeGraphConfigurationNotificationType = new IpcNotificationTy
'graph/configuration/didChange',
);
export interface DidChangeSubscriptionParams {
subscription: Subscription;
allowed: boolean;
}
export const DidChangeSubscriptionNotificationType = new IpcNotificationType<DidChangeSubscriptionParams>(
'graph/subscription/didChange',
);
export interface DidChangeCommitsParams {
rows: GraphRow[];
paging?: GraphPaging;

+ 150
- 11
src/webviews/apps/plus/graph/GraphWrapper.tsx View File

@ -14,6 +14,9 @@ import type {
GraphRepository,
State,
} from '../../../../plus/webviews/graph/protocol';
import type { Subscription } from '../../../../subscription';
import { SubscriptionState } from '../../../../subscription';
import { fromNow } from '../../shared/date';
export interface GraphWrapperProps extends State {
nonce?: string;
@ -114,6 +117,8 @@ export function GraphWrapper({
repositories = [],
rows = [],
selectedRepository,
subscription,
allowed,
config,
paging,
onSelectRepository,
@ -139,7 +144,12 @@ export function GraphWrapper({
const [mainWidth, setMainWidth] = useState<number>();
const [mainHeight, setMainHeight] = useState<number>();
const mainRef = useRef<HTMLElement>(null);
const [showBanner, setShowBanner] = useState(previewBanner);
// banner
const [showPreview, setShowPreview] = useState(previewBanner);
// account
const [showAccount, setShowAccount] = useState(true);
const [isAllowed, setIsAllowed] = useState(allowed ?? false);
const [subscriptionSnapshot, setSubscriptionSnapshot] = useState<Subscription | undefined>(subscription);
// repo selection UI
const [repoExpanded, setRepoExpanded] = useState(false);
@ -173,6 +183,8 @@ export function GraphWrapper({
setPagingState(state.paging);
setIsLoading(false);
setStyleProps(getStyleProps(state.mixedColumnColors));
setIsAllowed(state.allowed ?? false);
setSubscriptionSnapshot(state.subscription);
}
useEffect(() => {
@ -207,15 +219,135 @@ export function GraphWrapper({
onSelectionChange?.(graphRows.map(r => r.sha));
};
const handleDismissBanner = () => {
setShowBanner(false);
const handleDismissPreview = () => {
setShowPreview(false);
onDismissPreview?.();
};
const handleDismissAccount = () => {
setShowAccount(false);
};
const renderAlertContent = () => {
if (subscriptionSnapshot == null) return;
let icon = 'account';
let modifier = '';
let content;
let actions;
switch (subscriptionSnapshot.state) {
case SubscriptionState.Free:
case SubscriptionState.Paid:
return;
case SubscriptionState.FreeInPreview:
icon = 'calendar';
modifier = 'neutral';
content = (
<>
<p className="alert__title">Trial Preview</p>
<p className="alert__message">
You're able to view the Commit Graph with any repository until your preview expires
{subscriptionSnapshot.previewTrial
? ` ${fromNow(new Date(subscriptionSnapshot.previewTrial.expiresOn))}`
: ''}
.
</p>
</>
);
break;
case SubscriptionState.FreePreviewExpired:
icon = 'warning';
modifier = 'warning';
content = (
<>
<p className="alert__title">Extend Your Trial</p>
<p className="alert__message">Sign in to extend your free trial an additional 7-days.</p>
</>
);
actions = (
<>
<a className="alert-action" href="command:gitlens.plus.loginOrSignUp">
Try for 7-days
</a>{' '}
<a className="alert-action" href="command:gitlens.plus.purchase">
View Plans
</a>
</>
);
break;
case SubscriptionState.FreePlusInTrial:
icon = 'calendar';
modifier = 'neutral';
content = (
<>
<p className="alert__title">Extended Trial</p>
<p className="alert__message">
You're able to view the Commit Graph with any repository until your trial expires
{subscriptionSnapshot.previewTrial
? ` ${fromNow(new Date(subscriptionSnapshot.previewTrial.expiresOn))}`
: ''}
.
</p>
</>
);
break;
case SubscriptionState.FreePlusTrialExpired:
icon = 'warning';
modifier = 'warning';
content = (
<>
<p className="alert__title">Trial Expired</p>
<p className="alert__message">
Upgrade your account to use the Commit Graph and other GitLens+ features on private repos.
</p>
<p>
<a className="alert-action" href="command:gitlens.plus.purchase">
Upgrade Your Account
</a>
</p>
</>
);
break;
case SubscriptionState.VerificationRequired:
icon = 'unverified';
modifier = 'warning';
content = (
<>
<p className="alert__title">Please verify your email</p>
<p className="alert__message">Please verify the email for the account you created.</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 (
<div className={`alert${modifier !== '' ? ` alert--${modifier}` : ''}`}>
<span className={`alert__icon codicon codicon-${icon}`}></span>
<div className="alert__content">{content}</div>
{actions && <div className="alert__actions">{actions}</div>}
{isAllowed && (
<button className="alert__dismiss" type="button" onClick={() => handleDismissAccount()}>
<span className="codicon codicon-chrome-close"></span>
</button>
)}
</div>
);
};
return (
<>
{showBanner && (
<section className="graph-app__banner">
<section className="graph-app__banners">
{showPreview && (
<div className="alert">
<span className="alert__icon codicon codicon-search"></span>
<div className="alert__content">
@ -230,13 +362,20 @@ export function GraphWrapper({
.
</p>
</div>
<button className="alert__action" type="button" onClick={() => handleDismissBanner()}>
<button className="alert__dismiss" type="button" onClick={() => handleDismissPreview()}>
<span className="codicon codicon-chrome-close"></span>
</button>
</div>
</section>
)}
<main ref={mainRef} id="main" className="graph-app__main">
)}
{showAccount && renderAlertContent()}
</section>
<main
ref={mainRef}
id="main"
className={`graph-app__main${!isAllowed ? ' is-gated' : ''}`}
aria-hidden={!isAllowed}
>
{!isAllowed && <div className="graph-app__cover"></div>}
{currentRepository !== undefined ? (
<>
{mainWidth !== undefined && mainHeight !== undefined && (
@ -263,7 +402,7 @@ export function GraphWrapper({
<p>No repository is selected</p>
)}
</main>
<footer className="actionbar graph-app__footer">
<footer className={`actionbar graph-app__footer${!isAllowed ? ' is-gated' : ''}`} aria-hidden={!isAllowed}>
<div className="actionbar__group">
<div className="actioncombo">
<button
@ -321,7 +460,7 @@ export function GraphWrapper({
)}
</div>
</div>
{graphList.length > 0 && (
{isAllowed && graphList.length > 0 && (
<span className="actionbar__details">
showing {graphList.length} item{graphList.length ? 's' : ''}
</span>

+ 71
- 7
src/webviews/apps/plus/graph/graph.scss View File

@ -142,9 +142,10 @@ a {
}
.alert {
--alert-foreground: var(--vscode-input-foreground);
--alert-background: var(--vscode-inputValidation-infoBackground);
--alert-border-color: var(--vscode-inputValidation-infoBorder);
--alert-foreground: var(--color-alert-foreground);
--alert-background: var(--color-alert-infoBackground);
--alert-border-color: var(--color-alert-infoBorder);
--alert-hover-background: var(--color-alert-infoHoverBackground);
display: flex;
flex-direction: row;
justify-content: flex-start;
@ -160,7 +161,11 @@ a {
}
&__content {
flex: 1;
padding-top: 0.24rem;
> :last-child {
margin-bottom: 0;
}
}
&__title {
font-size: 1.3rem;
@ -177,7 +182,11 @@ a {
margin-top: 0.25rem;
}
&__action {
&__actions {
align-self: center;
}
&__dismiss {
border: 1px solid transparent;
background-color: transparent;
color: inherit;
@ -185,8 +194,44 @@ a {
width: 2rem;
height: 2rem;
padding: 0;
cursor: pointer;
margin-left: auto;
}
&--warning {
--alert-background: var(--color-alert-warningBackground);
--alert-border-color: var(--color-alert-warningBorder);
--alert-hover-background: var(--color-alert-warningHoverBackground);
}
&--error {
--alert-background: var(--color-alert-errorBackground);
--alert-border-color: var(--color-alert-errorBorder);
--alert-hover-background: var(--color-alert-errorHoverBackground);
}
&--neutral {
--alert-background: var(--color-alert-neutralBackground);
--alert-border-color: var(--color-alert-neutralBorder);
--alert-hover-background: var(--color-alert-neutralHoverBackground);
}
}
.alert-action {
display: inline-block;
padding: 0.4rem 0.8rem;
font-family: inherit;
font-size: inherit;
line-height: 1.4;
text-align: center;
text-decoration: none;
user-select: none;
background: transparent;
color: var(--alert-foreground);
cursor: pointer;
border: 1px solid var(--alert-border-color);
border-radius: 0.2rem;
&:hover {
text-decoration: none;
color: var(--alert-foreground);
background-color: var(--alert-hover-background);
}
}
@ -307,9 +352,23 @@ a {
padding: 0 2px;
}
&__banner {
&__banners {
flex: none;
padding: 0.5rem;
z-index: 2000;
> *:not(:first-child) {
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);
}
&__footer {
@ -320,6 +379,11 @@ a {
flex: 1 1 auto;
overflow: hidden;
}
&__main.is-gated {
position: relative;
pointer-events: none;
}
}
::-webkit-scrollbar-thumb {

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

@ -8,6 +8,7 @@ import {
DidChangeCommitsNotificationType,
DidChangeGraphConfigurationNotificationType,
DidChangeNotificationType,
DidChangeSubscriptionNotificationType,
DismissPreviewCommandType,
GetMoreCommitsCommandType,
UpdateColumnCommandType,
@ -154,6 +155,19 @@ export class GraphApp extends App {
});
break;
case DidChangeSubscriptionNotificationType.method:
this.log(`${this.appName}.onMessageReceived(${msg.id}): name=${msg.method}`);
onIpc(DidChangeSubscriptionNotificationType, msg, params => {
this.setState({
...this.state,
subscription: params.subscription,
allowed: params.allowed,
});
this.refresh(this.state);
});
break;
default:
super.onMessageReceived?.(e);
}

+ 40
- 10
src/webviews/apps/shared/theme.ts View File

@ -6,6 +6,10 @@ export function initializeAndWatchThemeColors(callback?: () => void) {
const body = document.body;
const computedStyle = window.getComputedStyle(body);
const isLightTheme =
body.classList.contains('vscode-light') || body.classList.contains('vscode-high-contrast-light');
const isHighContrastTheme = body.classList.contains('vscode-high-contrast');
const bodyStyle = body.style;
bodyStyle.setProperty('--font-family', computedStyle.getPropertyValue('--vscode-font-family').trim());
@ -42,17 +46,15 @@ export function initializeAndWatchThemeColors(callback?: () => void) {
color = computedStyle.getPropertyValue('--vscode-button-background').trim();
bodyStyle.setProperty('--color-button-background', color);
bodyStyle.setProperty('--color-button-background--darken-30', darken(color, 30));
color = computedStyle.getPropertyValue('--vscode-button-secondaryBackground').trim();
bodyStyle.setProperty('--color-button-secondary-background', color);
bodyStyle.setProperty('--color-button-secondary-background--darken-30', darken(color, 30));
color = computedStyle.getPropertyValue('--vscode-button-background').trim();
bodyStyle.setProperty('--color-highlight', color);
bodyStyle.setProperty('--color-highlight--75', opacity(color, 75));
bodyStyle.setProperty('--color-highlight--50', opacity(color, 50));
bodyStyle.setProperty('--color-highlight--25', opacity(color, 25));
color = computedStyle.getPropertyValue('--vscode-button-secondaryBackground').trim();
bodyStyle.setProperty('--color-button-secondary-background', color);
bodyStyle.setProperty('--color-button-secondary-background--darken-30', darken(color, 30));
color = computedStyle.getPropertyValue('--vscode-button-foreground').trim();
bodyStyle.setProperty('--color-button-foreground', color);
@ -97,12 +99,40 @@ export function initializeAndWatchThemeColors(callback?: () => void) {
bodyStyle.setProperty('--color-hover-statusBarBackground', color);
// graph-specific colors
const isLightTheme =
body.className.includes('vscode-light') || body.className.includes('vscode-high-contrast-light');
color = computedStyle.getPropertyValue('--vscode-editor-background').trim();
bodyStyle.setProperty('--graph-panel-bg', isLightTheme ? darken(color, 5) : lighten(color, 5));
bodyStyle.setProperty(
'--graph-panel-bg',
isLightTheme ? darken(backgroundColor, 5) : lighten(backgroundColor, 5),
);
bodyStyle.setProperty('--graph-theme-opacity-factor', isLightTheme ? '0.5' : '1');
// alert colors
color = computedStyle.getPropertyValue('--vscode-inputValidation-infoBackground').trim();
bodyStyle.setProperty('--color-alert-infoHoverBackground', isLightTheme ? darken(color, 5) : lighten(color, 5));
bodyStyle.setProperty('--color-alert-infoBackground', color);
color = computedStyle.getPropertyValue('--vscode-inputValidation-warningBackground').trim();
bodyStyle.setProperty(
'--color-alert-warningHoverBackground',
isLightTheme ? darken(color, 5) : lighten(color, 5),
);
bodyStyle.setProperty('--color-alert-warningBackground', color);
color = computedStyle.getPropertyValue('--vscode-inputValidation-errorBackground').trim();
bodyStyle.setProperty(
'--color-alert-errorHoverBackground',
isLightTheme ? darken(color, 5) : lighten(color, 5),
);
bodyStyle.setProperty('--color-alert-errorBackground', color);
color = computedStyle.getPropertyValue('--vscode-input-background').trim();
bodyStyle.setProperty(
'--color-alert-neutralHoverBackground',
isLightTheme ? darken(color, 5) : lighten(color, 5),
);
bodyStyle.setProperty('--color-alert-neutralBackground', color);
bodyStyle.setProperty('--color-alert-infoBorder', 'var(--vscode-inputValidation-infoBorder)');
bodyStyle.setProperty('--color-alert-warningBorder', 'var(--vscode-inputValidation-warningBorder)');
bodyStyle.setProperty('--color-alert-errorBorder', 'var(--vscode-inputValidation-errorBorder)');
bodyStyle.setProperty('--color-alert-neutralBorder', 'var(--vscode-input-foreground)');
bodyStyle.setProperty('--color-alert-foreground', 'var(--vscode-input-foreground)');
callback?.();
};

Loading…
Cancel
Save