@datagouv/components-next 1.0.2-dev.8 → 1.0.2-dev.81
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/assets/main.css +4 -0
- package/dist/Datafair.client-BzW-ctDf.js +30 -0
- package/dist/JsonPreview.client-BfMSzR07.js +40 -0
- package/dist/{MapContainer.client-DRkAmdOc.js → MapContainer.client-CLs-im9i.js} +35 -38
- package/dist/{PdfPreview.client-C-w6-w44.js → PdfPreview.client-C13PQCU_.js} +822 -865
- package/dist/{Pmtiles.client-BR7_ldHY.js → Pmtiles.client-CL7PXXDl.js} +574 -579
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-C6XnsZ-7.js +61 -0
- package/dist/XmlPreview.client-KaENrbbG.js +34 -0
- package/dist/components-next.css +3 -3
- package/dist/components-next.js +166 -148
- package/dist/components.css +1 -1
- package/dist/{index-SrYZwgCT.js → index-C7WVVGgD.js} +1 -1
- package/dist/{main-B2kXxWRG.js → main-K-42Oe8-.js} +91315 -75834
- package/dist/{vue3-xml-viewer.common-BRxsqI9j.js → vue3-xml-viewer.common-sHPSE-jD.js} +1 -1
- package/package.json +16 -10
- package/src/components/ActivityList/ActivityList.vue +0 -2
- package/src/components/Chart/ChartViewer.vue +226 -0
- package/src/components/Chart/ChartViewerWrapper.vue +170 -0
- package/src/components/Form/Listbox.vue +101 -0
- package/src/components/Form/SearchableSelect.vue +2 -1
- package/src/components/InfiniteLoader.vue +53 -0
- package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
- package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
- package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
- package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
- package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
- package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
- package/src/components/OpenApiViewer/openapi.ts +150 -0
- package/src/components/OrganizationNameWithCertificate.vue +3 -2
- package/src/components/Pagination.vue +8 -5
- package/src/components/ReadMore.vue +1 -1
- package/src/components/ResourceAccordion/Datafair.client.vue +4 -10
- package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -121
- package/src/components/ResourceAccordion/MapContainer.client.vue +7 -11
- package/src/components/ResourceAccordion/Metadata.vue +1 -2
- package/src/components/ResourceAccordion/PdfPreview.client.vue +24 -103
- package/src/components/ResourceAccordion/Pmtiles.client.vue +5 -10
- package/src/components/ResourceAccordion/Preview.vue +16 -21
- package/src/components/ResourceAccordion/PreviewLoader.vue +1 -2
- package/src/components/ResourceAccordion/PreviewUnavailable.vue +22 -0
- package/src/components/ResourceAccordion/PreviewWrapper.vue +82 -0
- package/src/components/ResourceAccordion/ResourceAccordion.vue +5 -7
- package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -115
- package/src/components/ResourceExplorer/ResourceExplorer.vue +81 -13
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +2 -2
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +30 -11
- package/src/components/Search/GlobalSearch.vue +173 -108
- package/src/components/Search/SearchInput.vue +3 -3
- package/src/components/TabularExplorer/TabularCell.vue +51 -0
- package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
- package/src/components/TabularExplorer/TabularExplorer.vue +870 -0
- package/src/components/TabularExplorer/TabularFilterContent.vue +351 -0
- package/src/components/TabularExplorer/TabularFilterPopover.vue +111 -0
- package/src/components/TabularExplorer/types.ts +83 -0
- package/src/composables/useHasTabularData.ts +6 -0
- package/src/composables/useResourceCapabilities.ts +1 -1
- package/src/composables/useSearchFilter.ts +118 -0
- package/src/composables/useStableQueryParams.ts +31 -3
- package/src/config.ts +3 -0
- package/src/functions/api.ts +34 -33
- package/src/functions/api.types.ts +1 -0
- package/src/functions/charts.ts +68 -0
- package/src/functions/datasets.ts +0 -17
- package/src/functions/resources.ts +56 -1
- package/src/functions/tabular.ts +60 -0
- package/src/functions/tabularApi.ts +138 -11
- package/src/main.ts +55 -7
- package/src/types/dataservices.ts +2 -0
- package/src/types/pages.ts +0 -5
- package/src/types/posts.ts +2 -2
- package/src/types/reports.ts +3 -0
- package/src/types/search.ts +52 -1
- package/src/types/site.ts +5 -3
- package/src/types/users.ts +0 -1
- package/src/types/visualizations.ts +89 -0
- package/assets/swagger-themes/newspaper.css +0 -1670
- package/dist/Datafair.client-E5D6ePRC.js +0 -35
- package/dist/JsonPreview.client-C-6eBbPw.js +0 -87
- package/dist/Swagger.client-D4-F6yEf.js +0 -4
- package/dist/XmlPreview.client-Dl2VCgXF.js +0 -79
- package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
- package/src/functions/pagination.ts +0 -9
- /package/assets/illustrations/{_microscope.svg → microscope.svg} +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { ref, watch, type Ref } from 'vue'
|
|
1
|
+
import { computed, ref, watch, type Ref } from 'vue'
|
|
2
2
|
import type { SearchTypeConfig } from '../types/search'
|
|
3
|
+
import { configKey, forEachActiveCustomFilter, type CustomFilterEntry } from './useSearchFilter'
|
|
3
4
|
|
|
4
5
|
type FilterRefs = Record<string, Ref<unknown>>
|
|
5
6
|
|
|
6
7
|
interface StableQueryParamsOptions {
|
|
7
8
|
typeConfig: SearchTypeConfig | undefined
|
|
8
9
|
allFilters: FilterRefs
|
|
10
|
+
customFilterRegistry: Map<string, CustomFilterEntry>
|
|
9
11
|
q: Ref<string>
|
|
10
12
|
sort: Ref<string | undefined>
|
|
11
13
|
page: Ref<number>
|
|
@@ -17,7 +19,7 @@ interface StableQueryParamsOptions {
|
|
|
17
19
|
* Applies hiddenFilters first, then user filters (which can override hiddenFilters).
|
|
18
20
|
*/
|
|
19
21
|
export function useStableQueryParams(options: StableQueryParamsOptions) {
|
|
20
|
-
const { typeConfig, allFilters, q, sort, page, pageSize } = options
|
|
22
|
+
const { typeConfig, allFilters, customFilterRegistry, q, sort, page, pageSize } = options
|
|
21
23
|
const stableParams = ref<Record<string, unknown>>({})
|
|
22
24
|
|
|
23
25
|
const buildParams = () => {
|
|
@@ -50,6 +52,22 @@ export function useStableQueryParams(options: StableQueryParamsOptions) {
|
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
// 3.5. Apply custom filter values. Concatenate into an array on collision
|
|
56
|
+
// so a custom filter mapped onto a built-in apiParam (e.g. theme → tag)
|
|
57
|
+
// combines with an existing built-in value instead of overwriting it.
|
|
58
|
+
// Pass the current type key so filters scoped to specific types are excluded
|
|
59
|
+
// from background fetches for other types.
|
|
60
|
+
const currentTypeKey = typeConfig ? configKey(typeConfig) : undefined
|
|
61
|
+
forEachActiveCustomFilter(customFilterRegistry, (apiParam, value) => {
|
|
62
|
+
const existing = params[apiParam]
|
|
63
|
+
if (existing === undefined) {
|
|
64
|
+
params[apiParam] = value
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
params[apiParam] = Array.isArray(existing) ? [...existing, value] : [existing, value]
|
|
68
|
+
}
|
|
69
|
+
}, currentTypeKey)
|
|
70
|
+
|
|
53
71
|
// 4. Always include q, sort (if valid for this type), page, page_size
|
|
54
72
|
if (q.value) {
|
|
55
73
|
params.q = q.value
|
|
@@ -66,9 +84,19 @@ export function useStableQueryParams(options: StableQueryParamsOptions) {
|
|
|
66
84
|
return params
|
|
67
85
|
}
|
|
68
86
|
|
|
87
|
+
// Computed that reads all custom filter values, establishing reactive dependencies
|
|
88
|
+
// on both the Map mutations (shallowReactive) and each entry's ref.value.
|
|
89
|
+
const customFilterValues = computed(() => {
|
|
90
|
+
const snapshot: Record<string, unknown> = {}
|
|
91
|
+
for (const [urlParam, entry] of customFilterRegistry) {
|
|
92
|
+
snapshot[urlParam] = entry.ref.value
|
|
93
|
+
}
|
|
94
|
+
return snapshot
|
|
95
|
+
})
|
|
96
|
+
|
|
69
97
|
// Watch all dependencies and update only if content changed
|
|
70
98
|
watch(
|
|
71
|
-
[q, sort, page, ...Object.values(allFilters)],
|
|
99
|
+
[q, sort, page, ...Object.values(allFilters), customFilterValues],
|
|
72
100
|
() => {
|
|
73
101
|
const newParams = buildParams()
|
|
74
102
|
// JSON.stringify comparison is safe here because buildParams() builds the object deterministically
|
package/src/config.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type { FetchOptions } from 'ofetch'
|
|
|
5
5
|
export type PluginConfig = {
|
|
6
6
|
name: string // Name of the application (ex: data.gouv.fr)
|
|
7
7
|
baseUrl: string
|
|
8
|
+
/** Hostnames allowed in Access-Control-Allow-Origin for resource preview CORS checks (e.g. data.gouv.fr). */
|
|
9
|
+
trustedDomains?: string[]
|
|
8
10
|
apiBase: string
|
|
9
11
|
devApiKey?: string | null
|
|
10
12
|
datasetQualityGuideUrl?: string
|
|
@@ -18,6 +20,7 @@ export type PluginConfig = {
|
|
|
18
20
|
schemasSiteUrl?: string
|
|
19
21
|
schemasSiteName?: string
|
|
20
22
|
tabularApiUrl?: string
|
|
23
|
+
chartsApiBase?: string
|
|
21
24
|
tabularApiPageSize?: number
|
|
22
25
|
tabularAllowRemote?: boolean
|
|
23
26
|
tabularApiDataserviceId?: string
|
package/src/functions/api.ts
CHANGED
|
@@ -24,6 +24,7 @@ export async function useFetch<DataT, ErrorT = never>(
|
|
|
24
24
|
return await config.customUseFetch(url, options)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
const isRaw = options?.raw
|
|
27
28
|
const data: Ref<DataT | null> = ref(null)
|
|
28
29
|
const error: Ref<ErrorT | null> = ref(null)
|
|
29
30
|
const status = ref<AsyncDataRequestStatus>('idle')
|
|
@@ -35,37 +36,39 @@ export async function useFetch<DataT, ErrorT = never>(
|
|
|
35
36
|
const query = deepToValue(options?.query)
|
|
36
37
|
status.value = 'pending'
|
|
37
38
|
try {
|
|
38
|
-
data.value =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
config.onRequest
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
39
|
+
data.value = isRaw
|
|
40
|
+
? await ofetch<DataT | null>(urlValue, { params: params ?? query })
|
|
41
|
+
: await ofetch<DataT | null>(urlValue, {
|
|
42
|
+
baseURL: config.apiBase,
|
|
43
|
+
params: params ?? query,
|
|
44
|
+
onRequest(param) {
|
|
45
|
+
if (config.onRequest) {
|
|
46
|
+
if (Array.isArray(config.onRequest)) {
|
|
47
|
+
config.onRequest.forEach(r => r(param))
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
config.onRequest(param)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const { options } = param
|
|
54
|
+
options.headers.set('Content-Type', 'application/json')
|
|
55
|
+
options.headers.set('Accept', 'application/json')
|
|
56
|
+
options.credentials = 'include'
|
|
57
|
+
if (config.devApiKey) {
|
|
58
|
+
options.headers.set('X-API-KEY', config.devApiKey)
|
|
59
|
+
}
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
if (locale) {
|
|
62
|
+
if (!options.params) {
|
|
63
|
+
options.params = {}
|
|
64
|
+
}
|
|
65
|
+
options.params['lang'] = locale
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
onRequestError: config.onRequestError,
|
|
69
|
+
onResponse: config.onResponse,
|
|
70
|
+
onResponseError: config.onResponseError,
|
|
71
|
+
})
|
|
69
72
|
status.value = 'success'
|
|
70
73
|
}
|
|
71
74
|
catch (e) {
|
|
@@ -90,9 +93,7 @@ export async function useFetch<DataT, ErrorT = never>(
|
|
|
90
93
|
|
|
91
94
|
return {
|
|
92
95
|
data,
|
|
93
|
-
refresh:
|
|
94
|
-
execute()
|
|
95
|
-
},
|
|
96
|
+
refresh: () => execute(),
|
|
96
97
|
execute,
|
|
97
98
|
clear: () => {
|
|
98
99
|
data.value = null
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Chart, ChartForm, ChartForApi, Filter } from '../types/visualizations'
|
|
2
|
+
|
|
3
|
+
export function toChartForm(chart: Chart) {
|
|
4
|
+
const seriesFilter = chart.series[0]?.filters as Filter | null
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
title: chart.title,
|
|
8
|
+
description: chart.description,
|
|
9
|
+
private: chart.private,
|
|
10
|
+
owned: chart.organization ? { organization: chart.organization.id, owner: null } : { owner: chart.owner.id, organization: null },
|
|
11
|
+
x_axis: {
|
|
12
|
+
column_x: chart.x_axis.column_x,
|
|
13
|
+
type: chart.x_axis.type,
|
|
14
|
+
sort_combined: chart.x_axis.sort_x_by && chart.x_axis.sort_x_direction
|
|
15
|
+
? `${chart.x_axis.sort_x_by}-${chart.x_axis.sort_x_direction}`
|
|
16
|
+
: '',
|
|
17
|
+
},
|
|
18
|
+
y_axis: {
|
|
19
|
+
label: chart.y_axis.label || '',
|
|
20
|
+
min: chart.y_axis.min,
|
|
21
|
+
max: chart.y_axis.max,
|
|
22
|
+
unit: chart.y_axis.unit || '',
|
|
23
|
+
unit_position: chart.y_axis.unit_position || 'suffix',
|
|
24
|
+
},
|
|
25
|
+
series: chart.series.map(serie => ({
|
|
26
|
+
...serie,
|
|
27
|
+
aggregate_y: serie.aggregate_y || '',
|
|
28
|
+
})),
|
|
29
|
+
extras: chart.extras,
|
|
30
|
+
chart_type: chart.series[0] ? chart.series[0].type : null,
|
|
31
|
+
filter: seriesFilter ?? null,
|
|
32
|
+
} satisfies ChartForm
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function toChartApi(chartForm: ChartForm): ChartForApi {
|
|
36
|
+
const xAxis = {
|
|
37
|
+
column_x: chartForm.x_axis.column_x,
|
|
38
|
+
type: chartForm.x_axis.type,
|
|
39
|
+
sort_x_by: chartForm.x_axis.sort_combined
|
|
40
|
+
? (chartForm.x_axis.sort_combined.split('-')[0] as 'axis_x' | 'axis_y')
|
|
41
|
+
: null,
|
|
42
|
+
sort_x_direction: chartForm.x_axis.sort_combined
|
|
43
|
+
? (chartForm.x_axis.sort_combined.split('-')[1] as 'asc' | 'desc')
|
|
44
|
+
: null,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
...chartForm.owned,
|
|
49
|
+
title: chartForm.title,
|
|
50
|
+
description: chartForm.description,
|
|
51
|
+
private: chartForm.private,
|
|
52
|
+
x_axis: xAxis,
|
|
53
|
+
y_axis: {
|
|
54
|
+
label: chartForm.y_axis.label ?? null,
|
|
55
|
+
min: chartForm.y_axis.min ?? null,
|
|
56
|
+
max: chartForm.y_axis.max ?? null,
|
|
57
|
+
unit: chartForm.y_axis.unit ?? null,
|
|
58
|
+
unit_position: chartForm.y_axis.unit_position ?? null,
|
|
59
|
+
},
|
|
60
|
+
series: chartForm.series.map((serie, index) => ({
|
|
61
|
+
...serie,
|
|
62
|
+
type: index === 0 && chartForm.chart_type ? chartForm.chart_type : serie.type,
|
|
63
|
+
aggregate_y: serie.aggregate_y || null,
|
|
64
|
+
filters: serie.filters || (index === 0 && chartForm.filter ? chartForm.filter : null),
|
|
65
|
+
})),
|
|
66
|
+
extras: chartForm.extras,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { useComponentsConfig } from '../config'
|
|
2
|
-
import type { Dataset, DatasetV2 } from '../types/datasets'
|
|
3
|
-
import type { CommunityResource, Resource } from '../types/resources'
|
|
4
2
|
|
|
5
3
|
function constructUrl(baseUrl: string, path: string): string {
|
|
6
4
|
const url = new URL(baseUrl)
|
|
@@ -14,18 +12,3 @@ export function getDatasetOEmbedHtml(type: string, id: string): string {
|
|
|
14
12
|
const staticUrl = constructUrl(config.baseUrl, 'oembed.js')
|
|
15
13
|
return `<div data-udata-${type}="${id}"></div><script data-udata="${config.baseUrl}" src="${staticUrl}" async defer></script>`
|
|
16
14
|
}
|
|
17
|
-
|
|
18
|
-
export function isCommunityResource(resource: Resource | CommunityResource): boolean {
|
|
19
|
-
return 'organization' in resource || 'owner' in resource
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function getResourceExternalUrl(dataset: Dataset | DatasetV2 | Omit<Dataset, 'resources' | 'community_resources'>, resource: Resource | CommunityResource): string {
|
|
23
|
-
return `${dataset.page}${isCommunityResource(resource) ? '/community-resources' : ''}?resource_id=${resource.id}`
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function getResourceFilesize(resource: Resource): null | number {
|
|
27
|
-
if (resource.filesize) return resource.filesize
|
|
28
|
-
if ('analysis:content-length' in resource.extras) return resource.extras['analysis:content-length'] as number
|
|
29
|
-
|
|
30
|
-
return null
|
|
31
|
-
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { readonly, type Component } from 'vue'
|
|
2
2
|
|
|
3
3
|
import { RiEarthLine, RiMap2Line } from '@remixicon/vue'
|
|
4
|
+
import { useComponentsConfig } from '../config'
|
|
4
5
|
import Archive from '../components/Icons/Archive.vue'
|
|
5
6
|
import Code from '../components/Icons/Code.vue'
|
|
7
|
+
import type { Dataset, DatasetV2 } from '../types/datasets'
|
|
6
8
|
import Documentation from '../components/Icons/Documentation.vue'
|
|
7
9
|
import Image from '../components/Icons/Image.vue'
|
|
8
10
|
import Link from '../components/Icons/Link.vue'
|
|
9
11
|
import Table from '../components/Icons/Table.vue'
|
|
10
|
-
import type { Resource } from '../types/resources'
|
|
12
|
+
import type { CommunityResource, Resource } from '../types/resources'
|
|
11
13
|
import { useTranslation } from '../composables/useTranslation'
|
|
12
14
|
|
|
13
15
|
export function getResourceFormatIcon(format: string): Component | null {
|
|
@@ -129,3 +131,56 @@ export const detectOgcService = (resource: Resource) => {
|
|
|
129
131
|
}
|
|
130
132
|
return false
|
|
131
133
|
}
|
|
134
|
+
|
|
135
|
+
export function isCommunityResource(resource: Resource | CommunityResource): boolean {
|
|
136
|
+
return 'organization' in resource || 'owner' in resource
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getResourceExternalUrl(dataset: Dataset | DatasetV2 | Omit<Dataset, 'resources' | 'community_resources'>, resource: Resource | CommunityResource): string {
|
|
140
|
+
return `${dataset.page}${isCommunityResource(resource) ? '/community-resources' : ''}?resource_id=${resource.id}`
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getResourceFilesize(resource: Resource): null | number {
|
|
144
|
+
if (resource.filesize) return resource.filesize
|
|
145
|
+
if ('analysis:content-length' in resource.extras) return resource.extras['analysis:content-length'] as number
|
|
146
|
+
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
type CorsStatus = 'allowed' | 'blocked' | 'unknown'
|
|
151
|
+
|
|
152
|
+
export const getResourceCorsStatus = (resource: Resource): CorsStatus => {
|
|
153
|
+
const extras = resource.extras
|
|
154
|
+
if (!extras || !('check:cors:allow-origin' in extras)) {
|
|
155
|
+
return 'unknown'
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const allowOrigin = extras['check:cors:allow-origin'] as string | undefined
|
|
159
|
+
const rawMethods = extras['check:cors:allow-methods'] as string | undefined
|
|
160
|
+
|
|
161
|
+
// Check if allow-origin is '*' or contains one of our trusted domains
|
|
162
|
+
const config = useComponentsConfig()
|
|
163
|
+
const trustedDomains = config.trustedDomains ?? []
|
|
164
|
+
const hasPublicCors = allowOrigin === '*'
|
|
165
|
+
const hasSpecificCors = allowOrigin
|
|
166
|
+
? trustedDomains.some((domain) => {
|
|
167
|
+
try {
|
|
168
|
+
const hostname = new URL(allowOrigin).hostname
|
|
169
|
+
return hostname === domain || hostname.endsWith(`.${domain}`)
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return false
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
: false
|
|
176
|
+
|
|
177
|
+
const isOriginAllowed = hasPublicCors || hasSpecificCors
|
|
178
|
+
|
|
179
|
+
// Ensure GET method is allowed
|
|
180
|
+
const allowedMethods = rawMethods
|
|
181
|
+
? rawMethods.split(',').map(m => m.trim().toUpperCase())
|
|
182
|
+
: []
|
|
183
|
+
const supportsGet = allowedMethods.length === 0 || allowedMethods.includes('GET')
|
|
184
|
+
|
|
185
|
+
return isOriginAllowed && supportsGet ? 'allowed' : 'blocked'
|
|
186
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Component } from 'vue'
|
|
2
|
+
import {
|
|
3
|
+
RiHashtag,
|
|
4
|
+
RiPriceTag3Line,
|
|
5
|
+
RiText,
|
|
6
|
+
RiCalendarLine,
|
|
7
|
+
RiCheckboxLine,
|
|
8
|
+
} from '@remixicon/vue'
|
|
9
|
+
import { useTranslation } from '../composables/useTranslation'
|
|
10
|
+
import type { ColumnFilters, ColumnType } from '../components/TabularExplorer/types'
|
|
11
|
+
|
|
12
|
+
export function hasFilterForColumn(filters: Record<string, ColumnFilters>, column: string): boolean {
|
|
13
|
+
const f = filters[column]
|
|
14
|
+
if (!f) return false
|
|
15
|
+
return !!(f.in?.length || f.exact != null || f.contains || f.null || f.min != null || f.max != null)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildTypeConfig(t: (s: string) => string): Record<ColumnType, { icon: Component, label: string }> {
|
|
19
|
+
return {
|
|
20
|
+
number: { icon: RiHashtag, label: t('Nombre') },
|
|
21
|
+
categorical: { icon: RiPriceTag3Line, label: t('Catégoriel') },
|
|
22
|
+
text: { icon: RiText, label: t('Texte') },
|
|
23
|
+
date: { icon: RiCalendarLine, label: t('Date') },
|
|
24
|
+
boolean: { icon: RiCheckboxLine, label: t('Booléen') },
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useFormatTabular() {
|
|
29
|
+
const { locale } = useTranslation()
|
|
30
|
+
|
|
31
|
+
function formatNumber(value: unknown): string {
|
|
32
|
+
const num = Number(value)
|
|
33
|
+
if (Number.isNaN(num)) return String(value)
|
|
34
|
+
return num.toLocaleString(locale)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatCellDate(value: unknown): string {
|
|
38
|
+
if (value == null || value === '') return '–'
|
|
39
|
+
const d = new Date(String(value))
|
|
40
|
+
if (Number.isNaN(d.getTime())) return String(value)
|
|
41
|
+
return new Intl.DateTimeFormat(locale, { day: '2-digit', month: '2-digit', year: 'numeric' }).format(d)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { formatNumber, formatCellDate }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const TRUTHY_VALUES = ['true', '1', 'oui', 'yes']
|
|
48
|
+
const FALSY_VALUES = ['false', '0', 'non', 'no']
|
|
49
|
+
|
|
50
|
+
export function isTruthy(value: unknown): boolean {
|
|
51
|
+
if (typeof value === 'boolean') return value
|
|
52
|
+
if (typeof value === 'string') return TRUTHY_VALUES.includes(value.toLowerCase())
|
|
53
|
+
return Boolean(value)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isFalsy(value: unknown): boolean {
|
|
57
|
+
if (typeof value === 'boolean') return !value
|
|
58
|
+
if (typeof value === 'string') return FALSY_VALUES.includes(value.toLowerCase())
|
|
59
|
+
return !value
|
|
60
|
+
}
|
|
@@ -1,20 +1,147 @@
|
|
|
1
1
|
import { ofetch } from 'ofetch'
|
|
2
2
|
import { useComponentsConfig, type PluginConfig } from '../config'
|
|
3
|
+
import type { GenericFilter } from '../types/visualizations'
|
|
4
|
+
import type { SortConfig } from '../components/TabularExplorer/types'
|
|
3
5
|
|
|
4
|
-
export type SortConfig
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
export type { SortConfig }
|
|
7
|
+
|
|
8
|
+
export type TabularDataResponse = {
|
|
9
|
+
data: Array<Record<string, unknown>>
|
|
10
|
+
links: {
|
|
11
|
+
profile: string
|
|
12
|
+
swagger: string
|
|
13
|
+
next: string
|
|
14
|
+
}
|
|
15
|
+
meta: { total: number }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type TabularAggregateType = 'avg' | 'sum' | 'count' | 'min' | 'max'
|
|
19
|
+
|
|
20
|
+
export type FetchTabularDataOptions = {
|
|
21
|
+
resourceId: string
|
|
22
|
+
page?: number
|
|
23
|
+
pageSize?: number
|
|
24
|
+
columns?: Array<string> | undefined
|
|
25
|
+
sort?: SortConfig
|
|
26
|
+
groupBy?: string | undefined
|
|
27
|
+
aggregation?: {
|
|
28
|
+
column: string
|
|
29
|
+
type: TabularAggregateType
|
|
30
|
+
} | undefined
|
|
31
|
+
filters?: GenericFilter | undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type TabularProfileResponse = {
|
|
35
|
+
profile: {
|
|
36
|
+
header: Array<string>
|
|
37
|
+
columns: Record<string, {
|
|
38
|
+
score: number
|
|
39
|
+
format: string
|
|
40
|
+
python_type: string
|
|
41
|
+
}>
|
|
42
|
+
formats: Record<string, Array<string>>
|
|
43
|
+
profile: Record<string, {
|
|
44
|
+
tops: Array<{ count: number, value: string }>
|
|
45
|
+
nb_distinct: number
|
|
46
|
+
nb_missing_values: number
|
|
47
|
+
min?: number
|
|
48
|
+
max?: number
|
|
49
|
+
std?: number
|
|
50
|
+
mean?: number
|
|
51
|
+
}>
|
|
52
|
+
encoding: string
|
|
53
|
+
separator: string
|
|
54
|
+
categorical: Array<string>
|
|
55
|
+
total_lines: number
|
|
56
|
+
nb_duplicates: number
|
|
57
|
+
columns_fields: Record<string, {
|
|
58
|
+
score: number
|
|
59
|
+
format: string
|
|
60
|
+
python_type: string
|
|
61
|
+
}>
|
|
62
|
+
columns_labels: Record<string, {
|
|
63
|
+
score: number
|
|
64
|
+
format: string
|
|
65
|
+
python_type: string
|
|
66
|
+
}>
|
|
67
|
+
header_row_idx: number
|
|
68
|
+
heading_columns: number
|
|
69
|
+
trailing_columns: number
|
|
70
|
+
}
|
|
71
|
+
deleted_at: string | null
|
|
72
|
+
dataset_id: string
|
|
73
|
+
indexes: null
|
|
74
|
+
}
|
|
8
75
|
|
|
9
76
|
/**
|
|
10
|
-
* Call Tabular-api to get table content
|
|
77
|
+
* Call Tabular-api to get table content with options object
|
|
11
78
|
*/
|
|
12
|
-
export async function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
79
|
+
export async function fetchTabularData(config: PluginConfig, options: FetchTabularDataOptions): Promise<TabularDataResponse> {
|
|
80
|
+
const page = options.page ?? 1
|
|
81
|
+
const pageSize = options.pageSize ?? config.tabularApiPageSize ?? 15
|
|
82
|
+
let url = `${config.tabularApiUrl}/api/resources/${options.resourceId}/data/?page=${page}&page_size=${pageSize}`
|
|
83
|
+
if (options.columns) {
|
|
84
|
+
url += `&columns=${options.columns.map(col => encodeURIComponent(col)).join(',')}`
|
|
85
|
+
}
|
|
86
|
+
if (options.sort) {
|
|
87
|
+
url += `&${encodeURIComponent(options.sort.column)}__sort=${encodeURIComponent(options.sort.direction)}`
|
|
16
88
|
}
|
|
17
|
-
|
|
89
|
+
if (options.groupBy && options.aggregation?.type) {
|
|
90
|
+
url += `&${encodeURIComponent(options.groupBy)}__groupby&${encodeURIComponent(options.aggregation.column)}__${encodeURIComponent(options.aggregation.type)}`
|
|
91
|
+
}
|
|
92
|
+
if (options.filters) {
|
|
93
|
+
const filterQuery = buildFilterQuery(options.filters)
|
|
94
|
+
if (filterQuery) {
|
|
95
|
+
url += `&${filterQuery}`
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return await ofetch<TabularDataResponse>(url)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function buildFilterQuery(filters: GenericFilter): string {
|
|
102
|
+
const params: Array<string> = []
|
|
103
|
+
if ('filters' in filters) {
|
|
104
|
+
for (const filter of filters.filters) {
|
|
105
|
+
if ('filters' in filter) {
|
|
106
|
+
params.push(buildFilterQuery(filter))
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
if (filter.condition === 'is_null') {
|
|
110
|
+
params.push(`${encodeURIComponent(filter.column)}__isnull`)
|
|
111
|
+
}
|
|
112
|
+
else if (filter.condition === 'is_not_null') {
|
|
113
|
+
params.push(`${encodeURIComponent(filter.column)}__isnotnull`)
|
|
114
|
+
}
|
|
115
|
+
else if (filter.value !== null && filter.value !== undefined && filter.value !== '') {
|
|
116
|
+
params.push(`${encodeURIComponent(filter.column)}__${encodeURIComponent(filter.condition)}=${encodeURIComponent(filter.value)}`)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const filter = filters
|
|
123
|
+
if (filter.condition === 'is_null') {
|
|
124
|
+
params.push(`${encodeURIComponent(filter.column)}__isnull`)
|
|
125
|
+
}
|
|
126
|
+
else if (filter.condition === 'is_not_null') {
|
|
127
|
+
params.push(`${encodeURIComponent(filter.column)}__isnotnull`)
|
|
128
|
+
}
|
|
129
|
+
else if (filter.value !== null && filter.value !== undefined && filter.value !== '') {
|
|
130
|
+
params.push(`${encodeURIComponent(filter.column)}__${encodeURIComponent(filter.condition)}=${encodeURIComponent(filter.value)}`)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return params.join('&')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Call Tabular-api to get table content
|
|
138
|
+
*/
|
|
139
|
+
export async function getData(config: PluginConfig, id: string, page: number, sortConfig?: SortConfig | null): Promise<TabularDataResponse> {
|
|
140
|
+
return fetchTabularData(config, {
|
|
141
|
+
resourceId: id,
|
|
142
|
+
page,
|
|
143
|
+
sort: sortConfig ?? undefined,
|
|
144
|
+
})
|
|
18
145
|
}
|
|
19
146
|
|
|
20
147
|
/**
|
|
@@ -22,5 +149,5 @@ export async function getData(config: PluginConfig, id: string, page: number, so
|
|
|
22
149
|
*/
|
|
23
150
|
export function useGetProfile() {
|
|
24
151
|
const config = useComponentsConfig()
|
|
25
|
-
return (id: string) => ofetch(`${config.tabularApiUrl}/api/resources/${id}/profile/`)
|
|
152
|
+
return (id: string) => ofetch<TabularProfileResponse>(`${config.tabularApiUrl}/api/resources/${id}/profile/`)
|
|
26
153
|
}
|