Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
5 changes: 5 additions & 0 deletions components/AdminSidebar/AdminSidebarMenu/AdminSidebarMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@
:label="$t('Réutilisations')"
to="/admin/site/reuses"
/>
<AdminSidebarLink
:icon="RiBarChartBoxLine"
Comment thread
nicolaskempf57 marked this conversation as resolved.
Outdated
:label="$t('Graphiques')"
to="/admin/site/charts"
/>
<AdminSidebarLink
:icon="RiBuilding2Line"
:label="$t('Organisations')"
Expand Down
81 changes: 81 additions & 0 deletions components/Charts/AdminChartsPage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<template>
<div>
<AdminBreadcrumb>
<BreadcrumbItem>{{ t('Graphiques') }}</BreadcrumbItem>
</AdminBreadcrumb>

<h1 class="font-extrabold text-2xl text-gray-title mb-5">
{{ t('Graphiques') }}
</h1>

<div
v-if="pageData"
class="flex flex-wrap gap-x-4 gap-y-2 items-center"
>
<div class="w-full flex-none md:flex-1">
<h2 class="text-sm font-bold uppercase m-0">
{{ t('{n} graphiques | {n} graphique | {n} graphiques', pageData.total) }}
</h2>
</div>
</div>

<LoadingBlock
v-slot="{ data: chartsPage }"
:status
:data="pageData"
>
<div v-if="chartsPage && chartsPage.total > 0">
<div class="not-prose grid gap-4">
<ChartCard
v-for="chart in chartsPage.data"
:key="chart.id"
:chart
:chart-url="`/admin/beta/charts/${chart.id}`"
/>
</div>
<Pagination
:page="page"
:page-size="pageSize"
:total-results="chartsPage.total"
@change="(changedPage: number) => page = changedPage"
/>
</div>
Comment thread
nicolaskempf57 marked this conversation as resolved.
</LoadingBlock>

<div
v-if="status != 'pending' && pageData && !pageData.total"
class="flex flex-col items-center"
>
<img
src="/illustrations/chart.svg"
class="h-20"
alt=""
>
<p class="fr-text--bold fr-my-3v">
{{ t(`Il n'y a pas encore de graphique sur le site`) }}
</p>
</div>
</div>
</template>

<script setup lang="ts">
import { ChartCard, LoadingBlock, Pagination, type Chart } from '@datagouv/components-next'
import { ref } from 'vue'
import AdminBreadcrumb from '../Breadcrumbs/AdminBreadcrumb.vue'
import BreadcrumbItem from '../Breadcrumbs/BreadcrumbItem.vue'
import type { PaginatedArray } from '~/types/types'

const { t } = useTranslation()

const page = ref(1)
const pageSize = ref(20)

const params = computed(() => {
return {
page_size: pageSize.value,
page: page.value,
}
})

const { data: pageData, status } = await useAPI<PaginatedArray<Chart>>('/api/1/visualizations/', { lazy: true, server: false, query: params })
</script>
124 changes: 38 additions & 86 deletions components/Charts/ChartConfigurator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -80,46 +80,6 @@
</div>

