@hisptz/dhis2-analytics 1.0.49 → 1.0.51

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.
Files changed (125) hide show
  1. package/.gitignore +5 -0
  2. package/build/cjs/components/ChartAnalytics/ChartAnalytics.test.js +1 -1
  3. package/build/cjs/components/ChartAnalytics/models/bar.js +24 -0
  4. package/build/cjs/components/ChartAnalytics/utils/chart.js +5 -0
  5. package/build/es/components/ChartAnalytics/ChartAnalytics.test.js +1 -1
  6. package/build/es/components/ChartAnalytics/models/bar.js +16 -0
  7. package/build/es/components/ChartAnalytics/utils/chart.js +5 -0
  8. package/build/types/components/ChartAnalytics/models/bar.d.ts +8 -0
  9. package/build/types/components/ChartAnalytics/types/props.d.ts +1 -1
  10. package/d2.config.js +8 -0
  11. package/i18n/en.pot +439 -0
  12. package/package.json +5 -5
  13. package/src/components/ChartAnalytics/ChartAnalytics.test.tsx +51 -0
  14. package/src/components/ChartAnalytics/components/DownloadMenu/components/Menu.tsx +48 -0
  15. package/src/components/ChartAnalytics/components/DownloadMenu/constants/menu.ts +38 -0
  16. package/src/components/ChartAnalytics/components/DownloadMenu/index.tsx +65 -0
  17. package/src/components/ChartAnalytics/components/DownloadMenu/interfaces/menu.ts +1 -0
  18. package/src/components/ChartAnalytics/hooks/useChart.ts +35 -0
  19. package/src/components/ChartAnalytics/index.tsx +28 -0
  20. package/src/components/ChartAnalytics/models/bar.ts +20 -0
  21. package/src/components/ChartAnalytics/models/column.ts +52 -0
  22. package/src/components/ChartAnalytics/models/index.ts +111 -0
  23. package/src/components/ChartAnalytics/models/line.ts +31 -0
  24. package/src/components/ChartAnalytics/models/multi-series.ts +115 -0
  25. package/src/components/ChartAnalytics/models/pie.ts +54 -0
  26. package/src/components/ChartAnalytics/services/export.ts +38 -0
  27. package/src/components/ChartAnalytics/styles/custom-highchart.css +48 -0
  28. package/src/components/ChartAnalytics/types/props.tsx +48 -0
  29. package/src/components/ChartAnalytics/utils/chart.ts +128 -0
  30. package/src/components/CircularProgressDashboard/CircularProgressIndicator.test.tsx +9 -0
  31. package/src/components/CircularProgressDashboard/index.tsx +36 -0
  32. package/src/components/CircularProgressDashboard/types/props.tsx +17 -0
  33. package/src/components/CustomPivotTable/components/Table/index.tsx +23 -0
  34. package/src/components/CustomPivotTable/components/TableBody/TableBody.module.css +12 -0
  35. package/src/components/CustomPivotTable/components/TableBody/index.tsx +96 -0
  36. package/src/components/CustomPivotTable/components/TableHeaders/TableHeaders.module.css +10 -0
  37. package/src/components/CustomPivotTable/components/TableHeaders/index.tsx +94 -0
  38. package/src/components/CustomPivotTable/index.tsx +63 -0
  39. package/src/components/CustomPivotTable/interfaces/index.ts +1 -0
  40. package/src/components/CustomPivotTable/services/engine.ts +102 -0
  41. package/src/components/CustomPivotTable/state/engine.tsx +22 -0
  42. package/src/components/Map/components/EarthEngineLayerConfiguration/EarthEngineLayerConfigModal.stories.tsx +28 -0
  43. package/src/components/Map/components/EarthEngineLayerConfiguration/EarthEngineLayerConfiguration.stories.tsx +34 -0
  44. package/src/components/Map/components/EarthEngineLayerConfiguration/index.tsx +412 -0
  45. package/src/components/Map/components/MapArea/index.tsx +83 -0
  46. package/src/components/Map/components/MapArea/interfaces/index.ts +39 -0
  47. package/src/components/Map/components/MapControls/components/CustomControl/index.tsx +24 -0
  48. package/src/components/Map/components/MapControls/components/DownloadControl/index.tsx +11 -0
  49. package/src/components/Map/components/MapControls/components/FullscreenControl/index.tsx +7 -0
  50. package/src/components/Map/components/MapControls/index.tsx +24 -0
  51. package/src/components/Map/components/MapLayer/components/BoundaryLayer/hooks/useBoundaryData.ts +7 -0
  52. package/src/components/Map/components/MapLayer/components/BoundaryLayer/index.tsx +55 -0
  53. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/components/EarthEngineLegend.tsx +74 -0
  54. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/constants/index.ts +430 -0
  55. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/hooks/index.ts +34 -0
  56. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/index.tsx +185 -0
  57. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/interfaces/index.ts +56 -0
  58. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/services/api.js +34241 -0
  59. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/services/engine.ts +431 -0
  60. package/src/components/Map/components/MapLayer/components/GoogleEngineLayer/utils/index.ts +105 -0
  61. package/src/components/Map/components/MapLayer/components/LegendArea/LegendArea.module.css +12 -0
  62. package/src/components/Map/components/MapLayer/components/LegendArea/components/LegendCardHeader/index.tsx +17 -0
  63. package/src/components/Map/components/MapLayer/components/LegendArea/index.tsx +167 -0
  64. package/src/components/Map/components/MapLayer/components/PointLayer/components/PointLegend/index.tsx +44 -0
  65. package/src/components/Map/components/MapLayer/components/PointLayer/hooks/index.ts +8 -0
  66. package/src/components/Map/components/MapLayer/components/PointLayer/index.tsx +36 -0
  67. package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Bubble/components/BubbleLegend/components/Bubble.tsx +48 -0
  68. package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Bubble/components/BubbleLegend/components/Bubbles.tsx +150 -0
  69. package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Bubble/components/BubbleLegend/index.tsx +39 -0
  70. package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Bubble/index.tsx +57 -0
  71. package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Choropleth/components/ChoroplethLegend.tsx +43 -0
  72. package/src/components/Map/components/MapLayer/components/ThematicLayer/components/Choropleth/index.tsx +38 -0
  73. package/src/components/Map/components/MapLayer/components/ThematicLayer/components/CustomTooltip/index.tsx +26 -0
  74. package/src/components/Map/components/MapLayer/components/ThematicLayer/hooks/config.ts +10 -0
  75. package/src/components/Map/components/MapLayer/components/ThematicLayer/index.tsx +46 -0
  76. package/src/components/Map/components/MapLayer/components/ThematicLayer/styles/legends.css +62 -0
  77. package/src/components/Map/components/MapLayer/index.tsx +32 -0
  78. package/src/components/Map/components/MapLayer/interfaces/index.ts +139 -0
  79. package/src/components/Map/components/MapProvider/components/MapLayerProvider/hooks/index.tsx +368 -0
  80. package/src/components/Map/components/MapProvider/components/MapLayerProvider/index.tsx +105 -0
  81. package/src/components/Map/components/MapProvider/hooks/index.ts +14 -0
  82. package/src/components/Map/components/MapProvider/index.tsx +93 -0
  83. package/src/components/Map/components/MapUpdater/index.tsx +8 -0
  84. package/src/components/Map/components/ThematicLayerConfiguration/ThematicLayerConfigModal.stories.tsx +28 -0
  85. package/src/components/Map/components/ThematicLayerConfiguration/ThematicLayerConfiguration.stories.tsx +34 -0
  86. package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/components/ColorScale/index.tsx +24 -0
  87. package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/constants/colors.ts +433 -0
  88. package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/index.tsx +50 -0
  89. package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/styles/ColorScale.module.css +15 -0
  90. package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/styles/ColorScaleSelect.module.css +12 -0
  91. package/src/components/Map/components/ThematicLayerConfiguration/components/ColorScaleSelect/utils/colors.ts +91 -0
  92. package/src/components/Map/components/ThematicLayerConfiguration/components/CustomLegend/index.tsx +45 -0
  93. package/src/components/Map/components/ThematicLayerConfiguration/components/IndicatorSelectorModal/index.tsx +47 -0
  94. package/src/components/Map/components/ThematicLayerConfiguration/components/LegendSetSelector/index.tsx +57 -0
  95. package/src/components/Map/components/ThematicLayerConfiguration/index.tsx +248 -0
  96. package/src/components/Map/constants/colors.ts +434 -0
  97. package/src/components/Map/constants/legendSet.ts +19 -0
  98. package/src/components/Map/hooks/map.ts +47 -0
  99. package/src/components/Map/index.tsx +65 -0
  100. package/src/components/Map/interfaces/index.ts +57 -0
  101. package/src/components/Map/state/index.tsx +31 -0
  102. package/src/components/Map/utils/colors.ts +95 -0
  103. package/src/components/Map/utils/helpers.ts +15 -0
  104. package/src/components/Map/utils/map.ts +150 -0
  105. package/src/components/SingleValueContainer/SingleValueContainer.test.tsx +24 -0
  106. package/src/components/SingleValueContainer/components/SingleValueItem/SingleValueItem.tsx +46 -0
  107. package/src/components/SingleValueContainer/components/SingleValueItem/SingleValuePercentage.tsx +12 -0
  108. package/src/components/SingleValueContainer/index.tsx +37 -0
  109. package/src/components/SingleValueContainer/styles/SingleValueContainer.module.css +39 -0
  110. package/src/components/SingleValueContainer/types/props.tsx +16 -0
  111. package/src/components/Visualization/components/AnalyticsDataProvider/index.tsx +76 -0
  112. package/src/components/Visualization/components/DimensionsProvider/index.tsx +51 -0
  113. package/src/components/Visualization/components/LayoutProvider/index.tsx +34 -0
  114. package/src/components/Visualization/components/VisualizationDimensionSelector/index.tsx +59 -0
  115. package/src/components/Visualization/components/VisualizationProvider/index.tsx +31 -0
  116. package/src/components/Visualization/components/VisualizationSelector/index.tsx +157 -0
  117. package/src/components/Visualization/components/VisualizationTypeProvider/index.tsx +40 -0
  118. package/src/components/Visualization/components/VisualizationTypeSelector/index.tsx +46 -0
  119. package/src/components/Visualization/index.tsx +103 -0
  120. package/src/index.ts +6 -0
  121. package/src/locales/en/translations.json +138 -0
  122. package/src/locales/index.js +16 -0
  123. package/tsconfig.build.json +46 -0
  124. package/tsconfig.json +51 -0
  125. package/LICENSE +0 -29
