Skip to content

Commit

Permalink
introduce support for teams and organization (#1261)
Browse files Browse the repository at this point in the history
Signed-off-by: Alisson Fabiano <[email protected]>
  • Loading branch information
esw-afabiano authored Nov 19, 2024
1 parent d3d078b commit 7f17c9f
Show file tree
Hide file tree
Showing 73 changed files with 2,657 additions and 974 deletions.
7 changes: 7 additions & 0 deletions workspaces/copilot/.changeset/six-cats-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@backstage-community/plugin-copilot-backend': minor
'@backstage-community/plugin-copilot-common': minor
'@backstage-community/plugin-copilot': minor
---

Introduced support for organizations and team metrics visualization in the Copilot plugin.
5 changes: 2 additions & 3 deletions workspaces/copilot/packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import { AppRouter, FlatRoutes } from '@backstage/core-app-api';
import { CatalogGraphPage } from '@backstage/plugin-catalog-graph';
import { RequirePermission } from '@backstage/plugin-permission-react';
import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha';
import { CopilotPage } from '@backstage-community/plugin-copilot';
import { CopilotIndexPage } from '@backstage-community/plugin-copilot';

const app = createApp({
apis,
Expand Down Expand Up @@ -111,10 +111,9 @@ const routes = (
</Route>
<Route path="/settings" element={<UserSettingsPage />} />
<Route path="/catalog-graph" element={<CatalogGraphPage />} />
<Route path="/copilot" element={<CopilotPage />} />
<Route path="/copilot" element={<CopilotIndexPage />} />
</FlatRoutes>
);

export default app.createRoot(
<>
<AlertDisplay />
Expand Down
4 changes: 2 additions & 2 deletions workspaces/copilot/packages/app/src/components/Root/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import HomeIcon from '@material-ui/icons/Home';
import ExtensionIcon from '@material-ui/icons/Extension';
import LibraryBooks from '@material-ui/icons/LibraryBooks';
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
import GithubIcon from '@material-ui/icons/GitHub';
import LogoFull from './LogoFull';
import LogoIcon from './LogoIcon';
import {
Expand All @@ -41,6 +40,7 @@ import {
} from '@backstage/core-components';
import MenuIcon from '@material-ui/icons/Menu';
import SearchIcon from '@material-ui/icons/Search';
import { CopilotSidebar } from '@backstage-community/plugin-copilot';

const useSidebarLogoStyles = makeStyles({
root: {
Expand Down Expand Up @@ -87,7 +87,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
{/* End global nav */}
<SidebarDivider />
<SidebarScrollWrapper>
<SidebarItem icon={GithubIcon} to="copilot" text="Copilot" />
<CopilotSidebar />
</SidebarScrollWrapper>
</SidebarGroup>
<SidebarSpace />
Expand Down
54 changes: 37 additions & 17 deletions workspaces/copilot/plugins/copilot-backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ To configure the plugin using the new backend system:

const backend = createBackend();

backend.add(import('@backstage-community/plugin-copilot'));
backend.add(import('@backstage-community/plugin-copilot-backend'));

backend.start();
```
Expand All @@ -24,17 +24,17 @@ To configure the plugin using the new backend system:

To install the plugin using the old method:

1. Add the `@backstage-community/plugin-copilot` package to your backend:
1. Add the `@backstage-community/plugin-copilot-backend` package to your backend:

```sh
yarn --cwd packages/backend add @backstage-community/plugin-copilot
yarn --cwd packages/backend add @backstage-community/plugin-copilot-backend
```

2. In your `packages/backend/src/plugins/copilot.ts` file, add the following code:

```typescript
import { TaskScheduleDefinition } from '@backstage/backend-tasks';
import { createRouterFromConfig } from '@backstage-community/plugin-copilot';
import { createRouterFromConfig } from '@backstage-community/plugin-copilot-backend';
export default async function createPlugin(): Promise<void> {
const schedule: TaskScheduleDefinition = {
Expand All @@ -53,9 +53,7 @@ To install the plugin using the old method:
import { createRouterFromConfig } from './plugins/copilot';
async function main() {
// Backend setup
const env = createEnv('copilot');
// Plugin registration
apiRouter.use('/copilot', await createRouterFromConfig(env));
}
```
Expand All @@ -68,14 +66,36 @@ To configure the GitHub Copilot plugin, you need to set the following environmen

- **`copilot.host`**: The host URL for your GitHub Copilot instance (e.g., `github.com` or `github.enterprise.com`).
- **`copilot.enterprise`**: The name of your GitHub Enterprise instance (e.g., `my-enterprise`).
- **`copilot.organization`**: The name of your GitHub Organization (e.g., `my-organization`).

These variables are used to configure the plugin and ensure it communicates with the correct GitHub instance.

### GitHub Credentials

**Important:** The GitHub token, which is necessary for authentication, should be managed within your Backstage integrations configuration. The token must be added to your GitHub integration settings, and the plugin will retrieve it through the `GithubCredentialsProvider`.
**Important:** The GitHub token, necessary for authentication, should be managed within your Backstage integrations configuration. Ensure that your GitHub integration in the Backstage configuration includes the necessary token for the `GithubCredentialsProvider` to function correctly.

Ensure that your GitHub integration in the Backstage configuration includes the necessary token for the `GithubCredentialsProvider` to work correctly.
### GitHub Token Scopes

To ensure the GitHub Copilot plugin operates correctly within your organization or enterprise, your GitHub access token must include specific scopes. These scopes grant the plugin the necessary permissions to interact with your GitHub organization and manage Copilot usage.

#### Required Scopes

1. **List Teams Endpoint**

- **Scope Required:** `read:org`
- **Purpose:** Allows the plugin to list all teams within your GitHub organization.

2. **Copilot Usage**
- **Scopes Required - enterprise:** `manage_billing:copilot`, `read:enterprise`
- **Scopes Required - organization:** `manage_billing:copilot`, `read:org`, or `read:enterprise`
- **Purpose:** Enables the plugin to manage and monitor GitHub Copilot usage within your organization or/and enterprise.

#### How to Configure Token Scopes

1. **Generate a Personal Access Token (PAT):**
- Navigate to [GitHub Personal Access Tokens](https://github.com/settings/tokens).
- Click on **Generate new token**.
- Select the scopes according to your needs

### YAML Configuration Example

Expand All @@ -90,20 +110,20 @@ copilot:
seconds: 15
host: YOUR_GITHUB_HOST_HERE
enterprise: YOUR_ENTERPRISE_NAME_HERE
```

### Generating GitHub Copilot Token
organization: YOUR_ORGANIZATION_NAME_HERE
To generate an access token for using GitHub Copilot:

- Visit [Generate GitHub Access Token](https://github.com/settings/tokens).
- Follow the instructions to create a new token with the `read:enterprise` scope.
integrations:
github:
- host: YOUR_GITHUB_HOST_HERE
token: YOUR_GENERATED_TOKEN
```

### API Documentation

For more details on using the GitHub Copilot API:
For more details on using the GitHub Copilot and Teams APIs, refer to the following documentation:

- Refer to the [API documentation](https://docs.github.com/en/rest/copilot/copilot-usage?apiVersion=2022-11-28) for comprehensive information on available functionalities.
- [GitHub Teams API - List Teams](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams)
- [GitHub Copilot API - Usage](https://docs.github.com/en/rest/copilot/copilot-usage?apiVersion=2022-11-28)

## Run

Expand Down
6 changes: 5 additions & 1 deletion workspaces/copilot/plugins/copilot-backend/config.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 The Backstage Authors
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -30,6 +30,10 @@ export interface Config {
* The name of the GitHub enterprise.
*/
enterprise?: string;
/**
* The name of the GitHub organization.
*/
organization?: string;
/**
* The host for GitHub Copilot integration.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @param {import('knex').Knex} knex
*/
exports.up = async function up(knex) {
await knex.schema.table('metrics', table => {
table
.string('type', 50)
.defaultTo('enterprise')
.notNullable()
.comment('Type of the metrics data: enterprise, organization');

table.string('team_name', 255).nullable().comment('Name of the team');

table.dropPrimary();

table.unique(['day', 'type', 'team_name'], 'uk_day_type_team_name');
});
};

/**
* @param {import('knex').Knex} knex
*/
exports.down = async function down(knex) {
await knex.schema.table('metrics', table => {
table.dropUnique(['day', 'type', 'team_name']);

table.dropColumn('type');
table.dropColumn('team_name');

table.primary('day');
table.index('day', 'idx_metrics_day');
});
};
3 changes: 2 additions & 1 deletion workspaces/copilot/plugins/copilot-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
"luxon": "^3.5.0",
"node-fetch": "^2.6.7",
"winston": "^3.2.1",
"yn": "^4.0.0"
"yn": "^4.0.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@backstage/backend-test-utils": "^1.0.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@

import { ResponseError } from '@backstage/errors';
import { Config } from '@backstage/config';
import { Metric } from '@backstage-community/plugin-copilot-common';
import { Metric, TeamInfo } from '@backstage-community/plugin-copilot-common';
import fetch from 'node-fetch';
import { getGithubInfo, GithubInfo } from '../utils/GithubUtils';

interface GithubApi {
getCopilotUsageDataForEnterprise: () => Promise<Metric[]>;
fetchEnterpriseCopilotUsage: () => Promise<Metric[]>;
fetchEnterpriseTeamCopilotUsage: (teamId: string) => Promise<Metric[]>;
fetchEnterpriseTeams: () => Promise<TeamInfo[]>;
fetchOrganizationCopilotUsage: () => Promise<Metric[]>;
fetchOrganizationTeamCopilotUsage: (teamId: string) => Promise<Metric[]>;
fetchOrganizationTeams: () => Promise<TeamInfo[]>;
}

export class GithubClient implements GithubApi {
Expand All @@ -32,11 +37,36 @@ export class GithubClient implements GithubApi {
return new GithubClient(info);
}

async getCopilotUsageDataForEnterprise(): Promise<Metric[]> {
async fetchEnterpriseCopilotUsage(): Promise<Metric[]> {
const path = `/enterprises/${this.props.enterprise}/copilot/usage`;
return this.get(path);
}

async fetchEnterpriseTeamCopilotUsage(teamId: string): Promise<Metric[]> {
const path = `/enterprises/${this.props.enterprise}/team/${teamId}/copilot/usage`;
return this.get(path);
}

async fetchEnterpriseTeams(): Promise<TeamInfo[]> {
const path = `/enterprises/${this.props.enterprise}/teams`;
return this.get(path);
}

async fetchOrganizationCopilotUsage(): Promise<Metric[]> {
const path = `/orgs/${this.props.organization}/copilot/usage`;
return this.get(path);
}

async fetchOrganizationTeamCopilotUsage(teamId: string): Promise<Metric[]> {
const path = `/orgs/${this.props.organization}/team/${teamId}/copilot/usage`;
return this.get(path);
}

async fetchOrganizationTeams(): Promise<TeamInfo[]> {
const path = `/orgs/${this.props.organization}/teams`;
return this.get(path);
}

private async get<T>(path: string): Promise<T> {
const response = await fetch(`${this.props.apiBaseUrl}${path}`, {
headers: {
Expand Down
Loading

0 comments on commit 7f17c9f

Please sign in to comment.