@geo2france/api-dashboard 1.5.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 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
- Si des données sensibles alimentent un tableau de bord, l'authentification et la sécurité sont gérées au niveau du serveur de données. C'est donc
12
- lui qui va s'assurer que l'utilisateur a un droit d'accès aux données. L'application peut donc tout être utilisée pour présenter
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.)
@@ -30,163 +29,7 @@ Les composants sont actuellement utilisés pour le [tableau de bord de l'Odema](
30
29
 
31
30
  ## Installation
32
31
 
33
- `npm install https://github.com/geo2france/api-dashboard/releases/latest/download/api-dashboard.tgz`
34
-
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
- ![block-graph](block-graph.png)
155
-
156
- <!---
157
- ```mermaid
158
- graph TD;
32
+ `npm i @geo2france/api-dashboard`
159
33
 
160
- subgraph "&lt;DashboardApp&gt;"
161
- subgraph "&lt;DashboardPage&gt;"
162
- subgraph "&lt;DashboardElement&gt;"
163
- subgraph "Chart(Echart)"
164
- end
165
- end
166
- subgraph "&lt;DashboardElement&gt;"
167
- subgraph "Chart(Echart)"
168
- end
169
- end
170
- subgraph "&lt;DashboardElement&gt;"
171
- subgraph "Chart(Echart)"
172
- end
173
- end
174
- end
34
+ Consulter la [documentation](https://geo2france.github.io/api-dashboard/) du projet.
175
35
 
176
- subgraph "&lt;DashboardPage&gt;"
177
- subgraph "&lt;DashboardElement&gt;"
178
- subgraph "Chart(Echart)"
179
- end
180
- end
181
- subgraph "&lt;DashboardElement&gt;"
182
- subgraph "Chart(Echart)"
183
- end
184
- end
185
- subgraph "&lt;DashboardElement&gt;"
186
- subgraph "Chart(Echart)"
187
- end
188
- end
189
- end
190
- end
191
- ```
192
- --->
@@ -38,5 +38,6 @@ export declare const ControlContext: React.Context<ControlContextType | undefine
38
38
  interface IDSLDashboardPageProps {
39
39
  children: React.ReactNode;
40
40
  name?: string;
41
+ columns?: number;
41
42
  }
42
43
  export declare const DSL_DashboardPage: React.FC<IDSLDashboardPageProps>;
@@ -40,7 +40,7 @@ export default DashboardPage;
40
40
  export const DatasetContext = createContext({});
41
41
  export const DatasetRegistryContext = createContext(() => { }); // A modifier, utiliser un seul context
42
42
  export const ControlContext = createContext(undefined);
43
- export const DSL_DashboardPage = ({ name = 'Tableau de bord', children }) => {
43
+ export const DSL_DashboardPage = ({ name = 'Tableau de bord', columns = 2, children }) => {
44
44
  const [datasets, setdatasets] = useState({});
45
45
  const [palette, setPalette] = useState(DEFAULT_PALETTE);
46
46
  //const allDatasetLoaded = Object.values(datasets).every(d => !d.isFetching);
@@ -76,5 +76,5 @@ export const DSL_DashboardPage = ({ name = 'Tableau de bord', children }) => {
76
76
  backgroundColor: "#fff",
77
77
  height: "auto",
78
78
  width: "100%",
79
- }, children: control_components }), _jsx(Row, { gutter: [8, 8], style: { margin: 16 }, children: visible_components.map((component, idx) => _jsx(Col, { xl: 12, xs: 24, children: _jsx(DSL_ChartBlock, { children: component }) }, idx)) }), logic_components] }) }) })] }));
79
+ }, children: control_components }), _jsx(Row, { gutter: [8, 8], style: { margin: 16 }, children: visible_components.map((component, idx) => _jsx(Col, { xl: 24 / columns, xs: 24, children: _jsx(DSL_ChartBlock, { children: component }) }, idx)) }), logic_components] }) }) })] }));
80
80
  };
@@ -6,13 +6,13 @@ interface AppContextProps {
6
6
  logo?: string;
7
7
  }
8
8
  export declare const AppContext: import("react").Context<AppContextProps>;
9
- interface DashboardApprProps {
9
+ export interface DashboardConfig {
10
10
  title?: string;
11
11
  subtitle?: string;
12
- route_config: RouteConfig[];
12
+ routes: RouteConfig[];
13
13
  theme?: ThemeConfig;
14
14
  logo: string;
15
15
  brands?: Partner[];
16
16
  }
17
- declare const DashboardApp: React.FC<DashboardApprProps>;
17
+ declare const DashboardApp: React.FC<DashboardConfig>;
18
18
  export default DashboardApp;
@@ -31,7 +31,7 @@ const default_theme = {
31
31
  }
32
32
  };
33
33
  export const AppContext = createContext({});
34
- const DashboardApp = ({ route_config, theme, logo, brands, title, subtitle }) => {
34
+ const DashboardApp = ({ routes, theme, logo, brands, title, subtitle }) => {
35
35
  const context_values = { title, subtitle, logo };
36
36
  /* CONTROLS */
37
37
  const [controls, setControles] = useState({});
@@ -41,6 +41,6 @@ const DashboardApp = ({ route_config, theme, logo, brands, title, subtitle }) =>
41
41
  ...c
42
42
  }));
