Skip to content

Commit

Permalink
Rich azure integration basics (#3976) (#3984)
Browse files Browse the repository at this point in the history
* Conects and syncs to AzureDevOps
(#3976, #3984)

* Shows Azure rich integration status in remotes, lets connect/disconnect
(#3976, #3984)

* Checks the Azure's domain and only return if it's non-enterprise
(#3976, #3984)
  • Loading branch information
sergeibbb authored Jan 28, 2025
1 parent d1f6477 commit bf589c2
Show file tree
Hide file tree
Showing 4 changed files with 22 additions and 110 deletions.
7 changes: 7 additions & 0 deletions src/constants.integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const supportedOrderedCloudIntegrationIds = [
SelfHostedIntegrationId.CloudGitHubEnterprise,
HostingIntegrationId.GitLab,
SelfHostedIntegrationId.CloudGitLabSelfHosted,
HostingIntegrationId.AzureDevOps,
IssueIntegrationId.Jira,
];

Expand Down Expand Up @@ -71,6 +72,12 @@ export const supportedCloudIntegrationDescriptors: IntegrationDescriptor[] = [
icon: 'gl-provider-gitlab',
supports: ['prs', 'issues'],
},
{
id: HostingIntegrationId.AzureDevOps,
name: 'Azure DevOps',
icon: 'gl-provider-azdo',
supports: ['prs', 'issues'],
},
{
id: IssueIntegrationId.Jira,
name: 'Jira',
Expand Down
108 changes: 2 additions & 106 deletions src/plus/integrations/authentication/azureDevOps.ts
Original file line number Diff line number Diff line change
@@ -1,112 +1,8 @@
import type { Disposable, QuickInputButton } from 'vscode';
import { env, ThemeIcon, Uri, window } from 'vscode';
import { HostingIntegrationId } from '../../../constants.integrations';
import { base64 } from '../../../system/string';
import type { IntegrationAuthenticationSessionDescriptor } from './integrationAuthenticationProvider';
import { LocalIntegrationAuthenticationProvider } from './integrationAuthenticationProvider';
import type { ProviderAuthenticationSession } from './models';
import { CloudIntegrationAuthenticationProvider } from './integrationAuthenticationProvider';

export class AzureDevOpsAuthenticationProvider extends LocalIntegrationAuthenticationProvider<HostingIntegrationId.AzureDevOps> {
export class AzureDevOpsAuthenticationProvider extends CloudIntegrationAuthenticationProvider<HostingIntegrationId.AzureDevOps> {
protected override get authProviderId(): HostingIntegrationId.AzureDevOps {
return HostingIntegrationId.AzureDevOps;
}

override async createSession(
descriptor: IntegrationAuthenticationSessionDescriptor,
): Promise<ProviderAuthenticationSession | undefined> {
let azureOrganization: string | undefined = descriptor.organization as string | undefined;
if (!azureOrganization) {
const orgInput = window.createInputBox();
orgInput.ignoreFocusOut = true;
const orgInputDisposables: Disposable[] = [];
try {
azureOrganization = await new Promise<string | undefined>(resolve => {
orgInputDisposables.push(
orgInput.onDidHide(() => resolve(undefined)),
orgInput.onDidChangeValue(() => (orgInput.validationMessage = undefined)),
orgInput.onDidAccept(() => {
const value = orgInput.value.trim();
if (!value) {
orgInput.validationMessage = 'An organization is required';
return;
}

resolve(value);
}),
);

orgInput.title = `Azure DevOps Authentication \u2022 ${descriptor.domain}`;
orgInput.placeholder = 'Organization';
orgInput.prompt = 'Enter your Azure DevOps organization';
orgInput.show();
});
} finally {
orgInput.dispose();
orgInputDisposables.forEach(d => void d.dispose());
}
}

if (!azureOrganization) return undefined;

const tokenInput = window.createInputBox();
tokenInput.ignoreFocusOut = true;

const disposables: Disposable[] = [];

let token;
try {
const infoButton: QuickInputButton = {
iconPath: new ThemeIcon(`link-external`),
tooltip: 'Open the Azure DevOps Access Tokens Page',
};

token = await new Promise<string | undefined>(resolve => {
disposables.push(
tokenInput.onDidHide(() => resolve(undefined)),
tokenInput.onDidChangeValue(() => (tokenInput.validationMessage = undefined)),
tokenInput.onDidAccept(() => {
const value = tokenInput.value.trim();
if (!value) {
tokenInput.validationMessage = 'A personal access token is required';
return;
}

resolve(value);
}),
tokenInput.onDidTriggerButton(e => {
if (e === infoButton) {
void env.openExternal(
Uri.parse(`https://${descriptor.domain}/${azureOrganization}/_usersSettings/tokens`),
);
}
}),
);

tokenInput.password = true;
tokenInput.title = `Azure DevOps Authentication \u2022 ${descriptor.domain}`;
tokenInput.placeholder = `Requires ${descriptor.scopes.join(', ') ?? 'all'} scopes`;
tokenInput.prompt = `Paste your [Azure DevOps Personal Access Token](https://${descriptor.domain}/${azureOrganization}/_usersSettings/tokens "Get your Azure DevOps Access Token")`;
tokenInput.buttons = [infoButton];

tokenInput.show();
});
} finally {
tokenInput.dispose();
disposables.forEach(d => void d.dispose());
}

if (!token) return undefined;

return {
id: this.configuredIntegrationService.getSessionId(descriptor),
accessToken: base64(`:${token}`),
scopes: descriptor.scopes,
account: {
id: '',
label: '',
},
cloud: false,
domain: descriptor.domain,
};
}
}
12 changes: 8 additions & 4 deletions src/plus/integrations/integrationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import type {
SupportedIssueIntegrationIds,
SupportedSelfHostedIntegrationIds,
} from './integration';
import { isAzureCloudDomain } from './providers/azureDevOps';
import { isCloudSelfHostedIntegrationId, isHostingIntegrationId, isSelfHostedIntegrationId } from './providers/models';
import type { ProvidersApi } from './providers/providersApi';
import { isGitHubDotCom, isGitLabDotCom } from './providers/utils';
Expand Down Expand Up @@ -668,10 +669,13 @@ export class IntegrationService implements Disposable {

switch (remote.provider.id) {
// TODO: Uncomment when we support these integrations
// case 'azure-devops':
// return get(HostingIntegrationId.AzureDevOps) as RT;
// case 'bitbucket':
// return get(HostingIntegrationId.Bitbucket) as RT;
case 'azure-devops':
if (isAzureCloudDomain(remote.provider.domain)) {
return get(HostingIntegrationId.AzureDevOps) as RT;
}
return (getOrGetCached === this.get ? Promise.resolve(undefined) : undefined) as RT;
case 'github':
if (remote.provider.domain != null && !isGitHubDotCom(remote.provider.domain)) {
return get(
Expand Down Expand Up @@ -1030,10 +1034,10 @@ export function remoteProviderIdToIntegrationId(
): SupportedCloudIntegrationIds | undefined {
switch (remoteProviderId) {
// TODO: Uncomment when we support these integrations
// case 'azure-devops':
// return HostingIntegrationId.AzureDevOps;
// case 'bitbucket':
// return HostingIntegrationId.Bitbucket;
case 'azure-devops':
return HostingIntegrationId.AzureDevOps;
case 'github':
return HostingIntegrationId.GitHub;
case 'gitlab':
Expand Down
5 changes: 5 additions & 0 deletions src/plus/integrations/providers/azureDevOps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,8 @@ export class AzureDevOpsIntegration extends HostingIntegration<
return Promise.resolve(undefined);
}
}

const azureCloudDomainRegex = /^dev\.azure\.com$|\bvisualstudio\.com$/i;
export function isAzureCloudDomain(domain: string | undefined): boolean {
return domain != null && azureCloudDomainRegex.test(domain);
}

0 comments on commit bf589c2

Please sign in to comment.