├── .github └── workflows │ └── dashboard.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── azure.yaml ├── docs ├── CopilotDashboard.png ├── CopilotDashboard.vsdx └── dashboard.jpeg ├── infra ├── README.md ├── main.bicep ├── main.parameters.json └── resources.bicep └── src ├── background ├── .gitignore ├── DataIngestion.sln └── DataIngestion │ ├── DataIngestion.csproj │ ├── Functions │ ├── CopilotMetricsIngestion.cs │ └── CopilotSeatsIngestion.cs │ ├── GithubMetricsApiOptions.cs │ ├── Models │ ├── CopilotMetrics.cs │ ├── CopilotSeats.cs │ └── GitHubModels.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Services │ ├── GitHubCopilotMetricsClient.cs │ ├── GitHubCopilotSeatsClient.cs │ └── Helpers.cs │ ├── TestData │ └── metrics.json │ └── host.json └── dashboard ├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── app ├── favicon.ico ├── globals.css ├── layout.tsx ├── loading.tsx ├── page.tsx └── seats │ ├── loading.tsx │ └── page.tsx ├── components.json ├── components └── ui │ ├── accordion.tsx │ ├── alert-dialog.tsx │ ├── alert.tsx │ ├── aspect-ratio.tsx │ ├── avatar.tsx │ ├── badge.tsx │ ├── breadcrumb.tsx │ ├── button.tsx │ ├── calendar.tsx │ ├── card.tsx │ ├── carousel.tsx │ ├── chart.tsx │ ├── checkbox.tsx │ ├── collapsible.tsx │ ├── command.tsx │ ├── context-menu.tsx │ ├── data-picker.tsx │ ├── data-table-column-header.tsx │ ├── data-table-export-options.tsx │ ├── data-table-faceted-filter.tsx │ ├── data-table-toolbar.tsx │ ├── data-table-view-options.tsx │ ├── data-table.tsx │ ├── dialog.tsx │ ├── drawer.tsx │ ├── dropdown-menu.tsx │ ├── form.tsx │ ├── hover-card.tsx │ ├── input-otp.tsx │ ├── input.tsx │ ├── label.tsx │ ├── menubar.tsx │ ├── navigation-menu.tsx │ ├── pagination.tsx │ ├── popover.tsx │ ├── progress.tsx │ ├── radio-group.tsx │ ├── resizable.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── skeleton.tsx │ ├── slider.tsx │ ├── sonner.tsx │ ├── switch.tsx │ ├── table.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ ├── toast.tsx │ ├── toaster.tsx │ ├── toggle-group.tsx │ ├── toggle.tsx │ ├── tooltip.tsx │ └── use-toast.ts ├── features ├── app-header │ ├── app-header.tsx │ ├── main-nav-item.tsx │ └── theme-toggle.tsx ├── common │ ├── chart-header.tsx │ ├── error-page.tsx │ ├── models.tsx │ ├── response-error.ts │ ├── server-action-response.ts │ └── theme-provider.tsx ├── dashboard │ ├── charts │ │ ├── acceptance-rate.test.ts │ │ ├── acceptance-rate.tsx │ │ ├── active-users.test.ts │ │ ├── active-users.tsx │ │ ├── chart-header.tsx │ │ ├── chat-acceptance-rate.tsx │ │ ├── common.ts │ │ ├── editor.tsx │ │ ├── language.tsx │ │ ├── stats-card.tsx │ │ ├── stats.tsx │ │ ├── total-chat-suggestions-and-acceptances.tsx │ │ ├── total-code-line-suggestions-and-acceptances.tsx │ │ └── total-suggestions-and-acceptances.tsx │ ├── dashboard-page.tsx │ ├── dashboard-state.tsx │ ├── filter │ │ ├── date-filter.tsx │ │ ├── dropdown-filter.tsx │ │ ├── header-filter.tsx │ │ ├── time-frame-toggle.tsx │ │ └── weekend-filter.tsx │ └── header.tsx ├── page-header │ └── page-header.tsx └── seats │ ├── filters │ └── date-filter.tsx │ ├── header.tsx │ ├── seats-list.tsx │ ├── seats-page.tsx │ ├── seats-state.tsx │ └── stats │ ├── stats-card.tsx │ └── stats.tsx ├── lib └── utils.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public └── copilot.png ├── services ├── copilot-metrics-service.ts ├── copilot-seat-service.ts ├── cosmos-db-service.ts ├── env-service.ts └── sample-data.tsx ├── tailwind.config.ts ├── tsconfig.json ├── types └── type.ts ├── utils ├── data-mapper.ts └── helpers.ts └── vitest.config.ts /.github/workflows/dashboard.yml: -------------------------------------------------------------------------------- 1 | name: Build & deploy dashboard app to Azure app service 2 | 3 | # When this action will be executed 4 | on: 5 | # Automatically trigger it when detected changes in repo 6 | #push: 7 | # branches: [main] 8 | 9 | # Allow manual workflow trigger 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: 🌱 Checkout to the branch 18 | uses: actions/checkout@v4 19 | 20 | - name: 🍏 Set up Node.js version 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: "20.x" 24 | 25 | - name: ⚙️ npm install and build 26 | run: | 27 | cd ./src/dashboard 28 | npm install 29 | npm run build --if-present 30 | cd .. 31 | 32 | - name: 📂 Copy standalone into the root 33 | run: cp -R ./src/dashboard/.next/standalone ./site-deploy 34 | 35 | - name: 📂 Copy static into the .next folder 36 | run: cp -R ./src/dashboard/.next/static ./site-deploy/.next/static 37 | 38 | - name: 📂 Copy Public folder 39 | run: cp -R ./src/dashboard/public ./site-deploy/public 40 | 41 | - name: 📦 Package Next application 42 | run: | 43 | cd ./site-deploy 44 | zip Nextjs-site.zip ./* .next -qr 45 | 46 | - name: 🔍 Diagnostics 47 | run: | 48 | ls ./src/dashboard 49 | ls ./src/dashboard/.next 50 | ls ./site-deploy 51 | 52 | - name: ⬆️ Publish Next Application artifact 53 | uses: actions/upload-artifact@v4 54 | with: 55 | name: Nextjs-site 56 | path: ./site-deploy/Nextjs-site.zip 57 | 58 | deploy: 59 | runs-on: ubuntu-latest 60 | needs: build 61 | environment: 62 | name: Production 63 | 64 | steps: 65 | - name: 🍏 Set up Node.js version 66 | uses: actions/setup-node@v4 67 | with: 68 | node-version: "20.x" 69 | 70 | - name: ⬇️ Download artifact from build job 71 | uses: actions/download-artifact@v4 72 | with: 73 | name: Nextjs-site 74 | 75 | - name: 🗝️ Azure Login 76 | uses: azure/login@v1 77 | with: 78 | creds: ${{ secrets.AZURE_CREDENTIALS }} 79 | 80 | # Set the build during deployment setting to false. This setting was added in the templates to all azd to work, but breaks deployment via webapps-deploy 81 | - name: Azure CLI script 82 | uses: azure/CLI@v1 83 | with: 84 | inlineScript: | 85 | rg=$(az webapp list --query "[?name=='${{ secrets.AZURE_APP_SERVICE_NAME }}'].resourceGroup" --output tsv) 86 | echo Setting SCM_DO_BUILD_DURING_DEPLOYMENT=false on app service ${{ secrets.AZURE_APP_SERVICE_NAME }} 87 | az webapp config appsettings set -n ${{ secrets.AZURE_APP_SERVICE_NAME }} -g $rg --settings SCM_DO_BUILD_DURING_DEPLOYMENT=false -o none 88 | echo Setting --startup-file=\"node server.js\" on app service ${{ secrets.AZURE_APP_SERVICE_NAME }} 89 | az webapp config set --startup-file="node server.js" -n ${{ secrets.AZURE_APP_SERVICE_NAME }} -g $rg -o none 90 | sleep 10 91 | 92 | - name: 🚀 Deploy to Azure Web App 93 | id: deploy-to-webapp 94 | uses: azure/webapps-deploy@v2 95 | with: 96 | app-name: ${{ secrets.AZURE_APP_SERVICE_NAME }} 97 | package: ${{ github.workspace }}/Nextjs-site.zip 98 | 99 | - name: 🧹 Cleanup 100 | run: rm ${{ github.workspace }}/Nextjs-site.zip 101 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .azure/ 2 | .idea/ 3 | .vscode/ 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | If you would like to contribute to the solution solution accelerator repository there are many ways you can help. 4 | 5 | ## Reporting issues 6 | 7 | We use [GitHub issues](https://github.com/microsoft/azurechat/issues) as an issue tracker for the repository. Firstly, please search in open issues and try to make sure your problem doesn't exist. If there is an issue, add your comments to this issue. 8 | If there are no issues yet, please open a new one. 9 | 10 | ## Contributing Code 11 | 12 | If you would like to contribute an improvement or a fix please create a Pull Request using the steps below. Ensure to describe the feature or bug as part of the pull request. 13 | 14 | ## Sending a Pull Request 15 | 16 | Before submitting a pull request please make sure the following is done: 17 | 18 | 1. Fork [the repository](https://github.com/microsoft/azurechat) 19 | 2. Create a branch from the `main` 20 | 3. Ensure that the code build and runs without any errors 21 | 4. If required update the README.md 22 | 5. Complete the [CLA](#contributor-license-agreement-cla) 23 | 24 | ### Code of Conduct 25 | 26 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 27 | 28 | ### Contributor License Agreement (CLA) 29 | 30 | You will need to complete a Contributor License Agreement (CLA). Briefly, this agreement testifies that you are granting us permission to use the submitted change according to the terms of the project's license, and that the work being submitted is under appropriate copyright. 31 | 32 | Please submit a Contributor License Agreement (CLA) before submitting a pull request. You may visit [https://cla.microsoft.com](https://cla.microsoft.com) to sign digitally. Alternatively, download the agreement ([Microsoft Contribution License Agreement.docx](https://www.codeplex.com/Download?ProjectName=typescript&DownloadId=822190)), sign, scan, and email it back to . Be sure to include your github user name along with the agreement. Once we have received the signed CLA, we'll review the request. 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | We use [GitHub issues](https://github.com/microsoft/azurechat/issues) as an issue tracker for the repository. Firstly, please search in open issues and try to make sure your problem doesn't exist. If there is an issue, add your comments to this issue. 6 | If there are no issues yet, please open a new one. 7 | 8 | ## Microsoft Support Policy 9 | 10 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 11 | -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json 2 | 3 | name: platform-engineering 4 | metadata: 5 | template: platform-engineering@0.0.1 6 | services: 7 | frontend: 8 | project: ./src/dashboard 9 | language: ts 10 | host: appservice 11 | ingestion: 12 | project: ./src/background/DataIngestion 13 | language: csharp 14 | host: function 15 | -------------------------------------------------------------------------------- /docs/CopilotDashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/copilot-metrics-dashboard/ef5e30a3788f49299ca06f1b683fe473511632fa/docs/CopilotDashboard.png -------------------------------------------------------------------------------- /docs/CopilotDashboard.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/copilot-metrics-dashboard/ef5e30a3788f49299ca06f1b683fe473511632fa/docs/CopilotDashboard.vsdx -------------------------------------------------------------------------------- /docs/dashboard.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/copilot-metrics-dashboard/ef5e30a3788f49299ca06f1b683fe473511632fa/docs/dashboard.jpeg -------------------------------------------------------------------------------- /infra/README.md: -------------------------------------------------------------------------------- 1 | # Unleash the Power of Azure Open AI 2 | -------------------------------------------------------------------------------- /infra/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | @minLength(1) 4 | @maxLength(64) 5 | @description('Name of the the environment which is used to generate a short unique hash used in all resources.') 6 | param name string 7 | 8 | @minLength(1) 9 | @description('Primary location for all resources') 10 | param location string 11 | 12 | @description('Name of GitHub enterprise') 13 | @minLength(1) 14 | param githubEnterpriseName string 15 | 16 | @description('Name of GitHub Organization') 17 | @minLength(1) 18 | param githubOrganizationName string 19 | 20 | @description('GitHub API scope: "enterprise" or "organization"') 21 | @allowed(['enterprise', 'organization']) 22 | param githubAPIScope string 23 | 24 | @secure() 25 | @description('PAT to call Github API') 26 | param githubToken string 27 | 28 | @description('API version for the GitHub API e.g. 2022-11-28') 29 | @minLength(1) 30 | param githubAPIVersion string = '2022-11-28' 31 | 32 | @description('True to use Test Data instead of calling the real API') 33 | param useTestData bool 34 | 35 | @description('List of team names - works with the new Metrics API') 36 | param teamNames array 37 | 38 | param resourceGroupName string = '' 39 | 40 | var resourceToken = toLower(uniqueString(subscription().id, name, location)) 41 | var tags = { 'azd-env-name': name } 42 | 43 | // Organize resources in a resource group 44 | resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { 45 | name: !empty(resourceGroupName) ? resourceGroupName : 'rg-${name}' 46 | location: location 47 | tags: tags 48 | } 49 | 50 | module resources 'resources.bicep' = { 51 | name: 'all-resources' 52 | scope: rg 53 | params: { 54 | name: name 55 | resourceToken: resourceToken 56 | tags: tags 57 | location: location 58 | githubToken: githubToken 59 | githubEnterpriseName: githubEnterpriseName 60 | githubOrganizationName: githubOrganizationName 61 | githubAPIVersion: githubAPIVersion 62 | githubAPIScope: githubAPIScope 63 | teamNames: teamNames 64 | useTestData: useTestData 65 | } 66 | } 67 | 68 | output APP_URL string = resources.outputs.url 69 | output AZURE_LOCATION string = location 70 | output AZURE_TENANT_ID string = tenant().tenantId 71 | -------------------------------------------------------------------------------- /infra/main.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "name": { 6 | "value": "${AZURE_ENV_NAME}" 7 | }, 8 | "location": { 9 | "value": "${AZURE_LOCATION}" 10 | }, 11 | "githubToken": { 12 | "value": "" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/background/DataIngestion.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.002.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataIngestion", "DataIngestion\DataIngestion.csproj", "{BDA3461C-B61E-4708-8D26-E4009D668C81}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {BDA3461C-B61E-4708-8D26-E4009D668C81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {BDA3461C-B61E-4708-8D26-E4009D668C81}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {BDA3461C-B61E-4708-8D26-E4009D668C81}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {BDA3461C-B61E-4708-8D26-E4009D668C81}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {7A792EA3-26E6-4CED-B7A5-15CDCE4A7E41} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /src/background/DataIngestion/DataIngestion.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0 4 | v4 5 | Exe 6 | enable 7 | enable 8 | Microsoft.CopilotDashboard.DataIngestion 9 | 96280f8a-e307-4d93-834f-b91b938e2df3 10 | Microsoft.CopilotDashboard.DataIngestion 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | PreserveNewest 24 | 25 | 26 | PreserveNewest 27 | Never 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/background/DataIngestion/Functions/CopilotMetricsIngestion.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Functions.Worker; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Options; 4 | using Microsoft.CopilotDashboard.DataIngestion.Models; 5 | using Microsoft.CopilotDashboard.DataIngestion.Services; 6 | 7 | namespace Microsoft.CopilotDashboard.DataIngestion.Functions; 8 | 9 | public class CopilotMetricsIngestion 10 | { 11 | private readonly ILogger _logger; 12 | private readonly GitHubCopilotMetricsClient _metricsClient; 13 | private readonly IOptions _options; 14 | 15 | public CopilotMetricsIngestion( 16 | ILogger logger, 17 | GitHubCopilotMetricsClient metricsClient, 18 | IOptions options) 19 | { 20 | _logger = logger; 21 | _metricsClient = metricsClient; 22 | _options = options; 23 | } 24 | 25 | 26 | [Function("GitHubCopilotMetricsIngestion")] 27 | [CosmosDBOutput(databaseName: "platform-engineering", containerName: "metrics_history", Connection = "AZURE_COSMOSDB_ENDPOINT", CreateIfNotExists = true)] 28 | public async Task> Run([TimerTrigger("0 0 * * * *")] TimerInfo myTimer) 29 | { 30 | _logger.LogInformation($"GitHubCopilotMetricsIngestion timer trigger function executed at: {DateTime.Now}"); 31 | 32 | var metrics = new List(); 33 | 34 | metrics.AddRange(await ExtractMetrics()); 35 | 36 | var teams = _options.Value.Teams; 37 | if (teams != null && teams.Any()) 38 | { 39 | foreach (var team in teams) 40 | { 41 | metrics.AddRange(await ExtractMetrics(team)); 42 | } 43 | } 44 | else 45 | { 46 | metrics.AddRange(await ExtractMetrics()); 47 | } 48 | 49 | if (myTimer.ScheduleStatus is not null) 50 | { 51 | _logger.LogInformation($"Finished ingestion. Next timer schedule at: {myTimer.ScheduleStatus.Next}"); 52 | } 53 | _logger.LogInformation($"Metrics count: {metrics.Count}"); 54 | return metrics; 55 | } 56 | 57 | private async Task ExtractMetrics(string? team = null) 58 | { 59 | if (_options.Value.UseTestData) 60 | { 61 | return await LoadTestData(team); 62 | } 63 | 64 | var scope = Environment.GetEnvironmentVariable("GITHUB_API_SCOPE"); 65 | if (!string.IsNullOrWhiteSpace(scope) && scope == "enterprise") 66 | { 67 | _logger.LogInformation("Fetching GitHub Copilot usage metrics for enterprise"); 68 | return await _metricsClient.GetCopilotMetricsForEnterpriseAsync(team); 69 | } 70 | 71 | _logger.LogInformation("Fetching GitHub Copilot usage metrics for organization"); 72 | return await _metricsClient.GetCopilotMetricsForOrganizationAsync(team); 73 | } 74 | 75 | private ValueTask LoadTestData(string? teamName) 76 | { 77 | return _metricsClient.GetTestCopilotMetrics(teamName); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/background/DataIngestion/Functions/CopilotSeatsIngestion.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Functions.Worker; 2 | using Microsoft.CopilotDashboard.DataIngestion.Models; 3 | using Microsoft.CopilotDashboard.DataIngestion.Services; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Microsoft.CopilotDashboard.DataIngestion.Functions; 8 | 9 | public class CopilotSeatsIngestion 10 | { 11 | private readonly ILogger _logger; 12 | private readonly GitHubCopilotSeatsClient _gitHubCopilotSeatsClient; 13 | 14 | public CopilotSeatsIngestion(GitHubCopilotSeatsClient gitHubCopilotSeatsClient, ILogger logger) 15 | { 16 | _gitHubCopilotSeatsClient = gitHubCopilotSeatsClient; 17 | _logger = logger; 18 | } 19 | 20 | [Function("GitHubCopilotSeatsIngestion")] 21 | [CosmosDBOutput(databaseName: "platform-engineering", containerName: "seats_history", Connection = "AZURE_COSMOSDB_ENDPOINT", CreateIfNotExists = true)] 22 | public async Task> Run([TimerTrigger("0 0 * * * *")] TimerInfo myTimer) 23 | { 24 | _logger.LogInformation($"GitHubCopilotSeatsIngestion timer trigger function executed at: {DateTime.Now}"); 25 | 26 | var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN")!; 27 | var scope = Environment.GetEnvironmentVariable("GITHUB_API_SCOPE")!; 28 | Boolean.TryParse(Environment.GetEnvironmentVariable("ENABLE_SEATS_INGESTION") ?? "true", out var seatsIngestionEnabled); 29 | if (!seatsIngestionEnabled) 30 | { 31 | _logger.LogInformation("Seats ingestion is disabled"); 32 | return Array.Empty(); 33 | } 34 | 35 | IEnumerable seatsPages; 36 | 37 | if (!string.IsNullOrWhiteSpace(scope) && scope == "enterprise") 38 | { 39 | var enterprise = Environment.GetEnvironmentVariable("GITHUB_ENTERPRISE")!; 40 | _logger.LogInformation("Fetching GitHub Copilot seats for enterprise"); 41 | seatsPages = await _gitHubCopilotSeatsClient.GetEnterpriseAssignedSeatsPagesAsync(enterprise, token); 42 | } 43 | else 44 | { 45 | var organization = Environment.GetEnvironmentVariable("GITHUB_ORGANIZATION")!; 46 | _logger.LogInformation("Fetching GitHub Copilot seats for organization"); 47 | seatsPages = await _gitHubCopilotSeatsClient.GetOrganizationAssignedSeatsPagesAsync(organization, token); 48 | } 49 | 50 | if (myTimer.ScheduleStatus is not null) 51 | { 52 | _logger.LogInformation($"Finished ingestion. Next timer schedule at: {myTimer.ScheduleStatus.Next}"); 53 | } 54 | 55 | return seatsPages; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/background/DataIngestion/GithubMetricsApiOptions.cs: -------------------------------------------------------------------------------- 1 | public class GithubMetricsApiOptions 2 | { 3 | public bool UseTestData { get; set; } 4 | public string[]? Teams { get; set; } 5 | } -------------------------------------------------------------------------------- /src/background/DataIngestion/Models/CopilotSeats.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Microsoft.CopilotDashboard.DataIngestion.Models; 4 | 5 | public class CopilotSeats 6 | { 7 | /// 8 | /// Gets or sets the ID of the seats information. 9 | /// 10 | [JsonPropertyName("id")] 11 | public string Id 12 | { 13 | get 14 | { 15 | return GetId(); 16 | } 17 | } 18 | 19 | /// 20 | /// Gets or sets the date for which the seats information is recorded. 21 | /// 22 | [JsonPropertyName("date")] 23 | public DateOnly Date { get; set; } 24 | 25 | /// 26 | /// Gets or sets the total number of seats. 27 | /// 28 | [JsonPropertyName("total_seats")] 29 | public int TotalSeats { get; set; } 30 | 31 | /// 32 | /// Gets or sets the total number of active seats. 33 | /// A seat is considered active if it has been used within the last 30 days. 34 | /// 35 | [JsonPropertyName("total_active_seats")] 36 | public int TotalActiveSeats { get; set; } 37 | 38 | /// 39 | /// Gets or sets the list of seats. 40 | /// 41 | [JsonPropertyName("seats")] 42 | public List Seats { get; set; } 43 | 44 | /// 45 | /// Gets or sets the enterprise name. 46 | /// 47 | [JsonPropertyName("enterprise")] 48 | public string Enterprise { get; set; } 49 | 50 | /// 51 | /// Gets or sets the organization name. 52 | /// 53 | [JsonPropertyName("organization")] 54 | public string Organization { get; set; } 55 | 56 | /// 57 | /// Gets or sets the page number. 58 | /// 59 | [JsonPropertyName("page")] 60 | public int Page { get; set; } 61 | 62 | /// 63 | /// Gets or sets the flag indicating if there is a next page. 64 | /// 65 | [JsonPropertyName("has_next_page")] 66 | public bool HasNextPage { get; set; } 67 | 68 | /// 69 | /// Gets or sets the date and time of the last update. 70 | /// 71 | [JsonPropertyName("last_update")] 72 | public DateTime LastUpdate { get; set; } = DateTime.UtcNow; 73 | 74 | private string GetId() 75 | { 76 | if (!string.IsNullOrWhiteSpace(this.Organization)) 77 | { 78 | return $"{this.Date.ToString("yyyy-MM-d")}-ORG-{this.Organization}-{this.Page}"; 79 | } 80 | else if (!string.IsNullOrWhiteSpace(this.Enterprise)) 81 | { 82 | return $"{this.Date.ToString("yyyy-MM-d")}-ENT-{this.Enterprise}-{this.Page}"; 83 | } 84 | return $"{this.Date.ToString("yyyy-MM-d")}-XXX-{this.Page}"; 85 | } 86 | } 87 | 88 | /// 89 | /// Represents a seat assigned to a user within GitHub Copilot. 90 | /// 91 | public class Seat 92 | { 93 | /// 94 | /// Gets or sets the date and time when the seat was created. 95 | /// 96 | [JsonPropertyName("created_at")] 97 | public DateTime CreatedAt { get; set; } 98 | 99 | /// 100 | /// Gets or sets the date and time when the seat was last updated. 101 | /// 102 | [JsonPropertyName("updated_at")] 103 | public DateTime UpdatedAt { get; set; } 104 | 105 | /// 106 | /// Gets or sets the pending cancellation date. 107 | /// 108 | [JsonPropertyName("pending_cancellation_date")] 109 | public string PendingCancellationDate { get; set; } 110 | 111 | /// 112 | /// Gets or sets the date and time of the last activity. 113 | /// 114 | [JsonPropertyName("last_activity_at")] 115 | public DateTime? LastActivityAt { get; set; } 116 | 117 | /// 118 | /// Gets or sets the editor used during the last activity. 119 | /// 120 | [JsonPropertyName("last_activity_editor")] 121 | public string? LastActivityEditor { get; set; } 122 | 123 | /// 124 | /// Gets or sets the type of plan associated with the seat. 125 | /// 126 | [JsonPropertyName("plan_type")] 127 | public string PlanType { get; set; } 128 | 129 | /// 130 | /// Gets or sets the user assigned to the seat. 131 | /// 132 | [JsonPropertyName("assignee")] 133 | public User Assignee { get; set; } 134 | 135 | /// 136 | /// Gets or sets the team that assigned the seat. 137 | /// 138 | [JsonPropertyName("assigning_team")] 139 | public Team AssigningTeam { get; set; } 140 | 141 | /// 142 | /// Gets or sets the organization associated with the seat. 143 | /// 144 | [JsonPropertyName("organization")] 145 | public Organization Organization { get; set; } 146 | } 147 | 148 | 149 | -------------------------------------------------------------------------------- /src/background/DataIngestion/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using Microsoft.CopilotDashboard.DataIngestion.Services; 5 | 6 | var host = new HostBuilder() 7 | .ConfigureFunctionsWebApplication() 8 | .ConfigureServices((ctx, services) => 9 | { 10 | services.Configure(ctx.Configuration.GetSection("GITHUB_METRICS")); 11 | services.AddHttpClient(ConfigureClient); 12 | services.AddHttpClient(ConfigureClient); 13 | }) 14 | .Build(); 15 | 16 | host.Run(); 17 | 18 | void ConfigureClient(HttpClient httpClient) 19 | { 20 | var apiVersion = Environment.GetEnvironmentVariable("GITHUB_API_VERSION"); 21 | var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); 22 | var gitHubApiBaseUrl = Environment.GetEnvironmentVariable("GITHUB_API_BASEURL") ?? "https://api.github.com/"; 23 | 24 | httpClient.BaseAddress = new Uri(gitHubApiBaseUrl); 25 | httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json")); 26 | httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); 27 | httpClient.DefaultRequestHeaders.Add("X-GitHub-Api-Version", apiVersion); 28 | httpClient.DefaultRequestHeaders.Add("User-Agent", "GitHubCopilotDataIngestion"); 29 | } -------------------------------------------------------------------------------- /src/background/DataIngestion/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "DataIngestion": { 4 | "commandName": "Project", 5 | "commandLineArgs": "--port 7106", 6 | "launchBrowser": false 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/background/DataIngestion/Services/GitHubCopilotMetricsClient.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using System.Text.Json; 3 | using Microsoft.CopilotDashboard.DataIngestion.Functions; 4 | using Microsoft.CopilotDashboard.DataIngestion.Models; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Microsoft.CopilotDashboard.DataIngestion.Services 8 | { 9 | internal enum MetricsType 10 | { 11 | Ent, 12 | Org 13 | } 14 | 15 | public class GitHubCopilotMetricsClient 16 | { 17 | private readonly HttpClient _httpClient; 18 | private readonly ILogger _logger; 19 | 20 | public GitHubCopilotMetricsClient(HttpClient httpClient, ILogger logger) 21 | { 22 | _httpClient = httpClient; 23 | _logger = logger; 24 | } 25 | 26 | public Task GetCopilotMetricsForEnterpriseAsync(string? team) 27 | { 28 | var enterprise = Environment.GetEnvironmentVariable("GITHUB_ENTERPRISE")!; 29 | 30 | var requestUri = string.IsNullOrWhiteSpace(team) 31 | ? $"/enterprises/{enterprise}/copilot/metrics" 32 | : $"/enterprises/{enterprise}/team/{team}/copilot/metrics"; 33 | 34 | return GetMetrics(requestUri, MetricsType.Ent, enterprise, team); 35 | } 36 | 37 | public Task GetCopilotMetricsForOrganizationAsync(string? team) 38 | { 39 | var organization = Environment.GetEnvironmentVariable("GITHUB_ORGANIZATION")!; 40 | 41 | var requestUri = string.IsNullOrWhiteSpace(team) 42 | ? $"/orgs/{organization}/copilot/metrics" 43 | : $"/orgs/{organization}/team/{team}/copilot/metrics"; 44 | 45 | return GetMetrics(requestUri, MetricsType.Org, organization, team); 46 | } 47 | 48 | private async Task GetMetrics(string requestUri, MetricsType type, string orgOrEnterpriseName, string? team = null) 49 | { 50 | try 51 | { 52 | var response = await _httpClient.GetAsync(requestUri); 53 | if (!response.IsSuccessStatusCode) 54 | { 55 | if (response.StatusCode == System.Net.HttpStatusCode.NotFound) 56 | { 57 | _logger.LogWarning($"Team not found: {team}. Continuing with available data."); 58 | return Array.Empty(); 59 | } 60 | throw new HttpRequestException($"Error fetching data: {response.StatusCode}"); 61 | } 62 | _logger.LogInformation($"Fetched data from {requestUri}"); 63 | var metrics = AddInfo((await response.Content.ReadFromJsonAsync())!, type, orgOrEnterpriseName, team); 64 | return metrics; 65 | } 66 | catch (HttpRequestException ex) 67 | { 68 | _logger.LogError(ex, $"Error fetching data from {requestUri}"); 69 | return Array.Empty(); 70 | } 71 | } 72 | 73 | public async ValueTask GetTestCopilotMetrics(string? team) 74 | { 75 | await using var reader = typeof(CopilotMetricsIngestion) 76 | .Assembly 77 | .GetManifestResourceStream( 78 | "Microsoft.CopilotDashboard.DataIngestion.TestData.metrics.json")!; 79 | 80 | return AddInfo((await JsonSerializer.DeserializeAsync(reader))!, MetricsType.Org, "test", team); 81 | } 82 | 83 | private Metrics[] AddInfo(Metrics[] metrics, MetricsType type, string orgOrEnterpriseName, string? team = null) 84 | { 85 | foreach (var metric in metrics) 86 | { 87 | metric.Team = team; 88 | if(type == MetricsType.Ent) 89 | { 90 | metric.Enterprise = orgOrEnterpriseName; 91 | } 92 | else 93 | { 94 | metric.Organization = orgOrEnterpriseName; 95 | } 96 | } 97 | 98 | return metrics; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/background/DataIngestion/Services/Helpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http.Headers; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Microsoft.CopilotDashboard.DataIngestion.Services 9 | { 10 | public class Helpers 11 | { 12 | public static string? GetNextPageUrl(HttpResponseHeaders headers) 13 | { 14 | if (headers.TryGetValues("Link", out var values)) 15 | { 16 | var linkHeader = values.FirstOrDefault(); 17 | if (linkHeader != null) 18 | { 19 | var links = linkHeader.Split(','); 20 | foreach (var link in links) 21 | { 22 | var parts = link.Split(';'); 23 | if (parts.Length == 2 && parts[1].Contains("rel=\"next\"")) 24 | { 25 | var urlPart = parts[0].Trim(); 26 | return urlPart.Trim('<', '>'); 27 | } 28 | } 29 | } 30 | } 31 | return null; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/background/DataIngestion/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | }, 9 | "enableLiveMetricsFilters": true 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/dashboard/.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_ENTERPRISE=your-github-enterprise-name 2 | GITHUB_ORGANIZATION=your-github-organization-name 3 | GITHUB_TOKEN=your-github-token 4 | GITHUB_API_VERSION=2022-11-28 5 | GITHUB_API_SCOPE=organization 6 | 7 | 8 | AZURE_COSMOSDB_ENDPOINT=your-azure-cosmosdb-endpoint -------------------------------------------------------------------------------- /src/dashboard/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /src/dashboard/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, install packages using: 6 | ```bash 7 | npm install 8 | # or 9 | yarn install 10 | # or 11 | pnpm install 12 | # or 13 | bun install 14 | ``` 15 | Then, run the development server: 16 | 17 | ```bash 18 | npm run dev 19 | # or 20 | yarn dev 21 | # or 22 | pnpm dev 23 | # or 24 | bun dev 25 | ``` 26 | 27 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 28 | 29 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 30 | 31 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 32 | 33 | ## Learn More 34 | 35 | To learn more about Next.js, take a look at the following resources: 36 | 37 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 38 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 39 | 40 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 41 | 42 | ## Deploy on Vercel 43 | 44 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 45 | 46 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 47 | -------------------------------------------------------------------------------- /src/dashboard/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/copilot-metrics-dashboard/ef5e30a3788f49299ca06f1b683fe473511632fa/src/dashboard/app/favicon.ico -------------------------------------------------------------------------------- /src/dashboard/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | --chart-1: 173 58% 39%; 37 | --chart-2: 12 76% 61%; 38 | --chart-3: 197 37% 24%; 39 | --chart-4: 43 74% 66%; 40 | --chart-5: 27 87% 67%; 41 | } 42 | 43 | .dark { 44 | --background: 222.2 84% 4.9%; 45 | --foreground: 210 40% 98%; 46 | 47 | --card: 222.2 84% 4.9%; 48 | --card-foreground: 210 40% 98%; 49 | 50 | --popover: 222.2 84% 4.9%; 51 | --popover-foreground: 210 40% 98%; 52 | 53 | --primary: 210 40% 98%; 54 | --primary-foreground: 222.2 47.4% 11.2%; 55 | 56 | --secondary: 217.2 32.6% 17.5%; 57 | --secondary-foreground: 210 40% 98%; 58 | 59 | --muted: 217.2 32.6% 17.5%; 60 | --muted-foreground: 215 20.2% 65.1%; 61 | 62 | --accent: 217.2 32.6% 17.5%; 63 | --accent-foreground: 210 40% 98%; 64 | 65 | --destructive: 0 62.8% 30.6%; 66 | --destructive-foreground: 210 40% 98%; 67 | 68 | --border: 217.2 32.6% 17.5%; 69 | --input: 217.2 32.6% 17.5%; 70 | --ring: 212.7 26.8% 83.9%; 71 | --ring: 240 4.9% 83.9%; 72 | --chart-1: 220 70% 50%; 73 | --chart-5: 160 60% 45%; 74 | --chart-3: 30 80% 55%; 75 | --chart-4: 280 65% 60%; 76 | --chart-2: 340 75% 55%; 77 | } 78 | } 79 | 80 | @layer base { 81 | * { 82 | @apply border-border; 83 | } 84 | body { 85 | @apply bg-background text-foreground; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/dashboard/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { AppHeader } from "@/features/app-header/app-header"; 2 | import { ThemeProvider } from "@/features/common/theme-provider"; 3 | import { cn } from "@/lib/utils"; 4 | import type { Metadata } from "next"; 5 | import { Inter } from "next/font/google"; 6 | import "./globals.css"; 7 | 8 | const inter = Inter({ subsets: ["latin"] }); 9 | 10 | export const metadata: Metadata = { 11 | title: "GitHub Copilot Metrics Dashboard", 12 | description: "GitHub Copilot Metrics Dashboard", 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: Readonly<{ 18 | children: React.ReactNode; 19 | }>) { 20 | return ( 21 | 22 | 23 | 24 |
25 | 26 | {children} 27 |
28 |
29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/dashboard/app/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | import { PageHeader, PageTitle } from "@/features/page-header/page-header"; 3 | 4 | export default function Loading() { 5 | return ( 6 |
7 | 8 | Dashboard 9 |
10 | 11 |
12 | 13 |
14 |
15 |
16 | 17 |
18 |
19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 |
28 |
29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/dashboard/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Dashboard, { IProps } from "@/features/dashboard/dashboard-page"; 2 | import { Suspense } from "react"; 3 | import Loading from "./loading"; 4 | 5 | export const dynamic = "force-dynamic"; 6 | export default function Home(props: IProps) { 7 | let id = "initial-dashboard"; 8 | 9 | if (props.searchParams.startDate && props.searchParams.endDate) { 10 | id = `${id}-${props.searchParams.startDate}-${props.searchParams.endDate}`; 11 | } 12 | 13 | return ( 14 | } key={id}> 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/dashboard/app/seats/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | import { PageHeader, PageTitle } from "@/features/page-header/page-header"; 3 | 4 | export default function Loading() { 5 | return ( 6 |
7 | 8 | Seats 9 |
10 | 11 |
12 | 13 |
14 |
15 |
16 | 17 |
18 |
19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 |
28 |
29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/dashboard/app/seats/page.tsx: -------------------------------------------------------------------------------- 1 | import Dashboard, { IProps } from "@/features/seats/seats-page"; 2 | import { Suspense } from "react"; 3 | import Loading from "./loading"; 4 | import { Metadata } from 'next'; 5 | 6 | export const metadata: Metadata = { 7 | title: "GitHub Copilot Seats Dashboard", 8 | description: "GitHub Copilot Seats Dashboard", 9 | }; 10 | export const dynamic = "force-dynamic"; 11 | export default function Home(props: IProps) { 12 | 13 | let id = "initial-seats-dashboard"; 14 | 15 | if (props.searchParams.date ) { 16 | id = `${id}-${props.searchParams.date}`; 17 | } 18 | 19 | return ( 20 | } key={id}> 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/dashboard/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /src/dashboard/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 5 | import { ChevronDownIcon } from "@radix-ui/react-icons" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Accordion = AccordionPrimitive.Root 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )) 21 | AccordionItem.displayName = "AccordionItem" 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180", 32 | className 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )) 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 52 |
{children}
53 |
54 | )) 55 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 56 | 57 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 58 | -------------------------------------------------------------------------------- /src/dashboard/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )) 33 | Alert.displayName = "Alert" 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )) 45 | AlertTitle.displayName = "AlertTitle" 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | AlertDescription.displayName = "AlertDescription" 58 | 59 | export { Alert, AlertTitle, AlertDescription } 60 | -------------------------------------------------------------------------------- /src/dashboard/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root 6 | 7 | export { AspectRatio } 8 | -------------------------------------------------------------------------------- /src/dashboard/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /src/dashboard/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /src/dashboard/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons" 3 | import { Slot } from "@radix-ui/react-slot" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Breadcrumb = React.forwardRef< 8 | HTMLElement, 9 | React.ComponentPropsWithoutRef<"nav"> & { 10 | separator?: React.ReactNode 11 | } 12 | >(({ ...props }, ref) =>