<div class="col-span-5 space-y-6 py-4 px-6 rounded-lg bg-white border border-new-gray-light">
<fieldset
v-if="isAdmin"
class="min-w-0 space-y-4"
>
<label
for="existing-charts"
class="mb-2 font-bold"
>
{{ $t('Graphiques existants') }}
</label>
<div class="flex gap-2">
<select
id="existing-charts"
v-model="selectedChartId"
class="flex-1 fr-select"
>
<option
value=""
disabled
>
{{ $t('Sélectionnez un graphique') }}
</option>
<option
v-for="column in charts?.data"
:key="column.id"
:value="column.id"
>
{{ column.title }}
</option>
</select>
<button
class="fr-btn"
type="button"
:disabled="!selectedChartId"
@click="loadSelectedChart"
>
{{ $t('Charger') }}
</button>
</div>
</fieldset>
<fieldset class="min-w-0">
<ProducerSelect
v-model="producer"
Expand Down Expand Up @@ -477,7 +437,6 @@ import { buildTypeConfig, buildColumnsFromProfile, useGetProfile, useHasTabularD
import type { Component } from 'vue'
import { computed, defineAsyncComponent, reactive, ref, watch } from 'vue'
import { RiAddLine, RiArrowDownLine, RiArrowDownSLine, RiArrowUpLine, RiBarChartLine, RiCalculatorLine, RiLineChartLine, RiText } from '@remixicon/vue'
import { useAPI } from '~/utils/api'
import { isMeAdmin } from '~/utils/auth'
import { keepValidSortCombined } from '~/utils/charts'
import ChartFilterRow from './ChartFilterRow.vue'
Expand Down Expand Up @@ -508,7 +467,6 @@ const { t } = useTranslation()
const hasTabularData = useHasTabularData()
const getProfile = useGetProfile()
const isAdmin = isMeAdmin()
const { data: charts, refresh } = await useAPI<PaginatedArray<Chart>>('/api/1/visualizations/', { lazy: true })

const $chartsApi = $fetch.create({
baseURL: runtimeConfig.public.chartsApiBase as string,
Expand All @@ -525,6 +483,10 @@ const form = defineModel<ChartForm>({
required: true,
})

const props = defineProps<{
initialChart?: Chart
}>()

const columns = ref<ColumnsDefinition>({})
const producer = ref<Owned | null>(null)
const dataset = ref<DatasetSuggest | null>(null)
Expand All @@ -541,7 +503,6 @@ watch(producer, (newProducer) => {
}
}, { immediate: true })
const savedChart = ref<Chart | null>(null)
const selectedChartId = ref('')

const chartForViewer = ref(toChartApi(form.value))

Expand Down Expand Up @@ -792,51 +753,46 @@ async function suggestDataset(q: string): Promise<Array<DatasetSuggest>> {
})
}

