Skip to content
Merged
Binary file added docs/pr-screenshots/observe-plugin-adoption.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,7 @@
"get": "get",
"get-started": "GET STARTED",
"github-stars-trend": "GitHub Stars Trend",
"global": "Global",
"go-back": "Go back",
"good": "Good",
"granted-at": "Granted At",
Expand Down Expand Up @@ -1503,6 +1504,18 @@
"native-observe-open-logs": "Open logs",
"native-observe-overview": "All versions summary",
"native-observe-overview-help": "Cards, charts, and action breakdown aggregate every reported native app version during {period}.",
"native-observe-plugin-adoption": "Updater plugin adoption",
"native-observe-plugin-adoption-help": "Current production devices grouped by their reported updater plugin version.",
"native-observe-plugin-distribution": "Plugin version distribution",
"native-observe-plugin-distribution-help": "Most reported updater plugin versions across current production device records. This data is not limited to the selected period.",
"native-observe-plugin-fleet-share": "Fleet share",
"native-observe-plugin-most-reported": "Most reported version",
"native-observe-plugin-most-reported-share": "Most reported share",
"native-observe-plugin-other-version-devices": "Devices on other versions",
"native-observe-plugin-production-devices": "Production devices",
"native-observe-plugin-scope": "Current production device records, excluding emulators. This data is not limited to the selected period.",
"native-observe-plugin-version": "Plugin version",
"native-observe-no-plugin-data": "No updater plugin versions reported yet",
"native-observe-p50": "P50",
"native-observe-p90": "P90",
"native-observe-p99": "P99",
Expand Down
38 changes: 38 additions & 0 deletions playwright/e2e/observe-tabs.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { expect, test } from '../support/commands'

test.describe('Observe sections', () => {
test.beforeEach(async ({ page }) => {
await page.login('test@capgo.app', 'testtest')
})

test('keeps Global and Plugins reachable on desktop and mobile', async ({ page }) => {
await page.goto('/app/com.demo.app/observe')

const globalTab = page.getByRole('button', { name: 'Global', exact: true })
const pluginsTab = page.getByRole('button', { name: 'Plugins', exact: true })

await expect(globalTab).toBeVisible()
await expect(pluginsTab).toBeVisible()
await expect(globalTab).toHaveAttribute('aria-current', 'page')

await pluginsTab.click()
await expect(page).toHaveURL(/\/app\/com\.demo\.app\/observe\/plugins$/)
await expect(pluginsTab).toHaveAttribute('aria-current', 'page')
await expect(page.locator('[data-test="observe-plugin-insights"]')).toBeVisible()
await expect(page.locator('[data-test="observe-plugin-insights"] table').getByText('4.15.3', { exact: true })).toBeVisible()

await page.setViewportSize({ width: 375, height: 667 })
await expect(globalTab).toBeVisible()
await expect(pluginsTab).toBeVisible()

const globalBox = await globalTab.boundingBox()
const pluginsBox = await pluginsTab.boundingBox()
expect(globalBox?.x).toBeGreaterThanOrEqual(0)
expect((pluginsBox?.x ?? 0) + (pluginsBox?.width ?? 0)).toBeLessThanOrEqual(375)
expect(pluginsBox?.height).toBeGreaterThanOrEqual(38)

await globalTab.click()
await expect(page).toHaveURL(/\/app\/com\.demo\.app\/observe$/)
await expect(page.getByRole('heading', { name: 'All versions summary', exact: true })).toBeVisible()
})
})
2 changes: 2 additions & 0 deletions playwright/visual-diff.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export const visualDiffRoutes: VisualDiffRoute[] = [
{ slug: 'app-overview', path: '/app/com.demo.app', auth: true },
{ slug: 'channels', path: '/app/com.demo.app/channels', auth: true },
{ slug: 'devices', path: '/app/com.demo.app/devices', auth: true },
{ slug: 'observe', path: '/app/com.demo.app/observe', auth: true },
{ slug: 'observe-plugins', path: '/app/com.demo.app/observe/plugins', auth: true },
]

