Skip to content

Commit 95bd78c

Browse files
Copilotkarpikpl
andauthored
feat: configurable tabs via NUXT_PUBLIC_HIDDEN_TABS + auto-hide teams tab without historical mode + bump version to 3.1.0 (#337)
* Initial plan * feat: add configurable tabs via NUXT_PUBLIC_HIDDEN_TABS env var, bump version to 3.1.0 Agent-Logs-Url: https://github.com/github-copilot-resources/copilot-metrics-viewer/sessions/a8b0e8fc-911c-4740-a466-daf75a648226 Co-authored-by: karpikpl <3539908+karpikpl@users.noreply.github.com> * feat: auto-hide teams tab when NUXT_PUBLIC_ENABLE_HISTORICAL_MODE is not true Agent-Logs-Url: https://github.com/github-copilot-resources/copilot-metrics-viewer/sessions/fa34e537-fd88-41a8-b723-5a3fd7a63a43 Co-authored-by: karpikpl <3539908+karpikpl@users.noreply.github.com> * docs: document NUXT_PUBLIC_HIDDEN_TABS and NUXT_PUBLIC_ENABLE_HISTORICAL_MODE in README Agent-Logs-Url: https://github.com/github-copilot-resources/copilot-metrics-viewer/sessions/270dc16f-a139-4703-b63b-f86ba1db0c21 Co-authored-by: karpikpl <3539908+karpikpl@users.noreply.github.com> * refactor: extract tab filtering into tabUtils.ts so tests exercise real production code Agent-Logs-Url: https://github.com/github-copilot-resources/copilot-metrics-viewer/sessions/fa007487-4ff9-4721-9a8e-c7bb39101019 Co-authored-by: karpikpl <3539908+karpikpl@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: karpikpl <3539908+karpikpl@users.noreply.github.com> Co-authored-by: Piotr Karpala <piotrkarpala@microsoft.com>
1 parent c8aa964 commit 95bd78c

10 files changed

Lines changed: 229 additions & 5 deletions

File tree

.env

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,9 @@ NUXT_OAUTH_GITHUB_CLIENT_SECRET=
2929
# HTTP_PROXY=http://proxy.company.com:8080
3030

3131
ENABLE_HISTORICAL_MODE=true
32-
NUXT_PUBLIC_USE_NEW_API=true
32+
NUXT_PUBLIC_USE_NEW_API=true
33+
34+
# Comma-separated list of tab names to hide from the dashboard UI.
35+
# Available tabs: languages, editors, copilot chat, agent activity, pull requests, github.com, seat analysis, user metrics, api response
36+
# Example: NUXT_PUBLIC_HIDDEN_TABS=api response,agent activity
37+
NUXT_PUBLIC_HIDDEN_TABS=

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,8 @@ Public variables:
202202
- `NUXT_PUBLIC_GITHUB_ENT`
203203
- `NUXT_PUBLIC_GITHUB_ORG`
204204
- `NUXT_PUBLIC_GITHUB_TEAM`
205+
- `NUXT_PUBLIC_HIDDEN_TABS`
206+
- `NUXT_PUBLIC_ENABLE_HISTORICAL_MODE`
205207

206208
can be overridden by route parameters, e.g.
207209
- `http://localhost:3000/enterprises/octo-demo-ent`
@@ -285,6 +287,28 @@ Variables required for GitHub Auth are:
285287
>[!WARNING]
286288
> Only users with permissions (scopes listed in [NUXT_GITHUB_TOKEN](#NUXT_GITHUB_TOKEN)) can view copilot metrics, GitHub uses the authenticated users permissions to make API calls for data.
287289
290+
#### NUXT_PUBLIC_HIDDEN_TABS
291+
292+
Comma-separated list of dashboard tab names to hide. Applies at startup without requiring a rebuild — useful for pre-built Docker deployments. The filter is case-insensitive and trims surrounding whitespace.
293+
294+
Available tab names: `languages`, `editors`, `copilot chat`, `agent activity`, `pull requests`, `github.com`, `seat analysis`, `user metrics`, `api response`
295+
296+
````
297+
# Hide the "Agent Activity" and "API Response" tabs
298+
NUXT_PUBLIC_HIDDEN_TABS=agent activity,api response
299+
````
300+
301+
#### NUXT_PUBLIC_ENABLE_HISTORICAL_MODE
302+
303+
Default is `false`. When set to `true`, the application uses a PostgreSQL database (configured via `DATABASE_URL`) to store and query historical Copilot metrics.
304+
305+
> [!IMPORTANT]
306+
> The **Teams** tab is automatically hidden when `NUXT_PUBLIC_ENABLE_HISTORICAL_MODE` is not `true`. Team-level metrics are derived from per-user daily records in the database (`user_day_metrics` table). Without the database, the teams comparison tab would display identical org-wide data for every team.
307+
308+
````
309+
NUXT_PUBLIC_ENABLE_HISTORICAL_MODE=false
310+
````
311+
288312
#### HTTP_PROXY
289313

290314
Solution supports HTTP Proxy settings when running in corporate environment. Simply set `HTTP_PROXY` environment variable.

app/components/MainComponent.vue

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ import DateRangeSelector from './DateRangeSelector.vue'
159159
import UserMetricsViewer from './UserMetricsViewer.vue'
160160
import { Options } from '@/model/Options';
161161
import { useRoute } from 'vue-router';
162+
import { applyHiddenTabs, applyHistoricalModeFilter } from '@/utils/tabUtils';
162163
163164
export default defineNuxtComponent({
164165
name: 'MainComponent',
@@ -349,6 +350,12 @@ export default defineNuxtComponent({
349350
}
350351
351352
this.config = useRuntimeConfig();
353+
354+
// Auto-hide teams tab when historical mode is disabled (team metrics require DB)
355+
this.tabItems = applyHistoricalModeFilter(this.tabItems, this.config.public.enableHistoricalMode as boolean | string);
356+
357+
// Filter out hidden tabs based on NUXT_PUBLIC_HIDDEN_TABS environment variable
358+
this.tabItems = applyHiddenTabs(this.tabItems, this.config.public.hiddenTabs as string);
352359
},
353360
async mounted() {
354361
// Load initial data

app/utils/tabUtils.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Filters tab items based on the NUXT_PUBLIC_HIDDEN_TABS configuration.
3+
* The config value is a comma-separated list of tab names (case-insensitive).
4+
*/
5+
export function applyHiddenTabs(tabItems: string[], hiddenTabsConfig: string): string[] {
6+
if (!hiddenTabsConfig) return tabItems
7+
const hiddenTabs = hiddenTabsConfig.split(',').map((t: string) => t.trim().toLowerCase()).filter(Boolean)
8+
if (hiddenTabs.length === 0) return tabItems
9+
return tabItems.filter((tab: string) => !hiddenTabs.includes(tab.toLowerCase()))
10+
}
11+
12+
/**
13+
* Auto-hides the "teams" tab when historical mode is disabled.
14+
* Team metrics require the user_day_metrics DB table; without it the teams
15+
* tab falls back to org-wide data and shows identical (incorrect) data for every team.
16+
*/
17+
export function applyHistoricalModeFilter(tabItems: string[], enableHistoricalMode: boolean | string): string[] {
18+
const historicalMode = enableHistoricalMode === true || enableHistoricalMode === 'true'
19+
if (!historicalMode && tabItems.includes('teams')) {
20+
return tabItems.filter((t: string) => t !== 'teams')
21+
}
22+
return tabItems
23+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { expect, test } from '@playwright/test';
2+
import { DashboardPage } from './pages/DashboardPage';
3+
4+
test.describe('Configurable Tabs', () => {
5+
test.describe.configure({ mode: 'serial' });
6+
7+
let dashboard: DashboardPage;
8+
9+
test.beforeAll(async ({ browser }) => {
10+
const page = await browser.newPage();
11+
await page.goto('/orgs/octo-demo-org?mock=true');
12+
13+
dashboard = new DashboardPage(page);
14+
15+
// wait for the data
16+
await dashboard.expectMetricLabelsVisible();
17+
});
18+
19+
test.afterAll(async () => {
20+
await dashboard?.close();
21+
});
22+
23+
test('should hide the "agent activity" tab when configured via NUXT_PUBLIC_HIDDEN_TABS', async () => {
24+
const agentActivityTab = dashboard.page.getByRole('tab', { name: 'agent activity' });
25+
await expect(agentActivityTab).not.toBeVisible();
26+
});
27+
28+
test('should still show other tabs that are not hidden', async () => {
29+
await expect(dashboard.languagesTabLink).toBeVisible();
30+
await expect(dashboard.editorsTabLink).toBeVisible();
31+
await expect(dashboard.copilotChatTabLink).toBeVisible();
32+
await expect(dashboard.seatAnalysisTabLink).toBeVisible();
33+
await expect(dashboard.userMetricsTabLink).toBeVisible();
34+
await expect(dashboard.apiResponseTabLink).toBeVisible();
35+
});
36+
37+
test('should show the teams tab when NUXT_PUBLIC_ENABLE_HISTORICAL_MODE is true', async () => {
38+
// Teams tab requires historical mode (DB); it is visible here because
39+
// NUXT_PUBLIC_ENABLE_HISTORICAL_MODE=true is set in playwright.config.ts
40+
await expect(dashboard.teamsTabLink).toBeVisible();
41+
});
42+
43+
test('should still show the organization scope tab', async () => {
44+
await expect(dashboard.orgTabLink).toBeVisible();
45+
});
46+
});

nuxt.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ export default defineNuxtConfig({
107107
deployInfo: '',
108108
// New API migration flags
109109
useLegacyApi: false, // Set true to use deprecated /copilot/metrics API (USE_LEGACY_API)
110-
enableHistoricalMode: false // Enable storage-backed historical queries (NUXT_PUBLIC_ENABLE_HISTORICAL_MODE)
110+
enableHistoricalMode: false, // Enable storage-backed historical queries (NUXT_PUBLIC_ENABLE_HISTORICAL_MODE)
111+
hiddenTabs: '' // Comma-separated list of tab names to hide (NUXT_PUBLIC_HIDDEN_TABS)
111112
}
112113
}
113114
})

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "copilot-metrics-viewer",
3-
"version": "3.0.4",
3+
"version": "3.1.0",
44
"private": true,
55
"type": "module",
66
"scripts": {

playwright.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import { isCI } from 'std-env'
66
// set the runtimeConfig values for the test
77
process.env.NUXT_PUBLIC_USING_GITHUB_AUTH = 'false'
88
process.env.NUXT_PUBLIC_IS_DATA_MOCKED = 'true'
9+
// Enable historical mode for e2e tests so the teams tab is visible for teams-comparison tests
10+
process.env.NUXT_PUBLIC_ENABLE_HISTORICAL_MODE = 'true'
11+
// Hide 'agent activity' tab to test the configurable tabs feature
12+
process.env.NUXT_PUBLIC_HIDDEN_TABS = 'agent activity'
913

1014
export default defineConfig<ConfigOptions>({
1115
testDir: 'e2e-tests',
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { describe, test, expect } from 'vitest'
2+
import { applyHiddenTabs, applyHistoricalModeFilter } from '../app/utils/tabUtils'
3+
4+
describe('MainComponent hidden tabs filtering', () => {
5+
const ALL_BASE_TABS = ['languages', 'editors', 'copilot chat', 'agent activity', 'pull requests', 'github.com', 'seat analysis', 'user metrics', 'api response']
6+
7+
test('should keep all tabs when hiddenTabs is empty', () => {
8+
const tabs = ['organization', 'teams', ...ALL_BASE_TABS]
9+
const result = applyHiddenTabs(tabs, '')
10+
expect(result).toEqual(tabs)
11+
})
12+
13+
test('should hide a single tab when configured', () => {
14+
const tabs = ['organization', 'teams', ...ALL_BASE_TABS]
15+
const result = applyHiddenTabs(tabs, 'api response')
16+
expect(result).not.toContain('api response')
17+
expect(result).toContain('languages')
18+
expect(result).toContain('editors')
19+
expect(result).toContain('seat analysis')
20+
})
21+
22+
test('should hide multiple tabs when configured with comma-separated list', () => {
23+
const tabs = ['organization', 'teams', ...ALL_BASE_TABS]
24+
const result = applyHiddenTabs(tabs, 'api response, agent activity, pull requests')
25+
expect(result).not.toContain('api response')
26+
expect(result).not.toContain('agent activity')
27+
expect(result).not.toContain('pull requests')
28+
expect(result).toContain('languages')
29+
expect(result).toContain('copilot chat')
30+
expect(result).toContain('seat analysis')
31+
expect(result).toContain('user metrics')
32+
})
33+
34+
test('should be case-insensitive when matching tab names', () => {
35+
const tabs = ['organization', ...ALL_BASE_TABS]
36+
const result = applyHiddenTabs(tabs, 'API Response, Seat Analysis')
37+
expect(result).not.toContain('api response')
38+
expect(result).not.toContain('seat analysis')
39+
expect(result).toContain('languages')
40+
})
41+
42+
test('should not hide scope tabs (organization, enterprise, team)', () => {
43+
const tabs = ['organization', 'teams', ...ALL_BASE_TABS]
44+
// When hidden tabs config only targets content tabs, scope tabs remain
45+
const result = applyHiddenTabs(tabs, 'api response')
46+
expect(result).toContain('organization')
47+
expect(result).toContain('teams')
48+
})
49+
50+
test('should handle whitespace in the hidden tabs config', () => {
51+
const tabs = ['organization', ...ALL_BASE_TABS]
52+
const result = applyHiddenTabs(tabs, ' api response , agent activity ')
53+
expect(result).not.toContain('api response')
54+
expect(result).not.toContain('agent activity')
55+
expect(result).toContain('languages')
56+
})
57+
58+
test('should return empty array if all tabs are hidden', () => {
59+
const tabs = ['languages', 'editors']
60+
const result = applyHiddenTabs(tabs, 'languages, editors')
61+
expect(result).toHaveLength(0)
62+
})
63+
64+
test('should ignore unknown tab names in hiddenTabs config', () => {
65+
const tabs = ['organization', ...ALL_BASE_TABS]
66+
const result = applyHiddenTabs(tabs, 'nonexistent-tab, api response')
67+
expect(result).not.toContain('api response')
68+
expect(result).toContain('languages')
69+
// Length should be original - 1 (only api response removed)
70+
expect(result).toHaveLength(tabs.length - 1)
71+
})
72+
73+
// Historical mode tests
74+
describe('auto-hide teams tab based on historical mode', () => {
75+
test('should hide teams tab when historical mode is false', () => {
76+
const tabs = ['organization', 'teams', ...ALL_BASE_TABS]
77+
const result = applyHistoricalModeFilter(tabs, false)
78+
expect(result).not.toContain('teams')
79+
expect(result).toContain('organization')
80+
expect(result).toContain('languages')
81+
})
82+
83+
test('should hide teams tab when historical mode is string "false"', () => {
84+
const tabs = ['organization', 'teams', ...ALL_BASE_TABS]
85+
const result = applyHistoricalModeFilter(tabs, 'false')
86+
expect(result).not.toContain('teams')
87+
})
88+
89+
test('should keep teams tab when historical mode is true', () => {
90+
const tabs = ['organization', 'teams', ...ALL_BASE_TABS]
91+
const result = applyHistoricalModeFilter(tabs, true)
92+
expect(result).toContain('teams')
93+
})
94+
95+
test('should keep teams tab when historical mode is string "true"', () => {
96+
const tabs = ['organization', 'teams', ...ALL_BASE_TABS]
97+
const result = applyHistoricalModeFilter(tabs, 'true')
98+
expect(result).toContain('teams')
99+
})
100+
101+
test('should not affect other tabs when hiding teams', () => {
102+
const tabs = ['organization', 'teams', ...ALL_BASE_TABS]
103+
const result = applyHistoricalModeFilter(tabs, false)
104+
expect(result).toHaveLength(tabs.length - 1)
105+
ALL_BASE_TABS.forEach(tab => expect(result).toContain(tab))
106+
})
107+
108+
test('should not modify tabs when teams is not present', () => {
109+
const tabs = ['team', ...ALL_BASE_TABS]
110+
const result = applyHistoricalModeFilter(tabs, false)
111+
expect(result).toEqual(tabs)
112+
})
113+
})
114+
})

0 commit comments

Comments
 (0)