From c0ad00bd6b5d23c8d2d58671b081307268bf1bc8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 19 Sep 2025 21:40:10 +0000
Subject: [PATCH 1/6] Initial plan
From f61b4acfd3d0da618df00aa2d61ee6527b42de37 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 19 Sep 2025 21:43:50 +0000
Subject: [PATCH 2/6] Initial analysis and plan for enterprise type support in
TEAMS tab
Co-authored-by: liguori <8238828+liguori@users.noreply.github.com>
---
package-lock.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package-lock.json b/package-lock.json
index bef656e1..6c33ef92 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,7 +17,7 @@
"nuxt-auth-utils": "^0.5.7",
"roboto-fontface": "^0.10.0",
"undici": ">=7.5.0",
- "vue": "*",
+ "vue": "latest",
"vue-chartjs": "^5.3.2",
"vuetify": "^3.7.3",
"webfontloader": "^1.6.28"
From 45be598bc0986889924cc398018228af576a060a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 19 Sep 2025 21:52:04 +0000
Subject: [PATCH 3/6] Implement enterprise type support for TEAMS tab
Co-authored-by: liguori <8238828+liguori@users.noreply.github.com>
---
.env | 5 ++
app/model/Options.ts | 53 ++++++++++++-
nuxt.config.ts | 1 +
server/api/teams.ts | 93 ++++++++++++++++++-----
tests/enterprise-type.spec.ts | 136 ++++++++++++++++++++++++++++++++++
5 files changed, 267 insertions(+), 21 deletions(-)
create mode 100644 tests/enterprise-type.spec.ts
diff --git a/.env b/.env
index ac0c1a85..fdbf2a3d 100644
--- a/.env
+++ b/.env
@@ -10,6 +10,11 @@ NUXT_PUBLIC_GITHUB_ORG=octodemo
NUXT_PUBLIC_GITHUB_ENT=
+# Determines the GitHub Enterprise type for enterprise-scoped deployments.
+# Can be 'full' (GitHub Enterprise with full features) or 'copilot-only' (Copilot Business Only).
+# This affects how teams are retrieved and team metrics are accessed.
+NUXT_PUBLIC_ENTERPRISE_TYPE=copilot-only
+
# Determines the team name if exists to target API calls.
NUXT_PUBLIC_GITHUB_TEAM=
diff --git a/app/model/Options.ts b/app/model/Options.ts
index 9d64face..e356ac81 100644
--- a/app/model/Options.ts
+++ b/app/model/Options.ts
@@ -6,6 +6,7 @@ import type { QueryObject } from 'ufo';
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router';
export type Scope = 'organization' | 'enterprise' | 'team-organization' | 'team-enterprise';
+export type EnterpriseType = 'full' | 'copilot-only';
export interface OptionsData {
since?: string;
@@ -15,6 +16,7 @@ export interface OptionsData {
githubEnt?: string;
githubTeam?: string;
scope?: Scope;
+ enterpriseType?: EnterpriseType;
excludeHolidays?: boolean;
locale?: string;
}
@@ -25,6 +27,7 @@ export interface RuntimeConfig {
githubOrg?: string;
githubEnt?: string;
githubTeam?: string;
+ enterpriseType?: string;
isDataMocked?: boolean;
};
}
@@ -54,6 +57,7 @@ export class Options {
public githubEnt?: string;
public githubTeam?: string;
public scope?: Scope;
+ public enterpriseType?: EnterpriseType;
public excludeHolidays?: boolean;
public locale?: string;
@@ -65,6 +69,7 @@ export class Options {
this.githubEnt = data.githubEnt;
this.githubTeam = data.githubTeam;
this.scope = data.scope;
+ this.enterpriseType = data.enterpriseType;
this.excludeHolidays = data.excludeHolidays;
this.locale = data.locale;
}
@@ -109,6 +114,7 @@ export class Options {
if (config.public.githubOrg) options.githubOrg = config.public.githubOrg;
if (config.public.githubEnt) options.githubEnt = config.public.githubEnt;
if (config.public.githubTeam) options.githubTeam = config.public.githubTeam;
+ if (config.public.enterpriseType) options.enterpriseType = config.public.enterpriseType as EnterpriseType;
}
return options;
@@ -126,6 +132,7 @@ export class Options {
githubEnt: params.get('githubEnt') || undefined,
githubTeam: params.get('githubTeam') || undefined,
scope: (params.get('scope') as Scope) || undefined,
+ enterpriseType: (params.get('enterpriseType') as EnterpriseType) || undefined,
locale: params.get('locale') || undefined
});
@@ -149,6 +156,7 @@ export class Options {
githubEnt: query.githubEnt as string | undefined,
githubTeam: query.githubTeam as string | undefined,
scope: (query.scope as Scope) || undefined,
+ enterpriseType: (query.enterpriseType as EnterpriseType) || undefined,
locale: query.locale as string | undefined
});
@@ -184,6 +192,7 @@ export class Options {
if (this.githubEnt) params.set('githubEnt', this.githubEnt);
if (this.githubTeam) params.set('githubTeam', this.githubTeam);
if (this.scope) params.set('scope', this.scope);
+ if (this.enterpriseType) params.set('enterpriseType', this.enterpriseType);
if (this.excludeHolidays) params.set('excludeHolidays', 'true');
if (this.locale) params.set('locale', this.locale);
@@ -199,6 +208,7 @@ export class Options {
if (this.githubEnt) params.githubEnt = this.githubEnt;
if (this.githubTeam) params.githubTeam = this.githubTeam;
if (this.scope) params.scope = this.scope;
+ if (this.enterpriseType) params.enterpriseType = this.enterpriseType;
if (this.excludeHolidays) params.excludeHolidays = String(this.excludeHolidays);
if (this.locale) params.locale = this.locale;
return params;
@@ -217,6 +227,7 @@ export class Options {
if (this.githubEnt !== undefined) result.githubEnt = this.githubEnt;
if (this.githubTeam !== undefined) result.githubTeam = this.githubTeam;
if (this.scope !== undefined) result.scope = this.scope;
+ if (this.enterpriseType !== undefined) result.enterpriseType = this.enterpriseType;
if (this.excludeHolidays !== undefined) result.excludeHolidays = this.excludeHolidays;
if (this.locale !== undefined) result.locale = this.locale;
@@ -242,6 +253,7 @@ export class Options {
githubEnt: other.githubEnt ?? this.githubEnt,
githubTeam: other.githubTeam ?? this.githubTeam,
scope: other.scope ?? this.scope,
+ enterpriseType: other.enterpriseType ?? this.enterpriseType,
excludeHolidays: other.excludeHolidays ?? this.excludeHolidays,
locale: other.locale ?? this.locale
});
@@ -287,7 +299,23 @@ export class Options {
if (!this.githubEnt || !this.githubTeam) {
throw new Error('GitHub enterprise and team must be set for team-enterprise scope');
}
- url = `${baseUrl}/enterprises/${this.githubEnt}/team/${this.githubTeam}/copilot/metrics`;
+ // For full enterprises, teams are organization-based, so we need to use org API
+ // For copilot-only enterprises, we use the enterprise team API
+ if (this.enterpriseType === 'full') {
+ // We need to determine which organization the team belongs to
+ // This will be handled by extracting org name from team slug format "org-name - team-name"
+ const teamParts = this.githubTeam.split(' - ');
+ if (teamParts.length >= 2) {
+ const orgName = teamParts[0];
+ const teamName = teamParts[1];
+ url = `${baseUrl}/orgs/${orgName}/team/${teamName}/copilot/metrics`;
+ } else {
+ throw new Error('Team slug must be in format "org-name - team-name" for full enterprise scope');
+ }
+ } else {
+ // Default to copilot-only behavior
+ url = `${baseUrl}/enterprises/${this.githubEnt}/team/${this.githubTeam}/copilot/metrics`;
+ }
break;
case 'enterprise':
@@ -355,13 +383,34 @@ export class Options {
if (!this.githubEnt) {
throw new Error('GitHub enterprise must be set for enterprise scope');
}
- return `${baseUrl}/enterprises/${this.githubEnt}/teams`;
+ // For full enterprises, we need to get teams from organizations within the enterprise
+ // For copilot-only enterprises, we use the enterprise teams API
+ if (this.enterpriseType === 'full') {
+ // This will be handled by the teams API to enumerate organizations first
+ return `${baseUrl}/enterprises/${this.githubEnt}/organizations`;
+ } else {
+ // Default to copilot-only behavior (enterprise teams API)
+ return `${baseUrl}/enterprises/${this.githubEnt}/teams`;
+ }
default:
throw new Error(`Invalid scope: ${this.scope}`);
}
}
+ /**
+ * Get the Enterprise Organizations API URL
+ */
+ getEnterpriseOrganizationsApiUrl(): string {
+ const baseUrl = 'https://api.github.com';
+
+ if (!this.githubEnt) {
+ throw new Error('GitHub enterprise must be set');
+ }
+
+ return `${baseUrl}/enterprises/${this.githubEnt}/organizations`;
+ }
+
/**
* Get the Teams API URL based on scope and configuration
*/
diff --git a/nuxt.config.ts b/nuxt.config.ts
index ec5dff8f..60a4f834 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -94,6 +94,7 @@ export default defineNuxtConfig({
githubOrg: '',
githubEnt: '',
githubTeam: '',
+ enterpriseType: 'copilot-only', // can be overridden by NUXT_PUBLIC_ENTERPRISE_TYPE environment variable
usingGithubAuth: false,
version,
isPublicApp: false
diff --git a/server/api/teams.ts b/server/api/teams.ts
index 31591c43..28773aa7 100644
--- a/server/api/teams.ts
+++ b/server/api/teams.ts
@@ -1,8 +1,9 @@
-import { Options, type Scope } from '@/model/Options'
+import { Options, type Scope, type EnterpriseType } from '@/model/Options'
import type { H3Event, EventHandlerRequest } from 'h3'
interface Team { name: string; slug: string; description: string }
interface GitHubTeam { name: string; slug: string; description?: string }
+interface GitHubOrganization { login: string; id: number }
class TeamsError extends Error {
statusCode: number
@@ -53,6 +54,7 @@ export async function getTeams(event: H3Event
From e5a6a7a56f48a4c6a98b810ce7f89cfd615a41c5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 19 Sep 2025 22:33:00 +0000
Subject: [PATCH 5/6] Fix organization retrieval for full enterprises to use
GraphQL instead of REST API
Co-authored-by: liguori <8238828+liguori@users.noreply.github.com>
---
app/model/Options.ts | 19 ++------
server/api/teams.ts | 89 +++++++++++++++++++----------------
tests/enterprise-type.spec.ts | 16 +------
3 files changed, 52 insertions(+), 72 deletions(-)
diff --git a/app/model/Options.ts b/app/model/Options.ts
index e356ac81..0bf5abab 100644
--- a/app/model/Options.ts
+++ b/app/model/Options.ts
@@ -383,11 +383,11 @@ export class Options {
if (!this.githubEnt) {
throw new Error('GitHub enterprise must be set for enterprise scope');
}
- // For full enterprises, we need to get teams from organizations within the enterprise
+ // For full enterprises, teams are fetched via GraphQL + organization teams APIs
// For copilot-only enterprises, we use the enterprise teams API
if (this.enterpriseType === 'full') {
- // This will be handled by the teams API to enumerate organizations first
- return `${baseUrl}/enterprises/${this.githubEnt}/organizations`;
+ // GraphQL will be used to get organizations, then org teams APIs
+ return `${baseUrl}/graphql`;
} else {
// Default to copilot-only behavior (enterprise teams API)
return `${baseUrl}/enterprises/${this.githubEnt}/teams`;
@@ -398,19 +398,6 @@ export class Options {
}
}
- /**
- * Get the Enterprise Organizations API URL
- */
- getEnterpriseOrganizationsApiUrl(): string {
- const baseUrl = 'https://api.github.com';
-
- if (!this.githubEnt) {
- throw new Error('GitHub enterprise must be set');
- }
-
- return `${baseUrl}/enterprises/${this.githubEnt}/organizations`;
- }
-
/**
* Get the Teams API URL based on scope and configuration
*/
diff --git a/server/api/teams.ts b/server/api/teams.ts
index 28773aa7..6a21fce4 100644
--- a/server/api/teams.ts
+++ b/server/api/teams.ts
@@ -3,7 +3,16 @@ import type { H3Event, EventHandlerRequest } from 'h3'
interface Team { name: string; slug: string; description: string }
interface GitHubTeam { name: string; slug: string; description?: string }
-interface GitHubOrganization { login: string; id: number }
+interface GraphQLOrganization { login: string; name: string; url: string }
+interface GraphQLResponse {
+ data: {
+ enterprise: {
+ organizations: {
+ nodes: GraphQLOrganization[]
+ }
+ }
+ }
+}
class TeamsError extends Error {
statusCode: number
@@ -80,52 +89,50 @@ export async function getTeams(event: H3Event