@@ -0,0 +1,28 @@
1
+ import {Story} from "@storybook/react";
2
+ import {EarthEngineLayerConfigModal, EarthEngineLayerConfigModalProps, EarthEngineLayerConfiguration} from "./index";
3
+ import React from "react";
4
+
5
+ const Template: Story<EarthEngineLayerConfigModalProps> = (args) => {
6
+ return <EarthEngineLayerConfigModal {...args} />;
7
+ };
8
+
9
+ export const Basic = Template.bind({});
10
+ Basic.args = {
11
+ onClose: () => {},
12
+ onChange: console.info,
13
+ open: true,
14
+ };
15
+
16
+ export default {
17
+ title: "Analytics/Map/Earth Engine Config Modal",
18
+ component: EarthEngineLayerConfiguration,
19
+ decorators: [
20
+ (MapStory: any) => {
21
+ return (
22
+ <div style={{ width: "50%", height: "50%" }}>
23
+ <MapStory />
24
+ </div>
25
+ );
26
+ },
27
+ ],
28
+ };
@@ -0,0 +1,34 @@
1
+ import {Story} from "@storybook/react";
2
+ import {EarthEngineLayerConfiguration, EarthEngineLayerConfigurationProps} from "./index";
3
+ import React from "react";
4
+ import {useForm} from "react-hook-form";
5
+ import {EarthEngineLayerConfig} from "../MapLayer/interfaces";
6
+ import i18n from "@dhis2/d2-i18n";
7
+ import {Button} from "@dhis2/ui";
8
+
9
+ const Template: Story<EarthEngineLayerConfigurationProps> = (args) => {
10
+ const form = useForm<EarthEngineLayerConfig>();
11
+ return (
12
+ <form className="column gap-16" onSubmit={form.handleSubmit(console.log)}>
13
+ <EarthEngineLayerConfiguration {...args} form={form} />
14
+ <Button type="submit">{i18n.t("Submit")}</Button>
15
+ </form>
16
+ );
17
+ };
18
+
19
+ export const Basic = Template.bind({});
20
+ Basic.args = {};
21
+
22
+ export default {
23
+ title: "Analytics/Map/Earth Engine Configuration",
24
+ component: EarthEngineLayerConfiguration,
25
+ decorators: [
26
+ (MapStory: any) => {
27
+ return (
28
+ <div style={{ width: "50%", height: "50%" }}>
29
+ <MapStory />
30
+ </div>
31
+ );
32
+ },
33
+ ],
34
+ };
@@ -0,0 +1,412 @@
1
+ import {EarthEngineLayerConfig} from "../MapLayer/interfaces";
2
+ import {Controller, FormProvider, useForm, useFormContext, UseFormReturn, useWatch} from "react-hook-form";
3
+ import React, {useEffect, useMemo} from "react";
4
+ import {EARTH_ENGINE_LAYERS, SUPPORTED_EARTH_ENGINE_LAYERS} from "../MapLayer/components/GoogleEngineLayer/constants";
5
+ import {capitalize, filter, find, head, isEmpty} from "lodash";
6
+ import i18n from "@dhis2/d2-i18n";
7
+ import {
8
+ Button,
9
+ ButtonStrip,
10
+ CenteredContent,
11
+ CircularLoader,
12
+ Field,
13
+ InputField,
14
+ Modal,
15
+ ModalActions,
16
+ ModalContent,
17
+ ModalTitle,
18
+ MultiSelectField,
19
+ MultiSelectOption,
20
+ SingleSelectField,
21
+ SingleSelectOption,
22
+ } from "@dhis2/ui";
23
+ import {useGoogleEngineToken} from "../MapLayer/components/GoogleEngineLayer/hooks";
24
+ import {EarthEngineOptions} from "../MapLayer/components/GoogleEngineLayer/interfaces";
25
+ import {EarthEngine} from "../MapLayer/components/GoogleEngineLayer/services/engine";
26
+ import {useQuery} from "react-query";
27
+ import ColorScaleSelect from "../ThematicLayerConfiguration/components/ColorScaleSelect";
28
+ import {
29
+ defaultClasses,
30
+ defaultColorScaleName,
31
+ getColorClasses,
32
+ getColorPalette,
33
+ getColorScale
34
+ } from "../../utils/colors";
35
+
36
+ export interface EarthEngineLayerConfigurationProps {
37
+ form: UseFormReturn<EarthEngineLayerConfig>;
38
+ excluded?: string[];
39
+ [key: string]: any;
40
+ }
41
+
42
+ function useType() {
43
+ const type = useWatch({
44
+ name: "type",
45
+ });
46
+ return find(EARTH_ENGINE_LAYERS, ["id", type]);
47
+ }
48
+
49
+ function AggregationSelector() {
50
+ const config = useType();
51
+
52
+ if (!config?.defaultAggregations) {
53
+ return null;
54
+ }
55
+
56
+ const supportedAggregations = config?.defaultAggregations ?? [];
57
+
58
+ const maxAggregations = config?.maxAggregations;
59
+
60
+ return (
61
+ <Controller
62
+ render={({ field, fieldState }) => {
63
+ return maxAggregations === 1 ? (
64
+ <SingleSelectField
65
+ clearable
66
+ error={Boolean(fieldState.error)}
67
+ validationText={fieldState?.error?.message}
68
+ selected={supportedAggregations.includes(head(field.value) ?? "") ? head(field.value) : undefined}
69
+ onChange={({ selected }: { selected: string }) => field.onChange([selected])}
70
+ label={i18n.t("Aggregation")}>
71
+ {supportedAggregations.map((aggregation) => (
72
+ <SingleSelectOption key={`${aggregation}-option`} label={capitalize(aggregation)} value={aggregation} />
73
+ ))}
74
+ </SingleSelectField>
75
+ ) : (
76
+ <MultiSelectField
77
+ error={Boolean(fieldState.error)}
78
+ validationText={fieldState?.error?.message}
79
+ selected={field.value?.filter((value: string) => supportedAggregations?.includes(value))}
80
+ onChange={({ selected }: { selected: string[] }) => field.onChange(selected)}
81
+ label={i18n.t("Aggregations")}>
82
+ {supportedAggregations.map((aggregation) => (
83
+ <MultiSelectOption key={`${aggregation}-option`} label={capitalize(aggregation)} value={aggregation} />
84
+ ))}
85
+ </MultiSelectField>
86
+ );
87
+ }}
88
+ name={"aggregations"}
89
+ />
90
+ );
91
+ }
92
+
93
+ function useDatasetInfo(shouldRun: boolean, config?: EarthEngineOptions) {
94
+ const { refresh } = useGoogleEngineToken();
95
+
96
+ async function getInfo() {
97
+ if (config) {
98
+ const tokenData = await refresh();
99
+ await EarthEngine.setToken(tokenData.token, refresh);
100
+ const engine = new EarthEngine({
101
+ options: config,
102
+ });
103
+ return engine.getPeriod();
104
+ }
105
+ }
106
+
107
+ const { data, error, isLoading } = useQuery([config], getInfo);
108
+
109
+ const periods = useMemo(() => {
110
+ const features = (data as any)?.features;
111
+ return features?.map((feature: any) => {
112
+ return new Date(feature?.properties["system:time_start"])?.getFullYear();
113
+ });
114
+ }, [data]);
115
+
116
+ return {
117
+ loading: isLoading,
118
+ error: error as any,
119
+ periods,
120
+ };
121
+ }
122
+
123
+ function PeriodSelector() {
124
+ const config = useType();
125
+ const { setValue, getValues } = useFormContext();
126
+ const filters = config?.filters ?? [];
127
+ const hasPeriodFilter = filters.includes("period");
128
+ const { loading, error, periods } = useDatasetInfo(hasPeriodFilter, config);
129
+ const initialPeriod = getValues("filters.period");
130
+
131
+ useEffect(() => {
132
+ if (!isEmpty(periods) && !initialPeriod) {
133
+ setValue("filters.period", head(periods));
134
+ }
135
+ }, [periods]);
136
+
137
+ if (!hasPeriodFilter) {
138
+ return null;
139
+ }
140
+
141
+ if (error) {
142
+ return (
143
+ <div style={{ minWidth: "100%", minHeight: 100 }}>
144
+ <CenteredContent>
145
+ <p>{error?.message ?? error?.toString()}</p>
146
+ </CenteredContent>
147
+ </div>
148
+ );
149
+ }
150
+
151
+ return (
152
+ <Controller
153
+ name="filters.period"
154
+ rules={{
155
+ required: i18n.t("Period is required"),
156
+ }}
157
+ render={({ field, fieldState }) => (
158
+ <div style={{ gap: 4 }} className="row align-items-center">
159
+ <div style={{ flex: 1 }}>
160
+ <SingleSelectField
161
+ helpText={i18n.t("Available periods are set by the source data")}
162
+ loading={loading}
163
+ filterable
164
+ label={i18n.t("Period")}
165
+ required
166
+ error={Boolean(fieldState.error)}
167
+ validationText={fieldState.error?.message}
168
+ onChange={({ selected }: { selected: string }) => field.onChange(parseInt(selected))}
169
+ selected={periods?.includes(field.value) ? field.value?.toString() : undefined}>
170
+ {periods?.map((period: number) => (
171
+ <SingleSelectOption key={`${period}-option`} value={period.toString()} label={period.toString()} />
172
+ ))}
173
+ </SingleSelectField>
174
+ </div>
175
+ {loading && <CircularLoader extrasmall />}
176
+ </div>
177
+ )}
178
+ />
179
+ );
180
+ }
181
+
182
+ function ColorConfig() {
183
+ return (
184
+ <div className="column gap-16">
185
+ <div className="row gap-8">
186
+ <Controller
187
+ render={({ field, fieldState }) => (
188
+ <InputField
189
+ {...field}
190
+ error={Boolean(fieldState.error)}
191
+ validationText={fieldState.error?.message}
192
+ value={field.value?.toString()}
193
+ onChange={({ value }: { value: string }) => field.onChange(parseInt(value))}
194
+ label={i18n.t("Min")}
195
+ type="number"
196
+ />
197
+ )}
198
+ name={"params.min"}
199
+ />
200
+ <Controller
201
+ render={({ field, fieldState }) => (
202
+ <InputField
203
+ {...field}
204
+ error={Boolean(fieldState.error)}
205
+ validationText={fieldState.error?.message}
206
+ value={field.value?.toString()}
207
+ onChange={({ value }: { value: string }) => field.onChange(parseInt(value))}
208
+ label={i18n.t("Max")}
209
+ type="number"
210
+ />
211
+ )}
212
+ name={"params.max"}
213
+ />
214
+ <Controller
215
+ name="params.palette"
216
+ render={({ field, fieldState }) => {
217
+ const palette = field.value;
218
+ const scale = getColorClasses(palette);
219
+ const colorClass = getColorScale(palette ?? "");
220
+
221
+ const onChange = ({ selected }: { selected: string }) => {
222
+ const palette = getColorPalette(colorClass as string, parseInt(selected))?.join(",");
223
+ field.onChange(palette);
224
+ };
225
+
226
+ return (
227
+ <SingleSelectField
228
+ validationText={fieldState.error?.message}
229
+ error={Boolean(fieldState.error)}
230
+ selected={scale?.toString() ?? defaultClasses.toString()}
231
+ label={i18n.t("Steps")}
232
+ onChange={onChange}
233
+ name="scale">
234
+ {[3, 4, 5, 6, 7, 8, 9].map((value) => (
235
+ <SingleSelectOption key={`${value}-classes-option`} label={`${value}`} value={value?.toString()} />
236
+ ))}
237
+ </SingleSelectField>
238
+ );
239
+ }}
240
+ />
241
+ </div>
242
+ <div>
243
+ <Controller
244
+ name="params.palette"
245
+ render={({ field, fieldState }) => {
246
+ const palette = field.value;
247
+ const scale = getColorClasses(palette);
248
+ const colorClass = getColorScale(palette ?? "");
249
+
250
+ const onChange = (colorClass: string) => {
251
+ const palette = getColorPalette(colorClass, scale)?.join(",");
252
+ field.onChange(palette);
253
+ };
254
+
255
+ return (
256
+ <Field error={Boolean(fieldState.error)} validationText={fieldState.error?.message} label={i18n.t("Colors")}>
257
+ <ColorScaleSelect count={scale ?? defaultClasses} colorClass={colorClass ?? defaultColorScaleName} width={300} onChange={onChange} />
258
+ </Field>
259
+ );
260
+ }}
261
+ />
262
+ </div>
263
+ </div>
264
+ );
265
+ }
266
+
267
+ function StylesConfig() {
268
+ const config = useType();
269
+ const hasParams = Boolean(config?.params);
270
+
271
+ if (!hasParams) {
272
+ return null;
273
+ }
274
+
275
+ return (
276
+ <div style={{ minWidth: 200, minHeight: 100 }} className="row gap-16">
277
+ <div className="column">
278
+ <p>
279
+ {i18n.t("Unit")}: {config?.unit}
280
+ </p>
281
+ <ColorConfig />
282
+ </div>
283
+ </div>
284
+ );
285
+ }
286
+
287
+ function Name() {
288
+ const config = useType();
289
+ const { setValue } = useFormContext();
290
+ useEffect(() => {
291
+ setValue("name", config?.name);
292
+ setValue("id", config?.id);
293
+ }, [config]);
294
+
295
+ return (
296
+ <Controller
297
+ name="name"
298
+ rules={{
299
+ required: i18n.t("Name is required"),
300
+ }}
301
+ render={({ field, fieldState }) => (
302
+ <InputField
303
+ label={i18n.t("Layer name")}
304
+ type="text"
305
+ required
306
+ error={Boolean(fieldState.error)}
307
+ validationText={fieldState.error?.message}
308
+ onChange={({ value }: { value: string }) => field.onChange(value)}
309
+ value={field.value}
310
+ />
311
+ )}
312
+ />
313
+ );
314
+ }
315
+
316
+ function TypeField({ excluded }: { excluded?: string[] }) {
317
+ const supportedLayers = filter(EARTH_ENGINE_LAYERS, ({ id }) => SUPPORTED_EARTH_ENGINE_LAYERS.includes(id) && !(excluded?.includes(id) ?? false));
318
+ const { setValue } = useFormContext();
319
+ const setConfigDefaults = (selected: string) => {
320
+ const config = find(supportedLayers, ["id", selected]);
321
+ if (!config) return;
322
+
323
+ if (config?.defaultAggregations) {
324
+ setValue("aggregations", config?.defaultAggregations);
325
+ } else {
326
+ setValue("aggregations", undefined);
327
+ }
328
+
329
+ if (config?.params) {
330
+ const { max, min, palette } = config.params;
331
+ setValue("params.max", max);
332
+ setValue("params.min", min);
333
+ setValue("params.palette", palette);
334
+ } else {
335
+ setValue("params", undefined);
336
+ }
337
+ };
338
+
339
+ return (
340
+ <Controller
341
+ name="type"
342
+ rules={{
343
+ required: i18n.t("Type is required"),
344
+ }}
345
+ render={({ field, fieldState }) => (
346
+ <SingleSelectField
347
+ label={i18n.t("Layer type")}
348
+ required
349
+ error={Boolean(fieldState.error)}
350
+ validationText={fieldState.error?.message}
351
+ onChange={({ selected }: { selected: string }) => {
352
+ setConfigDefaults(selected);
353
+ field.onChange(selected);
354
+ }}
355
+ selected={Boolean(find(supportedLayers, "id", field.value)) ? field.value : undefined}>
356
+ {supportedLayers?.map((layer) => (
357
+ <SingleSelectOption key={`${layer.id}-option`} value={layer.id} label={layer.name} />
358
+ ))}
359
+ </SingleSelectField>
360
+ )}
361
+ />
362
+ );
363
+ }
364
+
365
+ export function EarthEngineLayerConfiguration({ form, excluded }: EarthEngineLayerConfigurationProps) {
366
+ return (
367
+ <FormProvider {...form}>
368
+ <div className="column gap-16">
369
+ <TypeField excluded={excluded} />
370
+ <Name />
371
+ <AggregationSelector />
372
+ <PeriodSelector />
373
+ <StylesConfig />
374
+ </div>
375
+ </FormProvider>
376
+ );
377
+ }
378
+
379
+ export interface EarthEngineLayerConfigModalProps {
380
+ open: boolean;
381
+ config?: EarthEngineLayerConfig;
382
+ exclude?: string[];
383
+ onClose: () => void;
384
+ onChange: (config: EarthEngineLayerConfig) => void;
385
+ }
386
+
387
+ export function EarthEngineLayerConfigModal({ open, exclude, config, onClose, onChange, ...props }: EarthEngineLayerConfigModalProps) {
388
+ const form = useForm<EarthEngineLayerConfig>({
389
+ defaultValues: config ?? {},
390
+ });
391
+ const onSubmitClick = (values: EarthEngineLayerConfig) => {
392
+ onClose();
393
+ onChange(values);
394
+ };
395
+
396
+ return (
397
+ <Modal {...props} open={open} onClose={onClose}>
398
+ <ModalTitle>{i18n.t("Configure Earth Engine Layer")}</ModalTitle>
399
+ <ModalContent>
400
+ <EarthEngineLayerConfiguration form={form} excluded={exclude} />
401
+ </ModalContent>
402
+ <ModalActions>
403
+ <ButtonStrip>
404
+ <Button onClick={onClose}>{i18n.t("Cancel")}</Button>
405
+ <Button primary onClick={form.handleSubmit(onSubmitClick)}>
406
+ {i18n.t("Save")}
407
+ </Button>
408
+ </ButtonStrip>
409
+ </ModalActions>
410
+ </Modal>
411
+ );
412
+ }
@@ -0,0 +1,83 @@
1
+ import {uid} from "@hisptz/dhis2-utils";
2
+ import {Map as LeafletMap} from "leaflet";
3
+ import {isEmpty} from "lodash";
4
+ import React, {forwardRef, useRef} from "react";
5
+ import {LayersControl, MapContainer, TileLayer} from "react-leaflet";
6
+ import {useMapBounds} from "../../hooks/map";
7
+ import MapControl from "../MapControls";
8
+ import MapLayer from "../MapLayer";
9
+ import LegendArea from "../MapLayer/components/LegendArea";
10
+ import {CustomThematicLayer} from "../MapLayer/interfaces";
11
+ import {MapLayersProvider} from "../MapProvider/components/MapLayerProvider";
12
+ import {useMapLayers} from "../MapProvider/hooks";
13
+ import {MapAreaProps, MapControls, MapLegendConfig} from "./interfaces";
14
+ import MapUpdater from "../MapUpdater";
15
+
16
+ function MapLayerArea({
17
+ id,
18
+ base,
19
+ controls,
20
+ legends,
21
+ }: {
22
+ id: string;
23
+ base?: {
24
+ url: string;
25
+ attribution: string;
26
+ };
27
+ controls?: MapControls[];
28
+ legends?: MapLegendConfig;
29
+ }) {
30
+ const { layers } = useMapLayers();
31
+
32
+ return (
33
+ <>
34
+ <TileLayer
35
+ id={id}
36
+ attribution={
37
+ base?.attribution ??
38
+ '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> | &copy; <a href="https://carto.com/attribution">CARTO</a>'
39
+ }
40
+ url={base?.url ?? "https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png"}
41
+ />
42
+ {controls?.map((control) => (
43
+ <MapControl mapId={id} key={`${control.type}-control`} {...control} />
44
+ ))}
45
+ {!isEmpty(layers) && (
46
+ <LayersControl hideSingleBase position={"topleft"}>
47
+ {(layers as CustomThematicLayer[]).map((layer: CustomThematicLayer, index) => (
48
+ <MapLayer key={layer.id} layer={layer} index={index} />
49
+ ))}
50
+ </LayersControl>
51
+ )}
52
+ {!isEmpty(layers) && <LegendArea legends={legends} layers={layers as CustomThematicLayer[]} position={"topright"} />}
53
+ </>
54
+ );
55
+ }
56
+
57
+ const MapArea = ({ base, controls, mapOptions, key, legends, layers }: MapAreaProps, ref: React.Ref<LeafletMap> | undefined) => {
58
+ const { center, bounds } = useMapBounds();
59
+ const { current: id } = useRef<string>(uid());
60
+
61
+ return (
62
+ <div id={`${id}-"map-container`} style={{ height: "100%", width: "100%" }}>
63
+ <MapContainer
64
+ attributionControl
65
+ ref={ref}
66
+ id={id}
67
+ center={center}
68
+ bounceAtZoomLimits
69
+ bounds={bounds}
70
+ style={{ height: "100%", width: "100%", minHeight: 500 }}
71
+ key={key}
72
+ trackResize
73
+ {...mapOptions}>
74
+ <MapUpdater bounds={bounds} />
75
+ <MapLayersProvider layers={layers}>
76
+ <MapLayerArea base={base} id={id} controls={controls} legends={legends} />
77
+ </MapLayersProvider>
78
+ </MapContainer>
79
+ </div>
80
+ );
81
+ };
82
+
83
+ export default forwardRef(MapArea);
@@ -0,0 +1,39 @@
1
+ import {ControlPosition} from "leaflet";
2
+ import type {MapContainerProps} from "react-leaflet";
3
+ import {
4
+ CustomBoundaryLayer,
5
+ CustomPointLayer,
6
+ EarthEngineLayerConfig,
7
+ ThematicLayerConfig
8
+ } from "../../MapLayer/interfaces";
9
+
10
+ export interface MapControls {
11
+ position: ControlPosition;
12
+ type: "zoom" | "rotate" | "fullscreen" | "compass" | "scale" | "print";
13
+ options?: Record<string, any>;
14
+ }
15
+
16
+ export interface MapLegendConfig {
17
+ enabled: boolean;
18
+ position: ControlPosition;
19
+ collapsible: boolean;
20
+ }
21
+
22
+ export interface MapLayerConfig {
23
+ thematicLayers?: ThematicLayerConfig[];
24
+ boundaryLayers?: CustomBoundaryLayer[];
25
+ pointLayers?: CustomPointLayer[];
26
+ earthEngineLayers?: EarthEngineLayerConfig[];
27
+ }
28
+
29
+ export interface MapAreaProps {
30
+ base?: {
31
+ url: string;
32
+ attribution: string;
33
+ };
34
+ controls?: MapControls[];
35
+ mapOptions?: MapContainerProps;
36
+ legends?: MapLegendConfig;
37
+ layers: MapLayerConfig;
38
+ key?: string;
39
+ }
@@ -0,0 +1,24 @@
1
+ import {ControlOptions} from "leaflet";
2
+ import React from "react";
3
+
4
+ const POSITION_CLASSES = {
5
+ bottomleft: "leaflet-bottom leaflet-left",
6
+ bottomright: "leaflet-bottom leaflet-right",
7
+ topleft: "leaflet-top leaflet-left",
8
+ topright: "leaflet-top leaflet-right",
9
+ };
10
+
11
+ interface CustomControlOptions extends ControlOptions {
12
+ children: React.ReactNode;
13
+ }
14
+
15
+ export function CustomControl({ children, position, ...options }: CustomControlOptions) {
16
+ const positionClass = (position && POSITION_CLASSES[position]) || POSITION_CLASSES.topright;
17
+ return (
18
+ <div {...options} className={`${positionClass}`}>
19
+ <div style={{ overflow: "hidden", border: "none" }} className="leaflet-control leaflet-bar">
20
+ {children}
21
+ </div>
22
+ </div>
23
+ );
24
+ }
@@ -0,0 +1,11 @@
1
+ import {createControlComponent} from "@react-leaflet/core";
2
+ import L, {ControlPosition} from "leaflet";
3
+ import "leaflet-easyprint";
4
+ import React from "react";
5
+
6
+ const DownloadControlComponent = createControlComponent((props) => {
7
+ return (L as any).easyPrint(props);
8
+ });
9
+ export default function DownloadControl({ options, position, mapId }: { options: any; position: ControlPosition; mapId: string }) {
10
+ return <DownloadControlComponent {...{ ...options, position }} />;
11
+ }
@@ -0,0 +1,7 @@
1
+ import {createControlComponent} from "@react-leaflet/core";
2
+ import {control} from "leaflet";
3
+ import "leaflet.fullscreen";
4
+ import "leaflet.fullscreen/Control.FullScreen.css";
5
+
6
+ const FullscreenControl = createControlComponent((props) => (control as any).fullscreen(props));
7
+ export default FullscreenControl;
@@ -0,0 +1,24 @@
1
+ import React from "react";
2
+ import {ScaleControl, ZoomControl} from "react-leaflet";
3
+ import {MapControls} from "../MapArea/interfaces";
4
+ import FullscreenControl from "./components/FullscreenControl";
5
+ import DownloadControl from "./components/DownloadControl";
6
+
7
+ export interface MapControlProps extends MapControls {
8
+ mapId: string;
9
+ }
10
+
11
+ export default function MapControl({ type, options, position, mapId }: MapControlProps) {
12
+ switch (type) {
13
+ case "zoom":
14
+ return <ZoomControl position={position} {...options} />;
15
+ case "scale":
16
+ return <ScaleControl position={position} {...options} />;
17
+ case "fullscreen":
18
+ return <FullscreenControl position={position} {...options} />;
19
+ case "print":
20
+ return <DownloadControl mapId={mapId} position={position} options={options} />;
21
+ default:
22
+ return null;
23
+ }
24
+ }
@@ -0,0 +1,7 @@
1
+ import {useMapOrganisationUnit} from "../../../../MapProvider/hooks";
2
+
3
+ export function useBoundaryData() {
4
+ const { orgUnits } = useMapOrganisationUnit();
5
+
6
+ return orgUnits;
7
+ }