async function loadChart(id: string) {
async function loadChart(data: Chart) {
try {
const data = await $api<Chart>(`/api/1/visualizations/${id}/`)
if (data) {
savedChart.value = data

const chartResources = new Set<string>()
for (const serie of data.series) {
if (serie.resource_id) {
chartResources.add(serie.resource_id)
}
savedChart.value = data

const chartResources = new Set<string>()
for (const serie of data.series) {
if (serie.resource_id) {
chartResources.add(serie.resource_id)
}
}

form.value = toChartForm(data)
form.value = toChartForm(data)

await loadMissingResourcesForChart(Array.from(chartResources))
await loadColumnsForResources(Array.from(chartResources))
await loadMissingResourcesForChart(Array.from(chartResources))
await loadColumnsForResources(Array.from(chartResources))

if (data.organization) {
producer.value = { organization: data.organization, owner: null }
}
if (data.owner) {
producer.value = { organization: null, owner: data.owner }
}
if (data.organization) {
producer.value = { organization: data.organization, owner: null }
}
if (data.owner) {
producer.value = { organization: null, owner: data.owner }
}

if (!dataset.value && data.series.length > 0 && data.series[0]?.resource_id) {
try {
const resourceData = await $chartsApi<{ resource: Resource, dataset_id: string }>(`/api/2/datasets/resources/${data.series[0].resource_id}/`)
if (resourceData.dataset_id) {
const fetchedDataset = await $chartsApi<DatasetSuggest>(`/api/2/datasets/${resourceData.dataset_id}/`)
dataset.value = fetchedDataset
}
}
catch (error) {
console.error('Failed to load dataset for chart:', error)
if (!dataset.value && data.series.length > 0 && data.series[0]?.resource_id) {
try {
const resourceData = await $chartsApi<{ resource: Resource, dataset_id: string }>(`/api/2/datasets/resources/${data.series[0].resource_id}/`)
if (resourceData.dataset_id) {
const fetchedDataset = await $chartsApi<DatasetSuggest>(`/api/2/datasets/${resourceData.dataset_id}/`)
dataset.value = fetchedDataset
}
}

if (data.series.length > 0 && data.series[0]?.resource_id) {
await nextTick()

selectedResource.value = data.series[0].resource_id
catch (error) {
console.error('Failed to load dataset for chart:', error)
}
}

toast.success(t('Graphique chargé !'))
if (data.series.length > 0 && data.series[0]?.resource_id) {
await nextTick()

selectedResource.value = data.series[0].resource_id
}
}
catch (error) {
Expand All @@ -845,12 +801,6 @@ async function loadChart(id: string) {
}
}

function loadSelectedChart() {
if (selectedChartId.value) {
loadChart(selectedChartId.value)
}
}

async function saveChart() {
try {
const chartForApi = toChartApi(form.value)
Expand Down Expand Up @@ -881,8 +831,6 @@ async function saveChart() {
}

toast.success(update ? t('Graphique mis à jour !') : t('Graphique sauvegardé !'))
await refresh()
selectedChartId.value = savedChart.value.id
}
catch (error) {
console.error('Failed to save chart:', error)
Expand Down Expand Up @@ -1025,4 +973,8 @@ watch(
},
{ immediate: true },
)

if (props.initialChart) {
await loadChart(props.initialChart)
}
</script>
86 changes: 86 additions & 0 deletions datagouv-components/src/components/ChartCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<template>
<ObjectCard media-size="xl">
Comment thread
nicolaskempf57 marked this conversation as resolved.
<template #media>
<img
v-if="chart.image"
:src="chart.image"
class="w-full h-full object-cover"
:alt="chart.title"
>
<Placeholder
v-else
type="Chart"
class="w-full h-full"
/>
</template>

<ObjectCardHeader
:icon="chartIcon"
:url="chartUrl || chart.page"
>
{{ chart.title }}
</ObjectCardHeader>

<div
v-if="chart.organization || chart.owner"
class="text-sm flex flex-wrap gap-y-1 items-center truncate"
>
<ObjectCardOwner
:organization="chart.organization"
:owner="chart.owner"
:organization-url="organizationUrl"
/>
<RiSubtractLine
aria-hidden="true"
class="size-4 flex-none fill-gray-medium"
/>
<p class="text-sm whitespace-nowrap mb-0 text-gray-medium">
{{ t('Mis à jour {date}', { date: formatRelativeIfRecentDate(chart.last_modified, { dateStyle: 'medium' }) }) }}
</p>
Comment on lines +52 to +54

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can now use FormattedDate from #1198

</div>

<div class="mx-0 -mb-1 flex flex-wrap items-center text-sm text-gray-medium mt-1">
<p
class="text-sm mb-0 flex items-center gap-0.5"
:aria-label="t('{n} vues | {n} vue | {n} vues', chart.metrics.views)"
>
<RiEyeLine
aria-hidden="true"
class="size-3.5"
/>{{ summarize(chart.metrics.views) }}
</p>
</div>

<ObjectCardShortDescription :text="chart.description" />

<slot />
</ObjectCard>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { RiBarChartBoxLine, RiEyeLine, RiLineChartLine, RiSubtractLine } from '@remixicon/vue'
import type { RouteLocationRaw } from 'vue-router'
import type { Chart } from '../types/visualizations'
import { useFormatDate } from '../functions/dates'
import { summarize } from '../functions/helpers'
import { useTranslation } from '../composables/useTranslation'
import Placeholder from './Placeholder.vue'
import ObjectCard from './ObjectCard.vue'
import ObjectCardHeader from './ObjectCardHeader.vue'
import ObjectCardOwner from './ObjectCardOwner.vue'
import ObjectCardShortDescription from './ObjectCardShortDescription.vue'

const props = defineProps<{
chart: Chart
chartUrl?: RouteLocationRaw
organizationUrl?: RouteLocationRaw
}>()

const { t } = useTranslation()
const { formatRelativeIfRecentDate } = useFormatDate()

const chartIcon = computed(() => {
return props.chart.series[0]?.type === 'line' ? RiLineChartLine : RiBarChartBoxLine
})
</script>
5 changes: 4 additions & 1 deletion datagouv-components/src/components/ObjectCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,15 @@ import { computed } from 'vue'

const props = withDefaults(defineProps<{
articleClass?: string | string[] | Record<string, boolean>
mediaSize?: 'sm' | 'lg'
mediaSize?: 'sm' | 'lg' | 'xl'
}>(), {
mediaSize: 'sm',
})

const mediaContainerClass = computed(() => {
if (props.mediaSize === 'xl') {
return 'w-[250px] h-[190px]'
}
if (props.mediaSize === 'lg') {
return 'w-[225px] h-[120px]'
}
Expand Down
Loading
Loading