Skip to content

Commit f5dd7b7

Browse files
committed
[iris] Switch jobs view mobile/desktop with v-if instead of CSS
The CSS `sm:hidden` / `hidden sm:block` approach kept both variants in the DOM, which made the e2e smoke tests fail: Playwright's `page.locator("text=smoke-simple").first` was matching the hidden mobile card link, then `to_be_visible()` failed because that link is `display: none` at desktop widths. Render only one variant at a time, picked via a reactive viewport-width ref. Adds a minimal `useMediaQuery` composable. CI runs at desktop width and now sees only the desktop table.
1 parent f1dba03 commit f5dd7b7

3 files changed

Lines changed: 49 additions & 10 deletions

File tree

lib/iris/dashboard/src/components/controller/JobDetail.vue

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ import InfoCard from '@/components/shared/InfoCard.vue'
1717
import InfoRow from '@/components/shared/InfoRow.vue'
1818
import EmptyState from '@/components/shared/EmptyState.vue'
1919
import LogViewer from '@/components/shared/LogViewer.vue'
20+
import { useMediaQuery } from '@/composables/useMediaQuery'
21+
22+
// Tailwind's `sm` breakpoint is 640px. Cards on mobile, table on desktop.
23+
// v-if-switched (not CSS-hidden) so only one variant is in the DOM.
24+
const isMobile = useMediaQuery('(max-width: 639px)')
2025
2126
const props = defineProps<{
2227
jobId: string
@@ -836,7 +841,7 @@ async function handleProfile(taskId: string, profilerType: string, format: strin
836841
</h3>
837842
</div>
838843
<!-- Mobile: card grid (one card per child job) -->
839-
<div class="sm:hidden grid grid-cols-1 gap-2">
844+
<div v-if="isMobile" class="grid grid-cols-1 gap-2">
840845
<div
841846
v-for="node in flattenedChildJobs"
842847
:key="'child-card-' + node.job.jobId"
@@ -888,7 +893,7 @@ async function handleProfile(taskId: string, profilerType: string, format: strin
888893
</div>
889894

890895
<!-- Desktop: table -->
891-
<div class="hidden sm:block overflow-x-auto">
896+
<div v-else class="overflow-x-auto">
892897
<table class="w-full border-collapse">
893898
<thead>
894899
<tr class="border-b border-surface-border">
@@ -993,7 +998,7 @@ async function handleProfile(taskId: string, profilerType: string, format: strin
993998

994999
<template v-else>
9951000
<!-- Mobile: card grid (one card per task) -->
996-
<div class="sm:hidden grid grid-cols-1 gap-2">
1001+
<div v-if="isMobile" class="grid grid-cols-1 gap-2">
9971002
<div
9981003
v-for="task in paginatedTasks"
9991004
:key="'task-card-' + task.taskId"
@@ -1056,7 +1061,7 @@ async function handleProfile(taskId: string, profilerType: string, format: strin
10561061
</div>
10571062

10581063
<!-- Desktop: table -->
1059-
<div class="hidden sm:block overflow-x-auto">
1064+
<div v-else class="overflow-x-auto">
10601065
<table class="w-full border-collapse md:table-fixed">
10611066
<colgroup class="hidden md:table-column-group">
10621067
<col class="w-[4%]" />

lib/iris/dashboard/src/components/controller/JobsTab.vue

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ import { timestampMs, formatDuration, formatRelativeTime } from '@/utils/formatt
1010
import { flattenLoadedJobTree, getLeafJobName } from '@/utils/jobTree'
1111
import StatusBadge from '@/components/shared/StatusBadge.vue'
1212
import EmptyState from '@/components/shared/EmptyState.vue'
13+
import { useMediaQuery } from '@/composables/useMediaQuery'
14+
15+
// Tailwind's `sm` breakpoint is 640px. Below that we render mobile cards;
16+
// at/above we render the desktop table. Switched via v-if so only one
17+
// variant is in the DOM at a time (otherwise duplicate text trips Playwright
18+
// locator's `.first` matcher in CI).
19+
const isMobile = useMediaQuery('(max-width: 639px)')
1320
1421
const PAGE_SIZE = 50
1522
@@ -550,12 +557,12 @@ function sortIndicator(field: SortField): string {
550557
: (hasActiveFilter ? 'No jobs matching filter' : 'No jobs')"
551558
/>
552559

553-
<!-- Mobile/desktop split: cards on xs, table on sm+. Pagination is shared. -->
560+
<!-- Mobile/desktop split: cards on xs, table on sm+. Pagination is shared.
561+
Switched via v-if (not CSS) so only one variant renders, keeping the DOM
562+
free of duplicate text-content that confuses Playwright `.first` matchers. -->
554563
<template v-else>
555-
<!-- Mobile: stacked card-grid (one card per job).
556-
Tables don't fit on phones once you have a status badge + progress bar +
557-
a job-name column, so on xs we render a vertical grid of cards instead. -->
558-
<div class="sm:hidden grid grid-cols-1 gap-2">
564+
<!-- Mobile: stacked card-grid (one card per job). -->
565+
<div v-if="isMobile" class="grid grid-cols-1 gap-2">
559566
<div
560567
v-for="node in flattenedJobs"
561568
:key="'card-' + node.job.jobId"
@@ -637,7 +644,7 @@ function sortIndicator(field: SortField): string {
637644
</div>
638645

639646
<!-- Desktop: tabular layout (sm+) -->
640-
<div class="hidden sm:block overflow-x-auto">
647+
<div v-else class="overflow-x-auto">
641648
<table class="w-full border-collapse">
642649
<thead>
643650
<tr class="border-b border-surface-border">
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ref, onMounted, onUnmounted } from 'vue'
2+
3+
/**
4+
* Reactive boolean tied to a CSS media query. The returned ref updates as the
5+
* viewport crosses the query threshold (e.g. on resize or device rotation).
6+
*
7+
* SSR-safe: defaults to false on the server and during the very first render,
8+
* then syncs from `window.matchMedia` on mount.
9+
*/
10+
export function useMediaQuery(query: string) {
11+
const matches = ref(false)
12+
let mql: MediaQueryList | null = null
13+
const listener = (e: MediaQueryListEvent) => { matches.value = e.matches }
14+
15+
onMounted(() => {
16+
if (typeof window === 'undefined' || !window.matchMedia) return
17+
mql = window.matchMedia(query)
18+
matches.value = mql.matches
19+
mql.addEventListener('change', listener)
20+
})
21+
22+
onUnmounted(() => {
23+
if (mql) mql.removeEventListener('change', listener)
24+
})
25+
26+
return matches
27+
}

0 commit comments

Comments
 (0)