introduce support for teams and organization (#1261)
Signed-off-by: Alisson Fabiano <afabiano@eshopworld.com>
This commit is contained in:
parent
d3d078b35b
commit
7f17c9f54e
|
|
@ -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.
|
||||
|
|
@ -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,
|
||||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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: {
|
||||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
```
|
||||
|
|
@ -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 = {
|
||||
|
|
@ -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));
|
||||
}
|
||||
```
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -90,20 +110,20 @@ copilot:
|
|||
seconds: 15
|
||||
host: YOUR_GITHUB_HOST_HERE
|
||||
enterprise: YOUR_ENTERPRISE_NAME_HERE
|
||||
organization: YOUR_ORGANIZATION_NAME_HERE
|
||||
|
||||
integrations:
|
||||
github:
|
||||
- host: YOUR_GITHUB_HOST_HERE
|
||||
token: YOUR_GENERATED_TOKEN
|
||||
```
|
||||
|
||||
### Generating GitHub Copilot Token
|
||||
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
};
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
} from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
Metric,
|
||||
MetricsType,
|
||||
PeriodRange,
|
||||
} from '@backstage-community/plugin-copilot-common';
|
||||
import { Knex } from 'knex';
|
||||
|
|
@ -53,46 +54,72 @@ export class DatabaseHandler {
|
|||
|
||||
private constructor(private readonly db: Knex) {}
|
||||
|
||||
async getMetricsByPeriod(
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
): Promise<MetricDbRow[]> {
|
||||
const records = await this.db<MetricDbRow>('metrics').whereBetween('day', [
|
||||
startDate,
|
||||
endDate,
|
||||
]);
|
||||
return records ?? [];
|
||||
}
|
||||
async getPeriodRange(type: MetricsType): Promise<PeriodRange | undefined> {
|
||||
const query = this.db<MetricDbRow>('metrics').where('type', type);
|
||||
|
||||
async getPeriodRange(): Promise<PeriodRange | undefined> {
|
||||
const minDate = await this.db<MetricDbRow>('metrics')
|
||||
.orderBy('day', 'asc')
|
||||
.first('day');
|
||||
const maxDate = await this.db<MetricDbRow>('metrics')
|
||||
.orderBy('day', 'desc')
|
||||
.first('day');
|
||||
const minDate = await query.orderBy('day', 'asc').first('day');
|
||||
const maxDate = await query.orderBy('day', 'desc').first('day');
|
||||
|
||||
if (!minDate?.day || !maxDate?.day) return undefined;
|
||||
|
||||
return { minDate: minDate.day, maxDate: maxDate.day };
|
||||
}
|
||||
|
||||
async getTeams(
|
||||
type: MetricsType,
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
): Promise<Array<string | undefined>> {
|
||||
const result = await this.db<MetricDbRow>('metrics')
|
||||
.where('type', type)
|
||||
.whereBetween('day', [startDate, endDate])
|
||||
.whereNotNull('team_name')
|
||||
.distinct('team_name')
|
||||
.orderBy('team_name', 'asc')
|
||||
.select('team_name');
|
||||
|
||||
return result.map(x => x.team_name);
|
||||
}
|
||||
|
||||
async batchInsert(metrics: MetricDbRow[]): Promise<void> {
|
||||
await this.db<MetricDbRow[]>('metrics')
|
||||
.insert(metrics)
|
||||
.onConflict('day')
|
||||
.onConflict(['day', 'type', 'team_name'])
|
||||
.ignore();
|
||||
}
|
||||
|
||||
async getMostRecentDayFromMetrics(): Promise<string | undefined> {
|
||||
async getMostRecentDayFromMetrics(
|
||||
type: MetricsType,
|
||||
teamName?: string,
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const mostRecent = await this.db<MetricDbRow>('metrics')
|
||||
const query = await this.db<MetricDbRow>('metrics')
|
||||
.where('type', type)
|
||||
.where('team_name', teamName ?? null)
|
||||
.orderBy('day', 'desc')
|
||||
.first('day');
|
||||
|
||||
return mostRecent ? mostRecent.day : undefined;
|
||||
return query ? query.day : undefined;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async getMetrics(
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
type: MetricsType,
|
||||
teamName?: string,
|
||||
): Promise<MetricDbRow[]> {
|
||||
console.log(startDate, endDate, type, teamName);
|
||||
if (teamName) {
|
||||
return await this.db<MetricDbRow>('metrics')
|
||||
.where('type', type)
|
||||
.where('team_name', teamName)
|
||||
.whereBetween('day', [startDate, endDate]);
|
||||
}
|
||||
return this.db<MetricDbRow>('metrics')
|
||||
.where('type', type)
|
||||
.whereNull('team_name')
|
||||
.whereBetween('day', [startDate, endDate]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,11 +24,20 @@ import {
|
|||
SchedulerServiceTaskScheduleDefinition,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { Config } from '@backstage/config';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import { Metric } from '@backstage-community/plugin-copilot-common';
|
||||
import { DatabaseHandler } from '../db/DatabaseHandler';
|
||||
import Scheduler from '../task/Scheduler';
|
||||
import TaskManagement from '../task/TaskManagement';
|
||||
import { GithubClient } from '../client/GithubClient';
|
||||
import { DateTime } from 'luxon';
|
||||
import { validateQuery } from './validation/validateQuery';
|
||||
import {
|
||||
MetricsQuery,
|
||||
metricsQuerySchema,
|
||||
PeriodRangeQuery,
|
||||
periodRangeQuerySchema,
|
||||
TeamQuery,
|
||||
teamQuerySchema,
|
||||
} from './validation/schema';
|
||||
|
||||
/**
|
||||
* Options for configuring the Copilot plugin.
|
||||
|
|
@ -115,7 +124,8 @@ async function createRouter(
|
|||
await scheduler.scheduleTask({
|
||||
id: 'copilot-metrics',
|
||||
...(schedule ?? defaultSchedule),
|
||||
fn: async () => await Scheduler.create({ db, logger, api, config }).run(),
|
||||
fn: async () =>
|
||||
await TaskManagement.create({ db, logger, api, config }).runAsync(),
|
||||
});
|
||||
|
||||
const router = Router();
|
||||
|
|
@ -126,38 +136,48 @@ async function createRouter(
|
|||
response.json({ status: 'ok' });
|
||||
});
|
||||
|
||||
router.get('/metrics', async (request, response) => {
|
||||
const { startDate, endDate } = request.query;
|
||||
router.get(
|
||||
'/metrics',
|
||||
validateQuery(metricsQuerySchema),
|
||||
async (req, res) => {
|
||||
const { startDate, endDate, type, team } = req.query as MetricsQuery;
|
||||
|
||||
if (typeof startDate !== 'string' || typeof endDate !== 'string') {
|
||||
return response.status(400).json('Invalid query parameters');
|
||||
}
|
||||
const result = await db.getMetrics(startDate, endDate, type, team);
|
||||
|
||||
const parsedStartDate = DateTime.fromISO(startDate);
|
||||
const parsedEndDate = DateTime.fromISO(endDate);
|
||||
const metrics: Metric[] = result.map(metric => ({
|
||||
...metric,
|
||||
breakdown: JSON.parse(metric.breakdown),
|
||||
}));
|
||||
|
||||
if (!parsedStartDate.isValid || !parsedEndDate.isValid) {
|
||||
return response.status(400).json('Invalid date format');
|
||||
}
|
||||
return res.json(metrics);
|
||||
},
|
||||
);
|
||||
|
||||
const result = await db.getMetricsByPeriod(startDate, endDate);
|
||||
router.get(
|
||||
'/metrics/period-range',
|
||||
validateQuery(periodRangeQuerySchema),
|
||||
async (req, res) => {
|
||||
const { type } = req.query as PeriodRangeQuery;
|
||||
const result = await db.getPeriodRange(type);
|
||||
|
||||
const metrics: Metric[] = result.map(metric => ({
|
||||
...metric,
|
||||
breakdown: JSON.parse(metric.breakdown),
|
||||
}));
|
||||
if (!result) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
return response.json(metrics);
|
||||
});
|
||||
return res.json(result);
|
||||
},
|
||||
);
|
||||
|
||||
router.get('/metrics/period-range', async (_, response) => {
|
||||
const result = await db.getPeriodRange();
|
||||
router.get('/teams', validateQuery(teamQuerySchema), async (req, res) => {
|
||||
const { type, startDate, endDate } = req.query as TeamQuery;
|
||||
|
||||
const result = await db.getTeams(type, startDate, endDate);
|
||||
|
||||
if (!result) {
|
||||
return response.status(400).json('No available data');
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
return response.json(result);
|
||||
return res.json(result);
|
||||
});
|
||||
|
||||
router.use(MiddlewareFactory.create({ config, logger }).error);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
import { MetricsType } from '@backstage-community/plugin-copilot-common';
|
||||
|
||||
const metricsTypeSchema = z.enum(['enterprise', 'organization']);
|
||||
|
||||
const isoDateSchema = z.string().refine(date => !isNaN(Date.parse(date)), {
|
||||
message: 'Invalid date format',
|
||||
});
|
||||
|
||||
export type MetricsQuery = {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
type: MetricsType;
|
||||
team?: string;
|
||||
};
|
||||
|
||||
export type PeriodRangeQuery = {
|
||||
type: MetricsType;
|
||||
};
|
||||
|
||||
export type TeamQuery = {
|
||||
type: MetricsType;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
};
|
||||
|
||||
export const metricsQuerySchema = z.object({
|
||||
startDate: isoDateSchema,
|
||||
endDate: isoDateSchema,
|
||||
type: metricsTypeSchema,
|
||||
team: z.string().optional(),
|
||||
});
|
||||
|
||||
export const periodRangeQuerySchema = z.object({
|
||||
type: metricsTypeSchema,
|
||||
});
|
||||
|
||||
export const teamQuerySchema = z.object({
|
||||
startDate: isoDateSchema,
|
||||
endDate: isoDateSchema,
|
||||
type: metricsTypeSchema,
|
||||
});
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { ZodSchema } from 'zod';
|
||||
|
||||
export function validateQuery(schema: ZodSchema) {
|
||||
return (req: Request, _res: Response, next: NextFunction) => {
|
||||
const { error, data } = schema.safeParse(req.query);
|
||||
|
||||
if (error) {
|
||||
throw new InputError(error.errors[0].message, error);
|
||||
}
|
||||
|
||||
req.query = data;
|
||||
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import { MetricsType } from '@backstage-community/plugin-copilot-common';
|
||||
import { MetricDbRow } from '../db/DatabaseHandler';
|
||||
import { batchInsertInChunks } from '../utils/batchInsert';
|
||||
import {
|
||||
filterNewMetrics,
|
||||
prepareMetricsForInsert,
|
||||
} from '../utils/metricHelpers';
|
||||
import { TaskOptions } from './TaskManagement';
|
||||
|
||||
export async function discoverEnterpriseMetrics({
|
||||
api,
|
||||
logger,
|
||||
db,
|
||||
config,
|
||||
}: TaskOptions): Promise<void> {
|
||||
if (!config.getOptionalString('copilot.enterprise')) {
|
||||
logger.info(
|
||||
'[discoverEnterpriseMetrics] Skipping: Enterprise configuration not found.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const type: MetricsType = 'enterprise';
|
||||
|
||||
try {
|
||||
const metrics = await api.fetchEnterpriseCopilotUsage();
|
||||
logger.info(
|
||||
`[discoverEnterpriseMetrics] Fetched ${metrics.length} metrics`,
|
||||
);
|
||||
|
||||
const lastDay = await db.getMostRecentDayFromMetrics(type);
|
||||
logger.info(`[discoverEnterpriseMetrics] Found last day: ${lastDay}`);
|
||||
|
||||
const newMetrics = filterNewMetrics(metrics, lastDay);
|
||||
logger.info(
|
||||
`[discoverEnterpriseMetrics] Found ${newMetrics.length} new metrics to insert`,
|
||||
);
|
||||
|
||||
if (newMetrics.length > 0) {
|
||||
await batchInsertInChunks<MetricDbRow>(
|
||||
prepareMetricsForInsert(newMetrics, type),
|
||||
30,
|
||||
async (chunk: MetricDbRow[]) => {
|
||||
await db.batchInsert(chunk);
|
||||
},
|
||||
);
|
||||
logger.info(
|
||||
'[discoverEnterpriseMetrics] Inserted new metrics into the database',
|
||||
);
|
||||
} else {
|
||||
logger.info('[discoverEnterpriseMetrics] No new metrics found to insert');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[discoverEnterpriseMetrics] An error occurred while processing Github Copilot metrics: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import { MetricsType } from '@backstage-community/plugin-copilot-common';
|
||||
import { MetricDbRow } from '../db/DatabaseHandler';
|
||||
import { batchInsertInChunks } from '../utils/batchInsert';
|
||||
import {
|
||||
filterNewMetrics,
|
||||
prepareMetricsForInsert,
|
||||
} from '../utils/metricHelpers';
|
||||
import { TaskOptions } from './TaskManagement';
|
||||
|
||||
export async function discoverEnterpriseTeamMetrics({
|
||||
api,
|
||||
logger,
|
||||
db,
|
||||
config,
|
||||
}: TaskOptions): Promise<void> {
|
||||
if (!config.getOptionalString('copilot.enterprise')) {
|
||||
logger.info(
|
||||
'[discoverEnterpriseTeamMetrics] Skipping: Enterprise configuration not found.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const type: MetricsType = 'enterprise';
|
||||
|
||||
try {
|
||||
const teams = await api.fetchEnterpriseTeams();
|
||||
logger.info(
|
||||
`[discoverEnterpriseTeamMetrics] Fetched ${teams.length} teams`,
|
||||
);
|
||||
|
||||
for (const team of teams) {
|
||||
try {
|
||||
logger.info(
|
||||
`[discoverEnterpriseTeamMetrics] Fetching metrics for team: ${team.slug}`,
|
||||
);
|
||||
|
||||
const metrics = await api.fetchEnterpriseTeamCopilotUsage(team.slug);
|
||||
logger.info(
|
||||
`[discoverEnterpriseTeamMetrics] Fetched ${metrics.length} metrics for team: ${team.slug}`,
|
||||
);
|
||||
|
||||
const lastDay = await db.getMostRecentDayFromMetrics(type, team.slug);
|
||||
logger.info(
|
||||
`[discoverEnterpriseTeamMetrics] Found last processed day for team ${team.slug}: ${lastDay}`,
|
||||
);
|
||||
|
||||
const newMetrics = filterNewMetrics(metrics, lastDay);
|
||||
logger.info(
|
||||
`[discoverEnterpriseTeamMetrics] Found ${newMetrics.length} new metrics for team ${team.slug} to insert`,
|
||||
);
|
||||
|
||||
if (newMetrics.length > 0) {
|
||||
await batchInsertInChunks<MetricDbRow>(
|
||||
prepareMetricsForInsert(newMetrics, type, team.slug),
|
||||
30,
|
||||
async (chunk: MetricDbRow[]) => {
|
||||
await db.batchInsert(chunk);
|
||||
},
|
||||
);
|
||||
logger.info(
|
||||
`[discoverEnterpriseTeamMetrics] Successfully inserted new metrics for team ${team.slug} into the database`,
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`[discoverEnterpriseTeamMetrics] No new metrics found for team ${team.slug} to insert`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[discoverEnterpriseTeamMetrics] Error processing metrics for team ${team.slug}: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[discoverEnterpriseTeamMetrics] Error fetching teams: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import { MetricsType } from '@backstage-community/plugin-copilot-common';
|
||||
import { MetricDbRow } from '../db/DatabaseHandler';
|
||||
import { batchInsertInChunks } from '../utils/batchInsert';
|
||||
import {
|
||||
filterNewMetrics,
|
||||
prepareMetricsForInsert,
|
||||
} from '../utils/metricHelpers';
|
||||
import { TaskOptions } from './TaskManagement';
|
||||
|
||||
export async function discoverOrganizationMetrics({
|
||||
api,
|
||||
logger,
|
||||
db,
|
||||
config,
|
||||
}: TaskOptions): Promise<void> {
|
||||
if (!config.getOptionalString('copilot.organization')) {
|
||||
logger.info(
|
||||
'[discoverOrganizationMetrics] Skipping: Organization configuration not found.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const type: MetricsType = 'organization';
|
||||
|
||||
try {
|
||||
const metrics = await api.fetchOrganizationCopilotUsage();
|
||||
logger.info(
|
||||
`[discoverOrganizationMetrics] Fetched ${metrics.length} metrics`,
|
||||
);
|
||||
|
||||
const lastDay = await db.getMostRecentDayFromMetrics(type);
|
||||
logger.info(`[discoverOrganizationMetrics] Found last day: ${lastDay}`);
|
||||
|
||||
const newMetrics = filterNewMetrics(metrics, lastDay);
|
||||
logger.info(
|
||||
`[discoverOrganizationMetrics] Found ${newMetrics.length} new metrics to insert`,
|
||||
);
|
||||
|
||||
if (newMetrics.length > 0) {
|
||||
await batchInsertInChunks<MetricDbRow>(
|
||||
prepareMetricsForInsert(newMetrics, type),
|
||||
30,
|
||||
async (chunk: MetricDbRow[]) => {
|
||||
await db.batchInsert(chunk);
|
||||
},
|
||||
);
|
||||
logger.info(
|
||||
'[discoverOrganizationMetrics] Inserted new metrics into the database',
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
'[discoverOrganizationMetrics] No new metrics found to insert',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[discoverOrganizationMetrics] An error occurred while processing Github Copilot metrics: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { MetricsType } from '@backstage-community/plugin-copilot-common';
|
||||
import { MetricDbRow } from '../db/DatabaseHandler';
|
||||
import { batchInsertInChunks } from '../utils/batchInsert';
|
||||
import {
|
||||
filterNewMetrics,
|
||||
prepareMetricsForInsert,
|
||||
} from '../utils/metricHelpers';
|
||||
import { TaskOptions } from './TaskManagement';
|
||||
|
||||
export async function discoverOrganizationTeamMetrics({
|
||||
api,
|
||||
logger,
|
||||
db,
|
||||
config,
|
||||
}: TaskOptions): Promise<void> {
|
||||
if (!config.getOptionalString('copilot.organization')) {
|
||||
logger.info(
|
||||
'[discoverOrganizationTeamMetrics] Skipping: Organization configuration not found.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const type: MetricsType = 'organization';
|
||||
|
||||
try {
|
||||
const teams = await api.fetchOrganizationTeams();
|
||||
logger.info(
|
||||
`[discoverOrganizationTeamMetrics] Fetched ${teams.length} teams`,
|
||||
);
|
||||
|
||||
for (const team of teams) {
|
||||
try {
|
||||
logger.info(
|
||||
`[discoverOrganizationTeamMetrics] Fetching metrics for team: ${team.slug}`,
|
||||
);
|
||||
|
||||
const metrics = await api.fetchOrganizationTeamCopilotUsage(team.slug);
|
||||
logger.info(
|
||||
`[discoverOrganizationTeamMetrics] Fetched ${metrics.length} metrics for team: ${team.slug}`,
|
||||
);
|
||||
|
||||
const lastDay = await db.getMostRecentDayFromMetrics(type, team.slug);
|
||||
logger.info(
|
||||
`[discoverOrganizationTeamMetrics] Found last processed day for team ${team.slug}: ${lastDay}`,
|
||||
);
|
||||
|
||||
const newMetrics = filterNewMetrics(metrics, lastDay);
|
||||
logger.info(
|
||||
`[discoverOrganizationTeamMetrics] Found ${newMetrics.length} new metrics for team ${team.slug} to insert`,
|
||||
);
|
||||
|
||||
if (newMetrics.length > 0) {
|
||||
await batchInsertInChunks<MetricDbRow>(
|
||||
prepareMetricsForInsert(newMetrics, type, team.slug),
|
||||
30,
|
||||
async (chunk: MetricDbRow[]) => {
|
||||
await db.batchInsert(chunk);
|
||||
},
|
||||
);
|
||||
logger.info(
|
||||
`[discoverOrganizationTeamMetrics] Successfully inserted new metrics for team ${team.slug} into the database`,
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`[discoverOrganizationTeamMetrics] No new metrics found for team ${team.slug} to insert`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[discoverOrganizationTeamMetrics] Error processing metrics for team ${team.slug}: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[discoverOrganizationTeamMetrics] Error fetching teams: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { GithubClient } from '../client/GithubClient';
|
||||
import { DatabaseHandler, MetricDbRow } from '../db/DatabaseHandler';
|
||||
import { Config } from '@backstage/config';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
type Options = {
|
||||
api: GithubClient;
|
||||
config: Config;
|
||||
logger: LoggerService;
|
||||
db: DatabaseHandler;
|
||||
};
|
||||
|
||||
export default class Scheduler {
|
||||
constructor(private readonly options: Options) {}
|
||||
|
||||
static create(options: Options) {
|
||||
return new Scheduler(options);
|
||||
}
|
||||
|
||||
async run() {
|
||||
try {
|
||||
this.options.logger.info('Starting Github Copilot Processor');
|
||||
|
||||
const copilotMetrics =
|
||||
await this.options.api.getCopilotUsageDataForEnterprise();
|
||||
this.options.logger.info(`Fetched ${copilotMetrics.length} metrics`);
|
||||
|
||||
const lastDay = await this.options.db.getMostRecentDayFromMetrics();
|
||||
this.options.logger.info(`Found last day: ${lastDay}`);
|
||||
|
||||
const diff = copilotMetrics
|
||||
.sort(
|
||||
(a, b) =>
|
||||
DateTime.fromISO(a.day).toMillis() -
|
||||
DateTime.fromISO(b.day).toMillis(),
|
||||
)
|
||||
.filter(metric => {
|
||||
const metricDate = DateTime.fromISO(metric.day);
|
||||
const lastDayDate = lastDay ? DateTime.fromISO(lastDay) : null;
|
||||
return !lastDayDate || metricDate > lastDayDate;
|
||||
})
|
||||
.map(({ breakdown, ...rest }) => ({
|
||||
...rest,
|
||||
breakdown: JSON.stringify(breakdown),
|
||||
}));
|
||||
|
||||
this.options.logger.info(`Found ${diff.length} new metrics to insert`);
|
||||
|
||||
if (diff.length > 0) {
|
||||
await batchInsertInChunks<MetricDbRow>(
|
||||
diff,
|
||||
30,
|
||||
async (chunk: MetricDbRow[]) => {
|
||||
await this.options.db.batchInsert(chunk);
|
||||
},
|
||||
);
|
||||
this.options.logger.info('Inserted new metrics into the database');
|
||||
} else {
|
||||
this.options.logger.info('No new metrics found to insert');
|
||||
}
|
||||
} catch (error) {
|
||||
this.options.logger.error(
|
||||
`An error occurred while processing Github Copilot metrics: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function batchInsertInChunks<T>(
|
||||
data: T[],
|
||||
chunkSize: number,
|
||||
batchInsertFunc: (chunk: T[]) => Promise<void>,
|
||||
): Promise<void> {
|
||||
for (let i = 0; i < data.length; i += chunkSize) {
|
||||
const chunk = data.slice(i, i + chunkSize);
|
||||
await batchInsertFunc(chunk);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import { Config } from '@backstage/config';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { GithubClient } from '../client/GithubClient';
|
||||
import { DatabaseHandler } from '../db/DatabaseHandler';
|
||||
import { discoverOrganizationMetrics } from './OrganizationTask';
|
||||
import { discoverOrganizationTeamMetrics } from './OrganizationTeamTask';
|
||||
import { discoverEnterpriseMetrics } from './EnterpriseTask';
|
||||
import { discoverEnterpriseTeamMetrics } from './EnterpriseTeamTask';
|
||||
|
||||
export type TaskOptions = {
|
||||
api: GithubClient;
|
||||
config: Config;
|
||||
logger: LoggerService;
|
||||
db: DatabaseHandler;
|
||||
};
|
||||
|
||||
export default class TaskManagement {
|
||||
private readonly tasks: Array<() => Promise<void>>;
|
||||
|
||||
constructor(private readonly options: TaskOptions) {
|
||||
this.tasks = [
|
||||
() => discoverOrganizationMetrics(this.options),
|
||||
() => discoverOrganizationTeamMetrics(this.options),
|
||||
() => discoverEnterpriseMetrics(this.options),
|
||||
() => discoverEnterpriseTeamMetrics(this.options),
|
||||
];
|
||||
}
|
||||
|
||||
static create(options: TaskOptions) {
|
||||
return new TaskManagement(options);
|
||||
}
|
||||
|
||||
async runAsync() {
|
||||
this.options.logger.info(
|
||||
`[TaskManagement] Starting processing of ${this.tasks.length} tasks`,
|
||||
);
|
||||
|
||||
const taskPromises = this.tasks.map(async task => {
|
||||
try {
|
||||
await task();
|
||||
} catch (e) {
|
||||
this.options.logger.warn(
|
||||
`[TaskManagement] Failed to process task: ${e.message}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(taskPromises);
|
||||
|
||||
this.options.logger.info(
|
||||
`[TaskManagement] Completed processing of all tasks`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -20,23 +20,21 @@ import { GithubCredentials, ScmIntegrations } from '@backstage/integration';
|
|||
export type GithubInfo = {
|
||||
credentials: GithubCredentials;
|
||||
apiBaseUrl: string;
|
||||
enterprise: string;
|
||||
enterprise?: string;
|
||||
organization?: string;
|
||||
};
|
||||
|
||||
export const getGithubInfo = async (config: Config): Promise<GithubInfo> => {
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
|
||||
const host = config.getString('copilot.host');
|
||||
const enterprise = config.getString('copilot.enterprise');
|
||||
const enterprise = config.getOptionalString('copilot.enterprise');
|
||||
const organization = config.getOptionalString('copilot.organization');
|
||||
|
||||
if (!host) {
|
||||
throw new Error('The host configuration is missing from the config.');
|
||||
}
|
||||
|
||||
if (!enterprise) {
|
||||
throw new Error('The enterprise configuration is missing from the config.');
|
||||
}
|
||||
|
||||
const githubConfig = integrations.github.byHost(host)?.config;
|
||||
|
||||
if (!githubConfig) {
|
||||
|
|
@ -57,5 +55,6 @@ export const getGithubInfo = async (config: Config): Promise<GithubInfo> => {
|
|||
apiBaseUrl,
|
||||
credentials,
|
||||
enterprise,
|
||||
organization,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
export async function batchInsertInChunks<T>(
|
||||
data: T[],
|
||||
chunkSize: number,
|
||||
batchInsertFunc: (chunk: T[]) => Promise<void>,
|
||||
): Promise<void> {
|
||||
for (let i = 0; i < data.length; i += chunkSize) {
|
||||
const chunk = data.slice(i, i + chunkSize);
|
||||
await batchInsertFunc(chunk);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import { DateTime } from 'luxon';
|
||||
import { MetricDbRow } from '../db/DatabaseHandler';
|
||||
import {
|
||||
Metric,
|
||||
MetricsType,
|
||||
} from '@backstage-community/plugin-copilot-common';
|
||||
|
||||
export function filterNewMetrics(
|
||||
metrics: Metric[],
|
||||
lastDay?: string,
|
||||
): Metric[] {
|
||||
return metrics
|
||||
.sort(
|
||||
(a, b) =>
|
||||
DateTime.fromISO(a.day).toMillis() - DateTime.fromISO(b.day).toMillis(),
|
||||
)
|
||||
.filter(metric => {
|
||||
const metricDate = DateTime.fromISO(metric.day);
|
||||
|
||||
const lastDayDate = lastDay
|
||||
? DateTime.fromJSDate(new Date(lastDay))
|
||||
: null;
|
||||
|
||||
return !lastDay || (lastDayDate?.isValid && metricDate > lastDayDate);
|
||||
});
|
||||
}
|
||||
|
||||
export function prepareMetricsForInsert(
|
||||
metrics: Metric[],
|
||||
type: MetricsType,
|
||||
team_name?: string,
|
||||
): MetricDbRow[] {
|
||||
return metrics.map(({ breakdown, ...rest }) => ({
|
||||
...rest,
|
||||
type,
|
||||
team_name,
|
||||
breakdown: JSON.stringify(breakdown),
|
||||
})) as MetricDbRow[];
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ export interface Breakdown {
|
|||
export interface Metric {
|
||||
breakdown: Breakdown[];
|
||||
day: string;
|
||||
team_name?: string;
|
||||
total_acceptances_count: number;
|
||||
total_active_chat_users: number;
|
||||
total_active_users: number;
|
||||
|
|
@ -26,11 +27,22 @@ export interface Metric {
|
|||
total_lines_accepted: number;
|
||||
total_lines_suggested: number;
|
||||
total_suggestions_count: number;
|
||||
type: MetricsType;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type MetricsType = 'enterprise' | 'organization';
|
||||
|
||||
// @public
|
||||
export interface PeriodRange {
|
||||
maxDate: string;
|
||||
minDate: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface TeamInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -62,6 +62,13 @@ export interface Breakdown {
|
|||
suggestions_count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the possible types of metrics data.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type MetricsType = 'enterprise' | 'organization';
|
||||
|
||||
/**
|
||||
* Represents a detailed breakdown of metrics by language and editor.
|
||||
*
|
||||
|
|
@ -78,6 +85,18 @@ export interface Metric {
|
|||
*/
|
||||
day: string;
|
||||
|
||||
/**
|
||||
* The type of the metrics data.
|
||||
* Can be 'enterprise', 'organization'.
|
||||
*/
|
||||
type: MetricsType;
|
||||
|
||||
/**
|
||||
* The name of the team, applicable when the metric is for a specific team.
|
||||
* When null, it indicates metrics for all teams, aggregated at the 'enterprise' or 'organization' level.
|
||||
*/
|
||||
team_name?: string;
|
||||
|
||||
/**
|
||||
* The total number of suggestions accepted.
|
||||
*/
|
||||
|
|
@ -135,3 +154,25 @@ export interface PeriodRange {
|
|||
*/
|
||||
minDate: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents information about a team.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface TeamInfo {
|
||||
/**
|
||||
* The unique identifier of the team.
|
||||
*/
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The slug of the team, used for URL-friendly identifiers.
|
||||
*/
|
||||
slug: string;
|
||||
|
||||
/**
|
||||
* The name of the team.
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
Aqui está o README ajustado, agora incluindo a nova funcionalidade para selecionar um time e comparar suas métricas com os dados gerais:
|
||||
|
||||
---
|
||||
|
||||
# GitHub Copilot Plugin
|
||||
|
||||
Welcome to the GitHub Copilot Plugin!
|
||||
|
|
@ -6,17 +10,14 @@ Welcome to the GitHub Copilot Plugin!
|
|||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Overview
|
||||
|
||||
The GitHub Copilot Plugin enhances your Backstage experience by providing features tailored to enterprise management.
|
||||
The GitHub Copilot Plugin enhances your Backstage experience by providing features tailored to both enterprise and organization management.
|
||||
|
||||
## Features
|
||||
|
||||
- **Enterprise Integration**: Seamlessly integrate enterprise-level functionalities.
|
||||
- **Enterprise and Organization Integration**: Seamlessly integrate functionalities for GitHub Enterprise and GitHub Organizations.
|
||||
- **Team Metrics Comparison**: Select a team and compare its metrics with the overall data, offering more insights into individual team performance.
|
||||
|
||||
## Setup
|
||||
|
||||
|
|
@ -24,7 +25,7 @@ The following sections will help you get the GitHub Copilot Plugin setup and run
|
|||
|
||||
### Backend
|
||||
|
||||
You need to setup the Copilot backend plugin ([copilot-backend](../copilot-backend/README.md)) before you move forward with any of the following steps if you haven't already.
|
||||
You need to set up the Copilot backend plugin ([copilot-backend](../copilot-backend/README.md)) before you move forward with any of the following steps if you haven't already.
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -41,17 +42,18 @@ To start using the GitHub Copilot Plugin, follow these steps:
|
|||
**App.tsx**:
|
||||
|
||||
```tsx
|
||||
import { CopilotPage } from '@backstage-community/plugin-copilot';
|
||||
// Add route
|
||||
<Route path="/copilot" element={<CopilotPage />} />;
|
||||
import { CopilotIndexPage } from '@backstage-community/plugin-copilot';
|
||||
|
||||
// Add the routes
|
||||
<Route path="/copilot" element={<CopilotIndexPage />} />;
|
||||
```
|
||||
|
||||
**Root.tsx**:
|
||||
|
||||
```tsx
|
||||
import GithubIcon from '@mui/icons-material/GitHub';
|
||||
// Add sidebar item
|
||||
import { CopilotSidebar } from '@backstage-community/plugin-copilot';
|
||||
// Add the copilot sidebar
|
||||
<SidebarScrollWrapper>
|
||||
<SidebarItem icon={GithubIcon} to="copilot" text="Copilot" />
|
||||
<CopilotSidebar />
|
||||
</SidebarScrollWrapper>;
|
||||
```
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Configuration interface for the GitHub Copilot Plugin.
|
||||
*/
|
||||
export interface Config {
|
||||
/** Configuration options for the GitHub Copilot Plugin */
|
||||
copilot?: {
|
||||
/**
|
||||
* The name of the GitHub enterprise.
|
||||
* @visibility frontend
|
||||
*/
|
||||
enterprise?: string;
|
||||
/**
|
||||
* The name of the GitHub organization.
|
||||
* @visibility frontend
|
||||
*/
|
||||
organization?: string;
|
||||
};
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import { createDevApp } from '@backstage/dev-utils';
|
||||
import { CopilotPage, copilotPlugin } from '../src';
|
||||
import { copilotPlugin, CopilotSidebar, CopilotIndexPage } from '../src';
|
||||
import {
|
||||
UnifiedThemeProvider,
|
||||
themes as builtinThemes,
|
||||
|
|
@ -23,7 +23,6 @@ import {
|
|||
import DarkIcon from '@mui/icons-material/Brightness2';
|
||||
import LightIcon from '@mui/icons-material/WbSunny';
|
||||
import { AppTheme } from '@backstage/core-plugin-api';
|
||||
import { GitHubIcon } from '@backstage/core-components';
|
||||
|
||||
const customThemes: AppTheme[] = [
|
||||
{
|
||||
|
|
@ -49,11 +48,9 @@ const customThemes: AppTheme[] = [
|
|||
createDevApp()
|
||||
.addThemes(customThemes)
|
||||
.registerPlugin(copilotPlugin)
|
||||
.registerPlugin()
|
||||
.addSidebarItem(<CopilotSidebar />)
|
||||
.addPage({
|
||||
element: <CopilotPage />,
|
||||
title: 'Copilot Page',
|
||||
icon: GitHubIcon,
|
||||
element: <CopilotIndexPage />,
|
||||
path: '/copilot',
|
||||
})
|
||||
.render();
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 527 KiB After Width: | Height: | Size: 36 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 729 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 696 KiB |
|
|
@ -46,10 +46,13 @@
|
|||
"@mui/x-charts": "^7.6.1",
|
||||
"luxon": "^3.5.0",
|
||||
"react-use": "^17.3.1",
|
||||
"simple-date-range-calendar": "^1.0.10"
|
||||
"simple-date-range-calendar": "^2.0.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0 || ^18.0.0"
|
||||
"react": "^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^18.0.2",
|
||||
"react-router": "^6.3.0",
|
||||
"react-router-dom": "^6.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.28.0",
|
||||
|
|
@ -65,6 +68,8 @@
|
|||
"msw": "^1.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
"dist",
|
||||
"config.d.ts"
|
||||
],
|
||||
"configSchema": "config.d.ts"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,16 +8,22 @@
|
|||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
import { SubRouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
// @public
|
||||
export const CopilotPage: () => JSX_2.Element;
|
||||
export const CopilotIndexPage: () => JSX_2.Element;
|
||||
|
||||
// @public
|
||||
export const copilotPlugin: BackstagePlugin<
|
||||
{
|
||||
root: RouteRef<undefined>;
|
||||
copilot: RouteRef<undefined>;
|
||||
enterprise: SubRouteRef<undefined>;
|
||||
organization: SubRouteRef<undefined>;
|
||||
},
|
||||
{},
|
||||
{}
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const CopilotSidebar: () => JSX_2.Element;
|
||||
```
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
import { createApiRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
Metric,
|
||||
MetricsType,
|
||||
PeriodRange,
|
||||
} from '@backstage-community/plugin-copilot-common';
|
||||
|
||||
|
|
@ -25,6 +26,16 @@ export const copilotApiRef = createApiRef<CopilotApi>({
|
|||
});
|
||||
|
||||
export interface CopilotApi {
|
||||
getMetrics(startDate: Date, endDate: Date): Promise<Metric[]>;
|
||||
periodRange(): Promise<PeriodRange>;
|
||||
getMetrics(
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
type: MetricsType,
|
||||
team?: string,
|
||||
): Promise<Metric[]>;
|
||||
fetchTeams(
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
type: MetricsType,
|
||||
): Promise<string[]>;
|
||||
periodRange(type: MetricsType): Promise<PeriodRange>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { ResponseError } from '@backstage/errors';
|
|||
import { CopilotApi } from './CopilotApi';
|
||||
import {
|
||||
Metric,
|
||||
MetricsType,
|
||||
PeriodRange,
|
||||
} from '@backstage-community/plugin-copilot-common';
|
||||
import { DateTime } from 'luxon';
|
||||
|
|
@ -31,7 +32,12 @@ export class CopilotClient implements CopilotApi {
|
|||
},
|
||||
) {}
|
||||
|
||||
public async getMetrics(startDate: Date, endDate: Date): Promise<Metric[]> {
|
||||
public async getMetrics(
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
type: MetricsType,
|
||||
team?: string,
|
||||
): Promise<Metric[]> {
|
||||
const queryString = new URLSearchParams();
|
||||
|
||||
queryString.append(
|
||||
|
|
@ -43,17 +49,49 @@ export class CopilotClient implements CopilotApi {
|
|||
DateTime.fromJSDate(endDate).toFormat('yyyy-MM-dd'),
|
||||
);
|
||||
|
||||
queryString.append('type', type);
|
||||
|
||||
if (team) {
|
||||
queryString.append('team', team);
|
||||
}
|
||||
|
||||
const urlSegment = `metrics?${queryString}`;
|
||||
|
||||
return await this.get<Metric[]>(urlSegment);
|
||||
}
|
||||
|
||||
public async periodRange() {
|
||||
const urlSegment = `metrics/period-range`;
|
||||
public async periodRange(type: MetricsType): Promise<PeriodRange> {
|
||||
const queryString = new URLSearchParams();
|
||||
queryString.append('type', type);
|
||||
|
||||
const urlSegment = `metrics/period-range?${queryString}`;
|
||||
|
||||
return await this.get<PeriodRange>(urlSegment);
|
||||
}
|
||||
|
||||
public async fetchTeams(
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
type: MetricsType,
|
||||
): Promise<string[]> {
|
||||
const queryString = new URLSearchParams();
|
||||
|
||||
queryString.append(
|
||||
'startDate',
|
||||
DateTime.fromJSDate(startDate).toFormat('yyyy-MM-dd'),
|
||||
);
|
||||
queryString.append(
|
||||
'endDate',
|
||||
DateTime.fromJSDate(endDate).toFormat('yyyy-MM-dd'),
|
||||
);
|
||||
|
||||
queryString.append('type', type);
|
||||
|
||||
const urlSegment = `teams?${queryString}`;
|
||||
|
||||
return await this.get<string[]>(urlSegment);
|
||||
}
|
||||
|
||||
private async get<T>(path: string): Promise<T> {
|
||||
const baseUrl = await this.options.discoveryApi.getBaseUrl('copilot');
|
||||
const response = await this.options.fetchApi.fetch(`${baseUrl}/${path}`);
|
||||
|
|
|
|||
|
|
@ -18,12 +18,17 @@ import Box from '@mui/material/Box';
|
|||
import Divider from '@mui/material/Divider';
|
||||
import Icon from '@mui/material/Icon';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { InfoCard } from '@backstage/core-components';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
type CardItemProps = {
|
||||
team?: string;
|
||||
title: string;
|
||||
value: number | string;
|
||||
primaryValue: number | string;
|
||||
secondaryValue?: number | string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
icon: ElementType<ReactNode>;
|
||||
|
|
@ -31,17 +36,6 @@ type CardItemProps = {
|
|||
|
||||
const format = 'dd/MM/yyyy';
|
||||
|
||||
const MainBox = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 25,
|
||||
minWidth: 256,
|
||||
minHeight: 138,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
padding: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const TextGroup = styled(Box)(({ theme }) => ({
|
||||
padding: theme.spacing(1),
|
||||
}));
|
||||
|
|
@ -52,14 +46,16 @@ const FooterText = styled(Typography)(({ theme }) => ({
|
|||
}));
|
||||
|
||||
export const Card = ({
|
||||
team,
|
||||
title,
|
||||
value,
|
||||
primaryValue,
|
||||
secondaryValue,
|
||||
startDate,
|
||||
endDate,
|
||||
icon,
|
||||
}: CardItemProps) => {
|
||||
const message = useMemo(() => {
|
||||
if (value === 'N/A') return `No data available`;
|
||||
if (primaryValue === 'N/A') return `No data available`;
|
||||
|
||||
const endDateTime = DateTime.fromJSDate(endDate);
|
||||
const now = DateTime.now();
|
||||
|
|
@ -77,19 +73,38 @@ export const Card = ({
|
|||
return `From ${DateTime.fromJSDate(startDate).toFormat(
|
||||
format,
|
||||
)} to ${endDateTime.toFormat(format)}`;
|
||||
}, [value, startDate, endDate]);
|
||||
}, [primaryValue, startDate, endDate]);
|
||||
|
||||
return (
|
||||
<MainBox>
|
||||
<InfoCard divider={false}>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Icon component={icon} />
|
||||
<TextGroup>
|
||||
<Typography color="textSecondary" variant="subtitle2" component="h2">
|
||||
<Typography color="primary" variant="subtitle2" component="h2">
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="h3" component="h2">
|
||||
{value}
|
||||
</Typography>
|
||||
<Stack direction="row" gap={2} alignItems="flex-end">
|
||||
<Tooltip title={team && secondaryValue ? team : 'Overall'}>
|
||||
<Typography
|
||||
variant="h3"
|
||||
component="h2"
|
||||
color="textPrimary"
|
||||
mb={0}
|
||||
>
|
||||
{primaryValue}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
<Tooltip title={team && secondaryValue && 'Overall'}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="h5"
|
||||
color="textSecondary"
|
||||
mb={0}
|
||||
>
|
||||
{secondaryValue}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</TextGroup>
|
||||
</Box>
|
||||
<Divider />
|
||||
|
|
@ -98,6 +113,6 @@ export const Card = ({
|
|||
{message}
|
||||
</FooterText>
|
||||
</Box>
|
||||
</MainBox>
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import AssessmentIcon from '@mui/icons-material/Assessment';
|
||||
import ThumbUpIcon from '@mui/icons-material/ThumbUp';
|
||||
import CodeIcon from '@mui/icons-material/Code';
|
||||
import Box from '@mui/material/Box';
|
||||
import { Card } from './Card';
|
||||
import { CardsProps } from '../../types';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { Metric } from '@backstage-community/plugin-copilot-common';
|
||||
|
||||
const CardBox = styled(Box)({
|
||||
flex: '1 1 calc(50% - 10px)',
|
||||
minWidth: 300,
|
||||
maxWidth: 'calc(50% - 10px)',
|
||||
boxSizing: 'border-box',
|
||||
});
|
||||
|
||||
export const DashboardCards = ({
|
||||
team,
|
||||
metrics,
|
||||
metricsByTeam,
|
||||
startDate,
|
||||
endDate,
|
||||
}: PropsWithChildren<CardsProps>) => {
|
||||
const sumProperty = (data: any[], property: string) =>
|
||||
data.reduce((acc, item) => acc + (item[property] || 0), 0);
|
||||
|
||||
const calculateAcceptanceRate = (metricsData: Metric[]) => {
|
||||
const totalSuggested = sumProperty(metricsData, 'total_lines_suggested');
|
||||
const totalAccepted = sumProperty(metricsData, 'total_lines_accepted');
|
||||
return totalSuggested > 0
|
||||
? ((totalAccepted / totalSuggested) * 100).toFixed(2).concat('%')
|
||||
: 'N/A';
|
||||
};
|
||||
|
||||
const overallMetrics = {
|
||||
acceptanceRate: calculateAcceptanceRate(metrics),
|
||||
suggestionsCount:
|
||||
metrics.length > 0
|
||||
? sumProperty(metrics, 'total_suggestions_count')
|
||||
: 'N/A',
|
||||
acceptancesCount:
|
||||
metrics.length > 0
|
||||
? sumProperty(metrics, 'total_acceptances_count')
|
||||
: 'N/A',
|
||||
linesAccepted:
|
||||
metrics.length > 0 ? sumProperty(metrics, 'total_lines_accepted') : 'N/A',
|
||||
};
|
||||
|
||||
const teamMetrics = {
|
||||
acceptanceRate: calculateAcceptanceRate(metricsByTeam),
|
||||
suggestionsCount:
|
||||
metricsByTeam.length > 0
|
||||
? sumProperty(metricsByTeam, 'total_suggestions_count')
|
||||
: 'N/A',
|
||||
acceptancesCount:
|
||||
metricsByTeam.length > 0
|
||||
? sumProperty(metricsByTeam, 'total_acceptances_count')
|
||||
: 'N/A',
|
||||
linesAccepted:
|
||||
metricsByTeam.length > 0
|
||||
? sumProperty(metricsByTeam, 'total_lines_accepted')
|
||||
: 'N/A',
|
||||
};
|
||||
|
||||
return (
|
||||
<Box display="flex" flexWrap="wrap" gap={1} justifyContent="space-between">
|
||||
<CardBox>
|
||||
<Card
|
||||
team={team}
|
||||
title="Acceptance Rate Average"
|
||||
primaryValue={
|
||||
team ? teamMetrics.acceptanceRate : overallMetrics.acceptanceRate
|
||||
}
|
||||
secondaryValue={team ? overallMetrics.acceptanceRate : undefined}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
icon={() => (
|
||||
<CheckCircleIcon style={{ color: '#4CAF50' }} fontSize="large" />
|
||||
)}
|
||||
/>
|
||||
</CardBox>
|
||||
<CardBox>
|
||||
<Card
|
||||
team={team}
|
||||
title="Nº of Suggestions"
|
||||
primaryValue={
|
||||
team
|
||||
? teamMetrics.suggestionsCount
|
||||
: overallMetrics.suggestionsCount
|
||||
}
|
||||
secondaryValue={team ? overallMetrics.suggestionsCount : undefined}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
icon={() => (
|
||||
<AssessmentIcon style={{ color: '#2196F3' }} fontSize="large" />
|
||||
)}
|
||||
/>
|
||||
</CardBox>
|
||||
<CardBox>
|
||||
<Card
|
||||
team={team}
|
||||
title="Nº of Accepted Prompts"
|
||||
primaryValue={
|
||||
team
|
||||
? teamMetrics.acceptancesCount
|
||||
: overallMetrics.acceptancesCount
|
||||
}
|
||||
secondaryValue={team ? overallMetrics.acceptancesCount : undefined}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
icon={() => (
|
||||
<ThumbUpIcon style={{ color: '#FF9800' }} fontSize="large" />
|
||||
)}
|
||||
/>
|
||||
</CardBox>
|
||||
<CardBox>
|
||||
<Card
|
||||
team={team}
|
||||
title="Nº Lines of Code Accepted"
|
||||
primaryValue={
|
||||
team ? teamMetrics.linesAccepted : overallMetrics.linesAccepted
|
||||
}
|
||||
secondaryValue={team ? overallMetrics.linesAccepted : undefined}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
icon={() => (
|
||||
<CodeIcon style={{ color: '#FF5722' }} fontSize="large" />
|
||||
)}
|
||||
/>
|
||||
</CardBox>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import AssessmentIcon from '@mui/icons-material/Assessment';
|
||||
import ThumbUpIcon from '@mui/icons-material/ThumbUp';
|
||||
import CodeIcon from '@mui/icons-material/Code';
|
||||
import Box from '@mui/material/Box';
|
||||
import { Card } from './Card';
|
||||
import { CardsProps } from '../../types';
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
const CardBox = styled(Box)(() => ({
|
||||
flex: '1 1 0',
|
||||
}));
|
||||
|
||||
export const EnterpriseCards = ({
|
||||
metrics,
|
||||
startDate,
|
||||
endDate,
|
||||
}: PropsWithChildren<CardsProps>) => {
|
||||
const total_suggestions_count = metrics.reduce((acc, m) => {
|
||||
return acc + m.total_suggestions_count;
|
||||
}, 0);
|
||||
|
||||
const total_acceptances_count = metrics.reduce((acc, m) => {
|
||||
return acc + m.total_acceptances_count;
|
||||
}, 0);
|
||||
|
||||
const total_lines_accepted = metrics.reduce((acc, m) => {
|
||||
return acc + m.total_lines_accepted;
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<Box display="flex" flexWrap="wrap" gap={2}>
|
||||
<CardBox flex={1} minWidth="384px">
|
||||
<Card
|
||||
title="Acceptance Rate Average"
|
||||
value={
|
||||
metrics.length
|
||||
? ((total_acceptances_count / total_suggestions_count) * 100)
|
||||
.toFixed(2)
|
||||
.concat('%')
|
||||
: 'N/A'
|
||||
}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
icon={() => (
|
||||
<CheckCircleIcon style={{ color: '#4CAF50' }} fontSize="large" />
|
||||
)}
|
||||
/>
|
||||
</CardBox>
|
||||
<CardBox flex={1} minWidth="384px">
|
||||
<Card
|
||||
title="Nº of Suggestions"
|
||||
value={metrics.length ? total_suggestions_count : 'N/A'}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
icon={() => (
|
||||
<AssessmentIcon style={{ color: '#2196F3' }} fontSize="large" />
|
||||
)}
|
||||
/>
|
||||
</CardBox>
|
||||
<CardBox flex={1} minWidth="384px">
|
||||
<Card
|
||||
title="Nº of Accepted Prompts"
|
||||
value={metrics.length ? total_acceptances_count : 'N/A'}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
icon={() => (
|
||||
<ThumbUpIcon style={{ color: '#FF9800' }} fontSize="large" />
|
||||
)}
|
||||
/>
|
||||
</CardBox>
|
||||
<CardBox flex={1} minWidth="384px">
|
||||
<Card
|
||||
title="Nº Lines of Code Accepted"
|
||||
value={metrics.length ? total_lines_accepted : 'N/A'}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
icon={() => (
|
||||
<CodeIcon style={{ color: '#FF5722' }} fontSize="large" />
|
||||
)}
|
||||
/>
|
||||
</CardBox>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
@ -21,24 +21,39 @@ import { getLanguageStats } from '../../utils';
|
|||
import { CardsProps } from '../../types';
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
const CardBox = styled(Box)(() => ({
|
||||
flex: '1 1 0',
|
||||
maxWidth: 354,
|
||||
}));
|
||||
const CardBox = styled(Box)({
|
||||
flex: '1 1 calc(50% - 10px)',
|
||||
minWidth: 300,
|
||||
maxWidth: 'calc(50% - 10px)',
|
||||
boxSizing: 'border-box',
|
||||
});
|
||||
|
||||
export const LanguageCards = ({
|
||||
team,
|
||||
metrics,
|
||||
metricsByTeam,
|
||||
startDate,
|
||||
endDate,
|
||||
}: PropsWithChildren<CardsProps>) => {
|
||||
const languageStats = getLanguageStats(metrics);
|
||||
const overallLanguageStats =
|
||||
metrics.length > 0 ? getLanguageStats(metrics) : [];
|
||||
const teamLanguageStats =
|
||||
metricsByTeam.length > 0 ? getLanguageStats(metricsByTeam) : [];
|
||||
|
||||
return (
|
||||
<Box display="flex" flexWrap="wrap" gap={2}>
|
||||
<Box display="flex" flexWrap="wrap" gap={1} justifyContent="space-between">
|
||||
<CardBox>
|
||||
<Card
|
||||
team={team}
|
||||
title="Nº of Languages"
|
||||
value={metrics.length ? languageStats.length : 'N/A'}
|
||||
primaryValue={
|
||||
team
|
||||
? teamLanguageStats.length || 'N/A'
|
||||
: overallLanguageStats.length || 'N/A'
|
||||
}
|
||||
secondaryValue={
|
||||
team ? overallLanguageStats.length || 'N/A' : undefined
|
||||
}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
icon={() => (
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { EnterpriseCards } from './EnterpriseCards';
|
||||
export { DashboardCards } from './DashboardCards';
|
||||
export { LanguageCards } from './LanguageCards';
|
||||
|
|
|
|||
|
|
@ -18,23 +18,22 @@ import Box from '@mui/material/Box';
|
|||
import Divider from '@mui/material/Divider';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { InfoCard } from '@backstage/core-components';
|
||||
|
||||
type ChartProps = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
const MainBox = styled(Box)(({ theme }) => ({
|
||||
const InfoCardStyled = styled(InfoCard)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRadius: 25,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
padding: theme.spacing(2),
|
||||
width: '100%',
|
||||
}));
|
||||
|
||||
export const Chart = ({ title, children }: PropsWithChildren<ChartProps>) => {
|
||||
return (
|
||||
<MainBox>
|
||||
<InfoCardStyled divider={false} noPadding>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Typography variant="h3" component="h2">
|
||||
{title}
|
||||
|
|
@ -42,6 +41,6 @@ export const Chart = ({ title, children }: PropsWithChildren<ChartProps>) => {
|
|||
</Box>
|
||||
<Divider />
|
||||
{children}
|
||||
</MainBox>
|
||||
</InfoCardStyled>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { BarChart, LineChart } from '@mui/x-charts';
|
||||
import { Chart } from './Chart';
|
||||
import { ChartsProps } from '../../types';
|
||||
import { DateTime } from 'luxon';
|
||||
import {
|
||||
createAcceptanceRateSeries,
|
||||
createTotalSuggestionsAndAcceptancesSeries,
|
||||
createTotalLinesSuggestedAndAcceptedSeries,
|
||||
createTotalActiveUsersSeries,
|
||||
} from './seriesUtils';
|
||||
|
||||
const MainBox = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
}));
|
||||
|
||||
export const DashboardCharts = ({
|
||||
team,
|
||||
metrics,
|
||||
metricsByTeam,
|
||||
}: PropsWithChildren<ChartsProps>) => (
|
||||
<MainBox>
|
||||
<Chart title="Acceptance Rate %">
|
||||
<LineChart
|
||||
xAxis={[
|
||||
{
|
||||
id: 'days',
|
||||
data: metrics.map(x => new Date(x.day)),
|
||||
scaleType: 'point',
|
||||
valueFormatter: date =>
|
||||
DateTime.fromJSDate(date).toFormat('dd-MM-yy'),
|
||||
},
|
||||
]}
|
||||
bottomAxis={{
|
||||
tickLabelStyle: {
|
||||
angle: 30,
|
||||
textAnchor: 'start',
|
||||
},
|
||||
}}
|
||||
series={createAcceptanceRateSeries(metrics, metricsByTeam, team)}
|
||||
height={300}
|
||||
/>
|
||||
</Chart>
|
||||
<Chart title="Total Suggestions Count | Total Acceptances Count">
|
||||
<LineChart
|
||||
xAxis={[
|
||||
{
|
||||
id: 'days',
|
||||
data: metrics.map(x => new Date(x.day)),
|
||||
scaleType: 'point',
|
||||
valueFormatter: date =>
|
||||
DateTime.fromJSDate(date).toFormat('dd-MM-yy'),
|
||||
},
|
||||
]}
|
||||
bottomAxis={{
|
||||
tickLabelStyle: {
|
||||
angle: 30,
|
||||
textAnchor: 'start',
|
||||
},
|
||||
}}
|
||||
series={createTotalSuggestionsAndAcceptancesSeries(
|
||||
metrics,
|
||||
metricsByTeam,
|
||||
team,
|
||||
)}
|
||||
height={300}
|
||||
/>
|
||||
</Chart>
|
||||
<Chart title="Total Lines Suggested | Total Lines Accepted">
|
||||
<LineChart
|
||||
xAxis={[
|
||||
{
|
||||
id: 'days',
|
||||
data: metrics.map(x => new Date(x.day)),
|
||||
scaleType: 'point',
|
||||
valueFormatter: date =>
|
||||
DateTime.fromJSDate(date).toFormat('dd-MM-yy'),
|
||||
},
|
||||
]}
|
||||
bottomAxis={{
|
||||
tickLabelStyle: {
|
||||
angle: 30,
|
||||
textAnchor: 'start',
|
||||
},
|
||||
}}
|
||||
series={createTotalLinesSuggestedAndAcceptedSeries(
|
||||
metrics,
|
||||
metricsByTeam,
|
||||
team,
|
||||
)}
|
||||
height={300}
|
||||
/>
|
||||
</Chart>
|
||||
<Chart title="Total Active Users">
|
||||
<BarChart
|
||||
xAxis={[
|
||||
{
|
||||
data: metrics.map(x => new Date(x.day)),
|
||||
scaleType: 'band',
|
||||
valueFormatter: date =>
|
||||
DateTime.fromJSDate(date).toFormat('dd-MM-yy'),
|
||||
},
|
||||
]}
|
||||
bottomAxis={{
|
||||
tickLabelStyle: {
|
||||
angle: 30,
|
||||
textAnchor: 'start',
|
||||
},
|
||||
}}
|
||||
series={createTotalActiveUsersSeries(metrics, metricsByTeam, team)}
|
||||
height={300}
|
||||
/>
|
||||
</Chart>
|
||||
</MainBox>
|
||||
);
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { BarChart, LineChart } from '@mui/x-charts';
|
||||
import { Chart } from './Chart';
|
||||
import { ChartsProps } from '../../types';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
const MainBox = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
}));
|
||||
|
||||
export const EnterpriseCharts = ({
|
||||
metrics,
|
||||
}: PropsWithChildren<ChartsProps>) => {
|
||||
return (
|
||||
<MainBox>
|
||||
<Chart title="Acceptance Rate %">
|
||||
<LineChart
|
||||
xAxis={[
|
||||
{
|
||||
id: 'days',
|
||||
data: metrics.map(x => new Date(x.day)),
|
||||
scaleType: 'point',
|
||||
valueFormatter: date =>
|
||||
DateTime.fromJSDate(date).toFormat('dd-MM-yy'),
|
||||
},
|
||||
]}
|
||||
bottomAxis={{
|
||||
tickLabelStyle: {
|
||||
angle: 30,
|
||||
textAnchor: 'start',
|
||||
},
|
||||
}}
|
||||
series={[
|
||||
{
|
||||
id: 'total_lines_suggested',
|
||||
label: 'Acceptance Rate',
|
||||
valueFormatter: v => v?.toFixed(2).concat('%') ?? 'N/A',
|
||||
data: metrics.map(
|
||||
x => (x.total_lines_accepted / x.total_lines_suggested) * 100,
|
||||
),
|
||||
},
|
||||
]}
|
||||
height={300}
|
||||
/>
|
||||
</Chart>
|
||||
<Chart title="Total Suggestions Count | Total Acceptances Count">
|
||||
<LineChart
|
||||
xAxis={[
|
||||
{
|
||||
id: 'days',
|
||||
data: metrics.map(x => new Date(x.day)),
|
||||
scaleType: 'point',
|
||||
valueFormatter: date =>
|
||||
DateTime.fromJSDate(date).toFormat('dd-MM-yy'),
|
||||
},
|
||||
]}
|
||||
bottomAxis={{
|
||||
tickLabelStyle: {
|
||||
angle: 30,
|
||||
textAnchor: 'start',
|
||||
},
|
||||
}}
|
||||
series={[
|
||||
{
|
||||
id: 'total_suggestions_count',
|
||||
label: 'Total Suggestions',
|
||||
data: metrics.map(x => x.total_suggestions_count),
|
||||
},
|
||||
{
|
||||
id: 'total_acceptances_count',
|
||||
label: 'Total Acceptances',
|
||||
data: metrics.map(x => x.total_acceptances_count),
|
||||
},
|
||||
]}
|
||||
height={300}
|
||||
/>
|
||||
</Chart>
|
||||
<Chart title="Total Lines Suggested | Total Lines Accepted">
|
||||
<LineChart
|
||||
xAxis={[
|
||||
{
|
||||
id: 'days',
|
||||
data: metrics.map(x => new Date(x.day)),
|
||||
scaleType: 'point',
|
||||
valueFormatter: date =>
|
||||
DateTime.fromJSDate(date).toFormat('dd-MM-yy'),
|
||||
},
|
||||
]}
|
||||
bottomAxis={{
|
||||
tickLabelStyle: {
|
||||
angle: 30,
|
||||
textAnchor: 'start',
|
||||
},
|
||||
}}
|
||||
series={[
|
||||
{
|
||||
id: 'total_lines_suggested',
|
||||
label: 'Total Lines Suggested',
|
||||
data: metrics.map(x => x.total_lines_suggested),
|
||||
},
|
||||
{
|
||||
id: 'total_lines_accepted',
|
||||
label: 'Total Lines Accepted',
|
||||
data: metrics.map(x => x.total_lines_accepted),
|
||||
},
|
||||
]}
|
||||
height={300}
|
||||
/>
|
||||
</Chart>
|
||||
<Chart title="Total Active Users">
|
||||
<BarChart
|
||||
xAxis={[
|
||||
{
|
||||
data: metrics.map(x => new Date(x.day)),
|
||||
scaleType: 'band',
|
||||
valueFormatter: date =>
|
||||
DateTime.fromJSDate(date).toFormat('dd-MM-yy'),
|
||||
},
|
||||
]}
|
||||
bottomAxis={{
|
||||
tickLabelStyle: {
|
||||
angle: 30,
|
||||
textAnchor: 'start',
|
||||
},
|
||||
}}
|
||||
series={[
|
||||
{
|
||||
label: 'Total Active Users',
|
||||
data: metrics.map(x => x.total_active_users),
|
||||
},
|
||||
]}
|
||||
height={300}
|
||||
/>
|
||||
</Chart>
|
||||
</MainBox>
|
||||
);
|
||||
};
|
||||
|
|
@ -18,7 +18,7 @@ import Box from '@mui/material/Box';
|
|||
import { styled } from '@mui/material/styles';
|
||||
import { PieChart } from '@mui/x-charts';
|
||||
import { Chart } from './Chart';
|
||||
import { LanguagesBreakdownTable } from '../Table/LanguagesBreakdownTable';
|
||||
import { LanguagesComparisonTables } from '../Table/LanguagesComparisonTables';
|
||||
import {
|
||||
getLanguageStats,
|
||||
getTopLanguagesByAcceptanceRate,
|
||||
|
|
@ -36,55 +36,113 @@ const RowBox = styled(Box)(({ theme }) => ({
|
|||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: theme.spacing(2),
|
||||
width: '100%',
|
||||
}));
|
||||
|
||||
export const LanguageCharts = ({ metrics }: PropsWithChildren<ChartsProps>) => {
|
||||
const languageStats = getLanguageStats(metrics);
|
||||
const Fieldset = styled('fieldset')(({ theme }) => ({
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
padding: theme.spacing(2),
|
||||
marginBottom: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const topFiveLanguagesByAcceptedPrompts = getTopLanguagesByAcceptedPrompts(
|
||||
metrics,
|
||||
5,
|
||||
);
|
||||
const topFiveLanguagesByAcceptanceRate = getTopLanguagesByAcceptanceRate(
|
||||
metrics,
|
||||
const Legend = styled('legend')(({ theme }) => ({
|
||||
fontSize: theme.typography.h6.fontSize,
|
||||
padding: `0 ${theme.spacing(1)}`,
|
||||
}));
|
||||
|
||||
const renderPieCharts = (promptsData: any[], rateData: any[]) => (
|
||||
<RowBox>
|
||||
<Chart title="Top 5 Languages By Accepted Prompts">
|
||||
<PieChart
|
||||
margin={{ right: 200 }}
|
||||
series={[
|
||||
{
|
||||
data: promptsData.map(lan => ({
|
||||
id: lan.language,
|
||||
value: lan.totalAcceptances,
|
||||
label: `${lan.language}: ${lan.totalAcceptances}`,
|
||||
})),
|
||||
highlightScope: { fade: 'global', highlight: 'item' },
|
||||
faded: { innerRadius: 30, additionalRadius: -30, color: 'gray' },
|
||||
},
|
||||
]}
|
||||
height={300}
|
||||
/>
|
||||
</Chart>
|
||||
<Chart title="Top 5 Languages By Acceptance Rate">
|
||||
<PieChart
|
||||
margin={{ right: 200 }}
|
||||
series={[
|
||||
{
|
||||
data: rateData.map(lan => ({
|
||||
id: lan.language,
|
||||
value: lan.acceptanceRate * 100,
|
||||
label: `${lan.language}: ${(lan.acceptanceRate * 100).toFixed(
|
||||
2,
|
||||
)}%`,
|
||||
})),
|
||||
highlightScope: { fade: 'global', highlight: 'item' },
|
||||
faded: { innerRadius: 30, additionalRadius: -30, color: 'gray' },
|
||||
valueFormatter: item => `${item.value.toFixed(2)}%`,
|
||||
},
|
||||
]}
|
||||
height={300}
|
||||
/>
|
||||
</Chart>
|
||||
</RowBox>
|
||||
);
|
||||
|
||||
export const LanguageCharts = ({
|
||||
team,
|
||||
metrics,
|
||||
metricsByTeam,
|
||||
}: PropsWithChildren<ChartsProps>) => {
|
||||
const languageStats = getLanguageStats(metrics);
|
||||
const languageStatsByTeam = getLanguageStats(metricsByTeam);
|
||||
|
||||
const topFiveLanguagesByAcceptedPromptsOverall =
|
||||
getTopLanguagesByAcceptedPrompts(metrics, 5);
|
||||
const topFiveLanguagesByAcceptanceRateOverall =
|
||||
getTopLanguagesByAcceptanceRate(metrics, 5);
|
||||
const topFiveLanguagesByAcceptedPromptsTeam =
|
||||
getTopLanguagesByAcceptedPrompts(metricsByTeam, 5);
|
||||
const topFiveLanguagesByAcceptanceRateTeam = getTopLanguagesByAcceptanceRate(
|
||||
metricsByTeam,
|
||||
5,
|
||||
);
|
||||
|
||||
return (
|
||||
<MainBox>
|
||||
<RowBox>
|
||||
<Chart title="Top 5 Languages By Accepted Prompts">
|
||||
<PieChart
|
||||
series={[
|
||||
{
|
||||
data: topFiveLanguagesByAcceptedPrompts.map(lan => ({
|
||||
id: lan.language,
|
||||
value: lan.totalAcceptances,
|
||||
label: lan.language,
|
||||
})),
|
||||
},
|
||||
]}
|
||||
height={300}
|
||||
/>
|
||||
</Chart>
|
||||
<Chart title="Top 5 Languages By Acceptance Rate">
|
||||
<PieChart
|
||||
series={[
|
||||
{
|
||||
data: topFiveLanguagesByAcceptanceRate.map(lan => ({
|
||||
id: lan.language,
|
||||
value: lan.acceptanceRate * 100,
|
||||
label: lan.language,
|
||||
})),
|
||||
valueFormatter: item => `${item.value.toFixed(2)}%`,
|
||||
},
|
||||
]}
|
||||
height={300}
|
||||
/>
|
||||
</Chart>
|
||||
</RowBox>
|
||||
{team && (
|
||||
<Fieldset>
|
||||
<Legend>{team}</Legend>
|
||||
{renderPieCharts(
|
||||
topFiveLanguagesByAcceptedPromptsTeam,
|
||||
topFiveLanguagesByAcceptanceRateTeam,
|
||||
)}
|
||||
</Fieldset>
|
||||
)}
|
||||
{team && (
|
||||
<Fieldset>
|
||||
<Legend>Overall</Legend>
|
||||
{renderPieCharts(
|
||||
topFiveLanguagesByAcceptedPromptsOverall,
|
||||
topFiveLanguagesByAcceptanceRateOverall,
|
||||
)}
|
||||
</Fieldset>
|
||||
)}
|
||||
{!team &&
|
||||
renderPieCharts(
|
||||
topFiveLanguagesByAcceptedPromptsOverall,
|
||||
topFiveLanguagesByAcceptanceRateOverall,
|
||||
)}
|
||||
<Chart title="Languages Breakdown">
|
||||
<LanguagesBreakdownTable rows={languageStats} />
|
||||
<LanguagesComparisonTables
|
||||
team={team}
|
||||
overallRows={languageStats}
|
||||
teamRows={languageStatsByTeam}
|
||||
/>
|
||||
</Chart>
|
||||
</MainBox>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { EnterpriseCharts } from './EnterpriseCharts';
|
||||
export { DashboardCharts } from './DashboardCharts';
|
||||
export { LanguageCharts } from './LanguageCharts';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import { Metric } from '@backstage-community/plugin-copilot-common';
|
||||
|
||||
const addSeriesIfTeam = (
|
||||
series: any[],
|
||||
condition: boolean,
|
||||
seriesConfig: any,
|
||||
) => {
|
||||
if (condition) {
|
||||
return series.push(seriesConfig);
|
||||
}
|
||||
return series;
|
||||
};
|
||||
|
||||
export const createAcceptanceRateSeries = (
|
||||
metrics: Metric[],
|
||||
metricsByTeam: Metric[],
|
||||
team?: string,
|
||||
) => {
|
||||
const series = [
|
||||
{
|
||||
id: 'total_lines_suggested',
|
||||
label: 'Acceptance Rate (Overall)',
|
||||
valueFormatter: (v: number | null) => v?.toFixed(2).concat('%') ?? 'N/A',
|
||||
data: metrics.map(
|
||||
x => (x.total_lines_accepted / x.total_lines_suggested) * 100,
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
addSeriesIfTeam(series, !!team, {
|
||||
id: 'total_lines_suggested_by_team',
|
||||
label: `Acceptance Rate (${team})`,
|
||||
valueFormatter: (v: number | null) => v?.toFixed(2).concat('%') ?? 'N/A',
|
||||
data: metrics.map(metric => {
|
||||
const metricByteam = metricsByTeam.find(
|
||||
teamMetric => teamMetric.day === metric.day,
|
||||
);
|
||||
|
||||
if (!metricByteam) return null;
|
||||
|
||||
return (
|
||||
(metricByteam.total_lines_accepted /
|
||||
metricByteam.total_lines_suggested) *
|
||||
100
|
||||
);
|
||||
}),
|
||||
});
|
||||
|
||||
return series;
|
||||
};
|
||||
|
||||
export const createTotalSuggestionsAndAcceptancesSeries = (
|
||||
metrics: Metric[],
|
||||
metricsByTeam: Metric[],
|
||||
team?: string,
|
||||
) => {
|
||||
const series = [
|
||||
{
|
||||
id: 'total_suggestions_count_general',
|
||||
label: 'Total Suggestions (Overall)',
|
||||
valueFormatter: (v: number | null) => v?.toString() ?? 'N/A',
|
||||
data: metrics.map(metric => metric.total_suggestions_count),
|
||||
},
|
||||
{
|
||||
id: 'total_acceptances_count_general',
|
||||
label: 'Total Acceptances (Overall)',
|
||||
valueFormatter: (v: number | null) => v?.toString() ?? 'N/A',
|
||||
data: metrics.map(metric => metric.total_acceptances_count),
|
||||
},
|
||||
];
|
||||
|
||||
addSeriesIfTeam(series, !!team, {
|
||||
id: 'total_suggestions_count_by_team',
|
||||
label: `Total Suggestions (${team})`,
|
||||
valueFormatter: (v: number | null) => v?.toString() ?? 'N/A',
|
||||
data: metrics.map(
|
||||
metric =>
|
||||
metricsByTeam.find(teamMetric => teamMetric.day === metric.day)
|
||||
?.total_suggestions_count ?? null,
|
||||
),
|
||||
});
|
||||
|
||||
addSeriesIfTeam(series, !!team, {
|
||||
id: 'total_acceptances_count_by_team',
|
||||
label: `Total Acceptances (${team})`,
|
||||
valueFormatter: (v: number | null) => v?.toString() ?? 'N/A',
|
||||
data: metrics.map(
|
||||
metric =>
|
||||
metricsByTeam.find(teamMetric => teamMetric.day === metric.day)
|
||||
?.total_acceptances_count ?? null,
|
||||
),
|
||||
});
|
||||
|
||||
return series;
|
||||
};
|
||||
|
||||
export const createTotalLinesSuggestedAndAcceptedSeries = (
|
||||
metrics: Metric[],
|
||||
metricsByTeam: Metric[],
|
||||
team?: string,
|
||||
) => {
|
||||
const series = [
|
||||
{
|
||||
id: 'total_lines_suggested',
|
||||
label: 'Total Lines Suggested (Overall)',
|
||||
valueFormatter: (v: number | null) => v?.toString() ?? 'N/A',
|
||||
data: metrics.map(x => x.total_lines_suggested),
|
||||
},
|
||||
{
|
||||
id: 'total_lines_accepted',
|
||||
label: 'Total Lines Accepted (Overall)',
|
||||
valueFormatter: (v: number | null) => v?.toString() ?? 'N/A',
|
||||
data: metrics.map(x => x.total_lines_accepted),
|
||||
},
|
||||
];
|
||||
|
||||
addSeriesIfTeam(series, !!team, {
|
||||
id: 'total_lines_suggested_by_team',
|
||||
label: `Total Lines Suggested (${team})`,
|
||||
valueFormatter: (v: number | null) => v?.toString() ?? 'N/A',
|
||||
data: metrics.map(
|
||||
metric =>
|
||||
metricsByTeam.find(teamMetric => teamMetric.day === metric.day)
|
||||
?.total_lines_suggested ?? null,
|
||||
),
|
||||
});
|
||||
|
||||
addSeriesIfTeam(series, !!team, {
|
||||
id: 'total_lines_accepted_by_team',
|
||||
label: `Total Lines Accepted (${team})`,
|
||||
valueFormatter: (v: number | null) => v?.toString() ?? 'N/A',
|
||||
data: metrics.map(
|
||||
metric =>
|
||||
metricsByTeam.find(teamMetric => teamMetric.day === metric.day)
|
||||
?.total_lines_accepted ?? null,
|
||||
),
|
||||
});
|
||||
|
||||
return series;
|
||||
};
|
||||
|
||||
export const createTotalActiveUsersSeries = (
|
||||
metrics: Metric[],
|
||||
metricsByTeam: Metric[],
|
||||
team?: string,
|
||||
) => {
|
||||
const series = [
|
||||
{
|
||||
label: 'Total Active Users (Overall)',
|
||||
valueFormatter: (v: number | null) => v?.toString() ?? 'N/A',
|
||||
data: metrics.map(x => x.total_active_users),
|
||||
},
|
||||
];
|
||||
|
||||
addSeriesIfTeam(series, !!team, {
|
||||
id: 'total_active_users_by_team',
|
||||
label: `Total Active Users (${team})`,
|
||||
valueFormatter: (v: number | null) => v?.toString() ?? 'N/A',
|
||||
data: metrics.map(
|
||||
metric =>
|
||||
metricsByTeam.find(teamMetric => teamMetric.day === metric.day)
|
||||
?.total_active_users ?? null,
|
||||
),
|
||||
});
|
||||
|
||||
return series;
|
||||
};
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import React from 'react';
|
||||
import Select, { SelectChangeEvent } from '@mui/material/Select';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { FilterProps } from '../../types';
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
const InputStyled = styled(OutlinedInput)(({ theme }) => ({
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
}));
|
||||
|
||||
export function SelectTeamFilter({ options, team, setTeam }: FilterProps) {
|
||||
const handleChange = (event: SelectChangeEvent<string>) => {
|
||||
setTeam(event.target.value as string);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setTeam(undefined);
|
||||
};
|
||||
|
||||
const getPlaceholderMessage = () => {
|
||||
const teamCount = options.length ?? 0;
|
||||
if (teamCount === 0) {
|
||||
return 'No teams available for selection';
|
||||
}
|
||||
if (teamCount === 1) {
|
||||
return '1 team available for selection';
|
||||
}
|
||||
return `${teamCount} teams available for selection`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={team ?? ''}
|
||||
onChange={handleChange}
|
||||
input={
|
||||
<InputStyled
|
||||
endAdornment={
|
||||
team && (
|
||||
<IconButton onClick={handleClear} sx={{ mr: 2 }}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
/>
|
||||
}
|
||||
inputProps={{ 'aria-label': 'Team selection' }}
|
||||
fullWidth
|
||||
displayEmpty
|
||||
renderValue={selected => {
|
||||
if (!selected) {
|
||||
return <em>{getPlaceholderMessage()}</em>;
|
||||
}
|
||||
return selected;
|
||||
}}
|
||||
>
|
||||
{options?.map(option => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
export { SelectTeamFilter } from './SelectTeamFilter';
|
||||
|
|
@ -13,18 +13,20 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { styled, useTheme } from '@mui/material/styles';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { Calendar } from 'simple-date-range-calendar';
|
||||
import { useMetrics } from '../../hooks';
|
||||
import { CardsProps, ChartsProps } from '../../types';
|
||||
import { Progress } from '@backstage/core-components';
|
||||
import { useSharedDateRange } from '../../contexts';
|
||||
import { useMetrics, useMetricsByTeam, useTeams } from '../../hooks';
|
||||
import { CardsProps, ChartsProps, FilterProps } from '../../types';
|
||||
import { InfoCard, Progress } from '@backstage/core-components';
|
||||
import { useSharedDateRange, useSharedTeam } from '../../contexts';
|
||||
|
||||
type MetricsProps = {
|
||||
Cards: React.ElementType<CardsProps>;
|
||||
Charts: React.ElementType<ChartsProps>;
|
||||
Filters: React.ElementType<FilterProps>;
|
||||
};
|
||||
|
||||
const MainBox = styled(Box)(({ theme }) => ({
|
||||
|
|
@ -33,37 +35,72 @@ const MainBox = styled(Box)(({ theme }) => ({
|
|||
gap: theme.spacing(2),
|
||||
}));
|
||||
|
||||
export const RenderCharts = ({ Charts }: Omit<MetricsProps, 'Cards'>) => {
|
||||
export const RenderFilters = ({
|
||||
Filters,
|
||||
}: Omit<Omit<MetricsProps, 'Cards'>, 'Charts'>) => {
|
||||
const [state] = useSharedDateRange();
|
||||
const { items, loading } = useMetrics(state.startDate, state.endDate);
|
||||
const [team, setTeam] = useSharedTeam();
|
||||
const data = useTeams(state.startDate, state.endDate);
|
||||
const options = useMemo(
|
||||
() => data.items?.map(x => ({ label: x, value: x })) ?? [],
|
||||
[data.items],
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
if (!Filters) return null;
|
||||
|
||||
if (data.loading) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
return <Charts metrics={items || []} />;
|
||||
return <Filters options={options} team={team} setTeam={setTeam} />;
|
||||
};
|
||||
|
||||
export const RenderCards = ({ Cards }: Omit<MetricsProps, 'Charts'>) => {
|
||||
export const RenderCharts = ({
|
||||
Charts,
|
||||
}: Omit<Omit<MetricsProps, 'Cards'>, 'Filters'>) => {
|
||||
const [state] = useSharedDateRange();
|
||||
const { items, loading } = useMetrics(state.startDate, state.endDate);
|
||||
const [team] = useSharedTeam();
|
||||
const data = useMetrics(state.startDate, state.endDate);
|
||||
const dataPerTeam = useMetricsByTeam(state.startDate, state.endDate);
|
||||
|
||||
if (loading) {
|
||||
if (data.loading || dataPerTeam.loading) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Charts
|
||||
team={team}
|
||||
metrics={data.items ?? []}
|
||||
metricsByTeam={dataPerTeam.items ?? []}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const RenderCards = ({
|
||||
Cards,
|
||||
}: Omit<Omit<MetricsProps, 'Charts'>, 'Filters'>) => {
|
||||
const [state] = useSharedDateRange();
|
||||
const [team] = useSharedTeam();
|
||||
const data = useMetrics(state.startDate, state.endDate);
|
||||
const dataPerTeam = useMetricsByTeam(state.startDate, state.endDate);
|
||||
|
||||
if (data.loading || dataPerTeam.loading) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Cards
|
||||
team={team}
|
||||
startDate={state.startDate}
|
||||
endDate={state.endDate}
|
||||
metrics={items || []}
|
||||
metrics={data.items ?? []}
|
||||
metricsByTeam={dataPerTeam.items ?? []}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Metrics = ({ Cards, Charts }: MetricsProps) => {
|
||||
export const Metrics = ({ Cards, Charts, Filters }: MetricsProps) => {
|
||||
const [state, setState] = useSharedDateRange();
|
||||
const theme = useTheme();
|
||||
|
||||
const onDateRangeIsSelected = (start: Date, end: Date) => {
|
||||
setState({ startDate: start, endDate: end });
|
||||
|
|
@ -72,15 +109,20 @@ export const Metrics = ({ Cards, Charts }: MetricsProps) => {
|
|||
return (
|
||||
<MainBox>
|
||||
<Box display="flex" gap={2}>
|
||||
<Box flex={1} maxWidth={300}>
|
||||
<Calendar
|
||||
theme={theme.palette.mode}
|
||||
startDate={state.startDate}
|
||||
endDate={state.endDate}
|
||||
onDateRangeIsSelected={onDateRangeIsSelected}
|
||||
/>
|
||||
<Box flex={1} maxWidth={296}>
|
||||
<InfoCard divider={false} noPadding>
|
||||
<Calendar
|
||||
styles={{ borderRadius: 4, width: 296 }}
|
||||
startDate={state.startDate}
|
||||
endDate={state.endDate}
|
||||
onDateRangeIsSelected={onDateRangeIsSelected}
|
||||
/>
|
||||
</InfoCard>
|
||||
</Box>
|
||||
<Box flex={2}>
|
||||
<Box flex={1}>
|
||||
<Stack pb={1.5}>
|
||||
<RenderFilters Filters={Filters} />
|
||||
</Stack>
|
||||
<RenderCards Cards={Cards} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Routes, Route } from 'react-router';
|
||||
import { HomePage } from './HomePage';
|
||||
import { EnterprisePage } from './EnterprisePage';
|
||||
import { OrganizationPage } from './OrganizationPage';
|
||||
|
||||
export const CopilotIndexPage = () => (
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/enterprise" element={<EnterprisePage />} />
|
||||
<Route path="/organization" element={<OrganizationPage />} />
|
||||
</Routes>
|
||||
);
|
||||
|
|
@ -14,64 +14,30 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Header,
|
||||
Page,
|
||||
Content,
|
||||
TabbedCard,
|
||||
CardTab,
|
||||
} from '@backstage/core-components';
|
||||
import { Metrics } from '../Metrics';
|
||||
import { LanguageCards, EnterpriseCards } from '../Cards';
|
||||
import { LanguageCharts, EnterpriseCharts } from '../Charts';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { themes } from '@backstage/theme';
|
||||
import { appThemeApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import { ThemeProvider } from '@emotion/react';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { SharedDateRangeProvider } from '../../contexts';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { Header, Page, Content } from '@backstage/core-components';
|
||||
import { SharedDateRangeProvider, SharedTeamProvider } from '../../contexts';
|
||||
|
||||
const StyledContent = styled(Content)(({ theme }) => ({
|
||||
margin: `${theme.spacing(0)} !important`,
|
||||
padding: `${theme.spacing(0)} !important`,
|
||||
'& > div': {
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
}));
|
||||
|
||||
export const CopilotPage = () => {
|
||||
const appThemeApi = useApi(appThemeApiRef);
|
||||
const activeThemeId = useObservable(
|
||||
appThemeApi.activeThemeId$(),
|
||||
appThemeApi.getActiveThemeId(),
|
||||
);
|
||||
const prefersDarkMode = window.matchMedia(
|
||||
'(prefers-color-scheme: dark)',
|
||||
).matches;
|
||||
const mode = activeThemeId ?? (prefersDarkMode ? 'dark' : 'light');
|
||||
const theme = (mode === 'dark' ? themes.dark : themes.light).getTheme('v5')!;
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Page themeId="tool">
|
||||
<Header
|
||||
title="Copilot Adoption"
|
||||
subtitle="Exploring the Impact and Integration of AI Assistance in Development Workflows"
|
||||
/>
|
||||
<StyledContent>
|
||||
<SharedDateRangeProvider>
|
||||
<TabbedCard>
|
||||
<CardTab label="Enterprise">
|
||||
<Metrics Cards={EnterpriseCards} Charts={EnterpriseCharts} />
|
||||
</CardTab>
|
||||
<CardTab label="Languages">
|
||||
<Metrics Cards={LanguageCards} Charts={LanguageCharts} />
|
||||
</CardTab>
|
||||
</TabbedCard>
|
||||
</SharedDateRangeProvider>
|
||||
</StyledContent>
|
||||
</Page>
|
||||
</ThemeProvider>
|
||||
);
|
||||
type CopilotPageProps = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
themeId: string;
|
||||
};
|
||||
|
||||
export function CopilotPage({
|
||||
children,
|
||||
themeId,
|
||||
title,
|
||||
subtitle,
|
||||
}: PropsWithChildren<CopilotPageProps>): React.JSX.Element {
|
||||
return (
|
||||
<Page themeId={themeId}>
|
||||
<Header title={title} subtitle={subtitle} />
|
||||
<Content>
|
||||
<SharedDateRangeProvider>
|
||||
<SharedTeamProvider>{children}</SharedTeamProvider>
|
||||
</SharedDateRangeProvider>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { TabbedCard, CardTab } from '@backstage/core-components';
|
||||
import { Metrics } from '../Metrics';
|
||||
import { LanguageCards, DashboardCards } from '../Cards';
|
||||
import { LanguageCharts, DashboardCharts } from '../Charts';
|
||||
import { SelectTeamFilter } from '../Filters';
|
||||
import { CopilotPage } from './CopilotPage';
|
||||
import { configApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
export const EnterprisePage = (): React.JSX.Element => {
|
||||
const navigate = useNavigate();
|
||||
const configApi = useApi(configApiRef);
|
||||
|
||||
useEffect(() => {
|
||||
const enterpriseKey = configApi.getOptionalString('copilot.enterprise');
|
||||
if (!enterpriseKey) {
|
||||
navigate('/copilot');
|
||||
}
|
||||
}, [navigate, configApi]);
|
||||
|
||||
return (
|
||||
<CopilotPage
|
||||
title="Enterprise Metrics Overview"
|
||||
subtitle="Track AI Adoption and Performance Metrics Across Your Enterprise"
|
||||
themeId="tool"
|
||||
>
|
||||
<TabbedCard>
|
||||
<CardTab label="Enterprise">
|
||||
<Metrics
|
||||
Filters={SelectTeamFilter}
|
||||
Cards={DashboardCards}
|
||||
Charts={DashboardCharts}
|
||||
/>
|
||||
</CardTab>
|
||||
<CardTab label="Languages">
|
||||
<Metrics
|
||||
Filters={SelectTeamFilter}
|
||||
Cards={LanguageCards}
|
||||
Charts={LanguageCharts}
|
||||
/>
|
||||
</CardTab>
|
||||
</TabbedCard>
|
||||
</CopilotPage>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { configApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import Container from '@mui/material/Container';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import { CopilotPage } from './CopilotPage';
|
||||
|
||||
interface InfoCardProps {
|
||||
title: string;
|
||||
description: string[];
|
||||
buttonText: string;
|
||||
onClick: () => void;
|
||||
warning?: string | null;
|
||||
}
|
||||
|
||||
const InfoCard = ({
|
||||
title,
|
||||
description,
|
||||
buttonText,
|
||||
onClick,
|
||||
warning,
|
||||
}: InfoCardProps) => (
|
||||
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<CardContent sx={{ flexGrow: 1 }}>
|
||||
<Typography gutterBottom variant="h5" component="div">
|
||||
{title}
|
||||
</Typography>
|
||||
{description.map((text, index) => (
|
||||
<Typography
|
||||
key={index}
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
))}
|
||||
</CardContent>
|
||||
<Box sx={{ p: 2, pt: 0 }}>
|
||||
{warning ? (
|
||||
<Alert severity="warning" variant="outlined">
|
||||
{warning}
|
||||
</Alert>
|
||||
) : (
|
||||
<Button variant="contained" color="primary" fullWidth onClick={onClick}>
|
||||
{buttonText}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
|
||||
export const HomePage = (): React.JSX.Element => {
|
||||
const configApi = useApi(configApiRef);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const enterpriseConfig = configApi.getOptionalString('copilot.enterprise');
|
||||
const organizationConfig = configApi.getOptionalString(
|
||||
'copilot.organization',
|
||||
);
|
||||
|
||||
const handleNavigate = (path: string) => {
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<CopilotPage
|
||||
title="Copilot Dashboard"
|
||||
subtitle="Exploring the Impact and Integration of AI Assistance in Development Workflows"
|
||||
themeId="tool"
|
||||
>
|
||||
<Container
|
||||
maxWidth="lg"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '70vh',
|
||||
}}
|
||||
>
|
||||
<Grid container spacing={4} justifyContent="center" gap={2}>
|
||||
<Grid item xs={12} sm={6} md={5}>
|
||||
<InfoCard
|
||||
title="Enterprise"
|
||||
description={[
|
||||
'Dive deep into enterprise-level metrics to track performance, user engagement, and more. Get an overview of how AI assistance is being adopted across the entire enterprise.',
|
||||
'You can also explore metrics broken down by individual teams to gain more insights.',
|
||||
]}
|
||||
buttonText="Go to Enterprise"
|
||||
onClick={() => handleNavigate('/copilot/enterprise')}
|
||||
warning={
|
||||
enterpriseConfig
|
||||
? null
|
||||
: "Please add the 'copilot.enterprise' variable in the configuration to enable this feature."
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6} md={5}>
|
||||
<InfoCard
|
||||
title="Organization"
|
||||
description={[
|
||||
'Explore organization-wide statistics and gain insights into user activities and trends. Understand the broader impact of AI assistance within your organization.',
|
||||
'Additionally, view metrics by teams to get a granular understanding of adoption patterns.',
|
||||
]}
|
||||
buttonText="Go to Organization"
|
||||
onClick={() => handleNavigate('/copilot/organization')}
|
||||
warning={
|
||||
organizationConfig
|
||||
? null
|
||||
: "Please add the 'copilot.organization' variable in the configuration to enable this feature."
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</CopilotPage>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { TabbedCard, CardTab } from '@backstage/core-components';
|
||||
import { Metrics } from '../Metrics';
|
||||
import { LanguageCards, DashboardCards } from '../Cards';
|
||||
import { LanguageCharts, DashboardCharts } from '../Charts';
|
||||
import { CopilotPage } from './CopilotPage';
|
||||
import { SelectTeamFilter } from '../Filters';
|
||||
import { configApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
export const OrganizationPage = (): React.JSX.Element => {
|
||||
const navigate = useNavigate();
|
||||
const configApi = useApi(configApiRef);
|
||||
|
||||
useEffect(() => {
|
||||
const organizationKey = configApi.getOptionalString('copilot.organization');
|
||||
if (!organizationKey) {
|
||||
navigate('/copilot');
|
||||
}
|
||||
}, [navigate, configApi]);
|
||||
|
||||
return (
|
||||
<CopilotPage
|
||||
title="Organization Statistics"
|
||||
subtitle="Explore AI Usage and Activity Trends Within Your Organization"
|
||||
themeId="tool"
|
||||
>
|
||||
<TabbedCard>
|
||||
<CardTab label="Organization">
|
||||
<Metrics
|
||||
Filters={SelectTeamFilter}
|
||||
Cards={DashboardCards}
|
||||
Charts={DashboardCharts}
|
||||
/>
|
||||
</CardTab>
|
||||
<CardTab label="Languages">
|
||||
<Metrics
|
||||
Filters={SelectTeamFilter}
|
||||
Cards={LanguageCards}
|
||||
Charts={LanguageCharts}
|
||||
/>
|
||||
</CardTab>
|
||||
</TabbedCard>
|
||||
</CopilotPage>
|
||||
);
|
||||
};
|
||||
|
|
@ -13,4 +13,4 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { CopilotPage } from './CopilotPage';
|
||||
export { CopilotIndexPage } from './CopilotIndexPage';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {
|
||||
SidebarItem,
|
||||
SidebarSubmenu,
|
||||
SidebarSubmenuItem,
|
||||
} from '@backstage/core-components';
|
||||
import {
|
||||
configApiRef,
|
||||
IconComponent,
|
||||
useApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import {
|
||||
SupportAgent as SupportAgentIcon,
|
||||
Business as BusinessIcon,
|
||||
Group as GroupIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
export const Sidebar = (): React.JSX.Element => {
|
||||
const configApi = useApi(configApiRef);
|
||||
|
||||
const enterpriseConfig = configApi.getOptionalString('copilot.enterprise');
|
||||
const organizationConfig = configApi.getOptionalString(
|
||||
'copilot.organization',
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarItem
|
||||
icon={SupportAgentIcon as IconComponent}
|
||||
to="copilot"
|
||||
text="Copilot"
|
||||
>
|
||||
<SidebarSubmenu title="Copilot">
|
||||
{enterpriseConfig && (
|
||||
<SidebarSubmenuItem
|
||||
title="Enterprise"
|
||||
to="copilot/enterprise"
|
||||
icon={BusinessIcon as IconComponent}
|
||||
/>
|
||||
)}
|
||||
{organizationConfig && (
|
||||
<SidebarSubmenuItem
|
||||
title="Organization"
|
||||
to="copilot/organization"
|
||||
icon={GroupIcon as IconComponent}
|
||||
/>
|
||||
)}
|
||||
</SidebarSubmenu>
|
||||
</SidebarItem>
|
||||
);
|
||||
};
|
||||
|
|
@ -13,4 +13,4 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export {};
|
||||
export { Sidebar } from './Sidebar';
|
||||
|
|
@ -1,248 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TablePagination from '@mui/material/TablePagination';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import TableSortLabel from '@mui/material/TableSortLabel';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
type LanguageStats = {
|
||||
language: string;
|
||||
totalSuggestions: number;
|
||||
totalAcceptances: number;
|
||||
acceptanceRate: number;
|
||||
};
|
||||
|
||||
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
|
||||
if (b[orderBy] < a[orderBy]) {
|
||||
return -1;
|
||||
}
|
||||
if (b[orderBy] > a[orderBy]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
type Order = 'asc' | 'desc';
|
||||
|
||||
function getComparator<Key extends keyof any>(
|
||||
order: Order,
|
||||
orderBy: Key,
|
||||
): (
|
||||
a: { [key in Key]: number | string },
|
||||
b: { [key in Key]: number | string },
|
||||
) => number {
|
||||
return order === 'desc'
|
||||
? (a, b) => descendingComparator(a, b, orderBy)
|
||||
: (a, b) => -descendingComparator(a, b, orderBy);
|
||||
}
|
||||
|
||||
function stableSort<T>(array: T[], comparator: (a: T, b: T) => number) {
|
||||
const stabilizedThis = array.map((el, index) => [el, index] as [T, number]);
|
||||
stabilizedThis.sort((a, b) => {
|
||||
const order = comparator(a[0], b[0]);
|
||||
if (order !== 0) return order;
|
||||
return a[1] - b[1];
|
||||
});
|
||||
return stabilizedThis.map(el => el[0]);
|
||||
}
|
||||
|
||||
interface HeadCell {
|
||||
disablePadding: boolean;
|
||||
id: keyof LanguageStats;
|
||||
label: string;
|
||||
numeric: boolean;
|
||||
}
|
||||
|
||||
const headCells: HeadCell[] = [
|
||||
{
|
||||
id: 'language',
|
||||
numeric: false,
|
||||
disablePadding: true,
|
||||
label: 'Language',
|
||||
},
|
||||
{
|
||||
id: 'totalAcceptances',
|
||||
numeric: true,
|
||||
disablePadding: false,
|
||||
label: 'Accepted Prompts',
|
||||
},
|
||||
{
|
||||
id: 'totalSuggestions',
|
||||
numeric: true,
|
||||
disablePadding: false,
|
||||
label: 'Total Suggestions',
|
||||
},
|
||||
{
|
||||
id: 'acceptanceRate',
|
||||
numeric: true,
|
||||
disablePadding: false,
|
||||
label: 'Acceptance Rate (%)',
|
||||
},
|
||||
];
|
||||
|
||||
interface EnhancedTableProps {
|
||||
order: Order;
|
||||
orderBy: string;
|
||||
onRequestSort: (
|
||||
event: React.MouseEvent<unknown>,
|
||||
property: keyof LanguageStats,
|
||||
) => void;
|
||||
}
|
||||
|
||||
const EnhancedTableHead = (props: EnhancedTableProps) => {
|
||||
const { order, orderBy, onRequestSort } = props;
|
||||
|
||||
const createSortHandler =
|
||||
(property: keyof LanguageStats) => (event: React.MouseEvent<unknown>) => {
|
||||
onRequestSort(event, property);
|
||||
};
|
||||
|
||||
return (
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{headCells.map(headCell => (
|
||||
<TableCell
|
||||
key={headCell.id}
|
||||
align={headCell.numeric ? 'right' : 'left'}
|
||||
padding={headCell.disablePadding ? 'none' : 'normal'}
|
||||
sortDirection={orderBy === headCell.id ? order : false}
|
||||
>
|
||||
<TableSortLabel
|
||||
active={orderBy === headCell.id}
|
||||
direction={orderBy === headCell.id ? order : 'asc'}
|
||||
onClick={createSortHandler(headCell.id)}
|
||||
>
|
||||
{headCell.label}
|
||||
{orderBy === headCell.id ? (
|
||||
<Typography
|
||||
sx={{
|
||||
border: 0,
|
||||
clip: 'rect(0 0 0 0)',
|
||||
height: 1,
|
||||
margin: -1,
|
||||
overflow: 'hidden',
|
||||
padding: 0,
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
width: 1,
|
||||
}}
|
||||
>
|
||||
{order === 'desc' ? 'sorted descending' : 'sorted ascending'}
|
||||
</Typography>
|
||||
) : null}
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledTableContainer = styled(TableContainer)(({ theme }) => ({
|
||||
width: '100%',
|
||||
marginBottom: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledTable = styled(Table)(() => ({
|
||||
minWidth: 750,
|
||||
}));
|
||||
|
||||
export function LanguagesBreakdownTable({ rows }: { rows: LanguageStats[] }) {
|
||||
const [order, setOrder] = React.useState<Order>('desc');
|
||||
const [orderBy, setOrderBy] =
|
||||
React.useState<keyof LanguageStats>('totalAcceptances');
|
||||
const [page, setPage] = React.useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState(10);
|
||||
|
||||
const handleRequestSort = (
|
||||
_event: React.MouseEvent<unknown>,
|
||||
property: keyof LanguageStats,
|
||||
) => {
|
||||
const isAsc = orderBy === property && order === 'asc';
|
||||
setOrder(isAsc ? 'desc' : 'asc');
|
||||
setOrderBy(property);
|
||||
};
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const emptyRows =
|
||||
rowsPerPage - Math.min(rowsPerPage, rows.length - page * rowsPerPage);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledTableContainer>
|
||||
<StyledTable
|
||||
aria-labelledby="tableTitle"
|
||||
size="medium"
|
||||
aria-label="enhanced table"
|
||||
>
|
||||
<EnhancedTableHead
|
||||
order={order}
|
||||
orderBy={orderBy}
|
||||
onRequestSort={handleRequestSort}
|
||||
/>
|
||||
<TableBody>
|
||||
{stableSort(rows, getComparator(order, orderBy))
|
||||
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
.map(row => (
|
||||
<TableRow hover tabIndex={-1} key={row.language}>
|
||||
<TableCell component="th" scope="row" padding="none">
|
||||
{row.language}
|
||||
</TableCell>
|
||||
<TableCell align="right">{row.totalAcceptances}</TableCell>
|
||||
<TableCell align="right">{row.totalSuggestions}</TableCell>
|
||||
<TableCell align="right">
|
||||
{(row.acceptanceRate * 100).toFixed(2)}%
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{emptyRows > 0 && (
|
||||
<TableRow style={{ height: 53 * emptyRows }}>
|
||||
<TableCell colSpan={headCells.length} />
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</StyledTable>
|
||||
</StyledTableContainer>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[10, 15, 25]}
|
||||
component="div"
|
||||
count={rows.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TablePagination from '@mui/material/TablePagination';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import TableSortLabel from '@mui/material/TableSortLabel';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
type LanguageStats = {
|
||||
language: string;
|
||||
totalSuggestions: number;
|
||||
totalAcceptances: number;
|
||||
acceptanceRate: number;
|
||||
};
|
||||
|
||||
type Order = 'asc' | 'desc';
|
||||
|
||||
interface EnhancedTableProps {
|
||||
title?: string;
|
||||
rows: LanguageStats[];
|
||||
}
|
||||
|
||||
const headCells = [
|
||||
{ id: 'language', label: 'Language', numeric: false },
|
||||
{ id: 'totalAcceptances', label: 'Accepted Prompts', numeric: true },
|
||||
{ id: 'totalSuggestions', label: 'Accepted Lines of Code', numeric: true },
|
||||
{ id: 'acceptanceRate', label: 'Acceptance Rate (%)', numeric: true },
|
||||
];
|
||||
|
||||
const StyledTableContainer = styled(TableContainer)(() => ({
|
||||
flex: 1,
|
||||
}));
|
||||
|
||||
const StyledTableWrapper = styled(Box)(() => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
}));
|
||||
|
||||
const EnhancedTable = ({ title, rows }: EnhancedTableProps) => {
|
||||
const [order, setOrder] = React.useState<Order>('desc');
|
||||
const [orderBy, setOrderBy] =
|
||||
React.useState<keyof LanguageStats>('totalAcceptances');
|
||||
const [page, setPage] = React.useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState(10);
|
||||
|
||||
const handleRequestSort = (
|
||||
_event: React.MouseEvent<unknown>,
|
||||
property: keyof LanguageStats,
|
||||
) => {
|
||||
const isAsc = orderBy === property && order === 'asc';
|
||||
setOrder(isAsc ? 'desc' : 'asc');
|
||||
setOrderBy(property);
|
||||
};
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const sortedRows = [...rows].sort((a, b) => {
|
||||
const aValue = a[orderBy];
|
||||
const bValue = b[orderBy];
|
||||
|
||||
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
||||
return aValue.localeCompare(bValue) * (order === 'asc' ? 1 : -1);
|
||||
}
|
||||
|
||||
if (typeof aValue === 'number' && typeof bValue === 'number') {
|
||||
return (aValue - bValue) * (order === 'asc' ? 1 : -1);
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
const paginatedRows = sortedRows.slice(
|
||||
page * rowsPerPage,
|
||||
page * rowsPerPage + rowsPerPage,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledTableWrapper>
|
||||
{title && (
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
<StyledTableContainer>
|
||||
<Table
|
||||
aria-labelledby="tableTitle"
|
||||
size="medium"
|
||||
aria-label="enhanced table"
|
||||
>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{headCells.map(headCell => (
|
||||
<TableCell
|
||||
key={headCell.id}
|
||||
align={headCell.numeric ? 'right' : 'left'}
|
||||
sortDirection={orderBy === headCell.id ? order : false}
|
||||
>
|
||||
<TableSortLabel
|
||||
active={orderBy === headCell.id}
|
||||
direction={orderBy === headCell.id ? order : 'asc'}
|
||||
onClick={event =>
|
||||
handleRequestSort(
|
||||
event,
|
||||
headCell.id as keyof LanguageStats,
|
||||
)
|
||||
}
|
||||
>
|
||||
{headCell.label}
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{paginatedRows.map(row => (
|
||||
<TableRow hover tabIndex={-1} key={row.language}>
|
||||
<TableCell component="th" scope="row">
|
||||
{row.language}
|
||||
</TableCell>
|
||||
<TableCell align="right">{row.totalAcceptances}</TableCell>
|
||||
<TableCell align="right">{row.totalSuggestions}</TableCell>
|
||||
<TableCell align="right">
|
||||
{(row.acceptanceRate * 100).toFixed(2)}%
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</StyledTableContainer>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[10, 15, 25]}
|
||||
component="div"
|
||||
count={rows.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
/>
|
||||
</StyledTableWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export function LanguagesComparisonTables({
|
||||
team,
|
||||
overallRows,
|
||||
teamRows,
|
||||
}: {
|
||||
team?: string;
|
||||
overallRows: LanguageStats[];
|
||||
teamRows: LanguageStats[];
|
||||
}) {
|
||||
return (
|
||||
<Box display="flex" flexDirection="row" gap={4} height="100%">
|
||||
{team && (
|
||||
<Box flex={1}>
|
||||
<EnhancedTable title={team} rows={teamRows} />
|
||||
</Box>
|
||||
)}
|
||||
<Box flex={1}>
|
||||
<EnhancedTable
|
||||
title={team ? 'Overall' : undefined}
|
||||
rows={overallRows}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -13,4 +13,4 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { LanguagesBreakdownTable } from './LanguagesBreakdownTable';
|
||||
export { LanguagesComparisonTables } from './LanguagesComparisonTables';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
export { CopilotIndexPage } from './Pages';
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import createStateContext from 'react-use/lib/factory/createStateContext';
|
||||
|
||||
export const [useSharedTeam, SharedTeamProvider] = createStateContext<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
|
|
@ -17,3 +17,5 @@ export {
|
|||
useSharedDateRange,
|
||||
SharedDateRangeProvider,
|
||||
} from './SharedDateRangeContext';
|
||||
|
||||
export { useSharedTeam, SharedTeamProvider } from './SharedTeamContext';
|
||||
|
|
|
|||
|
|
@ -13,5 +13,8 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { useTeams } from './useTeams';
|
||||
export { useMetrics } from './useMetrics';
|
||||
export { useMetricsByTeam } from './useMetricsByTeam';
|
||||
export { usePeriodRange } from './usePeriodRange';
|
||||
export { useSetMetricsTypeFromRoute } from './useSetMetricsTypeFromRoute';
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import useAsync from 'react-use/lib/useAsync';
|
|||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { Metric } from '@backstage-community/plugin-copilot-common';
|
||||
import { copilotApiRef } from '../api';
|
||||
import { useSetMetricsTypeFromRoute } from './useSetMetricsTypeFromRoute';
|
||||
|
||||
export function useMetrics(
|
||||
startDate: Date,
|
||||
|
|
@ -29,10 +30,14 @@ export function useMetrics(
|
|||
} {
|
||||
const api = useApi(copilotApiRef);
|
||||
|
||||
const { value, loading, error } = useAsync(
|
||||
() => api.getMetrics(startDate, endDate),
|
||||
[api, startDate, endDate],
|
||||
);
|
||||
const type = useSetMetricsTypeFromRoute();
|
||||
|
||||
const { value, loading, error } = useAsync(() => {
|
||||
if (type) {
|
||||
return api.getMetrics(startDate, endDate, type);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}, [api, type, startDate, endDate]);
|
||||
|
||||
return {
|
||||
items: value,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { Metric } from '@backstage-community/plugin-copilot-common';
|
||||
import { copilotApiRef } from '../api';
|
||||
import { useSharedTeam } from '../contexts';
|
||||
import { useSetMetricsTypeFromRoute } from './useSetMetricsTypeFromRoute';
|
||||
|
||||
export function useMetricsByTeam(
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
): {
|
||||
items?: Metric[];
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
} {
|
||||
const api = useApi(copilotApiRef);
|
||||
const [team] = useSharedTeam();
|
||||
const type = useSetMetricsTypeFromRoute();
|
||||
|
||||
const { value, loading, error } = useAsync(() => {
|
||||
if (type && team) {
|
||||
return api.getMetrics(startDate, endDate, type, team);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}, [api, type, team, startDate, endDate]);
|
||||
|
||||
return {
|
||||
items: value,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import useAsync from 'react-use/lib/useAsync';
|
|||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { PeriodRange } from '@backstage-community/plugin-copilot-common';
|
||||
import { copilotApiRef } from '../api';
|
||||
import { useSetMetricsTypeFromRoute } from './useSetMetricsTypeFromRoute';
|
||||
|
||||
export function usePeriodRange(): {
|
||||
item: PeriodRange | undefined;
|
||||
|
|
@ -25,8 +26,14 @@ export function usePeriodRange(): {
|
|||
error?: Error;
|
||||
} {
|
||||
const api = useApi(copilotApiRef);
|
||||
const type = useSetMetricsTypeFromRoute();
|
||||
|
||||
const { value, loading, error } = useAsync(() => api.periodRange(), [api]);
|
||||
const { value, loading, error } = useAsync(() => {
|
||||
if (type) {
|
||||
return api.periodRange(type);
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}, [api, type]);
|
||||
|
||||
return {
|
||||
item: value,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { MetricsType } from '@backstage-community/plugin-copilot-common';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { mappingRoutes } from '../utils';
|
||||
|
||||
export function useSetMetricsTypeFromRoute(): MetricsType | undefined {
|
||||
const [type, setType] = useState<MetricsType>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!mappingRoutes[window.location.pathname])
|
||||
throw Error('Mapping not implemented');
|
||||
|
||||
setType(mappingRoutes[window.location.pathname]);
|
||||
}, []);
|
||||
|
||||
return type;
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import useAsync from 'react-use/esm/useAsync';
|
||||
import { copilotApiRef } from '../api';
|
||||
import { useSetMetricsTypeFromRoute } from './useSetMetricsTypeFromRoute';
|
||||
|
||||
export function useTeams(startDate: Date, endDate: Date) {
|
||||
const api = useApi(copilotApiRef);
|
||||
|
||||
const type = useSetMetricsTypeFromRoute();
|
||||
|
||||
const { value, loading, error } = useAsync(() => {
|
||||
if (type) {
|
||||
return api.fetchTeams(startDate, endDate, type);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}, [api, type, startDate, endDate]);
|
||||
|
||||
return {
|
||||
items: value,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
|
@ -20,4 +20,4 @@
|
|||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export { copilotPlugin, CopilotPage } from './plugin';
|
||||
export { copilotPlugin, CopilotSidebar, CopilotIndexPage } from './plugin';
|
||||
|
|
|
|||
|
|
@ -15,14 +15,18 @@
|
|||
*/
|
||||
import {
|
||||
createApiFactory,
|
||||
createComponentExtension,
|
||||
createPlugin,
|
||||
createRoutableExtension,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
import { rootRouteRef } from './routes';
|
||||
import { CopilotClient, copilotApiRef } from './api';
|
||||
import {
|
||||
copilotRouteRef,
|
||||
enterpriseRouteRef,
|
||||
organizationRouteRef,
|
||||
} from './routes';
|
||||
|
||||
/**
|
||||
* The Copilot plugin for Backstage.
|
||||
|
|
@ -43,19 +47,35 @@ export const copilotPlugin = createPlugin({
|
|||
}),
|
||||
],
|
||||
routes: {
|
||||
root: rootRouteRef,
|
||||
copilot: copilotRouteRef,
|
||||
enterprise: enterpriseRouteRef,
|
||||
organization: organizationRouteRef,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Copilot page component for the Copilot plugin.
|
||||
* CopilotIndexPage component for the Copilot plugin.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const CopilotPage = copilotPlugin.provide(
|
||||
export const CopilotIndexPage = copilotPlugin.provide(
|
||||
createRoutableExtension({
|
||||
name: 'CopilotPage',
|
||||
component: () => import('./components/Pages').then(m => m.CopilotPage),
|
||||
mountPoint: rootRouteRef,
|
||||
name: 'CopilotIndexPage',
|
||||
component: () => import('./components/Pages').then(m => m.CopilotIndexPage),
|
||||
mountPoint: copilotRouteRef,
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* CopilotSidebar component for the Copilot plugin.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const CopilotSidebar = copilotPlugin.provide(
|
||||
createComponentExtension({
|
||||
name: 'OrganizationCopilotPage',
|
||||
component: {
|
||||
lazy: () => import('./components/Sidebar').then(m => m.Sidebar),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -13,8 +13,20 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createRouteRef } from '@backstage/core-plugin-api';
|
||||
import { createRouteRef, createSubRouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
id: 'copilot',
|
||||
export const copilotRouteRef = createRouteRef({
|
||||
id: 'copilot-dashboard',
|
||||
});
|
||||
|
||||
export const enterpriseRouteRef = createSubRouteRef({
|
||||
id: 'enterprise-dashboard',
|
||||
parent: copilotRouteRef,
|
||||
path: '/enterprise',
|
||||
});
|
||||
|
||||
export const organizationRouteRef = createSubRouteRef({
|
||||
id: 'organization-dashboard',
|
||||
parent: copilotRouteRef,
|
||||
path: '/organization',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
|
||||
import { Metric } from '@backstage-community/plugin-copilot-common';
|
||||
import React from 'react';
|
||||
|
||||
export type LanguageStats = {
|
||||
language: string;
|
||||
|
|
@ -24,11 +25,28 @@ export type LanguageStats = {
|
|||
};
|
||||
|
||||
export type CardsProps = {
|
||||
team?: string;
|
||||
metrics: Metric[];
|
||||
metricsByTeam: Metric[];
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
};
|
||||
|
||||
export type ChartsProps = {
|
||||
team?: string;
|
||||
metrics: Metric[];
|
||||
metricsByTeam: Metric[];
|
||||
};
|
||||
|
||||
export type FilterProps = {
|
||||
team?: string;
|
||||
setTeam: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
options: FilterOptions;
|
||||
};
|
||||
|
||||
export type FilterOptions = FilterOption[];
|
||||
|
||||
export type FilterOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Metric } from '@backstage-community/plugin-copilot-common';
|
||||
import {
|
||||
Metric,
|
||||
MetricsType,
|
||||
} from '@backstage-community/plugin-copilot-common';
|
||||
import { LanguageStats } from '../types';
|
||||
|
||||
export function getTopLanguagesByAcceptedPrompts(
|
||||
|
|
@ -56,3 +59,8 @@ export function getLanguageStats(metricsArray: Metric[]): LanguageStats[] {
|
|||
});
|
||||
return Array.from(languageStatsMap.values());
|
||||
}
|
||||
|
||||
export const mappingRoutes: Record<string, MetricsType> = {
|
||||
'/copilot/enterprise': 'enterprise',
|
||||
'/copilot/organization': 'organization',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2773,6 +2773,7 @@ __metadata:
|
|||
supertest: ^6.2.4
|
||||
winston: ^3.2.1
|
||||
yn: ^4.0.0
|
||||
zod: ^3.23.8
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
|
|
@ -2811,9 +2812,12 @@ __metadata:
|
|||
luxon: ^3.5.0
|
||||
msw: ^1.0.0
|
||||
react-use: ^17.3.1
|
||||
simple-date-range-calendar: ^1.0.10
|
||||
simple-date-range-calendar: ^2.0.6
|
||||
peerDependencies:
|
||||
react: ^17.0.0 || ^18.0.0
|
||||
react-dom: ^18.0.2
|
||||
react-router: ^6.3.0
|
||||
react-router-dom: ^6.3.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
|
|
@ -6000,7 +6004,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@emotion/hash@npm:^0.9.1, @emotion/hash@npm:^0.9.2":
|
||||
"@emotion/hash@npm:^0.9.2":
|
||||
version: 0.9.2
|
||||
resolution: "@emotion/hash@npm:0.9.2"
|
||||
checksum: 379bde2830ccb0328c2617ec009642321c0e009a46aa383dfbe75b679c6aea977ca698c832d225a893901f29d7b3eef0e38cf341f560f6b2b56f1ff23c172387
|
||||
|
|
@ -8253,7 +8257,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mui/icons-material@npm:^5.16.4, @mui/icons-material@npm:^5.16.7":
|
||||
"@mui/icons-material@npm:^5.16.7":
|
||||
version: 5.16.7
|
||||
resolution: "@mui/icons-material@npm:5.16.7"
|
||||
dependencies:
|
||||
|
|
@ -8269,7 +8273,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mui/material@npm:^5.12.2, @mui/material@npm:^5.16.5, @mui/material@npm:^5.16.7":
|
||||
"@mui/material@npm:^5.12.2, @mui/material@npm:^5.16.7":
|
||||
version: 5.16.7
|
||||
resolution: "@mui/material@npm:5.16.7"
|
||||
dependencies:
|
||||
|
|
@ -8302,7 +8306,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mui/private-theming@npm:^5.16.5, @mui/private-theming@npm:^5.16.6":
|
||||
"@mui/private-theming@npm:^5.16.6":
|
||||
version: 5.16.6
|
||||
resolution: "@mui/private-theming@npm:5.16.6"
|
||||
dependencies:
|
||||
|
|
@ -8340,38 +8344,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mui/styles@npm:^5.16.4":
|
||||
version: 5.16.5
|
||||
resolution: "@mui/styles@npm:5.16.5"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.23.9
|
||||
"@emotion/hash": ^0.9.1
|
||||
"@mui/private-theming": ^5.16.5
|
||||
"@mui/types": ^7.2.15
|
||||
"@mui/utils": ^5.16.5
|
||||
clsx: ^2.1.0
|
||||
csstype: ^3.1.3
|
||||
hoist-non-react-statics: ^3.3.2
|
||||
jss: ^10.10.0
|
||||
jss-plugin-camel-case: ^10.10.0
|
||||
jss-plugin-default-unit: ^10.10.0
|
||||
jss-plugin-global: ^10.10.0
|
||||
jss-plugin-nested: ^10.10.0
|
||||
jss-plugin-props-sort: ^10.10.0
|
||||
jss-plugin-rule-value-function: ^10.10.0
|
||||
jss-plugin-vendor-prefixer: ^10.10.0
|
||||
prop-types: ^15.8.1
|
||||
peerDependencies:
|
||||
"@types/react": ^17.0.0 || ^18.0.0
|
||||
react: ^17.0.0
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: 1ce9083949a19f1cd9765412f26cd5bfa919b0dafe3a851f57710204aa831714da4a84095bbff1da3785f5cf7686532be41726b798d0ab067a94d5349482e880
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mui/system@npm:^5.15.15, @mui/system@npm:^5.16.5, @mui/system@npm:^5.16.7":
|
||||
"@mui/system@npm:^5.15.15, @mui/system@npm:^5.16.7":
|
||||
version: 5.16.7
|
||||
resolution: "@mui/system@npm:5.16.7"
|
||||
dependencies:
|
||||
|
|
@ -8411,7 +8384,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mui/utils@npm:^5.14.15, @mui/utils@npm:^5.15.14, @mui/utils@npm:^5.16.5, @mui/utils@npm:^5.16.6":
|
||||
"@mui/utils@npm:^5.14.15, @mui/utils@npm:^5.15.14, @mui/utils@npm:^5.16.6":
|
||||
version: 5.16.6
|
||||
resolution: "@mui/utils@npm:5.16.6"
|
||||
dependencies:
|
||||
|
|
@ -17663,10 +17636,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"date-fns@npm:^3.6.0":
|
||||
version: 3.6.0
|
||||
resolution: "date-fns@npm:3.6.0"
|
||||
checksum: 0daa1e9a436cf99f9f2ae9232b55e11f3dd46132bee10987164f3eebd29f245b2e066d7d7db40782627411ecf18551d8f4c9fcdf2226e48bb66545407d448ab7
|
||||
"date-fns@npm:^4.1.0":
|
||||
version: 4.1.0
|
||||
resolution: "date-fns@npm:4.1.0"
|
||||
checksum: fb681b242cccabed45494468f64282a7d375ea970e0adbcc5dcc92dcb7aba49b2081c2c9739d41bf71ce89ed68dd73bebfe06ca35129490704775d091895710b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -23729,7 +23702,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jss-plugin-camel-case@npm:^10.10.0, jss-plugin-camel-case@npm:^10.5.1":
|
||||
"jss-plugin-camel-case@npm:^10.5.1":
|
||||
version: 10.10.0
|
||||
resolution: "jss-plugin-camel-case@npm:10.10.0"
|
||||
dependencies:
|
||||
|
|
@ -23740,7 +23713,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jss-plugin-default-unit@npm:^10.10.0, jss-plugin-default-unit@npm:^10.5.1":
|
||||
"jss-plugin-default-unit@npm:^10.5.1":
|
||||
version: 10.10.0
|
||||
resolution: "jss-plugin-default-unit@npm:10.10.0"
|
||||
dependencies:
|
||||
|
|
@ -23750,7 +23723,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jss-plugin-global@npm:^10.10.0, jss-plugin-global@npm:^10.5.1":
|
||||
"jss-plugin-global@npm:^10.5.1":
|
||||
version: 10.10.0
|
||||
resolution: "jss-plugin-global@npm:10.10.0"
|
||||
dependencies:
|
||||
|
|
@ -23760,7 +23733,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jss-plugin-nested@npm:^10.10.0, jss-plugin-nested@npm:^10.5.1":
|
||||
"jss-plugin-nested@npm:^10.5.1":
|
||||
version: 10.10.0
|
||||
resolution: "jss-plugin-nested@npm:10.10.0"
|
||||
dependencies:
|
||||
|
|
@ -23771,7 +23744,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jss-plugin-props-sort@npm:^10.10.0, jss-plugin-props-sort@npm:^10.5.1":
|
||||
"jss-plugin-props-sort@npm:^10.5.1":
|
||||
version: 10.10.0
|
||||
resolution: "jss-plugin-props-sort@npm:10.10.0"
|
||||
dependencies:
|
||||
|
|
@ -23781,7 +23754,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jss-plugin-rule-value-function@npm:^10.10.0, jss-plugin-rule-value-function@npm:^10.5.1":
|
||||
"jss-plugin-rule-value-function@npm:^10.5.1":
|
||||
version: 10.10.0
|
||||
resolution: "jss-plugin-rule-value-function@npm:10.10.0"
|
||||
dependencies:
|
||||
|
|
@ -23792,7 +23765,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jss-plugin-vendor-prefixer@npm:^10.10.0, jss-plugin-vendor-prefixer@npm:^10.5.1":
|
||||
"jss-plugin-vendor-prefixer@npm:^10.5.1":
|
||||
version: 10.10.0
|
||||
resolution: "jss-plugin-vendor-prefixer@npm:10.10.0"
|
||||
dependencies:
|
||||
|
|
@ -23803,7 +23776,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jss@npm:10.10.0, jss@npm:^10.10.0, jss@npm:^10.5.1, jss@npm:~10.10.0":
|
||||
"jss@npm:10.10.0, jss@npm:^10.5.1, jss@npm:~10.10.0":
|
||||
version: 10.10.0
|
||||
resolution: "jss@npm:10.10.0"
|
||||
dependencies:
|
||||
|
|
@ -30685,23 +30658,18 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"simple-date-range-calendar@npm:^1.0.10":
|
||||
version: 1.0.10
|
||||
resolution: "simple-date-range-calendar@npm:1.0.10"
|
||||
"simple-date-range-calendar@npm:^2.0.6":
|
||||
version: 2.0.6
|
||||
resolution: "simple-date-range-calendar@npm:2.0.6"
|
||||
dependencies:
|
||||
date-fns: ^4.1.0
|
||||
peerDependencies:
|
||||
"@emotion/react": ^11.13.0
|
||||
"@emotion/styled": ^11.13.0
|
||||
"@mui/icons-material": ^5.16.4
|
||||
"@mui/material": ^5.16.5
|
||||
"@mui/styles": ^5.16.4
|
||||
"@mui/system": ^5.16.5
|
||||
date-fns: ^3.6.0
|
||||
typescript: ^5.5.3
|
||||
web-vitals: ^2.1.4
|
||||
peerDependencies:
|
||||
react: ^18.3.1
|
||||
react-dom: ^18.3.1
|
||||
checksum: a7a77d47cd8be2e1658b0c30dc9a39ce5f62663b8356c4a96150e21b10d369f628391c7051e83e2821075ce944599a4729b67f4a0598d47e81135e686edc1e27
|
||||
"@mui/material": ^5.16.7
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
checksum: f24d91e32136183106313ec35d795551ffc95b8943bbe4fb2075ad9ebf8c4d29f41da58b5ee83a1f68323f75c0f7fb2ae0d22c6b68e34ee0bdb20593d53ebea4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -32813,16 +32781,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@npm:^5.5.3, typescript@npm:~5.5.0":
|
||||
version: 5.5.4
|
||||
resolution: "typescript@npm:5.5.4"
|
||||
bin:
|
||||
tsc: bin/tsc
|
||||
tsserver: bin/tsserver
|
||||
checksum: b309040f3a1cd91c68a5a58af6b9fdd4e849b8c42d837b2c2e73f9a4f96a98c4f1ed398a9aab576ee0a4748f5690cf594e6b99dbe61de7839da748c41e6d6ca8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@npm:~5.3.0":
|
||||
version: 5.3.3
|
||||
resolution: "typescript@npm:5.3.3"
|
||||
|
|
@ -32833,6 +32791,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@npm:~5.5.0":
|
||||
version: 5.5.4
|
||||
resolution: "typescript@npm:5.5.4"
|
||||
bin:
|
||||
tsc: bin/tsc
|
||||
tsserver: bin/tsserver
|
||||
checksum: b309040f3a1cd91c68a5a58af6b9fdd4e849b8c42d837b2c2e73f9a4f96a98c4f1ed398a9aab576ee0a4748f5690cf594e6b99dbe61de7839da748c41e6d6ca8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@patch:typescript@5.4.2#~builtin<compat/typescript>":
|
||||
version: 5.4.2
|
||||
resolution: "typescript@patch:typescript@npm%3A5.4.2#~builtin<compat/typescript>::version=5.4.2&hash=a1c5e5"
|
||||
|
|
@ -32843,16 +32811,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@patch:typescript@^5.5.3#~builtin<compat/typescript>, typescript@patch:typescript@~5.5.0#~builtin<compat/typescript>":
|
||||
version: 5.5.4
|
||||
resolution: "typescript@patch:typescript@npm%3A5.5.4#~builtin<compat/typescript>::version=5.5.4&hash=a1c5e5"
|
||||
bin:
|
||||
tsc: bin/tsc
|
||||
tsserver: bin/tsserver
|
||||
checksum: fc52962f31a5bcb716d4213bef516885e4f01f30cea797a831205fc9ef12b405a40561c40eae3127ab85ba1548e7df49df2bcdee6b84a94bfbe3a0d7eff16b14
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@patch:typescript@~5.3.0#~builtin<compat/typescript>":
|
||||
version: 5.3.3
|
||||
resolution: "typescript@patch:typescript@npm%3A5.3.3#~builtin<compat/typescript>::version=5.3.3&hash=a1c5e5"
|
||||
|
|
@ -32863,6 +32821,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@patch:typescript@~5.5.0#~builtin<compat/typescript>":
|
||||
version: 5.5.4
|
||||
resolution: "typescript@patch:typescript@npm%3A5.5.4#~builtin<compat/typescript>::version=5.5.4&hash=a1c5e5"
|
||||
bin:
|
||||
tsc: bin/tsc
|
||||
tsserver: bin/tsserver
|
||||
checksum: fc52962f31a5bcb716d4213bef516885e4f01f30cea797a831205fc9ef12b405a40561c40eae3127ab85ba1548e7df49df2bcdee6b84a94bfbe3a0d7eff16b14
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"uc.micro@npm:^1.0.1, uc.micro@npm:^1.0.5":
|
||||
version: 1.0.6
|
||||
resolution: "uc.micro@npm:1.0.6"
|
||||
|
|
@ -33655,13 +33623,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"web-vitals@npm:^2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "web-vitals@npm:2.1.4"
|
||||
checksum: 03d3f47dbf55c3dce07beb0ff5de8ddd52e2d0a53a8df5c84e7a16dda93543341d67231fa79b1d9772b091419af4ec0fd395b8bcf451a0e26846e3f76b3d0efc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"webidl-conversions@npm:^3.0.0":
|
||||
version: 3.0.1
|
||||
resolution: "webidl-conversions@npm:3.0.1"
|
||||
|
|
@ -34543,7 +34504,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zod@npm:^3.22.4":
|
||||
"zod@npm:^3.22.4, zod@npm:^3.23.8":
|
||||
version: 3.23.8
|
||||
resolution: "zod@npm:3.23.8"
|
||||
checksum: 15949ff82118f59c893dacd9d3c766d02b6fa2e71cf474d5aa888570c469dbf5446ac5ad562bb035bf7ac9650da94f290655c194f4a6de3e766f43febd432c5c
|
||||
|
|
|
|||
Loading…
Reference in New Issue