@datagouv/components-next 1.0.2-dev.181 → 1.0.2-dev.183
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{Datafair.client-CaxMyNif.js → Datafair.client-D0E6rbay.js} +1 -1
- package/dist/{ImagePreview.client-CPmCyisP.js → ImagePreview.client-HoFfwk5b.js} +1 -1
- package/dist/{JsonPreview.client-CKOU-LBs.js → JsonPreview.client-B4zhw3J3.js} +2 -2
- package/dist/{MapContainer.client-IGYJWP5m.js → MapContainer.client-piLVEoQA.js} +2 -2
- package/dist/{PdfPreview.client-csUebU3A.js → PdfPreview.client-BjOjQS21.js} +2 -2
- package/dist/{Pmtiles.client-CLq7UHBg.js → Pmtiles.client-CgP56qtf.js} +1 -1
- package/dist/{PreviewWrapper.vue_vue_type_script_setup_true_lang-DeoHZXW1.js → PreviewWrapper.vue_vue_type_script_setup_true_lang-U260inZw.js} +1 -1
- package/dist/{XmlPreview.client-C1toAbQ0.js → XmlPreview.client-SbISXrsW.js} +3 -3
- package/dist/components-next.js +67 -66
- package/dist/{index-CaUlhl3k.js → index-NypeTAz3.js} +1 -1
- package/dist/{leaflet-src-ByYGrf8H.js → leaflet-src-C54gIk3N.js} +1 -1
- package/dist/{main-DD4ieUAo.js → main-OEOVgq_i.js} +7976 -7959
- package/dist/{vue3-xml-viewer.common-B1KPK4L4.js → vue3-xml-viewer.common-BBV8Gl3e.js} +1 -1
- package/package.json +1 -1
- package/src/composables/useResourceCapabilities.ts +2 -20
- package/src/functions/helpers.ts +15 -0
- package/src/functions/metrics.ts +57 -56
- package/src/functions/resourceCapabilities.ts +34 -0
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { computed, type MaybeRefOrGetter, toValue } from 'vue'
|
|
2
2
|
import { useComponentsConfig } from '../config'
|
|
3
3
|
import { isOrganizationCertified } from '../functions/organizations'
|
|
4
|
-
import { getWfsExportFormats } from '../functions/resourceCapabilities'
|
|
4
|
+
import { getGeneratedFormats, getWfsExportFormats } from '../functions/resourceCapabilities'
|
|
5
5
|
import { detectOgcService, getParsingErrorMessage, getParsingErrorStep, isImagePreviewFormat } from '../functions/resources'
|
|
6
6
|
import type { Dataset, DatasetV2 } from '../types/datasets'
|
|
7
7
|
import type { Resource, WfsMetadata } from '../types/resources'
|
|
8
8
|
import { useHasTabularData } from './useHasTabularData'
|
|
9
9
|
import { useTranslation } from './useTranslation'
|
|
10
10
|
|
|
11
|
-
const GENERATED_FORMATS = ['parquet', 'pmtiles', 'geojson']
|
|
12
11
|
const URL_FORMATS = ['url', 'doi', 'www:link', 'www:link-1.0-http--link', 'www:link-1.0-http--partners', 'www:link-1.0-http--related', 'www:link-1.0-http--samples']
|
|
13
12
|
|
|
14
13
|
// Which preview the data tab shows. Single source of truth: the viewer renders it and
|
|
@@ -54,24 +53,7 @@ export function useResourceCapabilities(
|
|
|
54
53
|
const hasPmtilesError = computed(() => getParsingErrorStep(toValue(resource)) === 'pmtiles_export')
|
|
55
54
|
const pmtilesError = computed(() => hasPmtilesError.value ? getParsingErrorMessage(toValue(resource)) : null)
|
|
56
55
|
|
|
57
|
-
const generatedFormats = computed(() =>
|
|
58
|
-
const r = toValue(resource)
|
|
59
|
-
const formats = GENERATED_FORMATS
|
|
60
|
-
.filter(format => `analysis:parsing:${format}_url` in r.extras)
|
|
61
|
-
.map(format => ({
|
|
62
|
-
url: r.extras[`analysis:parsing:${format}_url`] as string,
|
|
63
|
-
size: r.extras[`analysis:parsing:${format}_size`] as number | undefined,
|
|
64
|
-
format,
|
|
65
|
-
}))
|
|
66
|
-
if ('analysis:parsing:parsing_table' in r.extras) {
|
|
67
|
-
formats.push({
|
|
68
|
-
url: `${config.tabularApiUrl}/api/resources/${r.id}/data/json/`,
|
|
69
|
-
size: undefined,
|
|
70
|
-
format: 'json',
|
|
71
|
-
})
|
|
72
|
-
}
|
|
73
|
-
return formats
|
|
74
|
-
})
|
|
56
|
+
const generatedFormats = computed(() => getGeneratedFormats(toValue(resource), config.tabularApiUrl))
|
|
75
57
|
|
|
76
58
|
const wfsFormats = computed(() => {
|
|
77
59
|
return getWfsExportFormats(toValue(resource))
|
package/src/functions/helpers.ts
CHANGED
|
@@ -38,6 +38,21 @@ export const summarize = (val: number, fractionDigits = 0) => {
|
|
|
38
38
|
return `${toFixedIfNotZero(val)}Y`
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// Guards any value that ends up in an `href`: only absolute http(s) URLs are allowed.
|
|
42
|
+
// Some of these URLs come from resource extras, which are freely writable by the
|
|
43
|
+
// resource owner, so an unguarded value could carry a `javascript:` or `data:` scheme
|
|
44
|
+
// and execute in the platform origin when a visitor clicks the link.
|
|
45
|
+
export const isSafeHttpUrl = (value: unknown): value is string => {
|
|
46
|
+
if (typeof value !== 'string') return false
|
|
47
|
+
try {
|
|
48
|
+
const { protocol } = new URL(value)
|
|
49
|
+
return protocol === 'http:' || protocol === 'https:'
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
41
56
|
export const escapeCsvValue = (value: string | number | null | undefined): string => {
|
|
42
57
|
if (value === null || value === undefined || value === '') {
|
|
43
58
|
return ''
|
package/src/functions/metrics.ts
CHANGED
|
@@ -31,49 +31,73 @@ export type ReuseMetrics = {
|
|
|
31
31
|
visitsTotal: number
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
type MonthlyVisitsRow = { metric_month: string, monthly_visit: number }
|
|
35
|
+
type MonthlyDatasetRow = MonthlyVisitsRow & { monthly_download_resource: number }
|
|
36
|
+
type MonthlyOrganizationRow = {
|
|
37
|
+
metric_month: string
|
|
38
|
+
monthly_download_resource: number
|
|
39
|
+
monthly_visit_dataservice: number
|
|
40
|
+
monthly_visit_dataset: number
|
|
41
|
+
monthly_visit_reuse: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type TotalVisitsRow = { visit: number }
|
|
45
|
+
type TotalDatasetRow = TotalVisitsRow & { download_resource: number }
|
|
46
|
+
type TotalOrganizationRow = {
|
|
47
|
+
download_resource: number
|
|
48
|
+
visit_dataservice: number
|
|
49
|
+
visit_dataset: number
|
|
50
|
+
visit_reuse: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The metrics API is a separate service that answers over the network, so a call fails in
|
|
55
|
+
* three ways: the request itself, an error status, or a body that isn't the expected page
|
|
56
|
+
* of rows. Rejecting on the last two turns them into the same failure as the first, which
|
|
57
|
+
* callers already hand to `useAsyncData` — reading `page.data` straight would instead throw
|
|
58
|
+
* a `TypeError` a few lines later, naming a symptom rather than the service.
|
|
59
|
+
*/
|
|
60
|
+
async function fetchMetrics<Row>(url: string): Promise<Array<Row>> {
|
|
61
|
+
const response = await fetch(url)
|
|
62
|
+
if (!response.ok) throw new Error(`The metrics API answered ${response.status} for ${url}`)
|
|
63
|
+
|
|
64
|
+
const page: { data?: Array<Row> } = await response.json()
|
|
65
|
+
if (!Array.isArray(page?.data)) throw new Error(`The metrics API answered an unexpected body for ${url}`)
|
|
66
|
+
|
|
67
|
+
return page.data
|
|
68
|
+
}
|
|
69
|
+
|
|
34
70
|
/**
|
|
35
71
|
* There is only one metrics API endpoint to get these 3 values.
|
|
36
72
|
* The rest of the metrics aren't stored yet at the organization level
|
|
37
73
|
*/
|
|
38
74
|
export async function getOrganizationMetrics(oid: string, metricsApi: string): Promise<OrganizationMetrics> {
|
|
39
75
|
// Fetching last 12 months
|
|
40
|
-
const
|
|
41
|
-
const page = await response.json()
|
|
76
|
+
const rows = await fetchMetrics<MonthlyOrganizationRow>(`${metricsApi}/api/organizations/data/?organization_id__exact=${oid}&metric_month__sort=desc&page_size=12`)
|
|
42
77
|
|
|
43
78
|
const dataservicesViews: Record<string, number> = {}
|
|
44
79
|
const datasetsViews: Record<string, number> = {}
|
|
45
80
|
const downloads: Record<string, number> = {}
|
|
46
81
|
const reusesViews: Record<string, number> = {}
|
|
47
82
|
|
|
48
|
-
for (const { metric_month, monthly_download_resource, monthly_visit_dataservice, monthly_visit_dataset, monthly_visit_reuse } of
|
|
83
|
+
for (const { metric_month, monthly_download_resource, monthly_visit_dataservice, monthly_visit_dataset, monthly_visit_reuse } of rows) {
|
|
49
84
|
dataservicesViews[metric_month] = monthly_visit_dataservice
|
|
50
85
|
datasetsViews[metric_month] = monthly_visit_dataset
|
|
51
86
|
downloads[metric_month] = monthly_download_resource
|
|
52
87
|
reusesViews[metric_month] = monthly_visit_reuse
|
|
53
88
|
}
|
|
54
89
|
// Fetching totals
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
let dataservicesViewsTotal = 0
|
|
59
|
-
let datasetsViewsTotal = 0
|
|
60
|
-
let downloadsTotal = 0
|
|
61
|
-
let reusesViewsTotal = 0
|
|
62
|
-
if (page.data[0]) {
|
|
63
|
-
dataservicesViewsTotal = totalPage.data[0].visit_dataservice
|
|
64
|
-
datasetsViewsTotal = totalPage.data[0].visit_dataset
|
|
65
|
-
downloadsTotal = totalPage.data[0].download_resource
|
|
66
|
-
reusesViewsTotal = totalPage.data[0].visit_reuse
|
|
67
|
-
}
|
|
90
|
+
const total = (await fetchMetrics<TotalOrganizationRow>(`${metricsApi}/api/organizations_total/data/?organization_id__exact=${oid}`))[0]
|
|
91
|
+
|
|
68
92
|
return {
|
|
69
93
|
downloads,
|
|
70
|
-
downloadsTotal,
|
|
94
|
+
downloadsTotal: total?.download_resource ?? 0,
|
|
71
95
|
reusesViews,
|
|
72
|
-
reusesViewsTotal,
|
|
96
|
+
reusesViewsTotal: total?.visit_reuse ?? 0,
|
|
73
97
|
dataservicesViews,
|
|
74
|
-
dataservicesViewsTotal,
|
|
98
|
+
dataservicesViewsTotal: total?.visit_dataservice ?? 0,
|
|
75
99
|
datasetsViews,
|
|
76
|
-
datasetsViewsTotal,
|
|
100
|
+
datasetsViewsTotal: total?.visit_dataset ?? 0,
|
|
77
101
|
}
|
|
78
102
|
}
|
|
79
103
|
|
|
@@ -89,33 +113,24 @@ export function createOrganizationMetricsUrl(datasetsViews: Record<string, numbe
|
|
|
89
113
|
|
|
90
114
|
export async function getDatasetMetrics(datasetId: string, metricsApi: string): Promise<DatasetMetrics> {
|
|
91
115
|
// Fetching last 12 months
|
|
92
|
-
const
|
|
93
|
-
const page = await response.json()
|
|
116
|
+
const rows = await fetchMetrics<MonthlyDatasetRow>(`${metricsApi}/api/datasets/data/?dataset_id__exact=${datasetId}&metric_month__sort=desc&page_size=12`)
|
|
94
117
|
|
|
95
118
|
const visits: Record<string, number> = {}
|
|
96
119
|
const downloads: Record<string, number> = {}
|
|
97
120
|
|
|
98
|
-
for (const { metric_month, monthly_visit, monthly_download_resource } of
|
|
121
|
+
for (const { metric_month, monthly_visit, monthly_download_resource } of rows) {
|
|
99
122
|
visits[metric_month] = monthly_visit
|
|
100
123
|
downloads[metric_month] = monthly_download_resource
|
|
101
124
|
}
|
|
102
125
|
|
|
103
126
|
// Fetching totals
|
|
104
|
-
const
|
|
105
|
-
const totalPage = await totalResponse.json()
|
|
106
|
-
|
|
107
|
-
let visitsTotal = 0
|
|
108
|
-
let downloadsTotal = 0
|
|
109
|
-
if (totalPage.data[0]) {
|
|
110
|
-
visitsTotal = totalPage.data[0].visit
|
|
111
|
-
downloadsTotal = totalPage.data[0].download_resource
|
|
112
|
-
}
|
|
127
|
+
const total = (await fetchMetrics<TotalDatasetRow>(`${metricsApi}/api/datasets_total/data/?dataset_id__exact=${datasetId}`))[0]
|
|
113
128
|
|
|
114
129
|
return {
|
|
115
130
|
visits,
|
|
116
|
-
visitsTotal,
|
|
131
|
+
visitsTotal: total?.visit ?? 0,
|
|
117
132
|
downloads,
|
|
118
|
-
downloadsTotal,
|
|
133
|
+
downloadsTotal: total?.download_resource ?? 0,
|
|
119
134
|
}
|
|
120
135
|
}
|
|
121
136
|
|
|
@@ -151,52 +166,38 @@ export async function createDatasetsForOrganizationMetricsUrl(organizationId: st
|
|
|
151
166
|
|
|
152
167
|
export async function getDataserviceMetrics(dataserviceId: string, metricsApi: string): Promise<DataserviceMetrics> {
|
|
153
168
|
// Fetching last 12 months
|
|
154
|
-
const
|
|
155
|
-
const page = await response.json()
|
|
169
|
+
const rows = await fetchMetrics<MonthlyVisitsRow>(`${metricsApi}/api/dataservices/data/?dataservice_id__exact=${dataserviceId}&metric_month__sort=desc&page_size=12`)
|
|
156
170
|
|
|
157
171
|
const visits: Record<string, number> = {}
|
|
158
172
|
|
|
159
|
-
for (const { metric_month, monthly_visit } of
|
|
173
|
+
for (const { metric_month, monthly_visit } of rows) {
|
|
160
174
|
visits[metric_month] = monthly_visit
|
|
161
175
|
}
|
|
162
176
|
|
|
163
177
|
// Fetching totals
|
|
164
|
-
const
|
|
165
|
-
const totalPage = await totalResponse.json()
|
|
166
|
-
|
|
167
|
-
let visitsTotal = 0
|
|
168
|
-
if (totalPage.data[0]) {
|
|
169
|
-
visitsTotal = totalPage.data[0].visit
|
|
170
|
-
}
|
|
178
|
+
const total = (await fetchMetrics<TotalVisitsRow>(`${metricsApi}/api/dataservices_total/data/?dataservice_id__exact=${dataserviceId}`))[0]
|
|
171
179
|
|
|
172
180
|
return {
|
|
173
181
|
visits,
|
|
174
|
-
visitsTotal,
|
|
182
|
+
visitsTotal: total?.visit ?? 0,
|
|
175
183
|
}
|
|
176
184
|
}
|
|
177
185
|
|
|
178
186
|
export async function getReuseMetrics(reuseId: string, metricsApi: string): Promise<ReuseMetrics> {
|
|
179
187
|
// Fetching last 12 months
|
|
180
|
-
const
|
|
181
|
-
const page = await response.json()
|
|
188
|
+
const rows = await fetchMetrics<MonthlyVisitsRow>(`${metricsApi}/api/reuses/data/?reuse_id__exact=${reuseId}&metric_month__sort=desc&page_size=12`)
|
|
182
189
|
|
|
183
190
|
const visits: Record<string, number> = {}
|
|
184
191
|
|
|
185
|
-
for (const { metric_month, monthly_visit } of
|
|
192
|
+
for (const { metric_month, monthly_visit } of rows) {
|
|
186
193
|
visits[metric_month] = monthly_visit
|
|
187
194
|
}
|
|
188
195
|
|
|
189
196
|
// Fetching totals
|
|
190
|
-
const
|
|
191
|
-
const totalPage = await totalResponse.json()
|
|
192
|
-
|
|
193
|
-
let visitsTotal = 0
|
|
194
|
-
if (totalPage.data[0]) {
|
|
195
|
-
visitsTotal = totalPage.data[0].visit
|
|
196
|
-
}
|
|
197
|
+
const total = (await fetchMetrics<TotalVisitsRow>(`${metricsApi}/api/reuses_total/data/?reuse_id__exact=${reuseId}`))[0]
|
|
197
198
|
|
|
198
199
|
return {
|
|
199
200
|
visits,
|
|
200
|
-
visitsTotal,
|
|
201
|
+
visitsTotal: total?.visit ?? 0,
|
|
201
202
|
}
|
|
202
203
|
}
|
|
@@ -1,4 +1,35 @@
|
|
|
1
1
|
import type { Resource, WfsMetadata, OgcLayerInfo } from '../types/resources'
|
|
2
|
+
import { isSafeHttpUrl } from './helpers'
|
|
3
|
+
|
|
4
|
+
const GENERATED_FORMATS = ['parquet', 'pmtiles', 'geojson']
|
|
5
|
+
|
|
6
|
+
export type GeneratedFormat = {
|
|
7
|
+
url: string
|
|
8
|
+
size: number | undefined
|
|
9
|
+
format: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getGeneratedFormats(resource: Pick<Resource, 'extras' | 'id'>, tabularApiUrl: string | undefined): GeneratedFormat[] {
|
|
13
|
+
const formats: GeneratedFormat[] = GENERATED_FORMATS.flatMap((format) => {
|
|
14
|
+
const url = resource.extras[`analysis:parsing:${format}_url`]
|
|
15
|
+
// The URL is read verbatim from owner-writable extras: drop any entry that is not
|
|
16
|
+
// a plain http(s) URL so a `javascript:`/`data:` value never reaches an href.
|
|
17
|
+
if (!isSafeHttpUrl(url)) return []
|
|
18
|
+
return [{
|
|
19
|
+
url,
|
|
20
|
+
size: resource.extras[`analysis:parsing:${format}_size`] as number | undefined,
|
|
21
|
+
format,
|
|
22
|
+
}]
|
|
23
|
+
})
|
|
24
|
+
if ('analysis:parsing:parsing_table' in resource.extras) {
|
|
25
|
+
formats.push({
|
|
26
|
+
url: `${tabularApiUrl}/api/resources/${resource.id}/data/json/`,
|
|
27
|
+
size: undefined,
|
|
28
|
+
format: 'json',
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
return formats
|
|
32
|
+
}
|
|
2
33
|
|
|
3
34
|
const WFS_EXPORT_FORMATS = [
|
|
4
35
|
{
|
|
@@ -46,6 +77,9 @@ export function getWfsExportFormats(resource: Pick<Resource, 'extras' | 'url'>)
|
|
|
46
77
|
const outputFormats = wfsMetadata.output_formats.map((format: string) => format.toLowerCase())
|
|
47
78
|
const layer = wfsMetadata.detected_layer
|
|
48
79
|
if (!layer) return []
|
|
80
|
+
// Every download URL below is built on top of `resource.url`, so its scheme is the one
|
|
81
|
+
// of `resource.url`: guard it once here rather than on each of the built URLs.
|
|
82
|
+
if (!isSafeHttpUrl(resource.url)) return []
|
|
49
83
|
const formats = WFS_EXPORT_FORMATS.filter(format => outputFormats.includes(format.mimetype.toLowerCase()))
|
|
50
84
|
.map(format => ({
|
|
51
85
|
url: buildWfsDownloadUrl(resource.url, wfsMetadata, format, layer),
|