43
43
  };
44
- return (_jsx(QueryClientProvider, { client: queryClient, children: _jsx(ConfigProvider, { theme: theme || default_theme /* Merger plutôt ?*/, children: _jsx(HelmetProvider, { children: _jsx(AppContext.Provider, { value: context_values, children: _jsx(ControlContext.Provider, { value: { values: controls, pushValue: pushControl }, children: _jsx(HashRouter, { children: _jsx(Routes, { children: _jsxs(Route, { element: _jsxs(Layout, { children: [_jsxs(Layout, { children: [_jsx(DashboardSider, { route_config: route_config }), _jsx(Content, { style: { width: "85%" }, children: _jsx(Outlet, {}) })] }), _jsx(DasbhoardFooter, { brands: brands })] }), children: [generateRoutes(route_config), _jsx(Route, { path: "*", element: _jsx(ErrorComponent, {}) })] }) }) }) }) }) }) }) }));
44
+ return (_jsx(QueryClientProvider, { client: queryClient, children: _jsx(ConfigProvider, { theme: theme || default_theme /* Merger plutôt ?*/, children: _jsx(HelmetProvider, { children: _jsx(AppContext.Provider, { value: context_values, children: _jsx(ControlContext.Provider, { value: { values: controls, pushValue: pushControl }, children: _jsx(HashRouter, { children: _jsx(Routes, { children: _jsxs(Route, { element: _jsxs(Layout, { children: [_jsxs(Layout, { children: [_jsx(DashboardSider, { route_config: routes }), _jsx(Content, { style: { width: "85%" }, children: _jsx(Outlet, {}) })] }), _jsx(DasbhoardFooter, { brands: brands })] }), children: [generateRoutes(routes), _jsx(Route, { path: "*", element: _jsx(ErrorComponent, {}) })] }) }) }) }) }) }) }) }));
45
45
  };
46
46
  export default DashboardApp;
@@ -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 { cql_filter: queryFilters, bbox } = generateFilter(filters);
9
+ const { filter: queryFilters, bbox } = generateFilter(filters);
10
10
  const generatedSort = generateSort(sorters);
11
- const query = { service: 'WFS', request: 'GetFeature', sortby: '', version: '2.0.0', outputformat: 'application/json', typenames: resource,
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.count = pageSize;
15
+ query.maxfeatures = pageSize;
16
16
  }
17
17
  if (generatedSort) {
18
18
  query.sortby = generatedSort;
19
19
  }
20
20
  if (queryFilters) {
21
- query.cql_filter = queryFilters;
21
+ query.filter = queryFilters;
22
22
  }
23
23
  if (bbox !== '') {
24
24
  query.bbox = bbox;
25
25
  }
26
- const { data, headers: _headers } = await httpClient[requestMethod](`${url}?${queryString.stringify({ ...query, sortby: undefined })}&sortby=${query.sortby}&`, //"le + de sortby ne doit pas être urlencode"
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,4 +1,4 @@
1
1
  export declare const generateFilter: (filters?: any[]) => {
2
- cql_filter: string;
2
+ filter: string;
3
3
  bbox: string;
4
4
  };
@@ -1,45 +1,100 @@
1
- import { mapOperator } from "./mapOperator";
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
- if (filters) {
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.operator !== "or" && filter.operator !== "and" && "field" in filter) { // LogicalFilter
8
- const mappedOperator = mapOperator(filter.operator);
9
- if (filter.field === "geometry") {
10
- bbox = filter.value;
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 { cql_filter: array_filter.join(' and '), bbox: bbox };
99
+ return { filter: ogc_filter, bbox: bbox };
45
100
  };
@@ -1,4 +1,3 @@
1
- export { mapOperator } from "./mapOperator";
2
1
  export { generateSort } from "./generateSort";
3
2
  export { generateFilter } from "./generateFilter";
4
3
  export { axiosInstance } from "./axios";
@@ -1,4 +1,3 @@
1
- export { mapOperator } from "./mapOperator";
2
1
  export { generateSort } from "./generateSort";
3
2
  export { generateFilter } from "./generateFilter";
4
3
  export { axiosInstance } from "./axios";
package/dist/index.d.ts CHANGED
@@ -25,5 +25,6 @@ import { dataProvider as FileProvider } from "./data_providers/file";
25
25
  export { WfsProvider, DatafairProvider, FileProvider };
26
26
  export type { SimpleRecord, Partner, RouteConfig } from "./types";
27
27
  export type { LegendItem } from "./components/MapLegend/MapLegend";
28
+ export type { DashboardConfig } from "./components/Layout/DashboardApp";
28
29
  import * as DSL from './dsl';
29
30
  export { DSL };
@@ -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
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geo2france/api-dashboard",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
4
4
  "private": false,
5
5
  "description": "Build dashboards with JSX/TSX",
6
6
  "main": "dist/index.js",
@@ -92,7 +92,6 @@
92
92
  "react-dom": "^18.3.1",
93
93
  "react-icons": "5.4",
94
94
  "react-map-gl": "^7.1.7",
95
- "react-router-dom": "^6.25.1",
96
- "recharts": "^2.15.3"
95
+ "react-router-dom": "^6.25.1"
97
96
  }
98
97
  }