Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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/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.
9 changes: 7 additions & 2 deletions messages/en.context.json
Original file line number Diff line number Diff line change
Expand Up @@ -632,8 +632,12 @@
"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-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 +1505,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 +2173,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
7 changes: 6 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,10 @@
"builds-trend": "Native Builds Trend (Total, iOS, Android)",
Comment thread
riderx marked this conversation as resolved.
"bundle": "Bundle",
"bundle-active": "Active",
"bundle-adoption": "Bundle reach",
"bundle-adoption-devices": "{count} / {total} devices",
"bundle-adoption-empty": "No devices have checked in on {version} yet.",
"bundle-adoption-error": "Could not load how many devices currently have {version}.",
"bundle-comment": "Comment",
"bundle-comment-empty": "No comment",
"bundle-comment-placeholder": "Release notes, commit hash, or context",
Expand Down Expand Up @@ -1543,6 +1547,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 +1796,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
17 changes: 17 additions & 0 deletions playwright/e2e/bundle-adoption.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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 observe updater', async ({ page }) => {
await page.goto('/app/com.demo.app/observe/updater')
const reachCard = page.locator('[data-test="bundle-adoption-card"]').first()
await expect(reachCard).toBeVisible()
await expect(reachCard).toContainText('Bundle reach')

await reachCard.click()
await expect(page).toHaveURL(/\/app\/com\.demo\.app\/channel\/\d+\/statistics/)
})
})
5 changes: 5 additions & 0 deletions playwright/visual-diff.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ 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: 'channel-statistics',
path: '/app/com.demo.app/channel/1/statistics',
auth: true,
},
{
slug: 'api-keys-app-preview',
path: '/apikeys',
Expand Down
122 changes: 122 additions & 0 deletions src/components/bundle/BundleAdoptionCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import IconPackage from '~icons/lucide/package'
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 = defineProps<{
appId: string
versionName: string
linkedChannelId?: number | null
}>()

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)

const valueLabel = computed(() => {
if (loading.value || loadError.value || !hasDevices.value)
return '—'
return percentLabel.value
})

const detailLabel = computed(() => {
if (loading.value)
return t('loading-statistics')
if (loadError.value)
return t('bundle-adoption-error', { version: props.versionName })
if (!hasDevices.value)
return t('bundle-adoption-empty', { version: props.versionName })
return `${t('bundle-adoption-devices', { count: countLabel.value, total: totalLabel.value })} · ${props.versionName}`
})

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 openAnalytics() {
if (props.linkedChannelId) {
router.push(`/app/${props.appId}/channel/${props.linkedChannelId}/statistics`)
return
}
router.push({
path: `/app/${props.appId}/devices`,
query: { version: props.versionName },
})
}

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

<template>
<button
type="button"
data-test="bundle-adoption-card"
class="p-4 text-left bg-white border rounded-lg shadow-sm cursor-pointer dark:bg-slate-800 border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700/40"
@click="openAnalytics"
>
<div class="flex items-center gap-2 text-sm text-slate-600 dark:text-slate-400">
<IconPackage class="w-4 h-4" />
{{ t('bundle-adoption') }}
</div>
<div class="mt-2 text-lg font-semibold tabular-nums text-slate-900 dark:text-white">
{{ valueLabel }}
</div>
<div class="mt-1 text-xs truncate text-slate-500 dark:text-slate-400">
{{ detailLabel }}
</div>
</button>
</template>
48 changes: 46 additions & 2 deletions src/components/dashboard/ReleaseBanner.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
import { useRouter } from 'vue-router'
import IconCheckCircle from '~icons/lucide/check-circle'
import IconTrendingUp from '~icons/lucide/trending-up'
import { formatDistanceToNow } from '~/services/date'
import { getLatestDayVersionAdoption } from '~/services/bundleAdoption'
import { useChartData } from '~/services/chartDataService'
import { formatDistanceToNow, getChartDateRange } from '~/services/date'
import { formatNumberValue } from '~/services/formatLocale'
import { useSupabase } from '~/services/supabase'
import { useOrganizationStore } from '~/stores/organization'

Expand All @@ -21,6 +24,8 @@
const lastVersion = ref<string>('')
const lastReleaseDate = ref<string | null>(null)
const defaultChannelId = ref<number | null>(null)
const adoptionPercent = ref<number | null>(null)
let requestToken = 0

const HOURS_48_IN_DAYS = 2

Expand All @@ -30,6 +35,12 @@
return formatDistanceToNow(new Date(lastReleaseDate.value))
})

const adoptionPercentLabel = computed(() => {
if (adoptionPercent.value === null)
return ''
return `${formatNumberValue(adoptionPercent.value, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}%`
})

const hasRecentRelease = computed(() => {
if (!lastReleaseDate.value || isLoading.value)
return false
Expand All @@ -39,20 +50,24 @@
return daysSinceRelease <= HOURS_48_IN_DAYS
})

async function fetchReleaseInfo() {

Check failure on line 53 in src/components/dashboard/ReleaseBanner.vue

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 24 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=Cap-go_capgo&issues=AZ_63-IWXgDmGUzT8U45&open=AZ_63-IWXgDmGUzT8U45&pullRequest=3032
if (!props.appId) {
return
}

const currentToken = ++requestToken
isLoading.value = true
try {
await organizationStore.awaitInitialLoad()
if (currentToken !== requestToken)
return
const orgId = organizationStore.currentOrganization?.gid

if (!orgId) {
lastVersion.value = ''
lastReleaseDate.value = null
defaultChannelId.value = null
adoptionPercent.value = null
return
}

Expand All @@ -71,25 +86,51 @@
.eq('public', true)
.limit(1)

if (currentToken !== requestToken)
return

const latestVersion = versionsData?.[0]
const defaultChannel = channelsData?.[0]

if (latestVersion) {
lastVersion.value = latestVersion.name
lastReleaseDate.value = latestVersion.created_at
try {
const { startDate, endDate } = getChartDateRange(false)
const chartData = await useChartData(supabase, props.appId, startDate, endDate, 'bundle')
if (currentToken !== requestToken)
return
if (!chartData) {
adoptionPercent.value = null
}
else {
const adoption = getLatestDayVersionAdoption(chartData.datasets ?? [], latestVersion.name)
adoptionPercent.value = adoption && adoption.total > 0 ? adoption.percent : null
}
}
catch (error) {
console.error('Error fetching bundle adoption:', error)
if (currentToken !== requestToken)
return
adoptionPercent.value = null
}
}
else {
lastVersion.value = ''
lastReleaseDate.value = null
adoptionPercent.value = null
}

defaultChannelId.value = defaultChannel?.id || null
}
catch (error) {
if (currentToken !== requestToken)
return
console.error('Error fetching release info:', error)
}
finally {
isLoading.value = false
if (currentToken === requestToken)
isLoading.value = false
}
}

Expand Down Expand Up @@ -128,6 +169,9 @@
</p>
<p class="text-sm text-emerald-700 dark:text-emerald-300">
{{ t('version') }} {{ lastVersion }} — {{ t('released') }} {{ lastReleaseDisplay }}
<template v-if="adoptionPercentLabel">
· {{ t('release-banner-adoption', { percent: adoptionPercentLabel }) }}
</template>
</p>
</div>
</div>
Expand Down
Loading
Loading