@datagouv/components-next 0.0.1
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/README.md +150 -0
- package/assets/main.css +136 -0
- package/assets/placeholders/author.png +0 -0
- package/assets/placeholders/dataset.png +0 -0
- package/assets/placeholders/news.png +0 -0
- package/assets/placeholders/organization.png +0 -0
- package/assets/placeholders/reuse.png +0 -0
- package/assets/tailwind.config.js +24 -0
- package/dist/components.css +2 -0
- package/dist/locales/de.js +155 -0
- package/dist/locales/en.js +155 -0
- package/dist/locales/es.js +155 -0
- package/dist/locales/fr.js +155 -0
- package/dist/locales/it.js +155 -0
- package/dist/locales/pt.js +155 -0
- package/dist/locales/sr.js +155 -0
- package/package.json +72 -0
- package/src/components/AppLink.vue +51 -0
- package/src/components/Avatar.vue +27 -0
- package/src/components/AvatarWithName.vue +26 -0
- package/src/components/BannerAction.vue +39 -0
- package/src/components/BrandedButton.vue +170 -0
- package/src/components/CopyButton.vue +84 -0
- package/src/components/DataserviceCard.vue +184 -0
- package/src/components/DatasetCard.vue +198 -0
- package/src/components/DatasetInformationPanel.vue +210 -0
- package/src/components/DatasetQuality.vue +68 -0
- package/src/components/DatasetQualityInline.vue +32 -0
- package/src/components/DatasetQualityItem.vue +32 -0
- package/src/components/DatasetQualityItemWarning.vue +21 -0
- package/src/components/DatasetQualityScore.vue +35 -0
- package/src/components/DatasetQualityTooltipContent.vue +79 -0
- package/src/components/DescriptionDetails.vue +23 -0
- package/src/components/DescriptionList/DescriptionDetails.stories.ts +43 -0
- package/src/components/DescriptionList/DescriptionList.stories.ts +47 -0
- package/src/components/DescriptionList/DescriptionTerm.stories.ts +28 -0
- package/src/components/DescriptionList.vue +8 -0
- package/src/components/DescriptionTerm.vue +8 -0
- package/src/components/ExtraAccordion.vue +78 -0
- package/src/components/Icons/Archive.vue +21 -0
- package/src/components/Icons/Code.vue +21 -0
- package/src/components/Icons/Documentation.vue +21 -0
- package/src/components/Icons/File.vue +21 -0
- package/src/components/Icons/Image.vue +7 -0
- package/src/components/Icons/Link.vue +21 -0
- package/src/components/Icons/Table.vue +21 -0
- package/src/components/OrganizationCard.vue +68 -0
- package/src/components/OrganizationNameWithCertificate.vue +45 -0
- package/src/components/OwnerType.vue +43 -0
- package/src/components/OwnerTypeIcon.vue +18 -0
- package/src/components/Pagination.vue +205 -0
- package/src/components/Placeholder.vue +29 -0
- package/src/components/ReadMore.vue +107 -0
- package/src/components/ResourceAccordion/DataStructure.vue +87 -0
- package/src/components/ResourceAccordion/EditButton.vue +34 -0
- package/src/components/ResourceAccordion/Metadata.vue +171 -0
- package/src/components/ResourceAccordion/Preview.vue +229 -0
- package/src/components/ResourceAccordion/PreviewLoader.vue +148 -0
- package/src/components/ResourceAccordion/ResourceAccordion.vue +484 -0
- package/src/components/ResourceAccordion/ResourceIcon.vue +16 -0
- package/src/components/ResourceAccordion/SchemaBadge.vue +148 -0
- package/src/components/ResourceAccordion/SchemaLoader.vue +30 -0
- package/src/components/ResourceAccordion/Swagger.vue +46 -0
- package/src/components/ResourceAccordion/france.svg +1 -0
- package/src/components/ReuseCard.vue +106 -0
- package/src/components/ReuseDetails.vue +45 -0
- package/src/components/SimpleBanner.vue +24 -0
- package/src/components/SmallChart.vue +149 -0
- package/src/components/StatBox.vue +100 -0
- package/src/components/Tabs/Tab.vue +62 -0
- package/src/components/Tabs/TabGroup.vue +20 -0
- package/src/components/Tabs/TabList.vue +15 -0
- package/src/components/Tabs/TabPanel.vue +7 -0
- package/src/components/Tabs/TabPanels.vue +7 -0
- package/src/components/Toggletip.vue +62 -0
- package/src/components/ToggletipButton.vue +14 -0
- package/src/composables/useActiveDescendant.ts +103 -0
- package/src/composables/useReuseType.ts +14 -0
- package/src/config.ts +33 -0
- package/src/functions/api.ts +96 -0
- package/src/functions/api.types.ts +41 -0
- package/src/functions/config.ts +12 -0
- package/src/functions/datasets.ts +24 -0
- package/src/functions/dates.ts +85 -0
- package/src/functions/helpers.ts +38 -0
- package/src/functions/markdown.ts +47 -0
- package/src/functions/matomo.ts +3 -0
- package/src/functions/organizations.ts +85 -0
- package/src/functions/owned.ts +11 -0
- package/src/functions/resources.ts +99 -0
- package/src/functions/reuses.ts +28 -0
- package/src/functions/schemas.ts +96 -0
- package/src/functions/tabularApi.ts +27 -0
- package/src/functions/users.ts +7 -0
- package/src/locales/de.json +154 -0
- package/src/locales/en.json +154 -0
- package/src/locales/es.json +154 -0
- package/src/locales/fr.json +154 -0
- package/src/locales/it.json +154 -0
- package/src/locales/pt.json +154 -0
- package/src/locales/sr.json +154 -0
- package/src/main.ts +147 -0
- package/src/types/badges.ts +5 -0
- package/src/types/contact_point.ts +7 -0
- package/src/types/dataservices.ts +68 -0
- package/src/types/datasets.ts +80 -0
- package/src/types/frequency.ts +6 -0
- package/src/types/granularity.ts +6 -0
- package/src/types/harvest.ts +3 -0
- package/src/types/keyboard.ts +1 -0
- package/src/types/licenses.ts +9 -0
- package/src/types/organizations.ts +41 -0
- package/src/types/owned.ts +9 -0
- package/src/types/resources.ts +37 -0
- package/src/types/reuses.ts +49 -0
- package/src/types/site.ts +23 -0
- package/src/types/topics.ts +20 -0
- package/src/types/ui.ts +3 -0
- package/src/types/users.ts +10 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="border border-gray-default"
|
|
4
|
+
:class="{ 'fr-pb-4v': open }"
|
|
5
|
+
>
|
|
6
|
+
<header
|
|
7
|
+
:id="resourceHeaderId"
|
|
8
|
+
class="fr-p-4v flex items-center justify-between relative"
|
|
9
|
+
>
|
|
10
|
+
<div>
|
|
11
|
+
<div class="flex items-center fr-mb-1v">
|
|
12
|
+
<h4
|
|
13
|
+
:id="resourceTitleId"
|
|
14
|
+
class="fr-m-0"
|
|
15
|
+
>
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
class="fr-p-0 flex items-baseline text-base leading-none font-normal"
|
|
19
|
+
data-testid="expand-button"
|
|
20
|
+
:aria-expanded="open"
|
|
21
|
+
@click="toggle"
|
|
22
|
+
>
|
|
23
|
+
<ResourceIcon
|
|
24
|
+
:resource
|
|
25
|
+
class="size-3.5 mr-1"
|
|
26
|
+
/>
|
|
27
|
+
<span
|
|
28
|
+
:class="{
|
|
29
|
+
'font-bold': open,
|
|
30
|
+
}"
|
|
31
|
+
><component
|
|
32
|
+
:is="config.textClamp"
|
|
33
|
+
v-if="config && config.textClamp"
|
|
34
|
+
:max-lines="1"
|
|
35
|
+
:text="resource.title || t('Nameless file')"
|
|
36
|
+
/></span>
|
|
37
|
+
|
|
38
|
+
<span class="absolute inset-0 z-1" />
|
|
39
|
+
</button>
|
|
40
|
+
</h4>
|
|
41
|
+
<CopyButton
|
|
42
|
+
:label="$t('Copy link')"
|
|
43
|
+
:copied-label="$t('Link copied!')"
|
|
44
|
+
:text="resourceExternalUrl"
|
|
45
|
+
class="z-2"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="text-gray-medium subheaders-infos">
|
|
49
|
+
<SchemaBadge
|
|
50
|
+
:resource
|
|
51
|
+
class="dash-after"
|
|
52
|
+
/>
|
|
53
|
+
<span class="fr-text--xs fr-mb-0 dash-after">{{ t('Updated {date}', { date: formatRelativeIfRecentDate(lastUpdate) }) }}</span>
|
|
54
|
+
<span
|
|
55
|
+
v-if="resource.format"
|
|
56
|
+
class="fr-text--xs fr-mb-0 dash-after"
|
|
57
|
+
>
|
|
58
|
+
<span class="hidden show-on-small">{{ t("Format") }}</span>
|
|
59
|
+
{{ resource.format.trim().toLowerCase() }}
|
|
60
|
+
<span v-if="resource.filesize">({{ filesize(resource.filesize) }})</span>
|
|
61
|
+
</span>
|
|
62
|
+
<span
|
|
63
|
+
class="inline-flex items-center fr-text--xs fr-mb-0"
|
|
64
|
+
:aria-label="t('{n} downloads', resource.metrics.views)"
|
|
65
|
+
>
|
|
66
|
+
<span class="fr-icon-download-line fr-icon--xs fr-mr-1v" />
|
|
67
|
+
<span>{{ summarize(resource.metrics.views) }} <span class="hidden show-on-small">{{ t("downloads") }}</span></span>
|
|
68
|
+
</span>
|
|
69
|
+
</div>
|
|
70
|
+
<p
|
|
71
|
+
v-if="communityResource"
|
|
72
|
+
class="fr-mb-0 fr-mt-1v fr-text--xs text-gray-medium"
|
|
73
|
+
>
|
|
74
|
+
{{ t('From') }}
|
|
75
|
+
<a
|
|
76
|
+
v-if="communityResource.organization"
|
|
77
|
+
class="fr-link fr-text--xs"
|
|
78
|
+
:href="communityResource.organization.page"
|
|
79
|
+
>
|
|
80
|
+
<OrganizationNameWithCertificate :organization="communityResource.organization" />
|
|
81
|
+
</a>
|
|
82
|
+
<template v-else-if="owner">
|
|
83
|
+
{{ owner }}
|
|
84
|
+
</template>
|
|
85
|
+
</p>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="flex items-center fr-ml-4v buttons">
|
|
88
|
+
<p
|
|
89
|
+
v-if="unavailable"
|
|
90
|
+
class="text-default-warning fr-m-0 fr-mr-2v"
|
|
91
|
+
>
|
|
92
|
+
{{ t('Unavailable') }}
|
|
93
|
+
</p>
|
|
94
|
+
<p
|
|
95
|
+
v-if="resource.format === 'url'"
|
|
96
|
+
class="fr-col-auto fr-ml-3v fr-m-0 z-2"
|
|
97
|
+
>
|
|
98
|
+
<BrandedButton
|
|
99
|
+
:href="resource.latest"
|
|
100
|
+
:title="t('File link - opens a new window')"
|
|
101
|
+
:aria-describedby="resourceTitleId"
|
|
102
|
+
rel="ugc nofollow noopener"
|
|
103
|
+
new-tab
|
|
104
|
+
size="xs"
|
|
105
|
+
>
|
|
106
|
+
{{ $t('Visit') }}
|
|
107
|
+
</BrandedButton>
|
|
108
|
+
</p>
|
|
109
|
+
<p
|
|
110
|
+
v-else-if="ogcService"
|
|
111
|
+
class="fr-col-auto fr-ml-3v fr-m-0 z-2"
|
|
112
|
+
>
|
|
113
|
+
<BrandedButton
|
|
114
|
+
:id="resource.id + '-copy'"
|
|
115
|
+
:data-clipboard-text="resource.url"
|
|
116
|
+
:aria-describedby="resourceTitleId"
|
|
117
|
+
color="primary"
|
|
118
|
+
size="xs"
|
|
119
|
+
:icon="RiFileCopyLine"
|
|
120
|
+
>
|
|
121
|
+
{{ t('Copy link') }}
|
|
122
|
+
</BrandedButton>
|
|
123
|
+
</p>
|
|
124
|
+
<p
|
|
125
|
+
v-else
|
|
126
|
+
class="fr-col-auto fr-ml-3v fr-m-0"
|
|
127
|
+
>
|
|
128
|
+
<BrandedButton
|
|
129
|
+
:href="resource.latest"
|
|
130
|
+
rel="ugc nofollow noopener"
|
|
131
|
+
:title="t('Download file')"
|
|
132
|
+
download
|
|
133
|
+
class="relative text-transform-uppercase matomo_download z-2"
|
|
134
|
+
:icon="RiDownloadLine"
|
|
135
|
+
size="xs"
|
|
136
|
+
:aria-describedby="resourceTitleId"
|
|
137
|
+
>
|
|
138
|
+
<span class="sr-only">{{ t('Download file as ') }}</span>{{ format }}
|
|
139
|
+
</BrandedButton>
|
|
140
|
+
</p>
|
|
141
|
+
<p
|
|
142
|
+
v-if="canEdit"
|
|
143
|
+
class="fr-col-auto fr-ml-3v fr-m-0 z-2"
|
|
144
|
+
>
|
|
145
|
+
<EditButton
|
|
146
|
+
:dataset-id="dataset.id"
|
|
147
|
+
:resource-id="resource.id"
|
|
148
|
+
:is-community-resource="isCommunityResource"
|
|
149
|
+
/>
|
|
150
|
+
</p>
|
|
151
|
+
<div
|
|
152
|
+
class="fr-icon--sm fr-ml-4v"
|
|
153
|
+
:class="{ 'fr-icon-arrow-up-s-line': open, 'fr-icon-arrow-down-s-line': !open }"
|
|
154
|
+
/>
|
|
155
|
+
</div>
|
|
156
|
+
</header>
|
|
157
|
+
<section
|
|
158
|
+
v-if="open"
|
|
159
|
+
:id="resourceContentId"
|
|
160
|
+
:aria-labelledby="resourceTitleId"
|
|
161
|
+
>
|
|
162
|
+
<TabGroup
|
|
163
|
+
size="sm"
|
|
164
|
+
@change="switchTab"
|
|
165
|
+
>
|
|
166
|
+
<div class="fr-pl-4v fr-pr-4v fr-pb-4v">
|
|
167
|
+
<TabList style="max-width: 100%; overflow-y: auto;">
|
|
168
|
+
<Tab
|
|
169
|
+
v-for="tab in tabsOptions"
|
|
170
|
+
:key="tab.key"
|
|
171
|
+
>
|
|
172
|
+
{{ tab.label }}
|
|
173
|
+
</Tab>
|
|
174
|
+
</TabList>
|
|
175
|
+
</div>
|
|
176
|
+
<TabPanels>
|
|
177
|
+
<TabPanel
|
|
178
|
+
v-for="tab in tabsOptions"
|
|
179
|
+
:key="tab.key"
|
|
180
|
+
>
|
|
181
|
+
<div v-if="tab.key === 'data'">
|
|
182
|
+
<Preview :resource="resource" />
|
|
183
|
+
</div>
|
|
184
|
+
<div
|
|
185
|
+
v-if="tab.key === 'description'"
|
|
186
|
+
class="fr-pl-4v fr-pr-4v"
|
|
187
|
+
>
|
|
188
|
+
<div
|
|
189
|
+
class="fr-mt-0 markdown fr-text--sm text-mention-grey"
|
|
190
|
+
v-html="markdown(resource.description || '')"
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
<div
|
|
194
|
+
v-if="tab.key === 'data-structure'"
|
|
195
|
+
class="fr-pl-4v fr-pr-4v"
|
|
196
|
+
>
|
|
197
|
+
<DataStructure
|
|
198
|
+
v-if="hasPreview"
|
|
199
|
+
:resource="resource"
|
|
200
|
+
/>
|
|
201
|
+
</div>
|
|
202
|
+
<div
|
|
203
|
+
v-if="tab.key === 'metadata'"
|
|
204
|
+
class="fr-pl-4v fr-pr-4v"
|
|
205
|
+
>
|
|
206
|
+
<Metadata :resource />
|
|
207
|
+
</div>
|
|
208
|
+
<div
|
|
209
|
+
v-if="tab.key === 'downloads'"
|
|
210
|
+
class="fr-pl-4v fr-pr-4v"
|
|
211
|
+
>
|
|
212
|
+
<dl class="fr-pl-0">
|
|
213
|
+
<dt
|
|
214
|
+
v-if="resource.format === 'url'"
|
|
215
|
+
class="font-bold fr-text--sm fr-mb-0"
|
|
216
|
+
>
|
|
217
|
+
{{ $t('Original URL') }}
|
|
218
|
+
</dt>
|
|
219
|
+
<dt
|
|
220
|
+
v-else
|
|
221
|
+
class="font-bold fr-text--sm fr-mb-0"
|
|
222
|
+
>
|
|
223
|
+
{{ $t('Original format') }}
|
|
224
|
+
</dt>
|
|
225
|
+
<dd class="text-sm ml-0 mt-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center">
|
|
226
|
+
<span v-if="resource.format === 'url'">
|
|
227
|
+
<a
|
|
228
|
+
:href="resource.latest"
|
|
229
|
+
class="fr-link no-icon-after"
|
|
230
|
+
rel="ugc nofollow noopener"
|
|
231
|
+
target="_blank"
|
|
232
|
+
>
|
|
233
|
+
<component
|
|
234
|
+
:is="config.textClamp"
|
|
235
|
+
v-if="config && config.textClamp"
|
|
236
|
+
:auto-resize="true"
|
|
237
|
+
:max-lines="1"
|
|
238
|
+
:text="resource.url"
|
|
239
|
+
>
|
|
240
|
+
<template #after>
|
|
241
|
+
<span class="fr-ml-1v fr-icon-external-link-line fr-icon--sm" />
|
|
242
|
+
</template>
|
|
243
|
+
</component>
|
|
244
|
+
</a>
|
|
245
|
+
</span>
|
|
246
|
+
<span v-else>
|
|
247
|
+
<span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
|
|
248
|
+
<a
|
|
249
|
+
:href="resource.latest"
|
|
250
|
+
class="fr-link"
|
|
251
|
+
rel="ugc nofollow noopener"
|
|
252
|
+
>
|
|
253
|
+
<span>{{ $t('Format {format}', { format: resource.format }) }}<span v-if="resource.filesize"> - {{ filesize(resource.filesize) }}</span></span>
|
|
254
|
+
</a>
|
|
255
|
+
</span>
|
|
256
|
+
<CopyButton
|
|
257
|
+
:label="$t('Copy link')"
|
|
258
|
+
:copied-label="$t('Link copied!')"
|
|
259
|
+
:text="resource.latest"
|
|
260
|
+
class="relative"
|
|
261
|
+
/>
|
|
262
|
+
</dd>
|
|
263
|
+
<template v-if="resource.extras['analysis:parsing:parquet_url']">
|
|
264
|
+
<dt class="font-bold fr-text--sm fr-mb-0">
|
|
265
|
+
{{ $t('Auto-generated formats from {platform}', { platform: config.name }) }}
|
|
266
|
+
</dt>
|
|
267
|
+
<dd class="text-sm ml-0 mt-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center">
|
|
268
|
+
<span>
|
|
269
|
+
<span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
|
|
270
|
+
<a
|
|
271
|
+
:href="resource.extras['analysis:parsing:parquet_url']"
|
|
272
|
+
class="fr-link"
|
|
273
|
+
rel="ugc nofollow noopener"
|
|
274
|
+
>
|
|
275
|
+
<span>{{ $t('Format {format}', { format: 'parquet' }) }}<span v-if="resource.extras['analysis:parsing:parquet_size']"> - {{ filesize(resource.extras['analysis:parsing:parquet_size']) }}</span></span>
|
|
276
|
+
</a>
|
|
277
|
+
</span>
|
|
278
|
+
<CopyButton
|
|
279
|
+
:label="$t('Copy link')"
|
|
280
|
+
:copied-label="$t('Link copied!')"
|
|
281
|
+
:text="resource.extras['analysis:parsing:parquet_url']"
|
|
282
|
+
class="relative"
|
|
283
|
+
/>
|
|
284
|
+
</dd>
|
|
285
|
+
</template>
|
|
286
|
+
</dl>
|
|
287
|
+
</div>
|
|
288
|
+
<div
|
|
289
|
+
v-if="tab.key === 'swagger'"
|
|
290
|
+
class="fr-pl-4v fr-pr-4v"
|
|
291
|
+
>
|
|
292
|
+
<div>{{ t('Swagger automatically generated by data.gouv.fr. This swagger allows you to query data by API by filtering it by column value.') }}</div>
|
|
293
|
+
<Swagger
|
|
294
|
+
v-if="hasPreview"
|
|
295
|
+
:url="`${config.tabularApiUrl}/api/resources/${props.resource.id}/swagger/`"
|
|
296
|
+
/>
|
|
297
|
+
</div>
|
|
298
|
+
</TabPanel>
|
|
299
|
+
</TabPanels>
|
|
300
|
+
</TabGroup>
|
|
301
|
+
</section>
|
|
302
|
+
</div>
|
|
303
|
+
</template>
|
|
304
|
+
|
|
305
|
+
<script setup lang="ts">
|
|
306
|
+
import { ref, computed, defineAsyncComponent } from 'vue'
|
|
307
|
+
import { useI18n } from 'vue-i18n'
|
|
308
|
+
import { RiDownloadLine, RiFileCopyLine } from '@remixicon/vue'
|
|
309
|
+
import OrganizationNameWithCertificate from '../OrganizationNameWithCertificate.vue'
|
|
310
|
+
import { filesize, summarize } from '../../functions/helpers'
|
|
311
|
+
import { markdown } from '../../functions/markdown'
|
|
312
|
+
import { formatRelativeIfRecentDate } from '../../functions/dates'
|
|
313
|
+
import type { CommunityResource, Resource } from '../../types/resources'
|
|
314
|
+
import type { Dataset, DatasetV2 } from '../../types/datasets'
|
|
315
|
+
import TabGroup from '../Tabs/TabGroup.vue'
|
|
316
|
+
import TabList from '../Tabs/TabList.vue'
|
|
317
|
+
import Tab from '../Tabs/Tab.vue'
|
|
318
|
+
import TabPanels from '../Tabs/TabPanels.vue'
|
|
319
|
+
import TabPanel from '../Tabs/TabPanel.vue'
|
|
320
|
+
import { trackEvent } from '../../functions/matomo'
|
|
321
|
+
import CopyButton from '../CopyButton.vue'
|
|
322
|
+
import { useComponentsConfig } from '../../config'
|
|
323
|
+
import { getOwnerName } from '../../functions/owned'
|
|
324
|
+
import { getResourceFormatIcon, getResourceTitleId } from '../../functions/resources'
|
|
325
|
+
import BrandedButton from '../BrandedButton.vue'
|
|
326
|
+
import { getResourceExternalUrl } from '../../functions/datasets'
|
|
327
|
+
import Metadata from './Metadata.vue'
|
|
328
|
+
import SchemaBadge from './SchemaBadge.vue'
|
|
329
|
+
import ResourceIcon from './ResourceIcon.vue'
|
|
330
|
+
import EditButton from './EditButton.vue'
|
|
331
|
+
import DataStructure from './DataStructure.vue'
|
|
332
|
+
import Preview from './Preview.vue'
|
|
333
|
+
|
|
334
|
+
const OGC_SERVICES_FORMATS = ['ogc:wfs', 'ogc:wms', 'wfs', 'wms']
|
|
335
|
+
|
|
336
|
+
const props = withDefaults(defineProps<{
|
|
337
|
+
dataset: Dataset | DatasetV2
|
|
338
|
+
expandedOnMount?: boolean
|
|
339
|
+
isCommunityResource?: boolean
|
|
340
|
+
resource: Resource | CommunityResource
|
|
341
|
+
canEdit?: boolean
|
|
342
|
+
}>(), {
|
|
343
|
+
expandedOnMount: false,
|
|
344
|
+
isCommunityResource: false,
|
|
345
|
+
canEdit: false,
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
const config = useComponentsConfig()
|
|
349
|
+
|
|
350
|
+
const Swagger = defineAsyncComponent(() => import('./Swagger.vue'))
|
|
351
|
+
|
|
352
|
+
const { t } = useI18n()
|
|
353
|
+
|
|
354
|
+
const hasPreview = computed(() => {
|
|
355
|
+
return config.tabularApiUrl
|
|
356
|
+
&& props.resource.extras['analysis:parsing:finished_at']
|
|
357
|
+
&& !props.resource.extras['analysis:parsing:error']
|
|
358
|
+
&& (config.tabularAllowRemote || props.resource.filetype === 'file')
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
const format = computed(() => getResourceFormatIcon(props.resource.format) ? props.resource.format : t('File'))
|
|
362
|
+
|
|
363
|
+
const ogcService = computed(() => OGC_SERVICES_FORMATS.includes(props.resource.format))
|
|
364
|
+
|
|
365
|
+
const open = ref(props.expandedOnMount)
|
|
366
|
+
const toggle = () => {
|
|
367
|
+
open.value = !open.value
|
|
368
|
+
|
|
369
|
+
if (open.value) {
|
|
370
|
+
trackEvent(['Open resource', props.resource.id])
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
trackEvent(['Close resource', props.resource.id])
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const tabsOptions = computed(() => {
|
|
378
|
+
const options = []
|
|
379
|
+
|
|
380
|
+
if (hasPreview.value) {
|
|
381
|
+
options.push({ key: 'data', label: t('Data') })
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (props.resource.description) {
|
|
385
|
+
options.push({ key: 'description', label: t('Description') })
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (hasPreview.value) {
|
|
389
|
+
options.push({ key: 'data-structure', label: t('Data structure') })
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
options.push({ key: 'metadata', label: t('Metadata') })
|
|
393
|
+
options.push({ key: 'downloads', label: t('Downloads') })
|
|
394
|
+
|
|
395
|
+
if (hasPreview.value) {
|
|
396
|
+
options.push({ key: 'swagger', label: t('Swagger') })
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return options
|
|
400
|
+
})
|
|
401
|
+
const switchTab = (index: number) => {
|
|
402
|
+
const option = tabsOptions.value[index]
|
|
403
|
+
trackEvent(['View resource tab', props.resource.id, option.label])
|
|
404
|
+
|
|
405
|
+
if (option.key === 'data') {
|
|
406
|
+
trackEvent(['Show preview', props.resource.id])
|
|
407
|
+
}
|
|
408
|
+
if (option.key === 'data-structure') {
|
|
409
|
+
trackEvent(['Show data structure', props.resource.id])
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const communityResource = computed<CommunityResource | null>(() => {
|
|
414
|
+
if (!props.isCommunityResource) return null
|
|
415
|
+
return props.resource as CommunityResource
|
|
416
|
+
})
|
|
417
|
+
const owner = computed(() => communityResource.value ? getOwnerName(communityResource.value) : null)
|
|
418
|
+
|
|
419
|
+
const lastUpdate = props.resource.last_modified
|
|
420
|
+
const availabilityChecked = props.resource.extras && 'check:available' in props.resource.extras
|
|
421
|
+
const unavailable = availabilityChecked && props.resource.extras['check:available'] === false
|
|
422
|
+
|
|
423
|
+
const resourceExternalUrl = computed(() => getResourceExternalUrl(props.dataset, props.resource))
|
|
424
|
+
|
|
425
|
+
const resourceContentId = 'resource-' + props.resource.id
|
|
426
|
+
const resourceHeaderId = 'resource-' + props.resource.id + '-header'
|
|
427
|
+
const resourceTitleId = getResourceTitleId(props.resource)
|
|
428
|
+
</script>
|
|
429
|
+
|
|
430
|
+
<style scoped>
|
|
431
|
+
.fr-link--no-after::after {
|
|
432
|
+
display: none !important;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
header:hover {
|
|
436
|
+
background-color: #f6f6f6;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
If we do not put z-index, the header is fully clickable except for the DSFR icons (bad because one of the icons is the chevron up/down). It may be due to the usage of ::before to add the icon in the markup or the `mask-image`. We need to put a `z-2` on all elements that we want to be clickable over the header.
|
|
441
|
+
*/
|
|
442
|
+
.z-1 {
|
|
443
|
+
z-index: 1;
|
|
444
|
+
}
|
|
445
|
+
.z-2 {
|
|
446
|
+
z-index: 2;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.z-3 {
|
|
450
|
+
z-index: 3;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
article {
|
|
454
|
+
container-type: inline-size;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
@container (max-width: 600px) {
|
|
458
|
+
article header.flex {
|
|
459
|
+
flex-direction: column;
|
|
460
|
+
align-items: start;
|
|
461
|
+
justify-content: start;
|
|
462
|
+
}
|
|
463
|
+
article header .buttons {
|
|
464
|
+
margin-top: 1.25rem;
|
|
465
|
+
margin-left: auto !important;
|
|
466
|
+
}
|
|
467
|
+
/*
|
|
468
|
+
If we want to put subheaders info in column on mobile…
|
|
469
|
+
article header .subheaders-infos {
|
|
470
|
+
display: flex;
|
|
471
|
+
flex-direction: column
|
|
472
|
+
}
|
|
473
|
+
article header .subheaders-infos .hidden.show-on-small {
|
|
474
|
+
display: inline !important;
|
|
475
|
+
}
|
|
476
|
+
article header .dash-after::after {
|
|
477
|
+
content: ''
|
|
478
|
+
} */
|
|
479
|
+
|
|
480
|
+
/* article .fr-pl-4v fr-pr-4v {
|
|
481
|
+
padding: 0.75rem !important;
|
|
482
|
+
} */
|
|
483
|
+
}
|
|
484
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component
|
|
3
|
+
:is="(resource.filetype === 'remote' && resource.format ? getResourceFormatIcon(resource.format) : null) || File"
|
|
4
|
+
class="text-gray-800 shrink-0"
|
|
5
|
+
/>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script setup lang="ts">
|
|
9
|
+
import type { ResourceFileType } from '../../types/resources'
|
|
10
|
+
import File from '../Icons/File.vue'
|
|
11
|
+
import { getResourceFormatIcon } from '../../functions/resources'
|
|
12
|
+
|
|
13
|
+
defineProps<{
|
|
14
|
+
resource: { format?: string | null, filetype: ResourceFileType | null }
|
|
15
|
+
}>()
|
|
16
|
+
</script>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
v-if="title"
|
|
4
|
+
class="inline-flex fr-mb-0 align-items-baseline fr-text--xs"
|
|
5
|
+
>
|
|
6
|
+
<Toggletip
|
|
7
|
+
position="right"
|
|
8
|
+
no-margin
|
|
9
|
+
class="relative z-2"
|
|
10
|
+
>
|
|
11
|
+
<template #toggletip="{ close }">
|
|
12
|
+
<div class="flex justify-between border-bottom">
|
|
13
|
+
<h5 class="fr-text--sm fr-my-0 fr-p-2v">{{ $t("Data schema") }}</h5>
|
|
14
|
+
<button
|
|
15
|
+
type="button"
|
|
16
|
+
:title="t('Close')"
|
|
17
|
+
class="border-left close-button flex items-center justify-center"
|
|
18
|
+
@click="close"
|
|
19
|
+
>×</button>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="fr-p-3v">
|
|
22
|
+
<div v-if="validataStatus === 'ok'">
|
|
23
|
+
{{ t("This file is valid for the shema:") }} <component
|
|
24
|
+
:is="documentationUrl ? 'a' : 'span'"
|
|
25
|
+
:href="documentationUrl"
|
|
26
|
+
class="fr-link fr-text--sm"
|
|
27
|
+
>{{ title }}</component>.
|
|
28
|
+
</div>
|
|
29
|
+
<div v-if="validataStatus === 'warnings'">
|
|
30
|
+
{{ t("This file is valid for the shema:") }} <component
|
|
31
|
+
:is="documentationUrl ? 'a' : 'span'"
|
|
32
|
+
:href="documentationUrl"
|
|
33
|
+
class="fr-link fr-text--sm"
|
|
34
|
+
>{{ title }}</component>. {{ t("But its compliance could be improved.") }}
|
|
35
|
+
</div>
|
|
36
|
+
<div v-if="validataStatus === 'ko'">
|
|
37
|
+
{{ t("This file indicates to follow the schema:") }} <component
|
|
38
|
+
:is="documentationUrl ? 'a' : 'span'"
|
|
39
|
+
:href="documentationUrl"
|
|
40
|
+
class="fr-link fr-text--sm"
|
|
41
|
+
>{{ title }}</component>. {{ t("But is not compliant.") }}
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div
|
|
45
|
+
v-if="validataWarnings.length"
|
|
46
|
+
class="text-default-warning flex items-center fr-mt-4v"
|
|
47
|
+
>
|
|
48
|
+
<span class="fr-icon-alert-line fr-icon--sm fr-mr-1v" />
|
|
49
|
+
<span>{{ validataWarnings.length }} {{ t('advices') }}</span>
|
|
50
|
+
</div>
|
|
51
|
+
<div
|
|
52
|
+
v-if="validataStructureErrors.length"
|
|
53
|
+
class="text-default-warning flex items-center fr-mt-4v"
|
|
54
|
+
>
|
|
55
|
+
<span class="fr-icon-alert-line fr-icon--sm fr-mr-1v" />
|
|
56
|
+
<span>{{ validataStructureErrors.length }} {{ t('structure errors') }}</span>
|
|
57
|
+
</div>
|
|
58
|
+
<div
|
|
59
|
+
v-if="validataBodyErrors.length"
|
|
60
|
+
class="text-default-warning flex items-center fr-mt-4v"
|
|
61
|
+
>
|
|
62
|
+
<span class="fr-icon-alert-line fr-icon--sm fr-mr-1v" />
|
|
63
|
+
<span>{{ validataBodyErrors.length }} {{ t('body errors') }}</span>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div
|
|
67
|
+
v-if="validationUrl"
|
|
68
|
+
class="w-100 text-align-right fr-mt-5v"
|
|
69
|
+
target="_blank"
|
|
70
|
+
>
|
|
71
|
+
<a :href="validationUrl">{{ t('See validation report') }}</a>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</template>
|
|
75
|
+
</Toggletip>
|
|
76
|
+
<span class="fr-mr-1v text-gray-medium">{{ t("Schema:") }}</span>
|
|
77
|
+
<span class="flex items-center bg-danger-lightest rounded-sm">
|
|
78
|
+
<span class="fr-tag fr-tag--sm">{{ title }}</span>
|
|
79
|
+
<span
|
|
80
|
+
v-if="validataStatus === 'warnings'"
|
|
81
|
+
class="flex items-center padding-sm"
|
|
82
|
+
>
|
|
83
|
+
<span class="fr-icon-alert-line fr-icon--sm fr-mr-1v" />
|
|
84
|
+
<span>{{ t("Invalid") }}</span>
|
|
85
|
+
</span>
|
|
86
|
+
<span
|
|
87
|
+
v-if="validataStatus === 'ko'"
|
|
88
|
+
class="flex items-center text-warning-dark padding-sm"
|
|
89
|
+
>
|
|
90
|
+
<span class="fr-icon-error-line fr-icon--sm fr-mr-1v" />
|
|
91
|
+
<span>{{ t("Invalid") }}</span>
|
|
92
|
+
</span>
|
|
93
|
+
</span>
|
|
94
|
+
</span>
|
|
95
|
+
</template>
|
|
96
|
+
|
|
97
|
+
<script setup lang="ts">
|
|
98
|
+
import { computed, onMounted, ref } from 'vue'
|
|
99
|
+
import { useI18n } from 'vue-i18n'
|
|
100
|
+
import type { Resource } from '../../types/resources'
|
|
101
|
+
import Toggletip from '../Toggletip.vue'
|
|
102
|
+
import type { RegisteredSchema, ValidataError } from '../../functions/schemas'
|
|
103
|
+
import { findSchemaInCatalog, getCatalog, getSchemaDocumentation, getSchemaValidationUrl } from '../../functions/schemas'
|
|
104
|
+
|
|
105
|
+
const props = defineProps<{
|
|
106
|
+
resource: Resource
|
|
107
|
+
}>()
|
|
108
|
+
|
|
109
|
+
const { t } = useI18n()
|
|
110
|
+
|
|
111
|
+
const catalog = ref<Array<RegisteredSchema> | null>(null)
|
|
112
|
+
onMounted(async () => {
|
|
113
|
+
catalog.value = await getCatalog()
|
|
114
|
+
})
|
|
115
|
+
const catalogSchema = computed(() => catalog.value ? findSchemaInCatalog(catalog.value, props.resource.schema) : null)
|
|
116
|
+
const validationUrl = computed(() => catalogSchema.value ? getSchemaValidationUrl(props.resource, catalogSchema.value) : null)
|
|
117
|
+
const documentationUrl = computed(() => catalogSchema.value ? getSchemaDocumentation(catalogSchema.value.name) : null)
|
|
118
|
+
|
|
119
|
+
const title = computed(() => {
|
|
120
|
+
if (!props.resource.schema) return null
|
|
121
|
+
return props.resource.schema.name || props.resource.schema.url
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const validataErrors = computed<Array<ValidataError>>(() => props.resource.extras['validation-report:errors'] || [])
|
|
125
|
+
const validataWarnings = computed(() => validataErrors.value.filter(error => [''].includes(error.code)))
|
|
126
|
+
const validataBodyErrors = computed(() => validataErrors.value.filter(error => ['#body', '#cell', '#content', '#row', '#table'].some(tag => error.tags.includes(tag))))
|
|
127
|
+
const validataStructureErrors = computed(() => validataErrors.value.filter(error => ['#head', '#structure', '#header'].some(tag => error.tags.includes(tag))))
|
|
128
|
+
|
|
129
|
+
const validataStatus = computed<'ok' | 'warnings' | 'ko'>(() => {
|
|
130
|
+
if (validataErrors.value.length === 0) return 'ok'
|
|
131
|
+
if (validataErrors.value.length === validataWarnings.value.length) return 'warnings'
|
|
132
|
+
return 'ko'
|
|
133
|
+
})
|
|
134
|
+
</script>
|
|
135
|
+
|
|
136
|
+
<style scoped>
|
|
137
|
+
.close-button {
|
|
138
|
+
width: 40px;
|
|
139
|
+
font-size: 1.2rem;
|
|
140
|
+
line-height: 0;
|
|
141
|
+
}
|
|
142
|
+
.rounded-sm {
|
|
143
|
+
border-radius: 0.75rem;
|
|
144
|
+
}
|
|
145
|
+
.padding-sm {
|
|
146
|
+
padding: .125rem .5rem .125rem .25rem;
|
|
147
|
+
}
|
|
148
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ContentLoader
|
|
3
|
+
:width="454"
|
|
4
|
+
:height="40"
|
|
5
|
+
:speed="2"
|
|
6
|
+
primary-color="#f3f3f3"
|
|
7
|
+
secondary-color="#ecebeb"
|
|
8
|
+
>
|
|
9
|
+
<rect
|
|
10
|
+
x="0"
|
|
11
|
+
y="0"
|
|
12
|
+
rx="20"
|
|
13
|
+
ry="20"
|
|
14
|
+
width="196"
|
|
15
|
+
height="40"
|
|
16
|
+
/>
|
|
17
|
+
<rect
|
|
18
|
+
x="212"
|
|
19
|
+
y="0"
|
|
20
|
+
rx="20"
|
|
21
|
+
ry="20"
|
|
22
|
+
width="242"
|
|
23
|
+
height="40"
|
|
24
|
+
/>
|
|
25
|
+
</ContentLoader>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<script setup lang="ts">
|
|
29
|
+
import { ContentLoader } from 'vue-content-loader'
|
|
30
|
+
</script>
|