Skip to content

Commit 6de1abe

Browse files
authored
Merge pull request #49 from DevOps-zhuang/feature/SeatAnalysis
add seat analysis support for Github Enterprise, since newly released…
2 parents 60a46f4 + 081db07 commit 6de1abe

File tree

8 files changed

+75
-62
lines changed

8 files changed

+75
-62
lines changed

README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,19 +71,18 @@ The language breakdown analysis tab also displays a table showing the Accepted P
7171
4. **Total Active Copilot Chat Users:** a bar chart that illustrates the total number of users who have actively interacted with Copilot over the past 28 days.
7272

7373
## Seat Analysis
74+
<p align="center">
75+
<img width="800" alt="image" src="https://github.com/github-copilot-resources/copilot-metrics-viewer/assets/54096296/51747194-df30-4bfb-8849-54a0510fffcb">
76+
</p>
77+
1. **Total Assigned:** This metric represents the total number of Copilot seats assigned within current organization/enterprise.
7478

75-
![image](https://github.com/DevOps-zhuang/copilot-metrics-viewer/assets/54096296/d1fa9d1d-4fab-4e87-84ba-7be189dd4dd0)
76-
77-
1. **Total Assigned:** This metric represents the total number of Copilot seats assigned within current organization.
78-
79-
2. **Assigned But Never Used:** This metric shows seats that were assigned but never within the current organization. The assigned timestamp is also displayed in the below chart.
79+
2. **Assigned But Not Used (in the last 28 days):** This metric shows seats that were assigned but not used during the last 28 days within the current organization/enterprise. The assigned timestamp is also displayed in the chart.”
8080

8181
3. **No Activity in the Last 7 days:** never used seats or seats used, but with no activity in the past 7 days.
8282

8383
4. **No Activity in the last 7 days (including never used seats):** a table to display seats that have had no activity in the past 7 days, ordered by the date of last activity. Seats that were used earlier are displayed at the top.
8484

8585

86-
8786
## Setup instructions
8887

8988
In the `.env` file, you can configure several environment variables that control the behavior of the application.

src/api/ExtractSeats.ts

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,64 @@ import enterpriseMockedResponse_seats from '../assets/enterprise_response_sample
88
export const getSeatsApi = async (): Promise<Seat[]> => {
99
const perPage = 50;
1010
let page = 1;
11+
let seatUrl = `https://api.github.com/`;
1112
let seatsData: Seat[] = [];
1213

1314
let response;
14-
if (process.env.VUE_APP_SCOPE !== "organization") {
15-
// when the scope is not organization, return seatsData,by default it will return empty array
16-
return seatsData;
17-
}
18-
else{
19-
if (process.env.VUE_APP_MOCKED_DATA === "true") {
20-
response = organizationMockedResponse_seats;
15+
16+
17+
if (process.env.VUE_APP_MOCKED_DATA === "true") {
18+
console.log("Using mock data. Check VUE_APP_MOCKED_DATA variable.");
19+
if (process.env.VUE_APP_SCOPE === "organization") {
20+
response = organizationMockedResponse_seats;
21+
}
22+
else if (process.env.VUE_APP_SCOPE === "enterprise") {
23+
response = enterpriseMockedResponse_seats;
24+
}
25+
else {
26+
throw new Error(`Invalid VUE_APP_SCOPE value: ${process.env.VUE_APP_SCOPE}. Expected "organization" or "enterprise".`);
27+
}
2128
seatsData = seatsData.concat(response.seats.map((item: any) => new Seat(item)));
22-
}
23-
else if (process.env.VUE_APP_MOCKED_DATA === "false") {
29+
return seatsData;
30+
}
31+
else {
32+
// if VUE_APP_GITHUB_TOKEN is not set, throw an error
33+
if (!process.env.VUE_APP_GITHUB_TOKEN) {
34+
throw new Error("VUE_APP_GITHUB_TOKEN environment variable is not set.");
35+
return seatsData;
36+
}
37+
else if (process.env.VUE_APP_SCOPE === "organization") {
38+
seatUrl=seatUrl+`orgs/${process.env.VUE_APP_GITHUB_ORG}/copilot/billing/seats`;
39+
}
40+
else if (process.env.VUE_APP_SCOPE === "enterprise") {
41+
seatUrl=seatUrl+`enterprises/${process.env.VUE_APP_GITHUB_ENT}/copilot/billing/seats`;
42+
}
43+
else {
44+
throw new Error(`Invalid VUE_APP_SCOPE value: ${process.env.VUE_APP_SCOPE}. Expected "organization" or "enterprise".`);
45+
return seatsData;
46+
}
47+
2448
// Fetch the first page to get the total number of seats
25-
response = await axios.get(`https://api.github.com/orgs/${process.env.VUE_APP_GITHUB_ORG}/copilot/billing/seats`, {
49+
response = await axios.get(seatUrl, {
50+
headers: {
51+
Accept: "application/vnd.github+json",
52+
Authorization: `Bearer ${process.env.VUE_APP_GITHUB_TOKEN}`,
53+
"X-GitHub-Api-Version": "2022-11-28",
54+
},
55+
params: {
56+
per_page: perPage,
57+
page: page
58+
}
59+
});
60+
61+
seatsData = seatsData.concat(response.data.seats.map((item: any) => new Seat(item)));
62+
// Calculate the total pages
63+
const totalSeats = response.data.total_seats;
64+
const totalPages = Math.ceil(totalSeats / perPage);
65+
66+
// Fetch the remaining pages
67+
for (page = 2; page <= totalPages; page++) {
68+
response = await axios.get(seatUrl, {
2669
headers: {
2770
Accept: "application/vnd.github+json",
2871
Authorization: `Bearer ${process.env.VUE_APP_GITHUB_TOKEN}`,
@@ -33,29 +76,8 @@ export const getSeatsApi = async (): Promise<Seat[]> => {
3376
page: page
3477
}
3578
});
36-
3779
seatsData = seatsData.concat(response.data.seats.map((item: any) => new Seat(item)));
38-
// Calculate the total pages
39-
const totalSeats = response.data.total_seats;
40-
const totalPages = Math.ceil(totalSeats / perPage);
41-
42-
// Fetch the remaining pages
43-
for (page = 2; page <= totalPages; page++) {
44-
response = await axios.get(`https://api.github.com/orgs/${process.env.VUE_APP_GITHUB_ORG}/copilot/billing/seats`, {
45-
headers: {
46-
Accept: "application/vnd.github+json",
47-
Authorization: `Bearer ${process.env.VUE_APP_GITHUB_TOKEN}`,
48-
"X-GitHub-Api-Version": "2022-11-28",
49-
},
50-
params: {
51-
per_page: perPage,
52-
page: page
53-
}
54-
});
55-
56-
seatsData = seatsData.concat(response.data.seats.map((item: any) => new Seat(item)));
57-
} //end of else if (process.env.VUE_APP_MOCKED_DATA === "false")
58-
} //end of else if (process.env.VUE_APP_SCOPE !== "organization")
59-
return seatsData;
60-
}
61-
}
80+
}
81+
return seatsData;
82+
}
83+
}

src/assets/enterprise_response_sample_seats.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"last_activity_at": "2021-10-14T00:53:32-06:00",
99
"last_activity_editor": "vscode/1.77.3/copilot/1.86.82",
1010
"assignee": {
11-
"login": "octocat",
11+
"login": "octocat_byEnterprise",
1212
"id": 1,
1313
"node_id": "MDQ6VXNlcjE=",
1414
"avatar_url": "https://github.com/images/error/octocat_happy.gif",

src/assets/organization_response_sample_seats.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"last_activity_at": "2021-10-14T00:53:32-06:00",
99
"last_activity_editor": "vscode/1.77.3/copilot/1.86.82",
1010
"assignee": {
11-
"login": "octocat",
11+
"login": "octocat_org",
1212
"id": 1,
1313
"node_id": "MDQ6VXNlcjE=",
1414
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
@@ -50,7 +50,7 @@
5050
"last_activity_at": "2021-10-13T00:53:32-06:00",
5151
"last_activity_editor": "vscode/1.77.3/copilot/1.86.82",
5252
"assignee": {
53-
"login": "octokitten",
53+
"login": "octokitten_org",
5454
"id": 1,
5555
"node_id": "MDQ76VNlcjE=",
5656
"avatar_url": "https://github.com/images/error/octokitten_happy.gif",

src/components/ApiResponse.vue

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
</div>
1414

1515
<br><br>
16-
<div v-if="vueAppScope === 'organization'">
16+
1717
<v-card max-height="575px" class="overflow-y-auto">
1818
<pre ref="jsonText">{{ JSON.stringify(seats, null, 2) }}</pre>
1919
</v-card>
@@ -24,7 +24,6 @@
2424
<div v-if="showSeatMessage" :class="{'copy-message': true, 'error': isError}">{{ message }}</div>
2525
</transition>
2626
</div>
27-
</div>
2827
</v-container>
2928
</template>
3029

src/components/MainComponent.vue

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,7 @@
3131
<BreakdownComponent v-if="item === 'languages'" :metrics="metrics" :breakdownKey="'language'"/>
3232
<BreakdownComponent v-if="item === 'editors'" :metrics="metrics" :breakdownKey="'editor'"/>
3333
<CopilotChatViewer v-if="item === 'copilot chat'" :metrics="metrics" />
34-
<div v-if="isScopeOrganization">
3534
<SeatsAnalysisViewer v-if="item === 'seat analysis'" :seats="seats" />
36-
</div>
3735
<ApiResponse v-if="item === 'api response'" :metrics="metrics" :seats="seats" />
3836
</v-card>
3937
</v-window-item>
@@ -94,23 +92,14 @@ export default defineComponent({
9492
},
9593
data () {
9694
return {
97-
tabItems: ['languages', 'editors', 'copilot chat', 'api response'],
95+
tabItems: ['languages', 'editors', 'copilot chat','seat analysis' , 'api response'],
9896
tab: null
9997
}
10098
},
10199
created() {
102100
if(this.itemName !== 'invalid'){
103101
this.tabItems.unshift(this.itemName);
104102
}
105-
if (process.env.VUE_APP_SCOPE === 'organization') {
106-
// get the last item in the array,which is 'api response'
107-
//and add 'seat analysis' before it
108-
let lastItem = this.tabItems.pop();
109-
this.tabItems.push('seat analysis');
110-
if (lastItem) {
111-
this.tabItems.push(lastItem);
112-
}
113-
}
114103
},
115104
setup() {
116105
const metricsReady = ref(false);

src/components/SeatsAnalysisViewer.vue

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
<v-card-item class="d-flex justify-center align-center">
1818
<div class="tiles-text">
1919
<div class="text-overline mb-1" style="visibility: hidden;">filler</div>
20-
<div class="text-h6 mb-1">Assigned But Never Used</div>
20+
<div class="text-h6 mb-1">Assigned But Not Used</div>
2121
<div class="text-caption">
22-
No show seats
22+
No show seats in the past 28 days
2323
</div>
2424
<p class="text-h4">{{ NoshowSeats.length }}</p>
2525
</div>
@@ -50,6 +50,7 @@
5050
<tr>
5151
<td>{{ item.login }}</td>
5252
<td>{{ item.id }}</td>
53+
<td>{{ item.team }}</td>
5354
<td>{{ item.created_at }}</td>
5455
<td>{{ item.last_activity_at }}</td>
5556
<td>{{ item.last_activity_editor }}</td>
@@ -103,7 +104,8 @@ data() {
103104
headers: [
104105
{ title: 'Login', key: 'login' },
105106
{ title: 'GitHub ID', key: 'id' },
106-
{ title: 'Assigned to the Organization At', key: 'created_at' },
107+
{ title: 'Assigning team', key: 'team' },
108+
{ title: 'Assigned time', key: 'created_at' },
107109
{ title: 'Last Activity At', key: 'last_activity_at' },
108110
{ title: 'Last Activity Editor', key: 'last_activity_editor' },
109111
],

src/model/Seat.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
export class Seat {
22
login: string;
33
id: number;
4+
team: string;
45
created_at: string;
56
last_activity_at: string;
67
last_activity_editor: string;
78

89
constructor(data: any) {
910
this.login = data.assignee.login;
1011
this.id = data.assignee.id;
11-
this.created_at = data.created_at;
12+
this.team = data.assigning_team ? data.assigning_team.name : '';
13+
this.created_at = data.created_at;
1214
this.last_activity_at = data.last_activity_at;
1315
this.last_activity_editor = data.last_activity_editor;
1416
}

0 commit comments

Comments
 (0)