export const visualDiffViewport = {
Expand Down
47 changes: 36 additions & 11 deletions scripts/visual-diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ interface LoadedVisualDiffConfig {

async function loadVisualDiffConfig(): Promise<LoadedVisualDiffConfig> {
const configPath = resolve(repoRoot, 'playwright/visual-diff.config.ts')
const module = await import(`${pathToFileURL(configPath).href}?t=${Date.now()}`)
const module = await import(pathToFileURL(configPath).href)
return {
visualDiffRoutes: module.visualDiffRoutes as VisualDiffRoute[],
visualDiffViewport: module.visualDiffViewport as LoadedVisualDiffConfig['visualDiffViewport'],
Expand Down Expand Up @@ -465,8 +465,13 @@ async function settlePage(page: Page) {
})
}

async function captureScreenshots(phase: Phase, routeFilter: string[], options: { forceFrontend?: boolean, forceBackend?: boolean } = {}) {
const { visualDiffRoutes, visualDiffViewport } = await loadVisualDiffConfig()
async function captureScreenshots(
phase: Phase,
routeFilter: string[],
options: { forceFrontend?: boolean, forceBackend?: boolean } = {},
config?: LoadedVisualDiffConfig,
) {
const { visualDiffRoutes, visualDiffViewport } = config ?? await loadVisualDiffConfig()
const routes = selectedRoutes(visualDiffRoutes, routeFilter)
const targetDir = phaseDir(phase)
mkdirSync(targetDir, { recursive: true })
Expand Down Expand Up @@ -547,10 +552,14 @@ function writeDiffImage(beforePath: string, afterPath: string, diffPath: string)
}
}

async function compareScreenshots(thresholdPercent: number, routeFilter: string[]): Promise<DiffResult[]> {
async function compareScreenshots(
thresholdPercent: number,
routeFilter: string[],
config?: LoadedVisualDiffConfig,
): Promise<DiffResult[]> {
const beforeRoot = phaseDir('before')
const afterRoot = phaseDir('after')
const { visualDiffRoutes } = await loadVisualDiffConfig()
const { visualDiffRoutes } = config ?? await loadVisualDiffConfig()
const routes = selectedRoutes(visualDiffRoutes, routeFilter)
const results: DiffResult[] = []

Expand Down Expand Up @@ -695,21 +704,37 @@ async function runPipeline(options: CliOptions) {
const headSha = resolveGitRef(options.headRef, originalRef)

try {
if (!options.skipGitCheckout && originalRef !== headSha)
await checkoutRef(headSha)

// Keep the head route manifest while Git moves between revisions.
const visualDiffConfig = await loadVisualDiffConfig()

if (!options.skipGitCheckout) {
await stopFrontendStack()
await stopBackendStack()
await checkoutRef(baseSha)
await captureScreenshots('before', options.routes, { forceFrontend: true, forceBackend: true })
await captureScreenshots(
'before',
options.routes,
{ forceFrontend: true, forceBackend: true },
visualDiffConfig,
)
await stopFrontendStack()
await stopBackendStack()
await checkoutRef(headSha)
}

await captureScreenshots('after', options.routes, {
forceFrontend: !options.skipGitCheckout,
forceBackend: !options.skipGitCheckout,
})
const results = await compareScreenshots(options.thresholdPercent, options.routes)
await captureScreenshots(
'after',
options.routes,
{
forceFrontend: !options.skipGitCheckout,
forceBackend: !options.skipGitCheckout,
},
visualDiffConfig,
)
const results = await compareScreenshots(options.thresholdPercent, options.routes, visualDiffConfig)
const summary = generateReport(results, options.thresholdPercent)

if (summary.changedCount > 0)
Expand Down
16 changes: 14 additions & 2 deletions src/components/Tabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,13 @@ const labelClass = 'hidden md:block text-xs md:text-sm font-medium transition-co
<div class="min-w-0 pb-0">
<ul :class="[ulPrimaryClass, noWrap ? noWrapClass : 'flex-wrap']">
<li v-for="(tab, i) in tabs" :key="i" class="relative mr-2" :class="{ 'z-20': activeTab === tab.key }">
<button :class="[buttonPrimaryClass, activeTabColor(tab.key)]" @click="emit('update:activeTab', tab.key)">
<button
type="button"
:aria-current="activeTab === tab.key ? 'page' : undefined"
:aria-label="t(tab.label)"
:class="[buttonPrimaryClass, activeTabColor(tab.key)]"
@click="emit('update:activeTab', tab.key)"
>
<component :is="tab.icon" :class="iconClass" />
<span :class="labelClass">{{ t(tab.label) }}</span>
<span v-if="tab.badge" class="hidden px-1.5 py-0.5 text-[10px] font-semibold uppercase rounded border md:inline border-azure-500/40 bg-azure-500/10 text-azure-700 dark:text-azure-200">{{ t(tab.badge) }}</span>
Expand All @@ -55,7 +61,13 @@ const labelClass = 'hidden md:block text-xs md:text-sm font-medium transition-co
<div class="relative -mt-px border-t bg-blue-50 dark:bg-slate-800/40 border-blue-200/60 dark:border-blue-800/70" :class="secondaryTabs?.length ? 'z-10' : 'z-0'">
<ul v-if="secondaryTabs?.length" :class="[ulSecondaryClass, noWrap ? noWrapClass : 'flex-wrap']">
<li v-for="(tab, i) in secondaryTabs" :key="i" class="mr-2">
<button :class="[buttonSecondaryClass, activeTabColor(tab.key, true)]" @click="emit('update:secondaryActiveTab', tab.key)">
<button
type="button"
:aria-current="secondaryActiveTab === tab.key ? 'page' : undefined"
:aria-label="t(tab.label)"
:class="[buttonSecondaryClass, activeTabColor(tab.key, true)]"
@click="emit('update:secondaryActiveTab', tab.key)"
>
<component :is="tab.icon" :class="iconClass" />
<span :class="labelClass">{{ t(tab.label) }}</span>
<span v-if="tab.badge" class="hidden px-1.5 py-0.5 text-[10px] font-semibold uppercase rounded border md:inline border-azure-500/40 bg-azure-500/10 text-azure-700 dark:text-azure-200">{{ t(tab.badge) }}</span>
Expand Down
8 changes: 8 additions & 0 deletions src/constants/observeTabs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Tab } from '~/components/comp_def'
import IconChartBar from '~icons/heroicons/chart-bar'
import IconPuzzlePiece from '~icons/heroicons/puzzle-piece'

export const observeTabs: Tab[] = [
{ label: 'global', icon: IconChartBar, key: '' },
{ label: 'plugins', icon: IconPuzzlePiece, key: '/plugins' },
]
6 changes: 6 additions & 0 deletions src/layouts/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { bundleTabs } from '~/constants/bundleTabs'
import { channelTabs } from '~/constants/channelTabs'
import { deviceTabs } from '~/constants/deviceTabs'
import { logTabs } from '~/constants/logTabs'
import { observeTabs } from '~/constants/observeTabs'
import { useOrganizationStore } from '~/stores/organization'

const router = useRouter()
Expand Down Expand Up @@ -112,6 +113,8 @@ const tabs = computed<Tab[]>(() => {
const appSectionType = computed(() => {
if (/^\/app\/[^/]+\/logs(?:\/|$)/.test(route.path))
return 'logs'
if (/^\/app\/[^/]+\/observe(?:\/|$)/.test(route.path))
return 'observe'
return null
})

Expand All @@ -124,6 +127,8 @@ const secondaryTabBasePath = computed(() => {
return `/app/${appRouteSegment.value}/${resourceType.value}/${resourceId.value}`
if (secondaryTabType.value === 'logs')
return `/app/${appRouteSegment.value}/logs`
if (secondaryTabType.value === 'observe')
return `/app/${appRouteSegment.value}/observe`
return ''
})

Expand All @@ -133,6 +138,7 @@ const tabsConfig: Record<string, Tab[]> = {
device: deviceTabs,
bundle: bundleTabs,
logs: logTabs,
observe: observeTabs,
}

// Generate secondary tabs with full paths for the current resource or app section
Expand Down
Loading
Loading