@geo2france/api-dashboard 1.13.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Dataset/Dataset.js +1 -1
- package/dist/components/Layout/DashboardApp.d.ts +22 -0
- package/dist/components/Layout/DashboardApp.js +2 -2
- package/dist/components/Layout/Footer.d.ts +3 -0
- package/dist/components/Layout/Footer.js +49 -7
- package/dist/components/Map/Map.d.ts +5 -0
- package/dist/components/Map/Map.js +27 -10
- package/package.json +3 -1
|
@@ -47,7 +47,7 @@ export const DSL_Dataset = ({ children, id, provider: provider_input, type: prov
|
|
|
47
47
|
: [props.joinKey, props.joinKey];
|
|
48
48
|
const leftTable = data.length >= 1 ? data : [{ [leftKey]: null }];
|
|
49
49
|
const rightTable = otherData.length >= 1 ? otherData : [{ [rightKey]: null }];
|
|
50
|
-
return from(leftTable).join(from(rightTable), props.joinKey, undefined, aq_join_option).objects();
|
|
50
|
+
return from(leftTable).join(from(rightTable), props.joinKey, undefined, { suffix: ['', '_2'], ...aq_join_option }).objects();
|
|
51
51
|
};
|
|
52
52
|
return funct;
|
|
53
53
|
}
|
|
@@ -7,12 +7,34 @@ interface AppContextProps {
|
|
|
7
7
|
}
|
|
8
8
|
export declare const AppContext: import("react").Context<AppContextProps>;
|
|
9
9
|
export interface DashboardConfig {
|
|
10
|
+
/**
|
|
11
|
+
* Titre principal du tableau de bord (affiché dans le header ou le titre de page).
|
|
12
|
+
*/
|
|
10
13
|
title?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Sous-titre du tableau de bord (optionnel, peut être affiché sous le titre principal).
|
|
16
|
+
*/
|
|
11
17
|
subtitle?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Liste des routes de l'application (chaque route correspond à une page du tableau de bord).
|
|
20
|
+
*/
|
|
12
21
|
routes: RouteConfig[];
|
|
22
|
+
/**
|
|
23
|
+
* Configuration du thème Ant Design (permet de personnaliser les couleurs, la typographie, etc.).
|
|
24
|
+
*/
|
|
13
25
|
theme?: ThemeConfig;
|
|
26
|
+
/**
|
|
27
|
+
* URL ou chemin du logo à afficher dans le tableau de bord.
|
|
28
|
+
*/
|
|
14
29
|
logo: string;
|
|
30
|
+
/**
|
|
31
|
+
* Liste optionnelle de partenaires ou marques à afficher dans le footer ou ailleurs.
|
|
32
|
+
*/
|
|
15
33
|
brands?: Partner[];
|
|
34
|
+
/**
|
|
35
|
+
* Active ou désactive le mode “slider” dans le pied de page (faire défiler les logos de partenaires).
|
|
36
|
+
*/
|
|
37
|
+
footerSlider?: boolean;
|
|
16
38
|
}
|
|
17
39
|
declare const DashboardApp: React.FC<DashboardConfig>;
|
|
18
40
|
export default DashboardApp;
|
|
@@ -31,7 +31,7 @@ const default_theme = {
|
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
33
|
export const AppContext = createContext({});
|
|
34
|
-
const DashboardApp = ({ routes, theme, logo, brands, title, subtitle }) => {
|
|
34
|
+
const DashboardApp = ({ routes, theme, logo, brands, footerSlider, 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 = ({ routes, 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, { hasSider: true, style: { minHeight: '100vh' }, children: [_jsx(DashboardSider, { route_config: routes }), _jsxs(Layout, { children: [_jsx(Content, { style: { width: "100%" }, children: _jsx(Outlet, {}) }), _jsx(DasbhoardFooter, { brands: brands })] })] }), children: [generateRoutes(routes), _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, { hasSider: true, style: { minHeight: '100vh' }, children: [_jsx(DashboardSider, { route_config: routes }), _jsxs(Layout, { children: [_jsx(Content, { style: { width: "100%" }, children: _jsx(Outlet, {}) }), _jsx(DasbhoardFooter, { brands: brands, slider: footerSlider })] })] }), children: [generateRoutes(routes), _jsx(Route, { path: "*", element: _jsx(ErrorComponent, {}) })] }) }) }) }) }) }) }) }));
|
|
45
45
|
};
|
|
46
46
|
export default DashboardApp;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Partner } from "../../types";
|
|
2
|
+
import "slick-carousel/slick/slick.css";
|
|
3
|
+
import "slick-carousel/slick/slick-theme.css";
|
|
2
4
|
interface DbFooterProps {
|
|
3
5
|
brands?: Partner[];
|
|
6
|
+
slider?: boolean;
|
|
4
7
|
}
|
|
5
8
|
export declare const DasbhoardFooter: React.FC<DbFooterProps>;
|
|
6
9
|
export {};
|
|
@@ -1,19 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Button, Layout, Typography } from "antd";
|
|
3
3
|
import { useContext, useState } from "react";
|
|
4
|
+
import Slider from "@ant-design/react-slick";
|
|
4
5
|
import { UpOutlined, DownOutlined } from "@ant-design/icons";
|
|
5
6
|
import { AppContext } from "./DashboardApp";
|
|
7
|
+
import "slick-carousel/slick/slick.css";
|
|
8
|
+
import "slick-carousel/slick/slick-theme.css";
|
|
6
9
|
const { Text } = Typography;
|
|
7
|
-
export const DasbhoardFooter = ({ brands }) => {
|
|
10
|
+
export const DasbhoardFooter = ({ brands, slider = true }) => {
|
|
8
11
|
const [isCollapsed, setIsCollapsed] = useState(window.innerWidth < 768 ? true : false);
|
|
9
12
|
const toggleCollapse = () => {
|
|
10
13
|
setIsCollapsed(!isCollapsed);
|
|
11
14
|
};
|
|
12
15
|
const app_context = useContext(AppContext);
|
|
13
|
-
|
|
16
|
+
// Style avec slider (l'image occupe tout le bloc défilé)
|
|
17
|
+
const style_img = slider ? {
|
|
14
18
|
maxHeight: "60px",
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
maxWidth: "100%",
|
|
20
|
+
margin: "auto"
|
|
21
|
+
}
|
|
22
|
+
// Style sans slider
|
|
23
|
+
: {
|
|
24
|
+
maxHeight: "60px",
|
|
25
|
+
marginRight: "20px",
|
|
26
|
+
};
|
|
27
|
+
const nbBrands = brands?.length || 0;
|
|
28
|
+
// Contenu du footer = logos des partenaires
|
|
29
|
+
// TODO : doit pouvoir être surchargé par l'utilisateur
|
|
30
|
+
const footerContent = brands?.map((p) => (_jsx("a", { href: p.url, children: _jsx("img", { style: style_img, src: p.logo, alt: p.name }) }, p.name)));
|
|
17
31
|
return (_jsxs(Layout.Footer, { style: {
|
|
18
32
|
textAlign: "center",
|
|
19
33
|
color: "#fff",
|
|
@@ -23,12 +37,40 @@ export const DasbhoardFooter = ({ brands }) => {
|
|
|
23
37
|
right: "0",
|
|
24
38
|
width: "100%",
|
|
25
39
|
padding: 2,
|
|
26
|
-
height:
|
|
40
|
+
height: "auto",
|
|
41
|
+
minHeight: "40px",
|
|
27
42
|
transition: "height 0.5s ease-in-out",
|
|
28
43
|
overflow: "hidden",
|
|
29
44
|
borderTop: "1px solid #ccc",
|
|
30
45
|
zIndex: 600, // maplibre top zIndex if 500
|
|
31
|
-
}, children: [isCollapsed && (_jsxs(Text, { type: "secondary", children: [app_context?.title, " - ", app_context?.subtitle] })), _jsx("div", { style: { display: isCollapsed ? "none" : "block", padding: "10px 0" }, children:
|
|
46
|
+
}, children: [isCollapsed && (_jsxs(Text, { type: "secondary", children: [app_context?.title, " - ", app_context?.subtitle] })), _jsx("div", { style: { display: isCollapsed ? "none" : "block", padding: "10px 0" }, children: slider
|
|
47
|
+
// Logos avec défilement (choix par défaut)
|
|
48
|
+
? _jsx(Slider
|
|
49
|
+
// Défilement auto si plus de logos que la lagreur de l'écran ne peut en afficher
|
|
50
|
+
, {
|
|
51
|
+
// Défilement auto si plus de logos que la lagreur de l'écran ne peut en afficher
|
|
52
|
+
autoplay: nbBrands > 4, slidesToShow: Math.min(nbBrands, 4), responsive: [
|
|
53
|
+
{
|
|
54
|
+
breakpoint: 1024,
|
|
55
|
+
settings: {
|
|
56
|
+
autoplay: nbBrands > 3,
|
|
57
|
+
slidesToShow: Math.min(nbBrands, 3)
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
breakpoint: 600,
|
|
62
|
+
settings: {
|
|
63
|
+
autoplay: nbBrands > 2,
|
|
64
|
+
slidesToShow: Math.min(nbBrands, 2)
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
breakpoint: 480,
|
|
69
|
+
settings: { slidesToShow: 1 }
|
|
70
|
+
}
|
|
71
|
+
], slidesToScroll: 1, infinite: true, arrows: false, autoplaySpeed: 3000, speed: 1000, children: footerContent })
|
|
72
|
+
// Défilement désactivé
|
|
73
|
+
: footerContent }), _jsx(Button, { style: {
|
|
32
74
|
position: "absolute",
|
|
33
75
|
bottom: "5px",
|
|
34
76
|
right: "10px",
|
|
@@ -2,6 +2,7 @@ import type { AnyLayer } from 'react-map-gl/maplibre';
|
|
|
2
2
|
import { AnyPaint } from 'mapbox-gl';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
5
|
+
import { SimpleRecord } from '../../types';
|
|
5
6
|
type LayerType = AnyLayer["type"];
|
|
6
7
|
export declare const map_locale: {
|
|
7
8
|
'CooperativeGesturesHandler.WindowsHelpText': string;
|
|
@@ -15,6 +16,8 @@ export declare const map_locale: {
|
|
|
15
16
|
interface MapProps extends MapLayerProps {
|
|
16
17
|
/** Afficher une popup après un click sur la carte */
|
|
17
18
|
popup?: boolean;
|
|
19
|
+
/** Fonction callback permettant de définir le contenu de la popup */
|
|
20
|
+
popupFormatter?: ((param: SimpleRecord) => React.ReactNode);
|
|
18
21
|
/** Titre du graphique */
|
|
19
22
|
title?: string;
|
|
20
23
|
}
|
|
@@ -34,6 +37,8 @@ interface MapLayerProps {
|
|
|
34
37
|
xKey?: string;
|
|
35
38
|
/** Colonne contenant la coordonnées y / latitude */
|
|
36
39
|
yKey?: string;
|
|
40
|
+
/** Colonne contenant la geométrie au format GeoJSON(4326). Par défaut détection automatique ("geom" ou "geometry") */
|
|
41
|
+
geomKey?: string;
|
|
37
42
|
}
|
|
38
43
|
/**
|
|
39
44
|
* Composant à utiliser comme enfant d'une <Map>
|
|
@@ -5,7 +5,7 @@ import { useEffect, useRef, useState } from "react";
|
|
|
5
5
|
import { useDataset } from '../Dataset/hooks';
|
|
6
6
|
import bbox from '@turf/bbox';
|
|
7
7
|
import { getType } from '@turf/invariant';
|
|
8
|
-
import { featureCollection, point } from '@turf/helpers';
|
|
8
|
+
import { feature, featureCollection, point } from '@turf/helpers';
|
|
9
9
|
import { usePalette } from '../Palette/Palette';
|
|
10
10
|
import { from, op } from 'arquero';
|
|
11
11
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
@@ -19,20 +19,34 @@ export const map_locale = {
|
|
|
19
19
|
};
|
|
20
20
|
/** Construire un geojson a partir d'un tableau de données*/
|
|
21
21
|
const build_geojson = (params) => {
|
|
22
|
-
const { data, xKey, yKey } = params;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
const { data, xKey, yKey, geomKey } = params;
|
|
23
|
+
let features_collection;
|
|
24
|
+
if (xKey && yKey) { // Construction à partir des champs x et Y
|
|
25
|
+
features_collection = featureCollection(data.map((e) => {
|
|
26
|
+
const [x, y] = [parseNumber(e[xKey]), parseNumber(e[yKey])];
|
|
27
|
+
return point([x, y], { ...e });
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
else if (geomKey) { // Construction à partir de la GeomGeoJSON
|
|
31
|
+
features_collection = featureCollection(data.map((e) => {
|
|
32
|
+
return feature(e[geomKey], { ...e });
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
features_collection = undefined;
|
|
37
|
+
}
|
|
38
|
+
console.log(geomKey, features_collection);
|
|
27
39
|
return features_collection;
|
|
28
40
|
};
|
|
29
|
-
export const Map = ({ dataset, color, type, paint, categoryKey, popup = false, title, xKey, yKey }) => {
|
|
41
|
+
export const Map = ({ dataset, color, type, paint, categoryKey, popup = false, popupFormatter: popupFormatterUser, title, xKey, yKey }) => {
|
|
30
42
|
const mapRef = useRef(null);
|
|
31
43
|
const [clickedFeature, setClickedFeature] = useState(undefined);
|
|
32
44
|
useBlockConfig({ title: title });
|
|
33
45
|
const onClickMap = (evt) => {
|
|
34
46
|
setClickedFeature({ ...evt.features[0], ...{ lngLat: evt.lngLat } });
|
|
35
47
|
};
|
|
48
|
+
const current_row = clickedFeature?.properties;
|
|
49
|
+
const popupFormatter = popupFormatterUser || ((row) => categoryKey ? row?.[categoryKey] : undefined);
|
|
36
50
|
const onMouseMoveMap = (evt) => {
|
|
37
51
|
if (!mapRef.current) {
|
|
38
52
|
return;
|
|
@@ -45,7 +59,7 @@ export const Map = ({ dataset, color, type, paint, categoryKey, popup = false, t
|
|
|
45
59
|
}
|
|
46
60
|
};
|
|
47
61
|
return (_jsxs(Maplibre, { cooperativeGestures: true, locale: map_locale, ref: mapRef, interactiveLayerIds: [dataset], onClick: onClickMap, onMouseMove: onMouseMoveMap, style: { width: '100%', height: '500px' }, children: [_jsx(BaseLayer, { layer: "osm" }), _jsx(MapLayer, { dataset: dataset, color: color, type: type, paint: paint, categoryKey: categoryKey, xKey: xKey, yKey: yKey }), clickedFeature?.properties && categoryKey && popup &&
|
|
48
|
-
|
|
62
|
+
_jsx(Popup, { longitude: clickedFeature.lngLat.lng, latitude: clickedFeature.lngLat.lat, onClose: () => { setClickedFeature(null); }, children: _jsx("div", { children: popupFormatter(current_row) || clickedFeature?.properties[categoryKey] }) })] }));
|
|
49
63
|
};
|
|
50
64
|
/**
|
|
51
65
|
* Composant à utiliser comme enfant d'une <Map>
|
|
@@ -54,14 +68,17 @@ export const Map = ({ dataset, color, type, paint, categoryKey, popup = false, t
|
|
|
54
68
|
* @param { MapLayerProps } props
|
|
55
69
|
* @returns { ReactElement }
|
|
56
70
|
*/
|
|
57
|
-
export const MapLayer = ({ dataset, categoryKey, color = 'red', type = 'circle', paint, xKey, yKey }) => {
|
|
71
|
+
export const MapLayer = ({ dataset, categoryKey, color = 'red', type = 'circle', paint, xKey, yKey, geomKey: geomKey_input }) => {
|
|
58
72
|
const { current: map } = useMap();
|
|
59
73
|
const data = useDataset(dataset);
|
|
60
74
|
// src (lib proj4 pour convertir)
|
|
75
|
+
const keys = data?.data?.[0] ? Object.keys(data?.data?.[0]) : undefined;
|
|
76
|
+
const geomKey = [geomKey_input, "geom", "geometry"].find(c => c && keys?.includes(c));
|
|
77
|
+
console.log(data?.id, data?.data, geomKey);
|
|
61
78
|
// Si x et y sont definie, on construit le geojson
|
|
62
79
|
const geojson = xKey && yKey && data?.data ?
|
|
63
80
|
build_geojson({ data: data.data, xKey: xKey, yKey: yKey })
|
|
64
|
-
: data?.
|
|
81
|
+
: data?.data && build_geojson({ data: data?.data, geomKey: geomKey });
|
|
65
82
|
const geom_type = geojson?.features?.[0] && getType(geojson?.features?.[0]);
|
|
66
83
|
/** Type de données dans categoryKey (string ou number) */
|
|
67
84
|
const type_value = categoryKey && typeof (data?.data?.[0]?.[categoryKey]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geo2france/api-dashboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Build dashboards with JSX/TSX",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"test": "jest --watchAll"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
+
"@ant-design/react-slick": "^1.1.2",
|
|
48
49
|
"@iconify/react": "^6.0.1",
|
|
49
50
|
"@turf/bbox": "^7.2.0",
|
|
50
51
|
"@turf/helpers": "^7.2.0",
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
"react-error-boundary": "^6.0.0",
|
|
60
61
|
"react-helmet": "^6.1.0",
|
|
61
62
|
"react-helmet-async": "^2.0.5",
|
|
63
|
+
"slick-carousel": "^1.8.1",
|
|
62
64
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz"
|
|
63
65
|
},
|
|
64
66
|
"devDependencies": {
|