Skip to content

Commit 85cf974

Browse files
authored
feat: add search to metrics pages (#891)
Fix datagouv/data.gouv.fr#1919
1 parent a210e7e commit 85cf974

4 files changed

Lines changed: 174 additions & 35 deletions

File tree

‎components/Metrics/AdminDataservicesPage.vue‎

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
<template>
22
<div>
3-
<div
4-
v-if="downloadStatsUrl"
5-
class="flex justify-end -mt-14 pt-0.5 mb-5"
6-
>
3+
<div class="flex justify-end items-center gap-4 -mt-14 pt-0.5 mb-5">
4+
<AdminInput
5+
v-model="q"
6+
type="search"
7+
:icon="RiSearchLine"
8+
:placeholder="$t('Recherche')"
9+
/>
710
<BrandedButton
11+
v-if="downloadStatsUrl"
812
color="secondary"
9-
:disabled="!downloadStatsUrl"
10-
:href="downloadStatsUrl || ''"
13+
:href="downloadStatsUrl"
1114
:external="true"
1215
download="stats.csv"
1316
:icon="RiDownloadLine"
@@ -134,19 +137,34 @@
134137
src="/illustrations/dataservice.svg"
135138
class="h-20"
136139
/>
137-
<p class="fr-text--bold fr-my-3v">
138-
{{ $t(`Vous n'avez pas encore publié d'API`) }}
139-
</p>
140-
<AdminPublishButton type="dataservice" />
140+
<template v-if="q">
141+
<p class="fr-text--bold fr-my-3v">
142+
{{ $t(`Pas de résultats pour « {q} »`, { q }) }}
143+
</p>
144+
<BrandedButton
145+
color="primary"
146+
@click="q = qDebounced = ''"
147+
>
148+
{{ $t('Réinitialiser les filtres') }}
149+
</BrandedButton>
150+
</template>
151+
<template v-else>
152+
<p class="fr-text--bold fr-my-3v">
153+
{{ $t(`Vous n'avez pas encore publié d'API`) }}
154+
</p>
155+
<AdminPublishButton type="dataservice" />
156+
</template>
141157
</div>
142158
</div>
143159
</template>
144160

145161
<script setup lang="ts">
146162
import { BrandedButton, LoadingBlock, Pagination, summarize, Tooltip, type Dataservice, type Organization, type User } from '@datagouv/components-next'
147-
import { RiChat3Line, RiDownloadLine, RiEyeLine, RiStarSLine } from '@remixicon/vue'
163+
import { refDebounced } from '@vueuse/core'
164+
import { RiChat3Line, RiDownloadLine, RiEyeLine, RiSearchLine, RiStarSLine } from '@remixicon/vue'
148165
import AdminTable from '~/components/AdminTable/Table/AdminTable.vue'
149166
import AdminTableTh from '~/components/AdminTable/Table/AdminTableTh.vue'
167+
import AdminInput from '~/components/AdminInput.vue'
150168
import type { DataserviceSortedBy, PaginatedArray, SortDirection } from '~/types/types'
151169
152170
const props = defineProps<{
@@ -158,6 +176,9 @@ const config = useRuntimeConfig()
158176
159177
const page = ref(1)
160178
const pageSize = ref(20)
179+
const q = ref('')
180+
const qDebounced = refDebounced(q, config.public.searchDebounce)
181+
watch(qDebounced, () => page.value = 1)
161182
const sortedBy = ref<DataserviceSortedBy>('title')
162183
const direction = ref<SortDirection>('desc')
163184
const sortDirection = computed(() => `${direction.value === 'asc' ? '' : '-'}${sortedBy.value}`)
@@ -167,7 +188,7 @@ const params = computed(() => {
167188
return {
168189
organization: props.organization?.id,
169190
owner: props.user?.id,
170-
191+
q: qDebounced.value,
171192
sort: sortDirection.value,
172193
page_size: pageSize.value,
173194
page: page.value,

‎components/Metrics/AdminDatasetsPage.vue‎

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
<template>
22
<div>
3-
<div
4-
v-if="downloadStatsUrl"
5-
class="flex justify-end -mt-14 pt-0.5 mb-5"
6-
>
3+
<div class="flex justify-end items-center gap-4 -mt-14 pt-0.5 mb-5">
4+
<AdminInput
5+
v-model="q"
6+
type="search"
7+
:icon="RiSearchLine"
8+
:placeholder="$t('Recherche')"
9+
/>
710
<BrandedButton
11+
v-if="downloadStatsUrl"
812
color="secondary"
9-
:disabled="!downloadStatsUrl"
10-
:href="downloadStatsUrl || ''"
13+
:href="downloadStatsUrl"
1114
:external="true"
1215
download="stats.csv"
1316
:icon="RiDownloadLine"
@@ -172,19 +175,34 @@
172175
src="/illustrations/dataset.svg"
173176
class="h-20"
174177
/>
175-
<p class="fr-text--bold fr-my-3v">
176-
{{ $t(`Vous n'avez pas encore publié de jeu de données.`) }}
177-
</p>
178-
<AdminPublishButton type="dataset" />
178+
<template v-if="q">
179+
<p class="fr-text--bold fr-my-3v">
180+
{{ $t(`Pas de résultats pour « {q} »`, { q }) }}
181+
</p>
182+
<BrandedButton
183+
color="primary"
184+
@click="q = qDebounced = ''"
185+
>
186+
{{ $t('Réinitialiser les filtres') }}
187+
</BrandedButton>
188+
</template>
189+
<template v-else>
190+
<p class="fr-text--bold fr-my-3v">
191+
{{ $t(`Vous n'avez pas encore publié de jeu de données.`) }}
192+
</p>
193+
<AdminPublishButton type="dataset" />
194+
</template>
179195
</div>
180196
</div>
181197
</template>
182198

183199
<script setup lang="ts">
184200
import { BrandedButton, LoadingBlock, Pagination, summarize, Tooltip, type DatasetV2, type Organization, type User } from '@datagouv/components-next'
185-
import { RiDownloadLine, RiEyeLine, RiLineChartLine, RiStarSLine } from '@remixicon/vue'
201+
import { refDebounced } from '@vueuse/core'
202+
import { RiDownloadLine, RiEyeLine, RiLineChartLine, RiSearchLine, RiStarSLine } from '@remixicon/vue'
186203
import AdminTable from '~/components/AdminTable/Table/AdminTable.vue'
187204
import AdminTableTh from '~/components/AdminTable/Table/AdminTableTh.vue'
205+
import AdminInput from '~/components/AdminInput.vue'
188206
import type { DatasetSortedBy, PaginatedArray, SortDirection } from '~/types/types'
189207
190208
const props = defineProps<{
@@ -195,6 +213,9 @@ const props = defineProps<{
195213
const config = useRuntimeConfig()
196214
const page = ref(1)
197215
const pageSize = ref(20)
216+
const q = ref('')
217+
const qDebounced = refDebounced(q, config.public.searchDebounce)
218+
watch(qDebounced, () => page.value = 1)
198219
199220
const sortedBy = ref<DatasetSortedBy>('created')
200221
const direction = ref<SortDirection>('desc')
@@ -206,6 +227,7 @@ const params = computed(() => {
206227
organization: props.organization?.id,
207228
owner: props.user?.id,
208229
user: props.user?.id,
230+
q: qDebounced.value,
209231
sort: sortDirection.value,
210232
page_size: pageSize.value,
211233
page: page.value,

‎components/Metrics/AdminReusesPage.vue‎

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
<template>
22
<div>
3-
<div
4-
v-if="downloadStatsUrl"
5-
class="flex justify-end -mt-14 pt-0.5 mb-5"
6-
>
3+
<div class="flex justify-end items-center gap-4 -mt-14 pt-0.5 mb-5">
4+
<AdminInput
5+
v-model="q"
6+
type="search"
7+
:icon="RiSearchLine"
8+
:placeholder="$t('Recherche')"
9+
/>
710
<BrandedButton
11+
v-if="downloadStatsUrl"
812
color="secondary"
9-
:disabled="!downloadStatsUrl"
10-
:href="downloadStatsUrl || ''"
13+
:href="downloadStatsUrl"
1114
:external="true"
1215
download="stats.csv"
1316
:icon="RiDownloadLine"
@@ -144,19 +147,34 @@
144147
src="/illustrations/reuse.svg"
145148
class="h-20"
146149
/>
147-
<p class="fr-text--bold fr-my-3v">
148-
{{ $t(`Vous n'avez pas encore publié de réutilisation.`) }}
149-
</p>
150-
<AdminPublishButton type="reuse" />
150+
<template v-if="q">
151+
<p class="fr-text--bold fr-my-3v">
152+
{{ $t(`Pas de résultats pour « {q} »`, { q }) }}
153+
</p>
154+
<BrandedButton
155+
color="primary"
156+
@click="q = qDebounced = ''"
157+
>
158+
{{ $t('Réinitialiser les filtres') }}
159+
</BrandedButton>
160+
</template>
161+
<template v-else>
162+
<p class="fr-text--bold fr-my-3v">
163+
{{ $t(`Vous n'avez pas encore publié de réutilisation.`) }}
164+
</p>
165+
<AdminPublishButton type="reuse" />
166+
</template>
151167
</div>
152168
</div>
153169
</template>
154170

155171
<script setup lang="ts">
156-
import { RiDownloadLine, RiStarSLine } from '@remixicon/vue'
172+
import { RiDownloadLine, RiSearchLine, RiStarSLine } from '@remixicon/vue'
157173
import { BrandedButton, LoadingBlock, Pagination, summarize, Tooltip, type Organization, type Reuse, type User } from '@datagouv/components-next'
174+
import { refDebounced } from '@vueuse/core'
158175
import AdminTable from '~/components/AdminTable/Table/AdminTable.vue'
159176
import AdminTableTh from '~/components/AdminTable/Table/AdminTableTh.vue'
177+
import AdminInput from '~/components/AdminInput.vue'
160178
import type { PaginatedArray, ReuseSortedBy, SortDirection } from '~/types/types'
161179
162180
const props = defineProps<{
@@ -170,6 +188,9 @@ const downloadStatsUrl = computed(() => props.organization ? `/api/1/organizatio
170188
171189
const page = ref(1)
172190
const pageSize = ref(20)
191+
const q = ref('')
192+
const qDebounced = refDebounced(q, config.public.searchDebounce)
193+
watch(qDebounced, () => page.value = 1)
173194
const sortedBy = ref<ReuseSortedBy>('created')
174195
const direction = ref<SortDirection>('desc')
175196
const sortDirection = computed(() => `${direction.value === 'asc' ? '' : '-'}${sortedBy.value}`)
@@ -178,7 +199,7 @@ const params = computed(() => {
178199
return {
179200
organization: props.organization?.id,
180201
owner: props.user?.id,
181-
202+
q: qDebounced.value,
182203
sort: sortDirection.value,
183204
page_size: pageSize.value,
184205
page: page.value,
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { test, expect } from '@playwright/test'
2+
3+
const API_BASE = process.env.NUXT_PUBLIC_API_BASE || 'http://dev.local:7000'
4+
5+
test.describe('Dataset metrics', () => {
6+
let datasetA: { id: string, title: string }
7+
let datasetB: { id: string, title: string }
8+
const uniqueId = Date.now()
9+
10+
test.beforeAll(async ({ browser }) => {
11+
const context = await browser.newContext({ storageState: 'playwright/.auth/user.json' })
12+
const page = await context.newPage()
13+
14+
const [respA, respB] = await Promise.all([
15+
page.request.post(`${API_BASE}/api/1/datasets/`, {
16+
data: { title: `Alpha metrics ${uniqueId}`, description: 'Test', frequency: 'unknown' },
17+
}),
18+
page.request.post(`${API_BASE}/api/1/datasets/`, {
19+
data: { title: `Beta metrics ${uniqueId}`, description: 'Test', frequency: 'unknown' },
20+
}),
21+
])
22+
datasetA = await respA.json()
23+
datasetB = await respB.json()
24+
25+
await context.close()
26+
})
27+
28+
test.afterAll(async ({ browser }) => {
29+
const context = await browser.newContext({ storageState: 'playwright/.auth/user.json' })
30+
const page = await context.newPage()
31+
32+
await Promise.all([
33+
page.request.delete(`${API_BASE}/api/1/datasets/${datasetA.id}/`),
34+
page.request.delete(`${API_BASE}/api/1/datasets/${datasetB.id}/`),
35+
])
36+
37+
await context.close()
38+
})
39+
40+
test('lists datasets with their metrics', async ({ page }) => {
41+
await page.goto('/admin/me/metrics/datasets/')
42+
43+
await expect(page.getByRole('link', { name: datasetA.title })).toBeVisible()
44+
await expect(page.getByRole('link', { name: datasetB.title })).toBeVisible()
45+
})
46+
47+
test('can search datasets by title', async ({ page }) => {
48+
await page.goto('/admin/me/metrics/datasets/')
49+
50+
const searchInput = page.getByRole('searchbox', { name: 'Recherche' })
51+
await expect(searchInput).toBeVisible()
52+
53+
await searchInput.fill(`Alpha metrics ${uniqueId}`)
54+
await expect(page.getByRole('link', { name: datasetA.title })).toBeVisible()
55+
await expect(page.getByRole('link', { name: datasetB.title })).not.toBeVisible()
56+
57+
await searchInput.clear()
58+
await expect(page.getByRole('link', { name: datasetA.title })).toBeVisible()
59+
await expect(page.getByRole('link', { name: datasetB.title })).toBeVisible()
60+
})
61+
62+
test('shows empty state and reset button when search has no results', async ({ page }) => {
63+
await page.goto('/admin/me/metrics/datasets/')
64+
65+
const searchInput = page.getByRole('searchbox', { name: 'Recherche' })
66+
await searchInput.fill('zzz-no-match-zzz')
67+
68+
await expect(page.getByText('Pas de résultats pour « zzz-no-match-zzz »')).toBeVisible()
69+
70+
await page.getByRole('button', { name: 'Réinitialiser les filtres' }).click()
71+
72+
await expect(searchInput).toHaveValue('')
73+
await expect(page.getByRole('link', { name: datasetA.title })).toBeVisible()
74+
})
75+
})

0 commit comments

Comments
 (0)