Spaces:
Running
Running
export
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- frontend/package-lock.json +110 -0
- frontend/package.json +2 -0
- frontend/src/pages/AdminPage/AdminPage.tsx +73 -73
- frontend/src/pages/AnalyticsPage/AnalyticsPage.module.css +80 -0
- frontend/src/pages/AnalyticsPage/AnalyticsPage.tsx +988 -93
- frontend/src/pages/ExplorePage/ExplorePage.module.css +204 -1
- frontend/src/pages/ExplorePage/ExplorePage.tsx +579 -25
- frontend/src/pages/HelpPage.tsx +47 -2
- frontend/src/pages/MapDetailsPage/MapDetailPage.module.css +97 -1
- frontend/src/pages/MapDetailsPage/MapDetailPage.tsx +491 -22
- frontend/src/pages/UploadPage/UploadPage.module.css +15 -1
- frontend/src/pages/UploadPage/UploadPage.tsx +227 -2
- go-web-app-develop/.changeset/README.md +8 -0
- go-web-app-develop/.changeset/config.json +15 -0
- go-web-app-develop/.changeset/lovely-kids-boil.md +5 -0
- go-web-app-develop/.changeset/pre.json +15 -0
- go-web-app-develop/.changeset/solid-clubs-care.md +8 -0
- go-web-app-develop/.changeset/sweet-gifts-cheer.md +9 -0
- go-web-app-develop/.changeset/whole-lions-guess.md +7 -0
- go-web-app-develop/.dockerignore +148 -0
- go-web-app-develop/.github/ISSUE_TEMPLATE/01_bug_report.yml +92 -0
- go-web-app-develop/.github/ISSUE_TEMPLATE/02_feature_request.yml +39 -0
- go-web-app-develop/.github/ISSUE_TEMPLATE/03_epic_request.yml +37 -0
- go-web-app-develop/.github/ISSUE_TEMPLATE/config.yml +5 -0
- go-web-app-develop/.github/dependabot.yml +27 -0
- go-web-app-develop/.github/pull_request_template.md +30 -0
- go-web-app-develop/.github/workflows/add-issue-to-backlog.yml +16 -0
- go-web-app-develop/.github/workflows/chromatic.yml +127 -0
- go-web-app-develop/.github/workflows/ci.yml +304 -0
- go-web-app-develop/.github/workflows/publish-nginx-serve.yml +147 -0
- go-web-app-develop/.github/workflows/publish-storybook-nginx-serve.yml +127 -0
- go-web-app-develop/.gitignore +43 -0
- go-web-app-develop/.npmrc +1 -0
- go-web-app-develop/COLLABORATING.md +18 -0
- go-web-app-develop/CONTRIBUTING.md +81 -0
- go-web-app-develop/LICENSE +21 -0
- go-web-app-develop/README.md +117 -0
- go-web-app-develop/app/CHANGELOG.md +729 -0
- go-web-app-develop/app/env.ts +29 -0
- go-web-app-develop/app/eslint.config.js +165 -0
- go-web-app-develop/app/index.html +69 -0
- go-web-app-develop/app/package.json +119 -0
- go-web-app-develop/app/postcss.config.cjs +8 -0
- go-web-app-develop/app/public/go-icon.svg +4 -0
- go-web-app-develop/app/scripts/translatte/README.md +59 -0
- go-web-app-develop/app/scripts/translatte/commands/applyMigrations.test.ts +104 -0
- go-web-app-develop/app/scripts/translatte/commands/applyMigrations.ts +177 -0
- go-web-app-develop/app/scripts/translatte/commands/exportMigration.ts +62 -0
- go-web-app-develop/app/scripts/translatte/commands/generateMigration.test.ts +102 -0
- go-web-app-develop/app/scripts/translatte/commands/generateMigration.ts +195 -0
frontend/package-lock.json
CHANGED
|
@@ -10,6 +10,8 @@
|
|
| 10 |
"dependencies": {
|
| 11 |
"@ifrc-go/icons": "^2.0.1",
|
| 12 |
"@ifrc-go/ui": "^1.3.0",
|
|
|
|
|
|
|
| 13 |
"lucide-react": "^0.525.0",
|
| 14 |
"react": "^18.2.0",
|
| 15 |
"react-dom": "^18.2.0",
|
|
@@ -1995,6 +1997,15 @@
|
|
| 1995 |
"dev": true,
|
| 1996 |
"license": "MIT"
|
| 1997 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1998 |
"node_modules/@types/node": {
|
| 1999 |
"version": "24.1.0",
|
| 2000 |
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
|
|
@@ -2604,6 +2615,12 @@
|
|
| 2604 |
"url": "https://opencollective.com/core-js"
|
| 2605 |
}
|
| 2606 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2607 |
"node_modules/cross-spawn": {
|
| 2608 |
"version": "7.0.6",
|
| 2609 |
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
|
@@ -3268,6 +3285,12 @@
|
|
| 3268 |
"node": ">= 4"
|
| 3269 |
}
|
| 3270 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3271 |
"node_modules/import-fresh": {
|
| 3272 |
"version": "3.3.1",
|
| 3273 |
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
|
@@ -3295,6 +3318,12 @@
|
|
| 3295 |
"node": ">=0.8.19"
|
| 3296 |
}
|
| 3297 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3298 |
"node_modules/is-extglob": {
|
| 3299 |
"version": "2.1.1",
|
| 3300 |
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
|
@@ -3337,6 +3366,12 @@
|
|
| 3337 |
"node": ">=0.10.0"
|
| 3338 |
}
|
| 3339 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3340 |
"node_modules/isexe": {
|
| 3341 |
"version": "2.0.0",
|
| 3342 |
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
|
@@ -3420,6 +3455,18 @@
|
|
| 3420 |
"node": ">=6"
|
| 3421 |
}
|
| 3422 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3423 |
"node_modules/keyv": {
|
| 3424 |
"version": "4.5.4",
|
| 3425 |
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
|
@@ -3444,6 +3491,15 @@
|
|
| 3444 |
"node": ">= 0.8.0"
|
| 3445 |
}
|
| 3446 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3447 |
"node_modules/lightningcss": {
|
| 3448 |
"version": "1.30.1",
|
| 3449 |
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
|
@@ -3928,6 +3984,12 @@
|
|
| 3928 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 3929 |
}
|
| 3930 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3931 |
"node_modules/parent-module": {
|
| 3932 |
"version": "1.0.1",
|
| 3933 |
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
|
@@ -4031,6 +4093,12 @@
|
|
| 4031 |
"node": ">= 0.8.0"
|
| 4032 |
}
|
| 4033 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4034 |
"node_modules/prop-types": {
|
| 4035 |
"version": "15.8.1",
|
| 4036 |
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
|
@@ -4276,6 +4344,21 @@
|
|
| 4276 |
}
|
| 4277 |
}
|
| 4278 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4279 |
"node_modules/resolve-from": {
|
| 4280 |
"version": "4.0.0",
|
| 4281 |
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
|
@@ -4360,6 +4443,12 @@
|
|
| 4360 |
"queue-microtask": "^1.2.2"
|
| 4361 |
}
|
| 4362 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4363 |
"node_modules/sanitize-html": {
|
| 4364 |
"version": "2.17.0",
|
| 4365 |
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.0.tgz",
|
|
@@ -4396,6 +4485,12 @@
|
|
| 4396 |
"node": ">=10"
|
| 4397 |
}
|
| 4398 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4399 |
"node_modules/shebang-command": {
|
| 4400 |
"version": "2.0.0",
|
| 4401 |
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
|
@@ -4428,6 +4523,15 @@
|
|
| 4428 |
"node": ">=0.10.0"
|
| 4429 |
}
|
| 4430 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4431 |
"node_modules/strip-json-comments": {
|
| 4432 |
"version": "3.1.1",
|
| 4433 |
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
|
@@ -4704,6 +4808,12 @@
|
|
| 4704 |
}
|
| 4705 |
}
|
| 4706 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4707 |
"node_modules/vite": {
|
| 4708 |
"version": "7.1.3",
|
| 4709 |
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz",
|
|
|
|
| 10 |
"dependencies": {
|
| 11 |
"@ifrc-go/icons": "^2.0.1",
|
| 12 |
"@ifrc-go/ui": "^1.3.0",
|
| 13 |
+
"@types/jszip": "^3.4.0",
|
| 14 |
+
"jszip": "^3.10.1",
|
| 15 |
"lucide-react": "^0.525.0",
|
| 16 |
"react": "^18.2.0",
|
| 17 |
"react-dom": "^18.2.0",
|
|
|
|
| 1997 |
"dev": true,
|
| 1998 |
"license": "MIT"
|
| 1999 |
},
|
| 2000 |
+
"node_modules/@types/jszip": {
|
| 2001 |
+
"version": "3.4.0",
|
| 2002 |
+
"resolved": "https://registry.npmjs.org/@types/jszip/-/jszip-3.4.0.tgz",
|
| 2003 |
+
"integrity": "sha512-GFHqtQQP3R4NNuvZH3hNCYD0NbyBZ42bkN7kO3NDrU/SnvIZWMS8Bp38XCsRKBT5BXvgm0y1zqpZWp/ZkRzBzg==",
|
| 2004 |
+
"license": "MIT",
|
| 2005 |
+
"dependencies": {
|
| 2006 |
+
"jszip": "*"
|
| 2007 |
+
}
|
| 2008 |
+
},
|
| 2009 |
"node_modules/@types/node": {
|
| 2010 |
"version": "24.1.0",
|
| 2011 |
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
|
|
|
|
| 2615 |
"url": "https://opencollective.com/core-js"
|
| 2616 |
}
|
| 2617 |
},
|
| 2618 |
+
"node_modules/core-util-is": {
|
| 2619 |
+
"version": "1.0.3",
|
| 2620 |
+
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
| 2621 |
+
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
| 2622 |
+
"license": "MIT"
|
| 2623 |
+
},
|
| 2624 |
"node_modules/cross-spawn": {
|
| 2625 |
"version": "7.0.6",
|
| 2626 |
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
|
|
|
| 3285 |
"node": ">= 4"
|
| 3286 |
}
|
| 3287 |
},
|
| 3288 |
+
"node_modules/immediate": {
|
| 3289 |
+
"version": "3.0.6",
|
| 3290 |
+
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
| 3291 |
+
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
| 3292 |
+
"license": "MIT"
|
| 3293 |
+
},
|
| 3294 |
"node_modules/import-fresh": {
|
| 3295 |
"version": "3.3.1",
|
| 3296 |
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
|
|
|
| 3318 |
"node": ">=0.8.19"
|
| 3319 |
}
|
| 3320 |
},
|
| 3321 |
+
"node_modules/inherits": {
|
| 3322 |
+
"version": "2.0.4",
|
| 3323 |
+
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
| 3324 |
+
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
| 3325 |
+
"license": "ISC"
|
| 3326 |
+
},
|
| 3327 |
"node_modules/is-extglob": {
|
| 3328 |
"version": "2.1.1",
|
| 3329 |
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
|
|
|
| 3366 |
"node": ">=0.10.0"
|
| 3367 |
}
|
| 3368 |
},
|
| 3369 |
+
"node_modules/isarray": {
|
| 3370 |
+
"version": "1.0.0",
|
| 3371 |
+
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
| 3372 |
+
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
| 3373 |
+
"license": "MIT"
|
| 3374 |
+
},
|
| 3375 |
"node_modules/isexe": {
|
| 3376 |
"version": "2.0.0",
|
| 3377 |
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
|
|
|
| 3455 |
"node": ">=6"
|
| 3456 |
}
|
| 3457 |
},
|
| 3458 |
+
"node_modules/jszip": {
|
| 3459 |
+
"version": "3.10.1",
|
| 3460 |
+
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
| 3461 |
+
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
| 3462 |
+
"license": "(MIT OR GPL-3.0-or-later)",
|
| 3463 |
+
"dependencies": {
|
| 3464 |
+
"lie": "~3.3.0",
|
| 3465 |
+
"pako": "~1.0.2",
|
| 3466 |
+
"readable-stream": "~2.3.6",
|
| 3467 |
+
"setimmediate": "^1.0.5"
|
| 3468 |
+
}
|
| 3469 |
+
},
|
| 3470 |
"node_modules/keyv": {
|
| 3471 |
"version": "4.5.4",
|
| 3472 |
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
|
|
|
| 3491 |
"node": ">= 0.8.0"
|
| 3492 |
}
|
| 3493 |
},
|
| 3494 |
+
"node_modules/lie": {
|
| 3495 |
+
"version": "3.3.0",
|
| 3496 |
+
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
| 3497 |
+
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
| 3498 |
+
"license": "MIT",
|
| 3499 |
+
"dependencies": {
|
| 3500 |
+
"immediate": "~3.0.5"
|
| 3501 |
+
}
|
| 3502 |
+
},
|
| 3503 |
"node_modules/lightningcss": {
|
| 3504 |
"version": "1.30.1",
|
| 3505 |
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
|
|
|
| 3984 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 3985 |
}
|
| 3986 |
},
|
| 3987 |
+
"node_modules/pako": {
|
| 3988 |
+
"version": "1.0.11",
|
| 3989 |
+
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
| 3990 |
+
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
| 3991 |
+
"license": "(MIT AND Zlib)"
|
| 3992 |
+
},
|
| 3993 |
"node_modules/parent-module": {
|
| 3994 |
"version": "1.0.1",
|
| 3995 |
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
|
|
|
| 4093 |
"node": ">= 0.8.0"
|
| 4094 |
}
|
| 4095 |
},
|
| 4096 |
+
"node_modules/process-nextick-args": {
|
| 4097 |
+
"version": "2.0.1",
|
| 4098 |
+
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
| 4099 |
+
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
| 4100 |
+
"license": "MIT"
|
| 4101 |
+
},
|
| 4102 |
"node_modules/prop-types": {
|
| 4103 |
"version": "15.8.1",
|
| 4104 |
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
|
|
|
| 4344 |
}
|
| 4345 |
}
|
| 4346 |
},
|
| 4347 |
+
"node_modules/readable-stream": {
|
| 4348 |
+
"version": "2.3.8",
|
| 4349 |
+
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
| 4350 |
+
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
| 4351 |
+
"license": "MIT",
|
| 4352 |
+
"dependencies": {
|
| 4353 |
+
"core-util-is": "~1.0.0",
|
| 4354 |
+
"inherits": "~2.0.3",
|
| 4355 |
+
"isarray": "~1.0.0",
|
| 4356 |
+
"process-nextick-args": "~2.0.0",
|
| 4357 |
+
"safe-buffer": "~5.1.1",
|
| 4358 |
+
"string_decoder": "~1.1.1",
|
| 4359 |
+
"util-deprecate": "~1.0.1"
|
| 4360 |
+
}
|
| 4361 |
+
},
|
| 4362 |
"node_modules/resolve-from": {
|
| 4363 |
"version": "4.0.0",
|
| 4364 |
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
|
|
|
| 4443 |
"queue-microtask": "^1.2.2"
|
| 4444 |
}
|
| 4445 |
},
|
| 4446 |
+
"node_modules/safe-buffer": {
|
| 4447 |
+
"version": "5.1.2",
|
| 4448 |
+
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
| 4449 |
+
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
| 4450 |
+
"license": "MIT"
|
| 4451 |
+
},
|
| 4452 |
"node_modules/sanitize-html": {
|
| 4453 |
"version": "2.17.0",
|
| 4454 |
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.0.tgz",
|
|
|
|
| 4485 |
"node": ">=10"
|
| 4486 |
}
|
| 4487 |
},
|
| 4488 |
+
"node_modules/setimmediate": {
|
| 4489 |
+
"version": "1.0.5",
|
| 4490 |
+
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
| 4491 |
+
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
| 4492 |
+
"license": "MIT"
|
| 4493 |
+
},
|
| 4494 |
"node_modules/shebang-command": {
|
| 4495 |
"version": "2.0.0",
|
| 4496 |
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
|
|
|
| 4523 |
"node": ">=0.10.0"
|
| 4524 |
}
|
| 4525 |
},
|
| 4526 |
+
"node_modules/string_decoder": {
|
| 4527 |
+
"version": "1.1.1",
|
| 4528 |
+
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
| 4529 |
+
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
| 4530 |
+
"license": "MIT",
|
| 4531 |
+
"dependencies": {
|
| 4532 |
+
"safe-buffer": "~5.1.0"
|
| 4533 |
+
}
|
| 4534 |
+
},
|
| 4535 |
"node_modules/strip-json-comments": {
|
| 4536 |
"version": "3.1.1",
|
| 4537 |
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
|
|
|
| 4808 |
}
|
| 4809 |
}
|
| 4810 |
},
|
| 4811 |
+
"node_modules/util-deprecate": {
|
| 4812 |
+
"version": "1.0.2",
|
| 4813 |
+
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
| 4814 |
+
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
| 4815 |
+
"license": "MIT"
|
| 4816 |
+
},
|
| 4817 |
"node_modules/vite": {
|
| 4818 |
"version": "7.1.3",
|
| 4819 |
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz",
|
frontend/package.json
CHANGED
|
@@ -30,6 +30,8 @@
|
|
| 30 |
"dependencies": {
|
| 31 |
"@ifrc-go/icons": "^2.0.1",
|
| 32 |
"@ifrc-go/ui": "^1.3.0",
|
|
|
|
|
|
|
| 33 |
"lucide-react": "^0.525.0",
|
| 34 |
"react": "^18.2.0",
|
| 35 |
"react-dom": "^18.2.0",
|
|
|
|
| 30 |
"dependencies": {
|
| 31 |
"@ifrc-go/icons": "^2.0.1",
|
| 32 |
"@ifrc-go/ui": "^1.3.0",
|
| 33 |
+
"@types/jszip": "^3.4.0",
|
| 34 |
+
"jszip": "^3.10.1",
|
| 35 |
"lucide-react": "^0.525.0",
|
| 36 |
"react": "^18.2.0",
|
| 37 |
"react-dom": "^18.2.0",
|
frontend/src/pages/AdminPage/AdminPage.tsx
CHANGED
|
@@ -11,11 +11,11 @@ export default function AdminPage() {
|
|
| 11 |
const [error, setError] = useState('');
|
| 12 |
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
provider?: string;
|
| 20 |
model_id?: string;
|
| 21 |
config?: {
|
|
@@ -24,7 +24,7 @@ export default function AdminPage() {
|
|
| 24 |
model?: string;
|
| 25 |
stub?: boolean;
|
| 26 |
};
|
| 27 |
-
|
| 28 |
const [selectedModel, setSelectedModel] = useState<string>('');
|
| 29 |
|
| 30 |
// Model management state
|
|
@@ -57,31 +57,31 @@ export default function AdminPage() {
|
|
| 57 |
}
|
| 58 |
}, [isAuthenticated]);
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
console.log('Models data received:', modelsData);
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
|
| 86 |
const toggleModelAvailability = async (modelCode: string, currentStatus: boolean) => {
|
| 87 |
try {
|
|
@@ -317,9 +317,9 @@ Model "${newModelData.label}" added successfully!
|
|
| 317 |
return (
|
| 318 |
<PageContainer>
|
| 319 |
<div className="mx-auto max-w-md px-4 sm:px-6 lg:px-8 py-6 sm:py-10">
|
| 320 |
-
|
| 321 |
<Heading level={2}>Admin Login</Heading>
|
| 322 |
-
|
| 323 |
|
| 324 |
<form onSubmit={handleLogin} className="space-y-6">
|
| 325 |
<div>
|
|
@@ -393,12 +393,12 @@ Model "${newModelData.label}" added successfully!
|
|
| 393 |
<SelectInput
|
| 394 |
label="Model"
|
| 395 |
name="selected-model"
|
| 396 |
-
|
| 397 |
onChange={(newValue) => handleModelChange(newValue || '')}
|
| 398 |
options={[
|
| 399 |
{ value: 'random', label: 'Random' },
|
| 400 |
...availableModels
|
| 401 |
-
|
| 402 |
.map(model => ({
|
| 403 |
value: model.m_code,
|
| 404 |
label: model.label
|
|
@@ -473,7 +473,7 @@ Model "${newModelData.label}" added successfully!
|
|
| 473 |
Delete
|
| 474 |
</Button>
|
| 475 |
</div>
|
| 476 |
-
|
| 477 |
</tr>
|
| 478 |
))}
|
| 479 |
</tbody>
|
|
@@ -490,7 +490,7 @@ Model "${newModelData.label}" added successfully!
|
|
| 490 |
>
|
| 491 |
Add New Model
|
| 492 |
</Button>
|
| 493 |
-
|
| 494 |
)}
|
| 495 |
|
| 496 |
{/* Add Model Form - now below the table */}
|
|
@@ -560,13 +560,13 @@ Model "${newModelData.label}" added successfully!
|
|
| 560 |
>
|
| 561 |
Save Model
|
| 562 |
</Button>
|
| 563 |
-
|
| 564 |
name="cancel-add"
|
| 565 |
-
|
| 566 |
onClick={() => setShowAddModelForm(false)}
|
| 567 |
>
|
| 568 |
Cancel
|
| 569 |
-
|
| 570 |
</div>
|
| 571 |
</div>
|
| 572 |
)}
|
|
@@ -639,10 +639,10 @@ Model "${newModelData.label}" added successfully!
|
|
| 639 |
>
|
| 640 |
Update Model
|
| 641 |
</Button>
|
| 642 |
-
|
| 643 |
name="cancel-edit"
|
| 644 |
-
|
| 645 |
-
|
| 646 |
setShowEditModelForm(false);
|
| 647 |
setEditingModel(null);
|
| 648 |
setNewModelData({
|
|
@@ -652,27 +652,27 @@ Model "${newModelData.label}" added successfully!
|
|
| 652 |
provider: 'huggingface',
|
| 653 |
model_id: '',
|
| 654 |
is_available: false
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
Cancel
|
| 659 |
-
|
| 660 |
-
|
| 661 |
</div>
|
| 662 |
)}
|
| 663 |
-
|
| 664 |
-
|
| 665 |
|
| 666 |
|
| 667 |
|
| 668 |
{/* Utilities Section */}
|
| 669 |
-
|
| 670 |
heading="Utilities"
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
<Button
|
| 677 |
name="test-connection"
|
| 678 |
variant="secondary"
|
|
@@ -702,17 +702,17 @@ Model "${newModelData.label}" added successfully!
|
|
| 702 |
Test Connection
|
| 703 |
</Button>
|
| 704 |
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
console.log('Schemas Response:', data);
|
| 717 |
|
| 718 |
let results = '';
|
|
@@ -741,13 +741,13 @@ Model "${newModelData.label}" added successfully!
|
|
| 741 |
setTestResults(results);
|
| 742 |
setTestResultsTitle('Schemas Error');
|
| 743 |
setShowTestResultsModal(true);
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
</div>
|
| 752 |
</div>
|
| 753 |
|
|
|
|
| 11 |
const [error, setError] = useState('');
|
| 12 |
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
| 13 |
|
| 14 |
+
const [availableModels, setAvailableModels] = useState<Array<{
|
| 15 |
+
m_code: string;
|
| 16 |
+
label: string;
|
| 17 |
+
model_type: string;
|
| 18 |
+
is_available: boolean;
|
| 19 |
provider?: string;
|
| 20 |
model_id?: string;
|
| 21 |
config?: {
|
|
|
|
| 24 |
model?: string;
|
| 25 |
stub?: boolean;
|
| 26 |
};
|
| 27 |
+
}>>([]);
|
| 28 |
const [selectedModel, setSelectedModel] = useState<string>('');
|
| 29 |
|
| 30 |
// Model management state
|
|
|
|
| 57 |
}
|
| 58 |
}, [isAuthenticated]);
|
| 59 |
|
| 60 |
+
const fetchModels = () => {
|
| 61 |
+
fetch('/api/models')
|
| 62 |
+
.then(r => r.json())
|
| 63 |
+
.then(modelsData => {
|
| 64 |
console.log('Models data received:', modelsData);
|
| 65 |
+
setAvailableModels(modelsData.models || []);
|
| 66 |
+
|
| 67 |
+
const persistedModel = localStorage.getItem(SELECTED_MODEL_KEY);
|
| 68 |
+
if (modelsData.models && modelsData.models.length > 0) {
|
| 69 |
+
if (persistedModel === 'random') {
|
| 70 |
+
// Keep random selection
|
| 71 |
+
setSelectedModel('random');
|
| 72 |
+
} else if (persistedModel && modelsData.models.find((m: { m_code: string; is_available: boolean }) => m.m_code === persistedModel && m.is_available)) {
|
| 73 |
+
setSelectedModel(persistedModel);
|
| 74 |
+
} else {
|
| 75 |
+
const firstAvailableModel = modelsData.models.find((m: { is_available: boolean }) => m.is_available) || modelsData.models[0];
|
| 76 |
+
setSelectedModel(firstAvailableModel.m_code);
|
| 77 |
+
localStorage.setItem(SELECTED_MODEL_KEY, firstAvailableModel.m_code);
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
})
|
| 81 |
+
.catch(() => {
|
| 82 |
+
// Handle error silently
|
| 83 |
+
});
|
| 84 |
+
};
|
| 85 |
|
| 86 |
const toggleModelAvailability = async (modelCode: string, currentStatus: boolean) => {
|
| 87 |
try {
|
|
|
|
| 317 |
return (
|
| 318 |
<PageContainer>
|
| 319 |
<div className="mx-auto max-w-md px-4 sm:px-6 lg:px-8 py-6 sm:py-10">
|
| 320 |
+
<div className="text-center mb-8">
|
| 321 |
<Heading level={2}>Admin Login</Heading>
|
| 322 |
+
</div>
|
| 323 |
|
| 324 |
<form onSubmit={handleLogin} className="space-y-6">
|
| 325 |
<div>
|
|
|
|
| 393 |
<SelectInput
|
| 394 |
label="Model"
|
| 395 |
name="selected-model"
|
| 396 |
+
value={selectedModel}
|
| 397 |
onChange={(newValue) => handleModelChange(newValue || '')}
|
| 398 |
options={[
|
| 399 |
{ value: 'random', label: 'Random' },
|
| 400 |
...availableModels
|
| 401 |
+
.filter(model => model.is_available)
|
| 402 |
.map(model => ({
|
| 403 |
value: model.m_code,
|
| 404 |
label: model.label
|
|
|
|
| 473 |
Delete
|
| 474 |
</Button>
|
| 475 |
</div>
|
| 476 |
+
</td>
|
| 477 |
</tr>
|
| 478 |
))}
|
| 479 |
</tbody>
|
|
|
|
| 490 |
>
|
| 491 |
Add New Model
|
| 492 |
</Button>
|
| 493 |
+
</div>
|
| 494 |
)}
|
| 495 |
|
| 496 |
{/* Add Model Form - now below the table */}
|
|
|
|
| 560 |
>
|
| 561 |
Save Model
|
| 562 |
</Button>
|
| 563 |
+
<Button
|
| 564 |
name="cancel-add"
|
| 565 |
+
variant="secondary"
|
| 566 |
onClick={() => setShowAddModelForm(false)}
|
| 567 |
>
|
| 568 |
Cancel
|
| 569 |
+
</Button>
|
| 570 |
</div>
|
| 571 |
</div>
|
| 572 |
)}
|
|
|
|
| 639 |
>
|
| 640 |
Update Model
|
| 641 |
</Button>
|
| 642 |
+
<Button
|
| 643 |
name="cancel-edit"
|
| 644 |
+
variant="secondary"
|
| 645 |
+
onClick={() => {
|
| 646 |
setShowEditModelForm(false);
|
| 647 |
setEditingModel(null);
|
| 648 |
setNewModelData({
|
|
|
|
| 652 |
provider: 'huggingface',
|
| 653 |
model_id: '',
|
| 654 |
is_available: false
|
| 655 |
+
});
|
| 656 |
+
}}
|
| 657 |
+
>
|
| 658 |
Cancel
|
| 659 |
+
</Button>
|
| 660 |
+
</div>
|
| 661 |
</div>
|
| 662 |
)}
|
| 663 |
+
</div>
|
| 664 |
+
</Container>
|
| 665 |
|
| 666 |
|
| 667 |
|
| 668 |
{/* Utilities Section */}
|
| 669 |
+
<Container
|
| 670 |
heading="Utilities"
|
| 671 |
+
headingLevel={2}
|
| 672 |
+
withHeaderBorder
|
| 673 |
+
withInternalPadding
|
| 674 |
+
>
|
| 675 |
+
<div className="flex flex-wrap gap-4">
|
| 676 |
<Button
|
| 677 |
name="test-connection"
|
| 678 |
variant="secondary"
|
|
|
|
| 702 |
Test Connection
|
| 703 |
</Button>
|
| 704 |
|
| 705 |
+
<Button
|
| 706 |
+
name="view-schemas"
|
| 707 |
+
variant="secondary"
|
| 708 |
+
onClick={() => {
|
| 709 |
+
fetch('/api/schemas', {
|
| 710 |
+
headers: {
|
| 711 |
+
'Authorization': `Bearer ${localStorage.getItem('adminToken')}`
|
| 712 |
+
}
|
| 713 |
+
})
|
| 714 |
+
.then(r => r.json())
|
| 715 |
+
.then(data => {
|
| 716 |
console.log('Schemas Response:', data);
|
| 717 |
|
| 718 |
let results = '';
|
|
|
|
| 741 |
setTestResults(results);
|
| 742 |
setTestResultsTitle('Schemas Error');
|
| 743 |
setShowTestResultsModal(true);
|
| 744 |
+
});
|
| 745 |
+
}}
|
| 746 |
+
>
|
| 747 |
+
View Schemas
|
| 748 |
+
</Button>
|
| 749 |
+
</div>
|
| 750 |
+
</Container>
|
| 751 |
</div>
|
| 752 |
</div>
|
| 753 |
|
frontend/src/pages/AnalyticsPage/AnalyticsPage.module.css
CHANGED
|
@@ -93,6 +93,86 @@
|
|
| 93 |
}
|
| 94 |
}
|
| 95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
@media (max-width: 768px) {
|
| 97 |
.summaryStats {
|
| 98 |
grid-template-columns: 1fr;
|
|
|
|
| 93 |
}
|
| 94 |
}
|
| 95 |
|
| 96 |
+
.userInteractionCards {
|
| 97 |
+
display: grid;
|
| 98 |
+
grid-template-columns: 1fr;
|
| 99 |
+
gap: var(--go-ui-spacing-lg);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.userInteractionCard {
|
| 103 |
+
background-color: var(--go-ui-color-white);
|
| 104 |
+
border-radius: var(--go-ui-border-radius-lg);
|
| 105 |
+
border: var(--go-ui-width-separator-thin) solid var(--go-ui-color-separator);
|
| 106 |
+
padding: var(--go-ui-spacing-xl);
|
| 107 |
+
text-align: center;
|
| 108 |
+
box-shadow: var(--go-ui-box-shadow-sm);
|
| 109 |
+
transition: box-shadow 0.2s ease-in-out;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.userInteractionCard:hover {
|
| 113 |
+
box-shadow: var(--go-ui-box-shadow-md);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
.userInteractionCardValue {
|
| 117 |
+
font-size: var(--go-ui-font-size-2xl);
|
| 118 |
+
font-weight: var(--go-ui-font-weight-bold);
|
| 119 |
+
color: var(--go-ui-color-text);
|
| 120 |
+
margin-bottom: var(--go-ui-spacing-sm);
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.userInteractionCardLabel {
|
| 124 |
+
font-size: var(--go-ui-font-size-sm);
|
| 125 |
+
color: var(--go-ui-color-text-secondary);
|
| 126 |
+
margin-bottom: var(--go-ui-spacing-lg);
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.userInteractionCardButton {
|
| 130 |
+
width: 100%;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.summaryStatsCards {
|
| 134 |
+
display: grid;
|
| 135 |
+
grid-template-columns: 1fr;
|
| 136 |
+
gap: var(--go-ui-spacing-lg);
|
| 137 |
+
margin-bottom: var(--go-ui-spacing-lg);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.summaryStatsCard {
|
| 141 |
+
background-color: var(--go-ui-color-white);
|
| 142 |
+
border-radius: var(--go-ui-border-radius-lg);
|
| 143 |
+
border: var(--go-ui-width-separator-thin) solid var(--go-ui-color-separator);
|
| 144 |
+
padding: var(--go-ui-spacing-xl);
|
| 145 |
+
text-align: center;
|
| 146 |
+
box-shadow: var(--go-ui-box-shadow-sm);
|
| 147 |
+
transition: box-shadow 0.2s ease-in-out;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
.summaryStatsCard:hover {
|
| 151 |
+
box-shadow: var(--go-ui-box-shadow-md);
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.summaryStatsCardValue {
|
| 155 |
+
font-size: var(--go-ui-font-size-2xl);
|
| 156 |
+
font-weight: var(--go-ui-font-weight-bold);
|
| 157 |
+
color: var(--go-ui-color-text);
|
| 158 |
+
margin-bottom: var(--go-ui-spacing-sm);
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.summaryStatsCardLabel {
|
| 162 |
+
font-size: var(--go-ui-font-size-sm);
|
| 163 |
+
color: var(--go-ui-color-text-secondary);
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
@media (min-width: 768px) {
|
| 167 |
+
.userInteractionCards {
|
| 168 |
+
grid-template-columns: repeat(3, 1fr);
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.summaryStatsCards {
|
| 172 |
+
grid-template-columns: repeat(2, 1fr);
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
@media (max-width: 768px) {
|
| 177 |
.summaryStats {
|
| 178 |
grid-template-columns: 1fr;
|
frontend/src/pages/AnalyticsPage/AnalyticsPage.tsx
CHANGED
|
@@ -1,12 +1,7 @@
|
|
| 1 |
import {
|
| 2 |
-
PageContainer,
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
Spinner,
|
| 6 |
-
Container,
|
| 7 |
-
ProgressBar,
|
| 8 |
-
SegmentInput,
|
| 9 |
-
Table,
|
| 10 |
} from '@ifrc-go/ui';
|
| 11 |
import {
|
| 12 |
createStringColumn,
|
|
@@ -14,6 +9,7 @@ import {
|
|
| 14 |
numericIdSelector
|
| 15 |
} from '@ifrc-go/ui/utils';
|
| 16 |
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
|
|
| 17 |
import styles from './AnalyticsPage.module.css';
|
| 18 |
|
| 19 |
interface AnalyticsData {
|
|
@@ -28,8 +24,17 @@ interface AnalyticsData {
|
|
| 28 |
avgContext: number;
|
| 29 |
avgUsability: number;
|
| 30 |
totalScore: number;
|
|
|
|
| 31 |
};
|
| 32 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
}
|
| 34 |
|
| 35 |
interface LookupData {
|
|
@@ -70,6 +75,24 @@ interface ModelData {
|
|
| 70 |
totalScore: number;
|
| 71 |
}
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
interface MapData {
|
| 74 |
source?: string;
|
| 75 |
event_type?: string;
|
|
@@ -78,35 +101,83 @@ interface MapData {
|
|
| 78 |
accuracy?: number;
|
| 79 |
context?: number;
|
| 80 |
usability?: number;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
}
|
| 82 |
|
| 83 |
export default function AnalyticsPage() {
|
|
|
|
| 84 |
const [data, setData] = useState<AnalyticsData | null>(null);
|
| 85 |
const [loading, setLoading] = useState(true);
|
| 86 |
-
const [view, setView] = useState<'
|
| 87 |
const [sourcesLookup, setSourcesLookup] = useState<LookupData[]>([]);
|
| 88 |
const [typesLookup, setTypesLookup] = useState<LookupData[]>([]);
|
| 89 |
const [regionsLookup, setRegionsLookup] = useState<LookupData[]>([]);
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
const viewOptions = [
|
| 92 |
-
{ key: '
|
| 93 |
-
{ key: '
|
| 94 |
];
|
| 95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
const fetchAnalytics = useCallback(async () => {
|
| 97 |
setLoading(true);
|
| 98 |
try {
|
| 99 |
-
const res = await fetch('/api/images
|
| 100 |
const maps = await res.json();
|
| 101 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
const analytics: AnalyticsData = {
|
| 103 |
totalCaptions: maps.length,
|
| 104 |
sources: {},
|
| 105 |
types: {},
|
| 106 |
regions: {},
|
| 107 |
models: {},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
};
|
| 109 |
|
|
|
|
| 110 |
maps.forEach((map: MapData) => {
|
| 111 |
if (map.source) analytics.sources[map.source] = (analytics.sources[map.source] || 0) + 1;
|
| 112 |
if (map.event_type) analytics.types[map.event_type] = (analytics.types[map.event_type] || 0) + 1;
|
|
@@ -117,11 +188,22 @@ export default function AnalyticsPage() {
|
|
| 117 |
}
|
| 118 |
if (map.model) {
|
| 119 |
const m = map.model;
|
| 120 |
-
const ctr = analytics.models[m] ||= { count: 0, avgAccuracy: 0, avgContext: 0, avgUsability: 0, totalScore: 0 };
|
| 121 |
ctr.count++;
|
| 122 |
if (map.accuracy != null) ctr.avgAccuracy += map.accuracy;
|
| 123 |
if (map.context != null) ctr.avgContext += map.context;
|
| 124 |
if (map.usability != null) ctr.avgUsability += map.usability;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
}
|
| 126 |
});
|
| 127 |
|
|
@@ -146,7 +228,7 @@ export default function AnalyticsPage() {
|
|
| 146 |
const allModels = ['GPT-4', 'Claude', 'Gemini', 'Llama', 'Other'];
|
| 147 |
allModels.forEach(model => {
|
| 148 |
if (!analytics.models[model]) {
|
| 149 |
-
analytics.models[model] = { count: 0, avgAccuracy: 0, avgContext: 0, avgUsability: 0, totalScore: 0 };
|
| 150 |
}
|
| 151 |
});
|
| 152 |
|
|
@@ -159,6 +241,60 @@ export default function AnalyticsPage() {
|
|
| 159 |
}
|
| 160 |
});
|
| 161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
setData(analytics);
|
| 163 |
} catch {
|
| 164 |
|
|
@@ -166,7 +302,7 @@ export default function AnalyticsPage() {
|
|
| 166 |
} finally {
|
| 167 |
setLoading(false);
|
| 168 |
}
|
| 169 |
-
}, [sourcesLookup, typesLookup, regionsLookup]);
|
| 170 |
|
| 171 |
const fetchLookupData = useCallback(async () => {
|
| 172 |
try {
|
|
@@ -181,10 +317,19 @@ export default function AnalyticsPage() {
|
|
| 181 |
setSourcesLookup(sources);
|
| 182 |
setTypesLookup(types);
|
| 183 |
setRegionsLookup(regions);
|
| 184 |
-
} catch {
|
|
|
|
| 185 |
}
|
| 186 |
}, []);
|
| 187 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
useEffect(() => {
|
| 189 |
fetchLookupData();
|
| 190 |
}, []);
|
|
@@ -200,74 +345,109 @@ export default function AnalyticsPage() {
|
|
| 200 |
return source ? source.label : code;
|
| 201 |
}, [sourcesLookup]);
|
| 202 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
const getTypeLabel = useCallback((code: string) => {
|
| 204 |
const type = typesLookup.find(t => t.t_code === code);
|
| 205 |
return type ? type.label : code;
|
| 206 |
}, [typesLookup]);
|
| 207 |
|
| 208 |
-
const
|
| 209 |
-
if (!data || !regionsLookup.length) return [];
|
| 210 |
-
|
| 211 |
-
const allRegions = regionsLookup.reduce((acc, region) => {
|
| 212 |
-
if (region.r_code) {
|
| 213 |
-
acc[region.r_code] = {
|
| 214 |
-
name: region.label,
|
| 215 |
-
count: data.regions[region.r_code] || 0
|
| 216 |
-
};
|
| 217 |
-
}
|
| 218 |
-
return acc;
|
| 219 |
-
}, {} as Record<string, { name: string; count: number }>);
|
| 220 |
-
|
| 221 |
-
return Object.entries(allRegions)
|
| 222 |
-
.sort(([,a], [,b]) => b.count - a.count)
|
| 223 |
-
.map(([, { name, count }], index) => ({
|
| 224 |
-
id: index + 1,
|
| 225 |
-
name,
|
| 226 |
-
count,
|
| 227 |
-
percentage: data.totalCaptions > 0 ? Math.round((count / data.totalCaptions) * 100) : 0
|
| 228 |
-
}));
|
| 229 |
-
}, [data, regionsLookup]);
|
| 230 |
-
|
| 231 |
-
const typesTableData = useMemo(() => {
|
| 232 |
if (!data) return [];
|
| 233 |
|
| 234 |
-
return Object.entries(data.
|
| 235 |
-
.
|
| 236 |
-
.
|
|
|
|
| 237 |
id: index + 1,
|
| 238 |
-
name:
|
| 239 |
-
count,
|
| 240 |
-
|
|
|
|
|
|
|
| 241 |
}));
|
| 242 |
-
}, [data,
|
| 243 |
|
| 244 |
-
const
|
| 245 |
if (!data) return [];
|
| 246 |
|
| 247 |
-
return Object.entries(data.
|
| 248 |
-
.
|
| 249 |
-
.
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
|
| 257 |
-
|
|
|
|
| 258 |
if (!data) return [];
|
| 259 |
|
| 260 |
return Object.entries(data.models)
|
| 261 |
-
.
|
| 262 |
-
.map(([
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
}, [data]);
|
| 272 |
|
| 273 |
const regionsColumns = useMemo(() => [
|
|
@@ -383,8 +563,423 @@ export default function AnalyticsPage() {
|
|
| 383 |
maximumFractionDigits: 0,
|
| 384 |
},
|
| 385 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
], []);
|
| 387 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
if (loading) {
|
| 389 |
return (
|
| 390 |
<PageContainer>
|
|
@@ -405,9 +1000,7 @@ export default function AnalyticsPage() {
|
|
| 405 |
);
|
| 406 |
}
|
| 407 |
|
| 408 |
-
|
| 409 |
-
const typesChartData = Object.entries(data.types).filter(([, value]) => value > 0).map(([name, value]) => ({ name, value }));
|
| 410 |
-
const regionsChartData = Object.entries(data.regions).filter(([, value]) => value > 0).map(([name, value]) => ({ name, value }));
|
| 411 |
|
| 412 |
const ifrcColors = [
|
| 413 |
'#F5333F', '#F64752', '#F75C65', '#F87079', '#F9858C', '#FA999F', '#FBADB2', '#FCC2C5'
|
|
@@ -421,7 +1014,7 @@ export default function AnalyticsPage() {
|
|
| 421 |
name="analytics-view"
|
| 422 |
value={view}
|
| 423 |
onChange={(value) => {
|
| 424 |
-
if (value === '
|
| 425 |
setView(value);
|
| 426 |
}
|
| 427 |
}}
|
|
@@ -431,27 +1024,29 @@ export default function AnalyticsPage() {
|
|
| 431 |
/>
|
| 432 |
</div>
|
| 433 |
|
| 434 |
-
{view === '
|
| 435 |
<div className={styles.chartGrid}>
|
| 436 |
<Container heading="Summary Statistics" headingLevel={3} withHeaderBorder withInternalPadding>
|
| 437 |
-
<div className={styles.
|
| 438 |
-
<
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
|
|
|
|
|
|
| 448 |
</div>
|
| 449 |
<div className={styles.progressSection}>
|
| 450 |
<div className={styles.progressLabel}>
|
| 451 |
<span>Progress towards target</span>
|
| 452 |
-
<span>{Math.round((
|
| 453 |
</div>
|
| 454 |
-
<ProgressBar value={
|
| 455 |
</div>
|
| 456 |
</Container>
|
| 457 |
|
|
@@ -459,7 +1054,7 @@ export default function AnalyticsPage() {
|
|
| 459 |
<div className={styles.chartSection}>
|
| 460 |
<div className={styles.chartContainer}>
|
| 461 |
<PieChart
|
| 462 |
-
data={
|
| 463 |
valueSelector={d => d.value}
|
| 464 |
labelSelector={d => d.name}
|
| 465 |
keySelector={d => d.name}
|
|
@@ -469,7 +1064,7 @@ export default function AnalyticsPage() {
|
|
| 469 |
</div>
|
| 470 |
<div className={styles.tableContainer}>
|
| 471 |
<Table
|
| 472 |
-
data={
|
| 473 |
columns={regionsColumns}
|
| 474 |
keySelector={numericIdSelector}
|
| 475 |
filtered={false}
|
|
@@ -483,7 +1078,7 @@ export default function AnalyticsPage() {
|
|
| 483 |
<div className={styles.chartSection}>
|
| 484 |
<div className={styles.chartContainer}>
|
| 485 |
<PieChart
|
| 486 |
-
data={
|
| 487 |
valueSelector={d => d.value}
|
| 488 |
labelSelector={d => d.name}
|
| 489 |
keySelector={d => d.name}
|
|
@@ -493,7 +1088,7 @@ export default function AnalyticsPage() {
|
|
| 493 |
</div>
|
| 494 |
<div className={styles.tableContainer}>
|
| 495 |
<Table
|
| 496 |
-
data={
|
| 497 |
columns={sourcesColumns}
|
| 498 |
keySelector={numericIdSelector}
|
| 499 |
filtered={false}
|
|
@@ -507,7 +1102,7 @@ export default function AnalyticsPage() {
|
|
| 507 |
<div className={styles.chartSection}>
|
| 508 |
<div className={styles.chartContainer}>
|
| 509 |
<PieChart
|
| 510 |
-
data={
|
| 511 |
valueSelector={d => d.value}
|
| 512 |
labelSelector={d => d.name}
|
| 513 |
keySelector={d => d.name}
|
|
@@ -517,7 +1112,7 @@ export default function AnalyticsPage() {
|
|
| 517 |
</div>
|
| 518 |
<div className={styles.tableContainer}>
|
| 519 |
<Table
|
| 520 |
-
data={
|
| 521 |
columns={typesColumns}
|
| 522 |
keySelector={numericIdSelector}
|
| 523 |
filtered={false}
|
|
@@ -525,14 +1120,284 @@ export default function AnalyticsPage() {
|
|
| 525 |
/>
|
| 526 |
</div>
|
| 527 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 528 |
</Container>
|
| 529 |
</div>
|
| 530 |
) : (
|
| 531 |
<div className={styles.chartGrid}>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
<Container heading="Model Performance" headingLevel={3} withHeaderBorder withInternalPadding>
|
| 533 |
<div className={styles.modelPerformance}>
|
| 534 |
<Table
|
| 535 |
-
data={
|
| 536 |
columns={modelsColumns}
|
| 537 |
keySelector={numericIdSelector}
|
| 538 |
filtered={false}
|
|
@@ -540,9 +1405,39 @@ export default function AnalyticsPage() {
|
|
| 540 |
/>
|
| 541 |
</div>
|
| 542 |
</Container>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
</div>
|
| 544 |
)}
|
| 545 |
</div>
|
|
|
|
|
|
|
| 546 |
</PageContainer>
|
| 547 |
);
|
| 548 |
}
|
|
|
|
| 1 |
import {
|
| 2 |
+
PageContainer, Button,
|
| 3 |
+
Container, Spinner, SegmentInput,
|
| 4 |
+
Table, PieChart, ProgressBar,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
} from '@ifrc-go/ui';
|
| 6 |
import {
|
| 7 |
createStringColumn,
|
|
|
|
| 9 |
numericIdSelector
|
| 10 |
} from '@ifrc-go/ui/utils';
|
| 11 |
import { useState, useEffect, useMemo, useCallback } from 'react';
|
| 12 |
+
import { useSearchParams } from 'react-router-dom';
|
| 13 |
import styles from './AnalyticsPage.module.css';
|
| 14 |
|
| 15 |
interface AnalyticsData {
|
|
|
|
| 24 |
avgContext: number;
|
| 25 |
avgUsability: number;
|
| 26 |
totalScore: number;
|
| 27 |
+
deleteCount: number;
|
| 28 |
};
|
| 29 |
};
|
| 30 |
+
modelEditTimes: { [key: string]: number[] };
|
| 31 |
+
percentageModified: number;
|
| 32 |
+
modelPercentageData: { [key: string]: number[] };
|
| 33 |
+
totalDeleteCount: number;
|
| 34 |
+
deleteRate: number;
|
| 35 |
+
// Add separated image data for proper filtering
|
| 36 |
+
crisisMaps: MapData[];
|
| 37 |
+
droneImages: MapData[];
|
| 38 |
}
|
| 39 |
|
| 40 |
interface LookupData {
|
|
|
|
| 75 |
totalScore: number;
|
| 76 |
}
|
| 77 |
|
| 78 |
+
interface EditTimeData {
|
| 79 |
+
id: number;
|
| 80 |
+
name: string;
|
| 81 |
+
count: number;
|
| 82 |
+
avgEditTime: number;
|
| 83 |
+
minEditTime: number;
|
| 84 |
+
maxEditTime: number;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
interface PercentageModifiedData {
|
| 88 |
+
id: number;
|
| 89 |
+
name: string;
|
| 90 |
+
count: number;
|
| 91 |
+
avgPercentageModified: number;
|
| 92 |
+
minPercentageModified: number;
|
| 93 |
+
maxPercentageModified: number;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
interface MapData {
|
| 97 |
source?: string;
|
| 98 |
event_type?: string;
|
|
|
|
| 101 |
accuracy?: number;
|
| 102 |
context?: number;
|
| 103 |
usability?: number;
|
| 104 |
+
created_at?: string;
|
| 105 |
+
updated_at?: string;
|
| 106 |
+
generated?: string;
|
| 107 |
+
edited?: string;
|
| 108 |
+
image_type?: string;
|
| 109 |
}
|
| 110 |
|
| 111 |
export default function AnalyticsPage() {
|
| 112 |
+
const [searchParams] = useSearchParams();
|
| 113 |
const [data, setData] = useState<AnalyticsData | null>(null);
|
| 114 |
const [loading, setLoading] = useState(true);
|
| 115 |
+
const [view, setView] = useState<'crisis_maps' | 'drone_images'>('crisis_maps');
|
| 116 |
const [sourcesLookup, setSourcesLookup] = useState<LookupData[]>([]);
|
| 117 |
const [typesLookup, setTypesLookup] = useState<LookupData[]>([]);
|
| 118 |
const [regionsLookup, setRegionsLookup] = useState<LookupData[]>([]);
|
| 119 |
+
const [showEditTimeModal, setShowEditTimeModal] = useState(false);
|
| 120 |
+
const [showPercentageModal, setShowPercentageModal] = useState(false);
|
| 121 |
+
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
| 122 |
|
| 123 |
const viewOptions = [
|
| 124 |
+
{ key: 'crisis_maps' as const, label: 'Crisis Maps' },
|
| 125 |
+
{ key: 'drone_images' as const, label: 'Drone Images' }
|
| 126 |
];
|
| 127 |
|
| 128 |
+
// Helper function to calculate word similarity
|
| 129 |
+
const calculateWordSimilarity = useCallback((text1: string, text2: string): number => {
|
| 130 |
+
if (!text1 || !text2) return 0;
|
| 131 |
+
|
| 132 |
+
// Split into words, lowercase, and remove punctuation
|
| 133 |
+
const words1 = text1.toLowerCase().replace(/[^\w\s]/g, '').split(/\s+/).filter(word => word.length > 0);
|
| 134 |
+
const words2 = text2.toLowerCase().replace(/[^\w\s]/g, '').split(/\s+/).filter(word => word.length > 0);
|
| 135 |
+
|
| 136 |
+
if (words1.length === 0 && words2.length === 0) return 1; // Both empty = 100% similar
|
| 137 |
+
if (words1.length === 0 || words2.length === 0) return 0; // One empty = 0% similar
|
| 138 |
+
|
| 139 |
+
// Create sets of unique words
|
| 140 |
+
const set1 = new Set(words1);
|
| 141 |
+
const set2 = new Set(words2);
|
| 142 |
+
|
| 143 |
+
// Calculate intersection and union
|
| 144 |
+
const intersection = new Set([...set1].filter(word => set2.has(word)));
|
| 145 |
+
const union = new Set([...set1, ...set2]);
|
| 146 |
+
|
| 147 |
+
// Calculate similarity
|
| 148 |
+
const similarity = intersection.size / union.size;
|
| 149 |
+
return similarity;
|
| 150 |
+
}, []);
|
| 151 |
+
|
| 152 |
const fetchAnalytics = useCallback(async () => {
|
| 153 |
setLoading(true);
|
| 154 |
try {
|
| 155 |
+
const res = await fetch('/api/images');
|
| 156 |
const maps = await res.json();
|
| 157 |
|
| 158 |
+
// Calculate edit times for each model
|
| 159 |
+
const modelEditTimes: { [key: string]: number[] } = {};
|
| 160 |
+
|
| 161 |
+
// Separate images by type for proper filtering
|
| 162 |
+
const crisisMaps = maps.filter((map: MapData) => map.image_type === 'crisis_map');
|
| 163 |
+
const droneImages = maps.filter((map: MapData) => map.image_type === 'drone_image');
|
| 164 |
+
|
| 165 |
const analytics: AnalyticsData = {
|
| 166 |
totalCaptions: maps.length,
|
| 167 |
sources: {},
|
| 168 |
types: {},
|
| 169 |
regions: {},
|
| 170 |
models: {},
|
| 171 |
+
modelEditTimes: modelEditTimes,
|
| 172 |
+
percentageModified: 0,
|
| 173 |
+
modelPercentageData: {},
|
| 174 |
+
totalDeleteCount: 0,
|
| 175 |
+
deleteRate: 0,
|
| 176 |
+
crisisMaps: crisisMaps,
|
| 177 |
+
droneImages: droneImages,
|
| 178 |
};
|
| 179 |
|
| 180 |
+
// Process all images for global analytics
|
| 181 |
maps.forEach((map: MapData) => {
|
| 182 |
if (map.source) analytics.sources[map.source] = (analytics.sources[map.source] || 0) + 1;
|
| 183 |
if (map.event_type) analytics.types[map.event_type] = (analytics.types[map.event_type] || 0) + 1;
|
|
|
|
| 188 |
}
|
| 189 |
if (map.model) {
|
| 190 |
const m = map.model;
|
| 191 |
+
const ctr = analytics.models[m] ||= { count: 0, avgAccuracy: 0, avgContext: 0, avgUsability: 0, totalScore: 0, deleteCount: 0 };
|
| 192 |
ctr.count++;
|
| 193 |
if (map.accuracy != null) ctr.avgAccuracy += map.accuracy;
|
| 194 |
if (map.context != null) ctr.avgContext += map.context;
|
| 195 |
if (map.usability != null) ctr.avgUsability += map.usability;
|
| 196 |
+
|
| 197 |
+
// Calculate edit time if both timestamps exist
|
| 198 |
+
if (map.created_at && map.updated_at) {
|
| 199 |
+
const created = new Date(map.created_at).getTime();
|
| 200 |
+
const updated = new Date(map.updated_at).getTime();
|
| 201 |
+
const editTimeMs = updated - created;
|
| 202 |
+
if (editTimeMs > 0) {
|
| 203 |
+
if (!modelEditTimes[m]) modelEditTimes[m] = [];
|
| 204 |
+
modelEditTimes[m].push(editTimeMs);
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
}
|
| 208 |
});
|
| 209 |
|
|
|
|
| 228 |
const allModels = ['GPT-4', 'Claude', 'Gemini', 'Llama', 'Other'];
|
| 229 |
allModels.forEach(model => {
|
| 230 |
if (!analytics.models[model]) {
|
| 231 |
+
analytics.models[model] = { count: 0, avgAccuracy: 0, avgContext: 0, avgUsability: 0, totalScore: 0, deleteCount: 0 };
|
| 232 |
}
|
| 233 |
});
|
| 234 |
|
|
|
|
| 241 |
}
|
| 242 |
});
|
| 243 |
|
| 244 |
+
// Calculate percentage modified (median)
|
| 245 |
+
const textPairs = maps.filter((map: MapData) => map.generated && map.edited);
|
| 246 |
+
|
| 247 |
+
if (textPairs.length > 0) {
|
| 248 |
+
const similarities = textPairs.map((map: MapData) =>
|
| 249 |
+
calculateWordSimilarity(map.generated!, map.edited!)
|
| 250 |
+
);
|
| 251 |
+
const sortedSimilarities = [...similarities].sort((a, b) => a - b);
|
| 252 |
+
const mid = Math.floor(sortedSimilarities.length / 2);
|
| 253 |
+
const medianSimilarity = sortedSimilarities.length % 2 === 0
|
| 254 |
+
? (sortedSimilarities[mid - 1] + sortedSimilarities[mid]) / 2
|
| 255 |
+
: sortedSimilarities[mid];
|
| 256 |
+
analytics.percentageModified = Math.round((1 - medianSimilarity) * 100);
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
// Calculate percentage modified per model (median)
|
| 260 |
+
const modelPercentageData: { [key: string]: number[] } = {};
|
| 261 |
+
|
| 262 |
+
maps.forEach((map: MapData) => {
|
| 263 |
+
if (map.model && map.generated && map.edited) {
|
| 264 |
+
const similarity = calculateWordSimilarity(map.generated, map.edited);
|
| 265 |
+
const percentageModified = Math.round((1 - similarity) * 100);
|
| 266 |
+
|
| 267 |
+
if (!modelPercentageData[map.model]) {
|
| 268 |
+
modelPercentageData[map.model] = [];
|
| 269 |
+
}
|
| 270 |
+
modelPercentageData[map.model].push(percentageModified);
|
| 271 |
+
}
|
| 272 |
+
});
|
| 273 |
+
|
| 274 |
+
analytics.modelPercentageData = modelPercentageData;
|
| 275 |
+
|
| 276 |
+
// Fetch model data including delete counts
|
| 277 |
+
try {
|
| 278 |
+
const modelsRes = await fetch('/api/admin/models');
|
| 279 |
+
if (modelsRes.ok) {
|
| 280 |
+
const modelsData = await modelsRes.json();
|
| 281 |
+
|
| 282 |
+
// Update delete counts for each model
|
| 283 |
+
modelsData.forEach((model: { m_code: string; delete_count: number }) => {
|
| 284 |
+
if (analytics.models[model.m_code]) {
|
| 285 |
+
analytics.models[model.m_code].deleteCount = model.delete_count || 0;
|
| 286 |
+
}
|
| 287 |
+
});
|
| 288 |
+
|
| 289 |
+
// Calculate total delete count and delete rate
|
| 290 |
+
const totalDeleteCount = modelsData.reduce((sum: number, model: { delete_count: number }) => sum + (model.delete_count || 0), 0);
|
| 291 |
+
analytics.totalDeleteCount = totalDeleteCount;
|
| 292 |
+
analytics.deleteRate = totalDeleteCount > 0 ? Math.round((totalDeleteCount / (totalDeleteCount + maps.length)) * 100) : 0;
|
| 293 |
+
}
|
| 294 |
+
} catch (error) {
|
| 295 |
+
console.log('Could not fetch model delete counts:', error);
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
setData(analytics);
|
| 299 |
} catch {
|
| 300 |
|
|
|
|
| 302 |
} finally {
|
| 303 |
setLoading(false);
|
| 304 |
}
|
| 305 |
+
}, [sourcesLookup, typesLookup, regionsLookup, calculateWordSimilarity]);
|
| 306 |
|
| 307 |
const fetchLookupData = useCallback(async () => {
|
| 308 |
try {
|
|
|
|
| 317 |
setSourcesLookup(sources);
|
| 318 |
setTypesLookup(types);
|
| 319 |
setRegionsLookup(regions);
|
| 320 |
+
} catch (error) {
|
| 321 |
+
console.log('Could not fetch lookup data:', error);
|
| 322 |
}
|
| 323 |
}, []);
|
| 324 |
|
| 325 |
+
// Set initial view based on URL parameter
|
| 326 |
+
useEffect(() => {
|
| 327 |
+
const viewParam = searchParams.get('view');
|
| 328 |
+
if (viewParam === 'crisis_maps' || viewParam === 'drone_images') {
|
| 329 |
+
setView(viewParam);
|
| 330 |
+
}
|
| 331 |
+
}, [searchParams]);
|
| 332 |
+
|
| 333 |
useEffect(() => {
|
| 334 |
fetchLookupData();
|
| 335 |
}, []);
|
|
|
|
| 345 |
return source ? source.label : code;
|
| 346 |
}, [sourcesLookup]);
|
| 347 |
|
| 348 |
+
const getMedianEditTime = useCallback((editTimes: number[]) => {
|
| 349 |
+
if (editTimes.length === 0) return 0;
|
| 350 |
+
const sortedTimes = [...editTimes].sort((a, b) => a - b);
|
| 351 |
+
const mid = Math.floor(sortedTimes.length / 2);
|
| 352 |
+
return sortedTimes.length % 2 === 0
|
| 353 |
+
? Math.round((sortedTimes[mid - 1] + sortedTimes[mid]) / 2)
|
| 354 |
+
: sortedTimes[mid];
|
| 355 |
+
}, []);
|
| 356 |
+
|
| 357 |
+
const formatEditTime = useCallback((ms: number) => {
|
| 358 |
+
const seconds = Math.floor(ms / 1000);
|
| 359 |
+
const minutes = Math.floor(seconds / 60);
|
| 360 |
+
const hours = Math.floor(minutes / 60);
|
| 361 |
+
|
| 362 |
+
if (hours > 0) {
|
| 363 |
+
return `${hours}h ${minutes % 60}m`;
|
| 364 |
+
} else if (minutes > 0) {
|
| 365 |
+
return `${minutes}m ${seconds % 60}s`;
|
| 366 |
+
} else {
|
| 367 |
+
return `${seconds}s`;
|
| 368 |
+
}
|
| 369 |
+
}, []);
|
| 370 |
+
|
| 371 |
const getTypeLabel = useCallback((code: string) => {
|
| 372 |
const type = typesLookup.find(t => t.t_code === code);
|
| 373 |
return type ? type.label : code;
|
| 374 |
}, [typesLookup]);
|
| 375 |
|
| 376 |
+
const editTimeTableData = useMemo(() => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
if (!data) return [];
|
| 378 |
|
| 379 |
+
return Object.entries(data.modelEditTimes || {})
|
| 380 |
+
.filter(([, editTimes]) => editTimes.length > 0)
|
| 381 |
+
.sort(([, a], [, b]) => getMedianEditTime(b) - getMedianEditTime(a))
|
| 382 |
+
.map(([model, editTimes], index) => ({
|
| 383 |
id: index + 1,
|
| 384 |
+
name: model,
|
| 385 |
+
count: editTimes.length,
|
| 386 |
+
avgEditTime: getMedianEditTime(editTimes),
|
| 387 |
+
minEditTime: Math.min(...editTimes),
|
| 388 |
+
maxEditTime: Math.max(...editTimes)
|
| 389 |
}));
|
| 390 |
+
}, [data, getMedianEditTime]);
|
| 391 |
|
| 392 |
+
const percentageModifiedTableData = useMemo(() => {
|
| 393 |
if (!data) return [];
|
| 394 |
|
| 395 |
+
return Object.entries(data.modelPercentageData || {})
|
| 396 |
+
.filter(([, percentages]) => percentages.length > 0)
|
| 397 |
+
.sort(([, a], [, b]) => {
|
| 398 |
+
const sortedA = [...a].sort((x, y) => x - y);
|
| 399 |
+
const sortedB = [...b].sort((x, y) => x - y);
|
| 400 |
+
const midA = Math.floor(sortedA.length / 2);
|
| 401 |
+
const midB = Math.floor(sortedB.length / 2);
|
| 402 |
+
const medianA = sortedA.length % 2 === 0
|
| 403 |
+
? (sortedA[midA - 1] + sortedA[midA]) / 2
|
| 404 |
+
: sortedA[midA];
|
| 405 |
+
const medianB = sortedB.length % 2 === 0
|
| 406 |
+
? (sortedB[midB - 1] + sortedB[midB]) / 2
|
| 407 |
+
: sortedB[midB];
|
| 408 |
+
return medianB - medianA;
|
| 409 |
+
})
|
| 410 |
+
.map(([model, percentages], index) => {
|
| 411 |
+
const sortedPercentages = [...percentages].sort((a, b) => a - b);
|
| 412 |
+
const mid = Math.floor(sortedPercentages.length / 2);
|
| 413 |
+
const medianPercentage = sortedPercentages.length % 2 === 0
|
| 414 |
+
? Math.round((sortedPercentages[mid - 1] + sortedPercentages[mid]) / 2)
|
| 415 |
+
: sortedPercentages[mid];
|
| 416 |
+
|
| 417 |
+
return {
|
| 418 |
+
id: index + 1,
|
| 419 |
+
name: model,
|
| 420 |
+
count: percentages.length,
|
| 421 |
+
avgPercentageModified: medianPercentage,
|
| 422 |
+
minPercentageModified: Math.min(...percentages),
|
| 423 |
+
maxPercentageModified: Math.max(...percentages)
|
| 424 |
+
};
|
| 425 |
+
});
|
| 426 |
+
}, [data]);
|
| 427 |
+
|
| 428 |
|
| 429 |
+
|
| 430 |
+
const modelConsistencyData = useMemo(() => {
|
| 431 |
if (!data) return [];
|
| 432 |
|
| 433 |
return Object.entries(data.models)
|
| 434 |
+
.filter(([, model]) => model.count > 0)
|
| 435 |
+
.map(([name, model], index) => {
|
| 436 |
+
// Calculate consistency based on how close accuracy, context, and usability are
|
| 437 |
+
const scores = [model.avgAccuracy, model.avgContext, model.avgUsability];
|
| 438 |
+
const mean = scores.reduce((sum, score) => sum + score, 0) / scores.length;
|
| 439 |
+
const variance = scores.reduce((sum, score) => sum + Math.pow(score - mean, 2), 0) / scores.length;
|
| 440 |
+
const consistency = Math.round(100 - Math.sqrt(variance)); // Lower variance = higher consistency
|
| 441 |
+
|
| 442 |
+
return {
|
| 443 |
+
id: index + 1,
|
| 444 |
+
name,
|
| 445 |
+
consistency: Math.max(0, consistency),
|
| 446 |
+
avgScore: Math.round(mean),
|
| 447 |
+
count: model.count
|
| 448 |
+
};
|
| 449 |
+
})
|
| 450 |
+
.sort((a, b) => b.consistency - a.consistency);
|
| 451 |
}, [data]);
|
| 452 |
|
| 453 |
const regionsColumns = useMemo(() => [
|
|
|
|
| 563 |
maximumFractionDigits: 0,
|
| 564 |
},
|
| 565 |
),
|
| 566 |
+
|
| 567 |
+
], [formatEditTime]);
|
| 568 |
+
|
| 569 |
+
const editTimeColumns = useMemo(() => [
|
| 570 |
+
createStringColumn<EditTimeData, number>(
|
| 571 |
+
'name',
|
| 572 |
+
'Model',
|
| 573 |
+
(item) => item.name,
|
| 574 |
+
),
|
| 575 |
+
createNumberColumn<EditTimeData, number>(
|
| 576 |
+
'count',
|
| 577 |
+
'Count',
|
| 578 |
+
(item) => item.count,
|
| 579 |
+
),
|
| 580 |
+
createStringColumn<EditTimeData, number>(
|
| 581 |
+
'avgEditTime',
|
| 582 |
+
'Median Edit Time',
|
| 583 |
+
(item) => formatEditTime(item.avgEditTime),
|
| 584 |
+
),
|
| 585 |
+
createStringColumn<EditTimeData, number>(
|
| 586 |
+
'minEditTime',
|
| 587 |
+
'Min Edit Time',
|
| 588 |
+
(item) => formatEditTime(item.minEditTime),
|
| 589 |
+
),
|
| 590 |
+
createStringColumn<EditTimeData, number>(
|
| 591 |
+
'maxEditTime',
|
| 592 |
+
'Max Edit Time',
|
| 593 |
+
(item) => formatEditTime(item.maxEditTime),
|
| 594 |
+
),
|
| 595 |
+
], [formatEditTime]);
|
| 596 |
+
|
| 597 |
+
const percentageModifiedColumns = useMemo(() => [
|
| 598 |
+
createStringColumn<PercentageModifiedData, number>(
|
| 599 |
+
'name',
|
| 600 |
+
'Model',
|
| 601 |
+
(item) => item.name,
|
| 602 |
+
),
|
| 603 |
+
createNumberColumn<PercentageModifiedData, number>(
|
| 604 |
+
'count',
|
| 605 |
+
'Count',
|
| 606 |
+
(item) => item.count,
|
| 607 |
+
),
|
| 608 |
+
createNumberColumn<PercentageModifiedData, number>(
|
| 609 |
+
'avgPercentageModified',
|
| 610 |
+
'Median % Modified',
|
| 611 |
+
(item) => item.avgPercentageModified,
|
| 612 |
+
{
|
| 613 |
+
suffix: '%',
|
| 614 |
+
maximumFractionDigits: 0,
|
| 615 |
+
},
|
| 616 |
+
),
|
| 617 |
+
createNumberColumn<PercentageModifiedData, number>(
|
| 618 |
+
'minPercentageModified',
|
| 619 |
+
'Min % Modified',
|
| 620 |
+
(item) => item.minPercentageModified,
|
| 621 |
+
{
|
| 622 |
+
suffix: '%',
|
| 623 |
+
maximumFractionDigits: 0,
|
| 624 |
+
},
|
| 625 |
+
),
|
| 626 |
+
createNumberColumn<PercentageModifiedData, number>(
|
| 627 |
+
'maxPercentageModified',
|
| 628 |
+
'Max % Modified',
|
| 629 |
+
(item) => item.maxPercentageModified,
|
| 630 |
+
{
|
| 631 |
+
suffix: '%',
|
| 632 |
+
maximumFractionDigits: 0,
|
| 633 |
+
},
|
| 634 |
+
),
|
| 635 |
], []);
|
| 636 |
|
| 637 |
+
|
| 638 |
+
|
| 639 |
+
const qualityBySourceColumns = useMemo(() => [
|
| 640 |
+
createStringColumn<{ source: string; avgQuality: number; count: number }, number>(
|
| 641 |
+
'source',
|
| 642 |
+
'Source',
|
| 643 |
+
(item) => item.source,
|
| 644 |
+
),
|
| 645 |
+
createNumberColumn<{ source: string; avgQuality: number; count: number }, number>(
|
| 646 |
+
'avgQuality',
|
| 647 |
+
'Average Quality',
|
| 648 |
+
(item) => item.avgQuality,
|
| 649 |
+
{
|
| 650 |
+
suffix: '%',
|
| 651 |
+
maximumFractionDigits: 0,
|
| 652 |
+
},
|
| 653 |
+
),
|
| 654 |
+
createNumberColumn<{ source: string; avgQuality: number; count: number }, number>(
|
| 655 |
+
'count',
|
| 656 |
+
'Count',
|
| 657 |
+
(item) => item.count,
|
| 658 |
+
),
|
| 659 |
+
], []);
|
| 660 |
+
|
| 661 |
+
const modelConsistencyColumns = useMemo(() => [
|
| 662 |
+
createStringColumn<{ name: string; consistency: number; avgScore: number; count: number }, number>(
|
| 663 |
+
'name',
|
| 664 |
+
'Model',
|
| 665 |
+
(item) => item.name,
|
| 666 |
+
),
|
| 667 |
+
createNumberColumn<{ name: string; consistency: number; avgScore: number; count: number }, number>(
|
| 668 |
+
'consistency',
|
| 669 |
+
'Consistency',
|
| 670 |
+
(item) => item.consistency,
|
| 671 |
+
{
|
| 672 |
+
suffix: '%',
|
| 673 |
+
maximumFractionDigits: 0,
|
| 674 |
+
},
|
| 675 |
+
),
|
| 676 |
+
createNumberColumn<{ name: string; consistency: number; avgScore: number; count: number }, number>(
|
| 677 |
+
'avgScore',
|
| 678 |
+
'Average Score',
|
| 679 |
+
(item) => item.avgScore,
|
| 680 |
+
{
|
| 681 |
+
suffix: '%',
|
| 682 |
+
maximumFractionDigits: 0,
|
| 683 |
+
},
|
| 684 |
+
),
|
| 685 |
+
createNumberColumn<{ name: string; consistency: number; avgScore: number; count: number }, number>(
|
| 686 |
+
'count',
|
| 687 |
+
'Count',
|
| 688 |
+
(item) => item.count,
|
| 689 |
+
),
|
| 690 |
+
], []);
|
| 691 |
+
|
| 692 |
+
// Helper functions to filter data by image type
|
| 693 |
+
const getImageTypeCount = useCallback((imageType: string) => {
|
| 694 |
+
if (!data) return 0;
|
| 695 |
+
|
| 696 |
+
if (imageType === 'crisis_map') {
|
| 697 |
+
return data.crisisMaps.length;
|
| 698 |
+
} else if (imageType === 'drone_image') {
|
| 699 |
+
return data.droneImages.length;
|
| 700 |
+
}
|
| 701 |
+
|
| 702 |
+
return 0;
|
| 703 |
+
}, [data]);
|
| 704 |
+
|
| 705 |
+
const getImageTypeRegionsChartData = useCallback((imageType: string) => {
|
| 706 |
+
if (!data) return [];
|
| 707 |
+
|
| 708 |
+
// Get the appropriate image set based on type
|
| 709 |
+
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
|
| 710 |
+
|
| 711 |
+
// Calculate regions for this specific image type
|
| 712 |
+
const regions: { [key: string]: number } = {};
|
| 713 |
+
images.forEach((map: MapData) => {
|
| 714 |
+
if (map.countries) {
|
| 715 |
+
map.countries.forEach((c) => {
|
| 716 |
+
if (c.r_code) regions[c.r_code] = (regions[c.r_code] || 0) + 1;
|
| 717 |
+
});
|
| 718 |
+
}
|
| 719 |
+
});
|
| 720 |
+
|
| 721 |
+
return Object.entries(regions)
|
| 722 |
+
.filter(([, value]) => value > 0)
|
| 723 |
+
.map(([name, value]) => ({ name, value }));
|
| 724 |
+
}, [data]);
|
| 725 |
+
|
| 726 |
+
const getImageTypeRegionsTableData = useCallback((imageType: string) => {
|
| 727 |
+
if (!data) return [];
|
| 728 |
+
|
| 729 |
+
// Get the appropriate image set based on type
|
| 730 |
+
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
|
| 731 |
+
|
| 732 |
+
// Calculate regions for this specific image type
|
| 733 |
+
const regions: { [key: string]: number } = {};
|
| 734 |
+
images.forEach((map: MapData) => {
|
| 735 |
+
if (map.countries) {
|
| 736 |
+
map.countries.forEach((c) => {
|
| 737 |
+
if (c.r_code) regions[c.r_code] = (regions[c.r_code] || 0) + 1;
|
| 738 |
+
});
|
| 739 |
+
}
|
| 740 |
+
});
|
| 741 |
+
|
| 742 |
+
// Convert to table data format
|
| 743 |
+
const allRegions = regionsLookup.reduce((acc, region) => {
|
| 744 |
+
if (region.r_code) {
|
| 745 |
+
acc[region.r_code] = {
|
| 746 |
+
name: region.label,
|
| 747 |
+
count: regions[region.r_code] || 0
|
| 748 |
+
};
|
| 749 |
+
}
|
| 750 |
+
return acc;
|
| 751 |
+
}, {} as Record<string, { name: string; count: number }>);
|
| 752 |
+
|
| 753 |
+
return Object.entries(allRegions)
|
| 754 |
+
.sort(([,a], [,b]) => b.count - a.count)
|
| 755 |
+
.map(([, { name, count }], index) => ({
|
| 756 |
+
id: index + 1,
|
| 757 |
+
name,
|
| 758 |
+
count,
|
| 759 |
+
percentage: images.length > 0 ? Math.round((count / images.length) * 100) : 0
|
| 760 |
+
}));
|
| 761 |
+
}, [data, regionsLookup]);
|
| 762 |
+
|
| 763 |
+
const getImageTypeSourcesChartData = useCallback((imageType: string) => {
|
| 764 |
+
if (!data) return [];
|
| 765 |
+
|
| 766 |
+
// Get the appropriate image set based on type
|
| 767 |
+
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
|
| 768 |
+
|
| 769 |
+
// Calculate sources for this specific image type
|
| 770 |
+
const sources: { [key: string]: number } = {};
|
| 771 |
+
images.forEach((map: MapData) => {
|
| 772 |
+
if (map.source) sources[map.source] = (sources[map.source] || 0) + 1;
|
| 773 |
+
});
|
| 774 |
+
|
| 775 |
+
return Object.entries(sources)
|
| 776 |
+
.filter(([, value]) => value > 0)
|
| 777 |
+
.map(([name, value]) => ({ name, value }));
|
| 778 |
+
}, [data]);
|
| 779 |
+
|
| 780 |
+
const getImageTypeSourcesTableData = useCallback((imageType: string) => {
|
| 781 |
+
if (!data) return [];
|
| 782 |
+
|
| 783 |
+
// Get the appropriate image set based on type
|
| 784 |
+
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
|
| 785 |
+
|
| 786 |
+
// Calculate sources for this specific image type
|
| 787 |
+
const sources: { [key: string]: number } = {};
|
| 788 |
+
images.forEach((map: MapData) => {
|
| 789 |
+
if (map.source) sources[map.source] = (sources[map.source] || 0) + 1;
|
| 790 |
+
});
|
| 791 |
+
|
| 792 |
+
// Convert to table data format
|
| 793 |
+
return Object.entries(sources)
|
| 794 |
+
.sort(([,a], [,b]) => b - a)
|
| 795 |
+
.map(([sourceKey, count], index) => ({
|
| 796 |
+
id: index + 1,
|
| 797 |
+
name: getSourceLabel(sourceKey),
|
| 798 |
+
count,
|
| 799 |
+
percentage: images.length > 0 ? Math.round((count / images.length) * 100) : 0
|
| 800 |
+
}));
|
| 801 |
+
}, [data, getSourceLabel]);
|
| 802 |
+
|
| 803 |
+
const getImageTypeTypesChartData = useCallback((imageType: string) => {
|
| 804 |
+
if (!data) return [];
|
| 805 |
+
|
| 806 |
+
// Get the appropriate image set based on type
|
| 807 |
+
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
|
| 808 |
+
|
| 809 |
+
// Calculate types for this specific image type
|
| 810 |
+
const types: { [key: string]: number } = {};
|
| 811 |
+
images.forEach((map: MapData) => {
|
| 812 |
+
if (map.event_type) types[map.event_type] = (types[map.event_type] || 0) + 1;
|
| 813 |
+
});
|
| 814 |
+
|
| 815 |
+
return Object.entries(types)
|
| 816 |
+
.filter(([, value]) => value > 0)
|
| 817 |
+
.map(([name, value]) => ({ name, value }));
|
| 818 |
+
}, [data]);
|
| 819 |
+
|
| 820 |
+
const getImageTypeTypesTableData = useCallback((imageType: string) => {
|
| 821 |
+
if (!data) return [];
|
| 822 |
+
|
| 823 |
+
// Get the appropriate image set based on type
|
| 824 |
+
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
|
| 825 |
+
|
| 826 |
+
// Calculate types for this specific image type
|
| 827 |
+
const types: { [key: string]: number } = {};
|
| 828 |
+
images.forEach((map: MapData) => {
|
| 829 |
+
if (map.event_type) types[map.event_type] = (types[map.event_type] || 0) + 1;
|
| 830 |
+
});
|
| 831 |
+
|
| 832 |
+
// Convert to table data format
|
| 833 |
+
return Object.entries(types)
|
| 834 |
+
.sort(([,a], [,b]) => b - a)
|
| 835 |
+
.map(([typeKey, count], index) => ({
|
| 836 |
+
id: index + 1,
|
| 837 |
+
name: getTypeLabel(typeKey),
|
| 838 |
+
count,
|
| 839 |
+
percentage: images.length > 0 ? Math.round((count / images.length) * 100) : 0
|
| 840 |
+
}));
|
| 841 |
+
}, [data, getTypeLabel]);
|
| 842 |
+
|
| 843 |
+
const getImageTypeMedianEditTime = useCallback((imageType: string) => {
|
| 844 |
+
if (!data) return 'No data available';
|
| 845 |
+
// Filter edit times by image type
|
| 846 |
+
const filteredEditTimes = Object.entries(data.modelEditTimes).filter(([modelName]) => {
|
| 847 |
+
if (imageType === 'crisis_map') {
|
| 848 |
+
return modelName.includes('GPT') || modelName.includes('Claude') || modelName.includes('Gemini') || modelName.includes('STUB');
|
| 849 |
+
} else if (imageType === 'drone_image') {
|
| 850 |
+
return modelName.includes('Llama') || modelName.includes('Other');
|
| 851 |
+
}
|
| 852 |
+
return true;
|
| 853 |
+
});
|
| 854 |
+
|
| 855 |
+
const editTimes = filteredEditTimes.flatMap(([, times]) => times);
|
| 856 |
+
if (editTimes.length === 0) return 'No data available';
|
| 857 |
+
return formatEditTime(getMedianEditTime(editTimes));
|
| 858 |
+
}, [data, formatEditTime, getMedianEditTime]);
|
| 859 |
+
|
| 860 |
+
const getImageTypePercentageModified = useCallback(() => {
|
| 861 |
+
if (!data) return 'No data available';
|
| 862 |
+
const total = data.totalCaptions || 0;
|
| 863 |
+
const modified = data.percentageModified || 0;
|
| 864 |
+
|
| 865 |
+
return total > 0 ? Math.round((modified / total) * 100) : 0;
|
| 866 |
+
}, [data]);
|
| 867 |
+
|
| 868 |
+
const getImageTypeDeleteRate = useCallback(() => {
|
| 869 |
+
if (!data) return 'No data available';
|
| 870 |
+
// For now, we'll return the global delete rate since we don't have image_type filtering in the backend
|
| 871 |
+
// In a real implementation, you'd calculate this based on filtered data
|
| 872 |
+
return data.deleteRate >= 0 ? `${data.deleteRate}%` : 'No data available';
|
| 873 |
+
}, [data]);
|
| 874 |
+
|
| 875 |
+
const getImageTypeEditTimeTableData = useCallback((imageType: string) => {
|
| 876 |
+
if (!data) return [];
|
| 877 |
+
// Filter edit time data by image type
|
| 878 |
+
const filteredData = editTimeTableData.filter(d => {
|
| 879 |
+
if (imageType === 'crisis_map') {
|
| 880 |
+
return d.name.includes('GPT') || d.name.includes('Claude') || d.name.includes('Gemini') || d.name.includes('STUB');
|
| 881 |
+
} else if (imageType === 'drone_image') {
|
| 882 |
+
return d.name.includes('Llama') || d.name.includes('Other');
|
| 883 |
+
}
|
| 884 |
+
return true;
|
| 885 |
+
});
|
| 886 |
+
return filteredData;
|
| 887 |
+
}, [data, editTimeTableData]);
|
| 888 |
+
|
| 889 |
+
const getImageTypePercentageTableData = useCallback((imageType: string) => {
|
| 890 |
+
if (!data) return [];
|
| 891 |
+
// Filter percentage data by image type
|
| 892 |
+
const filteredData = percentageModifiedTableData.filter(d => {
|
| 893 |
+
if (imageType === 'crisis_map') {
|
| 894 |
+
return d.name.includes('GPT') || d.name.includes('Claude') || d.name.includes('Gemini') || d.name.includes('STUB');
|
| 895 |
+
} else if (imageType === 'drone_image') {
|
| 896 |
+
return d.name.includes('Llama') || d.name.includes('Other');
|
| 897 |
+
}
|
| 898 |
+
return true;
|
| 899 |
+
});
|
| 900 |
+
return filteredData;
|
| 901 |
+
}, [data, percentageModifiedTableData]);
|
| 902 |
+
|
| 903 |
+
const getImageTypeModelsTableData = useCallback((imageType: string) => {
|
| 904 |
+
if (!data) return [];
|
| 905 |
+
|
| 906 |
+
// Get the appropriate image set based on type
|
| 907 |
+
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
|
| 908 |
+
|
| 909 |
+
// Calculate models for this specific image type
|
| 910 |
+
const modelStats: { [key: string]: { count: number; totalAccuracy: number; totalContext: number; totalUsability: number } } = {};
|
| 911 |
+
|
| 912 |
+
images.forEach((map: MapData) => {
|
| 913 |
+
if (map.model) {
|
| 914 |
+
if (!modelStats[map.model]) {
|
| 915 |
+
modelStats[map.model] = { count: 0, totalAccuracy: 0, totalContext: 0, totalUsability: 0 };
|
| 916 |
+
}
|
| 917 |
+
modelStats[map.model].count++;
|
| 918 |
+
if (map.accuracy != null) modelStats[map.model].totalAccuracy += map.accuracy;
|
| 919 |
+
if (map.context != null) modelStats[map.model].totalContext += map.context;
|
| 920 |
+
if (map.usability != null) modelStats[map.model].totalUsability += map.usability;
|
| 921 |
+
}
|
| 922 |
+
});
|
| 923 |
+
|
| 924 |
+
// Convert to table data format
|
| 925 |
+
return Object.entries(modelStats)
|
| 926 |
+
.map(([modelName, stats], index) => ({
|
| 927 |
+
id: index + 1,
|
| 928 |
+
name: modelName,
|
| 929 |
+
count: stats.count,
|
| 930 |
+
accuracy: stats.count > 0 ? Math.round(stats.totalAccuracy / stats.count) : 0,
|
| 931 |
+
context: stats.count > 0 ? Math.round(stats.totalContext / stats.count) : 0,
|
| 932 |
+
usability: stats.count > 0 ? Math.round(stats.totalUsability / stats.count) : 0,
|
| 933 |
+
totalScore: stats.count > 0 ? Math.round((stats.totalAccuracy + stats.totalContext + stats.totalUsability) / (3 * stats.count)) : 0
|
| 934 |
+
}))
|
| 935 |
+
.sort((a, b) => b.totalScore - a.totalScore);
|
| 936 |
+
}, [data]);
|
| 937 |
+
|
| 938 |
+
const getImageTypeQualityBySourceTableData = useCallback((imageType: string) => {
|
| 939 |
+
if (!data) return [];
|
| 940 |
+
|
| 941 |
+
// Get the appropriate image set based on type
|
| 942 |
+
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
|
| 943 |
+
|
| 944 |
+
// Calculate quality by source for this specific image type
|
| 945 |
+
const sourceQuality: { [key: string]: { total: number; count: number; totalImages: number } } = {};
|
| 946 |
+
|
| 947 |
+
images.forEach((map: MapData) => {
|
| 948 |
+
if (map.source) {
|
| 949 |
+
if (!sourceQuality[map.source]) {
|
| 950 |
+
sourceQuality[map.source] = { total: 0, count: 0, totalImages: 0 };
|
| 951 |
+
}
|
| 952 |
+
sourceQuality[map.source].totalImages += 1;
|
| 953 |
+
if (map.accuracy != null) {
|
| 954 |
+
sourceQuality[map.source].total += map.accuracy;
|
| 955 |
+
sourceQuality[map.source].count += 1;
|
| 956 |
+
}
|
| 957 |
+
}
|
| 958 |
+
});
|
| 959 |
+
|
| 960 |
+
// Convert to table data format
|
| 961 |
+
return Object.entries(sourceQuality).map(([source, stats], index) => ({
|
| 962 |
+
id: index + 1,
|
| 963 |
+
source: getSourceLabel(source),
|
| 964 |
+
avgQuality: stats.count > 0 ? Math.round(stats.total / stats.count) : 0,
|
| 965 |
+
count: stats.totalImages
|
| 966 |
+
}));
|
| 967 |
+
}, [data, getSourceLabel]);
|
| 968 |
+
|
| 969 |
+
const getImageTypeModelConsistencyTableData = useCallback((imageType: string) => {
|
| 970 |
+
if (!data) return [];
|
| 971 |
+
// Filter model consistency table data by image type
|
| 972 |
+
const filteredData = modelConsistencyData.filter(d => {
|
| 973 |
+
if (imageType === 'crisis_map') {
|
| 974 |
+
return d.name.includes('GPT') || d.name.includes('Claude') || d.name.includes('Gemini') || d.name.includes('STUB');
|
| 975 |
+
} else if (imageType === 'drone_image') {
|
| 976 |
+
return d.name.includes('Llama') || d.name.includes('Other');
|
| 977 |
+
}
|
| 978 |
+
return true;
|
| 979 |
+
});
|
| 980 |
+
return filteredData;
|
| 981 |
+
}, [data, modelConsistencyData]);
|
| 982 |
+
|
| 983 |
if (loading) {
|
| 984 |
return (
|
| 985 |
<PageContainer>
|
|
|
|
| 1000 |
);
|
| 1001 |
}
|
| 1002 |
|
| 1003 |
+
|
|
|
|
|
|
|
| 1004 |
|
| 1005 |
const ifrcColors = [
|
| 1006 |
'#F5333F', '#F64752', '#F75C65', '#F87079', '#F9858C', '#FA999F', '#FBADB2', '#FCC2C5'
|
|
|
|
| 1014 |
name="analytics-view"
|
| 1015 |
value={view}
|
| 1016 |
onChange={(value) => {
|
| 1017 |
+
if (value === 'crisis_maps' || value === 'drone_images') {
|
| 1018 |
setView(value);
|
| 1019 |
}
|
| 1020 |
}}
|
|
|
|
| 1024 |
/>
|
| 1025 |
</div>
|
| 1026 |
|
| 1027 |
+
{view === 'crisis_maps' ? (
|
| 1028 |
<div className={styles.chartGrid}>
|
| 1029 |
<Container heading="Summary Statistics" headingLevel={3} withHeaderBorder withInternalPadding>
|
| 1030 |
+
<div className={styles.summaryStatsCards}>
|
| 1031 |
+
<div className={styles.summaryStatsCard}>
|
| 1032 |
+
<div className={styles.summaryStatsCardValue}>
|
| 1033 |
+
{getImageTypeCount('crisis_map')}
|
| 1034 |
+
</div>
|
| 1035 |
+
<div className={styles.summaryStatsCardLabel}>Total Crisis Maps</div>
|
| 1036 |
+
</div>
|
| 1037 |
+
<div className={styles.summaryStatsCard}>
|
| 1038 |
+
<div className={styles.summaryStatsCardValue}>
|
| 1039 |
+
2000
|
| 1040 |
+
</div>
|
| 1041 |
+
<div className={styles.summaryStatsCardLabel}>Target Amount</div>
|
| 1042 |
+
</div>
|
| 1043 |
</div>
|
| 1044 |
<div className={styles.progressSection}>
|
| 1045 |
<div className={styles.progressLabel}>
|
| 1046 |
<span>Progress towards target</span>
|
| 1047 |
+
<span>{Math.round((getImageTypeCount('crisis_map') / 2000) * 100)}%</span>
|
| 1048 |
</div>
|
| 1049 |
+
<ProgressBar value={getImageTypeCount('crisis_map')} totalValue={2000} />
|
| 1050 |
</div>
|
| 1051 |
</Container>
|
| 1052 |
|
|
|
|
| 1054 |
<div className={styles.chartSection}>
|
| 1055 |
<div className={styles.chartContainer}>
|
| 1056 |
<PieChart
|
| 1057 |
+
data={getImageTypeRegionsChartData('crisis_map')}
|
| 1058 |
valueSelector={d => d.value}
|
| 1059 |
labelSelector={d => d.name}
|
| 1060 |
keySelector={d => d.name}
|
|
|
|
| 1064 |
</div>
|
| 1065 |
<div className={styles.tableContainer}>
|
| 1066 |
<Table
|
| 1067 |
+
data={getImageTypeRegionsTableData('crisis_map')}
|
| 1068 |
columns={regionsColumns}
|
| 1069 |
keySelector={numericIdSelector}
|
| 1070 |
filtered={false}
|
|
|
|
| 1078 |
<div className={styles.chartSection}>
|
| 1079 |
<div className={styles.chartContainer}>
|
| 1080 |
<PieChart
|
| 1081 |
+
data={getImageTypeSourcesChartData('crisis_map')}
|
| 1082 |
valueSelector={d => d.value}
|
| 1083 |
labelSelector={d => d.name}
|
| 1084 |
keySelector={d => d.name}
|
|
|
|
| 1088 |
</div>
|
| 1089 |
<div className={styles.tableContainer}>
|
| 1090 |
<Table
|
| 1091 |
+
data={getImageTypeSourcesTableData('crisis_map')}
|
| 1092 |
columns={sourcesColumns}
|
| 1093 |
keySelector={numericIdSelector}
|
| 1094 |
filtered={false}
|
|
|
|
| 1102 |
<div className={styles.chartSection}>
|
| 1103 |
<div className={styles.chartContainer}>
|
| 1104 |
<PieChart
|
| 1105 |
+
data={getImageTypeTypesChartData('crisis_map')}
|
| 1106 |
valueSelector={d => d.value}
|
| 1107 |
labelSelector={d => d.name}
|
| 1108 |
keySelector={d => d.name}
|
|
|
|
| 1112 |
</div>
|
| 1113 |
<div className={styles.tableContainer}>
|
| 1114 |
<Table
|
| 1115 |
+
data={getImageTypeTypesTableData('crisis_map')}
|
| 1116 |
columns={typesColumns}
|
| 1117 |
keySelector={numericIdSelector}
|
| 1118 |
filtered={false}
|
|
|
|
| 1120 |
/>
|
| 1121 |
</div>
|
| 1122 |
</div>
|
| 1123 |
+
</Container>
|
| 1124 |
+
|
| 1125 |
+
{/* New Analytics Containers */}
|
| 1126 |
+
<Container heading="User Interaction Statistics" headingLevel={3} withHeaderBorder withInternalPadding>
|
| 1127 |
+
<div className={styles.userInteractionCards}>
|
| 1128 |
+
{/* Median Edit Time Card */}
|
| 1129 |
+
<div className={styles.userInteractionCard}>
|
| 1130 |
+
<div className={styles.userInteractionCardValue}>
|
| 1131 |
+
{getImageTypeMedianEditTime('crisis_map')}
|
| 1132 |
+
</div>
|
| 1133 |
+
<div className={styles.userInteractionCardLabel}>Median Edit Time</div>
|
| 1134 |
+
<Button
|
| 1135 |
+
name="view-edit-time-details"
|
| 1136 |
+
variant="secondary"
|
| 1137 |
+
onClick={() => setShowEditTimeModal(!showEditTimeModal)}
|
| 1138 |
+
className={styles.userInteractionCardButton}
|
| 1139 |
+
>
|
| 1140 |
+
{showEditTimeModal ? 'Hide Details' : 'View Details'}
|
| 1141 |
+
</Button>
|
| 1142 |
+
</div>
|
| 1143 |
+
|
| 1144 |
+
{/* Median % Modified Card */}
|
| 1145 |
+
<div className={styles.userInteractionCard}>
|
| 1146 |
+
<div className={styles.userInteractionCardValue}>
|
| 1147 |
+
{getImageTypePercentageModified()}
|
| 1148 |
+
</div>
|
| 1149 |
+
<div className={styles.userInteractionCardLabel}>Median % Modified</div>
|
| 1150 |
+
<Button
|
| 1151 |
+
name="view-percentage-details"
|
| 1152 |
+
variant="secondary"
|
| 1153 |
+
onClick={() => setShowPercentageModal(!showPercentageModal)}
|
| 1154 |
+
className={styles.userInteractionCardButton}
|
| 1155 |
+
>
|
| 1156 |
+
{showPercentageModal ? 'Hide Details' : 'View Details'}
|
| 1157 |
+
</Button>
|
| 1158 |
+
</div>
|
| 1159 |
+
|
| 1160 |
+
{/* Delete Rate Card */}
|
| 1161 |
+
<div className={styles.userInteractionCard}>
|
| 1162 |
+
<div className={styles.userInteractionCardValue}>
|
| 1163 |
+
{getImageTypeDeleteRate()}
|
| 1164 |
+
</div>
|
| 1165 |
+
<div className={styles.userInteractionCardLabel}>Delete Rate</div>
|
| 1166 |
+
<Button
|
| 1167 |
+
name="view-delete-details"
|
| 1168 |
+
variant="secondary"
|
| 1169 |
+
onClick={() => setShowDeleteModal(!showDeleteModal)}
|
| 1170 |
+
className={styles.userInteractionCardButton}
|
| 1171 |
+
>
|
| 1172 |
+
{showDeleteModal ? 'Hide Details' : 'View Details'}
|
| 1173 |
+
</Button>
|
| 1174 |
+
</div>
|
| 1175 |
+
</div>
|
| 1176 |
+
|
| 1177 |
+
{/* Edit Time Details Table */}
|
| 1178 |
+
{showEditTimeModal && (
|
| 1179 |
+
<div className={styles.modelPerformance}>
|
| 1180 |
+
<Table
|
| 1181 |
+
data={getImageTypeEditTimeTableData('crisis_map')}
|
| 1182 |
+
columns={editTimeColumns}
|
| 1183 |
+
keySelector={numericIdSelector}
|
| 1184 |
+
filtered={false}
|
| 1185 |
+
pending={false}
|
| 1186 |
+
/>
|
| 1187 |
+
</div>
|
| 1188 |
+
)}
|
| 1189 |
+
|
| 1190 |
+
{/* Percentage Modified Details Table */}
|
| 1191 |
+
{showPercentageModal && (
|
| 1192 |
+
<div className={styles.modelPerformance}>
|
| 1193 |
+
<Table
|
| 1194 |
+
data={getImageTypePercentageTableData('crisis_map')}
|
| 1195 |
+
columns={percentageModifiedColumns}
|
| 1196 |
+
keySelector={numericIdSelector}
|
| 1197 |
+
filtered={false}
|
| 1198 |
+
pending={false}
|
| 1199 |
+
/>
|
| 1200 |
+
</div>
|
| 1201 |
+
)}
|
| 1202 |
+
</Container>
|
| 1203 |
+
|
| 1204 |
+
<Container heading="Model Performance" headingLevel={3} withHeaderBorder withInternalPadding>
|
| 1205 |
+
<div className={styles.modelPerformance}>
|
| 1206 |
+
<Table
|
| 1207 |
+
data={getImageTypeModelsTableData('crisis_map')}
|
| 1208 |
+
columns={modelsColumns}
|
| 1209 |
+
keySelector={numericIdSelector}
|
| 1210 |
+
filtered={false}
|
| 1211 |
+
pending={false}
|
| 1212 |
+
/>
|
| 1213 |
+
</div>
|
| 1214 |
+
</Container>
|
| 1215 |
+
|
| 1216 |
+
|
| 1217 |
+
|
| 1218 |
+
<Container heading="Quality-Source Correlation" headingLevel={3} withHeaderBorder withInternalPadding>
|
| 1219 |
+
<div className={styles.tableContainer}>
|
| 1220 |
+
<Table
|
| 1221 |
+
data={getImageTypeQualityBySourceTableData('crisis_map')}
|
| 1222 |
+
columns={qualityBySourceColumns}
|
| 1223 |
+
keySelector={numericIdSelector}
|
| 1224 |
+
filtered={false}
|
| 1225 |
+
pending={false}
|
| 1226 |
+
/>
|
| 1227 |
+
</div>
|
| 1228 |
+
</Container>
|
| 1229 |
+
|
| 1230 |
+
<Container heading="Model Consistency Analysis" headingLevel={3} withHeaderBorder withInternalPadding>
|
| 1231 |
+
<div className={styles.tableContainer}>
|
| 1232 |
+
<Table
|
| 1233 |
+
data={getImageTypeModelConsistencyTableData('crisis_map')}
|
| 1234 |
+
columns={modelConsistencyColumns}
|
| 1235 |
+
keySelector={numericIdSelector}
|
| 1236 |
+
filtered={false}
|
| 1237 |
+
pending={false}
|
| 1238 |
+
/>
|
| 1239 |
+
</div>
|
| 1240 |
</Container>
|
| 1241 |
</div>
|
| 1242 |
) : (
|
| 1243 |
<div className={styles.chartGrid}>
|
| 1244 |
+
<Container heading="Summary Statistics" headingLevel={3} withHeaderBorder withInternalPadding>
|
| 1245 |
+
<div className={styles.summaryStatsCards}>
|
| 1246 |
+
<div className={styles.summaryStatsCard}>
|
| 1247 |
+
<div className={styles.summaryStatsCardValue}>
|
| 1248 |
+
{getImageTypeCount('drone_image')}
|
| 1249 |
+
</div>
|
| 1250 |
+
<div className={styles.summaryStatsCardLabel}>Total Drone Images</div>
|
| 1251 |
+
</div>
|
| 1252 |
+
<div className={styles.summaryStatsCard}>
|
| 1253 |
+
<div className={styles.summaryStatsCardValue}>
|
| 1254 |
+
2000
|
| 1255 |
+
</div>
|
| 1256 |
+
<div className={styles.summaryStatsCardLabel}>Target Amount</div>
|
| 1257 |
+
</div>
|
| 1258 |
+
</div>
|
| 1259 |
+
<div className={styles.progressSection}>
|
| 1260 |
+
<div className={styles.progressLabel}>
|
| 1261 |
+
<span>Progress towards target</span>
|
| 1262 |
+
<span>{Math.round((getImageTypeCount('drone_image') / 2000) * 100)}%</span>
|
| 1263 |
+
</div>
|
| 1264 |
+
<ProgressBar value={getImageTypeCount('drone_image')} totalValue={2000} />
|
| 1265 |
+
</div>
|
| 1266 |
+
</Container>
|
| 1267 |
+
|
| 1268 |
+
<Container heading="Regions Distribution" headingLevel={3} withHeaderBorder withInternalPadding>
|
| 1269 |
+
<div className={styles.chartSection}>
|
| 1270 |
+
<div className={styles.chartContainer}>
|
| 1271 |
+
<PieChart
|
| 1272 |
+
data={getImageTypeRegionsChartData('drone_image')}
|
| 1273 |
+
valueSelector={d => d.value}
|
| 1274 |
+
labelSelector={d => d.name}
|
| 1275 |
+
keySelector={d => d.name}
|
| 1276 |
+
colors={ifrcColors}
|
| 1277 |
+
showPercentageInLegend
|
| 1278 |
+
/>
|
| 1279 |
+
</div>
|
| 1280 |
+
<div className={styles.tableContainer}>
|
| 1281 |
+
<Table
|
| 1282 |
+
data={getImageTypeRegionsTableData('drone_image')}
|
| 1283 |
+
columns={regionsColumns}
|
| 1284 |
+
keySelector={numericIdSelector}
|
| 1285 |
+
filtered={false}
|
| 1286 |
+
pending={false}
|
| 1287 |
+
/>
|
| 1288 |
+
</div>
|
| 1289 |
+
</div>
|
| 1290 |
+
</Container>
|
| 1291 |
+
|
| 1292 |
+
|
| 1293 |
+
|
| 1294 |
+
<Container heading="Types Distribution" headingLevel={3} withHeaderBorder withInternalPadding>
|
| 1295 |
+
<div className={styles.chartSection}>
|
| 1296 |
+
<div className={styles.chartContainer}>
|
| 1297 |
+
<PieChart
|
| 1298 |
+
data={getImageTypeTypesChartData('drone_image')}
|
| 1299 |
+
valueSelector={d => d.value}
|
| 1300 |
+
labelSelector={d => d.name}
|
| 1301 |
+
keySelector={d => d.name}
|
| 1302 |
+
colors={ifrcColors}
|
| 1303 |
+
showPercentageInLegend
|
| 1304 |
+
/>
|
| 1305 |
+
</div>
|
| 1306 |
+
<div className={styles.tableContainer}>
|
| 1307 |
+
<Table
|
| 1308 |
+
data={getImageTypeTypesTableData('drone_image')}
|
| 1309 |
+
columns={typesColumns}
|
| 1310 |
+
keySelector={numericIdSelector}
|
| 1311 |
+
filtered={false}
|
| 1312 |
+
pending={false}
|
| 1313 |
+
/>
|
| 1314 |
+
</div>
|
| 1315 |
+
</div>
|
| 1316 |
+
</Container>
|
| 1317 |
+
|
| 1318 |
+
{/* User Interaction Statistics Box */}
|
| 1319 |
+
<Container heading="User Interaction Statistics" headingLevel={3} withHeaderBorder withInternalPadding>
|
| 1320 |
+
<div className={styles.userInteractionCards}>
|
| 1321 |
+
{/* Median Edit Time Card */}
|
| 1322 |
+
<div className={styles.userInteractionCard}>
|
| 1323 |
+
<div className={styles.userInteractionCardValue}>
|
| 1324 |
+
{getImageTypeMedianEditTime('drone_image')}
|
| 1325 |
+
</div>
|
| 1326 |
+
<div className={styles.userInteractionCardLabel}>Median Edit Time</div>
|
| 1327 |
+
<Button
|
| 1328 |
+
name="view-edit-time-details"
|
| 1329 |
+
variant="secondary"
|
| 1330 |
+
onClick={() => setShowEditTimeModal(!showEditTimeModal)}
|
| 1331 |
+
className={styles.userInteractionCardButton}
|
| 1332 |
+
>
|
| 1333 |
+
{showEditTimeModal ? 'Hide Details' : 'View Details'}
|
| 1334 |
+
</Button>
|
| 1335 |
+
</div>
|
| 1336 |
+
|
| 1337 |
+
{/* Median % Modified Card */}
|
| 1338 |
+
<div className={styles.userInteractionCard}>
|
| 1339 |
+
<div className={styles.userInteractionCardValue}>
|
| 1340 |
+
{getImageTypePercentageModified()}
|
| 1341 |
+
</div>
|
| 1342 |
+
<div className={styles.userInteractionCardLabel}>Median % Modified</div>
|
| 1343 |
+
<Button
|
| 1344 |
+
name="view-percentage-details"
|
| 1345 |
+
variant="secondary"
|
| 1346 |
+
onClick={() => setShowPercentageModal(!showPercentageModal)}
|
| 1347 |
+
className={styles.userInteractionCardButton}
|
| 1348 |
+
>
|
| 1349 |
+
{showPercentageModal ? 'Hide Details' : 'View Details'}
|
| 1350 |
+
</Button>
|
| 1351 |
+
</div>
|
| 1352 |
+
|
| 1353 |
+
{/* Delete Rate Card */}
|
| 1354 |
+
<div className={styles.userInteractionCard}>
|
| 1355 |
+
<div className={styles.userInteractionCardValue}>
|
| 1356 |
+
{getImageTypeDeleteRate()}
|
| 1357 |
+
</div>
|
| 1358 |
+
<div className={styles.userInteractionCardLabel}>Delete Rate</div>
|
| 1359 |
+
<Button
|
| 1360 |
+
name="view-delete-details"
|
| 1361 |
+
variant="secondary"
|
| 1362 |
+
onClick={() => setShowDeleteModal(!showDeleteModal)}
|
| 1363 |
+
className={styles.userInteractionCardButton}
|
| 1364 |
+
>
|
| 1365 |
+
{showDeleteModal ? 'Hide Details' : 'View Details'}
|
| 1366 |
+
</Button>
|
| 1367 |
+
</div>
|
| 1368 |
+
</div>
|
| 1369 |
+
|
| 1370 |
+
{/* Edit Time Details Table */}
|
| 1371 |
+
{showEditTimeModal && (
|
| 1372 |
+
<div className={styles.modelPerformance}>
|
| 1373 |
+
<Table
|
| 1374 |
+
data={getImageTypeEditTimeTableData('drone_image')}
|
| 1375 |
+
columns={editTimeColumns}
|
| 1376 |
+
keySelector={numericIdSelector}
|
| 1377 |
+
filtered={false}
|
| 1378 |
+
pending={false}
|
| 1379 |
+
/>
|
| 1380 |
+
</div>
|
| 1381 |
+
)}
|
| 1382 |
+
|
| 1383 |
+
{/* Percentage Modified Details Table */}
|
| 1384 |
+
{showPercentageModal && (
|
| 1385 |
+
<div className={styles.modelPerformance}>
|
| 1386 |
+
<Table
|
| 1387 |
+
data={getImageTypePercentageTableData('drone_image')}
|
| 1388 |
+
columns={percentageModifiedColumns}
|
| 1389 |
+
keySelector={numericIdSelector}
|
| 1390 |
+
filtered={false}
|
| 1391 |
+
pending={false}
|
| 1392 |
+
/>
|
| 1393 |
+
</div>
|
| 1394 |
+
)}
|
| 1395 |
+
</Container>
|
| 1396 |
+
|
| 1397 |
<Container heading="Model Performance" headingLevel={3} withHeaderBorder withInternalPadding>
|
| 1398 |
<div className={styles.modelPerformance}>
|
| 1399 |
<Table
|
| 1400 |
+
data={getImageTypeModelsTableData('drone_image')}
|
| 1401 |
columns={modelsColumns}
|
| 1402 |
keySelector={numericIdSelector}
|
| 1403 |
filtered={false}
|
|
|
|
| 1405 |
/>
|
| 1406 |
</div>
|
| 1407 |
</Container>
|
| 1408 |
+
|
| 1409 |
+
|
| 1410 |
+
|
| 1411 |
+
<Container heading="Quality-Source Correlation" headingLevel={3} withHeaderBorder withInternalPadding>
|
| 1412 |
+
<div className={styles.tableContainer}>
|
| 1413 |
+
<Table
|
| 1414 |
+
data={getImageTypeQualityBySourceTableData('drone_image')}
|
| 1415 |
+
columns={qualityBySourceColumns}
|
| 1416 |
+
keySelector={numericIdSelector}
|
| 1417 |
+
filtered={false}
|
| 1418 |
+
pending={false}
|
| 1419 |
+
/>
|
| 1420 |
+
</div>
|
| 1421 |
+
</Container>
|
| 1422 |
+
|
| 1423 |
+
|
| 1424 |
+
|
| 1425 |
+
<Container heading="Model Consistency Analysis" headingLevel={3} withHeaderBorder withInternalPadding>
|
| 1426 |
+
<div className={styles.tableContainer}>
|
| 1427 |
+
<Table
|
| 1428 |
+
data={getImageTypeModelConsistencyTableData('drone_image')}
|
| 1429 |
+
columns={modelConsistencyColumns}
|
| 1430 |
+
keySelector={numericIdSelector}
|
| 1431 |
+
filtered={false}
|
| 1432 |
+
pending={false}
|
| 1433 |
+
/>
|
| 1434 |
+
</div>
|
| 1435 |
+
</Container>
|
| 1436 |
</div>
|
| 1437 |
)}
|
| 1438 |
</div>
|
| 1439 |
+
|
| 1440 |
+
|
| 1441 |
</PageContainer>
|
| 1442 |
);
|
| 1443 |
}
|
frontend/src/pages/ExplorePage/ExplorePage.module.css
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
.tabSelector {
|
| 2 |
margin-bottom: var(--go-ui-spacing-lg);
|
| 3 |
display: flex;
|
| 4 |
-
justify-content:
|
|
|
|
| 5 |
}
|
| 6 |
|
| 7 |
.metadataTags {
|
|
@@ -128,3 +129,205 @@
|
|
| 128 |
padding: var(--go-ui-spacing-2xs) var(--go-ui-spacing-xs);
|
| 129 |
}
|
| 130 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
.tabSelector {
|
| 2 |
margin-bottom: var(--go-ui-spacing-lg);
|
| 3 |
display: flex;
|
| 4 |
+
justify-content: space-between;
|
| 5 |
+
align-items: center;
|
| 6 |
}
|
| 7 |
|
| 8 |
.metadataTags {
|
|
|
|
| 129 |
padding: var(--go-ui-spacing-2xs) var(--go-ui-spacing-xs);
|
| 130 |
}
|
| 131 |
}
|
| 132 |
+
|
| 133 |
+
/* Modal styles for export selection */
|
| 134 |
+
.fullSizeModalOverlay {
|
| 135 |
+
position: fixed;
|
| 136 |
+
top: 0;
|
| 137 |
+
left: 0;
|
| 138 |
+
right: 0;
|
| 139 |
+
bottom: 0;
|
| 140 |
+
background-color: rgba(0, 0, 0, 0.8);
|
| 141 |
+
display: flex;
|
| 142 |
+
justify-content: center;
|
| 143 |
+
align-items: center;
|
| 144 |
+
z-index: 1000;
|
| 145 |
+
padding: var(--go-ui-spacing-lg);
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.fullSizeModalContent {
|
| 149 |
+
background-color: var(--go-ui-color-white);
|
| 150 |
+
border-radius: var(--go-ui-border-radius-lg);
|
| 151 |
+
max-width: 95vw;
|
| 152 |
+
max-height: 95vh;
|
| 153 |
+
overflow: hidden;
|
| 154 |
+
box-shadow: var(--go-ui-box-shadow-xl);
|
| 155 |
+
display: flex;
|
| 156 |
+
flex-direction: column;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.ratingWarningContent {
|
| 160 |
+
padding: var(--go-ui-spacing-xl);
|
| 161 |
+
text-align: center;
|
| 162 |
+
max-width: 500px;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.ratingWarningTitle {
|
| 166 |
+
font-size: var(--go-ui-font-size-lg);
|
| 167 |
+
font-weight: var(--go-ui-font-weight-semibold);
|
| 168 |
+
color: var(--go-ui-color-gray-900);
|
| 169 |
+
margin-bottom: var(--go-ui-spacing-md);
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.ratingWarningText {
|
| 173 |
+
font-size: var(--go-ui-font-size-md);
|
| 174 |
+
color: var(--go-ui-color-gray-700);
|
| 175 |
+
line-height: 1.6;
|
| 176 |
+
margin-bottom: var(--go-ui-spacing-xl);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.ratingWarningButtons {
|
| 180 |
+
display: flex;
|
| 181 |
+
gap: var(--go-ui-spacing-md);
|
| 182 |
+
justify-content: center;
|
| 183 |
+
flex-wrap: wrap;
|
| 184 |
+
margin-top: var(--go-ui-spacing-xl);
|
| 185 |
+
text-align: center;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
/* Filter status step styles */
|
| 189 |
+
.filterStatusContainer {
|
| 190 |
+
margin-bottom: var(--go-ui-spacing-lg);
|
| 191 |
+
text-align: center;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.filterStatusMessage {
|
| 195 |
+
font-size: var(--go-ui-font-size-md);
|
| 196 |
+
font-weight: var(--go-ui-font-weight-medium);
|
| 197 |
+
color: var(--go-ui-color-gray-700);
|
| 198 |
+
margin-bottom: var(--go-ui-spacing-md);
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
.filterStatusCount {
|
| 202 |
+
font-size: var(--go-ui-font-size-lg);
|
| 203 |
+
font-weight: var(--go-ui-font-weight-semibold);
|
| 204 |
+
color: var(--go-ui-color-gray-900);
|
| 205 |
+
margin-bottom: var(--go-ui-spacing-md);
|
| 206 |
+
display: block;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.activeFiltersList {
|
| 210 |
+
display: flex;
|
| 211 |
+
flex-wrap: wrap;
|
| 212 |
+
gap: var(--go-ui-spacing-sm);
|
| 213 |
+
justify-content: center;
|
| 214 |
+
margin-bottom: var(--go-ui-spacing-lg);
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.activeFilter {
|
| 218 |
+
background-color: var(--go-ui-color-gray-100);
|
| 219 |
+
color: var(--go-ui-color-gray-700);
|
| 220 |
+
padding: var(--go-ui-spacing-xs) var(--go-ui-spacing-sm);
|
| 221 |
+
border-radius: var(--go-ui-border-radius-sm);
|
| 222 |
+
font-size: var(--go-ui-font-size-sm);
|
| 223 |
+
border: var(--go-ui-width-separator-thin) solid var(--go-ui-color-gray-200);
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
.filterStatusActions {
|
| 227 |
+
display: flex;
|
| 228 |
+
gap: var(--go-ui-spacing-md);
|
| 229 |
+
justify-content: center;
|
| 230 |
+
flex-wrap: wrap;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
/* Export mode and split configuration styles */
|
| 234 |
+
.exportModeSection {
|
| 235 |
+
margin-bottom: var(--go-ui-spacing-lg);
|
| 236 |
+
text-align: center;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.exportModeLabel {
|
| 240 |
+
font-size: var(--go-ui-font-size-sm);
|
| 241 |
+
font-weight: var(--go-ui-font-weight-medium);
|
| 242 |
+
color: var(--go-ui-color-gray-700);
|
| 243 |
+
margin-bottom: var(--go-ui-spacing-sm);
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.splitConfigSection {
|
| 247 |
+
margin-bottom: var(--go-ui-spacing-lg);
|
| 248 |
+
text-align: center;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.splitConfigTitle {
|
| 252 |
+
font-size: 1rem;
|
| 253 |
+
font-weight: 600;
|
| 254 |
+
margin-bottom: var(--go-ui-spacing-md);
|
| 255 |
+
color: var(--go-ui-color-text-primary);
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.splitInputsContainer {
|
| 259 |
+
display: flex;
|
| 260 |
+
justify-content: center;
|
| 261 |
+
gap: var(--go-ui-spacing-md);
|
| 262 |
+
margin-bottom: var(--go-ui-spacing-md);
|
| 263 |
+
flex-wrap: wrap;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.splitInputGroup {
|
| 267 |
+
display: flex;
|
| 268 |
+
flex-direction: column;
|
| 269 |
+
align-items: center;
|
| 270 |
+
text-align: center;
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
.splitInputLabel {
|
| 274 |
+
margin-bottom: var(--go-ui-spacing-xs);
|
| 275 |
+
font-weight: 500;
|
| 276 |
+
color: var(--go-ui-color-text-primary);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.splitInput {
|
| 280 |
+
width: 80px;
|
| 281 |
+
padding: var(--go-ui-spacing-sm);
|
| 282 |
+
border: 1px solid var(--go-ui-color-border);
|
| 283 |
+
border-radius: var(--go-ui-border-radius);
|
| 284 |
+
font-size: 1rem;
|
| 285 |
+
text-align: center;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.splitInput:focus {
|
| 289 |
+
outline: none;
|
| 290 |
+
border-color: var(--go-ui-color-red-50);
|
| 291 |
+
box-shadow: 0 0 0 2px var(--go-ui-color-red-20);
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
.splitInput:hover {
|
| 295 |
+
border-color: var(--go-ui-color-gray-400);
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.splitTotal {
|
| 299 |
+
font-size: var(--go-ui-font-size-xs);
|
| 300 |
+
color: var(--go-ui-color-gray-500);
|
| 301 |
+
text-align: center;
|
| 302 |
+
margin-top: var(--go-ui-spacing-xs);
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
.splitTotalError {
|
| 306 |
+
color: var(--go-ui-color-negative);
|
| 307 |
+
margin-left: var(--go-ui-spacing-xs);
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.checkboxesContainer {
|
| 311 |
+
text-align: center;
|
| 312 |
+
margin-bottom: var(--go-ui-spacing-lg);
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
/* Responsive adjustments for modal */
|
| 316 |
+
@media (max-width: 768px) {
|
| 317 |
+
.fullSizeModalOverlay {
|
| 318 |
+
padding: var(--go-ui-spacing-sm);
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.fullSizeModalContent {
|
| 322 |
+
max-width: 100vw;
|
| 323 |
+
max-height: 100vh;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
.splitInputsContainer {
|
| 327 |
+
gap: var(--go-ui-spacing-md);
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.splitInput {
|
| 331 |
+
width: 3.5rem;
|
| 332 |
+
}
|
| 333 |
+
}
|
frontend/src/pages/ExplorePage/ExplorePage.tsx
CHANGED
|
@@ -1,9 +1,8 @@
|
|
| 1 |
-
import { PageContainer, TextInput, SelectInput, MultiSelectInput, Container, SegmentInput, Spinner, Button } from '@ifrc-go/ui';
|
| 2 |
import { useState, useEffect, useMemo } from 'react';
|
| 3 |
import { useNavigate } from 'react-router-dom';
|
| 4 |
-
import
|
| 5 |
import { useFilterContext } from '../../contexts/FilterContext';
|
| 6 |
-
import
|
| 7 |
|
| 8 |
interface ImageWithCaptionOut {
|
| 9 |
image_id: string;
|
|
@@ -31,7 +30,6 @@ interface ImageWithCaptionOut {
|
|
| 31 |
|
| 32 |
export default function ExplorePage() {
|
| 33 |
const navigate = useNavigate();
|
| 34 |
-
const { isAuthenticated } = useAdmin();
|
| 35 |
const [view, setView] = useState<'explore' | 'mapDetails'>('explore');
|
| 36 |
const [captions, setCaptions] = useState<ImageWithCaptionOut[]>([]);
|
| 37 |
|
|
@@ -54,6 +52,14 @@ export default function ExplorePage() {
|
|
| 54 |
const [imageTypes, setImageTypes] = useState<{image_type: string, label: string}[]>([]);
|
| 55 |
const [isLoadingFilters, setIsLoadingFilters] = useState(true);
|
| 56 |
const [isLoadingContent, setIsLoadingContent] = useState(true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
const viewOptions = [
|
| 59 |
{ key: 'explore' as const, label: 'List' },
|
|
@@ -163,6 +169,301 @@ export default function ExplorePage() {
|
|
| 163 |
});
|
| 164 |
}, [captions, search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, showReferenceExamples]);
|
| 165 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
|
| 167 |
return (
|
| 168 |
<PageContainer>
|
|
@@ -183,6 +484,23 @@ export default function ExplorePage() {
|
|
| 183 |
keySelector={(o) => o.key}
|
| 184 |
labelSelector={(o) => o.label}
|
| 185 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
</div>
|
| 187 |
|
| 188 |
{view === 'explore' ? (
|
|
@@ -200,26 +518,24 @@ export default function ExplorePage() {
|
|
| 200 |
/>
|
| 201 |
</Container>
|
| 202 |
|
| 203 |
-
{/* Reference Examples Filter -
|
| 204 |
-
|
| 205 |
-
<
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
>
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
</Container>
|
| 222 |
-
)}
|
| 223 |
|
| 224 |
<Container withInternalPadding className="bg-white/20 backdrop-blur-sm rounded-md p-2">
|
| 225 |
<Button
|
|
@@ -347,7 +663,7 @@ export default function ExplorePage() {
|
|
| 347 |
<h3 className={styles.mapItemTitle}>
|
| 348 |
<div className="flex items-center gap-2">
|
| 349 |
<span>{c.title || 'Untitled'}</span>
|
| 350 |
-
{
|
| 351 |
<span className="text-red-500 text-lg" title="Starred image">★</span>
|
| 352 |
)}
|
| 353 |
</div>
|
|
@@ -399,6 +715,244 @@ export default function ExplorePage() {
|
|
| 399 |
</div>
|
| 400 |
)}
|
| 401 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
</PageContainer>
|
| 403 |
);
|
| 404 |
}
|
|
|
|
|
|
|
| 1 |
import { useState, useEffect, useMemo } from 'react';
|
| 2 |
import { useNavigate } from 'react-router-dom';
|
| 3 |
+
import { PageContainer, TextInput, SelectInput, MultiSelectInput, Container, SegmentInput, Spinner, Button, Checkbox } from '@ifrc-go/ui';
|
| 4 |
import { useFilterContext } from '../../contexts/FilterContext';
|
| 5 |
+
import styles from './ExplorePage.module.css';
|
| 6 |
|
| 7 |
interface ImageWithCaptionOut {
|
| 8 |
image_id: string;
|
|
|
|
| 30 |
|
| 31 |
export default function ExplorePage() {
|
| 32 |
const navigate = useNavigate();
|
|
|
|
| 33 |
const [view, setView] = useState<'explore' | 'mapDetails'>('explore');
|
| 34 |
const [captions, setCaptions] = useState<ImageWithCaptionOut[]>([]);
|
| 35 |
|
|
|
|
| 52 |
const [imageTypes, setImageTypes] = useState<{image_type: string, label: string}[]>([]);
|
| 53 |
const [isLoadingFilters, setIsLoadingFilters] = useState(true);
|
| 54 |
const [isLoadingContent, setIsLoadingContent] = useState(true);
|
| 55 |
+
const [showExportModal, setShowExportModal] = useState(false);
|
| 56 |
+
const [exportModalStage, setExportModalStage] = useState<'filters' | 'export'>('filters');
|
| 57 |
+
const [exportMode, setExportMode] = useState<'standard' | 'fine-tuning'>('standard');
|
| 58 |
+
const [trainSplit, setTrainSplit] = useState(80);
|
| 59 |
+
const [testSplit, setTestSplit] = useState(10);
|
| 60 |
+
const [valSplit, setValSplit] = useState(10);
|
| 61 |
+
const [crisisMapsSelected, setCrisisMapsSelected] = useState(true);
|
| 62 |
+
const [droneImagesSelected, setDroneImagesSelected] = useState(true);
|
| 63 |
|
| 64 |
const viewOptions = [
|
| 65 |
{ key: 'explore' as const, label: 'List' },
|
|
|
|
| 169 |
});
|
| 170 |
}, [captions, search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, showReferenceExamples]);
|
| 171 |
|
| 172 |
+
const exportDataset = async (images: ImageWithCaptionOut[], mode: 'standard' | 'fine-tuning' = 'fine-tuning') => {
|
| 173 |
+
if (images.length === 0) {
|
| 174 |
+
alert('No images to export');
|
| 175 |
+
return;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
try {
|
| 179 |
+
// Create a JSZip instance
|
| 180 |
+
const JSZip = (await import('jszip')).default;
|
| 181 |
+
const zip = new JSZip();
|
| 182 |
+
|
| 183 |
+
// Separate images by type
|
| 184 |
+
const crisisMaps = images.filter(img => img.image_type === 'crisis_map');
|
| 185 |
+
const droneImages = images.filter(img => img.image_type === 'drone_image');
|
| 186 |
+
|
| 187 |
+
// Create crisis_maps dataset
|
| 188 |
+
if (crisisMaps.length > 0) {
|
| 189 |
+
const crisisFolder = zip.folder('crisis_maps_dataset');
|
| 190 |
+
const crisisImagesFolder = crisisFolder?.folder('images');
|
| 191 |
+
|
| 192 |
+
if (crisisImagesFolder) {
|
| 193 |
+
// Download crisis map images and add to zip
|
| 194 |
+
const crisisImagePromises = crisisMaps.map(async (image, index) => {
|
| 195 |
+
try {
|
| 196 |
+
const response = await fetch(image.image_url);
|
| 197 |
+
if (!response.ok) throw new Error(`Failed to fetch image ${image.image_id}`);
|
| 198 |
+
|
| 199 |
+
const blob = await response.blob();
|
| 200 |
+
const fileExtension = image.file_key.split('.').pop() || 'jpg';
|
| 201 |
+
const fileName = `${String(index + 1).padStart(4, '0')}.${fileExtension}`;
|
| 202 |
+
|
| 203 |
+
crisisImagesFolder.file(fileName, blob);
|
| 204 |
+
return { success: true, fileName, image };
|
| 205 |
+
} catch (error) {
|
| 206 |
+
console.error(`Failed to process image ${image.image_id}:`, error);
|
| 207 |
+
return { success: false, fileName: '', image };
|
| 208 |
+
}
|
| 209 |
+
});
|
| 210 |
+
|
| 211 |
+
const crisisImageResults = await Promise.all(crisisImagePromises);
|
| 212 |
+
const successfulCrisisImages = crisisImageResults.filter(result => result.success);
|
| 213 |
+
|
| 214 |
+
if (mode === 'fine-tuning') {
|
| 215 |
+
// Create train.jsonl, test.jsonl, and val.jsonl with stratified sampling
|
| 216 |
+
const crisisTrainData: any[] = [];
|
| 217 |
+
const crisisTestData: any[] = [];
|
| 218 |
+
const crisisValData: any[] = [];
|
| 219 |
+
|
| 220 |
+
// Group crisis images by source for stratified sampling
|
| 221 |
+
const crisisImagesBySource = new Map<string, any[]>();
|
| 222 |
+
successfulCrisisImages.forEach(result => {
|
| 223 |
+
const source = result.image.source || 'unknown';
|
| 224 |
+
if (!crisisImagesBySource.has(source)) {
|
| 225 |
+
crisisImagesBySource.set(source, []);
|
| 226 |
+
}
|
| 227 |
+
crisisImagesBySource.get(source)!.push(result);
|
| 228 |
+
});
|
| 229 |
+
|
| 230 |
+
// Distribute images from each source proportionally across splits
|
| 231 |
+
crisisImagesBySource.forEach((images, source) => {
|
| 232 |
+
const totalImages = images.length;
|
| 233 |
+
const trainCount = Math.floor(totalImages * (trainSplit / 100));
|
| 234 |
+
const testCount = Math.floor(totalImages * (testSplit / 100));
|
| 235 |
+
const valCount = totalImages - trainCount - testCount;
|
| 236 |
+
|
| 237 |
+
// Shuffle images within each source group for randomness
|
| 238 |
+
const shuffledImages = [...images].sort(() => Math.random() - 0.5);
|
| 239 |
+
|
| 240 |
+
// Add to train set
|
| 241 |
+
crisisTrainData.push(...shuffledImages.slice(0, trainCount).map(result => ({
|
| 242 |
+
image: `images/${result.fileName}`,
|
| 243 |
+
caption: result.image.edited || result.image.generated || '',
|
| 244 |
+
metadata: {
|
| 245 |
+
image_id: result.image.image_id,
|
| 246 |
+
title: result.image.title,
|
| 247 |
+
source: result.image.source,
|
| 248 |
+
event_type: result.image.event_type,
|
| 249 |
+
image_type: result.image.image_type,
|
| 250 |
+
countries: result.image.countries,
|
| 251 |
+
starred: result.image.starred
|
| 252 |
+
}
|
| 253 |
+
})));
|
| 254 |
+
|
| 255 |
+
// Add to test set
|
| 256 |
+
crisisTestData.push(...shuffledImages.slice(trainCount, trainCount + testCount).map(result => ({
|
| 257 |
+
image: `images/${result.fileName}`,
|
| 258 |
+
caption: result.image.edited || result.image.generated || '',
|
| 259 |
+
metadata: {
|
| 260 |
+
image_id: result.image.image_id,
|
| 261 |
+
title: result.image.title,
|
| 262 |
+
source: result.image.source,
|
| 263 |
+
event_type: result.image.event_type,
|
| 264 |
+
image_type: result.image.image_type,
|
| 265 |
+
countries: result.image.countries,
|
| 266 |
+
starred: result.image.starred
|
| 267 |
+
}
|
| 268 |
+
})));
|
| 269 |
+
|
| 270 |
+
// Add to validation set
|
| 271 |
+
crisisValData.push(...shuffledImages.slice(trainCount + testCount).map(result => ({
|
| 272 |
+
image: `images/${result.fileName}`,
|
| 273 |
+
caption: result.image.edited || result.image.generated || '',
|
| 274 |
+
metadata: {
|
| 275 |
+
image_id: result.image.image_id,
|
| 276 |
+
title: result.image.title,
|
| 277 |
+
source: result.image.source,
|
| 278 |
+
event_type: result.image.event_type,
|
| 279 |
+
image_type: result.image.image_type,
|
| 280 |
+
countries: result.image.countries,
|
| 281 |
+
starred: result.image.starred
|
| 282 |
+
}
|
| 283 |
+
})));
|
| 284 |
+
});
|
| 285 |
+
|
| 286 |
+
// Add JSONL files to crisis folder
|
| 287 |
+
if (crisisFolder) {
|
| 288 |
+
crisisFolder.file('train.jsonl', JSON.stringify(crisisTrainData, null, 2));
|
| 289 |
+
crisisFolder.file('test.jsonl', JSON.stringify(crisisTestData, null, 2));
|
| 290 |
+
crisisFolder.file('val.jsonl', JSON.stringify(crisisValData, null, 2));
|
| 291 |
+
}
|
| 292 |
+
} else {
|
| 293 |
+
// Standard mode: create individual JSON files for each image
|
| 294 |
+
successfulCrisisImages.forEach((result, index) => {
|
| 295 |
+
const jsonData = {
|
| 296 |
+
image: `images/${result.fileName}`,
|
| 297 |
+
caption: result.image.edited || result.image.generated || '',
|
| 298 |
+
metadata: {
|
| 299 |
+
image_id: result.image.image_id,
|
| 300 |
+
title: result.image.title,
|
| 301 |
+
source: result.image.source,
|
| 302 |
+
event_type: result.image.event_type,
|
| 303 |
+
image_type: result.image.image_type,
|
| 304 |
+
countries: result.image.countries,
|
| 305 |
+
starred: result.image.starred
|
| 306 |
+
}
|
| 307 |
+
};
|
| 308 |
+
|
| 309 |
+
if (crisisFolder) {
|
| 310 |
+
crisisFolder.file(`${String(index + 1).padStart(4, '0')}.json`, JSON.stringify(jsonData, null, 2));
|
| 311 |
+
}
|
| 312 |
+
});
|
| 313 |
+
}
|
| 314 |
+
}
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
// Create drone_images dataset
|
| 318 |
+
if (droneImages.length > 0) {
|
| 319 |
+
const droneFolder = zip.folder('drone_images_dataset');
|
| 320 |
+
const droneImagesFolder = droneFolder?.folder('images');
|
| 321 |
+
|
| 322 |
+
if (droneImagesFolder) {
|
| 323 |
+
// Download drone images and add to zip
|
| 324 |
+
const droneImagePromises = droneImages.map(async (image, index) => {
|
| 325 |
+
try {
|
| 326 |
+
const response = await fetch(image.image_url);
|
| 327 |
+
if (!response.ok) throw new Error(`Failed to fetch image ${image.image_id}`);
|
| 328 |
+
|
| 329 |
+
const blob = await response.blob();
|
| 330 |
+
const fileExtension = image.file_key.split('.').pop() || 'jpg';
|
| 331 |
+
const fileName = `${String(index + 1).padStart(4, '0')}.${fileExtension}`;
|
| 332 |
+
|
| 333 |
+
droneImagesFolder.file(fileName, blob);
|
| 334 |
+
return { success: true, fileName, image };
|
| 335 |
+
} catch (error) {
|
| 336 |
+
console.error(`Failed to process image ${image.image_id}:`, error);
|
| 337 |
+
return { success: false, fileName: '', image };
|
| 338 |
+
}
|
| 339 |
+
});
|
| 340 |
+
|
| 341 |
+
const droneImageResults = await Promise.all(droneImagePromises);
|
| 342 |
+
const successfulDroneImages = droneImageResults.filter(result => result.success);
|
| 343 |
+
|
| 344 |
+
if (mode === 'fine-tuning') {
|
| 345 |
+
// Create train.jsonl, test.jsonl, and val.jsonl with stratified sampling
|
| 346 |
+
const droneTrainData: any[] = [];
|
| 347 |
+
const droneTestData: any[] = [];
|
| 348 |
+
const droneValData: any[] = [];
|
| 349 |
+
|
| 350 |
+
// Group drone images by event type for stratified sampling
|
| 351 |
+
const droneImagesByEventType = new Map<string, any[]>();
|
| 352 |
+
successfulDroneImages.forEach(result => {
|
| 353 |
+
const eventType = result.image.event_type || 'unknown';
|
| 354 |
+
if (!droneImagesByEventType.has(eventType)) {
|
| 355 |
+
droneImagesByEventType.set(eventType, []);
|
| 356 |
+
}
|
| 357 |
+
droneImagesByEventType.get(eventType)!.push(result);
|
| 358 |
+
});
|
| 359 |
+
|
| 360 |
+
// Distribute images from each event type proportionally across splits
|
| 361 |
+
droneImagesByEventType.forEach((images, eventType) => {
|
| 362 |
+
const totalImages = images.length;
|
| 363 |
+
const trainCount = Math.floor(totalImages * (trainSplit / 100));
|
| 364 |
+
const testCount = Math.floor(totalImages * (testSplit / 100));
|
| 365 |
+
const valCount = totalImages - trainCount - testCount;
|
| 366 |
+
|
| 367 |
+
// Shuffle images within each event type group for randomness
|
| 368 |
+
const shuffledImages = [...images].sort(() => Math.random() - 0.5);
|
| 369 |
+
|
| 370 |
+
// Add to train set
|
| 371 |
+
droneTrainData.push(...shuffledImages.slice(0, trainCount).map(result => ({
|
| 372 |
+
image: `images/${result.fileName}`,
|
| 373 |
+
caption: result.image.edited || result.image.generated || '',
|
| 374 |
+
metadata: {
|
| 375 |
+
image_id: result.image.image_id,
|
| 376 |
+
title: result.image.title,
|
| 377 |
+
source: result.image.source,
|
| 378 |
+
event_type: result.image.event_type,
|
| 379 |
+
image_type: result.image.image_type,
|
| 380 |
+
countries: result.image.countries,
|
| 381 |
+
starred: result.image.starred
|
| 382 |
+
}
|
| 383 |
+
})));
|
| 384 |
+
|
| 385 |
+
// Add to test set
|
| 386 |
+
droneTestData.push(...shuffledImages.slice(trainCount, trainCount + testCount).map(result => ({
|
| 387 |
+
image: `images/${result.fileName}`,
|
| 388 |
+
caption: result.image.edited || result.image.generated || '',
|
| 389 |
+
metadata: {
|
| 390 |
+
image_id: result.image.image_id,
|
| 391 |
+
title: result.image.title,
|
| 392 |
+
source: result.image.source,
|
| 393 |
+
event_type: result.image.event_type,
|
| 394 |
+
image_type: result.image.image_type,
|
| 395 |
+
countries: result.image.countries,
|
| 396 |
+
starred: result.image.starred
|
| 397 |
+
}
|
| 398 |
+
})));
|
| 399 |
+
|
| 400 |
+
// Add to validation set
|
| 401 |
+
droneValData.push(...shuffledImages.slice(trainCount + testCount).map(result => ({
|
| 402 |
+
image: `images/${result.fileName}`,
|
| 403 |
+
caption: result.image.edited || result.image.generated || '',
|
| 404 |
+
metadata: {
|
| 405 |
+
image_id: result.image.image_id,
|
| 406 |
+
title: result.image.title,
|
| 407 |
+
source: result.image.source,
|
| 408 |
+
event_type: result.image.event_type,
|
| 409 |
+
image_type: result.image.image_type,
|
| 410 |
+
countries: result.image.countries,
|
| 411 |
+
starred: result.image.starred
|
| 412 |
+
}
|
| 413 |
+
})));
|
| 414 |
+
});
|
| 415 |
+
|
| 416 |
+
// Add JSONL files to drone folder
|
| 417 |
+
if (droneFolder) {
|
| 418 |
+
droneFolder.file('train.jsonl', JSON.stringify(droneTrainData, null, 2));
|
| 419 |
+
droneFolder.file('test.jsonl', JSON.stringify(droneTestData, null, 2));
|
| 420 |
+
droneFolder.file('val.jsonl', JSON.stringify(droneValData, null, 2));
|
| 421 |
+
}
|
| 422 |
+
} else {
|
| 423 |
+
// Standard mode: create individual JSON files for each image
|
| 424 |
+
successfulDroneImages.forEach((result, index) => {
|
| 425 |
+
const jsonData = {
|
| 426 |
+
image: `images/${result.fileName}`,
|
| 427 |
+
caption: result.image.edited || result.image.generated || '',
|
| 428 |
+
metadata: {
|
| 429 |
+
image_id: result.image.image_id,
|
| 430 |
+
title: result.image.title,
|
| 431 |
+
source: result.image.source,
|
| 432 |
+
event_type: result.image.event_type,
|
| 433 |
+
image_type: result.image.image_type,
|
| 434 |
+
countries: result.image.countries,
|
| 435 |
+
starred: result.image.starred
|
| 436 |
+
}
|
| 437 |
+
};
|
| 438 |
+
|
| 439 |
+
if (droneFolder) {
|
| 440 |
+
droneFolder.file(`${String(index + 1).padStart(4, '0')}.json`, JSON.stringify(jsonData, null, 2));
|
| 441 |
+
}
|
| 442 |
+
});
|
| 443 |
+
}
|
| 444 |
+
}
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
// Generate and download zip
|
| 448 |
+
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
| 449 |
+
const url = URL.createObjectURL(zipBlob);
|
| 450 |
+
const link = document.createElement('a');
|
| 451 |
+
link.href = url;
|
| 452 |
+
link.download = `datasets_${mode}_${new Date().toISOString().split('T')[0]}.zip`;
|
| 453 |
+
document.body.appendChild(link);
|
| 454 |
+
link.click();
|
| 455 |
+
document.body.removeChild(link);
|
| 456 |
+
URL.revokeObjectURL(url);
|
| 457 |
+
|
| 458 |
+
const totalImages = (crisisMaps.length || 0) + (droneImages.length || 0);
|
| 459 |
+
console.log(`Exported ${mode} datasets with ${totalImages} total images:`);
|
| 460 |
+
if (crisisMaps.length > 0) console.log(`- Crisis maps: ${crisisMaps.length} images`);
|
| 461 |
+
if (droneImages.length > 0) console.log(`- Drone images: ${droneImages.length} images`);
|
| 462 |
+
} catch (error) {
|
| 463 |
+
console.error('Export failed:', error);
|
| 464 |
+
alert('Failed to export dataset. Please try again.');
|
| 465 |
+
}
|
| 466 |
+
};
|
| 467 |
|
| 468 |
return (
|
| 469 |
<PageContainer>
|
|
|
|
| 484 |
keySelector={(o) => o.key}
|
| 485 |
labelSelector={(o) => o.label}
|
| 486 |
/>
|
| 487 |
+
|
| 488 |
+
{/* Export Dataset Button */}
|
| 489 |
+
<Button
|
| 490 |
+
name="export-dataset"
|
| 491 |
+
variant="secondary"
|
| 492 |
+
onClick={() => {
|
| 493 |
+
setShowExportModal(true);
|
| 494 |
+
// Skip to export stage if no filters are applied
|
| 495 |
+
if (search || srcFilter || catFilter || regionFilter || countryFilter || imageTypeFilter || showReferenceExamples) {
|
| 496 |
+
setExportModalStage('filters');
|
| 497 |
+
} else {
|
| 498 |
+
setExportModalStage('export');
|
| 499 |
+
}
|
| 500 |
+
}}
|
| 501 |
+
>
|
| 502 |
+
Export Dataset
|
| 503 |
+
</Button>
|
| 504 |
</div>
|
| 505 |
|
| 506 |
{view === 'explore' ? (
|
|
|
|
| 518 |
/>
|
| 519 |
</Container>
|
| 520 |
|
| 521 |
+
{/* Reference Examples Filter - Available to all users */}
|
| 522 |
+
<Container withInternalPadding className="bg-white/20 backdrop-blur-sm rounded-md p-2">
|
| 523 |
+
<Button
|
| 524 |
+
name="reference-examples"
|
| 525 |
+
variant={showReferenceExamples ? "primary" : "secondary"}
|
| 526 |
+
onClick={() => setShowReferenceExamples(!showReferenceExamples)}
|
| 527 |
+
className="whitespace-nowrap"
|
| 528 |
+
>
|
| 529 |
+
<span className="mr-2">
|
| 530 |
+
{showReferenceExamples ? (
|
| 531 |
+
<span className="text-yellow-400">★</span>
|
| 532 |
+
) : (
|
| 533 |
+
<span className="text-yellow-400">☆</span>
|
| 534 |
+
)}
|
| 535 |
+
</span>
|
| 536 |
+
Reference Examples
|
| 537 |
+
</Button>
|
| 538 |
+
</Container>
|
|
|
|
|
|
|
| 539 |
|
| 540 |
<Container withInternalPadding className="bg-white/20 backdrop-blur-sm rounded-md p-2">
|
| 541 |
<Button
|
|
|
|
| 663 |
<h3 className={styles.mapItemTitle}>
|
| 664 |
<div className="flex items-center gap-2">
|
| 665 |
<span>{c.title || 'Untitled'}</span>
|
| 666 |
+
{c.starred && (
|
| 667 |
<span className="text-red-500 text-lg" title="Starred image">★</span>
|
| 668 |
)}
|
| 669 |
</div>
|
|
|
|
| 715 |
</div>
|
| 716 |
)}
|
| 717 |
</div>
|
| 718 |
+
|
| 719 |
+
{/* Export Selection Modal */}
|
| 720 |
+
{showExportModal && (
|
| 721 |
+
<div className={styles.fullSizeModalOverlay} onClick={() => setShowExportModal(false)}>
|
| 722 |
+
<div className={styles.fullSizeModalContent} onClick={(e) => e.stopPropagation()}>
|
| 723 |
+
<div className={styles.ratingWarningContent}>
|
| 724 |
+
{exportModalStage === 'filters' ? (
|
| 725 |
+
<>
|
| 726 |
+
<h3 className={styles.ratingWarningTitle}>Export Dataset</h3>
|
| 727 |
+
|
| 728 |
+
{/* Filter Status Message */}
|
| 729 |
+
<div className={styles.filterStatusContainer}>
|
| 730 |
+
{(search || srcFilter || catFilter || regionFilter || countryFilter || imageTypeFilter || showReferenceExamples) ? (
|
| 731 |
+
<>
|
| 732 |
+
<div className={styles.filterStatusMessage}>
|
| 733 |
+
Filters are being applied
|
| 734 |
+
</div>
|
| 735 |
+
<div className={styles.filterStatusCount}>
|
| 736 |
+
{filtered.length} of {captions.length} examples
|
| 737 |
+
</div>
|
| 738 |
+
<div className={styles.activeFiltersList}>
|
| 739 |
+
{search && <span className={styles.activeFilter}>Search: "{search}"</span>}
|
| 740 |
+
{srcFilter && <span className={styles.activeFilter}>Source: {sources.find(s => s.s_code === srcFilter)?.label || srcFilter}</span>}
|
| 741 |
+
{catFilter && <span className={styles.activeFilter}>Category: {types.find(t => t.t_code === catFilter)?.label || catFilter}</span>}
|
| 742 |
+
{regionFilter && <span className={styles.activeFilter}>Region: {regions.find(r => r.r_code === regionFilter)?.label || regionFilter}</span>}
|
| 743 |
+
{countryFilter && <span className={styles.activeFilter}>Country: {countries.find(c => c.c_code === countryFilter)?.label || countryFilter}</span>}
|
| 744 |
+
{imageTypeFilter && <span className={styles.activeFilter}>Type: {imageTypes.find(it => it.image_type === imageTypeFilter)?.label || imageTypeFilter}</span>}
|
| 745 |
+
{showReferenceExamples && <span className={styles.activeFilter}>Reference Examples Only</span>}
|
| 746 |
+
</div>
|
| 747 |
+
<div className={styles.filterStatusActions}>
|
| 748 |
+
<Button
|
| 749 |
+
name="clear-filters-modal"
|
| 750 |
+
variant="secondary"
|
| 751 |
+
size={1}
|
| 752 |
+
onClick={clearAllFilters}
|
| 753 |
+
>
|
| 754 |
+
Clear Filters
|
| 755 |
+
</Button>
|
| 756 |
+
<Button
|
| 757 |
+
name="continue-with-filters"
|
| 758 |
+
variant="primary"
|
| 759 |
+
size={1}
|
| 760 |
+
onClick={() => setExportModalStage('export')}
|
| 761 |
+
>
|
| 762 |
+
Continue
|
| 763 |
+
</Button>
|
| 764 |
+
</div>
|
| 765 |
+
</>
|
| 766 |
+
) : (
|
| 767 |
+
<>
|
| 768 |
+
<div className={styles.filterStatusCount}>
|
| 769 |
+
{captions.length} examples available
|
| 770 |
+
</div>
|
| 771 |
+
<Button
|
| 772 |
+
name="continue-no-filters"
|
| 773 |
+
variant="primary"
|
| 774 |
+
size={1}
|
| 775 |
+
onClick={() => setExportModalStage('export')}
|
| 776 |
+
>
|
| 777 |
+
Continue
|
| 778 |
+
</Button>
|
| 779 |
+
</>
|
| 780 |
+
)}
|
| 781 |
+
</div>
|
| 782 |
+
|
| 783 |
+
<div className={styles.ratingWarningButtons}>
|
| 784 |
+
<Button
|
| 785 |
+
name="cancel-export"
|
| 786 |
+
variant="tertiary"
|
| 787 |
+
onClick={() => {
|
| 788 |
+
setShowExportModal(false);
|
| 789 |
+
setExportModalStage('filters');
|
| 790 |
+
}}
|
| 791 |
+
>
|
| 792 |
+
Cancel
|
| 793 |
+
</Button>
|
| 794 |
+
</div>
|
| 795 |
+
</>
|
| 796 |
+
) : (
|
| 797 |
+
<>
|
| 798 |
+
<h3 className={styles.ratingWarningTitle}>Export Dataset</h3>
|
| 799 |
+
|
| 800 |
+
{/* Export Mode Switch */}
|
| 801 |
+
<div className={styles.exportModeSection}>
|
| 802 |
+
<SegmentInput
|
| 803 |
+
name="export-mode"
|
| 804 |
+
value={exportMode}
|
| 805 |
+
onChange={(value) => {
|
| 806 |
+
if (value === 'standard' || value === 'fine-tuning') {
|
| 807 |
+
setExportMode(value);
|
| 808 |
+
}
|
| 809 |
+
}}
|
| 810 |
+
options={[
|
| 811 |
+
{ key: 'standard' as const, label: 'Standard' },
|
| 812 |
+
{ key: 'fine-tuning' as const, label: 'Fine-tuning' }
|
| 813 |
+
]}
|
| 814 |
+
keySelector={(o) => o.key}
|
| 815 |
+
labelSelector={(o) => o.label}
|
| 816 |
+
/>
|
| 817 |
+
</div>
|
| 818 |
+
|
| 819 |
+
{/* Train/Test/Val Split Configuration - Only show for Fine-tuning mode */}
|
| 820 |
+
{exportMode === 'fine-tuning' && (
|
| 821 |
+
<div className={styles.splitConfigSection}>
|
| 822 |
+
<div className={styles.splitConfigTitle}>Dataset Split Configuration</div>
|
| 823 |
+
<div className={styles.splitInputsContainer}>
|
| 824 |
+
<div className={styles.splitInputGroup}>
|
| 825 |
+
<label htmlFor="train-split" className={styles.splitInputLabel}>Train (%)</label>
|
| 826 |
+
<input
|
| 827 |
+
id="train-split"
|
| 828 |
+
type="number"
|
| 829 |
+
min="0"
|
| 830 |
+
max="100"
|
| 831 |
+
value={trainSplit}
|
| 832 |
+
onChange={(e) => {
|
| 833 |
+
const newTrain = parseInt(e.target.value) || 0;
|
| 834 |
+
const remaining = 100 - newTrain;
|
| 835 |
+
if (remaining >= 0) {
|
| 836 |
+
setTrainSplit(newTrain);
|
| 837 |
+
// Distribute remaining between test and val
|
| 838 |
+
if (testSplit + valSplit > remaining) {
|
| 839 |
+
setTestSplit(Math.floor(remaining / 2));
|
| 840 |
+
setValSplit(remaining - Math.floor(remaining / 2));
|
| 841 |
+
}
|
| 842 |
+
}
|
| 843 |
+
}}
|
| 844 |
+
className={styles.splitInput}
|
| 845 |
+
/>
|
| 846 |
+
</div>
|
| 847 |
+
|
| 848 |
+
<div className={styles.splitInputGroup}>
|
| 849 |
+
<label htmlFor="test-split" className={styles.splitInputLabel}>Test (%)</label>
|
| 850 |
+
<input
|
| 851 |
+
id="test-split"
|
| 852 |
+
type="number"
|
| 853 |
+
min="0"
|
| 854 |
+
max="100"
|
| 855 |
+
value={testSplit}
|
| 856 |
+
onChange={(e) => {
|
| 857 |
+
const newTest = parseInt(e.target.value) || 0;
|
| 858 |
+
const remaining = 100 - trainSplit - newTest;
|
| 859 |
+
if (remaining >= 0) {
|
| 860 |
+
setTestSplit(newTest);
|
| 861 |
+
setValSplit(remaining);
|
| 862 |
+
}
|
| 863 |
+
}}
|
| 864 |
+
className={styles.splitInput}
|
| 865 |
+
/>
|
| 866 |
+
</div>
|
| 867 |
+
|
| 868 |
+
<div className={styles.splitInputGroup}>
|
| 869 |
+
<label htmlFor="val-split" className={styles.splitInputLabel}>Val (%)</label>
|
| 870 |
+
<input
|
| 871 |
+
id="val-split"
|
| 872 |
+
type="number"
|
| 873 |
+
min="0"
|
| 874 |
+
max="100"
|
| 875 |
+
value={valSplit}
|
| 876 |
+
onChange={(e) => {
|
| 877 |
+
const newVal = parseInt(e.target.value) || 0;
|
| 878 |
+
const remaining = 100 - trainSplit - newVal;
|
| 879 |
+
if (remaining >= 0) {
|
| 880 |
+
setValSplit(newVal);
|
| 881 |
+
setTestSplit(remaining);
|
| 882 |
+
}
|
| 883 |
+
}}
|
| 884 |
+
className={styles.splitInput}
|
| 885 |
+
/>
|
| 886 |
+
</div>
|
| 887 |
+
</div>
|
| 888 |
+
|
| 889 |
+
{trainSplit + testSplit + valSplit !== 100 && (
|
| 890 |
+
<div className={styles.splitTotal}>
|
| 891 |
+
<span className={styles.splitTotalError}>Must equal 100%</span>
|
| 892 |
+
</div>
|
| 893 |
+
)}
|
| 894 |
+
</div>
|
| 895 |
+
)}
|
| 896 |
+
|
| 897 |
+
<div className={styles.checkboxesContainer}>
|
| 898 |
+
<div className="flex items-center gap-3">
|
| 899 |
+
<Checkbox
|
| 900 |
+
name="crisis-maps"
|
| 901 |
+
label={`Crisis Maps (${filtered.filter(img => img.image_type === 'crisis_map').length} images)`}
|
| 902 |
+
value={crisisMapsSelected}
|
| 903 |
+
onChange={(value, name) => setCrisisMapsSelected(value)}
|
| 904 |
+
disabled={isLoadingFilters}
|
| 905 |
+
/>
|
| 906 |
+
</div>
|
| 907 |
+
|
| 908 |
+
<div className="flex items-center gap-3">
|
| 909 |
+
<Checkbox
|
| 910 |
+
name="drone-images"
|
| 911 |
+
label={`Drone Images (${filtered.filter(img => img.image_type === 'drone_image').length} images)`}
|
| 912 |
+
value={droneImagesSelected}
|
| 913 |
+
onChange={(value, name) => setDroneImagesSelected(value)}
|
| 914 |
+
disabled={isLoadingFilters}
|
| 915 |
+
/>
|
| 916 |
+
</div>
|
| 917 |
+
</div>
|
| 918 |
+
|
| 919 |
+
<div className={styles.ratingWarningButtons}>
|
| 920 |
+
{(search || srcFilter || catFilter || regionFilter || countryFilter || imageTypeFilter || showReferenceExamples) && (
|
| 921 |
+
<Button
|
| 922 |
+
name="back-to-filters"
|
| 923 |
+
variant="secondary"
|
| 924 |
+
onClick={() => setExportModalStage('filters')}
|
| 925 |
+
>
|
| 926 |
+
Back to Filters
|
| 927 |
+
</Button>
|
| 928 |
+
)}
|
| 929 |
+
<Button
|
| 930 |
+
name="confirm-export"
|
| 931 |
+
onClick={() => {
|
| 932 |
+
if (!crisisMapsSelected && !droneImagesSelected) {
|
| 933 |
+
alert('Please select at least one image type to export.');
|
| 934 |
+
return;
|
| 935 |
+
}
|
| 936 |
+
|
| 937 |
+
const selectedTypes: string[] = [];
|
| 938 |
+
if (crisisMapsSelected) selectedTypes.push('crisis_map');
|
| 939 |
+
if (droneImagesSelected) selectedTypes.push('drone_image');
|
| 940 |
+
|
| 941 |
+
const filteredByType = filtered.filter(img => selectedTypes.includes(img.image_type));
|
| 942 |
+
exportDataset(filteredByType, exportMode);
|
| 943 |
+
setShowExportModal(false);
|
| 944 |
+
setExportModalStage('filters');
|
| 945 |
+
}}
|
| 946 |
+
>
|
| 947 |
+
Export Selected
|
| 948 |
+
</Button>
|
| 949 |
+
</div>
|
| 950 |
+
</>
|
| 951 |
+
)}
|
| 952 |
+
</div>
|
| 953 |
+
</div>
|
| 954 |
+
</div>
|
| 955 |
+
)}
|
| 956 |
</PageContainer>
|
| 957 |
);
|
| 958 |
}
|
frontend/src/pages/HelpPage.tsx
CHANGED
|
@@ -1,10 +1,28 @@
|
|
| 1 |
-
import { PageContainer, Heading, Container } from '@ifrc-go/ui';
|
|
|
|
|
|
|
| 2 |
|
| 3 |
export default function HelpPage() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
return (
|
| 5 |
<PageContainer className="py-10">
|
| 6 |
<Container withInternalPadding className="max-w-4xl mx-auto">
|
| 7 |
-
|
| 8 |
|
| 9 |
<div className="space-y-8">
|
| 10 |
<Container withInternalPadding className="p-8">
|
|
@@ -16,6 +34,15 @@ export default function HelpPage() {
|
|
| 16 |
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
|
| 17 |
culpa qui officia deserunt mollit anim id est laborum.
|
| 18 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</Container>
|
| 20 |
|
| 21 |
<Container withInternalPadding className="p-8">
|
|
@@ -26,6 +53,15 @@ export default function HelpPage() {
|
|
| 26 |
dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit,
|
| 27 |
sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
|
| 28 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
</Container>
|
| 30 |
|
| 31 |
<Container withInternalPadding className="p-8">
|
|
@@ -36,6 +72,15 @@ export default function HelpPage() {
|
|
| 36 |
provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum
|
| 37 |
fuga. Et harum quidem rerum facilis est et expedita distinctio.
|
| 38 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
</Container>
|
| 40 |
</div>
|
| 41 |
</Container>
|
|
|
|
| 1 |
+
import { PageContainer, Heading, Container, Button } from '@ifrc-go/ui';
|
| 2 |
+
import { useNavigate } from 'react-router-dom';
|
| 3 |
+
import { useFilterContext } from '../contexts/FilterContext';
|
| 4 |
|
| 5 |
export default function HelpPage() {
|
| 6 |
+
const navigate = useNavigate();
|
| 7 |
+
const { setShowReferenceExamples } = useFilterContext();
|
| 8 |
+
|
| 9 |
+
const handleUploadNow = () => {
|
| 10 |
+
navigate('/upload');
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
const handleSeeExamples = () => {
|
| 14 |
+
setShowReferenceExamples(true);
|
| 15 |
+
navigate('/explore');
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
const handleViewVlmDetails = () => {
|
| 19 |
+
navigate('/analytics?view=crisis_maps');
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
return (
|
| 23 |
<PageContainer className="py-10">
|
| 24 |
<Container withInternalPadding className="max-w-4xl mx-auto">
|
| 25 |
+
|
| 26 |
|
| 27 |
<div className="space-y-8">
|
| 28 |
<Container withInternalPadding className="p-8">
|
|
|
|
| 34 |
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
|
| 35 |
culpa qui officia deserunt mollit anim id est laborum.
|
| 36 |
</p>
|
| 37 |
+
<div className="mt-6">
|
| 38 |
+
<Button
|
| 39 |
+
name="upload-now"
|
| 40 |
+
variant="primary"
|
| 41 |
+
onClick={handleUploadNow}
|
| 42 |
+
>
|
| 43 |
+
Upload now →
|
| 44 |
+
</Button>
|
| 45 |
+
</div>
|
| 46 |
</Container>
|
| 47 |
|
| 48 |
<Container withInternalPadding className="p-8">
|
|
|
|
| 53 |
dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit,
|
| 54 |
sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
|
| 55 |
</p>
|
| 56 |
+
<div className="mt-6">
|
| 57 |
+
<Button
|
| 58 |
+
name="see-examples"
|
| 59 |
+
variant="primary"
|
| 60 |
+
onClick={handleSeeExamples}
|
| 61 |
+
>
|
| 62 |
+
See examples →
|
| 63 |
+
</Button>
|
| 64 |
+
</div>
|
| 65 |
</Container>
|
| 66 |
|
| 67 |
<Container withInternalPadding className="p-8">
|
|
|
|
| 72 |
provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum
|
| 73 |
fuga. Et harum quidem rerum facilis est et expedita distinctio.
|
| 74 |
</p>
|
| 75 |
+
<div className="mt-6">
|
| 76 |
+
<Button
|
| 77 |
+
name="view-vlm-details"
|
| 78 |
+
variant="primary"
|
| 79 |
+
onClick={handleViewVlmDetails}
|
| 80 |
+
>
|
| 81 |
+
View VLM details →
|
| 82 |
+
</Button>
|
| 83 |
+
</div>
|
| 84 |
</Container>
|
| 85 |
</div>
|
| 86 |
</Container>
|
frontend/src/pages/MapDetailsPage/MapDetailPage.module.css
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
.tabSelector {
|
| 2 |
margin-bottom: var(--go-ui-spacing-lg);
|
| 3 |
display: flex;
|
| 4 |
-
justify-content:
|
|
|
|
| 5 |
}
|
| 6 |
|
| 7 |
.backButton {
|
|
@@ -239,6 +240,8 @@
|
|
| 239 |
gap: var(--go-ui-spacing-md);
|
| 240 |
justify-content: center;
|
| 241 |
flex-wrap: wrap;
|
|
|
|
|
|
|
| 242 |
}
|
| 243 |
|
| 244 |
/* Responsive adjustments for modal */
|
|
@@ -252,3 +255,96 @@
|
|
| 252 |
max-height: 100vh;
|
| 253 |
}
|
| 254 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
.tabSelector {
|
| 2 |
margin-bottom: var(--go-ui-spacing-lg);
|
| 3 |
display: flex;
|
| 4 |
+
justify-content: space-between;
|
| 5 |
+
align-items: center;
|
| 6 |
}
|
| 7 |
|
| 8 |
.backButton {
|
|
|
|
| 240 |
gap: var(--go-ui-spacing-md);
|
| 241 |
justify-content: center;
|
| 242 |
flex-wrap: wrap;
|
| 243 |
+
margin-top: var(--go-ui-spacing-xl);
|
| 244 |
+
text-align: center;
|
| 245 |
}
|
| 246 |
|
| 247 |
/* Responsive adjustments for modal */
|
|
|
|
| 255 |
max-height: 100vh;
|
| 256 |
}
|
| 257 |
}
|
| 258 |
+
|
| 259 |
+
/* Export mode and split configuration styles */
|
| 260 |
+
.exportModeSection {
|
| 261 |
+
margin-bottom: var(--go-ui-spacing-lg);
|
| 262 |
+
text-align: center;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.exportModeLabel {
|
| 266 |
+
font-size: var(--go-ui-font-size-sm);
|
| 267 |
+
font-weight: var(--go-ui-font-weight-medium);
|
| 268 |
+
color: var(--go-ui-color-gray-700);
|
| 269 |
+
margin-bottom: var(--go-ui-spacing-sm);
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.splitConfigSection {
|
| 273 |
+
margin-bottom: var(--go-ui-spacing-lg);
|
| 274 |
+
text-align: center;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
.splitConfigTitle {
|
| 278 |
+
font-size: 1rem;
|
| 279 |
+
font-weight: 600;
|
| 280 |
+
margin-bottom: var(--go-ui-spacing-md);
|
| 281 |
+
color: var(--go-ui-color-text-primary);
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
.splitInputsContainer {
|
| 285 |
+
display: flex;
|
| 286 |
+
justify-content: center;
|
| 287 |
+
gap: var(--go-ui-spacing-md);
|
| 288 |
+
margin-bottom: var(--go-ui-spacing-md);
|
| 289 |
+
flex-wrap: wrap;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
.splitInputGroup {
|
| 293 |
+
display: flex;
|
| 294 |
+
flex-direction: column;
|
| 295 |
+
align-items: center;
|
| 296 |
+
text-align: center;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.splitInputLabel {
|
| 300 |
+
margin-bottom: var(--go-ui-spacing-xs);
|
| 301 |
+
font-weight: 500;
|
| 302 |
+
color: var(--go-ui-color-text-primary);
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
.splitInput {
|
| 306 |
+
width: 80px;
|
| 307 |
+
padding: var(--go-ui-spacing-sm);
|
| 308 |
+
border: 1px solid var(--go-ui-color-border);
|
| 309 |
+
border-radius: var(--go-ui-border-radius);
|
| 310 |
+
font-size: 1rem;
|
| 311 |
+
text-align: center;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
.splitInput:focus {
|
| 315 |
+
outline: none;
|
| 316 |
+
border-color: var(--go-ui-color-red-50);
|
| 317 |
+
box-shadow: 0 0 0 2px var(--go-ui-color-red-20);
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
.splitInput:hover {
|
| 321 |
+
border-color: var(--go-ui-color-gray-400);
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.splitTotal {
|
| 325 |
+
font-size: var(--go-ui-font-size-xs);
|
| 326 |
+
color: var(--go-ui-color-gray-500);
|
| 327 |
+
text-align: center;
|
| 328 |
+
margin-top: var(--go-ui-spacing-xs);
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.splitTotalError {
|
| 332 |
+
color: var(--go-ui-color-negative);
|
| 333 |
+
margin-left: var(--go-ui-spacing-xs);
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
/* Responsive adjustments for split inputs */
|
| 337 |
+
@media (max-width: 768px) {
|
| 338 |
+
.splitInputsContainer {
|
| 339 |
+
gap: var(--go-ui-spacing-md);
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.splitInput {
|
| 343 |
+
width: 3.5rem;
|
| 344 |
+
}
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.checkboxesContainer {
|
| 348 |
+
text-align: center;
|
| 349 |
+
margin-bottom: var(--go-ui-spacing-lg);
|
| 350 |
+
}
|
frontend/src/pages/MapDetailsPage/MapDetailPage.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import { PageContainer, Container, Button, Spinner, SegmentInput, TextInput, SelectInput, MultiSelectInput } from '@ifrc-go/ui';
|
| 2 |
import { useParams, useNavigate } from 'react-router-dom';
|
| 3 |
import { useState, useEffect, useMemo, useCallback } from 'react';
|
| 4 |
import { ChevronLeftLineIcon, ChevronRightLineIcon, DeleteBinLineIcon } from '@ifrc-go/icons';
|
|
@@ -55,6 +55,13 @@ export default function MapDetailPage() {
|
|
| 55 |
|
| 56 |
// Add delete confirmation state
|
| 57 |
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
// Add flag to prevent auto-navigation during delete operations
|
| 60 |
const [isDeleting, setIsDeleting] = useState(false);
|
|
@@ -474,6 +481,301 @@ export default function MapDetailPage() {
|
|
| 474 |
}
|
| 475 |
};
|
| 476 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
|
| 478 |
if (loading) {
|
| 479 |
return (
|
|
@@ -528,6 +830,15 @@ export default function MapDetailPage() {
|
|
| 528 |
keySelector={(o) => o.key}
|
| 529 |
labelSelector={(o) => o.label}
|
| 530 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 531 |
</div>
|
| 532 |
|
| 533 |
{/* Search and Filters */}
|
|
@@ -543,26 +854,24 @@ export default function MapDetailPage() {
|
|
| 543 |
/>
|
| 544 |
</Container>
|
| 545 |
|
| 546 |
-
{/* Reference Examples Filter -
|
| 547 |
-
|
| 548 |
-
<
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
>
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
</Container>
|
| 565 |
-
)}
|
| 566 |
|
| 567 |
<Container withInternalPadding className="bg-white/20 backdrop-blur-sm rounded-md p-2">
|
| 568 |
<Button
|
|
@@ -653,7 +962,7 @@ export default function MapDetailPage() {
|
|
| 653 |
heading={
|
| 654 |
<div className="flex items-center gap-2">
|
| 655 |
<span>{filteredMap.title || "Map Image"}</span>
|
| 656 |
-
{
|
| 657 |
<span className="text-red-500 text-xl" title="Starred image">★</span>
|
| 658 |
)}
|
| 659 |
</div>
|
|
@@ -887,6 +1196,166 @@ export default function MapDetailPage() {
|
|
| 887 |
</div>
|
| 888 |
</div>
|
| 889 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 890 |
</PageContainer>
|
| 891 |
);
|
| 892 |
}
|
|
|
|
| 1 |
+
import { PageContainer, Container, Button, Spinner, SegmentInput, TextInput, SelectInput, MultiSelectInput, Checkbox } from '@ifrc-go/ui';
|
| 2 |
import { useParams, useNavigate } from 'react-router-dom';
|
| 3 |
import { useState, useEffect, useMemo, useCallback } from 'react';
|
| 4 |
import { ChevronLeftLineIcon, ChevronRightLineIcon, DeleteBinLineIcon } from '@ifrc-go/icons';
|
|
|
|
| 55 |
|
| 56 |
// Add delete confirmation state
|
| 57 |
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
| 58 |
+
const [showExportModal, setShowExportModal] = useState(false);
|
| 59 |
+
const [exportMode, setExportMode] = useState<'standard' | 'fine-tuning'>('standard');
|
| 60 |
+
const [trainSplit, setTrainSplit] = useState(80);
|
| 61 |
+
const [testSplit, setTestSplit] = useState(10);
|
| 62 |
+
const [valSplit, setValSplit] = useState(10);
|
| 63 |
+
const [crisisMapsSelected, setCrisisMapsSelected] = useState(true);
|
| 64 |
+
const [droneImagesSelected, setDroneImagesSelected] = useState(true);
|
| 65 |
|
| 66 |
// Add flag to prevent auto-navigation during delete operations
|
| 67 |
const [isDeleting, setIsDeleting] = useState(false);
|
|
|
|
| 481 |
}
|
| 482 |
};
|
| 483 |
|
| 484 |
+
// Helper function to create image data
|
| 485 |
+
const createImageData = (map: any, fileName: string) => ({
|
| 486 |
+
image: `images/${fileName}`,
|
| 487 |
+
caption: map.edited || map.generated || '',
|
| 488 |
+
metadata: {
|
| 489 |
+
image_id: map.image_id,
|
| 490 |
+
title: map.title,
|
| 491 |
+
source: map.source,
|
| 492 |
+
event_type: map.event_type,
|
| 493 |
+
image_type: map.image_type,
|
| 494 |
+
countries: map.countries,
|
| 495 |
+
starred: map.starred
|
| 496 |
+
}
|
| 497 |
+
});
|
| 498 |
+
|
| 499 |
+
const exportDataset = async (mode: 'standard' | 'fine-tuning') => {
|
| 500 |
+
if (!map) {
|
| 501 |
+
alert('No map to export');
|
| 502 |
+
return;
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
try {
|
| 506 |
+
// Create a JSZip instance
|
| 507 |
+
const JSZip = (await import('jszip')).default;
|
| 508 |
+
const zip = new JSZip();
|
| 509 |
+
|
| 510 |
+
// Determine which dataset to create based on image type
|
| 511 |
+
if (map.image_type === 'crisis_map') {
|
| 512 |
+
const crisisFolder = zip.folder('crisis_maps_dataset');
|
| 513 |
+
const crisisImagesFolder = crisisFolder?.folder('images');
|
| 514 |
+
|
| 515 |
+
if (crisisImagesFolder) {
|
| 516 |
+
// Download the current crisis map image and add to zip
|
| 517 |
+
try {
|
| 518 |
+
const response = await fetch(map.image_url);
|
| 519 |
+
if (!response.ok) throw new Error(`Failed to fetch image ${map.image_id}`);
|
| 520 |
+
|
| 521 |
+
const blob = await response.blob();
|
| 522 |
+
const fileExtension = map.file_key.split('.').pop() || 'jpg';
|
| 523 |
+
const fileName = `0001.${fileExtension}`;
|
| 524 |
+
|
| 525 |
+
crisisImagesFolder.file(fileName, blob);
|
| 526 |
+
|
| 527 |
+
if (mode === 'fine-tuning') {
|
| 528 |
+
// Create train.jsonl, test.jsonl, and val.jsonl with stratified sampling
|
| 529 |
+
const trainData: any[] = [];
|
| 530 |
+
const testData: any[] = [];
|
| 531 |
+
const valData: any[] = [];
|
| 532 |
+
|
| 533 |
+
if (String(map?.image_type) === 'crisis_map') {
|
| 534 |
+
// For crisis maps, group by source
|
| 535 |
+
const source = map.source || 'unknown';
|
| 536 |
+
const totalImages = 1; // Only one image in MapDetailPage
|
| 537 |
+
|
| 538 |
+
// Since we only have one image, distribute it based on the split
|
| 539 |
+
const random = Math.random();
|
| 540 |
+
if (random < trainSplit / 100) {
|
| 541 |
+
trainData.push(createImageData(map, '0001'));
|
| 542 |
+
} else if (random < (trainSplit + testSplit) / 100) {
|
| 543 |
+
testData.push(createImageData(map, '0001'));
|
| 544 |
+
} else {
|
| 545 |
+
valData.push(createImageData(map, '0001'));
|
| 546 |
+
}
|
| 547 |
+
} else if (String(map?.image_type) === 'drone_image') {
|
| 548 |
+
// For drone images, group by event type
|
| 549 |
+
const eventType = map.event_type || 'unknown';
|
| 550 |
+
const totalImages = 1; // Only one image in MapDetailPage
|
| 551 |
+
|
| 552 |
+
// Since we only have one image, distribute it based on the split
|
| 553 |
+
const random = Math.random();
|
| 554 |
+
if (random < trainSplit / 100) {
|
| 555 |
+
trainData.push(createImageData(map, '0001'));
|
| 556 |
+
} else if (random < (trainSplit + testSplit) / 100) {
|
| 557 |
+
testData.push(createImageData(map, '0001'));
|
| 558 |
+
} else {
|
| 559 |
+
valData.push(createImageData(map, '0001'));
|
| 560 |
+
}
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
// Add JSONL files to dataset folder
|
| 564 |
+
if (crisisFolder) {
|
| 565 |
+
crisisFolder.file('train.jsonl', JSON.stringify(trainData, null, 2));
|
| 566 |
+
crisisFolder.file('test.jsonl', JSON.stringify(testData, null, 2));
|
| 567 |
+
crisisFolder.file('val.jsonl', JSON.stringify(valData, null, 2));
|
| 568 |
+
}
|
| 569 |
+
} else {
|
| 570 |
+
// Standard mode: create individual JSON file
|
| 571 |
+
const jsonData = {
|
| 572 |
+
image: `images/${fileName}`,
|
| 573 |
+
caption: map.edited || map.generated || '',
|
| 574 |
+
metadata: {
|
| 575 |
+
image_id: map.image_id,
|
| 576 |
+
title: map.title,
|
| 577 |
+
source: map.source,
|
| 578 |
+
event_type: map.event_type,
|
| 579 |
+
image_type: map.image_type,
|
| 580 |
+
countries: map.countries,
|
| 581 |
+
starred: map.starred
|
| 582 |
+
}
|
| 583 |
+
};
|
| 584 |
+
|
| 585 |
+
if (crisisFolder) {
|
| 586 |
+
crisisFolder.file('0001.json', JSON.stringify(jsonData, null, 2));
|
| 587 |
+
}
|
| 588 |
+
}
|
| 589 |
+
} catch (error) {
|
| 590 |
+
console.error(`Failed to process image ${map.image_id}:`, error);
|
| 591 |
+
throw error;
|
| 592 |
+
}
|
| 593 |
+
}
|
| 594 |
+
} else if (map.image_type === 'drone_image') {
|
| 595 |
+
const droneFolder = zip.folder('drone_images_dataset');
|
| 596 |
+
const droneImagesFolder = droneFolder?.folder('images');
|
| 597 |
+
|
| 598 |
+
if (droneImagesFolder) {
|
| 599 |
+
// Download the current drone image and add to zip
|
| 600 |
+
try {
|
| 601 |
+
const response = await fetch(map.image_url);
|
| 602 |
+
if (!response.ok) throw new Error(`Failed to fetch image ${map.image_id}`);
|
| 603 |
+
|
| 604 |
+
const blob = await response.blob();
|
| 605 |
+
const fileExtension = map.file_key.split('.').pop() || 'jpg';
|
| 606 |
+
const fileName = `0001.${fileExtension}`;
|
| 607 |
+
|
| 608 |
+
droneImagesFolder.file(fileName, blob);
|
| 609 |
+
|
| 610 |
+
if (mode === 'fine-tuning') {
|
| 611 |
+
// Create train.jsonl, test.jsonl, and val.jsonl with stratified sampling
|
| 612 |
+
const trainData: any[] = [];
|
| 613 |
+
const testData: any[] = [];
|
| 614 |
+
const valData: any[] = [];
|
| 615 |
+
|
| 616 |
+
if (String(map?.image_type) === 'crisis_map') {
|
| 617 |
+
// For crisis maps, group by source
|
| 618 |
+
const source = map.source || 'unknown';
|
| 619 |
+
const totalImages = 1; // Only one image in MapDetailPage
|
| 620 |
+
|
| 621 |
+
// Since we only have one image, distribute it based on the split
|
| 622 |
+
const random = Math.random();
|
| 623 |
+
if (random < trainSplit / 100) {
|
| 624 |
+
trainData.push(createImageData(map, '0001'));
|
| 625 |
+
} else if (random < (trainSplit + testSplit) / 100) {
|
| 626 |
+
testData.push(createImageData(map, '0001'));
|
| 627 |
+
} else {
|
| 628 |
+
valData.push(createImageData(map, '0001'));
|
| 629 |
+
}
|
| 630 |
+
} else if (String(map?.image_type) === 'drone_image') {
|
| 631 |
+
// For drone images, group by event type
|
| 632 |
+
const eventType = map.event_type || 'unknown';
|
| 633 |
+
const totalImages = 1; // Only one image in MapDetailPage
|
| 634 |
+
|
| 635 |
+
// Since we only have one image, distribute it based on the split
|
| 636 |
+
const random = Math.random();
|
| 637 |
+
if (random < trainSplit / 100) {
|
| 638 |
+
trainData.push(createImageData(map, '0001'));
|
| 639 |
+
} else if (random < (trainSplit + testSplit) / 100) {
|
| 640 |
+
testData.push(createImageData(map, '0001'));
|
| 641 |
+
} else {
|
| 642 |
+
valData.push(createImageData(map, '0001'));
|
| 643 |
+
}
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
// Add JSONL files to dataset folder
|
| 647 |
+
if (droneFolder) {
|
| 648 |
+
droneFolder.file('train.jsonl', JSON.stringify(trainData, null, 2));
|
| 649 |
+
droneFolder.file('test.jsonl', JSON.stringify(testData, null, 2));
|
| 650 |
+
droneFolder.file('val.jsonl', JSON.stringify(valData, null, 2));
|
| 651 |
+
}
|
| 652 |
+
} else {
|
| 653 |
+
// Standard mode: create individual JSON file
|
| 654 |
+
const jsonData = {
|
| 655 |
+
image: `images/${fileName}`,
|
| 656 |
+
caption: map.edited || map.generated || '',
|
| 657 |
+
metadata: {
|
| 658 |
+
image_id: map.image_id,
|
| 659 |
+
title: map.title,
|
| 660 |
+
source: map.source,
|
| 661 |
+
event_type: map.event_type,
|
| 662 |
+
image_type: map.image_type,
|
| 663 |
+
countries: map.countries,
|
| 664 |
+
starred: map.starred
|
| 665 |
+
}
|
| 666 |
+
};
|
| 667 |
+
|
| 668 |
+
if (droneFolder) {
|
| 669 |
+
droneFolder.file('0001.json', JSON.stringify(jsonData, null, 2));
|
| 670 |
+
}
|
| 671 |
+
}
|
| 672 |
+
} catch (error) {
|
| 673 |
+
console.error(`Failed to process image ${map.image_id}:`, error);
|
| 674 |
+
throw error;
|
| 675 |
+
}
|
| 676 |
+
}
|
| 677 |
+
} else {
|
| 678 |
+
// For other image types, create a generic dataset
|
| 679 |
+
const genericFolder = zip.folder('generic_dataset');
|
| 680 |
+
const genericImagesFolder = genericFolder?.folder('images');
|
| 681 |
+
|
| 682 |
+
if (genericImagesFolder) {
|
| 683 |
+
try {
|
| 684 |
+
const response = await fetch(map.image_url);
|
| 685 |
+
if (!response.ok) throw new Error(`Failed to fetch image ${map.image_id}`);
|
| 686 |
+
|
| 687 |
+
const blob = await response.blob();
|
| 688 |
+
const fileExtension = map.file_key.split('.').pop() || 'jpg';
|
| 689 |
+
const fileName = `0001.${fileExtension}`;
|
| 690 |
+
|
| 691 |
+
genericImagesFolder.file(fileName, blob);
|
| 692 |
+
|
| 693 |
+
if (mode === 'fine-tuning') {
|
| 694 |
+
// Create train.jsonl, test.jsonl, and val.jsonl with stratified sampling
|
| 695 |
+
const trainData: any[] = [];
|
| 696 |
+
const testData: any[] = [];
|
| 697 |
+
const valData: any[] = [];
|
| 698 |
+
|
| 699 |
+
if (String(map?.image_type) === 'crisis_map') {
|
| 700 |
+
// For crisis maps, group by source
|
| 701 |
+
const source = map.source || 'unknown';
|
| 702 |
+
const totalImages = 1; // Only one image in MapDetailPage
|
| 703 |
+
|
| 704 |
+
// Since we only have one image, distribute it based on the split
|
| 705 |
+
const random = Math.random();
|
| 706 |
+
if (random < trainSplit / 100) {
|
| 707 |
+
trainData.push(createImageData(map, '0001'));
|
| 708 |
+
} else if (random < (trainSplit + testSplit) / 100) {
|
| 709 |
+
testData.push(createImageData(map, '0001'));
|
| 710 |
+
} else {
|
| 711 |
+
valData.push(createImageData(map, '0001'));
|
| 712 |
+
}
|
| 713 |
+
} else if (String(map?.image_type) === 'drone_image') {
|
| 714 |
+
// For drone images, group by event type
|
| 715 |
+
const eventType = map.event_type || 'unknown';
|
| 716 |
+
const totalImages = 1; // Only one image in MapDetailPage
|
| 717 |
+
|
| 718 |
+
// Since we only have one image, distribute it based on the split
|
| 719 |
+
const random = Math.random();
|
| 720 |
+
if (random < trainSplit / 100) {
|
| 721 |
+
trainData.push(createImageData(map, '0001'));
|
| 722 |
+
} else if (random < (trainSplit + testSplit) / 100) {
|
| 723 |
+
testData.push(createImageData(map, '0001'));
|
| 724 |
+
} else {
|
| 725 |
+
valData.push(createImageData(map, '0001'));
|
| 726 |
+
}
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
// Add JSONL files to dataset folder
|
| 730 |
+
if (genericFolder) {
|
| 731 |
+
genericFolder.file('train.jsonl', JSON.stringify(trainData, null, 2));
|
| 732 |
+
genericFolder.file('test.jsonl', JSON.stringify(testData, null, 2));
|
| 733 |
+
genericFolder.file('val.jsonl', JSON.stringify(valData, null, 2));
|
| 734 |
+
}
|
| 735 |
+
} else {
|
| 736 |
+
// Standard mode: create individual JSON file
|
| 737 |
+
const jsonData = {
|
| 738 |
+
image: `images/${fileName}`,
|
| 739 |
+
caption: map.edited || map.generated || '',
|
| 740 |
+
metadata: {
|
| 741 |
+
image_id: map.image_id,
|
| 742 |
+
title: map.title,
|
| 743 |
+
source: map.source,
|
| 744 |
+
event_type: map.event_type,
|
| 745 |
+
image_type: map.image_type,
|
| 746 |
+
countries: map.countries,
|
| 747 |
+
starred: map.starred
|
| 748 |
+
}
|
| 749 |
+
};
|
| 750 |
+
|
| 751 |
+
if (genericFolder) {
|
| 752 |
+
genericFolder.file('0001.json', JSON.stringify(jsonData, null, 2));
|
| 753 |
+
}
|
| 754 |
+
}
|
| 755 |
+
} catch (error) {
|
| 756 |
+
console.error(`Failed to process image ${map.image_id}:`, error);
|
| 757 |
+
throw error;
|
| 758 |
+
}
|
| 759 |
+
}
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
// Generate and download zip
|
| 763 |
+
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
| 764 |
+
const url = URL.createObjectURL(zipBlob);
|
| 765 |
+
const link = document.createElement('a');
|
| 766 |
+
link.href = url;
|
| 767 |
+
link.download = `dataset_${map.image_type}_${map.image_id}_${mode}_${new Date().toISOString().split('T')[0]}.zip`;
|
| 768 |
+
document.body.appendChild(link);
|
| 769 |
+
link.click();
|
| 770 |
+
document.body.removeChild(link);
|
| 771 |
+
URL.revokeObjectURL(url);
|
| 772 |
+
|
| 773 |
+
console.log(`Exported ${map.image_type} dataset with 1 image in ${mode} mode`);
|
| 774 |
+
} catch (error) {
|
| 775 |
+
console.error('Export failed:', error);
|
| 776 |
+
alert('Failed to export dataset. Please try again.');
|
| 777 |
+
}
|
| 778 |
+
};
|
| 779 |
|
| 780 |
if (loading) {
|
| 781 |
return (
|
|
|
|
| 830 |
keySelector={(o) => o.key}
|
| 831 |
labelSelector={(o) => o.label}
|
| 832 |
/>
|
| 833 |
+
|
| 834 |
+
{/* Export Dataset Button */}
|
| 835 |
+
<Button
|
| 836 |
+
name="export-dataset"
|
| 837 |
+
variant="secondary"
|
| 838 |
+
onClick={() => setShowExportModal(true)}
|
| 839 |
+
>
|
| 840 |
+
Export Dataset
|
| 841 |
+
</Button>
|
| 842 |
</div>
|
| 843 |
|
| 844 |
{/* Search and Filters */}
|
|
|
|
| 854 |
/>
|
| 855 |
</Container>
|
| 856 |
|
| 857 |
+
{/* Reference Examples Filter - Available to all users */}
|
| 858 |
+
<Container withInternalPadding className="bg-white/20 backdrop-blur-sm rounded-md p-2">
|
| 859 |
+
<Button
|
| 860 |
+
name="reference-examples"
|
| 861 |
+
variant={showReferenceExamples ? "primary" : "secondary"}
|
| 862 |
+
onClick={() => setShowReferenceExamples(!showReferenceExamples)}
|
| 863 |
+
className="whitespace-nowrap"
|
| 864 |
+
>
|
| 865 |
+
<span className="mr-2">
|
| 866 |
+
{showReferenceExamples ? (
|
| 867 |
+
<span className="text-yellow-400">★</span>
|
| 868 |
+
) : (
|
| 869 |
+
<span className="text-yellow-400">☆</span>
|
| 870 |
+
)}
|
| 871 |
+
</span>
|
| 872 |
+
Reference Examples
|
| 873 |
+
</Button>
|
| 874 |
+
</Container>
|
|
|
|
|
|
|
| 875 |
|
| 876 |
<Container withInternalPadding className="bg-white/20 backdrop-blur-sm rounded-md p-2">
|
| 877 |
<Button
|
|
|
|
| 962 |
heading={
|
| 963 |
<div className="flex items-center gap-2">
|
| 964 |
<span>{filteredMap.title || "Map Image"}</span>
|
| 965 |
+
{filteredMap.starred && (
|
| 966 |
<span className="text-red-500 text-xl" title="Starred image">★</span>
|
| 967 |
)}
|
| 968 |
</div>
|
|
|
|
| 1196 |
</div>
|
| 1197 |
</div>
|
| 1198 |
)}
|
| 1199 |
+
|
| 1200 |
+
{/* Export Selection Modal */}
|
| 1201 |
+
{showExportModal && (
|
| 1202 |
+
<div className={styles.fullSizeModalOverlay} onClick={() => setShowExportModal(false)}>
|
| 1203 |
+
<div className={styles.fullSizeModalContent} onClick={(e) => e.stopPropagation()}>
|
| 1204 |
+
<div className={styles.ratingWarningContent}>
|
| 1205 |
+
<h3 className={styles.ratingWarningTitle}>Export Dataset</h3>
|
| 1206 |
+
|
| 1207 |
+
{/* Export Mode Switch */}
|
| 1208 |
+
<div className={styles.exportModeSection}>
|
| 1209 |
+
<SegmentInput
|
| 1210 |
+
name="export-mode"
|
| 1211 |
+
value={exportMode}
|
| 1212 |
+
onChange={(value) => {
|
| 1213 |
+
if (value === 'standard' || value === 'fine-tuning') {
|
| 1214 |
+
setExportMode(value);
|
| 1215 |
+
}
|
| 1216 |
+
}}
|
| 1217 |
+
options={[
|
| 1218 |
+
{ key: 'standard' as const, label: 'Standard' },
|
| 1219 |
+
{ key: 'fine-tuning' as const, label: 'Fine-tuning' }
|
| 1220 |
+
]}
|
| 1221 |
+
keySelector={(o) => o.key}
|
| 1222 |
+
labelSelector={(o) => o.label}
|
| 1223 |
+
/>
|
| 1224 |
+
</div>
|
| 1225 |
+
|
| 1226 |
+
{/* Train/Test/Val Split Configuration - Only show for Fine-tuning mode */}
|
| 1227 |
+
{exportMode === 'fine-tuning' && (
|
| 1228 |
+
<div className={styles.splitConfigSection}>
|
| 1229 |
+
<div className={styles.splitConfigTitle}>Dataset Split Configuration</div>
|
| 1230 |
+
<div className={styles.splitInputsContainer}>
|
| 1231 |
+
<div className={styles.splitInputGroup}>
|
| 1232 |
+
<label htmlFor="train-split" className={styles.splitInputLabel}>Train (%)</label>
|
| 1233 |
+
<input
|
| 1234 |
+
id="train-split"
|
| 1235 |
+
type="number"
|
| 1236 |
+
min="0"
|
| 1237 |
+
max="100"
|
| 1238 |
+
value={trainSplit}
|
| 1239 |
+
onChange={(e) => {
|
| 1240 |
+
const newTrain = parseInt(e.target.value) || 0;
|
| 1241 |
+
const remaining = 100 - newTrain;
|
| 1242 |
+
if (remaining >= 0) {
|
| 1243 |
+
setTrainSplit(newTrain);
|
| 1244 |
+
// Distribute remaining between test and val
|
| 1245 |
+
if (testSplit + valSplit > remaining) {
|
| 1246 |
+
setTestSplit(Math.floor(remaining / 2));
|
| 1247 |
+
setValSplit(remaining - Math.floor(remaining / 2));
|
| 1248 |
+
}
|
| 1249 |
+
}
|
| 1250 |
+
}}
|
| 1251 |
+
className={styles.splitInput}
|
| 1252 |
+
/>
|
| 1253 |
+
</div>
|
| 1254 |
+
|
| 1255 |
+
<div className={styles.splitInputGroup}>
|
| 1256 |
+
<label htmlFor="test-split" className={styles.splitInputLabel}>Test (%)</label>
|
| 1257 |
+
<input
|
| 1258 |
+
id="test-split"
|
| 1259 |
+
type="number"
|
| 1260 |
+
min="0"
|
| 1261 |
+
max="100"
|
| 1262 |
+
value={testSplit}
|
| 1263 |
+
onChange={(e) => {
|
| 1264 |
+
const newTest = parseInt(e.target.value) || 0;
|
| 1265 |
+
const remaining = 100 - trainSplit - newTest;
|
| 1266 |
+
if (remaining >= 0) {
|
| 1267 |
+
setTestSplit(newTest);
|
| 1268 |
+
setValSplit(remaining);
|
| 1269 |
+
}
|
| 1270 |
+
}}
|
| 1271 |
+
className={styles.splitInput}
|
| 1272 |
+
/>
|
| 1273 |
+
</div>
|
| 1274 |
+
|
| 1275 |
+
<div className={styles.splitInputGroup}>
|
| 1276 |
+
<label htmlFor="val-split" className={styles.splitInputLabel}>Val (%)</label>
|
| 1277 |
+
<input
|
| 1278 |
+
id="val-split"
|
| 1279 |
+
type="number"
|
| 1280 |
+
min="0"
|
| 1281 |
+
max="100"
|
| 1282 |
+
value={valSplit}
|
| 1283 |
+
onChange={(e) => {
|
| 1284 |
+
const newVal = parseInt(e.target.value) || 0;
|
| 1285 |
+
const remaining = 100 - trainSplit - newVal;
|
| 1286 |
+
if (remaining >= 0) {
|
| 1287 |
+
setValSplit(newVal);
|
| 1288 |
+
setTestSplit(remaining);
|
| 1289 |
+
}
|
| 1290 |
+
}}
|
| 1291 |
+
className={styles.splitInput}
|
| 1292 |
+
/>
|
| 1293 |
+
</div>
|
| 1294 |
+
</div>
|
| 1295 |
+
|
| 1296 |
+
{trainSplit + testSplit + valSplit !== 100 && (
|
| 1297 |
+
<div className={styles.splitTotal}>
|
| 1298 |
+
<span className={styles.splitTotalError}>Must equal 100%</span>
|
| 1299 |
+
</div>
|
| 1300 |
+
)}
|
| 1301 |
+
</div>
|
| 1302 |
+
)}
|
| 1303 |
+
|
| 1304 |
+
<div className={styles.checkboxesContainer}>
|
| 1305 |
+
<div className="flex items-center gap-3">
|
| 1306 |
+
<Checkbox
|
| 1307 |
+
name="crisis-maps"
|
| 1308 |
+
label="Crisis Maps"
|
| 1309 |
+
value={crisisMapsSelected}
|
| 1310 |
+
onChange={(value, name) => setCrisisMapsSelected(value)}
|
| 1311 |
+
/>
|
| 1312 |
+
</div>
|
| 1313 |
+
|
| 1314 |
+
<div className="flex items-center gap-3">
|
| 1315 |
+
<Checkbox
|
| 1316 |
+
name="drone-images"
|
| 1317 |
+
label="Drone Images"
|
| 1318 |
+
value={droneImagesSelected}
|
| 1319 |
+
onChange={(value, name) => setDroneImagesSelected(value)}
|
| 1320 |
+
/>
|
| 1321 |
+
</div>
|
| 1322 |
+
</div>
|
| 1323 |
+
|
| 1324 |
+
<div className={styles.ratingWarningButtons}>
|
| 1325 |
+
<Button
|
| 1326 |
+
name="confirm-export"
|
| 1327 |
+
onClick={() => {
|
| 1328 |
+
if (!crisisMapsSelected && !droneImagesSelected) {
|
| 1329 |
+
alert('Please select at least one image type to export.');
|
| 1330 |
+
return;
|
| 1331 |
+
}
|
| 1332 |
+
|
| 1333 |
+
// For MapDetailPage, we only export the current image if its type is selected
|
| 1334 |
+
if ((map?.image_type === 'crisis_map' && crisisMapsSelected) ||
|
| 1335 |
+
(map?.image_type === 'drone_image' && droneImagesSelected)) {
|
| 1336 |
+
exportDataset(exportMode);
|
| 1337 |
+
} else {
|
| 1338 |
+
alert('The current image type is not selected for export.');
|
| 1339 |
+
return;
|
| 1340 |
+
}
|
| 1341 |
+
|
| 1342 |
+
setShowExportModal(false);
|
| 1343 |
+
}}
|
| 1344 |
+
>
|
| 1345 |
+
Export Selected
|
| 1346 |
+
</Button>
|
| 1347 |
+
<Button
|
| 1348 |
+
name="cancel-export"
|
| 1349 |
+
variant="tertiary"
|
| 1350 |
+
onClick={() => setShowExportModal(false)}
|
| 1351 |
+
>
|
| 1352 |
+
Cancel
|
| 1353 |
+
</Button>
|
| 1354 |
+
</div>
|
| 1355 |
+
</div>
|
| 1356 |
+
</div>
|
| 1357 |
+
</div>
|
| 1358 |
+
)}
|
| 1359 |
</PageContainer>
|
| 1360 |
);
|
| 1361 |
}
|
frontend/src/pages/UploadPage/UploadPage.module.css
CHANGED
|
@@ -579,7 +579,21 @@
|
|
| 579 |
display: flex;
|
| 580 |
gap: var(--go-ui-spacing-md);
|
| 581 |
justify-content: center;
|
| 582 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 583 |
}
|
| 584 |
|
| 585 |
|
|
|
|
| 579 |
display: flex;
|
| 580 |
gap: var(--go-ui-spacing-md);
|
| 581 |
justify-content: center;
|
| 582 |
+
margin-top: var(--go-ui-spacing-lg);
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
.preprocessingProgress {
|
| 586 |
+
margin-top: var(--go-ui-spacing-lg);
|
| 587 |
+
text-align: center;
|
| 588 |
+
padding: var(--go-ui-spacing-lg);
|
| 589 |
+
background-color: var(--go-ui-color-background-light);
|
| 590 |
+
border-radius: var(--go-ui-border-radius-md);
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
.preprocessingProgress p {
|
| 594 |
+
margin-bottom: var(--go-ui-spacing-md);
|
| 595 |
+
color: var(--go-ui-color-text);
|
| 596 |
+
font-weight: var(--go-ui-font-weight-medium);
|
| 597 |
}
|
| 598 |
|
| 599 |
|
frontend/src/pages/UploadPage/UploadPage.tsx
CHANGED
|
@@ -295,6 +295,12 @@ export default function UploadPage() {
|
|
| 295 |
setDraft('');
|
| 296 |
setShowFallbackNotification(false);
|
| 297 |
setFallbackInfo(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
};
|
| 299 |
const [scores, setScores] = useState({
|
| 300 |
accuracy: 50,
|
|
@@ -315,15 +321,152 @@ export default function UploadPage() {
|
|
| 315 |
reason: string;
|
| 316 |
} | null>(null);
|
| 317 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
|
| 319 |
const onDrop = (e: DragEvent<HTMLDivElement>) => {
|
| 320 |
e.preventDefault();
|
| 321 |
const dropped = e.dataTransfer.files?.[0];
|
| 322 |
-
if (dropped)
|
|
|
|
|
|
|
|
|
|
| 323 |
};
|
| 324 |
|
| 325 |
const onFileChange = (file: File | undefined) => {
|
| 326 |
-
if (file)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
};
|
| 328 |
|
| 329 |
useEffect(() => {
|
|
@@ -394,6 +537,15 @@ export default function UploadPage() {
|
|
| 394 |
if (!mapRes.ok) throw new Error((mapJson.error as string) || 'Upload failed');
|
| 395 |
setImageUrl(mapJson.image_url as string);
|
| 396 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
const mapIdVal = mapJson.image_id as string;
|
| 398 |
if (!mapIdVal) throw new Error('Upload failed: image_id not found');
|
| 399 |
setUploadedImageId(mapIdVal);
|
|
@@ -507,6 +659,15 @@ export default function UploadPage() {
|
|
| 507 |
const json = await readJsonSafely(res);
|
| 508 |
if (!res.ok) throw new Error((json.error as string) || 'Upload failed');
|
| 509 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
const newId = json.image_id as string;
|
| 511 |
setUploadedImageId(newId);
|
| 512 |
setImageUrl(json.image_url as string);
|
|
@@ -1307,6 +1468,70 @@ export default function UploadPage() {
|
|
| 1307 |
</div>
|
| 1308 |
)}
|
| 1309 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1310 |
</div>
|
| 1311 |
</div>
|
| 1312 |
)}
|
|
|
|
| 295 |
setDraft('');
|
| 296 |
setShowFallbackNotification(false);
|
| 297 |
setFallbackInfo(null);
|
| 298 |
+
setShowPreprocessingNotification(false);
|
| 299 |
+
setPreprocessingInfo(null);
|
| 300 |
+
setShowPreprocessingModal(false);
|
| 301 |
+
setPreprocessingFile(null);
|
| 302 |
+
setIsPreprocessing(false);
|
| 303 |
+
setPreprocessingProgress('');
|
| 304 |
};
|
| 305 |
const [scores, setScores] = useState({
|
| 306 |
accuracy: 50,
|
|
|
|
| 321 |
reason: string;
|
| 322 |
} | null>(null);
|
| 323 |
|
| 324 |
+
const [showPreprocessingNotification, setShowPreprocessingNotification] = useState(false);
|
| 325 |
+
const [preprocessingInfo, setPreprocessingInfo] = useState<{
|
| 326 |
+
original_filename: string;
|
| 327 |
+
processed_filename: string;
|
| 328 |
+
original_mime_type: string;
|
| 329 |
+
processed_mime_type: string;
|
| 330 |
+
was_preprocessed: boolean;
|
| 331 |
+
error?: string;
|
| 332 |
+
} | null>(null);
|
| 333 |
+
|
| 334 |
+
// Enhanced preprocessing flow state
|
| 335 |
+
const [showPreprocessingModal, setShowPreprocessingModal] = useState(false);
|
| 336 |
+
const [preprocessingFile, setPreprocessingFile] = useState<File | null>(null);
|
| 337 |
+
const [isPreprocessing, setIsPreprocessing] = useState(false);
|
| 338 |
+
const [preprocessingProgress, setPreprocessingProgress] = useState<string>('');
|
| 339 |
+
|
| 340 |
|
| 341 |
const onDrop = (e: DragEvent<HTMLDivElement>) => {
|
| 342 |
e.preventDefault();
|
| 343 |
const dropped = e.dataTransfer.files?.[0];
|
| 344 |
+
if (dropped) {
|
| 345 |
+
// Use onFileChange to trigger preprocessing detection
|
| 346 |
+
onFileChange(dropped);
|
| 347 |
+
}
|
| 348 |
};
|
| 349 |
|
| 350 |
const onFileChange = (file: File | undefined) => {
|
| 351 |
+
if (file) {
|
| 352 |
+
console.log('File selected:', file.name, 'Type:', file.type, 'Size:', file.size);
|
| 353 |
+
|
| 354 |
+
// Check if file needs preprocessing
|
| 355 |
+
if (needsPreprocessing(file)) {
|
| 356 |
+
console.log('File needs preprocessing, showing modal');
|
| 357 |
+
setPreprocessingFile(file);
|
| 358 |
+
setShowPreprocessingModal(true);
|
| 359 |
+
} else {
|
| 360 |
+
console.log('File does not need preprocessing, setting directly');
|
| 361 |
+
setFile(file);
|
| 362 |
+
}
|
| 363 |
+
}
|
| 364 |
+
};
|
| 365 |
+
|
| 366 |
+
// Check if file needs preprocessing (non-JPEG/PNG)
|
| 367 |
+
const needsPreprocessing = (file: File): boolean => {
|
| 368 |
+
const supportedTypes = ['image/jpeg', 'image/jpg', 'image/png'];
|
| 369 |
+
const supportedExtensions = ['.jpg', '.jpeg', '.png'];
|
| 370 |
+
|
| 371 |
+
// Check MIME type first
|
| 372 |
+
let needsPreprocess = !supportedTypes.includes(file.type);
|
| 373 |
+
|
| 374 |
+
// If MIME type check is inconclusive, check file extension
|
| 375 |
+
if (!needsPreprocess && file.name) {
|
| 376 |
+
const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.'));
|
| 377 |
+
needsPreprocess = !supportedExtensions.includes(fileExtension);
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
console.log('Preprocessing check:', {
|
| 381 |
+
fileName: file.name,
|
| 382 |
+
fileType: file.type,
|
| 383 |
+
fileExtension: file.name ? file.name.toLowerCase().substring(file.name.lastIndexOf('.')) : 'none',
|
| 384 |
+
supportedTypes,
|
| 385 |
+
supportedExtensions,
|
| 386 |
+
needsPreprocess
|
| 387 |
+
});
|
| 388 |
+
|
| 389 |
+
return needsPreprocess;
|
| 390 |
+
};
|
| 391 |
+
|
| 392 |
+
// Handle preprocessing confirmation
|
| 393 |
+
const handlePreprocessingConfirm = async () => {
|
| 394 |
+
if (!preprocessingFile) return;
|
| 395 |
+
|
| 396 |
+
setIsPreprocessing(true);
|
| 397 |
+
setPreprocessingProgress('Starting file conversion...');
|
| 398 |
+
|
| 399 |
+
try {
|
| 400 |
+
// Create FormData for preprocessing
|
| 401 |
+
const formData = new FormData();
|
| 402 |
+
formData.append('file', preprocessingFile);
|
| 403 |
+
formData.append('preprocess_only', 'true'); // Flag to indicate preprocessing only
|
| 404 |
+
|
| 405 |
+
setPreprocessingProgress('Converting file format...');
|
| 406 |
+
|
| 407 |
+
// Call preprocessing endpoint
|
| 408 |
+
const response = await fetch('/api/images/preprocess', {
|
| 409 |
+
method: 'POST',
|
| 410 |
+
body: formData
|
| 411 |
+
});
|
| 412 |
+
|
| 413 |
+
if (!response.ok) {
|
| 414 |
+
throw new Error('Preprocessing failed');
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
const result = await response.json();
|
| 418 |
+
|
| 419 |
+
setPreprocessingProgress('Finalizing conversion...');
|
| 420 |
+
|
| 421 |
+
// Decode base64 content
|
| 422 |
+
const processedContent = atob(result.processed_content);
|
| 423 |
+
const processedBytes = new Uint8Array(processedContent.length);
|
| 424 |
+
for (let i = 0; i < processedContent.length; i++) {
|
| 425 |
+
processedBytes[i] = processedContent.charCodeAt(i);
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
// Create a new File object from the processed data
|
| 429 |
+
const processedFile = new File(
|
| 430 |
+
[processedBytes],
|
| 431 |
+
result.processed_filename,
|
| 432 |
+
{ type: result.processed_mime_type }
|
| 433 |
+
);
|
| 434 |
+
|
| 435 |
+
// Create preview URL
|
| 436 |
+
const previewUrl = URL.createObjectURL(processedFile);
|
| 437 |
+
|
| 438 |
+
// Update the main file state
|
| 439 |
+
setFile(processedFile);
|
| 440 |
+
setPreview(previewUrl);
|
| 441 |
+
|
| 442 |
+
setPreprocessingProgress('Conversion complete!');
|
| 443 |
+
|
| 444 |
+
// Close modal after a brief delay
|
| 445 |
+
setTimeout(() => {
|
| 446 |
+
setShowPreprocessingModal(false);
|
| 447 |
+
setPreprocessingFile(null);
|
| 448 |
+
setIsPreprocessing(false);
|
| 449 |
+
setPreprocessingProgress('');
|
| 450 |
+
}, 1000);
|
| 451 |
+
|
| 452 |
+
} catch (error) {
|
| 453 |
+
console.error('Preprocessing error:', error);
|
| 454 |
+
setPreprocessingProgress('Conversion failed. Please try again.');
|
| 455 |
+
setTimeout(() => {
|
| 456 |
+
setShowPreprocessingModal(false);
|
| 457 |
+
setPreprocessingFile(null);
|
| 458 |
+
setIsPreprocessing(false);
|
| 459 |
+
setPreprocessingProgress('');
|
| 460 |
+
}, 2000);
|
| 461 |
+
}
|
| 462 |
+
};
|
| 463 |
+
|
| 464 |
+
// Handle preprocessing cancellation
|
| 465 |
+
const handlePreprocessingCancel = () => {
|
| 466 |
+
setShowPreprocessingModal(false);
|
| 467 |
+
setPreprocessingFile(null);
|
| 468 |
+
setIsPreprocessing(false);
|
| 469 |
+
setPreprocessingProgress('');
|
| 470 |
};
|
| 471 |
|
| 472 |
useEffect(() => {
|
|
|
|
| 537 |
if (!mapRes.ok) throw new Error((mapJson.error as string) || 'Upload failed');
|
| 538 |
setImageUrl(mapJson.image_url as string);
|
| 539 |
|
| 540 |
+
// Check for preprocessing info and show notification if needed
|
| 541 |
+
if (mapJson.preprocessing_info &&
|
| 542 |
+
typeof mapJson.preprocessing_info === 'object' &&
|
| 543 |
+
'was_preprocessed' in mapJson.preprocessing_info &&
|
| 544 |
+
mapJson.preprocessing_info.was_preprocessed === true) {
|
| 545 |
+
setPreprocessingInfo(mapJson.preprocessing_info as any);
|
| 546 |
+
setShowPreprocessingNotification(true);
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
const mapIdVal = mapJson.image_id as string;
|
| 550 |
if (!mapIdVal) throw new Error('Upload failed: image_id not found');
|
| 551 |
setUploadedImageId(mapIdVal);
|
|
|
|
| 659 |
const json = await readJsonSafely(res);
|
| 660 |
if (!res.ok) throw new Error((json.error as string) || 'Upload failed');
|
| 661 |
|
| 662 |
+
// Check for preprocessing info and show notification if needed
|
| 663 |
+
if (json.preprocessing_info &&
|
| 664 |
+
typeof json.preprocessing_info === 'object' &&
|
| 665 |
+
'was_preprocessed' in json.preprocessing_info &&
|
| 666 |
+
json.preprocessing_info.was_preprocessed === true) {
|
| 667 |
+
setPreprocessingInfo(json.preprocessing_info as any);
|
| 668 |
+
setShowPreprocessingNotification(true);
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
const newId = json.image_id as string;
|
| 672 |
setUploadedImageId(newId);
|
| 673 |
setImageUrl(json.image_url as string);
|
|
|
|
| 1468 |
</div>
|
| 1469 |
)}
|
| 1470 |
|
| 1471 |
+
{/* Image Preprocessing Notification Modal */}
|
| 1472 |
+
{showPreprocessingNotification && preprocessingInfo && (
|
| 1473 |
+
<div className={styles.fullSizeModalOverlay} onClick={() => setShowPreprocessingNotification(false)}>
|
| 1474 |
+
<div className={styles.fullSizeModalContent} onClick={(e) => e.stopPropagation()}>
|
| 1475 |
+
<div className={styles.ratingWarningContent}>
|
| 1476 |
+
<h3 className={styles.ratingWarningTitle}>File Converted</h3>
|
| 1477 |
+
<p className={styles.ratingWarningText}>
|
| 1478 |
+
Your file <strong>{preprocessingInfo.original_filename}</strong> has been converted from
|
| 1479 |
+
<strong> {preprocessingInfo.original_mime_type}</strong> to
|
| 1480 |
+
<strong> {preprocessingInfo.processed_mime_type}</strong> for better compatibility.
|
| 1481 |
+
<br /><br />
|
| 1482 |
+
This process may take a bit longer, but ensures your file works properly in the system.
|
| 1483 |
+
</p>
|
| 1484 |
+
<div className={styles.ratingWarningButtons}>
|
| 1485 |
+
<Button
|
| 1486 |
+
name="close-preprocessing"
|
| 1487 |
+
variant="secondary"
|
| 1488 |
+
onClick={() => setShowPreprocessingNotification(false)}
|
| 1489 |
+
>
|
| 1490 |
+
Got it
|
| 1491 |
+
</Button>
|
| 1492 |
+
</div>
|
| 1493 |
+
</div>
|
| 1494 |
+
</div>
|
| 1495 |
+
</div>
|
| 1496 |
+
)}
|
| 1497 |
+
|
| 1498 |
+
{/* Preprocessing Modal */}
|
| 1499 |
+
{showPreprocessingModal && (
|
| 1500 |
+
<div className={styles.fullSizeModalOverlay} onClick={handlePreprocessingCancel}>
|
| 1501 |
+
<div className={styles.fullSizeModalContent} onClick={(e) => e.stopPropagation()}>
|
| 1502 |
+
<div className={styles.ratingWarningContent}>
|
| 1503 |
+
<h3 className={styles.ratingWarningTitle}>File Conversion Required</h3>
|
| 1504 |
+
<p className={styles.ratingWarningText}>
|
| 1505 |
+
The file you selected is not in a supported format (JPEG or PNG).
|
| 1506 |
+
We will convert it to a compatible format for better compatibility.
|
| 1507 |
+
</p>
|
| 1508 |
+
<div className={styles.ratingWarningButtons}>
|
| 1509 |
+
<Button
|
| 1510 |
+
name="confirm-preprocessing"
|
| 1511 |
+
variant="secondary"
|
| 1512 |
+
onClick={handlePreprocessingConfirm}
|
| 1513 |
+
>
|
| 1514 |
+
Convert File
|
| 1515 |
+
</Button>
|
| 1516 |
+
<Button
|
| 1517 |
+
name="cancel-preprocessing"
|
| 1518 |
+
variant="tertiary"
|
| 1519 |
+
onClick={handlePreprocessingCancel}
|
| 1520 |
+
>
|
| 1521 |
+
Cancel
|
| 1522 |
+
</Button>
|
| 1523 |
+
</div>
|
| 1524 |
+
{isPreprocessing && (
|
| 1525 |
+
<div className={styles.preprocessingProgress}>
|
| 1526 |
+
<p>{preprocessingProgress}</p>
|
| 1527 |
+
<Spinner className="text-ifrcRed" />
|
| 1528 |
+
</div>
|
| 1529 |
+
)}
|
| 1530 |
+
</div>
|
| 1531 |
+
</div>
|
| 1532 |
+
</div>
|
| 1533 |
+
)}
|
| 1534 |
+
|
| 1535 |
</div>
|
| 1536 |
</div>
|
| 1537 |
)}
|
go-web-app-develop/.changeset/README.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Changesets
|
| 2 |
+
|
| 3 |
+
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
| 4 |
+
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
| 5 |
+
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
| 6 |
+
|
| 7 |
+
We have a quick list of common questions to get you started engaging with this project in
|
| 8 |
+
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
go-web-app-develop/.changeset/config.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
|
| 3 |
+
"changelog": "@changesets/cli/changelog",
|
| 4 |
+
"commit": false,
|
| 5 |
+
"fixed": [],
|
| 6 |
+
"linked": [],
|
| 7 |
+
"access": "public",
|
| 8 |
+
"baseBranch": "develop",
|
| 9 |
+
"updateInternalDependencies": "patch",
|
| 10 |
+
"ignore": [],
|
| 11 |
+
"privatePackages": {
|
| 12 |
+
"version": true,
|
| 13 |
+
"tag": true
|
| 14 |
+
}
|
| 15 |
+
}
|
go-web-app-develop/.changeset/lovely-kids-boil.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
"go-web-app": patch
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
Fix use of operational timeframe date in imminent final report form
|
go-web-app-develop/.changeset/pre.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"mode": "pre",
|
| 3 |
+
"tag": "beta",
|
| 4 |
+
"initialVersions": {
|
| 5 |
+
"go-web-app": "7.20.2",
|
| 6 |
+
"go-ui-storybook": "1.0.7",
|
| 7 |
+
"@ifrc-go/ui": "1.5.1"
|
| 8 |
+
},
|
| 9 |
+
"changesets": [
|
| 10 |
+
"lovely-kids-boil",
|
| 11 |
+
"solid-clubs-care",
|
| 12 |
+
"sweet-gifts-cheer",
|
| 13 |
+
"whole-lions-guess"
|
| 14 |
+
]
|
| 15 |
+
}
|
go-web-app-develop/.changeset/solid-clubs-care.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
"go-web-app": minor
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
Add Crisis categorization update date
|
| 6 |
+
|
| 7 |
+
- Add updated date for crisis categorization in emergency page.
|
| 8 |
+
- Add consent checkbox over situational overview in field report form.
|
go-web-app-develop/.changeset/sweet-gifts-cheer.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
"go-web-app": minor
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
Add support for DREF imminent v2 in final report
|
| 6 |
+
|
| 7 |
+
- Add a separate route for the old dref final report form
|
| 8 |
+
- Update dref final report to accomodate imminent v2 changes
|
| 9 |
+
|
go-web-app-develop/.changeset/whole-lions-guess.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
"go-web-app": patch
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
- Fix calculation of Operation End date in Final report form
|
| 6 |
+
- Fix icon position issue in the implementation table of DREF PDF export
|
| 7 |
+
- Update the label for last update date in the crisis categorization pop-up
|
go-web-app-develop/.dockerignore
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Swap files
|
| 2 |
+
*.swp
|
| 3 |
+
|
| 4 |
+
# Byte-compiled / optimized / DLL files
|
| 5 |
+
__pycache__
|
| 6 |
+
*.py[cod]
|
| 7 |
+
*$py.class
|
| 8 |
+
|
| 9 |
+
# C extensions
|
| 10 |
+
*.so
|
| 11 |
+
|
| 12 |
+
# Distribution / packaging
|
| 13 |
+
.Python
|
| 14 |
+
env
|
| 15 |
+
build
|
| 16 |
+
develop-eggs
|
| 17 |
+
dist
|
| 18 |
+
downloads
|
| 19 |
+
eggs
|
| 20 |
+
.eggs
|
| 21 |
+
lib
|
| 22 |
+
lib64
|
| 23 |
+
parts
|
| 24 |
+
sdist
|
| 25 |
+
var
|
| 26 |
+
*.egg-info
|
| 27 |
+
.installed.cfg
|
| 28 |
+
*.egg
|
| 29 |
+
|
| 30 |
+
# PyInstaller
|
| 31 |
+
# Usually these files are written by a python script from a template
|
| 32 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 33 |
+
*.manifest
|
| 34 |
+
*.spec
|
| 35 |
+
|
| 36 |
+
# Installer logs
|
| 37 |
+
pip-log.txt
|
| 38 |
+
pip-delete-this-directory.txt
|
| 39 |
+
|
| 40 |
+
# Unit test / coverage reports
|
| 41 |
+
htmlcov
|
| 42 |
+
.tox
|
| 43 |
+
.coverage
|
| 44 |
+
.coverage.*
|
| 45 |
+
.cache
|
| 46 |
+
nosetests.xml
|
| 47 |
+
coverage.xml
|
| 48 |
+
*,cover
|
| 49 |
+
.hypothesis
|
| 50 |
+
|
| 51 |
+
# Translations
|
| 52 |
+
*.mo
|
| 53 |
+
*.pot
|
| 54 |
+
|
| 55 |
+
# Django stuff:
|
| 56 |
+
*.log
|
| 57 |
+
|
| 58 |
+
# Sphinx documentation
|
| 59 |
+
docs/_build
|
| 60 |
+
|
| 61 |
+
# PyBuilder
|
| 62 |
+
target
|
| 63 |
+
|
| 64 |
+
#Ipython Notebook
|
| 65 |
+
.ipynb_checkpoints
|
| 66 |
+
|
| 67 |
+
# SASS cache
|
| 68 |
+
.sass-cache
|
| 69 |
+
media_test
|
| 70 |
+
|
| 71 |
+
# Rope project settings
|
| 72 |
+
.ropeproject
|
| 73 |
+
|
| 74 |
+
# Logs
|
| 75 |
+
logs
|
| 76 |
+
*.log
|
| 77 |
+
npm-debug.log*
|
| 78 |
+
yarn-debug.log*
|
| 79 |
+
yarn-error.log*
|
| 80 |
+
|
| 81 |
+
# Runtime data
|
| 82 |
+
pids
|
| 83 |
+
*.pid
|
| 84 |
+
*.seed
|
| 85 |
+
*.pid.lock
|
| 86 |
+
|
| 87 |
+
# Directory for instrumented libs generated by jscoverage/JSCover
|
| 88 |
+
lib-cov
|
| 89 |
+
|
| 90 |
+
# Coverage directory used by tools like istanbul
|
| 91 |
+
coverage
|
| 92 |
+
|
| 93 |
+
# nyc test coverage
|
| 94 |
+
.nyc_output
|
| 95 |
+
|
| 96 |
+
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
| 97 |
+
.grunt
|
| 98 |
+
|
| 99 |
+
# Bower dependency directory (https://bower.io/)
|
| 100 |
+
bower_components
|
| 101 |
+
|
| 102 |
+
# node-waf configuration
|
| 103 |
+
.lock-wscript
|
| 104 |
+
|
| 105 |
+
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
| 106 |
+
build/Release
|
| 107 |
+
|
| 108 |
+
# Dependency directories
|
| 109 |
+
node_modules
|
| 110 |
+
jspm_packages
|
| 111 |
+
|
| 112 |
+
# Typescript v1 declaration files
|
| 113 |
+
typings
|
| 114 |
+
|
| 115 |
+
# Optional npm cache directory
|
| 116 |
+
.npm
|
| 117 |
+
|
| 118 |
+
# Optional eslint cache
|
| 119 |
+
.eslintcache
|
| 120 |
+
|
| 121 |
+
# Optional REPL history
|
| 122 |
+
.node_repl_history
|
| 123 |
+
|
| 124 |
+
# Output of 'npm pack'
|
| 125 |
+
*.tgz
|
| 126 |
+
|
| 127 |
+
# Yarn Integrity file
|
| 128 |
+
.yarn-integrity
|
| 129 |
+
|
| 130 |
+
# dotenv environment variables file
|
| 131 |
+
.env
|
| 132 |
+
.env*
|
| 133 |
+
|
| 134 |
+
# Sensitive Deploy Files
|
| 135 |
+
deploy/eb/
|
| 136 |
+
|
| 137 |
+
# tox
|
| 138 |
+
./.tox
|
| 139 |
+
|
| 140 |
+
# Helm
|
| 141 |
+
.helm-charts/
|
| 142 |
+
|
| 143 |
+
# Docker
|
| 144 |
+
Dockerfile
|
| 145 |
+
.dockerignore
|
| 146 |
+
|
| 147 |
+
# git
|
| 148 |
+
.gitignore
|
go-web-app-develop/.github/ISSUE_TEMPLATE/01_bug_report.yml
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "Bug Report"
|
| 2 |
+
description: "Report a technical or visual issue."
|
| 3 |
+
labels: ["type: bug"]
|
| 4 |
+
type: "Bug"
|
| 5 |
+
body:
|
| 6 |
+
- type: markdown
|
| 7 |
+
attributes:
|
| 8 |
+
value: |
|
| 9 |
+
**Bug Report**
|
| 10 |
+
Please fill out the form below with as much detail as possible.
|
| 11 |
+
If the issue is visual, screenshots or videos are greatly appreciated.
|
| 12 |
+
**Please review [our guide on reporting bugs](https://github.com/IFRCGo/go-web-app/blob/develop/CONTRIBUTING.md#reporting-bugs) before opening a new issue.**
|
| 13 |
+
|
| 14 |
+
- type: input
|
| 15 |
+
attributes:
|
| 16 |
+
label: "Page URL"
|
| 17 |
+
description: "The URL of the page where you encountered the issue."
|
| 18 |
+
placeholder: "https://go.ifrc.org/"
|
| 19 |
+
validations:
|
| 20 |
+
required: true
|
| 21 |
+
|
| 22 |
+
- type: dropdown
|
| 23 |
+
attributes:
|
| 24 |
+
label: "Environment"
|
| 25 |
+
description: "Please select the environment where the bug occurred."
|
| 26 |
+
options:
|
| 27 |
+
- "Alpha"
|
| 28 |
+
- "Staging"
|
| 29 |
+
- "Production"
|
| 30 |
+
validations:
|
| 31 |
+
required: true
|
| 32 |
+
|
| 33 |
+
- type: input
|
| 34 |
+
attributes:
|
| 35 |
+
label: "Browser"
|
| 36 |
+
description: "Which browser are you using? (e.g., Chrome, Firefox, Safari)"
|
| 37 |
+
placeholder: "Chrome"
|
| 38 |
+
validations:
|
| 39 |
+
required: true
|
| 40 |
+
|
| 41 |
+
- type: textarea
|
| 42 |
+
attributes:
|
| 43 |
+
label: "Steps to Reproduce the Issue"
|
| 44 |
+
description: |
|
| 45 |
+
Please describe the issue in detail, including:
|
| 46 |
+
1. What actions led to the issue?
|
| 47 |
+
2. If possible, attach screenshots or videos demonstrating the problem.
|
| 48 |
+
placeholder: |
|
| 49 |
+
1. I clicked on...
|
| 50 |
+
2. [Attach screenshots/videos if available]
|
| 51 |
+
validations:
|
| 52 |
+
required: true
|
| 53 |
+
|
| 54 |
+
- type: textarea
|
| 55 |
+
attributes:
|
| 56 |
+
label: "Expected Behavior"
|
| 57 |
+
description: "Describe what you expected to happen."
|
| 58 |
+
placeholder: "I expected the page to..."
|
| 59 |
+
validations:
|
| 60 |
+
required: true
|
| 61 |
+
|
| 62 |
+
- type: textarea
|
| 63 |
+
attributes:
|
| 64 |
+
label: "Actual Behavior"
|
| 65 |
+
description: "Describe what actually happened, including any error messages."
|
| 66 |
+
placeholder: "Instead, I saw..."
|
| 67 |
+
validations:
|
| 68 |
+
required: true
|
| 69 |
+
|
| 70 |
+
- type: dropdown
|
| 71 |
+
attributes:
|
| 72 |
+
label: "Priority"
|
| 73 |
+
description: "How urgent is this issue?"
|
| 74 |
+
options:
|
| 75 |
+
- "Low (Minor inconvenience)"
|
| 76 |
+
- "Medium (Affects functionality, but there is a workaround)"
|
| 77 |
+
- "High (Major functionality is broken)"
|
| 78 |
+
- "Critical (Site is unusable)"
|
| 79 |
+
validations:
|
| 80 |
+
required: false
|
| 81 |
+
|
| 82 |
+
- type: textarea
|
| 83 |
+
attributes:
|
| 84 |
+
label: "Additional Context (Optional)"
|
| 85 |
+
description: |
|
| 86 |
+
Provide any extra details, such as:
|
| 87 |
+
- Related links.
|
| 88 |
+
- Previous occurrences of this issue.
|
| 89 |
+
- Workarounds you have tried.
|
| 90 |
+
placeholder: "This issue also happened on [link]."
|
| 91 |
+
validations:
|
| 92 |
+
required: false
|
go-web-app-develop/.github/ISSUE_TEMPLATE/02_feature_request.yml
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "Feature Request"
|
| 2 |
+
description: "Suggest a new idea or enhancement."
|
| 3 |
+
labels: ["type: feature-request"]
|
| 4 |
+
type: "Feature"
|
| 5 |
+
body:
|
| 6 |
+
- type: markdown
|
| 7 |
+
attributes:
|
| 8 |
+
value: |
|
| 9 |
+
**Feature Request**
|
| 10 |
+
Thank you for suggesting a new feature!
|
| 11 |
+
Please provide as much detail as possible to help us understand and evaluate your idea.
|
| 12 |
+
**Please review [our guide on suggesting enhancements](https://github.com/IFRCGo/go-web-app/blob/develop/CONTRIBUTING.md#suggesting-enhancements).**
|
| 13 |
+
|
| 14 |
+
- type: textarea
|
| 15 |
+
attributes:
|
| 16 |
+
label: "Feature Description"
|
| 17 |
+
description: |
|
| 18 |
+
Describe your feature request in detail, including:
|
| 19 |
+
- What the feature is.
|
| 20 |
+
- Why it is needed and how it will improve the project.
|
| 21 |
+
- How it will benefit users (e.g., As a user, I want to [do something] so that [desired outcome].).
|
| 22 |
+
placeholder: "As a user, I want to filter search results by date so that I can quickly find recent information."
|
| 23 |
+
validations:
|
| 24 |
+
required: true
|
| 25 |
+
|
| 26 |
+
- type: textarea
|
| 27 |
+
attributes:
|
| 28 |
+
label: "Additional Context"
|
| 29 |
+
description: |
|
| 30 |
+
Provide any extra details or supporting information, such as:
|
| 31 |
+
- Links to references or related resources.
|
| 32 |
+
- Examples from other projects or systems.
|
| 33 |
+
- Screenshots, mockups, or diagrams.
|
| 34 |
+
*Tip: You can attach files by clicking here and dragging them in.*
|
| 35 |
+
placeholder: |
|
| 36 |
+
Here's a link to a similar feature in another project: [link].
|
| 37 |
+
I've also attached a mockup of what this could look like.
|
| 38 |
+
validations:
|
| 39 |
+
required: false
|
go-web-app-develop/.github/ISSUE_TEMPLATE/03_epic_request.yml
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "Epic"
|
| 2 |
+
description: "Track a larger initiative with multiple related tasks and deliverables."
|
| 3 |
+
labels: ["type: epic"]
|
| 4 |
+
type: "Feature"
|
| 5 |
+
body:
|
| 6 |
+
- type: markdown
|
| 7 |
+
attributes:
|
| 8 |
+
value: |
|
| 9 |
+
**Epic**
|
| 10 |
+
Use this to define a large, overarching initiative.
|
| 11 |
+
**Please review [our guide on suggesting enhancements](https://github.com/IFRCGo/go-web-app/blob/develop/CONTRIBUTING.md#suggesting-enhancements).**
|
| 12 |
+
|
| 13 |
+
- type: textarea
|
| 14 |
+
attributes:
|
| 15 |
+
label: "Epic Summary"
|
| 16 |
+
description: |
|
| 17 |
+
Provide a clear and concise summary of the epic.
|
| 18 |
+
- What is this epic about?
|
| 19 |
+
- What problem does it solve or what goal does it achieve?
|
| 20 |
+
- How does it align with the project’s objectives?
|
| 21 |
+
placeholder: |
|
| 22 |
+
Example:
|
| 23 |
+
This epic focuses on implementing a new feature.
|
| 24 |
+
validations:
|
| 25 |
+
required: true
|
| 26 |
+
|
| 27 |
+
- type: textarea
|
| 28 |
+
attributes:
|
| 29 |
+
label: "Additional Context or Resources"
|
| 30 |
+
description: "Provide any additional information, links, or resources that will help the team understand and execute this epic."
|
| 31 |
+
placeholder: |
|
| 32 |
+
Examples:
|
| 33 |
+
- Link to design mockups: [link]
|
| 34 |
+
- Technical specs document: [link]
|
| 35 |
+
- Reference to similar features: [link]
|
| 36 |
+
validations:
|
| 37 |
+
required: false
|
go-web-app-develop/.github/ISSUE_TEMPLATE/config.yml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
blank_issues_enabled: true
|
| 2 |
+
contact_links:
|
| 3 |
+
- name: Documentation
|
| 4 |
+
url: https://go-wiki.ifrc.org/en/home
|
| 5 |
+
about: Please consult the wiki to know more about IFRC GO.
|
go-web-app-develop/.github/dependabot.yml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: 2
|
| 2 |
+
updates:
|
| 3 |
+
- package-ecosystem: npm
|
| 4 |
+
directory: /
|
| 5 |
+
schedule:
|
| 6 |
+
interval: weekly
|
| 7 |
+
groups:
|
| 8 |
+
eslint:
|
| 9 |
+
patterns:
|
| 10 |
+
- "*eslint*"
|
| 11 |
+
vite:
|
| 12 |
+
patterns:
|
| 13 |
+
- "*vite*"
|
| 14 |
+
postcss:
|
| 15 |
+
patterns:
|
| 16 |
+
- "*postcss*"
|
| 17 |
+
stylelint:
|
| 18 |
+
patterns:
|
| 19 |
+
- "*stylelint*"
|
| 20 |
+
all-other-dependencies:
|
| 21 |
+
patterns:
|
| 22 |
+
- "*"
|
| 23 |
+
exclude-patterns:
|
| 24 |
+
- "*eslint*"
|
| 25 |
+
- "*vite*"
|
| 26 |
+
- "*postcss*"
|
| 27 |
+
- "*stylelint*"
|
go-web-app-develop/.github/pull_request_template.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## Summary
|
| 2 |
+
|
| 3 |
+
Provide a brief description of what this PR addresses and its purpose.
|
| 4 |
+
|
| 5 |
+
## Addresses
|
| 6 |
+
|
| 7 |
+
* Issue(s): *List related issues or tickets.*
|
| 8 |
+
|
| 9 |
+
## Depends On
|
| 10 |
+
|
| 11 |
+
* Other PRs or Dependencies: *List PRs or dependencies this PR relies on.*
|
| 12 |
+
|
| 13 |
+
## Changes
|
| 14 |
+
|
| 15 |
+
* Detailed list or prose of changes
|
| 16 |
+
* Breaking changes
|
| 17 |
+
* Changes to configurations
|
| 18 |
+
|
| 19 |
+
## This PR Ensures:
|
| 20 |
+
|
| 21 |
+
* \[ ] No typos or grammatical errors
|
| 22 |
+
* \[ ] No conflict markers left in the code
|
| 23 |
+
* \[ ] No unwanted comments, temporary files, or auto-generated files
|
| 24 |
+
* \[ ] No inclusion of secret keys or sensitive data
|
| 25 |
+
* \[ ] No `console.log` statements meant for debugging
|
| 26 |
+
* \[ ] All CI checks have passed
|
| 27 |
+
|
| 28 |
+
## Additional Notes
|
| 29 |
+
|
| 30 |
+
*Optional: Add any other relevant context, screenshots, or details here.*
|
go-web-app-develop/.github/workflows/add-issue-to-backlog.yml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Add issues to Backlog
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
issues:
|
| 5 |
+
types:
|
| 6 |
+
- opened
|
| 7 |
+
|
| 8 |
+
jobs:
|
| 9 |
+
add-to-project:
|
| 10 |
+
name: Add issue to project
|
| 11 |
+
runs-on: ubuntu-latest
|
| 12 |
+
steps:
|
| 13 |
+
- uses: actions/[email protected]
|
| 14 |
+
with:
|
| 15 |
+
project-url: https://github.com/orgs/IFRCGo/projects/12
|
| 16 |
+
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
|
go-web-app-develop/.github/workflows/chromatic.yml
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: 'Chromatic'
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
pull_request:
|
| 5 |
+
push:
|
| 6 |
+
branches:
|
| 7 |
+
- develop
|
| 8 |
+
|
| 9 |
+
concurrency:
|
| 10 |
+
group: ${{ github.workflow }}-${{ github.ref }}-chromatic
|
| 11 |
+
cancel-in-progress: true
|
| 12 |
+
|
| 13 |
+
permissions:
|
| 14 |
+
actions: write
|
| 15 |
+
contents: read
|
| 16 |
+
pages: write
|
| 17 |
+
id-token: write
|
| 18 |
+
|
| 19 |
+
jobs:
|
| 20 |
+
changed-files:
|
| 21 |
+
name: Check for changed files
|
| 22 |
+
runs-on: ubuntu-latest
|
| 23 |
+
outputs:
|
| 24 |
+
all_changed_files: ${{ steps.changed-files.outputs.all_changed_files }}
|
| 25 |
+
any_changed: ${{ steps.changed-files.outputs.any_changed }}
|
| 26 |
+
steps:
|
| 27 |
+
- uses: actions/checkout@v4
|
| 28 |
+
with:
|
| 29 |
+
fetch-depth: 0
|
| 30 |
+
- name: Get changed files
|
| 31 |
+
id: changed-files
|
| 32 |
+
uses: tj-actions/changed-files@v44
|
| 33 |
+
with:
|
| 34 |
+
files: |
|
| 35 |
+
packages/ui/**
|
| 36 |
+
packages/go-ui-storybook/**
|
| 37 |
+
ui:
|
| 38 |
+
name: Build UI Library
|
| 39 |
+
environment: 'test'
|
| 40 |
+
runs-on: ubuntu-latest
|
| 41 |
+
needs: [changed-files]
|
| 42 |
+
if: ${{ needs.changed-files.outputs.any_changed == 'true' }}
|
| 43 |
+
defaults:
|
| 44 |
+
run:
|
| 45 |
+
working-directory: packages/ui
|
| 46 |
+
steps:
|
| 47 |
+
- uses: actions/checkout@v4
|
| 48 |
+
with:
|
| 49 |
+
fetch-depth: 0
|
| 50 |
+
- name: Install pnpm
|
| 51 |
+
uses: pnpm/action-setup@v4
|
| 52 |
+
- name: Install Node.js
|
| 53 |
+
uses: actions/setup-node@v4
|
| 54 |
+
with:
|
| 55 |
+
node-version: 20
|
| 56 |
+
cache: 'pnpm'
|
| 57 |
+
- name: Install dependencies
|
| 58 |
+
run: pnpm install
|
| 59 |
+
- name: Typecheck
|
| 60 |
+
run: pnpm typecheck
|
| 61 |
+
- name: Lint CSS
|
| 62 |
+
run: pnpm lint:css
|
| 63 |
+
- name: Lint JS
|
| 64 |
+
run: pnpm lint:js
|
| 65 |
+
- name: build UI library
|
| 66 |
+
run: pnpm build
|
| 67 |
+
- uses: actions/upload-artifact@v4
|
| 68 |
+
with:
|
| 69 |
+
name: ui-build
|
| 70 |
+
path: packages/ui/dist
|
| 71 |
+
chromatic:
|
| 72 |
+
name: Chromatic Deploy
|
| 73 |
+
runs-on: ubuntu-latest
|
| 74 |
+
needs: [ui]
|
| 75 |
+
steps:
|
| 76 |
+
- uses: actions/checkout@v4
|
| 77 |
+
with:
|
| 78 |
+
fetch-depth: 0
|
| 79 |
+
- name: Install pnpm
|
| 80 |
+
uses: pnpm/action-setup@v4
|
| 81 |
+
- name: Install Node.js
|
| 82 |
+
uses: actions/setup-node@v4
|
| 83 |
+
with:
|
| 84 |
+
node-version: 20
|
| 85 |
+
cache: 'pnpm'
|
| 86 |
+
- name: Install dependencies
|
| 87 |
+
run: pnpm install
|
| 88 |
+
- uses: actions/download-artifact@v4
|
| 89 |
+
with:
|
| 90 |
+
name: ui-build
|
| 91 |
+
path: packages/ui/dist
|
| 92 |
+
- name: Run Chromatic
|
| 93 |
+
uses: chromaui/action@v1
|
| 94 |
+
with:
|
| 95 |
+
exitZeroOnChanges: true
|
| 96 |
+
exitOnceUploaded: true
|
| 97 |
+
onlyChanged: true
|
| 98 |
+
skip: "@(renovate/**|dependabot/**)"
|
| 99 |
+
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
| 100 |
+
token: ${{ secrets.GITHUB_TOKEN }}
|
| 101 |
+
autoAcceptChanges: "develop"
|
| 102 |
+
workingDir: packages/go-ui-storybook
|
| 103 |
+
github-pages:
|
| 104 |
+
name: Deploy to Github Pages
|
| 105 |
+
runs-on: ubuntu-latest
|
| 106 |
+
needs: [ui]
|
| 107 |
+
steps:
|
| 108 |
+
- uses: actions/checkout@v4
|
| 109 |
+
with:
|
| 110 |
+
fetch-depth: 0
|
| 111 |
+
- name: Install pnpm
|
| 112 |
+
uses: pnpm/action-setup@v4
|
| 113 |
+
- name: Install Node.js
|
| 114 |
+
uses: actions/setup-node@v4
|
| 115 |
+
with:
|
| 116 |
+
node-version: 20
|
| 117 |
+
cache: 'pnpm'
|
| 118 |
+
- uses: actions/download-artifact@v4
|
| 119 |
+
with:
|
| 120 |
+
name: ui-build
|
| 121 |
+
path: packages/ui/dist
|
| 122 |
+
- uses: bitovi/[email protected]
|
| 123 |
+
with:
|
| 124 |
+
install_command: pnpm install
|
| 125 |
+
build_command: pnpm build-storybook
|
| 126 |
+
path: packages/go-ui-storybook/storybook-static
|
| 127 |
+
checkout: false
|
go-web-app-develop/.github/workflows/ci.yml
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: CI
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
pull_request:
|
| 5 |
+
push:
|
| 6 |
+
branches:
|
| 7 |
+
- 'develop'
|
| 8 |
+
|
| 9 |
+
env:
|
| 10 |
+
APP_ADMIN_URL: ${{ vars.APP_ADMIN_URL }}
|
| 11 |
+
APP_API_ENDPOINT: ${{ vars.APP_API_ENDPOINT }}
|
| 12 |
+
APP_ENVIRONMENT: ${{ vars.APP_ENVIRONMENT }}
|
| 13 |
+
APP_MAPBOX_ACCESS_TOKEN: ${{ vars.APP_MAPBOX_ACCESS_TOKEN }}
|
| 14 |
+
APP_RISK_ADMIN_URL: ${{ vars.APP_RISK_ADMIN_URL }}
|
| 15 |
+
APP_RISK_API_ENDPOINT: ${{ vars.APP_RISK_API_ENDPOINT }}
|
| 16 |
+
APP_SENTRY_DSN: ${{ vars.APP_SENTRY_DSN }}
|
| 17 |
+
APP_SENTRY_NORMALIZE_DEPTH: ${{ vars.APP_SENTRY_NORMALIZE_DEPTH }}
|
| 18 |
+
APP_SENTRY_TRACES_SAMPLE_RATE: ${{ vars.APP_SENTRY_TRACES_SAMPLE_RATE }}
|
| 19 |
+
APP_SHOW_ENV_BANNER: ${{ vars.APP_SHOW_ENV_BANNER }}
|
| 20 |
+
APP_TINY_API_KEY: ${{ vars.APP_TINY_API_KEY }}
|
| 21 |
+
APP_TITLE: ${{ vars.APP_TITLE }}
|
| 22 |
+
GITHUB_WORKFLOW: true
|
| 23 |
+
|
| 24 |
+
concurrency:
|
| 25 |
+
group: ${{ github.workflow }}-${{ github.ref }}
|
| 26 |
+
cancel-in-progress: true
|
| 27 |
+
|
| 28 |
+
jobs:
|
| 29 |
+
ui:
|
| 30 |
+
name: Build UI Library
|
| 31 |
+
environment: 'test'
|
| 32 |
+
runs-on: ubuntu-latest
|
| 33 |
+
defaults:
|
| 34 |
+
run:
|
| 35 |
+
working-directory: packages/ui
|
| 36 |
+
steps:
|
| 37 |
+
- uses: actions/checkout@v4
|
| 38 |
+
- name: Install pnpm
|
| 39 |
+
uses: pnpm/action-setup@v4
|
| 40 |
+
- name: Install Node.js
|
| 41 |
+
uses: actions/setup-node@v4
|
| 42 |
+
with:
|
| 43 |
+
node-version: 20
|
| 44 |
+
cache: 'pnpm'
|
| 45 |
+
- name: Install dependencies
|
| 46 |
+
run: pnpm install
|
| 47 |
+
|
| 48 |
+
- name: Typecheck
|
| 49 |
+
run: pnpm typecheck
|
| 50 |
+
|
| 51 |
+
- name: Lint CSS
|
| 52 |
+
run: pnpm lint:css
|
| 53 |
+
|
| 54 |
+
- name: Lint JS
|
| 55 |
+
run: pnpm lint:js
|
| 56 |
+
|
| 57 |
+
- name: Build
|
| 58 |
+
run: pnpm build
|
| 59 |
+
|
| 60 |
+
- uses: actions/upload-artifact@v4
|
| 61 |
+
with:
|
| 62 |
+
name: ui-build
|
| 63 |
+
path: packages/ui/dist
|
| 64 |
+
|
| 65 |
+
test:
|
| 66 |
+
name: Run tests
|
| 67 |
+
environment: 'test'
|
| 68 |
+
runs-on: ubuntu-latest
|
| 69 |
+
defaults:
|
| 70 |
+
run:
|
| 71 |
+
working-directory: app
|
| 72 |
+
needs: [ui]
|
| 73 |
+
steps:
|
| 74 |
+
- uses: actions/checkout@v4
|
| 75 |
+
- name: Install pnpm
|
| 76 |
+
uses: pnpm/action-setup@v4
|
| 77 |
+
- name: Install Node.js
|
| 78 |
+
uses: actions/setup-node@v4
|
| 79 |
+
with:
|
| 80 |
+
node-version: 20
|
| 81 |
+
cache: 'pnpm'
|
| 82 |
+
- name: Install dependencies
|
| 83 |
+
run: pnpm install
|
| 84 |
+
|
| 85 |
+
- uses: actions/download-artifact@v4
|
| 86 |
+
with:
|
| 87 |
+
name: ui-build
|
| 88 |
+
path: packages/ui/dist
|
| 89 |
+
|
| 90 |
+
- name: Run test
|
| 91 |
+
run: pnpm test
|
| 92 |
+
|
| 93 |
+
translation:
|
| 94 |
+
continue-on-error: true
|
| 95 |
+
name: Identify error with translation files
|
| 96 |
+
runs-on: ubuntu-latest
|
| 97 |
+
defaults:
|
| 98 |
+
run:
|
| 99 |
+
working-directory: app
|
| 100 |
+
needs: [ui]
|
| 101 |
+
steps:
|
| 102 |
+
- uses: actions/checkout@v4
|
| 103 |
+
- name: Install pnpm
|
| 104 |
+
uses: pnpm/action-setup@v4
|
| 105 |
+
- name: Install Node.js
|
| 106 |
+
uses: actions/setup-node@v4
|
| 107 |
+
with:
|
| 108 |
+
node-version: 20
|
| 109 |
+
cache: 'pnpm'
|
| 110 |
+
- name: Install dependencies
|
| 111 |
+
run: pnpm install
|
| 112 |
+
|
| 113 |
+
- uses: actions/download-artifact@v4
|
| 114 |
+
with:
|
| 115 |
+
name: ui-build
|
| 116 |
+
path: packages/ui/dist
|
| 117 |
+
|
| 118 |
+
- name: Identify error with translation files
|
| 119 |
+
run: pnpm lint:translation
|
| 120 |
+
|
| 121 |
+
translation-migrations:
|
| 122 |
+
if: |
|
| 123 |
+
(github.event_name == 'pull_request' && github.base_ref == 'develop') ||
|
| 124 |
+
(github.event_name == 'push' && github.ref == 'refs/heads/develop')
|
| 125 |
+
continue-on-error: true
|
| 126 |
+
name: Identify if translation migrations need to be generated
|
| 127 |
+
runs-on: ubuntu-latest
|
| 128 |
+
defaults:
|
| 129 |
+
run:
|
| 130 |
+
working-directory: app
|
| 131 |
+
needs: [ui]
|
| 132 |
+
steps:
|
| 133 |
+
- uses: actions/checkout@v4
|
| 134 |
+
- name: Install pnpm
|
| 135 |
+
uses: pnpm/action-setup@v4
|
| 136 |
+
- name: Install Node.js
|
| 137 |
+
uses: actions/setup-node@v4
|
| 138 |
+
with:
|
| 139 |
+
node-version: 20
|
| 140 |
+
cache: 'pnpm'
|
| 141 |
+
- name: Install dependencies
|
| 142 |
+
run: pnpm install
|
| 143 |
+
|
| 144 |
+
- uses: actions/download-artifact@v4
|
| 145 |
+
with:
|
| 146 |
+
name: ui-build
|
| 147 |
+
path: packages/ui/dist
|
| 148 |
+
|
| 149 |
+
- name: Identify if translation migrations need to be generated
|
| 150 |
+
run: |
|
| 151 |
+
if pnpm translatte:generate; then
|
| 152 |
+
# The step should fail if generation is possible
|
| 153 |
+
exit 1
|
| 154 |
+
fi
|
| 155 |
+
|
| 156 |
+
unused:
|
| 157 |
+
name: Identify unused files
|
| 158 |
+
runs-on: ubuntu-latest
|
| 159 |
+
needs: [ui]
|
| 160 |
+
steps:
|
| 161 |
+
- uses: actions/checkout@v4
|
| 162 |
+
- name: Install pnpm
|
| 163 |
+
uses: pnpm/action-setup@v4
|
| 164 |
+
- name: Install Node.js
|
| 165 |
+
uses: actions/setup-node@v4
|
| 166 |
+
with:
|
| 167 |
+
node-version: 20
|
| 168 |
+
cache: 'pnpm'
|
| 169 |
+
- name: Install dependencies
|
| 170 |
+
run: pnpm install
|
| 171 |
+
|
| 172 |
+
- name: Initialize types
|
| 173 |
+
run: pnpm initialize:type
|
| 174 |
+
working-directory: app
|
| 175 |
+
|
| 176 |
+
- name: Identify unused files
|
| 177 |
+
run: pnpm lint:unused
|
| 178 |
+
|
| 179 |
+
lint:
|
| 180 |
+
name: Lint JS
|
| 181 |
+
runs-on: ubuntu-latest
|
| 182 |
+
defaults:
|
| 183 |
+
run:
|
| 184 |
+
working-directory: app
|
| 185 |
+
needs: [ui]
|
| 186 |
+
steps:
|
| 187 |
+
- uses: actions/checkout@v4
|
| 188 |
+
- name: Install pnpm
|
| 189 |
+
uses: pnpm/action-setup@v4
|
| 190 |
+
- name: Install Node.js
|
| 191 |
+
uses: actions/setup-node@v4
|
| 192 |
+
with:
|
| 193 |
+
node-version: 20
|
| 194 |
+
cache: 'pnpm'
|
| 195 |
+
- name: Install dependencies
|
| 196 |
+
run: pnpm install
|
| 197 |
+
|
| 198 |
+
- uses: actions/download-artifact@v4
|
| 199 |
+
with:
|
| 200 |
+
name: ui-build
|
| 201 |
+
path: packages/ui/dist
|
| 202 |
+
|
| 203 |
+
- name: Lint JS
|
| 204 |
+
run: pnpm lint:js
|
| 205 |
+
|
| 206 |
+
lint-css:
|
| 207 |
+
name: Lint CSS
|
| 208 |
+
runs-on: ubuntu-latest
|
| 209 |
+
defaults:
|
| 210 |
+
run:
|
| 211 |
+
working-directory: app
|
| 212 |
+
needs: [ui]
|
| 213 |
+
steps:
|
| 214 |
+
- uses: actions/checkout@v4
|
| 215 |
+
- name: Install pnpm
|
| 216 |
+
uses: pnpm/action-setup@v4
|
| 217 |
+
- name: Install Node.js
|
| 218 |
+
uses: actions/setup-node@v4
|
| 219 |
+
with:
|
| 220 |
+
node-version: 20
|
| 221 |
+
cache: 'pnpm'
|
| 222 |
+
- name: Install dependencies
|
| 223 |
+
run: pnpm install
|
| 224 |
+
|
| 225 |
+
- uses: actions/download-artifact@v4
|
| 226 |
+
with:
|
| 227 |
+
name: ui-build
|
| 228 |
+
path: packages/ui/dist
|
| 229 |
+
|
| 230 |
+
- name: Lint CSS
|
| 231 |
+
run: pnpm lint:css
|
| 232 |
+
|
| 233 |
+
# FIXME: Identify a way to generate schema before we run typecheck
|
| 234 |
+
# typecheck:
|
| 235 |
+
# name: Typecheck
|
| 236 |
+
# runs-on: ubuntu-latest
|
| 237 |
+
# steps:
|
| 238 |
+
# - uses: actions/checkout@v4
|
| 239 |
+
# - name: Install pnpm
|
| 240 |
+
# uses: pnpm/action-setup@v4
|
| 241 |
+
# - name: Install Node.js
|
| 242 |
+
# uses: actions/setup-node@v4
|
| 243 |
+
# with:
|
| 244 |
+
# node-version: 20
|
| 245 |
+
# cache: 'pnpm'
|
| 246 |
+
# - name: Install dependencies
|
| 247 |
+
# run: pnpm install
|
| 248 |
+
#
|
| 249 |
+
# - name: Typecheck
|
| 250 |
+
# run: pnpm typecheck
|
| 251 |
+
|
| 252 |
+
typos:
|
| 253 |
+
name: Spell Check with Typos
|
| 254 |
+
runs-on: ubuntu-latest
|
| 255 |
+
steps:
|
| 256 |
+
- name: Checkout Actions Repository
|
| 257 |
+
uses: actions/checkout@v4
|
| 258 |
+
|
| 259 |
+
- name: Check spelling
|
| 260 |
+
uses: crate-ci/[email protected]
|
| 261 |
+
|
| 262 |
+
build:
|
| 263 |
+
name: Build GO Web App
|
| 264 |
+
environment: 'test'
|
| 265 |
+
runs-on: ubuntu-latest
|
| 266 |
+
defaults:
|
| 267 |
+
run:
|
| 268 |
+
working-directory: app
|
| 269 |
+
needs: [lint, lint-css, test, ui]
|
| 270 |
+
steps:
|
| 271 |
+
- uses: actions/checkout@v4
|
| 272 |
+
- name: Install pnpm
|
| 273 |
+
uses: pnpm/action-setup@v4
|
| 274 |
+
- name: Install Node.js
|
| 275 |
+
uses: actions/setup-node@v4
|
| 276 |
+
with:
|
| 277 |
+
node-version: 20
|
| 278 |
+
cache: 'pnpm'
|
| 279 |
+
- name: Install dependencies
|
| 280 |
+
run: pnpm install
|
| 281 |
+
|
| 282 |
+
- uses: actions/download-artifact@v4
|
| 283 |
+
with:
|
| 284 |
+
name: ui-build
|
| 285 |
+
path: packages/ui/dist
|
| 286 |
+
|
| 287 |
+
- name: Build
|
| 288 |
+
run: pnpm build
|
| 289 |
+
|
| 290 |
+
validate_helm:
|
| 291 |
+
name: Validate Helm
|
| 292 |
+
runs-on: ubuntu-latest
|
| 293 |
+
|
| 294 |
+
steps:
|
| 295 |
+
- uses: actions/checkout@main
|
| 296 |
+
|
| 297 |
+
- name: Install Helm
|
| 298 |
+
uses: azure/setup-helm@v4
|
| 299 |
+
|
| 300 |
+
- name: Helm lint
|
| 301 |
+
run: helm lint ./nginx-serve/helm --values ./nginx-serve/helm/values-test.yaml
|
| 302 |
+
|
| 303 |
+
- name: Helm template
|
| 304 |
+
run: helm template ./nginx-serve/helm --values ./nginx-serve/helm/values-test.yaml
|
go-web-app-develop/.github/workflows/publish-nginx-serve.yml
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Publish Helm
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
workflow_dispatch:
|
| 5 |
+
push:
|
| 6 |
+
branches:
|
| 7 |
+
- develop
|
| 8 |
+
- project/*
|
| 9 |
+
|
| 10 |
+
permissions:
|
| 11 |
+
packages: write
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
jobs:
|
| 15 |
+
publish_image:
|
| 16 |
+
name: Publish Docker Image
|
| 17 |
+
runs-on: ubuntu-latest
|
| 18 |
+
|
| 19 |
+
outputs:
|
| 20 |
+
docker_image_name: ${{ steps.prep.outputs.tagged_image_name }}
|
| 21 |
+
docker_image_tag: ${{ steps.prep.outputs.tag }}
|
| 22 |
+
docker_image: ${{ steps.prep.outputs.tagged_image }}
|
| 23 |
+
|
| 24 |
+
steps:
|
| 25 |
+
- uses: actions/checkout@main
|
| 26 |
+
|
| 27 |
+
- name: Login to GitHub Container Registry
|
| 28 |
+
uses: docker/login-action@v3
|
| 29 |
+
with:
|
| 30 |
+
registry: ghcr.io
|
| 31 |
+
username: ${{ github.actor }}
|
| 32 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
| 33 |
+
|
| 34 |
+
- name: 🐳 Prepare Docker
|
| 35 |
+
id: prep
|
| 36 |
+
env:
|
| 37 |
+
IMAGE_NAME: ghcr.io/${{ github.repository }}
|
| 38 |
+
run: |
|
| 39 |
+
BRANCH_NAME=$(echo $GITHUB_REF_NAME | sed 's|[/:]|-|' | tr '[:upper:]' '[:lower:]' | sed 's/_/-/g' | cut -c1-100 | sed 's/-*$//')
|
| 40 |
+
|
| 41 |
+
# XXX: Check if there is a slash in the BRANCH_NAME eg: project/add-docker
|
| 42 |
+
if [[ "$BRANCH_NAME" == *"/"* ]]; then
|
| 43 |
+
# XXX: Change the docker image package to -alpha
|
| 44 |
+
IMAGE_NAME="$IMAGE_NAME-alpha"
|
| 45 |
+
TAG="$(echo "$BRANCH_NAME" | sed 's|/|-|g').$(echo $GITHUB_SHA | head -c7)"
|
| 46 |
+
else
|
| 47 |
+
TAG="$BRANCH_NAME.$(echo $GITHUB_SHA | head -c7)"
|
| 48 |
+
fi
|
| 49 |
+
|
| 50 |
+
IMAGE_NAME=$(echo $IMAGE_NAME | tr '[:upper:]' '[:lower:]')
|
| 51 |
+
echo "tagged_image_name=${IMAGE_NAME}" >> $GITHUB_OUTPUT
|
| 52 |
+
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
| 53 |
+
echo "tagged_image=${IMAGE_NAME}:${TAG}" >> $GITHUB_OUTPUT
|
| 54 |
+
echo "::notice::Tagged docker image: ${IMAGE_NAME}:${TAG}"
|
| 55 |
+
|
| 56 |
+
- name: 🐳 Set up Docker Buildx
|
| 57 |
+
id: buildx
|
| 58 |
+
uses: docker/setup-buildx-action@v3
|
| 59 |
+
|
| 60 |
+
- name: 🐳 Cache Docker layers
|
| 61 |
+
uses: actions/cache@v4
|
| 62 |
+
with:
|
| 63 |
+
path: /tmp/.buildx-cache
|
| 64 |
+
key: ${{ runner.os }}-buildx-${{ github.ref }}
|
| 65 |
+
restore-keys: |
|
| 66 |
+
${{ runner.os }}-buildx-refs/develop
|
| 67 |
+
${{ runner.os }}-buildx-
|
| 68 |
+
|
| 69 |
+
- name: 🐳 Docker build
|
| 70 |
+
uses: docker/build-push-action@v6
|
| 71 |
+
with:
|
| 72 |
+
context: .
|
| 73 |
+
builder: ${{ steps.buildx.outputs.name }}
|
| 74 |
+
file: nginx-serve/Dockerfile
|
| 75 |
+
target: nginx-serve
|
| 76 |
+
load: true
|
| 77 |
+
push: true
|
| 78 |
+
tags: ${{ steps.prep.outputs.tagged_image }}
|
| 79 |
+
cache-from: type=local,src=/tmp/.buildx-cache
|
| 80 |
+
cache-to: type=local,dest=/tmp/.buildx-cache-new
|
| 81 |
+
build-args: |
|
| 82 |
+
"APP_SENTRY_TRACES_SAMPLE_RATE=0.8"
|
| 83 |
+
"APP_SENTRY_REPLAYS_SESSION_SAMPLE_RATE=0.8"
|
| 84 |
+
"APP_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE=0.8"
|
| 85 |
+
|
| 86 |
+
- name: 🐳 Move docker cache
|
| 87 |
+
run: |
|
| 88 |
+
rm -rf /tmp/.buildx-cache
|
| 89 |
+
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
| 90 |
+
|
| 91 |
+
publish_helm:
|
| 92 |
+
name: Publish Helm
|
| 93 |
+
needs: publish_image
|
| 94 |
+
runs-on: ubuntu-latest
|
| 95 |
+
|
| 96 |
+
steps:
|
| 97 |
+
- name: Checkout code
|
| 98 |
+
uses: actions/checkout@v4
|
| 99 |
+
|
| 100 |
+
- name: Login to GitHub Container Registry
|
| 101 |
+
uses: docker/login-action@v3
|
| 102 |
+
with:
|
| 103 |
+
registry: ghcr.io
|
| 104 |
+
username: ${{ github.actor }}
|
| 105 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
| 106 |
+
|
| 107 |
+
- name: Install Helm
|
| 108 |
+
uses: azure/setup-helm@v3
|
| 109 |
+
|
| 110 |
+
- name: Tag docker image in Helm Chart values.yaml
|
| 111 |
+
env:
|
| 112 |
+
IMAGE_NAME: ${{ needs.publish_image.outputs.docker_image_name }}
|
| 113 |
+
IMAGE_TAG: ${{ needs.publish_image.outputs.docker_image_tag }}
|
| 114 |
+
run: |
|
| 115 |
+
# Update values.yaml with latest docker image
|
| 116 |
+
sed -i "s|SET-BY-CICD-IMAGE|$IMAGE_NAME|" nginx-serve/helm/values.yaml
|
| 117 |
+
sed -i "s/SET-BY-CICD-TAG/$IMAGE_TAG/" nginx-serve/helm/values.yaml
|
| 118 |
+
|
| 119 |
+
- name: Package Helm Chart
|
| 120 |
+
id: set-variables
|
| 121 |
+
run: |
|
| 122 |
+
# XXX: Check if there is a slash in the BRANCH_NAME eg: project/add-docker
|
| 123 |
+
if [[ "$GITHUB_REF_NAME" == *"/"* ]]; then
|
| 124 |
+
# XXX: Change the helm chart to <chart-name>-alpha
|
| 125 |
+
sed -i 's/^name: \(.*\)/name: \1-alpha/' nginx-serve/helm/Chart.yaml
|
| 126 |
+
fi
|
| 127 |
+
|
| 128 |
+
SHA_SHORT=$(git rev-parse --short HEAD)
|
| 129 |
+
sed -i "s/SET-BY-CICD/$SHA_SHORT/g" nginx-serve/helm/Chart.yaml
|
| 130 |
+
helm package ./nginx-serve/helm -d .helm-charts
|
| 131 |
+
|
| 132 |
+
- name: Push Helm Chart
|
| 133 |
+
env:
|
| 134 |
+
IMAGE: ${{ needs.publish_image.outputs.docker_image }}
|
| 135 |
+
OCI_REPO: oci://ghcr.io/${{ github.repository }}
|
| 136 |
+
run: |
|
| 137 |
+
OCI_REPO=$(echo $OCI_REPO | tr '[:upper:]' '[:lower:]')
|
| 138 |
+
PACKAGE_FILE=$(ls .helm-charts/*.tgz | head -n 1)
|
| 139 |
+
echo "# Helm Chart" >> $GITHUB_STEP_SUMMARY
|
| 140 |
+
echo "" >> $GITHUB_STEP_SUMMARY
|
| 141 |
+
echo "Tagged Image: **$IMAGE**" >> $GITHUB_STEP_SUMMARY
|
| 142 |
+
echo "" >> $GITHUB_STEP_SUMMARY
|
| 143 |
+
echo "Helm push output" >> $GITHUB_STEP_SUMMARY
|
| 144 |
+
echo "" >> $GITHUB_STEP_SUMMARY
|
| 145 |
+
echo '```bash' >> $GITHUB_STEP_SUMMARY
|
| 146 |
+
helm push "$PACKAGE_FILE" $OCI_REPO >> $GITHUB_STEP_SUMMARY
|
| 147 |
+
echo '```' >> $GITHUB_STEP_SUMMARY
|
go-web-app-develop/.github/workflows/publish-storybook-nginx-serve.yml
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Publish Storybook Helm
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
workflow_dispatch:
|
| 5 |
+
push:
|
| 6 |
+
branches:
|
| 7 |
+
- develop
|
| 8 |
+
|
| 9 |
+
permissions:
|
| 10 |
+
packages: write
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
jobs:
|
| 14 |
+
publish_image:
|
| 15 |
+
name: 🐳 Publish Docker Image
|
| 16 |
+
runs-on: ubuntu-latest
|
| 17 |
+
|
| 18 |
+
outputs:
|
| 19 |
+
docker_image_name: ${{ steps.prep.outputs.tagged_image_name }}
|
| 20 |
+
docker_image_tag: ${{ steps.prep.outputs.tag }}
|
| 21 |
+
docker_image: ${{ steps.prep.outputs.tagged_image }}
|
| 22 |
+
|
| 23 |
+
steps:
|
| 24 |
+
- uses: actions/checkout@main
|
| 25 |
+
|
| 26 |
+
- name: Login to GitHub Container Registry
|
| 27 |
+
uses: docker/login-action@v3
|
| 28 |
+
with:
|
| 29 |
+
registry: ghcr.io
|
| 30 |
+
username: ${{ github.actor }}
|
| 31 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
| 32 |
+
|
| 33 |
+
- name: 🐳 Prepare Docker
|
| 34 |
+
id: prep
|
| 35 |
+
env:
|
| 36 |
+
IMAGE_NAME: ghcr.io/${{ github.repository }}/go-ui-storybook
|
| 37 |
+
run: |
|
| 38 |
+
BRANCH_NAME=$(echo $GITHUB_REF_NAME | sed 's|[/:]|-|' | tr '[:upper:]' '[:lower:]' | sed 's/_/-/g' | cut -c1-100 | sed 's/-*$//')
|
| 39 |
+
TAG="$BRANCH_NAME.$(echo $GITHUB_SHA | head -c7)"
|
| 40 |
+
IMAGE_NAME=$(echo $IMAGE_NAME | tr '[:upper:]' '[:lower:]')
|
| 41 |
+
echo "tagged_image_name=${IMAGE_NAME}" >> $GITHUB_OUTPUT
|
| 42 |
+
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
| 43 |
+
echo "tagged_image=${IMAGE_NAME}:${TAG}" >> $GITHUB_OUTPUT
|
| 44 |
+
echo "::notice::Tagged docker image: ${IMAGE_NAME}:${TAG}"
|
| 45 |
+
|
| 46 |
+
- name: 🐳 Set up Docker Buildx
|
| 47 |
+
id: buildx
|
| 48 |
+
uses: docker/setup-buildx-action@v3
|
| 49 |
+
|
| 50 |
+
- name: 🐳 Cache Docker layers
|
| 51 |
+
uses: actions/cache@v4
|
| 52 |
+
with:
|
| 53 |
+
path: /tmp/.buildx-cache
|
| 54 |
+
key: ${{ runner.os }}-buildx-${{ github.ref }}
|
| 55 |
+
restore-keys: |
|
| 56 |
+
${{ runner.os }}-buildx-refs/develop
|
| 57 |
+
${{ runner.os }}-buildx-
|
| 58 |
+
|
| 59 |
+
- name: 🐳 Docker build
|
| 60 |
+
uses: docker/build-push-action@v6
|
| 61 |
+
with:
|
| 62 |
+
context: .
|
| 63 |
+
builder: ${{ steps.buildx.outputs.name }}
|
| 64 |
+
file: packages/go-ui-storybook/nginx-serve/Dockerfile
|
| 65 |
+
target: nginx-serve
|
| 66 |
+
load: true
|
| 67 |
+
push: true
|
| 68 |
+
tags: ${{ steps.prep.outputs.tagged_image }}
|
| 69 |
+
cache-from: type=local,src=/tmp/.buildx-cache
|
| 70 |
+
cache-to: type=local,dest=/tmp/.buildx-cache-new
|
| 71 |
+
|
| 72 |
+
- name: 🐳 Move docker cache
|
| 73 |
+
run: |
|
| 74 |
+
rm -rf /tmp/.buildx-cache
|
| 75 |
+
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
| 76 |
+
|
| 77 |
+
publish_helm:
|
| 78 |
+
name: ⎈ Publish Helm
|
| 79 |
+
needs: publish_image
|
| 80 |
+
runs-on: ubuntu-latest
|
| 81 |
+
|
| 82 |
+
steps:
|
| 83 |
+
- name: Checkout code
|
| 84 |
+
uses: actions/checkout@v4
|
| 85 |
+
|
| 86 |
+
- name: Login to GitHub Container Registry
|
| 87 |
+
uses: docker/login-action@v3
|
| 88 |
+
with:
|
| 89 |
+
registry: ghcr.io
|
| 90 |
+
username: ${{ github.actor }}
|
| 91 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
| 92 |
+
|
| 93 |
+
- name: ⎈ Install Helm
|
| 94 |
+
uses: azure/setup-helm@v3
|
| 95 |
+
|
| 96 |
+
- name: ⎈ Tag docker image in Helm Chart values.yaml
|
| 97 |
+
env:
|
| 98 |
+
IMAGE_NAME: ${{ needs.publish_image.outputs.docker_image_name }}
|
| 99 |
+
IMAGE_TAG: ${{ needs.publish_image.outputs.docker_image_tag }}
|
| 100 |
+
run: |
|
| 101 |
+
# Update values.yaml with latest docker image
|
| 102 |
+
sed -i "s|SET-BY-CICD-IMAGE|$IMAGE_NAME|" packages/go-ui-storybook/nginx-serve/helm/values.yaml
|
| 103 |
+
sed -i "s/SET-BY-CICD-TAG/$IMAGE_TAG/" packages/go-ui-storybook/nginx-serve/helm/values.yaml
|
| 104 |
+
|
| 105 |
+
- name: ⎈ Package Helm Chart
|
| 106 |
+
id: set-variables
|
| 107 |
+
run: |
|
| 108 |
+
SHA_SHORT=$(git rev-parse --short HEAD)
|
| 109 |
+
sed -i "s/SET-BY-CICD/$SHA_SHORT/g" packages/go-ui-storybook/nginx-serve/helm/Chart.yaml
|
| 110 |
+
helm package ./packages/go-ui-storybook/nginx-serve/helm -d .helm-charts
|
| 111 |
+
|
| 112 |
+
- name: ⎈ Push Helm Chart
|
| 113 |
+
env:
|
| 114 |
+
IMAGE: ${{ needs.publish_image.outputs.docker_image }}
|
| 115 |
+
OCI_REPO: oci://ghcr.io/${{ github.repository }}
|
| 116 |
+
run: |
|
| 117 |
+
OCI_REPO=$(echo $OCI_REPO | tr '[:upper:]' '[:lower:]')
|
| 118 |
+
PACKAGE_FILE=$(ls .helm-charts/*.tgz | head -n 1)
|
| 119 |
+
echo "## 🚀 IFRC GO UI Helm Chart 🚀" >> $GITHUB_STEP_SUMMARY
|
| 120 |
+
echo "" >> $GITHUB_STEP_SUMMARY
|
| 121 |
+
echo "🐳 Tagged Image: **$IMAGE**" >> $GITHUB_STEP_SUMMARY
|
| 122 |
+
echo "" >> $GITHUB_STEP_SUMMARY
|
| 123 |
+
echo "⎈ Helm push output" >> $GITHUB_STEP_SUMMARY
|
| 124 |
+
echo "" >> $GITHUB_STEP_SUMMARY
|
| 125 |
+
echo '```bash' >> $GITHUB_STEP_SUMMARY
|
| 126 |
+
helm push "$PACKAGE_FILE" $OCI_REPO >> $GITHUB_STEP_SUMMARY
|
| 127 |
+
echo '```' >> $GITHUB_STEP_SUMMARY
|
go-web-app-develop/.gitignore
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logs
|
| 2 |
+
logs
|
| 3 |
+
*.log
|
| 4 |
+
npm-debug.log*
|
| 5 |
+
yarn-debug.log*
|
| 6 |
+
yarn-error.log*
|
| 7 |
+
pnpm-debug.log*
|
| 8 |
+
lerna-debug.log*
|
| 9 |
+
|
| 10 |
+
node_modules
|
| 11 |
+
dist
|
| 12 |
+
dist-ssr
|
| 13 |
+
build
|
| 14 |
+
build-ssr
|
| 15 |
+
*.local
|
| 16 |
+
|
| 17 |
+
# Editor directories and files
|
| 18 |
+
.vscode/*
|
| 19 |
+
!.vscode/extensions.json
|
| 20 |
+
.idea
|
| 21 |
+
.DS_Store
|
| 22 |
+
*.suo
|
| 23 |
+
*.ntvs*
|
| 24 |
+
*.njsproj
|
| 25 |
+
*.sln
|
| 26 |
+
*.sw?
|
| 27 |
+
|
| 28 |
+
.env*
|
| 29 |
+
!.env.example
|
| 30 |
+
.eslintcache
|
| 31 |
+
tsconfig.tsbuildinfo
|
| 32 |
+
|
| 33 |
+
# Custom ignores
|
| 34 |
+
|
| 35 |
+
stats.html
|
| 36 |
+
generated/
|
| 37 |
+
coverage/
|
| 38 |
+
|
| 39 |
+
# storybook build
|
| 40 |
+
storybook-static/
|
| 41 |
+
|
| 42 |
+
# Helm
|
| 43 |
+
.helm-charts/
|
go-web-app-develop/.npmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
enable-pre-post-scripts=true
|
go-web-app-develop/COLLABORATING.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# IFRC GO Collaboration Guide
|
| 2 |
+
|
| 3 |
+
This document offers guidelines for collaborators on codebase maintenance, testing, building and deployment, and issue management.
|
| 4 |
+
|
| 5 |
+
## Repository
|
| 6 |
+
|
| 7 |
+
* [Issues and Pull Requests](./collaborating/issues-and-pull-requests.md)
|
| 8 |
+
* [Structure](./collaborating/repository-structure.md)
|
| 9 |
+
* [Linting](./collaborating/linting.md)
|
| 10 |
+
* [Technology Used](./collaborating/technology.md)
|
| 11 |
+
|
| 12 |
+
## Development
|
| 13 |
+
|
| 14 |
+
* [Developing](./collaborating/developing.md)
|
| 15 |
+
* [Translation](./collaborating/translation.md)
|
| 16 |
+
* [Building](./collaborating/building.md)
|
| 17 |
+
* [Testing](./collaborating/testing.md)
|
| 18 |
+
* [Release](./collaborating/release.md)
|
go-web-app-develop/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# IFRC GO Web Application Contributing Guide
|
| 2 |
+
|
| 3 |
+
First off, thanks for taking the time to contribute! ❤️
|
| 4 |
+
|
| 5 |
+
All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution.
|
| 6 |
+
|
| 7 |
+
## Table of Contents
|
| 8 |
+
|
| 9 |
+
* [I Have a Question](#i-have-a-question)
|
| 10 |
+
* [I Want To Contribute](#i-want-to-contribute)
|
| 11 |
+
* [What should I know before I get started?](#what-should-i-know-before-i-get-started)
|
| 12 |
+
* [Reporting Bugs](#reporting-bugs)
|
| 13 |
+
* [Suggesting Enhancements](#suggesting-enhancements)
|
| 14 |
+
* [Becoming a Collaborator](#becoming-a-collaborator)
|
| 15 |
+
|
| 16 |
+
## I Have a Question
|
| 17 |
+
|
| 18 |
+
> If you want to ask a question, we assume that you have read the available [documentation](https://go-wiki.ifrc.org/en/home).
|
| 19 |
+
|
| 20 |
+
Before you ask a question, it is best to search for existing [issues](https://github.com/IFRCGo/go-web-app/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue.
|
| 21 |
+
|
| 22 |
+
If you then still feel the need to ask a question and need clarification, we recommend the following:
|
| 23 |
+
|
| 24 |
+
* Open a [discussion](https://github.com/IFRCGo/go-web-app/discussions).
|
| 25 |
+
* Open an [issue](https://github.com/IFRCGo/go-web-app/issues/new/choose).
|
| 26 |
+
* Provide as much context as you can about what you're running into.
|
| 27 |
+
|
| 28 |
+
## I Want To Contribute
|
| 29 |
+
|
| 30 |
+
Any individual is welcome to contribute to IFRC GO. The repository currently has two kinds of contribution personas:
|
| 31 |
+
|
| 32 |
+
* A **Contributor** is any individual who creates an issue/PR, comments on an issue/PR, or contributes in some other way.
|
| 33 |
+
* A **Collaborator** is a contributor with write access to the repository.
|
| 34 |
+
|
| 35 |
+
### What should I know before I get started?
|
| 36 |
+
|
| 37 |
+
### IFRC GO and Packages
|
| 38 |
+
|
| 39 |
+
The project is hosted at <https://go.ifrc.org/>.
|
| 40 |
+
|
| 41 |
+
The project comprises several [repositories](https://github.com/orgs/IFRCGo/repositories), with notable ones including:
|
| 42 |
+
|
| 43 |
+
* [go-web-app](https://github.com/IFRCGo/go-web-app/) - The frontend repository for the IFRC GO project.
|
| 44 |
+
* [go-api](https://github.com/IFRCGo/go-api) - The backed repository for the IFRC GO project.
|
| 45 |
+
|
| 46 |
+
### Reporting Bugs
|
| 47 |
+
|
| 48 |
+
#### Before Submitting a Bug Report
|
| 49 |
+
|
| 50 |
+
Ensure the issue is not a user error by reviewing the documentation. Check the [existing bug reports](https://github.com/IFRCGo/go-web-app/issues?q=is%3Aissue%20state%3Aopen%20type%3ABug) to confirm if the issue has already been reported.
|
| 51 |
+
|
| 52 |
+
#### Submitting the Bug Report
|
| 53 |
+
|
| 54 |
+
1. Open a new [Issue](https://github.com/IFRCGo/go-web-app/issues/new?q=is%3Aissue+state%3Aopen+type%3ABug\&template=01_bug_report.yml).
|
| 55 |
+
2. Provide all relevant details.
|
| 56 |
+
|
| 57 |
+
#### After Submitting the Issue
|
| 58 |
+
|
| 59 |
+
* The team will categorize and attempt to reproduce the issue.
|
| 60 |
+
* If reproducible, the team will work on resolving the bug.
|
| 61 |
+
|
| 62 |
+
### Suggesting Enhancements
|
| 63 |
+
|
| 64 |
+
#### Before Submitting an Enhancement
|
| 65 |
+
|
| 66 |
+
* Review the [documentation](https://go-wiki.ifrc.org/en/home) to ensure the functionality isn't already covered.
|
| 67 |
+
* Perform a [search](https://github.com/IFRCGo/go-web-app/issues) to check if the enhancement has been suggested. If so, comment on the existing issue.
|
| 68 |
+
* Confirm that your suggestion aligns with the project’s scope and objectives.
|
| 69 |
+
|
| 70 |
+
#### How to Submit an Enhancement Suggestion
|
| 71 |
+
|
| 72 |
+
Enhancements are tracked as [GitHub issues](https://github.com/IFRCGo/go-web-app/issues).
|
| 73 |
+
|
| 74 |
+
* Open a new [feature request](https://github.com/IFRCGo/go-web-app/issues/new?q=is%3Aissue+state%3Aopen+type%3ABug\&template=02_feature_request.yml) or [Epic ticket](https://github.com/IFRCGo/go-web-app/issues/new?q=is%3Aissue+state%3Aopen+type%3ABug\&template=03_epic_request.yml) depending on the scale of the enhancement.
|
| 75 |
+
* Provide a clear description and submit the ticket.
|
| 76 |
+
|
| 77 |
+
## Becoming a Collaborator
|
| 78 |
+
|
| 79 |
+
Collaborators are key members of the IFRC GO Web Application Team, responsible for its development. Members should have expertise in modern web technologies and standards.
|
| 80 |
+
|
| 81 |
+
For detailed guidelines, refer to the [Collaboration Guide](./COLLABORATING.md).
|
go-web-app-develop/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2023 GO
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
go-web-app-develop/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<p align="center">
|
| 2 |
+
<br />
|
| 3 |
+
<a href="https://go.ifrc.org/">
|
| 4 |
+
<picture>
|
| 5 |
+
<img src="https://github.com/IFRCGo/go-web-app/blob/develop/app/src/assets/icons/go-logo-2020.svg" width="200px" alt="IFRC GO Logo">
|
| 6 |
+
</picture>
|
| 7 |
+
</a>
|
| 8 |
+
</p>
|
| 9 |
+
|
| 10 |
+
# IFRC GO
|
| 11 |
+
|
| 12 |
+
[IFRC GO](https://go.ifrc.org/) is the platform of the International Federation of Red Cross and Red Crescent, aimed at connecting crucial information on emergency needs with the appropriate response. This repository houses the frontend source code for the application, developed using [React](https://react.dev/), [Vite](https://vitejs.dev/), and associated technologies.
|
| 13 |
+
|
| 14 |
+
## Built With
|
| 15 |
+
|
| 16 |
+
[![React][react-shields]][react-url] [![Vite][vite-shields]][vite-url] [![TypeScript][typescript-shields]][typescript-url] [![pnpm][pnpm-shields]][pnpm-url]
|
| 17 |
+
|
| 18 |
+
## Getting Started
|
| 19 |
+
|
| 20 |
+
Below are the steps to guide you through preparing your local environment for IFRC GO Web application development. The repository is set up as a [monorepo](https://monorepo.tools/). The [app](https://github.com/IFRCGo/go-web-app/tree/develop/app) directory houses the application code, while the [packages](https://github.com/IFRCGo/go-web-app/tree/develop/packages) directory contains related packages, including the [IFRC GO UI](https://www.npmjs.com/package/@ifrc-go/ui) components library.
|
| 21 |
+
|
| 22 |
+
### Prerequisites
|
| 23 |
+
|
| 24 |
+
To begin, ensure you have network access. Then, you'll need the following:
|
| 25 |
+
|
| 26 |
+
1. [Git](https://git-scm.com/)
|
| 27 |
+
2. [Node.js](https://nodejs.org/en/) as specified under `engines` section in `package.json` file
|
| 28 |
+
3. [pnpm](https://pnpm.io/) as specified under `engines` section in `package.json` file
|
| 29 |
+
4. Alternatively, you can use [Docker](https://www.docker.com/) to build the application.
|
| 30 |
+
|
| 31 |
+
> \[!NOTE]\
|
| 32 |
+
> Make sure the correct versions of pnpm and Node.js are installed. They are specified under `engines` section in `package.json` file.
|
| 33 |
+
|
| 34 |
+
### Local Development
|
| 35 |
+
|
| 36 |
+
1. Clone the repository using HTTPS, SSH, or GitHub CLI:
|
| 37 |
+
|
| 38 |
+
```bash
|
| 39 |
+
git clone https://github.com/IFRCGo/go-web-app.git # HTTPS
|
| 40 |
+
git clone [email protected]:IFRCGo/go-web-app.git # SSH
|
| 41 |
+
gh repo clone IFRCGo/go-web-app # GitHub CLI
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
2. Install the dependencies:
|
| 45 |
+
|
| 46 |
+
```bash
|
| 47 |
+
pnpm install
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
3. Create a `.env` file in the `app` directory and add variables from [env.ts](https://github.com/IFRCGo/go-web-app/blob/develop/app/env.ts). Any variables marked with `.optional()` are not mandatory for setup and can be skipped.
|
| 51 |
+
|
| 52 |
+
```bash
|
| 53 |
+
cd app
|
| 54 |
+
touch .env
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
Example `.env` file
|
| 58 |
+
```
|
| 59 |
+
APP_TITLE=IFRC GO
|
| 60 |
+
APP_ENVIRONMENT=testing
|
| 61 |
+
...
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
4. Start the development server:
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
pnpm start:app
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
## Contributing
|
| 71 |
+
|
| 72 |
+
* Check out existing [Issues](https://github.com/IFRCGo/go-web-app/issues) and [Pull Requests](https://github.com/IFRCGo/go-web-app/pulls) to contribute.
|
| 73 |
+
* To request a feature or report a bug, [create a GitHub Issue](https://github.com/IFRCGo/go-web-app/issues/new/choose).
|
| 74 |
+
* [Contribution Guide →](./CONTRIBUTING.md)
|
| 75 |
+
* [Collaboration Guide →](./COLLABORATING.md)
|
| 76 |
+
|
| 77 |
+
## Additional Packages
|
| 78 |
+
|
| 79 |
+
The repository hosts multiple packages under the `packages` directory.
|
| 80 |
+
|
| 81 |
+
1. [IFRC GO UI](https://github.com/IFRCGo/go-web-app/tree/develop/packages/ui) is a React UI components library tailored to meet the specific requirements of the IFRC GO community and its associated projects.
|
| 82 |
+
2. [IFRC GO UI Storybook](https://github.com/IFRCGo/go-web-app/tree/develop/packages/go-ui-storybook) serves as the comprehensive showcase for the IFRC GO UI components library. It is hosted on [Chromatic](https://66557be6b68dacbf0a96db23-zctxglhsnk.chromatic.com/).
|
| 83 |
+
|
| 84 |
+
## IFRC GO Backend
|
| 85 |
+
|
| 86 |
+
The backend that serves the frontend application is maintained in a separate [repository](https://github.com/IFRCGo/go-api/).
|
| 87 |
+
|
| 88 |
+
## Previous Repository
|
| 89 |
+
|
| 90 |
+
[Go Frontend](https://github.com/IFRCGo/go-frontend) is the previous version of the project which contains the original codebase and project history.
|
| 91 |
+
|
| 92 |
+
## Community & Support
|
| 93 |
+
|
| 94 |
+
* Visit the [IFRC GO Wiki](https://go-wiki.ifrc.org/) for documentation of the IFRC GO platform.
|
| 95 |
+
* Stay informed about the latest project updates on [Medium](https://ifrcgoproject.medium.com/).
|
| 96 |
+
|
| 97 |
+
## License
|
| 98 |
+
|
| 99 |
+
[MIT](https://github.com/IFRCGo/go-web-app/blob/develop/LICENSE)
|
| 100 |
+
|
| 101 |
+
<!-- MARKDOWN LINKS & IMAGES -->
|
| 102 |
+
|
| 103 |
+
[react-shields]: https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB
|
| 104 |
+
|
| 105 |
+
[react-url]: https://reactjs.org/
|
| 106 |
+
|
| 107 |
+
[vite-shields]: https://img.shields.io/badge/vite-%23646CFF.svg?style=for-the-badge&logo=vite&logoColor=white
|
| 108 |
+
|
| 109 |
+
[vite-url]: https://vitejs.dev/
|
| 110 |
+
|
| 111 |
+
[typescript-shields]: https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white
|
| 112 |
+
|
| 113 |
+
[typescript-url]: https://www.typescriptlang.org/
|
| 114 |
+
|
| 115 |
+
[pnpm-shields]: https://img.shields.io/badge/pnpm-F69220?style=for-the-badge&logo=pnpm&logoColor=fff
|
| 116 |
+
|
| 117 |
+
[pnpm-url]: https://pnpm.io/
|
go-web-app-develop/app/CHANGELOG.md
ADDED
|
@@ -0,0 +1,729 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# go-web-app
|
| 2 |
+
|
| 3 |
+
## 7.21.0-beta.2
|
| 4 |
+
|
| 5 |
+
### Patch Changes
|
| 6 |
+
|
| 7 |
+
- b949fcd: Fix use of operational timeframe date in imminent final report form
|
| 8 |
+
|
| 9 |
+
## 7.21.0-beta.1
|
| 10 |
+
|
| 11 |
+
### Patch Changes
|
| 12 |
+
|
| 13 |
+
- 84b4802: - Fix calculation of Operation End date in Final report form
|
| 14 |
+
- Fix icon position issue in the implementation table of DREF PDF export
|
| 15 |
+
- Update the label for last update date in the crisis categorization pop-up
|
| 16 |
+
|
| 17 |
+
## 7.21.0-beta.0
|
| 18 |
+
|
| 19 |
+
### Minor Changes
|
| 20 |
+
|
| 21 |
+
- 039c488: Add Crisis categorization update date
|
| 22 |
+
|
| 23 |
+
- Add updated date for crisis categorization in emergency page.
|
| 24 |
+
- Add consent checkbox over situational overview in field report form.
|
| 25 |
+
|
| 26 |
+
- 3ee9979: Add support for DREF imminent v2 in final report
|
| 27 |
+
|
| 28 |
+
- Add a separate route for the old dref final report form
|
| 29 |
+
- Update dref final report to accomodate imminent v2 changes
|
| 30 |
+
|
| 31 |
+
## 7.20.2
|
| 32 |
+
|
| 33 |
+
### Patch Changes
|
| 34 |
+
|
| 35 |
+
- 8090b9a: Fix other action section visibility condition in DREF export
|
| 36 |
+
|
| 37 |
+
## 7.20.1
|
| 38 |
+
|
| 39 |
+
### Patch Changes
|
| 40 |
+
|
| 41 |
+
- 4418171: Fix DREF form to properly save major coordination mechanism [#1928](https://github.com/IFRCGo/go-web-app/issues/1928)
|
| 42 |
+
|
| 43 |
+
## 7.20.1-beta.0
|
| 44 |
+
|
| 45 |
+
### Patch Changes
|
| 46 |
+
|
| 47 |
+
- 4418171: Fix DREF form to properly save major coordination mechanism [#1928](https://github.com/IFRCGo/go-web-app/issues/1928)
|
| 48 |
+
|
| 49 |
+
## 7.20.0
|
| 50 |
+
|
| 51 |
+
### Minor Changes
|
| 52 |
+
|
| 53 |
+
- 5771a6b: Update DREF application form and export
|
| 54 |
+
|
| 55 |
+
- add new field hazard date and location
|
| 56 |
+
- update hazard date as forcasted day of event
|
| 57 |
+
- update the section in dref application export
|
| 58 |
+
- remove Current National Society Actions from the export
|
| 59 |
+
|
| 60 |
+
## 7.20.0-beta.0
|
| 61 |
+
|
| 62 |
+
### Minor Changes
|
| 63 |
+
|
| 64 |
+
- 5771a6b: Update DREF application form and export
|
| 65 |
+
|
| 66 |
+
- add new field hazard date and location
|
| 67 |
+
- update hazard date as forcasted day of event
|
| 68 |
+
- update the section in dref application export
|
| 69 |
+
- remove Current National Society Actions from the export
|
| 70 |
+
|
| 71 |
+
## 7.19.0
|
| 72 |
+
|
| 73 |
+
### Minor Changes
|
| 74 |
+
|
| 75 |
+
- 456a145: Fix versioning
|
| 76 |
+
|
| 77 |
+
### Patch Changes
|
| 78 |
+
|
| 79 |
+
- 47786f8: Fix the undefined society name issue in surge page [#1899](https://github.com/IFRCGo/go-web-app/issues/1899)
|
| 80 |
+
|
| 81 |
+
## 7.18.2
|
| 82 |
+
|
| 83 |
+
### Patch Changes
|
| 84 |
+
|
| 85 |
+
- e51a80f: Update the action for the DREF Ops update form for imminent.
|
| 86 |
+
- Remove change to response modal in the ops update form for type imminent.
|
| 87 |
+
- Fix the order of the field in operational timeframe tab.
|
| 88 |
+
- Add description text under upload assessment report button in DREF operation update form
|
| 89 |
+
- Fix the error while viewing PER process [#1838](https://github.com/IFRCGo/go-web-app/issues/1838).
|
| 90 |
+
|
| 91 |
+
## 7.18.1
|
| 92 |
+
|
| 93 |
+
### Patch Changes
|
| 94 |
+
|
| 95 |
+
- 75bf525: Fix logic to disable ops update for old imminents
|
| 96 |
+
|
| 97 |
+
## 7.18.0
|
| 98 |
+
|
| 99 |
+
### Minor Changes
|
| 100 |
+
|
| 101 |
+
- bfcaecf: Address [Dref imminent Application](https://github.com/IFRCGo/go-web-app/issues/1455)
|
| 102 |
+
- Update logic for creation of dref final report for imminent
|
| 103 |
+
- Update allocatioon form for dref imminent
|
| 104 |
+
- Add Activity input in proposed action for dref type imminent
|
| 105 |
+
- Add proposed actions icons
|
| 106 |
+
- Show proposed actions for existing imminent dref applications
|
| 107 |
+
- Hide unused sections for dref imminent export and preserve proposed actions order
|
| 108 |
+
- Prevent selection of past dates for the `hazard_date` in dref imminent
|
| 109 |
+
- Add auto total population calculation in dref
|
| 110 |
+
- Add a confirmation popup before creating ops. update from imminent dref
|
| 111 |
+
|
| 112 |
+
### Patch Changes
|
| 113 |
+
|
| 114 |
+
- ee1bd60: Add proper redirect for Non-sovereign country in the country ongoing emergencies page
|
| 115 |
+
- 771d085: Community Based Surveillance updates (Surge CoS Health)
|
| 116 |
+
- Changed page: https://go.ifrc.org/surge/catalogue/health/community-based-surveillance
|
| 117 |
+
- The changes affect team size and some standard components (e.g. kit content)
|
| 118 |
+
- Updated dependencies [bfcaecf]
|
| 119 |
+
- @ifrc-go/[email protected]
|
| 120 |
+
|
| 121 |
+
## 7.17.4
|
| 122 |
+
|
| 123 |
+
### Patch Changes
|
| 124 |
+
|
| 125 |
+
- 14a7f2c: Update People assisted field label in the export of Dref final report.
|
| 126 |
+
|
| 127 |
+
## 7.17.3
|
| 128 |
+
|
| 129 |
+
### Patch Changes
|
| 130 |
+
|
| 131 |
+
- fc8b427: Update field label in DrefFinalReport form and export
|
| 132 |
+
|
| 133 |
+
## 7.17.2
|
| 134 |
+
|
| 135 |
+
### Patch Changes
|
| 136 |
+
|
| 137 |
+
- 54df6ff: Update DREF final report form
|
| 138 |
+
|
| 139 |
+
- The DREF final report form and export now include a new "Assisted Population" field, replacing the "Targeted Population" field.
|
| 140 |
+
|
| 141 |
+
## 7.17.1
|
| 142 |
+
|
| 143 |
+
### Patch Changes
|
| 144 |
+
|
| 145 |
+
- 215030a: Update DREF forms
|
| 146 |
+
|
| 147 |
+
- Move Response strategy description from placeholder to below the input
|
| 148 |
+
- Add DREF allocation field in event details for the Loan type Ops. update form
|
| 149 |
+
|
| 150 |
+
## 7.17.0
|
| 151 |
+
|
| 152 |
+
### Minor Changes
|
| 153 |
+
|
| 154 |
+
- 0b351d1: Address [DREF Superticket 2 bugs](https://github.com/IFRCGo/go-web-app/issues/1784)
|
| 155 |
+
|
| 156 |
+
- Update no of images in for "Description of event" from 2 to 4
|
| 157 |
+
- Update descriptions of few fields
|
| 158 |
+
- Replace \* with bullet in description of planned interventions in DREF import
|
| 159 |
+
- Add some of the missing fields to exports
|
| 160 |
+
- Remove warnings for previously removed fields
|
| 161 |
+
|
| 162 |
+
## 7.16.2
|
| 163 |
+
|
| 164 |
+
### Patch Changes
|
| 165 |
+
|
| 166 |
+
- c086629: Update Learn > Resources > Montandon page
|
| 167 |
+
- Update styling of 'API Access' buttons
|
| 168 |
+
- Reword 'Access API' link to 'Access Montandon API'
|
| 169 |
+
- Reword 'Explore Radiant Earth API' to 'Explore data in STAC browser'
|
| 170 |
+
- 2ee6a1e: Remove a broken image from Catalogue of Surge Services > Health > ERU Hospital page
|
| 171 |
+
|
| 172 |
+
## 7.16.1
|
| 173 |
+
|
| 174 |
+
### Patch Changes
|
| 175 |
+
|
| 176 |
+
- d561dc4: - Update Montandon landing page - Fix typo in Justin's name and email - Update description
|
| 177 |
+
- Fix position and deploying organisation in ongoing RR deployment table
|
| 178 |
+
|
| 179 |
+
## 7.16.0
|
| 180 |
+
|
| 181 |
+
### Minor Changes
|
| 182 |
+
|
| 183 |
+
- 9dcdd38: Add Montandon landing page
|
| 184 |
+
|
| 185 |
+
- Add a basic landing page for Montandon with links and information
|
| 186 |
+
- Add link to Montandon landing page to Learn > Resources menu
|
| 187 |
+
|
| 188 |
+
## 7.15.0
|
| 189 |
+
|
| 190 |
+
### Minor Changes
|
| 191 |
+
|
| 192 |
+
- c26bda4: Implement [ERU Readiness](https://github.com/IFRCGo/go-web-app/issues/1710)
|
| 193 |
+
|
| 194 |
+
- Restucture surge page to acommodate ERU
|
| 195 |
+
- Move surge deployment related sections to a new dedicated tab **Active Surge Deployments**
|
| 196 |
+
- Update active deployments to improve scaling of points in the map
|
| 197 |
+
- Add **Active Surge Support per Emergency** section
|
| 198 |
+
- Revamp **Surge Overview** tab
|
| 199 |
+
- Add **Rapid Response Personnel** sub-tab
|
| 200 |
+
- Update existings charts and add new related tables/charts
|
| 201 |
+
- Add **Emergency Response Unit** sub-tab
|
| 202 |
+
- Add section to visualize ERU capacity and readiness
|
| 203 |
+
- Add section to view ongoing ERU deployments
|
| 204 |
+
- Add a form to update ERU Readiness
|
| 205 |
+
- Add option to export ERU Readiness data
|
| 206 |
+
- Update **Respond > Surge/Deployments** menu to include **Active Surge Deployments**
|
| 207 |
+
|
| 208 |
+
- 9ed8181: Address feedbacks in [DREF superticket feedbacks](https://github.com/IFRCGo/go-web-app/issues/1816)
|
| 209 |
+
|
| 210 |
+
- Make end date of operation readonly field in all DREF forms
|
| 211 |
+
- Fix font and spacing issues in the DREF exports (caused by link text overflow)
|
| 212 |
+
- Update styling of Risk and Security Considerations section to match that of Previous Operations
|
| 213 |
+
- Update visibility condition of National Society Actions in Final Report export
|
| 214 |
+
|
| 215 |
+
### Patch Changes
|
| 216 |
+
|
| 217 |
+
- Updated dependencies [c26bda4]
|
| 218 |
+
- @ifrc-go/[email protected]
|
| 219 |
+
|
| 220 |
+
## 7.14.0
|
| 221 |
+
|
| 222 |
+
### Minor Changes
|
| 223 |
+
|
| 224 |
+
- 18ccc85:
|
| 225 |
+
- Update styling of vertical NavigationTab
|
| 226 |
+
- Hide register URL in the T&C page for logged in user
|
| 227 |
+
- Update styling of T&C page
|
| 228 |
+
- Make the page responsive
|
| 229 |
+
- Make sidebar sticky
|
| 230 |
+
- Update url for [monty docs](https://github.com/IFRCGo/go-web-app/issues/1418#issuecomment-2422371363)
|
| 231 |
+
- 8d3a7bd: Initiate shutdown for 3W
|
| 232 |
+
- Remove "Submit 3W Projects" from the menu Prepare > Global 3W projects
|
| 233 |
+
- Rename "Global 3W Projects" to "Programmatic Partnerships" in Prepare menu
|
| 234 |
+
- Update global 3W page
|
| 235 |
+
- Update title and description for Programmatic Partnerships
|
| 236 |
+
- Remove all the contents related to 3W
|
| 237 |
+
- Replace contents in various places with project shutdown message
|
| 238 |
+
- Regional 3W tab
|
| 239 |
+
- 3W Projects section in Accounts > My Form > 3W
|
| 240 |
+
- Projects tab in Country > Ongoing Activities
|
| 241 |
+
- All Projects page
|
| 242 |
+
- New, edit 3W project form
|
| 243 |
+
- View 3W project page
|
| 244 |
+
- Remove NS Activities section in Country > NS overview > NS Activities page
|
| 245 |
+
- Remove Projects section from search results page
|
| 246 |
+
|
| 247 |
+
### Patch Changes
|
| 248 |
+
|
| 249 |
+
- Updated dependencies [18ccc85]
|
| 250 |
+
- @ifrc-go/[email protected]
|
| 251 |
+
|
| 252 |
+
## 7.13.0
|
| 253 |
+
|
| 254 |
+
### Minor Changes
|
| 255 |
+
|
| 256 |
+
- 69fd74f: - Update page title for Emergency to include the name
|
| 257 |
+
- Update page title of Flash update to include the name
|
| 258 |
+
- Fix the user registration link in the Terms & Condition page
|
| 259 |
+
- 680c673: Implement [DREF Superticket 2.0](https://github.com/IFRCGo/go-web-app/issues/1695)
|
| 260 |
+
|
| 261 |
+
### Patch Changes
|
| 262 |
+
|
| 263 |
+
- fe4b727: - Upgrade pnpm to v10.6.1
|
| 264 |
+
- Cleanup Dockerfile
|
| 265 |
+
- Configure depandabot to track other dependencies updates
|
| 266 |
+
- Upgrade eslint
|
| 267 |
+
- Use workspace protocol to reference workspace packages
|
| 268 |
+
- 9f20016: Enable user to edit their position field in [#1647](https://github.com/IFRCGo/go-web-app/issues/1647)
|
| 269 |
+
- ef15af1: Add secondary ordering in tables for rows with same date
|
| 270 |
+
- Updated dependencies [fe4b727]
|
| 271 |
+
- @ifrc-go/[email protected]
|
| 272 |
+
|
| 273 |
+
## 7.12.1
|
| 274 |
+
|
| 275 |
+
### Patch Changes
|
| 276 |
+
|
| 277 |
+
- Fix nullable type of assessment for NS capacity
|
| 278 |
+
|
| 279 |
+
## 7.12.0
|
| 280 |
+
|
| 281 |
+
### Minor Changes
|
| 282 |
+
|
| 283 |
+
- f766bc7: Add link to IFRC Survey Designer in the tools section under learn menu
|
| 284 |
+
|
| 285 |
+
### Patch Changes
|
| 286 |
+
|
| 287 |
+
- 7f51854: - Surge CoS: Health fix
|
| 288 |
+
- 3a1cac8: Hide focal point details based on user permissions
|
| 289 |
+
- 43d3bf1: - Add Surge CoS Administration section
|
| 290 |
+
- Add Surge CoS Faecal Sludge Management (FSM) section
|
| 291 |
+
- Update Surge CoS IT&T section
|
| 292 |
+
- Update Surge CoS Basecamp section (as OSH)
|
| 293 |
+
|
| 294 |
+
## 7.11.1
|
| 295 |
+
|
| 296 |
+
### Patch Changes
|
| 297 |
+
|
| 298 |
+
- ff426cd: Use current language for field report title generation
|
| 299 |
+
|
| 300 |
+
## 7.11.0
|
| 301 |
+
|
| 302 |
+
### Minor Changes
|
| 303 |
+
|
| 304 |
+
- Field report number generation: Change only when the country or event changes
|
| 305 |
+
|
| 306 |
+
## 7.10.1
|
| 307 |
+
|
| 308 |
+
### Patch Changes
|
| 309 |
+
|
| 310 |
+
- 14567f1: Improved tables by adding default and second-level ordering in [#1633](https://github.com/IFRCGo/go-web-app/issues/1633)
|
| 311 |
+
|
| 312 |
+
- Appeal Documents table, `emergencies/{xxx}/reports` page
|
| 313 |
+
- Recent Emergencies in Regions – All Appeals table
|
| 314 |
+
- All Deployed Personnel – Default sorting (filters to be added)
|
| 315 |
+
- Deployed ERUs – Changed filter title
|
| 316 |
+
- Key Documents tables in Countries
|
| 317 |
+
- Response documents
|
| 318 |
+
- Main page – Active Operations table
|
| 319 |
+
- The same `AppealsTable` is used in:
|
| 320 |
+
- Active Operations in Regions
|
| 321 |
+
- Previous Operations in Countries
|
| 322 |
+
|
| 323 |
+
- 78d25b2:
|
| 324 |
+
|
| 325 |
+
- Update on the ERU MHPSS Module in the Catalogue of Services in [#1648](https://github.com/IFRCGo/go-web-app/issues/1648)
|
| 326 |
+
- Update on a PER role profile in [#1648](https://github.com/IFRCGo/go-web-app/issues/1648)
|
| 327 |
+
- Update link to the IM Technical Competency Framework in [#1483](https://github.com/IFRCGo/go-web-app/issues/1483)
|
| 328 |
+
|
| 329 |
+
- 44623a7: Undo DREF Imminent changes
|
| 330 |
+
- b57c453: Show the number of people assisted in the DREF Final Report export in [#1665](https://github.com/IFRCGo/go-web-app/issues/1665)
|
| 331 |
+
|
| 332 |
+
## 7.10.0
|
| 333 |
+
|
| 334 |
+
### Minor Changes
|
| 335 |
+
|
| 336 |
+
- 4f89133: Fix DREF PGA export styling
|
| 337 |
+
|
| 338 |
+
## 7.9.0
|
| 339 |
+
|
| 340 |
+
### Minor Changes
|
| 341 |
+
|
| 342 |
+
- 7927522: Update Imminent DREF Application in [#1455](https://github.com/IFRCGo/go-web-app/issues/1455)
|
| 343 |
+
|
| 344 |
+
- Hide sections/fields
|
| 345 |
+
- Rename sections/fields
|
| 346 |
+
- Remove sections/fields
|
| 347 |
+
- Reflect changes in the PDF export
|
| 348 |
+
|
| 349 |
+
### Patch Changes
|
| 350 |
+
|
| 351 |
+
- Updated dependencies [4032688]
|
| 352 |
+
- @ifrc-go/[email protected]
|
| 353 |
+
|
| 354 |
+
## 7.8.1
|
| 355 |
+
|
| 356 |
+
### Patch Changes
|
| 357 |
+
|
| 358 |
+
- 9c51dee: Remove `summary` field from field report form
|
| 359 |
+
- Update @ifrc-go/ui version
|
| 360 |
+
|
| 361 |
+
## 7.8.0
|
| 362 |
+
|
| 363 |
+
### Minor Changes
|
| 364 |
+
|
| 365 |
+
- 4843cb0: Added Operational Learning 2.0
|
| 366 |
+
|
| 367 |
+
- Key Figures Overview in Operational Learning
|
| 368 |
+
- Map View for Operational Learning
|
| 369 |
+
- Learning by Sector Bar Chart
|
| 370 |
+
- Learning by Region Bar Chart
|
| 371 |
+
- Sources Over Time Line Chart
|
| 372 |
+
- Methodology changes for the prioritization step
|
| 373 |
+
- Added an option to regenerate cached summaries
|
| 374 |
+
- Summary post-processing and cleanup
|
| 375 |
+
- Enabled MDR code search in admin
|
| 376 |
+
|
| 377 |
+
### Patch Changes
|
| 378 |
+
|
| 379 |
+
- f96e177: Move field report/emergency title generation logic from client to server
|
| 380 |
+
- e85fc32: Integrate `crate-ci/typos` for code spell checking
|
| 381 |
+
- 4cdea2b: Add redirection logic for `preparedness#operational-learning`
|
| 382 |
+
- 9a50443: Add appeal doc type for appeal documents
|
| 383 |
+
- 817d56d: Display properly formatted appeal type in search results
|
| 384 |
+
- 1159fa4: Redirect obsolete URLs to recent ones
|
| 385 |
+
- redirect `/reports/` to `/field-reports/`
|
| 386 |
+
- redirect `/deployments/` -> `/surge/overview`
|
| 387 |
+
- Updated dependencies [4843cb0]
|
| 388 |
+
- @ifrc-go/[email protected]
|
| 389 |
+
|
| 390 |
+
## 7.7.0
|
| 391 |
+
|
| 392 |
+
### Minor Changes
|
| 393 |
+
|
| 394 |
+
- 3258b96: Add local unit validation workflow
|
| 395 |
+
|
| 396 |
+
### Patch Changes
|
| 397 |
+
|
| 398 |
+
- Updated dependencies [c5a446f]
|
| 399 |
+
- @ifrc-go/[email protected]
|
| 400 |
+
|
| 401 |
+
## 7.6.6
|
| 402 |
+
|
| 403 |
+
### Patch Changes
|
| 404 |
+
|
| 405 |
+
- 8cdc946: Hide Local unit contact details on the list view for logged in users in [#1485](https://github.com/ifRCGo/go-web-app/issues/1485)
|
| 406 |
+
Update `tinymce-react` plugin to the latest version and enabled additional plugins, including support for lists in [#1481](https://github.com/ifRCGo/go-web-app/issues/1481)
|
| 407 |
+
- ecca810: Replace the from-communication-copied text of CoS Health header
|
| 408 |
+
- 7cf2514: Prioritize GDACS as the Primary Source for Imminent Risk Watch in [#1547](https://github.com/IFRCGo/go-web-app/issues/1547)
|
| 409 |
+
- 8485076: Add Organization type and Learning type filter in Operational learning in [#1469](https://github.com/IFRCGo/go-web-app/issues/1469)
|
| 410 |
+
- 766d98d: Auto append https:// for incomplete URLs in [#1505](https://github.com/IFRCGo/go-web-app/issues/1505)
|
| 411 |
+
|
| 412 |
+
## 7.6.5
|
| 413 |
+
|
| 414 |
+
### Patch Changes
|
| 415 |
+
|
| 416 |
+
- 478e73b: Update labels for severity control in Imminent Risk Map
|
| 417 |
+
Update navigation for the events in Imminent Risk Map
|
| 418 |
+
Fix issue displayed when opening a DREF import template
|
| 419 |
+
Fix submission issue when importing a DREF import file
|
| 420 |
+
- f82f846: Update Health Section in Catalogue of Surge Services
|
| 421 |
+
- ade84aa: Display ICRC Presence
|
| 422 |
+
- Display ICRC presence across partner countries
|
| 423 |
+
- Highlight key operational countries
|
| 424 |
+
|
| 425 |
+
## 7.6.4
|
| 426 |
+
|
| 427 |
+
### Patch Changes
|
| 428 |
+
|
| 429 |
+
- d85f64d: Update Imminent Events
|
| 430 |
+
|
| 431 |
+
- Hide WFP ADAM temporarily from list sources
|
| 432 |
+
- Show exposure control for cyclones from GDACS only
|
| 433 |
+
|
| 434 |
+
## 7.6.3
|
| 435 |
+
|
| 436 |
+
### Patch Changes
|
| 437 |
+
|
| 438 |
+
- 7bbf3d2: Update key insights disclaimer text in Ops. Learning
|
| 439 |
+
- 0e40681: Update FDRS data in Country / Context and Structure / NS indicators
|
| 440 |
+
|
| 441 |
+
- Add separate icon for each field for data year
|
| 442 |
+
- Use separate icon for disaggregation
|
| 443 |
+
- Update descriptions on dref import template (more details on _Missing / to be implemented_ section in https://github.com/IFRCGo/go-web-app/pull/1434#issuecomment-2459034932)
|
| 444 |
+
|
| 445 |
+
- Updated dependencies [801ec3c]
|
| 446 |
+
- @ifrc-go/[email protected]
|
| 447 |
+
|
| 448 |
+
## 7.6.2
|
| 449 |
+
|
| 450 |
+
### Patch Changes
|
| 451 |
+
|
| 452 |
+
- 4fa6a36: Updated PER terminology and add PER logo in PER PDF export
|
| 453 |
+
- 813e93f: Add link to GO UI storybook in resources page
|
| 454 |
+
- 20dfeb3: Update DREF import template
|
| 455 |
+
- Update guidance
|
| 456 |
+
- Improve template stylings
|
| 457 |
+
- Update message in error popup when import fails
|
| 458 |
+
- 8a18ad8: Add beta tag, URL redirect, and link to old dashboard on Ops Learning
|
| 459 |
+
|
| 460 |
+
## 7.6.1
|
| 461 |
+
|
| 462 |
+
### Patch Changes
|
| 463 |
+
|
| 464 |
+
- 7afaf34: Fix null event in appeal for operational learning
|
| 465 |
+
|
| 466 |
+
## 7.6.0
|
| 467 |
+
|
| 468 |
+
### Minor Changes
|
| 469 |
+
|
| 470 |
+
- Add new Operational Learning Page
|
| 471 |
+
|
| 472 |
+
- Add link to Operational Learning page under `Learn` navigation menu
|
| 473 |
+
- Integrate LLM summaries for Operational Learning
|
| 474 |
+
|
| 475 |
+
## 7.5.3
|
| 476 |
+
|
| 477 |
+
### Patch Changes
|
| 478 |
+
|
| 479 |
+
- d7f5f53: Revamp risk imminent events for cyclone
|
| 480 |
+
- Visualize storm position, forecast uncertainty, track line and exposed area differently
|
| 481 |
+
- Add option to toggle visibility of these different layers
|
| 482 |
+
- Add severity legend for exposure
|
| 483 |
+
- Update styling for items in event list
|
| 484 |
+
- Update styling for event details page
|
| 485 |
+
- 36a64fa: Integrate multi-select functionality in operational learning filters to allow selection of multiple filter items.
|
| 486 |
+
- 894d00c: Add a new 404 page
|
| 487 |
+
- 7757e54: Add an option to download excel import template for DREF (Response) which user can fill up and import.
|
| 488 |
+
- a8d021d: Update resources page
|
| 489 |
+
- Add a new video for LocalUnits
|
| 490 |
+
- Update ordering of videos
|
| 491 |
+
- aea512d: Prevent users from pasting images into rich text field
|
| 492 |
+
- fd54657: Add Terms and Conditions page
|
| 493 |
+
- bf55ccc: Add Cookie Policy page
|
| 494 |
+
- df80c4f: Fix contact details in Field Report being always required when filled once
|
| 495 |
+
- 81dc3bd: Added color mapping based on PER Area and Rating across all PER charts
|
| 496 |
+
- Updated dependencies [dd92691]
|
| 497 |
+
- Updated dependencies [d7f5f53]
|
| 498 |
+
- Updated dependencies [fe6a455]
|
| 499 |
+
- Updated dependencies [81dc3bd]
|
| 500 |
+
- @ifrc-go/[email protected]
|
| 501 |
+
|
| 502 |
+
## 7.5.2
|
| 503 |
+
|
| 504 |
+
### Patch Changes
|
| 505 |
+
|
| 506 |
+
- 37bba31: Add collaboration guide
|
| 507 |
+
|
| 508 |
+
## 7.5.1
|
| 509 |
+
|
| 510 |
+
### Patch Changes
|
| 511 |
+
|
| 512 |
+
- 2a5e4a1: Add Core Competency Framework link to Resources page in [#1331](https://github.com/IFRCGo/go-web-app/issues/1331)
|
| 513 |
+
- 31eaa97: Add Health Mapping Report to Resources page in [#1331](https://github.com/IFRCGo/go-web-app/issues/1331)
|
| 514 |
+
- 4192da1: - Local Units popup, view/edit mode improvements in [#1178](https://github.com/IFRCGo/go-web-app/issues/1178)
|
| 515 |
+
- Remove ellipsize heading option in local units map popup
|
| 516 |
+
- Local units title on popup are now clickable that opens up a modal to show details
|
| 517 |
+
- Added an Edit button to the View Mode for users with edit permissions
|
| 518 |
+
- Users will now see a **disabled grey button** when the content is already validated
|
| 519 |
+
- 5c7ab88: Display the public visibility field report to public users in [#1743](https://github.com/IFRCGo/go-web-app/issues/1343)
|
| 520 |
+
|
| 521 |
+
## 7.5.0
|
| 522 |
+
|
| 523 |
+
### Minor Changes
|
| 524 |
+
|
| 525 |
+
- 5845699: Clean up Resources page
|
| 526 |
+
|
| 527 |
+
## 7.4.2
|
| 528 |
+
|
| 529 |
+
### Patch Changes
|
| 530 |
+
|
| 531 |
+
- d734e04: - Fix duplication volunteer label in the Field Report details
|
| 532 |
+
- Fix rating visibility in the Country > NS Overview > Strategic priorities page
|
| 533 |
+
|
| 534 |
+
## 7.4.1
|
| 535 |
+
|
| 536 |
+
### Patch Changes
|
| 537 |
+
|
| 538 |
+
- a4f77ab: Fetch and use latest available WorldBank data in [#571](https://github.com/IFRCGo/go-api/issues/2224)
|
| 539 |
+
- ebf033a: Update Technical Competencies Link on the Cash page of the Catalogue of Surge Services in [#1290](https://github.com/IFRCGo/go-web-app/issues/1290)
|
| 540 |
+
- 18d0dc9: Use `molnix status` to filter surge alerts in [#2208](https://github.com/IFRCGo/go-api/issues/2208)
|
| 541 |
+
- b070c66: Check guest user permission for local units
|
| 542 |
+
- 72df1f2: Add new drone icon for UAV team in [#1280](https://github.com/IFRCGo/go-web-app/issues/1280)
|
| 543 |
+
- 2ff7940: Link version number to release notes on GitHub in [#1004](https://github.com/IFRCGo/go-web-app/issues/1004)
|
| 544 |
+
Updated @ifrc-go/icons to v2.0.1
|
| 545 |
+
- Updated dependencies [72df1f2]
|
| 546 |
+
- @ifrc-go/[email protected]
|
| 547 |
+
|
| 548 |
+
## 7.4.0
|
| 549 |
+
|
| 550 |
+
### Minor Changes
|
| 551 |
+
|
| 552 |
+
- b6bd6aa: Implement Guest User Permission in [#1237](https://github.com/IFRCGo/go-web-app/issues/1237)
|
| 553 |
+
|
| 554 |
+
## 7.3.13
|
| 555 |
+
|
| 556 |
+
### Patch Changes
|
| 557 |
+
|
| 558 |
+
- 453a397: - Update Local Unit map, table and form to match the updated design in [#1178](https://github.com/IFRCGo/go-web-app/issues/1178)
|
| 559 |
+
- Add delete button in Local units table and form
|
| 560 |
+
- Use filter prop in container and remove manual stylings
|
| 561 |
+
- Update size of WikiLink to match height of other action items
|
| 562 |
+
- Add error boundary to BaseMap component
|
| 563 |
+
- Updated dependencies [453a397]
|
| 564 |
+
- @ifrc-go/[email protected]
|
| 565 |
+
|
| 566 |
+
## 7.3.12
|
| 567 |
+
|
| 568 |
+
### Patch Changes
|
| 569 |
+
|
| 570 |
+
- ba6734e: Show admin labels in maps in different languages, potentially fixing [#1036](https://github.com/IFRCGo/go-web-app/issues/1036)
|
| 571 |
+
|
| 572 |
+
## 7.3.11
|
| 573 |
+
|
| 574 |
+
### Patch Changes
|
| 575 |
+
|
| 576 |
+
- d9491a2: Fix appeals statistics calculation
|
| 577 |
+
|
| 578 |
+
## 7.3.10
|
| 579 |
+
|
| 580 |
+
### Patch Changes
|
| 581 |
+
|
| 582 |
+
- 3508c83: Add missing validations in DREF forms
|
| 583 |
+
- 3508c83: Fix region filter in All Appeals table
|
| 584 |
+
- 073fa1e: Remove personal detail for focal point in local units table
|
| 585 |
+
- b508475: Add June 2024 Catalogue of Surge Services Updates
|
| 586 |
+
- 3508c83: Handle countries with no bounding box
|
| 587 |
+
- d9491a2: Fix appeals based statistics calculation
|
| 588 |
+
- Updated dependencies [073fa1e]
|
| 589 |
+
- @ifrc-go/[email protected]
|
| 590 |
+
|
| 591 |
+
## 7.3.9
|
| 592 |
+
|
| 593 |
+
### Patch Changes
|
| 594 |
+
|
| 595 |
+
- 49f5410: - Reorder CoS list
|
| 596 |
+
- Update texts in CoS strategic partnerships resource mobilisation
|
| 597 |
+
|
| 598 |
+
## 7.3.8
|
| 599 |
+
|
| 600 |
+
### Patch Changes
|
| 601 |
+
|
| 602 |
+
- 478ab69: Hide contact information from IFRC Presence
|
| 603 |
+
- 3fbe60f: Hide add/edit local units on production environment
|
| 604 |
+
- 90678ed: Show Organization Type properly in Account Details page
|
| 605 |
+
|
| 606 |
+
## 7.3.7
|
| 607 |
+
|
| 608 |
+
### Patch Changes
|
| 609 |
+
|
| 610 |
+
- 909a5e2: Fix Appeals table for Africa Region
|
| 611 |
+
- 5a1ae43: Add presentation mode in local units map
|
| 612 |
+
- 96120aa: Fix DREF exports margins and use consistent date format
|
| 613 |
+
- 8a4f26d: Avoid crash on country pages for countries without bbox
|
| 614 |
+
|
| 615 |
+
## 7.3.6
|
| 616 |
+
|
| 617 |
+
### Patch Changes
|
| 618 |
+
|
| 619 |
+
- 1b4b6df: Add local unit form
|
| 620 |
+
- 2631a9f: Add office type and location information for IFRC delegation office
|
| 621 |
+
- 2d7a6a5: - Enable ability to start PER in IFRC supported languages
|
| 622 |
+
- Make PER forms `readOnly` in case of language mismatch
|
| 623 |
+
- e4bf098: Fix incorrect statistics for past appeals of a country
|
| 624 |
+
- Updated dependencies [0ab207d]
|
| 625 |
+
- Updated dependencies [66151a7]
|
| 626 |
+
- @ifrc-go/[email protected]
|
| 627 |
+
|
| 628 |
+
## 7.3.5
|
| 629 |
+
|
| 630 |
+
### Patch Changes
|
| 631 |
+
|
| 632 |
+
- 894a762: Fix seasonal risk score in regional and global risk watch
|
| 633 |
+
|
| 634 |
+
## 7.3.4
|
| 635 |
+
|
| 636 |
+
### Patch Changes
|
| 637 |
+
|
| 638 |
+
- d368ada: Fix GNI per capita in country profile overview
|
| 639 |
+
|
| 640 |
+
## 7.3.3
|
| 641 |
+
|
| 642 |
+
### Patch Changes
|
| 643 |
+
|
| 644 |
+
- 73e1966: Update CoS pages as mentioned in #913
|
| 645 |
+
- 179a073: Show all head of delegation under IFRC Presence
|
| 646 |
+
- 98d6b62: Fix region operation map to apply filter for Africa
|
| 647 |
+
|
| 648 |
+
## 7.3.2
|
| 649 |
+
|
| 650 |
+
### Patch Changes
|
| 651 |
+
|
| 652 |
+
- f83c12b: Show Local name when available and use English name as fallback for local units data
|
| 653 |
+
|
| 654 |
+
## 7.3.1
|
| 655 |
+
|
| 656 |
+
### Patch Changes
|
| 657 |
+
|
| 658 |
+
- 7f0212b: Integrate mapbox street view for local units map
|
| 659 |
+
- Updated dependencies [7f0212b]
|
| 660 |
+
- @ifrc-go/[email protected]
|
| 661 |
+
|
| 662 |
+
## 7.3.0
|
| 663 |
+
|
| 664 |
+
### Minor Changes
|
| 665 |
+
|
| 666 |
+
- 0dffd52: Add table view in NS local units
|
| 667 |
+
|
| 668 |
+
## 7.2.5
|
| 669 |
+
|
| 670 |
+
### Patch Changes
|
| 671 |
+
|
| 672 |
+
- 556766e: - Refetch token list after new token is created
|
| 673 |
+
- Update link for terms and conditions for Montandon
|
| 674 |
+
|
| 675 |
+
## 7.2.4
|
| 676 |
+
|
| 677 |
+
### Patch Changes
|
| 678 |
+
|
| 679 |
+
- 30eac3c: Add option to generate API token for Montandon in the user profile
|
| 680 |
+
|
| 681 |
+
## 7.2.3
|
| 682 |
+
|
| 683 |
+
### Patch Changes
|
| 684 |
+
|
| 685 |
+
- Fix crash due to undefined ICRC presence in country page
|
| 686 |
+
|
| 687 |
+
## 7.2.2
|
| 688 |
+
|
| 689 |
+
### Patch Changes
|
| 690 |
+
|
| 691 |
+
- - Update country risk page sources
|
| 692 |
+
- Update CoS pages
|
| 693 |
+
- Updated dependencies [a1c0554]
|
| 694 |
+
- Updated dependencies [e9552b4]
|
| 695 |
+
- @ifrc-go/[email protected]
|
| 696 |
+
|
| 697 |
+
## 7.2.1
|
| 698 |
+
|
| 699 |
+
### Patch Changes
|
| 700 |
+
|
| 701 |
+
- Remove personal identifiable information for local units
|
| 702 |
+
|
| 703 |
+
## 7.2.0
|
| 704 |
+
|
| 705 |
+
### Minor Changes
|
| 706 |
+
|
| 707 |
+
- 9657d4b: Update country pages with appropriate source links
|
| 708 |
+
- 66fa7cf: Show FDRS data retrieval year in NS indicators
|
| 709 |
+
- b69e8e5: Update IFRC legal status link
|
| 710 |
+
- 300250a: Show latest strategic plan of National Society under Strategic Priorities
|
| 711 |
+
- 9657d4b: Add GO Wiki links for country page sections
|
| 712 |
+
- b38d9d9: Improve overall styling of country pages
|
| 713 |
+
- Make loading animation consistent across all pages
|
| 714 |
+
- Make empty message consistent
|
| 715 |
+
- Use ChartContainer and update usage of charting hooks
|
| 716 |
+
- Update BaseMap to extend defaultMapOptions (instead of replacing it)
|
| 717 |
+
- Add an option to provide popupClassName in MapPopup
|
| 718 |
+
- 80be711: Rename `Supporting Partners` to `Partners`.
|
| 719 |
+
- Update IFRC legal status link.
|
| 720 |
+
- Update the name of the strategic priorities link to indicate that they were created by the National Society.
|
| 721 |
+
- 176e01b: Simplify usage of PER question group in PER assessment form
|
| 722 |
+
- Add min widths in account table columns
|
| 723 |
+
|
| 724 |
+
## 7.1.5
|
| 725 |
+
|
| 726 |
+
### Patch Changes
|
| 727 |
+
|
| 728 |
+
- Updated dependencies
|
| 729 |
+
- @ifrc-go/[email protected]
|
go-web-app-develop/app/env.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig, Schema } from '@julr/vite-plugin-validate-env';
|
| 2 |
+
|
| 3 |
+
export default defineConfig({
|
| 4 |
+
APP_TITLE: Schema.string(),
|
| 5 |
+
APP_ENVIRONMENT: (key, value) => {
|
| 6 |
+
// NOTE: APP_ENVIRONMENT_PLACEHOLDER is meant to be used with image builds
|
| 7 |
+
// The value will be later replaced with the actual value
|
| 8 |
+
const regex = /^production|staging|testing|alpha-\d+|development|APP_ENVIRONMENT_PLACEHOLDER$/;
|
| 9 |
+
const valid = !!value && (value.match(regex) !== null);
|
| 10 |
+
if (!valid) {
|
| 11 |
+
throw new Error(`Value for environment variable "${key}" must match regex "${regex}", instead received "${value}"`);
|
| 12 |
+
}
|
| 13 |
+
if (value === 'APP_ENVIRONMENT_PLACEHOLDER') {
|
| 14 |
+
console.warn(`Using ${value} for app environment. Make sure to not use this for builds without helm chart`)
|
| 15 |
+
}
|
| 16 |
+
return value as ('production' | 'staging' | 'testing' | `alpha-${number}` | 'development' | 'APP_ENVIRONMENT_PLACEHOLDER');
|
| 17 |
+
},
|
| 18 |
+
APP_API_ENDPOINT: Schema.string({ format: 'url', protocol: true, tld: false }),
|
| 19 |
+
APP_ADMIN_URL: Schema.string.optional({ format: 'url', protocol: true, tld: false }),
|
| 20 |
+
APP_MAPBOX_ACCESS_TOKEN: Schema.string(),
|
| 21 |
+
APP_TINY_API_KEY: Schema.string(),
|
| 22 |
+
APP_RISK_API_ENDPOINT: Schema.string({ format: 'url', protocol: true }),
|
| 23 |
+
APP_SDT_URL: Schema.string.optional({ format: 'url', protocol: true, tld: false }),
|
| 24 |
+
APP_SENTRY_DSN: Schema.string.optional(),
|
| 25 |
+
APP_SENTRY_TRACES_SAMPLE_RATE: Schema.number.optional(),
|
| 26 |
+
APP_SENTRY_REPLAYS_SESSION_SAMPLE_RATE: Schema.number.optional(),
|
| 27 |
+
APP_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE: Schema.number.optional(),
|
| 28 |
+
APP_GOOGLE_ANALYTICS_ID: Schema.string.optional(),
|
| 29 |
+
});
|
go-web-app-develop/app/eslint.config.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FlatCompat } from '@eslint/eslintrc';
|
| 2 |
+
import js from '@eslint/js';
|
| 3 |
+
import json from "@eslint/json";
|
| 4 |
+
import tseslint from "typescript-eslint";
|
| 5 |
+
import process from 'process';
|
| 6 |
+
|
| 7 |
+
const dirname = process.cwd();
|
| 8 |
+
|
| 9 |
+
const compat = new FlatCompat({
|
| 10 |
+
baseDirectory: dirname,
|
| 11 |
+
resolvePluginsRelativeTo: dirname,
|
| 12 |
+
});
|
| 13 |
+
|
| 14 |
+
const appConfigs = compat.config({
|
| 15 |
+
env: {
|
| 16 |
+
node: true,
|
| 17 |
+
browser: true,
|
| 18 |
+
es2020: true,
|
| 19 |
+
},
|
| 20 |
+
root: true,
|
| 21 |
+
extends: [
|
| 22 |
+
'airbnb',
|
| 23 |
+
'airbnb/hooks',
|
| 24 |
+
'plugin:@typescript-eslint/recommended',
|
| 25 |
+
'plugin:react-hooks/recommended',
|
| 26 |
+
],
|
| 27 |
+
parser: '@typescript-eslint/parser',
|
| 28 |
+
parserOptions: {
|
| 29 |
+
ecmaVersion: 'latest',
|
| 30 |
+
sourceType: 'module',
|
| 31 |
+
},
|
| 32 |
+
plugins: [
|
| 33 |
+
'@typescript-eslint',
|
| 34 |
+
'react-refresh',
|
| 35 |
+
'simple-import-sort',
|
| 36 |
+
'import-newlines'
|
| 37 |
+
],
|
| 38 |
+
settings: {
|
| 39 |
+
'import/parsers': {
|
| 40 |
+
'@typescript-eslint/parser': ['.ts', '.tsx']
|
| 41 |
+
},
|
| 42 |
+
'import/resolver': {
|
| 43 |
+
typescript: {
|
| 44 |
+
project: [
|
| 45 |
+
'./tsconfig.json',
|
| 46 |
+
],
|
| 47 |
+
},
|
| 48 |
+
},
|
| 49 |
+
},
|
| 50 |
+
rules: {
|
| 51 |
+
'react-refresh/only-export-components': 'warn',
|
| 52 |
+
|
| 53 |
+
'no-unused-vars': 0,
|
| 54 |
+
'@typescript-eslint/no-unused-vars': 1,
|
| 55 |
+
|
| 56 |
+
'no-use-before-define': 0,
|
| 57 |
+
'@typescript-eslint/no-use-before-define': 1,
|
| 58 |
+
|
| 59 |
+
'no-shadow': 0,
|
| 60 |
+
'@typescript-eslint/no-shadow': ['error'],
|
| 61 |
+
|
| 62 |
+
'@typescript-eslint/consistent-type-imports': [
|
| 63 |
+
'warn',
|
| 64 |
+
{
|
| 65 |
+
disallowTypeAnnotations: false,
|
| 66 |
+
fixStyle: 'inline-type-imports',
|
| 67 |
+
prefer: 'type-imports',
|
| 68 |
+
},
|
| 69 |
+
],
|
| 70 |
+
|
| 71 |
+
'import/no-extraneous-dependencies': [
|
| 72 |
+
'error',
|
| 73 |
+
{
|
| 74 |
+
devDependencies: [
|
| 75 |
+
'**/*.test.{ts,tsx}',
|
| 76 |
+
'eslint.config.js',
|
| 77 |
+
'postcss.config.cjs',
|
| 78 |
+
'stylelint.config.cjs',
|
| 79 |
+
'vite.config.ts',
|
| 80 |
+
],
|
| 81 |
+
optionalDependencies: false,
|
| 82 |
+
},
|
| 83 |
+
],
|
| 84 |
+
|
| 85 |
+
indent: ['error', 4, { SwitchCase: 1 }],
|
| 86 |
+
|
| 87 |
+
'import/no-cycle': ['error', { allowUnsafeDynamicCyclicDependency: true }],
|
| 88 |
+
|
| 89 |
+
'react/react-in-jsx-scope': 'off',
|
| 90 |
+
'camelcase': 'off',
|
| 91 |
+
|
| 92 |
+
'react/jsx-indent': ['error', 4],
|
| 93 |
+
'react/jsx-indent-props': ['error', 4],
|
| 94 |
+
'react/jsx-filename-extension': ['error', { extensions: ['.js', '.jsx', '.ts', '.tsx'] }],
|
| 95 |
+
|
| 96 |
+
'import/extensions': ['off', 'never'],
|
| 97 |
+
|
| 98 |
+
'react-hooks/rules-of-hooks': 'error',
|
| 99 |
+
'react-hooks/exhaustive-deps': 'warn',
|
| 100 |
+
|
| 101 |
+
'react/require-default-props': ['warn', { ignoreFunctionalComponents: true }],
|
| 102 |
+
'simple-import-sort/imports': 'warn',
|
| 103 |
+
'simple-import-sort/exports': 'warn',
|
| 104 |
+
'import-newlines/enforce': ['warn', 1]
|
| 105 |
+
},
|
| 106 |
+
overrides: [
|
| 107 |
+
{
|
| 108 |
+
files: ['*.js', '*.jsx', '*.ts', '*.tsx'],
|
| 109 |
+
rules: {
|
| 110 |
+
'simple-import-sort/imports': [
|
| 111 |
+
'error',
|
| 112 |
+
{
|
| 113 |
+
'groups': [
|
| 114 |
+
// side effect imports
|
| 115 |
+
['^\\u0000'],
|
| 116 |
+
// packages `react` related packages come first
|
| 117 |
+
['^react', '^@?\\w'],
|
| 118 |
+
// internal packages
|
| 119 |
+
['^#.+$'],
|
| 120 |
+
// parent imports. Put `..` last
|
| 121 |
+
// other relative imports. Put same-folder imports and `.` last
|
| 122 |
+
['^\\.\\.(?!/?$)', '^\\.\\./?$', '^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
|
| 123 |
+
// style imports
|
| 124 |
+
['^.+\\.json$', '^.+\\.module.css$'],
|
| 125 |
+
]
|
| 126 |
+
}
|
| 127 |
+
]
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
]
|
| 131 |
+
}).map((conf) => ({
|
| 132 |
+
...conf,
|
| 133 |
+
files: ['src/**/*.tsx', 'src/**/*.jsx', 'src/**/*.ts', 'src/**/*.js'],
|
| 134 |
+
ignores: [
|
| 135 |
+
"node_modules/",
|
| 136 |
+
"build/",
|
| 137 |
+
"coverage/",
|
| 138 |
+
'src/generated/types.ts'
|
| 139 |
+
],
|
| 140 |
+
}));
|
| 141 |
+
|
| 142 |
+
const otherConfig = {
|
| 143 |
+
files: ['*.js', '*.ts', '*.cjs'],
|
| 144 |
+
...js.configs.recommended,
|
| 145 |
+
...tseslint.configs.recommended,
|
| 146 |
+
};
|
| 147 |
+
|
| 148 |
+
const jsonConfig = {
|
| 149 |
+
files: ['**/*.json'],
|
| 150 |
+
language: 'json/json',
|
| 151 |
+
rules: {
|
| 152 |
+
'json/no-duplicate-keys': 'error',
|
| 153 |
+
},
|
| 154 |
+
};
|
| 155 |
+
|
| 156 |
+
export default [
|
| 157 |
+
{
|
| 158 |
+
plugins: {
|
| 159 |
+
json,
|
| 160 |
+
},
|
| 161 |
+
},
|
| 162 |
+
...appConfigs,
|
| 163 |
+
otherConfig,
|
| 164 |
+
jsonConfig,
|
| 165 |
+
];
|
go-web-app-develop/app/index.html
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html
|
| 3 |
+
lang="en"
|
| 4 |
+
translate="no"
|
| 5 |
+
>
|
| 6 |
+
<head>
|
| 7 |
+
<meta charset="UTF-8" />
|
| 8 |
+
<link
|
| 9 |
+
rel="icon"
|
| 10 |
+
type="image/svg+xml"
|
| 11 |
+
href="/go-icon.svg"
|
| 12 |
+
/>
|
| 13 |
+
<meta
|
| 14 |
+
name="viewport"
|
| 15 |
+
content="width=device-width, initial-scale=1.0"
|
| 16 |
+
/>
|
| 17 |
+
<meta
|
| 18 |
+
name="description"
|
| 19 |
+
content=""
|
| 20 |
+
/>
|
| 21 |
+
<title>
|
| 22 |
+
%APP_TITLE%
|
| 23 |
+
</title>
|
| 24 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 25 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 26 |
+
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 27 |
+
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 28 |
+
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 29 |
+
|
| 30 |
+
<style>
|
| 31 |
+
html, body {
|
| 32 |
+
margin: 0;
|
| 33 |
+
padding: 0;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
body {
|
| 37 |
+
font-family: Poppins, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
@media screen {
|
| 41 |
+
body {
|
| 42 |
+
background-color: #f7f7f7;
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
#webapp-preload {
|
| 47 |
+
width: 100vw;
|
| 48 |
+
height: 100vh;
|
| 49 |
+
display: flex;
|
| 50 |
+
align-items: center;
|
| 51 |
+
justify-content: center;
|
| 52 |
+
}
|
| 53 |
+
</style>
|
| 54 |
+
</head>
|
| 55 |
+
<body>
|
| 56 |
+
<noscript>
|
| 57 |
+
%APP_TITLE% needs JS.
|
| 58 |
+
</noscript>
|
| 59 |
+
<div id="webapp-root">
|
| 60 |
+
<div id="webapp-preload">
|
| 61 |
+
%APP_TITLE% loading...
|
| 62 |
+
</div>
|
| 63 |
+
</div>
|
| 64 |
+
<script
|
| 65 |
+
type="module"
|
| 66 |
+
src="/src/index.tsx"
|
| 67 |
+
></script>
|
| 68 |
+
</body>
|
| 69 |
+
</html>
|
go-web-app-develop/app/package.json
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "go-web-app",
|
| 3 |
+
"version": "7.21.0-beta.2",
|
| 4 |
+
"type": "module",
|
| 5 |
+
"private": true,
|
| 6 |
+
"license": "MIT",
|
| 7 |
+
"repository": {
|
| 8 |
+
"type": "git",
|
| 9 |
+
"url": "git+https://github.com/IFRCGo/go-web-app.git",
|
| 10 |
+
"directory": "app"
|
| 11 |
+
},
|
| 12 |
+
"scripts": {
|
| 13 |
+
"translatte": "tsx scripts/translatte/main.ts",
|
| 14 |
+
"translatte:generate": "pnpm translatte generate-migration ../translationMigrations ./src/**/i18n.json ../packages/ui/src/**/i18n.json",
|
| 15 |
+
"translatte:lint": "pnpm translatte lint ./src/**/i18n.json ../packages/ui/src/**/i18n.json",
|
| 16 |
+
"initialize:type": "mkdir -p generated/ && pnpm initialize:type:go-api && pnpm initialize:type:risk-api",
|
| 17 |
+
"initialize:type:go-api": "test -f ./generated/types.ts && true || cp types.stub.ts ./generated/types.ts",
|
| 18 |
+
"initialize:type:risk-api": "test -f ./generated/riskTypes.ts && true || cp types.stub.ts ./generated/riskTypes.ts",
|
| 19 |
+
"generate:type": "pnpm generate:type:go-api && pnpm generate:type:risk-api",
|
| 20 |
+
"generate:type:go-api": "dotenv -- cross-var openapi-typescript \"%APP_API_ENDPOINT%api-docs/\" -o ./generated/types.ts --alphabetize",
|
| 21 |
+
"generate:type:risk-api": "dotenv -- cross-var openapi-typescript \"%APP_RISK_API_ENDPOINT%api-docs/\" -o ./generated/riskTypes.ts --alphabetize",
|
| 22 |
+
"prestart": "pnpm initialize:type",
|
| 23 |
+
"start": "pnpm -F @ifrc-go/ui build && vite",
|
| 24 |
+
"prebuild": "pnpm initialize:type",
|
| 25 |
+
"build": "pnpm -F @ifrc-go/ui build && vite build",
|
| 26 |
+
"preview": "vite preview",
|
| 27 |
+
"pretypecheck": "pnpm initialize:type",
|
| 28 |
+
"typecheck": "tsc",
|
| 29 |
+
"prelint:js": "pnpm initialize:type",
|
| 30 |
+
"lint:js": "eslint src",
|
| 31 |
+
"lint:css": "stylelint \"./src/**/*.css\"",
|
| 32 |
+
"lint:translation": "pnpm translatte:lint",
|
| 33 |
+
"lint": "pnpm lint:js && pnpm lint:css && pnpm lint:translation",
|
| 34 |
+
"lint:fix": "pnpm lint:js --fix && pnpm lint:css --fix",
|
| 35 |
+
"test": "vitest",
|
| 36 |
+
"test:coverage": "vitest run --coverage",
|
| 37 |
+
"surge:deploy": "branch=$(git rev-parse --symbolic-full-name --abbrev-ref HEAD); branch=$(echo $branch | tr ./ -); cp ../build/index.html ../build/200.html; surge -p ../build/ -d https://ifrc-go-$branch.surge.sh",
|
| 38 |
+
"surge:teardown": "branch=$(git rev-parse --symbolic-full-name --abbrev-ref HEAD); branch=$(echo $branch | tr ./ -); surge teardown https://ifrc-go-$branch.surge.sh"
|
| 39 |
+
},
|
| 40 |
+
"dependencies": {
|
| 41 |
+
"@ifrc-go/icons": "^2.0.1",
|
| 42 |
+
"@ifrc-go/ui": "workspace:^",
|
| 43 |
+
"@sentry/react": "^7.81.1",
|
| 44 |
+
"@tinymce/tinymce-react": "^5.1.1",
|
| 45 |
+
"@togglecorp/fujs": "^2.1.1",
|
| 46 |
+
"@togglecorp/re-map": "^0.3.0",
|
| 47 |
+
"@togglecorp/toggle-form": "^2.0.4",
|
| 48 |
+
"@togglecorp/toggle-request": "^1.0.0-beta.3",
|
| 49 |
+
"@turf/bbox": "^6.5.0",
|
| 50 |
+
"@turf/buffer": "^6.5.0",
|
| 51 |
+
"exceljs": "^4.3.0",
|
| 52 |
+
"file-saver": "^2.0.5",
|
| 53 |
+
"html-to-image": "^1.11.11",
|
| 54 |
+
"mapbox-gl": "^1.13.0",
|
| 55 |
+
"papaparse": "^5.4.1",
|
| 56 |
+
"react": "^18.2.0",
|
| 57 |
+
"react-dom": "^18.2.0",
|
| 58 |
+
"react-router-dom": "^6.18.0",
|
| 59 |
+
"sanitize-html": "^2.10.0"
|
| 60 |
+
},
|
| 61 |
+
"devDependencies": {
|
| 62 |
+
"@eslint/eslintrc": "^3.1.0",
|
| 63 |
+
"@eslint/js": "^9.20.0",
|
| 64 |
+
"@eslint/json": "^0.5.0",
|
| 65 |
+
"@julr/vite-plugin-validate-env": "^1.0.1",
|
| 66 |
+
"@types/file-saver": "^2.0.5",
|
| 67 |
+
"@types/mapbox-gl": "^1.13.0",
|
| 68 |
+
"@types/node": "^20.11.6",
|
| 69 |
+
"@types/papaparse": "^5.3.8",
|
| 70 |
+
"@types/react": "^18.0.28",
|
| 71 |
+
"@types/react-dom": "^18.0.11",
|
| 72 |
+
"@types/sanitize-html": "^2.9.0",
|
| 73 |
+
"@types/yargs": "^17.0.32",
|
| 74 |
+
"@typescript-eslint/eslint-plugin": "^8.11.0",
|
| 75 |
+
"@typescript-eslint/parser": "^8.11.0",
|
| 76 |
+
"@vitejs/plugin-react-swc": "^3.5.0",
|
| 77 |
+
"@vitest/coverage-v8": "^1.2.2",
|
| 78 |
+
"autoprefixer": "^10.4.14",
|
| 79 |
+
"cross-var": "^1.1.0",
|
| 80 |
+
"dotenv-cli": "^7.4.2",
|
| 81 |
+
"eslint": "^9.20.1",
|
| 82 |
+
"eslint-config-airbnb": "^19.0.4",
|
| 83 |
+
"eslint-import-resolver-typescript": "^3.6.3",
|
| 84 |
+
"eslint-plugin-import": "^2.31.0",
|
| 85 |
+
"eslint-plugin-import-exports-imports-resolver": "^1.0.1",
|
| 86 |
+
"eslint-plugin-import-newlines": "^1.3.4",
|
| 87 |
+
"eslint-plugin-jsx-a11y": "^6.10.1",
|
| 88 |
+
"eslint-plugin-react": "^7.37.4",
|
| 89 |
+
"eslint-plugin-react-hooks": "^5.0.0",
|
| 90 |
+
"eslint-plugin-react-refresh": "^0.4.13",
|
| 91 |
+
"eslint-plugin-simple-import-sort": "^12.1.1",
|
| 92 |
+
"fast-glob": "^3.3.2",
|
| 93 |
+
"happy-dom": "^9.18.3",
|
| 94 |
+
"openapi-typescript": "6.5.5",
|
| 95 |
+
"postcss": "^8.5.3",
|
| 96 |
+
"postcss-nested": "^7.0.2",
|
| 97 |
+
"postcss-normalize": "^13.0.1",
|
| 98 |
+
"postcss-preset-env": "^10.1.5",
|
| 99 |
+
"rollup-plugin-visualizer": "^5.9.0",
|
| 100 |
+
"stylelint": "^16.17.0",
|
| 101 |
+
"stylelint-config-concentric": "^2.0.2",
|
| 102 |
+
"stylelint-config-recommended": "^15.0.0",
|
| 103 |
+
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
|
| 104 |
+
"surge": "^0.23.1",
|
| 105 |
+
"ts-md5": "^1.3.1",
|
| 106 |
+
"tsx": "^4.7.2",
|
| 107 |
+
"typescript": "^5.5.2",
|
| 108 |
+
"typescript-eslint": "^8.26.0",
|
| 109 |
+
"vite": "^5.0.10",
|
| 110 |
+
"vite-plugin-checker": "^0.7.0",
|
| 111 |
+
"vite-plugin-compression2": "^0.11.0",
|
| 112 |
+
"vite-plugin-radar": "^0.9.2",
|
| 113 |
+
"vite-plugin-svgr": "^4.2.0",
|
| 114 |
+
"vite-plugin-webfont-dl": "^3.9.4",
|
| 115 |
+
"vite-tsconfig-paths": "^4.2.2",
|
| 116 |
+
"vitest": "^1.2.2",
|
| 117 |
+
"yargs": "^17.7.2"
|
| 118 |
+
}
|
| 119 |
+
}
|
go-web-app-develop/app/postcss.config.cjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = {
|
| 2 |
+
plugins: [
|
| 3 |
+
require('postcss-preset-env'),
|
| 4 |
+
require('postcss-nested'),
|
| 5 |
+
require('postcss-normalize'),
|
| 6 |
+
require('autoprefixer'),
|
| 7 |
+
],
|
| 8 |
+
};
|
go-web-app-develop/app/public/go-icon.svg
ADDED
|
|
go-web-app-develop/app/scripts/translatte/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# translatte
|
| 2 |
+
|
| 3 |
+
A simple script to synchronize translations in source code to translations in
|
| 4 |
+
server
|
| 5 |
+
|
| 6 |
+
## Usecase
|
| 7 |
+
|
| 8 |
+
### Generating migrations
|
| 9 |
+
|
| 10 |
+
When adding a new feature or updating existing feature or removing an
|
| 11 |
+
existing feature on the codebase, we may need to update the strings used
|
| 12 |
+
in the application.
|
| 13 |
+
|
| 14 |
+
Developers can change the translations using their preferred choice of editor.
|
| 15 |
+
|
| 16 |
+
Once all of the changes have been made, we can generate a migration file for the translations using:
|
| 17 |
+
|
| 18 |
+
```bash
|
| 19 |
+
pnpm translatte generate-migration ./src/translationMigrations ./src/**/i18n.json
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
Once the migration file has been created, the migration file can be committed to the VCS.
|
| 23 |
+
|
| 24 |
+
### Applying migrations
|
| 25 |
+
|
| 26 |
+
When we are deploying the changes to the server, we will need to update
|
| 27 |
+
the strings in the server.
|
| 28 |
+
|
| 29 |
+
We can generate the new set of strings for the server using:
|
| 30 |
+
|
| 31 |
+
```bash
|
| 32 |
+
pnpm translatte apply-migrations ./src/translationMigrations --last-migration "name_of_last_migration" --source "strings_json_from_server.json" --destination "new_strings_json_for_server.json"
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
### Merge migrations
|
| 36 |
+
|
| 37 |
+
Once the migrations are applied to the strings in the server, we can merge the migrations into a single file.
|
| 38 |
+
|
| 39 |
+
To merge migrations, we can run the following command:
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
pnpm translatte merge-migrations ./src/translationMigrations --from 'initial_migration.json' --to 'final_migration.json'
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
### Checking migrations
|
| 46 |
+
|
| 47 |
+
We can use the following command to check for valid migrations:
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
pnpm translatte lint ./src/**/i18n.json
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
### Listing migrations
|
| 54 |
+
|
| 55 |
+
We can use the following command to list all migrations:
|
| 56 |
+
|
| 57 |
+
```bash
|
| 58 |
+
pnpm translatte list-migrations ./src/translationMigrations
|
| 59 |
+
```
|
go-web-app-develop/app/scripts/translatte/commands/applyMigrations.test.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { expect } from 'vitest';
|
| 2 |
+
import { mkdirSync } from 'fs';
|
| 3 |
+
import { join } from 'path';
|
| 4 |
+
|
| 5 |
+
import { testWithTmpDir } from '../testHelpers';
|
| 6 |
+
import {
|
| 7 |
+
writeFilePromisify,
|
| 8 |
+
readJsonFilesContents,
|
| 9 |
+
} from '../utils';
|
| 10 |
+
import {
|
| 11 |
+
migrationContent1,
|
| 12 |
+
migrationContent2,
|
| 13 |
+
migrationContent3,
|
| 14 |
+
migrationContent4,
|
| 15 |
+
migrationContent5,
|
| 16 |
+
migrationContent6,
|
| 17 |
+
|
| 18 |
+
strings1,
|
| 19 |
+
strings2,
|
| 20 |
+
} from '../mockData';
|
| 21 |
+
import applyMigrations from './applyMigrations';
|
| 22 |
+
import { SourceFileContent } from '../types';
|
| 23 |
+
|
| 24 |
+
testWithTmpDir('test applyMigrations with no data in server', async ({ tmpdir }) => {
|
| 25 |
+
mkdirSync(join(tmpdir, 'migrations'));
|
| 26 |
+
const migrations = [
|
| 27 |
+
{ name: '000001-1000000000000.json', content: migrationContent1 },
|
| 28 |
+
{ name: '000002-1000000000000.json', content: migrationContent2 },
|
| 29 |
+
{ name: '000003-1000000000000.json', content: migrationContent3 },
|
| 30 |
+
{ name: '000004-1000000000000.json', content: migrationContent4 },
|
| 31 |
+
{ name: '000005-1000000000000.json', content: migrationContent5 },
|
| 32 |
+
].map(({ name, content }) => writeFilePromisify(
|
| 33 |
+
join(tmpdir, 'migrations', name),
|
| 34 |
+
JSON.stringify(content, null, 4),
|
| 35 |
+
'utf8',
|
| 36 |
+
));
|
| 37 |
+
await Promise.all(migrations);
|
| 38 |
+
|
| 39 |
+
mkdirSync(join(tmpdir, 'strings'));
|
| 40 |
+
|
| 41 |
+
const emptySourceFile: SourceFileContent = {
|
| 42 |
+
last_migration: undefined,
|
| 43 |
+
strings: [],
|
| 44 |
+
};
|
| 45 |
+
await writeFilePromisify(
|
| 46 |
+
join(tmpdir, 'strings', 'before.json'),
|
| 47 |
+
JSON.stringify(emptySourceFile),
|
| 48 |
+
'utf8',
|
| 49 |
+
);
|
| 50 |
+
|
| 51 |
+
await applyMigrations(
|
| 52 |
+
tmpdir,
|
| 53 |
+
join(tmpdir, 'strings', 'before.json'),
|
| 54 |
+
join(tmpdir, 'strings', 'after.json'),
|
| 55 |
+
'migrations',
|
| 56 |
+
['np'],
|
| 57 |
+
undefined,
|
| 58 |
+
false,
|
| 59 |
+
);
|
| 60 |
+
|
| 61 |
+
const newSourceFiles = await readJsonFilesContents([
|
| 62 |
+
join(tmpdir, 'strings', 'after.json'),
|
| 63 |
+
]);
|
| 64 |
+
const newSourceFileContent = newSourceFiles[0].content;
|
| 65 |
+
|
| 66 |
+
expect(newSourceFileContent).toEqual(strings1)
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
testWithTmpDir('test applyMigrations with data in server', async ({ tmpdir }) => {
|
| 70 |
+
mkdirSync(join(tmpdir, 'migrations'));
|
| 71 |
+
const migrations = [
|
| 72 |
+
{ name: '000006-1000000000000.json', content: migrationContent6 },
|
| 73 |
+
].map(({ name, content }) => writeFilePromisify(
|
| 74 |
+
join(tmpdir, 'migrations', name),
|
| 75 |
+
JSON.stringify(content, null, 4),
|
| 76 |
+
'utf8',
|
| 77 |
+
));
|
| 78 |
+
await Promise.all(migrations);
|
| 79 |
+
|
| 80 |
+
mkdirSync(join(tmpdir, 'strings'));
|
| 81 |
+
|
| 82 |
+
await writeFilePromisify(
|
| 83 |
+
join(tmpdir, 'strings', 'before.json'),
|
| 84 |
+
JSON.stringify(strings1),
|
| 85 |
+
'utf8',
|
| 86 |
+
);
|
| 87 |
+
|
| 88 |
+
await applyMigrations(
|
| 89 |
+
tmpdir,
|
| 90 |
+
join(tmpdir, 'strings', 'before.json'),
|
| 91 |
+
join(tmpdir, 'strings', 'after.json'),
|
| 92 |
+
'migrations',
|
| 93 |
+
['np'],
|
| 94 |
+
undefined,
|
| 95 |
+
false,
|
| 96 |
+
);
|
| 97 |
+
|
| 98 |
+
const newSourceFiles = await readJsonFilesContents([
|
| 99 |
+
join(tmpdir, 'strings', 'after.json'),
|
| 100 |
+
]);
|
| 101 |
+
const newSourceFileContent = newSourceFiles[0].content;
|
| 102 |
+
|
| 103 |
+
expect(newSourceFileContent).toEqual(strings2)
|
| 104 |
+
});
|
go-web-app-develop/app/scripts/translatte/commands/applyMigrations.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Md5 } from 'ts-md5';
|
| 2 |
+
import { listToMap, isDefined, unique } from '@togglecorp/fujs';
|
| 3 |
+
import { isAbsolute, join, basename } from 'path';
|
| 4 |
+
import {
|
| 5 |
+
readSource,
|
| 6 |
+
getMigrationFilesAttrs,
|
| 7 |
+
readMigrations,
|
| 8 |
+
writeFilePromisify,
|
| 9 |
+
} from '../utils';
|
| 10 |
+
import { merge } from './mergeMigrations';
|
| 11 |
+
import {
|
| 12 |
+
SourceFileContent,
|
| 13 |
+
MigrationFileContent,
|
| 14 |
+
SourceStringItem,
|
| 15 |
+
} from '../types';
|
| 16 |
+
|
| 17 |
+
function apply(
|
| 18 |
+
strings: SourceStringItem[],
|
| 19 |
+
migrationActions: MigrationFileContent['actions'],
|
| 20 |
+
languages: string[],
|
| 21 |
+
): SourceStringItem[] {
|
| 22 |
+
const stringsMapping = listToMap(
|
| 23 |
+
strings,
|
| 24 |
+
(item) => `${item.page_name}:${item.key}:${item.language}` as string,
|
| 25 |
+
(item) => item,
|
| 26 |
+
);
|
| 27 |
+
|
| 28 |
+
const newMapping: {
|
| 29 |
+
[key: string]: SourceStringItem | null;
|
| 30 |
+
} = { };
|
| 31 |
+
|
| 32 |
+
unique(['en', ...languages]).forEach((language) => {
|
| 33 |
+
migrationActions.forEach((action) => {
|
| 34 |
+
const isSourceLanguage = language === 'en';
|
| 35 |
+
const key = `${action.namespace}:${action.key}:${language}`;
|
| 36 |
+
if (action.action === 'add') {
|
| 37 |
+
const hash = Md5.hashStr(action.value);
|
| 38 |
+
|
| 39 |
+
const prevValue = stringsMapping[key];
|
| 40 |
+
// NOTE: we are comparing hash instead of value so that this works for source language as well as other languages
|
| 41 |
+
if (prevValue && prevValue.hash !== hash) {
|
| 42 |
+
throw `Add: We already have string with different value for namespace '${action.namespace}' and key '${action.key}'`;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
if (newMapping[key]) {
|
| 46 |
+
throw `Add: We already have string for namespace '${action.namespace}' and key '${action.key}' in migration`;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
newMapping[key] = {
|
| 50 |
+
hash,
|
| 51 |
+
key: action.key,
|
| 52 |
+
page_name: action.namespace,
|
| 53 |
+
language,
|
| 54 |
+
value: isSourceLanguage
|
| 55 |
+
? action.value
|
| 56 |
+
: '',
|
| 57 |
+
};
|
| 58 |
+
} else if (action.action === 'remove') {
|
| 59 |
+
// NOTE: We can add or move string so we might have value in newMapping
|
| 60 |
+
if (!newMapping[key]) {
|
| 61 |
+
newMapping[key] = null;
|
| 62 |
+
}
|
| 63 |
+
} else {
|
| 64 |
+
const prevValue = stringsMapping[key];
|
| 65 |
+
if (!prevValue) {
|
| 66 |
+
throw `Update: We do not have string with namespace '${action.namespace}' and key '${action.key}'`;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
const newKey = action.newKey ?? prevValue.key;
|
| 70 |
+
const newNamespace = action.newNamespace ?? prevValue.page_name;
|
| 71 |
+
const newValue = isSourceLanguage
|
| 72 |
+
? action.newValue ?? prevValue.value
|
| 73 |
+
: prevValue.value;
|
| 74 |
+
const newHash = isSourceLanguage
|
| 75 |
+
? Md5.hashStr(newValue)
|
| 76 |
+
: prevValue.hash;
|
| 77 |
+
|
| 78 |
+
const newCanonicalKey = `${newNamespace}:${newKey}:${language}`;
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
// NOTE: remove the old key and add new key
|
| 82 |
+
if (!newMapping[key]) {
|
| 83 |
+
newMapping[key] = null;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
const newItem = {
|
| 87 |
+
hash: newHash,
|
| 88 |
+
key: newKey,
|
| 89 |
+
page_name: newNamespace,
|
| 90 |
+
language,
|
| 91 |
+
value: newValue,
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
if (newMapping[newCanonicalKey]) {
|
| 95 |
+
throw `Update: We already have string for namespace '${action.namespace}' and key '${action.key}' in migration`;
|
| 96 |
+
}
|
| 97 |
+
newMapping[newCanonicalKey] = newItem;
|
| 98 |
+
}
|
| 99 |
+
});
|
| 100 |
+
});
|
| 101 |
+
|
| 102 |
+
const finalMapping: typeof newMapping = {
|
| 103 |
+
...stringsMapping,
|
| 104 |
+
...newMapping,
|
| 105 |
+
};
|
| 106 |
+
|
| 107 |
+
return Object.values(finalMapping)
|
| 108 |
+
.filter(isDefined)
|
| 109 |
+
.sort((foo, bar) => (
|
| 110 |
+
foo.page_name.localeCompare(bar.page_name)
|
| 111 |
+
|| foo.key.localeCompare(bar.key)
|
| 112 |
+
|| foo.language.localeCompare(bar.language)
|
| 113 |
+
))
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
async function applyMigrations(
|
| 117 |
+
projectPath: string,
|
| 118 |
+
sourceFileName: string,
|
| 119 |
+
destinationFileName: string,
|
| 120 |
+
migrationFilePath: string,
|
| 121 |
+
languages: string[],
|
| 122 |
+
from: string | undefined,
|
| 123 |
+
dryRun: boolean | undefined,
|
| 124 |
+
) {
|
| 125 |
+
const sourcePath = isAbsolute(sourceFileName)
|
| 126 |
+
? sourceFileName
|
| 127 |
+
: join(projectPath, sourceFileName)
|
| 128 |
+
const sourceFile = await readSource(sourcePath)
|
| 129 |
+
|
| 130 |
+
const migrationFilesAttrs = await getMigrationFilesAttrs(projectPath, migrationFilePath);
|
| 131 |
+
const selectedMigrationFilesAttrs = from
|
| 132 |
+
? migrationFilesAttrs.filter((item) => (item.migrationName > from))
|
| 133 |
+
: migrationFilesAttrs;
|
| 134 |
+
|
| 135 |
+
console.info(`Found ${selectedMigrationFilesAttrs.length} migration files`);
|
| 136 |
+
|
| 137 |
+
if (selectedMigrationFilesAttrs.length < 1) {
|
| 138 |
+
throw 'There should be at least 1 migration file';
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
const selectedMigrations = await readMigrations(
|
| 142 |
+
selectedMigrationFilesAttrs.map((migration) => migration.fileName),
|
| 143 |
+
);
|
| 144 |
+
|
| 145 |
+
const lastMigration = selectedMigrations[selectedMigrations.length - 1];
|
| 146 |
+
|
| 147 |
+
const mergedMigrationActions = merge(
|
| 148 |
+
selectedMigrations.map((migration) => migration.content),
|
| 149 |
+
);
|
| 150 |
+
|
| 151 |
+
const outputSourceFileContent: SourceFileContent = {
|
| 152 |
+
...sourceFile.content,
|
| 153 |
+
last_migration: basename(lastMigration.file),
|
| 154 |
+
strings: apply(
|
| 155 |
+
sourceFile.content.strings,
|
| 156 |
+
mergedMigrationActions,
|
| 157 |
+
languages,
|
| 158 |
+
),
|
| 159 |
+
};
|
| 160 |
+
|
| 161 |
+
const destinationPath = isAbsolute(destinationFileName)
|
| 162 |
+
? destinationFileName
|
| 163 |
+
: join(projectPath, destinationFileName)
|
| 164 |
+
|
| 165 |
+
if (dryRun) {
|
| 166 |
+
console.info(`Creating file '${destinationPath}'`);
|
| 167 |
+
console.info(outputSourceFileContent);
|
| 168 |
+
} else {
|
| 169 |
+
await writeFilePromisify(
|
| 170 |
+
destinationPath,
|
| 171 |
+
JSON.stringify(outputSourceFileContent, null, 4),
|
| 172 |
+
'utf8',
|
| 173 |
+
);
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
export default applyMigrations;
|
go-web-app-develop/app/scripts/translatte/commands/exportMigration.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import xlsx from 'exceljs';
|
| 2 |
+
|
| 3 |
+
import { readMigrations } from '../utils';
|
| 4 |
+
import { isNotDefined } from '@togglecorp/fujs';
|
| 5 |
+
|
| 6 |
+
async function exportMigration(
|
| 7 |
+
migrationFilePath: string,
|
| 8 |
+
exportFileName: string,
|
| 9 |
+
) {
|
| 10 |
+
const migrations = await readMigrations(
|
| 11 |
+
[migrationFilePath]
|
| 12 |
+
);
|
| 13 |
+
|
| 14 |
+
const actions = migrations[0].content.actions;
|
| 15 |
+
const workbook = new xlsx.Workbook();
|
| 16 |
+
const now = new Date();
|
| 17 |
+
workbook.created = now;
|
| 18 |
+
|
| 19 |
+
const yyyy = now.getFullYear();
|
| 20 |
+
const mm = (now.getMonth() + 1).toString().padStart(2, '0');
|
| 21 |
+
const dd = now.getDate().toString().padStart(2, '0');
|
| 22 |
+
const worksheet = workbook.addWorksheet(
|
| 23 |
+
`${yyyy}-${mm}-${dd}`
|
| 24 |
+
);
|
| 25 |
+
|
| 26 |
+
worksheet.columns = [
|
| 27 |
+
{ header: 'Namespace', key: 'namespace' },
|
| 28 |
+
{ header: 'Key', key: 'key' },
|
| 29 |
+
{ header: 'EN', key: 'en' },
|
| 30 |
+
{ header: 'FR', key: 'fr' },
|
| 31 |
+
{ header: 'ES', key: 'es' },
|
| 32 |
+
{ header: 'AR', key: 'ar' },
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
actions.forEach((actionItem) => {
|
| 36 |
+
if (actionItem.action === 'remove') {
|
| 37 |
+
return;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
if (actionItem.action === 'update' && isNotDefined(actionItem.newValue)) {
|
| 41 |
+
return;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
const value = actionItem.action === 'update'
|
| 45 |
+
? actionItem.newValue
|
| 46 |
+
: actionItem.value;
|
| 47 |
+
|
| 48 |
+
worksheet.addRow({
|
| 49 |
+
namespace: actionItem.namespace,
|
| 50 |
+
key: actionItem.key,
|
| 51 |
+
en: value,
|
| 52 |
+
});
|
| 53 |
+
});
|
| 54 |
+
|
| 55 |
+
const fileName = isNotDefined(exportFileName)
|
| 56 |
+
? `go-strings-${yyyy}-${mm}-${dd}`
|
| 57 |
+
: exportFileName;
|
| 58 |
+
|
| 59 |
+
await workbook.xlsx.writeFile(`${fileName}.xlsx`);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
export default exportMigration;
|
go-web-app-develop/app/scripts/translatte/commands/generateMigration.test.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { expect } from 'vitest';
|
| 2 |
+
import { mkdirSync } from 'fs';
|
| 3 |
+
import { join } from 'path';
|
| 4 |
+
|
| 5 |
+
import generateMigration from './generateMigration';
|
| 6 |
+
import { testWithTmpDir } from '../testHelpers';
|
| 7 |
+
import { writeFilePromisify, readMigrations } from '../utils';
|
| 8 |
+
import {
|
| 9 |
+
migrationContent1,
|
| 10 |
+
migrationContent2,
|
| 11 |
+
migrationContent3,
|
| 12 |
+
migrationContent4,
|
| 13 |
+
migrationContent5,
|
| 14 |
+
loginContent,
|
| 15 |
+
registerContent,
|
| 16 |
+
updatedLoginContent,
|
| 17 |
+
updatedRegisterContent,
|
| 18 |
+
migrationContent6,
|
| 19 |
+
} from '../mockData';
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
testWithTmpDir('test generateMigration with no change', async ({ tmpdir }) => {
|
| 23 |
+
mkdirSync(join(tmpdir, 'migrations'));
|
| 24 |
+
const migrations = [
|
| 25 |
+
{ name: '000001-1000000000000.json', content: migrationContent1 },
|
| 26 |
+
{ name: '000002-1000000000000.json', content: migrationContent2 },
|
| 27 |
+
{ name: '000003-1000000000000.json', content: migrationContent3 },
|
| 28 |
+
{ name: '000004-1000000000000.json', content: migrationContent4 },
|
| 29 |
+
{ name: '000005-1000000000000.json', content: migrationContent5 },
|
| 30 |
+
].map(({ name, content }) => writeFilePromisify(
|
| 31 |
+
join(tmpdir, 'migrations', name),
|
| 32 |
+
JSON.stringify(content, null, 4),
|
| 33 |
+
'utf8',
|
| 34 |
+
));
|
| 35 |
+
await Promise.all(migrations);
|
| 36 |
+
|
| 37 |
+
mkdirSync(join(tmpdir, 'src'));
|
| 38 |
+
const translations = [
|
| 39 |
+
{ name: 'home.i18n.json', content: loginContent },
|
| 40 |
+
{ name: 'register.i18n.json', content: registerContent },
|
| 41 |
+
].map(({ name, content }) => writeFilePromisify(
|
| 42 |
+
join(tmpdir, 'src', name),
|
| 43 |
+
JSON.stringify(content, null, 4),
|
| 44 |
+
'utf8',
|
| 45 |
+
));
|
| 46 |
+
await Promise.all(translations);
|
| 47 |
+
|
| 48 |
+
await expect(
|
| 49 |
+
() => generateMigration(
|
| 50 |
+
tmpdir,
|
| 51 |
+
'migrations',
|
| 52 |
+
'src/**/*.i18n.json',
|
| 53 |
+
new Date().getTime(),
|
| 54 |
+
false,
|
| 55 |
+
),
|
| 56 |
+
).rejects.toThrow('Nothing to do');
|
| 57 |
+
});
|
| 58 |
+
|
| 59 |
+
testWithTmpDir('test generateMigration with change', async ({ tmpdir }) => {
|
| 60 |
+
mkdirSync(join(tmpdir, 'migrations'));
|
| 61 |
+
const migrations = [
|
| 62 |
+
{ name: '000001-1000000000000.json', content: migrationContent1 },
|
| 63 |
+
{ name: '000002-1000000000000.json', content: migrationContent2 },
|
| 64 |
+
{ name: '000003-1000000000000.json', content: migrationContent3 },
|
| 65 |
+
{ name: '000004-1000000000000.json', content: migrationContent4 },
|
| 66 |
+
{ name: '000005-1000000000000.json', content: migrationContent5 },
|
| 67 |
+
].map(({ name, content }) => writeFilePromisify(
|
| 68 |
+
join(tmpdir, 'migrations', name),
|
| 69 |
+
JSON.stringify(content, null, 4),
|
| 70 |
+
'utf8',
|
| 71 |
+
));
|
| 72 |
+
await Promise.all(migrations);
|
| 73 |
+
|
| 74 |
+
mkdirSync(join(tmpdir, 'src'));
|
| 75 |
+
|
| 76 |
+
const translations = [
|
| 77 |
+
{ name: 'home.i18n.json', content: updatedLoginContent },
|
| 78 |
+
{ name: 'register.i18n.json', content: updatedRegisterContent },
|
| 79 |
+
].map(({ name, content }) => writeFilePromisify(
|
| 80 |
+
join(tmpdir, 'src', name),
|
| 81 |
+
JSON.stringify(content, null, 4),
|
| 82 |
+
'utf8',
|
| 83 |
+
));
|
| 84 |
+
await Promise.all(translations);
|
| 85 |
+
|
| 86 |
+
const timestamp = new Date().getTime();
|
| 87 |
+
|
| 88 |
+
await generateMigration(
|
| 89 |
+
tmpdir,
|
| 90 |
+
'migrations',
|
| 91 |
+
'src/**/*.i18n.json',
|
| 92 |
+
timestamp,
|
| 93 |
+
false,
|
| 94 |
+
);
|
| 95 |
+
|
| 96 |
+
const generatedMigrations = await readMigrations([
|
| 97 |
+
join(tmpdir, 'migrations', `000006-${timestamp}.json`)
|
| 98 |
+
]);
|
| 99 |
+
const generatedMigrationContent = generatedMigrations[0].content;
|
| 100 |
+
|
| 101 |
+
expect(generatedMigrationContent).toEqual(migrationContent6)
|
| 102 |
+
});
|
go-web-app-develop/app/scripts/translatte/commands/generateMigration.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Md5 } from 'ts-md5';
|
| 2 |
+
import { join, isAbsolute } from 'path';
|
| 3 |
+
|
| 4 |
+
import {
|
| 5 |
+
writeFilePromisify,
|
| 6 |
+
oneOneMapping,
|
| 7 |
+
readTranslations,
|
| 8 |
+
getTranslationFileNames,
|
| 9 |
+
getMigrationFilesAttrs,
|
| 10 |
+
readMigrations,
|
| 11 |
+
oneOneMappingNonUnique,
|
| 12 |
+
} from '../utils';
|
| 13 |
+
import { MigrationActionItem, MigrationFileContent } from '../types';
|
| 14 |
+
import { merge } from './mergeMigrations';
|
| 15 |
+
|
| 16 |
+
function getCombinedKey(key: string, namespace: string) {
|
| 17 |
+
return `${namespace}:${key}`;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
type StateItem = {
|
| 21 |
+
filename?: string;
|
| 22 |
+
namespace: string;
|
| 23 |
+
key: string;
|
| 24 |
+
value: string;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// FIXME: The output should be stable
|
| 28 |
+
function generateMigration(
|
| 29 |
+
prevState: StateItem[],
|
| 30 |
+
currentState: StateItem[],
|
| 31 |
+
): MigrationActionItem[] {
|
| 32 |
+
/*
|
| 33 |
+
console.info('prevState length', prevState.length);
|
| 34 |
+
console.info('currentState length', currentState.length);
|
| 35 |
+
console.info('Total change', Math.abs(prevState.length - currentState.length));
|
| 36 |
+
*/
|
| 37 |
+
|
| 38 |
+
const {
|
| 39 |
+
// Same, key, namespace and same value
|
| 40 |
+
validCommonItems: identicalStateItems,
|
| 41 |
+
|
| 42 |
+
// Same, key, namespace but different value
|
| 43 |
+
invalidCommonItems: valueUpdatedStateItems,
|
| 44 |
+
|
| 45 |
+
// items with different key or namespace or both
|
| 46 |
+
prevStateRemainder: potentiallyRemovedStateItems,
|
| 47 |
+
|
| 48 |
+
// items with different key or namespace or both
|
| 49 |
+
currentStateRemainder: potentiallyAddedStateItems,
|
| 50 |
+
} = oneOneMapping(
|
| 51 |
+
prevState,
|
| 52 |
+
currentState,
|
| 53 |
+
({ key, namespace }) => getCombinedKey(key, namespace),
|
| 54 |
+
(prev, current) => prev.value === current.value,
|
| 55 |
+
);
|
| 56 |
+
|
| 57 |
+
console.info(`Unchanged strings: ${identicalStateItems.length}`)
|
| 58 |
+
console.info(`Value updated strings: ${valueUpdatedStateItems.length}`)
|
| 59 |
+
|
| 60 |
+
console.info(`Potentially removed: ${potentiallyRemovedStateItems.length}`)
|
| 61 |
+
console.info(`Potentially added: ${potentiallyAddedStateItems.length}`)
|
| 62 |
+
|
| 63 |
+
const {
|
| 64 |
+
commonItems: namespaceUpdatedStateItems,
|
| 65 |
+
prevStateRemainder: potentiallyRemovedStateItemsAfterNamespaceChange,
|
| 66 |
+
currentStateRemainder: potentiallyAddedStateItemsAfterNamespaceChange,
|
| 67 |
+
} = oneOneMappingNonUnique(
|
| 68 |
+
potentiallyRemovedStateItems,
|
| 69 |
+
potentiallyAddedStateItems,
|
| 70 |
+
(item) => getCombinedKey(item.key, Md5.hashStr(item.value)),
|
| 71 |
+
);
|
| 72 |
+
|
| 73 |
+
const {
|
| 74 |
+
commonItems: keyUpdatedStateItems,
|
| 75 |
+
prevStateRemainder: removedStateItems,
|
| 76 |
+
currentStateRemainder: addedStateItems,
|
| 77 |
+
} = oneOneMappingNonUnique(
|
| 78 |
+
potentiallyRemovedStateItemsAfterNamespaceChange,
|
| 79 |
+
potentiallyAddedStateItemsAfterNamespaceChange,
|
| 80 |
+
(item) => getCombinedKey(item.namespace, Md5.hashStr(item.value)),
|
| 81 |
+
);
|
| 82 |
+
|
| 83 |
+
console.info(`Namespace updated strings: ${namespaceUpdatedStateItems.length}`)
|
| 84 |
+
console.info(`Added strings: ${addedStateItems.length}`)
|
| 85 |
+
console.info(`Removed strings: ${removedStateItems.length}`)
|
| 86 |
+
|
| 87 |
+
return [
|
| 88 |
+
...valueUpdatedStateItems.map(({ prevStateItem, currentStateItem }) => ({
|
| 89 |
+
action: 'update' as const,
|
| 90 |
+
key: prevStateItem.key,
|
| 91 |
+
namespace: prevStateItem.namespace,
|
| 92 |
+
newValue: currentStateItem.value,
|
| 93 |
+
})),
|
| 94 |
+
...namespaceUpdatedStateItems.map(({ prevStateItem, currentStateItem }) => ({
|
| 95 |
+
action: 'update' as const,
|
| 96 |
+
key: prevStateItem.key,
|
| 97 |
+
namespace: prevStateItem.namespace,
|
| 98 |
+
newNamespace: currentStateItem.namespace,
|
| 99 |
+
})),
|
| 100 |
+
...keyUpdatedStateItems.map(({ prevStateItem, currentStateItem }) => ({
|
| 101 |
+
action: 'update' as const,
|
| 102 |
+
key: prevStateItem.key,
|
| 103 |
+
newKey: currentStateItem.key,
|
| 104 |
+
namespace: prevStateItem.namespace,
|
| 105 |
+
})),
|
| 106 |
+
...addedStateItems.map((item) => ({
|
| 107 |
+
action: 'add' as const,
|
| 108 |
+
key: item.key,
|
| 109 |
+
namespace: item.namespace,
|
| 110 |
+
value: item.value,
|
| 111 |
+
})),
|
| 112 |
+
...removedStateItems.map((item) => ({
|
| 113 |
+
action: 'remove' as const,
|
| 114 |
+
key: item.key,
|
| 115 |
+
namespace: item.namespace,
|
| 116 |
+
})),
|
| 117 |
+
].sort((foo, bar) => (
|
| 118 |
+
foo.namespace.localeCompare(bar.namespace)
|
| 119 |
+
|| foo.action.localeCompare(bar.action)
|
| 120 |
+
|| foo.key.localeCompare(bar.key)
|
| 121 |
+
));
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
async function generate(
|
| 125 |
+
projectPath: string,
|
| 126 |
+
migrationFilePath: string,
|
| 127 |
+
translationFileName: string | string[],
|
| 128 |
+
timestamp: number,
|
| 129 |
+
dryRun: boolean | undefined,
|
| 130 |
+
) {
|
| 131 |
+
const migrationFilesAttrs = await getMigrationFilesAttrs(projectPath, migrationFilePath);
|
| 132 |
+
const selectedMigrationFilesAttrs = migrationFilesAttrs;
|
| 133 |
+
console.info(`Found ${selectedMigrationFilesAttrs.length} migration files`);
|
| 134 |
+
const selectedMigrations = await readMigrations(
|
| 135 |
+
selectedMigrationFilesAttrs.map((migration) => migration.fileName),
|
| 136 |
+
);
|
| 137 |
+
const mergedMigrationActions = merge(
|
| 138 |
+
selectedMigrations.map((migration) => migration.content),
|
| 139 |
+
);
|
| 140 |
+
|
| 141 |
+
const serverState: StateItem[] = mergedMigrationActions.map((item) => {
|
| 142 |
+
if (item.action !== 'add') {
|
| 143 |
+
throw `The action should be "add" but found "${item.action}"`;
|
| 144 |
+
}
|
| 145 |
+
return {
|
| 146 |
+
filename: undefined,
|
| 147 |
+
namespace: item.namespace,
|
| 148 |
+
key: item.key,
|
| 149 |
+
value: item.value,
|
| 150 |
+
}
|
| 151 |
+
});
|
| 152 |
+
const translationFiles = await getTranslationFileNames(
|
| 153 |
+
projectPath,
|
| 154 |
+
Array.isArray(translationFileName) ? translationFileName : [translationFileName],
|
| 155 |
+
);
|
| 156 |
+
const { translations } = await readTranslations(translationFiles);
|
| 157 |
+
const fileState = translations.map((item) => ({
|
| 158 |
+
...item,
|
| 159 |
+
}));
|
| 160 |
+
|
| 161 |
+
const migrationActionItems = generateMigration(
|
| 162 |
+
serverState,
|
| 163 |
+
fileState,
|
| 164 |
+
);
|
| 165 |
+
|
| 166 |
+
if (migrationActionItems.length <= 0) {
|
| 167 |
+
throw 'Nothing to do';
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
const lastMigration = migrationFilesAttrs[migrationFilesAttrs.length - 1];
|
| 171 |
+
|
| 172 |
+
const migrationContent: MigrationFileContent = {
|
| 173 |
+
parent: lastMigration?.migrationName,
|
| 174 |
+
actions: migrationActionItems,
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
const num = String(Number(lastMigration?.num ?? '000000') + 1).padStart(6, '0');
|
| 178 |
+
|
| 179 |
+
const outputMigrationFile = isAbsolute(migrationFilePath)
|
| 180 |
+
? join(migrationFilePath, `${num}-${timestamp}.json`)
|
| 181 |
+
: join(projectPath, migrationFilePath, `${num}-${timestamp}.json`)
|
| 182 |
+
|
| 183 |
+
if (dryRun) {
|
| 184 |
+
console.info(`Creating migration file '${outputMigrationFile}'`);
|
| 185 |
+
console.info(migrationContent);
|
| 186 |
+
} else {
|
| 187 |
+
await writeFilePromisify(
|
| 188 |
+
outputMigrationFile,
|
| 189 |
+
JSON.stringify(migrationContent, null, 4),
|
| 190 |
+
'utf8',
|
| 191 |
+
);
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
export default generate;
|