Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added artifacts/bundle-reach.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added artifacts/observe-reach.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: 11 additions & 2 deletions messages/en.context.json
Original file line number Diff line number Diff line change
Expand Up @@ -632,8 +632,13 @@
"builds-last-month-trend": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI sentence. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"builds-period": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"builds-trend": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI sentence. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"bundle": "Used in Capgo web console areas: app, components/dashboard, components/tables, composables, layouts. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"bundle": "Used in Capgo web console areas: app, components/bundle, components/dashboard, components/tables, composables. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"bundle-active": "Used in Capgo web console areas: pages/app. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"bundle-adoption": "Used in Capgo web console areas: components/bundle. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"bundle-adoption-devices": "Used in Capgo web console areas: components/bundle. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"bundle-adoption-empty": "Used in Capgo web console areas: components/bundle. Role: empty state text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"bundle-adoption-error": "Used in Capgo web console areas: components/bundle. Role: toast or status message. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"bundle-adoption-help": "Used in Capgo web console areas: components/bundle. Role: helper or description text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"bundle-assigned-to-this-channel": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"bundle-comment": "Used in Capgo web console areas: pages/app. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"bundle-comment-empty": "Used in Capgo web console areas: pages/app. Role: empty state text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
Expand Down Expand Up @@ -1501,7 +1506,7 @@
"link-this-bundle-to-another-channel": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"linked-bundle": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"loading": "Used in Capgo web console areas: components, components/bundle, components/dashboard, components/permissions, components/tables. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"loading-statistics": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"loading-statistics": "Used in Capgo web console areas: components/bundle, pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"loading-version": "Used in Capgo web console areas: components/tables. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"log-as": "Used in Capgo web console areas: components/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"log-in": "Used in Capgo web console areas: pages. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
Expand Down Expand Up @@ -2169,6 +2174,7 @@
"registration-to-subscription-conversion": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"registrations-by-source": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"registrations-by-source-description": "Used in Capgo web console areas: pages/admin/dashboard. Role: helper or description text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"release-banner-adoption": "Used in Capgo web console areas: components/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"release-or-30-days": "Used in Capgo web console. Role: filter or date-range option label about \"release or 30 days\". Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"release-status": "Used in Capgo web console. Role: UI label about \"release status\". Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"released": "Used in Capgo web console areas: components/dashboard, pages/app. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
Expand Down Expand Up @@ -2652,6 +2658,9 @@
"view": "Used in Capgo web console areas: components/tables, composables, pages/admin/dashboard, pages/settings/organization. Role: short UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"view-action-logs": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"view-adoption": "Used in Capgo web console areas: components/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"view-bundle-devices": "Used in Capgo web console areas: components/bundle. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"view-channel-adoption": "Used in Capgo web console areas: components/bundle. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"view-channel-adoption-named": "Used in Capgo web console areas: components/bundle. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"view-deliveries": "Used in Capgo web console areas: pages/settings/organization. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"view-dependencies": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"view-logs": "Used in Capgo web console areas: pages/app. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
Expand Down
11 changes: 10 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,11 @@
"builds-trend": "Native Builds Trend (Total, iOS, Android)",
Comment thread
riderx marked this conversation as resolved.
Resolved
"bundle": "Bundle",
"bundle-active": "Active",
"bundle-adoption": "Bundle reach",
"bundle-adoption-devices": "{count} / {total} devices",
"bundle-adoption-empty": "No devices have reported {version} yet. They pick it up the next time they check for updates.",
"bundle-adoption-error": "Could not load how many devices currently have {version}.",
"bundle-adoption-help": "{count} of {total} devices ({percent}) currently report {version}.",
"bundle-comment": "Comment",
"bundle-comment-empty": "No comment",
"bundle-comment-placeholder": "Release notes, commit hash, or context",
Expand Down Expand Up @@ -1543,6 +1548,7 @@
"new-name-to-short": "The apikey name is too short. It must be at least 4 characters long",
"new-record": "New Record",
"new-release-available": "New release available",
"release-banner-adoption": "{percent} of devices",
"next": "Next",
"next-run": "Next update",
"no": "no",
Expand Down Expand Up @@ -1791,7 +1797,7 @@
"org-notifications-bundle-deployed-desc": "Send bundle deployment notifications to the organization's management email",
"org-notifications-channel-self-rejected-desc": "Send channel self-assignment rejection notifications to the organization's management email",
"org-notifications-credit-usage-desc": "Send credit usage alerts to the organization's management email",
"org-notifications-deploy-stats-desc": "Send 24-hour deployment statistics to the organization's management email",
"org-notifications-deploy-stats-desc": "Send 24-hour install counts and how many devices reached the deployed bundle to the organization's management email",
"org-notifications-description": "Control which email notifications are sent to your organization's management email address. These settings only apply when the management email is different from individual admin email addresses.",
"org-notifications-device-error-desc": "Send device update error notifications to the organization's management email",
"org-notifications-monthly-stats-desc": "Send monthly statistics to the organization's management email",
Expand Down Expand Up @@ -2490,6 +2496,9 @@
"version-rules-tester-description": "Learn how versions work and simulate update scenarios",
"view": "View",
"view-adoption": "View adoption",
"view-bundle-devices": "View devices on this bundle",
"view-channel-adoption": "View channel adoption",
"view-channel-adoption-named": "View {channel} adoption",
"view-deliveries": "View Deliveries",
"view-dependencies": "View dependencies",
"view-on-npm": "View on npm",
Expand Down
27 changes: 27 additions & 0 deletions playwright/e2e/bundle-adoption.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { expect, test } from '../support/commands'

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

