@geo2france/api-dashboard 1.6.0 → 1.6.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 +3 -160
- package/dist/data_providers/wfs/index.js +11 -6
- package/dist/data_providers/wfs/utils/generateFilter.d.ts +1 -1
- package/dist/data_providers/wfs/utils/generateFilter.js +92 -37
- package/dist/data_providers/wfs/utils/index.d.ts +0 -1
- package/dist/data_providers/wfs/utils/index.js +0 -1
- package/dist/utils/useApi.js +4 -0
- package/package.json +1 -1
package/README.MD
CHANGED
|
@@ -8,9 +8,8 @@ Il s'agit d'une application React (Javascript) s'executant dans le navigateur de
|
|
|
8
8
|
l'application récupère les données via API **auprès d'un partenaire** (plateforme régionale, portail open-data, etc.) ou sur **votre serveur de données**.
|
|
9
9
|
Les données sont ensuites traitées par le client et présentées à l'utilisateur via des graphiques ou cartes.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
des données sensibles ou même des graphiques dont les données diffèrent selon les droits de l'utilisateur.
|
|
11
|
+
Le tableau de bord est construit de manière déclarative **JSX** (accronyme de JavaScript XML).
|
|
12
|
+
Comme son nom l'indique, il permet de combiner la clarté et l'efficacité du XML, avec la souplesse et la puissance du JavaScript.
|
|
14
13
|
|
|
15
14
|
Les API suivantes sont actuellement supportées (interrogation, filtre, pagination, etc. ) :
|
|
16
15
|
- [WFS](src/data_providers/wfs/) : API proposée par la plupart des serveurs geographiques (QGIS Server, GeoServer, ArcGIS server, etc.)
|
|
@@ -32,161 +31,5 @@ Les composants sont actuellement utilisés pour le [tableau de bord de l'Odema](
|
|
|
32
31
|
|
|
33
32
|
`npm i @geo2france/api-dashboard`
|
|
34
33
|
|
|
34
|
+
Consulter la [documentation](https://geo2france.github.io/api-dashboard/) du projet.
|
|
35
35
|
|
|
36
|
-
## Utilisation
|
|
37
|
-
|
|
38
|
-
Le tableau de bord est construit de manière déclarative **JSX** (accronyme de JavaScript XML).
|
|
39
|
-
Comme son nom l'indique, il permet de combiner la clarté et l'efficacité du XML, avec la souplesse et la puissance du JavaScript.
|
|
40
|
-
|
|
41
|
-
La construction du tableau de bord repose sur l'articulation de 3 grands concepts :
|
|
42
|
-
|
|
43
|
-
- Les `<Dataset>` permettent de rappatrier, filter et traiter laes données depuis des sources distantes : [documentation](./src/components/Dataset/README.md)
|
|
44
|
-
- Les graphiques `<ChartXXX>` et `<MapXXX>` constituent la partie essentielle du tableau de bord
|
|
45
|
-
- Les `<Control>` pour que les utilisateurs puissent interragir avec le tableau de bord (filter, activer/désactiver une option, etc.) : [documentation](./src/components/Control/README.md)
|
|
46
|
-
|
|
47
|
-
Le JSX permet aussi d'ajouter des éléments dynamiques en Javascript.
|
|
48
|
-
|
|
49
|
-
```jsx
|
|
50
|
-
|
|
51
|
-
import { Transform, Dashboard, Dataset, Filter, Producer, Control, ChartPie, useControl } from "api-dashboard/dsl"
|
|
52
|
-
|
|
53
|
-
export const MaPremierePage = () => (
|
|
54
|
-
|
|
55
|
-
<Dashboard>
|
|
56
|
-
|
|
57
|
-
<Dataset
|
|
58
|
-
id="dma_collecte_traitement"
|
|
59
|
-
resource="sinoe-(r)-destination-des-dma-collectes-par-type-de-traitement/lines"
|
|
60
|
-
url="https://data.ademe.fr/data-fair/api/v1/datasets"
|
|
61
|
-
type="datafair"
|
|
62
|
-
pageSize={5000}>
|
|
63
|
-
<Filter field="L_REGION">Hauts-de-France</Filter>
|
|
64
|
-
{/* Un filtre statique appliqué à l'API qui fournie les données */}
|
|
65
|
-
|
|
66
|
-
<Filter field="ANNEE">{useControl('annee')}</Filter>
|
|
67
|
-
{/* Un second filtre : l'année choisie par l'utilisateur */}
|
|
68
|
-
|
|
69
|
-
<Transform>SELECT [ANNEE], [L_TYP_REG_DECHET], SUM([TONNAGE_DMA]) as [TONNAGE_DMA] FROM ? GROUP BY [ANNEE], [L_TYP_REG_DECHET]</Transform>
|
|
70
|
-
{/* Transformation local des données */}
|
|
71
|
-
|
|
72
|
-
<Producer url="https://odema-hautsdefrance.org/">Odema</Producer>
|
|
73
|
-
{/* Pour créditer les graphiques */}
|
|
74
|
-
</Dataset>
|
|
75
|
-
|
|
76
|
-
<Control>
|
|
77
|
-
<Select name="annee" options={[2021,2019,2017]} initial_value={2019} arrows={true} />
|
|
78
|
-
</Control>
|
|
79
|
-
{/* Un control permettant à mon utilisateur de choisir l'année */}
|
|
80
|
-
|
|
81
|
-
<ChartPie title={`Tonnages de déchets en ${useControl('annee')}`} dataset='dma_collecte_traitement' nameKey='L_TYP_REG_DECHET' dataKey='TONNAGE_DMA' />
|
|
82
|
-
{/* Un graphique camembert standard. J'indique mon jeu de données et les colonnes à utiliser */}
|
|
83
|
-
|
|
84
|
-
</Dashboard>
|
|
85
|
-
)
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## Bien débuter
|
|
89
|
-
|
|
90
|
-
1. Mise en place du projet template (_en cours_)
|
|
91
|
-
2. Créer une nouvelle page
|
|
92
|
-
3. Ajouter des [jeux de données](./src/components/Dataset/README.md)
|
|
93
|
-
4. Ajouter un [graphique](./src/components/Charts/README.md)
|
|
94
|
-
5. Personnaliser son tableau : ajouter une [Palette](./src/components/Palette/README.md), configurer le theme [theme AntDesign](https://ant.design/docs/react/customize-theme#customize-design-token) (_TODO_)
|
|
95
|
-
6. Ajouter de l'interactivité : les [contrôles utilisateur](./src/components/Control/README.md)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
## Aller plus loin (développeur)
|
|
99
|
-
|
|
100
|
-
### Développement de composants dataviz
|
|
101
|
-
|
|
102
|
-
La bibliothèque propose des graphiques de bases, mais il possible de développer ses propres composants React.
|
|
103
|
-
Des fonctions sont proposés afin d'aider le développeur dans cette tâche : accéder facilement aux données ou aux options utilisateurs, gestion des erreurs, etc.
|
|
104
|
-
|
|
105
|
-
Le développeur est libre d'utiliser bibliothèque dataviz ou carto de son choix, à partir du moment où le composant retourne un élément visuelle.
|
|
106
|
-
Nous préconisons [Echarts](https://echarts.apache.org), mais d'autres sont utilisables ([Recharts](https://recharts.org/), [Chart.js](https://www.chartjs.org/), etc.)
|
|
107
|
-
|
|
108
|
-
`TODO : guide développement composant`
|
|
109
|
-
|
|
110
|
-
### Développement de contrôles
|
|
111
|
-
|
|
112
|
-
Il est également possible de développer des contrôle utilisateurs (éléments de formulaires) personalisés.
|
|
113
|
-
|
|
114
|
-
`TODO : guide développement control. Le composant doit retourner un <Form.Item />`
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
## Documentation (ancienne)
|
|
120
|
-
|
|
121
|
-
⭐ Essentiel
|
|
122
|
-
👨💻 Utilisateur avancé
|
|
123
|
-
|
|
124
|
-
### Mise en page et structure
|
|
125
|
-
|
|
126
|
-
- [DashboardApp](/src/components/Layout/) ⭐
|
|
127
|
-
- [DashboardPage](/src/components/DashboardPage/) ⭐
|
|
128
|
-
- [DashboardElement](/src/components/DashboardElement/) ⭐
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
### Composants
|
|
132
|
-
|
|
133
|
-
- [KeyFigure](/src/components/KeyFigure/) ⭐
|
|
134
|
-
- [NextPrevSelect](/src/components/NextPrevSelect/) ⭐
|
|
135
|
-
- [MapLegend](/src/components/MapLegend/) ⭐
|
|
136
|
-
- [FlipCard](/src/components/FlipCard/)
|
|
137
|
-
- [LoadingContainer](/src/components/LoadingContainer/)
|
|
138
|
-
|
|
139
|
-
### Hooks et fonctions
|
|
140
|
-
|
|
141
|
-
- [useApi](/src/utils/README.MD) ⭐
|
|
142
|
-
- [useSearchParamsState](/src/utils/README.MD) ⭐
|
|
143
|
-
- [useChartEvents](/src/utils/README.MD) 👨💻
|
|
144
|
-
- [useChartActionHightlight](/src/utils/README.MD) 👨💻
|
|
145
|
-
- [useMapControl](/src/utils/README.MD) 👨💻
|
|
146
|
-
- [useChartExport](/src/utils/README.MD)
|
|
147
|
-
|
|
148
|
-
### Fournisseur de données
|
|
149
|
-
|
|
150
|
-
- [WFS](/src/data_providers/wfs/) ⭐
|
|
151
|
-
- [Datafair](/src/data_providers/datafair/) ⭐
|
|
152
|
-
- [Filte](/src/data_providers/file/) ⭐
|
|
153
|
-
|
|
154
|
-

|
|
155
|
-
|
|
156
|
-
<!---
|
|
157
|
-
```mermaid
|
|
158
|
-
graph TD;
|
|
159
|
-
|
|
160
|
-
subgraph "<DashboardApp>"
|
|
161
|
-
subgraph "<DashboardPage>"
|
|
162
|
-
subgraph "<DashboardElement>"
|
|
163
|
-
subgraph "Chart(Echart)"
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
subgraph "<DashboardElement>"
|
|
167
|
-
subgraph "Chart(Echart)"
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
subgraph "<DashboardElement>"
|
|
171
|
-
subgraph "Chart(Echart)"
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
subgraph "<DashboardPage>"
|
|
177
|
-
subgraph "<DashboardElement>"
|
|
178
|
-
subgraph "Chart(Echart)"
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
subgraph "<DashboardElement>"
|
|
182
|
-
subgraph "Chart(Echart)"
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
subgraph "<DashboardElement>"
|
|
186
|
-
subgraph "Chart(Echart)"
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
```
|
|
192
|
-
--->
|
|
@@ -2,28 +2,33 @@ import { axiosInstance, generateSort, generateFilter } from "./utils";
|
|
|
2
2
|
import queryString from "query-string";
|
|
3
3
|
export const dataProvider = (apiUrl, httpClient = axiosInstance) => ({
|
|
4
4
|
getList: async ({ resource, pagination, filters, sorters, meta }) => {
|
|
5
|
-
const url = `${apiUrl}
|
|
5
|
+
const url = `${apiUrl}`;
|
|
6
6
|
const { current = 1, pageSize = 10, mode = "off" } = pagination ?? {};
|
|
7
7
|
const { headers: headersFromMeta, method } = meta ?? {};
|
|
8
8
|
const requestMethod = method ?? "get";
|
|
9
|
-
const {
|
|
9
|
+
const { filter: queryFilters, bbox } = generateFilter(filters);
|
|
10
10
|
const generatedSort = generateSort(sorters);
|
|
11
|
-
const query = { service: 'WFS', request: 'GetFeature', sortby: '', version: '
|
|
11
|
+
const query = { service: 'WFS', request: 'GetFeature', sortby: '', version: '1.1.0', outputformat: 'application/json', typename: resource,
|
|
12
12
|
srsname: meta?.srsname, propertyname: meta?.properties?.join(',') };
|
|
13
13
|
if (mode === "server") {
|
|
14
14
|
query.startindex = (current - 1) * pageSize;
|
|
15
|
-
query.
|
|
15
|
+
query.maxfeatures = pageSize;
|
|
16
16
|
}
|
|
17
17
|
if (generatedSort) {
|
|
18
18
|
query.sortby = generatedSort;
|
|
19
19
|
}
|
|
20
20
|
if (queryFilters) {
|
|
21
|
-
query.
|
|
21
|
+
query.filter = queryFilters;
|
|
22
22
|
}
|
|
23
23
|
if (bbox !== '') {
|
|
24
24
|
query.bbox = bbox;
|
|
25
25
|
}
|
|
26
|
-
const
|
|
26
|
+
const urlObj = new URL(url);
|
|
27
|
+
const base_params = Object.fromEntries(urlObj.searchParams.entries()); // Params renseignés par l'utilisateur dans l'URL (notament MAP=XXX pour qgis server)
|
|
28
|
+
const base_url = urlObj.origin + urlObj.pathname;
|
|
29
|
+
const { data, headers: _headers } = await httpClient[requestMethod](`${base_url}?${queryString.stringify({ ...query, sortby: undefined })}
|
|
30
|
+
&sortby=${query.sortby}
|
|
31
|
+
&${Object.entries(base_params).map(([k, v]) => `${k}=${v}`).join('&')}`, //le + de sortby et les paramètres de bases ne doivent pas être urlencode
|
|
27
32
|
{
|
|
28
33
|
headers: headersFromMeta,
|
|
29
34
|
});
|
|
@@ -1,45 +1,100 @@
|
|
|
1
|
-
|
|
1
|
+
const map_ogc_filer = (input) => {
|
|
2
|
+
switch (input) {
|
|
3
|
+
case "eq":
|
|
4
|
+
case "ne":
|
|
5
|
+
return "PropertyIsEqualTo";
|
|
6
|
+
case "gt":
|
|
7
|
+
return "PropertyIsGreaterThan";
|
|
8
|
+
case "gte":
|
|
9
|
+
return "PropertyIsGreaterThanOrEqualTo";
|
|
10
|
+
case "lt":
|
|
11
|
+
return "PropertyIsLessThan";
|
|
12
|
+
case "lte":
|
|
13
|
+
return "PropertyIsLessThanOrEqualTo";
|
|
14
|
+
case "contains":
|
|
15
|
+
case "containss":
|
|
16
|
+
case "ncontains":
|
|
17
|
+
case "ncontainss":
|
|
18
|
+
case "startswith":
|
|
19
|
+
case "startswiths":
|
|
20
|
+
case "nstartswith":
|
|
21
|
+
case "nstartswiths":
|
|
22
|
+
case "endswith":
|
|
23
|
+
case "endswiths":
|
|
24
|
+
case "nendswith":
|
|
25
|
+
return "PropertyIsLike";
|
|
26
|
+
default:
|
|
27
|
+
throw new Error(`[wfs-data-provider]: Unssuported operator ${input}`);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
2
30
|
export const generateFilter = (filters) => {
|
|
3
|
-
const array_filter = [];
|
|
4
31
|
let bbox = '';
|
|
5
|
-
|
|
32
|
+
let ogc_filter = '';
|
|
33
|
+
if (filters && filters.length > 0) {
|
|
34
|
+
const doc = document.implementation.createDocument('', 'Filter', null);
|
|
35
|
+
//doc.documentElement.setAttribute('xmlns:fes',"http://www.opengis.net/fes/2.0") // Namespace pour WFS 2
|
|
36
|
+
const and = doc.createElement('And');
|
|
37
|
+
let filters_root;
|
|
38
|
+
if (filters.length > 1) {
|
|
39
|
+
doc.documentElement.appendChild(and);
|
|
40
|
+
filters_root = and;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
filters_root = doc.documentElement;
|
|
44
|
+
}
|
|
6
45
|
filters.map((filter) => {
|
|
7
|
-
if (filter.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
else {
|
|
13
|
-
const value = (() => {
|
|
14
|
-
switch (filter.operator) {
|
|
15
|
-
case "contains":
|
|
16
|
-
case "containss":
|
|
17
|
-
case "ncontains":
|
|
18
|
-
case "ncontainss":
|
|
19
|
-
return `'%${filter.value}%'`;
|
|
20
|
-
case "startswith":
|
|
21
|
-
case "startswiths":
|
|
22
|
-
case "nstartswith":
|
|
23
|
-
case "nstartswiths":
|
|
24
|
-
return `'${filter.value}%'`;
|
|
25
|
-
case "endswith":
|
|
26
|
-
case "endswiths":
|
|
27
|
-
case "nendswith":
|
|
28
|
-
case "nendswiths":
|
|
29
|
-
return `'%${filter.value}'`;
|
|
30
|
-
case "in":
|
|
31
|
-
return `(${filter.value.map((i) => `'${i}'`).join(',')})`;
|
|
32
|
-
default:
|
|
33
|
-
return `'${filter.value}'`;
|
|
34
|
-
}
|
|
35
|
-
})();
|
|
36
|
-
array_filter.push(`${filter.field} ${mappedOperator} ${value}`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
else { //Conditionnal filter
|
|
46
|
+
if (filter.field === "geometry") { // TODO utiliser les filtres OGC pour filtrage geom ?
|
|
47
|
+
bbox = filter.value;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (filter.operator === "or" && filter.operator === "and") {
|
|
40
51
|
throw new Error(`[wfs-data-provider]: Condtionnal filter 'OR' not implemented yet `);
|
|
41
52
|
}
|
|
53
|
+
const f = doc.createElement(map_ogc_filer(filter.operator));
|
|
54
|
+
let el = doc.createElement('PropertyName'); // ValueReference sur WFS 2 ?
|
|
55
|
+
el.textContent = filter.field;
|
|
56
|
+
f.appendChild(el);
|
|
57
|
+
el = doc.createElement('Literal');
|
|
58
|
+
if ((["contains", "containss", "ncontains", "ncontainss",
|
|
59
|
+
"startswith", "startswiths", "nstartswith", "nstartswiths",
|
|
60
|
+
"endswith", "endswiths", "nendswith", "nendswiths" // Opérateurs match
|
|
61
|
+
].includes(filter.operator))) {
|
|
62
|
+
f.setAttribute('wildCard', '%');
|
|
63
|
+
f.setAttribute('singleChar', '_');
|
|
64
|
+
f.setAttribute('escapeChar', '\\');
|
|
65
|
+
f.setAttribute('matchCase', 'false');
|
|
66
|
+
}
|
|
67
|
+
if ((["containss", "ncontainss",
|
|
68
|
+
"startswiths", "nstartswiths",
|
|
69
|
+
,
|
|
70
|
+
"endswiths", , "nendswiths" // Opérateurs case-sensitve
|
|
71
|
+
].includes(filter.operator))) {
|
|
72
|
+
f.setAttribute('matchCase', 'true');
|
|
73
|
+
}
|
|
74
|
+
if (["contains", "containss", "ncontains", "ncontainss"].includes(filter.operator)) {
|
|
75
|
+
el.textContent = `%${filter.value}%`;
|
|
76
|
+
}
|
|
77
|
+
else if (["startswith", "startswiths", "nstartswith", "nstartswiths"].includes(filter.operator)) {
|
|
78
|
+
el.textContent = `${filter.value}%`;
|
|
79
|
+
}
|
|
80
|
+
else if (["endswith", "endswiths", "nendswith", "nendswiths"].includes(filter.operator)) {
|
|
81
|
+
el.textContent = `%${filter.value}`;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
el.textContent = filter.value;
|
|
85
|
+
}
|
|
86
|
+
f.appendChild(el);
|
|
87
|
+
if (filter.operator.startsWith('n')) { // Opérateur NOT
|
|
88
|
+
const not = doc.createElement('Not');
|
|
89
|
+
not.appendChild(f);
|
|
90
|
+
filters_root.appendChild(not);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
filters_root.appendChild(f);
|
|
94
|
+
}
|
|
42
95
|
});
|
|
96
|
+
ogc_filter = new XMLSerializer().serializeToString(doc);
|
|
97
|
+
//console.log('xml', ogc_filter)
|
|
43
98
|
}
|
|
44
|
-
return {
|
|
99
|
+
return { filter: ogc_filter, bbox: bbox };
|
|
45
100
|
};
|
package/dist/utils/useApi.js
CHANGED
|
@@ -11,4 +11,8 @@ export const useApi = ({ dataProvider, resource, filters, pagination, sorters, m
|
|
|
11
11
|
enabled: enabled,
|
|
12
12
|
placeholderData: (prev) => prev,
|
|
13
13
|
staleTime: 5 * 60 * 1e3, //Default staletime 5min
|
|
14
|
+
throwOnError: (err) => {
|
|
15
|
+
console.error(err);
|
|
16
|
+
return false;
|
|
17
|
+
},
|
|
14
18
|
}));
|