Skip to content

Commit 7794c61

Browse files
authored
fix(mobile): show the not-yet-synced status once on the Security Agent dashboard (#9378)
* fix(mobile): state the not-yet-synced status once on the Security Agent dashboard A scope that has never synced repeated "Not yet synced" in every stat tile and again in the Act first card, while the filter row under the repository header already stated it. The screen said the same thing six times and the repeated line pushed the tile labels apart. The tiles now keep only their dash and their label: a detail line describes a measurement, and there is none before the first sync. The Act first card keeps its section and shows the same muted dash as every other unmeasured count, instead of the status line and its clock icon. * fix(mobile): keep the security dashboard tiles from moving on load The sync-status read decides whether a stat tile carries a detail line, so the tiles could be drawn in their measured shape and then shrink by a line when the read resolved with "never synced", pushing everything below them. The tiles now wait for that read, the same way they already wait for the stats read, and the sections follow it too because the Act first card's body depends on the same fact. * fix(mobile): explain the empty Act first card on a never-synced scope The card lost its body when the repeated status line came out of the stat tiles, so a scope that has never synced showed a lone dash under its own section title and read as a broken card. The card now says what it will hold once the first sync finishes, which keeps the sync status stated once under the repository filter and leaves the tiles with their label and dash. The sentence is a new catalog entry, so en.json gains it and all 86 other catalogs carry its translation. The mounted test asserts the new body, that the status line does not return to the card, and that the card still opens nothing. * fix(mobile): describe the card's body in its test header The mounted test's file header still said the priority card carries the not-yet-synced line, which the card no longer does: it explains what it will hold after the first sync. The comment now matches the assertions below it.
1 parent 3496859 commit 7794c61

91 files changed

Lines changed: 163 additions & 26 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/mobile/src/components/security-agent/dashboard-screen.mounted.test.tsx‎

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ const dashboardStats = vi.hoisted(() => ({
6262
const lastSync = vi.hoisted(() => ({
6363
data: undefined as { lastSyncTime?: string | null } | undefined,
6464
isError: false,
65+
isPending: false,
6566
refetch: vi.fn(),
6667
}));
6768
const repositories = vi.hoisted(() => ({
@@ -1041,13 +1042,10 @@ describe('buildLocalizedMetrics', () => {
10411042
const metrics = buildLocalizedMetrics(data, true, true);
10421043

10431044
// Zero counts and the line that read the zeros as a clean report both
1044-
// became a dash and the screen's own not-yet-synced state.
1045+
// became a dash; the filter row above the tiles states the not-yet-synced
1046+
// status once, so a tile keeps no detail line to repeat it.
10451047
expect(metrics.map(metric => metric.value)).toEqual(['—', '—', '—']);
1046-
expect(metrics.map(metric => metric.detail)).toEqual([
1047-
i18n.t('securityAgent.dashboard.notYetSynced'),
1048-
i18n.t('securityAgent.dashboard.notYetSynced'),
1049-
i18n.t('securityAgent.dashboard.notYetSynced'),
1050-
]);
1048+
expect(metrics.map(metric => metric.detail)).toEqual(['', '', '']);
10511049
expect(metrics.every(metric => metric.tone === 'neutral')).toBe(true);
10521050
});
10531051

@@ -1326,6 +1324,12 @@ describe('DashboardScreen pending stats', () => {
13261324
},
13271325
};
13281326

1327+
// Each case decides for itself whether the sync-status read has settled.
1328+
beforeEach(() => {
1329+
lastSync.isPending = false;
1330+
lastSync.data = undefined;
1331+
});
1332+
13291333
it('paints the metric skeleton while a paused stats read is still pending', () => {
13301334
// A NetInfo-paused read is pending without fetching, so `isLoading` is
13311335
// false. The body must still show its loading state instead of an empty
@@ -1360,4 +1364,36 @@ describe('DashboardScreen pending stats', () => {
13601364
i18n.t('securityAgent.dashboardMetrics.openFindings')
13611365
);
13621366
});
1367+
1368+
// `neverSynced` decides whether a tile carries a detail line, so a tile drawn
1369+
// before that read settles would shrink by a line the moment it resolves and
1370+
// move everything below it. The tiles wait for the read instead.
1371+
it('keeps the tiles in the skeleton state while the sync-status read is pending', () => {
1372+
dashboardStats.isPending = false;
1373+
dashboardStats.data = resolvedData;
1374+
lastSync.isPending = true;
1375+
lastSync.data = undefined;
1376+
sharedMetrics.build.mockReturnValue([dashboardMetric('openFindings')]);
1377+
1378+
const root = renderScreen();
1379+
1380+
expect(
1381+
root.root.findAll(n => typeof n.type === 'string' && (n.type as string) === 'Skeleton')
1382+
).toHaveLength(4);
1383+
expect(renderedTexts(root.root)).not.toContain(
1384+
i18n.t('securityAgent.dashboardMetrics.openFindings')
1385+
);
1386+
});
1387+
1388+
it('renders the sections only after the sync-status read settles', () => {
1389+
dashboardStats.isPending = false;
1390+
dashboardStats.data = resolvedData;
1391+
lastSync.isPending = true;
1392+
lastSync.data = undefined;
1393+
sectionsProps.props = [];
1394+
1395+
renderScreen();
1396+
1397+
expect(sectionsProps.props).toHaveLength(0);
1398+
});
13631399
});

‎apps/mobile/src/components/security-agent/dashboard-screen.tsx‎

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -186,15 +186,16 @@ export function buildLocalizedMetrics(
186186
// Before the first sync nothing has been measured, so every tile read a zero
187187
// count and, under it, a line that described the zeros as a clean report
188188
// ("All open findings tracked", "No findings have an SLA deadline yet").
189-
// A dash in the numeric slot and the screen's own not-yet-synced line state
190-
// the measurement is simply absent.
189+
// The filter row above already states that nothing has synced, so a dash in
190+
// the numeric slot carries the measurement's absence and the tile keeps no
191+
// detail line to repeat the status.
191192
if (!neverSynced) {
192193
return localized;
193194
}
194195
return localized.map(metric =>
195196
Object.assign(metric, {
196197
value: UNMEASURED_VALUE,
197-
detail: i18n.t('securityAgent.dashboard.notYetSynced'),
198+
detail: '',
198199
tone: 'neutral' as const,
199200
})
200201
);
@@ -592,8 +593,11 @@ export function DashboardScreen({ scope }: Readonly<{ scope: string }>) {
592593

593594
{/* `isPending` rather than `isLoading`: a NetInfo-paused read is pending
594595
without fetching, and it must show this skeleton instead of an empty
595-
metrics row. */}
596-
{dashboardStats.isPending ? (
596+
metrics row. The sync-status read joins it because `neverSynced`
597+
decides whether a tile carries a detail line: a tile that rendered
598+
the measured shape first would shrink by a line the moment the read
599+
resolved, which moves the whole dashboard. */}
600+
{dashboardStats.isPending || lastSync.isPending ? (
597601
<View className="flex-row flex-wrap gap-3">
598602
<Skeleton className="h-24 w-[47%] rounded-lg" />
599603
<Skeleton className="h-24 w-[47%] rounded-lg" />
@@ -615,9 +619,11 @@ export function DashboardScreen({ scope }: Readonly<{ scope: string }>) {
615619
>
616620
{metric.value}
617621
</Text>
618-
<Text variant="muted" className="text-[11px]">
619-
{metric.detail}
620-
</Text>
622+
{metric.detail ? (
623+
<Text variant="muted" className="text-[11px]">
624+
{metric.detail}
625+
</Text>
626+
) : null}
621627
</View>
622628
))}
623629
</View>
@@ -661,7 +667,10 @@ export function DashboardScreen({ scope }: Readonly<{ scope: string }>) {
661667
/>
662668
) : null}
663669

664-
{data ? (
670+
{/* The Act first card's body depends on `neverSynced` too, so it waits
671+
for the sync-status read rather than swapping one shape for the
672+
other once the read resolves. */}
673+
{data && !lastSync.isPending ? (
665674
<DashboardSections
666675
scope={scope}
667676
data={data}

‎apps/mobile/src/components/security-agent/dashboard-sections.mounted.test.tsx‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// "All repositories". Before the first sync nothing has been measured, so the
33
// priority card must not report the empty result as a clean codebase: the
44
// green check and the all-clear line gave a never-synced organization a
5-
// verified look. The neutral state carries the screen's own not-yet-synced
6-
// line instead, and opens nothing.
5+
// verified look. The card explains what it will hold instead, and opens
6+
// nothing.
77

88
import { type ComponentProps } from 'react';
99
import { act, TestRenderer } from '@/test/renderer';
@@ -148,9 +148,13 @@ describe('DashboardSections priority card', () => {
148148
it('reports no measurement while the scope has never synced', () => {
149149
const card = priorityCard(renderSections({ neverSynced: true }));
150150

151-
// The neutral state carries the screen's own not-yet-synced line; the
152-
// all-clear copy and its green check belong to a measured report.
153-
expect(copy(card)).toContain('securityAgent.dashboard.notYetSynced');
151+
// The card is a content section, so it explains what it will hold instead
152+
// of showing the lone dash a measurement row shows or repeating the status
153+
// the filter above states. The all-clear copy and its green check belong to
154+
// a measured report, and nothing here opens.
155+
expect(copy(card)).toContain('securityAgent.dashboard.actFirst');
156+
expect(copy(card)).toContain('securityAgent.dashboard.priorityFindingsPending');
157+
expect(copy(card)).not.toContain('securityAgent.dashboard.notYetSynced');
154158
expect(copy(card)).not.toContain('securityAgent.dashboard.noOpenFindings');
155159
expect(card.findAllByType('ShieldCheck')).toHaveLength(0);
156160
expect(card.findAllByType('Pressable')).toHaveLength(0);

‎apps/mobile/src/components/security-agent/dashboard-sections.tsx‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable max-lines -- The dashboard sections share one data contract and navigation scope. */
22
import { getAnalysisIncompleteCount } from '@kilocode/app-shared/security-agent';
33
import { type Href } from 'expo-router';
4-
import { Clock, FolderGit2, ShieldCheck } from '@/components/ui/icons';
4+
import { FolderGit2, ShieldCheck } from '@/components/ui/icons';
55
import { DirectionalArrowRight } from '@/components/ui/directional-icons';
66
import { type ReactNode } from 'react';
77
import { Pressable, View } from 'react-native';
@@ -102,15 +102,16 @@ function PriorityFindingSection({ scope, data, slaEnabled, neverSynced }: Sectio
102102

103103
// A scope that has never synced has found nothing yet because nothing ran,
104104
// not because the codebase is clean: the green check and the all-clear line
105-
// read as a verified result. The neutral row states the same not-yet-synced
106-
// fact the filter above shows, and opens nothing.
105+
// read as a verified result. The filter above already states that nothing
106+
// has synced, so this card explains what it will hold instead of repeating
107+
// the status, and it opens nothing: a lone dash left a content section
108+
// looking broken.
107109
if (neverSynced) {
108110
return (
109111
<SectionCard title={t('securityAgent.dashboard.actFirst')}>
110-
<View className="flex-row items-center gap-2 py-3">
111-
<Clock size={16} color={colors.mutedForeground} />
112+
<View className="py-3">
112113
<Text variant="muted" className="text-sm">
113-
{t('securityAgent.dashboard.notYetSynced')}
114+
{t('securityAgent.dashboard.priorityFindingsPending')}
114115
</Text>
115116
</View>
116117
</SectionCard>

‎apps/mobile/src/i18n/locales/af.json‎

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

‎apps/mobile/src/i18n/locales/am.json‎

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

‎apps/mobile/src/i18n/locales/ar.json‎

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

‎apps/mobile/src/i18n/locales/az.json‎

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

‎apps/mobile/src/i18n/locales/be.json‎

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

‎apps/mobile/src/i18n/locales/bg.json‎

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

0 commit comments

Comments
 (0)