test('shows reach on the bundle page and observe updater', async ({ page }) => {
await page.goto('/app/com.demo.app/bundle/3')
const bundleCard = page.locator('[data-test="bundle-adoption-card"]')
await expect(bundleCard).toBeVisible()
await expect(bundleCard.getByRole('heading', { name: 'Bundle reach' })).toBeVisible()

await page.locator('[data-test="bundle-adoption-devices"]').click()
await expect(page).toHaveURL(/\/app\/com\.demo\.app\/devices\?version=1\.0\.0/)

await page.goto('/app/com.demo.app/bundle/3')
await page.locator('[data-test="bundle-adoption-channel"]').click()
await expect(page).toHaveURL(/\/app\/com\.demo\.app\/channel\/\d+\/statistics/)

await page.goto('/app/com.demo.app/observe/updater')
await expect(page.locator('[data-test="bundle-adoption-card"]')).toBeVisible()

await page.goto('/app/com.demo.app')
await expect(page.locator('[data-test="release-banner"]')).toBeVisible()
})
})
10 changes: 10 additions & 0 deletions playwright/visual-diff.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ export const visualDiffRoutes: VisualDiffRoute[] = [
{ slug: 'observe-native', path: '/app/com.demo.app/observe/native', auth: true },
{ slug: 'observe-compatibility', path: '/app/com.demo.app/observe/compatibility', auth: true },
{ slug: 'observe-plugins', path: '/app/com.demo.app/observe/plugins', auth: true },
{
slug: 'bundle-detail',
path: '/app/com.demo.app/bundle/3',
auth: true,
},
{
slug: 'channel-statistics',
path: '/app/com.demo.app/channel/1/statistics',
auth: true,
},
{
slug: 'api-keys-app-preview',
path: '/apikeys',
Expand Down
173 changes: 173 additions & 0 deletions src/components/bundle/BundleAdoptionCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import IconSmartphone from '~icons/lucide/smartphone'
import IconTrendingUp from '~icons/lucide/trending-up'
import { getLatestDayVersionAdoption } from '~/services/bundleAdoption'
import { useChartData } from '~/services/chartDataService'
import { getChartDateRange } from '~/services/date'
import { formatNumberValue } from '~/services/formatLocale'
import { useSupabase } from '~/services/supabase'

const props = withDefaults(defineProps<{
appId: string
versionName: string
linkedChannelId?: number | null
linkedChannelName?: string | null
compact?: boolean
}>(), {
linkedChannelId: null,
linkedChannelName: null,
compact: false,
})

const { t } = useI18n()
const router = useRouter()
const supabase = useSupabase()

const loading = ref(true)
const loadError = ref(false)
const adoption = ref<ReturnType<typeof getLatestDayVersionAdoption>>(null)
let requestToken = 0

const percentLabel = computed(() => {
const percent = adoption.value?.percent ?? 0
return `${formatNumberValue(percent, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}%`
})

const countLabel = computed(() => formatNumberValue(adoption.value?.count ?? 0))
const totalLabel = computed(() => formatNumberValue(adoption.value?.total ?? 0))
const hasDevices = computed(() => (adoption.value?.total ?? 0) > 0)

async function loadAdoption() {
if (!props.appId || !props.versionName) {
adoption.value = null
loadError.value = false
loading.value = false
return
}

const currentToken = ++requestToken
loading.value = true
loadError.value = false
try {
const { startDate, endDate } = getChartDateRange(false)
const data = await useChartData(supabase, props.appId, startDate, endDate, 'bundle')
if (currentToken !== requestToken)
return
if (!data) {
loadError.value = true
adoption.value = null
return
}
adoption.value = getLatestDayVersionAdoption(data.datasets ?? [], props.versionName)
}
catch (error) {
console.error('[BundleAdoptionCard] Failed to load adoption', error)
if (currentToken !== requestToken)
return
loadError.value = true
adoption.value = null
}
finally {
if (currentToken === requestToken)
loading.value = false
}
}

function openDevices() {
router.push({
path: `/app/${props.appId}/devices`,
query: { version: props.versionName },
})
}

function openChannelStats() {
if (!props.linkedChannelId)
return
router.push(`/app/${props.appId}/channel/${props.linkedChannelId}/statistics`)
}

watch(() => [props.appId, props.versionName] as const, () => {
void loadAdoption()
}, { immediate: true })
</script>

<template>
<section
data-test="bundle-adoption-card"
class="p-4 border rounded-lg shadow-sm"
:class="hasDevices
? 'bg-sky-50 border-sky-200 dark:bg-sky-950/20 dark:border-sky-800'
: 'bg-white border-slate-200 dark:bg-slate-800 dark:border-slate-700'"
>
<div class="flex flex-col gap-4" :class="{ 'lg:flex-row lg:items-center lg:justify-between': compact }">
<div class="flex items-start gap-3 min-w-0">
<IconTrendingUp class="w-6 h-6 mt-0.5 shrink-0 text-sky-600 dark:text-sky-300" />
<div class="min-w-0">
<h3 class="font-semibold text-slate-900 dark:text-white">
{{ t('bundle-adoption') }}
</h3>
<p class="mt-1 text-sm text-slate-600 dark:text-slate-300">
<template v-if="loading">
{{ t('loading-statistics') }}
</template>
<template v-else-if="loadError">
{{ t('bundle-adoption-error', { version: versionName }) }}
</template>
<template v-else-if="hasDevices">
{{ t('bundle-adoption-help', {
version: versionName,
count: countLabel,
total: totalLabel,
percent: percentLabel,
}) }}
</template>
<template v-else>
{{ t('bundle-adoption-empty', { version: versionName }) }}
</template>
</p>
</div>
</div>

<div v-if="!loading && !loadError" class="flex flex-col gap-3 min-w-0" :class="{ 'lg:w-80': compact }">
<div class="flex items-baseline gap-2">
<span class="text-2xl font-semibold tabular-nums text-slate-900 dark:text-white">
{{ percentLabel }}
</span>
<span class="text-sm text-slate-600 dark:text-slate-300">
{{ t('bundle-adoption-devices', { count: countLabel, total: totalLabel }) }}
</span>
</div>
<progress
class="w-full h-2 d-progress d-progress-info"
:value="adoption?.percent ?? 0"
max="100"
:aria-label="t('bundle-adoption')"
/>
<div class="flex flex-wrap gap-2">
<button
type="button"
class="gap-2 d-btn d-btn-sm d-btn-outline"
data-test="bundle-adoption-devices"
@click="openDevices"
>
<IconSmartphone class="w-4 h-4" />
{{ t('view-bundle-devices') }}
</button>
<button
v-if="linkedChannelId"
type="button"
class="gap-2 d-btn d-btn-sm d-btn-primary"
data-test="bundle-adoption-channel"
@click="openChannelStats"
>
<IconTrendingUp class="w-4 h-4" />
{{ linkedChannelName ? t('view-channel-adoption-named', { channel: linkedChannelName }) : t('view-channel-adoption') }}
</button>
</div>
</div>
</div>
</section>
</template>
Loading
Loading