├── frontend ├── src │ ├── app │ │ ├── app.component.scss │ │ ├── app.component.html │ │ ├── main │ │ │ ├── copilot │ │ │ │ ├── copilot-seats │ │ │ │ │ ├── copilot-seats.component.scss │ │ │ │ │ ├── copilot-seats.component.html │ │ │ │ │ ├── copilot-seats.component.spec.ts │ │ │ │ │ └── copilot-seat │ │ │ │ │ │ └── copilot-seat.component.scss │ │ │ │ ├── copilot-metrics │ │ │ │ │ ├── copilot-metrics.component.scss │ │ │ │ │ ├── copilot-metrics.component.spec.ts │ │ │ │ │ ├── copilot-metrics-pie-chart │ │ │ │ │ │ └── copilot-metrics-pie-chart.component.ts │ │ │ │ │ └── copilot-metrics.component.html │ │ │ │ ├── copilot-surveys │ │ │ │ │ ├── copilot-surveys.component.scss │ │ │ │ │ ├── copilot-survey-details │ │ │ │ │ │ ├── copilot-survey.component.scss │ │ │ │ │ │ ├── copilot-survey.component.ts │ │ │ │ │ │ └── copilot-survey.component.html │ │ │ │ │ ├── copilot-surveys.component.html │ │ │ │ │ ├── github.service.ts │ │ │ │ │ ├── copilot-surveys.component.spec.ts │ │ │ │ │ └── new-copilot-survey │ │ │ │ │ │ ├── new-copilot-survey.component.spec.ts │ │ │ │ │ │ └── new-copilot-survey.component.scss │ │ │ │ ├── copilot-dashboard │ │ │ │ │ ├── dashboard-card │ │ │ │ │ │ ├── dashboard-card-line-chart │ │ │ │ │ │ │ ├── dashboard-card-line-chart.component.scss │ │ │ │ │ │ │ ├── dashboard-card-line-chart.component.html │ │ │ │ │ │ │ ├── dashboard-card-line-chart.component.spec.ts │ │ │ │ │ │ │ └── dashboard-card-line-chart.component.ts │ │ │ │ │ │ ├── dashboard-card-drilldown-bar-chart │ │ │ │ │ │ │ ├── dashboard-card-drilldown-bar-chart.component.scss │ │ │ │ │ │ │ ├── dashboard-card-drilldown-bar-chart.component.html │ │ │ │ │ │ │ ├── dashboard-card-drilldown-bar-chart.component.spec.ts │ │ │ │ │ │ │ └── dashboard-card-drilldown-bar-chart.component.ts │ │ │ │ │ │ ├── dashboard-card.scss │ │ │ │ │ │ ├── dashboard-card-value │ │ │ │ │ │ │ ├── dashboard-card-value.component.scss │ │ │ │ │ │ │ ├── dashboard-card-value.component.spec.ts │ │ │ │ │ │ │ ├── dashboard-card-value.component.html │ │ │ │ │ │ │ └── dashboard-card-value.component.ts │ │ │ │ │ │ └── dashboard-card-bars │ │ │ │ │ │ │ ├── dashboard-card-bars.component.scss │ │ │ │ │ │ │ ├── dashboard-card-bars.component.html │ │ │ │ │ │ │ ├── dashboard-card-bars.component.spec.ts │ │ │ │ │ │ │ └── dashboard-card-bars.component.ts │ │ │ │ │ ├── status │ │ │ │ │ │ ├── status.component.scss │ │ │ │ │ │ ├── status.component.html │ │ │ │ │ │ └── status.component.ts │ │ │ │ │ ├── dashboard.component.spec.ts │ │ │ │ │ ├── dashboard.component.scss │ │ │ │ │ └── dashboard.component.html │ │ │ │ ├── copilot-value │ │ │ │ │ ├── adoption-chart │ │ │ │ │ │ ├── adoption-chart.component.scss │ │ │ │ │ │ ├── adoption-chart.component.html │ │ │ │ │ │ └── adoption-chart.component.spec.ts │ │ │ │ │ ├── time-saved-chart │ │ │ │ │ │ ├── time-saved-chart.component.scss │ │ │ │ │ │ ├── time-saved-chart.component.html │ │ │ │ │ │ └── time-saved-chart.component.spec.ts │ │ │ │ │ ├── daily-activity-chart │ │ │ │ │ │ ├── daily-activity-chart.component.scss │ │ │ │ │ │ ├── daily-activity-chart.component.html │ │ │ │ │ │ └── daily-activity-chart.component.spec.ts │ │ │ │ │ ├── value.component.scss │ │ │ │ │ ├── value.component.spec.ts │ │ │ │ │ └── value.component.html │ │ │ │ └── copilot-value-modeling │ │ │ │ │ ├── copilot-value-modeling.component.scss │ │ │ │ │ └── copilot-value-modeling.component.spec.ts │ │ │ ├── settings │ │ │ │ ├── settings.component.spec.ts │ │ │ │ └── settings.component.scss │ │ │ ├── main.component.spec.ts │ │ │ └── main.component.scss │ │ ├── shared │ │ │ ├── date-range-select │ │ │ │ ├── date-range-select.component.scss │ │ │ │ ├── date-range-select.component.spec.ts │ │ │ │ └── date-range-select.component.html │ │ │ ├── pipes │ │ │ │ └── currency.pipe.ts │ │ │ ├── table │ │ │ │ ├── table.component.spec.ts │ │ │ │ ├── table.component.scss │ │ │ │ └── table.component.html │ │ │ └── loading-spinner │ │ │ │ ├── loading-spinner.component.spec.ts │ │ │ │ └── loading-spinner.component.ts │ │ ├── services │ │ │ ├── server.service.ts │ │ │ ├── api │ │ │ │ ├── teams.service.ts │ │ │ │ ├── settings.service.ts │ │ │ │ ├── copilot-survey.service.ts │ │ │ │ ├── members.service.ts │ │ │ │ ├── setup.service.ts │ │ │ │ ├── metrics.service.ts │ │ │ │ ├── adoption.service.ts │ │ │ │ ├── metrics.service.interfaces.ts │ │ │ │ └── installations.service.ts │ │ │ └── theme.service.ts │ │ ├── error │ │ │ ├── error.component.html │ │ │ ├── error.component.scss │ │ │ ├── error.component.spec.ts │ │ │ └── error.component.ts │ │ ├── database │ │ │ ├── database.component.scss │ │ │ ├── confetti.service.ts │ │ │ └── database.component.html │ │ ├── install │ │ │ ├── dialog-create-app.scss │ │ │ ├── install.component.spec.ts │ │ │ ├── install.component.scss │ │ │ └── install.component.html │ │ ├── app.component.spec.ts │ │ ├── app.config.ts │ │ ├── app.module.ts │ │ ├── types │ │ │ └── diagnostics.types.ts │ │ ├── guards │ │ │ └── setup.guard.ts │ │ ├── material.module.ts │ │ └── app.routes.ts │ ├── assets │ │ └── images │ │ │ ├── copilot.png │ │ │ ├── GitHub_Logo.png │ │ │ ├── github-mark.png │ │ │ ├── Copilot-App-Icon.png │ │ │ ├── GitHub_Logo_White.png │ │ │ ├── github-mark-white.png │ │ │ ├── GitHub_Logo_nopadding.png │ │ │ ├── github-mark-white.svg │ │ │ ├── github-mark.svg │ │ │ └── github-copilot-icon.svg │ ├── main.ts │ ├── index.html │ └── styles.scss ├── public │ ├── favicon.ico │ ├── favicon-96x96.png │ ├── apple-touch-icon.png │ ├── web-app-manifest-192x192.png │ ├── web-app-manifest-512x512.png │ └── site.webmanifest ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── tasks.json ├── .editorconfig ├── tsconfig.app.json ├── tsconfig.spec.json ├── .gitignore ├── tsconfig.json ├── eslint.config.js ├── README.md └── package.json ├── helm └── README.md ├── backend ├── src │ ├── services │ │ ├── query.service.test.ts │ │ ├── sequence.service.ts │ │ ├── duplicate.service.ts │ │ ├── logger.ts │ │ └── status.service.ts │ ├── .DS_Store │ ├── __tests__ │ │ ├── .DS_Store │ │ ├── __mock__ │ │ │ ├── types.js │ │ │ ├── .DS_Store │ │ │ ├── survey-gen │ │ │ │ ├── exampleSurvey.json │ │ │ │ ├── mockSurveyGenerator.ts │ │ │ │ └── runSurveyGenerator.ts │ │ │ ├── seats-gen │ │ │ │ ├── testStatefulMetrics.ts │ │ │ │ ├── fetch-merge-seats.sh │ │ │ │ ├── seatsExampleTest.json │ │ │ │ ├── runSeatsGenerator.js │ │ │ │ ├── runSeatsGenerator.ts │ │ │ │ └── mockSeatsGenerator.ts │ │ │ ├── types.ts │ │ │ └── mock.mongo.ts │ │ └── services │ │ │ ├── surveyServiceInsertStandalone.ts │ │ │ ├── teams.service.spec.ts │ │ │ ├── seatServiceInsertStandalone.ts │ │ │ ├── seatServiceInsert.spec.ts │ │ │ └── settings.service.spec.ts │ ├── models │ │ ├── settings.model.ts │ │ ├── counter.model.ts │ │ ├── target.model.ts │ │ ├── survey.model.ts │ │ ├── seats.model.ts │ │ └── teams.model.ts │ ├── index.ts │ ├── routes │ │ └── status.route.ts │ └── controllers │ │ ├── metrics.controller.ts │ │ ├── adoption.controller.ts │ │ ├── settings.controller.ts │ │ ├── teams.controller.ts │ │ ├── seats.controller.ts │ │ └── target.controller.ts ├── .gitignore ├── .DS_Store ├── vitest.setup.ts ├── vitest.config.ts ├── eslint.config.mjs ├── README.md ├── github-manifest.json ├── tsconfig.json ├── .env.example └── package.json ├── .DS_Store ├── .vscode ├── settings.json └── launch.json ├── .gitignore ├── heroku.yml ├── .github ├── workflows │ ├── docker-compose.yml │ ├── frontend.yml │ ├── lint.yml │ ├── dependency-review.yml │ ├── backend.yml │ ├── anchore.yml │ └── docker-publish.yml ├── dependabot.yml └── actions │ └── eslint │ └── action.yml ├── .devcontainer ├── Dockerfile ├── compose.yml └── devcontainer.json ├── compose.yml ├── LICENSE └── Dockerfile /frontend/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /helm/README.md: -------------------------------------------------------------------------------- 1 | Helm chart goes here -------------------------------------------------------------------------------- /backend/src/services/query.service.test.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-seats/copilot-seats.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/austenstone/github-value/HEAD/.DS_Store -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-surveys/copilot-surveys.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/ 3 | logs/**/* 4 | .env 5 | /**/.idea/* -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ "./backend", "./frontend" ], 3 | } -------------------------------------------------------------------------------- /backend/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/austenstone/github-value/HEAD/backend/.DS_Store -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-surveys/copilot-survey-details/copilot-survey.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/austenstone/github-value/HEAD/backend/src/.DS_Store -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/austenstone/github-value/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /backend/src/__tests__/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/austenstone/github-value/HEAD/backend/src/__tests__/.DS_Store -------------------------------------------------------------------------------- /frontend/src/app/shared/date-range-select/date-range-select.component.scss: -------------------------------------------------------------------------------- 1 | mat-form-field { 2 | margin-right: 12px; 3 | } -------------------------------------------------------------------------------- /backend/src/__tests__/__mock__/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /frontend/public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/austenstone/github-value/HEAD/frontend/public/favicon-96x96.png -------------------------------------------------------------------------------- /frontend/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/austenstone/github-value/HEAD/frontend/public/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/dashboard-card/dashboard-card-line-chart/dashboard-card-line-chart.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/src/models/settings.model.ts: -------------------------------------------------------------------------------- 1 | type SettingsType = { 2 | name: string; 3 | value: string; 4 | } 5 | 6 | export { SettingsType }; -------------------------------------------------------------------------------- /frontend/src/assets/images/copilot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/austenstone/github-value/HEAD/frontend/src/assets/images/copilot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | /package-lock.json 4 | /packge.json 5 | db/**/* 6 | .env 7 | logs/**/* 8 | eslint-results.sarif 9 | .vs -------------------------------------------------------------------------------- /backend/src/__tests__/__mock__/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/austenstone/github-value/HEAD/backend/src/__tests__/__mock__/.DS_Store -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-value/adoption-chart/adoption-chart.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | height: 100%; 3 | width: 100%; 4 | } -------------------------------------------------------------------------------- /frontend/public/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/austenstone/github-value/HEAD/frontend/public/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /frontend/public/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/austenstone/github-value/HEAD/frontend/public/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/dashboard-card/dashboard-card-drilldown-bar-chart/dashboard-card-drilldown-bar-chart.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-value/time-saved-chart/time-saved-chart.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | height: 100%; 3 | width: 100%; 4 | } -------------------------------------------------------------------------------- /frontend/src/assets/images/GitHub_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/austenstone/github-value/HEAD/frontend/src/assets/images/GitHub_Logo.png -------------------------------------------------------------------------------- /frontend/src/assets/images/github-mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/austenstone/github-value/HEAD/frontend/src/assets/images/github-mark.png -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-value/daily-activity-chart/daily-activity-chart.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | height: 100%; 3 | width: 100%; 4 | } -------------------------------------------------------------------------------- /frontend/src/assets/images/Copilot-App-Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/austenstone/github-value/HEAD/frontend/src/assets/images/Copilot-App-Icon.png -------------------------------------------------------------------------------- /frontend/src/assets/images/GitHub_Logo_White.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/austenstone/github-value/HEAD/frontend/src/assets/images/GitHub_Logo_White.png -------------------------------------------------------------------------------- /frontend/src/assets/images/github-mark-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/austenstone/github-value/HEAD/frontend/src/assets/images/github-mark-white.png -------------------------------------------------------------------------------- /backend/src/models/counter.model.ts: -------------------------------------------------------------------------------- 1 | type CounterType = { 2 | _id: string; // Ensure _id is of type string 3 | seq: number; 4 | } 5 | 6 | export default CounterType; -------------------------------------------------------------------------------- /frontend/src/assets/images/GitHub_Logo_nopadding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/austenstone/github-value/HEAD/frontend/src/assets/images/GitHub_Logo_nopadding.png -------------------------------------------------------------------------------- /frontend/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /backend/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest'; 2 | 3 | // Mock global objects or setup global configurations here 4 | global.console = { 5 | ...console, 6 | log: vi.fn(), 7 | error: vi.fn() 8 | }; 9 | -------------------------------------------------------------------------------- /backend/src/models/target.model.ts: -------------------------------------------------------------------------------- 1 | type TargetValuesType = { 2 | targetedRoomForImprovement: number; 3 | targetedNumberOfDevelopers: number; 4 | targetedPercentOfTimeSaved: number; 5 | } 6 | 7 | export { TargetValuesType }; 8 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-value/time-saved-chart/time-saved-chart.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /backend/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: 'node', 7 | setupFiles: './vitest.setup.ts' 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-value/adoption-chart/adoption-chart.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/dashboard-card/dashboard-card-drilldown-bar-chart/dashboard-card-drilldown-bar-chart.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/src/app/services/server.service.ts: -------------------------------------------------------------------------------- 1 | import { isDevMode } from '@angular/core'; 2 | 3 | export const serverUrl = (() => { 4 | if (isDevMode()) { 5 | return 'http://localhost:80'; 6 | } else { 7 | return ''; 8 | } 9 | })(); 10 | 11 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | setup: 2 | addons: 3 | - plan: jawsdb:leopard # MySQL addon for Heroku 4 | as: DATABASE 5 | build: 6 | docker: 7 | web: Dockerfile 8 | config: 9 | DOCKER_BUILDKIT: 1 10 | NODE_ENV: production 11 | run: 12 | web: npm run start -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-value/daily-activity-chart/daily-activity-chart.component.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig) 6 | .catch((err) => console.error(err)); 7 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/dashboard-card/dashboard-card-line-chart/dashboard-card-line-chart.component.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/docker-compose.yml: -------------------------------------------------------------------------------- 1 | name: Docker Compose 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - run: docker compose build -------------------------------------------------------------------------------- /frontend/src/app/error/error.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

{{ error.status + ' ' || ''}}Error Occurred

5 |

{{ error.message }}

6 | Back to Home 7 |
8 |
9 |
-------------------------------------------------------------------------------- /frontend/src/app/error/error.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100vh; 3 | width: 100%; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | } 8 | 9 | .intro { 10 | text-align: center; 11 | } 12 | 13 | .error-container { 14 | padding: 2rem; 15 | } 16 | 17 | h1 { 18 | margin-bottom: 1rem; 19 | } -------------------------------------------------------------------------------- /backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import App from './app.js'; 2 | 3 | const DEFAULT_PORT = 80; 4 | const app = new App(Number(process.env.PORT) || DEFAULT_PORT); 5 | 6 | ['SIGTERM', 'SIGINT', 'SIGQUIT'].forEach(signal => { 7 | process.on(signal, async () => { 8 | await app.stop(); 9 | process.exit(0); 10 | }); 11 | }); 12 | 13 | app.start(); 14 | 15 | export default app; 16 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-seats/copilot-seats.component.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 7 |
-------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | ij_typescript_use_double_quotes = false 14 | 15 | [*.md] 16 | max_line_length = off 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/dashboard-card/dashboard-card.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | 3 | .dashboard-card { 4 | @include mat.elevation-transition(); 5 | // background: var(--sys-surface-container); 6 | user-select: none; 7 | height: 100%; 8 | } 9 | 10 | .dashboard-card:hover { 11 | @include mat.elevation(2); 12 | // background: var(--sys-surface-container); 13 | } -------------------------------------------------------------------------------- /backend/src/models/survey.model.ts: -------------------------------------------------------------------------------- 1 | type SurveyType = { 2 | id?: number; 3 | org: string; 4 | repo: string; 5 | prNumber: number; 6 | status: 'pending' | 'completed'; 7 | hits: number; 8 | userId: string; 9 | usedCopilot: boolean; 10 | percentTimeSaved: number; 11 | timeUsedFor: string; 12 | reason: string; 13 | kudos?: number; 14 | createdAt?: Date; 15 | updatedAt?: Date; 16 | } 17 | 18 | export { SurveyType }; -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-value-modeling/copilot-value-modeling.component.scss: -------------------------------------------------------------------------------- 1 | table { 2 | width: 100%; 3 | } 4 | 5 | .mat-mdc-form-field { 6 | font-size: 14px; 7 | width: 100%; 8 | } 9 | 10 | td, th { 11 | width: 25%; 12 | } 13 | 14 | h4 { 15 | margin-top: 16px; 16 | } 17 | 18 | .actions-cell button { 19 | visibility: hidden; 20 | } 21 | 22 | tr:hover .actions-cell button { 23 | visibility: visible; 24 | } -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/dashboard-card/dashboard-card-value/dashboard-card-value.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .primary-value { 3 | font-size: 48px; 4 | margin: 0; 5 | } 6 | 7 | .trend-indicator { 8 | display: flex; 9 | gap: 8px; 10 | font-size: var(--mat-card-title-text-size, var(--mat-app-title-large-size)); 11 | .positive { 12 | color: #4caf50; 13 | } 14 | .negative { 15 | color: #f44336; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | import tseslint from "typescript-eslint"; 4 | 5 | 6 | /** @type {import('eslint').Linter.Config[]} */ 7 | export default [ 8 | {files: ["**/*.{js,mjs,cjs,ts}"]}, 9 | {languageOptions: { globals: globals.browser }}, 10 | pluginJs.configs.recommended, 11 | ...tseslint.configs.recommended, 12 | { 13 | ignores: ["src/__tests__/**"] 14 | } 15 | ]; -------------------------------------------------------------------------------- /backend/src/models/seats.model.ts: -------------------------------------------------------------------------------- 1 | type SeatType = { 2 | id?: number; 3 | org: string; 4 | team?: string; 5 | queryAt: Date; 6 | created_at: string | null; 7 | updated_at: string | null; 8 | pending_cancellation_date: string | null; 9 | last_activity_at: Date | null; 10 | last_activity_editor: string | null; 11 | plan_type: string; 12 | assignee_id: number; 13 | createdAt: Date; 14 | updatedAt: Date; 15 | }; 16 | 17 | export { SeatType }; -------------------------------------------------------------------------------- /backend/src/__tests__/__mock__/survey-gen/exampleSurvey.json: -------------------------------------------------------------------------------- 1 | { 2 | "surveys": [{ 3 | "id": 0, 4 | "userId": "mattg57", 5 | "org": "octodemo", 6 | "repo": "github-value2", 7 | "prNumber": "23", 8 | "usedCopilot": true, 9 | "percentTimeSaved": 35, 10 | "reason": "reason", 11 | "timeUsedFor": "repoHousekeeping", 12 | "createdAt": "2025-01-09T18:36:58.737Z", 13 | "updatedAt": "2025-01-09T18:36:58.737Z" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /frontend/src/app/database/database.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | .stepper-container { 8 | padding: 20px; 9 | } 10 | 11 | .mat-horizontal-stepper-header-container { 12 | margin-bottom: 40px; 13 | } 14 | 15 | .stepper-content { 16 | margin: 10px 0px; 17 | width: 100%; 18 | height: 100%; 19 | } 20 | 21 | .database-form { 22 | display: flex; 23 | flex-direction: column; 24 | width: 100%; 25 | } -------------------------------------------------------------------------------- /frontend/src/app/shared/pipes/currency.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'matCurrencyPipe' 5 | }) 6 | export class CurrencyPipe implements PipeTransform { 7 | transform(value: number | string): string { 8 | if (typeof value === 'string') { 9 | value = parseFloat(value.replace(/,/g, '')); 10 | } 11 | return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value); 12 | } 13 | } -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-surveys/copilot-surveys.component.html: -------------------------------------------------------------------------------- 1 |
2 | 9 | 10 |
11 | -------------------------------------------------------------------------------- /frontend/src/app/install/dialog-create-app.scss: -------------------------------------------------------------------------------- 1 | 2 | #manifest { 3 | display: none; 4 | } 5 | 6 | .example-full-width { 7 | width: 100%; 8 | } 9 | 10 | .stepper-margin-top { 11 | margin-top: 16px; 12 | } 13 | #existingAppForm { 14 | .mat-stepper-vertical { 15 | margin-top: 8px; 16 | } 17 | 18 | .mdc-button, 19 | .mat-mdc-form-field { 20 | margin-top: 16px; 21 | } 22 | } 23 | 24 | .mat-error-container { 25 | padding: 0 16px; 26 | font-size:12px; 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/dashboard-card/dashboard-card-bars/dashboard-card-bars.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | padding-top: 16px; 3 | display: block; 4 | } 5 | 6 | .bar-item { 7 | margin-bottom: 24px; 8 | } 9 | 10 | .bar-header { 11 | display: flex; 12 | align-items: center; 13 | margin-bottom: 8px; 14 | } 15 | 16 | .bar-title { 17 | margin-left: 8px; 18 | } 19 | 20 | .bar-footer { 21 | display: flex; 22 | justify-content: space-between; 23 | margin-top: 4px; 24 | } 25 | -------------------------------------------------------------------------------- /frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/app", 7 | "types": [] 8 | }, 9 | "files": [ 10 | "src/main.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/frontend.yml: -------------------------------------------------------------------------------- 1 | name: Frontend 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: '20' 19 | - run: npm ci 20 | working-directory: ./frontend 21 | - run: npm run build 22 | working-directory: ./frontend 23 | -------------------------------------------------------------------------------- /frontend/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/spec", 7 | "types": [ 8 | "jasmine" 9 | ] 10 | }, 11 | "include": [ 12 | "src/**/*.spec.ts", 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Backend 2 | 3 | ## Overview 4 | 5 | 6 | ## Development 7 | ```bash 8 | npm run dev 9 | ``` 10 | 11 | ## Database 12 | The backend uses a MongoDB database to store data. 13 | 14 | You can run the docker compose file to start the database. Just shutdown the backend server so you can free up the port. 15 | ```bash 16 | docker-compose up -d mongo 17 | ``` 18 | 19 | ## API Endpoints 20 | Refer to the routes defined in the [`src/routes/index.ts`](./src/routes/index.ts) file for available API endpoints. 21 | -------------------------------------------------------------------------------- /backend/src/__tests__/__mock__/seats-gen/testStatefulMetrics.ts: -------------------------------------------------------------------------------- 1 | // testStatefulMetrics 2 | // run via "npx tsx src/__tests__/__mock__/seats-gen/testStatefulMetrics.ts" 3 | 4 | import { generateStatefulMetrics } from './runSeatsGenerator.js'; 5 | 6 | // To simulate repeated calls for stateful generation 7 | for (let i = 0; i < 3; i++) { 8 | const repeatedStatefulData = generateStatefulMetrics(); 9 | console.log(`Stateful Data Iteration ${i + 1}:`, JSON.stringify(repeatedStatefulData.seats[0].last_activity_at, null, 2)); 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: ./.github/actions/eslint 17 | with: 18 | working-directory: ./backend 19 | category: backend 20 | - uses: ./.github/actions/eslint 21 | with: 22 | working-directory: ./frontend 23 | category: frontend -------------------------------------------------------------------------------- /frontend/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | imports: [AppComponent], 8 | }).compileComponents(); 9 | }); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /frontend/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GitHub Value", 3 | "short_name": "Value", 4 | "icons": [ 5 | { 6 | "src": "/web-app-manifest-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "maskable" 10 | }, 11 | { 12 | "src": "/web-app-manifest-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "maskable" 16 | } 17 | ], 18 | "theme_color": "#ffffff", 19 | "background_color": "#ffffff", 20 | "display": "standalone" 21 | } -------------------------------------------------------------------------------- /backend/src/services/sequence.service.ts: -------------------------------------------------------------------------------- 1 | import CounterType from 'models/counter.model.js'; 2 | import mongoose from 'mongoose'; 3 | 4 | class SequenceService { 5 | async getNextSequenceValue(sequenceName: string): Promise { 6 | const Counter = mongoose.model('Counter'); 7 | const sequenceCount : CounterType = await Counter.findOneAndUpdate( 8 | { _id: sequenceName }, 9 | { $inc: { seq: 1 } }, 10 | { new: true, upsert: true } 11 | ); 12 | return sequenceCount.seq; 13 | } 14 | } 15 | 16 | export default new SequenceService(); 17 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-surveys/github.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Member } from '../../../services/api/members.service'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class GithubService { 9 | private apiUrl = 'http://localhost:8080/api'; // Replace with your actual API base URL 10 | 11 | constructor(private http: HttpClient) {} 12 | 13 | getOrgMembers() { 14 | return this.http.get(`${this.apiUrl}/members`); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /frontend/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/status/status.component.scss: -------------------------------------------------------------------------------- 1 | .status { 2 | display: flex; 3 | flex-direction: row; 4 | padding: 17.6px 20px 16px 20px; 5 | .status-icon { 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | } 10 | } 11 | 12 | .error { 13 | background-color: var(--error) !important; 14 | color:white; 15 | } 16 | 17 | .success { 18 | background-color: var(--success) !important; 19 | color:white; 20 | } 21 | 22 | .warning { 23 | background-color: var(--warning) !important; 24 | color:white; 25 | } -------------------------------------------------------------------------------- /frontend/src/app/services/api/teams.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { serverUrl } from '../server.service'; 4 | import { Endpoints } from '@octokit/types'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class TeamsService { 10 | private apiUrl = `${serverUrl}/api/teams`; 11 | 12 | constructor(private http: HttpClient) { } 13 | 14 | getAllTeams() { 15 | return this.http.get(`${this.apiUrl}`); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-value/value.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .example-container { 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | .example-sidenav-content { 8 | display: flex; 9 | height: 100%; 10 | // align-items: center; 11 | justify-content: center; 12 | } 13 | 14 | .cards-grid { 15 | display: grid; 16 | grid-template-columns: 100%; 17 | grid-template-rows: repeat(auto-fit, minmax(400px, 1fr)); 18 | gap: 24px; 19 | } 20 | 21 | .page-header { 22 | mat-form-field { 23 | margin: 0 8px; 24 | } 25 | } 26 | 27 | mat-card { 28 | min-height: 400px; 29 | } -------------------------------------------------------------------------------- /backend/src/services/duplicate.service.ts: -------------------------------------------------------------------------------- 1 | class DuplicateService { 2 | private processed = new Set(); 3 | private queue: string[] = []; 4 | private readonly maxEntries = 50; 5 | 6 | async isDuplicate(id: string) { 7 | return this.processed.has(id); 8 | } 9 | 10 | async register(id: string) { 11 | if (!this.processed.has(id)) { 12 | this.processed.add(id); 13 | this.queue.push(id); 14 | if (this.queue.length > this.maxEntries) { 15 | const oldest = this.queue.shift()!; 16 | this.processed.delete(oldest); 17 | } 18 | } 19 | } 20 | } 21 | export default new DuplicateService(); 22 | -------------------------------------------------------------------------------- /backend/src/routes/status.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import StatusService from '../services/status.service.js'; 3 | import logger from '../services/logger.js'; 4 | 5 | const router = express.Router(); 6 | const statusService = new StatusService(); 7 | 8 | router.get('/', async (req, res) => { 9 | try { 10 | // Pass the request object to getStatus 11 | const status = await statusService.getStatus(req); 12 | res.json(status); 13 | } catch (error) { 14 | logger.error('Error fetching status:', error); 15 | res.status(500).json({ error: 'Failed to fetch status' }); 16 | } 17 | }); 18 | 19 | export default router; 20 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/dashboard-card/dashboard-card-bars/dashboard-card-bars.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{section.icon}} 5 | {{section.name}} 6 |
7 | 10 | 11 | 15 |
16 |
-------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/status/status.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{title}} 4 | {{message}} 5 |
6 | 7 |
8 | 21 |
22 |
23 | -------------------------------------------------------------------------------- /frontend/src/app/error/error.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ErrorComponent } from './error.component'; 4 | 5 | describe('ErrorComponent', () => { 6 | let component: ErrorComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ErrorComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ErrorComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/app/shared/table/table.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TableComponent } from './table.component'; 4 | 5 | describe('TableComponent', () => { 6 | let component: TableComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [TableComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(TableComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/app/install/install.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { InstallComponent } from './install.component'; 4 | 5 | describe('WelcomeComponent', () => { 6 | let component: InstallComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [InstallComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(InstallComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /backend/github-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Demonstrate GitHub Value", 3 | "description": "This GitHub App demonstrates the value of GitHub", 4 | "url": "https://www.example.com", 5 | "hook_attributes": { 6 | "url": "https://example.com/github/events" 7 | }, 8 | "setup_url": "http://localhost:8080/api/setup/install/complete", 9 | "redirect_url": "http://localhost:8080/api/setup/registration/complete", 10 | "public": false, 11 | "default_permissions": { 12 | "members": "read", 13 | "metadata": "read", 14 | "organization_copilot_seat_management": "read", 15 | "pull_requests": "read", 16 | "issues": "write" 17 | }, 18 | "default_events": [ 19 | "pull_request", 20 | "team", 21 | "team_add" 22 | ] 23 | } -------------------------------------------------------------------------------- /frontend/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; 2 | import { provideRouter, withInMemoryScrolling } from '@angular/router'; 3 | 4 | import { routes } from './app.routes'; 5 | import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; 6 | import { provideHttpClient } from '@angular/common/http'; 7 | 8 | export const appConfig: ApplicationConfig = { 9 | providers: [ 10 | provideZoneChangeDetection({ eventCoalescing: true }), 11 | provideRouter(routes, withInMemoryScrolling({ 12 | scrollPositionRestoration: 'enabled', 13 | anchorScrolling: 'enabled', 14 | })), 15 | provideAnimationsAsync(), 16 | provideHttpClient(), 17 | ] 18 | }; 19 | -------------------------------------------------------------------------------- /frontend/src/app/main/settings/settings.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SettingsComponent } from './settings.component'; 4 | 5 | describe('SettingsComponent', () => { 6 | let component: SettingsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [SettingsComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(SettingsComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /frontend/src/app/install/install.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | width: 100%; 4 | height: 100%; 5 | overflow: hidden; 6 | } 7 | 8 | .container { 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | flex-direction: column; 13 | width: 100%; 14 | height: 100%; 15 | } 16 | 17 | .intro { 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | flex-direction: column; 22 | width: 450px; 23 | img-container { 24 | height: 18em; 25 | } 26 | img { 27 | display: block; 28 | margin: 20px; 29 | height: 18em; 30 | } 31 | } 32 | 33 | .example-header-image { 34 | background-image: url('https://material.angular.io/assets/img/examples/shiba1.jpg'); 35 | background-size: cover; 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-value/value.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CopilotValueComponent } from './value.component'; 4 | 5 | describe('CopilotDashboardComponent', () => { 6 | let component: CopilotValueComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [CopilotValueComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(CopilotValueComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-seats/copilot-seats.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CopilotSeatsComponent } from './copilot-seats.component'; 4 | 5 | describe('SeatsComponent', () => { 6 | let component: CopilotSeatsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [CopilotSeatsComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(CopilotSeatsComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "nodenext", 5 | "moduleResolution": "nodenext", 6 | "rootDir": "./src", 7 | "outDir": "./dist", 8 | "baseUrl": "./src", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "sourceMap": true, 14 | "inlineSources": true, 15 | "resolveJsonModule": true, 16 | "allowImportingTsExtensions": false, 17 | "noEmit": false 18 | }, 19 | "include": [ 20 | "src/**/*.ts" 21 | ], 22 | "exclude": [ 23 | "node_modules", 24 | "src/__tests__/**", // temp 25 | "**/*.spec.ts" 26 | ], 27 | "ts-node": { 28 | "esm": true, 29 | "experimentalSpecifiers": true 30 | } 31 | } -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # https://github.com/devcontainers/images/tree/main/src/typescript-node 2 | FROM mcr.microsoft.com/devcontainers/typescript-node 3 | 4 | # [Optional] Uncomment this section to install additional OS packages. 5 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 6 | # && apt-get -y install --no-install-recommends 7 | 8 | # [Optional] Uncomment if you want to install an additional version of node using nvm 9 | # ARG EXTRA_NODE_VERSION=10 10 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 11 | 12 | # [Optional] Uncomment if you want to install more global node packages 13 | ARG EXTRA_NODE_PACKAGES="@angular/cli" 14 | RUN su node -c "npm install -g ${EXTRA_NODE_PACKAGES}" 15 | ENV NG_FORCE_TTY=false 16 | -------------------------------------------------------------------------------- /backend/src/controllers/metrics.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import MetricsService from '../services/metrics.service.js'; 3 | 4 | class MetricsController { 5 | async getMetrics(req: Request, res: Response): Promise { 6 | try { 7 | const metrics = await MetricsService.getMetrics(req.query) 8 | res.status(200).json(metrics); 9 | } catch (error) { 10 | res.status(500).json(error); 11 | } 12 | } 13 | 14 | async getMetricsTotals(req: Request, res: Response): Promise { 15 | try { 16 | const metrics = await MetricsService.getMetricsTotals(req.query) 17 | res.status(200).json(metrics); 18 | } catch (error) { 19 | res.status(500).json(error); 20 | } 21 | } 22 | } 23 | 24 | export default new MetricsController(); -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/dashboard.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CopilotDashboardComponent } from './dashboard.component'; 4 | 5 | describe('HomeComponent', () => { 6 | let component: CopilotDashboardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [CopilotDashboardComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(CopilotDashboardComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CopilotMetricsComponent } from './copilot-metrics.component'; 4 | 5 | describe('MetricsComponent', () => { 6 | let component: CopilotMetricsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [CopilotMetricsComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(CopilotMetricsComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-surveys/copilot-surveys.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CopilotSurveysComponent } from './copilot-surveys.component'; 4 | 5 | describe('SurveysComponent', () => { 6 | let component: CopilotSurveysComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [CopilotSurveysComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(CopilotSurveysComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/app/shared/loading-spinner/loading-spinner.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoadingSpinnerComponent } from './loading-spinner.component'; 4 | 5 | describe('LoadingSpinnerComponent', () => { 6 | let component: LoadingSpinnerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [LoadingSpinnerComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(LoadingSpinnerComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-value/adoption-chart/adoption-chart.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AdoptionChartComponent } from './adoption-chart.component'; 4 | 5 | describe('AdoptionChartComponent', () => { 6 | let component: AdoptionChartComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [AdoptionChartComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(AdoptionChartComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/app/shared/date-range-select/date-range-select.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DateRangeSelectComponent } from './date-range-select.component'; 4 | 5 | describe('DateRangeSelectComponent', () => { 6 | let component: DateRangeSelectComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [DateRangeSelectComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(DateRangeSelectComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/status/status.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; 2 | import { MatButtonModule } from '@angular/material/button'; 3 | import { MatCardModule } from '@angular/material/card'; 4 | import { MatIconModule } from '@angular/material/icon'; 5 | 6 | @Component({ 7 | selector: 'app-status', 8 | standalone: true, 9 | imports: [ 10 | MatCardModule, 11 | MatIconModule, 12 | MatButtonModule 13 | ], 14 | templateUrl: './status.component.html', 15 | styleUrl: './status.component.scss', 16 | changeDetection: ChangeDetectionStrategy.OnPush, 17 | }) 18 | export class StatusComponent { 19 | @Input() title?: string; 20 | @Input() message?: string; 21 | @Input() status?: 'success' | 'error' | 'warning' = 'error'; 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-value/time-saved-chart/time-saved-chart.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TimeSavedChartComponent } from './time-saved-chart.component'; 4 | 5 | describe('AdoptionChartComponent', () => { 6 | let component: TimeSavedChartComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [TimeSavedChartComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(TimeSavedChartComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/app/main/main.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 3 | 4 | import { MainComponent } from './main.component'; 5 | 6 | describe('MainComponent', () => { 7 | let component: MainComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | imports: [NoopAnimationsModule] 13 | }).compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(MainComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should compile', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NewCopilotSurveyComponent } from './new-copilot-survey.component'; 4 | 5 | describe('CopilotSurveyComponent', () => { 6 | let component: NewCopilotSurveyComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [NewCopilotSurveyComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(NewCopilotSurveyComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-value/daily-activity-chart/daily-activity-chart.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DailyActivityChartComponent } from './daily-activity-chart.component'; 4 | 5 | describe('AdoptionChartComponent', () => { 6 | let component: DailyActivityChartComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [DailyActivityChartComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(DailyActivityChartComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-value-modeling/copilot-value-modeling.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CopilotValueModelingComponent } from './copilot-value-modeling.component'; 4 | 5 | describe('CopilotValueModelingComponent', () => { 6 | let component: CopilotValueModelingComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [CopilotValueModelingComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(CopilotValueModelingComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "npm" 9 | directory: "/backend" 10 | schedule: 11 | interval: "weekly" 12 | groups: 13 | production-dependencies: 14 | dependency-type: "production" 15 | development-dependencies: 16 | dependency-type: "development" 17 | 18 | - package-ecosystem: "npm" 19 | directory: "/frontend" 20 | schedule: 21 | interval: "weekly" 22 | groups: 23 | production-dependencies: 24 | dependency-type: "production" 25 | development-dependencies: 26 | dependency-type: "development" 27 | angular: 28 | patterns: 29 | - "@angular*" 30 | update-types: 31 | - "minor" 32 | - "patch" 33 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/dashboard-card/dashboard-card-bars/dashboard-card-bars.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DashboardCardBarsComponent } from './dashboard-card-bars.component'; 4 | 5 | describe('DashboardBarsCardComponent', () => { 6 | let component: DashboardCardBarsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [DashboardCardBarsComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(DashboardCardBarsComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ReactiveFormsModule } from '@angular/forms'; 3 | import { MaterialModule } from './material.module'; 4 | import { RouterLink, RouterModule, RouterOutlet } from '@angular/router'; 5 | import { CommonModule } from '@angular/common'; 6 | import { TableComponent } from './shared/table/table.component'; 7 | 8 | 9 | @NgModule({ 10 | imports: [ 11 | RouterOutlet, 12 | RouterLink, 13 | RouterModule, //.forRoot(routes, { anchorScrolling: 'enabled', scrollOffset: [0, 64] }), 14 | ReactiveFormsModule, 15 | MaterialModule, 16 | TableComponent 17 | ], 18 | exports: [ 19 | RouterOutlet, 20 | RouterLink, 21 | RouterModule, 22 | ReactiveFormsModule, 23 | MaterialModule, 24 | CommonModule, 25 | TableComponent 26 | ] 27 | }) 28 | export class AppModule { } 29 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/dashboard-card/dashboard-card-value/dashboard-card-value.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DashboardCardValueComponent } from './dashboard-card-value.component'; 4 | 5 | describe('DashboardCardComponent', () => { 6 | let component: DashboardCardValueComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [DashboardCardValueComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(DashboardCardValueComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/app/shared/loading-spinner/loading-spinner.component.ts: -------------------------------------------------------------------------------- 1 | // loading-spinner.component.ts 2 | import { Component } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; 5 | 6 | @Component({ 7 | selector: 'app-loading-spinner', 8 | standalone: true, 9 | imports: [CommonModule, MatProgressSpinnerModule], 10 | template: ` 11 |
12 | 13 |
14 | `, 15 | styles: [` 16 | :host { 17 | width: 100%; 18 | height: 100%; 19 | } 20 | .spinner-container { 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | min-height: 100px; 25 | width: 100%; 26 | height: 100%; 27 | } 28 | `] 29 | }) 30 | export class LoadingSpinnerComponent {} -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/dashboard-card/dashboard-card-line-chart/dashboard-card-line-chart.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DashboardCardLineChartComponent } from './dashboard-card-line-chart.component'; 4 | 5 | describe('DashboardCardLineChartComponent', () => { 6 | let component: DashboardCardLineChartComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [DashboardCardLineChartComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(DashboardCardLineChartComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GithubValue 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /frontend/src/assets/images/github-mark-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/images/github-mark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.devcontainer/compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | dev: 3 | build: 4 | dockerfile: ./Dockerfile 5 | volumes: 6 | - ../..:/workspaces:cached 7 | network_mode: service:mongo 8 | command: sleep infinity 9 | depends_on: 10 | mongo: 11 | condition: service_healthy 12 | environment: 13 | PORT: 8080 14 | NODE_ENV: development 15 | MONGODB_URI: mongodb://root:octocat@mongo:27017 16 | 17 | mongo: 18 | image: mongo 19 | restart: always 20 | ports: 21 | - '27017:27017' 22 | environment: 23 | MONGO_INITDB_ROOT_USERNAME: root 24 | MONGO_INITDB_ROOT_PASSWORD: octocat 25 | volumes: 26 | - mongo-data:/data/db 27 | healthcheck: 28 | test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')", "-u", "root", "-p", "octocat", "--authenticationDatabase", "admin"] 29 | interval: 10s 30 | timeout: 5s 31 | retries: 10 32 | start_period: 20s 33 | 34 | volumes: 35 | mongo-data: 36 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/dashboard-card/dashboard-card-drilldown-bar-chart/dashboard-card-drilldown-bar-chart.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DashboardCardDrilldownBarChartComponent } from './dashboard-card-drilldown-bar-chart.component'; 4 | 5 | describe('DashboardCardDrilldownBarChartComponent', () => { 6 | let component: DashboardCardDrilldownBarChartComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [DashboardCardDrilldownBarChartComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(DashboardCardDrilldownBarChartComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | backend: 3 | restart: always 4 | build: 5 | dockerfile: Dockerfile 6 | ports: 7 | - "8080:8080" 8 | environment: 9 | PORT: 8080 10 | NODE_ENV: production 11 | NODE_OPTIONS: --enable-source-maps 12 | MONGODB_URI: mongodb://root:octocat@mongo:27017 13 | depends_on: 14 | mongo: 15 | condition: service_healthy 16 | volumes: 17 | - ./:/src 18 | 19 | mongo: 20 | image: mongo 21 | restart: always 22 | ports: 23 | - '27017:27017' 24 | environment: 25 | MONGO_INITDB_ROOT_USERNAME: root 26 | MONGO_INITDB_ROOT_PASSWORD: octocat 27 | volumes: 28 | - mongo-data:/data/db 29 | healthcheck: 30 | test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')", "-u", "root", "-p", "octocat", "--authenticationDatabase", "admin"] 31 | interval: 10s 32 | timeout: 5s 33 | retries: 10 34 | start_period: 20s 35 | 36 | volumes: 37 | mongo-data: 38 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Frontend", 6 | "request": "launch", 7 | "type": "chrome", 8 | "url": "http://localhost:4200", 9 | "preLaunchTask": "npm: start - frontend", 10 | "webRoot": "${workspaceFolder}/frontend", 11 | "sourceMaps": true, 12 | }, 13 | { 14 | "request": "launch", 15 | "name": "Debug Backend", 16 | "type": "node", 17 | "program": "${workspaceFolder}/backend/src/index.ts", 18 | "preLaunchTask": "npm: build - backend", 19 | "cwd": "${workspaceFolder}/backend", 20 | "envFile": "${workspaceFolder}/backend/.env", 21 | "args": [ 22 | "|", 23 | "bunyan", 24 | ], 25 | "skipFiles": [ 26 | "/**" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/backend/dist/**/*.js" 30 | ], 31 | "sourceMaps": true, 32 | "console": "integratedTerminal" 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-seats/copilot-seat/copilot-seat.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | } 4 | 5 | .container { 6 | padding: 20px; 7 | } 8 | 9 | .header { 10 | display: flex; 11 | justify-content: space-between; 12 | align-items: center; 13 | margin-bottom: 20px; 14 | } 15 | 16 | .time-range-selector { 17 | margin-bottom: 20px; 18 | } 19 | 20 | .info-card { 21 | margin-bottom: 20px; 22 | } 23 | 24 | .chart-card { 25 | margin-bottom: 20px; 26 | } 27 | 28 | .user-info { 29 | display: flex; 30 | align-items: center; 31 | } 32 | 33 | .avatar { 34 | width: 80px; 35 | height: 80px; 36 | border-radius: 50%; 37 | margin-right: 20px; 38 | } 39 | 40 | .details { 41 | h2 { 42 | margin-top: 0; 43 | margin-bottom: 10px; 44 | } 45 | 46 | p { 47 | margin: 5px 0; 48 | } 49 | } 50 | 51 | .loading { 52 | display: flex; 53 | justify-content: center; 54 | align-items: center; 55 | height: 300px; 56 | font-size: 18px; 57 | color: #666; 58 | } 59 | -------------------------------------------------------------------------------- /backend/src/__tests__/__mock__/seats-gen/fetch-merge-seats.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Create a temporary directory for individual JSON files 4 | mkdir -p temp_json 5 | 6 | # Fetch data for pages 1-9 7 | for page in {1..9} 8 | do 9 | echo "Fetching page $page..." 10 | gh api "/orgs/octodemo/copilot/billing/seats?per_page=100&page=$page" > "temp_json/seats_page_$page.json" 11 | done 12 | 13 | # Use jq to combine all seats arrays into a single file 14 | # First create the combined array from the first file 15 | jq '.seats' "temp_json/seats_page_1.json" > merged_seats.json 16 | 17 | # Then append the seats arrays from the remaining files 18 | for page in {2..9} 19 | do 20 | # Use jq to add the seats array from each subsequent file 21 | jq -s '.[0] + .[1]' merged_seats.json <(jq '.seats' "temp_json/seats_page_$page.json") > temp.json 22 | mv temp.json merged_seats.json 23 | done 24 | 25 | # Clean up temporary files 26 | rm -rf temp_json 27 | 28 | echo "All seats arrays have been merged into merged_seats.json" 29 | -------------------------------------------------------------------------------- /frontend/src/app/error/error.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { MatButtonModule } from '@angular/material/button'; 3 | import { RouterModule, Router } from '@angular/router'; 4 | 5 | @Component({ 6 | selector: 'app-error', 7 | imports: [ 8 | RouterModule, 9 | MatButtonModule 10 | ], 11 | templateUrl: './error.component.html', 12 | styleUrl: './error.component.scss' 13 | }) 14 | export class ErrorComponent { 15 | error = { 16 | code: 'UNKNOWN', 17 | message: 'An unknown error occurred', 18 | status: 0 19 | }; 20 | 21 | constructor( 22 | private router: Router 23 | ) { 24 | const navigation = this.router.getCurrentNavigation(); 25 | const state = navigation?.extras?.state; 26 | 27 | if (state) { 28 | if (state['error']) { 29 | this.error = state['error'] || JSON.stringify(state['error']); 30 | } else { 31 | this.error.message = 'An unknown error occurred'; 32 | } 33 | } else { 34 | this.router.navigate(['/']); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/app/install/install.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | 7 | 8 |

GitHub Value App

9 |
10 | GitHub Mark 13 |
14 | 15 |

16 | To get started you'll need a GitHub App. 17 |

18 |
19 | 20 | 21 | 22 | 23 |
24 |
-------------------------------------------------------------------------------- /frontend/src/app/shared/table/table.component.scss: -------------------------------------------------------------------------------- 1 | table { 2 | width: 100%; 3 | } 4 | 5 | .mat-mdc-form-field { 6 | width: 100%; 7 | } 8 | 9 | td, th { 10 | // width: 25%; 11 | } 12 | 13 | .table-avatar { 14 | border-radius: 50%; 15 | object-fit: cover; 16 | } 17 | 18 | .nowrap { 19 | white-space: nowrap; 20 | } 21 | 22 | .loading-shade { 23 | position: absolute; 24 | top: 0; 25 | left: 0; 26 | bottom: 0; 27 | right: 0; 28 | background: rgba(0, 0, 0, 0.15); 29 | z-index: 1; 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | } 34 | 35 | .mat-elevation-z4 { 36 | position: relative; 37 | } 38 | 39 | .filter-not-found-row { 40 | text-align: center; 41 | td { 42 | padding: 20px; 43 | } 44 | } 45 | 46 | .clickable { 47 | &.mat-mdc-row .mat-mdc-cell { 48 | cursor: pointer; 49 | transition: all 0.1s ease-in-out; 50 | } 51 | 52 | &.mat-mdc-row:hover { 53 | background: var(--sys-surface-container-high); 54 | color: var(--sys-on-surface); 55 | border-color: var(--sys-primary); 56 | } 57 | 58 | } 59 | 60 | mat-icon { 61 | color: var(--sys-primary); 62 | } -------------------------------------------------------------------------------- /frontend/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/app/database/confetti.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import confetti from 'canvas-confetti'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class ConfettiService { 8 | celebrate() { 9 | const duration = 15 * 1000; 10 | const animationEnd = Date.now() + duration; 11 | const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 999 }; 12 | 13 | function randomInRange(min: number, max: number) { 14 | return Math.random() * (max - min) + min; 15 | } 16 | 17 | const interval = setInterval(function () { 18 | const timeLeft = animationEnd - Date.now(); 19 | 20 | if (timeLeft <= 0) { 21 | return clearInterval(interval); 22 | } 23 | 24 | const particleCount = 50 * (timeLeft / duration); 25 | // since particles fall down, start a bit higher than random 26 | confetti({ ...defaults, particleCount, origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 } }); 27 | confetti({ ...defaults, particleCount, origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 } }); 28 | }, 250); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Austen Stone 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 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "compileOnSave": false, 5 | "compilerOptions": { 6 | "outDir": "./dist/out-tsc", 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "skipLibCheck": true, 13 | "isolatedModules": true, 14 | "esModuleInterop": true, 15 | "sourceMap": true, 16 | "declaration": false, 17 | "experimentalDecorators": true, 18 | "moduleResolution": "bundler", 19 | "importHelpers": true, 20 | "target": "ES2022", 21 | "module": "ES2022", 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ] 26 | }, 27 | "angularCompilerOptions": { 28 | "enableI18nLegacyMessageIdFormat": false, 29 | "strictInjectionParameters": true, 30 | "strictInputAccessModifiers": true, 31 | "strictTemplates": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const eslint = require("@eslint/js"); 3 | const tseslint = require("typescript-eslint"); 4 | const angular = require("angular-eslint"); 5 | 6 | module.exports = tseslint.config( 7 | { 8 | files: ["**/*.ts"], 9 | extends: [ 10 | eslint.configs.recommended, 11 | ...tseslint.configs.recommended, 12 | ...tseslint.configs.stylistic, 13 | ...angular.configs.tsRecommended, 14 | ], 15 | processor: angular.processInlineTemplates, 16 | rules: { 17 | "@angular-eslint/directive-selector": [ 18 | "error", 19 | { 20 | type: "attribute", 21 | prefix: "app", 22 | style: "camelCase", 23 | }, 24 | ], 25 | "@angular-eslint/component-selector": [ 26 | "error", 27 | { 28 | type: "element", 29 | prefix: "app", 30 | style: "kebab-case", 31 | }, 32 | ], 33 | }, 34 | }, 35 | { 36 | files: ["**/*.html"], 37 | extends: [ 38 | ...angular.configs.templateRecommended, 39 | ...angular.configs.templateAccessibility, 40 | ], 41 | rules: {}, 42 | } 43 | ); 44 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG VARIANT=22-bullseye-slim 2 | 3 | FROM node:${VARIANT} AS backend-builder 4 | WORKDIR /app/backend 5 | COPY backend/package.json backend/package-lock.json ./ 6 | RUN npm install 7 | COPY backend/ ./ 8 | RUN npm run build 9 | 10 | FROM node:${VARIANT} AS frontend-builder 11 | WORKDIR /app/frontend 12 | COPY frontend/package.json frontend/package-lock.json ./ 13 | RUN npm install 14 | COPY frontend/ ./ 15 | RUN npm run build 16 | 17 | FROM node:${VARIANT} 18 | WORKDIR /app 19 | 20 | COPY --from=backend-builder /app/backend/dist ./backend/dist 21 | COPY --from=backend-builder /app/backend/package.json ./backend/package.json 22 | COPY --from=backend-builder /app/backend/node_modules ./backend/node_modules 23 | COPY --from=backend-builder /app/backend/github-manifest.json ./backend/github-manifest.json 24 | 25 | COPY --from=frontend-builder /app/frontend/dist ./frontend/dist 26 | COPY --from=frontend-builder /app/frontend/package.json ./frontend/package.json 27 | COPY --from=frontend-builder /app/frontend/node_modules ./frontend/node_modules 28 | 29 | EXPOSE 8080 30 | 31 | WORKDIR /app/backend 32 | CMD node dist/index.js | ./node_modules/.bin/bunyan -o short -l info 33 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # GithubValue 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.2.8. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. 28 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/dashboard-card/dashboard-card-value/dashboard-card-value.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{title}} 4 | {{subtitle}} 5 | 6 | 7 | 8 |
9 |

{{value | number:'1.0-0'}}

10 |
11 | {{change > 0 ? 12 | 'trending_up' : 13 | 'trending_down'}} 14 | {{icon}} 15 | {{change | number:'1.0-0'}}{{changeSuffix}}{{changeDescription ? changeDescription : ''}} 16 |
17 |
18 |
19 | 20 | 21 | 22 |
23 |
-------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/dashboard-card/dashboard-card-value/dashboard-card-value.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, Input } from '@angular/core'; 3 | import { MatCardModule } from '@angular/material/card'; 4 | import { MatIconModule } from '@angular/material/icon'; 5 | import { LoadingSpinnerComponent } from '../../../../../shared/loading-spinner/loading-spinner.component'; 6 | 7 | @Component({ 8 | selector: 'app-dashboard-card-value', 9 | standalone: true, 10 | imports: [ 11 | MatCardModule, 12 | MatIconModule, 13 | CommonModule, 14 | LoadingSpinnerComponent 15 | ], 16 | templateUrl: './dashboard-card-value.component.html', 17 | styleUrls: [ 18 | './dashboard-card-value.component.scss', 19 | '../dashboard-card.scss' 20 | ] 21 | }) 22 | export class DashboardCardValueComponent { 23 | @Input() title?: string; 24 | @Input() value?: number; 25 | @Input() description?: string; 26 | @Input() change?: number; 27 | @Input() changeSuffix?: string = '%'; 28 | @Input() changeDescription?: string; 29 | @Input() icon?: string; 30 | @Input() subtitle?: string; 31 | Math = Math; 32 | } 33 | -------------------------------------------------------------------------------- /.github/actions/eslint/action.yml: -------------------------------------------------------------------------------- 1 | name: 'ESLint Analysis' 2 | description: 'Run ESLint analysis and upload SARIF results' 3 | 4 | inputs: 5 | working-directory: 6 | description: 'Directory containing the code to analyze' 7 | required: false 8 | default: '.' 9 | category: 10 | description: 'The category of the codeql-analysis run' 11 | required: false 12 | 13 | runs: 14 | using: "composite" 15 | steps: 16 | - name: Install ESLint and SARIF formatter 17 | shell: bash 18 | run: | 19 | npm i eslint@^8.9.0 20 | npm i @microsoft/eslint-formatter-sarif 21 | working-directory: ${{ inputs.working-directory }} 22 | 23 | - name: Run ESLint 24 | shell: bash 25 | run: npm run lint -- --format @microsoft/eslint-formatter-sarif --output-file eslint-results.sarif 26 | env: 27 | SARIF_ESLINT_IGNORE_SUPPRESSED: "true" 28 | working-directory: ${{ inputs.working-directory }} 29 | continue-on-error: true 30 | 31 | - name: Upload SARIF results 32 | uses: github/codeql-action/upload-sarif@v3 33 | with: 34 | sarif_file: ${{ inputs.working-directory }}/eslint-results.sarif 35 | wait-for-processing: true 36 | category: ${{ inputs.category }} 37 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/dashboard-card/dashboard-card-line-chart/dashboard-card-line-chart.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnChanges } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HighchartsChartModule } from 'highcharts-angular'; 4 | import * as Highcharts from 'highcharts'; 5 | import { CopilotMetrics } from '../../../../../services/api/metrics.service.interfaces'; 6 | import { HighchartsService } from '../../../../../services/highcharts.service'; 7 | 8 | @Component({ 9 | selector: 'app-dashboard-card-line-chart', 10 | standalone: true, 11 | imports: [CommonModule, HighchartsChartModule], 12 | templateUrl: './dashboard-card-line-chart.component.html', 13 | styleUrls: ['./dashboard-card-line-chart.component.scss'] 14 | }) 15 | export class DashboardCardLineChartComponent implements OnChanges { 16 | @Input() data?: CopilotMetrics[]; 17 | 18 | Highcharts: typeof Highcharts = Highcharts; 19 | options: Highcharts.Options | undefined; 20 | 21 | constructor(private highchartsService: HighchartsService) {} 22 | 23 | ngOnChanges(): void { 24 | if (this.data && this.data.length) { 25 | this.options = this.highchartsService.getLanguageTrendsChart(this.data); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/dashboard.component.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | 3 | .cards-grid { 4 | margin-top:24px; 5 | display: grid; 6 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 7 | gap: 24px; 8 | 9 | mat-card-content { 10 | min-height: 400px; 11 | height: 100%; 12 | max-width: 100%; 13 | overflow: hidden; 14 | } 15 | #drilldown-bar-chart { 16 | // grid-row: span 4; 17 | grid-column: span 2; 18 | } 19 | #card-bars { 20 | // grid-row: span 4; 21 | } 22 | #adoption, #activity, #time-saved { 23 | max-height: 300px; 24 | height: 300px; 25 | } 26 | mat-card { 27 | max-width: 100%; 28 | width: 100%; 29 | overflow: hidden; 30 | } 31 | 32 | // #status { 33 | // grid-column: span 3; 34 | // } 35 | } 36 | 37 | /* Add media query for smaller screens */ 38 | @media (max-width: 768px) { 39 | .cards-grid { 40 | } 41 | 42 | /* Reset the grid spans on mobile */ 43 | .cards-grid>* { 44 | grid-row: auto !important; 45 | grid-column: auto !important; 46 | } 47 | } 48 | 49 | .dashboard-card { 50 | // background: var(--sys-surface-container); 51 | user-select: none; 52 | } 53 | 54 | .dashboard-card:hover { 55 | @include mat.elevation(2); 56 | } -------------------------------------------------------------------------------- /frontend/src/app/main/settings/settings.component.scss: -------------------------------------------------------------------------------- 1 | .full-width { 2 | width: 100%; 3 | } 4 | 5 | form { 6 | width: 100%; 7 | display: grid; 8 | grid-template-columns: auto 1fr; /* Left header, right content */ 9 | gap: 24px; 10 | align-items: start; 11 | } 12 | 13 | .settings-header { 14 | grid-column: 1; 15 | max-width: 200px; 16 | } 17 | 18 | h4, h3 { 19 | margin: 0; 20 | } 21 | 22 | .settings-content { 23 | width: 100%; 24 | grid-column: 2; 25 | mat-form-field { 26 | width: 100%; 27 | } 28 | max-width: 100%; 29 | } 30 | 31 | .auto-fit-grid { 32 | display: grid; 33 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 34 | gap: 16px; 35 | } 36 | 37 | .settings-divider { 38 | grid-column: 1 / -1; 39 | } 40 | 41 | @media (max-width: 768px) { 42 | form { 43 | grid-template-columns: 1fr; 44 | gap: 16px; 45 | } 46 | 47 | .settings-header, 48 | .settings-content { 49 | grid-column: 1; 50 | width: 100%; 51 | max-width: 100%; 52 | } 53 | } 54 | 55 | .example-right-align { 56 | text-align: right; 57 | } 58 | 59 | input.example-right-align::-webkit-outer-spin-button, 60 | input.example-right-align::-webkit-inner-spin-button { 61 | display: none; 62 | } 63 | 64 | input.example-right-align { 65 | -moz-appearance: textfield; 66 | } 67 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-surveys/copilot-survey-details/copilot-survey.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { CopilotSurveyService, Survey } from '../../../../services/api/copilot-survey.service'; 4 | import { MatCardModule } from '@angular/material/card'; 5 | import { CommonModule } from '@angular/common'; 6 | import { MatChipsModule } from '@angular/material/chips'; 7 | import { MatIconModule } from '@angular/material/icon'; 8 | 9 | @Component({ 10 | selector: 'app-copilot-survey', 11 | standalone: true, 12 | templateUrl: './copilot-survey.component.html', 13 | styleUrls: ['./copilot-survey.component.scss'], 14 | imports: [ 15 | MatCardModule, 16 | CommonModule, 17 | MatChipsModule, 18 | MatIconModule 19 | ] 20 | }) 21 | export class CopilotSurveyComponent implements OnInit { 22 | survey?: Survey; 23 | 24 | constructor( 25 | private route: ActivatedRoute, 26 | private copilotSurveyService: CopilotSurveyService 27 | ) { } 28 | 29 | ngOnInit() { 30 | const id = this.route.snapshot.paramMap.get('id'); 31 | if (id) { 32 | this.copilotSurveyService.getSurveyById(+id).subscribe((survey) => { 33 | this.survey = survey; 34 | }); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/src/__tests__/services/surveyServiceInsertStandalone.ts: -------------------------------------------------------------------------------- 1 | // surveyServiceInsertStandalone.ts run via "npx tsx src/__tests__/services/surveyServiceInsertStandalone.ts" 2 | import SurveyService from '../../services/survey.service.js'; 3 | import { generateSurveys } from '../__mock__/survey-gen/runSurveyGenerator.js'; 4 | import Database from '../../database.js'; 5 | import 'dotenv/config'; 6 | 7 | if (!process.env.MONGODB_URI) throw new Error('MONGODB_URI is not defined'); 8 | const database = new Database(process.env.MONGODB_URI); 9 | 10 | async function runTest() { 11 | try { 12 | await database.connect(); 13 | 14 | // Test data setup 15 | const surveys = generateSurveys(); 16 | 17 | // Insert each survey 18 | for (const survey of surveys.surveys) { 19 | await SurveyService.createSurvey(survey); 20 | } 21 | 22 | // Verify the insertion 23 | // const insertedSurveys = await SurveyService.getRecentSurveysWithGoodReasons(5); 24 | 25 | // Assertions 26 | // if (!insertedSurveys || insertedSurveys.length === 0) { 27 | // throw new Error('No surveys were inserted.'); 28 | // } 29 | 30 | // console.log('Inserted Surveys:', insertedSurveys); 31 | console.log('Test passed successfully.'); 32 | } catch (error) { 33 | console.error('Test failed:', error); 34 | } finally { 35 | await database.disconnect(); 36 | } 37 | } 38 | 39 | runTest(); 40 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-surveys/copilot-survey-details/copilot-survey.component.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 | 7 | {{ '@' + survey.userId }} 8 | {{ survey.createdAt | date: 'short' }} 9 | 10 | 11 | 12 | 13 | {{survey.status === 'pending' ? 'pending' : 'check'}} 14 | {{ survey.status | titlecase }} 15 | 16 | 17 | 18 |

Used Copilot: {{ survey.usedCopilot ? 'Yes' : 'No' }}

19 |

Time Saved: {{ survey.percentTimeSaved }}%

20 |

Reason: {{ survey.reason }}

21 |

Time Used For: {{ survey.timeUsedFor }}

22 |
23 |

PR: {{ survey.repo }}#{{ survey.prNumber }}

24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /backend/src/__tests__/services/teams.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, beforeAll, beforeEach } from 'vitest'; 2 | import mongoose from 'mongoose'; 3 | import {readFileSync} from 'fs'; 4 | import 'dotenv/config' 5 | 6 | import Database from '../../database'; 7 | 8 | const org = null; 9 | 10 | beforeAll(async () => { 11 | if (!process.env.MONGODB_URI) throw new Error('MONGODB_URI is not defined'); 12 | const database = new Database(process.env.MONGODB_URI); 13 | await database.connect(); 14 | }); 15 | 16 | beforeEach(async () => { 17 | const Members = mongoose.model('Member'); 18 | await Members.deleteMany({ org }); 19 | }); 20 | 21 | describe('team.service.spec.ts test', () => { 22 | test('should upsert members correctly', async () => { 23 | const members = JSON.parse(readFileSync('src/__tests__/__mock__/members.json', 'utf8')); 24 | const Members = mongoose.model('Member'); 25 | 26 | // Use bulkWrite with updateOne operations 27 | const bulkOps = members.map((member: any) => ({ 28 | updateOne: { 29 | filter: { org, id: member.id }, 30 | update: { $set: member }, 31 | upsert: true 32 | } 33 | })); 34 | 35 | await Members.bulkWrite(bulkOps, { ordered: false }); 36 | 37 | const membersRsp = await Members.find({ org }).sort({ login: 1 }); 38 | 39 | // Compare relevant fields 40 | expect(membersRsp.length).toEqual(members.length); 41 | expect(membersRsp[0]).toEqual(members[0]); 42 | }); 43 | }); -------------------------------------------------------------------------------- /frontend/src/app/types/diagnostics.types.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for the diagnostic response from backend 2 | export interface OctokitTestResult { 3 | success: boolean; 4 | appName?: string; 5 | appOwner?: string; 6 | permissions?: Record; 7 | error?: string; 8 | } 9 | 10 | export interface InstallationDiagnostic { 11 | index: number; 12 | installationId: number; 13 | accountLogin: string; 14 | accountId: string | number; 15 | accountType: string; 16 | accountAvatarUrl: string; 17 | appId: number; 18 | appSlug: string; 19 | targetType: string; 20 | permissions: Record; 21 | events: string[]; 22 | createdAt: string; 23 | updatedAt: string; 24 | suspendedAt: string | null; 25 | suspendedBy: { login: string; id: number } | null; 26 | hasOctokit: boolean; 27 | octokitTest: OctokitTestResult | null; 28 | isValid: boolean; 29 | validationErrors: string[]; 30 | } 31 | 32 | export interface AppInfo { 33 | name: string; 34 | description: string; 35 | owner: string; 36 | htmlUrl: string; 37 | permissions: Record; 38 | events: string[]; 39 | } 40 | 41 | export interface DiagnosticsResponse { 42 | timestamp: string; 43 | appConnected: boolean; 44 | totalInstallations: number; 45 | installations: InstallationDiagnostic[]; 46 | errors: string[]; 47 | appInfo: AppInfo | null; 48 | summary: { 49 | validInstallations: number; 50 | invalidInstallations: number; 51 | organizationNames: string[]; 52 | accountTypes: Record; 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-value/value.component.html: -------------------------------------------------------------------------------- 1 |
2 | 8 |
9 | 10 | 11 | Adoption 12 | 13 | 19 | 20 | 21 | 22 | 23 | Daily Activity 24 | 25 | 26 | 27 | 28 | 29 | Weekly Time Saved 30 | 31 | 32 | 33 |
34 |
-------------------------------------------------------------------------------- /backend/src/controllers/adoption.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import SeatsService from '../services/seats.service.js'; 3 | import adoptionService from '../services/adoption.service.js'; 4 | 5 | class AdoptionController { 6 | async getAdoptions(req: Request, res: Response): Promise { 7 | const { enterprise, org, team, since, until, seats } = req.query as { [key: string]: string | undefined };; 8 | try { 9 | const dateFilter = { 10 | ...(since && { $gte: new Date(since) }), 11 | ...(until && { $lte: new Date(until) }) 12 | }; 13 | 14 | const query = { 15 | filter: { 16 | ...enterprise || !org && !team ? { enterprise: 'enterprise' } : undefined, 17 | ...org ? { org } : undefined, 18 | ...team ? { team } : undefined, 19 | ...(Object.keys(dateFilter).length && { date: dateFilter }), 20 | }, 21 | projection: { 22 | ...seats === '1' ? {} : { seats: 0 }, 23 | _id: 0, 24 | __v: 0, 25 | } 26 | } 27 | const adoptions = await adoptionService.getAllAdoptions2(query); 28 | res.status(200).json(adoptions); 29 | } catch (error) { 30 | res.status(500).json(error); 31 | } 32 | } 33 | 34 | async getAdoptionTotals(req: Request, res: Response): Promise { 35 | try { 36 | const totals = await SeatsService.getMembersActivityTotals2(req.query); 37 | res.status(200).json(totals); 38 | } catch (error) { 39 | res.status(500).json(error); 40 | } 41 | } 42 | } 43 | 44 | export default new AdoptionController(); -------------------------------------------------------------------------------- /frontend/src/app/services/api/settings.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { serverUrl } from '../server.service'; 4 | 5 | export interface Settings { 6 | devCostPerYear?: number | null; 7 | developerCount?: number | null; 8 | hoursPerYear?: number | null; 9 | percentCoding?: number | null; 10 | percentTimeSaved?: number | null; 11 | metricsCronExpression?: string | null; 12 | baseUrl?: string | null; 13 | webhookProxyUrl?: string | null; 14 | webhookSecret?: string | null; 15 | developerTotal?: number | null; 16 | adopterCount?: number | null; 17 | perLicenseCost?: number | null; 18 | perDevCostPerYear?: number | null; 19 | perDevHoursPerYear?: number | null; 20 | percentofHoursCoding?: number | null; 21 | [key: string]: string | number | null | undefined; 22 | } 23 | 24 | @Injectable({ 25 | providedIn: 'root' 26 | }) 27 | export class SettingsHttpService { 28 | private apiUrl = `${serverUrl}/api/settings`; 29 | 30 | constructor(private http: HttpClient) { } 31 | 32 | getAllSettings() { 33 | return this.http.get(`${this.apiUrl}`); 34 | } 35 | 36 | getSettingsByName(name: string) { 37 | return this.http.get(`${this.apiUrl}/${name}`); 38 | } 39 | 40 | createSettings(data: Settings) { 41 | return this.http.post(`${this.apiUrl}`, data); 42 | } 43 | 44 | updateSettings(data: Settings) { 45 | return this.http.put(`${this.apiUrl}`, data); 46 | } 47 | 48 | deleteSettings(name: string) { 49 | return this.http.delete(`${this.apiUrl}/${name}`); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-value-app", 3 | "dockerComposeFile": "./compose.yml", 4 | "service": "dev", 5 | "remoteUser": "node", 6 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 7 | "postCreateCommand": "cd /workspaces/${localWorkspaceFolderBasename}/frontend && npm install && npm run build && cd /workspaces/${localWorkspaceFolderBasename}/backend && npm install && npm run build && echo 'use npm run dev to start the app'", 8 | "features": { 9 | "ghcr.io/devcontainers/features/git:1": {}, 10 | "ghcr.io/devcontainers/features/github-cli:1": {}, 11 | "ghcr.io/devcontainers/features/docker-outside-of-docker": {} 12 | }, 13 | "customizations": { 14 | "vscode": { 15 | "settings": { 16 | "extensions.ignoreRecommendations": true, 17 | "gitlens.showWelcomeOnInstall": false, 18 | "gitlens.showWhatsNewAfterUpgrades": false, 19 | "mdb.showMongoDBHelpExplorer": false, 20 | "mdb.presetConnections": [ 21 | { 22 | "name": "mongo (github-value)", 23 | "connectionString": "mongodb://root:octocat@mongo:27017" 24 | } 25 | ] 26 | }, 27 | "extensions": [ 28 | "ms-vscode.vscode-typescript-next", 29 | "dbaeumer.vscode-eslint", 30 | "angular.ng-template", 31 | "sibiraj-s.vscode-scss-formatter", 32 | "ms-azuretools.vscode-docker", 33 | "Github.vscode-github-actions", 34 | "GitHub.copilot", 35 | "GitHub.copilot-chat", 36 | "GitHub.vscode-pull-request-github", 37 | "mongodb.mongodb-vscode" 38 | ] 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /backend/src/__tests__/__mock__/seats-gen/seatsExampleTest.json: -------------------------------------------------------------------------------- 1 | { 2 | "total_seats": 1, 3 | "seats": [ 4 | { 5 | "created_at": "2023-08-28T19:50:42-04:00", 6 | "assignee": { 7 | "login": "nathos", 8 | "id": 4215, 9 | "node_id": "MDQ6VXNlcjQyMTU=", 10 | "avatar_url": "https://avatars.githubusercontent.com/u/4215?v=4", 11 | "gravatar_id": "", 12 | "url": "https://api.github.com/users/nathos", 13 | "html_url": "https://github.com/nathos", 14 | "followers_url": "https://api.github.com/users/nathos/followers", 15 | "following_url": "https://api.github.com/users/nathos/following{/other_user}", 16 | "gists_url": "https://api.github.com/users/nathos/gists{/gist_id}", 17 | "starred_url": "https://api.github.com/users/nathos/starred{/owner}{/repo}", 18 | "subscriptions_url": "https://api.github.com/users/nathos/subscriptions", 19 | "organizations_url": "https://api.github.com/users/nathos/orgs", 20 | "repos_url": "https://api.github.com/users/nathos/repos", 21 | "events_url": "https://api.github.com/users/nathos/events{/privacy}", 22 | "received_events_url": "https://api.github.com/users/nathos/received_events", 23 | "type": "User", 24 | "user_view_type": "public", 25 | "site_admin": true 26 | }, 27 | "pending_cancellation_date": null, 28 | "plan_type": "enterprise", 29 | "updated_at": "2024-01-31T19:00:00-05:00", 30 | "last_activity_at": "2024-07-22T10:00:00-04:00", 31 | "last_activity_editor": "copilot-chat-platform" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /frontend/src/app/shared/date-range-select/date-range-select.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Date Range 4 | 5 | Current Week 6 | Last Week 7 | Current Month 8 | Last Month 9 | Last 7 Days 10 | Last 30 Days 11 | Last 90 Days 12 | Last Year 13 | Custom Range 14 | 15 | 16 | 17 | 18 | Choose a date range 19 | 20 | 21 | 22 | 23 | 24 | 25 | 30 | 31 |
-------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | # This file is automatically populated during the setup process 2 | # but you can optionally fill it out yourself 3 | 4 | # Database configuration 5 | # This will be requested during the setup process but you can set it manually 6 | MONGODB_URI=mongodb://localhost:27017/github-value 7 | 8 | # Proxy Trust Configuration 9 | # Controls how Express handles X-Forwarded-For headers for rate limiting and IP detection 10 | # Options: 11 | # - 'true': Trust all proxies (least secure, use only in development) 12 | # - 'false': Don't trust any proxies (default for development) 13 | # - '1': Trust first proxy hop (recommended for production behind single load balancer) 14 | # - '2': Trust first 2 proxy hops (for multiple proxy layers) 15 | # - '10.0.0.1,172.16.0.0/12': Comma-separated trusted proxy IPs/ranges (most secure) 16 | # If not set, defaults to '1' in production, 'false' in development 17 | # TRUST_PROXY=1 18 | 19 | # GitHub App configuration 20 | # This will be automatically populated during the setup process but you can set it manually 21 | # GITHUB_WEBHOOK_SECRET= 22 | # GITHUB_APP_ID= 23 | # GITHUB_APP_PRIVATE_KEY='-----BEGIN RSA PRIVATE KEY----- 24 | # ... 25 | # -----END RSA PRIVATE KEY-----' 26 | 27 | # Base URL so we can generate links in GitHub PR comments 28 | # BASE_URL=http://localhost:4200 29 | 30 | # Webhook proxy URL (optional) 31 | # This is only needed if you don't want to use smee.io 32 | # WEBHOOK_PROXY_URL=https://5950-2601-589-4885-e850-b1eb-a754-b856-6038.ngrok-free.app 33 | # 34 | 35 | # Log rotation settings 36 | # In observing organizations with large amounts of activity, the debug logs can 37 | # grow quite large, so having flexibility about how long to store those helps. 38 | # LOG_ROTATION_PERIOD=1d 39 | # LOG_ROTATION_COUNT=14 40 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-value-frontend", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test", 10 | "docker-build": "docker build -t github-value-frontend .", 11 | "docker-start": "docker run -p 80:80 github-value-frontend", 12 | "lint": "ng lint" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/animations": "^19.2.5", 17 | "@angular/cdk": "^19.2.8", 18 | "@angular/common": "^19.2.5", 19 | "@angular/compiler": "^19.2.5", 20 | "@angular/core": "^19.2.5", 21 | "@angular/forms": "^19.2.5", 22 | "@angular/material": "^19.2.8", 23 | "@angular/platform-browser": "^19.2.5", 24 | "@angular/platform-browser-dynamic": "^19.2.5", 25 | "@angular/router": "^19.2.5", 26 | "@octokit/types": "^13.10.0", 27 | "canvas-confetti": "^1.9.3", 28 | "cronstrue": "^2.57.0", 29 | "dayjs": "^1.11.13", 30 | "highcharts": "^11.4.8", 31 | "highcharts-angular": "^4.0.1", 32 | "rxjs": "~7.8.0" 33 | }, 34 | "devDependencies": { 35 | "@angular-devkit/build-angular": "^19.2.6", 36 | "@angular-eslint/builder": "^19.3.0", 37 | "@angular/cli": "^19.2.6", 38 | "@angular/compiler-cli": "^19.2.5", 39 | "@types/canvas-confetti": "^1.9.0", 40 | "@types/jasmine": "~5.1.7", 41 | "angular-eslint": "19.3.0", 42 | "eslint": "9.23", 43 | "jasmine-core": "~5.6.0", 44 | "karma": "~6.4.0", 45 | "karma-chrome-launcher": "~3.2.0", 46 | "karma-coverage": "~2.2.0", 47 | "karma-jasmine": "~5.1.0", 48 | "karma-jasmine-html-reporter": "~2.1.0", 49 | "typescript": "~5.8.2", 50 | "typescript-eslint": "8.28.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /backend/src/controllers/settings.controller.ts: -------------------------------------------------------------------------------- 1 | import app from '../index.js'; 2 | import { Request, Response } from 'express'; 3 | 4 | class SettingsController { 5 | async getAllSettings(req: Request, res: Response) { 6 | try { 7 | const settings = await app.settingsService.getAllSettings(); 8 | if (!settings) { 9 | res.status(404).json({ error: 'Settings not found' }); 10 | } 11 | res.json(settings); 12 | } catch (error) { 13 | res.status(500).json(error); 14 | } 15 | } 16 | 17 | async getSettingsByName(req: Request, res: Response) { 18 | try { 19 | const { name } = req.params; 20 | const settings = await app.settingsService.getSettingsByName(name); 21 | if (settings) { 22 | res.json(settings); 23 | } else { 24 | res.status(404).json({ error: 'Settings not found' }); 25 | } 26 | } catch (error) { 27 | res.status(500).json(error); 28 | } 29 | } 30 | 31 | async createSettings(req: Request, res: Response) { 32 | try { 33 | const newSettings = await app.settingsService.updateSettings(req.body); 34 | res.status(201).json(newSettings); 35 | } catch (error) { 36 | res.status(500).json(error); 37 | } 38 | } 39 | 40 | async updateSettings(req: Request, res: Response) { 41 | try { 42 | await app.settingsService.updateSettings(req.body); 43 | res.status(200).end(); 44 | } catch (error) { 45 | res.status(500).json(error); 46 | } 47 | } 48 | 49 | async deleteSettings(req: Request, res: Response) { 50 | try { 51 | const { name } = req.params; 52 | await app.settingsService.deleteSettings(name); 53 | res.status(200).end(); 54 | } catch (error) { 55 | res.status(500).json(error); 56 | } 57 | } 58 | } 59 | 60 | export default new SettingsController(); -------------------------------------------------------------------------------- /frontend/src/app/guards/setup.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, GuardResult, MaybeAsync, Router } from '@angular/router'; 3 | import { of } from 'rxjs'; 4 | import { catchError, map } from 'rxjs/operators'; 5 | import { InstallationsService, statusResponse } from '../services/api/installations.service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class SetupStatusGuard implements CanActivate { 11 | responseCache?: statusResponse; 12 | 13 | constructor( 14 | private installationsService: InstallationsService, 15 | private router: Router 16 | ) {} 17 | 18 | canActivate(): MaybeAsync { 19 | if (this.responseCache?.isSetup === true) return of(true); 20 | return this.installationsService.refreshStatus().pipe( 21 | map((response) => { 22 | this.responseCache = response; 23 | if (!response.dbConnected) { 24 | this.router.navigate(['/setup/db']); 25 | return false; 26 | } 27 | if (!response.isSetup) { 28 | this.router.navigate(['/setup/db']); 29 | return false; 30 | } 31 | // if (!response.installations?.some(i => Object.values(i).some(j => !j)) && !isDevMode()) { 32 | // this.router.navigate(['/setup/loading']); 33 | // return false; 34 | // } 35 | return true; 36 | }), 37 | catchError((error) => { 38 | const serializedError = { 39 | message: error.message || 'An unknown error occurred', 40 | code: error.code || 'UNKNOWN', 41 | status: error.status || 500 42 | }; 43 | this.router.navigate(['/error'], { state: { error: serializedError } }); 44 | return of(false); 45 | }) 46 | ); 47 | } 48 | 49 | canActivateChild() { 50 | return this.canActivate(); 51 | } 52 | } -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/dashboard-card/dashboard-card-bars/dashboard-card-bars.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, Input, OnChanges } from '@angular/core'; 3 | import { MatCardModule } from '@angular/material/card'; 4 | import { MatIconModule } from '@angular/material/icon'; 5 | import { MatProgressBarModule } from '@angular/material/progress-bar'; 6 | import { CopilotMetrics } from '../../../../../services/api/metrics.service.interfaces'; 7 | import { HighchartsService } from '../../../../../services/highcharts.service'; 8 | 9 | export interface DashboardCardBarsInput { 10 | value: number; 11 | maxValue: number; 12 | icon: string; 13 | name: string; 14 | percentage?: number; 15 | }; 16 | 17 | @Component({ 18 | selector: 'app-dashboard-card-bars', 19 | standalone: true, 20 | imports: [ 21 | MatCardModule, 22 | MatIconModule, 23 | CommonModule, 24 | MatProgressBarModule, 25 | MatIconModule 26 | ], 27 | templateUrl: './dashboard-card-bars.component.html', 28 | styleUrls: [ 29 | './dashboard-card-bars.component.scss', 30 | '../dashboard-card.scss' 31 | ] 32 | }) 33 | export class DashboardCardBarsComponent implements OnChanges { 34 | @Input() data?: CopilotMetrics; 35 | @Input() totalSeats?: number; 36 | sections?: DashboardCardBarsInput[]; 37 | percentages: number[] = [] 38 | Math = Math; 39 | 40 | constructor( 41 | private highchartsService: HighchartsService 42 | ) {} 43 | 44 | ngOnChanges() { 45 | if (this.data && this.totalSeats) { 46 | this.sections = this.highchartsService.transformCopilotMetricsToBars(this.data, this.totalSeats); 47 | if (this.sections) { 48 | this.sections.forEach((row) => { 49 | row.percentage = (row.value / (row.maxValue || 100)) * 100; 50 | }) 51 | } 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-value-backend", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "Demonstrate the value of GitHub", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "start": "node --enable-source-maps dist/index.js | bunyan -o short -l info", 9 | "test": "vitest", 10 | "build": "tsc", 11 | "dev": "tsx watch src/index.ts | bunyan -o short -l info", 12 | "lint": "eslint src/**/*.ts", 13 | "compose:start": "docker-compose -f ../compose.yml up -d", 14 | "db:start": "docker-compose -f ../compose.yml up -d mongo", 15 | "dotenv": "cp -n .env.example .env || true" 16 | }, 17 | "dependencies": { 18 | "@octokit/core": "^6.1.4", 19 | "bunyan": "^1.8.15", 20 | "cors": "^2.8.5", 21 | "cron": "^4.1.3", 22 | "date-fns": "^4.1.0", 23 | "dayjs": "^1.11.13", 24 | "dotenv": "^16.4.7", 25 | "eventsource": "^3.0.6", 26 | "express": "^4.21.2", 27 | "express-mongo-sanitize": "^2.2.0", 28 | "express-rate-limit": "^7.5.0", 29 | "mongoose": "^8.13.2", 30 | "mysql2": "^3.14.0", 31 | "octokit": "^4.1.2", 32 | "smee-client": "^3.1.1", 33 | "update-dotenv": "^1.1.1", 34 | "validator": "^13.15.0", 35 | "why-is-node-running": "^3.2.2" 36 | }, 37 | "devDependencies": { 38 | "@eslint/js": "^9.26.0", 39 | "@octokit/types": "^14.0.0", 40 | "@types/bunyan": "^1.8.11", 41 | "@types/cors": "^2.8.18", 42 | "@types/eventsource": "^3.0.0", 43 | "@types/express": "^4.17.21", 44 | "@types/node": "^22.15.17", 45 | "@types/validator": "^13.15.0", 46 | "eslint": "9.26", 47 | "globals": "^16.1.0", 48 | "ts-node": "^10.9.2", 49 | "tsx": "^4.19.4", 50 | "typescript": "^5.8.3", 51 | "typescript-eslint": "^8.32.1", 52 | "vitest": "^3.1.3" 53 | }, 54 | "engines": { 55 | "node": ">=18.0.0" 56 | }, 57 | "license": "MIT" 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, 4 | # surfacing known-vulnerable versions of the packages declared or updated in the PR. 5 | # Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable 6 | # packages will be blocked from merging. 7 | # 8 | # Source repository: https://github.com/actions/dependency-review-action 9 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 10 | name: 'Dependency review' 11 | on: 12 | pull_request: 13 | branches: [ "main" ] 14 | 15 | # If using a dependency submission action in this workflow this permission will need to be set to: 16 | # 17 | # permissions: 18 | # contents: write 19 | # 20 | # https://docs.github.com/en/enterprise-cloud@latest/code-security/supply-chain-security/understanding-your-software-supply-chain/using-the-dependency-submission-api 21 | permissions: 22 | contents: read 23 | # Write permissions for pull-requests are required for using the `comment-summary-in-pr` option, comment out if you aren't using this option 24 | pull-requests: write 25 | 26 | jobs: 27 | dependency-review: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: 'Checkout repository' 31 | uses: actions/checkout@v4 32 | - name: 'Dependency Review' 33 | uses: actions/dependency-review-action@v4 34 | # Commonly enabled options, see https://github.com/actions/dependency-review-action#configuration-options for all available options. 35 | with: 36 | comment-summary-in-pr: always 37 | # fail-on-severity: moderate 38 | # deny-licenses: GPL-1.0-or-later, LGPL-2.0-or-later 39 | # retry-on-snapshot-warnings: true 40 | -------------------------------------------------------------------------------- /frontend/src/app/services/api/copilot-survey.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { serverUrl } from '../server.service'; 4 | 5 | export interface Survey { 6 | id?: number; 7 | org: string; 8 | repo: string; 9 | prNumber: number; 10 | status?: 'pending' | 'completed'; 11 | hits?: number; 12 | userId: string; 13 | usedCopilot: boolean; 14 | percentTimeSaved: number; 15 | timeUsedFor: string; 16 | reason: string; 17 | createdAt?: Date; 18 | updatedAt?: Date; 19 | kudos?: number; // Add kudos property 20 | } 21 | 22 | @Injectable({ 23 | providedIn: 'root' 24 | }) 25 | export class CopilotSurveyService { 26 | private apiUrl = `${serverUrl}/api/survey`; 27 | 28 | constructor(private http: HttpClient) { } 29 | 30 | createSurvey(survey: Survey) { 31 | return this.http.post(this.apiUrl, survey); 32 | } 33 | 34 | createSurveyGitHub(survey: Survey) { 35 | return this.http.post(`${this.apiUrl}/${survey.id}/github`, survey); 36 | } 37 | 38 | getAllSurveys(params?: { 39 | org?: string; 40 | team?: string; 41 | reasonLength?: number; 42 | since?: string; 43 | until?: string; 44 | status?: 'pending' | 'completed'; 45 | userId?: string; 46 | }) { 47 | if (!params?.org) delete params?.org; 48 | return this.http.get(this.apiUrl, { 49 | params 50 | }); 51 | } 52 | 53 | getSurveyById(id: number) { 54 | return this.http.get(`${this.apiUrl}/${id}`); 55 | } 56 | 57 | getSurveysByUserId(userId: string) { 58 | return this.getAllSurveys({ userId: userId.toString() }); 59 | } 60 | 61 | deleteSurvey(id: number) { 62 | return this.http.delete(`${this.apiUrl}/${id}`); 63 | } 64 | 65 | updateSurvey(survey: Partial) { 66 | return this.http.put(`${this.apiUrl}/${survey.id}`, survey); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /.github/workflows/backend.yml: -------------------------------------------------------------------------------- 1 | name: Backend 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: '20' 19 | - run: npm ci 20 | working-directory: ./backend 21 | - run: npm run build 22 | working-directory: ./backend 23 | - uses: actions/upload-artifact@v4 24 | with: 25 | name: backend 26 | path: ./backend/dist 27 | 28 | run: 29 | runs-on: ubuntu-latest 30 | needs: build 31 | services: 32 | mongodb: 33 | image: mongo:latest 34 | ports: 35 | - 27017:27017 36 | options: >- 37 | --health-cmd "mongosh --eval 'db.adminCommand(\"ping\")'" 38 | --health-interval 5s 39 | --health-timeout 5s 40 | --health-retries 10 41 | env: 42 | MONGODB_URI: mongodb://localhost:27017 43 | PORT: 3000 44 | GITHUB_APP_ID: ${{ secrets.APP_ID }} 45 | GITHUB_APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} 46 | GITHUB_WEBHOOK_SECRET: ${{ secrets.WEBHOOK_SECRET }} 47 | strategy: 48 | matrix: 49 | node-version: ['18', '20', '22'] 50 | name: Node v${{ matrix.node-version }} 51 | steps: 52 | - uses: actions/checkout@v4 53 | - uses: actions/setup-node@v4 54 | with: 55 | node-version: ${{ matrix.node-version }} 56 | - run: npm ci 57 | working-directory: ./backend 58 | - uses: actions/download-artifact@v4 59 | with: 60 | name: backend 61 | path: ./backend/dist 62 | - run: | 63 | npm start & 64 | sleep 30 65 | kill $! 66 | working-directory: ./backend 67 | timeout-minutes: 1 68 | -------------------------------------------------------------------------------- /frontend/src/app/database/database.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |

Enter your database credentials

6 |
7 | Database 8 | 9 | Connection String URI 10 | 12 | 13 |
14 |
15 |
16 | 22 |
23 |
24 |
25 |
26 | 27 |
28 | 29 |
30 |
31 | 32 |

All Setup!

33 |
34 | 35 | 36 | 37 | table 38 | 39 | 40 | 41 | 42 | 43 | rocket_launch 44 | 45 |
46 |
-------------------------------------------------------------------------------- /frontend/src/app/services/api/members.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { serverUrl } from '../server.service'; 3 | import { HttpClient } from '@angular/common/http'; 4 | import { Endpoints } from '@octokit/types'; 5 | import { catchError, Observable } from 'rxjs'; 6 | import { throwError } from 'rxjs'; 7 | 8 | export interface Member { 9 | org: string; 10 | login: string; 11 | id: number; 12 | node_id?: string; 13 | avatar_url?: string; 14 | gravatar_id?: string; 15 | url?: string; 16 | html_url?: string; 17 | followers_url?: string; 18 | following_url?: string; 19 | gists_url?: string; 20 | starred_url?: string; 21 | subscriptions_url?: string; 22 | organizations_url?: string; 23 | repos_url?: string; 24 | events_url?: string; 25 | received_events_url?: string; 26 | type?: string; 27 | site_admin?: boolean; 28 | name?: string; 29 | email?: string; 30 | starred_at?: string; 31 | user_view_type?: string; 32 | } 33 | 34 | @Injectable({ 35 | providedIn: 'root' 36 | }) 37 | export class MembersService { 38 | private apiUrl = `${serverUrl}/api/members`; 39 | 40 | constructor(private http: HttpClient) { } 41 | 42 | getAllMembers(org?: string) { 43 | return this.http.get(`${this.apiUrl}`, { 44 | params: org ? { org } : undefined 45 | }); 46 | } 47 | 48 | getMemberByLogin(login: string, exact = true) { 49 | return this.http.get( 50 | `${this.apiUrl}/${login}`, 51 | { params: { exact: String(exact) } } // make this boolean not string. 52 | ).pipe( 53 | catchError(error => { 54 | return throwError(() => error); 55 | }) 56 | ); 57 | } 58 | 59 | searchMembersByLogin(query: string): Observable { 60 | return this.http.get(`${serverUrl}/api/members/search`, { params: { query } }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /backend/src/__tests__/services/seatServiceInsertStandalone.ts: -------------------------------------------------------------------------------- 1 | // seatServiceInsertStandalone.ts run via "npx tsx src/__tests__/services/seatServiceInsertStandalone.ts" 2 | import SeatService from '../../services/seats.service.js'; 3 | import { generateStatefulMetrics } from '../__mock__/seats-gen/runSeatsGenerator.js'; 4 | import Database from '../../database.js'; 5 | import 'dotenv/config'; 6 | 7 | if (!process.env.MONGODB_URI) throw new Error('MONGODB_URI is not defined'); 8 | const database = new Database(process.env.MONGODB_URI); 9 | 10 | async function runTest() { 11 | try { 12 | await database.connect(); 13 | 14 | // Test data setup 15 | const org = 'octodemo'; 16 | const queryAt = new Date(); 17 | const seats = generateStatefulMetrics(); 18 | 19 | 20 | //loop through each seat and if the last_activity_at is "false" print the seat object to console. 21 | seats.seats.forEach((seat: any) => { 22 | if (seat.last_activity_at === "false") { 23 | console.log("a false last activity was found", seat); 24 | } 25 | }); 26 | // Perform the insertion 27 | await SeatService.insertSeats(org, queryAt, seats.seats); 28 | 29 | // Verify the insertion 30 | const insertedSeats = await SeatService.getAllSeats(org); 31 | 32 | // Assertions 33 | //console.log('Inserted Seats:', insertedSeats); 34 | if (!insertedSeats || insertedSeats.length === 0) { 35 | throw new Error('No seats were inserted.'); 36 | } 37 | if (insertedSeats[0].org !== org) { 38 | console.log('Received org:', insertedSeats[0].org); 39 | throw new Error('Organization mismatch.'); 40 | } 41 | if (insertedSeats[0].queryAt.getTime() !== queryAt.getTime()) { 42 | throw new Error('QueryAt mismatch.'); 43 | } 44 | 45 | console.log('Test passed successfully.'); 46 | } catch (error) { 47 | console.error('Test failed:', error); 48 | } finally { 49 | await database.disconnect(); 50 | } 51 | } 52 | 53 | runTest(); 54 | -------------------------------------------------------------------------------- /.github/workflows/anchore.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow checks out code, builds an image, performs a container image 7 | # vulnerability scan with Anchore's Grype tool, and integrates the results with GitHub Advanced Security 8 | # code scanning feature. For more information on the Anchore scan action usage 9 | # and parameters, see https://github.com/anchore/scan-action. For more 10 | # information on Anchore's container image scanning tool Grype, see 11 | # https://github.com/anchore/grype 12 | name: Anchore Grype vulnerability scan 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '24 15 * * 5' 22 | 23 | permissions: 24 | contents: read 25 | 26 | jobs: 27 | Anchore-Build-Scan: 28 | permissions: 29 | contents: read # for actions/checkout to fetch code 30 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results 31 | actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Check out the code 35 | uses: actions/checkout@v4 36 | - name: Build the Container image 37 | run: docker build . --file Dockerfile --tag localbuild/testimage:latest 38 | - name: Run the Anchore Grype scan action 39 | uses: anchore/scan-action@v6 40 | id: scan 41 | continue-on-error: true 42 | with: 43 | image: "localbuild/testimage:latest" 44 | fail-build: true 45 | severity-cutoff: critical 46 | - name: Upload vulnerability report 47 | uses: github/codeql-action/upload-sarif@v3 48 | with: 49 | sarif_file: ${{ steps.scan.outputs.sarif }} 50 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker Deploy GHCR 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '*' 9 | workflow_call: 10 | inputs: 11 | artifact-name: 12 | default: "dist" 13 | required: false 14 | type: string 15 | outputs: 16 | tags: 17 | description: "The first output string" 18 | value: ${{ jobs.docker-ghcr.outputs.tags }} 19 | labels: 20 | description: "The first output string" 21 | value: ${{ jobs.docker-ghcr.outputs.labels }} 22 | 23 | jobs: 24 | docker-ghcr: 25 | outputs: 26 | tags: ${{ steps.meta.outputs.tags }} 27 | labels: ${{ steps.meta.outputs.labels }} 28 | environment: 29 | name: GitHub Container Registry 30 | url: https://github.com/${{ github.event.repository.owner.login }}/${{ github.event.repository.name }}/pkgs/container/${{ github.event.repository.name }} 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: docker/setup-qemu-action@v3 34 | - uses: docker/setup-buildx-action@v3 35 | - uses: docker/login-action@v3 36 | with: 37 | registry: ghcr.io 38 | username: ${{ github.repository_owner }} 39 | password: ${{ secrets.GITHUB_TOKEN }} 40 | - uses: actions/checkout@v4 41 | - uses: docker/metadata-action@v5 42 | id: meta 43 | with: 44 | tags: | 45 | type=schedule 46 | type=ref,event=branch 47 | type=ref,event=tag 48 | type=ref,event=pr 49 | type=sha,format=long 50 | images: ghcr.io/${{ github.event.repository.owner.login }}/${{ github.event.repository.name }} 51 | - uses: docker/build-push-action@v6 52 | with: 53 | context: . 54 | push: ${{ github.event_name != 'pull_request' }} 55 | tags: ${{ steps.meta.outputs.tags }} 56 | labels: ${{ steps.meta.outputs.labels }} 57 | cache-from: type=gha 58 | cache-to: type=gha,mode=max -------------------------------------------------------------------------------- /backend/src/controllers/teams.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import teamsService from '../services/teams.service.js'; 3 | 4 | class TeamsController { 5 | async getAllTeams(req: Request, res: Response): Promise { 6 | try { 7 | const { org } = req.query; 8 | if (org && typeof org !== 'string') { 9 | res.status(400).json({ message: 'Invalid org parameter' }); 10 | return; 11 | } 12 | const teams = teamsService.getTeams(org); 13 | res.json(teams); 14 | } catch (error) { 15 | res.status(500).json(error); 16 | } 17 | } 18 | 19 | async getAllMembers(req: Request, res: Response): Promise { 20 | try { 21 | const { org } = req.query; 22 | if (org && typeof org !== 'string') { 23 | res.status(400).json({ message: 'Invalid org parameter' }); 24 | return; 25 | } 26 | const members = await teamsService.getAllMembers(org); 27 | res.json(members); 28 | } catch (error) { 29 | res.status(500).json(error); 30 | } 31 | } 32 | 33 | async getMemberByLogin(req: Request, res: Response): Promise { 34 | try { 35 | const { login } = req.params; 36 | const member = await teamsService.getMemberByLogin(login); 37 | if (member) { 38 | res.json(member); 39 | } else { 40 | res.status(404).json({ message: 'User not found' }); 41 | } 42 | } catch (error) { 43 | res.status(500).json(error); 44 | } 45 | } 46 | 47 | async searchMembersByLogin(req: Request, res: Response): Promise { 48 | try { 49 | const { query } = req.query; 50 | if (!query || typeof query !== 'string') { 51 | res.status(400).json({ message: 'Invalid query parameter' }); 52 | return; 53 | } 54 | const members = await teamsService.searchMembersByLogin(query); 55 | res.json(members); 56 | } catch (error) { 57 | res.status(500).json(error); 58 | } 59 | } 60 | } 61 | 62 | export default new TeamsController(); -------------------------------------------------------------------------------- /backend/src/__tests__/__mock__/types.ts: -------------------------------------------------------------------------------- 1 | // src/types.ts 2 | export type TrendType = 'grow' | 'stable' | 'fixed' | 'decline'; 3 | 4 | export interface MetricConfig { 5 | baseValue: number; 6 | range: { 7 | min: number; 8 | max: number; 9 | }; 10 | trend: TrendType; 11 | growthRate?: number; // For 'grow' and 'decline' trends 12 | volatility?: number; // For 'stable' trend, controls random variation 13 | } 14 | 15 | export interface MetricsConfig { 16 | total_active_users: MetricConfig; 17 | total_engaged_users: MetricConfig; 18 | code_suggestions: MetricConfig; 19 | code_acceptances: MetricConfig; 20 | code_lines_suggested: MetricConfig; 21 | code_lines_accepted: MetricConfig; 22 | chats: MetricConfig; 23 | chat_insertions: MetricConfig; 24 | chat_copies: MetricConfig; 25 | pr_summaries: MetricConfig; 26 | total_code_reviews: MetricConfig; 27 | total_code_review_comments: MetricConfig; 28 | } 29 | 30 | export interface MockConfig { 31 | startDate: Date; 32 | endDate: Date; 33 | updateFrequency: 'daily' | 'weekly' | 'monthly'; 34 | metrics: MetricsConfig; 35 | models: Array<{ 36 | name: string; 37 | is_custom_model: boolean; 38 | custom_model_training_date: string | null; 39 | }>; 40 | languages: string[]; 41 | editors: string[]; 42 | repositories: string[]; 43 | } 44 | 45 | export type UsagePattern = 'light' | 'heavy-but-siloed' | 'moderate' | 'heavy'; 46 | 47 | export interface SeatsMockConfig { 48 | startDate: Date; 49 | endDate: Date; 50 | usagePattern: UsagePattern; 51 | heavyUsers: string[]; // List of user logins who are heavy users 52 | specificUser: string; // Specific user to generate data for 53 | editors: string[]; // List of possible editors 54 | } 55 | 56 | export interface AssigneeActivity { 57 | login: string; 58 | lastActivityAt: Date; 59 | } 60 | 61 | export interface SurveyMockConfig { 62 | startDate: Date; 63 | endDate: Date; 64 | userIds: string[]; 65 | orgs: string[]; 66 | repos: string[]; 67 | reasons: string[]; 68 | timeUsedFors: string[]; 69 | } 70 | 71 | -------------------------------------------------------------------------------- /frontend/src/app/services/api/setup.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { serverUrl } from '../server.service'; 4 | import { Endpoints } from '@octokit/types'; 5 | import { BehaviorSubject } from 'rxjs'; 6 | import { DiagnosticsResponse } from '../../types/diagnostics.types'; 7 | 8 | export interface InstallationStatus { 9 | installation?: Endpoints["GET /app/installations"]["response"]["data"][number], 10 | usage: boolean; 11 | metrics: boolean; 12 | copilotSeats: boolean; 13 | teamsAndMembers: boolean; 14 | } 15 | 16 | @Injectable({ 17 | providedIn: 'root' 18 | }) 19 | export class SetupService { 20 | private apiUrl = `${serverUrl}/api/setup`; 21 | installations = new BehaviorSubject([]); 22 | 23 | constructor(private http: HttpClient) { } 24 | 25 | getManifest() { 26 | return this.http.get<{ 27 | name: string; 28 | description: string; 29 | url: string; 30 | hook_attributes: { 31 | url: string; 32 | }; 33 | setup_url: string; 34 | redirect_url: string; 35 | public: boolean; 36 | default_permissions: { 37 | members: 'read'; 38 | metadata: 'read'; 39 | organization_copilot_seat_management: 'read'; 40 | pull_requests: 'write'; 41 | }; 42 | default_events: ('pull_request')[]; 43 | }>( 44 | `${this.apiUrl}/manifest` 45 | ); 46 | } 47 | 48 | addExistingApp(request: { 49 | appId: string, 50 | privateKey: string, 51 | webhookSecret: string 52 | }) { 53 | return this.http.post<{ 54 | installUrl: string 55 | }>(`${this.apiUrl}/existing-app`, request); 56 | } 57 | 58 | setupDB(request: { // should be url or fields 59 | uri: string; 60 | }) { 61 | return this.http.post(`${this.apiUrl}/db`, request); 62 | } 63 | 64 | validateInstallations() { 65 | return this.http.get(`${this.apiUrl}/validate-installations`); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /backend/src/services/logger.ts: -------------------------------------------------------------------------------- 1 | import bunyan from 'bunyan'; 2 | import { existsSync, mkdirSync, readFileSync } from 'fs'; 3 | import { Request, Response, NextFunction } from 'express'; 4 | import path, { dirname } from 'path'; 5 | import { fileURLToPath } from 'url'; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = dirname(__filename); 9 | const logsDir = path.resolve(__dirname, '../../logs'); 10 | 11 | if (!existsSync(logsDir)) { 12 | mkdirSync(logsDir, { recursive: true }); 13 | } 14 | 15 | const packageJsonPath = path.resolve(__dirname, '../../package.json'); 16 | const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); 17 | export const appName = packageJson.name || 'GitHub Value'; 18 | 19 | const period = process.env.LOG_ROTATION_PERIOD || '1d'; 20 | const count = process.env.LOG_ROTATION_COUNT ? parseInt(process.env.LOG_ROTATION_COUNT) : 14; 21 | 22 | const logger = bunyan.createLogger({ 23 | name: appName, 24 | level: 'debug', 25 | serializers: { 26 | ...bunyan.stdSerializers, 27 | req: (req: Request) => ({ 28 | method: req.method, 29 | url: req.url, 30 | remoteAddress: req.connection.remoteAddress, 31 | remotePort: req.connection.remotePort 32 | }), 33 | res: (res: Response) => ({ 34 | statusCode: res.statusCode 35 | }), 36 | }, 37 | streams: [ 38 | { 39 | level: 'debug', 40 | stream: process.stdout 41 | }, 42 | { 43 | level: 'error', 44 | stream: process.stderr 45 | }, 46 | { 47 | path: `${logsDir}/error.json`, 48 | type: 'rotating-file', 49 | period, 50 | count, 51 | level: 'error' 52 | }, 53 | { 54 | path: `${logsDir}/debug.json`, 55 | type: 'rotating-file', 56 | period, 57 | count, 58 | level: 'debug' 59 | } 60 | ] 61 | }); 62 | 63 | export const expressLoggerMiddleware = (req: Request, res: Response, next: NextFunction) => { 64 | logger.debug(req); 65 | res.on('finish', () => logger.debug(res)); 66 | next(); 67 | }; 68 | 69 | export default logger; 70 | -------------------------------------------------------------------------------- /backend/src/models/teams.model.ts: -------------------------------------------------------------------------------- 1 | import { components } from "@octokit/openapi-types"; 2 | import { SeatType } from "./seats.model.js"; 3 | import mongoose from "mongoose"; 4 | 5 | export type TeamType = Omit & { 6 | _id?: mongoose.Types.ObjectId; 7 | org: string; 8 | team?: string; 9 | parent_id?: number | null; 10 | createdAt?: Date; 11 | updatedAt?: Date; 12 | parent?: TeamType | null; 13 | }; 14 | 15 | type MemberType = { 16 | org: string; 17 | login: string; 18 | id: number; 19 | node_id: string; 20 | avatar_url: string; 21 | gravatar_id: string | null; 22 | url: string; 23 | html_url: string; 24 | followers_url: string; 25 | following_url: string; 26 | gists_url: string; 27 | starred_url: string; 28 | subscriptions_url: string; 29 | organizations_url: string; 30 | repos_url: string; 31 | events_url: string; 32 | received_events_url: string; 33 | type: string; 34 | site_admin: boolean; 35 | name: string | null; 36 | email: string | null; 37 | starred_at?: string; 38 | user_view_type?: string; 39 | createdAt?: Date; 40 | updatedAt?: Date; 41 | activity?: SeatType[]; 42 | }; 43 | 44 | type MemberActivityType = { 45 | org: string; 46 | login: string; 47 | id: number; 48 | node_id: string; 49 | avatar_url: string; 50 | gravatar_id: string | null; 51 | url: string; 52 | html_url: string; 53 | followers_url: string; 54 | following_url: string; 55 | gists_url: string; 56 | starred_url: string; 57 | subscriptions_url: string; 58 | organizations_url: string; 59 | repos_url: string; 60 | events_url: string; 61 | received_events_url: string; 62 | type: string; 63 | site_admin: boolean; 64 | name: string | null; 65 | email: string | null; 66 | starred_at?: string; 67 | user_view_type?: string; 68 | createdAt?: Date; 69 | updatedAt?: Date; 70 | activity: SeatType[]; 71 | }; 72 | 73 | type TeamMemberAssociationType = { 74 | TeamId: number; 75 | MemberId: number; 76 | }; 77 | 78 | export { 79 | MemberType, 80 | TeamMemberAssociationType, 81 | MemberActivityType 82 | }; 83 | -------------------------------------------------------------------------------- /frontend/src/app/services/api/metrics.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { serverUrl } from '../server.service'; 4 | import { CopilotMetrics } from './metrics.service.interfaces'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class MetricsService { 10 | private apiUrl = `${serverUrl}/api/metrics`; 11 | 12 | constructor(private http: HttpClient) { } 13 | 14 | getMetrics(queryParams?: { 15 | org?: string | undefined; 16 | type?: 'none' | 'copilot_ide_code_completions' | 'copilot_ide_chat' | 'copilot_dotcom_chat' | 'copilot_dotcom_pull_requests'; 17 | since?: string; 18 | until?: string; 19 | editor?: 'vscode' | 'JetBrains' | 'Xcode' | 'Neovim' | string; 20 | language?: string; 21 | model?: 'default' | string; 22 | }) { 23 | if (!queryParams?.org) delete queryParams?.org; 24 | return this.http.get(this.apiUrl, { 25 | params: queryParams 26 | }); 27 | } 28 | 29 | getMetricsTotals(queryParams?: { 30 | org?: string; 31 | type?: 'none' | 'copilot_ide_code_completions' | 'copilot_ide_chat' | 'copilot_dotcom_chat' | 'copilot_dotcom_pull_requests'; 32 | since?: string; 33 | until?: string; 34 | editor?: 'vscode' | 'JetBrains' | 'Xcode' | 'Neovim' | string; 35 | language?: string; 36 | model?: 'default' | string; 37 | }) { 38 | if (!queryParams?.org) delete queryParams?.org; 39 | return this.http.get(`${this.apiUrl}/totals`, { 40 | params: queryParams 41 | }); 42 | } 43 | 44 | getMetricsTotalsArray(queryParams?: { 45 | org?: string; 46 | type?: 'none' | 'copilot_ide_code_completions' | 'copilot_ide_chat' | 'copilot_dotcom_chat' | 'copilot_dotcom_pull_requests'; 47 | since?: string; 48 | until?: string; 49 | editor?: 'vscode' | 'JetBrains' | 'Xcode' | 'Neovim' | string; 50 | language?: string; 51 | model?: 'default' | string; 52 | }) { 53 | if (!queryParams?.org) delete queryParams?.org; 54 | return this.http.get(`${this.apiUrl}/totals`, { 55 | params: queryParams 56 | }); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /frontend/src/app/services/api/adoption.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpParams } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { serverUrl } from '../server.service'; 5 | 6 | export interface GetAdoptionsParams { 7 | enterprise?: string; 8 | daysInactive: number; 9 | org?: string; 10 | team?: string; 11 | precision?: string; 12 | since?: string; 13 | until?: string; 14 | } 15 | //TODO remove old params 16 | 17 | export interface Adoption { 18 | _id: { 19 | $oid: string; 20 | }; 21 | date: { 22 | $date: string; 23 | }; 24 | totalSeats: number; 25 | totalActive: number; 26 | totalInactive: number; 27 | seats: { 28 | $oid: string; 29 | }[]; 30 | createdAt: { 31 | $date: string; 32 | }; 33 | updatedAt: { 34 | $date: string; 35 | }; 36 | __v: number; 37 | } 38 | 39 | @Injectable({ 40 | providedIn: 'root', 41 | }) 42 | export class AdoptionService { 43 | private apiUrl = `${serverUrl}/api/seats/activity`; 44 | 45 | constructor(private http: HttpClient) { } 46 | 47 | getAdoptions(params: GetAdoptionsParams): Observable { 48 | let httpParams = new HttpParams().set('daysInactive', params.daysInactive.toString()); 49 | 50 | if (params.enterprise) { 51 | httpParams = httpParams.set('enterprise', params.enterprise); 52 | } 53 | 54 | if (params.daysInactive) { 55 | httpParams = httpParams.set('daysInactive', params.daysInactive.toString()); 56 | } 57 | 58 | if (params.org) { 59 | httpParams = httpParams.set('org', params.org); 60 | } 61 | if (params.team) { 62 | httpParams = httpParams.set('team', params.team); 63 | } 64 | 65 | if (params.precision) { 66 | httpParams = httpParams.set('precision', params.precision); 67 | } 68 | if (params.since) { 69 | httpParams = httpParams.set('since', params.since); 70 | } 71 | if (params.until) { 72 | httpParams = httpParams.set('until', params.until); 73 | } 74 | 75 | const adoptionResponse = this.http.get(this.apiUrl, { params: httpParams }); 76 | 77 | return adoptionResponse; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /frontend/src/app/main/main.component.scss: -------------------------------------------------------------------------------- 1 | .sidenav-container { 2 | height: 100%; 3 | } 4 | 5 | .sidenav { 6 | width: auto; 7 | padding: 0 12px; 8 | 9 | &.full-width { 10 | width: 100vw !important; // full viewport width on mobile 11 | } 12 | } 13 | 14 | .sidenav .mat-toolbar { 15 | background: inherit; 16 | } 17 | 18 | .mat-toolbar.mat-primary { 19 | position: sticky; 20 | top: 0; 21 | z-index: 1; 22 | } 23 | 24 | mat-sidenav { 25 | mat-toolbar { 26 | display: flex; 27 | justify-content: center; 28 | align-items: center; 29 | padding: 0 8px !important; 30 | 31 | img { 32 | // really we want to add 8px of margin to button 33 | margin-left: 8px; 34 | } 35 | } 36 | } 37 | 38 | mat-card-header { 39 | padding: 0 0 0 8px; 40 | 41 | mat-card-title { 42 | font-size: 32px; 43 | } 44 | 45 | >* { 46 | margin: 0; 47 | } 48 | } 49 | 50 | .hide-nav-text { 51 | span { 52 | display: none; 53 | } 54 | 55 | .mat-mdc-list-item { 56 | padding-right: 0 !important; 57 | } 58 | } 59 | 60 | .sidenav-content { 61 | position: relative; 62 | flex: 0 0 auto; 63 | flex-grow: 1; 64 | display: flex; 65 | flex-direction: column; 66 | } 67 | 68 | mat-divider { 69 | margin: 12px 0; 70 | } 71 | 72 | .date-selector { 73 | caret-color: transparent; 74 | } 75 | 76 | .header { 77 | display: flex; 78 | box-sizing: border-box; 79 | width: 100%; 80 | flex-direction: row; 81 | align-items: center; 82 | white-space: nowrap; 83 | height: var(--mat-toolbar-standard-height); 84 | 85 | .mat-card-avatar { 86 | background-size: cover; 87 | border-radius: 6px; 88 | box-shadow: 0 0 0 1px rgba(31, 35, 40, 0.15); 89 | background-color: var(--sys-inverse-on-surface); 90 | width: 48px; 91 | height: 48px; 92 | vertical-align: middle; 93 | margin: 0; 94 | } 95 | 96 | h1 { 97 | min-width: 200px; 98 | 99 | .mat-mdc-select { 100 | font-size: 32px; 101 | line-height: 32px; 102 | } 103 | 104 | padding: 0 16px; 105 | font-size: 32px; 106 | 107 | ::ng-deep .mat-mdc-select-arrow-wrapper { 108 | margin-left: 10px !important; 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /backend/src/controllers/seats.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import SeatsService from '../services/seats.service.js'; 3 | 4 | class SeatsController { 5 | async getAllSeats(req: Request, res: Response): Promise { 6 | const org = req.query.org?.toString() 7 | try { 8 | const seats = await SeatsService.getAllSeats(org); 9 | res.status(200).json(seats); 10 | } catch (error: unknown) { 11 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 12 | res.status(500).json({ error: errorMessage }); 13 | } 14 | } 15 | 16 | async getSeat(req: Request, res: Response): Promise { 17 | const { id } = req.params; 18 | const { since, until, org } = req.query as { [key: string]: string | undefined }; 19 | 20 | try { 21 | const sanitizedOrg = typeof org === 'string' ? org : undefined; 22 | const params = { since, until, org: sanitizedOrg }; 23 | 24 | // Use our new unified getSeat method that handles both ID and login 25 | // Pass the ID directly without conversion - the service will handle it 26 | const seat = await SeatsService.getSeat(id, params); 27 | 28 | res.status(200).json(seat); 29 | } catch (error: unknown) { 30 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 31 | res.status(500).json({ error: errorMessage }); 32 | } 33 | } 34 | 35 | async getActivity(req: Request, res: Response): Promise { 36 | const org = req.query.org?.toString() 37 | const { daysInactive, precision } = req.query; 38 | const _daysInactive = Number(daysInactive); 39 | if (!daysInactive || isNaN(_daysInactive)) { 40 | res.status(400).json({ error: 'daysInactive query parameter is required' }); 41 | return; 42 | } 43 | try { 44 | const activityDays = await SeatsService.getMembersActivity({ 45 | org, 46 | daysInactive: _daysInactive, 47 | precision: precision as 'hour' | 'day' 48 | }); 49 | res.status(200).json(activityDays); 50 | } catch (error: unknown) { 51 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 52 | res.status(500).json({ error: errorMessage }); 53 | } 54 | } 55 | } 56 | 57 | export default new SeatsController(); -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component.scss: -------------------------------------------------------------------------------- 1 | .page-header { 2 | margin-bottom: 28px; 3 | h1 { 4 | margin-bottom: 0; 5 | } 6 | } 7 | 8 | .survey-container { 9 | display:grid; 10 | grid-template-columns: 600px 1fr; 11 | gap: 28px; 12 | } 13 | 14 | @media screen and (max-width: 1150px) { 15 | .survey-container { 16 | grid-template-columns: 1fr; 17 | } 18 | } 19 | 20 | .example-radio-group { 21 | display: flex; 22 | flex-direction: column; 23 | margin: 15px 0; 24 | align-items: flex-start; 25 | } 26 | 27 | .example-radio-button { 28 | margin: 5px; 29 | } 30 | 31 | .example-form-field { 32 | width: 100%; 33 | } 34 | 35 | .example-mat-slider { 36 | width: 95%; 37 | } 38 | 39 | .slider-labels-container { 40 | display: inline-block; 41 | margin: 0 8px; 42 | width: 95%; 43 | .slider-labels { 44 | display: block; 45 | width: 100%; 46 | height: 22px; 47 | margin-top: -10px; 48 | position: relative; 49 | margin-bottom: 15px; 50 | > * { 51 | position:absolute; 52 | transform:translateX(-50%); 53 | } 54 | } 55 | } 56 | 57 | label { 58 | // margin-left: -15px; 59 | margin-bottom: 20px; 60 | display: block; 61 | } 62 | 63 | #slider, 64 | mat-radio-group, 65 | mat-form-field { 66 | box-sizing: border-box; 67 | padding-left: 20px !important; 68 | } 69 | 70 | #survey-responses { 71 | position: relative; 72 | max-height: 1153px; 73 | overflow: hidden; 74 | .survey-response-container { 75 | height: 100%; 76 | overflow-y: auto; 77 | -ms-overflow-style: none; /* Internet Explorer 10+ */ 78 | scrollbar-width: none; /* Firefox */ 79 | 80 | mat-card { 81 | margin-bottom: 20px; 82 | } 83 | } 84 | .survey-response-container::-webkit-scrollbar { 85 | display: none; /* Safari and Chrome */ 86 | } 87 | } 88 | #survey-responses:after { 89 | content: ""; 90 | position: absolute; 91 | z-index: 1; 92 | bottom: 0; 93 | left: 0; 94 | pointer-events: none; 95 | background-image: linear-gradient(to bottom, rgba(255,255,255,0), var(--sys-background) 90%); 96 | width: 100%; 97 | height: 8em; 98 | } 99 | 100 | .footer-text { 101 | margin-top: 35px; 102 | color: var(--sys-on-surface-variant) 103 | } -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-metrics/copilot-metrics-pie-chart/copilot-metrics-pie-chart.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges } from '@angular/core'; 2 | import { CopilotMetrics } from '../../../../services/api/metrics.service.interfaces'; 3 | import * as Highcharts from 'highcharts'; 4 | import { HighchartsChartModule } from 'highcharts-angular'; 5 | import { HighchartsService } from '../../../../services/highcharts.service'; 6 | 7 | @Component({ 8 | selector: 'app-copilot-metrics-ide-completion-pie-chart', 9 | standalone: true, 10 | imports: [ 11 | HighchartsChartModule 12 | ], 13 | template: ` 14 | `, 15 | // styleUrl: './copilot-metrics-ide-completion-pie-chart.component.css', 16 | changeDetection: ChangeDetectionStrategy.OnPush, 17 | }) 18 | export class CopilotMetricsPieChartComponent implements OnChanges { 19 | Highcharts: typeof Highcharts = Highcharts; 20 | @Input() metricsTotals?: CopilotMetrics; 21 | chartOptions: Highcharts.Options = { 22 | tooltip: { 23 | positioner: function () { 24 | return { x: 0, y: 0 }; 25 | }, 26 | outside: true, 27 | backgroundColor: undefined, 28 | headerFormat: '{series.name}
', 29 | pointFormat: '{point.name}: ' + 30 | '{point.y:.2f}% of total
' 31 | }, 32 | series: [{ 33 | type: 'pie', 34 | }, { 35 | type: 'pie', 36 | }], 37 | drilldown: { 38 | series: [] 39 | } 40 | }; 41 | _chartOptions?: Highcharts.Options; 42 | updateFlag = false; 43 | 44 | constructor( 45 | private highchartsService: HighchartsService, 46 | private cdr: ChangeDetectorRef 47 | ) { } 48 | 49 | ngOnChanges() { 50 | if (this.metricsTotals) { 51 | this.chartOptions = { 52 | ...this.chartOptions, 53 | ...this.highchartsService.transformMetricsToPieDrilldown(this.metricsTotals) 54 | }; 55 | this.updateFlag = true; 56 | this.cdr.detectChanges(); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/dashboard-card/dashboard-card-drilldown-bar-chart/dashboard-card-drilldown-bar-chart.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; 2 | import { MatCardModule } from '@angular/material/card'; 3 | import * as Highcharts from 'highcharts'; 4 | import HC_drilldown from 'highcharts/modules/drilldown'; 5 | HC_drilldown(Highcharts); 6 | import { HighchartsChartModule } from 'highcharts-angular'; 7 | import { CommonModule } from '@angular/common'; 8 | import { HighchartsService } from '../../../../../services/highcharts.service'; 9 | import { CopilotMetrics } from '../../../../../services/api/metrics.service.interfaces'; 10 | 11 | @Component({ 12 | selector: 'app-dashboard-card-drilldown-bar-chart', 13 | standalone: true, 14 | imports: [ 15 | MatCardModule, 16 | CommonModule, 17 | HighchartsChartModule 18 | ], 19 | templateUrl: './dashboard-card-drilldown-bar-chart.component.html', 20 | styleUrls: [ 21 | './dashboard-card-drilldown-bar-chart.component.scss', 22 | '../dashboard-card.scss' 23 | ] 24 | }) 25 | export class DashboardCardDrilldownBarChartComponent implements OnChanges { 26 | Highcharts: typeof Highcharts = Highcharts; 27 | @Input() data?: CopilotMetrics[] = []; 28 | chartOptions: Highcharts.Options = { 29 | chart: { 30 | type: 'column' 31 | }, 32 | xAxis: { 33 | type: 'category', 34 | }, 35 | tooltip: { 36 | headerFormat: '{series.name}
', 37 | pointFormat: '{point.name}: ' + 38 | '{point.y} users
' 39 | }, 40 | legend: { 41 | enabled: false 42 | }, 43 | series: [{ 44 | type: 'column', 45 | }], 46 | drilldown: { 47 | series: [{ 48 | type: 'column' 49 | }] 50 | } 51 | }; 52 | _chartOptions?: Highcharts.Options; 53 | updateFlag = false; 54 | 55 | constructor( 56 | private highchartsService: HighchartsService 57 | ) { } 58 | 59 | ngOnChanges(changes: SimpleChanges) { 60 | if (changes['data'] && this.data) { 61 | this._chartOptions = this.highchartsService.transformCopilotMetricsToBarChartDrilldown(this.data); 62 | this.chartOptions = { 63 | ...this.chartOptions, 64 | ...this._chartOptions 65 | }; 66 | this.updateFlag = true; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /backend/src/__tests__/services/seatServiceInsert.spec.ts: -------------------------------------------------------------------------------- 1 | //seatServiceInsert.spec.ts run via "npx tsx src/__tests__/services/seatServiceInsert.spec.ts" 2 | import { describe, expect, test, it, vi, beforeAll, afterAll, beforeEach } from 'vitest'; 3 | import SeatService from '../../services/seats.service.js'; 4 | import { SeatsMockConfig } from '../__mock__/types.js'; 5 | import { SeatType } from '../../models/seats.model.js'; 6 | import 'dotenv/config'; 7 | import Database from '../../database.js'; 8 | import { generateStatelessMetrics } from '../__mock__/seats-gen/runSeatsGenerator.js'; 9 | import { now } from 'mongoose'; 10 | 11 | if (!process.env.MONGODB_URI) throw new Error('MONGODB_URI is not defined'); 12 | const database = new Database(process.env.MONGODB_URI); 13 | 14 | 15 | beforeAll(async () => { 16 | await database.connect(); 17 | }); 18 | 19 | describe('SeatService', () => { 20 | describe('insertSeats', () => { 21 | it('should insert seats correctly', async () => { 22 | try { 23 | // Test data setup 24 | const org = 'test-org'; 25 | const queryAt = new Date(now()); 26 | const seats = generateStatelessMetrics(); 27 | 28 | // Perform the insertion 29 | await SeatService.insertSeats(org, queryAt, seats); 30 | 31 | // Verify the insertion 32 | const insertedSeats = await SeatService.getAllSeats(org); 33 | 34 | // Assertions 35 | expect(insertedSeats).toBeDefined(); 36 | expect(insertedSeats.length).toBeGreaterThan(0); 37 | expect(insertedSeats[0].organization).toBe(org); 38 | expect(insertedSeats[0].queryAt).toEqual(queryAt); 39 | 40 | // Verify the structure of inserted seats 41 | const firstSeat = insertedSeats[0]; 42 | expect(firstSeat).toMatchObject({ 43 | organization: org, 44 | queryAt: queryAt, 45 | // Add other expected properties based on your data model 46 | }); 47 | 48 | } catch (error) { 49 | console.error('Test failed:', error); 50 | throw error; 51 | } 52 | }); 53 | }); 54 | }); 55 | 56 | afterAll(async () => { 57 | await database.disconnect(); 58 | }); 59 | -------------------------------------------------------------------------------- /backend/src/controllers/target.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import TargetValuesService from '../services/target.service.js'; 3 | import { TargetCalculationService } from '../services/target-calculation-service.js'; 4 | import logger from '../services/logger.js'; 5 | 6 | class TargetValuesController { 7 | async getTargetValues(req: Request, res: Response): Promise { 8 | try { 9 | const targetValues = await TargetValuesService.getTargetValues(); 10 | res.status(200).json(targetValues); 11 | } catch (error) { 12 | logger.error('Error getting target values:', error); 13 | res.status(500).json(error); 14 | } 15 | } 16 | 17 | async updateTargetValues(req: Request, res: Response): Promise { 18 | try { 19 | const updatedTargetValues = await TargetValuesService.updateTargetValues(req.body); 20 | res.status(200).json(updatedTargetValues); 21 | } catch (error) { 22 | logger.error('Error updating target values:', error); 23 | res.status(500).json(error); 24 | } 25 | } 26 | 27 | /** 28 | * Calculate targets based on current metrics, adoption, and survey data 29 | * @route GET /targets/calculate 30 | */ 31 | async calculateTargetValues(req: Request, res: Response): Promise { 32 | try { 33 | // Only use org if it's explicitly passed in the query parameters 34 | const org = req.query.org ? String(req.query.org) : null; 35 | const enableLogging = req.query.enableLogging === 'true'; 36 | const includeLogsInResponse = req.query.includeLogs === 'true'; 37 | 38 | 39 | // Use the static method from TargetCalculationService to avoid instantiation issues 40 | const result = await TargetCalculationService.fetchAndCalculateTargets( 41 | org, // Pass null if no org was provided 42 | enableLogging, 43 | includeLogsInResponse 44 | ); 45 | 46 | // Check if we have logs before sending the response 47 | if (includeLogsInResponse) { 48 | logger.info(`Response will include ${result.logs?.length || 0} logs`); 49 | } 50 | 51 | res.status(200).json(result); 52 | } catch (error) { 53 | logger.error('Error calculating target values:', error); 54 | res.status(500).json({ error: `Failed to calculate target values: ${error}` }); 55 | } 56 | } 57 | } 58 | 59 | export default new TargetValuesController(); 60 | -------------------------------------------------------------------------------- /backend/src/__tests__/__mock__/seats-gen/runSeatsGenerator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.generateStatelessMetrics = generateStatelessMetrics; 4 | exports.generateStatefulMetrics = generateStatefulMetrics; 5 | var mockSeatsGenerator_js_1 = require("./mockSeatsGenerator.js"); 6 | //import seatsExample from '../seats-gen/seatsExample.json' ; 7 | var fs_1 = require("fs"); 8 | var path_1 = require("path"); 9 | console.log("seatsExample:", "begin"); 10 | var seatsExample = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../seats-gen/seatsExample.json'), 'utf8')); 11 | console.log("seatsExample:", seatsExample[0].length); 12 | var mockConfig = { 13 | startDate: new Date('2024-01-01'), 14 | endDate: new Date('2024-12-31'), 15 | usagePattern: 'heavy', 16 | heavyUsers: ['nathos', 'arfon', 'kyanny'], 17 | editors: [ 18 | 'copilot-chat-platform', 19 | 'vscode/1.96.2/copilot/1.254.0', 20 | 'GitHubGhostPilot/1.0.0/unknown', 21 | 'vscode/1.96.2/', 22 | 'vscode/1.97.0-insider/copilot-chat/0.24.2024122001' 23 | ] 24 | }; 25 | // Load template data from seatsExample.json 26 | var templateData = seatsExample; 27 | var staticTemplateData = null; 28 | function generateStatelessMetrics() { 29 | var generator = new mockSeatsGenerator_js_1.MockSeatsGenerator(mockConfig, templateData); 30 | return generator.generateMetrics(); 31 | } 32 | function generateStatefulMetrics() { 33 | if (!staticTemplateData) { 34 | staticTemplateData = templateData; 35 | } 36 | var generator = new mockSeatsGenerator_js_1.MockSeatsGenerator(mockConfig, staticTemplateData); 37 | var generatedData = generator.generateMetrics(); 38 | staticTemplateData = generatedData; 39 | return generatedData; 40 | } 41 | // // Example usage 42 | //import templateData from '.seats-gen/seatsExample.json' assert { type: 'json' }; 43 | var statelessData = generateStatelessMetrics(); 44 | console.log("Stateless Data:", JSON.stringify(statelessData, null, 2)); 45 | // const statefulData = generateStatefulMetrics(); 46 | // console.log("Stateful Data:", JSON.stringify(statefulData, null, 2)); 47 | // // To simulate repeated calls for stateful generation 48 | // for (let i = 0; i < 5; i++) { 49 | // const repeatedStatefulData = generateStatefulMetrics(); 50 | // console.log(`Stateful Data Iteration ${i + 1}:`, JSON.stringify(repeatedStatefulData, null, 2)); 51 | // } 52 | -------------------------------------------------------------------------------- /frontend/src/assets/images/github-copilot-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/src/services/status.service.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import app from "../index.js"; 3 | import { Endpoints } from "@octokit/types"; 4 | import { Request } from "express"; 5 | 6 | export interface StatusType { 7 | github?: boolean; 8 | seatsHistory?: { 9 | oldestCreatedAt: string; 10 | daysSinceOldestCreatedAt?: number; 11 | }; 12 | installations: { 13 | installation: Endpoints["GET /app/installations"]["response"]["data"][0] 14 | repos: Endpoints["GET /app/installations"]["response"]["data"]; 15 | }[]; 16 | surveyCount: number; 17 | auth?: { 18 | user?: string; 19 | email?: string; 20 | authenticated: boolean; 21 | groups?: string[]; 22 | headers?: string[]; // Add this to store header names 23 | }; 24 | } 25 | 26 | class StatusService { 27 | 28 | constructor() { } 29 | 30 | async getStatus(req?: Request): Promise { 31 | const status = {} as StatusType; 32 | 33 | // Add authentication information if request is provided 34 | if (req) { 35 | const user = req.headers['x-auth-request-user'] as string; 36 | const email = req.headers['x-auth-request-email'] as string; 37 | const groups = req.headers['x-auth-request-groups'] as string[]; 38 | 39 | status.auth = { 40 | user, 41 | email, 42 | authenticated: !!user, 43 | groups, 44 | headers: Object.keys(req.headers) // Add all header names as an array 45 | }; 46 | } 47 | 48 | const Seats = mongoose.model('Seats'); 49 | 50 | const oldestSeat = await Seats.findOne().sort({ createdAt: 1 }); 51 | const daysSince = oldestSeat ? Math.floor((new Date().getTime() - oldestSeat.createdAt.getTime()) / (1000 * 3600 * 24)) : undefined; 52 | status.seatsHistory = { 53 | oldestCreatedAt: oldestSeat?.createdAt.toISOString() || 'No data', 54 | daysSinceOldestCreatedAt: daysSince 55 | } 56 | 57 | status.installations = []; 58 | for (const installation of app.github.installations) { 59 | const repos = await installation.octokit.request(installation.installation.repositories_url); 60 | status.installations.push({ 61 | installation: installation.installation, 62 | repos: repos.data.repositories 63 | }); 64 | } 65 | 66 | // const surveys = await Survey.findAll({ 67 | // order: [['updatedAt', 'DESC']] 68 | // }); 69 | 70 | // if (surveys) { 71 | // status.surveyCount = surveys.length; 72 | // } 73 | 74 | return status; 75 | } 76 | } 77 | 78 | export default StatusService; -------------------------------------------------------------------------------- /frontend/src/app/services/theme.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject, Observable } from 'rxjs'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class ThemeService { 8 | private readonly THEME_KEY = 'preferred-theme'; 9 | private isDarkThemeSubject = new BehaviorSubject(false); 10 | private themeSubject = new BehaviorSubject('light-theme'); 11 | 12 | constructor() { 13 | this.initializeTheme(); 14 | } 15 | 16 | private initializeTheme(): void { 17 | const savedTheme = localStorage.getItem(this.THEME_KEY); 18 | if (!savedTheme || savedTheme === 'system') { 19 | this.saveThemePreference('system'); 20 | const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; 21 | this.isDarkThemeSubject.next(prefersDark); 22 | } else { 23 | this.isDarkThemeSubject.next(savedTheme === 'dark-theme'); 24 | this.themeSubject.next(savedTheme); 25 | } 26 | this.applyTheme(); 27 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { 28 | if (this.themeSubject.value === 'system') { 29 | this.isDarkThemeSubject.next(e.matches); 30 | this.applyTheme(); 31 | } 32 | }); 33 | } 34 | 35 | getTheme(): Observable { 36 | return this.themeSubject.asObservable(); 37 | } 38 | 39 | isDarkTheme(): Observable { 40 | return this.isDarkThemeSubject.asObservable(); 41 | } 42 | 43 | toggleTheme(): void { 44 | this.isDarkThemeSubject.next(!this.isDarkThemeSubject.value); 45 | this.applyTheme(); 46 | } 47 | 48 | setDarkTheme(isDark: boolean): void { 49 | this.isDarkThemeSubject.next(isDark); 50 | this.applyTheme(); 51 | } 52 | 53 | saveThemePreference(theme: 'light-theme' | 'dark-theme' | 'system'): void { 54 | if (theme === 'system') { 55 | this.isDarkThemeSubject.next(window.matchMedia('(prefers-color-scheme: dark)').matches); 56 | } else { 57 | this.isDarkThemeSubject.next(theme === 'dark-theme'); 58 | } 59 | this.applyTheme(); 60 | this.themeSubject.next(theme); 61 | localStorage.setItem(this.THEME_KEY, theme); 62 | } 63 | 64 | private applyTheme(): void { 65 | const isDark = this.isDarkThemeSubject.value; 66 | document.body.classList.toggle('dark-theme', isDark); 67 | document.body.classList.toggle('light-theme', !isDark); 68 | 69 | const theme = isDark ? 'dark-theme' : 'light-theme'; 70 | document.body.classList.remove('dark-theme', 'light-theme'); 71 | document.body.classList.add(theme); 72 | } 73 | } -------------------------------------------------------------------------------- /backend/src/__tests__/__mock__/mock.mongo.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, beforeAll, beforeEach } from 'vitest'; 2 | import mongoose from 'mongoose'; 3 | import 'dotenv/config'; 4 | import Database from '../../database.js'; 5 | import SettingsService, { SettingsType } from '../../services/settings.service.js'; 6 | 7 | const org = null; 8 | const defaultSettings: SettingsType = { 9 | baseUrl: 'http://localhost', 10 | webhookProxyUrl: 'http://localhost/proxy', 11 | webhookSecret: 'secret', 12 | metricsCronExpression: '0 0 * * *', 13 | devCostPerYear: '100000', 14 | developerCount: '10', 15 | hoursPerYear: '2000', 16 | percentTimeSaved: '20', 17 | percentCoding: '50', 18 | }; 19 | 20 | beforeAll(async () => { 21 | if (!process.env.MONGODB_URI) throw new Error('MONGODB_URI is not defined'); 22 | const database = new Database(process.env.MONGODB_URI); 23 | await database.connect(); 24 | }); 25 | 26 | beforeEach(async () => { 27 | const Settings = mongoose.model('Settings'); 28 | await Settings.deleteMany({}); 29 | }); 30 | 31 | describe('settings.service.spec.ts test', () => { 32 | test('should initialize settings correctly', async () => { 33 | const settingsService = new SettingsService(defaultSettings); 34 | const initializedSettings = await settingsService.initialize(); 35 | 36 | expect(initializedSettings).toEqual(defaultSettings); 37 | }); 38 | 39 | test('should update settings correctly', async () => { 40 | const settingsService = new SettingsService(defaultSettings); 41 | await settingsService.initialize(); 42 | 43 | const newSettings = { 44 | baseUrl: 'http://new-url', 45 | webhookProxyUrl: 'http://new-url/proxy', 46 | }; 47 | 48 | await settingsService.updateSettings(newSettings); 49 | 50 | const updatedSettings = await settingsService.getAllSettings(); 51 | const baseUrlSetting = updatedSettings.find((setting: SettingsType) => setting.name === 'baseUrl'); 52 | expect(baseUrlSetting?.value).toEqual(newSettings.baseUrl); 53 | const webhookProxyUrlSetting = updatedSettings.find((setting: SettingsType) => setting.name === 'webhookProxyUrl'); 54 | expect(webhookProxyUrlSetting?.value).toEqual(newSettings.webhookProxyUrl); 55 | }); 56 | 57 | test('should delete settings correctly', async () => { 58 | const settingsService = new SettingsService(defaultSettings); 59 | await settingsService.initialize(); 60 | 61 | await settingsService.deleteSettings('baseUrl'); 62 | 63 | const settings = await settingsService.getAllSettings(); 64 | expect(settings['baseUrl']).toBeUndefined(); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /frontend/src/app/services/api/metrics.service.interfaces.ts: -------------------------------------------------------------------------------- 1 | interface LanguageMetrics extends IdeCodeCompletionsShared { 2 | name: string; 3 | } 4 | 5 | interface ModelBase { 6 | name: string; 7 | is_custom_model: boolean; 8 | custom_model_training_date: string | null; 9 | } 10 | 11 | export interface CodeModel extends ModelBase, IdeCodeCompletionsShared { 12 | languages: LanguageMetrics[]; 13 | } 14 | 15 | export interface ChatModel extends ModelBase, IdeChatShared { 16 | } 17 | 18 | interface DotComChatModel extends ModelBase, DotComChatShared { 19 | } 20 | 21 | interface PullRequestModel extends ModelBase, DotComPullRequestsShared { 22 | } 23 | 24 | interface CodeEditor extends IdeCodeCompletionsShared { 25 | name: string; 26 | models: CodeModel[]; 27 | } 28 | 29 | interface ChatEditor extends IdeChatShared { 30 | name: string; 31 | models: ChatModel[]; 32 | } 33 | 34 | interface Repository extends DotComPullRequestsShared { 35 | name: string; 36 | models: PullRequestModel[]; 37 | } 38 | 39 | export interface IdeCodeCompletionsShared { 40 | total_engaged_users: number; 41 | total_code_acceptances: number; 42 | total_code_lines_accepted: number; 43 | total_code_lines_suggested: number; 44 | total_code_suggestions: number; 45 | } 46 | export interface IdeCodeCompletions extends IdeCodeCompletionsShared { 47 | languages: LanguageMetrics[]; 48 | editors: CodeEditor[]; 49 | } 50 | 51 | export interface IdeChatShared { 52 | total_engaged_users: number; 53 | total_chats: number; 54 | total_chat_insertion_events?: number; 55 | total_chat_copy_events?: number; 56 | } 57 | export interface IdeChat extends IdeChatShared { 58 | total_chats: number; 59 | total_engaged_users: number; 60 | editors: ChatEditor[]; 61 | } 62 | 63 | export interface DotComChatShared { 64 | total_engaged_users: number; 65 | total_chats: number; 66 | } 67 | export interface DotComChat extends DotComChatShared { 68 | models: DotComChatModel[]; 69 | } 70 | 71 | export interface DotComPullRequestsShared { 72 | total_engaged_users: number; 73 | total_pr_summaries_created: number; 74 | } 75 | export interface DotComPullRequests extends DotComPullRequestsShared { 76 | repositories: Repository[]; 77 | } 78 | 79 | interface CopilotMetrics { 80 | date: string; 81 | total_active_users: number; 82 | total_engaged_users: number; 83 | copilot_ide_code_completions: IdeCodeCompletions | null; 84 | copilot_ide_chat: IdeChat | null; 85 | copilot_dotcom_chat: DotComChat | null; 86 | copilot_dotcom_pull_requests: DotComPullRequests | null; 87 | } 88 | 89 | export { 90 | type CopilotMetrics 91 | } -------------------------------------------------------------------------------- /backend/src/__tests__/services/settings.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, beforeAll, beforeEach } from 'vitest'; 2 | import mongoose from 'mongoose'; 3 | import {readFileSync} from 'fs'; 4 | import 'dotenv/config'; 5 | 6 | import Database from '../../database'; 7 | import SettingsService, { SettingsType } from '../../services/settings.service'; 8 | 9 | const org = null; 10 | const defaultSettings: SettingsType = { 11 | baseUrl: 'http://localhost', 12 | webhookProxyUrl: 'http://localhost/proxy', 13 | webhookSecret: 'secret', 14 | metricsCronExpression: '0 0 * * *', 15 | devCostPerYear: '100000', 16 | developerCount: '10', 17 | hoursPerYear: '2000', 18 | percentTimeSaved: '20', 19 | percentCoding: '50', 20 | }; 21 | 22 | beforeAll(async () => { 23 | if (!process.env.MONGODB_URI) throw new Error('MONGODB_URI is not defined'); 24 | const database = new Database(process.env.MONGODB_URI); 25 | await database.connect(); 26 | }); 27 | 28 | beforeEach(async () => { 29 | const Settings = mongoose.model('Settings'); 30 | await Settings.deleteMany({}); 31 | }); 32 | 33 | describe('settings.service.spec.ts test', () => { 34 | test('should initialize settings correctly', async () => { 35 | const settingsService = new SettingsService(defaultSettings); 36 | const initializedSettings = await settingsService.initialize(); 37 | 38 | expect(initializedSettings).toEqual(defaultSettings); 39 | }); 40 | 41 | test('should update settings correctly', async () => { 42 | const settingsService = new SettingsService(defaultSettings); 43 | await settingsService.initialize(); 44 | 45 | const newSettings = { 46 | baseUrl: 'http://new-url', 47 | webhookProxyUrl: 'http://new-url/proxy', 48 | }; 49 | 50 | await settingsService.updateSettings(newSettings); 51 | 52 | const updatedSettings = await settingsService.getAllSettings(); 53 | console.log(updatedSettings); 54 | const baseUrlSetting = updatedSettings.find(setting => setting.name === 'baseUrl'); 55 | expect(baseUrlSetting?.value).toEqual(newSettings.baseUrl); 56 | const webhookProxyUrlSetting = updatedSettings.find(setting => setting.name === 'webhookProxyUrl'); 57 | expect(webhookProxyUrlSetting?.value).toEqual(newSettings.webhookProxyUrl); 58 | }); 59 | 60 | test('should delete settings correctly', async () => { 61 | const settingsService = new SettingsService(defaultSettings); 62 | await settingsService.initialize(); 63 | 64 | await settingsService.deleteSettings('baseUrl'); 65 | 66 | const settings = await settingsService.getAllSettings(); 67 | expect(settings.find(setting => setting.name === 'baseUrl')).toBeUndefined(); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 |
2 | 6 |
7 | 8 | 16 | 17 | 18 | 19 | Adoption 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Daily Activity 28 | 29 | 30 | 32 | 33 | 34 | 35 | Time Saved 36 | 37 | 38 | 40 | 41 | 42 | 43 | @for (status of statuses; track $index) { 44 | 45 | 46 | 47 | } 48 | 49 |
50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /frontend/src/app/material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { MatPaginatorModule } from '@angular/material/paginator'; 4 | import { MatSortModule } from '@angular/material/sort'; 5 | import { MatTableModule } from '@angular/material/table'; 6 | import { MatInputModule } from '@angular/material/input'; 7 | import { MatFormFieldModule } from '@angular/material/form-field'; 8 | import { MatButtonModule } from '@angular/material/button'; 9 | import { MatIconModule } from '@angular/material/icon'; 10 | import { MatDatepickerModule } from '@angular/material/datepicker'; 11 | import { MatNativeDateModule } from '@angular/material/core'; 12 | import { MatSelectModule } from '@angular/material/select'; 13 | import { MatAutocompleteModule } from '@angular/material/autocomplete'; 14 | import { MatProgressBarModule } from '@angular/material/progress-bar'; 15 | import { MatButtonToggleModule } from '@angular/material/button-toggle'; 16 | import { FormsModule } from '@angular/forms'; 17 | import { MatDialogModule } from '@angular/material/dialog'; 18 | import { MatChipsModule } from '@angular/material/chips'; 19 | import { MatTabsModule } from '@angular/material/tabs'; 20 | import { MatListModule } from '@angular/material/list'; 21 | import { MatTooltipModule } from '@angular/material/tooltip'; 22 | import { MatSliderModule } from '@angular/material/slider'; 23 | import { MatCardModule } from '@angular/material/card'; 24 | import { MatRadioModule } from '@angular/material/radio'; 25 | import { MatToolbarModule } from '@angular/material/toolbar'; 26 | import { MatStepperModule } from '@angular/material/stepper'; 27 | import { MatSidenavModule } from '@angular/material/sidenav'; 28 | 29 | @NgModule({ 30 | exports: [ 31 | MatFormFieldModule, 32 | MatInputModule, 33 | MatTableModule, 34 | MatSortModule, 35 | MatPaginatorModule, 36 | MatButtonModule, 37 | MatTableModule, 38 | MatPaginatorModule, 39 | MatSortModule, 40 | MatIconModule, 41 | MatDatepickerModule, 42 | MatNativeDateModule, 43 | MatSelectModule, 44 | MatAutocompleteModule, 45 | MatProgressBarModule, 46 | MatButtonToggleModule, 47 | MatInputModule, 48 | FormsModule, 49 | MatButtonModule, 50 | MatDialogModule, 51 | MatChipsModule, 52 | MatTabsModule, 53 | MatListModule, 54 | MatTooltipModule, 55 | MatSliderModule, 56 | MatFormFieldModule, 57 | MatInputModule, 58 | FormsModule, 59 | MatCardModule, 60 | MatRadioModule, 61 | MatIconModule, 62 | MatToolbarModule, 63 | MatDialogModule, 64 | MatStepperModule, 65 | MatSidenavModule 66 | ] 67 | }) 68 | export class MaterialModule { } 69 | 70 | 71 | /** Copyright 2021 Google LLC. All Rights Reserved. 72 | Use of this source code is governed by an MIT-style license that 73 | can be found in the LICENSE file at http://angular.io/license */ -------------------------------------------------------------------------------- /frontend/src/app/services/api/installations.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnDestroy } from '@angular/core'; 2 | import { BehaviorSubject, of, Subject, tap } from 'rxjs'; 3 | import { serverUrl } from '../server.service'; 4 | import { Endpoints } from '@octokit/types'; 5 | import { HttpClient } from '@angular/common/http'; 6 | 7 | export interface InstallationStatus { 8 | installation?: Endpoints["GET /app/installations"]["response"]["data"][number], 9 | usage: boolean; 10 | metrics: boolean; 11 | copilotSeats: boolean; 12 | teamsAndMembers: boolean; 13 | } 14 | export interface statusResponse { 15 | isSetup: boolean; 16 | dbConnected: boolean; 17 | installations: InstallationStatus[], 18 | } 19 | 20 | export type Installations = Endpoints["GET /app/installations"]["response"]["data"] 21 | export type Installation = Installations[number] 22 | @Injectable({ 23 | providedIn: 'root' 24 | }) 25 | export class InstallationsService implements OnDestroy { 26 | private apiUrl = `${serverUrl}/api/setup`; 27 | status?: statusResponse; 28 | installations = new BehaviorSubject([]); 29 | currentInstallation = new BehaviorSubject(undefined); 30 | currentInstallationId = localStorage.getItem('installation') ? parseInt(localStorage.getItem('installation')!) : 0; 31 | private readonly _destroy$ = new Subject(); 32 | readonly destroy$ = this._destroy$.asObservable(); 33 | 34 | constructor(private http: HttpClient) { 35 | const id = localStorage.getItem('installation'); 36 | if (id) { 37 | this.setInstallation(Number(id)); 38 | } 39 | } 40 | 41 | ngOnDestroy(): void { 42 | this._destroy$.next(); 43 | this._destroy$.complete(); 44 | } 45 | 46 | getStatus() { 47 | if (!this.status) { 48 | return this.refreshStatus(); 49 | } 50 | return of(this.status); 51 | } 52 | 53 | refreshStatus() { 54 | return this.http.get(`${this.apiUrl}/status`).pipe( 55 | tap((status) => { 56 | this.status = status; 57 | if (status.installations) { 58 | this.installations.next(status.installations.map(i => i.installation!)); 59 | if (this.installations.value.length === 1) { 60 | this.setInstallation(this.installations.value[0].id); 61 | } else { 62 | this.setInstallation(this.currentInstallationId); 63 | } 64 | } 65 | }) 66 | ); 67 | } 68 | 69 | getInstallations() { 70 | return this.installations.asObservable(); 71 | } 72 | 73 | setInstallation(id: number) { 74 | this.currentInstallationId = id; 75 | this.currentInstallation.next(this.installations.value.find(i => i.id === id)); 76 | localStorage.setItem('installation', id.toString()); 77 | } 78 | 79 | getStatus2() { 80 | return this.http.get(`${serverUrl}/api/status`); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /backend/src/__tests__/__mock__/seats-gen/runSeatsGenerator.ts: -------------------------------------------------------------------------------- 1 | 2 | //runSeatsGenerator.ts - a script to run the seats generator and output the generated data via "npx tsx src/__tests__/__mock__/seats-gen/runSeatsGenerator.ts" 3 | import { MockSeatsGenerator as MockSeatsGenerator } from './mockSeatsGenerator.js'; 4 | import { SeatsMockConfig } from '../types.js'; 5 | import seatsExample from './seats.json'; type: 'json'; 6 | 7 | 8 | const mockConfig: SeatsMockConfig = { 9 | startDate: new Date('2024-11-01'), 10 | endDate: new Date('2024-12-31'), 11 | usagePattern: 'heavy', 12 | heavyUsers: ['nathos', 'arfon', 'kyanny'], 13 | specificUser: 'nathos', 14 | editors: [ 15 | 'copilot-chat-platform', 16 | 'vscode/1.96.2/copilot/1.254.0', 17 | 'GitHubGhostPilot/1.0.0/unknown', 18 | 'vscode/1.96.2/', 19 | 'vscode/1.97.0-insider/copilot-chat/0.24.2024122001' 20 | ] 21 | }; 22 | 23 | // Load template data from seatsExample.json 24 | const templateData: any = seatsExample; 25 | 26 | let staticTemplateData: any = null; 27 | 28 | const generateStatelessMetrics = () => { 29 | console.log('Starting to generate stateless metrics...'); 30 | try { 31 | const generator = new MockSeatsGenerator(mockConfig, templateData); 32 | const metrics = generator.generateMetrics(); 33 | console.log('Successfully generated stateless metrics:', metrics.length); 34 | return metrics; 35 | } catch (error) { 36 | console.error('Error generating stateless metrics:', error); 37 | throw error; // Re-throw the error after logging it 38 | } 39 | } 40 | 41 | const generateStatefulMetrics = () => { 42 | console.log('Starting to generate statefull metrics...'); 43 | try { 44 | if (!staticTemplateData) { 45 | staticTemplateData = templateData; 46 | } 47 | const generator = new MockSeatsGenerator(mockConfig, staticTemplateData); 48 | const generatedData = generator.generateMetrics(); 49 | staticTemplateData = generatedData; 50 | console.log('Successfully generated stateful metrics:', generatedData.length); 51 | return generatedData; 52 | } catch (error) { 53 | console.error('Error generating stateful metrics:', error); 54 | throw error; // Re-throw the error after logging it 55 | } 56 | } 57 | 58 | // // Example usage 59 | //import templateData from '.seats-gen/seatsExample.json' assert { type: 'json' }; 60 | //const statelessData = generateStatelessMetrics(); 61 | // console.log("Stateless Data:", JSON.stringify(statelessData, null, 2)); 62 | 63 | // const statefulData = generateStatefulMetrics(); 64 | // console.log("Stateful Data:", JSON.stringify(statefulData, null, 2)); 65 | 66 | // // To simulate repeated calls for stateful generation 67 | // for (let i = 0; i < 5; i++) { 68 | // const repeatedStatefulData = generateStatefulMetrics(); 69 | // console.log(`Stateful Data Iteration ${i + 1}:`, JSON.stringify(repeatedStatefulData, null, 2)); 70 | // } 71 | 72 | // Export functions 73 | export { generateStatelessMetrics, generateStatefulMetrics }; 74 | 75 | -------------------------------------------------------------------------------- /backend/src/__tests__/__mock__/survey-gen/mockSurveyGenerator.ts: -------------------------------------------------------------------------------- 1 | import { addDays } from 'date-fns'; 2 | import mongoose from 'mongoose'; 3 | import { SurveyMockConfig } from '../types.js'; 4 | import SequenceService from '../../../services/sequence.service.js'; 5 | import surveyExample from './exampleSurvey.json' assert { type: 'json' }; 6 | import { SurveyType } from "../models/survey.model.js"; 7 | 8 | class MockSurveyGenerator { 9 | private config: SurveyMockConfig; 10 | private baseData: any = surveyExample; // The template data structure 11 | 12 | constructor(config: SurveyMockConfig, templateData: any) { 13 | this.config = config; 14 | this.baseData = templateData; 15 | } 16 | 17 | private getRandomUserId(): string { 18 | return this.config.userIds[Math.floor(Math.random() * this.config.userIds.length)]; 19 | } 20 | 21 | private getRandomOrg(): string { 22 | return this.config.orgs[Math.floor(Math.random() * this.config.orgs.length)]; 23 | } 24 | 25 | private getRandomRepo(): string { 26 | return this.config.repos[Math.floor(Math.random() * this.config.repos.length)]; 27 | } 28 | 29 | private getRandomPrNumber(): number { 30 | return Math.floor(Math.random() * 100); 31 | } 32 | 33 | private getRandomPercentTimeSaved(): number { 34 | const x = Math.floor(Math.random() * 100); 35 | if (x < 10 || x > 65) { 36 | return Math.floor(Math.random() * 45) || 55; 37 | } 38 | return 30; 39 | } 40 | 41 | private getRandomReason(): string { 42 | return this.config.reasons[Math.floor(Math.random() * this.config.reasons.length)]; 43 | } 44 | 45 | private getRandomTimeUsedFor(): string { 46 | return this.config.timeUsedFors[Math.floor(Math.random() * this.config.timeUsedFors.length)]; 47 | } 48 | 49 | private getRandomDate(): Date { 50 | return addDays(this.config.startDate, Math.floor(Math.random() * (this.config.endDate.getTime() - this.config.startDate.getTime()) / (1000 * 60 * 60 * 24))); 51 | } 52 | 53 | public async generateSurveys() { 54 | const newData = JSON.parse(JSON.stringify(this.baseData)); 55 | 56 | newData.surveys = await Promise.all(newData.surveys.map(async (survey: SurveyType) => { 57 | //survey.id = await SequenceService.getNextSequenceValue('survey-sequence'); 58 | survey.userId = this.getRandomUserId(); 59 | survey.org = this.getRandomOrg(); 60 | //survey.repo = this.getRandomRepo(); 61 | survey.prNumber = this.getRandomPrNumber(); 62 | survey.usedCopilot = Math.random() > 0.15; 63 | if (survey.usedCopilot) { 64 | 65 | survey.percentTimeSaved = this.getRandomPercentTimeSaved(); 66 | survey.reason = this.getRandomReason(); 67 | survey.timeUsedFor = this.getRandomTimeUsedFor(); 68 | survey.createdAt = this.getRandomDate(); 69 | survey.updatedAt = this.getRandomDate(); 70 | } 71 | return survey; 72 | })); 73 | 74 | return newData; 75 | } 76 | } 77 | 78 | export { MockSurveyGenerator }; 79 | -------------------------------------------------------------------------------- /frontend/src/app/shared/table/table.component.html: -------------------------------------------------------------------------------- 1 | 2 | Filter 3 | 4 | 5 | 6 |
7 |
8 | 9 |
10 | 11 | 12 | @for (column of columns; track column) { 13 | 14 | 17 | 46 | 47 | } 48 | 49 | 50 | 52 | 53 | 54 | 55 | @if (this.data?.length === 0) { 56 | 57 | } @else if (this.isLoadingResults) { 58 | 59 | } @else { 60 | 61 | } 62 | 63 |
15 | {{column.header}} 16 | 18 | @if (column.isImage) { 19 | 21 | } @else if (column.chipList) { 22 | 23 | 24 | 25 | {{column.cell(row)}} 26 | {{icon.startsWith('svg') ? '' : icon}} 28 | 29 | 30 | 31 | {{column.cell(row)}} 32 | 33 | 34 | } @else if (column.isIcon) { 35 | 36 | 37 | {{icon.startsWith('svg') ? '' : icon}} 38 | 39 | 40 | } @else if (column.link) { 41 | {{column.cell(row)}} 42 | } @else { 43 | {{column.cell(row)}} 44 | } 45 |
No dataloading...No data matching the filter "{{input.value}}"
64 | 65 | 66 |
-------------------------------------------------------------------------------- /frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |

6 | 7 |

8 | 10 | 11 |
12 | 13 | 14 | IDE Completions 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Most Active Users 25 | 26 | 27 | 28 | 29 | 30 | 39 | 40 | 41 | Engagement Breakdown 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | Language Acceptance Trends 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
60 |
61 | 62 | 63 | -------------------------------------------------------------------------------- /backend/src/__tests__/__mock__/seats-gen/mockSeatsGenerator.ts: -------------------------------------------------------------------------------- 1 | // mockSeatsGenerator.ts 2 | import { addHours, addDays, addWeeks } from 'date-fns'; 3 | import { SeatsMockConfig } from '../types.js'; 4 | import { randomInt } from 'crypto'; 5 | 6 | class MockSeatsGenerator { 7 | private config: SeatsMockConfig; 8 | private activities: Map; 9 | private baseData: any; // The template data structure 10 | private editors: string[]; 11 | 12 | constructor(config: SeatsMockConfig, templateData: any) { 13 | this.config = config; 14 | this.baseData = templateData; 15 | //this.activities = new Map(); 16 | this.editors = config.editors; 17 | 18 | // Ensure seats exist in templateData 19 | if (!this.baseData.seats) { 20 | throw new Error("Template data must include a 'seats' property."); 21 | }; 22 | } 23 | 24 | private getRandomEditor(): string { 25 | return this.editors[Math.floor(Math.random() * this.editors.length)]; 26 | } 27 | 28 | 29 | 30 | private getNextActivityIncrement(login: string): number { 31 | const isHeavyUser = this.config.heavyUsers.includes(login); 32 | 33 | switch (this.config.usagePattern) { 34 | case 'heavy': 35 | return 4; // 4 hours 36 | case 'heavy-but-siloed': 37 | return isHeavyUser ? 12 : 24; // 12 hours or 24 hours 38 | case 'moderate': 39 | return 24; // 24 hours 40 | case 'light': 41 | return 168; // 7 days 42 | } 43 | return 24; // Default to moderate 44 | } 45 | 46 | private updateActivity(login: string, lastActivity: Date): Date { 47 | const currentActivity : Date = lastActivity; 48 | 49 | const incrementHours = this.getNextActivityIncrement(login); 50 | 51 | const newActivity : Date = addHours(currentActivity, 100); 52 | 53 | // Don't go beyond end date 54 | if (newActivity > this.config.endDate) { 55 | return this.config.endDate; 56 | } 57 | 58 | return newActivity; 59 | } 60 | 61 | public generateSeats(counter) { 62 | const newSeatsTemplate = JSON.parse(JSON.stringify(this.baseData)); 63 | const newSeatsResponse : any = newSeatsTemplate; 64 | newSeatsResponse.seats = newSeatsTemplate.seats.map((seat: any) => { 65 | const login = seat.assignee.login; 66 | seat.created_at = this.config.startDate; 67 | seat.updated_at = this.config.endDate; 68 | //seat.specificUser = this.config.specificUser; 69 | 70 | 71 | 72 | if ( Math.random() < (0.35 * Math.random() * 2 + counter/2000) ) { 73 | seat.last_activity_editor = this.getRandomEditor(); 74 | 75 | console.log('last_activity_at \n', seat.last_activity_at); 76 | console.log('config.startDate \n', this.config.startDate); 77 | seat.last_activity_at = this.config.endDate ; 78 | seat.created_at = this.config.startDate; 79 | seat.updated_at = this.config.endDate; 80 | 81 | } 82 | 83 | return seat; 84 | }); 85 | return newSeatsResponse.seats; 86 | } 87 | } 88 | 89 | export { MockSeatsGenerator }; -------------------------------------------------------------------------------- /backend/src/__tests__/__mock__/survey-gen/runSurveyGenerator.ts: -------------------------------------------------------------------------------- 1 | // runSurveyGenerator.ts - a script to run the survey generator and output the generated data via "npx tsx src/__tests__/__mock__/survey-gen/runSurveyGenerator.ts" 2 | import { MockSurveyGenerator } from './mockSurveyGenerator.js'; 3 | import { SurveyMockConfig } from '../types.js'; 4 | import surveyExample from './exampleSurvey.json' assert { type: 'json' }; 5 | import { fileURLToPath } from 'url'; 6 | import { dirname, join } from 'path'; 7 | import Database from '../../../database.js'; 8 | import 'dotenv/config'; 9 | 10 | // Convert the URL to a file path and get the directory name 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = dirname(__filename); 13 | 14 | const mockConfig: SurveyMockConfig = { 15 | startDate: new Date('2024-12-30'), 16 | endDate: new Date('2024-12-31'), 17 | userIds: ['nathos', 'mattg57', '04Surf','azizshamim','beardofedu','austenstone','arfon', 'kyanny', 'amandahmt', 'jefeish', 'sdehm', 'dgreif', 'matthewisabel', '2percentsilk', 'mariorod','bekahwhittle','AdamTheCreator','bevns','mfilosa'], 18 | orgs: ['octodemo'], 19 | repos: ['github-value', 'github-value-chart'], 20 | reasons: ['Prefilled code blocks for me', 'Helped me generate test data', 'I was able to build a new feature without learning the framework'], 21 | timeUsedFors: ['Faster PRs', 'Faster Releases', 'Tech Debt, Reduce Defects and Vulns'] 22 | }; 23 | 24 | // Load template data from exampleSurvey.json 25 | const templateData: any = surveyExample; 26 | 27 | const runSurveys = async () => { 28 | console.log('Starting to generate surveys...'); 29 | try { 30 | const generator = new MockSurveyGenerator(mockConfig, templateData); 31 | const surveys = await generator.generateSurveys(); 32 | console.log('Successfully generated surveys:', surveys.surveys.length); 33 | return surveys; 34 | } catch (error) { 35 | console.error('Error generating surveys:', error); 36 | throw error; // Re-throw the error after logging it 37 | } 38 | } 39 | 40 | const runSurveysForDate = async (datetime: Date) => { 41 | mockConfig.startDate = datetime; 42 | mockConfig.endDate = datetime; 43 | //add other configuration as needed 44 | //console.log('Starting to generate surveys...'); 45 | try { 46 | const generator = new MockSurveyGenerator(mockConfig, templateData); 47 | const surveys = await generator.generateSurveys(); 48 | console.log('Successfully generated surveys:', surveys.surveys.length); 49 | return surveys; 50 | } catch (error) { 51 | console.error('Error generating surveys:', error); 52 | throw error; // Re-throw the error after logging it 53 | } 54 | } 55 | 56 | // let db: Database | null = null; 57 | // try { 58 | // const mongoUri = process.env.MONGODB_URI; 59 | // if (!mongoUri) { 60 | // throw new Error('MONGODB_URI is not defined'); 61 | // } 62 | // db = new Database(mongoUri); 63 | // await db.connect(); 64 | // let surveys = await runSurveys(); 65 | // console.log('surveys:', surveys); 66 | 67 | // } catch (error) { 68 | // console.error('Error connecting to the database:', error); 69 | // } 70 | 71 | // Export function 72 | export { runSurveys, runSurveysForDate }; -------------------------------------------------------------------------------- /frontend/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { NewCopilotSurveyComponent } from './main/copilot/copilot-surveys/new-copilot-survey/new-copilot-survey.component'; 3 | import { CopilotSurveysComponent } from './main/copilot/copilot-surveys/copilot-surveys.component'; 4 | import { SettingsComponent } from './main/settings/settings.component'; 5 | import { InstallComponent } from './install/install.component'; 6 | import { SetupStatusGuard } from './guards/setup.guard'; 7 | import { MainComponent } from './main/main.component'; 8 | import { CopilotDashboardComponent } from './main/copilot/copilot-dashboard/dashboard.component'; 9 | import { CopilotValueComponent } from './main/copilot/copilot-value/value.component'; 10 | import { CopilotMetricsComponent } from './main/copilot/copilot-metrics/copilot-metrics.component'; 11 | import { CopilotSeatsComponent } from './main/copilot/copilot-seats/copilot-seats.component'; 12 | import { DbLoadingComponent } from './database/db-loading.component'; 13 | import { CopilotSurveyComponent } from './main/copilot/copilot-surveys/copilot-survey-details/copilot-survey.component'; 14 | import { CopilotSeatComponent } from './main/copilot/copilot-seats/copilot-seat/copilot-seat.component'; 15 | import { DatabaseComponent } from './database/database.component'; 16 | import { ErrorComponent } from './error/error.component'; 17 | import { CopilotValueModelingComponent } from './main/copilot/copilot-value-modeling/copilot-value-modeling.component'; 18 | import { MainDiagnosticsComponent } from './main/diagnostics/main-diagnostics.component'; 19 | 20 | export const routes: Routes = [ 21 | { path: 'setup', component: InstallComponent }, 22 | { path: 'setup/loading', component: DbLoadingComponent }, 23 | { path: 'setup/db', component: DatabaseComponent }, 24 | { path: 'error', component: ErrorComponent }, 25 | { 26 | path: '', 27 | component: MainComponent, 28 | canActivate: [SetupStatusGuard], 29 | canActivateChild: [SetupStatusGuard], 30 | children: [ 31 | { path: 'copilot', component: CopilotDashboardComponent, title: 'Dashboard' }, 32 | { path: 'copilot/value', component: CopilotValueComponent, title: 'Value' }, 33 | { path: 'copilot/metrics', component: CopilotMetricsComponent, title: 'Metrics' }, 34 | { path: 'copilot/seats', component: CopilotSeatsComponent, title: 'Seats' }, 35 | { path: 'copilot/seats/:id', component: CopilotSeatComponent, title: 'Seat' }, 36 | { path: 'copilot/surveys', component: CopilotSurveysComponent, title: 'Surveys' }, 37 | { path: 'copilot/surveys/new', component: NewCopilotSurveyComponent, title: 'New Survey' }, 38 | { path: 'copilot/surveys/new/:id', component: NewCopilotSurveyComponent, title: 'New Survey' }, 39 | { path: 'copilot/surveys/:id', component: CopilotSurveyComponent, title: 'Survey' }, 40 | { path: 'copilot/value-modeling', component: CopilotValueModelingComponent, title: 'Value Modeling' }, 41 | { path: 'settings', component: SettingsComponent, title: 'Settings' }, 42 | { path: 'diagnostics', component: MainDiagnosticsComponent, title: 'Diagnostics' }, 43 | { path: '', redirectTo: 'copilot', pathMatch: 'full' } 44 | ] 45 | }, 46 | { path: '**', redirectTo: '' } 47 | ]; 48 | -------------------------------------------------------------------------------- /frontend/src/styles.scss: -------------------------------------------------------------------------------- 1 | // Custom Theming for Angular Material 2 | // For more information: https://material.angular.io/guide/theming 3 | @use '@angular/material' as mat; 4 | // Plus imports for other components in your app. 5 | @use 'm3-theme' as theme; // Import the m3 theme 6 | 7 | // Include the common styles for Angular Material. We include this here so that you only 8 | // have to load a single css file for Angular Material in your app. 9 | // Be sure that you only ever include this mixin once! 10 | @include mat.elevation-classes(); 11 | @include mat.app-background(); 12 | 13 | // Include theme styles for core and each component used in your app. 14 | // Alternatively, you can import and @include the theme mixins for each component 15 | // that you are using. 16 | :root { 17 | // Apply the dark theme by default 18 | @include mat.elevation-classes(); 19 | @include mat.app-background(); 20 | @include mat.all-component-themes(theme.$dark-theme); 21 | @include mat.system-level-colors(theme.$dark-theme); 22 | @include mat.system-level-typography(theme.$dark-theme); 23 | 24 | // Apply the light theme only when the user prefers light themes. 25 | .light-theme { 26 | // Use the `-color` mixins to only apply color styles without reapplying the same 27 | // typography and density styles. 28 | @include mat.all-component-colors(theme.$light-theme); 29 | @include mat.system-level-colors(theme.$light-theme); 30 | a:not(.mat-mdc-button-base) { 31 | color: mat.get-theme-color(theme.$light-theme, primary, 40); 32 | } 33 | } 34 | 35 | .dark-theme { 36 | color-scheme: dark; 37 | // @include mat.strong-focus-indicators-color($dark-theme); 38 | a:not(.mat-mdc-button-base) { 39 | color: mat.get-theme-color(theme.$dark-theme, primary, 80); 40 | } 41 | } 42 | } 43 | 44 | // Comment out the line below if you want to use the pre-defined typography utility classes. 45 | // For more information: https://material.angular.io/guide/typography#using-typography-styles-in-your-application. 46 | @include mat.typography-hierarchy(theme.$light-theme); 47 | 48 | // Comment out the line below if you want to use the deprecated `color` inputs. 49 | // @include mat.color-variants-backwards-compatibility($light-theme); 50 | /* You can add global styles to this file, and also import other style files */ 51 | 52 | html, 53 | body { 54 | height: 100%; 55 | } 56 | 57 | body { 58 | margin: 0; 59 | font-family: Roboto, "Helvetica Neue", sans-serif; 60 | } 61 | 62 | a { 63 | text-decoration: none; 64 | } 65 | 66 | .noselect { 67 | -webkit-touch-callout: none; 68 | -webkit-user-select: none; 69 | -khtml-user-select: none; 70 | -moz-user-select: none; 71 | -ms-user-select: none; 72 | user-select: none; 73 | } 74 | 75 | .page-container { 76 | padding: 0 1rem; 77 | box-sizing: border-box; 78 | max-width: 1250px; 79 | margin: auto; 80 | width: 100%; 81 | margin-bottom: 48px; 82 | } 83 | 84 | .page-header { 85 | display: flex; 86 | } 87 | 88 | .spacer { 89 | flex: 1 1 auto; 90 | } 91 | 92 | highcharts-chart { 93 | display: block; 94 | width: 100% !important; 95 | height: 100% !important; 96 | } 97 | 98 | :root { 99 | --error: #93000a; /* Error */ 100 | --success: #388e3c; /* Green */ 101 | --warning: #f57c00; /* Orange/Yellow */ 102 | } --------------------------------------------------------------------------------