Skip to content
Open
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
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="RiPieChartLine"
:label="$t('Graphiques')"
to="/admin/site/charts"
/>
<AdminSidebarLink
:icon="RiBuilding2Line"
:label="$t('Organisations')"
Expand Down Expand Up @@ -201,7 +206,7 @@
import { Avatar, OrganizationLogo } from '@datagouv/components-next'
import type { OrganizationReference, User } from '@datagouv/components-next'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
import { RiAlarmWarningLine, RiArticleLine, RiAwardLine, RiBarChartBoxLine, RiBookShelfLine, RiBuilding2Line, RiChat3Line, RiDatabase2Line, RiGitPullRequestLine, RiGroup3Line, RiLineChartLine, RiParentLine, RiPlanetLine, RiTerminalLine, RiServerLine, RiUserLine } from '@remixicon/vue'
import { RiAlarmWarningLine, RiArticleLine, RiAwardLine, RiBarChartBoxLine, RiBookShelfLine, RiBuilding2Line, RiChat3Line, RiDatabase2Line, RiGitPullRequestLine, RiGroup3Line, RiLineChartLine, RiParentLine, RiPieChartLine, RiPlanetLine, RiTerminalLine, RiServerLine, RiUserLine } from '@remixicon/vue'
import { key, type AccordionRegister } from '~/components/Accordion/injectionKey'
import AdminSidebarLink from '~/components/AdminSidebar/AdminSidebarLink/AdminSidebarLink.vue'

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, query: params })
</script>
193 changes: 68 additions & 125 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 @@ -473,11 +433,10 @@

<script setup lang="ts">
import type { Resource, PaginatedArray, ChartForm, Chart, Filter, AndFilters, GenericFilter, ColumnType, ColumnDefinition, ColumnsDefinition, DataSeriesType, DataSeriesForm, FilterCondition, CombinedSort, Owned, XAxisType } from '@datagouv/components-next'
import { buildTypeConfig, buildColumnsFromProfile, useGetProfile, useHasTabularData, toast, BrandedButton, toChartApi, toChartForm, SearchableSelect, Listbox, useTranslation } from '@datagouv/components-next'
import { buildTypeConfig, buildColumnsFromProfile, useGetProfile, useHasTabularData, toast, BrandedButton, toChartApi, SearchableSelect, Listbox, useTranslation } from '@datagouv/components-next'
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,14 @@ const form = defineModel<ChartForm>({
required: true,
})

const props = defineProps<{
/**
* Full chart being edited, used only to initialize producer/resources/dataset
* and to keep the chart id for saving — the form data itself comes from the v-model
*/
initialChart?: Chart
}>()

const columns = ref<ColumnsDefinition>({})
const producer = ref<Owned | null>(null)
const dataset = ref<DatasetSuggest | null>(null)
Expand All @@ -541,7 +507,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,101 +757,75 @@ async function suggestDataset(q: string): Promise<Array<DatasetSuggest>> {
})
}

async function loadChart(id: string) {
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)
}
}

form.value = toChartForm(data)
async function initializeFromChart(data: Chart) {
savedChart.value = data

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

if (data.organization) {
producer.value = { organization: data.organization, owner: null }
}
if (data.owner) {
producer.value = { organization: null, owner: data.owner }
}
await loadMissingResourcesForChart(Array.from(chartResources))
await loadColumnsForResources(Array.from(chartResources))

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 (data.series.length > 0 && data.series[0]?.resource_id) {
await nextTick()
if (data.organization) {
producer.value = { organization: data.organization, owner: null }
}
if (data.owner) {
producer.value = { organization: null, owner: data.owner }
}

selectedResource.value = data.series[0].resource_id
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
}

toast.success(t('Graphique chargé !'))
}
catch (error) {
console.error('Failed to load dataset for chart:', error)
}
}
catch (error) {
console.error('Failed to load chart:', error)
toast.error(t('Erreur lors du chargement du graphique'))
}
}

function loadSelectedChart() {
if (selectedChartId.value) {
loadChart(selectedChartId.value)
if (data.series.length > 0 && data.series[0]?.resource_id) {
await nextTick()

selectedResource.value = data.series[0].resource_id
}
}

async function saveChart() {
try {
const chartForApi = toChartApi(form.value)
const update = savedChart.value?.id
if (update) {
savedChart.value = await $api<Chart>(`/api/1/visualizations/${savedChart.value!.id}/`, {
method: 'PATCH',
body: JSON.stringify(chartForApi),
})
}
else {
savedChart.value = await $api<Chart>('/api/1/visualizations/', {
method: 'POST',
body: JSON.stringify(chartForApi),
})
}

const imageUrl = chartViewerWrapperRef.value?.capture()
if (imageUrl) {
const i = await fetch(imageUrl)
const imageBlob = await i.blob()
const formData = new FormData()
formData.set('file', imageBlob, 'image.png')
await $fileApi(`/api/1/visualizations/${savedChart.value.id}/image/`, {
method: 'POST',
body: formData,
})
}

toast.success(update ? t('Graphique mis à jour !') : t('Graphique sauvegardé !'))
await refresh()
selectedChartId.value = savedChart.value.id
const chartForApi = toChartApi(form.value)
const update = savedChart.value?.id
if (update) {
savedChart.value = await $api<Chart>(`/api/1/visualizations/${savedChart.value!.id}/`, {
method: 'PATCH',
body: JSON.stringify(chartForApi),
})
}
catch (error) {
console.error('Failed to save chart:', error)
else {
savedChart.value = await $api<Chart>('/api/1/visualizations/', {
method: 'POST',
body: JSON.stringify(chartForApi),
})
}

const imageUrl = chartViewerWrapperRef.value?.capture()
if (imageUrl) {
const i = await fetch(imageUrl)
const imageBlob = await i.blob()
const formData = new FormData()
formData.set('file', imageBlob, 'image.png')
await $fileApi(`/api/1/visualizations/${savedChart.value.id}/image/`, {
method: 'POST',
body: formData,
})
}

toast.success(update ? t('Graphique mis à jour !') : t('Graphique sauvegardé !'))
}

function removeFilter(index: number) {
Expand Down Expand Up @@ -1025,4 +964,8 @@ watch(
},
{ immediate: true },
)

if (props.initialChart) {
await initializeFromChart(props.initialChart)
}
</script>
Loading
Loading